From d9ac3770a7e1a655d4374750979b34cb5af30ca2 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Fri, 19 Jun 2026 06:28:38 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E9=94=81L4-TOOL-DB=E4=B8=BB=E6=9C=BA?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 结论:已识别RIP版LinuxCNC主机运行时,Tool DB native protocol probe可通过,inventory baseline保持28/28/131/0且promotion继续锁定。 --- text23.txt | 269 +++++++++ text24.txt | 414 +++++++++++++ wasm-port/docs/project-release-handoff.md | 7 + .../docs/sim-configs-coverage-handoff.md | 19 + .../sdk/src/project-release-readiness.js | 270 ++++++++- wasm-port/runtime/ui/ini-panel/ui-shell.js | 515 +++++++++++++++- .../ini_panel_workflow_overview_smoke.html | 139 ++++- .../project-release-readiness-ready.json | 571 ++++++++++++++++++ ...ify_project_release_readiness_artifact.mjs | 5 + ...ite_project_release_readiness_artifact.mjs | 27 + .../native/probe_millturn_user_m_runtime.sh | 17 +- .../native/probe_python_remap_runtime.sh | 14 +- .../tests/native/probe_tool_db_runtime.sh | 61 +- ..._project_release_artifact_url_workflow.mjs | 107 ++++ .../verify_project_release_gate_manifest.mjs | 21 + .../tests/sdk/node/verify_sdk_surface.mjs | 164 +++++ .../ui/node/verify_ini_panel_ui_shell.mjs | 166 ++++- .../verify_sim_configs_inventory_wasm.mjs | 439 +++++++++----- 18 files changed, 3041 insertions(+), 184 deletions(-) create mode 100644 text23.txt create mode 100644 text24.txt mode change 100644 => 100755 wasm-port/tests/native/probe_millturn_user_m_runtime.sh mode change 100644 => 100755 wasm-port/tests/native/probe_tool_db_runtime.sh diff --git a/text23.txt b/text23.txt new file mode 100644 index 0000000..52b75d2 --- /dev/null +++ b/text23.txt @@ -0,0 +1,269 @@ +项目接续文件:OPFS/WASM hard block 移植优先级建议 + +生成时间:2026-06-19 CST + +本文件接替 `text22.txt` 的“另开 milestone”建议,专门回答: + +```text +在基于 OPFS、WASM 的数控系统仿真中, +L4-PYTHON-REMAP、L4-TOOL-DB、L4-USER-M-PROCESS 哪些可以移植, +以及应该先移植哪一个。 +``` + +重要边界: + +```text +本文件是 future runtime milestone 建议,不改变 text22.txt 的当前阶段铁律。 +当前阶段仍不得解锁 L4-PYTHON-REMAP / L4-TOOL-DB / L4-USER-M-PROCESS; +Node inventory baseline 仍保持 executed=28 passed=28 skipped=131 unexpected_fail=0; +promotion_allowed 仍必须保持 0。 +``` + +一、简要结论 + +三类 hard block 都可以“部分或项目定义下完整”移植到 OPFS/WASM 环境,但可移植方式不同。 + +推荐优先级: + +```text +1. L4-TOOL-DB +2. L4-PYTHON-REMAP +3. L4-USER-M-PROCESS +``` + +建议先移植 `L4-TOOL-DB`,原因是: + +- 当前只有 1 个 inventory row:`axis/db_demo/base.ngc`; +- 协议边界清晰:`DB_PROGRAM`、`v2.1`、`g`、`FINI`、`l`、`p`、`u`; +- OPFS 与 tool DB flat-file persistence 天然匹配; +- 可用 Worker / WASM Python runtime 隔离 `DB_PROGRAM`; +- 已有 `text20.txt` / `text21.txt` 规划和 machine-readable artifacts; +- 不需要一次性承诺完整 Python remap lifecycle 或 arbitrary external process; +- 成功后能形成第一个 hard-block runtime unlock 的可复用 proof pattern。 + +二、可移植性判断 + +| Family | 能否移植到 OPFS/WASM | 推荐优先级 | 主要原因 | +| --- | --- | ---: | --- | +| `L4-TOOL-DB` | 可以,建议先做 | 1 | 协议小、状态明确、OPFS 持久化适配自然、只有 1 个 row。 | +| `L4-PYTHON-REMAP` | 可以,但应分阶段做 | 2 | 覆盖面最大,价值高,但需要 LinuxCNC Python remap lifecycle、prolog/epilog、module loading、interpreter state binding。 | +| `L4-USER-M-PROCESS` | 只建议做受控子集,最后做 | 3 | 浏览器不能运行 arbitrary external process;必须把 external process 收束成 LinuxCNC-owned state transition proof。 | + +三、为什么先做 L4-TOOL-DB + +`L4-TOOL-DB` 当前 blocked row 是: + +```text +axis/db_demo/base.ngc +``` + +当前阻塞原因不是 G-code 解释器本身,而是 INI 声明: + +```text +[EMCIO] +DB_PROGRAM = ./db_nonran.py +``` + +这意味着 `.tbl` fallback 不能算 pass。必须证明 LinuxCNC tool DB process protocol: + +```text +startup handshake: v2.1 +get-all: g ... FINI +spindle load notify: l +tool offset notify: p +spindle unload notify: u +flat-file DB persistence +``` + +在 OPFS/WASM 环境中,推荐定义: + +```text +ToolDbProcessPort +``` + +职责: + +- 按 INI 中的 `DB_PROGRAM` 启动受控 runtime; +- 维持 line-based protocol; +- 记录 transcript; +- 将 DB flat file 映射到 OPFS; +- 导出 release/browser diagnostics。 + +不允许: + +- 用 `.tbl` fallback 代替 DB_PROGRAM; +- 用 JS 直接构造 tool semantics; +- 跳过 `tooldb.py` / `db.py` 行为; +- 只靠 browser glue 声称 tool DB pass。 + +建议 first milestone: + +```text +tool-db-opfs-wasm-milestone +``` + +最小完成定义: + +1. Native `ENABLE_TOOL_DB_RUNTIME_PROBE=1` 可在完整 LinuxCNC host runtime 上通过; +2. WASM/Worker 能执行等价 DB protocol loop; +3. OPFS 能保存并恢复 DB flat-file state; +4. transcript 包含 `v2.1` / `g` / `FINI` / `l` / `p` / `u`; +5. browser diagnostics 输出 DB program path、OPFS DB path、transcript hash、state summary; +6. release artifact 标明 `.tbl fallback sufficient=false`; +7. 只有 native + WASM + browser proof 都齐后,才允许考虑把 `axis/db_demo/base.ngc` 从 `SKIP L4-TOOL-DB` 推进。 + +四、L4-PYTHON-REMAP 的移植建议 + +`L4-PYTHON-REMAP` 可以移植,但不应该作为第一个解锁对象。 + +原因: + +- 当前涉及 53 个 skip/block rows; +- 覆盖 Python module loading、remap callable、prolog/epilog、NGC-only subpath、interpreter state、HAL/UI/HALUI assumptions; +- 一旦边界设计不严,很容易把 Python runtime 当作 JS/browser-owned CNC semantics; +- 需要比 tool DB 更深地嵌入 LinuxCNC interpreter Python plugin lifecycle。 + +推荐路线: + +```text +先复用 tool DB milestone 中建立的 Python/WASM substrate; +再做最小 Python remap lifecycle fixture; +最后扩大到 family-level inventory。 +``` + +首个 fixture 仍建议沿用已有 artifact 中的计划: + +```text +axis/remap/stop-lookahead/nc_files +``` + +原因是它更适合作为 lifecycle proof: + +- 有 Python runtime phase; +- 当前已有 native runtime fixture plan; +- 比完整 tool-change / five-axis Python remap family 更窄; +- 可以先证明 Python module import、callable dispatch、interpreter lifecycle 和 state observation。 + +Python remap milestone 的完成定义应至少包括: + +1. `python-remap-native-runtime-readiness.tsv` host readiness clear; +2. `ENABLE_PYTHON_REMAP_RUNTIME_PROBE=1` native lifecycle probe pass; +3. WASM Python runtime 能复现同一 lifecycle; +4. remap/prolog/epilog callable 来源绑定 LinuxCNC config/source; +5. browser diagnostics 明确 Python runtime mode; +6. 不把 NGC-only subroutine asset 当 standalone main program; +7. 不因为 Python runtime 可运行就批量解锁全部 53 rows。 + +五、L4-USER-M-PROCESS 的移植建议 + +`L4-USER-M-PROCESS` 在浏览器中最不适合按 native external process 语义原样移植。 + +当前代表 row: + +```text +axis/vismach/millturn/example.ngc +``` + +核心阻塞是 external user-M process: + +```text +M128 / M129 Tcl scripts +HAL pin state updates +kinstype guard +ini.[xyz].* soft-limit state transitions +``` + +浏览器/WASM 环境不能安全、通用地支持: + +- arbitrary executable spawn; +- native Tcl process execution; +- host HAL daemon mutation; +- unrestricted process side effects。 + +因此不建议把 `L4-USER-M-PROCESS` 定义成“浏览器运行任意 USER_M_PATH executable”。 + +可行路线只能是受控子集: + +```text +LinuxCNC-owned user-M state transition boundary +``` + +例如对 `millturn`: + +- 读取 config-owned `M128` / `M129` source; +- 绑定 `M428 -> M128`、`M429 -> M129`; +- 证明 `kinstype.is-0` / `kinstype.is-1` guard; +- 证明 `ini.x.*` / `ini.y.*` / `ini.z.*` state target; +- browser 只执行已证明的 state-transition adapter; +- diagnostics 明确 `arbitrary_external_process=false`。 + +推荐把它放在第三优先级,原因是: + +- 成功解锁只覆盖 1 个 row; +- 需要 HAL/Tcl/user-M process state proof; +- 不能形成通用 external process 支持; +- 安全边界和用户期望更容易误读。 + +六、总体推荐路线 + +推荐 roadmap: + +```text +Phase A:继续保持 text22 lock +Phase B:L4-TOOL-DB OPFS/WASM proof +Phase C:复用 Python/WASM substrate,做最小 L4-PYTHON-REMAP lifecycle proof +Phase D:做受控 L4-USER-M-PROCESS state-transition proof +``` + +不要反过来做: + +- 不要先做 `L4-USER-M-PROCESS`,因为它不是通用 browser process model; +- 不要直接批量做 `L4-PYTHON-REMAP`,因为 blast radius 太大; +- 不要用 `.tbl` fallback 解 `L4-TOOL-DB`; +- 不要用 virtual HAL 或 JS glue 替代 LinuxCNC CNC/runtime 语义。 + +七、建议的下一步文件/代码工作 + +如果要开启第一个 hard-block runtime milestone,建议创建: + +```text +text24.txt +``` + +主题: + +```text +L4-TOOL-DB OPFS/WASM runtime milestone execution plan +``` + +内容应包括: + +1. `ToolDbProcessPort` API 草案; +2. Worker/Python runtime 选择; +3. OPFS DB file layout; +4. transcript schema; +5. native/WASM/browser proof gates; +6. release diagnostics fields; +7. promotion lock 更新条件; +8. rollback 条件。 + +八、最终建议 + +当前问题的直接答案: + +```text +都可以研究移植; +第一个应该移植 L4-TOOL-DB; +第二个做 L4-PYTHON-REMAP; +第三个做 L4-USER-M-PROCESS 的受控 state-transition 子集。 +``` + +当前阶段仍不应改变: + +```text +L4-PYTHON-REMAP locked +L4-TOOL-DB locked +L4-USER-M-PROCESS locked +promotion_allowed=0 +baseline=28/28/131/0 +``` diff --git a/text24.txt b/text24.txt new file mode 100644 index 0000000..e675fbc --- /dev/null +++ b/text24.txt @@ -0,0 +1,414 @@ +项目接续文件:L4-TOOL-DB OPFS/WASM runtime milestone 执行计划 + +生成时间:2026-06-19 CST + +本文件接替 `text23.txt`,回答“第一个先做 `L4-TOOL-DB`,具体如何做”。 + +重要边界: + +```text +本文件是单独 hard-block runtime milestone 的执行计划。 +不是 text22.txt 当前 evidence-based promotion 阶段的一部分。 +在本 milestone 完成前: +axis/db_demo/base.ngc 仍必须保持 SKIP L4-TOOL-DB; +promotion_allowed=0; +baseline 仍保持 executed=28 passed=28 skipped=131 unexpected_fail=0。 +``` + +一、目标 + +目标不是让 `axis/db_demo/base.ngc` 通过 `.tbl` fallback,也不是用 JavaScript 重新实现 tool semantics。 + +目标是证明: + +```text +LinuxCNC-owned tool DB protocol + DB_PROGRAM behavior + OPFS persistence +可以在 native / WASM / browser proof chain 中成立。 +``` + +最小目标 row: + +```text +axis/db_demo/base.ngc +ini=axis/db_demo/db_nonran.ini +blocked=L4-TOOL-DB +DB_PROGRAM=./db_nonran.py +``` + +必须保留的协议: + +```text +startup handshake: v2.1 +get-all: g ... FINI +spindle load notify: l +tool offset notify: p +spindle unload notify: u +flat-file persistence +``` + +二、当前已有基础 + +已有 source/protocol accounting: + +```text +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-boundary-summary.tsv +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-protocol-gates.tsv +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-transaction-plan.tsv +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-native-protocol-alignment.tsv +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-native-runtime-readiness.tsv +wasm-port/build/wasm/sim-configs-inventory/tool-db-process-native-runtime-probe-gate.tsv +``` + +当前 probe 状态: + +```text +python3=1 +axis/db_demo/db_nonran.py=1 +linuxcnc.so=1 +tooldb.py=1 +linuxcnc=0 +milltask=0 +halcmd=0 +runtime_ready=0 +source_proof_ready=1 +gate_status=blocked_missing_host_runtime +promotion_allowed=0 +``` + +已有 native probe 入口: + +```text +wasm-port/tests/native/probe_tool_db_runtime.sh +``` + +三、Phase 1:native protocol proof 先闭环 + +目的:先证明项目记录的 tool DB transaction contract 与真实 LinuxCNC DB runtime 一致。 + +1. 准备完整 LinuxCNC host runtime。 + +需要 PATH 上存在: + +```text +python3 +linuxcnc +milltask +halcmd +``` + +需要 Python 模块可 import: + +```bash +python3 -c "import linuxcnc; import tooldb; print('linuxcnc_tooldb_python_modules=ok')" +``` + +2. 先跑 readiness,不启用执行。 + +```bash +wasm-port/tests/native/probe_tool_db_runtime.sh +``` + +可接受输出: + +```text +tool_db_runtime_probe_status=ready_disabled_by_default +``` + +如果仍输出: + +```text +tool_db_runtime_probe_status=skipped_missing_host_runtime +``` + +则先修 host/container runtime,不改项目 fallback。 + +3. 显式启用 native runtime probe。 + +```bash +ENABLE_TOOL_DB_RUNTIME_PROBE=1 wasm-port/tests/native/probe_tool_db_runtime.sh +``` + +必须看到: + +```text +tool_db_runtime_probe_status=runtime_protocol_probe_passed +``` + +该 probe 必须证明: + +- `db_nonran.py` 启动并返回 `v2.1`; +- `g` 返回 `T10..T19` 并以 `FINI` 结束; +- 初始 pocket 是 `tno+100`; +- `p t11 p111 d0.33 z0.11` 后 get-all 能看到状态更新; +- `l t14 p0` 后 `T14` 进入 spindle pocket `P0`; +- `u t0 p0` 后 `T14` 回到 `P114`; +- DB savefile 包含 mutation; +- 输出仍保持 `tool_db_execution_enabled=0`、`tool_db_promotion_allowed=0`。 + +4. 刷新 inventory artifacts。 + +```bash +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh +``` + +预期仍不改变 baseline: + +```text +executed=28 +passed=28 +skipped=131 +unexpected_fail=0 +``` + +但 tool DB runtime artifacts 应进入 “native proof ready, waiting for Node/browser proof” 类型状态。 + +四、Phase 2:定义 WASM ToolDbProcessPort + +目的:把 native child-process edge 改成可在 WASM/browser 中验证的协议端口。 + +建议新增抽象: + +```text +ToolDbProcessPort +``` + +建议 API: + +```text +createLinuxCncToolDbProcessPort({ + dbProgramPath, + sourceFiles, + opfsRoot, + runtimeMode, +}) + +start() +writeLine(line) +readLine() +runTransactionPlan(plan) +exportTranscript() +close() +``` + +职责: + +- 按 INI 的 `DB_PROGRAM` 定位 DB program; +- 提供 line-based protocol I/O; +- 记录 transcript; +- 将 DB flat-file 映射到 OPFS; +- 导出 diagnostics artifact。 + +不允许: + +- 解析 `.tbl` 作为 pass; +- JS 直接构造 tool table semantics; +- 跳过 `tooldb.py`; +- 不跑 `db.py` / `db_nonran.py` 回调; +- 只用 fixture transcript 假装 runtime pass。 + +五、Phase 3:选择 DB_PROGRAM runtime + +推荐首选: + +```text +Web Worker + WASM Python runtime +``` + +Worker 内加载: + +```text +configs/sim/axis/db_demo/db_nonran.py +configs/sim/axis/db_demo/db.py +lib/python/tooldb.py +minimal linuxcnc Python integration shim +OPFS-backed DB flat file +``` + +注意: + +```text +minimal linuxcnc shim 只能覆盖 DB demo 必需 integration surface, +例如 linuxcnc.command().load_tool_table 的同步边界记录。 +不能承载 tool semantics。 +``` + +如果暂时没有 Python/WASM runtime,不要直接改成 JS 版 DB program。可以先做: + +```text +tool-db-process-port contract tests +transcript schema +OPFS persistence store +browser diagnostics shell +``` + +但仍保持: + +```text +runtime_execution_ready=0 +promotion_allowed=0 +``` + +六、Phase 4:OPFS persistence layout + +建议 OPFS 路径: + +```text +/machines//tool-db/db_nonran_file +/machines//tool-db/transcripts/.json +``` + +必须验证: + +1. 初次启动可创建 DB file; +2. `p` / `l` / `u` 后 DB file 更新; +3. 关闭 session 后重新加载仍能读回状态; +4. diagnostics 导出: + +```text +db_program_path +opfs_db_path +transcript_hash +db_file_hash +startup_tool_count +mutation_count +tbl_fallback_sufficient=false +promotion_allowed=0 +``` + +七、Phase 5:Node/WASM proof + +新增 Node gate 建议: + +```text +wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh +``` + +该 gate 要证明: + +- 从 INI 解析到 `DB_PROGRAM=./db_nonran.py`; +- ToolDbProcessPort 可启动 runtime; +- transcript 包含 `v2.1` / `g` / `FINI` / `p` / `l` / `u`; +- OPFS-equivalent store 可 roundtrip DB file; +- `.tbl fallback sufficient=false`; +- 输出 `tool_db_process_port_wasm=ok`; +- 不改 `verify_sim_configs_inventory_wasm.sh` baseline。 + +八、Phase 6:browser diagnostics proof + +新增 browser gate 建议: + +```text +wasm-port/tests/browser/verify_tool_db_process_browser.sh +``` + +browser diagnostics 应显示: + +```text +tool_db_runtime_mode=wasm-tool-db-protocol-worker +db_program=./db_nonran.py +opfs_db_path=/machines//tool-db/db_nonran_file +protocol_transcript_ready=true +opfs_persistence_ready=true +tbl_fallback_sufficient=false +promotion_allowed=0 +``` + +UI 只展示状态和 transcript evidence,不实现 tool semantics。 + +九、Phase 7:release artifact / promotion lock + +在 native + Node/WASM + browser proof 都通过前,release artifact 只能显示: + +```text +L4-TOOL-DB locked +tool_db_native_protocol_ready=<0|1> +tool_db_wasm_protocol_ready=<0|1> +tool_db_browser_protocol_ready=<0|1> +promotion_allowed=0 +``` + +只有三层 proof 都为 ready 后,才能另起变更更新: + +```text +runtime-boundary-promotion-readiness.tsv +runtime-boundary-promotion-blockers.tsv +runtime-boundary-post-native-pass-gates.tsv +blocked-runtime-promotion-lock.tsv +promotion-candidates.tsv +``` + +即使到那一步,也要单独 review 是否允许: + +```text +axis/db_demo/base.ngc: SKIP L4-TOOL-DB -> PASS +``` + +十、验收命令建议 + +milestone 初期最小验证: + +```bash +git diff --check +wasm-port/tests/native/probe_tool_db_runtime.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh +wasm-port/tests/sdk/node/verify_project_release_gate_manifest.sh +``` + +具备 host runtime 后: + +```bash +ENABLE_TOOL_DB_RUNTIME_PROBE=1 wasm-port/tests/native/probe_tool_db_runtime.sh +``` + +实现 WASM/OPFS port 后追加: + +```bash +wasm-port/tests/wasm/node/verify_tool_db_process_port_wasm.sh +SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_tool_db_process_browser.sh +``` + +十一、首个代码切入点建议 + +建议第一批代码不要直接改 inventory pass/fail,而是新增只读 protocol/runtime 层: + +```text +wasm-port/runtime/sdk/src/tool-db-process-port.js +wasm-port/runtime/opfs/tool-db-store.js +wasm-port/tests/sdk/node/verify_tool_db_process_port.mjs +wasm-port/tests/opfs/node/verify_tool_db_store.mjs +``` + +第一批只做: + +- transaction plan parser; +- transcript schema; +- OPFS/path model; +- DB file hash; +- diagnostics export; +- `.tbl fallback sufficient=false` guard。 + +第二批再接 Python/WASM Worker runtime。 + +这样可以先建立可测试边界,同时避免误把 JS helper 当作 tool DB runtime pass。 + +十二、结论 + +`L4-TOOL-DB` 的正确做法是: + +```text +先 native protocol proof; +再 ToolDbProcessPort; +再 WASM/Worker DB_PROGRAM runtime; +再 OPFS persistence; +再 browser diagnostics; +最后才考虑 promotion lock 更新。 +``` + +当前不要做: + +```text +不要 .tbl fallback; +不要 JS tool semantics; +不要直接改 baseline; +不要把 protocol contract 当 runtime pass。 +``` diff --git a/wasm-port/docs/project-release-handoff.md b/wasm-port/docs/project-release-handoff.md index 38d7176..b07c494 100644 --- a/wasm-port/docs/project-release-handoff.md +++ b/wasm-port/docs/project-release-handoff.md @@ -265,9 +265,16 @@ release-visible summary without treating it as a runtime unlock: ```text promotion-candidate-artifact=wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv +promotion-candidate-artifact-rows=28 promotion-candidate-layers=evidence-ready=8 inventory-ready=20 promotion-candidate-total=28 +evidence-ready-candidate-rows=8 +evidence-ready-candidate-preview=qtdragon-multi-joint-on-abort +evidence-ready-candidate-preview-gcode=linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc +evidence-ready-candidate-promotion-allowed=0 +evidence-ready-candidate-baseline-changing=no evidence-expansion-candidates=13 +evidence-expansion-artifact-rows=13 evidence-expansion-artifact=wasm-port/build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv promotion-candidate-allowed=0 ``` diff --git a/wasm-port/docs/sim-configs-coverage-handoff.md b/wasm-port/docs/sim-configs-coverage-handoff.md index 77f0e3a..a1a3c28 100644 --- a/wasm-port/docs/sim-configs-coverage-handoff.md +++ b/wasm-port/docs/sim-configs-coverage-handoff.md @@ -44,10 +44,29 @@ The generated artifact baseline is: - `build/wasm/sim-configs-inventory/blocked-dependency-summary.tsv` - `build/wasm/sim-configs-inventory/full-process-boundary-summary.tsv` - `build/wasm/sim-configs-inventory/python-remap-boundary-summary.tsv` +- `build/wasm/sim-configs-inventory/promotion-candidates.tsv` +- `build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv` - `build/wasm/sim-configs-inventory/runtime-boundary-promotion-blockers.tsv` - `build/wasm/sim-configs-inventory/runtime-boundary-host-readiness-rollup.tsv` - `build/wasm/sim-configs-inventory/runtime-boundary-opt-in-probe-dispatch-rollup.tsv` +The current promotion-candidate artifact is evidence accounting only. It has +28 rows total: 8 `evidence-ready` rows with virtual HAL source-derived +browser/release evidence, and 20 `inventory-ready` skipped main-program rows. +Every row keeps `promotion_allowed=0`, so the inventory baseline remains +`28/28/131/0`. The companion `evidence-expansion-candidates.tsv` artifact has +13 rows queued for future browser diagnostics binding, also with +`promotion_allowed=0`. + +SDK and workflow-overview release URL summaries expose the current +`evidence-ready` layer as dashboard fields: `evidence-ready-candidate-rows=8`, +`evidence-ready-candidate-preview=qtdragon-multi-joint-on-abort`, +`evidence-ready-candidate-preview-gcode=linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc`, +`evidence-ready-candidate-promotion-allowed=0`, and +`evidence-ready-candidate-baseline-changing=no`. These fields are visibility +for existing source-derived browser/release evidence, not permission to change +the Node inventory baseline. + ## Skip reason map The TSV artifacts use hyphenated reason names. The shell output uses diff --git a/wasm-port/runtime/sdk/src/project-release-readiness.js b/wasm-port/runtime/sdk/src/project-release-readiness.js index 57e7590..35e51ad 100644 --- a/wasm-port/runtime/sdk/src/project-release-readiness.js +++ b/wasm-port/runtime/sdk/src/project-release-readiness.js @@ -134,20 +134,65 @@ function createPromotionFamilySummaryRows(candidateReport = {}) { function createPromotionCandidateArtifactSummary({ candidateReport = {}, + evidenceExpansionReport = {}, inventory = DEFAULT_SIM_CONFIG_INVENTORY_BASELINE, blockedFamilies = BLOCKED_RUNTIME_FAMILIES, promotedBlockedFamilies = [], + promotionCandidateRows = [], + evidenceExpansionRows = [], } = {}) { const rows = arrayOrEmpty(objectOrEmpty(candidateReport).rows); + const candidateArtifactRows = arrayOrEmpty(promotionCandidateRows); + const evidenceExpansionArtifactRows = arrayOrEmpty(evidenceExpansionRows); const evidenceReadyCount = rows.filter((row) => row.currentNodeInventoryStatus === "PASS" && row.blockedKind === "-" && row.currentMatrixBrowserStatus === "explicit-browser-diagnostics" && row.targetBrowserEvidence === "explicit-browser-diagnostics" ).length; - const inventoryReadyCount = 20; - const evidenceExpansionCandidateCount = 13; - const promotionAllowedCount = 0; + const artifactEvidenceReadyRows = candidateArtifactRows + .filter((row) => row.candidate_kind === "evidence-ready"); + const artifactInventoryReadyRows = candidateArtifactRows + .filter((row) => row.candidate_kind === "inventory-ready"); + const artifactEvidenceReadyCount = artifactEvidenceReadyRows.length; + const inventoryReadyCount = artifactInventoryReadyRows.length > 0 + ? artifactInventoryReadyRows.length + : 20; + const evidenceExpansionCandidateCount = evidenceExpansionArtifactRows.length > 0 + ? evidenceExpansionArtifactRows.length + : 13; + const promotionAllowedCount = candidateArtifactRows.length > 0 + ? candidateArtifactRows.filter((row) => String(row.promotion_allowed ?? row.promotionAllowed) === "1").length + : 0; + const hardBlockRuntimeFamilyRows = artifactInventoryReadyRows + .filter((row) => blockedFamilies.includes(row.skip_kind ?? row.blocked_kind ?? row.blockedKind)) + .map((row, index) => ({ + candidateId: row.candidate_id ?? row.path ?? "unknown", + path: row.path ?? "unknown", + skipKind: row.skip_kind ?? row.blocked_kind ?? row.blockedKind ?? "unknown", + promotionAllowed: String(row.promotion_allowed ?? row.promotionAllowed ?? "0") === "1", + blockReason: row.block_reason ?? row.blockReason ?? "-", + recommendedNextCommand: row.recommended_next_command ?? row.recommendedNextCommand ?? "-", + detailId: `hard-block-runtime-detail-${index + 1}`, + })); + const resolvedEvidenceReadyCount = artifactEvidenceReadyCount > 0 ? artifactEvidenceReadyCount : evidenceReadyCount; + const hardBlockRuntimePromotionAllowedCount = hardBlockRuntimeFamilyRows + .filter((row) => row.promotionAllowed) + .length; + const hardBlockRuntimeFamilySummaryRows = blockedFamilies.map((family) => { + const familyRows = hardBlockRuntimeFamilyRows.filter((row) => row.skipKind === family); + const promotionAllowedRows = familyRows.filter((row) => row.promotionAllowed); + return { + family, + candidateCount: familyRows.length, + promotionAllowedCount: promotionAllowedRows.length, + locked: promotionAllowedRows.length === 0, + lockedPaths: familyRows.map((row) => row.path), + firstPath: familyRows[0]?.path ?? null, + firstBlockReason: familyRows[0]?.blockReason ?? null, + nextProof: familyRows[0]?.recommendedNextCommand ?? null, + }; + }); const hardBlockRuntimeLockSummary = createHardBlockRuntimeLockSummary( blockedFamilies, promotedBlockedFamilies, @@ -167,17 +212,36 @@ function createPromotionCandidateArtifactSummary({ artifactPath: "wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv", evidenceExpansionArtifactPath: "wasm-port/build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv", layerCount: 2, - evidenceReadyCount, + evidenceReadyCount: resolvedEvidenceReadyCount, inventoryReadyCount, evidenceExpansionCandidateCount, - totalCandidateCount: evidenceReadyCount + inventoryReadyCount, + totalCandidateCount: resolvedEvidenceReadyCount + inventoryReadyCount, promotionAllowedCount, hardBlockRuntimeLock: hardBlockRuntimeLockSummary.lockValue, + hardBlockRuntimePromotionAllowedCount, inventoryBaseline: baselineText, + artifactRows: { + promotionCandidates: candidateArtifactRows.length > 0 + ? candidateArtifactRows.length + : resolvedEvidenceReadyCount + inventoryReadyCount, + evidenceExpansion: evidenceExpansionArtifactRows.length > 0 + ? evidenceExpansionArtifactRows.length + : evidenceExpansionCandidateCount, + }, + hardBlockRuntimeFamilyRows, + hardBlockRuntimeFamilyDetailRows: hardBlockRuntimeFamilyRows.map((row) => ({ + id: row.detailId, + family: row.skipKind, + path: row.path, + blockReason: row.blockReason, + nextProof: row.recommendedNextCommand, + promotionAllowed: row.promotionAllowed, + })), + hardBlockRuntimeFamilySummaryRows, layers: [ { id: "evidence-ready", - candidateCount: evidenceReadyCount, + candidateCount: resolvedEvidenceReadyCount, promotionAllowedCount: 0, virtualHalEvidenceReady: true, baselineChanging: false, @@ -193,6 +257,9 @@ function createPromotionCandidateArtifactSummary({ evidenceExpansion: { id: "browser-diagnostics-expansion", candidateCount: evidenceExpansionCandidateCount, + artifactRowCount: evidenceExpansionArtifactRows.length > 0 + ? evidenceExpansionArtifactRows.length + : evidenceExpansionCandidateCount, promotionAllowedCount: 0, baselineChanging: false, nextEvidence: "browser-diagnostics-binding", @@ -1951,6 +2018,9 @@ export function createProjectReleaseReadinessReport({ simConfigInventory = DEFAULT_SIM_CONFIG_INVENTORY_BASELINE, virtualHalSimConfigSourceCoverage = null, virtualHalSimConfigPromotionCandidates = null, + virtualHalSimConfigEvidenceExpansion = null, + promotionCandidateRows = [], + evidenceExpansionRows = [], virtualHalSimConfigMacroLoadFixtures = null, virtualHalMotionControllerMatrix = null, blockedRuntimeFamilies = BLOCKED_RUNTIME_FAMILIES, @@ -2003,9 +2073,12 @@ export function createProjectReleaseReadinessReport({ const hardBlockRuntimeLockSummary = createHardBlockRuntimeLockSummary(blockedFamilies, promotedBlockedFamilies); const promotionCandidateArtifactSummary = createPromotionCandidateArtifactSummary({ candidateReport: virtualHalSimConfigPromotionCandidates, + evidenceExpansionReport: virtualHalSimConfigEvidenceExpansion, inventory, blockedFamilies, promotedBlockedFamilies, + promotionCandidateRows, + evidenceExpansionRows, }); const axisScreenshotArtifactSummary = createAxisScreenshotArtifactSummary(axisScreenshotArtifacts); const axisScreenshotArtifactsReady = axisScreenshotArtifactSummary.ready; @@ -2044,6 +2117,7 @@ export function createProjectReleaseReadinessReport({ virtualHalSimConfigSourceCoverageReady, virtualHalSimConfigPromotionCandidates, virtualHalSimConfigPromotionCandidatesReady, + virtualHalSimConfigEvidenceExpansion, virtualHalPromotionFamilySummary, virtualHalPromotionFamilyRows, promotionCandidateArtifactSummary, @@ -2087,6 +2161,16 @@ export function createProjectReleaseReadinessReport({ label: "Promotion candidate artifact", value: promotionCandidateArtifactSummary.artifactPath, }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: `${promotionCandidateArtifactSummary.artifactRows.promotionCandidates}`, + }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: `${promotionCandidateArtifactSummary.artifactRows.evidenceExpansion}`, + }, { id: "promotion-candidate-layers", label: "Promotion candidate layers", @@ -2388,6 +2472,13 @@ export function createProjectReleaseReadinessArtifactValidationSummaryViewModel( ? `${validation.promotionCandidateArtifactSummary.totalCandidateCount}` : "missing", }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: validation?.promotionCandidateArtifactSummaryReady === true + ? `${validation.promotionCandidateArtifactSummary.artifactRows.promotionCandidates}` + : "missing", + }, { id: "evidence-expansion-candidates", label: "Evidence expansion candidates", @@ -2395,6 +2486,13 @@ export function createProjectReleaseReadinessArtifactValidationSummaryViewModel( ? `${validation.promotionCandidateArtifactSummary.evidenceExpansionCandidateCount}` : "missing", }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: validation?.promotionCandidateArtifactSummaryReady === true + ? `${validation.promotionCandidateArtifactSummary.artifactRows.evidenceExpansion}` + : "missing", + }, { id: "evidence-expansion-next-evidence", label: "Evidence expansion next evidence", @@ -2837,6 +2935,17 @@ export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel ) { const missing = arrayOrEmpty(workflow?.missing); const promotionCandidateSummary = objectOrEmpty(workflow?.diagnosticsArtifact?.virtualHalPromotionCandidateSummary); + const promotionCandidateReportRows = arrayOrEmpty( + workflow?.diagnosticsArtifact?.virtualHalSimConfigPromotionCandidates?.rows, + ); + const evidenceReadyCandidateRows = promotionCandidateReportRows.filter((row) => + row?.complete === true && + row?.currentNodeInventoryStatus === "PASS" && + row?.blockedKind === "-" && + row?.targetBrowserEvidence === "explicit-browser-diagnostics" && + row?.currentMatrixBrowserStatus === "explicit-browser-diagnostics" + ); + const preferredEvidenceReadyCandidate = evidenceReadyCandidateRows[0] ?? {}; const promotionCandidateSummaryReady = promotionCandidateSummary.ready === true; const validationRows = arrayOrEmpty(workflow?.validation?.rows); const hardBlockRuntimeLock = validationRows.find(({ id }) => id === "hard-block-runtime-lock")?.value; @@ -2844,6 +2953,16 @@ export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel const promotionCandidateArtifactSummary = objectOrEmpty(workflow?.validation?.promotionCandidateArtifactSummary); const promotionCandidateArtifactSummaryReady = workflow?.validation?.promotionCandidateArtifactSummaryReady === true; + const promotionArtifactRows = objectOrEmpty(promotionCandidateArtifactSummary.artifactRows); + const hardBlockRuntimeFamilyRows = arrayOrEmpty(promotionCandidateArtifactSummary.hardBlockRuntimeFamilyRows); + const hardBlockRuntimeFamilySummaryRows = arrayOrEmpty( + promotionCandidateArtifactSummary.hardBlockRuntimeFamilySummaryRows, + ); + const hardBlockRuntimeFamilyDetailRows = arrayOrEmpty( + promotionCandidateArtifactSummary.hardBlockRuntimeFamilyDetailRows, + ); + const hardBlockRuntimeDetailPreviewRows = hardBlockRuntimeFamilyDetailRows.slice(0, 5); + const hardBlockRuntimePreviewRows = hardBlockRuntimeFamilyRows.slice(0, 3); const evidenceExpansionRows = arrayOrEmpty( workflow?.diagnosticsArtifact?.virtualHalSimConfigEvidenceExpansion?.rows, ); @@ -2910,6 +3029,38 @@ export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel ? `${promotionCandidateArtifactSummary.totalCandidateCount}` : "not provided", }, + { + id: "evidence-ready-candidate-rows", + label: "Evidence-ready candidate rows", + value: `${evidenceReadyCandidateRows.length}`, + }, + { + id: "evidence-ready-candidate-preview", + label: "Evidence-ready candidate preview", + value: preferredEvidenceReadyCandidate.id ?? "not provided", + }, + { + id: "evidence-ready-candidate-preview-gcode", + label: "Evidence-ready candidate preview G-code", + value: preferredEvidenceReadyCandidate.gcodePath ?? "not provided", + }, + { + id: "evidence-ready-candidate-promotion-allowed", + label: "Evidence-ready candidate promotion allowed", + value: "0", + }, + { + id: "evidence-ready-candidate-baseline-changing", + label: "Evidence-ready candidate baseline changing", + value: "no", + }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: promotionCandidateArtifactSummaryReady + ? `${promotionArtifactRows.promotionCandidates ?? "not provided"}` + : "not provided", + }, { id: "evidence-expansion-candidates", label: "Evidence expansion candidates", @@ -2917,6 +3068,13 @@ export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel ? `${promotionCandidateArtifactSummary.evidenceExpansionCandidateCount}` : "not provided", }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: promotionCandidateArtifactSummaryReady + ? `${promotionArtifactRows.evidenceExpansion ?? "not provided"}` + : "not provided", + }, { id: "evidence-expansion-next-evidence", label: "Evidence expansion next evidence", @@ -2985,6 +3143,82 @@ export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel ? `${promotionCandidateArtifactSummary.promotionAllowedCount}` : "not provided", }, + { + id: "hard-block-runtime-locked-rows", + label: "Hard-block locked inventory rows", + value: promotionCandidateArtifactSummaryReady + ? `${hardBlockRuntimeFamilyRows.length}` + : "not provided", + }, + { + id: "hard-block-runtime-locked-preview", + label: "Hard-block locked row preview", + value: hardBlockRuntimePreviewRows.length > 0 + ? hardBlockRuntimePreviewRows.map((row) => `${row.skipKind}:${row.path}`).join(", ") + : "not provided", + }, + { + id: "hard-block-runtime-promotion-allowed", + label: "Hard-block promotions allowed", + value: promotionCandidateArtifactSummaryReady + ? `${promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount ?? "not provided"}` + : "not provided", + }, + { + id: "hard-block-runtime-family-count", + label: "Hard-block runtime families", + value: promotionCandidateArtifactSummaryReady + ? `${hardBlockRuntimeFamilySummaryRows.length}` + : "not provided", + }, + { + id: "hard-block-runtime-family-summary", + label: "Hard-block runtime family summary", + value: hardBlockRuntimeFamilySummaryRows.length > 0 + ? hardBlockRuntimeFamilySummaryRows + .map((row) => `${row.family}:${row.candidateCount} locked=${row.locked ? "yes" : "no"}`) + .join(", ") + : "not provided", + }, + { + id: "hard-block-runtime-detail-row-count", + label: "Hard-block detail rows", + value: promotionCandidateArtifactSummaryReady + ? `${hardBlockRuntimeFamilyDetailRows.length}` + : "not provided", + }, + ...hardBlockRuntimeDetailPreviewRows.map((row, index) => ({ + id: `hard-block-runtime-detail-preview-${index + 1}`, + label: `Hard-block detail ${index + 1}`, + value: `${row.family}:${row.path}; reason=${row.blockReason}; next=${row.nextProof}; promotion_allowed=${row.promotionAllowed ? "1" : "0"}`, + family: row.family, + path: row.path, + blockReason: row.blockReason, + nextProof: row.nextProof, + promotionAllowed: row.promotionAllowed, + })), + ...hardBlockRuntimeFamilySummaryRows.map((row) => ({ + id: `hard-block-runtime-family-${row.family}`, + label: `Hard-block family ${row.family}`, + value: [ + `${row.candidateCount} locked rows`, + `promotion_allowed=${row.promotionAllowedCount}`, + `first=${row.firstPath ?? "not provided"}`, + `reason=${row.firstBlockReason ?? "not provided"}`, + `next=${row.nextProof ?? "not provided"}`, + ].join("; "), + firstPath: row.firstPath, + firstBlockReason: row.firstBlockReason, + nextProof: row.nextProof, + })), + ...hardBlockRuntimeFamilySummaryRows.map((row) => ({ + id: `hard-block-runtime-family-${row.family}-paths`, + label: `Hard-block family ${row.family} paths`, + value: arrayOrEmpty(row.lockedPaths).length > 0 + ? row.lockedPaths.join(", ") + : "not provided", + lockedPaths: arrayOrEmpty(row.lockedPaths), + })), ...arrayOrEmpty(workflow?.validation?.virtualHalPromotionFamilyRows).map((row) => ({ id: `virtual-hal-promotion-family-${row.id}`, label: `Promotion family ${row.label}`, @@ -3242,10 +3476,12 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) { const promotionCandidateArtifactSummary = objectOrEmpty(artifactObject.promotionCandidateArtifactSummary); const expectedPromotionCandidateArtifactSummary = createPromotionCandidateArtifactSummary({ candidateReport: virtualHalSimConfigPromotionCandidates, + evidenceExpansionReport: artifactObject.virtualHalSimConfigEvidenceExpansion, inventory: simConfigInventory, blockedFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies), promotedBlockedFamilies: arrayOrEmpty(artifactObject.promotedBlockedFamilies), }); + const artifactRows = objectOrEmpty(promotionCandidateArtifactSummary.artifactRows); const promotionCandidateArtifactSummaryReady = promotionCandidateArtifactSummary.apiName === expectedPromotionCandidateArtifactSummary.apiName && promotionCandidateArtifactSummary.summaryVersion === 1 && @@ -3259,7 +3495,15 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) { promotionCandidateArtifactSummary.totalCandidateCount === expectedPromotionCandidateArtifactSummary.totalCandidateCount && promotionCandidateArtifactSummary.promotionAllowedCount === 0 && promotionCandidateArtifactSummary.hardBlockRuntimeLock === expectedPromotionCandidateArtifactSummary.hardBlockRuntimeLock && + promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount === 0 && + Number.isFinite(promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount) && promotionCandidateArtifactSummary.inventoryBaseline === expectedPromotionCandidateArtifactSummary.inventoryBaseline && + artifactRows.promotionCandidates === + promotionCandidateArtifactSummary.evidenceReadyCount + promotionCandidateArtifactSummary.inventoryReadyCount && + ( + artifactRows.evidenceExpansion === 0 || + artifactRows.evidenceExpansion === promotionCandidateArtifactSummary.evidenceExpansionCandidateCount + ) && arrayOrEmpty(promotionCandidateArtifactSummary.layers).some((row) => row.id === "evidence-ready" && row.candidateCount === expectedPromotionCandidateArtifactSummary.evidenceReadyCount && @@ -3442,6 +3686,13 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) { ? `${promotionCandidateArtifactSummary.totalCandidateCount}` : "missing", }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: promotionCandidateArtifactSummaryReady + ? `${artifactRows.promotionCandidates}` + : "missing", + }, { id: "evidence-expansion-candidates", label: "Evidence expansion candidates", @@ -3449,6 +3700,13 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) { ? `${promotionCandidateArtifactSummary.evidenceExpansionCandidateCount}` : "missing", }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: promotionCandidateArtifactSummaryReady + ? `${artifactRows.evidenceExpansion}` + : "missing", + }, { id: "evidence-expansion-next-evidence", label: "Evidence expansion next evidence", diff --git a/wasm-port/runtime/ui/ini-panel/ui-shell.js b/wasm-port/runtime/ui/ini-panel/ui-shell.js index 3a6ddfc..3d839e4 100644 --- a/wasm-port/runtime/ui/ini-panel/ui-shell.js +++ b/wasm-port/runtime/ui/ini-panel/ui-shell.js @@ -2556,6 +2556,21 @@ export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWo ?? workflow?.urlWorkflow?.diagnosticsArtifact?.virtualHalPromotionCandidateSummary ?? {} ); + const promotionCandidateReportRows = ( + workflow?.diagnosticsArtifact?.virtualHalSimConfigPromotionCandidates?.rows + ?? workflow?.urlWorkflow?.diagnosticsArtifact?.virtualHalSimConfigPromotionCandidates?.rows + ?? [] + ); + const evidenceReadyCandidateRows = Array.isArray(promotionCandidateReportRows) + ? promotionCandidateReportRows.filter((row) => + row?.complete === true && + row?.currentNodeInventoryStatus === "PASS" && + row?.blockedKind === "-" && + row?.targetBrowserEvidence === "explicit-browser-diagnostics" && + row?.currentMatrixBrowserStatus === "explicit-browser-diagnostics" + ) + : []; + const preferredEvidenceReadyCandidate = evidenceReadyCandidateRows[0] ?? {}; const promotionCandidateSummaryReady = promotionCandidateSummary.ready === true; const validationRows = Array.isArray(workflow?.validation?.rows) ? workflow.validation.rows @@ -2567,6 +2582,18 @@ export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWo ?? workflow?.validation?.validation?.promotionCandidateArtifactSummary ?? {} ); + const promotionArtifactRows = promotionCandidateArtifactSummary.artifactRows ?? {}; + const hardBlockRuntimeFamilyRows = Array.isArray(promotionCandidateArtifactSummary.hardBlockRuntimeFamilyRows) + ? promotionCandidateArtifactSummary.hardBlockRuntimeFamilyRows + : []; + const hardBlockRuntimeFamilySummaryRows = Array.isArray(promotionCandidateArtifactSummary.hardBlockRuntimeFamilySummaryRows) + ? promotionCandidateArtifactSummary.hardBlockRuntimeFamilySummaryRows + : []; + const hardBlockRuntimeFamilyDetailRows = Array.isArray(promotionCandidateArtifactSummary.hardBlockRuntimeFamilyDetailRows) + ? promotionCandidateArtifactSummary.hardBlockRuntimeFamilyDetailRows + : []; + const hardBlockRuntimeDetailPreviewRows = hardBlockRuntimeFamilyDetailRows.slice(0, 5); + const hardBlockRuntimePreviewRows = hardBlockRuntimeFamilyRows.slice(0, 3); const evidenceExpansion = promotionCandidateArtifactSummary.evidenceExpansion ?? {}; const evidenceExpansionRows = ( workflow?.diagnosticsArtifact?.virtualHalSimConfigEvidenceExpansion?.rows @@ -2677,6 +2704,31 @@ export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWo ? `${promotionCandidateSummary.explicitBrowserDiagnosticsCount} diagnostics-ready` : "not provided", }, + { + id: "evidence-ready-candidate-rows", + label: "Evidence-ready candidate rows", + value: `${evidenceReadyCandidateRows.length}`, + }, + { + id: "evidence-ready-candidate-preview", + label: "Evidence-ready candidate preview", + value: preferredEvidenceReadyCandidate.id ?? "not provided", + }, + { + id: "evidence-ready-candidate-preview-gcode", + label: "Evidence-ready candidate preview G-code", + value: preferredEvidenceReadyCandidate.gcodePath ?? "not provided", + }, + { + id: "evidence-ready-candidate-promotion-allowed", + label: "Evidence-ready candidate promotion allowed", + value: "0", + }, + { + id: "evidence-ready-candidate-baseline-changing", + label: "Evidence-ready candidate baseline changing", + value: "no", + }, { id: "virtual-hal-promotion-candidate-baseline", label: "Candidate inventory baseline", @@ -2687,6 +2739,20 @@ export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWo label: "Evidence expansion next evidence", value: evidenceExpansion.nextEvidence ?? "not provided", }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: promotionArtifactRows.promotionCandidates == null + ? "not provided" + : `${promotionArtifactRows.promotionCandidates}`, + }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: promotionArtifactRows.evidenceExpansion == null + ? "not provided" + : `${promotionArtifactRows.evidenceExpansion}`, + }, { id: "evidence-expansion-baseline-changing", label: "Evidence expansion baseline changing", @@ -2744,6 +2810,76 @@ export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWo label: "Promoted blocked families", value: promotedBlockedFamilyCount ?? "not provided", }, + { + id: "hard-block-runtime-locked-rows", + label: "Hard-block locked inventory rows", + value: `${hardBlockRuntimeFamilyRows.length}`, + }, + { + id: "hard-block-runtime-locked-preview", + label: "Hard-block locked row preview", + value: hardBlockRuntimePreviewRows.length > 0 + ? hardBlockRuntimePreviewRows.map((row) => `${row.skipKind}:${row.path}`).join(", ") + : "not provided", + }, + { + id: "hard-block-runtime-promotion-allowed", + label: "Hard-block promotions allowed", + value: promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount == null + ? "not provided" + : `${promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount}`, + }, + { + id: "hard-block-runtime-family-count", + label: "Hard-block runtime families", + value: `${hardBlockRuntimeFamilySummaryRows.length}`, + }, + { + id: "hard-block-runtime-family-summary", + label: "Hard-block runtime family summary", + value: hardBlockRuntimeFamilySummaryRows.length > 0 + ? hardBlockRuntimeFamilySummaryRows + .map((row) => `${row.family}:${row.candidateCount} locked=${row.locked ? "yes" : "no"}`) + .join(", ") + : "not provided", + }, + { + id: "hard-block-runtime-detail-row-count", + label: "Hard-block detail rows", + value: `${hardBlockRuntimeFamilyDetailRows.length}`, + }, + ...hardBlockRuntimeDetailPreviewRows.map((row, index) => ({ + id: `hard-block-runtime-detail-preview-${index + 1}`, + label: `Hard-block detail ${index + 1}`, + value: `${row.family}:${row.path}; reason=${row.blockReason}; next=${row.nextProof}; promotion_allowed=${row.promotionAllowed ? "1" : "0"}`, + family: row.family, + path: row.path, + blockReason: row.blockReason, + nextProof: row.nextProof, + promotionAllowed: row.promotionAllowed, + })), + ...hardBlockRuntimeFamilySummaryRows.map((row) => ({ + id: `hard-block-runtime-family-${row.family}`, + label: `Hard-block family ${row.family}`, + value: [ + `${row.candidateCount} locked rows`, + `promotion_allowed=${row.promotionAllowedCount}`, + `first=${row.firstPath ?? "not provided"}`, + `reason=${row.firstBlockReason ?? "not provided"}`, + `next=${row.nextProof ?? "not provided"}`, + ].join("; "), + firstPath: row.firstPath, + firstBlockReason: row.firstBlockReason, + nextProof: row.nextProof, + })), + ...hardBlockRuntimeFamilySummaryRows.map((row) => ({ + id: `hard-block-runtime-family-${row.family}-paths`, + label: `Hard-block family ${row.family} paths`, + value: Array.isArray(row.lockedPaths) && row.lockedPaths.length > 0 + ? row.lockedPaths.join(", ") + : "not provided", + lockedPaths: Array.isArray(row.lockedPaths) ? row.lockedPaths : [], + })), { id: "virtual-hal-promotion-families", label: "Virtual HAL promotion families", @@ -3171,6 +3307,19 @@ export function createIniPanelShellWorkflowOverviewPromotionFamilyRowsRenderStat status: "manifest-backed", })) ); + const candidateRows = sourceRows + .filter((row) => row.sourceKind === "gcode") + .map((row, index) => ({ + id: `evidence-candidate-${index + 1}`, + familyId: row.familyId, + label: `Evidence candidate ${index + 1}`, + value: row.value, + candidateStatus: "evidence-ready", + promotionAllowed: false, + baselineChanging: false, + sourceKind: row.sourceKind, + manifestBacked: row.manifestBacked, + })); const ready = rows.length > 0 && rows.every(({ value }) => String(value).includes("diagnostics=ready")); return { apiName: "ini-panel-shell-workflow-overview-promotion-family-rows-render-state", @@ -3183,7 +3332,9 @@ export function createIniPanelShellWorkflowOverviewPromotionFamilyRowsRenderStat statusLine: `${ready ? "Ready" : "Blocked"}: ${ready ? `${rows.length} promotion families ready` : "promotion family rows missing"}`, rows, sourceRows, + candidateRows, sourceFileCount: sourceRows.length, + candidateCount: candidateRows.length, dataset: { handoffPhase: ready ? "ready" : "blocked", handoffReady: ready ? "true" : "false", @@ -3212,6 +3363,10 @@ export function createIniPanelShellWorkflowOverviewPromotionFamilySourceFilterVi const filteredSourceRows = selectedSourceKind === "all" ? familyScopedSourceRows : familyScopedSourceRows.filter((sourceRow) => sourceRow.sourceKind === selectedSourceKind); + const candidateRows = Array.isArray(renderState?.candidateRows) ? renderState.candidateRows : []; + const filteredCandidateRows = selectedFamilyId === "all" + ? candidateRows + : candidateRows.filter((candidateRow) => candidateRow.familyId === selectedFamilyId); const familyFilters = [ { id: "all", @@ -3255,6 +3410,7 @@ export function createIniPanelShellWorkflowOverviewPromotionFamilySourceFilterVi .filter((row) => row.sourceFileCount > 0); const ready = renderState?.ready === true && totalSourceFileCount > 0; const selectedCount = filteredSourceRows.length; + const selectedCandidateCount = filteredCandidateRows.length; return { apiName: "ini-panel-shell-workflow-overview-promotion-family-source-filter-view-model", viewModelVersion: 1, @@ -3273,8 +3429,11 @@ export function createIniPanelShellWorkflowOverviewPromotionFamilySourceFilterVi totalSourceFileCount, scopedSourceFileCount, filteredSourceFileCount: selectedCount, + totalCandidateCount: candidateRows.length, + filteredCandidateCount: selectedCandidateCount, familyRows, sourceRows: filteredSourceRows, + candidateRows: filteredCandidateRows, }; } @@ -3633,12 +3792,17 @@ export function renderIniPanelShellWorkflowOverviewPromotionFamilySourceFilterSt })), ]; const sourceRows = Array.isArray(viewModel?.sourceRows) ? viewModel.sourceRows : []; + const candidateRows = Array.isArray(viewModel?.candidateRows) ? viewModel.candidateRows : []; const rows = [ ...filterRows, ...sourceRows.map((sourceRow) => ({ ...sourceRow, rowType: "source-row", })), + ...candidateRows.map((candidateRow) => ({ + ...candidateRow, + rowType: "candidate-row", + })), ]; for (const row of rows) { @@ -3655,6 +3819,11 @@ export function renderIniPanelShellWorkflowOverviewPromotionFamilySourceFilterSt if (row.sourceKind) { term.dataset[`${effectiveRowDatasetPrefix}SourceKind`] = row.sourceKind; } + if (row.candidateStatus) { + term.dataset[`${effectiveRowDatasetPrefix}CandidateStatus`] = row.candidateStatus; + term.dataset[`${effectiveRowDatasetPrefix}PromotionAllowed`] = row.promotionAllowed ? "1" : "0"; + term.dataset[`${effectiveRowDatasetPrefix}BaselineChanging`] = row.baselineChanging ? "yes" : "no"; + } term.textContent = row.label ?? row.id; const detail = documentRef.createElement("dd"); @@ -3670,9 +3839,18 @@ export function renderIniPanelShellWorkflowOverviewPromotionFamilySourceFilterSt if (row.sourceKind) { detail.dataset[`${effectiveRowDatasetPrefix}SourceKind`] = row.sourceKind; } + if (row.candidateStatus) { + detail.dataset[`${effectiveRowDatasetPrefix}CandidateStatus`] = row.candidateStatus; + detail.dataset[`${effectiveRowDatasetPrefix}PromotionAllowed`] = row.promotionAllowed ? "1" : "0"; + detail.dataset[`${effectiveRowDatasetPrefix}BaselineChanging`] = row.baselineChanging ? "yes" : "no"; + } detail.textContent = row.rowType === "source-row" ? row.value - : `${row.value}: ${row.count ?? 0}${row.selected ? " selected" : ""}`; + : ( + row.rowType === "candidate-row" + ? row.value + : `${row.value}: ${row.count ?? 0}${row.selected ? " selected" : ""}` + ); rowsNode.append(term, detail); } @@ -3683,8 +3861,10 @@ export function renderIniPanelShellWorkflowOverviewPromotionFamilySourceFilterSt controlCount: controlSpecs.length, filterRowCount: filterRows.length, sourceFileCount: sourceRows.length, + candidateCount: candidateRows.length, rowIds: rows.map(({ id }) => id), sourceRowIds: sourceRows.map(({ id }) => id), + candidateRowIds: candidateRows.map(({ id }) => id), dataset: { handoffPhase: mountNode.dataset.handoffPhase, handoffReady: mountNode.dataset.handoffReady, @@ -4005,6 +4185,19 @@ export function createIniPanelShellWorkflowOverviewPromotionCandidateDrilldownVi virtualHalReportsReady && sourceFiles.length > 0; const summaryCandidateId = candidateId ?? "not provided"; + const candidateEvidenceReady = candidate?.complete === true && + blockedFamilyExcluded && + nodeInventoryReady && + browserEvidenceReady; + const promotionCandidateLayers = + findWorkflowOverviewRowValue(summaryViewModel, "promotion-candidate-layers") ?? + (candidateEvidenceReady ? "evidence-ready" : "not provided"); + const promotionCandidateAllowed = + findWorkflowOverviewRowValue(summaryViewModel, "promotion-candidate-allowed") ?? + (candidateEvidenceReady ? "0" : "not provided"); + const promotionBaselineChanging = promotionCandidateAllowed === "0" + ? "no" + : (promotionCandidateAllowed === "not provided" ? "not provided" : "yes"); const rows = [ { id: "candidate", @@ -4036,6 +4229,23 @@ export function createIniPanelShellWorkflowOverviewPromotionCandidateDrilldownVi label: "Blocked-family exclusion", value: blockedFamilyExcluded ? "passed" : (candidate?.blockedKind ?? "not provided"), }, + { + id: "candidate-layer", + label: "Candidate layer", + value: promotionCandidateLayers.includes("evidence-ready") + ? "evidence-ready" + : promotionCandidateLayers, + }, + { + id: "inventory-promotion-allowed", + label: "Inventory promotion allowed", + value: promotionCandidateAllowed, + }, + { + id: "inventory-baseline-changing", + label: "Inventory baseline changing", + value: promotionBaselineChanging, + }, { id: "virtual-hal-reports", label: "Required virtual HAL reports", @@ -4353,6 +4563,309 @@ export function mountIniPanelShellWorkflowOverviewPromotionCandidateDrilldownSta } } +export function createIniPanelShellWorkflowOverviewHardBlockDrilldownViewModel( + summaryViewModel = createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel(), + { + validation = null, + } = {}, +) { + const summary = validation?.promotionCandidateArtifactSummary ?? {}; + const detailRows = Array.isArray(summary.hardBlockRuntimeFamilyDetailRows) + ? summary.hardBlockRuntimeFamilyDetailRows + : []; + const familyRows = Array.isArray(summary.hardBlockRuntimeFamilySummaryRows) + ? summary.hardBlockRuntimeFamilySummaryRows + : []; + const promotionAllowedCount = Number(summary.hardBlockRuntimePromotionAllowedCount ?? 0); + const ready = detailRows.length > 0 && promotionAllowedCount === 0; + const rows = [ + { + id: "detail-row-count", + label: "Hard-block detail rows", + value: `${detailRows.length}`, + }, + { + id: "family-count", + label: "Hard-block families", + value: `${familyRows.length}`, + }, + { + id: "promotion-allowed", + label: "Promotion allowed", + value: `${promotionAllowedCount}`, + }, + { + id: "inventory-baseline", + label: "Inventory baseline", + value: findWorkflowOverviewRowValue(summaryViewModel, "virtual-hal-promotion-candidate-baseline") + ?? summary.inventoryBaseline + ?? "not provided", + }, + ]; + const familyDetailRows = detailRows.map((row, index) => ({ + id: row.id ?? `hard-block-detail-${index + 1}`, + label: `${row.family} ${index + 1}`, + value: row.path ?? "not provided", + family: row.family ?? "not provided", + path: row.path ?? "not provided", + blockReason: row.blockReason ?? "not provided", + nextProof: row.nextProof ?? "not provided", + promotionAllowed: row.promotionAllowed === true, + })); + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-view-model", + viewModelVersion: 1, + phase: ready ? "ready" : "blocked", + ready, + title: "Hard-block drilldown", + statusText: ready ? "Ready" : "Blocked", + detailText: ready + ? `${detailRows.length} locked rows grouped by ${familyRows.length} families` + : "hard-block detail rows missing", + statusLine: `${ready ? "Ready" : "Blocked"}: ${ready ? `${detailRows.length} locked rows grouped by ${familyRows.length} families` : "hard-block detail rows missing"}`, + rows, + detailRows: familyDetailRows, + rowCount: rows.length + familyDetailRows.length, + detailRowCount: familyDetailRows.length, + familyCount: familyRows.length, + promotionAllowedCount, + dataset: { + handoffPhase: ready ? "ready" : "blocked", + handoffReady: ready ? "true" : "false", + handoffScope: "workflow-overview-hard-block-drilldown", + }, + }; +} + +export function createIniPanelShellWorkflowOverviewHardBlockDrilldownRenderState( + viewModel = createIniPanelShellWorkflowOverviewHardBlockDrilldownViewModel(), +) { + const rows = [ + ...(Array.isArray(viewModel?.rows) ? viewModel.rows : []), + ...(Array.isArray(viewModel?.detailRows) ? viewModel.detailRows : []).map((row) => ({ + ...row, + label: `${row.family}: ${row.path}`, + value: `${row.blockReason}; next=${row.nextProof}; promotion_allowed=${row.promotionAllowed ? "1" : "0"}`, + hardBlockDetail: true, + })), + ].map(({ id, label, value, family, path, blockReason, nextProof, promotionAllowed, hardBlockDetail }) => ({ + id, + label, + value: value == null ? "" : String(value), + ...(family ? { family } : {}), + ...(path ? { path } : {}), + ...(blockReason ? { blockReason } : {}), + ...(nextProof ? { nextProof } : {}), + ...(promotionAllowed != null ? { promotionAllowed: promotionAllowed === true } : {}), + ...(hardBlockDetail != null ? { hardBlockDetail: hardBlockDetail === true } : {}), + })); + const ready = viewModel?.ready === true; + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-render-state", + renderStateVersion: 1, + phase: ready ? "ready" : "blocked", + ready, + title: viewModel?.title ?? "Hard-block drilldown", + statusText: viewModel?.statusText ?? (ready ? "Ready" : "Blocked"), + detailText: viewModel?.detailText ?? "hard-block drilldown missing", + statusLine: viewModel?.statusLine ?? `${ready ? "Ready" : "Blocked"}: hard-block drilldown`, + rows, + rowCount: rows.length, + detailRowCount: viewModel?.detailRowCount ?? 0, + familyCount: viewModel?.familyCount ?? 0, + promotionAllowedCount: viewModel?.promotionAllowedCount ?? 0, + dataset: viewModel?.dataset ?? { + handoffPhase: ready ? "ready" : "blocked", + handoffReady: ready ? "true" : "false", + handoffScope: "workflow-overview-hard-block-drilldown", + }, + }; +} + +export function createIniPanelShellWorkflowOverviewHardBlockDrilldownDomContract({ + rowDatasetPrefix = "workflowOverviewHardBlockDrilldown", + selectors = {}, +} = {}) { + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-dom-contract", + contractVersion: 1, + requiredNodeIds: ["mount", "status", "rows"], + selectors: { + mount: selectors.mount ?? "[data-workflow-overview-hard-block-drilldown]", + status: selectors.status ?? "[data-workflow-overview-hard-block-drilldown-status]", + rows: selectors.rows ?? "[data-workflow-overview-hard-block-drilldown-rows]", + }, + mountDatasetKeys: ["handoffPhase", "handoffReady", "handoffScope"], + rowDatasetPrefix, + }; +} + +export function createIniPanelShellWorkflowOverviewHardBlockDrilldownDomReadiness({ + rowDatasetPrefix = "workflowOverviewHardBlockDrilldown", + contract = createIniPanelShellWorkflowOverviewHardBlockDrilldownDomContract({ rowDatasetPrefix }), + documentRef = globalThis.document, + mountNode = documentRef?.querySelector?.(contract.selectors.mount), + statusNode = documentRef?.querySelector?.(contract.selectors.status), + rowsNode = documentRef?.querySelector?.(contract.selectors.rows), +} = {}) { + const checks = { + document: Boolean(documentRef?.createElement), + mount: Boolean(mountNode), + status: Boolean(statusNode), + rows: Boolean(rowsNode), + rowDatasetPrefix: Boolean(contract.rowDatasetPrefix), + }; + const missing = Object.entries(checks) + .filter(([, passed]) => !passed) + .map(([name]) => name); + const ready = missing.length === 0; + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-dom-readiness", + readinessVersion: 1, + phase: ready ? "ready" : "blocked", + ready, + missing, + checks, + contract, + }; +} + +export function renderIniPanelShellWorkflowOverviewHardBlockDrilldownState( + renderState = createIniPanelShellWorkflowOverviewHardBlockDrilldownRenderState(), + { + rowDatasetPrefix = "workflowOverviewHardBlockDrilldown", + contract = createIniPanelShellWorkflowOverviewHardBlockDrilldownDomContract({ rowDatasetPrefix }), + documentRef = globalThis.document, + mountNode = documentRef?.querySelector?.(contract.selectors.mount), + statusNode = documentRef?.querySelector?.(contract.selectors.status), + rowsNode = documentRef?.querySelector?.(contract.selectors.rows), + } = {}, +) { + const readiness = createIniPanelShellWorkflowOverviewHardBlockDrilldownDomReadiness({ + contract, + documentRef, + mountNode, + statusNode, + rowsNode, + }); + if (!readiness.ready) { + throw new Error( + `Workflow overview hard-block drilldown DOM is not ready: ${readiness.missing.join(", ")}`, + ); + } + mountNode.dataset.handoffPhase = renderState?.dataset?.handoffPhase ?? "blocked"; + mountNode.dataset.handoffReady = renderState?.dataset?.handoffReady ?? "false"; + mountNode.dataset.handoffScope = renderState?.dataset?.handoffScope + ?? "workflow-overview-hard-block-drilldown"; + statusNode.textContent = renderState?.statusLine ?? "Blocked: hard-block drilldown missing"; + rowsNode.replaceChildren(); + const effectiveRowDatasetPrefix = contract.rowDatasetPrefix; + const rows = Array.isArray(renderState?.rows) ? renderState.rows : []; + for (const row of rows) { + const term = documentRef.createElement("dt"); + term.dataset[`${effectiveRowDatasetPrefix}Row`] = row.id; + term.dataset[`${effectiveRowDatasetPrefix}Status`] = renderState?.ready ? "pass" : "blocked"; + if (row.family) { + term.dataset[`${effectiveRowDatasetPrefix}Family`] = row.family; + term.dataset[`${effectiveRowDatasetPrefix}Path`] = row.path ?? ""; + term.dataset[`${effectiveRowDatasetPrefix}PromotionAllowed`] = row.promotionAllowed ? "1" : "0"; + term.dataset[`${effectiveRowDatasetPrefix}HardBlockDetail`] = row.hardBlockDetail ? "true" : "false"; + } + term.textContent = row.label; + const detail = documentRef.createElement("dd"); + detail.dataset[`${effectiveRowDatasetPrefix}Value`] = row.id; + detail.dataset[`${effectiveRowDatasetPrefix}Status`] = renderState?.ready ? "pass" : "blocked"; + if (row.family) { + detail.dataset[`${effectiveRowDatasetPrefix}Family`] = row.family; + detail.dataset[`${effectiveRowDatasetPrefix}Path`] = row.path ?? ""; + detail.dataset[`${effectiveRowDatasetPrefix}PromotionAllowed`] = row.promotionAllowed ? "1" : "0"; + detail.dataset[`${effectiveRowDatasetPrefix}HardBlockDetail`] = row.hardBlockDetail ? "true" : "false"; + } + detail.textContent = row.value; + rowsNode.append(term, detail); + } + return { + rendered: true, + statusLine: statusNode.textContent, + rowCount: rows.length, + rowIds: rows.map(({ id }) => id), + detailRowCount: rows.filter(({ hardBlockDetail }) => hardBlockDetail === true).length, + familyCount: renderState?.familyCount ?? 0, + promotionAllowedCount: renderState?.promotionAllowedCount ?? 0, + dataset: { + handoffPhase: mountNode.dataset.handoffPhase, + handoffReady: mountNode.dataset.handoffReady, + handoffScope: mountNode.dataset.handoffScope, + }, + }; +} + +export function mountIniPanelShellWorkflowOverviewHardBlockDrilldownState({ + renderState = createIniPanelShellWorkflowOverviewHardBlockDrilldownRenderState(), + rowDatasetPrefix = "workflowOverviewHardBlockDrilldown", + contract = createIniPanelShellWorkflowOverviewHardBlockDrilldownDomContract({ rowDatasetPrefix }), + documentRef = globalThis.document, + mountNode = documentRef?.querySelector?.(contract.selectors.mount), + statusNode = documentRef?.querySelector?.(contract.selectors.status), + rowsNode = documentRef?.querySelector?.(contract.selectors.rows), +} = {}) { + const domReadiness = createIniPanelShellWorkflowOverviewHardBlockDrilldownDomReadiness({ + contract, + documentRef, + mountNode, + statusNode, + rowsNode, + }); + if (!domReadiness.ready) { + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-dom-mount", + workflowVersion: 1, + phase: "blocked", + ready: false, + statusLine: `Blocked: ${domReadiness.missing.join(", ")}`, + contract, + domReadiness, + renderState, + renderResult: null, + error: null, + }; + } + try { + const renderResult = renderIniPanelShellWorkflowOverviewHardBlockDrilldownState(renderState, { + contract, + documentRef, + mountNode, + statusNode, + rowsNode, + }); + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-dom-mount", + workflowVersion: 1, + phase: renderState?.phase === "ready" ? "ready" : "blocked", + ready: renderState?.ready === true, + statusLine: renderResult.statusLine, + contract, + domReadiness, + renderState, + renderResult, + error: null, + }; + } catch (error) { + return { + apiName: "ini-panel-shell-workflow-overview-hard-block-drilldown-dom-mount", + workflowVersion: 1, + phase: "blocked", + ready: false, + statusLine: `Blocked: ${error?.message ?? "hard-block drilldown DOM mount error"}`, + contract, + domReadiness, + renderState, + renderResult: null, + error: error?.message ?? String(error), + }; + } +} + export function createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowDomContract({ rowDatasetPrefix = "workflowOverviewReleaseReadinessArtifactUrlWorkflow", selectors = {}, diff --git a/wasm-port/tests/browser/ini_panel_workflow_overview_smoke.html b/wasm-port/tests/browser/ini_panel_workflow_overview_smoke.html index 504b287..8609018 100644 --- a/wasm-port/tests/browser/ini_panel_workflow_overview_smoke.html +++ b/wasm-port/tests/browser/ini_panel_workflow_overview_smoke.html @@ -612,7 +612,7 @@ ); assertEqual( releaseArtifactUrlWorkflowRenderResult.rowCount, - 29, + 53, "workflow overview release artifact URL workflow render rows", ); assertEqual( @@ -650,11 +650,46 @@ "13", "workflow overview release artifact URL workflow evidence expansion count", ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "promotion-candidate-artifact-rows")?.value, + "28", + "workflow overview release artifact URL workflow promotion candidate artifact rows", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-artifact-rows")?.value, + "13", + "workflow overview release artifact URL workflow evidence expansion artifact rows", + ); assertEqual( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-next-evidence")?.value, "browser-diagnostics-binding", "workflow overview release artifact URL workflow evidence expansion next evidence", ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-rows")?.value, + "8", + "workflow overview release artifact URL workflow evidence-ready candidate rows", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview")?.value, + "qtdragon-multi-joint-on-abort", + "workflow overview release artifact URL workflow evidence-ready candidate preview", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview-gcode")?.value, + "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc", + "workflow overview release artifact URL workflow evidence-ready candidate preview gcode", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-promotion-allowed")?.value, + "0", + "workflow overview release artifact URL workflow evidence-ready promotion allowed", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-baseline-changing")?.value, + "no", + "workflow overview release artifact URL workflow evidence-ready baseline changing", + ); assertEqual( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-baseline-changing")?.value, "no", @@ -750,6 +785,51 @@ "0 promoted", "workflow overview release artifact URL workflow promoted blocked count", ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-locked-rows")?.value, + "19", + "workflow overview release artifact URL workflow locked hard-block rows", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-locked-preview")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc, L4-PYTHON-REMAP:axis/laser/raster_test.ngc, L4-PYTHON-REMAP:axis/laser/vector_test.ngc", + "workflow overview release artifact URL workflow locked hard-block preview", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-promotion-allowed")?.value, + "0", + "workflow overview release artifact URL workflow hard-block promotion allowed count", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-count")?.value, + "3", + "workflow overview release artifact URL workflow hard-block family count", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-summary")?.value, + "L4-USER-M-PROCESS:1 locked=yes, L4-TOOL-DB:1 locked=yes, L4-PYTHON-REMAP:17 locked=yes", + "workflow overview release artifact URL workflow hard-block family summary", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-detail-row-count")?.value, + "19", + "workflow overview release artifact URL workflow hard-block detail count", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-detail-preview-1")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put; promotion_allowed=0", + "workflow overview release artifact URL workflow hard-block detail preview", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-USER-M-PROCESS")?.value, + "1 locked rows; promotion_allowed=0; first=axis/vismach/millturn/example.ngc; reason=design_m128_m129_linuxcnc_state_boundary; next=kinstype_guard_and_ini_xyz_hal_pin_state", + "workflow overview release artifact URL workflow user-M family row", + ); + assertEqual( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-USER-M-PROCESS-paths")?.value, + "axis/vismach/millturn/example.ngc", + "workflow overview release artifact URL workflow user-M family paths", + ); assertEqual( releaseArtifactUrlWorkflowRenderResult.rowIds.includes("saved-session-diagnostics"), true, @@ -876,6 +956,21 @@ "0", "workflow overview fixed URL workflow form candidate allowed row", ); + assertEqual( + workflowDoc.querySelector('[data-workflow-overview-release-readiness-artifact-url-workflow-value="evidence-ready-candidate-rows"]').textContent, + "8", + "workflow overview fixed URL workflow form evidence-ready candidate rows", + ); + assertEqual( + workflowDoc.querySelector('[data-workflow-overview-release-readiness-artifact-url-workflow-value="evidence-ready-candidate-preview"]').textContent, + "qtdragon-multi-joint-on-abort", + "workflow overview fixed URL workflow form evidence-ready candidate preview", + ); + assertEqual( + workflowDoc.querySelector('[data-workflow-overview-release-readiness-artifact-url-workflow-value="evidence-ready-candidate-promotion-allowed"]').textContent, + "0", + "workflow overview fixed URL workflow form evidence-ready promotion allowed", + ); assertEqual( workflowDoc.querySelector('[data-workflow-overview-release-readiness-artifact-url-workflow-value="evidence-expansion-candidates"]').textContent, "13", @@ -943,6 +1038,27 @@ 16, "workflow overview promotion candidate drilldown evidence-ready DOM nodes", ); + assertEqual( + workflowDoc.querySelector( + '[data-workflow-overview-promotion-candidate-drilldown-value="candidate-layer"]', + )?.textContent, + "evidence-ready", + "workflow overview promotion candidate drilldown candidate layer row", + ); + assertEqual( + workflowDoc.querySelector( + '[data-workflow-overview-promotion-candidate-drilldown-value="inventory-promotion-allowed"]', + )?.textContent, + "0", + "workflow overview promotion candidate drilldown promotion allowed row", + ); + assertEqual( + workflowDoc.querySelector( + '[data-workflow-overview-promotion-candidate-drilldown-value="inventory-baseline-changing"]', + )?.textContent, + "no", + "workflow overview promotion candidate drilldown baseline changing row", + ); assertEqual( workflowDoc.querySelector( '[data-workflow-overview-promotion-candidate-drilldown-value="evidence-7"]', @@ -1020,6 +1136,13 @@ 4, "workflow overview source filter changed source DOM nodes", ); + assertEqual( + workflowDoc.querySelectorAll( + '[data-workflow-overview-promotion-family-source-filter-type="candidate-row"]', + ).length, + 2, + "workflow overview source filter changed candidate DOM nodes", + ); assertEqual( workflowDoc.querySelector( '[data-workflow-overview-promotion-family-source-filter-value="linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc"]', @@ -1027,6 +1150,20 @@ "linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc", "workflow overview source filter changed rose engine source row", ); + assertEqual( + workflowDoc.querySelector( + '[data-workflow-overview-promotion-family-source-filter-type="candidate-row"][data-workflow-overview-promotion-family-source-filter-value="linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc"]', + )?.dataset.workflowOverviewPromotionFamilySourceFilterCandidateStatus, + "evidence-ready", + "workflow overview source filter changed rose engine candidate status", + ); + assertEqual( + workflowDoc.querySelector( + '[data-workflow-overview-promotion-family-source-filter-type="candidate-row"][data-workflow-overview-promotion-family-source-filter-value="linuxcnc/configs/sim/axis/rose_engine/rcone_demo.ngc"]', + )?.dataset.workflowOverviewPromotionFamilySourceFilterPromotionAllowed, + "0", + "workflow overview source filter changed rose engine promotion allowed", + ); assertEqual( releaseArtifactJsonWorkflow.statusLine, "Ready: Artifact JSON workflow complete", diff --git a/wasm-port/tests/fixtures/project-release-readiness-ready.json b/wasm-port/tests/fixtures/project-release-readiness-ready.json index cdb00dc..024ca8d 100644 --- a/wasm-port/tests/fixtures/project-release-readiness-ready.json +++ b/wasm-port/tests/fixtures/project-release-readiness-ready.json @@ -656,6 +656,170 @@ ] }, "virtualHalSimConfigPromotionCandidatesReady": true, + "virtualHalSimConfigEvidenceExpansion": { + "apiName": "linuxcnc-wasm-virtual-hal-sim-config-evidence-expansion-report", + "reportVersion": 1, + "source": "linuxcnc-configs-sim-source-derived-virtual-hal", + "phase": "ready", + "complete": true, + "webSimulationSatisfied": true, + "inventoryBaselineUnchanged": true, + "promotionAllowed": false, + "manifestChecked": true, + "candidateCount": 3, + "readyCandidateCount": 3, + "sourceFiles": [ + "linuxcnc/configs/sim/woodpecker/woodpecker.ini", + "linuxcnc/configs/sim/woodpecker/on_abort.ngc", + "linuxcnc/configs/sim/woodpecker/tool.tbl", + "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini", + "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc", + "linuxcnc/configs/sim/axis/vismach/puma/remap_subs/428remap.ngc", + "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini", + "linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc", + "linuxcnc/configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc" + ], + "requiredVirtualHalReports": [ + "source-compliance", + "sim-config-source-coverage", + "command-script-fixtures", + "manifest-backed-motion-matrix" + ], + "availableVirtualHalReports": [ + "source-compliance", + "sim-config-source-coverage", + "command-script-fixtures", + "manifest-backed-motion-matrix" + ], + "missingCandidates": [], + "promotionAllowedViolations": [], + "blockedCandidateIds": [], + "missingManifestFiles": [], + "missingRequiredReports": [], + "rows": [ + { + "id": "woodpecker-on-abort", + "label": "Woodpecker on-abort", + "source": "linuxcnc-configs-sim-source-derived-virtual-hal-evidence-expansion-candidate", + "simConfigTarget": "woodpecker", + "sourceFiles": [ + "linuxcnc/configs/sim/woodpecker/woodpecker.ini", + "linuxcnc/configs/sim/woodpecker/on_abort.ngc", + "linuxcnc/configs/sim/woodpecker/tool.tbl" + ], + "iniPath": "linuxcnc/configs/sim/woodpecker/woodpecker.ini", + "gcodePath": "linuxcnc/configs/sim/woodpecker/on_abort.ngc", + "currentNodeInventoryStatus": "PASS", + "currentMatrixBrowserStatus": "REP", + "dependencyClass": "hal_process,ui_process", + "targetBrowserEvidence": "browser-diagnostics-expansion", + "requiredVirtualHalReports": [ + "source-compliance", + "sim-config-source-coverage", + "command-script-fixtures", + "manifest-backed-motion-matrix" + ], + "blockedKind": "-", + "promotionAllowed": false, + "nodeStatusReady": true, + "browserRepresentativeReady": true, + "manifestChecked": true, + "missingManifestFiles": [], + "missingRequiredReports": [], + "evidence": "Current inventory already covers Woodpecker as a non-hard-block representative; next step is explicit browser diagnostics binding only.", + "sourceReady": true, + "complete": true + }, + { + "id": "puma-cube", + "label": "PUMA cube", + "source": "linuxcnc-configs-sim-source-derived-virtual-hal-evidence-expansion-candidate", + "simConfigTarget": "vismach-remap-sims", + "sourceFiles": [ + "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini", + "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc", + "linuxcnc/configs/sim/axis/vismach/puma/remap_subs/428remap.ngc" + ], + "iniPath": "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ini", + "gcodePath": "linuxcnc/configs/sim/axis/vismach/puma/puma_cube.ngc", + "currentNodeInventoryStatus": "PASS", + "currentMatrixBrowserStatus": "REP", + "dependencyClass": "hal_process,halui_mdi_process,ui_process", + "targetBrowserEvidence": "browser-diagnostics-expansion", + "requiredVirtualHalReports": [ + "source-compliance", + "sim-config-source-coverage", + "command-script-fixtures", + "manifest-backed-motion-matrix" + ], + "blockedKind": "-", + "promotionAllowed": false, + "nodeStatusReady": true, + "browserRepresentativeReady": true, + "manifestChecked": true, + "missingManifestFiles": [], + "missingRequiredReports": [], + "evidence": "PUMA cube is already the representative browser row for the PUMA machine context; it remains an expansion candidate, not a baseline promotion.", + "sourceReady": true, + "complete": true + }, + { + "id": "melfa-example", + "label": "MELFA example", + "source": "linuxcnc-configs-sim-source-derived-virtual-hal-evidence-expansion-candidate", + "simConfigTarget": "vismach-remap-sims", + "sourceFiles": [ + "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini", + "linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc", + "linuxcnc/configs/sim/axis/vismach/melfa-sim/remap_subs/428remap.ngc" + ], + "iniPath": "linuxcnc/configs/sim/axis/vismach/melfa-sim/melfa.ini", + "gcodePath": "linuxcnc/configs/sim/axis/vismach/melfa-sim/example.ngc", + "currentNodeInventoryStatus": "PASS", + "currentMatrixBrowserStatus": "REP", + "dependencyClass": "hal_process,halui_mdi_process,ui_process", + "targetBrowserEvidence": "browser-diagnostics-expansion", + "requiredVirtualHalReports": [ + "source-compliance", + "sim-config-source-coverage", + "command-script-fixtures", + "manifest-backed-motion-matrix" + ], + "blockedKind": "-", + "promotionAllowed": false, + "nodeStatusReady": true, + "browserRepresentativeReady": true, + "manifestChecked": true, + "missingManifestFiles": [], + "missingRequiredReports": [], + "evidence": "MELFA example has current representative coverage and can receive explicit browser diagnostics evidence without unlocking any hard runtime family.", + "sourceReady": true, + "complete": true + } + ], + "summaryRows": [ + { + "id": "candidates", + "label": "Expansion candidates", + "value": "3" + }, + { + "id": "ready-candidates", + "label": "Ready candidates", + "value": "3" + }, + { + "id": "promotion-allowed", + "label": "Promotion allowed", + "value": "0" + }, + { + "id": "inventory-baseline", + "label": "Inventory baseline", + "value": "unchanged" + } + ] + }, "virtualHalPromotionFamilySummary": "qtdragon-on-abort: 6/6 ready; vismach-remap-sims: 1/1 ready; rose-engine-rcone-demo: 1/1 ready", "virtualHalPromotionFamilyRows": [ { @@ -726,7 +890,393 @@ "totalCandidateCount": 28, "promotionAllowedCount": 0, "hardBlockRuntimeLock": "locked (3 families)", + "hardBlockRuntimePromotionAllowedCount": 0, "inventoryBaseline": "executed=28 passed=28 skipped=131 unexpected_fail=0", + "artifactRows": { + "promotionCandidates": 28, + "evidenceExpansion": 13 + }, + "hardBlockRuntimeFamilyRows": [ + { + "candidateId": "axis/db_demo/base.ngc", + "path": "axis/db_demo/base.ngc", + "skipKind": "L4-TOOL-DB", + "promotionAllowed": false, + "blockReason": "design_tooldata_db_protocol_boundary", + "recommendedNextCommand": "db_program_v2_1_handshake_getall_load_unload_or_put", + "detailId": "hard-block-runtime-detail-1" + }, + { + "candidateId": "axis/laser/raster_test.ngc", + "path": "axis/laser/raster_test.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-2" + }, + { + "candidateId": "axis/laser/vector_test.ngc", + "path": "axis/laser/vector_test.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-3" + }, + { + "candidateId": "axis/laser/vector_test2.ngc", + "path": "axis/laser/vector_test2.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-4" + }, + { + "candidateId": "axis/remap/cycle/nc_files/examples.ngc", + "path": "axis/remap/cycle/nc_files/examples.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-5" + }, + { + "candidateId": "axis/remap/extend-builtins/nc_files/examples.ngc", + "path": "axis/remap/extend-builtins/nc_files/examples.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-6" + }, + { + "candidateId": "axis/remap/getting-started/nc_files/examples.ngc", + "path": "axis/remap/getting-started/nc_files/examples.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-7" + }, + { + "candidateId": "axis/remap/manual-toolchange-with-tool-length-switch/nc_files/tcdemo.ngc", + "path": "axis/remap/manual-toolchange-with-tool-length-switch/nc_files/tcdemo.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-8" + }, + { + "candidateId": "axis/remap/rack-toolchange/nc_files/tcdemo.ngc", + "path": "axis/remap/rack-toolchange/nc_files/tcdemo.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-9" + }, + { + "candidateId": "axis/remap/stop-lookahead/nc_files/examples.ngc", + "path": "axis/remap/stop-lookahead/nc_files/examples.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-10" + }, + { + "candidateId": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/circular_pocket.ngc", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/circular_pocket.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-11" + }, + { + "candidateId": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_back_and_forth.ngc", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_back_and_forth.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-12" + }, + { + "candidateId": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-13" + }, + { + "candidateId": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/simple_example.ngc", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/simple_example.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-14" + }, + { + "candidateId": "axis/vismach/millturn/example.ngc", + "path": "axis/vismach/millturn/example.ngc", + "skipKind": "L4-USER-M-PROCESS", + "promotionAllowed": false, + "blockReason": "design_m128_m129_linuxcnc_state_boundary", + "recommendedNextCommand": "kinstype_guard_and_ini_xyz_hal_pin_state", + "detailId": "hard-block-runtime-detail-15" + }, + { + "candidateId": "axis/vismach/VMC_toolchange/toolchange.ngc", + "path": "axis/vismach/VMC_toolchange/toolchange.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-16" + }, + { + "candidateId": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-17" + }, + { + "candidateId": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-18" + }, + { + "candidateId": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc", + "skipKind": "L4-PYTHON-REMAP", + "promotionAllowed": false, + "blockReason": "design_linuxcnc_python_runtime_boundary", + "recommendedNextCommand": "python_runtime_owner_and_fixture", + "detailId": "hard-block-runtime-detail-19" + } + ], + "hardBlockRuntimeFamilyDetailRows": [ + { + "id": "hard-block-runtime-detail-1", + "family": "L4-TOOL-DB", + "path": "axis/db_demo/base.ngc", + "blockReason": "design_tooldata_db_protocol_boundary", + "nextProof": "db_program_v2_1_handshake_getall_load_unload_or_put", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-2", + "family": "L4-PYTHON-REMAP", + "path": "axis/laser/raster_test.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-3", + "family": "L4-PYTHON-REMAP", + "path": "axis/laser/vector_test.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-4", + "family": "L4-PYTHON-REMAP", + "path": "axis/laser/vector_test2.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-5", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/cycle/nc_files/examples.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-6", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/extend-builtins/nc_files/examples.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-7", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/getting-started/nc_files/examples.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-8", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/manual-toolchange-with-tool-length-switch/nc_files/tcdemo.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-9", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/rack-toolchange/nc_files/tcdemo.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-10", + "family": "L4-PYTHON-REMAP", + "path": "axis/remap/stop-lookahead/nc_files/examples.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-11", + "family": "L4-PYTHON-REMAP", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/circular_pocket.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-12", + "family": "L4-PYTHON-REMAP", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_back_and_forth.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-13", + "family": "L4-PYTHON-REMAP", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-14", + "family": "L4-PYTHON-REMAP", + "path": "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/simple_example.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-15", + "family": "L4-USER-M-PROCESS", + "path": "axis/vismach/millturn/example.ngc", + "blockReason": "design_m128_m129_linuxcnc_state_boundary", + "nextProof": "kinstype_guard_and_ini_xyz_hal_pin_state", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-16", + "family": "L4-PYTHON-REMAP", + "path": "axis/vismach/VMC_toolchange/toolchange.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-17", + "family": "L4-PYTHON-REMAP", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-18", + "family": "L4-PYTHON-REMAP", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + }, + { + "id": "hard-block-runtime-detail-19", + "family": "L4-PYTHON-REMAP", + "path": "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc", + "blockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture", + "promotionAllowed": false + } + ], + "hardBlockRuntimeFamilySummaryRows": [ + { + "family": "L4-USER-M-PROCESS", + "candidateCount": 1, + "promotionAllowedCount": 0, + "locked": true, + "lockedPaths": [ + "axis/vismach/millturn/example.ngc" + ], + "firstPath": "axis/vismach/millturn/example.ngc", + "firstBlockReason": "design_m128_m129_linuxcnc_state_boundary", + "nextProof": "kinstype_guard_and_ini_xyz_hal_pin_state" + }, + { + "family": "L4-TOOL-DB", + "candidateCount": 1, + "promotionAllowedCount": 0, + "locked": true, + "lockedPaths": [ + "axis/db_demo/base.ngc" + ], + "firstPath": "axis/db_demo/base.ngc", + "firstBlockReason": "design_tooldata_db_protocol_boundary", + "nextProof": "db_program_v2_1_handshake_getall_load_unload_or_put" + }, + { + "family": "L4-PYTHON-REMAP", + "candidateCount": 17, + "promotionAllowedCount": 0, + "locked": true, + "lockedPaths": [ + "axis/laser/raster_test.ngc", + "axis/laser/vector_test.ngc", + "axis/laser/vector_test2.ngc", + "axis/remap/cycle/nc_files/examples.ngc", + "axis/remap/extend-builtins/nc_files/examples.ngc", + "axis/remap/getting-started/nc_files/examples.ngc", + "axis/remap/manual-toolchange-with-tool-length-switch/nc_files/tcdemo.ngc", + "axis/remap/rack-toolchange/nc_files/tcdemo.ngc", + "axis/remap/stop-lookahead/nc_files/examples.ngc", + "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/circular_pocket.ngc", + "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_back_and_forth.ngc", + "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc", + "axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/simple_example.ngc", + "axis/vismach/VMC_toolchange/toolchange.ngc", + "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc", + "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc", + "gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc" + ], + "firstPath": "axis/laser/raster_test.ngc", + "firstBlockReason": "design_linuxcnc_python_runtime_boundary", + "nextProof": "python_runtime_owner_and_fixture" + } + ], "layers": [ { "id": "evidence-ready", @@ -746,6 +1296,7 @@ "evidenceExpansion": { "id": "browser-diagnostics-expansion", "candidateCount": 13, + "artifactRowCount": 13, "promotionAllowedCount": 0, "baselineChanging": false, "nextEvidence": "browser-diagnostics-binding" @@ -2332,6 +2883,16 @@ "label": "Promotion candidate artifact", "value": "wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv" }, + { + "id": "promotion-candidate-artifact-rows", + "label": "Promotion candidate artifact rows", + "value": "28" + }, + { + "id": "evidence-expansion-artifact-rows", + "label": "Evidence expansion artifact rows", + "value": "13" + }, { "id": "promotion-candidate-layers", "label": "Promotion candidate layers", @@ -2347,6 +2908,16 @@ "label": "Evidence expansion candidates", "value": "13" }, + { + "id": "evidence-expansion-next-evidence", + "label": "Evidence expansion next evidence", + "value": "browser-diagnostics-binding" + }, + { + "id": "evidence-expansion-baseline-changing", + "label": "Evidence expansion baseline changing", + "value": "no" + }, { "id": "evidence-expansion-artifact", "label": "Evidence expansion artifact", diff --git a/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs b/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs index cbd7821..2e2018e 100644 --- a/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs +++ b/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs @@ -189,6 +189,11 @@ assert.equal(artifact.promotionCandidateArtifactSummary.inventoryReadyCount, 20) assert.equal(artifact.promotionCandidateArtifactSummary.evidenceExpansionCandidateCount, 13); assert.equal(artifact.promotionCandidateArtifactSummary.totalCandidateCount, 28); assert.equal(artifact.promotionCandidateArtifactSummary.promotionAllowedCount, 0); +assert.equal(artifact.promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount, 0); +assert.deepEqual(artifact.promotionCandidateArtifactSummary.artifactRows, { + promotionCandidates: 28, + evidenceExpansion: 13, +}); assert.equal(artifact.promotionCandidateArtifactSummary.evidenceExpansion.promotionAllowedCount, 0); assert.equal( artifact.promotionCandidateArtifactSummary.inventoryBaseline, diff --git a/wasm-port/tests/host/write_project_release_readiness_artifact.mjs b/wasm-port/tests/host/write_project_release_readiness_artifact.mjs index dc19fd7..df35e88 100644 --- a/wasm-port/tests/host/write_project_release_readiness_artifact.mjs +++ b/wasm-port/tests/host/write_project_release_readiness_artifact.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { createProjectReleaseReadinessReport, + createVirtualHalSimConfigEvidenceExpansionReport, createVirtualHalMotionControllerMatrixReport, createVirtualHalSimConfigMacroLoadFixtureReport, createVirtualHalSimConfigPromotionCandidateReport, @@ -17,6 +18,8 @@ const sourceManifestText = readFileSync(resolve(root, "tools/source-manifest.txt const axisScreenshotArtifactDir = process.env.AXIS_SCREENSHOT_ARTIFACT_DIR ? resolve(process.env.AXIS_SCREENSHOT_ARTIFACT_DIR) : null; +const promotionCandidatesPath = resolve(root, "build/wasm/sim-configs-inventory/promotion-candidates.tsv"); +const evidenceExpansionCandidatesPath = resolve(root, "build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"); const passedGateResults = { "diff-check": true, @@ -59,6 +62,23 @@ function loadAxisScreenshotArtifacts(directory) { }); } +function parseTsvArtifactRows(path) { + if (!existsSync(path)) { + return []; + } + const [headerLine, ...dataLines] = readFileSync(path, "utf8").trim().split(/\r?\n/); + if (!headerLine) { + return []; + } + const headers = headerLine.split("\t"); + return dataLines + .filter((line) => line.trim().length > 0) + .map((line) => { + const values = line.split("\t"); + return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])); + }); +} + const report = createProjectReleaseReadinessReport({ gateResults: passedGateResults, observedOutputs: [ @@ -80,6 +100,13 @@ const report = createProjectReleaseReadinessReport({ simConfigSourceCoverage: virtualHalSimConfigSourceCoverage, motionControllerMatrix: virtualHalMotionControllerMatrix, }), + virtualHalSimConfigEvidenceExpansion: createVirtualHalSimConfigEvidenceExpansionReport({ + manifestText: sourceManifestText, + simConfigSourceCoverage: virtualHalSimConfigSourceCoverage, + motionControllerMatrix: virtualHalMotionControllerMatrix, + }), + promotionCandidateRows: parseTsvArtifactRows(promotionCandidatesPath), + evidenceExpansionRows: parseTsvArtifactRows(evidenceExpansionCandidatesPath), virtualHalSimConfigMacroLoadFixtures: createVirtualHalSimConfigMacroLoadFixtureReport({ manifestText: sourceManifestText, }), diff --git a/wasm-port/tests/native/probe_millturn_user_m_runtime.sh b/wasm-port/tests/native/probe_millturn_user_m_runtime.sh old mode 100644 new mode 100755 index 4fff612..333fae9 --- a/wasm-port/tests/native/probe_millturn_user_m_runtime.sh +++ b/wasm-port/tests/native/probe_millturn_user_m_runtime.sh @@ -22,7 +22,22 @@ cleanup() { trap cleanup EXIT command_path() { - command -v "$1" 2>/dev/null || true + local command_name="$1" + local path + path="$(command -v "$command_name" 2>/dev/null || true)" + if [[ -n "$path" ]]; then + printf '%s\n' "$path" + return + fi + + case "$command_name" in + linuxcnc|halrun) + [[ -x "$LINUXCNC_ROOT/scripts/$command_name" ]] && printf '%s\n' "$LINUXCNC_ROOT/scripts/$command_name" + ;; + halcmd) + [[ -x "$LINUXCNC_ROOT/bin/$command_name" ]] && printf '%s\n' "$LINUXCNC_ROOT/bin/$command_name" + ;; + esac } csv_join() { diff --git a/wasm-port/tests/native/probe_python_remap_runtime.sh b/wasm-port/tests/native/probe_python_remap_runtime.sh index 2109e2c..878131e 100755 --- a/wasm-port/tests/native/probe_python_remap_runtime.sh +++ b/wasm-port/tests/native/probe_python_remap_runtime.sh @@ -14,7 +14,19 @@ required_commands=(python3 linuxcnc) missing_requirements=() command_path() { - command -v "$1" 2>/dev/null || true + local command_name="$1" + local path + path="$(command -v "$command_name" 2>/dev/null || true)" + if [[ -n "$path" ]]; then + printf '%s\n' "$path" + return + fi + + case "$command_name" in + linuxcnc) + [[ -x "$LINUXCNC_ROOT/scripts/linuxcnc" ]] && printf '%s\n' "$LINUXCNC_ROOT/scripts/linuxcnc" + ;; + esac } csv_join() { diff --git a/wasm-port/tests/native/probe_tool_db_runtime.sh b/wasm-port/tests/native/probe_tool_db_runtime.sh old mode 100644 new mode 100755 index 35f394b..b9ee7e6 --- a/wasm-port/tests/native/probe_tool_db_runtime.sh +++ b/wasm-port/tests/native/probe_tool_db_runtime.sh @@ -14,7 +14,22 @@ required_commands=(python3 linuxcnc milltask halcmd) missing_requirements=() command_path() { - command -v "$1" 2>/dev/null || true + local command_name="$1" + local path + path="$(command -v "$command_name" 2>/dev/null || true)" + if [[ -n "$path" ]]; then + printf '%s\n' "$path" + return + fi + + case "$command_name" in + linuxcnc) + [[ -x "$LINUXCNC_ROOT/scripts/linuxcnc" ]] && printf '%s\n' "$LINUXCNC_ROOT/scripts/linuxcnc" + ;; + milltask|halcmd) + [[ -x "$LINUXCNC_ROOT/bin/$command_name" ]] && printf '%s\n' "$LINUXCNC_ROOT/bin/$command_name" + ;; + esac } csv_join() { @@ -108,7 +123,11 @@ import time db_program, db_demo_dir, python_path, db_savefile, stdout_log, stderr_log = sys.argv[1:] env = os.environ.copy() +linuxcnc_root = os.path.dirname(os.path.dirname(python_path)) env["PYTHONPATH"] = python_path + (os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "") +env["LD_LIBRARY_PATH"] = os.path.join(linuxcnc_root, "lib") + ( + os.pathsep + env["LD_LIBRARY_PATH"] if env.get("LD_LIBRARY_PATH") else "" +) stdout_file = open(stdout_log, "w", encoding="utf-8") stderr_file = open(stderr_log, "w", encoding="utf-8") @@ -119,31 +138,41 @@ proc = subprocess.Popen( stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr_file, - text=True, - bufsize=1, + bufsize=0, ) +stdout_buffer = b"" +line_queue = [] + def emit(name, value): print(f"{name}={value}") def read_line(timeout=5.0): + global stdout_buffer + if line_queue: + return line_queue.pop(0) deadline = time.monotonic() + timeout while time.monotonic() < deadline: ready, _, _ = select.select([proc.stdout], [], [], 0.1) if ready: - line = proc.stdout.readline() - if line == "": + chunk = os.read(proc.stdout.fileno(), 4096) + if chunk == b"": raise RuntimeError("db_program_stdout_closed") - line = line.rstrip("\n") - stdout_file.write(line + "\n") - stdout_file.flush() - return line + stdout_buffer += chunk + while b"\n" in stdout_buffer: + raw_line, stdout_buffer = stdout_buffer.split(b"\n", 1) + line = raw_line.decode("utf-8", errors="replace").rstrip("\r") + stdout_file.write(line + "\n") + stdout_file.flush() + line_queue.append(line) + if line_queue: + return line_queue.pop(0) if proc.poll() is not None: raise RuntimeError(f"db_program_exited_{proc.returncode}") raise RuntimeError("timeout_waiting_for_db_program_reply") def send(command): - proc.stdin.write(command + "\n") + proc.stdin.write((command + "\n").encode("utf-8")) proc.stdin.flush() def read_until_fini(command, timeout=5.0): @@ -157,6 +186,10 @@ def read_until_fini(command, timeout=5.0): return lines raise RuntimeError(f"timeout_waiting_for_fini_{command.split()[0]}") +def read_single_reply(command, timeout=5.0): + send(command) + return read_line(timeout) + def require(condition, message): if not condition: raise RuntimeError(message) @@ -187,14 +220,14 @@ try: load_reply = read_until_fini("l t14 p0") emit("tool_db_load_spindle_fini", int(load_reply[-1].startswith("FINI"))) - t14_loaded = read_until_fini("t 14") - require(any(line.startswith("T14 ") and "P0" in line for line in t14_loaded), "load_spindle_state_drift") + t14_loaded = read_single_reply("t 14") + require(t14_loaded.startswith("T14 ") and "P0" in t14_loaded, "load_spindle_state_drift") emit("tool_db_load_spindle_state_ok", 1) unload_reply = read_until_fini("u t0 p0") emit("tool_db_unload_spindle_fini", int(unload_reply[-1].startswith("FINI"))) - t14_unloaded = read_until_fini("t 14") - require(any(line.startswith("T14 ") and "P114" in line for line in t14_unloaded), "unload_spindle_state_drift") + t14_unloaded = read_single_reply("t 14") + require(t14_unloaded.startswith("T14 ") and "P114" in t14_unloaded, "unload_spindle_state_drift") emit("tool_db_unload_spindle_state_ok", 1) require(os.path.exists(db_savefile), "db_savefile_missing") diff --git a/wasm-port/tests/sdk/node/verify_project_release_artifact_url_workflow.mjs b/wasm-port/tests/sdk/node/verify_project_release_artifact_url_workflow.mjs index fcf8e73..e3962a7 100644 --- a/wasm-port/tests/sdk/node/verify_project_release_artifact_url_workflow.mjs +++ b/wasm-port/tests/sdk/node/verify_project_release_artifact_url_workflow.mjs @@ -24,6 +24,23 @@ import { const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, "../../.."); const sourceManifestText = readFileSync(resolve(root, "tools/source-manifest.txt"), "utf8"); +function parseTsvRows(text) { + const [headerLine, ...dataLines] = text.trim().split(/\r?\n/); + const headers = headerLine.split("\t"); + return dataLines + .filter((line) => line.trim().length > 0) + .map((line) => { + const values = line.split("\t"); + return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])); + }); +} + +const promotionCandidateRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/promotion-candidates.tsv"), "utf8"), +); +const evidenceExpansionRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"), "utf8"), +); const manifest = createProjectReleaseGateManifest(); const virtualHalSimConfigSourceCoverage = createVirtualHalSimConfigSourceCoverageReport(); const virtualHalMotionControllerMatrix = createVirtualHalMotionControllerMatrixReport({ manifestText: sourceManifestText }); @@ -92,6 +109,8 @@ const readyArtifactJson = JSON.stringify(createProjectReleaseReadinessReport({ virtualHalSimConfigSourceCoverage, virtualHalSimConfigPromotionCandidates, virtualHalSimConfigEvidenceExpansion, + promotionCandidateRows, + evidenceExpansionRows, virtualHalSimConfigMacroLoadFixtures, virtualHalMotionControllerMatrix: browserDiagnosticsArtifact.virtualHalMotionControllerMatrix, })); @@ -198,10 +217,38 @@ assert.equal( readySummary.rows.find(({ id }) => id === "promotion-candidate-total")?.value, "28", ); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-ready-candidate-rows")?.value, + "8", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview")?.value, + "qtdragon-multi-joint-on-abort", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview-gcode")?.value, + "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-ready-candidate-promotion-allowed")?.value, + "0", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-ready-candidate-baseline-changing")?.value, + "no", +); assert.equal( readySummary.rows.find(({ id }) => id === "evidence-expansion-candidates")?.value, "13", ); +assert.equal( + readySummary.rows.find(({ id }) => id === "promotion-candidate-artifact-rows")?.value, + "28", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "evidence-expansion-artifact-rows")?.value, + "13", +); assert.equal( readySummary.rows.find(({ id }) => id === "evidence-expansion-next-evidence")?.value, "browser-diagnostics-binding", @@ -242,6 +289,42 @@ assert.equal( readySummary.rows.find(({ id }) => id === "promotion-candidate-allowed")?.value, "0", ); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-locked-rows")?.value, + "19", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-locked-preview")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc, L4-PYTHON-REMAP:axis/laser/raster_test.ngc, L4-PYTHON-REMAP:axis/laser/vector_test.ngc", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-promotion-allowed")?.value, + "0", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-family-count")?.value, + "3", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-family-summary")?.value, + "L4-USER-M-PROCESS:1 locked=yes, L4-TOOL-DB:1 locked=yes, L4-PYTHON-REMAP:17 locked=yes", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-detail-row-count")?.value, + "19", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-detail-preview-1")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put; promotion_allowed=0", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-TOOL-DB")?.value, + "1 locked rows; promotion_allowed=0; first=axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put", +); +assert.equal( + readySummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-TOOL-DB-paths")?.value, + "axis/db_demo/base.ngc", +); assert.equal( readySummary.rows.find(({ id }) => id === "hard-block-runtime-lock")?.value, "locked (3 families)", @@ -270,7 +353,14 @@ assert.deepEqual( ["promotion-candidate-artifact", "wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv"], ["promotion-candidate-layers", "evidence-ready=8 inventory-ready=20"], ["promotion-candidate-total", "28"], + ["evidence-ready-candidate-rows", "8"], + ["evidence-ready-candidate-preview", "qtdragon-multi-joint-on-abort"], + ["evidence-ready-candidate-preview-gcode", "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc"], + ["evidence-ready-candidate-promotion-allowed", "0"], + ["evidence-ready-candidate-baseline-changing", "no"], + ["promotion-candidate-artifact-rows", "28"], ["evidence-expansion-candidates", "13"], + ["evidence-expansion-artifact-rows", "13"], ["evidence-expansion-next-evidence", "browser-diagnostics-binding"], ["evidence-expansion-baseline-changing", "no"], ["evidence-expansion-preferred-candidate", "woodpecker-on-abort"], @@ -281,6 +371,23 @@ assert.deepEqual( ["evidence-expansion-source-count-list", "3, 3, 3"], ["evidence-expansion-artifact", "wasm-port/build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"], ["promotion-candidate-allowed", "0"], + ["hard-block-runtime-locked-rows", "19"], + ["hard-block-runtime-locked-preview", "L4-TOOL-DB:axis/db_demo/base.ngc, L4-PYTHON-REMAP:axis/laser/raster_test.ngc, L4-PYTHON-REMAP:axis/laser/vector_test.ngc"], + ["hard-block-runtime-promotion-allowed", "0"], + ["hard-block-runtime-family-count", "3"], + ["hard-block-runtime-family-summary", "L4-USER-M-PROCESS:1 locked=yes, L4-TOOL-DB:1 locked=yes, L4-PYTHON-REMAP:17 locked=yes"], + ["hard-block-runtime-detail-row-count", "19"], + ["hard-block-runtime-detail-preview-1", "L4-TOOL-DB:axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put; promotion_allowed=0"], + ["hard-block-runtime-detail-preview-2", "L4-PYTHON-REMAP:axis/laser/raster_test.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture; promotion_allowed=0"], + ["hard-block-runtime-detail-preview-3", "L4-PYTHON-REMAP:axis/laser/vector_test.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture; promotion_allowed=0"], + ["hard-block-runtime-detail-preview-4", "L4-PYTHON-REMAP:axis/laser/vector_test2.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture; promotion_allowed=0"], + ["hard-block-runtime-detail-preview-5", "L4-PYTHON-REMAP:axis/remap/cycle/nc_files/examples.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture; promotion_allowed=0"], + ["hard-block-runtime-family-L4-USER-M-PROCESS", "1 locked rows; promotion_allowed=0; first=axis/vismach/millturn/example.ngc; reason=design_m128_m129_linuxcnc_state_boundary; next=kinstype_guard_and_ini_xyz_hal_pin_state"], + ["hard-block-runtime-family-L4-TOOL-DB", "1 locked rows; promotion_allowed=0; first=axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put"], + ["hard-block-runtime-family-L4-PYTHON-REMAP", "17 locked rows; promotion_allowed=0; first=axis/laser/raster_test.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture"], + ["hard-block-runtime-family-L4-USER-M-PROCESS-paths", "axis/vismach/millturn/example.ngc"], + ["hard-block-runtime-family-L4-TOOL-DB-paths", "axis/db_demo/base.ngc"], + ["hard-block-runtime-family-L4-PYTHON-REMAP-paths", "axis/laser/raster_test.ngc, axis/laser/vector_test.ngc, axis/laser/vector_test2.ngc, axis/remap/cycle/nc_files/examples.ngc, axis/remap/extend-builtins/nc_files/examples.ngc, axis/remap/getting-started/nc_files/examples.ngc, axis/remap/manual-toolchange-with-tool-length-switch/nc_files/tcdemo.ngc, axis/remap/rack-toolchange/nc_files/tcdemo.ngc, axis/remap/stop-lookahead/nc_files/examples.ngc, axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/circular_pocket.ngc, axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_back_and_forth.ngc, axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition.ngc, axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/simple_example.ngc, axis/vismach/VMC_toolchange/toolchange.ngc, gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzac.ngc, gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/boat-xyzbc.ngc, gmoccapy/non_trivial_kinematics/table-rotary-tilting/examples/impeller-7bl-xyzac.ngc"], ...expectedPromotionFamilyRows, ["browser-diagnostics", "ready"], ["virtual-hal-promotion-candidate-summary", "ready"], diff --git a/wasm-port/tests/sdk/node/verify_project_release_gate_manifest.mjs b/wasm-port/tests/sdk/node/verify_project_release_gate_manifest.mjs index 9befc06..794fed6 100644 --- a/wasm-port/tests/sdk/node/verify_project_release_gate_manifest.mjs +++ b/wasm-port/tests/sdk/node/verify_project_release_gate_manifest.mjs @@ -397,7 +397,9 @@ assert.deepEqual( ["promotion-candidate-artifact", "wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv"], ["promotion-candidate-layers", "evidence-ready=8 inventory-ready=20"], ["promotion-candidate-total", "28"], + ["promotion-candidate-artifact-rows", "28"], ["evidence-expansion-candidates", "13"], + ["evidence-expansion-artifact-rows", "13"], ["evidence-expansion-next-evidence", "browser-diagnostics-binding"], ["evidence-expansion-baseline-changing", "no"], ["evidence-expansion-artifact", "wasm-port/build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"], @@ -496,7 +498,14 @@ assert.deepEqual( ["promotion-candidate-artifact", "wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv"], ["promotion-candidate-layers", "evidence-ready=8 inventory-ready=20"], ["promotion-candidate-total", "28"], + ["evidence-ready-candidate-rows", "0"], + ["evidence-ready-candidate-preview", "not provided"], + ["evidence-ready-candidate-preview-gcode", "not provided"], + ["evidence-ready-candidate-promotion-allowed", "0"], + ["evidence-ready-candidate-baseline-changing", "no"], + ["promotion-candidate-artifact-rows", "28"], ["evidence-expansion-candidates", "13"], + ["evidence-expansion-artifact-rows", "13"], ["evidence-expansion-next-evidence", "browser-diagnostics-binding"], ["evidence-expansion-baseline-changing", "no"], ["evidence-expansion-preferred-candidate", "not provided"], @@ -507,6 +516,18 @@ assert.deepEqual( ["evidence-expansion-source-count-list", "not provided"], ["evidence-expansion-artifact", "wasm-port/build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"], ["promotion-candidate-allowed", "0"], + ["hard-block-runtime-locked-rows", "0"], + ["hard-block-runtime-locked-preview", "not provided"], + ["hard-block-runtime-promotion-allowed", "0"], + ["hard-block-runtime-family-count", "3"], + ["hard-block-runtime-family-summary", "L4-USER-M-PROCESS:0 locked=yes, L4-TOOL-DB:0 locked=yes, L4-PYTHON-REMAP:0 locked=yes"], + ["hard-block-runtime-detail-row-count", "0"], + ["hard-block-runtime-family-L4-USER-M-PROCESS", "0 locked rows; promotion_allowed=0; first=not provided; reason=not provided; next=not provided"], + ["hard-block-runtime-family-L4-TOOL-DB", "0 locked rows; promotion_allowed=0; first=not provided; reason=not provided; next=not provided"], + ["hard-block-runtime-family-L4-PYTHON-REMAP", "0 locked rows; promotion_allowed=0; first=not provided; reason=not provided; next=not provided"], + ["hard-block-runtime-family-L4-USER-M-PROCESS-paths", "not provided"], + ["hard-block-runtime-family-L4-TOOL-DB-paths", "not provided"], + ["hard-block-runtime-family-L4-PYTHON-REMAP-paths", "not provided"], ...expectedPromotionFamilyRows, ["browser-diagnostics", "not requested"], ["virtual-hal-promotion-candidate-summary", "not requested"], diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index a377da6..92123b8 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -183,6 +183,24 @@ const sdkIndexText = readFileSync(resolve(root, "runtime/sdk/src/index.js"), "ut const sdkReadmeText = readFileSync(resolve(root, "runtime/sdk/README.md"), "utf8"); const sourceManifestText = readFileSync(resolve(root, "tools/source-manifest.txt"), "utf8"); const trackerText = readFileSync(resolve(root, "../PROJECT_COMPLETION_TRACKER.md"), "utf8"); + +function parseTsvRows(text) { + const [headerLine, ...dataLines] = text.trim().split(/\r?\n/); + const headers = headerLine.split("\t"); + return dataLines + .filter((line) => line.trim().length > 0) + .map((line) => { + const values = line.split("\t"); + return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])); + }); +} + +const promotionCandidateArtifactRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/promotion-candidates.tsv"), "utf8"), +); +const evidenceExpansionArtifactRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"), "utf8"), +); const expectedPromotionFamilySummary = "qtdragon-on-abort: 6/6 ready; vismach-remap-sims: 1/1 ready; rose-engine-rcone-demo: 1/1 ready"; const expectedPromotionFamilyRows = [ @@ -204,7 +222,46 @@ const expectedPromotionCandidateArtifactSummary = { totalCandidateCount: 28, promotionAllowedCount: 0, hardBlockRuntimeLock: "locked (3 families)", + hardBlockRuntimePromotionAllowedCount: 0, inventoryBaseline: "executed=28 passed=28 skipped=131 unexpected_fail=0", + artifactRows: { + promotionCandidates: 28, + evidenceExpansion: 13, + }, + hardBlockRuntimeFamilyRows: [], + hardBlockRuntimeFamilyDetailRows: [], + hardBlockRuntimeFamilySummaryRows: [ + { + family: "L4-USER-M-PROCESS", + candidateCount: 0, + promotionAllowedCount: 0, + locked: true, + lockedPaths: [], + firstPath: null, + firstBlockReason: null, + nextProof: null, + }, + { + family: "L4-TOOL-DB", + candidateCount: 0, + promotionAllowedCount: 0, + locked: true, + lockedPaths: [], + firstPath: null, + firstBlockReason: null, + nextProof: null, + }, + { + family: "L4-PYTHON-REMAP", + candidateCount: 0, + promotionAllowedCount: 0, + locked: true, + lockedPaths: [], + firstPath: null, + firstBlockReason: null, + nextProof: null, + }, + ], layers: [ { id: "evidence-ready", @@ -224,6 +281,7 @@ const expectedPromotionCandidateArtifactSummary = { evidenceExpansion: { id: "browser-diagnostics-expansion", candidateCount: 13, + artifactRowCount: 13, promotionAllowedCount: 0, baselineChanging: false, nextEvidence: "browser-diagnostics-binding", @@ -1571,6 +1629,14 @@ assert.deepEqual( releaseReadinessReady.promotionCandidateArtifactSummary, expectedPromotionCandidateArtifactSummary, ); +assert.equal( + releaseReadinessReady.rows.find(({ id }) => id === "promotion-candidate-artifact-rows")?.value, + "28", +); +assert.equal( + releaseReadinessReady.rows.find(({ id }) => id === "evidence-expansion-artifact-rows")?.value, + "13", +); assert.deepEqual( releaseReadinessReady.virtualHalPromotionFamilyRows.map(({ id, value }) => [ `virtual-hal-promotion-family-${id}`, @@ -1597,6 +1663,78 @@ assert.equal( createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).rows.find(({ id }) => id === "promotion-candidate-layers")?.value, "evidence-ready=8 inventory-ready=20", ); +const releaseReadinessWithCandidateArtifacts = createProjectReleaseReadinessReport({ + observedOutputs: ["project_release_gate=ok"], + virtualHalSimConfigSourceCoverage: simConfigSourceCoverageReport, + virtualHalSimConfigPromotionCandidates: simConfigPromotionCandidateReport, + virtualHalSimConfigMacroLoadFixtures: simConfigMacroLoadFixtureReport, + virtualHalMotionControllerMatrix: manifestCheckedMotionControllerMatrixReport, + promotionCandidateRows: promotionCandidateArtifactRows, + evidenceExpansionRows: evidenceExpansionArtifactRows, +}); +assert.equal(releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.artifactRows.promotionCandidates, 28); +assert.equal(releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.artifactRows.evidenceExpansion, 13); +assert.equal( + releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.hardBlockRuntimeFamilyRows.length, + 19, +); +assert.equal( + releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.hardBlockRuntimePromotionAllowedCount, + 0, +); +assert.equal( + releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.hardBlockRuntimeFamilyRows + .some((row) => row.skipKind === "L4-TOOL-DB" && row.promotionAllowed === false), + true, +); +assert.deepEqual( + releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.hardBlockRuntimeFamilySummaryRows + .map(({ family, candidateCount, promotionAllowedCount, locked }) => [ + family, + candidateCount, + promotionAllowedCount, + locked, + ]), + [ + ["L4-USER-M-PROCESS", 1, 0, true], + ["L4-TOOL-DB", 1, 0, true], + ["L4-PYTHON-REMAP", 17, 0, true], + ], +); +assert.deepEqual( + releaseReadinessWithCandidateArtifacts.promotionCandidateArtifactSummary.hardBlockRuntimeFamilyDetailRows + .slice(0, 3) + .map(({ family, path, blockReason, nextProof, promotionAllowed }) => [ + family, + path, + blockReason, + nextProof, + promotionAllowed, + ]), + [ + [ + "L4-TOOL-DB", + "axis/db_demo/base.ngc", + "design_tooldata_db_protocol_boundary", + "db_program_v2_1_handshake_getall_load_unload_or_put", + false, + ], + [ + "L4-PYTHON-REMAP", + "axis/laser/raster_test.ngc", + "design_linuxcnc_python_runtime_boundary", + "python_runtime_owner_and_fixture", + false, + ], + [ + "L4-PYTHON-REMAP", + "axis/laser/vector_test.ngc", + "design_linuxcnc_python_runtime_boundary", + "python_runtime_owner_and_fixture", + false, + ], + ], +); const releaseReadinessWithScreenshots = createProjectReleaseReadinessReport({ gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])), virtualHalSimConfigSourceCoverage: simConfigSourceCoverageReport, @@ -1735,11 +1873,21 @@ assert.deepEqual(releaseReadinessArtifactValidation, { label: "Promotion candidate total", value: "28", }, + { + id: "promotion-candidate-artifact-rows", + label: "Promotion candidate artifact rows", + value: "28", + }, { id: "evidence-expansion-candidates", label: "Evidence expansion candidates", value: "13", }, + { + id: "evidence-expansion-artifact-rows", + label: "Evidence expansion artifact rows", + value: "13", + }, { id: "evidence-expansion-next-evidence", label: "Evidence expansion next evidence", @@ -1899,6 +2047,22 @@ assert.equal( ).rows.find(({ id }) => id === "promotion-candidate-layers")?.value, "evidence-ready=8 inventory-ready=20", ); +assert.equal( + createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel( + await loadProjectReleaseReadinessArtifactUrlWorkflow({ + artifactUrl: "/project-release-readiness.json", + diagnosticsUrl: "/browser-diagnostics.json", + fetchRef: async (url) => ({ + ok: true, + status: 200, + text: async () => url === "/browser-diagnostics.json" + ? JSON.stringify(browserDiagnosticsArtifact) + : JSON.stringify(releaseReadinessArtifact), + }), + }), + ).rows.find(({ id }) => id === "evidence-ready-candidate-rows")?.value, + "8", +); assert.equal( createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel( await loadProjectReleaseReadinessArtifactUrlWorkflow({ diff --git a/wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs b/wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs index 08da8b7..d49344f 100644 --- a/wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs +++ b/wasm-port/tests/ui/node/verify_ini_panel_ui_shell.mjs @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { createProjectReleaseBrowserDiagnosticsArtifactValidation, @@ -18,9 +21,6 @@ import { createVirtualHalState, } from "../../../runtime/sdk/src/linuxcnc-hal.js"; -const VIRTUAL_HAL_MOTION_MATRIX_MANIFEST_ENTRIES = - VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS.flatMap((target) => target.sourceFiles ?? []); - import { INI_PANEL_ENTRIES, controlPageStatusText, @@ -161,6 +161,30 @@ import { renderMachineSessionControlView, } from "../../../runtime/ui/ini-panel/ui-shell.js"; +const VIRTUAL_HAL_MOTION_MATRIX_MANIFEST_ENTRIES = + VIRTUAL_HAL_SIM_CONFIG_SOURCE_TARGETS.flatMap((target) => target.sourceFiles ?? []); + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = resolve(__dirname, "../../.."); + +function parseTsvRows(text) { + const [headerLine, ...dataLines] = text.trim().split(/\r?\n/); + const headers = headerLine.split("\t"); + return dataLines + .filter((line) => line.trim().length > 0) + .map((line) => { + const values = line.split("\t"); + return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])); + }); +} + +const promotionCandidateRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/promotion-candidates.tsv"), "utf8"), +); +const evidenceExpansionRows = parseTsvRows( + readFileSync(resolve(root, "build/wasm/sim-configs-inventory/evidence-expansion-candidates.tsv"), "utf8"), +); + assert.deepEqual(getIniPanelEntryManifest(), INI_PANEL_ENTRIES); assert.deepEqual(getVisibleIniPanelEntries(), INI_PANEL_ENTRIES); assert.equal(controlPageStatusText({ lastAction: "x", runStatus: "ok", error: null }), "x | ok | -"); @@ -1454,6 +1478,8 @@ const releaseReadinessArtifactJson = JSON.stringify(createProjectReleaseReadines virtualHalSimConfigSourceCoverage: createVirtualHalSimConfigSourceCoverageReport(), virtualHalSimConfigPromotionCandidates: releaseReadinessPromotionCandidates, virtualHalSimConfigEvidenceExpansion: releaseReadinessEvidenceExpansion, + promotionCandidateRows, + evidenceExpansionRows, virtualHalSimConfigMacroLoadFixtures: releaseReadinessMacroLoadFixtures, virtualHalMotionControllerMatrix: releaseReadinessMotionControllerMatrix, })); @@ -1807,7 +1833,7 @@ const releaseArtifactMountResult = mountIniPanelShellWorkflowOverviewReleaseRead rowsNode: releaseArtifactRowsNode, }); assert.equal(releaseArtifactRenderResult.rendered, true); -assert.equal(releaseArtifactRenderResult.rowCount, 28); +assert.equal(releaseArtifactRenderResult.rowCount, 30); assert.equal(releaseArtifactRenderResult.dataset.handoffScope, "workflow-overview-release-readiness-artifact"); assert.equal( releaseArtifactRenderResult.rowIds.includes("gate-execution-manifest"), @@ -1858,7 +1884,7 @@ assert.equal( true, ); assert.equal(releaseArtifactMountResult.ready, true); -assert.equal(releaseArtifactMountResult.renderResult.rowCount, 28); +assert.equal(releaseArtifactMountResult.renderResult.rowCount, 30); assert.deepEqual( validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson("{").missing, ["artifact-json"], @@ -1917,7 +1943,7 @@ assert.equal( ); assert.equal(releaseArtifactUrlWorkflow.renderState.statusLine, "Ready: No blocking reasons"); assert.equal(releaseArtifactUrlWorkflow.mountResult.ready, true); -assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 28); +assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 30); const releaseArtifactUrlWorkflowSummary = createIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrlWorkflowSummaryViewModel( releaseArtifactUrlWorkflow, @@ -1981,10 +2007,38 @@ assert.equal( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "virtual-hal-promotion-browser-diagnostics-count")?.value, "8 diagnostics-ready", ); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-rows")?.value, + "8", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview")?.value, + "qtdragon-multi-joint-on-abort", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-preview-gcode")?.value, + "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-promotion-allowed")?.value, + "0", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-ready-candidate-baseline-changing")?.value, + "no", +); assert.equal( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-next-evidence")?.value, "browser-diagnostics-binding", ); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "promotion-candidate-artifact-rows")?.value, + "28", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-artifact-rows")?.value, + "13", +); assert.equal( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "evidence-expansion-baseline-changing")?.value, "no", @@ -2025,6 +2079,42 @@ assert.equal( releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "promoted-blocked-family-count")?.value, "0 promoted", ); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-locked-rows")?.value, + "19", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-locked-preview")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc, L4-PYTHON-REMAP:axis/laser/raster_test.ngc, L4-PYTHON-REMAP:axis/laser/vector_test.ngc", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-promotion-allowed")?.value, + "0", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-count")?.value, + "3", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-summary")?.value, + "L4-USER-M-PROCESS:1 locked=yes, L4-TOOL-DB:1 locked=yes, L4-PYTHON-REMAP:17 locked=yes", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-detail-row-count")?.value, + "19", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-detail-preview-1")?.value, + "L4-TOOL-DB:axis/db_demo/base.ngc; reason=design_tooldata_db_protocol_boundary; next=db_program_v2_1_handshake_getall_load_unload_or_put; promotion_allowed=0", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-PYTHON-REMAP")?.value, + "17 locked rows; promotion_allowed=0; first=axis/laser/raster_test.ngc; reason=design_linuxcnc_python_runtime_boundary; next=python_runtime_owner_and_fixture", +); +assert.equal( + releaseArtifactUrlWorkflowSummary.rows.find(({ id }) => id === "hard-block-runtime-family-L4-USER-M-PROCESS-paths")?.value, + "axis/vismach/millturn/example.ngc", +); const promotionCandidateDrilldownViewModel = createIniPanelShellWorkflowOverviewPromotionCandidateDrilldownViewModel( releaseArtifactUrlWorkflowSummary, @@ -2062,6 +2152,18 @@ assert.equal( promotionCandidateDrilldownViewModel.rows.find(({ id }) => id === "blocked-family-exclusion")?.value, "passed", ); +assert.equal( + promotionCandidateDrilldownViewModel.rows.find(({ id }) => id === "candidate-layer")?.value, + "evidence-ready", +); +assert.equal( + promotionCandidateDrilldownViewModel.rows.find(({ id }) => id === "inventory-promotion-allowed")?.value, + "0", +); +assert.equal( + promotionCandidateDrilldownViewModel.rows.find(({ id }) => id === "inventory-baseline-changing")?.value, + "no", +); assert.equal(promotionCandidateDrilldownViewModel.sourceFileCount, 2); assert.equal(promotionCandidateDrilldownViewModel.evidenceRowCount, 8); assert.equal( @@ -2091,7 +2193,7 @@ assert.deepEqual( ], ); assert.equal(promotionCandidateDrilldownRenderState.ready, true); -assert.equal(promotionCandidateDrilldownRenderState.rowCount, 19); +assert.equal(promotionCandidateDrilldownRenderState.rowCount, 22); assert.equal(promotionCandidateDrilldownRenderState.sourceFileCount, 2); assert.equal(promotionCandidateDrilldownRenderState.evidenceRowCount, 8); assert.equal( @@ -2133,7 +2235,7 @@ const promotionCandidateDrilldownMountResult = rowsNode: promotionCandidateDrilldownRowsNode, }); assert.equal(promotionCandidateDrilldownRenderResult.rendered, true); -assert.equal(promotionCandidateDrilldownRenderResult.rowCount, 19); +assert.equal(promotionCandidateDrilldownRenderResult.rowCount, 22); assert.equal(promotionCandidateDrilldownRenderResult.evidenceRowCount, 8); assert.deepEqual(promotionCandidateDrilldownRenderResult.dataset, { handoffPhase: "ready", @@ -2145,6 +2247,18 @@ assert.equal(promotionCandidateDrilldownRowsNode.children.some((node) => node.dataset?.workflowOverviewPromotionCandidateDrilldownValue === "blocked-family-exclusion" && node.textContent === "passed" ), true); +assert.equal(promotionCandidateDrilldownRowsNode.children.some((node) => + node.dataset?.workflowOverviewPromotionCandidateDrilldownValue === "candidate-layer" && + node.textContent === "evidence-ready" +), true); +assert.equal(promotionCandidateDrilldownRowsNode.children.some((node) => + node.dataset?.workflowOverviewPromotionCandidateDrilldownValue === "inventory-promotion-allowed" && + node.textContent === "0" +), true); +assert.equal(promotionCandidateDrilldownRowsNode.children.some((node) => + node.dataset?.workflowOverviewPromotionCandidateDrilldownValue === "inventory-baseline-changing" && + node.textContent === "no" +), true); assert.equal(promotionCandidateDrilldownRowsNode.children.some((node) => node.dataset?.workflowOverviewPromotionCandidateDrilldownSourceKind === "gcode" && node.dataset?.workflowOverviewPromotionCandidateDrilldownManifestBacked === "true" && @@ -2196,6 +2310,16 @@ assert.equal(promotionFamilyRowsRenderState.statusLine, "Ready: 3 promotion fami assert.equal(promotionFamilyRowsRenderState.rows.length, 3); assert.equal(promotionFamilyRowsRenderState.sourceFileCount, 17); assert.equal(promotionFamilyRowsRenderState.sourceRows.length, 17); +assert.equal(promotionFamilyRowsRenderState.candidateCount, 8); +assert.equal(promotionFamilyRowsRenderState.candidateRows.length, 8); +assert.equal( + promotionFamilyRowsRenderState.candidateRows.every(({ candidateStatus, promotionAllowed, baselineChanging }) => + candidateStatus === "evidence-ready" && + promotionAllowed === false && + baselineChanging === false + ), + true, +); assert.equal( promotionFamilyRowsRenderState.rows.find(({ id }) => id === "virtual-hal-promotion-family-qtdragon-on-abort") ?.sourceKindSummary, @@ -2215,6 +2339,8 @@ assert.equal(promotionFamilySourceFilterViewModel.ready, true); assert.equal(promotionFamilySourceFilterViewModel.statusLine, "Ready: 17/17 source files selected"); assert.equal(promotionFamilySourceFilterViewModel.filteredSourceFileCount, 17); assert.equal(promotionFamilySourceFilterViewModel.totalSourceFileCount, 17); +assert.equal(promotionFamilySourceFilterViewModel.filteredCandidateCount, 8); +assert.equal(promotionFamilySourceFilterViewModel.totalCandidateCount, 8); assert.deepEqual( promotionFamilySourceFilterViewModel.familyFilters.map(({ id, count, selected }) => [id, count, selected]), [ @@ -2249,6 +2375,7 @@ assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.selectedSourceKind, "gc assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.statusLine, "Ready: 6/17 source files selected"); assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.scopedSourceFileCount, 12); assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.filteredSourceFileCount, 6); +assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.filteredCandidateCount, 6); assert.equal(promotionFamilyQtDragonGcodeFilterViewModel.familyRows.length, 1); assert.equal( promotionFamilyQtDragonGcodeFilterViewModel.familyRows[0]?.sourceKindSummary, @@ -2325,7 +2452,8 @@ assert.equal(promotionFamilySourceFilterRenderResult.statusLine, "Ready: 6/17 so assert.equal(promotionFamilySourceFilterRenderResult.controlCount, 2); assert.equal(promotionFamilySourceFilterRenderResult.filterRowCount, 7); assert.equal(promotionFamilySourceFilterRenderResult.sourceFileCount, 6); -assert.equal(promotionFamilySourceFilterRenderResult.rowCount, 13); +assert.equal(promotionFamilySourceFilterRenderResult.candidateCount, 6); +assert.equal(promotionFamilySourceFilterRenderResult.rowCount, 19); assert.deepEqual(promotionFamilySourceFilterRenderResult.dataset, { handoffPhase: "ready", handoffReady: "true", @@ -2385,8 +2513,20 @@ assert.equal( ), true, ); +assert.equal( + promotionFamilySourceFilterRowsNode.children.some((node) => + node.dataset?.workflowOverviewPromotionFamilySourceFilterType === "candidate-row" && + node.dataset?.workflowOverviewPromotionFamilySourceFilterFamily === + "virtual-hal-promotion-family-qtdragon-on-abort" && + node.dataset?.workflowOverviewPromotionFamilySourceFilterCandidateStatus === "evidence-ready" && + node.dataset?.workflowOverviewPromotionFamilySourceFilterPromotionAllowed === "0" && + node.dataset?.workflowOverviewPromotionFamilySourceFilterBaselineChanging === "no" && + node.textContent === "linuxcnc/configs/sim/qtdragon/qtdragon_multi_joint/on_abort.ngc" + ), + true, +); assert.equal(promotionFamilySourceFilterMountResult.ready, true); -assert.equal(promotionFamilySourceFilterMountResult.renderResult.rowCount, 13); +assert.equal(promotionFamilySourceFilterMountResult.renderResult.rowCount, 19); const promotionFamilySourceFilterSelectionActionPlan = createIniPanelShellWorkflowOverviewPromotionFamilySourceFilterSelectionActionPlan( promotionFamilyRowsRenderState, @@ -2436,6 +2576,8 @@ assert.equal(promotionFamilySourceFilterSelectionChange.selectedSourceKind, "all assert.equal(promotionFamilySourceFilterSelectionChange.actionPlan.changed, true); assert.equal(promotionFamilySourceFilterSelectionChange.viewModel.filteredSourceFileCount, 2); assert.equal(promotionFamilySourceFilterSelectionChange.renderResult.sourceFileCount, 2); +assert.equal(promotionFamilySourceFilterSelectionChange.viewModel.filteredCandidateCount, 1); +assert.equal(promotionFamilySourceFilterSelectionChange.renderResult.candidateCount, 1); assert.equal(promotionFamilySourceFilterSelectionChange.renderResult.dataset.selectedSourceKind, "all"); assert.equal( promotionFamilySourceFilterRowsNode.children.some((node) => @@ -2673,7 +2815,7 @@ const releaseArtifactUrlWorkflowMountResult = rowsNode: releaseArtifactUrlWorkflowRowsNode, }); assert.equal(releaseArtifactUrlWorkflowRenderResult.rendered, true); -assert.equal(releaseArtifactUrlWorkflowRenderResult.rowCount, 35); +assert.equal(releaseArtifactUrlWorkflowRenderResult.rowCount, 59); assert.equal( releaseArtifactUrlWorkflowRenderResult.rowIds.includes("browser-diagnostics"), true, @@ -2723,7 +2865,7 @@ assert.equal( true, ); assert.equal(releaseArtifactUrlWorkflowMountResult.ready, true); -assert.equal(releaseArtifactUrlWorkflowMountResult.renderResult.rowCount, 35); +assert.equal(releaseArtifactUrlWorkflowMountResult.renderResult.rowCount, 59); const releaseArtifactUrlWorkflowMissingDom = await loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl({ artifactUrl: "/project-release-readiness.json", fetchRef: async () => ({ diff --git a/wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.mjs b/wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.mjs index c12c163..121a5f2 100644 --- a/wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.mjs +++ b/wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.mjs @@ -2108,6 +2108,26 @@ function linuxCncSourceExecutable(relPath) { } } +function linuxCncHostCommandPath(command) { + const pathCommand = commandPath(command); + if (pathCommand !== "") { + return pathCommand; + } + const ripScriptCommands = new Set(["linuxcnc", "halrun"]); + const ripRelativePath = ripScriptCommands.has(command) + ? `scripts/${command}` + : `bin/${command}`; + const ripPath = resolve(workspaceRoot, "linuxcnc", ripRelativePath); + try { + if ((statSync(ripPath).mode & 0o111) !== 0) { + return ripPath; + } + } catch { + return ""; + } + return ""; +} + function toolDbProcessNativeRuntimeReadinessRows(transactionRows) { const first = transactionRows[0]; if (!first) { @@ -2126,22 +2146,22 @@ function toolDbProcessNativeRuntimeReadinessRows(transactionRows) { kind: "host_command", requirement: "linuxcnc", purpose: "provide_linuxcnc_python_runtime_and_task_context", - available: commandPath("linuxcnc") !== "", - evidence: commandPath("linuxcnc"), + available: linuxCncHostCommandPath("linuxcnc") !== "", + evidence: linuxCncHostCommandPath("linuxcnc"), }, { kind: "host_command", requirement: "milltask", purpose: "own_db_program_startup_and_tooldata_protocol", - available: commandPath("milltask") !== "", - evidence: commandPath("milltask"), + available: linuxCncHostCommandPath("milltask") !== "", + evidence: linuxCncHostCommandPath("milltask"), }, { kind: "host_command", requirement: "halcmd", purpose: "provide_linuxcnc_runtime_environment_for_db_program_sync", - available: commandPath("halcmd") !== "", - evidence: commandPath("halcmd"), + available: linuxCncHostCommandPath("halcmd") !== "", + evidence: linuxCncHostCommandPath("halcmd"), }, { kind: "source_config_file", @@ -2326,9 +2346,20 @@ function toolDbProcessNativeRuntimeProbeGateRows({ pythonModulesReady && protocolContractReady ); - const gateStatus = runtimeReady && sourceProofReady - ? "ready_to_implement_protocol_probe" - : "blocked_missing_host_runtime"; + const nativeRuntimeProbeRows = existsSync(nativeRuntimeProbeSummarySourcePath) + ? parseTsv(readFileSync(nativeRuntimeProbeSummarySourcePath, "utf8")) + : []; + const nativeToolDbProbe = nativeRuntimeProbeRows.find((row) => + row.boundary_class === "L4-TOOL-DB" && + row.runtime_probe === "linuxcnc_tool_db_runtime_probe", + ); + const protocolProbePassed = nativeToolDbProbe?.probe_status === "runtime_protocol_probe_passed"; + const gateStatus = protocolProbePassed + ? "native_protocol_probe_passed_waiting_for_node_browser_proof" + : runtimeReady && sourceProofReady + ? "ready_to_implement_protocol_probe" + : "blocked_missing_host_runtime"; + const proofStatus = protocolProbePassed ? "native_protocol_probe_passed" : "pending"; return [[ first.path, @@ -2351,12 +2382,14 @@ function toolDbProcessNativeRuntimeProbeGateRows({ flagValue(sourceProofReady), "native_db_process_protocol_probe_required", gateStatus, - "pending", + proofStatus, "0", "0", - runtimeReady - ? "host_and_source_ready_write_native_db_protocol_probe_python3_and_tbl_fallback_not_sufficient" - : "python3_and_tool_table_fallback_not_sufficient_keep_tool_db_blocked", + protocolProbePassed + ? "native_protocol_probe_passed_keep_tool_db_locked_until_node_browser_proof" + : runtimeReady + ? "host_and_source_ready_write_native_db_protocol_probe_python3_and_tbl_fallback_not_sufficient" + : "python3_and_tool_table_fallback_not_sufficient_keep_tool_db_blocked", ].map(tsvValue).join("\t")]; } @@ -2437,13 +2470,26 @@ function verifyToolDbProcessNativeRuntimeProbeGateRows({ assert.equal(row.runtime_ready, flagValue(allRuntimeReady), "tool DB runtime readiness drift"); assert.equal(row.source_proof_ready, "1", "tool DB source proof must be ready before runtime probe implementation"); assert.equal(row.required_native_proof, "native_db_process_protocol_probe_required", "tool DB native proof drift"); - assert.equal(row.proof_status, "pending", "tool DB probe gate must remain pending"); + assert.ok( + ["pending", "native_protocol_probe_passed"].includes(row.proof_status), + "tool DB probe gate proof status drift", + ); assert.equal(row.execution_enabled, "0", "tool DB probe gate must not enable execution"); assert.equal(row.promotion_allowed, "0", "tool DB probe gate must not allow promotion"); if (allRuntimeReady) { assert.equal(row.missing_requirements, "-", "ready tool DB runtime has missing requirements"); - assert.equal(row.gate_status, "ready_to_implement_protocol_probe", "ready tool DB gate status drift"); + assert.ok( + [ + "ready_to_implement_protocol_probe", + "native_protocol_probe_passed_waiting_for_node_browser_proof", + ].includes(row.gate_status), + "ready tool DB gate status drift", + ); + if (row.gate_status === "native_protocol_probe_passed_waiting_for_node_browser_proof") { + assert.equal(row.proof_status, "native_protocol_probe_passed", "passed tool DB proof status drift"); + assert.ok(row.notes.includes("node_browser_proof"), "passed tool DB gate must keep node/browser proof lock"); + } } else { assert.deepEqual( splitSummaryList(row.missing_requirements).sort(), @@ -4168,8 +4214,8 @@ function pythonRemapNativeRuntimeReadinessRows(contractRows) { kind: "host_command", requirement: "linuxcnc", purpose: "provide_linuxcnc_task_interpreter_python_context", - available: commandPath("linuxcnc") !== "", - evidence: commandPath("linuxcnc"), + available: linuxCncHostCommandPath("linuxcnc") !== "", + evidence: linuxCncHostCommandPath("linuxcnc"), }, { kind: "linuxcnc_source_file", @@ -5563,7 +5609,7 @@ function boundaryPhaseCompletionSummaryRows({ ) ); const runtimeBoundaryHostUnblockPlanOk = ( - runtimeBoundaryHostUnblockPlanRows.length === 4 && + runtimeBoundaryHostUnblockPlanRows.length === runtimeBoundaryHostRequirementSummaryRows.filter((row) => row.missing_blocks_runtime === "1").length && runtimeBoundaryHostUnblockPlanRows.every((row) => row.requirement_kind === "host_command" && row.available === "0" && @@ -5581,14 +5627,21 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryFamilyHostReadinessOk = ( runtimeBoundaryFamilyHostReadinessRows.length === 3 && runtimeBoundaryFamilyHostReadinessRows.every((row) => - /^[1-9][0-9]*$/.test(row.missing_requirement_count) && - row.missing_host_requirements !== "-" && + /^[0-9]+$/.test(row.missing_requirement_count) && /^[1-9][0-9]*$/.test(row.available_requirement_count) && row.available_requirements !== "-" && /^ENABLE_[A-Z0-9_]+=1$/.test(row.opt_in_env) && row.execution_command.includes(row.opt_in_env) && - row.preflight_status === "blocked_missing_host_runtime" && - row.family_host_status === "blocked_missing_host_requirements" && + ( + (row.missing_requirement_count === "0" && + row.missing_host_requirements === "-" && + row.preflight_status === "ready_to_run_opt_in_native_probe" && + row.family_host_status === "host_ready_to_run_opt_in_probe") || + (row.missing_requirement_count !== "0" && + row.missing_host_requirements !== "-" && + row.preflight_status === "blocked_missing_host_runtime" && + row.family_host_status === "blocked_missing_host_requirements") + ) && row.execution_enabled === "0" && row.promotion_allowed === "0", ) @@ -5596,17 +5649,17 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryHostReadinessRollupOk = ( runtimeBoundaryHostReadinessRollupRows.length === 1 && runtimeBoundaryHostReadinessRollupRows.every((row) => - row.scope === "blocked_runtime_host_readiness" && + row.scope === "blocked_runtime_host_readiness" && row.family_count === "3" && - row.host_ready_family_count === "0" && - row.host_blocked_family_count === "3" && - row.blocked_families !== "-" && - row.missing_host_requirements !== "-" && - row.missing_host_requirement_count === "4" && - row.ready_opt_in_command_count === "0" && - row.ready_opt_in_commands === "-" && - row.blocked_opt_in_commands !== "-" && - row.host_readiness_status === "host_blocked_for_all_opt_in_native_probes" && + /^[0-9]+$/.test(row.host_ready_family_count) && + /^[0-9]+$/.test(row.host_blocked_family_count) && + /^[0-9]+$/.test(row.missing_host_requirement_count) && + /^[0-9]+$/.test(row.ready_opt_in_command_count) && + [ + "host_ready_for_all_opt_in_native_probes", + "host_partially_ready_for_opt_in_native_probes", + "host_blocked_for_all_opt_in_native_probes", + ].includes(row.host_readiness_status) && row.execution_enabled === "0" && row.promotion_allowed === "0", ) @@ -5616,10 +5669,16 @@ function boundaryPhaseCompletionSummaryRows({ runtimeBoundaryOptInProbeDispatchPlanRows.every((row) => /^ENABLE_[A-Z0-9_]+=1$/.test(row.opt_in_env) && row.execution_command.includes(row.opt_in_env) && - row.family_host_status === "blocked_missing_host_requirements" && - row.missing_host_requirements !== "-" && - row.host_action === "skip_missing_host_requirements" && - row.dispatch_allowed === "0" && + ( + (row.family_host_status === "host_ready_to_run_opt_in_probe" && + row.missing_host_requirements === "-" && + row.host_action === "ready_for_manual_opt_in_native_probe" && + row.dispatch_allowed === "1") || + (row.family_host_status === "blocked_missing_host_requirements" && + row.missing_host_requirements !== "-" && + row.host_action === "skip_missing_host_requirements" && + row.dispatch_allowed === "0") + ) && row.execution_enabled === "0" && row.promotion_allowed === "0", ) @@ -5627,12 +5686,16 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryOptInProbeDispatchRollupOk = ( runtimeBoundaryOptInProbeDispatchRollupRows.length === 1 && runtimeBoundaryOptInProbeDispatchRollupRows.every((row) => - row.scope === "blocked_runtime_opt_in_probe_dispatch" && + row.scope === "blocked_runtime_opt_in_probe_dispatch" && row.probe_count === "3" && - row.dispatch_allowed_count === "0" && - row.dispatch_blocked_count === "3" && - row.dispatch_actions === "skip_missing_host_requirements" && - row.dispatch_status === "dispatch_blocked_for_all_opt_in_native_probes" && + /^[0-9]+$/.test(row.dispatch_allowed_count) && + /^[0-9]+$/.test(row.dispatch_blocked_count) && + row.dispatch_actions !== "-" && + [ + "dispatch_allowed_for_all_opt_in_native_probes", + "dispatch_partially_allowed_for_opt_in_native_probes", + "dispatch_blocked_for_all_opt_in_native_probes", + ].includes(row.dispatch_status) && row.execution_enabled === "0" && row.promotion_allowed === "0", ) @@ -5640,13 +5703,19 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryOptInProbeSkipEvidenceContractOk = ( runtimeBoundaryOptInProbeSkipEvidenceContractRows.length === 3 && runtimeBoundaryOptInProbeSkipEvidenceContractRows.every((row) => - row.host_action === "skip_missing_host_requirements" && - row.skip_reason === "missing_host_requirements" && - row.missing_host_requirements !== "-" && - row.current_probe_status === "skipped_missing_host_runtime" && + ( + (row.host_action === "ready_for_manual_opt_in_native_probe" && + row.skip_reason === "-" && + row.missing_host_requirements === "-" && + row.skip_evidence_status === "skip_contract_not_applicable_dispatch_allowed") || + (row.host_action === "skip_missing_host_requirements" && + row.skip_reason === "missing_host_requirements" && + row.missing_host_requirements !== "-" && + row.current_probe_status === "skipped_missing_host_runtime" && + row.skip_evidence_status === "skip_valid_until_host_requirements_available") + ) && row.evidence_required_now === "0" && row.observed_evidence_ready === "0" && - row.skip_evidence_status === "skip_valid_until_host_requirements_available" && row.execution_enabled === "0" && row.promotion_allowed === "0", ) @@ -5654,12 +5723,12 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryOptInProbeSkipEvidenceRollupOk = ( runtimeBoundaryOptInProbeSkipEvidenceRollupRows.length === 1 && runtimeBoundaryOptInProbeSkipEvidenceRollupRows.every((row) => - row.scope === "blocked_runtime_opt_in_probe_skip_evidence" && + row.scope === "blocked_runtime_opt_in_probe_skip_evidence" && row.probe_count === "3" && - row.skip_count === "3" && + /^[0-9]+$/.test(row.skip_count) && row.evidence_required_now_count === "0" && row.observed_evidence_ready_count === "0" && - row.skip_evidence_statuses === "skip_valid_until_host_requirements_available" && + row.skip_evidence_statuses !== "-" && row.evidence_rollup_status === "no_native_pass_evidence_accepted_while_host_blocked" && row.execution_enabled === "0" && row.promotion_allowed === "0", @@ -5668,11 +5737,10 @@ function boundaryPhaseCompletionSummaryRows({ const runtimeBoundaryNativeEvidenceAcceptanceGateOk = ( runtimeBoundaryNativeEvidenceAcceptanceGateRows.length === 3 && runtimeBoundaryNativeEvidenceAcceptanceGateRows.every((row) => - row.current_probe_status === "skipped_missing_host_runtime" && - row.native_evidence_status === "pending_until_native_pass" && + row.native_evidence_status === "pending_until_native_pass" && row.evidence_required_now === "0" && row.observed_evidence_ready === "0" && - row.native_evidence_gate === "blocked_until_host_requirements_available" && + ["blocked_until_host_requirements_available", "blocked_until_native_pass_evidence"].includes(row.native_evidence_gate) && row.evidence_acceptance_allowed === "0" && row.promotion_ready === "0" && row.execution_enabled === "0" && @@ -5698,7 +5766,7 @@ function boundaryPhaseCompletionSummaryRows({ classOrderMatches(runtimeBoundaryOptInProbeSkipEvidenceContractRows) && classOrderMatches(runtimeBoundaryNativeEvidenceAcceptanceGateRows) && runtimeBoundaryOptInProbeDispatchPlanRows.every((row) => - row.dispatch_allowed === "0" && + ["0", "1"].includes(row.dispatch_allowed) && row.execution_enabled === "0" && row.promotion_allowed === "0", ) && @@ -5708,8 +5776,7 @@ function boundaryPhaseCompletionSummaryRows({ row.promotion_allowed === "0", ) && runtimeBoundaryPromotionBlockerRows.every((row) => - row.blocker_keys.includes("host_runtime_requirements_missing") && - row.blocker_keys.includes("promotion_lock_active") && + row.blocker_keys.includes("promotion_lock_active") && row.blocker_keys.includes("manual_lock_update_required"), ) ); @@ -6085,7 +6152,11 @@ function verifyBoundaryPhaseCompletionSummaryRows(rows) { ); for (const row of parsedRows) { assert.equal(row.passed, "1", `${row.criterion}: completion criterion failed`); - assert.ok(/^[1-9][0-9]*$/.test(row.count), `${row.criterion}: invalid criterion count`); + if (row.criterion === "runtime_boundary_host_unblock_plan") { + assert.ok(/^[0-9]+$/.test(row.count), `${row.criterion}: invalid criterion count`); + } else { + assert.ok(/^[1-9][0-9]*$/.test(row.count), `${row.criterion}: invalid criterion count`); + } assert.notEqual(row.evidence, "-", `${row.criterion}: missing criterion evidence`); } const rowByCriterion = new Map(parsedRows.map((row) => [row.criterion, row])); @@ -7233,9 +7304,9 @@ function verifyRuntimeBoundaryHostRequirementSummaryRows(rows, preflightRows) { const byRequirement = new Map(parsedRows.map((row) => [row.requirement, row])); for (const required of [ - ["linuxcnc", "0", "3", "L4-PYTHON-REMAP,L4-TOOL-DB,L4-USER-M-PROCESS"], - ["halcmd", "0", "2", "L4-TOOL-DB,L4-USER-M-PROCESS"], - ["milltask", "0", "1", "L4-TOOL-DB"], + ["linuxcnc", "1", "3", "-"], + ["halcmd", "1", "2", "-"], + ["milltask", "1", "1", "-"], ["python3", "1", "2", "-"], ["tclsh", "1", "1", "-"], ]) { @@ -7439,45 +7510,22 @@ function verifyRuntimeBoundaryFamilyHostReadinessRows(rows, preflightRows) { expectedHeaders, ); const preflightByClass = new Map(preflightRows.map((row) => [row.boundary_class, row])); - const expected = new Map([ - [ - "L4-USER-M-PROCESS", - { - missingCount: "3", - missing: "halrun,halcmd,linuxcnc", - available: "tclsh", - }, - ], - [ - "L4-TOOL-DB", - { - missingCount: "3", - missing: "linuxcnc,milltask,halcmd", - available: "python3,axis/db_demo/db_nonran.py,linuxcnc.so,tooldb.py", - }, - ], - [ - "L4-PYTHON-REMAP", - { - missingCount: "1", - missing: "linuxcnc", - available: "python3,src/emc/rs274ngc/interp_python.cc,src/emc/pythonplugin/python_plugin.cc,axis/remap/stop-lookahead/python/remap.py,axis/remap/stop-lookahead/python/toplevel.py", - }, - ], - ]); + const expectedClasses = [ + "L4-USER-M-PROCESS", + "L4-TOOL-DB", + "L4-PYTHON-REMAP", + ]; - assert.equal(parsedRows.length, expected.size, "family host readiness row count drift"); + assert.equal(parsedRows.length, expectedClasses.length, "family host readiness row count drift"); assert.deepEqual( parsedRows.map((row) => row.boundary_class), - [...expected.keys()], + expectedClasses, "family host readiness order drift", ); for (const row of parsedRows) { const preflight = preflightByClass.get(row.boundary_class); - const expectedRow = expected.get(row.boundary_class); assert.ok(preflight, `${row.boundary_class}: family host readiness lacks preflight row`); - assert.ok(expectedRow, `${row.boundary_class}: unexpected family host readiness row`); assert.equal(row.boundary_kind, preflight.boundary_kind, `${row.boundary_class}: family host readiness kind drift`); assert.equal(row.target, preflight.target, `${row.boundary_class}: family host readiness target drift`); assert.equal(row.blocked, preflight.blocked, `${row.boundary_class}: family host readiness blocked drift`); @@ -7486,11 +7534,20 @@ function verifyRuntimeBoundaryFamilyHostReadinessRows(rows, preflightRows) { assert.equal(row.execution_command, preflight.execution_command, `${row.boundary_class}: family host readiness command drift`); assert.equal(row.current_probe_status, preflight.current_probe_status, `${row.boundary_class}: family host readiness probe status drift`); assert.equal(row.preflight_status, preflight.preflight_status, `${row.boundary_class}: family host readiness preflight status drift`); - assert.equal(row.missing_requirement_count, expectedRow.missingCount, `${row.boundary_class}: family host readiness missing count drift`); - assert.equal(row.missing_host_requirements, expectedRow.missing, `${row.boundary_class}: family host readiness missing requirements drift`); - assert.equal(row.available_requirements, expectedRow.available, `${row.boundary_class}: family host readiness available requirements drift`); - assert.equal(row.available_requirement_count, String(splitSummaryList(expectedRow.available).length), `${row.boundary_class}: family host readiness available count drift`); - assert.equal(row.family_host_status, "blocked_missing_host_requirements", `${row.boundary_class}: family host readiness should remain blocked on this host`); + assert.equal(row.missing_host_requirements, preflight.missing_requirements, `${row.boundary_class}: family host readiness missing requirements drift`); + assert.equal(row.missing_requirement_count, String(splitSummaryList(preflight.missing_requirements).length), `${row.boundary_class}: family host readiness missing count drift`); + assert.equal( + row.available_requirement_count, + String(splitSummaryList(row.available_requirements).length), + `${row.boundary_class}: family host readiness available count drift`, + ); + if (preflight.runtime_ready === "1") { + assert.equal(row.missing_host_requirements, "-", `${row.boundary_class}: ready family must not list missing requirements`); + assert.equal(row.family_host_status, "host_ready_to_run_opt_in_probe", `${row.boundary_class}: ready family host status drift`); + } else { + assert.notEqual(row.missing_host_requirements, "-", `${row.boundary_class}: blocked family must list missing requirements`); + assert.equal(row.family_host_status, "blocked_missing_host_requirements", `${row.boundary_class}: blocked family host status drift`); + } assert.equal(row.execution_enabled, "0", `${row.boundary_class}: family host readiness must not enable execution`); assert.equal(row.promotion_allowed, "0", `${row.boundary_class}: family host readiness must not allow promotion`); assert.ok(row.notes.includes("no_automatic_promotion"), `${row.boundary_class}: family host readiness note drift`); @@ -7571,22 +7628,33 @@ function verifyRuntimeBoundaryHostReadinessRollupRows({ .filter((requirement) => requirement.missing_blocks_runtime === "1") .map((requirement) => requirement.requirement), ); + const readyFamilies = familyHostReadinessRows.filter((family) => + family.family_host_status === "host_ready_to_run_opt_in_probe", + ); + const blockedFamilies = familyHostReadinessRows.filter((family) => + family.family_host_status !== "host_ready_to_run_opt_in_probe", + ); assert.equal(parsedRows.length, 1, "host readiness rollup row count drift"); assert.equal(row.scope, "blocked_runtime_host_readiness", "host readiness rollup scope drift"); assert.equal(row.family_count, String(familyHostReadinessRows.length), "host readiness rollup family count drift"); - assert.equal(row.host_ready_family_count, "0", "host readiness rollup must have no ready families on this host"); - assert.equal(row.host_blocked_family_count, String(familyHostReadinessRows.length), "host readiness rollup blocked family count drift"); - assert.equal(row.blocked_families, "L4-PYTHON-REMAP,L4-TOOL-DB,L4-USER-M-PROCESS", "host readiness rollup blocked families drift"); + assert.equal(row.host_ready_family_count, String(readyFamilies.length), "host readiness rollup ready family count drift"); + assert.equal(row.host_blocked_family_count, String(blockedFamilies.length), "host readiness rollup blocked family count drift"); + assert.equal(row.blocked_families, listValue(uniqueSorted(blockedFamilies.map((family) => family.boundary_class))), "host readiness rollup blocked families drift"); assert.equal(row.missing_host_requirements, listValue(expectedMissingRequirements), "host readiness rollup missing requirements drift"); assert.equal(row.missing_host_requirement_count, String(expectedMissingRequirements.length), "host readiness rollup missing requirement count drift"); - assert.equal(row.ready_opt_in_command_count, "0", "host readiness rollup must not report ready opt-in commands"); - assert.equal(row.ready_opt_in_commands, "-", "host readiness rollup must not list ready opt-in commands"); - assert.notEqual(row.blocked_opt_in_commands, "-", "host readiness rollup must list blocked opt-in commands"); - for (const family of familyHostReadinessRows) { + assert.equal(row.ready_opt_in_command_count, String(readyFamilies.length), "host readiness rollup ready command count drift"); + assert.equal(row.ready_opt_in_commands, listValue(uniqueSorted(readyFamilies.map((family) => family.execution_command))), "host readiness rollup ready commands drift"); + assert.equal(row.blocked_opt_in_commands, listValue(uniqueSorted(blockedFamilies.map((family) => family.execution_command))), "host readiness rollup blocked commands drift"); + for (const family of blockedFamilies) { assert.ok(row.blocked_opt_in_commands.includes(family.execution_command), `${family.boundary_class}: host readiness rollup missing blocked command`); } - assert.equal(row.host_readiness_status, "host_blocked_for_all_opt_in_native_probes", "host readiness rollup status drift"); + const expectedHostReadinessStatus = readyFamilies.length === familyHostReadinessRows.length + ? "host_ready_for_all_opt_in_native_probes" + : readyFamilies.length > 0 + ? "host_partially_ready_for_opt_in_native_probes" + : "host_blocked_for_all_opt_in_native_probes"; + assert.equal(row.host_readiness_status, expectedHostReadinessStatus, "host readiness rollup status drift"); assert.equal(row.execution_enabled, "0", "host readiness rollup must not enable execution"); assert.equal(row.promotion_allowed, "0", "host readiness rollup must not allow promotion"); assert.ok(row.notes.includes("no_automatic_promotion"), "host readiness rollup note drift"); @@ -7685,14 +7753,19 @@ function verifyRuntimeBoundaryOptInProbeDispatchPlanRows({ assert.equal(row.missing_host_requirements, readiness.missing_host_requirements, `${row.boundary_class}: dispatch missing requirement drift`); assert.equal(row.host_readiness_status, rollup.host_readiness_status, `${row.boundary_class}: dispatch rollup status drift`); assert.ok(row.execution_command.includes(row.opt_in_env), `${row.boundary_class}: dispatch command must include opt-in env`); - assert.equal(row.family_host_status, "blocked_missing_host_requirements", `${row.boundary_class}: current host must remain blocked`); - assert.notEqual(row.missing_host_requirements, "-", `${row.boundary_class}: blocked dispatch must list missing requirements`); - assert.equal(row.host_readiness_status, "host_blocked_for_all_opt_in_native_probes", `${row.boundary_class}: dispatch host readiness status drift`); - assert.equal(row.host_action, "skip_missing_host_requirements", `${row.boundary_class}: current host dispatch action drift`); - assert.equal(row.dispatch_allowed, "0", `${row.boundary_class}: current host dispatch must not be allowed`); + if (readiness.family_host_status === "host_ready_to_run_opt_in_probe") { + assert.equal(row.missing_host_requirements, "-", `${row.boundary_class}: ready dispatch must not list missing requirements`); + assert.equal(row.host_action, "ready_for_manual_opt_in_native_probe", `${row.boundary_class}: ready dispatch action drift`); + assert.equal(row.dispatch_allowed, "1", `${row.boundary_class}: ready dispatch should be allowed manually`); + assert.ok(row.next_action.includes("manually_run_opt_in_probe"), `${row.boundary_class}: ready dispatch next action drift`); + } else { + assert.notEqual(row.missing_host_requirements, "-", `${row.boundary_class}: blocked dispatch must list missing requirements`); + assert.equal(row.host_action, "skip_missing_host_requirements", `${row.boundary_class}: blocked dispatch action drift`); + assert.equal(row.dispatch_allowed, "0", `${row.boundary_class}: blocked dispatch must not be allowed`); + assert.ok(row.next_action.includes(row.missing_host_requirements), `${row.boundary_class}: blocked dispatch next action must name missing requirements`); + } assert.equal(row.execution_enabled, "0", `${row.boundary_class}: dispatch plan must not enable execution`); assert.equal(row.promotion_allowed, "0", `${row.boundary_class}: dispatch plan must not allow promotion`); - assert.ok(row.next_action.includes(row.missing_host_requirements), `${row.boundary_class}: dispatch next action must name missing requirements`); assert.ok(row.notes.includes("no_automatic_promotion"), `${row.boundary_class}: dispatch plan note drift`); } @@ -7752,6 +7825,7 @@ function verifyRuntimeBoundaryOptInProbeDispatchRollupRows({ expectedHeaders, ); const row = parsedRows[0]; + const dispatchAllowedRows = dispatchPlanRows.filter((plan) => plan.dispatch_allowed === "1"); const dispatchBlockedRows = dispatchPlanRows.filter((plan) => plan.dispatch_allowed !== "1"); const expectedMissingRequirements = uniqueSorted(dispatchBlockedRows.flatMap((plan) => splitSummaryList(plan.missing_host_requirements), @@ -7760,17 +7834,22 @@ function verifyRuntimeBoundaryOptInProbeDispatchRollupRows({ assert.equal(parsedRows.length, 1, "opt-in probe dispatch rollup row count drift"); assert.equal(row.scope, "blocked_runtime_opt_in_probe_dispatch", "opt-in probe dispatch rollup scope drift"); assert.equal(row.probe_count, String(dispatchPlanRows.length), "opt-in probe dispatch rollup probe count drift"); - assert.equal(row.dispatch_allowed_count, "0", "opt-in probe dispatch rollup must not allow probes on this host"); - assert.equal(row.dispatch_blocked_count, String(dispatchPlanRows.length), "opt-in probe dispatch rollup blocked count drift"); - assert.equal(row.blocked_families, "L4-PYTHON-REMAP,L4-TOOL-DB,L4-USER-M-PROCESS", "opt-in probe dispatch rollup blocked families drift"); - assert.equal(row.dispatch_actions, "skip_missing_host_requirements", "opt-in probe dispatch rollup action drift"); - assert.equal(row.dispatch_allowed_commands, "-", "opt-in probe dispatch rollup must not list allowed commands"); - assert.notEqual(row.dispatch_blocked_commands, "-", "opt-in probe dispatch rollup must list blocked commands"); - for (const plan of dispatchPlanRows) { + assert.equal(row.dispatch_allowed_count, String(dispatchAllowedRows.length), "opt-in probe dispatch rollup allowed count drift"); + assert.equal(row.dispatch_blocked_count, String(dispatchBlockedRows.length), "opt-in probe dispatch rollup blocked count drift"); + assert.equal(row.blocked_families, listValue(uniqueSorted(dispatchBlockedRows.map((plan) => plan.boundary_class))), "opt-in probe dispatch rollup blocked families drift"); + assert.equal(row.dispatch_actions, listValue(uniqueSorted(dispatchPlanRows.map((plan) => plan.host_action))), "opt-in probe dispatch rollup action drift"); + assert.equal(row.dispatch_allowed_commands, listValue(uniqueSorted(dispatchAllowedRows.map((plan) => plan.execution_command))), "opt-in probe dispatch rollup allowed commands drift"); + assert.equal(row.dispatch_blocked_commands, listValue(uniqueSorted(dispatchBlockedRows.map((plan) => plan.execution_command))), "opt-in probe dispatch rollup blocked commands drift"); + for (const plan of dispatchBlockedRows) { assert.ok(row.dispatch_blocked_commands.includes(plan.execution_command), `${plan.boundary_class}: opt-in probe dispatch rollup missing blocked command`); } assert.equal(row.missing_host_requirements, listValue(expectedMissingRequirements), "opt-in probe dispatch rollup missing requirement drift"); - assert.equal(row.dispatch_status, "dispatch_blocked_for_all_opt_in_native_probes", "opt-in probe dispatch rollup status drift"); + const expectedDispatchStatus = dispatchAllowedRows.length === dispatchPlanRows.length + ? "dispatch_allowed_for_all_opt_in_native_probes" + : dispatchAllowedRows.length > 0 + ? "dispatch_partially_allowed_for_opt_in_native_probes" + : "dispatch_blocked_for_all_opt_in_native_probes"; + assert.equal(row.dispatch_status, expectedDispatchStatus, "opt-in probe dispatch rollup status drift"); assert.equal(row.execution_enabled, "0", "opt-in probe dispatch rollup must not enable execution"); assert.equal(row.promotion_allowed, "0", "opt-in probe dispatch rollup must not allow promotion"); assert.ok(row.notes.includes("no_automatic_promotion"), "opt-in probe dispatch rollup note drift"); @@ -7869,12 +7948,8 @@ function verifyRuntimeBoundaryOptInProbeSkipEvidenceContractRows({ assert.equal(row.runtime_probe, evidence.runtime_probe, `${row.boundary_class}: skip evidence runtime probe drift`); assert.equal(row.opt_in_env, dispatch.opt_in_env, `${row.boundary_class}: skip evidence opt-in drift`); assert.equal(row.execution_command, dispatch.execution_command, `${row.boundary_class}: skip evidence command drift`); - assert.equal(row.host_action, "skip_missing_host_requirements", `${row.boundary_class}: current host skip action drift`); assert.equal(row.host_action, dispatch.host_action, `${row.boundary_class}: skip evidence host action drift`); - assert.equal(row.skip_reason, "missing_host_requirements", `${row.boundary_class}: skip evidence reason drift`); assert.equal(row.missing_host_requirements, dispatch.missing_host_requirements, `${row.boundary_class}: skip evidence missing requirements drift`); - assert.notEqual(row.missing_host_requirements, "-", `${row.boundary_class}: skip evidence must list missing host requirements`); - assert.equal(row.current_probe_status, "skipped_missing_host_runtime", `${row.boundary_class}: skip evidence current probe status drift`); assert.equal(row.current_probe_status, evidence.current_probe_status, `${row.boundary_class}: skip evidence probe status must match pass evidence contract`); assert.equal(row.evidence_required_now, "0", `${row.boundary_class}: skipped probe must not require evidence now`); assert.equal(row.evidence_required_now, evidence.evidence_required_now, `${row.boundary_class}: skip evidence required-now drift`); @@ -7882,7 +7957,18 @@ function verifyRuntimeBoundaryOptInProbeSkipEvidenceContractRows({ assert.equal(row.observed_evidence_ready, evidence.observed_evidence_ready, `${row.boundary_class}: skip evidence readiness drift`); assert.equal(row.native_evidence_status, "pending_until_native_pass", `${row.boundary_class}: skip evidence status drift`); assert.equal(row.native_evidence_status, evidence.evidence_status, `${row.boundary_class}: skip evidence native status drift`); - assert.equal(row.skip_evidence_status, "skip_valid_until_host_requirements_available", `${row.boundary_class}: skip evidence contract status drift`); + if (dispatch.dispatch_allowed === "1") { + assert.equal(row.host_action, "ready_for_manual_opt_in_native_probe", `${row.boundary_class}: ready skip evidence action drift`); + assert.equal(row.skip_reason, "-", `${row.boundary_class}: ready dispatch must not have skip reason`); + assert.equal(row.missing_host_requirements, "-", `${row.boundary_class}: ready dispatch must not list missing host requirements`); + assert.equal(row.skip_evidence_status, "skip_contract_not_applicable_dispatch_allowed", `${row.boundary_class}: ready skip evidence status drift`); + } else { + assert.equal(row.host_action, "skip_missing_host_requirements", `${row.boundary_class}: blocked skip evidence action drift`); + assert.equal(row.skip_reason, "missing_host_requirements", `${row.boundary_class}: blocked skip evidence reason drift`); + assert.notEqual(row.missing_host_requirements, "-", `${row.boundary_class}: blocked skip evidence must list missing host requirements`); + assert.equal(row.current_probe_status, "skipped_missing_host_runtime", `${row.boundary_class}: blocked skip evidence current probe status drift`); + assert.equal(row.skip_evidence_status, "skip_valid_until_host_requirements_available", `${row.boundary_class}: blocked skip evidence status drift`); + } assert.equal(row.execution_enabled, "0", `${row.boundary_class}: skip evidence contract must not enable execution`); assert.equal(row.promotion_allowed, "0", `${row.boundary_class}: skip evidence contract must not allow promotion`); assert.ok(row.notes.includes("no_automatic_promotion"), `${row.boundary_class}: skip evidence contract note drift`); @@ -7947,6 +8033,7 @@ function verifyRuntimeBoundaryOptInProbeSkipEvidenceRollupRows({ expectedHeaders, ); const row = parsedRows[0]; + const skippedRows = skipEvidenceContractRows.filter((contract) => contract.skip_reason !== "-"); const expectedMissingRequirements = uniqueSorted(skipEvidenceContractRows.flatMap((contract) => splitSummaryList(contract.missing_host_requirements), )); @@ -7954,15 +8041,15 @@ function verifyRuntimeBoundaryOptInProbeSkipEvidenceRollupRows({ assert.equal(parsedRows.length, 1, "opt-in probe skip evidence rollup row count drift"); assert.equal(row.scope, "blocked_runtime_opt_in_probe_skip_evidence", "opt-in probe skip evidence rollup scope drift"); assert.equal(row.probe_count, String(skipEvidenceContractRows.length), "opt-in probe skip evidence rollup probe count drift"); - assert.equal(row.skip_count, String(skipEvidenceContractRows.length), "opt-in probe skip evidence rollup skip count drift"); + assert.equal(row.skip_count, String(skippedRows.length), "opt-in probe skip evidence rollup skip count drift"); assert.equal(row.evidence_required_now_count, "0", "opt-in probe skip evidence rollup must not require evidence now"); assert.equal(row.observed_evidence_ready_count, "0", "opt-in probe skip evidence rollup must not report evidence ready"); - assert.equal(row.skipped_families, "L4-PYTHON-REMAP,L4-TOOL-DB,L4-USER-M-PROCESS", "opt-in probe skip evidence rollup family drift"); - assert.equal(row.skip_reasons, "missing_host_requirements", "opt-in probe skip evidence rollup reason drift"); + assert.equal(row.skipped_families, listValue(uniqueSorted(skippedRows.map((contract) => contract.boundary_class))), "opt-in probe skip evidence rollup family drift"); + assert.equal(row.skip_reasons, listValue(uniqueSorted(skippedRows.map((contract) => contract.skip_reason))), "opt-in probe skip evidence rollup reason drift"); assert.equal(row.missing_host_requirements, listValue(expectedMissingRequirements), "opt-in probe skip evidence rollup missing requirements drift"); - assert.equal(row.current_probe_statuses, "skipped_missing_host_runtime", "opt-in probe skip evidence rollup probe status drift"); + assert.equal(row.current_probe_statuses, listValue(uniqueSorted(skipEvidenceContractRows.map((contract) => contract.current_probe_status))), "opt-in probe skip evidence rollup probe status drift"); assert.equal(row.native_evidence_statuses, "pending_until_native_pass", "opt-in probe skip evidence rollup native evidence status drift"); - assert.equal(row.skip_evidence_statuses, "skip_valid_until_host_requirements_available", "opt-in probe skip evidence rollup skip evidence status drift"); + assert.equal(row.skip_evidence_statuses, listValue(uniqueSorted(skipEvidenceContractRows.map((contract) => contract.skip_evidence_status))), "opt-in probe skip evidence rollup skip evidence status drift"); assert.equal(row.evidence_rollup_status, "no_native_pass_evidence_accepted_while_host_blocked", "opt-in probe skip evidence rollup status drift"); assert.equal(row.execution_enabled, "0", "opt-in probe skip evidence rollup must not enable execution"); assert.equal(row.promotion_allowed, "0", "opt-in probe skip evidence rollup must not allow promotion"); @@ -8075,7 +8162,6 @@ function verifyRuntimeBoundaryNativeEvidenceAcceptanceGateRows({ assert.equal(row.target, skip.target, `${row.boundary_class}: native evidence gate target drift`); assert.equal(row.blocked, skip.blocked, `${row.boundary_class}: native evidence gate blocked drift`); assert.equal(row.runtime_probe, skip.runtime_probe, `${row.boundary_class}: native evidence gate runtime probe drift`); - assert.equal(row.current_probe_status, "skipped_missing_host_runtime", `${row.boundary_class}: native evidence gate probe status drift`); assert.equal(row.current_probe_status, skip.current_probe_status, `${row.boundary_class}: native evidence gate skip status drift`); assert.equal(row.native_evidence_status, "pending_until_native_pass", `${row.boundary_class}: native evidence gate evidence status drift`); assert.equal(row.native_evidence_status, skip.native_evidence_status, `${row.boundary_class}: native evidence gate native evidence status drift`); @@ -8083,16 +8169,21 @@ function verifyRuntimeBoundaryNativeEvidenceAcceptanceGateRows({ assert.equal(row.evidence_required_now, skip.evidence_required_now, `${row.boundary_class}: native evidence gate required-now drift`); assert.equal(row.observed_evidence_ready, "0", `${row.boundary_class}: native evidence gate must not observe evidence ready`); assert.equal(row.observed_evidence_ready, skip.observed_evidence_ready, `${row.boundary_class}: native evidence gate readiness drift`); - assert.equal(row.skip_evidence_status, "skip_valid_until_host_requirements_available", `${row.boundary_class}: native evidence gate skip status drift`); + assert.equal(row.skip_evidence_status, skip.skip_evidence_status, `${row.boundary_class}: native evidence gate skip status drift`); assert.equal(row.promotion_ready, "0", `${row.boundary_class}: native evidence gate must not be promotion-ready`); assert.equal(row.promotion_ready, readiness.promotion_ready, `${row.boundary_class}: native evidence gate promotion readiness drift`); assert.equal(row.node_gate_status, "blocked_until_native_pass_evidence", `${row.boundary_class}: native evidence gate Node status drift`); assert.equal(row.node_gate_status, postNativeGate.node_gate_status, `${row.boundary_class}: native evidence gate post-native Node status drift`); assert.equal(row.browser_gate_status, "blocked_until_node_gate_complete", `${row.boundary_class}: native evidence gate browser status drift`); assert.equal(row.browser_gate_status, postNativeGate.browser_gate_status, `${row.boundary_class}: native evidence gate post-native browser status drift`); - assert.equal(row.native_evidence_gate, "blocked_until_host_requirements_available", `${row.boundary_class}: native evidence gate status drift`); + if (skip.skip_reason === "missing_host_requirements") { + assert.equal(row.native_evidence_gate, "blocked_until_host_requirements_available", `${row.boundary_class}: native evidence gate status drift`); + assert.ok(row.next_action.includes(skip.missing_host_requirements), `${row.boundary_class}: native evidence gate next action must name missing requirements`); + } else { + assert.equal(row.native_evidence_gate, "blocked_until_native_pass_evidence", `${row.boundary_class}: native evidence gate status drift`); + assert.equal(row.next_action, "run_opt_in_native_runtime_probe_and_collect_evidence", `${row.boundary_class}: native evidence gate next action drift`); + } assert.equal(row.evidence_acceptance_allowed, "0", `${row.boundary_class}: native evidence gate must not allow evidence acceptance`); - assert.ok(row.next_action.includes(skip.missing_host_requirements), `${row.boundary_class}: native evidence gate next action must name missing requirements`); assert.equal(row.execution_enabled, "0", `${row.boundary_class}: native evidence gate must not enable execution`); assert.equal(row.promotion_allowed, "0", `${row.boundary_class}: native evidence gate must not allow promotion`); assert.ok(row.notes.includes("no_automatic_promotion"), `${row.boundary_class}: native evidence gate note drift`); @@ -8223,12 +8314,16 @@ function verifyBlockedRuntimeOptInGateConsistency({ assert.equal(skipEvidence.missing_host_requirements, execution.missing_requirements, `${boundaryClass}: skip-evidence missing requirements drift`); assert.equal(blocker.missing_runtime_requirements, execution.missing_requirements, `${boundaryClass}: blocker missing requirements drift`); assert.equal(evidenceGate.evidence_acceptance_allowed, "0", `${boundaryClass}: evidence acceptance must remain blocked`); - assert.equal(dispatch.dispatch_allowed, "0", `${boundaryClass}: dispatch must remain blocked on this host`); + assert.equal(dispatch.dispatch_allowed, execution.runtime_ready, `${boundaryClass}: dispatch readiness drift`); assert.equal(readiness.promotion_lock_active, "1", `${boundaryClass}: readiness must keep promotion lock active`); assert.equal(postNative.promotion_lock_active, "1", `${boundaryClass}: post-native gate must keep promotion lock active`); assert.ok(blocker.blocker_keys.includes("promotion_lock_active"), `${boundaryClass}: blocker lacks promotion lock key`); assert.ok(blocker.blocker_keys.includes("manual_lock_update_required"), `${boundaryClass}: blocker lacks manual lock key`); - assert.ok(blocker.blocker_keys.includes("host_runtime_requirements_missing"), `${boundaryClass}: blocker lacks host runtime key`); + if (execution.runtime_ready === "0") { + assert.ok(blocker.blocker_keys.includes("host_runtime_requirements_missing"), `${boundaryClass}: blocker lacks host runtime key`); + } else { + assert.equal(blocker.blocker_keys.includes("host_runtime_requirements_missing"), false, `${boundaryClass}: ready runtime must not keep host runtime blocker`); + } assert.ok(evidenceGate.native_evidence_gate.includes("blocked"), `${boundaryClass}: evidence gate must be blocked`); } } @@ -8303,23 +8398,40 @@ function verifyBlockedRuntimeProbeExecutionConsistency({ assert.equal(dispatch.missing_host_requirements, execution.missing_requirements, `${execution.boundary_class}: dispatch missing requirement drift`); assert.equal(skip.missing_host_requirements, execution.missing_requirements, `${execution.boundary_class}: skip evidence missing requirement drift`); - assert.equal(execution.runtime_ready, "0", `${execution.boundary_class}: current host must not be runtime-ready`); assert.equal(execution.source_proof_ready, "1", `${execution.boundary_class}: source proof should remain ready before opt-in runtime`); - assert.equal(execution.current_probe_status, "skipped_missing_host_runtime", `${execution.boundary_class}: probe should remain skipped on this host`); - assert.equal(execution.plan_status, "blocked_missing_host_runtime", `${execution.boundary_class}: execution plan must remain host-blocked`); - assert.equal(preflight.preflight_status, "blocked_missing_host_runtime", `${execution.boundary_class}: preflight must remain host-blocked`); - assert.equal(dispatch.host_readiness_status, "host_blocked_for_all_opt_in_native_probes", `${execution.boundary_class}: dispatch host readiness drift`); - assert.equal(dispatch.host_action, "skip_missing_host_requirements", `${execution.boundary_class}: dispatch action must skip`); - assert.equal(dispatch.dispatch_allowed, "0", `${execution.boundary_class}: dispatch must not be allowed`); - assert.equal(skip.host_action, "skip_missing_host_requirements", `${execution.boundary_class}: skip evidence host action drift`); - assert.equal(skip.skip_reason, "missing_host_requirements", `${execution.boundary_class}: skip reason drift`); + assert.ok(["0", "1"].includes(execution.runtime_ready), `${execution.boundary_class}: invalid runtime readiness`); + if (execution.runtime_ready === "1") { + assert.equal(execution.current_probe_status, "ready_disabled_by_default", `${execution.boundary_class}: ready probe must remain opt-in`); + assert.equal(execution.plan_status, "ready_to_run_opt_in_probe", `${execution.boundary_class}: execution plan must remain opt-in`); + assert.equal(execution.missing_requirements, "-", `${execution.boundary_class}: ready execution plan must not list missing requirements`); + } else { + assert.equal(execution.current_probe_status, "skipped_missing_host_runtime", `${execution.boundary_class}: probe should remain skipped on missing host`); + assert.equal(execution.plan_status, "blocked_missing_host_runtime", `${execution.boundary_class}: execution plan must remain host-blocked`); + assert.notEqual(execution.missing_requirements, "-", `${execution.boundary_class}: blocked execution plan must list missing requirements`); + } + if (execution.runtime_ready === "1") { + assert.equal(preflight.preflight_status, "ready_to_run_opt_in_native_probe", `${execution.boundary_class}: ready preflight status drift`); + assert.equal(dispatch.host_action, "ready_for_manual_opt_in_native_probe", `${execution.boundary_class}: ready dispatch action drift`); + assert.equal(dispatch.dispatch_allowed, "1", `${execution.boundary_class}: ready dispatch must be manually allowed`); + assert.equal(skip.host_action, "ready_for_manual_opt_in_native_probe", `${execution.boundary_class}: ready skip evidence host action drift`); + assert.equal(skip.skip_reason, "-", `${execution.boundary_class}: ready skip reason drift`); + } else { + assert.equal(preflight.preflight_status, "blocked_missing_host_runtime", `${execution.boundary_class}: preflight must remain host-blocked`); + assert.equal(dispatch.host_action, "skip_missing_host_requirements", `${execution.boundary_class}: dispatch action must skip`); + assert.equal(dispatch.dispatch_allowed, "0", `${execution.boundary_class}: dispatch must not be allowed`); + assert.equal(skip.host_action, "skip_missing_host_requirements", `${execution.boundary_class}: skip evidence host action drift`); + assert.equal(skip.skip_reason, "missing_host_requirements", `${execution.boundary_class}: skip reason drift`); + } assert.equal(pass.evidence_required_now, "0", `${execution.boundary_class}: pass evidence must not be required yet`); assert.equal(skip.evidence_required_now, "0", `${execution.boundary_class}: skip evidence must not be required yet`); assert.equal(pass.observed_evidence_ready, "0", `${execution.boundary_class}: pass evidence must not be ready yet`); assert.equal(skip.observed_evidence_ready, "0", `${execution.boundary_class}: skip evidence must not be ready yet`); assert.equal(pass.evidence_status, "pending_until_native_pass", `${execution.boundary_class}: pass evidence status drift`); assert.equal(skip.native_evidence_status, "pending_until_native_pass", `${execution.boundary_class}: skip evidence native status drift`); - assert.equal(skip.skip_evidence_status, "skip_valid_until_host_requirements_available", `${execution.boundary_class}: skip evidence contract status drift`); + assert.ok( + ["skip_valid_until_host_requirements_available", "skip_contract_not_applicable_dispatch_allowed"].includes(skip.skip_evidence_status), + `${execution.boundary_class}: skip evidence contract status drift`, + ); for (const row of [execution, pass, preflight, dispatch, skip]) { assert.equal(row.execution_enabled, "0", `${execution.boundary_class}: probe execution row must not enable execution`); @@ -8362,12 +8474,26 @@ function verifyBlockedRuntimeRollupConsistency({ assert.equal(dispatch.missing_host_requirements, skipEvidence.missing_host_requirements, "blocked runtime rollup dispatch/skip missing requirement drift"); assert.equal(host.ready_opt_in_commands, dispatch.dispatch_allowed_commands, "blocked runtime rollup allowed command drift"); assert.equal(host.blocked_opt_in_commands, dispatch.dispatch_blocked_commands, "blocked runtime rollup blocked command drift"); - assert.equal(host.host_readiness_status, "host_blocked_for_all_opt_in_native_probes", "blocked runtime rollup host status drift"); - assert.equal(dispatch.dispatch_status, "dispatch_blocked_for_all_opt_in_native_probes", "blocked runtime rollup dispatch status drift"); + assert.ok( + [ + "host_ready_for_all_opt_in_native_probes", + "host_partially_ready_for_opt_in_native_probes", + "host_blocked_for_all_opt_in_native_probes", + ].includes(host.host_readiness_status), + "blocked runtime rollup host status drift", + ); + assert.ok( + [ + "dispatch_allowed_for_all_opt_in_native_probes", + "dispatch_partially_allowed_for_opt_in_native_probes", + "dispatch_blocked_for_all_opt_in_native_probes", + ].includes(dispatch.dispatch_status), + "blocked runtime rollup dispatch status drift", + ); assert.equal(skipEvidence.evidence_rollup_status, "no_native_pass_evidence_accepted_while_host_blocked", "blocked runtime rollup evidence status drift"); - assert.equal(skipEvidence.current_probe_statuses, "skipped_missing_host_runtime", "blocked runtime rollup probe status drift"); + assert.notEqual(skipEvidence.current_probe_statuses, "-", "blocked runtime rollup probe status drift"); assert.equal(skipEvidence.native_evidence_statuses, "pending_until_native_pass", "blocked runtime rollup native evidence status drift"); - assert.equal(skipEvidence.skip_evidence_statuses, "skip_valid_until_host_requirements_available", "blocked runtime rollup skip evidence status drift"); + assert.notEqual(skipEvidence.skip_evidence_statuses, "-", "blocked runtime rollup skip evidence status drift"); for (const [label, row] of [ ["host readiness", host], @@ -8589,7 +8715,10 @@ function verifyBlockedRuntimePromotionGateConsistency({ assert.equal(postNative.node_gate_status, "blocked_until_native_pass_evidence", `${boundaryClass}: post-native Node gate must wait for native evidence`); assert.equal(postNative.browser_gate_status, "blocked_until_node_gate_complete", `${boundaryClass}: post-native browser gate must wait for Node gate`); assert.equal(postNative.gate_status, "blocked_before_native_pass", `${boundaryClass}: post-native gate status drift`); - assert.equal(evidenceGate.native_evidence_gate, "blocked_until_host_requirements_available", `${boundaryClass}: native evidence gate must remain host-blocked`); + assert.ok( + ["blocked_until_host_requirements_available", "blocked_until_native_pass_evidence"].includes(evidenceGate.native_evidence_gate), + `${boundaryClass}: native evidence gate must remain blocked`, + ); assert.equal(evidenceGate.evidence_acceptance_allowed, "0", `${boundaryClass}: native evidence acceptance must remain blocked`); for (const row of [readiness, blocker, postNative, evidenceGate, passEvidence, lock]) { @@ -10816,7 +10945,12 @@ function userMProcessNativeRuntimeReadinessRows(runtimeStatePlanRows) { ["host_command", "linuxcnc", "run_linuxcnc_owned_task_hal_environment"], ]; const commandAvailability = new Map( - requirements.map(([, requirement]) => [requirement, commandPath(requirement)]), + requirements.map(([, requirement]) => [ + requirement, + ["halrun", "halcmd", "linuxcnc"].includes(requirement) + ? linuxCncHostCommandPath(requirement) + : commandPath(requirement), + ]), ); const allAvailable = requirements.every(([, requirement]) => commandAvailability.get(requirement), @@ -12231,6 +12365,15 @@ function verifyPromotionCandidateRows(rows, summaryRows, boundaryRows) { 0, "current baseline has no direct skipped-main inventory promotion candidate", ); + for (const lockedKind of ["L4-PYTHON-REMAP", "L4-TOOL-DB", "L4-USER-M-PROCESS", "UPSTREAM-DEMO"]) { + const lockedRows = parsedInventoryReady.filter((row) => row.skip_kind === lockedKind); + assert.ok(lockedRows.length > 0, `${lockedKind}: promotion-candidates missing locked inventory rows`); + assert.equal( + lockedRows.filter((row) => row.promotion_allowed !== "0").length, + 0, + `${lockedKind}: locked inventory rows must not allow promotion`, + ); + } for (const row of parsedEvidenceReady) { assert.equal(row.current_status, "PASS", `${row.path}: evidence-ready row must be a current inventory PASS`); assert.equal(row.skip_kind, "-", `${row.path}: evidence-ready row must not have a skip kind`);