Implement configuration, runtime, frontend, and task governance closure
This commit is contained in:
@@ -10,11 +10,9 @@
|
||||
- 来源索引:`coverage/source-index.jsonl`,由 `scripts/generate_source_inventory.py` 生成。
|
||||
- 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。
|
||||
- WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。
|
||||
- 前端、Worker、HAL、解释器、TP、Python、OPFS 和 UI 功能仍须按文档里程碑实现。
|
||||
- `K0-INV-01`、`K0-INV-02`、`K0-ORACLE-01`、`K1-CONTRACT-01`、`K1-CONTRACT-02`、`K1-NUM-01`、`K2-TRIV-01`、`K2-TRIV-02`、`K2-COUPLED-01`、`K2-HAL-01`、`K3-5AXIS-01`、`K3-TRT-01`、`K3-ROBOT-01` 和 `K3-GENSER-01` 已完成;Genser melfa/puma560 DH、迭代 residual、固定上限和 native/WASM 对照见 `coverage/genser-kinematics.report.json`;其它运动学证据保持在对应 `coverage/*-kinematics.report.json`。旧 `M2-CORE-04` 因锁定来源缺少声明的 `linuxcnc/unit_tests/kinematics/**` 输入而保留为 blocked 证据。
|
||||
- `K4-DELTA-01` 已完成 linear/rotary Delta 的统一解析 FK/IK、XYZ/extra-joint mapping、默认/alternate root、工作空间/限位/奇异边界和 native/WASM 对照;四个真实配置候选、缺失 HAL 资源和官方 oracle blocked 清单见 `coverage/delta-kinematics.report.json`。旧 `M2-CORE-04` 因锁定来源缺少声明的 `linuxcnc/unit_tests/kinematics/**` 输入而保留为 blocked 证据。
|
||||
- `K4-TRIPOD-01` 已完成 point-vertex tripod 的统一 FK/IK/mapping;真实 `tripod.ini`/`tripodsim.hal`、XYZ/AD-BD-CD 映射、above/below 根分支、无解/基面奇异/限位/退化几何和 native/WASM state hash 对照见 `coverage/tripod-kinematics.report.json`。`nextTask.md` 当前进入 `K4-HEX-01`;官方 oracle 因锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 继续显式 blocked。
|
||||
- `K4-HEX-01` 已完成 genhex 六腿 Stewart 平台的统一 FK/IK/mapping;真实 `hexapod.ini`/`kinematics.hal`、Newton 6x6 正解、闭式逆解、迭代/奇异/限位边界和 native/WASM 对照见 `coverage/genhex-kinematics.report.json`。`nextTask.md` 当前进入 `K4-PENTA-01`;官方 oracle 继续显式 blocked。
|
||||
- 前端应用壳、真实 WorkerClient、完整配置加载、HAL/数字孪生、实际 Pyodide bridge 和 UI/Three.js 工作流仍须按文档里程碑实现;当前下一任务是 `M3-CFG-01`,详见 [`后续工作基线`](docs/linuxcnc-web-wasm-next-work.md)。
|
||||
- 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;这是发布验收阻塞,不是继续重复解析的理由。
|
||||
- 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 不得被误报为运动学未完成。
|
||||
|
||||
## 重建 M0
|
||||
|
||||
|
||||
@@ -66,21 +66,21 @@
|
||||
"bytes": 11539,
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix",
|
||||
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
@@ -167,7 +167,7 @@
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_matrix_unverified",
|
||||
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9",
|
||||
"report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
|
||||
"review_policy": {
|
||||
"current_environment_limit": "Safari binary and browser farm are unavailable; observed Chromium/Firefox probes do not establish a release minimum",
|
||||
"forbidden": [
|
||||
|
||||
208
coverage/browser-performance-matrix.report.json
Normal file
208
coverage/browser-performance-matrix.report.json
Normal 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"
|
||||
}
|
||||
@@ -260,10 +260,10 @@
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 20236,
|
||||
"bytes": 20468,
|
||||
"path": "docs/linuxcnc-web-wasm-full-port-implementation.md",
|
||||
"role": "implementation_contract",
|
||||
"sha256": "16ed7fdf62f0dd377e50947109dfa57bc947df6313a21d0eb034d9b8e8ceb70f",
|
||||
"sha256": "52261ecce35d945b20d85a40abdeeb408b4a53caf38162c840be2b4864e8b5ee",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
@@ -352,7 +352,7 @@
|
||||
}
|
||||
],
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"report_sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"schema": "release-compatibility-matrix.v1",
|
||||
"source_baseline": {
|
||||
"context_lock": {
|
||||
|
||||
@@ -82,14 +82,14 @@
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
@@ -13852,7 +13852,7 @@
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_resource_gaps",
|
||||
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1",
|
||||
"report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
|
||||
"review_policy": {
|
||||
"forbidden": [
|
||||
"drop_missing_rows",
|
||||
|
||||
257656
coverage/configuration-startup-closure.report.json
Normal file
257656
coverage/configuration-startup-closure.report.json
Normal file
File diff suppressed because it is too large
Load Diff
144
coverage/frontend-m1.report.json
Normal file
144
coverage/frontend-m1.report.json
Normal 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"
|
||||
}
|
||||
119
coverage/frontend-m2.report.json
Normal file
119
coverage/frontend-m2.report.json
Normal 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"
|
||||
}
|
||||
103
coverage/frontend-m3.report.json
Normal file
103
coverage/frontend-m3.report.json
Normal 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"
|
||||
}
|
||||
110
coverage/frontend-m4.report.json
Normal file
110
coverage/frontend-m4.report.json
Normal 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"
|
||||
}
|
||||
5021
coverage/full-configuration-startup-matrix.report.json
Normal file
5021
coverage/full-configuration-startup-matrix.report.json
Normal file
File diff suppressed because it is too large
Load Diff
75649
coverage/hal-component-manifest.report.json
Normal file
75649
coverage/hal-component-manifest.report.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1003,21 +1003,21 @@
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_pending_owner_decision",
|
||||
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca",
|
||||
"report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
|
||||
"schema": "implementation-license-remediation.v1",
|
||||
"status": "complete",
|
||||
"task_id": "K8-REL-13"
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
"status": "verified"
|
||||
},
|
||||
"audit_self_hash": {
|
||||
"declared": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f",
|
||||
"declared": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
|
||||
"status": "verified"
|
||||
},
|
||||
"blocker_preservation": {
|
||||
@@ -184,22 +184,22 @@
|
||||
},
|
||||
{
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"status": "verified"
|
||||
}
|
||||
],
|
||||
@@ -238,28 +238,28 @@
|
||||
"bytes": 26665,
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"role": "post_release_audit",
|
||||
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 1945,
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 1435099,
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b",
|
||||
"report_sha256": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
|
||||
"schema": "k8-milestone-closeout.v1",
|
||||
"source_baseline": {
|
||||
"context_lock": {
|
||||
|
||||
@@ -100052,14 +100052,14 @@
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
@@ -100072,7 +100072,7 @@
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_pending_manifest_review",
|
||||
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179",
|
||||
"report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
|
||||
"review_policy": {
|
||||
"forbidden": [
|
||||
"promote_to_accepted",
|
||||
|
||||
@@ -71,21 +71,21 @@
|
||||
"bytes": 11539,
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix.report",
|
||||
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate.report",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "release_remediation_intake.report",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
@@ -148,7 +148,7 @@
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_missing_locked_inputs",
|
||||
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d",
|
||||
"report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
|
||||
"review_policy": {
|
||||
"forbidden": [
|
||||
"promote_matrixkins_to_official",
|
||||
|
||||
@@ -79,28 +79,28 @@
|
||||
"bytes": 9239,
|
||||
"path": "coverage/remediation-evidence-roundup.report.json",
|
||||
"role": "remediation_roundup",
|
||||
"sha256": "f91376e3cd4a7871610b9881b9f997a627fc205c58919d47c7e7e43b8545dd29",
|
||||
"sha256": "6746fda47f7efc4cc0902478854dabbb91e42b0bd6e58fc62393b7c9ebc2d40e",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 26665,
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"role": "post_release_audit",
|
||||
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
"next_transition": "collect owner decisions, create a new context when locked inputs change, then regenerate closure/audit/gate",
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "fc7d7160ceae3bed7a9cbc2e942b2abe21af51f17c9e235d86b340868c001246",
|
||||
"report_sha256": "5006286fd2d25bf73e262633e3a6d12b89b28af9d159ef84ad58132bb79e6de2",
|
||||
"review_status": "owner_input_required",
|
||||
"schema": "owner-review-gate-refresh.v1",
|
||||
"status": "blocked",
|
||||
|
||||
@@ -262,9 +262,9 @@
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"evidence": "coverage/release-closure.report.json",
|
||||
"expected": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"expected": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"name": "closure_compatibility_report_hash",
|
||||
"status": "verified"
|
||||
},
|
||||
@@ -276,16 +276,16 @@
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"evidence": "coverage/release-closure.report.json",
|
||||
"expected": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"expected": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"name": "closure_evidence_index_report_hash",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"evidence": "coverage/release-evidence-index.json",
|
||||
"expected": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"expected": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"name": "index_compatibility_report_hash",
|
||||
"status": "verified"
|
||||
},
|
||||
@@ -366,27 +366,27 @@
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1945,
|
||||
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"expected_bytes": 1945,
|
||||
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1435099,
|
||||
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"expected_bytes": 1435099,
|
||||
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 11539,
|
||||
"actual_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"actual_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"expected_bytes": 11539,
|
||||
"expected_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"expected_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix",
|
||||
"status": "verified"
|
||||
@@ -460,26 +460,26 @@
|
||||
"report_self_hashes": {
|
||||
"reports": [
|
||||
{
|
||||
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1",
|
||||
"declared": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1",
|
||||
"actual": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
|
||||
"declared": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"declared": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"declared": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"declared": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44",
|
||||
"actual": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"declared": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6",
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
@@ -503,8 +503,8 @@
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"status": "verified"
|
||||
}
|
||||
@@ -538,27 +538,27 @@
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1945,
|
||||
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"expected_bytes": 1945,
|
||||
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1435099,
|
||||
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"expected_bytes": 1435099,
|
||||
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 11539,
|
||||
"actual_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"actual_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"expected_bytes": 11539,
|
||||
"expected_sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"expected_sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix",
|
||||
"status": "verified"
|
||||
@@ -632,7 +632,7 @@
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
@@ -670,7 +670,7 @@
|
||||
},
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f",
|
||||
"report_sha256": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
|
||||
"schema": "post-release-audit.v1",
|
||||
"source_baseline": {
|
||||
"context_lock": {
|
||||
|
||||
207
coverage/project-task-readiness.report.json
Normal file
207
coverage/project-task-readiness.report.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
163
coverage/python-worker-bridge.report.json
Normal file
163
coverage/python-worker-bridge.report.json
Normal 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"
|
||||
}
|
||||
102
coverage/recovery-offline-fault-matrix.report.json
Normal file
102
coverage/recovery-offline-fault-matrix.report.json
Normal 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"
|
||||
}
|
||||
@@ -1890,35 +1890,35 @@
|
||||
"bytes": 1435099,
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 4628776,
|
||||
"path": "coverage/manifest-ownership-remediation.report.json",
|
||||
"role": "manifest_ownership_remediation",
|
||||
"sha256": "6b8b416aa7dd50c1411094d85370af8668334db53dd35dc72a4d46cbb464e4f3",
|
||||
"sha256": "d9573847f82d1c138cd08ca9af9a4ed6d390876530bee4523f149de9fd34d203",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"remediation_status": "blocked_no_eligible_entries",
|
||||
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4",
|
||||
"report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
|
||||
"schema": "release-acceptance-remediation.v1",
|
||||
"status": "complete",
|
||||
"task_id": "K8-REL-09"
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"schema": "release-closure-report.v1",
|
||||
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"status": "blocked"
|
||||
},
|
||||
{
|
||||
@@ -113,7 +113,7 @@
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"schema": "release-evidence-index.v1",
|
||||
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"status": "blocked"
|
||||
},
|
||||
{
|
||||
@@ -122,7 +122,7 @@
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix",
|
||||
"schema": "release-compatibility-matrix.v1",
|
||||
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"status": "blocked"
|
||||
},
|
||||
{
|
||||
@@ -208,7 +208,7 @@
|
||||
},
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"report_sha256": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"schema": "release-candidate-gate.v1",
|
||||
"source_baseline": {
|
||||
"context_lock": {
|
||||
|
||||
@@ -38,12 +38,12 @@
|
||||
],
|
||||
"compatibility": {
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44"
|
||||
"sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6"
|
||||
},
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"evidence_index": {
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"sha256": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229"
|
||||
"sha256": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1"
|
||||
},
|
||||
"integrity": {
|
||||
"all_affected_entries_have_required_evidence": true,
|
||||
@@ -63,7 +63,7 @@
|
||||
"sha256": "ce7451b5c428f170f37c6357877f08f0c2f32dc9502490cae3fe947cc43252e8"
|
||||
},
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1",
|
||||
"report_sha256": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
|
||||
"schema": "release-closure-report.v1",
|
||||
"status": "blocked"
|
||||
}
|
||||
|
||||
@@ -37349,7 +37349,7 @@
|
||||
],
|
||||
"compatibility_report": {
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"sha256": "1d41b7af74c24776e820f9305d56b4ab3c2a749a23b611693e68b60fbc4c3c44"
|
||||
"sha256": "82ba7df0a5ad50d72fd3b5f392d59623b2eac3ac6be26537a60b863b2fa4b5d6"
|
||||
},
|
||||
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
|
||||
"context_lock": {
|
||||
@@ -37427,7 +37427,7 @@
|
||||
]
|
||||
},
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"report_sha256": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"reports": [
|
||||
{
|
||||
"bytes": 497244,
|
||||
@@ -37791,7 +37791,7 @@
|
||||
"path": "coverage/compatibility-matrix.report.json",
|
||||
"role": "compatibility_matrix",
|
||||
"schema": "release-compatibility-matrix.v1",
|
||||
"sha256": "7e5e946031b080bd22c3630313907383863b61e19d6e959c74c14b2dba112ea7",
|
||||
"sha256": "1079e4da3d18044a98b38e4e083eff90b26c6adfaa0d4903a636d19988af963b",
|
||||
"status": "blocked"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,36 +60,36 @@
|
||||
"comparisons": [
|
||||
{
|
||||
"actual_bytes": 26665,
|
||||
"actual_sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"actual_sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"expected_bytes": 26665,
|
||||
"expected_sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"expected_sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"role": "post_release_audit",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 7196,
|
||||
"actual_sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"actual_sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"expected_bytes": 7196,
|
||||
"expected_sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"expected_sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1945,
|
||||
"actual_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"actual_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"expected_bytes": 1945,
|
||||
"expected_sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"expected_sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual_bytes": 1435099,
|
||||
"actual_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"actual_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"expected_bytes": 1435099,
|
||||
"expected_sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"expected_sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"status": "verified"
|
||||
@@ -112,32 +112,32 @@
|
||||
"source_report_self_hashes": {
|
||||
"reports": [
|
||||
{
|
||||
"actual": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b",
|
||||
"declared": "5979dc1dd8275cb163bf8eed396a6c6afbe80714a5c9db91ccc8ab7fd5f9720b",
|
||||
"actual": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
|
||||
"declared": "9b55de44782ac77b58d4c7d0709bdbcc9647fdc550137883755c3fef83aa2d91",
|
||||
"path": "coverage/k8-milestone-closeout.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f",
|
||||
"declared": "515d7c8a1fccc01a04d2c611a3338b06418d178c20b84263c75f145e65b6424f",
|
||||
"actual": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
|
||||
"declared": "2011f99b5e93b3d16697d0b347bdd55f572a99475ca518e9de9316baacf2b0aa",
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"declared": "b55d68c975511f6dede4aa1bc03eeae5a815947d6217d7e3a8061fa1cba201b8",
|
||||
"actual": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"declared": "9b75b848d1ab9c27cfe431776a59a3b5b15fab23c420c694a795c2db9f8fabc0",
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1",
|
||||
"declared": "2d73eaeaf8cc7798ef1279e309417ee72ba0e122b4ab5312998668208d100fa1",
|
||||
"actual": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
|
||||
"declared": "26b10dd3747ec06a29b6188dca47a9aeb7d83ceaa4821fe93261086848f2f5d1",
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"status": "verified"
|
||||
},
|
||||
{
|
||||
"actual": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"declared": "d7f0d686152e6317925d7cbf104efdd1980dd5b74bdf503f2f532df18d60c229",
|
||||
"actual": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"declared": "f6e86b87f39436c1bac0ceb30cb19cae670dc4782a51448b3f5bb6404c9136d1",
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"status": "verified"
|
||||
}
|
||||
@@ -168,35 +168,35 @@
|
||||
"bytes": 10299,
|
||||
"path": "coverage/k8-milestone-closeout.report.json",
|
||||
"role": "k8_milestone_closeout",
|
||||
"sha256": "6fe244b20e501083e4fabd46f51f9f1736c4c289fe7233e77d2f10924fadc823",
|
||||
"sha256": "4fed8e96ae515e5c68c7c3c270cfe5bbab9416874260f19382a6085aa2490108",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 26665,
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"role": "post_release_audit",
|
||||
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 1945,
|
||||
"path": "coverage/release-closure.report.json",
|
||||
"role": "release_closure",
|
||||
"sha256": "d586d30720723ad9e9375bc7309751bef5db37d34c6c0de98f7f0a7f9b2e6900",
|
||||
"sha256": "7dea93eaf2ae9edcdc53c484171629f8bed1904679c062a9a328ddc15fa74986",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 1435099,
|
||||
"path": "coverage/release-evidence-index.json",
|
||||
"role": "release_evidence_index",
|
||||
"sha256": "92435805bfcbc19244d90d322293711e0306f445997c233a5dd98ebbc63fd333",
|
||||
"sha256": "5eecf795a2d5a887a46ef0a0fcd0e9ea40de77a47553f582c8239da7d41161e5",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
@@ -278,7 +278,7 @@
|
||||
},
|
||||
"release_action": "STOP_RELEASE",
|
||||
"release_decision": "blocked_not_release_candidate",
|
||||
"report_sha256": "683a0ed187fec180705ce42becf3ce6d921018c1d0bfb4ebb99c9a85c768bb49",
|
||||
"report_sha256": "3b142c107e8174602c4d371b958b73c891cd3621b2142db0535213be0fac1e54",
|
||||
"schema": "release-remediation-intake.v1",
|
||||
"source_baseline": {
|
||||
"context_lock": {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/manifest-ownership-remediation.report.json",
|
||||
"remediation_status": "blocked_pending_manifest_review",
|
||||
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179",
|
||||
"report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/release-acceptance-remediation.report.json",
|
||||
"remediation_status": "blocked_no_eligible_entries",
|
||||
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4",
|
||||
"report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -45,7 +45,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/official-oracle-remediation.report.json",
|
||||
"remediation_status": "blocked_missing_locked_inputs",
|
||||
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d",
|
||||
"report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -53,7 +53,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/configuration-resource-remediation.report.json",
|
||||
"remediation_status": "blocked_resource_gaps",
|
||||
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1",
|
||||
"report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -61,7 +61,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/browser-matrix-remediation.report.json",
|
||||
"remediation_status": "blocked_matrix_unverified",
|
||||
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9",
|
||||
"report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -69,7 +69,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/implementation-license-remediation.report.json",
|
||||
"remediation_status": "blocked_pending_owner_decision",
|
||||
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca",
|
||||
"report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
}
|
||||
@@ -95,70 +95,70 @@
|
||||
"bytes": 15574,
|
||||
"path": "coverage/release-remediation-intake.report.json",
|
||||
"role": "remediation_intake",
|
||||
"sha256": "7d5916f02e12e952ec517d59ff436237d09854c90a961ddbd1375f4598b4c2a3",
|
||||
"sha256": "6e0e41e4f0ff3cb7f62ecfe60cf07863878539547296263a210f9b6b1765c881",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 7196,
|
||||
"path": "coverage/release-candidate-gate.report.json",
|
||||
"role": "release_candidate_gate",
|
||||
"sha256": "7ef2381f4ee627a4792cf5448362c31c4bc8b3670ccda53a70b7955093398f67",
|
||||
"sha256": "2092257cc0aea41c5988b19a0e7aef74ed6ccae09062213e1a98f44adc4314b1",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 26665,
|
||||
"path": "coverage/post-release-audit.report.json",
|
||||
"role": "post_release_audit",
|
||||
"sha256": "3ab0c8bf670141cd23a5698bbb94df8416020939b046e1c1b79a1ee9ae69d872",
|
||||
"sha256": "6794b4fd2b3b2ffb54b6a48d298265a8ac9b700bf5e1da434d80c9bb2a45f328",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 10299,
|
||||
"path": "coverage/k8-milestone-closeout.report.json",
|
||||
"role": "k8_closeout",
|
||||
"sha256": "6fe244b20e501083e4fabd46f51f9f1736c4c289fe7233e77d2f10924fadc823",
|
||||
"sha256": "4fed8e96ae515e5c68c7c3c270cfe5bbab9416874260f19382a6085aa2490108",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 4628776,
|
||||
"path": "coverage/manifest-ownership-remediation.report.json",
|
||||
"role": "K8-REL-08",
|
||||
"sha256": "6b8b416aa7dd50c1411094d85370af8668334db53dd35dc72a4d46cbb464e4f3",
|
||||
"sha256": "d9573847f82d1c138cd08ca9af9a4ed6d390876530bee4523f149de9fd34d203",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 143970,
|
||||
"path": "coverage/release-acceptance-remediation.report.json",
|
||||
"role": "K8-REL-09",
|
||||
"sha256": "153fdbaec1d88f83c21415eb6fa85e03593f70b70917830541521a1cbe3c5880",
|
||||
"sha256": "93ee6c0544fba2c24efdf6686eb566f03ff8dcec74d899721f7253d3ad8bd899",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 5541,
|
||||
"path": "coverage/official-oracle-remediation.report.json",
|
||||
"role": "K8-REL-10",
|
||||
"sha256": "c577f1eeeb392022abb39bce666a39cc14346638d767b1be43b6b78734c65f7c",
|
||||
"sha256": "45c66d3704cba5da3499832c4993f2f11e847f1e5945a8afcf1e4d700627ee16",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 754595,
|
||||
"path": "coverage/configuration-resource-remediation.report.json",
|
||||
"role": "K8-REL-11",
|
||||
"sha256": "64f15f9ffda1004ec95caa6fb962a52bd9e1731a2bffe440ddb346adf256e5fb",
|
||||
"sha256": "f07e88f3d49ca36232627238a68a61561beee35a1f3b8158fae57038263f3616",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 6180,
|
||||
"path": "coverage/browser-matrix-remediation.report.json",
|
||||
"role": "K8-REL-12",
|
||||
"sha256": "df201f0165aaebb52df6b5709c1e3a2da8e60ba552a18e03c86d984550bddc9a",
|
||||
"sha256": "6f150474a385a41caf71b964d6c11133d34e742a211f985f207908687aecb37c",
|
||||
"status": "present"
|
||||
},
|
||||
{
|
||||
"bytes": 41237,
|
||||
"path": "coverage/implementation-license-remediation.report.json",
|
||||
"role": "K8-REL-13",
|
||||
"sha256": "5f244d1c379e72fcbda6a34685e44af5d34175660b5c622c461e3d468db18a0e",
|
||||
"sha256": "6dec4492d81ccebd7a25a3962e5cd8f0975795f8513f580324d3df0bef49ab68",
|
||||
"status": "present"
|
||||
}
|
||||
],
|
||||
@@ -195,7 +195,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/manifest-ownership-remediation.report.json",
|
||||
"remediation_status": "blocked_pending_manifest_review",
|
||||
"report_sha256": "59d06376a4cdd0b7e7983f476a894f9f522b4d9ae133670ab153fd5f6228e179",
|
||||
"report_sha256": "7ddd6e6af4264387c15f98f2cc9b923058ed29cfac0d8ca46f2587032c81a922",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -203,7 +203,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/release-acceptance-remediation.report.json",
|
||||
"remediation_status": "blocked_no_eligible_entries",
|
||||
"report_sha256": "ff9a00c973885443184cf92e9a6ae0a32f95ae7b9c9d6676f5af53c836510db4",
|
||||
"report_sha256": "62e150f223606a6529c5a11b5f60def68f35249a773bdea39cb8f9b5cec6a207",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -211,7 +211,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/official-oracle-remediation.report.json",
|
||||
"remediation_status": "blocked_missing_locked_inputs",
|
||||
"report_sha256": "bcd7d38f6eb6a7c1cd18ed54e39d7b9e05788a58632eb29dcde13ad2fa55d76d",
|
||||
"report_sha256": "44b4bc2a64b9f43d0a4a0b4eb4afd176396cda938c733b70a5f257d9e5abd248",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -219,7 +219,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/configuration-resource-remediation.report.json",
|
||||
"remediation_status": "blocked_resource_gaps",
|
||||
"report_sha256": "7d8d03e576d83e2a0878275d72dff80fa47aeb6ace4a1f6e447c5e37c6bbc6c1",
|
||||
"report_sha256": "419cd1afd030105fc766849c3f79d461bc1894a40247ce40ea98744f0ce844c1",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -227,7 +227,7 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/browser-matrix-remediation.report.json",
|
||||
"remediation_status": "blocked_matrix_unverified",
|
||||
"report_sha256": "7ca6f79dda1aa3ea0b5bbce9ec1d96152a0aa0f5e942563084286290621545c9",
|
||||
"report_sha256": "64c89092fe6dc4eeb86c33ee3984b320bf5aa05231415acbd835877cd5f1c7e8",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
},
|
||||
@@ -235,12 +235,12 @@
|
||||
"context": "verified",
|
||||
"path": "coverage/implementation-license-remediation.report.json",
|
||||
"remediation_status": "blocked_pending_owner_decision",
|
||||
"report_sha256": "cdf6f6c7db71f0e572057c4c180f442b5cd396aaba1e89e18303494f1d0401ca",
|
||||
"report_sha256": "8cc56f6d59aba518e53e0c5b1dac8dfabd33f814776cdd4956326b309fb90591",
|
||||
"self_hash": "verified",
|
||||
"status": "complete"
|
||||
}
|
||||
},
|
||||
"report_sha256": "93ab5bc729394f209e281c769a7fb381286f2ae65281414ed23a226f9fff8152",
|
||||
"report_sha256": "7258fe80e7bd4ea9e7adcdc1c6f64d414f046ce88b6eb928fb03e4643de10505",
|
||||
"review_policy": {
|
||||
"forbidden": [
|
||||
"promote_blocked_to_accepted",
|
||||
|
||||
459
coverage/virtual-device-fault-matrix.report.json
Normal file
459
coverage/virtual-device-fault-matrix.report.json
Normal 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"
|
||||
}
|
||||
114
coverage/workspace-session-integration.report.json
Normal file
114
coverage/workspace-session-integration.report.json
Normal 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"
|
||||
}
|
||||
@@ -249,10 +249,10 @@ OPFS 只保存已提交工程、journal、checkpoint、日志和可删除缓存
|
||||
|
||||
## 11. 当前立即执行顺序
|
||||
|
||||
1. 按 `nextTask.md` 执行 `K0-INV-01`,处理旧 `M2-CORE-04` 的 kinematics 输入阻塞,先确认锁定提交中不存在声明路径,并保留报告。
|
||||
2. 若要采用替代 oracle,先创建可审查的契约变更和新的 context lock;否则保持 blocked,不推进 HAL 节点。
|
||||
3. 在 kinematics 契约闭合后,完成静态模块注册、`forward/inverse/limits` ABI、确定性状态哈希和失败路径测试。
|
||||
4. 继续 `M2-CORE-05` 前,先让 native runner、WASM runner 和 `sim_step` 共享同一 tick/golden schema。
|
||||
5. 只有 M2 核心条目达到 `verified`,才进入 HAL/数字孪生/OPFS 恢复;只有 M3 通过,才把 mock UI 替换成真实 Worker 工作流。
|
||||
1. 按 `nextTask.md` 执行 `M3-CFG-01`,把 264 个仿真配置入口和 16,093 条资源边从静态索引推进为确定性启动解析;保留 missing/unresolved/host path,禁止静默同名替换。
|
||||
2. 完成 `M3-RT-02`,将 HAL pin/signal/thread/function、`.comp` 元数据和配置图接入可执行 preflight;运动学只作为已完成的注册/调用依赖,不再重复解析。
|
||||
3. 完成 `M3-RT-03`,把 spindle/tool/probe/limit/estop 数字孪生接入 Worker command path,并让故障注入可录制、回放和恢复。
|
||||
4. 完成 `M3-FS-01` 与 `M3-PY-01`,将 workspace recovery/migration 和实际 Pyodide 虚拟 API 接入真实 session;保持现有 schema 和宿主能力拒绝契约。
|
||||
5. 完成 `FE-M1` 最小真实前端垂直切片,再运行 `REG-01/03/04` 的配置、恢复、浏览器和性能矩阵;K8 release owner blocker 只在真实 owner 输入或新 lock context 到位后单独刷新。
|
||||
|
||||
本文件完成的是实施规则和证据闭环,不改变当前任务指针,也不把尚未实现的 LinuxCNC 功能标为完成。
|
||||
|
||||
43
docs/linuxcnc-web-wasm-next-work.md
Normal file
43
docs/linuxcnc-web-wasm-next-work.md
Normal 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。
|
||||
45
docs/linuxcnc-web-wasm-task-map.md
Normal file
45
docs/linuxcnc-web-wasm-task-map.md
Normal 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。
|
||||
@@ -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-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-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` |
|
||||
|
||||
## 追溯规则
|
||||
|
||||
|
||||
73
nextTask.md
73
nextTask.md
@@ -9,18 +9,18 @@ pointer_version: 3
|
||||
updated: 2026-08-21
|
||||
context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1
|
||||
milestone: K8
|
||||
last_verified_checkpoint: K8-REL-15-r01
|
||||
last_verified_checkpoint: PROJECT-AUDIT-02
|
||||
task:
|
||||
id: K8-REL-15
|
||||
id: WAIT-OWNER-01
|
||||
status: blocked
|
||||
title: owner review and release gate refresh
|
||||
owner: WS-7
|
||||
title: pause until external owner decisions or new lock input
|
||||
owner: WS-5
|
||||
profile: milestone
|
||||
next:
|
||||
id: K8-REL-16
|
||||
status: pending
|
||||
title: new context and release gate refresh after owner decisions
|
||||
owner: WS-7
|
||||
id: OWNER-INPUT-01
|
||||
status: blocked
|
||||
title: six owner decisions or new locked context input required
|
||||
owner: WS-5
|
||||
profile: milestone
|
||||
```
|
||||
|
||||
@@ -41,48 +41,57 @@ python3 scripts/validate_task_pointer.py
|
||||
|
||||
## 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` 提升为 `Current.task`,更新 `last_verified_checkpoint` 和最近证据摘要,并重新运行 `Read` 中的门禁。不得跳过未完成依赖;同一时刻只允许一个 current task。
|
||||
本任务是 external wait,不执行自动重试;只有六项 owner decision 或新 locked context input 到位时,才新建 context 并重新打开 K8 release task。
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
python3 scripts/generate_release_closure.py
|
||||
python3 scripts/generate_post_release_audit.py
|
||||
python3 scripts/generate_k8_milestone_closeout.py
|
||||
python3 scripts/generate_release_remediation_intake.py
|
||||
python3 scripts/generate_manifest_ownership_remediation.py
|
||||
python3 scripts/generate_release_acceptance_remediation.py
|
||||
python3 scripts/generate_official_oracle_remediation.py
|
||||
python3 scripts/generate_configuration_resource_remediation.py
|
||||
python3 scripts/generate_browser_matrix_remediation.py
|
||||
python3 scripts/generate_implementation_license_remediation.py
|
||||
python3 scripts/generate_remediation_evidence_roundup.py
|
||||
python3 scripts/generate_owner_review_gate_refresh.py
|
||||
node web/worker/test_replay.mjs
|
||||
node web/workspace/test_recovery.mjs
|
||||
node web/workspace/test_migration.mjs
|
||||
python3 scripts/generate_configuration_startup_closure.py
|
||||
python3 scripts/generate_hal_component_manifest.py
|
||||
python3 scripts/generate_virtual_device_fault_matrix.py
|
||||
python3 scripts/generate_workspace_session_integration.py
|
||||
python3 scripts/generate_project_task_readiness.py
|
||||
python3 scripts/test_project_task_readiness.py
|
||||
node python-runtime/test_runtime.mjs
|
||||
node web/worker/test_python_bridge.mjs
|
||||
python3 scripts/generate_python_worker_bridge.py
|
||||
python3 scripts/generate_frontend_m1.py
|
||||
node web/app/test_frontend_m1.mjs
|
||||
node web/app/test_frontend_m2.mjs
|
||||
python3 scripts/generate_frontend_m2.py
|
||||
node web/app/test_frontend_m3.mjs
|
||||
python3 scripts/generate_frontend_m3.py
|
||||
python3 scripts/generate_workspace_session_integration.py
|
||||
python3 scripts/validate_baseline.py
|
||||
python3 scripts/validate_task_pointer.py
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
26
scripts/generate_browser_performance_matrix.py
Normal file
26
scripts/generate_browser_performance_matrix.py
Normal 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())
|
||||
@@ -7,6 +7,7 @@ import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
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\"'=;,]+)")
|
||||
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] = {}
|
||||
_LOCKED_SUBMODULE_CACHE: dict[Path, set[str] | None] = {}
|
||||
|
||||
|
||||
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())
|
||||
|
||||
|
||||
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,
|
||||
target: Path | None, raw: str, kind: str, edge_kind: str, line: int, status: str | 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]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
locked = locked_submodule_files(repo_root)
|
||||
ini_rows = parse_ini(entrypoint)
|
||||
row_numbers = {(section, key, value): index for index, (section, key, value) in enumerate(ini_rows, 1)}
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -187,7 +228,8 @@ def generate(repo_root: Path, output: Path, report_path: Path) -> tuple[int, str
|
||||
config_root = repo_root / CONFIG_ROOT
|
||||
if not config_root.is_dir():
|
||||
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]] = []
|
||||
for entrypoint in entrypoints:
|
||||
rows.extend(scan_entrypoint(repo_root, config_root, entrypoint))
|
||||
|
||||
253
scripts/generate_configuration_startup_closure.py
Normal file
253
scripts/generate_configuration_startup_closure.py
Normal 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())
|
||||
88
scripts/generate_frontend_m1.py
Normal file
88
scripts/generate_frontend_m1.py
Normal 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())
|
||||
77
scripts/generate_frontend_m2.py
Normal file
77
scripts/generate_frontend_m2.py
Normal 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())
|
||||
71
scripts/generate_frontend_m3.py
Normal file
71
scripts/generate_frontend_m3.py
Normal 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())
|
||||
69
scripts/generate_frontend_m4.py
Normal file
69
scripts/generate_frontend_m4.py
Normal 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())
|
||||
103
scripts/generate_full_configuration_startup_matrix.py
Normal file
103
scripts/generate_full_configuration_startup_matrix.py
Normal 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())
|
||||
222
scripts/generate_hal_component_manifest.py
Normal file
222
scripts/generate_hal_component_manifest.py
Normal 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())
|
||||
108
scripts/generate_project_task_readiness.py
Normal file
108
scripts/generate_project_task_readiness.py
Normal 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())
|
||||
88
scripts/generate_python_worker_bridge.py
Normal file
88
scripts/generate_python_worker_bridge.py
Normal 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())
|
||||
41
scripts/generate_recovery_offline_fault_matrix.py
Normal file
41
scripts/generate_recovery_offline_fault_matrix.py
Normal 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())
|
||||
109
scripts/generate_virtual_device_fault_matrix.py
Normal file
109
scripts/generate_virtual_device_fault_matrix.py
Normal 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())
|
||||
106
scripts/generate_workspace_session_integration.py
Normal file
106
scripts/generate_workspace_session_integration.py
Normal 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())
|
||||
11
scripts/test_browser_performance_matrix.py
Normal file
11
scripts/test_browser_performance_matrix.py
Normal 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()
|
||||
45
scripts/test_configuration_startup_closure.py
Normal file
45
scripts/test_configuration_startup_closure.py
Normal 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()
|
||||
28
scripts/test_frontend_m1.py
Normal file
28
scripts/test_frontend_m1.py
Normal 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()
|
||||
27
scripts/test_frontend_m2.py
Normal file
27
scripts/test_frontend_m2.py
Normal 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()
|
||||
27
scripts/test_frontend_m3.py
Normal file
27
scripts/test_frontend_m3.py
Normal 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()
|
||||
25
scripts/test_frontend_m4.py
Normal file
25
scripts/test_frontend_m4.py
Normal 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()
|
||||
29
scripts/test_full_configuration_startup_matrix.py
Normal file
29
scripts/test_full_configuration_startup_matrix.py
Normal 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()
|
||||
49
scripts/test_hal_component_manifest.py
Normal file
49
scripts/test_hal_component_manifest.py
Normal 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()
|
||||
29
scripts/test_project_task_readiness.py
Normal file
29
scripts/test_project_task_readiness.py
Normal 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()
|
||||
28
scripts/test_python_worker_bridge.py
Normal file
28
scripts/test_python_worker_bridge.py
Normal 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()
|
||||
13
scripts/test_recovery_offline_fault_matrix.py
Normal file
13
scripts/test_recovery_offline_fault_matrix.py
Normal 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()
|
||||
32
scripts/test_virtual_device_fault_matrix.py
Normal file
32
scripts/test_virtual_device_fault_matrix.py
Normal 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()
|
||||
31
scripts/test_workspace_session_integration.py
Normal file
31
scripts/test_workspace_session_integration.py
Normal 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
108
web/app/app.mjs
Normal 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
42
web/app/controls.mjs
Normal 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
10
web/app/device-panel.mjs
Normal 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
9
web/app/hal-panel.mjs
Normal 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
14
web/app/index.html
Normal 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
10
web/app/machine-view.mjs
Normal 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;
|
||||
}
|
||||
48
web/app/project-workflow.mjs
Normal file
48
web/app/project-workflow.mjs
Normal 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 }); }
|
||||
}
|
||||
27
web/app/recovery-panel.mjs
Normal file
27
web/app/recovery-panel.mjs
Normal 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
42
web/app/state-panel.mjs
Normal 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
36
web/app/styles.css
Normal 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; } }
|
||||
13
web/app/test_browser_performance.mjs
Normal file
13
web/app/test_browser_performance.mjs
Normal 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 }));
|
||||
45
web/app/test_frontend_m1.mjs
Normal file
45
web/app/test_frontend_m1.mjs
Normal 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);
|
||||
17
web/app/test_frontend_m2.mjs
Normal file
17
web/app/test_frontend_m2.mjs
Normal 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}`);
|
||||
23
web/app/test_frontend_m3.mjs
Normal file
23
web/app/test_frontend_m3.mjs
Normal 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}`);
|
||||
16
web/app/test_frontend_m4.mjs
Normal file
16
web/app/test_frontend_m4.mjs
Normal 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
105
web/app/worker-client.mjs
Normal 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?.();
|
||||
}
|
||||
}
|
||||
104
web/worker/python-bridge.mjs
Normal file
104
web/worker/python-bridge.mjs
Normal 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 };
|
||||
@@ -2,6 +2,10 @@ import { ReplayTimeline, fnv1a64 } from "./replay.mjs";
|
||||
import { capabilities, validateLimit } from "./capabilities.mjs";
|
||||
import { mapCoreError } from "./error-map.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 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",
|
||||
"PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP",
|
||||
"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) {
|
||||
@@ -52,6 +59,10 @@ export class SimSession {
|
||||
this.created = false;
|
||||
this.timeline = new ReplayTimeline();
|
||||
this.operations = new Map();
|
||||
this.devices = new VirtualDevices();
|
||||
this.python = new PythonBridge();
|
||||
this.configurationState = null;
|
||||
this.halState = null;
|
||||
call(core, "sim_protocol_reset");
|
||||
if (typeof core.ccall === "function") core.ccall("sim_event_reset", null, [], []);
|
||||
}
|
||||
@@ -97,9 +108,27 @@ export class SimSession {
|
||||
tick: this.tick,
|
||||
created: this.created,
|
||||
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() {
|
||||
const latest = this.timeline.snapshots.latest();
|
||||
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) {
|
||||
const replayDevices = new VirtualDevices({ state: checkpoint?.state?.devices });
|
||||
const replayPython = new PythonBridge();
|
||||
if (checkpoint?.state?.python) replayPython.restoreState(checkpoint.state.python);
|
||||
const result = recoverSimulation({
|
||||
checkpoint,
|
||||
contextId: this.contextId,
|
||||
events,
|
||||
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") {
|
||||
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);
|
||||
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);
|
||||
@@ -198,6 +241,17 @@ export class SimSession {
|
||||
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) {
|
||||
case "HELLO":
|
||||
return finish(this.envelope("CAPABILITIES", input, capabilities(this.contextId)), "verified");
|
||||
@@ -211,6 +265,41 @@ export class SimSession {
|
||||
case "LOAD_WORKSPACE": {
|
||||
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 (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);
|
||||
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);
|
||||
if (stepResult !== 0) return finish(this.coreError(input, stepResult, "sim_step"), "failed");
|
||||
this.tick += cycles;
|
||||
this.python.setTick(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");
|
||||
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 }));
|
||||
}
|
||||
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":
|
||||
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": {
|
||||
|
||||
26
web/worker/test_configuration_preflight.mjs
Normal file
26
web/worker/test_configuration_preflight.mjs
Normal 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");
|
||||
29
web/worker/test_device_recovery.mjs
Normal file
29
web/worker/test_device_recovery.mjs
Normal 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}`);
|
||||
22
web/worker/test_hal_preflight.mjs
Normal file
22
web/worker/test_hal_preflight.mjs
Normal 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");
|
||||
15
web/worker/test_python_bridge.mjs
Normal file
15
web/worker/test_python_bridge.mjs
Normal 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()}`);
|
||||
23
web/worker/test_python_session.mjs
Normal file
23
web/worker/test_python_session.mjs
Normal 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}`);
|
||||
29
web/worker/test_recovery_offline_faults.mjs
Normal file
29
web/worker/test_recovery_offline_faults.mjs
Normal 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");
|
||||
60
web/worker/test_virtual_devices.mjs
Normal file
60
web/worker/test_virtual_devices.mjs
Normal 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));
|
||||
68
web/worker/test_workspace_session_integration.mjs
Normal file
68
web/worker/test_workspace_session_integration.mjs
Normal 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));
|
||||
219
web/worker/virtual-devices.mjs
Normal file
219
web/worker/virtual-devices.mjs
Normal 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); }
|
||||
}
|
||||
145
web/workspace/configuration.mjs
Normal file
145
web/workspace/configuration.mjs
Normal 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 };
|
||||
49
web/workspace/hal-configuration.mjs
Normal file
49
web/workspace/hal-configuration.mjs
Normal 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,
|
||||
};
|
||||
}
|
||||
34
web/workspace/test_configuration.mjs
Normal file
34
web/workspace/test_configuration.mjs
Normal 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}`);
|
||||
12
web/workspace/test_hal_configuration.mjs
Normal file
12
web/workspace/test_hal_configuration.mjs
Normal 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}`);
|
||||
Reference in New Issue
Block a user