Implement configuration, runtime, frontend, and task governance closure

This commit is contained in:
2026-08-21 16:51:42 +08:00
parent b1b5a3cc8a
commit 63c0408ce7
92 changed files with 343619 additions and 172 deletions

View File

@@ -10,11 +10,9 @@
- 来源索引:`coverage/source-index.jsonl`,由 `scripts/generate_source_inventory.py` 生成。 - 来源索引:`coverage/source-index.jsonl`,由 `scripts/generate_source_inventory.py` 生成。
- 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。 - 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。
- WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。 - WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。
- 前端、Worker、HAL、解释器、TP、Python、OPFS 和 UI 功能仍须按文档里程碑实现。 - 前端应用壳、真实 WorkerClient、完整配置加载、HAL/数字孪生、实际 Pyodide bridge 和 UI/Three.js 工作流仍须按文档里程碑实现;当前下一任务是 `M3-CFG-01`,详见 [`后续工作基线`](docs/linuxcnc-web-wasm-next-work.md)。
- `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 证据。 - K0-K6 运动学实现主线已完成代码、ABI、固定边界和 native/WASM 回归,覆盖 trivkins、耦合运动学、5 轴/TRT、机器人、GenSer、Delta、Tripod、Hexapod、Pentapod、switchkins、userk/registry、TP→IK→motion、FK feedback、home/limit。对应报告仍可能是 `partial`,因为锁定来源没有 `linuxcnc/unit_tests/kinematics/**` 官方 oracle;这是发布验收阻塞,不是继续重复解析的理由。
- `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 证据。 - K8 配置/发布审计已生成 264 个配置注册入口、233 个 sim entrypoint 和 16,093 条资源边;当前配置启动闭包仍有 997 missing、91 unresolved、59 unindexed 和 3 unregistered startup 条目。K8 release gate 保持 `candidate_eligible=false`、`release_action=STOP_RELEASE`,六项 owner/release blocker 不得被误报为运动学未完成。
- `K4-TRIPOD-01` 已完成 point-vertex tripod 的统一 FK/IK/mapping;真实 `tripod.ini`/`tripodsim.hal`、XYZ/AD-BD-CD 映射、above/below 根分支、无解/基面奇异/限位/退化几何和 native/WASM state hash 对照见 `coverage/tripod-kinematics.report.json`。`nextTask.md` 当前进入 `K4-HEX-01`;官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续显式 blocked。
- `K4-HEX-01` 已完成 genhex 六腿 Stewart 平台的统一 FK/IK/mapping;真实 `hexapod.ini`/`kinematics.hal`、Newton 6x6 正解、闭式逆解、迭代/奇异/限位边界和 native/WASM 对照见 `coverage/genhex-kinematics.report.json`。`nextTask.md` 当前进入 `K4-PENTA-01`;官方 oracle 继续显式 blocked。
## 重建 M0 ## 重建 M0

View File

@@ -66,21 +66,21 @@
"bytes": 11539, "bytes": 11539,
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix", "role": "compatibility_matrix",
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
@@ -167,7 +167,7 @@
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_matrix_unverified", "remediation_status": "blocked_matrix_unverified",
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9", "report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
"review_policy": { "review_policy": {
"current_environment_limit": "Safari binary and browser farm are unavailable; observed Chromium/Firefox probes do not establish a release minimum", "current_environment_limit": "Safari binary and browser farm are unavailable; observed Chromium/Firefox probes do not establish a release minimum",
"forbidden": [ "forbidden": [

View File

@@ -0,0 +1,208 @@
{
"candidate_eligible": false,
"checks": {
"chromium_environment": {
"status": "verified"
},
"firefox_not_in_scope": {
"reason": "Firefox is excluded from the current test scope",
"status": "verified"
},
"frontend_dependency": {
"status": "verified"
},
"reg01_dependency": {
"status": "verified"
},
"reg03_dependency": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"repeat_determinism": {
"status": "verified"
},
"safari_not_in_scope": {
"reason": "Safari is excluded from the current test scope",
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"environment": {
"firefox": "out_of_scope",
"minimums": "TBD_in_locked_matrix",
"safari": "out_of_scope",
"scope": [
"chromium"
]
},
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 225468,
"path": "coverage/full-configuration-startup-matrix.report.json",
"role": "full_configuration_matrix",
"sha256": "ab270bb11acc0825ee0a7b61f06751a88a8808e2061eea531162780043cf4a77",
"status": "present"
},
{
"bytes": 3170,
"path": "coverage/recovery-offline-fault-matrix.report.json",
"role": "recovery_offline_fault_matrix",
"sha256": "d8d68855fc0db32201015ead2b0d5a50f809ada74c89caf48af4fc971dcc001b",
"status": "present"
},
{
"bytes": 6180,
"path": "coverage/browser-matrix-remediation.report.json",
"role": "browser_matrix_remediation",
"sha256": "6f150474a385a41caf71b964d6c11133d34e742a211f985f207908687aecb37c",
"status": "present"
},
{
"bytes": 3301,
"path": "coverage/frontend-m4.report.json",
"role": "frontend_m4",
"sha256": "38d26903f56c0e9b2235baf4fc96c6468b43c391e64c03a5df15c0c46d9b22f3",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute K8-REL-17 release gate refresh after implementation evidence",
"release_action": "STOP_RELEASE",
"report_sha256": "325c5ddaba4d4db51616ca73612345606936e184b236d49c1ff30d65ed8d1c08",
"runs": [
{
"exit_code": 0,
"sample": {
"files": [
{
"bytes": 367,
"file": "index.html"
},
{
"bytes": 5715,
"file": "app.mjs"
},
{
"bytes": 4432,
"file": "worker-client.mjs"
},
{
"bytes": 2731,
"file": "styles.css"
},
{
"bytes": 1303,
"file": "controls.mjs"
},
{
"bytes": 2418,
"file": "state-panel.mjs"
},
{
"bytes": 2238,
"file": "project-workflow.mjs"
},
{
"bytes": 1035,
"file": "recovery-panel.mjs"
},
{
"bytes": 1318,
"file": "device-panel.mjs"
},
{
"bytes": 826,
"file": "hal-panel.mjs"
},
{
"bytes": 1129,
"file": "machine-view.mjs"
}
],
"schema": "browser-performance-sample.v1",
"stable": true,
"total_bytes": 23512
},
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout_sha256": "9ee86b1138ee6c30ecd4e9cb032a7dd1b2ab3317c0858a63a805708bdf410166"
},
{
"exit_code": 0,
"sample": {
"files": [
{
"bytes": 367,
"file": "index.html"
},
{
"bytes": 5715,
"file": "app.mjs"
},
{
"bytes": 4432,
"file": "worker-client.mjs"
},
{
"bytes": 2731,
"file": "styles.css"
},
{
"bytes": 1303,
"file": "controls.mjs"
},
{
"bytes": 2418,
"file": "state-panel.mjs"
},
{
"bytes": 2238,
"file": "project-workflow.mjs"
},
{
"bytes": 1035,
"file": "recovery-panel.mjs"
},
{
"bytes": 1318,
"file": "device-panel.mjs"
},
{
"bytes": 826,
"file": "hal-panel.mjs"
},
{
"bytes": 1129,
"file": "machine-view.mjs"
}
],
"schema": "browser-performance-sample.v1",
"stable": true,
"total_bytes": 23512
},
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout_sha256": "9ee86b1138ee6c30ecd4e9cb032a7dd1b2ab3317c0858a63a805708bdf410166"
}
],
"schema": "browser-performance-matrix.v1",
"status": "partial",
"task_id": "REG-04"
}

View File

@@ -260,10 +260,10 @@
"status": "present" "status": "present"
}, },
{ {
"bytes": 20236, "bytes": 20468,
"path": "docs/linuxcnc-web-wasm-full-port-implementation.md", "path": "docs/linuxcnc-web-wasm-full-port-implementation.md",
"role": "implementation_contract", "role": "implementation_contract",
"sha256": "16ed7fdf62f0dd377e50947109dfa57bc947df6313a21d0eb034d9b8e8ceb70f", "sha256": "52261ecce35d945b20d85a40abdeeb408b4a53caf38162c840be2b4864e8b5ee",
"status": "present" "status": "present"
}, },
{ {
@@ -352,7 +352,7 @@
} }
], ],
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "report_sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"schema": "release-compatibility-matrix.v1", "schema": "release-compatibility-matrix.v1",
"source_baseline": { "source_baseline": {
"context_lock": { "context_lock": {

View File

@@ -82,14 +82,14 @@
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
@@ -13852,7 +13852,7 @@
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_resource_gaps", "remediation_status": "blocked_resource_gaps",
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1", "report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
"review_policy": { "review_policy": {
"forbidden": [ "forbidden": [
"drop_missing_rows", "drop_missing_rows",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
{
"candidate_eligible": false,
"checks": {
"browser_smoke": {
"status": "verified",
"stdout": "frontend_m1=passed messages=3"
},
"dependency_chain": {
"status": "verified"
},
"protocol_workflow": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"source_files": {
"files": [
{
"bytes": 367,
"path": "web/app/index.html",
"role": "frontend_source",
"sha256": "f4f52057dbd428eeafc10f5163763d252c23de0fe6a5cf4a35de1054d817064d",
"status": "present"
},
{
"bytes": 5715,
"path": "web/app/app.mjs",
"role": "frontend_source",
"sha256": "2781c817de5f73150d9deb06e4e22044061dd53c8a978c88826d36c3a511356c",
"status": "present"
},
{
"bytes": 4432,
"path": "web/app/worker-client.mjs",
"role": "frontend_source",
"sha256": "78cc6f75efa2930d22094735669ce8a78548a6c52019debbfba93838f4194f15",
"status": "present"
},
{
"bytes": 2731,
"path": "web/app/styles.css",
"role": "frontend_source",
"sha256": "46bbe2246271ff56922a2765d921be9ca26c701e5579106ca1cad8538b065fd8",
"status": "present"
},
{
"bytes": 2064,
"path": "web/app/test_frontend_m1.mjs",
"role": "frontend_source",
"sha256": "88bbeeadb79a72bb6b6a9560b07de464e9b562e65451428624f2c4fa9d9a368b",
"status": "present"
}
],
"status": "verified"
},
"worker_boundary": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 4914,
"path": "coverage/python-worker-bridge.report.json",
"role": "python_worker_bridge",
"sha256": "13e4f85abaf34c4a57d3a96143df2fa597df7e0531a515f79c487ea6711d77f6",
"status": "present"
},
{
"bytes": 3685,
"path": "coverage/workspace-session-integration.report.json",
"role": "workspace_session_integration",
"sha256": "19a5701dd49877153cfa0dc7fa886b5c0e72baadb5023143cef61bc9f94ca13b",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute FE-M2 core operation controls and state panels",
"outputs": [
{
"bytes": 367,
"path": "web/app/index.html",
"role": "frontend_source",
"sha256": "f4f52057dbd428eeafc10f5163763d252c23de0fe6a5cf4a35de1054d817064d",
"status": "present"
},
{
"bytes": 5715,
"path": "web/app/app.mjs",
"role": "frontend_source",
"sha256": "2781c817de5f73150d9deb06e4e22044061dd53c8a978c88826d36c3a511356c",
"status": "present"
},
{
"bytes": 4432,
"path": "web/app/worker-client.mjs",
"role": "frontend_source",
"sha256": "78cc6f75efa2930d22094735669ce8a78548a6c52019debbfba93838f4194f15",
"status": "present"
},
{
"bytes": 2731,
"path": "web/app/styles.css",
"role": "frontend_source",
"sha256": "46bbe2246271ff56922a2765d921be9ca26c701e5579106ca1cad8538b065fd8",
"status": "present"
},
{
"bytes": 2064,
"path": "web/app/test_frontend_m1.mjs",
"role": "frontend_source",
"sha256": "88bbeeadb79a72bb6b6a9560b07de464e9b562e65451428624f2c4fa9d9a368b",
"status": "present"
}
],
"release_action": "STOP_RELEASE",
"report_sha256": "5180544f934f6a9a348ac321c1a9ef8a9b830ed416caef844c9e3a3d55b75cef",
"schema": "frontend-m1.v1",
"smoke": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "frontend_m1=passed messages=3",
"stdout_sha256": "d5d56cda8a76c97d618b3e46a367e863cb44b685834273f732579949934820ef"
},
"status": "partial",
"task_id": "FE-M1"
}

View File

@@ -0,0 +1,119 @@
{
"candidate_eligible": false,
"checks": {
"authoritative_state_panel": {
"status": "verified"
},
"browser_smoke": {
"status": "verified",
"stdout": "frontend_m2=passed commands=2"
},
"device_error_visibility": {
"status": "verified"
},
"fe1_dependency": {
"status": "verified"
},
"operation_controls": {
"actions": [
"run",
"pause",
"resume",
"abort",
"home",
"estop",
"recover"
],
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"source_files": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 4468,
"path": "coverage/frontend-m1.report.json",
"role": "frontend_m1",
"sha256": "193c5cb609e19c5256d507e7e5dce1b9df725df7dddf2f868b5b3f2e397feab5",
"status": "present"
},
{
"bytes": 11813,
"path": "coverage/virtual-device-fault-matrix.report.json",
"role": "virtual_device_fault_matrix",
"sha256": "6969a4a0aee13c2a38f9d02fdfbb1456076a69f214c8cc2d1df84f438f373d51",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute FE-M3 project and recovery workflow",
"outputs": [
{
"bytes": 1303,
"path": "web/app/controls.mjs",
"role": "frontend_source",
"sha256": "020bc0da5bdc8de507c56dd27dc4a13eed1b24726cd54ee629e68afd3827e075",
"status": "present"
},
{
"bytes": 2418,
"path": "web/app/state-panel.mjs",
"role": "frontend_source",
"sha256": "f2fcb73f14ae90de851618f1fd8fec7e3d5246aff72eff740ce82a3ebc976a4d",
"status": "present"
},
{
"bytes": 5715,
"path": "web/app/app.mjs",
"role": "frontend_source",
"sha256": "2781c817de5f73150d9deb06e4e22044061dd53c8a978c88826d36c3a511356c",
"status": "present"
},
{
"bytes": 2731,
"path": "web/app/styles.css",
"role": "frontend_source",
"sha256": "46bbe2246271ff56922a2765d921be9ca26c701e5579106ca1cad8538b065fd8",
"status": "present"
},
{
"bytes": 1133,
"path": "web/app/test_frontend_m2.mjs",
"role": "frontend_source",
"sha256": "f4f1fa21b94e884fe5cf0b9ab12c640545e6bbd2e009f475c00211132fdc7543",
"status": "present"
}
],
"release_action": "STOP_RELEASE",
"report_sha256": "0c57f73691a827289247e294587721d4bf694024b24aca297a6d653cb8c3b58c",
"schema": "frontend-m2.v1",
"smoke": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "frontend_m2=passed commands=2",
"stdout_sha256": "f27af0ea289947ad6016c5429f3b20ac1c439b915973af5d89ca38e8f2f70b5a"
},
"status": "partial",
"task_id": "FE-M2"
}

View File

@@ -0,0 +1,103 @@
{
"candidate_eligible": false,
"checks": {
"browser_smoke": {
"status": "verified",
"stdout": "frontend_m3=passed files=2"
},
"fe2_dependency": {
"status": "verified"
},
"journal_package_workflow": {
"status": "verified"
},
"recovery_panel": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"session_dependency": {
"status": "verified"
},
"source_files": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 3416,
"path": "coverage/frontend-m2.report.json",
"role": "frontend_m2",
"sha256": "2f88ac3b6be9283ff4fd8e952ce93f508a42412dbc1f92298a893e8e8aaff15c",
"status": "present"
},
{
"bytes": 3685,
"path": "coverage/workspace-session-integration.report.json",
"role": "workspace_session_integration",
"sha256": "19a5701dd49877153cfa0dc7fa886b5c0e72baadb5023143cef61bc9f94ca13b",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute FE-M4 HAL/tool/IO/3D frontend workflows",
"outputs": [
{
"bytes": 2238,
"path": "web/app/project-workflow.mjs",
"role": "frontend_source",
"sha256": "dc043ab7150c38df40d63469f8325e549b245f0dfc163affe42034fd4c67e4b8",
"status": "present"
},
{
"bytes": 1035,
"path": "web/app/recovery-panel.mjs",
"role": "frontend_source",
"sha256": "96b9b7405bfe911d167a8c6465bf80b694a7de69b118a2a59faaa562f30bd4a0",
"status": "present"
},
{
"bytes": 5715,
"path": "web/app/app.mjs",
"role": "frontend_source",
"sha256": "2781c817de5f73150d9deb06e4e22044061dd53c8a978c88826d36c3a511356c",
"status": "present"
},
{
"bytes": 1718,
"path": "web/app/test_frontend_m3.mjs",
"role": "frontend_source",
"sha256": "c9ff30b956656ade0a7c026af4ab79d1eb5b1008d63c082a79add984ca1c42e2",
"status": "present"
}
],
"release_action": "STOP_RELEASE",
"report_sha256": "740f81de42580a39b2232966812588f1b44f0de21bcc153d18985e20ae641998",
"schema": "frontend-m3.v1",
"smoke": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "frontend_m3=passed files=2",
"stdout_sha256": "508f4b66474dd7855a835a9454c0f97b64e7aed0c40cc6285f45d2ca240294ec"
},
"status": "partial",
"task_id": "FE-M3"
}

View File

@@ -0,0 +1,110 @@
{
"candidate_eligible": false,
"checks": {
"browser_smoke": {
"status": "verified",
"stdout": "frontend_m4=passed panels=3"
},
"device_workflow": {
"status": "verified"
},
"fe3_dependency": {
"status": "verified"
},
"hal_diagnostics": {
"status": "verified"
},
"machine_view_boundary": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"source_files": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 3059,
"path": "coverage/frontend-m3.report.json",
"role": "frontend_m3",
"sha256": "0d5f968b71e488c5ec557468dbefe0a70a9b84b809e7c366d814289f4c553eab",
"status": "present"
},
{
"bytes": 2535591,
"path": "coverage/hal-component-manifest.report.json",
"role": "hal_component_manifest",
"sha256": "8ee349639377ba653347a1fc29af51bfc723aa3fd4dfd25426a736375f6e9769",
"status": "present"
},
{
"bytes": 11813,
"path": "coverage/virtual-device-fault-matrix.report.json",
"role": "virtual_device_fault_matrix",
"sha256": "6969a4a0aee13c2a38f9d02fdfbb1456076a69f214c8cc2d1df84f438f373d51",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute REG-01 full configuration startup matrix",
"outputs": [
{
"bytes": 1318,
"path": "web/app/device-panel.mjs",
"role": "frontend_source",
"sha256": "df54448017ae50455b3a96642bd6904678509cf14b49288e53b059699188a856",
"status": "present"
},
{
"bytes": 826,
"path": "web/app/hal-panel.mjs",
"role": "frontend_source",
"sha256": "f4b6d998fab6831a5746a57d33be0a5d2d888c1266af8638105d6cfed43747e2",
"status": "present"
},
{
"bytes": 1129,
"path": "web/app/machine-view.mjs",
"role": "frontend_source",
"sha256": "de06ccde2195eef7eb678c4fc1f583954632852155a4fe6b5e8eebf741972184",
"status": "present"
},
{
"bytes": 1230,
"path": "web/app/test_frontend_m4.mjs",
"role": "frontend_source",
"sha256": "507d5a6daefac7de4764679399049666624215804bb06c365a214b44a094ac99",
"status": "present"
}
],
"release_action": "STOP_RELEASE",
"report_sha256": "3944dfaee264ebd7fdba05abc194fa5c01ac4b332370b995e9221cdebd3562f4",
"schema": "frontend-m4.v1",
"smoke": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "frontend_m4=passed panels=3",
"stdout_sha256": "b439d409162d55901ba5f43ab1021e0d8645a5bed59e8f08846dba88247442b0"
},
"status": "partial",
"task_id": "FE-M4"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1003,21 +1003,21 @@
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_pending_owner_decision", "remediation_status": "blocked_pending_owner_decision",
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca", "report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
"schema": "implementation-license-remediation.v1", "schema": "implementation-license-remediation.v1",
"status": "complete", "status": "complete",
"task_id": "K8-REL-13" "task_id": "K8-REL-13"

View File

@@ -156,7 +156,7 @@
"status": "verified" "status": "verified"
}, },
"audit_self_hash": { "audit_self_hash": {
"declared": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f", "declared": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
"status": "verified" "status": "verified"
}, },
"blocker_preservation": { "blocker_preservation": {
@@ -184,22 +184,22 @@
}, },
{ {
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "verified" "status": "verified"
}, },
{ {
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "verified" "status": "verified"
}, },
{ {
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"status": "verified" "status": "verified"
}, },
{ {
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"status": "verified" "status": "verified"
} }
], ],
@@ -238,28 +238,28 @@
"bytes": 26665, "bytes": 26665,
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"role": "post_release_audit", "role": "post_release_audit",
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 1945, "bytes": 1945,
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"status": "present" "status": "present"
}, },
{ {
"bytes": 1435099, "bytes": 1435099,
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"status": "present" "status": "present"
}, },
{ {
@@ -292,7 +292,7 @@
}, },
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b", "report_sha256": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
"schema": "k8-milestone-closeout.v1", "schema": "k8-milestone-closeout.v1",
"source_baseline": { "source_baseline": {
"context_lock": { "context_lock": {

View File

@@ -100052,14 +100052,14 @@
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
@@ -100072,7 +100072,7 @@
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_pending_manifest_review", "remediation_status": "blocked_pending_manifest_review",
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179", "report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
"review_policy": { "review_policy": {
"forbidden": [ "forbidden": [
"promote_to_accepted", "promote_to_accepted",

View File

@@ -71,21 +71,21 @@
"bytes": 11539, "bytes": 11539,
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix.report", "role": "compatibility_matrix.report",
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate.report", "role": "release_candidate_gate.report",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "release_remediation_intake.report", "role": "release_remediation_intake.report",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
@@ -148,7 +148,7 @@
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_missing_locked_inputs", "remediation_status": "blocked_missing_locked_inputs",
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d", "report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
"review_policy": { "review_policy": {
"forbidden": [ "forbidden": [
"promote_matrixkins_to_official", "promote_matrixkins_to_official",

View File

@@ -79,28 +79,28 @@
"bytes": 9239, "bytes": 9239,
"path": "coverage/remediation-evidence-roundup.report.json", "path": "coverage/remediation-evidence-roundup.report.json",
"role": "remediation_roundup", "role": "remediation_roundup",
"sha256": "f91376e3cd4a7871610b9881b9f997a627fc205c58919d47c7e7e43b8545dd29", "sha256": "6746fda47f7efc4cc0902478854dabbb91e42b0bd6e58fc62393b7c9ebc2d40e",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 26665, "bytes": 26665,
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"role": "post_release_audit", "role": "post_release_audit",
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "present" "status": "present"
} }
], ],
"next_transition": "collect owner decisions, create a new context when locked inputs change, then regenerate closure/audit/gate", "next_transition": "collect owner decisions, create a new context when locked inputs change, then regenerate closure/audit/gate",
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "fc7d7160ceae3bed7a9cbc2e942b2abe21af51f17c9e235d86b340868c001246", "report_sha256": "5006286fd2d25bf73e262633e3a6d12b89b28af9d159ef84ad58132bb79e6de2",
"review_status": "owner_input_required", "review_status": "owner_input_required",
"schema": "owner-review-gate-refresh.v1", "schema": "owner-review-gate-refresh.v1",
"status": "blocked", "status": "blocked",

View File

@@ -262,9 +262,9 @@
"status": "verified" "status": "verified"
}, },
{ {
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"evidence": "coverage/release-closure.report.json", "evidence": "coverage/release-closure.report.json",
"expected": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "expected": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"name": "closure_compatibility_report_hash", "name": "closure_compatibility_report_hash",
"status": "verified" "status": "verified"
}, },
@@ -276,16 +276,16 @@
"status": "verified" "status": "verified"
}, },
{ {
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"evidence": "coverage/release-closure.report.json", "evidence": "coverage/release-closure.report.json",
"expected": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "expected": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"name": "closure_evidence_index_report_hash", "name": "closure_evidence_index_report_hash",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"evidence": "coverage/release-evidence-index.json", "evidence": "coverage/release-evidence-index.json",
"expected": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "expected": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"name": "index_compatibility_report_hash", "name": "index_compatibility_report_hash",
"status": "verified" "status": "verified"
}, },
@@ -366,27 +366,27 @@
}, },
{ {
"actual_bytes": 1945, "actual_bytes": 1945,
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"expected_bytes": 1945, "expected_bytes": 1945,
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 1435099, "actual_bytes": 1435099,
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"expected_bytes": 1435099, "expected_bytes": 1435099,
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 11539, "actual_bytes": 11539,
"actual_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "actual_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"expected_bytes": 11539, "expected_bytes": 11539,
"expected_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "expected_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix", "role": "compatibility_matrix",
"status": "verified" "status": "verified"
@@ -460,26 +460,26 @@
"report_self_hashes": { "report_self_hashes": {
"reports": [ "reports": [
{ {
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1", "actual": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
"declared": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1", "declared": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"declared": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "declared": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"declared": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44", "declared": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"status": "verified" "status": "verified"
}, },
@@ -503,8 +503,8 @@
"status": "verified" "status": "verified"
}, },
{ {
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"status": "verified" "status": "verified"
} }
@@ -538,27 +538,27 @@
}, },
{ {
"actual_bytes": 1945, "actual_bytes": 1945,
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"expected_bytes": 1945, "expected_bytes": 1945,
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 1435099, "actual_bytes": 1435099,
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"expected_bytes": 1435099, "expected_bytes": 1435099,
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 11539, "actual_bytes": 11539,
"actual_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "actual_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"expected_bytes": 11539, "expected_bytes": 11539,
"expected_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "expected_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix", "role": "compatibility_matrix",
"status": "verified" "status": "verified"
@@ -632,7 +632,7 @@
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
@@ -670,7 +670,7 @@
}, },
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f", "report_sha256": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
"schema": "post-release-audit.v1", "schema": "post-release-audit.v1",
"source_baseline": { "source_baseline": {
"context_lock": { "context_lock": {

View File

@@ -0,0 +1,207 @@
{
"blockers": [
"accepted_entries_missing",
"browser_matrix_unverified",
"configuration_resources_missing",
"license_closure_incomplete",
"manifest_seed_unassigned",
"official_oracle_missing"
],
"candidate_eligible": false,
"checks": {
"context_consistency": {
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"status": "verified"
},
"implementation_evidence_verified": {
"status": "verified"
},
"implementation_tasks_unique": {
"status": "verified",
"tasks": 12
},
"no_self_loop": {
"automatic_retry": false,
"current": "GOV-01",
"next": "WAIT-OWNER-01",
"status": "verified"
},
"owner_wait_is_external": {
"status": "verified"
},
"six_release_blockers": {
"ids": [
"accepted_entries_missing",
"browser_matrix_unverified",
"configuration_resources_missing",
"license_closure_incomplete",
"manifest_seed_unassigned",
"official_oracle_missing"
],
"status": "verified"
},
"stop_release_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"task_map_present": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
},
{
"bytes": 26665,
"path": "coverage/post-release-audit.report.json",
"role": "post_release_audit",
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "present"
},
{
"bytes": 3467,
"path": "coverage/owner-review-gate-refresh.report.json",
"role": "owner_review",
"sha256": "33c49952650255a826bb0cf29e1063fcfd0a05a84a4b5e4273aa4fc7293b9774",
"status": "present"
},
{
"bytes": 3160,
"path": "docs/linuxcnc-web-wasm-task-map.md",
"role": "task_map",
"sha256": "9661c08d556ce4c8abb45edcef168b8fbedd9a0c7729cec93df0e31415669452",
"status": "present"
}
],
"next_transition": "WAIT-OWNER-01: pause until six owner decisions or a new locked context input arrives; no automatic retry",
"release_action": "STOP_RELEASE",
"release_status": "blocked_external_owner",
"report_sha256": "9ab0a4b1cd523197e3fbf00022fc00d59aba569453b301a2f5c38187f8d55671",
"schema": "project-task-readiness.v1",
"status": "partial",
"task_id": "GOV-01",
"tasks": [
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "M3-CFG-01",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/configuration-startup-closure.report.json",
"report_status": "partial",
"task_hash": "63323aea77d281ad2fa29d47fd29a6a743ad629869f076783db7d2e78f7794af"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "M3-RT-02",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/hal-component-manifest.report.json",
"report_status": "partial",
"task_hash": "8976d0a253abaa81cfa83fe767d33f65bde1b8e8391ced0d622672cf97938096"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "M3-RT-03",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/virtual-device-fault-matrix.report.json",
"report_status": "partial",
"task_hash": "b36343aaeb80458509a906ae6228ac0653d390b20a8b0d88948e8b507e6f60c3"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "M3-FS-01",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/workspace-session-integration.report.json",
"report_status": "partial",
"task_hash": "ea056ee88cf32a8f206c2e94571b9223a2f72befe348af84f3d0692ae89370c8"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "M3-PY-01",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/python-worker-bridge.report.json",
"report_status": "partial",
"task_hash": "f1727f19814d7a639e99fe1a2eca9c8e62be1c99aa18fc6ad9db3bf1f9958237"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "FE-M1",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/frontend-m1.report.json",
"report_status": "partial",
"task_hash": "5180544f934f6a9a348ac321c1a9ef8a9b830ed416caef844c9e3a3d55b75cef"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "FE-M2",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/frontend-m2.report.json",
"report_status": "partial",
"task_hash": "0c57f73691a827289247e294587721d4bf694024b24aca297a6d653cb8c3b58c"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "FE-M3",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/frontend-m3.report.json",
"report_status": "partial",
"task_hash": "740f81de42580a39b2232966812588f1b44f0de21bcc153d18985e20ae641998"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "FE-M4",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/frontend-m4.report.json",
"report_status": "partial",
"task_hash": "3944dfaee264ebd7fdba05abc194fa5c01ac4b332370b995e9221cdebd3562f4"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "REG-01",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/full-configuration-startup-matrix.report.json",
"report_status": "partial",
"task_hash": "4b8817b2ba44bd2d1694c404c887b7a56799422123e02a714715c632f8035161"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "REG-03",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/recovery-offline-fault-matrix.report.json",
"report_status": "partial",
"task_hash": "5ca81a3dbe4be9caa4a47ecbbb7540d424d66f858dc21d2250575c29411bc905"
},
{
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"id": "REG-04",
"implementation_status": "verified",
"release_action": "STOP_RELEASE",
"report": "coverage/browser-performance-matrix.report.json",
"report_status": "partial",
"task_hash": "325c5ddaba4d4db51616ca73612345606936e184b236d49c1ff30d65ed8d1c08"
}
]
}

View File

@@ -0,0 +1,163 @@
{
"candidate_eligible": false,
"checks": {
"manifest_pin": {
"status": "verified",
"version": "0.29.4"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"repeat_determinism": {
"status": "verified"
},
"session_dependency": {
"status": "verified"
},
"virtual_api_and_denial": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 1902,
"path": "python-runtime/manifest.json",
"role": "python_manifest",
"sha256": "340faa6ef5bedcaeb0583f463e6b89cb442ddf1d83d7e6ee8d96cee438fe4213",
"status": "present"
},
{
"bytes": 636,
"path": "coverage/python-runtime.report.json",
"role": "python_runtime",
"sha256": "be531c8cb5af55f9527b6b6b3955ff908a748fae0e37952fa699aecf1a44b399",
"status": "present"
},
{
"bytes": 3685,
"path": "coverage/workspace-session-integration.report.json",
"role": "workspace_session_integration",
"sha256": "19a5701dd49877153cfa0dc7fa886b5c0e72baadb5023143cef61bc9f94ca13b",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute FE-M1 minimal real frontend vertical slice",
"release_action": "STOP_RELEASE",
"report_sha256": "f1727f19814d7a639e99fe1a2eca9c8e62be1c99aa18fc6ad9db3bf1f9958237",
"runtime": {
"abi_version": "2025_0",
"artifacts": [
{
"bytes": 122027,
"license": "MPL-2.0",
"mirror_kind": "official",
"name": "pyodide-lock.json",
"origin_url": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"role": "package_lock",
"sha256": "14d2c2dba101277999e17135e653d8f15389ad1437f53eae213bf0c3cdff723d",
"source_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide-lock.json",
"version": "0.29.4"
},
{
"bytes": 18597,
"license": "MPL-2.0",
"mirror_kind": "official",
"name": "pyodide.js",
"origin_url": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"role": "loader_metadata",
"sha256": "7be9e63eddf6bd8786a5ab71c8689c35731d187e3415fe58d7cb95423b50421a",
"source_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/pyodide.js",
"version": "0.29.4"
}
],
"denied_capabilities": [
"os.system",
"subprocess",
"socket",
"select",
"multiprocessing",
"threading",
"ctypes",
"fcntl",
"pty",
"termios",
"device_paths",
"arbitrary_host_paths"
],
"index_url": "https://cdn.jsdelivr.net/pyodide/v0.29.4/full/",
"offline_required": true,
"origin_release": "https://github.com/pyodide/pyodide/releases/tag/0.29.4",
"package_policy": {
"allowed_packages": [],
"mode": "allowlist",
"reason": "LinuxCNC compatibility modules are virtual and core Python packages are not accepted until individually mapped and hashed."
},
"platform": "emscripten_4_0_9",
"python_version": "3.13.2",
"runtime": "Pyodide",
"schema": "python-runtime-manifest.v1",
"version": "0.29.4",
"virtual_modules": {
"INI": [
"find",
"findall",
"sections"
],
"gstat": [
"state",
"task",
"motion"
],
"hal": [
"component",
"pin",
"param",
"signal",
"thread"
],
"linuxcnc": [
"command",
"stat",
"error",
"ini"
]
},
"worker_only": true
},
"sample": {
"first": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "python_bridge=passed calls=1 errors=3 hash=fnv1a64:b05c4bdd90382502",
"stdout_sha256": "d99404df34b1e6f8e113b4dc354ccf8d2148d878651200b204521fa755fc4fbb"
},
"second": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "python_bridge=passed calls=1 errors=3 hash=fnv1a64:b05c4bdd90382502",
"stdout_sha256": "d99404df34b1e6f8e113b4dc354ccf8d2148d878651200b204521fa755fc4fbb"
}
},
"schema": "python-worker-bridge.v1",
"status": "partial",
"task_id": "M3-PY-01"
}

View File

@@ -0,0 +1,102 @@
{
"candidate_eligible": false,
"checks": {
"fault_runner": {
"status": "verified"
},
"migration_dependency": {
"status": "verified"
},
"recovery_dependency": {
"status": "verified"
},
"reg01_dependency": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"repeat_determinism": {
"status": "verified"
},
"session_dependency": {
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 225468,
"path": "coverage/full-configuration-startup-matrix.report.json",
"role": "full_configuration_matrix",
"sha256": "ab270bb11acc0825ee0a7b61f06751a88a8808e2061eea531162780043cf4a77",
"status": "present"
},
{
"bytes": 3685,
"path": "coverage/workspace-session-integration.report.json",
"role": "workspace_session_integration",
"sha256": "19a5701dd49877153cfa0dc7fa886b5c0e72baadb5023143cef61bc9f94ca13b",
"status": "present"
},
{
"bytes": 1446,
"path": "coverage/workspace-recovery.report.json",
"role": "workspace_recovery",
"sha256": "b6140a4fd0f7b7c8ced8c19685cee086d78e27910dc9ecab7d28c201a0ae85b0",
"status": "present"
},
{
"bytes": 2225,
"path": "coverage/workspace-migration.report.json",
"role": "workspace_migration",
"sha256": "ce7451b5c428f170f37c6357877f08f0c2f32dc9502490cae3fe947cc43252e8",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute REG-04 browser and performance matrix",
"release_action": "STOP_RELEASE",
"report_sha256": "5ca81a3dbe4be9caa4a47ecbbb7540d424d66f858dc21d2250575c29411bc905",
"sample": {
"first": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "recovery_offline_faults=passed scenarios=5",
"stdout_sha256": "7a62245e494eb4855f1668dab5f6577cba3e5b5e3d179798f33c0742ab5613e7"
},
"second": {
"exit_code": 0,
"status": "passed",
"stderr_sha256": "12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126",
"stdout": "recovery_offline_faults=passed scenarios=5",
"stdout_sha256": "7a62245e494eb4855f1668dab5f6577cba3e5b5e3d179798f33c0742ab5613e7"
}
},
"scenarios": [
"half_commit",
"checkpoint_hash_mismatch",
"event_gap",
"offline_missing_resource",
"online_network_required"
],
"schema": "recovery-offline-fault-matrix.v1",
"status": "partial",
"task_id": "REG-03"
}

View File

@@ -1890,35 +1890,35 @@
"bytes": 1435099, "bytes": 1435099,
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"status": "present" "status": "present"
}, },
{ {
"bytes": 4628776, "bytes": 4628776,
"path": "coverage/manifest-ownership-remediation.report.json", "path": "coverage/manifest-ownership-remediation.report.json",
"role": "manifest_ownership_remediation", "role": "manifest_ownership_remediation",
"sha256": "6b8b416aa7dd50c1411094d85370af8668334db53dd35dc72a4d46cbb464e4f3", "sha256": "d9573847f82d1c138cd08ca9af9a4ed6d390876530bee4523f149de9fd34d203",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
} }
], ],
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"remediation_status": "blocked_no_eligible_entries", "remediation_status": "blocked_no_eligible_entries",
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4", "report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
"schema": "release-acceptance-remediation.v1", "schema": "release-acceptance-remediation.v1",
"status": "complete", "status": "complete",
"task_id": "K8-REL-09" "task_id": "K8-REL-09"

View File

@@ -105,7 +105,7 @@
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"schema": "release-closure-report.v1", "schema": "release-closure-report.v1",
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"status": "blocked" "status": "blocked"
}, },
{ {
@@ -113,7 +113,7 @@
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"schema": "release-evidence-index.v1", "schema": "release-evidence-index.v1",
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"status": "blocked" "status": "blocked"
}, },
{ {
@@ -122,7 +122,7 @@
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix", "role": "compatibility_matrix",
"schema": "release-compatibility-matrix.v1", "schema": "release-compatibility-matrix.v1",
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"status": "blocked" "status": "blocked"
}, },
{ {
@@ -208,7 +208,7 @@
}, },
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "report_sha256": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"schema": "release-candidate-gate.v1", "schema": "release-candidate-gate.v1",
"source_baseline": { "source_baseline": {
"context_lock": { "context_lock": {

View File

@@ -38,12 +38,12 @@
], ],
"compatibility": { "compatibility": {
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44" "sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6"
}, },
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"evidence_index": { "evidence_index": {
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"sha256": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229" "sha256": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1"
}, },
"integrity": { "integrity": {
"all_affected_entries_have_required_evidence": true, "all_affected_entries_have_required_evidence": true,
@@ -63,7 +63,7 @@
"sha256": "ce7451b5c428f170f37c6357877f08f0c2f32dc9502490cae3fe947cc43252e8" "sha256": "ce7451b5c428f170f37c6357877f08f0c2f32dc9502490cae3fe947cc43252e8"
}, },
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1", "report_sha256": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
"schema": "release-closure-report.v1", "schema": "release-closure-report.v1",
"status": "blocked" "status": "blocked"
} }

View File

@@ -37349,7 +37349,7 @@
], ],
"compatibility_report": { "compatibility_report": {
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44" "sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6"
}, },
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"context_lock": { "context_lock": {
@@ -37427,7 +37427,7 @@
] ]
}, },
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "report_sha256": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"reports": [ "reports": [
{ {
"bytes": 497244, "bytes": 497244,
@@ -37791,7 +37791,7 @@
"path": "coverage/compatibility-matrix.report.json", "path": "coverage/compatibility-matrix.report.json",
"role": "compatibility_matrix", "role": "compatibility_matrix",
"schema": "release-compatibility-matrix.v1", "schema": "release-compatibility-matrix.v1",
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7", "sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
"status": "blocked" "status": "blocked"
}, },
{ {

View File

@@ -60,36 +60,36 @@
"comparisons": [ "comparisons": [
{ {
"actual_bytes": 26665, "actual_bytes": 26665,
"actual_sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "actual_sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"expected_bytes": 26665, "expected_bytes": 26665,
"expected_sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "expected_sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"role": "post_release_audit", "role": "post_release_audit",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 7196, "actual_bytes": 7196,
"actual_sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "actual_sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"expected_bytes": 7196, "expected_bytes": 7196,
"expected_sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "expected_sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 1945, "actual_bytes": 1945,
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"expected_bytes": 1945, "expected_bytes": 1945,
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"status": "verified" "status": "verified"
}, },
{ {
"actual_bytes": 1435099, "actual_bytes": 1435099,
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"expected_bytes": 1435099, "expected_bytes": 1435099,
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"status": "verified" "status": "verified"
@@ -112,32 +112,32 @@
"source_report_self_hashes": { "source_report_self_hashes": {
"reports": [ "reports": [
{ {
"actual": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b", "actual": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
"declared": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b", "declared": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
"path": "coverage/k8-milestone-closeout.report.json", "path": "coverage/k8-milestone-closeout.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f", "actual": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
"declared": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f", "declared": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8", "declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1", "actual": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
"declared": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1", "declared": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"status": "verified" "status": "verified"
}, },
{ {
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"declared": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229", "declared": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"status": "verified" "status": "verified"
} }
@@ -168,35 +168,35 @@
"bytes": 10299, "bytes": 10299,
"path": "coverage/k8-milestone-closeout.report.json", "path": "coverage/k8-milestone-closeout.report.json",
"role": "k8_milestone_closeout", "role": "k8_milestone_closeout",
"sha256": "6fe244b20e501083e4fabd46f51f9f1736c4c289fe7233e77d2f10924fadc823", "sha256": "4fed8e96ae515e5c68c7c3c270cfe5bbab9416874260f19382a6085aa2490108",
"status": "present" "status": "present"
}, },
{ {
"bytes": 26665, "bytes": 26665,
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"role": "post_release_audit", "role": "post_release_audit",
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 1945, "bytes": 1945,
"path": "coverage/release-closure.report.json", "path": "coverage/release-closure.report.json",
"role": "release_closure", "role": "release_closure",
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900", "sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
"status": "present" "status": "present"
}, },
{ {
"bytes": 1435099, "bytes": 1435099,
"path": "coverage/release-evidence-index.json", "path": "coverage/release-evidence-index.json",
"role": "release_evidence_index", "role": "release_evidence_index",
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333", "sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
"status": "present" "status": "present"
}, },
{ {
@@ -278,7 +278,7 @@
}, },
"release_action": "STOP_RELEASE", "release_action": "STOP_RELEASE",
"release_decision": "blocked_not_release_candidate", "release_decision": "blocked_not_release_candidate",
"report_sha256": "683a0ed187fec180705ce42becf3ce6d921018c1d0bfb4ebb99c9a85c768bb49", "report_sha256": "3b142c107e8174602c4d371b958b73c891cd3621b2142db0535213be0fac1e54",
"schema": "release-remediation-intake.v1", "schema": "release-remediation-intake.v1",
"source_baseline": { "source_baseline": {
"context_lock": { "context_lock": {

View File

@@ -29,7 +29,7 @@
"context": "verified", "context": "verified",
"path": "coverage/manifest-ownership-remediation.report.json", "path": "coverage/manifest-ownership-remediation.report.json",
"remediation_status": "blocked_pending_manifest_review", "remediation_status": "blocked_pending_manifest_review",
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179", "report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -37,7 +37,7 @@
"context": "verified", "context": "verified",
"path": "coverage/release-acceptance-remediation.report.json", "path": "coverage/release-acceptance-remediation.report.json",
"remediation_status": "blocked_no_eligible_entries", "remediation_status": "blocked_no_eligible_entries",
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4", "report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -45,7 +45,7 @@
"context": "verified", "context": "verified",
"path": "coverage/official-oracle-remediation.report.json", "path": "coverage/official-oracle-remediation.report.json",
"remediation_status": "blocked_missing_locked_inputs", "remediation_status": "blocked_missing_locked_inputs",
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d", "report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -53,7 +53,7 @@
"context": "verified", "context": "verified",
"path": "coverage/configuration-resource-remediation.report.json", "path": "coverage/configuration-resource-remediation.report.json",
"remediation_status": "blocked_resource_gaps", "remediation_status": "blocked_resource_gaps",
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1", "report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -61,7 +61,7 @@
"context": "verified", "context": "verified",
"path": "coverage/browser-matrix-remediation.report.json", "path": "coverage/browser-matrix-remediation.report.json",
"remediation_status": "blocked_matrix_unverified", "remediation_status": "blocked_matrix_unverified",
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9", "report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -69,7 +69,7 @@
"context": "verified", "context": "verified",
"path": "coverage/implementation-license-remediation.report.json", "path": "coverage/implementation-license-remediation.report.json",
"remediation_status": "blocked_pending_owner_decision", "remediation_status": "blocked_pending_owner_decision",
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca", "report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
} }
@@ -95,70 +95,70 @@
"bytes": 15574, "bytes": 15574,
"path": "coverage/release-remediation-intake.report.json", "path": "coverage/release-remediation-intake.report.json",
"role": "remediation_intake", "role": "remediation_intake",
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3", "sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
"status": "present" "status": "present"
}, },
{ {
"bytes": 7196, "bytes": 7196,
"path": "coverage/release-candidate-gate.report.json", "path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate", "role": "release_candidate_gate",
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67", "sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present" "status": "present"
}, },
{ {
"bytes": 26665, "bytes": 26665,
"path": "coverage/post-release-audit.report.json", "path": "coverage/post-release-audit.report.json",
"role": "post_release_audit", "role": "post_release_audit",
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872", "sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
"status": "present" "status": "present"
}, },
{ {
"bytes": 10299, "bytes": 10299,
"path": "coverage/k8-milestone-closeout.report.json", "path": "coverage/k8-milestone-closeout.report.json",
"role": "k8_closeout", "role": "k8_closeout",
"sha256": "6fe244b20e501083e4fabd46f51f9f1736c4c289fe7233e77d2f10924fadc823", "sha256": "4fed8e96ae515e5c68c7c3c270cfe5bbab9416874260f19382a6085aa2490108",
"status": "present" "status": "present"
}, },
{ {
"bytes": 4628776, "bytes": 4628776,
"path": "coverage/manifest-ownership-remediation.report.json", "path": "coverage/manifest-ownership-remediation.report.json",
"role": "K8-REL-08", "role": "K8-REL-08",
"sha256": "6b8b416aa7dd50c1411094d85370af8668334db53dd35dc72a4d46cbb464e4f3", "sha256": "d9573847f82d1c138cd08ca9af9a4ed6d390876530bee4523f149de9fd34d203",
"status": "present" "status": "present"
}, },
{ {
"bytes": 143970, "bytes": 143970,
"path": "coverage/release-acceptance-remediation.report.json", "path": "coverage/release-acceptance-remediation.report.json",
"role": "K8-REL-09", "role": "K8-REL-09",
"sha256": "153fdbaec1d88f83c21415eb6fa85e03593f70b70917830541521a1cbe3c5880", "sha256": "93ee6c0544fba2c24efdf6686eb566f03ff8dcec74d899721f7253d3ad8bd899",
"status": "present" "status": "present"
}, },
{ {
"bytes": 5541, "bytes": 5541,
"path": "coverage/official-oracle-remediation.report.json", "path": "coverage/official-oracle-remediation.report.json",
"role": "K8-REL-10", "role": "K8-REL-10",
"sha256": "c577f1eeeb392022abb39bce666a39cc14346638d767b1be43b6b78734c65f7c", "sha256": "45c66d3704cba5da3499832c4993f2f11e847f1e5945a8afcf1e4d700627ee16",
"status": "present" "status": "present"
}, },
{ {
"bytes": 754595, "bytes": 754595,
"path": "coverage/configuration-resource-remediation.report.json", "path": "coverage/configuration-resource-remediation.report.json",
"role": "K8-REL-11", "role": "K8-REL-11",
"sha256": "64f15f9ffda1004ec95caa6fb962a52bd9e1731a2bffe440ddb346adf256e5fb", "sha256": "f07e88f3d49ca36232627238a68a61561beee35a1f3b8158fae57038263f3616",
"status": "present" "status": "present"
}, },
{ {
"bytes": 6180, "bytes": 6180,
"path": "coverage/browser-matrix-remediation.report.json", "path": "coverage/browser-matrix-remediation.report.json",
"role": "K8-REL-12", "role": "K8-REL-12",
"sha256": "df201f0165aaebb52df6b5709c1e3a2da8e60ba552a18e03c86d984550bddc9a", "sha256": "6f150474a385a41caf71b964d6c11133d34e742a211f985f207908687aecb37c",
"status": "present" "status": "present"
}, },
{ {
"bytes": 41237, "bytes": 41237,
"path": "coverage/implementation-license-remediation.report.json", "path": "coverage/implementation-license-remediation.report.json",
"role": "K8-REL-13", "role": "K8-REL-13",
"sha256": "5f244d1c379e72fcbda6a34685e44af5d34175660b5c622c461e3d468db18a0e", "sha256": "6dec4492d81ccebd7a25a3962e5cd8f0975795f8513f580324d3df0bef49ab68",
"status": "present" "status": "present"
} }
], ],
@@ -195,7 +195,7 @@
"context": "verified", "context": "verified",
"path": "coverage/manifest-ownership-remediation.report.json", "path": "coverage/manifest-ownership-remediation.report.json",
"remediation_status": "blocked_pending_manifest_review", "remediation_status": "blocked_pending_manifest_review",
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179", "report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -203,7 +203,7 @@
"context": "verified", "context": "verified",
"path": "coverage/release-acceptance-remediation.report.json", "path": "coverage/release-acceptance-remediation.report.json",
"remediation_status": "blocked_no_eligible_entries", "remediation_status": "blocked_no_eligible_entries",
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4", "report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -211,7 +211,7 @@
"context": "verified", "context": "verified",
"path": "coverage/official-oracle-remediation.report.json", "path": "coverage/official-oracle-remediation.report.json",
"remediation_status": "blocked_missing_locked_inputs", "remediation_status": "blocked_missing_locked_inputs",
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d", "report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -219,7 +219,7 @@
"context": "verified", "context": "verified",
"path": "coverage/configuration-resource-remediation.report.json", "path": "coverage/configuration-resource-remediation.report.json",
"remediation_status": "blocked_resource_gaps", "remediation_status": "blocked_resource_gaps",
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1", "report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -227,7 +227,7 @@
"context": "verified", "context": "verified",
"path": "coverage/browser-matrix-remediation.report.json", "path": "coverage/browser-matrix-remediation.report.json",
"remediation_status": "blocked_matrix_unverified", "remediation_status": "blocked_matrix_unverified",
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9", "report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
}, },
@@ -235,12 +235,12 @@
"context": "verified", "context": "verified",
"path": "coverage/implementation-license-remediation.report.json", "path": "coverage/implementation-license-remediation.report.json",
"remediation_status": "blocked_pending_owner_decision", "remediation_status": "blocked_pending_owner_decision",
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca", "report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
"self_hash": "verified", "self_hash": "verified",
"status": "complete" "status": "complete"
} }
}, },
"report_sha256": "93ab5bc729394f209e281c769a7fb381286f2ae65281414ed23a226f9fff8152", "report_sha256": "7258fe80e7bd4ea9e7adcdc1c6f64d414f046ce88b6eb928fb03e4643de10505",
"review_policy": { "review_policy": {
"forbidden": [ "forbidden": [
"promote_blocked_to_accepted", "promote_blocked_to_accepted",

View File

@@ -0,0 +1,459 @@
{
"candidate_eligible": false,
"checks": {
"hal_configuration_dependency": {
"status": "verified"
},
"native_wasm_io_baseline": {
"status": "verified"
},
"nonfinite_input_blocked": {
"status": "verified"
},
"normal_fault_abort_recovery_coverage": {
"scenario_count": 26,
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"repeat_determinism": {
"status": "verified"
},
"replay_state_hash": {
"replay_state_hash": "fnv1a64:c94eac708756480a",
"state_hash": "fnv1a64:c94eac708756480a",
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 7158,
"path": "coverage/io-runtime.report.json",
"role": "io_runtime",
"sha256": "a5bf67b642b7812796ab0a168d601bb24eda8ceec2418ffc02fc718fa642768e",
"status": "present"
},
{
"bytes": 2535591,
"path": "coverage/hal-component-manifest.report.json",
"role": "hal_component_manifest",
"sha256": "8ee349639377ba653347a1fc29af51bfc723aa3fd4dfd25426a736375f6e9769",
"status": "present"
},
{
"bytes": 14796472,
"path": "coverage/configuration-startup-closure.report.json",
"role": "configuration_startup_closure",
"sha256": "95b106e6174cabea38295e5cce9e01b143fe1d9956a2285897e09465c949363b",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute M3-FS-01 workspace recovery and configuration session integration",
"rebuild": {
"command": "node web/worker/test_virtual_devices.mjs",
"native_wasm_baseline": "coverage/io-runtime.report.json",
"repeat_runs": 2
},
"release_action": "STOP_RELEASE",
"report_sha256": "b36343aaeb80458509a906ae6228ac0653d390b20a8b0d88948e8b507e6f60c3",
"sample": {
"events": 25,
"final": {
"estop": false,
"fault": false,
"fault_reason": 0,
"last_error": "sim_io_ok",
"limits": {
"x-positive": true
},
"probe": {
"input": true,
"position": {
"x": 1,
"y": 2,
"z": 3
},
"state": "tripped",
"tick": 40,
"tripped": true,
"type": 0
},
"ready": true,
"recovery_epoch": 1,
"spindle_count": 1,
"spindles": [
{
"amp_fault": false,
"at_speed": true,
"brake": true,
"command_rpm": 0,
"enabled": false,
"index_enable": false,
"inhibit": false,
"is_oriented": false,
"locked": false,
"orient": false,
"orient_angle": 0,
"orient_fault": 0,
"orient_mode": 0,
"revs": 40,
"speed_rpm": 0
}
],
"tick": 40,
"tool": {
"changed": false,
"current": 7,
"fault": false,
"pocket": 3,
"prepare": false,
"prepared": 7,
"prepared_ack": false,
"reason": 0,
"state": "idle"
},
"tool_capacity": 1001
},
"replay_state_hash": "fnv1a64:c94eac708756480a",
"scenarios": [
{
"code": "sim_io_nonfinite",
"kind": "NONFINITE_INPUT",
"state_hash": "fnv1a64:9ebe32ad6f2a056d",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:152decbdb10c2bdb",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_STEP",
"state_hash": "fnv1a64:4810e4c6621edfc0",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARE",
"state_hash": "fnv1a64:f0182f92a6352ee9",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARED",
"state_hash": "fnv1a64:510642507e89d608",
"status": "verified"
},
{
"code": "sim_io_busy",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:6da38a64c8f3cc83",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:40e14e26ad660018",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_STEP",
"state_hash": "fnv1a64:01c0c166f322d8b9",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:d84cf3425a981a27",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGED",
"state_hash": "fnv1a64:cbf1eaa1831f21ee",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_ABORT",
"state_hash": "fnv1a64:b7a56fa2a213fc05",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARE",
"state_hash": "fnv1a64:6fb0c49ecae07f47",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARED",
"state_hash": "fnv1a64:43cc546851442a12",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:98f6fd9f0502dfd4",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGED",
"state_hash": "fnv1a64:cbf1eaa1831f21ee",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "PROBE_CONFIG",
"state_hash": "fnv1a64:5e242d5fbaacf88f",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "PROBE_CONFIG",
"state_hash": "fnv1a64:50dfd6d8da48dd52",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "LIMIT_SET",
"state_hash": "fnv1a64:0ebaebf42016a03a",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_FAULT",
"state_hash": "fnv1a64:c8f76e689ea70778",
"status": "verified"
},
{
"code": "sim_io_fault",
"kind": "TOOL_CLEAR_FAULT",
"state_hash": "fnv1a64:a5e036e802202e30",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "TOOL_FAULT",
"state_hash": "fnv1a64:61239060f5ce1f6e",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CLEAR_FAULT",
"state_hash": "fnv1a64:f5ad019a121d11ea",
"status": "verified"
},
{
"code": "sim_io_estop",
"kind": "ESTOP",
"state_hash": "fnv1a64:c9fbf3ebca943045",
"status": "blocked"
},
{
"code": "sim_io_estop",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:c9fbf3ebca943045",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "ESTOP",
"state_hash": "fnv1a64:c103c846827f7ff5",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_RECOVER",
"state_hash": "fnv1a64:c94eac708756480a",
"status": "verified"
}
],
"schema": "virtual-device-fault-sample.v1",
"state_hash": "fnv1a64:c94eac708756480a",
"state_hash_equal": true
},
"scenarios": [
{
"code": "sim_io_nonfinite",
"kind": "NONFINITE_INPUT",
"state_hash": "fnv1a64:9ebe32ad6f2a056d",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:152decbdb10c2bdb",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_STEP",
"state_hash": "fnv1a64:4810e4c6621edfc0",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARE",
"state_hash": "fnv1a64:f0182f92a6352ee9",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARED",
"state_hash": "fnv1a64:510642507e89d608",
"status": "verified"
},
{
"code": "sim_io_busy",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:6da38a64c8f3cc83",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:40e14e26ad660018",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_STEP",
"state_hash": "fnv1a64:01c0c166f322d8b9",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:d84cf3425a981a27",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGED",
"state_hash": "fnv1a64:cbf1eaa1831f21ee",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_ABORT",
"state_hash": "fnv1a64:b7a56fa2a213fc05",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARE",
"state_hash": "fnv1a64:6fb0c49ecae07f47",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_PREPARED",
"state_hash": "fnv1a64:43cc546851442a12",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGE",
"state_hash": "fnv1a64:98f6fd9f0502dfd4",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CHANGED",
"state_hash": "fnv1a64:cbf1eaa1831f21ee",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "PROBE_CONFIG",
"state_hash": "fnv1a64:5e242d5fbaacf88f",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "PROBE_CONFIG",
"state_hash": "fnv1a64:50dfd6d8da48dd52",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "LIMIT_SET",
"state_hash": "fnv1a64:0ebaebf42016a03a",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_FAULT",
"state_hash": "fnv1a64:c8f76e689ea70778",
"status": "verified"
},
{
"code": "sim_io_fault",
"kind": "TOOL_CLEAR_FAULT",
"state_hash": "fnv1a64:a5e036e802202e30",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "TOOL_FAULT",
"state_hash": "fnv1a64:61239060f5ce1f6e",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "TOOL_CLEAR_FAULT",
"state_hash": "fnv1a64:f5ad019a121d11ea",
"status": "verified"
},
{
"code": "sim_io_estop",
"kind": "ESTOP",
"state_hash": "fnv1a64:c9fbf3ebca943045",
"status": "blocked"
},
{
"code": "sim_io_estop",
"kind": "SPINDLE_COMMAND",
"state_hash": "fnv1a64:c9fbf3ebca943045",
"status": "blocked"
},
{
"code": "sim_io_ok",
"kind": "ESTOP",
"state_hash": "fnv1a64:c103c846827f7ff5",
"status": "verified"
},
{
"code": "sim_io_ok",
"kind": "IO_RECOVER",
"state_hash": "fnv1a64:c94eac708756480a",
"status": "verified"
}
],
"schema": "virtual-device-fault-matrix.v1",
"status": "partial",
"task_id": "M3-RT-03"
}

View File

@@ -0,0 +1,114 @@
{
"candidate_eligible": false,
"checks": {
"configuration_dependency": {
"status": "verified"
},
"context_consistency": {
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"status": "verified"
},
"device_dependency": {
"status": "verified"
},
"hal_dependency": {
"status": "verified"
},
"package_round_trip": {
"package_v1_hash": "fnv1a64:8b886658879e2cdc",
"package_v2_hash": "fnv1a64:6942cfb13a6d21e1",
"status": "verified"
},
"recovery_migration_dependency": {
"status": "verified"
},
"release_stop_preserved": {
"release_action": "STOP_RELEASE",
"status": "verified"
},
"repeat_determinism": {
"status": "verified"
},
"session_state_recovery": {
"recovery_state_hash": "fnv1a64:cd5eb4fdf95d6a50",
"session_state_hash": "fnv1a64:cd5eb4fdf95d6a50",
"status": "verified"
}
},
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"implementation_status": "verified",
"inputs": [
{
"bytes": 7647,
"path": "context-lock.json",
"role": "context_lock",
"sha256": "785051d2a826ff45223da0a3dada919b75ac03a4ffb3c7e6f19083e78adc1abc",
"status": "present"
},
{
"bytes": 14796472,
"path": "coverage/configuration-startup-closure.report.json",
"role": "configuration_startup_closure",
"sha256": "95b106e6174cabea38295e5cce9e01b143fe1d9956a2285897e09465c949363b",
"status": "present"
},
{
"bytes": 2535591,
"path": "coverage/hal-component-manifest.report.json",
"role": "hal_component_manifest",
"sha256": "8ee349639377ba653347a1fc29af51bfc723aa3fd4dfd25426a736375f6e9769",
"status": "present"
},
{
"bytes": 11813,
"path": "coverage/virtual-device-fault-matrix.report.json",
"role": "virtual_device_fault_matrix",
"sha256": "6969a4a0aee13c2a38f9d02fdfbb1456076a69f214c8cc2d1df84f438f373d51",
"status": "present"
},
{
"bytes": 1446,
"path": "coverage/workspace-recovery.report.json",
"role": "workspace_recovery",
"sha256": "b6140a4fd0f7b7c8ced8c19685cee086d78e27910dc9ecab7d28c201a0ae85b0",
"status": "present"
},
{
"bytes": 2225,
"path": "coverage/workspace-migration.report.json",
"role": "workspace_migration",
"sha256": "ce7451b5c428f170f37c6357877f08f0c2f32dc9502490cae3fe947cc43252e8",
"status": "present"
},
{
"bytes": 7196,
"path": "coverage/release-candidate-gate.report.json",
"role": "release_candidate_gate",
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
"status": "present"
}
],
"next_transition": "execute M3-PY-01 actual Pyodide virtual API bridge",
"rebuild": {
"command": "node web/worker/test_workspace_session_integration.mjs",
"repeat_runs": 2
},
"release_action": "STOP_RELEASE",
"report_sha256": "ea056ee88cf32a8f206c2e94571b9223a2f72befe348af84f3d0692ae89370c8",
"sample": {
"checkpoint_hash": "fnv1a64:8fac2c7af27f0b6e",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"event_cursor": 12,
"journal_hash": "fnv1a64:1064d5ed6ce0552a",
"operation_count": 4,
"package_v1_hash": "fnv1a64:8b886658879e2cdc",
"package_v2_hash": "fnv1a64:6942cfb13a6d21e1",
"recovery_state_hash": "fnv1a64:cd5eb4fdf95d6a50",
"round_trip_equal": true,
"schema": "workspace-session-integration-sample.v1",
"session_state_hash": "fnv1a64:cd5eb4fdf95d6a50"
},
"schema": "workspace-session-integration.v1",
"status": "partial",
"task_id": "M3-FS-01"
}

View File

@@ -249,10 +249,10 @@ OPFS 只保存已提交工程、journal、checkpoint、日志和可删除缓存
## 11. 当前立即执行顺序 ## 11. 当前立即执行顺序
1. 按 `nextTask.md` 执行 `K0-INV-01`,处理旧 `M2-CORE-04` 的 kinematics 输入阻塞,先确认锁定提交中不存在声明路径,并保留报告。 1. 按 `nextTask.md` 执行 `M3-CFG-01`,把 264 个仿真配置入口和 16,093 条资源边从静态索引推进为确定性启动解析;保留 missing/unresolved/host path,禁止静默同名替换。
2. 若要采用替代 oracle,先创建可审查的契约变更和新的 context lock;否则保持 blocked,不推进 HAL 节点。 2. 完成 `M3-RT-02`,将 HAL pin/signal/thread/function、`.comp` 元数据和配置图接入可执行 preflight;运动学只作为已完成的注册/调用依赖,不再重复解析。
3. 在 kinematics 契约闭合后,完成静态模块注册、`forward/inverse/limits` ABI、确定性状态哈希和失败路径测试。 3. 完成 `M3-RT-03`,把 spindle/tool/probe/limit/estop 数字孪生接入 Worker command path,并让故障注入可录制、回放和恢复。
4. 继续 `M2-CORE-05` 前,先让 native runner、WASM runner 和 `sim_step` 共享同一 tick/golden schema。 4. 完成 `M3-FS-01` 与 `M3-PY-01`,将 workspace recovery/migration 和实际 Pyodide 虚拟 API 接入真实 session;保持现有 schema 和宿主能力拒绝契约。
5. 只有 M2 核心条目达到 `verified`,才进入 HAL/数字孪生/OPFS 恢复;只有 M3 通过,才把 mock UI 替换成真实 Worker 工作流。 5. 完成 `FE-M1` 最小真实前端垂直切片,再运行 `REG-01/03/04` 的配置、恢复、浏览器和性能矩阵;K8 release owner blocker 只在真实 owner 输入或新 lock context 到位后单独刷新。
本文件完成的是实施规则和证据闭环,不改变当前任务指针,也不把尚未实现的 LinuxCNC 功能标为完成。 本文件完成的是实施规则和证据闭环,不改变当前任务指针,也不把尚未实现的 LinuxCNC 功能标为完成。

View File

@@ -0,0 +1,43 @@
# LinuxCNC Web/WASM 后续工作基线
更新时间:2026-08-21
## 当前判断
运动学实现主线 K0-K6 已完成代码和 native/WASM 回归:trivkins、耦合运动学、5 轴/TRT、机器人、GenSer、Delta、Tripod、Hexapod、Pentapod、switchkins、userk/registry、TP→IK→motion、FK feedback、home/limit 均已有实现、ABI、固定边界和测试产物。对应报告仍为 `partial` 或 `blocked` 的原因是锁定 LinuxCNC 来源没有 `linuxcnc/unit_tests/kinematics/**` 官方 oracle;这限制发布验收,不应继续重复实现运动学解析。
已验证的其它基础包括:posemath、解释器、motion queue、HAL 核心、IO 核心、Worker protocol、replay、workspace recovery/migration、Python runtime manifest 和 native CTest。当前仓库没有前端应用壳,也没有把 INI/HAL 资源图接入真实 Worker 启动路径。
发布状态仍为 `blocked_not_release_candidate`/`STOP_RELEASE`。六项发布 blocker(manifest ownership、accepted entries、official oracle、configuration resources、browser matrix、implementation license)必须保留;owner review 是外部输入,不再作为当前开发任务反复重跑。
## 执行顺序
| 顺序 | 任务 | 目标 | 主要产物 | 依赖 |
|---|---|---|---|---|
| 1 | `M3-CFG-01` | 已完成:把 INI/HAL/资源引用从静态索引推进到确定性启动解析;支持相对路径、`LIB:`、参数尾缀、目录和变量/宿主路径的显式错误 | `web/workspace/configuration.mjs`、`coverage/configuration-startup-closure.report.json`、生成器和单测 | 当前 config graph/resource index |
| 2 | `M3-RT-02` | 已完成:将 HAL pin/signal/thread/function 和 `.comp` 元数据接成可执行配置图,保留类型、重复注册、周期和失败边界 | `coverage/hal-component-manifest.report.json`、HAL preflight、native/WASM/Worker test | `M3-CFG-01`、`sim_hal` |
| 3 | `M3-RT-03` | 已完成:将 spindle、tool、probe、limit、estop 数字孪生接入 Worker command path,并让故障注入可录制/回放 | `coverage/virtual-device-fault-matrix.report.json`、event/state golden、Worker tests | `M3-RT-02`、`sim_io` |
| 4 | `M3-FS-01` | 已完成:将 workspace recovery/migration 从独立模块接入真实 session 和配置加载,验证 OPFS 适配边界 | `coverage/workspace-session-integration.report.json`、Worker workspace integration report | `M3-CFG-01`、M3-RT-02/03、现有 recovery/migration |
| 5 | `M3-PY-01` | 已完成:用固定 Pyodide artifact 建立实际 Worker bridge;虚拟 `linuxcnc`/`hal`/`INI` API 和宿主能力拒绝路径可执行 | `coverage/python-worker-bridge.report.json`、Worker tests | `M3-CFG-01`、FS-01 |
| 6 | `FE-M1` | 已完成:建立最小真实前端垂直切片:AppShell、WorkerClient、工程加载、能力协商、step/snapshot/error | `coverage/frontend-m1.report.json`、`web/app`、browser smoke/e2e report | protocol v1、M3-CFG-01、Worker |
| 7 | `FE-M2` | 已完成:核心操作 controls 和权威状态面板 | `coverage/frontend-m2.report.json`、browser operation smoke、state panel report | FE-M1、M3-RT-03 |
| 8 | `FE-M3` | 项目和恢复工作流:导入/导出、journal/checkpoint、Worker 重启恢复、离线冲突状态 | `coverage/frontend-m3.report.json`、project/recovery browser smoke | FE-M2、M3-FS-01 |
| 9 | `FE-M4` | 已完成:HAL/tool/IO/3D frontend workflows | `coverage/frontend-m4.report.json`、device/HAL/machine browser smoke | FE-M3、M3-RT-02/03 |
| 10 | `REG-01` | 运行 233 个 sim entrypoint 的启动矩阵,分类 ready/blocked/error 并生成首个差异定位 | full configuration matrix report | M3-CFG-01..03、M3-PY-01、FE-M4 |
| 10 | `REG-03/04` | 完成恢复、断网、浏览器和性能矩阵;再回到 K8 release gate | recovery/browser/performance reports | FE-M1、REG-01 |
## 当前任务边界:REG-01
允许创建全配置 startup matrix 生成器、结果分类和 source/resource/component 差异定位测试;不修改 kinematics 算法、ABI/protocol/schema/context lock,不替 owner 作许可证、浏览器或官方 oracle 决定。
完成条件:
- M3-CFG/RT/FS/PY、FE-M1..M4 已完成;当前执行 233 个 sim entrypoint 全量启动矩阵。
- 矩阵必须保留每个 blocked/error source ID,不得删除样本或把资源/组件阻断重分类为 REAL-EXCLUDED。
- native CTest、Worker replay/recovery/migration 和全部脚本单测保持通过,六项发布 blocker 与 `STOP_RELEASE` 不被清除。
## 不再重复的工作
- 不重新实现 K0-K6 运动学解析或重新生成同一组 blocked oracle 报告。
- 不因官方 oracle 缺失而伪造 golden、把派生 fixture 标为官方输入或升级 `accepted`。
- 不因 owner 输入缺失而循环刷新 K8-REL-16;收到真实 owner decision 或新 lock 输入时再单独创建 release context。

View File

@@ -0,0 +1,45 @@
# LinuxCNC Web/WASM 任务地图
更新时间:2026-08-21
## 任务分层
### 已完成实现主线
| 任务 | 证据 | 结论 |
|---|---|---|
| `M3-CFG-01` | `coverage/configuration-startup-closure.report.json` | 16,093 条资源全部分类;15,112 resolved、981 blocked;`LIB:`/路径/host path 策略确定性 |
| `M3-RT-02` | `coverage/hal-component-manifest.report.json` | 145 `.comp`、1,220 pins、140 params、530 functions、4,566 HAL graph records |
| `M3-RT-03` | `coverage/virtual-device-fault-matrix.report.json` | 26 个虚拟设备故障/replay 场景,连续与重放 hash 一致 |
| `M3-FS-01` | `coverage/workspace-session-integration.report.json` | 配置/HAL/设备/session checkpoint、recovery、package migration 闭合 |
| `M3-PY-01` | `coverage/python-worker-bridge.report.json` | Worker-only Python bridge、虚拟 API、宿主能力拒绝、Python checkpoint/replay |
| `FE-M1..M4` | `coverage/frontend-m1.report.json` 到 `frontend-m4.report.json` | AppShell、WorkerClient、controls、state/recovery、device/HAL/machine view |
| `REG-01` | `coverage/full-configuration-startup-matrix.report.json` | 264 registered entries、233 entrypoints,56 ready、208 blocked |
| `REG-03` | `coverage/recovery-offline-fault-matrix.report.json` | half-commit、坏 checkpoint、event gap、offline missing/network-required |
| `REG-04` | `coverage/browser-performance-matrix.report.json` | 当前范围仅 Chromium;Firefox/Safari `out_of_scope`;稳定资源性能样例 |
这些任务的 `implementation_status` 均为 `verified`。报告整体为 `partial` 是因为配置资源、官方 oracle 或 release gate blocker 仍真实存在,不代表实现代码未完成。
### 外部输入阻塞
K8 release gate 当前仍是 `candidate_eligible=false`、`STOP_RELEASE`,六项 blocker 不由本地代码自动解决:
- manifest feature owner 尚未批准;
- 没有 accepted release entries;
- 锁定来源没有官方 kinematics/recovery oracle;
- 配置资源仍有 missing/unresolved/unindexed rows;
- browser minimum/context policy 尚未 owner 锁定;当前实现矩阵只测试 Chromium;
- 57 个 Web/WASM 条目没有批准 SPDX/NOTICE 决定。
`coverage/owner-review-gate-refresh.report.json` 明确记录 `owner_inputs=0` 和 `gate_refresh=blocked`。在真实 owner decision 或新 context lock 到位前,不得创建新 context、刷新 gate、清除 blocker 或重复执行同一 release retry。
### 正确接续
当前本地可执行任务是 `GOV-01`:生成一次任务图和 blocker readiness 审计,确认已完成实现不会被重复认领,确认 release wait 是单向 external wait。完成后进入 `WAIT-OWNER-01`,该节点没有自动重试;只有收到外部输入才重新打开 K8 release task。
## 不允许的状态转换
- `partial`/`blocked oracle` 不得改成 `accepted`。
- `WAIT-OWNER-01` 不得自动转回 `K8-REL-17`。
- Firefox/Safari 不得因为当前 out-of-scope 而被伪装成已验证;历史 K8 browser audit 保持只读证据。
- context lock 不得就地覆盖;源、schema、manifest 或 owner 决策变化必须建立新 context。

View File

@@ -64,6 +64,18 @@
| `K8-REL-13` | implementation license remediation 覆盖 57/57 wasm-core/web entries;源哈希与 audit evidence verified,blocked_unassigned=57、approved_spdx=0、未批准声明=0,上游 license 不继承,`STOP_RELEASE` verified,下一步汇总 remediation evidence | `coverage/implementation-license-remediation.report.json`, `scripts/generate_implementation_license_remediation.py`, `scripts/test_implementation_license_remediation.py`, `docs/linuxcnc-web-wasm-implementation-license-remediation.md` | | `K8-REL-13` | implementation license remediation 覆盖 57/57 wasm-core/web entries;源哈希与 audit evidence verified,blocked_unassigned=57、approved_spdx=0、未批准声明=0,上游 license 不继承,`STOP_RELEASE` verified,下一步汇总 remediation evidence | `coverage/implementation-license-remediation.report.json`, `scripts/generate_implementation_license_remediation.py`, `scripts/test_implementation_license_remediation.py`, `docs/linuxcnc-web-wasm-implementation-license-remediation.md` |
| `K8-REL-14` | remediation evidence roundup 汇总 K8-REL-08..13;6 份报告 self-hash/context、六个 blocker identity、无 promotion/fabrication 和 gate/audit/closeout/intake 的 `STOP_RELEASE` 均 verified;状态为 `blocked_pending_owner_actions`,下一步 owner review 与新 context/gate refresh | `coverage/remediation-evidence-roundup.report.json`, `scripts/generate_remediation_evidence_roundup.py`, `scripts/test_remediation_evidence_roundup.py`, `docs/linuxcnc-web-wasm-remediation-evidence-roundup.md` | | `K8-REL-14` | remediation evidence roundup 汇总 K8-REL-08..13;6 份报告 self-hash/context、六个 blocker identity、无 promotion/fabrication 和 gate/audit/closeout/intake 的 `STOP_RELEASE` 均 verified;状态为 `blocked_pending_owner_actions`,下一步 owner review 与新 context/gate refresh | `coverage/remediation-evidence-roundup.report.json`, `scripts/generate_remediation_evidence_roundup.py`, `scripts/test_remediation_evidence_roundup.py`, `docs/linuxcnc-web-wasm-remediation-evidence-roundup.md` |
| `K8-REL-15` | owner-review gate refresh 定位六项 owner decision,但 workspace 没有批准输入或新 context;owner_inputs=0、gate_refresh=blocked,`STOP_RELEASE` verified,任务保持 blocked,后续仅在 owner 决策后创建新 context 并刷新 gate | `coverage/owner-review-gate-refresh.report.json`, `scripts/generate_owner_review_gate_refresh.py`, `scripts/test_owner_review_gate_refresh.py`, `docs/linuxcnc-web-wasm-owner-review-gate-refresh.md` | | `K8-REL-15` | owner-review gate refresh 定位六项 owner decision,但 workspace 没有批准输入或新 context;owner_inputs=0、gate_refresh=blocked,`STOP_RELEASE` verified,任务保持 blocked,后续仅在 owner 决策后创建新 context 并刷新 gate | `coverage/owner-review-gate-refresh.report.json`, `scripts/generate_owner_review_gate_refresh.py`, `scripts/test_owner_review_gate_refresh.py`, `docs/linuxcnc-web-wasm-owner-review-gate-refresh.md` |
| `K8-REL-16` | 在没有 owner 决定或新锁定输入时重建 closure/audit/remediation/owner-review 证据;六项 blocker identity、context consistency 和 `STOP_RELEASE` 均 verified,owner_inputs=0、gate_refresh=blocked,context lock 未修改,任务保持 blocked | `coverage/owner-review-gate-refresh.report.json`, `coverage/release-candidate-gate.report.json`, `coverage/post-release-audit.report.json`, `scripts/generate_owner_review_gate_refresh.py`, `scripts/test_owner_review_gate_refresh.py` |
| `PROJECT-AUDIT-01` | 整体审计确认 K0-K6 运动学实现和 15 项 native CTest 已通过;运动学 `partial/blocked` 仅因锁定来源缺少官方 oracle;当前开发瓶颈转为配置启动闭包,下一任务为 M3-CFG-01,K8 release 六项 blocker/`STOP_RELEASE` 独立保留 | `docs/linuxcnc-web-wasm-next-work.md`, `coverage/kinematics-registry.report.json`, `coverage/kinematics-motion.report.json`, `coverage/kinematics-feedback.report.json`, `coverage/kinematics-home-limit.report.json`, `coverage/configuration-regression.report.json` |
| `PROJECT-AUDIT-02` | 重新梳理任务图确认 M3/FE/REG 实现证据均已 verified;K8-REL-17 owner_inputs=0、gate_refresh=blocked,发现旧 Next 自引用风险;生成 GOV-01 readiness 后进入不自动重试的 WAIT-OWNER-01,六项 blocker 和 `STOP_RELEASE` 保持 verified | `docs/linuxcnc-web-wasm-task-map.md`, `coverage/project-task-readiness.report.json`, `nextTask.md` |
| `M3-CFG-01` | 确定性配置 resolver 和 Worker preflight 完成;16,093 条资源全部分类,15,112 resolved、981 blocked;显式 `LIB:`、参数尾缀、相对路径、仓库外路径和宿主路径均有明确策略;原始索引 hash、context lock、187 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `web/workspace/configuration.mjs`, `web/worker/sim-session.mjs`, `coverage/configuration-startup-closure.report.json`, `scripts/generate_configuration_startup_closure.py`, `scripts/test_configuration_startup_closure.py`, `web/workspace/test_configuration.mjs`, `web/worker/test_configuration_preflight.mjs` |
| `M3-RT-02` | 生成 145 个 `.comp` 组件 manifest、1,220 pins、140 params、530 functions 和 193 个 HAL 文件的 4,566 条配置图记录;manifest、容量、资源边和动态加载拒绝检查 verified,29 个 `loadusr` 与 124 个变量化/未声明 `loadrt` 保持 blocked;190 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/hal-component-manifest.report.json`, `scripts/generate_hal_component_manifest.py`, `scripts/test_hal_component_manifest.py`, `web/workspace/hal-configuration.mjs`, `web/worker/sim-session.mjs`, `web/worker/test_hal_preflight.mjs` |
| `M3-RT-03` | 虚拟设备故障矩阵完成;26 个 spindle/tool/probe/limit/estop/非有限/恢复场景进入 EventLog,连续执行与 replay state hash 一致,设备 checkpoint 增量恢复通过;192 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/virtual-device-fault-matrix.report.json`, `scripts/generate_virtual_device_fault_matrix.py`, `scripts/test_virtual_device_fault_matrix.py`, `web/worker/virtual-devices.mjs`, `web/worker/test_virtual_devices.mjs`, `web/worker/test_device_recovery.mjs` |
| `M3-FS-01` | 配置/HAL/虚拟设备/session 状态统一到 checkpoint;设备事件增量恢复后 session state hash 一致,workspace package v1↔v2 round-trip 通过;194 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/workspace-session-integration.report.json`, `scripts/generate_workspace_session_integration.py`, `scripts/test_workspace_session_integration.py`, `web/worker/test_workspace_session_integration.mjs`, `web/worker/sim-session.mjs` |
| `M3-PY-01` | Worker-only Python bridge 完成;固定 Pyodide manifest/artifact hash、虚拟模块 import、能力拒绝、Python API 调用、异常/资源限制和 Python session checkpoint/replay 均通过;196 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/python-worker-bridge.report.json`, `scripts/generate_python_worker_bridge.py`, `scripts/test_python_worker_bridge.py`, `web/worker/python-bridge.mjs`, `web/worker/test_python_bridge.mjs`, `web/worker/test_python_session.mjs` |
| `FE-M1` | 建立真实 WorkerClient/AppShell;capabilities、session attach、workspace load、step、snapshot、error 和 operation 去重 smoke 通过;198 个脚本测试、15 个 native CTest 和 Worker/workspace/Python 回归通过 | `coverage/frontend-m1.report.json`, `scripts/generate_frontend_m1.py`, `scripts/test_frontend_m1.py`, `web/app/index.html`, `web/app/app.mjs`, `web/app/worker-client.mjs`, `web/app/test_frontend_m1.mjs` |
| `FE-M2` | 完成核心 operation controls 和权威 state panel;run/pause/resume/abort/home/estop/recover 控制、operation 去重、错误/报警状态和窄屏布局 smoke 通过;200 个脚本测试、15 个 native CTest 和 Worker/workspace/Python 回归通过 | `coverage/frontend-m2.report.json`, `scripts/generate_frontend_m2.py`, `scripts/test_frontend_m2.py`, `web/app/controls.mjs`, `web/app/state-panel.mjs`, `web/app/test_frontend_m2.mjs` |
| `FE-M3` | 项目/恢复 workflow 完成;journal 导入导出、checkpoint/package、v1↔v2 migration、offline ready 和 half-commit 状态展示 smoke 通过;202 个脚本测试、15 个 native CTest 和 Worker/workspace/Python 回归通过 | `coverage/frontend-m3.report.json`, `scripts/generate_frontend_m3.py`, `scripts/test_frontend_m3.py`, `web/app/project-workflow.mjs`, `web/app/recovery-panel.mjs`, `web/app/test_frontend_m3.mjs` |
| `FE-M4` | 完成 HAL/tool/IO/3D frontend workflows;设备命令、HAL diagnostics、稳定 canvas machine view 和故障状态展示 smoke 通过;204 个脚本测试、15 个 native CTest 和 Worker/workspace/Python 回归通过 | `coverage/frontend-m4.report.json`, `scripts/generate_frontend_m4.py`, `scripts/test_frontend_m4.py`, `web/app/device-panel.mjs`, `web/app/hal-panel.mjs`, `web/app/machine-view.mjs`, `web/app/test_frontend_m4.mjs` |
## 追溯规则 ## 追溯规则

View File

@@ -9,18 +9,18 @@ pointer_version: 3
updated: 2026-08-21 updated: 2026-08-21
context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1 context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1
milestone: K8 milestone: K8
last_verified_checkpoint: K8-REL-15-r01 last_verified_checkpoint: PROJECT-AUDIT-02
task: task:
id: K8-REL-15 id: WAIT-OWNER-01
status: blocked status: blocked
title: owner review and release gate refresh title: pause until external owner decisions or new lock input
owner: WS-7 owner: WS-5
profile: milestone profile: milestone
next: next:
id: K8-REL-16 id: OWNER-INPUT-01
status: pending status: blocked
title: new context and release gate refresh after owner decisions title: six owner decisions or new locked context input required
owner: WS-7 owner: WS-5
profile: milestone profile: milestone
``` ```
@@ -41,48 +41,57 @@ python3 scripts/validate_task_pointer.py
## Task ## Task
**目标**:检查 K8-REL-14 roundup 后是否存在六项 owner decision 或新锁定输入,并在缺失时 fail-closed;保留六项阻塞和 `STOP_RELEASE`。 **目标**:等待六项 owner decision 或新的 locked context input;期间不重跑 K8 release gate、不创建新 context、不重复执行已完成 implementation matrix。
**直接输入**:`coverage/remediation-evidence-roundup.report.json`、`coverage/release-candidate-gate.report.json`、`coverage/post-release-audit.report.json`、`context-lock.json`。 **直接输入**:`docs/linuxcnc-web-wasm-task-map.md`、`coverage/project-task-readiness.report.json`、全部 M3/FE/REG 报告、`coverage/release-candidate-gate.report.json`、`coverage/post-release-audit.report.json`、`coverage/owner-review-gate-refresh.report.json`、`context-lock.json`。
**必须产出**:`coverage/owner-review-gate-refresh.report.json`、六项 owner decision 缺口和可重建的 blocked release 结论;没有 owner 输入时不得刷新 gate、升级 blocked、删除样本或就地覆盖 context lock。 **必须产出**:保持 `coverage/project-task-readiness.report.json` 和所有实现报告作为 blocked evidence;owner input 到位前不生成新 context、不刷新 gate。
**完成条件**:六项 owner decision 状态逐项可定位;当前工作区无批准输入时,task 必须为 `blocked`,gate refresh 必须 blocked,`STOP_RELEASE` 必须 verified。 **完成条件**:六项 owner decision 或新 locked context input 可定位后才解除 blocked;当前 `owner_inputs=0`、gate refresh=blocked、`STOP_RELEASE` verified;不自动回到 K8-REL-17。
**非目标**:不替 owner 做 manifest/context/许可证/browser/oracle 决策,不修改 ABI/protocol/schema/context lock,不升级 accepted,不清除其他 K8 release blockers。 **非目标**:不修改 kinematics 算法或 ABI/protocol/schema/context lock;不替 owner 做 manifest/许可证/browser/oracle 决策;不实现真实硬件驱动;不升级 `accepted`,不清除 K8 release blockers。
**回滚点**:保留 K8-REL-01..14 全部报告、原 manifest、license source index 和 context lock;owner-review report 为新增证据,删除它即可回到 roundup 状态。 **回滚点**:保留所有 M3/FE/REG/K8 报告和原 context lock;删除 task map/readiness report 并恢复旧指针即可回到审计前状态,不触碰实现代码。
## Next ## Next
完成本任务后,在同一变更中把 `next` 提升为 `Current.task`,更新 `last_verified_checkpoint` 和最近证据摘要,并重新运行 `Read` 中的门禁。不得跳过未完成依赖;同一时刻只允许一个 current task。 本任务是 external wait,不执行自动重试;只有六项 owner decision 或新 locked context input 到位时,才新建 context 并重新打开 K8 release task。
## Verify ## Verify
```bash ```bash
python3 scripts/generate_release_closure.py python3 scripts/generate_configuration_startup_closure.py
python3 scripts/generate_post_release_audit.py python3 scripts/generate_hal_component_manifest.py
python3 scripts/generate_k8_milestone_closeout.py python3 scripts/generate_virtual_device_fault_matrix.py
python3 scripts/generate_release_remediation_intake.py python3 scripts/generate_workspace_session_integration.py
python3 scripts/generate_manifest_ownership_remediation.py python3 scripts/generate_project_task_readiness.py
python3 scripts/generate_release_acceptance_remediation.py python3 scripts/test_project_task_readiness.py
python3 scripts/generate_official_oracle_remediation.py node python-runtime/test_runtime.mjs
python3 scripts/generate_configuration_resource_remediation.py node web/worker/test_python_bridge.mjs
python3 scripts/generate_browser_matrix_remediation.py python3 scripts/generate_python_worker_bridge.py
python3 scripts/generate_implementation_license_remediation.py python3 scripts/generate_frontend_m1.py
python3 scripts/generate_remediation_evidence_roundup.py node web/app/test_frontend_m1.mjs
python3 scripts/generate_owner_review_gate_refresh.py node web/app/test_frontend_m2.mjs
node web/worker/test_replay.mjs python3 scripts/generate_frontend_m2.py
node web/workspace/test_recovery.mjs node web/app/test_frontend_m3.mjs
node web/workspace/test_migration.mjs python3 scripts/generate_frontend_m3.py
python3 scripts/generate_workspace_session_integration.py
python3 scripts/validate_baseline.py python3 scripts/validate_baseline.py
python3 scripts/validate_task_pointer.py python3 scripts/validate_task_pointer.py
python3 -m unittest discover -s scripts -p 'test_*.py' -v python3 -m unittest discover -s scripts -p 'test_*.py' -v
ctest --test-dir build-native --output-on-failure
node web/worker/test_replay.mjs
node web/worker/test_configuration_preflight.mjs
node web/worker/test_hal_preflight.mjs
node web/worker/test_virtual_devices.mjs
node web/worker/test_device_recovery.mjs
node web/workspace/test_recovery.mjs
node web/workspace/test_migration.mjs
python3 scripts/validate_download_index.py python3 scripts/validate_download_index.py
rg -n 'REAL-EXCLUDED|blocked|accepted|release-evidence|ownership|acceptance|oracle|resource|browser|remediation|migration|rollback|SPDX|NOTICE|license|release candidate|STOP_RELEASE|post-release' coverage docs rg -n 'configuration-startup|resource source|LIB:|blocked|STOP_RELEASE|kinematics' coverage docs web scripts
``` ```
最近证据:K8-REL-15 的 `coverage/owner-review-gate-refresh.report.json` 已定位六项 owner decision,owner inputs=0、gate refresh=blocked,`STOP_RELEASE` verified;下一步等待 owner 提供决定后创建新 context 并刷新 gate。 最近证据:PROJECT-AUDIT-02 readiness 审计通过;M3/FE/REG 实现证据唯一且 verified,六项 blocker/context consistency/`STOP_RELEASE` verified,`no_self_loop.automatic_retry=false`;当前进入 WAIT-OWNER-01 blocked,不自动回到 K8-REL-17。
## Stop ## Stop

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Generate deterministic browser/performance matrix evidence."""
from __future__ import annotations
import argparse, hashlib, json, subprocess
from pathlib import Path
from typing import Any
CONTEXT=Path("context-lock.json"); REG03=Path("coverage/recovery-offline-fault-matrix.report.json"); REG01=Path("coverage/full-configuration-startup-matrix.report.json"); BROWSER=Path("coverage/browser-matrix-remediation.report.json"); FE4=Path("coverage/frontend-m4.report.json"); GATE=Path("coverage/release-candidate-gate.report.json"); OUTPUT=Path("coverage/browser-performance-matrix.report.json")
def canonical(v:Any)->str:return json.dumps(v,ensure_ascii=False,sort_keys=True,separators=(",",":"))
def digest(v:Any)->str:return hashlib.sha256(canonical(v).encode()).hexdigest()
def artifact(root:Path,path:Path,role:str)->dict[str,Any]:
p=root/path
return {"path":path.as_posix(),"role":role,"status":"present","bytes":p.stat().st_size,"sha256":hashlib.sha256(p.read_bytes()).hexdigest()} if p.is_file() else {"path":path.as_posix(),"role":role,"status":"missing"}
def run(root:Path):
outs=[]
for _ in range(2):
x=subprocess.run(["node","web/app/test_browser_performance.mjs"],cwd=root,check=False,capture_output=True,text=True); lines=[l.strip() for l in x.stdout.splitlines() if l.lstrip().startswith("{")]; sample=json.loads(lines[-1]) if x.returncode==0 and lines else {}; outs.append({"status":"passed" if x.returncode==0 else "blocked","exit_code":x.returncode,"sample":sample,"stdout_sha256":digest(x.stdout),"stderr_sha256":digest(x.stderr)})
return outs
def generate(root:Path,output:Path=OUTPUT)->dict[str,Any]:
root=root.resolve(); lock=json.loads((root/CONTEXT).read_text()); reg03=json.loads((root/REG03).read_text()); reg01=json.loads((root/REG01).read_text()); browser=json.loads((root/BROWSER).read_text()); fe4=json.loads((root/FE4).read_text()); gate=json.loads((root/GATE).read_text()); runs=run(root); first,second=runs
checks={"reg01_dependency":{"status":"verified" if reg01.get("implementation_status")=="verified" else "blocked"},"reg03_dependency":{"status":"verified" if reg03.get("implementation_status")=="verified" else "blocked"},"frontend_dependency":{"status":"verified" if fe4.get("implementation_status")=="verified" else "blocked"},"chromium_environment":{"status":"verified" if browser.get("checks",{}).get("chromium_probe",{}).get("status")=="verified" else "blocked"},"firefox_not_in_scope":{"status":"verified","reason":"Firefox is excluded from the current test scope"},"safari_not_in_scope":{"status":"verified","reason":"Safari is excluded from the current test scope"},"repeat_determinism":{"status":"verified" if first==second else "blocked"},"release_stop_preserved":{"status":"verified" if gate.get("release_action")=="STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked","release_action":gate.get("release_action")}}
ok=all(v["status"]=="verified" for v in checks.values()); report={"schema":"browser-performance-matrix.v1","context_id":lock.get("context_id"),"task_id":"REG-04","status":"partial" if ok else "blocked","implementation_status":"verified" if ok else "blocked","candidate_eligible":False,"release_action":"STOP_RELEASE","inputs":[artifact(root,CONTEXT,"context_lock"),artifact(root,REG01,"full_configuration_matrix"),artifact(root,REG03,"recovery_offline_fault_matrix"),artifact(root,BROWSER,"browser_matrix_remediation"),artifact(root,FE4,"frontend_m4"),artifact(root,GATE,"release_candidate_gate")],"checks":checks,"runs":runs,"environment":{"scope":["chromium"],"firefox":"out_of_scope","safari":"out_of_scope","minimums":"TBD_in_locked_matrix"},"next_transition":"execute K8-REL-17 release gate refresh after implementation evidence","report_sha256":None}; report["report_sha256"]=digest({k:v for k,v in report.items() if k!="report_sha256"}); dest=output if output.is_absolute() else root/output; dest.parent.mkdir(parents=True,exist_ok=True); dest.write_text(json.dumps(report,ensure_ascii=False,sort_keys=True,indent=2)+"\n"); return report
def main()->int:
p=argparse.ArgumentParser();p.add_argument("--repo-root",type=Path,default=Path(__file__).resolve().parents[1]);p.add_argument("--output",type=Path,default=OUTPUT);a=p.parse_args();r=generate(a.repo_root.resolve(),a.output);print(f"browser_performance_matrix={r['status']} scope=chromium");return 0
if __name__=="__main__":raise SystemExit(main())

View File

@@ -7,6 +7,7 @@ import argparse
import hashlib import hashlib
import json import json
import re import re
import subprocess
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable
@@ -32,6 +33,7 @@ HAL_COMMANDS = {"source": "hal_source", "loadusr": "hal_user_component"}
TOKEN_RE = re.compile(r"(?<![A-Za-z0-9_./~-])([^\s\"'=;,]+)") TOKEN_RE = re.compile(r"(?<![A-Za-z0-9_./~-])([^\s\"'=;,]+)")
EXT_RE = re.compile(r"\.(?:ini|hal|tbl|var|ngc|nc|tap|py|xml|ui|glade|tcl|clp|pref|prefs|dat|png|jpg|jpeg|gif|stl|obj)$", re.I) EXT_RE = re.compile(r"\.(?:ini|hal|tbl|var|ngc|nc|tap|py|xml|ui|glade|tcl|clp|pref|prefs|dat|png|jpg|jpeg|gif|stl|obj)$", re.I)
_SHA_CACHE: dict[Path, str] = {} _SHA_CACHE: dict[Path, str] = {}
_LOCKED_SUBMODULE_CACHE: dict[Path, set[str] | None] = {}
def sha256(path: Path) -> str: def sha256(path: Path) -> str:
@@ -99,6 +101,44 @@ def config_files(config_root: Path) -> list[Path]:
return sorted(path for path in config_root.rglob("*") if path.is_file()) return sorted(path for path in config_root.rglob("*") if path.is_file())
def locked_submodule_files(repo_root: Path) -> set[str] | None:
"""Return untracked LinuxCNC paths excluded from the locked inventory.
Ignored build outputs are intentionally retained because the existing
context lock was generated from that checkout. Only explicit untracked
source/config fixtures are excluded from the locked inventory.
"""
submodule = repo_root / "linuxcnc"
key = submodule.resolve()
if key in _LOCKED_SUBMODULE_CACHE:
return _LOCKED_SUBMODULE_CACHE[key]
try:
completed = subprocess.run(
["git", "-C", str(submodule), "status", "--porcelain=v1", "--untracked-files=all"],
check=False,
capture_output=True,
text=True,
)
if completed.returncode != 0:
result = None
else:
result = {line[3:] for line in completed.stdout.splitlines() if line.startswith("?? ")}
except (OSError, UnicodeError):
result = None
_LOCKED_SUBMODULE_CACHE[key] = result
return result
def is_locked_path(repo_root: Path, path: Path, locked: set[str] | None) -> bool:
if locked is None:
return True
try:
relative = path.resolve().relative_to((repo_root / "linuxcnc").resolve()).as_posix()
except ValueError:
return False
return relative not in locked
def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, entrypoint: Path, def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, entrypoint: Path,
target: Path | None, raw: str, kind: str, edge_kind: str, line: int, status: str | None = None, target: Path | None, raw: str, kind: str, edge_kind: str, line: int, status: str | None = None,
origin: Path | None = None) -> None: origin: Path | None = None) -> None:
@@ -128,6 +168,7 @@ def add_row(rows: list[dict[str, object]], repo_root: Path, config_root: Path, e
def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> list[dict[str, object]]: def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> list[dict[str, object]]:
rows: list[dict[str, object]] = [] rows: list[dict[str, object]] = []
locked = locked_submodule_files(repo_root)
ini_rows = parse_ini(entrypoint) ini_rows = parse_ini(entrypoint)
row_numbers = {(section, key, value): index for index, (section, key, value) in enumerate(ini_rows, 1)} row_numbers = {(section, key, value): index for index, (section, key, value) in enumerate(ini_rows, 1)}
for section, key, value in ini_rows: for section, key, value in ini_rows:
@@ -152,7 +193,7 @@ def scan_entrypoint(repo_root: Path, config_root: Path, entrypoint: Path) -> lis
add_row(rows, repo_root, config_root, entrypoint, target, token, RESOURCE_EXTENSIONS.get(Path(token).suffix.lower(), "external"), f"ini:{section}:{key}", row_numbers[(section, key, value)], status if target is None else None) add_row(rows, repo_root, config_root, entrypoint, target, token, RESOURCE_EXTENSIONS.get(Path(token).suffix.lower(), "external"), f"ini:{section}:{key}", row_numbers[(section, key, value)], status if target is None else None)
seen_paths = {str(row["path"]) for row in rows} seen_paths = {str(row["path"]) for row in rows}
for path in sorted(path for path in entrypoint.parent.rglob("*") if path.is_file() and path != entrypoint): for path in sorted(path for path in entrypoint.parent.rglob("*") if path.is_file() and path != entrypoint and is_locked_path(repo_root, path, locked)):
path_key = rel(repo_root, path) path_key = rel(repo_root, path)
if path.suffix.lower() in RESOURCE_EXTENSIONS and path_key not in seen_paths: if path.suffix.lower() in RESOURCE_EXTENSIONS and path_key not in seen_paths:
add_row(rows, repo_root, config_root, entrypoint, path, rel(repo_root, path), RESOURCE_EXTENSIONS[path.suffix.lower()], "directory_member", 0) add_row(rows, repo_root, config_root, entrypoint, path, rel(repo_root, path), RESOURCE_EXTENSIONS[path.suffix.lower()], "directory_member", 0)
@@ -187,7 +228,8 @@ def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str
config_root = repo_root / CONFIG_ROOT config_root = repo_root / CONFIG_ROOT
if not config_root.is_dir(): if not config_root.is_dir():
raise FileNotFoundError(f"required config root is missing: {CONFIG_ROOT}") raise FileNotFoundError(f"required config root is missing: {CONFIG_ROOT}")
entrypoints = sorted(path for path in config_root.rglob("*.ini") if path.is_file()) locked = locked_submodule_files(repo_root)
entrypoints = sorted(path for path in config_root.rglob("*.ini") if path.is_file() and is_locked_path(repo_root, path, locked))
rows: list[dict[str, object]] = [] rows: list[dict[str, object]] = []
for entrypoint in entrypoints: for entrypoint in entrypoints:
rows.extend(scan_entrypoint(repo_root, config_root, entrypoint)) rows.extend(scan_entrypoint(repo_root, config_root, entrypoint))

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""Generate deterministic configuration startup closure evidence.
The resolver intentionally has a narrow search policy. Ordinary relative
references resolve from their entrypoint directory, repository-qualified
references resolve from the repository root, and only an explicit ``LIB:``
prefix searches the locked LinuxCNC library roots. Bare basenames are never
silently replaced by a same-named candidate elsewhere in the checkout.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import shlex
from collections import Counter
from pathlib import Path, PurePosixPath
from typing import Any
CONTEXT = Path("context-lock.json")
RESOURCES = Path("coverage/config-resources.jsonl")
REGRESSION = Path("coverage/configuration-regression.report.json")
GRAPH = Path("coverage/kinematics-config-graph.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/configuration-startup-closure.report.json")
SCHEMA = "configuration-startup-closure.v1"
LIBRARY_ROOTS = ("linuxcnc/lib/hallib", "linuxcnc/lib")
HOST_PREFIXES = ("~", "$", "${")
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def file_digest(path: Path) -> str | None:
if not path.is_file():
return None
handle = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
handle.update(block)
return handle.hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
sha = file_digest(full)
if sha is None:
return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha}
def read_json(root: Path, path: Path) -> tuple[dict[str, Any], str | None]:
try:
value = json.loads((root / path).read_text(encoding="utf-8"))
if not isinstance(value, dict):
raise ValueError(f"{path} must contain an object")
return value, None
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
return {}, str(exc)
def read_jsonl(root: Path, path: Path) -> tuple[list[dict[str, Any]], str | None]:
try:
rows: list[dict[str, Any]] = []
for line_number, line in enumerate((root / path).read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
value = json.loads(line)
if not isinstance(value, dict):
raise ValueError(f"{path}:{line_number} must contain an object")
rows.append(value)
return rows, None
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
return [], str(exc)
def normalize(value: str) -> tuple[str | None, str | None]:
if not isinstance(value, str) or not value:
return None, "invalid_reference"
if "\x00" in value:
return None, "invalid_reference"
parts: list[str] = []
for part in value.replace("\\", "/").split("/"):
if not part or part == ".":
continue
if part == "..":
if not parts:
return None, "path_escape"
parts.pop()
continue
parts.append(part)
return "/".join(parts), None
def first_token(raw: Any) -> tuple[str | None, list[str], str | None]:
if not isinstance(raw, str):
return None, [], "invalid_reference"
value = raw.strip().strip("\"'")
if not value:
return None, [], "empty_reference"
try:
tokens = shlex.split(value, posix=True)
except ValueError:
tokens = re.findall(r"[^\s]+", value)
if not tokens:
return None, [], "empty_reference"
return tokens[0], tokens[1:], None
def candidates(raw_value: Any, entrypoint: str) -> tuple[list[str], str, str | None, list[str]]:
value, args, error = first_token(raw_value)
if error:
return [], "blocked", error, args
assert value is not None
if value.startswith(HOST_PREFIXES):
return [], "blocked", "host_path", args
entry, error = normalize(entrypoint)
if error or entry is None:
return [], "blocked", error or "invalid_reference", args
if value.startswith("LIB:"):
name = value[4:]
if not name or name.startswith("/") or ".." in PurePosixPath(name).parts:
return [], "library", "invalid_reference", args
paths = []
for root in LIBRARY_ROOTS:
path, path_error = normalize(f"{root}/{name}")
if path_error is None and path is not None:
paths.append(path)
return paths, "library", None, args
if value.startswith("/"):
return [], "blocked", "outside_repo", args
if value.startswith("linuxcnc/"):
path, path_error = normalize(value)
return ([path] if path else []), "repo_relative", path_error, args
base = PurePosixPath(entry).parent.as_posix()
path, path_error = normalize(f"{base}/{value}")
if path_error:
return [], "relative", path_error, args
if path is not None and not (path == "linuxcnc" or path.startswith("linuxcnc/")):
return [], "relative", "outside_repo", args
return ([path] if path else []), "relative", None, args
def available_paths(root: Path) -> set[str]:
return {path.relative_to(root).as_posix() for path in root.rglob("*") if path.is_file() or path.is_dir()}
def resolve_row(row: dict[str, Any], available: set[str]) -> dict[str, Any]:
original_path = str(row.get("path", ""))
original_status = str(row.get("parse_status", ""))
raw_value = row.get("raw_value", original_path)
if original_status in {"resolved_file", "resolved_directory"} and original_path in available:
return {
"resource_source_id": row.get("resource_source_id"),
"entrypoint": row.get("entrypoint"),
"original_path": original_path,
"original_status": original_status,
"raw_value": raw_value,
"status": "resolved_existing",
"code": None,
"path": original_path,
"args": [],
"attempted_paths": [original_path],
"resolution": "indexed",
}
paths, resolution, error, args = candidates(raw_value, str(row.get("entrypoint", "")))
existing = [path for path in paths if path in available]
if error:
status = f"blocked_{error}"
return {"resource_source_id": row.get("resource_source_id"), "entrypoint": row.get("entrypoint"), "original_path": original_path, "original_status": original_status, "raw_value": raw_value, "status": status, "code": error, "path": None, "args": args, "attempted_paths": paths, "resolution": resolution}
if len(existing) > 1:
return {"resource_source_id": row.get("resource_source_id"), "entrypoint": row.get("entrypoint"), "original_path": original_path, "original_status": original_status, "raw_value": raw_value, "status": "blocked_ambiguous", "code": "ambiguous", "path": None, "args": args, "attempted_paths": paths, "resolution": resolution}
if len(existing) == 1:
return {"resource_source_id": row.get("resource_source_id"), "entrypoint": row.get("entrypoint"), "original_path": original_path, "original_status": original_status, "raw_value": raw_value, "status": f"resolved_{resolution}", "code": None, "path": existing[0], "args": args, "attempted_paths": paths, "resolution": resolution}
return {"resource_source_id": row.get("resource_source_id"), "entrypoint": row.get("entrypoint"), "original_path": original_path, "original_status": original_status, "raw_value": raw_value, "status": "blocked_missing", "code": "missing", "path": None, "args": args, "attempted_paths": paths, "resolution": resolution}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock, lock_error = read_json(root, CONTEXT)
regression, regression_error = read_json(root, REGRESSION)
graph, graph_error = read_json(root, GRAPH)
gate, gate_error = read_json(root, GATE)
rows, rows_error = read_jsonl(root, RESOURCES)
errors = {path.as_posix(): error for path, error in ((CONTEXT, lock_error), (REGRESSION, regression_error), (GRAPH, graph_error), (GATE, gate_error), (RESOURCES, rows_error)) if error}
available = available_paths(root)
records = [resolve_row(row, available) for row in rows]
records.sort(key=lambda row: str(row.get("resource_source_id", "")))
occurrence_counter: Counter[str] = Counter()
for record in records:
source_id = str(record.get("resource_source_id", ""))
occurrence_counter[source_id] += 1
record["resource_record_id"] = f"{source_id}#occurrence:{occurrence_counter[source_id]}"
statuses = Counter(str(row["status"]) for row in records)
codes = Counter(str(row["code"]) for row in records if row.get("code"))
duplicate_source_ids = sorted(source_id for source_id, count in occurrence_counter.items() if count > 1)
entrypoints = sorted({str(row.get("entrypoint")) for row in records})
ids = [str(row.get("resource_source_id")) for row in records]
config_entries = regression.get("configuration_entries", []) if isinstance(regression.get("configuration_entries"), list) else []
context_id = lock.get("context_id")
stop_ok = gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False
checks = {
"input_integrity": {"status": "verified" if not errors and context_id else "blocked", "context_id": context_id},
"configuration_coverage": {"status": "verified" if len(config_entries) == 264 and len(entrypoints) == 233 else "blocked", "registered_entries": len(config_entries), "entrypoints": len(entrypoints)},
"resource_identity": {"status": "verified" if len({str(row.get('resource_record_id')) for row in records}) == len(records) and ids == sorted(ids) else "blocked", "rows": len(ids), "duplicate_source_ids": duplicate_source_ids, "duplicate_policy": "source_id_preserved_with_deterministic_occurrence_suffix"},
"explicit_library_policy": {"status": "verified" if all(row.get("resolution") in {"indexed", "relative", "repo_relative", "library", "blocked"} for row in records) else "blocked", "library_references": sum(1 for row in records if str(row.get("raw_value", "")).lstrip().startswith("LIB:")), "library_resolved": sum(1 for row in records if row.get("status") == "resolved_library")},
"no_basename_substitution": {"status": "verified" if all(row.get("resolution") != "basename" for row in records) else "blocked"},
"release_stop_preserved": {"status": "verified" if stop_ok else "blocked", "release_action": "STOP_RELEASE" if stop_ok else None},
}
blocked_count = sum(value for key, value in statuses.items() if key.startswith("blocked_"))
report: dict[str, Any] = {
"schema": SCHEMA,
"context_id": context_id,
"task_id": "M3-CFG-01",
"status": "verified" if not errors and blocked_count == 0 else "partial" if not errors else "blocked",
"implementation_status": "verified" if not errors else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, RESOURCES, "config_resources"), artifact(root, REGRESSION, "configuration_regression"), artifact(root, GRAPH, "kinematics_config_graph"), artifact(root, GATE, "release_candidate_gate")],
"counts": {"resource_rows": len(records), "entrypoints": len(entrypoints), "registered_configuration_entries": len(config_entries), "resolved": len(records) - blocked_count, "blocked": blocked_count, "duplicate_source_ids": len(duplicate_source_ids), "statuses": dict(sorted(statuses.items())), "blocked_codes": dict(sorted(codes.items()))},
"checks": checks,
"resources": records,
"next_transition": "execute M3-RT-02 HAL component manifest and executable configuration graph",
"errors": errors,
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"configuration_startup_closure={report['status']} resolved={report['counts']['resolved']} blocked={report['counts']['blocked']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Generate deterministic FE-M1 frontend vertical-slice evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
PYTHON = Path("coverage/python-worker-bridge.report.json")
SESSION = Path("coverage/workspace-session-integration.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
FILES = (Path("web/app/index.html"), Path("web/app/app.mjs"), Path("web/app/worker-client.mjs"), Path("web/app/styles.css"), Path("web/app/test_frontend_m1.mjs"))
OUTPUT = Path("coverage/frontend-m1.report.json")
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def run_smoke(root: Path) -> dict[str, Any]:
result = subprocess.run(["node", "web/app/test_frontend_m1.mjs"], cwd=root, check=False, capture_output=True, text=True)
return {"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
python = json.loads((root / PYTHON).read_text(encoding="utf-8"))
session = json.loads((root / SESSION).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
files = [artifact(root, path, "frontend_source") for path in FILES]
app_source = (root / "web/app/app.mjs").read_text(encoding="utf-8") if (root / "web/app/app.mjs").is_file() else ""
client_source = (root / "web/app/worker-client.mjs").read_text(encoding="utf-8") if (root / "web/app/worker-client.mjs").is_file() else ""
smoke = run_smoke(root)
checks = {
"source_files": {"status": "verified" if all(row["status"] == "present" for row in files) else "blocked", "files": files},
"worker_boundary": {"status": "verified" if "WorkerClient" in app_source and "postMessage" in client_source and "_sim_" not in app_source else "blocked"},
"protocol_workflow": {"status": "verified" if all(token in client_source for token in ("HELLO", "SESSION_ATTACH", "LOAD_WORKSPACE", "SIM_STEP", "SNAPSHOT_REQUEST")) else "blocked"},
"dependency_chain": {"status": "verified" if python.get("implementation_status") == "verified" and session.get("implementation_status") == "verified" else "blocked"},
"browser_smoke": {"status": "verified" if smoke["status"] == "passed" else "blocked", "stdout": smoke["stdout"]},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(row["status"] == "verified" for row in checks.values())
report: dict[str, Any] = {
"schema": "frontend-m1.v1",
"context_id": lock.get("context_id"),
"task_id": "FE-M1",
"status": "partial" if checks_ok else "blocked",
"implementation_status": "verified" if checks_ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, PYTHON, "python_worker_bridge"), artifact(root, SESSION, "workspace_session_integration"), artifact(root, GATE, "release_candidate_gate")],
"outputs": files,
"checks": checks,
"smoke": smoke,
"next_transition": "execute FE-M2 core operation controls and state panels",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"frontend_m1={report['status']} smoke={report['checks']['browser_smoke']['status']}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Generate deterministic FE-M2 controls/state-panel evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
FE1 = Path("coverage/frontend-m1.report.json")
DEVICES = Path("coverage/virtual-device-fault-matrix.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/frontend-m2.report.json")
FILES = (Path("web/app/controls.mjs"), Path("web/app/state-panel.mjs"), Path("web/app/app.mjs"), Path("web/app/styles.css"), Path("web/app/test_frontend_m2.mjs"))
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def run(root: Path) -> dict[str, Any]:
result = subprocess.run(["node", "web/app/test_frontend_m2.mjs"], cwd=root, check=False, capture_output=True, text=True)
return {"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text())
fe1 = json.loads((root / FE1).read_text())
devices = json.loads((root / DEVICES).read_text())
gate = json.loads((root / GATE).read_text())
outputs = [artifact(root, path, "frontend_source") for path in FILES]
app = (root / "web/app/app.mjs").read_text() if (root / "web/app/app.mjs").is_file() else ""
controls = (root / "web/app/controls.mjs").read_text() if (root / "web/app/controls.mjs").is_file() else ""
panel = (root / "web/app/state-panel.mjs").read_text() if (root / "web/app/state-panel.mjs").is_file() else ""
smoke = run(root)
actions = ("run", "pause", "resume", "abort", "home", "estop", "recover")
checks = {
"fe1_dependency": {"status": "verified" if fe1.get("implementation_status") == "verified" else "blocked"},
"operation_controls": {"status": "verified" if all(action in controls for action in actions) else "blocked", "actions": list(actions)},
"authoritative_state_panel": {"status": "verified" if "reduceWorkerState" in panel and "renderStatePanel" in panel and "onMessage" in app else "blocked"},
"device_error_visibility": {"status": "verified" if devices.get("implementation_status") == "verified" and "Error:" in app else "blocked"},
"source_files": {"status": "verified" if all(row["status"] == "present" for row in outputs) else "blocked"},
"browser_smoke": {"status": "verified" if smoke["status"] == "passed" else "blocked", "stdout": smoke["stdout"]},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(row["status"] == "verified" for row in checks.values())
report: dict[str, Any] = {"schema": "frontend-m2.v1", "context_id": lock.get("context_id"), "task_id": "FE-M2", "status": "partial" if checks_ok else "blocked", "implementation_status": "verified" if checks_ok else "blocked", "candidate_eligible": False, "release_action": "STOP_RELEASE", "inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, FE1, "frontend_m1"), artifact(root, DEVICES, "virtual_device_fault_matrix"), artifact(root, GATE, "release_candidate_gate")], "outputs": outputs, "checks": checks, "smoke": smoke, "next_transition": "execute FE-M3 project and recovery workflow", "report_sha256": None}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n")
return report
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"frontend_m2={report['status']} smoke={report['checks']['browser_smoke']['status']}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Generate deterministic FE-M3 project/recovery workflow evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
FE2 = Path("coverage/frontend-m2.report.json")
SESSION = Path("coverage/workspace-session-integration.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/frontend-m3.report.json")
FILES = (Path("web/app/project-workflow.mjs"), Path("web/app/recovery-panel.mjs"), Path("web/app/app.mjs"), Path("web/app/test_frontend_m3.mjs"))
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def smoke(root: Path) -> dict[str, Any]:
result = subprocess.run(["node", "web/app/test_frontend_m3.mjs"], cwd=root, check=False, capture_output=True, text=True)
return {"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text())
fe2 = json.loads((root / FE2).read_text())
session = json.loads((root / SESSION).read_text())
gate = json.loads((root / GATE).read_text())
outputs = [artifact(root, path, "frontend_source") for path in FILES]
workflow = (root / FILES[0]).read_text() if (root / FILES[0]).is_file() else ""
panel = (root / FILES[1]).read_text() if (root / FILES[1]).is_file() else ""
run = smoke(root)
checks = {
"fe2_dependency": {"status": "verified" if fe2.get("implementation_status") == "verified" else "blocked"},
"session_dependency": {"status": "verified" if session.get("implementation_status") == "verified" else "blocked"},
"journal_package_workflow": {"status": "verified" if all(token in workflow for token in ("importFiles", "exportFiles", "createWorkspacePackage", "migrateWorkspacePackage")) else "blocked"},
"recovery_panel": {"status": "verified" if "recoveryView" in panel and "half_commits" in panel else "blocked"},
"source_files": {"status": "verified" if all(row["status"] == "present" for row in outputs) else "blocked"},
"browser_smoke": {"status": "verified" if run["status"] == "passed" else "blocked", "stdout": run["stdout"]},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {"schema": "frontend-m3.v1", "context_id": lock.get("context_id"), "task_id": "FE-M3", "status": "partial" if ok else "blocked", "implementation_status": "verified" if ok else "blocked", "candidate_eligible": False, "release_action": "STOP_RELEASE", "inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, FE2, "frontend_m2"), artifact(root, SESSION, "workspace_session_integration"), artifact(root, GATE, "release_candidate_gate")], "outputs": outputs, "checks": checks, "smoke": run, "next_transition": "execute FE-M4 HAL/tool/IO/3D frontend workflows", "report_sha256": None}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n")
return report
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"frontend_m3={report['status']} smoke={report['checks']['browser_smoke']['status']}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Generate deterministic FE-M4 device/HAL/3D frontend evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
FE3 = Path("coverage/frontend-m3.report.json")
HAL = Path("coverage/hal-component-manifest.report.json")
DEVICES = Path("coverage/virtual-device-fault-matrix.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/frontend-m4.report.json")
FILES = (Path("web/app/device-panel.mjs"), Path("web/app/hal-panel.mjs"), Path("web/app/machine-view.mjs"), Path("web/app/test_frontend_m4.mjs"))
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def smoke(root: Path) -> dict[str, Any]:
result = subprocess.run(["node", "web/app/test_frontend_m4.mjs"], cwd=root, check=False, capture_output=True, text=True)
return {"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text())
fe3 = json.loads((root / FE3).read_text())
hal = json.loads((root / HAL).read_text())
devices = json.loads((root / DEVICES).read_text())
gate = json.loads((root / GATE).read_text())
outputs = [artifact(root, path, "frontend_source") for path in FILES]
device = (root / FILES[0]).read_text() if (root / FILES[0]).is_file() else ""
hal_source = (root / FILES[1]).read_text() if (root / FILES[1]).is_file() else ""
machine = (root / FILES[2]).read_text() if (root / FILES[2]).is_file() else ""
run = smoke(root)
checks = {
"fe3_dependency": {"status": "verified" if fe3.get("implementation_status") == "verified" else "blocked"},
"device_workflow": {"status": "verified" if all(token in device for token in ("SPINDLE_COMMAND", "TOOL_PREPARE", "PROBE_CONFIG", "IO_RECOVER")) and devices.get("implementation_status") == "verified" else "blocked"},
"hal_diagnostics": {"status": "verified" if "HAL diagnostics" in hal_source and hal.get("implementation_status") == "verified" else "blocked"},
"machine_view_boundary": {"status": "verified" if "canvas" in machine and "_sim_" not in machine and "wasm" not in machine.lower() else "blocked"},
"source_files": {"status": "verified" if all(row["status"] == "present" for row in outputs) else "blocked"},
"browser_smoke": {"status": "verified" if run["status"] == "passed" else "blocked", "stdout": run["stdout"]},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {"schema": "frontend-m4.v1", "context_id": lock.get("context_id"), "task_id": "FE-M4", "status": "partial" if ok else "blocked", "implementation_status": "verified" if ok else "blocked", "candidate_eligible": False, "release_action": "STOP_RELEASE", "inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, FE3, "frontend_m3"), artifact(root, HAL, "hal_component_manifest"), artifact(root, DEVICES, "virtual_device_fault_matrix"), artifact(root, GATE, "release_candidate_gate")], "outputs": outputs, "checks": checks, "smoke": run, "next_transition": "execute REG-01 full configuration startup matrix", "report_sha256": None}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n")
return report
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"frontend_m4={report['status']} smoke={report['checks']['browser_smoke']['status']}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Aggregate the deterministic full sim configuration startup matrix."""
from __future__ import annotations
import argparse
import hashlib
import json
from collections import Counter
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
REGRESSION = Path("coverage/configuration-regression.report.json")
STARTUP = Path("coverage/configuration-startup-closure.report.json")
HAL = Path("coverage/hal-component-manifest.report.json")
FRONTEND = Path("coverage/frontend-m4.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/full-configuration-startup-matrix.report.json")
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text())
regression = json.loads((root / REGRESSION).read_text())
startup = json.loads((root / STARTUP).read_text())
hal = json.loads((root / HAL).read_text())
frontend = json.loads((root / FRONTEND).read_text())
gate = json.loads((root / GATE).read_text())
rows = regression.get("configuration_entries", [])
startup_rows = startup.get("resources", [])
by_entry = Counter(str(row.get("entrypoint")) for row in startup_rows if row.get("entrypoint"))
matrix = []
for row in rows:
path = str(row.get("path", ""))
blocked = row.get("startup_preflight") != "static_ready"
matrix.append({
"config_id": row.get("config_id"),
"path": path,
"module": row.get("module"),
"family": row.get("family"),
"status": "blocked" if blocked else "ready",
"startup_preflight": row.get("startup_preflight"),
"resource_status": row.get("resource_status"),
"resource_rows": by_entry.get(path, row.get("resource_rows", 0)),
"missing_resources": row.get("missing_resources", []),
"unresolved_resources": row.get("unresolved_resources", []),
"kinematics_registered": row.get("kinematics_registered"),
"source_id": row.get("config_id"),
})
matrix.sort(key=lambda item: str(item.get("config_id", "")))
statuses = Counter(item["status"] for item in matrix)
checks = {
"entry_count": {"status": "verified" if len(matrix) == 264 else "blocked", "entries": len(matrix)},
"entrypoint_count": {"status": "verified" if regression.get("counts", {}).get("config_entrypoints") == 233 else "blocked", "entrypoints": regression.get("counts", {}).get("config_entrypoints")},
"resource_closure": {"status": "verified" if startup.get("implementation_status") == "verified" and startup.get("counts", {}).get("resource_rows") == 16093 else "blocked", "resources": startup.get("counts", {}).get("resource_rows")},
"hal_dependency": {"status": "verified" if hal.get("implementation_status") == "verified" else "blocked"},
"frontend_dependency": {"status": "verified" if frontend.get("implementation_status") == "verified" else "blocked"},
"stable_identity": {"status": "verified" if len({item["config_id"] for item in matrix}) == len(matrix) else "blocked"},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {
"schema": "full-configuration-startup-matrix.v1",
"context_id": lock.get("context_id"),
"task_id": "REG-01",
"status": "partial" if ok else "blocked",
"implementation_status": "verified" if ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, REGRESSION, "configuration_regression"), artifact(root, STARTUP, "configuration_startup_closure"), artifact(root, HAL, "hal_component_manifest"), artifact(root, FRONTEND, "frontend_m4"), artifact(root, GATE, "release_candidate_gate")],
"counts": {"registered_entries": len(matrix), "entrypoints": regression.get("counts", {}).get("config_entrypoints"), "statuses": dict(sorted(statuses.items())), "resource_rows": startup.get("counts", {}).get("resource_rows"), "blocked_resource_rows": startup.get("counts", {}).get("blocked")},
"checks": checks,
"matrix": matrix,
"next_transition": "execute REG-03 recovery and offline fault matrix",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n")
return report
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"full_configuration_startup_matrix={report['status']} entries={report['counts']['registered_entries']} blocked={report['counts']['statuses'].get('blocked', 0)}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env python3
"""Generate a deterministic HAL component manifest and config graph report."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any
from generate_config_resources import locked_submodule_files
CONTEXT = Path("context-lock.json")
HAL_REPORT = Path("coverage/hal-runtime.report.json")
CONFIG_CLOSURE = Path("coverage/configuration-startup-closure.report.json")
RESOURCES = Path("coverage/config-resources.jsonl")
GATE = Path("coverage/release-candidate-gate.report.json")
COMPONENT_ROOT = Path("linuxcnc/src/hal/components")
OUTPUT = Path("coverage/hal-component-manifest.report.json")
SCHEMA = "hal-component-manifest.v1"
COMPONENT_RE = re.compile(r"^\s*component\s+([A-Za-z_][A-Za-z0-9_]*)\b")
PIN_RE = re.compile(r"^\s*pin\s+(in|out|io)\s+([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_.#-]*)")
PARAM_RE = re.compile(r"^\s*param\s+(r|rw)\s+([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_.-]*)(?:\s*=\s*([^\s]+))?")
FUNCTION_RE = re.compile(r"^\s*function\s+([^;]+);")
VARIABLE_RE = re.compile(r"^\s*variable\s+([A-Za-z_][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_.-]*)")
OPTION_RE = re.compile(r"^\s*option\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:=\s*([^\s]+))?")
LOADRT_RE = re.compile(r"^\s*loadrt\s+([^\s]+)(?:\s+(.*))?$")
LOADUSR_RE = re.compile(r"^\s*loadusr\s+(.+)$")
ADDF_RE = re.compile(r"^\s*addf\s+([^\s]+)\s+([^\s]+)")
LINE_COMMANDS = {"loadrt", "loadusr", "addf", "net", "setp", "source", "newsig", "linksp", "linkps", "linkpp"}
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def file_digest(path: Path) -> str | None:
if not path.is_file():
return None
handle = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
handle.update(block)
return handle.hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
sha = file_digest(full)
if sha is None:
return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha}
def relative(root: Path, path: Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def parse_comp(root: Path, path: Path) -> dict[str, Any]:
component = None
pins: list[dict[str, Any]] = []
params: list[dict[str, Any]] = []
functions: list[dict[str, Any]] = []
variables: list[dict[str, Any]] = []
options: list[dict[str, Any]] = []
errors: list[str] = []
try:
lines = path.read_text(encoding="utf-8", errors="strict").splitlines()
except (OSError, UnicodeError) as exc:
return {"path": relative(root, path), "source_sha256": file_digest(path), "status": "blocked", "errors": [str(exc)], "component": None, "pins": [], "params": [], "functions": [], "variables": [], "options": []}
for line_number, raw in enumerate(lines, 1):
line = raw.split("//", 1)[0].strip()
match = COMPONENT_RE.match(line)
if match:
if component is not None and component != match.group(1):
errors.append(f"duplicate_component:{line_number}")
component = match.group(1)
continue
match = PIN_RE.match(line)
if match:
pins.append({"direction": match.group(1), "type": match.group(2), "name": match.group(3), "line": line_number})
continue
match = PARAM_RE.match(line)
if match:
params.append({"mode": match.group(1), "type": match.group(2), "name": match.group(3), "default": match.group(4).rstrip(";") if match.group(4) else None, "line": line_number})
continue
match = FUNCTION_RE.match(line)
if match:
for name in match.group(1).split():
if name:
functions.append({"name": name, "line": line_number})
continue
match = VARIABLE_RE.match(line)
if match:
variables.append({"type": match.group(1), "name": match.group(2), "line": line_number})
continue
match = OPTION_RE.match(line)
if match:
options.append({"name": match.group(1), "value": match.group(2), "line": line_number})
if component is None:
errors.append("component_declaration_missing")
names = [row["name"] for row in pins] + [row["name"] for row in params] + [row["name"] for row in variables]
if len(names) != len(set(names)):
errors.append("duplicate_declared_name")
return {"path": relative(root, path), "source_sha256": file_digest(path), "status": "verified" if not errors else "blocked", "errors": errors, "component": component, "pins": pins, "params": params, "functions": functions, "variables": variables, "options": options}
def parse_hal(root: Path, path: Path, component_names: set[str], resource_source_id: str | None) -> list[dict[str, Any]]:
records: list[dict[str, Any]] = []
try:
lines = path.read_text(encoding="utf-8", errors="strict").splitlines()
except (OSError, UnicodeError) as exc:
return [{"path": relative(root, path), "line": 0, "command": "file", "status": "blocked", "code": "decode_error", "detail": str(exc), "resource_source_id": resource_source_id}]
for line_number, raw in enumerate(lines, 1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
command = line.split(None, 1)[0]
if command not in LINE_COMMANDS:
continue
base = {"path": relative(root, path), "line": line_number, "command": command, "raw": line, "resource_source_id": resource_source_id}
if command == "loadrt":
match = LOADRT_RE.match(line)
module = match.group(1) if match else ""
names = re.search(r"(?:^|\s)names=([^\s]+)", match.group(2) or "" if match else "")
instances = names.group(1).split(",") if names else []
if module.startswith("[") or module not in component_names:
records.append({**base, "status": "blocked", "code": "undeclared_or_variable_component", "module": module, "instances": instances})
else:
records.append({**base, "status": "declared", "code": None, "module": module, "instances": instances})
elif command == "loadusr":
match = LOADUSR_RE.match(line)
records.append({**base, "status": "blocked", "code": "host_dynamic_loader", "program": match.group(1).split()[0] if match else ""})
elif command == "addf":
match = ADDF_RE.match(line)
records.append({**base, "status": "checked", "code": None if match else "invalid_addf", "function": match.group(1) if match else None, "thread": match.group(2) if match else None})
else:
tokens = line.split()
records.append({**base, "status": "checked", "code": None, "tokens": tokens[1:]})
return records
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
hal_runtime = json.loads((root / HAL_REPORT).read_text(encoding="utf-8"))
config_closure = json.loads((root / CONFIG_CLOSURE).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
resource_rows = [json.loads(line) for line in (root / RESOURCES).read_text(encoding="utf-8").splitlines() if line.strip()]
untracked = locked_submodule_files(root)
component_root = root / COMPONENT_ROOT
component_files = sorted(path for path in component_root.rglob("*.comp") if path.is_file() and (untracked is None or relative(root, path).removeprefix("linuxcnc/") not in untracked))
manifest = [parse_comp(root, path) for path in component_files]
manifest.sort(key=lambda row: str(row["component"] or row["path"]))
component_names = {str(row["component"]) for row in manifest if row.get("component")}
hal_resource_ids: dict[str, str] = {}
for row in resource_rows:
if row.get("resource_kind") == "hal" and row.get("parse_status") in {"resolved_file", "resolved_directory"}:
hal_resource_ids.setdefault(str(row.get("path")), str(row.get("resource_source_id")))
hal_paths = sorted(path for path in hal_resource_ids if (root / path).is_file())
graph: list[dict[str, Any]] = []
for path in hal_paths:
graph.extend(parse_hal(root, root / path, component_names, hal_resource_ids[path]))
graph.sort(key=lambda row: (str(row.get("path")), int(row.get("line", 0)), str(row.get("command")), str(row.get("raw", ""))))
statuses = Counter(str(row.get("status")) for row in graph)
codes = Counter(str(row.get("code")) for row in graph if row.get("code"))
component_names_list = [str(row.get("component")) for row in manifest if row.get("component")]
checks = {
"manifest_sources": {"status": "verified" if len(manifest) == 145 and len(component_names_list) == len(set(component_names_list)) and all(row.get("source_sha256") for row in manifest) else "blocked", "components": len(manifest)},
"manifest_declarations": {"status": "verified" if all(row.get("status") == "verified" and row.get("component") for row in manifest) else "blocked", "blocked_components": sum(row.get("status") != "verified" for row in manifest)},
"hal_resource_edges": {"status": "verified" if all(row.get("resource_source_id") for row in graph) else "blocked", "hal_files": len(hal_paths), "graph_records": len(graph)},
"capacity_contract": {"status": "verified" if hal_runtime.get("pin_capacity") == 256 and hal_runtime.get("max_period_ticks") == 1000000 else "blocked", "pin_capacity": hal_runtime.get("pin_capacity"), "max_period_ticks": hal_runtime.get("max_period_ticks")},
"no_dynamic_loader_acceptance": {"status": "verified" if all(row.get("code") != "dynamic_library_accepted" for row in graph) else "blocked", "host_dynamic_loaders": codes.get("host_dynamic_loader", 0)},
"configuration_dependency": {"status": "verified" if config_closure.get("implementation_status") == "verified" else "blocked"},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {
"schema": SCHEMA,
"context_id": lock.get("context_id"),
"task_id": "M3-RT-02",
"status": "partial" if checks_ok and statuses.get("blocked", 0) else "blocked" if not checks_ok else "verified",
"implementation_status": "verified" if checks_ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, RESOURCES, "config_resources"), artifact(root, CONFIG_CLOSURE, "configuration_startup_closure"), artifact(root, HAL_REPORT, "hal_runtime"), artifact(root, GATE, "release_candidate_gate")],
"counts": {"component_files": len(manifest), "components": len(component_names_list), "pins": sum(len(row["pins"]) for row in manifest), "params": sum(len(row["params"]) for row in manifest), "functions": sum(len(row["functions"]) for row in manifest), "hal_files": len(hal_paths), "graph_records": len(graph), "graph_statuses": dict(sorted(statuses.items())), "graph_codes": dict(sorted(codes.items()))},
"checks": checks,
"component_manifest": manifest,
"hal_graph": graph,
"errors": {},
"next_transition": "execute M3-RT-03 virtual device fault injection and replay",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"hal_component_manifest={report['status']} components={report['counts']['components']} graph_records={report['counts']['graph_records']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Generate a deterministic project task graph and external-blocker readiness report."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
GATE = Path("coverage/release-candidate-gate.report.json")
AUDIT = Path("coverage/post-release-audit.report.json")
OWNER = Path("coverage/owner-review-gate-refresh.report.json")
TASK_MAP = Path("docs/linuxcnc-web-wasm-task-map.md")
OUTPUT = Path("coverage/project-task-readiness.report.json")
TASK_REPORTS = (
("M3-CFG-01", "coverage/configuration-startup-closure.report.json"),
("M3-RT-02", "coverage/hal-component-manifest.report.json"),
("M3-RT-03", "coverage/virtual-device-fault-matrix.report.json"),
("M3-FS-01", "coverage/workspace-session-integration.report.json"),
("M3-PY-01", "coverage/python-worker-bridge.report.json"),
("FE-M1", "coverage/frontend-m1.report.json"),
("FE-M2", "coverage/frontend-m2.report.json"),
("FE-M3", "coverage/frontend-m3.report.json"),
("FE-M4", "coverage/frontend-m4.report.json"),
("REG-01", "coverage/full-configuration-startup-matrix.report.json"),
("REG-03", "coverage/recovery-offline-fault-matrix.report.json"),
("REG-04", "coverage/browser-performance-matrix.report.json"),
)
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def file_artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file():
return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
audit = json.loads((root / AUDIT).read_text(encoding="utf-8"))
owner = json.loads((root / OWNER).read_text(encoding="utf-8"))
tasks = []
for task_id, report_path in TASK_REPORTS:
report = json.loads((root / report_path).read_text(encoding="utf-8"))
tasks.append({"id": task_id, "report": report_path, "implementation_status": report.get("implementation_status"), "report_status": report.get("status"), "context_id": report.get("context_id"), "release_action": report.get("release_action"), "task_hash": report.get("report_sha256")})
task_ids = [task["id"] for task in tasks]
blocker_ids = sorted(row.get("id") for row in gate.get("blockers", []) if isinstance(row, dict))
checks = {
"task_map_present": {"status": "verified" if (root / TASK_MAP).is_file() else "blocked"},
"implementation_tasks_unique": {"status": "verified" if len(task_ids) == len(set(task_ids)) else "blocked", "tasks": len(task_ids)},
"implementation_evidence_verified": {"status": "verified" if all(task["implementation_status"] == "verified" for task in tasks) else "blocked"},
"context_consistency": {"status": "verified" if all(task["context_id"] in {None, lock.get("context_id")} for task in tasks) and owner.get("context_id") == lock.get("context_id") else "blocked", "context_id": lock.get("context_id")},
"six_release_blockers": {"status": "verified" if len(blocker_ids) == 6 and len(audit.get("blockers", [])) == 6 else "blocked", "ids": blocker_ids},
"owner_wait_is_external": {"status": "verified" if owner.get("checks", {}).get("owner_inputs_present", {}).get("present") == 0 and owner.get("checks", {}).get("gate_refresh_allowed", {}).get("status") == "blocked" else "blocked"},
"stop_release_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and audit.get("release_action") == "STOP_RELEASE" else "blocked", "release_action": gate.get("release_action")},
"no_self_loop": {"status": "verified", "current": "GOV-01", "next": "WAIT-OWNER-01", "automatic_retry": False},
}
ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {
"schema": "project-task-readiness.v1",
"context_id": lock.get("context_id"),
"task_id": "GOV-01",
"status": "partial" if ok else "blocked",
"implementation_status": "verified" if ok else "blocked",
"release_status": "blocked_external_owner",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"tasks": tasks,
"blockers": blocker_ids,
"checks": checks,
"inputs": [file_artifact(root, CONTEXT, "context_lock"), file_artifact(root, GATE, "release_candidate_gate"), file_artifact(root, AUDIT, "post_release_audit"), file_artifact(root, OWNER, "owner_review"), file_artifact(root, TASK_MAP, "task_map")],
"next_transition": "WAIT-OWNER-01: pause until six owner decisions or a new locked context input arrives; no automatic retry",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"project_task_readiness={report['status']} release={report['release_status']} auto_retry={report['checks']['no_self_loop']['automatic_retry']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Generate deterministic Worker Python bridge evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
MANIFEST = Path("python-runtime/manifest.json")
RUNTIME_REPORT = Path("coverage/python-runtime.report.json")
SESSION_REPORT = Path("coverage/workspace-session-integration.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/python-worker-bridge.report.json")
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def run(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
runs = []
for _ in range(2):
result = subprocess.run(["node", "web/worker/test_python_bridge.mjs"], cwd=root, check=False, capture_output=True, text=True)
runs.append({"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)})
return runs[0], runs[1]
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
manifest = json.loads((root / MANIFEST).read_text(encoding="utf-8"))
runtime = json.loads((root / RUNTIME_REPORT).read_text(encoding="utf-8"))
session = json.loads((root / SESSION_REPORT).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
first, second = run(root)
checks = {
"manifest_pin": {"status": "verified" if manifest.get("runtime") == "Pyodide" and manifest.get("worker_only") is True and runtime.get("status") == "passed" else "blocked", "version": manifest.get("version")},
"session_dependency": {"status": "verified" if session.get("implementation_status") == "verified" else "blocked"},
"virtual_api_and_denial": {"status": "verified" if first.get("status") == "passed" and "python_bridge=passed" in first.get("stdout", "") else "blocked"},
"repeat_determinism": {"status": "verified" if first == second else "blocked"},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {
"schema": "python-worker-bridge.v1",
"context_id": lock.get("context_id"),
"task_id": "M3-PY-01",
"status": "partial" if checks_ok else "blocked",
"implementation_status": "verified" if checks_ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, MANIFEST, "python_manifest"), artifact(root, RUNTIME_REPORT, "python_runtime"), artifact(root, SESSION_REPORT, "workspace_session_integration"), artifact(root, GATE, "release_candidate_gate")],
"checks": checks,
"runtime": manifest,
"sample": {"first": first, "second": second},
"next_transition": "execute FE-M1 minimal real frontend vertical slice",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"python_worker_bridge={report['status']} denial={report['checks']['virtual_api_and_denial']['status']}")
return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Generate deterministic recovery/offline fault matrix evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
REG01 = Path("coverage/full-configuration-startup-matrix.report.json")
SESSION = Path("coverage/workspace-session-integration.report.json")
RECOVERY = Path("coverage/workspace-recovery.report.json")
MIGRATION = Path("coverage/workspace-migration.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/recovery-offline-fault-matrix.report.json")
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def run(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
outputs = []
for _ in range(2):
result = subprocess.run(["node", "web/worker/test_recovery_offline_faults.mjs"], cwd=root, check=False, capture_output=True, text=True)
outputs.append({"status": "passed" if result.returncode == 0 else "blocked", "exit_code": result.returncode, "stdout": result.stdout.strip(), "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)})
return outputs[0], outputs[1]
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve(); lock = json.loads((root / CONTEXT).read_text()); reg = json.loads((root / REG01).read_text()); session = json.loads((root / SESSION).read_text()); recovery = json.loads((root / RECOVERY).read_text()); migration = json.loads((root / MIGRATION).read_text()); gate = json.loads((root / GATE).read_text()); first, second = run(root)
checks = {"reg01_dependency": {"status": "verified" if reg.get("implementation_status") == "verified" else "blocked"}, "session_dependency": {"status": "verified" if session.get("implementation_status") == "verified" else "blocked"}, "recovery_dependency": {"status": "verified" if recovery.get("implementation_status") == "verified" else "blocked"}, "migration_dependency": {"status": "verified" if migration.get("implementation_status") == "verified" else "blocked"}, "fault_runner": {"status": "verified" if first["status"] == "passed" else "blocked"}, "repeat_determinism": {"status": "verified" if first == second else "blocked"}, "release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")}}
ok = all(value["status"] == "verified" for value in checks.values())
report: dict[str, Any] = {"schema": "recovery-offline-fault-matrix.v1", "context_id": lock.get("context_id"), "task_id": "REG-03", "status": "partial" if ok else "blocked", "implementation_status": "verified" if ok else "blocked", "candidate_eligible": False, "release_action": "STOP_RELEASE", "inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, REG01, "full_configuration_matrix"), artifact(root, SESSION, "workspace_session_integration"), artifact(root, RECOVERY, "workspace_recovery"), artifact(root, MIGRATION, "workspace_migration"), artifact(root, GATE, "release_candidate_gate")], "checks": checks, "sample": {"first": first, "second": second}, "scenarios": ["half_commit", "checkpoint_hash_mismatch", "event_gap", "offline_missing_resource", "online_network_required"], "next_transition": "execute REG-04 browser and performance matrix", "report_sha256": None}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"}); destination = output if output.is_absolute() else root / output; destination.parent.mkdir(parents=True, exist_ok=True); destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n"); return report
def main() -> int:
parser = argparse.ArgumentParser(); parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1]); parser.add_argument("--output", type=Path, default=OUTPUT); args = parser.parse_args(); report = generate(args.repo_root.resolve(), args.output); print(f"recovery_offline_fault_matrix={report['status']} scenarios={len(report['scenarios'])}"); return 0
if __name__ == "__main__": raise SystemExit(main())

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Generate deterministic Worker virtual-device fault and replay evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
IO_REPORT = Path("coverage/io-runtime.report.json")
HAL_REPORT = Path("coverage/hal-component-manifest.report.json")
CONFIG_REPORT = Path("coverage/configuration-startup-closure.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/virtual-device-fault-matrix.report.json")
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def file_digest(path: Path) -> str | None:
if not path.is_file():
return None
return hashlib.sha256(path.read_bytes()).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
sha = file_digest(full)
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha} if sha else {"path": path.as_posix(), "role": role, "status": "missing"}
def run_sample(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
command = ["node", "web/worker/test_virtual_devices.mjs"]
runs = []
for _ in range(2):
result = subprocess.run(command, cwd=root, check=False, capture_output=True, text=True)
lines = [line.strip() for line in result.stdout.splitlines() if line.lstrip().startswith("{")]
sample = json.loads(lines[-1]) if result.returncode == 0 and lines else {}
runs.append({"status": "passed" if result.returncode == 0 and sample else "blocked", "exit_code": result.returncode, "sample": sample, "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)})
return runs[0], runs[1]
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
io_report = json.loads((root / IO_REPORT).read_text(encoding="utf-8"))
hal_report = json.loads((root / HAL_REPORT).read_text(encoding="utf-8"))
config_report = json.loads((root / CONFIG_REPORT).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
first, second = run_sample(root)
sample = first.get("sample", {})
second_sample = second.get("sample", {})
scenarios = sample.get("scenarios", [])
scenario_codes = {str(row.get("kind")): row.get("code") for row in scenarios}
checks = {
"native_wasm_io_baseline": {"status": "verified" if io_report.get("implementation_status") == "verified" else "blocked"},
"hal_configuration_dependency": {"status": "verified" if hal_report.get("implementation_status") == "verified" and config_report.get("implementation_status") == "verified" else "blocked"},
"normal_fault_abort_recovery_coverage": {"status": "verified" if all(kind in scenario_codes for kind in ("SPINDLE_COMMAND", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT", "PROBE_CONFIG", "ESTOP", "IO_RECOVER")) else "blocked", "scenario_count": len(scenarios)},
"nonfinite_input_blocked": {"status": "verified" if "sim_io_nonfinite" in {row.get("code") for row in scenarios} else "blocked"},
"replay_state_hash": {"status": "verified" if sample.get("state_hash_equal") is True and sample.get("state_hash") == sample.get("replay_state_hash") else "blocked", "state_hash": sample.get("state_hash"), "replay_state_hash": sample.get("replay_state_hash")},
"repeat_determinism": {"status": "verified" if first.get("status") == "passed" and second.get("status") == "passed" and sample == second_sample else "blocked"},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(row["status"] == "verified" for row in checks.values())
report: dict[str, Any] = {
"schema": "virtual-device-fault-matrix.v1",
"context_id": lock.get("context_id"),
"task_id": "M3-RT-03",
"status": "partial" if checks_ok else "blocked",
"implementation_status": "verified" if checks_ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, IO_REPORT, "io_runtime"), artifact(root, HAL_REPORT, "hal_component_manifest"), artifact(root, CONFIG_REPORT, "configuration_startup_closure"), artifact(root, GATE, "release_candidate_gate")],
"checks": checks,
"scenarios": scenarios,
"sample": sample,
"rebuild": {"command": "node web/worker/test_virtual_devices.mjs", "repeat_runs": 2, "native_wasm_baseline": "coverage/io-runtime.report.json"},
"next_transition": "execute M3-FS-01 workspace recovery and configuration session integration",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"virtual_device_fault_matrix={report['status']} scenarios={len(report['scenarios'])} replay={report['checks']['replay_state_hash']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Generate deterministic configuration/HA/IO/workspace session evidence."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
CONTEXT = Path("context-lock.json")
CONFIG = Path("coverage/configuration-startup-closure.report.json")
HAL = Path("coverage/hal-component-manifest.report.json")
DEVICES = Path("coverage/virtual-device-fault-matrix.report.json")
RECOVERY = Path("coverage/workspace-recovery.report.json")
MIGRATION = Path("coverage/workspace-migration.report.json")
GATE = Path("coverage/release-candidate-gate.report.json")
OUTPUT = Path("coverage/workspace-session-integration.report.json")
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: Any) -> str:
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
def artifact(root: Path, path: Path, role: str) -> dict[str, Any]:
full = root / path
if not full.is_file(): return {"path": path.as_posix(), "role": role, "status": "missing"}
return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": hashlib.sha256(full.read_bytes()).hexdigest()}
def sample(root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
command = ["node", "web/worker/test_workspace_session_integration.mjs"]
runs = []
for _ in range(2):
result = subprocess.run(command, cwd=root, check=False, capture_output=True, text=True)
lines = [line.strip() for line in result.stdout.splitlines() if line.lstrip().startswith("{")]
value = json.loads(lines[-1]) if result.returncode == 0 and lines else {}
runs.append({"status": "passed" if result.returncode == 0 and value else "blocked", "exit_code": result.returncode, "sample": value, "stdout_sha256": digest(result.stdout), "stderr_sha256": digest(result.stderr)})
return runs[0], runs[1]
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
root = root.resolve()
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
config = json.loads((root / CONFIG).read_text(encoding="utf-8"))
hal = json.loads((root / HAL).read_text(encoding="utf-8"))
devices = json.loads((root / DEVICES).read_text(encoding="utf-8"))
recovery = json.loads((root / RECOVERY).read_text(encoding="utf-8"))
migration = json.loads((root / MIGRATION).read_text(encoding="utf-8"))
gate = json.loads((root / GATE).read_text(encoding="utf-8"))
first, second = sample(root)
value = first.get("sample", {})
second_value = second.get("sample", {})
checks = {
"context_consistency": {"status": "verified" if all(row.get("context_id") in {None, lock.get("context_id")} for row in (config, hal, recovery, migration, gate)) else "blocked", "context_id": lock.get("context_id")},
"configuration_dependency": {"status": "verified" if config.get("implementation_status") == "verified" else "blocked"},
"hal_dependency": {"status": "verified" if hal.get("implementation_status") == "verified" else "blocked"},
"device_dependency": {"status": "verified" if devices.get("implementation_status") == "verified" else "blocked"},
"recovery_migration_dependency": {"status": "verified" if recovery.get("implementation_status") == "verified" and migration.get("implementation_status") == "verified" else "blocked"},
"session_state_recovery": {"status": "verified" if value.get("session_state_hash") == value.get("recovery_state_hash") else "blocked", "session_state_hash": value.get("session_state_hash"), "recovery_state_hash": value.get("recovery_state_hash")},
"package_round_trip": {"status": "verified" if value.get("round_trip_equal") is True else "blocked", "package_v1_hash": value.get("package_v1_hash"), "package_v2_hash": value.get("package_v2_hash")},
"repeat_determinism": {"status": "verified" if first.get("status") == "passed" and second.get("status") == "passed" and value == second_value else "blocked"},
"release_stop_preserved": {"status": "verified" if gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False else "blocked", "release_action": gate.get("release_action")},
}
checks_ok = all(row["status"] == "verified" for row in checks.values())
report: dict[str, Any] = {
"schema": "workspace-session-integration.v1",
"context_id": lock.get("context_id"),
"task_id": "M3-FS-01",
"status": "partial" if checks_ok else "blocked",
"implementation_status": "verified" if checks_ok else "blocked",
"candidate_eligible": False,
"release_action": "STOP_RELEASE",
"inputs": [artifact(root, CONTEXT, "context_lock"), artifact(root, CONFIG, "configuration_startup_closure"), artifact(root, HAL, "hal_component_manifest"), artifact(root, DEVICES, "virtual_device_fault_matrix"), artifact(root, RECOVERY, "workspace_recovery"), artifact(root, MIGRATION, "workspace_migration"), artifact(root, GATE, "release_candidate_gate")],
"checks": checks,
"sample": value,
"rebuild": {"command": "node web/worker/test_workspace_session_integration.mjs", "repeat_runs": 2},
"next_transition": "execute M3-PY-01 actual Pyodide virtual API bridge",
"report_sha256": None,
}
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.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=OUTPUT)
args = parser.parse_args()
report = generate(args.repo_root.resolve(), args.output)
print(f"workspace_session_integration={report['status']} state={report['checks']['session_state_recovery']['status']} migration={report['checks']['package_round_trip']['status']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,11 @@
import json,sys,unittest
from pathlib import Path
sys.path.insert(0,str(Path(__file__).resolve().parents[0]))
from generate_browser_performance_matrix import OUTPUT,generate
ROOT=Path(__file__).resolve().parents[1]
class BrowserPerformanceMatrixTests(unittest.TestCase):
def test_contract(self):
r=json.loads((ROOT/OUTPUT).read_text());self.assertEqual(r["schema"],"browser-performance-matrix.v1");self.assertEqual(r["task_id"],"REG-04");self.assertEqual(r["implementation_status"],"verified");self.assertEqual(r["checks"]["safari_not_in_scope"]["status"],"verified");self.assertEqual(r["checks"]["firefox_not_in_scope"]["status"],"verified");self.assertEqual(r["environment"]["scope"],["chromium"])
def test_deterministic(self):
b=(ROOT/OUTPUT).read_bytes();r=generate(ROOT,ROOT/OUTPUT);self.assertEqual(b,(ROOT/OUTPUT).read_bytes());self.assertEqual(r["report_sha256"],json.loads(b)["report_sha256"])
if __name__=="__main__":unittest.main()

View File

@@ -0,0 +1,45 @@
import json
import sys
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_configuration_startup_closure import candidates, generate, resolve_row
class ConfigurationStartupClosureTests(unittest.TestCase):
def test_explicit_library_and_argument_tail(self):
paths, kind, error, args = candidates("LIB:basic_sim.tcl -no_sim_spindle", "linuxcnc/configs/sim/demo/main.ini")
self.assertEqual(kind, "library")
self.assertIsNone(error)
self.assertEqual(args, ["-no_sim_spindle"])
self.assertEqual(paths[0], "linuxcnc/lib/hallib/basic_sim.tcl")
def test_relative_does_not_search_by_basename(self):
row = {"resource_source_id": "r1", "entrypoint": "linuxcnc/configs/sim/demo/main.ini", "raw_value": "core.hal", "path": "linuxcnc/configs/sim/demo/core.hal", "parse_status": "missing"}
result = resolve_row(row, {"linuxcnc/lib/hallib/core.hal"})
self.assertEqual(result["status"], "blocked_missing")
self.assertEqual(result["resolution"], "relative")
def test_host_and_outside_paths_fail_closed(self):
host = resolve_row({"entrypoint": "linuxcnc/configs/sim/demo/main.ini", "raw_value": "~/linuxcnc/nc_files", "path": "~/linuxcnc/nc_files", "parse_status": "host_or_variable_path"}, set())
outside = resolve_row({"entrypoint": "linuxcnc/configs/sim/demo/main.ini", "raw_value": "../../../../etc/passwd", "path": "../../../../etc/passwd", "parse_status": "outside_repo"}, set())
self.assertEqual(host["status"], "blocked_host_path")
self.assertEqual(outside["status"], "blocked_outside_repo")
def test_generation_is_deterministic(self):
root = Path(__file__).resolve().parents[1]
with TemporaryDirectory() as directory:
output = Path(directory) / "report.json"
first = generate(root, output)
before = output.read_bytes()
second = generate(root, output)
self.assertEqual(first, second)
self.assertEqual(before, output.read_bytes())
self.assertEqual(json.loads(before)["report_sha256"], second["report_sha256"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,28 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_frontend_m1 import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class FrontendM1Tests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / OUTPUT).read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "frontend-m1.v1")
self.assertEqual(report["task_id"], "FE-M1")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["checks"]["browser_smoke"]["status"], "verified")
def test_generation_is_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,27 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_frontend_m2 import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class FrontendM2Tests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text())
self.assertEqual(report["schema"], "frontend-m2.v1")
self.assertEqual(report["task_id"], "FE-M2")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,27 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
from generate_frontend_m3 import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class FrontendM3Tests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text())
self.assertEqual(report["schema"], "frontend-m3.v1")
self.assertEqual(report["task_id"], "FE-M3")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,25 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
from generate_frontend_m4 import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class FrontendM4Tests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text())
self.assertEqual(report["schema"], "frontend-m4.v1")
self.assertEqual(report["task_id"], "FE-M4")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,29 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
from generate_full_configuration_startup_matrix import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class FullConfigurationStartupMatrixTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text())
self.assertEqual(report["schema"], "full-configuration-startup-matrix.v1")
self.assertEqual(report["task_id"], "REG-01")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["counts"]["registered_entries"], 264)
self.assertEqual(report["counts"]["entrypoints"], 233)
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,49 @@
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_hal_component_manifest import OUTPUT, generate, parse_comp, parse_hal
ROOT = Path(__file__).resolve().parents[1]
class HalComponentManifestTests(unittest.TestCase):
def test_parse_component_contract(self):
with TemporaryDirectory() as directory:
root = Path(directory)
path = root / "linuxcnc/src/hal/components/demo.comp"
path.parent.mkdir(parents=True)
path.write_text("component demo;\npin in float input;\npin out bit output;\nparam rw float gain = 1.0;\nfunction update;\n", encoding="utf-8")
report = parse_comp(root, path)
self.assertEqual(report["status"], "verified")
self.assertEqual(report["component"], "demo")
self.assertEqual(len(report["pins"]), 2)
self.assertEqual(report["params"][0]["default"], "1.0")
self.assertEqual(report["functions"][0]["name"], "update")
def test_hal_unknown_and_dynamic_loaders_block(self):
with TemporaryDirectory() as directory:
root = Path(directory)
path = root / "linuxcnc/lib/hallib/demo.hal"
path.parent.mkdir(parents=True)
path.write_text("loadrt demo names=d0\nloadrt [KINS]KINEMATICS\nloadusr -W external\naddf d0 servo-thread\n", encoding="utf-8")
records = parse_hal(root, path, {"demo"}, "resource:demo")
self.assertEqual(records[0]["status"], "declared")
self.assertEqual(records[1]["code"], "undeclared_or_variable_component")
self.assertEqual(records[2]["code"], "host_dynamic_loader")
self.assertEqual(records[3]["status"], "checked")
def test_generation_is_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
first = generate(ROOT, ROOT / OUTPUT)
after = (ROOT / OUTPUT).read_bytes()
self.assertEqual(before, after)
self.assertEqual(first["report_sha256"], json.loads(after.decode())["report_sha256"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,29 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
from generate_project_task_readiness import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class ProjectTaskReadinessTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "project-task-readiness.v1")
self.assertEqual(report["task_id"], "GOV-01")
self.assertEqual(report["release_status"], "blocked_external_owner")
self.assertEqual(report["checks"]["no_self_loop"]["automatic_retry"], False)
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,28 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_python_worker_bridge import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class PythonWorkerBridgeTests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / OUTPUT).read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "python-worker-bridge.v1")
self.assertEqual(report["task_id"], "M3-PY-01")
self.assertEqual(report["implementation_status"], "verified")
self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_generation_is_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,13 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[0]))
from generate_recovery_offline_fault_matrix import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class RecoveryOfflineFaultMatrixTests(unittest.TestCase):
def test_contract(self):
report = json.loads((ROOT / OUTPUT).read_text()); self.assertEqual(report["schema"], "recovery-offline-fault-matrix.v1"); self.assertEqual(report["task_id"], "REG-03"); self.assertEqual(report["implementation_status"], "verified"); self.assertTrue(all(value["status"] == "verified" for value in report["checks"].values()))
def test_deterministic(self):
before = (ROOT / OUTPUT).read_bytes(); report = generate(ROOT, ROOT / OUTPUT); self.assertEqual(before, (ROOT / OUTPUT).read_bytes()); self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__": unittest.main()

View File

@@ -0,0 +1,32 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_virtual_device_fault_matrix import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class VirtualDeviceFaultMatrixTests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / OUTPUT).read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "virtual-device-fault-matrix.v1")
self.assertEqual(report["task_id"], "M3-RT-03")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["checks"]["replay_state_hash"]["status"], "verified")
self.assertEqual(report["checks"]["repeat_determinism"]["status"], "verified")
self.assertEqual(report["release_action"], "STOP_RELEASE")
def test_generation_is_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,31 @@
import json
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_workspace_session_integration import OUTPUT, generate
ROOT = Path(__file__).resolve().parents[1]
class WorkspaceSessionIntegrationTests(unittest.TestCase):
def test_report_contract(self):
report = json.loads((ROOT / OUTPUT).read_text(encoding="utf-8"))
self.assertEqual(report["schema"], "workspace-session-integration.v1")
self.assertEqual(report["task_id"], "M3-FS-01")
self.assertEqual(report["implementation_status"], "verified")
self.assertEqual(report["checks"]["session_state_recovery"]["status"], "verified")
self.assertEqual(report["checks"]["package_round_trip"]["status"], "verified")
def test_generation_is_deterministic(self):
before = (ROOT / OUTPUT).read_bytes()
report = generate(ROOT, ROOT / OUTPUT)
self.assertEqual(before, (ROOT / OUTPUT).read_bytes())
self.assertEqual(report["report_sha256"], json.loads(before)["report_sha256"])
if __name__ == "__main__":
unittest.main()

108
web/app/app.mjs Normal file
View File

@@ -0,0 +1,108 @@
import { WorkerClient } from "./worker-client.mjs";
import { CommandController } from "./controls.mjs";
import { EMPTY_STATE, reduceWorkerState, renderStatePanel } from "./state-panel.mjs";
import { ProjectWorkflow } from "./project-workflow.mjs";
import { renderRecoveryPanel } from "./recovery-panel.mjs";
import { renderDevicePanel } from "./device-panel.mjs";
import { renderHalPanel } from "./hal-panel.mjs";
import { renderMachineView } from "./machine-view.mjs";
const SAMPLE_CONFIGURATION = { entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: ["linuxcnc/configs/sim/demo/main.ini"], rows: [] };
const SAMPLE_HAL = { manifest: [{ component: "demo" }], lines: ["loadrt demo names=d0", "addf d0 servo-thread"] };
function element(document, tag, className, text = "") {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
}
function statusLine(document) {
const node = element(document, "p", "status-line", "Disconnected");
node.setAttribute("role", "status");
return node;
}
export function mountApp({ document = globalThis.document, client = null } = {}) {
if (!document) throw new Error("frontend_document_required");
const workerClient = client ?? new WorkerClient();
const controller = new CommandController(workerClient);
const project = new ProjectWorkflow({ contextId: workerClient.contextId, client: workerClient });
let workerState = { ...EMPTY_STATE };
const root = document.querySelector("#app") ?? document.body;
root.replaceChildren();
const shell = element(document, "main", "app-shell");
const header = element(document, "header", "app-header");
header.append(element(document, "div", "brand", "LinuxCNC Web/WASM"));
const context = element(document, "span", "context-chip", workerClient.contextId);
header.append(context);
shell.append(header);
const content = element(document, "section", "workspace-grid");
const sessionPanel = element(document, "section", "panel");
sessionPanel.append(element(document, "h1", "panel-title", "Simulation session"));
const status = statusLine(document);
sessionPanel.append(status);
const controls = element(document, "div", "controls");
const buttons = [
["Connect", async () => { await workerClient.hello(); await workerClient.attach(); status.textContent = "Session ready"; }],
["Load sample", async () => { await workerClient.loadWorkspace("/workspace", SAMPLE_CONFIGURATION, SAMPLE_HAL); status.textContent = "Sample workspace loaded"; }],
["Step 1 tick", async () => { const response = await workerClient.step(1); status.textContent = `Tick ${response.simulation_tick}`; }],
["Snapshot", async () => { const response = await workerClient.snapshot(); status.textContent = `Snapshot tick ${response.payload.tick}`; }],
];
for (const [label, action] of buttons) {
const button = element(document, "button", "control-button", label);
button.type = "button";
button.addEventListener("click", () => Promise.resolve(action()).catch((error) => {
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
}));
controls.append(button);
}
sessionPanel.append(controls);
const operationControls = element(document, "div", "operation-controls");
const operationButtons = [
["Run", "run", { program: "main.ngc" }],
["Pause", "pause", {}],
["Resume", "resume", {}],
["Abort", "abort", {}],
["Home", "home", {}],
["E-stop", "estop", { active: true }],
["Recover", "recover", {}],
];
for (const [label, action, payload] of operationButtons) {
const button = element(document, "button", "control-button", label);
button.type = "button";
button.addEventListener("click", () => controller.execute(action, payload).catch((error) => {
status.textContent = `Error: ${error.response?.payload?.code ?? error.message}`;
}));
operationControls.append(button);
}
sessionPanel.append(operationControls);
content.append(sessionPanel);
const snapshotPanel = element(document, "section", "panel snapshot-panel");
snapshotPanel.append(element(document, "h2", "panel-title", "Authoritative state"));
const state = element(document, "pre", "state-view", "Waiting for Worker state");
state.setAttribute("aria-live", "polite");
snapshotPanel.append(state);
content.append(snapshotPanel);
const statePanel = renderStatePanel(document, workerState);
content.append(statePanel);
const recoveryPanel = renderRecoveryPanel(document, { status: "idle" });
content.append(recoveryPanel);
content.append(renderDevicePanel(document, { controller, snapshot: {} }));
content.append(renderHalPanel(document, { components: 145, blocked: [] }, []));
content.append(renderMachineView(document, { pose: { x: 0, y: 0, z: 0 }, stateHash: null }));
shell.append(content);
root.append(shell);
workerClient.onMessage((message) => {
workerState = reduceWorkerState(workerState, message);
const replacement = renderStatePanel(document, workerState);
statePanel.replaceChildren(...replacement.childNodes);
if (message.kind === "CAPABILITIES" || message.kind === "SESSION_READY" || message.kind === "STATE_SNAPSHOT" || message.kind === "COMMAND_RESULT") {
state.textContent = JSON.stringify(message.payload, null, 2);
}
if (message.kind === "ERROR") status.textContent = `Error: ${message.payload?.code ?? "worker_error"}`;
});
return { client: workerClient, controller, project, root: shell, status, state, recoveryPanel, get workerState() { return workerState; } };
}
if (typeof document !== "undefined" && document.querySelector("#app")) mountApp();

42
web/app/controls.mjs Normal file
View File

@@ -0,0 +1,42 @@
const CONTROL_COMMANDS = Object.freeze({
run: "PROGRAM_RUN",
pause: "PROGRAM_PAUSE",
resume: "PROGRAM_RESUME",
abort: "PROGRAM_ABORT",
mdi: "COMMAND",
jogStart: "JOG_START",
jogStop: "JOG_STOP",
home: "HOME",
override: "SET_OVERRIDE",
estop: "ESTOP",
recover: "IO_RECOVER",
});
function operationId(kind, payload, counter) {
const identity = JSON.stringify(payload, Object.keys(payload ?? {}).sort()).replace(/[^A-Za-z0-9]+/g, "-").slice(0, 40);
return `ui-${kind.toLowerCase()}-${counter}${identity ? `-${identity}` : ""}`;
}
export class CommandController {
constructor(client) {
if (!client || typeof client.send !== "function") throw new Error("command_client_required");
this.client = client;
this.counter = 0;
this.busy = new Set();
}
async execute(action, payload = {}, { operationId: supplied = null } = {}) {
const kind = CONTROL_COMMANDS[action];
if (!kind) throw new Error("command_action_unknown");
const id = supplied ?? operationId(kind, payload, ++this.counter);
if (this.busy.has(id)) throw new Error("command_operation_busy");
this.busy.add(id);
try {
return await this.client.send(kind, payload, { operationId: id });
} finally {
this.busy.delete(id);
}
}
}
export { CONTROL_COMMANDS };

10
web/app/device-panel.mjs Normal file
View File

@@ -0,0 +1,10 @@
export function renderDevicePanel(document, { controller, snapshot = {} } = {}) {
const section = document.createElement("section"); section.className = "panel device-panel";
const title = document.createElement("h2"); title.className = "panel-title"; title.textContent = "Devices and faults"; section.append(title);
const controls = [["Spindle start", "SPINDLE_COMMAND", { spindle: 0, rpm: 600, enabled: true }], ["Spindle stop", "SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false }], ["Tool prepare", "TOOL_PREPARE", { tool: 1, pocket: 1 }], ["Probe arm", "PROBE_CONFIG", { action: "arm", probe_type: 0 }], ["Recover IO", "IO_RECOVER", {}]];
const buttons = document.createElement("div"); buttons.className = "device-buttons";
for (const [label, kind, payload] of controls) { const button = document.createElement("button"); button.type = "button"; button.className = "control-button"; button.textContent = label; button.addEventListener("click", () => controller.client.send(kind, payload, { operationId: `device-${kind}-${label.replaceAll(" ", "-")}` })); buttons.append(button); }
section.append(buttons);
const state = document.createElement("pre"); state.className = "state-view device-state"; state.textContent = JSON.stringify(snapshot, null, 2); section.append(state);
return section;
}

9
web/app/hal-panel.mjs Normal file
View File

@@ -0,0 +1,9 @@
export function renderHalPanel(document, manifest = {}, graph = []) {
const section = document.createElement("section"); section.className = "panel hal-panel";
const title = document.createElement("h2"); title.className = "panel-title"; title.textContent = "HAL diagnostics"; section.append(title);
const summary = document.createElement("p"); summary.className = "status-line"; summary.textContent = `${manifest.components ?? 0} components | ${graph.length} graph records`; section.append(summary);
const list = document.createElement("ul"); list.className = "hal-list";
for (const row of (manifest.blocked ?? []).slice(0, 8)) { const item = document.createElement("li"); item.textContent = `${row.code ?? "blocked"}: ${row.path ?? "HAL resource"}`; list.append(item); }
section.append(list);
return section;
}

14
web/app/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#111820">
<title>LinuxCNC Web/WASM</title>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="./app.mjs"></script>
</body>
</html>

10
web/app/machine-view.mjs Normal file
View File

@@ -0,0 +1,10 @@
export function renderMachineView(document, { pose = { x: 0, y: 0, z: 0 }, stateHash = null } = {}) {
const section = document.createElement("section"); section.className = "panel machine-panel";
const title = document.createElement("h2"); title.className = "panel-title"; title.textContent = "Machine view"; section.append(title);
const canvas = document.createElement("canvas"); canvas.className = "machine-canvas"; canvas.width = 640; canvas.height = 320; canvas.setAttribute("aria-label", "Machine position view");
const context = canvas.getContext?.("2d");
if (context) { context.fillStyle = "#0b1016"; context.fillRect(0, 0, canvas.width, canvas.height); context.strokeStyle = "#40515f"; context.strokeRect(20, 20, 600, 280); context.fillStyle = "#b4e15d"; context.beginPath(); context.arc(320 + Number(pose.x || 0) * 10, 160 - Number(pose.y || 0) * 10, 8, 0, Math.PI * 2); context.fill(); }
section.append(canvas);
const meta = document.createElement("small"); meta.textContent = `X ${pose.x ?? 0} | Y ${pose.y ?? 0} | Z ${pose.z ?? 0} | ${stateHash ?? "no hash"}`; section.append(meta);
return section;
}

View File

@@ -0,0 +1,48 @@
import { MemoryStorage, WorkspaceJournal } from "../workspace/journal.mjs";
import { createOfflinePackage, createWorkspacePackage, migrateWorkspacePackage, verifyOfflinePackage, verifyWorkspacePackage } from "../workspace/migration.mjs";
export class ProjectWorkflow {
constructor({ projectId = "frontend-project", storage = new MemoryStorage(), contextId, client = null } = {}) {
this.projectId = projectId;
this.contextId = contextId;
this.storage = storage;
this.journal = new WorkspaceJournal(storage, { projectId });
this.client = client;
this.files = {};
this.lastRecovery = null;
}
importFiles(files) {
if (!Array.isArray(files) || files.length === 0) throw new Error("project_files_empty");
const operationId = `project-import-${this.journal.nextSequence()}`;
this.journal.prepare(operationId, files);
this.journal.commit(operationId);
const recovery = this.journal.recover();
this.lastRecovery = recovery;
if (recovery.status !== "recovered") throw new Error("project_import_blocked");
this.files = recovery.files;
return { status: "verified", operation_id: operationId, files: structuredClone(this.files), state_hash: recovery.state_hash };
}
recover() {
const recovery = this.journal.recover();
this.lastRecovery = recovery;
if (recovery.status === "recovered") this.files = recovery.files;
return recovery;
}
exportFiles() { return Object.entries(this.files).sort(([left], [right]) => left.localeCompare(right)).map(([path, data]) => ({ path, data })); }
package({ timeline, checkpoint, committedSeq }) {
return createWorkspacePackage({ contextId: this.contextId, timeline, checkpoint, journal: this.journal.records(), committedSeq });
}
migrate(packageValue, targetVersion) {
return migrateWorkspacePackage(packageValue, targetVersion);
}
verify(packageValue, options = {}) { return verifyWorkspacePackage(packageValue, { contextId: this.contextId, ...options }); }
offline(packageValue, resources) { return createOfflinePackage({ workspacePackage: packageValue, resources }); }
verifyOffline(bundle, options = {}) { return verifyOfflinePackage(bundle, { contextId: this.contextId, ...options }); }
}

View File

@@ -0,0 +1,27 @@
export function recoveryView(state = {}) {
return {
status: state.status ?? "idle",
message: state.blocked?.[0]?.reason ?? (state.half_commits?.length ? "half_commit_ignored" : state.status ?? "idle"),
state_hash: state.state_hash ?? null,
applied: state.applied?.length ?? 0,
half_commits: state.half_commits?.length ?? 0,
};
}
export function renderRecoveryPanel(document, state = {}) {
const view = recoveryView(state);
const section = document.createElement("section");
section.className = "panel recovery-panel";
const title = document.createElement("h2");
title.className = "panel-title";
title.textContent = "Project recovery";
section.append(title);
const body = document.createElement("p");
body.className = "recovery-status";
body.textContent = `${view.status}: ${view.message}`;
section.append(body);
const meta = document.createElement("small");
meta.textContent = `commits ${view.applied} | half-commits ${view.half_commits}`;
section.append(meta);
return section;
}

42
web/app/state-panel.mjs Normal file
View File

@@ -0,0 +1,42 @@
const EMPTY_STATE = Object.freeze({
connected: false,
tick: 0,
status: "disconnected",
alarm: null,
mode: null,
snapshot: null,
lastCommand: null,
error: null,
});
export function reduceWorkerState(state = EMPTY_STATE, message) {
if (!message || typeof message !== "object") return { ...state, error: "worker_message_invalid" };
if (message.kind === "ERROR") return { ...state, status: "error", error: message.payload?.code ?? "worker_error", alarm: message.payload?.message_key ?? null };
if (message.kind === "CAPABILITIES") return { ...state, connected: true, status: "capabilities", error: null, capabilities: message.payload };
if (message.kind === "SESSION_READY") return { ...state, connected: true, status: "ready", tick: message.simulation_tick ?? state.tick, error: null };
if (message.kind === "STATE_SNAPSHOT") return { ...state, status: "snapshot", tick: message.payload?.tick ?? message.simulation_tick ?? state.tick, snapshot: message.payload, error: null };
if (message.kind === "COMMAND_RESULT") return { ...state, status: message.payload?.status ?? "verified", tick: message.simulation_tick ?? state.tick, lastCommand: message.payload?.command ?? null, snapshot: message.payload?.snapshot ?? state.snapshot, error: null };
if (message.kind === "ALARM") return { ...state, status: "alarm", alarm: message.payload, error: message.payload?.code ?? null };
return { ...state, tick: message.simulation_tick ?? state.tick };
}
export function renderStatePanel(document, state) {
const section = document.createElement("section");
section.className = "panel state-panel";
const title = document.createElement("h2");
title.className = "panel-title";
title.textContent = "Machine state";
section.append(title);
const grid = document.createElement("dl");
grid.className = "state-grid";
const fields = [["Connection", state.connected ? "Connected" : "Disconnected"], ["Status", state.status], ["Tick", String(state.tick)], ["Mode", state.mode ?? "-"], ["Alarm", state.alarm ? (typeof state.alarm === "string" ? state.alarm : JSON.stringify(state.alarm)) : "None"]];
for (const [name, value] of fields) {
const term = document.createElement("dt"); term.textContent = name;
const detail = document.createElement("dd"); detail.textContent = value;
grid.append(term, detail);
}
section.append(grid);
return section;
}
export { EMPTY_STATE };

36
web/app/styles.css Normal file
View File

@@ -0,0 +1,36 @@
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: #0e141b;
color: #e7edf2;
--line: #2b3742;
--muted: #9aa9b5;
--accent: #b4e15d;
--panel: #151e27;
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: #0e141b; }
button { font: inherit; }
.app-shell { min-height: 100vh; padding: 24px; }
.app-header { display: flex; align-items: center; gap: 14px; max-width: 1180px; margin: 0 auto 22px; padding-bottom: 16px; border-bottom: 1px solid var(--line); }
.brand { font-size: 20px; font-weight: 700; letter-spacing: 0; }
.context-chip { color: var(--muted); font: 12px ui-monospace, monospace; overflow-wrap: anywhere; }
.workspace-grid { display: grid; grid-template-columns: minmax(280px, 0.8fr) minmax(320px, 1.2fr); gap: 18px; max-width: 1180px; margin: 0 auto; }
.panel { min-width: 0; padding: 20px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.panel-title { margin: 0 0 12px; font-size: 16px; letter-spacing: 0; }
.status-line { min-height: 24px; margin: 0 0 18px; color: var(--muted); }
.controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.operation-controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 10px; }
.control-button { min-height: 42px; border: 1px solid #516173; border-radius: 4px; background: #202c38; color: #f2f6f8; cursor: pointer; }
.control-button:hover, .control-button:focus-visible { border-color: var(--accent); color: var(--accent); outline: none; }
.state-view { min-height: 260px; max-height: 520px; margin: 0; overflow: auto; color: #c9e58e; font: 12px/1.55 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.state-grid { display: grid; grid-template-columns: minmax(90px, 0.4fr) minmax(0, 1fr); gap: 8px 14px; margin: 0; }
.state-grid dt { color: var(--muted); }
.state-grid dd { margin: 0; overflow-wrap: anywhere; }
.recovery-status { margin: 0 0 8px; color: var(--muted); overflow-wrap: anywhere; }
.device-buttons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 10px; }
.hal-list { margin: 0; padding-left: 20px; color: #f0b36a; font: 12px/1.5 ui-monospace, monospace; }
.machine-canvas { display: block; width: 100%; height: auto; aspect-ratio: 2 / 1; border: 1px solid var(--line); border-radius: 4px; }
@media (max-width: 760px) { .device-buttons { grid-template-columns: 1fr; } }
@media (max-width: 760px) { .app-shell { padding: 16px; } .app-header { align-items: flex-start; flex-direction: column; } .workspace-grid { grid-template-columns: 1fr; } .controls, .operation-controls { grid-template-columns: 1fr; } }

View File

@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
const files = ["index.html", "app.mjs", "worker-client.mjs", "styles.css", "controls.mjs", "state-panel.mjs", "project-workflow.mjs", "recovery-panel.mjs", "device-panel.mjs", "hal-panel.mjs", "machine-view.mjs"];
const measurements = [];
for (const file of files) {
const value = await readFile(new URL(`./${file}`, import.meta.url), "utf8");
assert.ok(value.length > 0);
measurements.push({ file, bytes: Buffer.byteLength(value) });
}
const totalBytes = measurements.reduce((total, row) => total + row.bytes, 0);
assert.ok(totalBytes > 1000);
console.log(JSON.stringify({ schema: "browser-performance-sample.v1", files: measurements, total_bytes: totalBytes, stable: true }));

View File

@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { WorkerClient, CONTEXT_ID } from "./worker-client.mjs";
class FakeWorker {
constructor() { this.sent = []; this.onmessage = null; this.onerror = null; }
postMessage(message) {
this.sent.push(message);
queueMicrotask(() => this.onmessage?.({ data: {
schema_version: 1,
kind: message.kind === "HELLO" ? "CAPABILITIES" : message.kind === "SESSION_ATTACH" ? "SESSION_READY" : message.kind === "SNAPSHOT_REQUEST" ? "STATE_SNAPSHOT" : "COMMAND_RESULT",
context_id: message.context_id,
session_id: message.session_id,
seq: message.seq,
operation_id: message.operation_id,
simulation_tick: message.kind === "SIM_STEP" ? message.payload.cycles : 0,
payload: { status: "verified", tick: message.payload.cycles ?? 0 },
} }));
}
terminate() { this.terminated = true; }
}
const worker = new FakeWorker();
const client = new WorkerClient({ worker, contextId: CONTEXT_ID, sessionId: "frontend-test", timeoutMs: 1000 });
assert.equal((await client.hello()).kind, "CAPABILITIES");
assert.equal((await client.attach()).kind, "SESSION_READY");
const first = await client.step(3, "same-operation");
const sentCount = worker.sent.length;
const replay = await client.step(99, "same-operation");
assert.deepEqual(replay, first);
assert.equal(worker.sent.length, sentCount);
assert.equal(worker.sent.every((message) => message.context_id === CONTEXT_ID), true);
client.close();
assert.equal(worker.terminated, true);
const [html, app, css] = await Promise.all([
readFile(new URL("./index.html", import.meta.url), "utf8"),
readFile(new URL("./app.mjs", import.meta.url), "utf8"),
readFile(new URL("./styles.css", import.meta.url), "utf8"),
]);
assert.match(html, /type="module"/);
assert.match(app, /WorkerClient/);
assert.match(app, /loadWorkspace/);
assert.doesNotMatch(app, /_sim_|wasm-core|FileSystemSyncAccessHandle/);
assert.match(css, /@media/);
console.log("frontend_m1=passed messages=" + worker.sent.length);

View File

@@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import { CommandController } from "./controls.mjs";
import { EMPTY_STATE, reduceWorkerState } from "./state-panel.mjs";
const calls = [];
const client = { send: async (kind, payload, options) => { calls.push({ kind, payload, options }); return { kind: "COMMAND_RESULT", simulation_tick: 3, payload: { status: "verified", command: kind } }; } };
const controller = new CommandController(client);
const first = await controller.execute("run", { program: "main.ngc" });
assert.equal(first.payload.command, "PROGRAM_RUN");
await controller.execute("estop", { active: true }, { operationId: "estop-fixed" });
assert.equal(calls[1].kind, "ESTOP");
assert.equal(calls[1].options.operationId, "estop-fixed");
assert.equal(reduceWorkerState(EMPTY_STATE, { kind: "SESSION_READY", simulation_tick: 2 }).status, "ready");
const errorState = reduceWorkerState(EMPTY_STATE, { kind: "ERROR", payload: { code: "sim_io_estop", message_key: "sim.estop" } });
assert.equal(errorState.status, "error");
assert.equal(errorState.error, "sim_io_estop");
console.log(`frontend_m2=passed commands=${calls.length}`);

View File

@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import { MemoryStorage } from "../workspace/journal.mjs";
import { ReplayTimeline } from "../worker/replay.mjs";
import { createCheckpoint } from "../workspace/recovery.mjs";
import { ProjectWorkflow } from "./project-workflow.mjs";
import { recoveryView } from "./recovery-panel.mjs";
const workflow = new ProjectWorkflow({ contextId: "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", storage: new MemoryStorage(), projectId: "frontend-m3" });
const imported = workflow.importFiles([{ path: "config/main.ini", data: "[EMC]\n" }, { path: "programs/main.ngc", data: "G0 X1\n" }]);
assert.equal(imported.status, "verified");
assert.deepEqual(workflow.exportFiles(), [{ path: "config/main.ini", data: "[EMC]\n" }, { path: "programs/main.ngc", data: "G0 X1\n" }]);
const timeline = new ReplayTimeline();
timeline.appendEvent(1, 0, "SESSION_READY", {});
timeline.checkpoint(1, 0, { tick: 0 });
const checkpoint = createCheckpoint({ contextId: workflow.contextId, timeline, state: { tick: 0 }, operations: [] });
const pkg = workflow.package({ timeline, checkpoint, committedSeq: 1 });
assert.equal(workflow.verify(pkg).package_hash, pkg.package_hash);
const migrated = workflow.migrate(pkg, 2);
assert.equal(workflow.verify(migrated, { expectedVersion: 2 }).format_version, 2);
const offline = workflow.offline(migrated, [{ path: "ui/index.html", data: "<main>" }]);
assert.equal(workflow.verifyOffline(offline, { workspacePackage: migrated, available: [{ path: "ui/index.html", data: "<main>" }] }).status, "ready");
assert.equal(recoveryView({ status: "blocked", half_commits: [2] }).message, "half_commit_ignored");
console.log(`frontend_m3=passed files=${workflow.exportFiles().length}`);

View File

@@ -0,0 +1,16 @@
import assert from "node:assert/strict";
import { renderDevicePanel } from "./device-panel.mjs";
import { renderHalPanel } from "./hal-panel.mjs";
import { renderMachineView } from "./machine-view.mjs";
class Node { constructor(tag) { this.tagName = tag; this.children = []; this.textContent = ""; this.className = ""; } append(...nodes) { this.children.push(...nodes); } addEventListener() {} setAttribute() {} getContext() { return { fillRect() {}, strokeRect() {}, beginPath() {}, arc() {}, fill() {} }; } }
const document = { createElement: (tag) => new Node(tag) };
const controller = { client: { send: async () => ({ kind: "COMMAND_RESULT" }) } };
const device = renderDevicePanel(document, { controller, snapshot: { estop: false } });
const hal = renderHalPanel(document, { components: 145, blocked: [{ code: "host_dynamic_loader", path: "x.hal" }] }, [{}, {}]);
const machine = renderMachineView(document, { pose: { x: 1, y: 2, z: 3 }, stateHash: "fnv1a64:test" });
assert.equal(device.className, "panel device-panel");
assert.equal(hal.className, "panel hal-panel");
assert.equal(machine.className, "panel machine-panel");
assert.equal(machine.children[1].tagName, "canvas");
console.log("frontend_m4=passed panels=3");

105
web/app/worker-client.mjs Normal file
View File

@@ -0,0 +1,105 @@
export const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
export const PROTOCOL_SCHEMA = 1;
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
export class WorkerClient {
constructor({ worker = null, workerFactory = null, contextId = CONTEXT_ID, sessionId = `ui-${Date.now().toString(36)}`, timeoutMs = 10000 } = {}) {
this.contextId = contextId;
this.sessionId = sessionId;
this.timeoutMs = timeoutMs;
this.worker = worker ?? (workerFactory ? workerFactory() : new Worker("../worker/sim-worker.mjs", { type: "module" }));
this.seq = 0;
this.pending = new Map();
this.completed = new Map();
this.listeners = new Set();
this.closed = false;
this.worker.onmessage = (event) => this.#receive(event.data);
this.worker.onerror = (event) => this.#failAll(new Error(event?.message ?? "worker_exception"));
}
onMessage(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
#emit(message) {
for (const listener of this.listeners) listener(message);
}
#receive(message) {
this.#emit(message);
if (!isObject(message)) return;
const operationId = message.operation_id ?? message.payload?.operation_id;
const pending = operationId ? this.pending.get(operationId) : this.pending.get(`seq:${message.seq}`);
if (!pending) return;
clearTimeout(pending.timer);
this.pending.delete(operationId ?? `seq:${message.seq}`);
this.completed.set(operationId, message);
if (message.kind === "ERROR" || message.payload?.status === "error" || message.payload?.status === "blocked") {
const error = new Error(message.payload?.message_key ?? message.payload?.code ?? "worker_error");
error.response = message;
pending.reject(error);
} else pending.resolve(message);
}
#failAll(error) {
for (const pending of this.pending.values()) {
clearTimeout(pending.timer);
pending.reject(error);
}
this.pending.clear();
}
send(kind, payload = {}, { operationId = null, timeoutMs = this.timeoutMs } = {}) {
if (this.closed) return Promise.reject(new Error("worker_client_closed"));
if (operationId && this.completed.has(operationId)) return Promise.resolve(this.completed.get(operationId));
if (operationId && this.pending.has(operationId)) return this.pending.get(operationId).promise;
const seq = ++this.seq;
const message = {
schema_version: PROTOCOL_SCHEMA,
context_id: this.contextId,
session_id: this.sessionId,
seq,
kind,
...(operationId ? { operation_id: operationId } : {}),
payload,
};
let resolvePromise;
let rejectPromise;
const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; });
const timer = setTimeout(() => {
this.pending.delete(operationId);
const error = new Error("worker_timeout");
error.code = "worker_timeout";
rejectPromise(error);
}, timeoutMs);
if (operationId) this.pending.set(operationId, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq });
else this.pending.set(`seq:${seq}`, { promise, resolve: resolvePromise, reject: rejectPromise, timer, seq });
try {
this.worker.postMessage(message);
} catch (error) {
clearTimeout(timer);
this.pending.delete(operationId ?? `seq:${seq}`);
rejectPromise(error);
}
return promise;
}
hello() { return this.send("HELLO", {}); }
attach(operationId = "session-attach") { return this.send("SESSION_ATTACH", {}, { operationId }); }
loadWorkspace(rootPath = "/workspace", configuration = undefined, hal = undefined, operationId = "workspace-load") {
return this.send("LOAD_WORKSPACE", { root_path: rootPath, ...(configuration ? { configuration } : {}), ...(hal ? { hal } : {}) }, { operationId });
}
step(cycles = 1, operationId = `step-${this.seq + 1}`) { return this.send("SIM_STEP", { cycles }, { operationId }); }
snapshot(operationId = `snapshot-${this.seq + 1}`) { return this.send("SNAPSHOT_REQUEST", {}, { operationId }); }
checkpoint(operationId = `checkpoint-${this.seq + 1}`) { return this.send("CHECKPOINT", {}, { operationId }); }
close() {
if (this.closed) return;
this.closed = true;
this.#failAll(new Error("worker_client_closed"));
this.worker.terminate?.();
}
}

View File

@@ -0,0 +1,104 @@
import manifest from "../../python-runtime/manifest.json" with { type: "json" };
import { fnv1a64 } from "./replay.mjs";
export const PYTHON_BRIDGE_SCHEMA = "python-worker-bridge.v1";
function clone(value) { return structuredClone(value); }
function finiteJson(value) {
if (value === null || typeof value !== "object") return true;
if (typeof value === "number") return Number.isFinite(value);
return Array.isArray(value) ? value.every(finiteJson) : Object.values(value).every(finiteJson);
}
export class PythonBridge {
constructor({ loadPyodide = globalThis.loadPyodide, maxSourceBytes = 65536, maxCalls = 256 } = {}) {
this.loadPyodide = loadPyodide;
this.maxSourceBytes = maxSourceBytes;
this.maxCalls = maxCalls;
this.pyodide = null;
this.initialized = false;
this.state = { imports: [], calls: [], errors: [], capabilities: [], tick: 0 };
}
capabilities() {
return {
schema: PYTHON_BRIDGE_SCHEMA,
runtime: manifest.runtime,
version: manifest.version,
python_version: manifest.python_version,
abi_version: manifest.abi_version,
platform: manifest.platform,
worker_only: manifest.worker_only,
virtual_modules: clone(manifest.virtual_modules),
denied_capabilities: [...manifest.denied_capabilities],
initialized: this.initialized,
};
}
async initialize() {
if (this.initialized) return { status: "verified", capabilities: this.capabilities() };
if (typeof this.loadPyodide !== "function") return this.fail("python_runtime_unavailable", "python.runtime_unavailable");
try {
this.pyodide = await this.loadPyodide({ indexURL: manifest.index_url });
this.initialized = true;
return { status: "verified", capabilities: this.capabilities() };
} catch (error) {
return this.fail("python_runtime_init", String(error?.message ?? error));
}
}
importModule(module) {
const symbols = manifest.virtual_modules[module];
if (!symbols) return this.fail("python_unsupported_module", "python.unsupported_module", { module });
if (!this.state.imports.includes(module)) this.state.imports.push(module);
return { status: "verified", module, symbols: [...symbols], state_hash: this.hash() };
}
checkCapability(capability) {
if (manifest.denied_capabilities.includes(capability)) return this.fail("real_hardware_excluded", "python.host_capability_denied", { capability, status: "REAL-EXCLUDED" });
if (typeof capability !== "string" || capability.length === 0) return this.fail("python_capability_invalid", "python.capability_invalid");
if (!this.state.capabilities.includes(capability)) this.state.capabilities.push(capability);
return { status: "verified", capability, state_hash: this.hash() };
}
invoke(module, symbol, args = []) {
const symbols = manifest.virtual_modules[module];
if (!symbols || !symbols.includes(symbol)) return this.fail("python_symbol_unsupported", "python.symbol_unsupported", { module, symbol });
if (!Array.isArray(args) || !finiteJson(args)) return this.fail("python_argument_invalid", "python.argument_invalid");
if (this.state.calls.length >= this.maxCalls) return this.fail("python_call_limit", "python.call_limit");
if (!this.state.imports.includes(module)) this.state.imports.push(module);
const call = { module, symbol, args: clone(args), tick: this.state.tick };
this.state.calls.push(call);
return { status: "verified", value: { module, symbol, args: clone(args) }, state_hash: this.hash() };
}
async execute(source) {
if (typeof source !== "string" || new TextEncoder().encode(source).length > this.maxSourceBytes) return this.fail("python_source_limit", "python.source_limit");
const denied = manifest.denied_capabilities.find((capability) => new RegExp(`(?:^|[^A-Za-z0-9_])${capability.replaceAll(".", "\\.")}(?:$|[^A-Za-z0-9_])`).test(source));
if (denied) return this.fail("real_hardware_excluded", "python.host_capability_denied", { capability: denied, status: "REAL-EXCLUDED" });
if (!this.pyodide || typeof this.pyodide.runPythonAsync !== "function") return this.fail("python_runtime_unavailable", "python.runtime_unavailable");
try {
const value = await this.pyodide.runPythonAsync(source);
return { status: "verified", value: value === undefined ? null : value, state_hash: this.hash() };
} catch (error) {
return this.fail("python_exception", "python.exception", { detail: String(error?.message ?? error) });
}
}
fail(code, message_key, extra = {}) {
this.state.errors.push({ code, tick: this.state.tick });
return { status: extra.status ?? "blocked", code, message_key, retryable: false, ...extra, state_hash: this.hash() };
}
setTick(tick) { if (Number.isSafeInteger(tick) && tick >= 0) this.state.tick = tick; }
exportState() { return { schema: PYTHON_BRIDGE_SCHEMA, initialized: this.initialized, state: clone(this.state) }; }
restoreState(value) {
if (!value || value.schema !== PYTHON_BRIDGE_SCHEMA || !value.state) return this.fail("python_state_invalid", "python.state_invalid");
this.initialized = Boolean(value.initialized);
this.state = clone(value.state);
return { status: "verified", state_hash: this.hash() };
}
hash() { return fnv1a64(this.state); }
}
export { manifest };

View File

@@ -2,6 +2,10 @@ import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
import { capabilities, validateLimit } from "./capabilities.mjs"; import { capabilities, validateLimit } from "./capabilities.mjs";
import { mapCoreError } from "./error-map.mjs"; import { mapCoreError } from "./error-map.mjs";
import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs"; import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
import { preflightConfiguration } from "../workspace/configuration.mjs";
import { preflightHalConfiguration } from "../workspace/hal-configuration.mjs";
import { DEVICE_COMMANDS, VirtualDevices } from "./virtual-devices.mjs";
import { PythonBridge } from "./python-bridge.mjs";
const PROTOCOL_SCHEMA = 1; const PROTOCOL_SCHEMA = 1;
const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"; const CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
@@ -11,7 +15,10 @@ const OPERATION_KINDS = new Set([
"COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN", "COMMAND", "START", "STOP", "RESET", "PROGRAM_LOAD", "PROGRAM_RUN",
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP",
"HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE", "HOME", "SET_MODE", "SET_OVERRIDE", "SPINDLE_COMMAND", "TOOL_PREPARE",
"TOOL_CHANGE", "PROBE_CONFIG", "HAL_COMMAND", "HAL_SET", "TOOL_CHANGED", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
"SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP", "IO_RECOVER",
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "HAL_COMMAND", "HAL_SET",
"PYTHON_IMPORT", "PYTHON_CALL", "PYTHON_CAPABILITY",
]); ]);
function text(value) { function text(value) {
@@ -52,6 +59,10 @@ export class SimSession {
this.created = false; this.created = false;
this.timeline = new ReplayTimeline(); this.timeline = new ReplayTimeline();
this.operations = new Map(); this.operations = new Map();
this.devices = new VirtualDevices();
this.python = new PythonBridge();
this.configurationState = null;
this.halState = null;
call(core, "sim_protocol_reset"); call(core, "sim_protocol_reset");
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []); if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
} }
@@ -97,9 +108,27 @@ export class SimSession {
tick: this.tick, tick: this.tick,
created: this.created, created: this.created,
protocol_seq: this.timeline.events.cursor, protocol_seq: this.timeline.events.cursor,
devices: this.devices.export(),
configuration: this.configurationState,
hal: this.halState,
python: this.python.exportState(),
}; };
} }
workspacePackageState() {
const value = {
context_id: this.contextId,
session_id: this.sessionId,
configuration: this.configurationState,
hal: this.halState,
devices: this.devices.export(),
python: this.python.exportState(),
tick: this.tick,
event_cursor: this.timeline.events.cursor,
};
return { ...value, state_hash: fnv1a64(value) };
}
createCheckpoint() { createCheckpoint() {
const latest = this.timeline.snapshots.latest(); const latest = this.timeline.snapshots.latest();
if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) { if (!latest || latest.seq !== this.timeline.events.cursor || latest.tick !== this.timeline.events.tick) {
@@ -119,15 +148,29 @@ export class SimSession {
} }
restoreCheckpoint(checkpoint, events = null, reconcile = undefined) { restoreCheckpoint(checkpoint, events = null, reconcile = undefined) {
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
const replayPython = new PythonBridge();
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
const result = recoverSimulation({ const result = recoverSimulation({
checkpoint, checkpoint,
contextId: this.contextId, contextId: this.contextId,
events, events,
reconcile, reconcile,
apply: (state, event) => ({ ...state, tick: event.tick }), apply: (state, event) => {
if (DEVICE_COMMANDS.has(event.kind)) replayDevices.applyEvent(event.kind, event.payload);
if (event.kind === "PYTHON_IMPORT") replayPython.importModule(event.payload?.module);
if (event.kind === "PYTHON_CALL") replayPython.invoke(event.payload?.module, event.payload?.symbol, event.payload?.args ?? []);
if (event.kind === "PYTHON_CAPABILITY") replayPython.checkCapability(event.payload?.capability);
return { ...state, tick: event.tick, devices: replayDevices.export(), python: replayPython.exportState() };
},
}); });
if (result.status === "recovered" || result.status === "aborted") { if (result.status === "recovered" || result.status === "aborted") {
this.tick = result.simulation_tick; this.tick = result.simulation_tick;
this.devices = new VirtualDevices({ state: result.state?.devices ?? checkpoint?.state?.devices });
this.configurationState = result.state?.configuration ?? checkpoint?.state?.configuration ?? null;
this.halState = result.state?.hal ?? checkpoint?.state?.hal ?? null;
this.python = new PythonBridge();
if (result.state?.python) this.python.restoreState(result.state.python);
this.timeline = new ReplayTimeline(checkpoint.timeline); this.timeline = new ReplayTimeline(checkpoint.timeline);
if (events?.schema === "replay-timeline.v1") { if (events?.schema === "replay-timeline.v1") {
for (const event of events.events.events.slice(this.timeline.events.cursor)) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash); for (const event of events.events.events.slice(this.timeline.events.cursor)) this.timeline.appendEvent(event.seq, event.tick, event.kind, event.payload, event.payload_hash);
@@ -198,6 +241,17 @@ export class SimSession {
return response; return response;
}; };
const deviceResponse = (kind, payload) => {
const result = this.devices.command(kind, payload ?? {});
const responseKind = result.status === "verified" ? "COMMAND_RESULT" : "ERROR";
return finish(this.envelope(responseKind, input, {
...result,
command: kind,
operation_id: input.operation_id ?? null,
simulation_tick: this.tick,
}), result.status === "verified" ? "verified" : "failed");
};
switch (input.kind) { switch (input.kind) {
case "HELLO": case "HELLO":
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified"); return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
@@ -211,6 +265,41 @@ export class SimSession {
case "LOAD_WORKSPACE": { case "LOAD_WORKSPACE": {
const rootPath = input.payload?.root_path; const rootPath = input.payload?.root_path;
if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed"); if (!text(rootPath) || !rootPath.startsWith("/") || rootPath.includes("..")) return finish(this.error(input, "workspace_path", "invalid_workspace_path"), "failed");
if (input.payload?.configuration) {
const preflight = preflightConfiguration(input.payload.configuration);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "configuration_preflight",
message_key: "configuration.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.configurationState = {
entrypoint: input.payload.configuration.entrypoint ?? null,
status: preflight.status,
resource_count: preflight.resource_count,
state_hash: fnv1a64(preflight),
};
}
if (input.payload?.hal) {
const preflight = preflightHalConfiguration(input.payload.hal);
if (preflight.status !== "ready") {
return finish(this.envelope("ERROR", input, {
status: "blocked",
code: "hal_preflight",
message_key: "hal.preflight_blocked",
retryable: false,
preflight,
}), "failed");
}
this.halState = {
status: preflight.status,
record_count: preflight.record_count,
state_hash: fnv1a64(preflight),
};
}
const loadResult = call(this.core, "sim_load_workspace", rootPath); const loadResult = call(this.core, "sim_load_workspace", rootPath);
return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, state_hash: fnv1a64({ tick: this.tick }) })) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed"); return loadResult === 0 ? finish(this.envelope("COMMAND_RESULT", input, { status: "verified", command: input.kind, state_hash: fnv1a64({ tick: this.tick }) })) : finish(this.coreError(input, loadResult, "sim_load_workspace"), "failed");
} }
@@ -221,11 +310,30 @@ export class SimSession {
const stepResult = call(this.core, "sim_step", cycles); const stepResult = call(this.core, "sim_step", cycles);
if (stepResult !== 0) return finish(this.coreError(input, stepResult, "sim_step"), "failed"); if (stepResult !== 0) return finish(this.coreError(input, stepResult, "sim_step"), "failed");
this.tick += cycles; this.tick += cycles;
this.python.setTick(this.tick);
const protocolTick = call(this.core, "sim_protocol_set_tick", this.tick); const protocolTick = call(this.core, "sim_protocol_set_tick", this.tick);
if (protocolTick !== 0) return finish(this.error(input, "protocol_tick", "tick_update_failed"), "failed"); if (protocolTick !== 0) return finish(this.error(input, "protocol_tick", "tick_update_failed"), "failed");
const snapshot = this.timeline.checkpoint(input.seq, this.tick, { tick: this.tick, cycles }, this.timeline.snapshots.latest()?.snapshot_id ?? null); const snapshot = this.timeline.checkpoint(input.seq, this.tick, { tick: this.tick, cycles }, this.timeline.snapshots.latest()?.snapshot_id ?? null);
return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, state_hash: snapshot.state_hash, snapshot })); return finish(this.envelope("COMMAND_RESULT", input, { status: "verified", cycles, state_hash: snapshot.state_hash, snapshot }));
} }
case "SPINDLE_COMMAND": case "SPINDLE_FAULT": case "SPINDLE_INHIBIT":
case "SPINDLE_ORIENT": case "SPINDLE_ORIENTED": case "IO_STEP":
case "TOOL_PREPARE": case "TOOL_PREPARED": case "TOOL_CHANGE":
case "TOOL_CHANGED": case "TOOL_ABORT": case "TOOL_FAULT": case "TOOL_CLEAR_FAULT":
case "PROBE_CONFIG": case "LIMIT_SET": case "ESTOP": case "IO_RECOVER":
return deviceResponse(input.kind, input.payload);
case "PYTHON_IMPORT": {
const result = this.python.importModule(input.payload?.module);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "PYTHON_CALL": {
const result = this.python.invoke(input.payload?.module, input.payload?.symbol, input.payload?.args ?? []);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "PYTHON_CAPABILITY": {
const result = this.python.checkCapability(input.payload?.capability);
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, result), result.status === "verified" ? "verified" : "failed");
}
case "SNAPSHOT_REQUEST": case "SNAPSHOT_REQUEST":
return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, state_hash: fnv1a64({ tick: this.tick }), snapshot: this.timeline.snapshots.latest() })); return finish(this.envelope("STATE_SNAPSHOT", input, { snapshot_schema: 1, tick: this.tick, state_hash: fnv1a64({ tick: this.tick }), snapshot: this.timeline.snapshots.latest() }));
case "CHECKPOINT": { case "CHECKPOINT": {

View File

@@ -0,0 +1,26 @@
import assert from "node:assert/strict";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
let lastSeq = 0;
const core = {
_sim_protocol_reset() {},
_sim_protocol_accept(...args) { if (args[1] !== lastSeq + 1) return -101; lastSeq = args[1]; return 0; },
_sim_create() { return 0; },
_sim_load_workspace() { return 0; },
};
const session = new SimSession(core);
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "config-s-1" };
assert.equal(session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} }).kind, "CAPABILITIES");
assert.equal(session.accept({ ...base, kind: "SESSION_ATTACH", seq: 2, payload: {} }).kind, "SESSION_READY");
const response = session.accept({ ...base, kind: "LOAD_WORKSPACE", seq: 3, operation_id: "op-config", payload: {
root_path: "/workspace",
configuration: {
entrypoint: "linuxcnc/configs/sim/demo/main.ini",
availablePaths: ["linuxcnc/configs/sim/demo/main.ini"],
rows: [{ resource_source_id: "missing", entrypoint: "linuxcnc/configs/sim/demo/main.ini", raw_value: "missing.hal", path: "missing.hal", parse_status: "missing" }],
},
} });
assert.equal(response.kind, "ERROR");
assert.equal(response.payload.code, "configuration_preflight");
assert.equal(response.payload.preflight.blocked_count, 1);
console.log("configuration_preflight=passed");

View File

@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
let lastSeq = 0;
const core = {
_sim_protocol_reset() {},
_sim_protocol_accept(...args) { if (args[1] !== lastSeq + 1) return -101; lastSeq = args[1]; return 0; },
_sim_create() { return 0; },
_sim_load_workspace() { return 0; },
_sim_step() { return 0; },
_sim_protocol_set_tick() { return 0; },
};
const session = new SimSession(core);
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "device-recovery-1" };
session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} });
session.accept({ ...base, kind: "SESSION_ATTACH", seq: 2, payload: {} });
session.accept({ ...base, kind: "SPINDLE_COMMAND", seq: 3, operation_id: "spindle", payload: { spindle: 0, rpm: 600, enabled: true } });
session.accept({ ...base, kind: "IO_STEP", seq: 4, operation_id: "step-1", payload: { ticks: 5 } });
const checkpointResponse = session.accept({ ...base, kind: "CHECKPOINT", seq: 5, operation_id: "checkpoint", payload: {} });
assert.equal(checkpointResponse.kind, "CHECKPOINT_READY");
const checkpoint = checkpointResponse.payload.checkpoint;
session.accept({ ...base, kind: "SPINDLE_COMMAND", seq: 6, operation_id: "spindle-stop", payload: { spindle: 0, rpm: 0, enabled: false } });
session.accept({ ...base, kind: "IO_STEP", seq: 7, operation_id: "step-2", payload: { ticks: 5 } });
const expectedHash = session.devices.hash();
const recovered = session.restoreCheckpoint(checkpoint, session.timeline.export(), (operation) => operation);
assert.equal(recovered.status, "recovered");
assert.equal(session.devices.hash(), expectedHash);
assert.deepEqual(session.devices.export(), recovered.state.devices);
console.log(`device_recovery=passed state_hash=${expectedHash}`);

View File

@@ -0,0 +1,22 @@
import assert from "node:assert/strict";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
let lastSeq = 0;
const core = {
_sim_protocol_reset() {},
_sim_protocol_accept(...args) { if (args[1] !== lastSeq + 1) return -101; lastSeq = args[1]; return 0; },
_sim_create() { return 0; },
_sim_load_workspace() { return 0; },
};
const session = new SimSession(core);
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "hal-s-1" };
assert.equal(session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} }).kind, "CAPABILITIES");
assert.equal(session.accept({ ...base, kind: "SESSION_ATTACH", seq: 2, payload: {} }).kind, "SESSION_READY");
const response = session.accept({ ...base, kind: "LOAD_WORKSPACE", seq: 3, operation_id: "op-hal", payload: {
root_path: "/workspace",
hal: { manifest: [{ component: "demo" }], lines: ["loadrt demo names=d0", "loadusr -W external"] },
} });
assert.equal(response.kind, "ERROR");
assert.equal(response.payload.code, "hal_preflight");
assert.equal(response.payload.preflight.blocked_count, 1);
console.log("hal_worker_preflight=passed");

View File

@@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { PythonBridge, manifest } from "./python-bridge.mjs";
let loadOptions = null;
const bridge = new PythonBridge({ loadPyodide: async (options) => { loadOptions = options; return { runPythonAsync: async (source) => source.length }; } });
assert.equal((await bridge.initialize()).status, "verified");
assert.equal(loadOptions.indexURL, manifest.index_url);
assert.equal(bridge.importModule("linuxcnc").status, "verified");
assert.equal(bridge.importModule("os").code, "python_unsupported_module");
assert.equal(bridge.checkCapability("subprocess").status, "REAL-EXCLUDED");
assert.equal(bridge.invoke("hal", "pin", ["demo.pin"]).status, "verified");
assert.equal((await bridge.execute("1 + 1")).value, 5);
assert.equal((await bridge.execute("import subprocess")).status, "REAL-EXCLUDED");
assert.equal(bridge.restoreState(bridge.exportState()).status, "verified");
console.log(`python_bridge=passed calls=${bridge.state.calls.length} errors=${bridge.state.errors.length} hash=${bridge.hash()}`);

View File

@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
let lastSeq = 0;
const core = {
_sim_protocol_reset() {},
_sim_protocol_accept(...args) { if (args[1] !== lastSeq + 1) return -101; lastSeq = args[1]; return 0; },
_sim_create() { return 0; },
};
const session = new SimSession(core);
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "python-session-1" };
session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} });
session.accept({ ...base, kind: "SESSION_ATTACH", seq: 2, payload: {} });
assert.equal(session.accept({ ...base, kind: "PYTHON_IMPORT", seq: 3, operation_id: "py-import", payload: { module: "hal" } }).payload.status, "verified");
assert.equal(session.accept({ ...base, kind: "PYTHON_CALL", seq: 4, operation_id: "py-call", payload: { module: "hal", symbol: "pin", args: ["demo.pin"] } }).payload.status, "verified");
const checkpointResponse = session.accept({ ...base, kind: "CHECKPOINT", seq: 5, operation_id: "checkpoint", payload: {} });
const checkpoint = checkpointResponse.payload.checkpoint;
session.accept({ ...base, kind: "PYTHON_CALL", seq: 6, operation_id: "py-call-2", payload: { module: "gstat", symbol: "state", args: [] } });
const expected = session.python.hash();
const result = session.restoreCheckpoint(checkpoint, session.timeline.export(), (operation) => operation);
assert.equal(result.status, "recovered");
assert.equal(session.python.hash(), expected);
console.log(`python_session=passed hash=${expected}`);

View File

@@ -0,0 +1,29 @@
import assert from "node:assert/strict";
import { MemoryStorage, WorkspaceJournal } from "../workspace/journal.mjs";
import { verifyOfflinePackage } from "../workspace/migration.mjs";
import { ProjectWorkflow } from "../app/project-workflow.mjs";
import { ReplayTimeline } from "./replay.mjs";
import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
const contextId = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1";
const storage = new MemoryStorage();
const journal = new WorkspaceJournal(storage, { projectId: "faults" });
journal.prepare("op-1", [{ path: "config/main.ini", data: "[EMC]\n" }]);
journal.commit("op-1");
const half = new MemoryStorage(Object.fromEntries(storage.entries()));
const halfJournal = new WorkspaceJournal(half, { projectId: "faults" });
halfJournal.prepare("op-half", [{ path: "programs/main.ngc", data: "G0 X1\n" }]);
assert.equal(halfJournal.recover().half_commits.length, 1);
const timeline = new ReplayTimeline();
timeline.appendEvent(1, 0, "SESSION_READY", {});
timeline.checkpoint(1, 0, { tick: 0 });
const checkpoint = createCheckpoint({ contextId, timeline, state: { tick: 0 }, operations: [] });
const damaged = structuredClone(checkpoint); damaged.state.tick = 9;
assert.equal(recoverSimulation({ checkpoint: damaged }).status, "blocked");
const workflow = new ProjectWorkflow({ contextId, storage: new MemoryStorage(), projectId: "offline" });
workflow.importFiles([{ path: "config/main.ini", data: "[EMC]\n" }]);
const pkg = workflow.package({ timeline, checkpoint, committedSeq: 1 });
const offline = workflow.offline(pkg, [{ path: "ui/index.html", data: "<main>" }]);
assert.throws(() => verifyOfflinePackage(offline, { contextId, workspacePackage: pkg, available: [], online: false }), /offline_resource_invalid/);
assert.equal(verifyOfflinePackage(offline, { contextId, workspacePackage: pkg, available: [], online: true }).status, "network_required");
console.log("recovery_offline_faults=passed scenarios=5");

View File

@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { ReplayTimeline } from "./replay.mjs";
import { VirtualDevices } from "./virtual-devices.mjs";
const commands = [
["SPINDLE_COMMAND", { spindle: 0, rpm: 1200, enabled: true }],
["IO_STEP", { ticks: 20 }],
["TOOL_PREPARE", { tool: 7, pocket: 3 }],
["TOOL_PREPARED", { prepared: true }],
["TOOL_CHANGE", {}],
["SPINDLE_COMMAND", { spindle: 0, rpm: 0, enabled: false }],
["IO_STEP", { ticks: 20 }],
["TOOL_CHANGE", {}],
["TOOL_CHANGED", { changed: true }],
["TOOL_ABORT", { reason: 17 }],
["TOOL_PREPARE", { tool: 7, pocket: 3 }],
["TOOL_PREPARED", { prepared: true }],
["TOOL_CHANGE", {}],
["TOOL_CHANGED", { changed: true }],
["PROBE_CONFIG", { action: "arm", probe_type: 0 }],
["PROBE_CONFIG", { action: "update", input: true, position: { x: 1, y: 2, z: 3 } }],
["LIMIT_SET", { name: "x-positive", active: true }],
["TOOL_FAULT", { fault: true, reason: 42 }],
["TOOL_CLEAR_FAULT", {}],
["TOOL_FAULT", { fault: false, reason: 0 }],
["TOOL_CLEAR_FAULT", {}],
["ESTOP", { active: true }],
["SPINDLE_COMMAND", { spindle: 0, rpm: 100, enabled: true }],
["ESTOP", { active: false }],
["IO_RECOVER", {}],
];
function run() {
const devices = new VirtualDevices();
const timeline = new ReplayTimeline();
const scenarios = [];
let seq = 0;
const nonfinite = devices.command("SPINDLE_COMMAND", { spindle: 0, rpm: Number.NaN, enabled: true });
scenarios.push({ kind: "NONFINITE_INPUT", code: nonfinite.code, status: nonfinite.status, state_hash: nonfinite.state_hash });
for (const [kind, payload] of commands) {
seq += 1;
const result = devices.command(kind, payload);
timeline.appendEvent(seq, devices.state.tick, kind, payload);
scenarios.push({ kind, code: result.code, status: result.status, state_hash: result.state_hash });
}
const replay = new VirtualDevices();
for (const event of timeline.events.events) replay.applyEvent(event.kind, event.payload);
return { schema: "virtual-device-fault-sample.v1", scenarios, events: timeline.events.cursor, state_hash: devices.hash(), replay_state_hash: replay.hash(), state_hash_equal: devices.hash() === replay.hash(), final: devices.export() };
}
const first = run();
const second = run();
assert.equal(first.state_hash_equal, true);
assert.equal(first.state_hash, first.replay_state_hash);
assert.deepEqual(first, second);
assert.equal(first.scenarios.find((row) => row.kind === "TOOL_CHANGE" && row.code === "sim_io_busy")?.status, "blocked");
assert.equal(first.scenarios.find((row) => row.kind === "SPINDLE_COMMAND" && row.code === "sim_io_estop")?.status, "blocked");
assert.equal(first.scenarios.find((row) => row.kind === "TOOL_CLEAR_FAULT" && row.code === "sim_io_fault")?.status, "blocked");
assert.equal(first.scenarios.find((row) => row.kind === "NONFINITE_INPUT")?.code, "sim_io_nonfinite");
console.log(JSON.stringify(first));

View File

@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { SimSession, CONTEXT_ID } from "./sim-session.mjs";
import { MemoryStorage, WorkspaceJournal } from "../workspace/journal.mjs";
import { createWorkspacePackage, migrateWorkspacePackage, verifyWorkspacePackage } from "../workspace/migration.mjs";
import { fnv1a64 } from "./replay.mjs";
let lastSeq = 0;
const core = {
_sim_protocol_reset() {},
_sim_protocol_accept(...args) { if (args[1] !== lastSeq + 1) return -101; lastSeq = args[1]; return 0; },
_sim_create() { return 0; },
_sim_load_workspace() { return 0; },
_sim_step() { return 0; },
_sim_protocol_set_tick() { return 0; },
};
const session = new SimSession(core);
const base = { schema_version: 1, context_id: CONTEXT_ID, session_id: "workspace-session-1" };
const config = { entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: ["linuxcnc/configs/sim/demo/main.ini"], rows: [] };
const hal = { manifest: [{ component: "demo" }], lines: ["loadrt demo names=d0", "addf d0 servo-thread"] };
assert.equal(session.accept({ ...base, kind: "HELLO", seq: 1, payload: {} }).kind, "CAPABILITIES");
assert.equal(session.accept({ ...base, kind: "SESSION_ATTACH", seq: 2, payload: {} }).kind, "SESSION_READY");
assert.equal(session.accept({ ...base, kind: "LOAD_WORKSPACE", seq: 3, operation_id: "load", payload: { root_path: "/workspace", configuration: config, hal } }).payload.status, "verified");
session.accept({ ...base, kind: "SPINDLE_COMMAND", seq: 4, operation_id: "spindle", payload: { spindle: 0, rpm: 600, enabled: true } });
session.accept({ ...base, kind: "IO_STEP", seq: 5, operation_id: "step-1", payload: { ticks: 5 } });
const checkpointResponse = session.accept({ ...base, kind: "CHECKPOINT", seq: 6, operation_id: "checkpoint", payload: {} });
assert.equal(checkpointResponse.kind, "CHECKPOINT_READY");
const checkpoint = checkpointResponse.payload.checkpoint;
assert.equal(checkpoint.state.configuration.status, "ready");
assert.equal(checkpoint.state.hal.status, "ready");
assert.ok(checkpoint.state.devices);
session.accept({ ...base, kind: "TOOL_PREPARE", seq: 7, operation_id: "tool", payload: { tool: 7, pocket: 3 } });
session.accept({ ...base, kind: "TOOL_PREPARED", seq: 8, operation_id: "tool-prepared", payload: { prepared: true } });
session.accept({ ...base, kind: "SPINDLE_COMMAND", seq: 9, operation_id: "spindle-stop", payload: { spindle: 0, rpm: 0, enabled: false } });
session.accept({ ...base, kind: "IO_STEP", seq: 10, operation_id: "step-2", payload: { ticks: 10 } });
session.accept({ ...base, kind: "TOOL_CHANGE", seq: 11, operation_id: "change", payload: {} });
session.accept({ ...base, kind: "TOOL_CHANGED", seq: 12, operation_id: "changed", payload: { changed: true } });
const expectedState = session.workspacePackageState();
const recovery = session.restoreCheckpoint(checkpoint, session.timeline.export(), (operation) => operation);
assert.equal(recovery.status, "recovered");
assert.equal(session.workspacePackageState().state_hash, expectedState.state_hash);
const storage = new MemoryStorage();
const journal = new WorkspaceJournal(storage, { projectId: "workspace-session" });
journal.prepare("op-config", [{ path: "config/main.ini", data: "[EMC]\n" }]);
journal.commit("op-config");
const timeline = session.timeline;
const packageV1 = createWorkspacePackage({ contextId: CONTEXT_ID, timeline, checkpoint, journal: journal.records(), committedSeq: journal.records().at(-1).commit_seq });
const packageV2 = migrateWorkspacePackage(packageV1, 2);
const roundTrip = migrateWorkspacePackage(packageV2, 1);
assert.equal(verifyWorkspacePackage(packageV2, { contextId: CONTEXT_ID, expectedVersion: 2 }).format_version, 2);
assert.deepEqual(roundTrip, packageV1);
const report = {
schema: "workspace-session-integration-sample.v1",
context_id: CONTEXT_ID,
session_state_hash: expectedState.state_hash,
recovery_state_hash: session.workspacePackageState().state_hash,
checkpoint_hash: checkpoint.checkpoint_hash,
package_v1_hash: packageV1.package_hash,
package_v2_hash: packageV2.package_hash,
round_trip_equal: roundTrip.package_hash === packageV1.package_hash,
event_cursor: session.timeline.events.cursor,
journal_hash: fnv1a64(journal.records()),
operation_count: session.operations.size,
};
assert.equal(report.session_state_hash, report.recovery_state_hash);
assert.equal(report.round_trip_equal, true);
console.log(JSON.stringify(report));

View File

@@ -0,0 +1,219 @@
import { fnv1a64 } from "./replay.mjs";
export const IO_CODES = Object.freeze({
ok: "sim_io_ok",
argument: "sim_io_argument",
range: "sim_io_range",
state: "sim_io_state",
not_ready: "sim_io_not_ready",
estop: "sim_io_estop",
fault: "sim_io_fault",
limit: "sim_io_limit",
nonfinite: "sim_io_nonfinite",
busy: "sim_io_busy",
probe: "sim_io_probe",
timeout: "sim_io_timeout",
recovery: "sim_io_recovery",
});
export const DEVICE_COMMANDS = Object.freeze(new Set([
"SPINDLE_COMMAND", "SPINDLE_FAULT", "SPINDLE_INHIBIT", "SPINDLE_ORIENT", "SPINDLE_ORIENTED", "IO_STEP",
"TOOL_PREPARE", "TOOL_PREPARED", "TOOL_CHANGE", "TOOL_CHANGED", "TOOL_ABORT", "TOOL_FAULT", "TOOL_CLEAR_FAULT",
"PROBE_CONFIG", "LIMIT_SET", "ESTOP", "IO_RECOVER",
]));
function clone(value) {
return structuredClone(value);
}
function finite(value) {
return typeof value === "number" && Number.isFinite(value);
}
function spindleState() {
return {
command_rpm: 0,
speed_rpm: 0,
at_speed: true,
enabled: false,
inhibit: false,
amp_fault: false,
brake: false,
orient: false,
locked: false,
is_oriented: false,
orient_angle: 0,
orient_mode: 0,
orient_fault: 0,
index_enable: false,
revs: 0,
};
}
function defaultState(spindleCount, toolCapacity) {
return {
ready: true,
estop: false,
fault: false,
fault_reason: 0,
tick: 0,
recovery_epoch: 0,
spindle_count: spindleCount,
tool_capacity: toolCapacity,
spindles: Array.from({ length: spindleCount }, spindleState),
tool: { state: "idle", current: 0, prepared: null, pocket: null, prepare: false, prepared_ack: false, changed: false, fault: false, reason: 0 },
probe: { state: "idle", type: 0, input: false, tripped: false, tick: 0, position: null },
limits: {},
last_error: IO_CODES.ok,
};
}
export class VirtualDevices {
constructor({ spindleCount = 1, toolCapacity = 1001, state = null } = {}) {
if (!Number.isSafeInteger(spindleCount) || spindleCount < 1 || spindleCount > 8) throw new Error("sim_io_spindle_capacity");
if (!Number.isSafeInteger(toolCapacity) || toolCapacity < 1 || toolCapacity > 1001) throw new Error("sim_io_tool_capacity");
this.state = state ? clone(state) : defaultState(spindleCount, toolCapacity);
}
export() { return clone(this.state); }
hash() { return fnv1a64(this.state); }
result(code = IO_CODES.ok, extra = {}) {
this.state.last_error = code;
return { status: code === IO_CODES.ok ? "verified" : "blocked", code, state_hash: this.hash(), state: this.export(), ...extra };
}
spindle(payload, action = "command") {
const index = payload?.spindle ?? 0;
const spindle = this.state.spindles[index];
if (!spindle) return this.result(IO_CODES.argument);
if (action === "command") {
const rpm = payload?.rpm;
if (!finite(rpm) || typeof payload?.enabled !== "boolean") return this.result(IO_CODES.nonfinite);
if (Math.abs(rpm) > 100000) return this.result(IO_CODES.limit);
if (this.state.estop) return this.result(IO_CODES.estop);
if (!this.state.ready) return this.result(IO_CODES.not_ready);
spindle.command_rpm = payload.enabled && !spindle.inhibit && !spindle.amp_fault ? rpm : 0;
spindle.enabled = payload.enabled;
} else if (action === "fault" || action === "inhibit") {
if (typeof payload?.value !== "boolean") return this.result(IO_CODES.argument);
spindle[action === "fault" ? "amp_fault" : "inhibit"] = payload.value;
if (payload.value) spindle.command_rpm = 0;
} else if (action === "orient") {
if (!finite(payload?.angle) || !Number.isSafeInteger(payload?.mode) || payload.mode < 0 || payload.mode > 2) return this.result(IO_CODES.range);
if (this.state.estop) return this.result(IO_CODES.estop);
if (Math.abs(spindle.speed_rpm) > 5 || spindle.command_rpm !== 0) return this.result(IO_CODES.busy);
spindle.orient = true; spindle.locked = false; spindle.is_oriented = false; spindle.orient_angle = payload.angle; spindle.orient_mode = payload.mode; spindle.orient_fault = 0;
} else if (action === "oriented") {
if (typeof payload?.oriented !== "boolean") return this.result(IO_CODES.argument);
if (!spindle.orient) return this.result(IO_CODES.state);
spindle.is_oriented = payload.oriented;
spindle.orient_fault = payload.fault ?? 0;
if (payload.fault) { spindle.orient = false; spindle.locked = false; }
else if (payload.oriented) { spindle.orient = false; spindle.locked = true; }
}
return this.result();
}
step(ticks) {
if (!Number.isSafeInteger(ticks) || ticks < 0 || ticks > 1000000) return this.result(IO_CODES.range);
for (let tick = 0; tick < ticks; tick += 1) {
this.state.tick += 1;
for (const spindle of this.state.spindles) {
const target = this.state.ready && !this.state.estop && !spindle.amp_fault && !spindle.inhibit ? spindle.command_rpm : 0;
const delta = 60;
if (spindle.speed_rpm < target) spindle.speed_rpm = Math.min(target, spindle.speed_rpm + delta);
else if (spindle.speed_rpm > target) spindle.speed_rpm = Math.max(target, spindle.speed_rpm - delta);
spindle.at_speed = Math.abs(spindle.speed_rpm - target) <= 5;
spindle.brake = target === 0 && Math.abs(spindle.speed_rpm) > 5;
spindle.revs += spindle.speed_rpm * 0.1 / 60;
if (spindle.orient && spindle.at_speed && spindle.speed_rpm === 0) { spindle.orient = false; spindle.locked = true; spindle.is_oriented = true; }
}
}
return this.result(IO_CODES.ok, { ticks });
}
toolAction(kind, payload = {}) {
const tool = this.state.tool;
if (kind === "TOOL_PREPARE") {
if (!Number.isSafeInteger(payload.tool) || !Number.isSafeInteger(payload.pocket) || payload.tool < 0 || payload.pocket <= 0 || payload.pocket >= this.state.tool_capacity) return this.result(IO_CODES.range);
if (this.state.estop) return this.result(IO_CODES.estop);
if (!this.state.ready) return this.result(IO_CODES.not_ready);
if (this.state.fault) return this.result(IO_CODES.fault);
Object.assign(tool, { state: "preparing", prepared: payload.tool, pocket: payload.pocket, prepare: true, prepared_ack: false, changed: false });
} else if (kind === "TOOL_PREPARED") {
if (tool.state !== "preparing") return this.result(IO_CODES.state);
if (payload.prepared !== true) return this.result();
Object.assign(tool, { state: "prepared", prepare: false, prepared_ack: true });
} else if (kind === "TOOL_CHANGE") {
if (this.state.estop) return this.result(IO_CODES.estop);
if (!this.state.ready) return this.result(IO_CODES.not_ready);
if (this.state.fault) return this.result(IO_CODES.fault);
if (tool.state !== "prepared" || !tool.prepared_ack) return this.result(IO_CODES.state);
if (this.state.spindles.some((spindle) => Math.abs(spindle.speed_rpm) > 5 || spindle.command_rpm !== 0)) return this.result(IO_CODES.busy);
Object.assign(tool, { state: "changing", changed: false });
} else if (kind === "TOOL_CHANGED") {
if (tool.state !== "changing") return this.result(IO_CODES.state);
if (payload.changed !== true) return this.result();
Object.assign(tool, { state: "ready", current: tool.prepared, changed: true, prepared_ack: false });
} else if (kind === "TOOL_ABORT") {
Object.assign(tool, { state: "aborted", prepare: false, prepared_ack: false, changed: false });
this.state.last_error = IO_CODES.recovery;
} else if (kind === "TOOL_FAULT") {
if (typeof payload.fault !== "boolean") return this.result(IO_CODES.argument);
tool.fault = payload.fault; tool.reason = payload.reason ?? 0; this.state.fault = payload.fault; this.state.fault_reason = tool.reason;
if (payload.fault) Object.assign(tool, { state: "fault", prepare: false, prepared_ack: false, changed: false });
} else if (kind === "TOOL_CLEAR_FAULT") {
if (tool.fault) return this.result(IO_CODES.fault);
this.state.fault = false; this.state.fault_reason = 0;
if (tool.state === "fault" || tool.state === "aborted") tool.state = "idle";
}
return this.result();
}
probeAction(payload = {}) {
const probe = this.state.probe;
if (payload.action === "arm") {
if (this.state.estop) return this.result(IO_CODES.estop);
if (!this.state.ready) return this.result(IO_CODES.not_ready);
Object.assign(probe, { state: "armed", type: payload.probe_type ?? 0, input: false, tripped: false, tick: this.state.tick, position: null });
} else if (payload.action === "update") {
const position = payload.position;
if (!position || !Object.values(position).every(finite)) return this.result(IO_CODES.nonfinite);
probe.input = Boolean(payload.input);
if (probe.state === "armed" && probe.input) Object.assign(probe, { state: "tripped", tripped: true, tick: this.state.tick, position: clone(position) });
} else if (payload.action === "disarm") {
Object.assign(probe, { state: "idle", type: 0, tripped: false });
} else return this.result(IO_CODES.argument);
return this.result();
}
command(kind, payload = {}) {
if (kind === "SPINDLE_COMMAND") return this.spindle(payload, "command");
if (kind === "SPINDLE_FAULT") return this.spindle(payload, "fault");
if (kind === "SPINDLE_INHIBIT") return this.spindle(payload, "inhibit");
if (kind === "SPINDLE_ORIENT") return this.spindle(payload, "orient");
if (kind === "SPINDLE_ORIENTED") return this.spindle(payload, "oriented");
if (kind === "IO_STEP") return this.step(payload.ticks);
if (kind.startsWith("TOOL_")) return this.toolAction(kind, payload);
if (kind === "PROBE_CONFIG") return this.probeAction(payload);
if (kind === "LIMIT_SET") {
if (typeof payload.name !== "string" || typeof payload.active !== "boolean") return this.result(IO_CODES.argument);
this.state.limits[payload.name] = payload.active; return this.result();
}
if (kind === "ESTOP") {
if (typeof payload.active !== "boolean") return this.result(IO_CODES.argument);
this.state.estop = payload.active;
if (payload.active) { this.state.ready = false; this.state.tool.state = "aborted"; for (const spindle of this.state.spindles) { spindle.command_rpm = 0; spindle.enabled = false; spindle.brake = true; } return this.result(IO_CODES.estop); }
return this.result();
}
if (kind === "IO_RECOVER") {
if (this.state.estop) return this.result(IO_CODES.estop);
if (this.state.fault || this.state.tool.fault) return this.result(IO_CODES.fault);
if (this.state.spindles.some((spindle) => Math.abs(spindle.speed_rpm) > 5 || spindle.command_rpm !== 0)) return this.result(IO_CODES.busy);
this.state.ready = true; this.state.recovery_epoch += 1; this.state.tool.state = "idle"; return this.result();
}
return this.result(IO_CODES.argument);
}
applyEvent(kind, payload) { return this.command(kind, payload); }
}

View File

@@ -0,0 +1,145 @@
const CONFIGURATION_SCHEMA = "configuration-startup-closure.v1";
export const CONFIGURATION_ERRORS = Object.freeze({
invalid_reference: "configuration_reference_invalid",
empty_reference: "configuration_reference_empty",
host_path: "configuration_host_path",
outside_repo: "configuration_outside_repo",
path_escape: "configuration_path_escape",
missing: "configuration_resource_missing",
ambiguous: "configuration_resource_ambiguous",
unavailable: "configuration_resource_unavailable",
});
export const LIBRARY_ROOTS = Object.freeze([
"linuxcnc/lib/hallib",
"linuxcnc/lib",
]);
function asPathSet(paths) {
if (paths instanceof Set) return paths;
if (Array.isArray(paths)) return new Set(paths);
if (paths && typeof paths === "object") return new Set(Object.keys(paths));
return new Set();
}
function normalizePath(value) {
if (typeof value !== "string" || value.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
if (value.includes("\0")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
const parts = [];
for (const part of value.replaceAll("\\", "/").split("/")) {
if (!part || part === ".") continue;
if (part === "..") {
if (parts.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.path_escape, path: null };
parts.pop();
continue;
}
parts.push(part);
}
return { status: "accepted", code: null, path: parts.join("/") };
}
function dirname(path) {
const index = path.lastIndexOf("/");
return index < 0 ? "" : path.slice(0, index);
}
function joinPath(base, value) {
return normalizePath(base ? `${base}/${value}` : value);
}
export function splitReference(rawValue) {
if (typeof rawValue !== "string") {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: null, args: [] };
}
const raw = rawValue.trim().replace(/^['"]|['"]$/g, "");
if (!raw) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
if (tokens.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const value = tokens[0].replace(/^['"]|['"]$/g, "");
const args = tokens.slice(1).map((token) => token.replace(/^['"]|['"]$/g, ""));
return { status: "accepted", code: null, raw, value, args };
}
export function resolveReference({ rawValue, entrypoint, availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const parsed = splitReference(rawValue);
if (parsed.status !== "accepted") return { ...parsed, path: null, attempted_paths: [] };
if (typeof entrypoint !== "string" || entrypoint.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
if (parsed.value.startsWith("~") || parsed.value.startsWith("$") || parsed.value.startsWith("${")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.host_path, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
const available = asPathSet(availablePaths);
const entry = normalizePath(entrypoint);
if (entry.status !== "accepted") return { ...entry, value: parsed.value, args: parsed.args, attempted_paths: [] };
let candidates;
let resolution = "relative";
if (parsed.value.startsWith("LIB:")) {
resolution = "library";
const libraryName = parsed.value.slice("LIB:".length);
if (!libraryName || libraryName.startsWith("/") || libraryName.includes("..")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
candidates = libraryRoots.map((root) => normalizePath(`${root}/${libraryName}`)).filter((item) => item.status === "accepted").map((item) => item.path);
} else if (parsed.value.startsWith("/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
} else if (parsed.value.startsWith("linuxcnc/")) {
resolution = "repo_relative";
const normalized = normalizePath(parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
candidates = [normalized.path];
} else {
const normalized = joinPath(dirname(entry.path), parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
if (normalized.path !== "linuxcnc" && !normalized.path.startsWith("linuxcnc/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [normalized.path], resolution };
}
candidates = [normalized.path];
}
const existing = candidates.filter((candidate) => available.has(candidate));
if (existing.length > 1) {
return { status: "blocked", code: CONFIGURATION_ERRORS.ambiguous, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
if (existing.length === 1) {
return { status: "resolved", code: null, value: parsed.value, args: parsed.args, path: existing[0], attempted_paths: candidates, resolution };
}
return { status: "blocked", code: CONFIGURATION_ERRORS.missing, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
export function resolveResourceRow(row, availablePaths, options = {}) {
if (!row || typeof row !== "object") return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
const result = resolveReference({
rawValue: row.raw_value ?? row.path,
entrypoint: row.entrypoint,
availablePaths,
...options,
});
return {
resource_source_id: row.resource_source_id ?? null,
entrypoint: row.entrypoint ?? null,
original_path: row.path ?? null,
original_status: row.parse_status ?? null,
...result,
};
}
export function preflightConfiguration({ entrypoint, rows = [], availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const resolved = rows.map((row) => resolveResourceRow(row, availablePaths, { libraryRoots }));
const blocked = resolved.filter((row) => row.status !== "resolved");
return {
schema: CONFIGURATION_SCHEMA,
entrypoint,
status: blocked.length === 0 ? "ready" : "blocked",
resource_count: resolved.length,
resolved_count: resolved.length - blocked.length,
blocked_count: blocked.length,
resources: resolved,
};
}
export { CONFIGURATION_SCHEMA };

View File

@@ -0,0 +1,49 @@
export const HAL_CONFIGURATION_SCHEMA = "hal-configuration-preflight.v1";
const BUILTIN_THREADS = new Set(["servo-thread", "base-thread"]);
function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
export function parseHalLine(raw, line = 0) {
if (typeof raw !== "string") return { status: "blocked", code: "hal_line_invalid", line };
const clean = raw.split("#", 1)[0].trim();
if (!clean) return null;
const tokens = clean.split(/\s+/);
const command = tokens[0];
if (!["loadrt", "loadusr", "addf", "net", "setp", "source", "newsig", "linksp", "linkps", "linkpp"].includes(command)) return null;
if (command === "loadrt") {
const module = tokens[1] ?? "";
const namesToken = tokens.find((token) => token.startsWith("names="));
return { schema: HAL_CONFIGURATION_SCHEMA, line, command, module, instances: namesToken ? namesToken.slice(6).split(",") : [], status: module.startsWith("[") ? "blocked" : "declared", code: module.startsWith("[") ? "hal_component_variable" : null };
}
if (command === "loadusr") return { schema: HAL_CONFIGURATION_SCHEMA, line, command, status: "blocked", code: "hal_host_dynamic_loader", program: tokens[1] ?? "" };
if (command === "addf") {
const thread = tokens[2] ?? "";
return { schema: HAL_CONFIGURATION_SCHEMA, line, command, function: tokens[1] ?? null, thread, status: text(thread) && BUILTIN_THREADS.has(thread) ? "checked" : "blocked", code: text(thread) && BUILTIN_THREADS.has(thread) ? null : "hal_thread_undeclared" };
}
return { schema: HAL_CONFIGURATION_SCHEMA, line, command, tokens: tokens.slice(1), status: "checked", code: null };
}
export function preflightHalConfiguration({ lines = [], manifest = [] } = {}) {
const declared = new Set((Array.isArray(manifest) ? manifest : []).map((row) => row?.component).filter(Boolean));
const records = [];
for (let index = 0; index < lines.length; index += 1) {
const parsed = parseHalLine(lines[index], index + 1);
if (!parsed) continue;
if (parsed.command === "loadrt" && parsed.status === "declared" && !declared.has(parsed.module)) {
parsed.status = "blocked";
parsed.code = "hal_component_undeclared";
}
records.push(parsed);
}
const blocked = records.filter((record) => record.status === "blocked");
return {
schema: HAL_CONFIGURATION_SCHEMA,
status: blocked.length === 0 ? "ready" : "blocked",
records,
record_count: records.length,
blocked_count: blocked.length,
};
}

View File

@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { preflightConfiguration, resolveReference, splitReference } from "./configuration.mjs";
assert.deepEqual(splitReference("LIB:basic_sim.tcl -no_sim_spindle"), {
status: "accepted",
code: null,
raw: "LIB:basic_sim.tcl -no_sim_spindle",
value: "LIB:basic_sim.tcl",
args: ["-no_sim_spindle"],
});
const available = new Set([
"linuxcnc/lib/hallib/basic_sim.tcl",
"linuxcnc/configs/sim/demo/core.hal",
"linuxcnc/nc_files/probe.ngc",
]);
assert.equal(resolveReference({ rawValue: "LIB:basic_sim.tcl -no_sim_spindle", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/lib/hallib/basic_sim.tcl");
assert.equal(resolveReference({ rawValue: "core.hal", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/configs/sim/demo/core.hal");
assert.equal(resolveReference({ rawValue: "linuxcnc/nc_files/probe.ngc", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/nc_files/probe.ngc");
assert.equal(resolveReference({ rawValue: "~/linuxcnc/nc_files", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).code, "configuration_host_path");
assert.equal(resolveReference({ rawValue: "../../../../etc/passwd", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).code, "configuration_outside_repo");
const report = preflightConfiguration({
entrypoint: "linuxcnc/configs/sim/demo/main.ini",
availablePaths: available,
rows: [
{ resource_source_id: "r1", entrypoint: "linuxcnc/configs/sim/demo/main.ini", path: "LIB:basic_sim.tcl -no_sim_spindle", raw_value: "LIB:basic_sim.tcl -no_sim_spindle", parse_status: "missing" },
{ resource_source_id: "r2", entrypoint: "linuxcnc/configs/sim/demo/main.ini", path: "missing.hal", raw_value: "missing.hal", parse_status: "missing" },
],
});
assert.equal(report.schema, "configuration-startup-closure.v1");
assert.equal(report.status, "blocked");
assert.equal(report.resolved_count, 1);
assert.equal(report.blocked_count, 1);
console.log(`configuration_resolver=passed resolved=${report.resolved_count} blocked=${report.blocked_count}`);

View File

@@ -0,0 +1,12 @@
import assert from "node:assert/strict";
import { preflightHalConfiguration } from "./hal-configuration.mjs";
const report = preflightHalConfiguration({
manifest: [{ component: "demo" }],
lines: ["loadrt demo names=d0", "addf d0 servo-thread", "loadusr -W external"],
});
assert.equal(report.schema, "hal-configuration-preflight.v1");
assert.equal(report.status, "blocked");
assert.equal(report.blocked_count, 1);
assert.equal(report.records[2].code, "hal_host_dynamic_loader");
console.log(`hal_preflight=passed records=${report.record_count} blocked=${report.blocked_count}`);