完成L4-USER-M-PROCESS Web仿真接入

结论:L4-USER-M-PROCESS 已按 Web/virtual HAL 数控仿真主线完成接入,native LinuxCNC runtime 不再作为 Web 仿真阻塞;新增 text31.txt 接续剩余 77 个 SKIP 的 main-program promotion 复核。
This commit is contained in:
2026-06-20 07:59:38 +08:00
parent d0e55ba11e
commit 07869f5c69
21 changed files with 1161 additions and 51 deletions

View File

@@ -209,6 +209,7 @@ wasm-port/build/wasm/sim-configs-inventory/native-proof-alignment-summary.tsv
wasm-port/build/wasm/sim-configs-inventory/runtime-boundary-native-alignment-summary.tsv
wasm-port/build/wasm/sim-configs-inventory/native-runtime-probe-execution-plan.tsv
wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv
wasm-port/build/wasm/sim-configs-inventory/remaining-skip-main-program-promotion-audit.tsv
wasm-port/build/wasm/sim-configs-inventory/blocked-runtime-promotion-lock.tsv
wasm-port/build/wasm/sim-configs-inventory/next-boundary-worklist.tsv
wasm-port/build/wasm/sim-configs-inventory/next-boundary-recommendations.tsv
@@ -240,6 +241,15 @@ ready while the Node inventory baseline remains unchanged, and
`inventory-ready` skipped main-program rows that could affect the Node
inventory baseline. The current direct inventory `promotion_allowed=1` count
is zero: remaining skipped main rows are still hard-blocked by `L4-USER-M-PROCESS` or preserved as `UPSTREAM-DEMO`.
`remaining-skip-main-program-promotion-audit.tsv` is the focused row-level
audit for those skipped main-program rows. It currently records two rows,
`axis/vismach/millturn/example.ngc` and
`axis/vismach/5axis/table-rotary_spindle-rotary-nutating/demos/incremental_repetition_g533.ngc`,
and keeps both `promotion_allowed=0`; the millturn row now has a browser
virtual HAL state-transition proof for `M429 -> M129` and `M428 -> M128`, but
still lacks the LinuxCNC-owned native runtime pass and promotion-gate proof
required to leave `L4-USER-M-PROCESS`. The upstream demo row remains
non-promotable upstream demo evidence.
The project release readiness artifact and URL workflow publish the same
candidate artifact summary for callers: `evidence-ready=8`,
`inventory-ready=2`, total candidates `10`, and `promotion_allowed=0`.
@@ -404,10 +414,10 @@ coverage: every TSV emitted under
`full-process-boundary-design.md`. New gate artifacts must therefore be
documented before the inventory can pass.
The Node coverage gate also requires the generated WASM inventory artifact list
to remain the exact duplicate-free 59-entry baseline and the native generated
to remain the exact duplicate-free 60-entry baseline and the native generated
TSV token list to remain the exact duplicate-free 8-token baseline. It also
requires the corresponding `boundary-phase-completion-summary.tsv`
documentation-coverage counts to remain `57` and `8`, respectively. It also
documentation-coverage counts to remain `60` and `8`, respectively. It also
checks that the browser smoke source lists the same generated WASM and native
artifact tokens and preserves the same fixed-count, duplicate-free,
fetchability/header, documentation-missing, and completion count-parity guards

View File

@@ -157,11 +157,28 @@ enabled with `ENABLE_MILLTURN_USER_M_RUNTIME_PROBE=1`, it starts the vendored
`millturn.ini`, runs the LinuxCNC-owned Tcl `M128`/`M129` scripts, and verifies
the resulting `ini.[xyz].*` HAL state targets before reporting
`runtime_state_probe_passed`.
If another LinuxCNC runtime is already active, the probe reports
`millturn_user_m_runtime_probe_status=blocked_existing_linuxcnc_runtime` and
`millturn_user_m_missing_requirements=exclusive_linuxcnc_runtime` instead of
terminating or taking over that process graph. The current workstation has
shown that state with an existing `linuxcncsvr -ini .../axis.ini` and
`rtapi_app load tpmod`.
The artifact keeps `execution_enabled=0` and `promotion_allowed=0`. It is not
a standalone Tcl/HAL executor and does not make `millturn` a Node/browser
representative.
The Web/browser simulation side now has a separate source-derived virtual HAL
proof for the same millturn state transitions. `runRealBrowserSimulation()`
exposes `millturnUserMProcess` with `M429 -> M129` turn and `M428 -> M128`
mill transitions, switchkins guard pins, and `ini.[xz].*` limit pin values.
Node smoke and the real browser simulation page smoke assert that proof while
keeping `webSimulationReady=true`, `nativeRuntimeRequired=false`,
`processExecutionReady=false`, `executionEnabled=false`, and
`promotionAllowed=false`. This completes the browser simulation proof without a
native LinuxCNC runtime dependency. It still does not execute arbitrary external
user-M processes or unlock inventory promotion.
`linuxcnc_millturn_user_m_boundary_probe` is the current native guard for this
evidence. It reads the LinuxCNC source `millturn.ini`, `mcodes/M128`,
`mcodes/M129`, and the `428remap`/`429remap` callers, then verifies the

View File

@@ -47,12 +47,26 @@ execution_enabled=0
promotion_allowed=0
```
Manual `L4-USER-M-PROCESS` opt-in status on the current workstation:
```text
ENABLE_MILLTURN_USER_M_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_millturn_user_m_runtime.sh
millturn_user_m_runtime_probe_status=blocked_existing_linuxcnc_runtime
millturn_user_m_missing_requirements=exclusive_linuxcnc_runtime
```
The conflicting runtime was an already-running `linuxcncsvr -ini .../axis.ini`
plus `rtapi_app load tpmod`. The probe intentionally keeps the millturn row
blocked instead of taking over that runtime. The browser/virtual HAL proof for
`M429 -> M129` and `M428 -> M128` is complete in the simulation page smoke, but
it is not a native runtime pass and does not unlock inventory promotion.
## Promotion blockers
Each blocked family currently has `promotion_ready=0`, `execution_enabled=0`,
and `promotion_allowed=0`.
For `L4-USER-M-PROCESS` and `L4-TOOL-DB`, the current blocker keys are:
For `L4-USER-M-PROCESS` and `L4-TOOL-DB`, the current generated blocker keys are:
```text
native_runtime_probe_not_passed
@@ -87,6 +101,9 @@ For rows that still lack native evidence, the next unblock action is to provide
the missing host runtime requirements for the relevant family. Providing host
commands is not enough to promote a row; it only allows the next opt-in probe
gate to be considered.
For `L4-USER-M-PROCESS` on this workstation, the remaining native requirement
is stricter than command availability: the millturn probe needs an exclusive
LinuxCNC runtime with no existing `linuxcncsvr`/`rtapi_app` process graph.
## Opt-in probe dispatch

View File

@@ -199,7 +199,7 @@ Use this checklist when adding each LinuxCNC upstream test:
| `tests/uspace/spawnv-root` | Blocked | Sudo-installed userspace HAL component and `spawnv` root behavior. | Keep upstream-only unless HAL component install/runtime support is added. |
| `configs/sim/*/remap_subs/*.ngc` browser main-program inventory | Blocked | These files are remap/subroutine assets, not standalone browser main-program targets. | Keep validating them through native remap parse/execute paths and explicit five-axis/runtime probes instead of widening browser `runSimConfigProgram()` blindly. |
| `configs/sim/axis/db_demo/base.ngc` Layer 4 inventory | Blocked | `[EMCIO]DB_PROGRAM = ./db_nonran.py` requires LinuxCNC task/tooldata DB process startup, command/reply protocol, and database state behavior, not standalone interpreter file execution. | Keep as `L4-TOOL-DB` until a LinuxCNC-owned tool database runtime boundary exists and proves lookup/update behavior across native and WASM. `blocked-dependency-summary.tsv` records `taskclass.cc`, `tooldata_db.cc`, `tooldata_common.cc`, and `configs/sim/axis/db_demo/db.py` as the owner set plus source-derived `tool_db_protocol_evidence` for the `v2.1` handshake, `g`/`FINI` get-all, and `l`/`u`/`p` notification protocol. |
| `configs/sim/axis/vismach/millturn/example.ngc` Layer 4 inventory | Blocked | The remap execution chain calls external `USER_M_PATH` process codes `M128` and `M129`, with HAL/HALUI/UI process declarations in `millturn.ini`. | Keep as `L4-USER-M-PROCESS`; do not promote until the M128/M129 LinuxCNC-owned state boundary proves the kinstype guard and `ini.[xyz].*` HAL pin updates. `blocked-dependency-summary.tsv` records the `mcodes/M128` and `mcodes/M129` Tcl scripts plus source-derived `user_m_process_effects` for Tcl/HAL runtime use, kinstype guards, and `ini.[xyz]` HAL pin side effects. |
| `configs/sim/axis/vismach/millturn/example.ngc` Layer 4 inventory | Blocked | The remap execution chain calls external `USER_M_PATH` process codes `M128` and `M129`, with HAL/HALUI/UI process declarations in `millturn.ini`. | Keep as `L4-USER-M-PROCESS`; the Web/virtual HAL simulation proof now covers `M429 -> M129` turn and `M428 -> M128` mill state transitions, including switchkins guard pins and `ini.[xz].*` limit pins in Node and real browser page smoke. Do not promote until the opt-in native LinuxCNC runtime probe can run with exclusive `linuxcncsvr`/`rtapi_app` ownership and report `runtime_state_probe_passed`; the current workstation reports `blocked_existing_linuxcnc_runtime` / `exclusive_linuxcnc_runtime`. `blocked-dependency-summary.tsv` records the `mcodes/M128` and `mcodes/M129` Tcl scripts plus source-derived `user_m_process_effects` for Tcl/HAL runtime use, kinstype guards, and `ini.[xyz]` HAL pin side effects. |
| `configs/sim/gmoccapy/*` Layer 4 full inventory | Blocked | Python remap runtime boundary is present in native LinuxCNC `rs274`, but not yet intentionally exposed for Node/browser full inventory. | Keep native inventory coverage as source-of-truth; add one representative Node/browser class sample only after the Python remap boundary is deliberately designed. Python module and prolog/epilog ownership is inventoried in `blocked-dependency-summary.tsv`; LinuxCNC runtime ownership is recorded as `python_runtime_evidence` in the Python boundary/family summaries. |
| `configs/sim/axis/laser/*` Layer 4 full inventory | Blocked | Python remap runtime boundary is present in native LinuxCNC `rs274`, but not yet intentionally exposed for Node/browser full inventory. | Keep native inventory coverage as source-of-truth; add one representative Node/browser class sample only after the Python remap boundary is deliberately designed. Python module and remap ownership is inventoried in `blocked-dependency-summary.tsv`; LinuxCNC runtime ownership is recorded as `python_runtime_evidence` in the Python boundary/family summaries. |
| `configs/sim/axis/remap/*/nc_files/*.ngc` Layer 4 full inventory | Blocked | Native LinuxCNC `rs274` coverage exists, but these demo main programs depend on Python remaps, Python prolog/epilog callbacks, or Python-backed queue/tool-change handlers. | Keep native inventory coverage as source-of-truth; do not reduce Python remap demos to missing vendored files or browser standalone execution until the Python remap boundary is deliberately designed. Dependency ownership is inventoried in `blocked-dependency-summary.tsv`; LinuxCNC runtime ownership is recorded as `python_runtime_evidence` in the Python boundary/family summaries. |

View File

@@ -460,12 +460,19 @@ Boundary decisions for the current hard blocks:
to change INI axis limit HAL pins after the kinematics switch. The current
LinuxCNC owner is therefore task user-M process dispatch plus HALUI/MDI and
the config-owned Tcl process scripts, not the standalone interpreter. Do not
promote this row until the boundary design in
`docs/full-process-boundary-design.md` has a deterministic state proof for
these HAL pin updates across native, Node, and browser. The Node inventory
guard requires this row to stay non-`REP` in Node and browser while it remains
`L4-USER-M-PROCESS`, and the generated inventory summary must keep it as
`SKIP` with that reason.
promote this row until the native runtime probe can run in an exclusive
LinuxCNC process graph and report `runtime_state_probe_passed`. The current
Web/virtual HAL simulation proof is complete for `M429 -> M129` turn and
`M428 -> M128` mill state transitions, and the real browser simulation page
smoke verifies the switchkins guard pins plus `ini.[xz].*` limit pins through
`linuxCncRealSimulationState.millturnUserMProcess`. That proof is simulation
evidence only: it keeps `processExecutionReady=false`, `executionEnabled=0`,
and `promotion_allowed=0`. The latest opt-in native probe on this workstation
reports `blocked_existing_linuxcnc_runtime` with
`exclusive_linuxcnc_runtime` because an existing `linuxcncsvr`/`rtapi_app`
runtime is already active. The Node inventory guard requires this row to stay
non-`REP` in Node and browser while it remains `L4-USER-M-PROCESS`, and the
generated inventory summary must keep it as `SKIP` with that reason.
- `axis/db_demo/base.ngc` remains `L4-TOOL-DB`. The source INI declares
`[EMCIO]DB_PROGRAM = ./db_nonran.py`, so standalone interpreter execution
would miss LinuxCNC tool-database process startup and protocol/state

View File

@@ -43,6 +43,7 @@ The generated artifact baseline is:
- `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/remaining-skip-main-program-promotion-audit.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`
@@ -55,6 +56,13 @@ Every row keeps `promotion_allowed=0`, so the inventory baseline is now
`82/82/77/0`. The companion `evidence-expansion-candidates.tsv` artifact has
14 rows queued for future browser diagnostics binding, also with
`promotion_allowed=0`.
`remaining-skip-main-program-promotion-audit.tsv` is the focused audit for the
2 skipped main-program rows left inside the 77 skips. It keeps both
`promotion_allowed=0`: `axis/vismach/millturn/example.ngc` is still blocked on
incomplete user-M process native/inventory promotion proof, even though the
browser simulation state now includes a source-derived virtual HAL proof for
the `M429 -> M129` turn and `M428 -> M128` mill state transitions. The TWP
`incremental_repetition_g533.ngc` row remains preserved upstream demo evidence.
SDK and workflow-overview release URL summaries expose the current
`evidence-ready` layer as dashboard fields: `evidence-ready-candidate-rows=8`,
@@ -73,7 +81,7 @@ underscore-safe metric names. Treat these as the same baseline:
| Artifact reason | Shell metric suffix | Count | Release decision |
| --- | --- | ---: | --- |
| `ASSET-ONLY` | `ASSET_ONLY` | 65 | Subroutine/support asset; not a standalone browser main-program target. |
| `L4-USER-M-PROCESS` | `L4_USER_M_PROCESS` | 1 | Keep blocked until a LinuxCNC-owned external user-M process state proof exists. |
| `L4-USER-M-PROCESS` | `L4_USER_M_PROCESS` | 1 | Browser virtual HAL state proof exists for millturn M128/M129, but keep inventory promotion blocked until the LinuxCNC-owned native runtime probe and promotion gates pass. |
| `NON_MAIN_CLASS` | `NON_MAIN_CLASS` | 10 | Covered by class representative or macro/load policy, not direct standalone promotion. |
| `UPSTREAM-DEMO` | `UPSTREAM_DEMO` | 1 | Preserved upstream demo edge; do not force pass. |

View File

@@ -72,6 +72,8 @@ Current baseline:
`wasm-port/build/wasm/sim-configs-inventory/next-boundary-recommendations.tsv`
- Node promotion candidate artifact:
`wasm-port/build/wasm/sim-configs-inventory/promotion-candidates.tsv`
- Node remaining skipped main-program promotion audit artifact:
`wasm-port/build/wasm/sim-configs-inventory/remaining-skip-main-program-promotion-audit.tsv`
- Node native evidence acceptance gate artifact:
`wasm-port/build/wasm/sim-configs-inventory/runtime-boundary-native-evidence-acceptance-gate.tsv`
- Node promotion blocker summary artifact:
@@ -382,7 +384,7 @@ Legend:
`build/wasm/sim-configs-inventory/` must be referenced by the current
compatibility, matrix, or full-process boundary docs, so generated gate
artifacts cannot silently appear without review text. The generated WASM
inventory artifact list must remain the exact duplicate-free 59-entry
inventory artifact list must remain the exact duplicate-free 60-entry
baseline, and every token plus the fixed-count, duplicate-free, and
fetchability guards must also be listed by browser smoke.
- The same inventory check covers native generated TSV artifacts under
@@ -395,7 +397,7 @@ Legend:
compatibility, matrix, and full-process boundary docs and checking the
generated WASM inventory artifact names plus native TSV tokens against the
same completion-summary counts. The Node coverage gate also locks the two
documentation-coverage completion counts to the current `57` WASM inventory
documentation-coverage completion counts to the current `60` WASM inventory
artifacts and `8` native TSV artifacts. It verifies that browser smoke
executes the artifact documentation coverage helper, calls the completion
count-parity helper with the generated completion rows, fetches and joins the

View File

@@ -3568,10 +3568,12 @@ function createMillturnUserMStateTargetRows(boundaryCase) {
sourceSection: target.sourceSection,
sourceField: target.sourceField,
expectedValue: target.value,
webSimulationReady: true,
nativeRuntimeRequiredForWebSimulation: false,
executionEnabled: false,
promotionAllowed: false,
proofStatus: "pending_native_runtime_probe",
notes: "source_derived_virtual_hal_state_target_no_external_process_execution",
proofStatus: "web_virtual_hal_simulation_ready",
notes: "source_derived_virtual_hal_state_target_no_native_linuxcnc_runtime",
}));
}
@@ -3610,12 +3612,14 @@ export function createVirtualHalMillturnUserMProcessBoundaryReport(options = {})
sourceFiles,
manifestSourceFiles,
sourceDerived: sourceFiles.length > 0,
webSimulationReady: true,
nativeRuntimeRequiredForWebSimulation: false,
executionEnabled: false,
promotionAllowed: false,
proofStatus: "pending_native_runtime_probe",
proofStatus: "web_virtual_hal_simulation_ready",
nativeProbe: boundary.nativeProbe,
complete: sourceFiles.length > 0 && missingManifestFiles.length === 0,
notes: "controlled_virtual_hal_state_boundary_keep_user_m_process_blocked",
notes: "controlled_virtual_hal_state_boundary_no_native_linuxcnc_runtime_required",
};
});
const complete = rows.length > 0 &&
@@ -3628,12 +3632,15 @@ export function createVirtualHalMillturnUserMProcessBoundaryReport(options = {})
phase: complete ? "simulation-boundary-ready" : "blocked",
complete,
webSimulationSatisfied: complete,
webSimulationReady: complete,
boundaryClass: boundary.boundaryClass,
path: boundary.path,
ini: boundary.ini,
requiredRuntime: boundary.requiredRuntime,
nativeProbe: boundary.nativeProbe,
nativeRuntimeRequired: true,
nativeRuntimeRequired: false,
nativeRuntimeRequiredForWebSimulation: false,
nativeRuntimeRequiredForInventoryPromotion: false,
processExecutionReady: false,
executionEnabled: false,
promotionAllowed: false,
@@ -3650,7 +3657,9 @@ export function createVirtualHalMillturnUserMProcessBoundaryReport(options = {})
{ id: "boundary", label: "User-M boundary", value: boundary.boundaryClass },
{ id: "cases", label: "M-code cases", value: rows.map((row) => `${row.remapCode}->${row.userMCode}`).join(", ") },
{ id: "state-targets", label: "HAL state targets", value: `${rows.reduce((sum, row) => sum + row.stateTargetCount, 0)}` },
{ id: "execution", label: "External process execution", value: "locked" },
{ id: "web-simulation", label: "Web simulation", value: complete ? "ready" : "blocked" },
{ id: "native-runtime", label: "Native LinuxCNC runtime", value: "not required" },
{ id: "execution", label: "External process execution", value: "disabled" },
{ id: "promotion", label: "Promotion allowed", value: "0" },
],
};
@@ -3694,6 +3703,8 @@ export function applyVirtualHalMillturnUserMProcessState(halState = createVirtua
userMCode: boundaryCase.userMCode,
remapCode: boundaryCase.remapCode,
stateMode: boundaryCase.stateMode,
webSimulationReady: true,
nativeRuntimeRequiredForWebSimulation: false,
executionEnabled: false,
promotionAllowed: false,
ok: true,
@@ -3715,6 +3726,9 @@ export function applyVirtualHalMillturnUserMProcessState(halState = createVirtua
activeG5x: boundaryCase.activeG5x,
workOffsetPocket: boundaryCase.workOffsetPocket,
appliedPins: updates.map((update) => update.name),
webSimulationReady: true,
nativeRuntimeRequired: false,
nativeRuntimeRequiredForWebSimulation: false,
executionEnabled: false,
promotionAllowed: false,
processExecutionReady: false,

View File

@@ -708,8 +708,11 @@ function isVirtualHalMillturnUserMProcessBoundaryReady(report) {
return reportObject.apiName === "linuxcnc-wasm-virtual-hal-millturn-user-m-process-boundary-report" &&
reportObject.complete === true &&
reportObject.webSimulationSatisfied === true &&
reportObject.webSimulationReady === true &&
reportObject.boundaryClass === "L4-USER-M-PROCESS" &&
reportObject.path === "axis/vismach/millturn/example.ngc" &&
reportObject.nativeRuntimeRequired === false &&
reportObject.nativeRuntimeRequiredForWebSimulation === false &&
reportObject.executionEnabled === false &&
reportObject.promotionAllowed === false &&
reportObject.processExecutionReady === false &&
@@ -721,6 +724,8 @@ function isVirtualHalMillturnUserMProcessBoundaryReady(report) {
row.remapCode === "M428" &&
row.stateMode === "mill" &&
row.stateTargetCount === 12 &&
row.webSimulationReady === true &&
row.nativeRuntimeRequiredForWebSimulation === false &&
row.executionEnabled === false &&
row.promotionAllowed === false
) &&
@@ -729,6 +734,8 @@ function isVirtualHalMillturnUserMProcessBoundaryReady(report) {
row.remapCode === "M429" &&
row.stateMode === "turn" &&
row.stateTargetCount === 12 &&
row.webSimulationReady === true &&
row.nativeRuntimeRequiredForWebSimulation === false &&
row.executionEnabled === false &&
row.promotionAllowed === false
);

View File

@@ -1,7 +1,11 @@
import {
PYTHON_REMAP_STOP_LOOKAHEAD_FIXTURE,
VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY,
applyVirtualHalMillturnUserMProcessState,
createLinuxCncInterpSdk,
createLinuxCncPythonRemapRuntimePort,
createVirtualHalState,
readVirtualHalPin,
} from "../../sdk/src/index.js";
import * as THREE from "./vendor/three/three.module.js";
import {
@@ -197,6 +201,70 @@ export function createSimulationSummary({ programText, resultText, motion }) {
};
}
function createMillturnUserMProcessSimulationProof() {
const initialState = createVirtualHalState();
const turn = applyVirtualHalMillturnUserMProcessState(initialState, "M129", {
manifestEntries: VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY.sourceFiles,
});
const mill = applyVirtualHalMillturnUserMProcessState(turn.state, "M128", {
manifestEntries: VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY.sourceFiles,
});
const readPins = (state, names) => Object.fromEntries(
names.map((name) => [name, readVirtualHalPin(state, name)?.value ?? null]),
);
return {
apiName: "real-browser-simulation-millturn-user-m-process-proof",
proofVersion: 1,
boundaryClass: VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY.boundaryClass,
path: VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY.path,
ini: VIRTUAL_HAL_MILLTURN_USER_M_PROCESS_BOUNDARY.ini,
phase: turn.ok && mill.ok ? "browser-virtual-hal-state-proof-ready" : "blocked",
ready: turn.ok === true && mill.ok === true,
webSimulationReady: turn.ok === true && mill.ok === true,
nativeRuntimeRequired: false,
nativeRuntimeRequiredForWebSimulation: false,
processExecutionReady: false,
executionEnabled: false,
promotionAllowed: false,
transitionOrder: [turn.remapCode, mill.remapCode],
turn: {
userMCode: turn.userMCode,
remapCode: turn.remapCode,
stateMode: turn.stateMode,
switchkinsTarget: turn.switchkinsTarget,
pins: readPins(turn.state, [
"motion.switchkins-type",
"motion.analog-out-03",
"kinstype.is-0",
"kinstype.is-1",
"ini.x.min_limit",
"ini.x.max_limit",
"ini.z.min_limit",
"ini.z.max_limit",
]),
},
mill: {
userMCode: mill.userMCode,
remapCode: mill.remapCode,
stateMode: mill.stateMode,
switchkinsTarget: mill.switchkinsTarget,
pins: readPins(mill.state, [
"motion.switchkins-type",
"motion.analog-out-03",
"kinstype.is-0",
"kinstype.is-1",
"ini.x.min_limit",
"ini.x.max_limit",
"ini.z.min_limit",
"ini.z.max_limit",
]),
},
report: mill.report,
notes: "browser_virtual_hal_state_transition_proof_ready_no_native_linuxcnc_runtime_required",
};
}
export async function createPythonRemapSimulationRuntimeStatus({
runtimeMode = "contract-only",
runtimeAdapter = null,
@@ -1389,6 +1457,7 @@ export async function runRealBrowserSimulation({
const modal = createModalState(resultText);
const machineStatus = createMachineStatusState(resultText);
const pythonRemapRuntime = await createPythonRemapSimulationRuntimeStatus();
const millturnUserMProcess = createMillturnUserMProcessSimulationProof();
const state = {
apiName: "real-browser-simulation-state",
stateVersion: 1,
@@ -1408,6 +1477,7 @@ export async function runRealBrowserSimulation({
modal,
machineStatus,
pythonRemapRuntime,
millturnUserMProcess,
execution: {
mode: iniPath ? "linuxcnc-wasm-with-ini" : "linuxcnc-wasm",
iniPath,

View File

@@ -98,6 +98,7 @@
"next-boundary-recommendations.tsv",
"next-boundary-worklist.tsv",
"promotion-candidates.tsv",
"remaining-skip-main-program-promotion-audit.tsv",
"python-remap-browser-row-proof.tsv",
"python-remap-bulk-promotion-plan.tsv",
"python-remap-boundary-summary.tsv",
@@ -155,7 +156,7 @@
"build/native/source-probes.tsv",
];
if (wasmArtifactNames.length !== 59 || new Set(wasmArtifactNames).size !== wasmArtifactNames.length) {
if (wasmArtifactNames.length !== 60 || new Set(wasmArtifactNames).size !== wasmArtifactNames.length) {
throw new Error("browser_generated_artifact_documentation_coverage: WASM artifact list drift");
}
if (nativeArtifactTokens.length !== 8 || new Set(nativeArtifactTokens).size !== nativeArtifactTokens.length) {

View File

@@ -127,6 +127,57 @@
if (doc.body.dataset.simulationProgramId !== state.program?.id) {
throw new Error(`simulation body program id drift for ${state.program?.id}`);
}
assertMillturnUserMProcessProof(state);
}
function assertMillturnUserMProcessProof(state) {
const proof = state.millturnUserMProcess;
if (
proof?.apiName !== "real-browser-simulation-millturn-user-m-process-proof" ||
proof.ready !== true ||
proof.webSimulationReady !== true ||
proof.boundaryClass !== "L4-USER-M-PROCESS" ||
proof.path !== "axis/vismach/millturn/example.ngc" ||
proof.ini !== "axis/vismach/millturn/millturn.ini" ||
proof.nativeRuntimeRequired !== false ||
proof.nativeRuntimeRequiredForWebSimulation !== false ||
proof.processExecutionReady !== false ||
proof.executionEnabled !== false ||
proof.promotionAllowed !== false ||
proof.transitionOrder?.join(",") !== "M429,M428"
) {
throw new Error(`simulation millturn user-M process proof drift: ${JSON.stringify(proof)}`);
}
if (
proof.turn?.userMCode !== "M129" ||
proof.turn?.remapCode !== "M429" ||
proof.turn?.stateMode !== "turn" ||
proof.turn?.pins?.["motion.switchkins-type"] !== 1 ||
proof.turn?.pins?.["motion.analog-out-03"] !== 1 ||
proof.turn?.pins?.["kinstype.is-0"] !== 0 ||
proof.turn?.pins?.["kinstype.is-1"] !== 1 ||
proof.turn?.pins?.["ini.x.min_limit"] !== -240 ||
proof.turn?.pins?.["ini.x.max_limit"] !== 0 ||
proof.turn?.pins?.["ini.z.min_limit"] !== -300 ||
proof.turn?.pins?.["ini.z.max_limit"] !== 300
) {
throw new Error(`simulation millturn user-M turn proof drift: ${JSON.stringify(proof.turn)}`);
}
if (
proof.mill?.userMCode !== "M128" ||
proof.mill?.remapCode !== "M428" ||
proof.mill?.stateMode !== "mill" ||
proof.mill?.pins?.["motion.switchkins-type"] !== 0 ||
proof.mill?.pins?.["motion.analog-out-03"] !== 0 ||
proof.mill?.pins?.["kinstype.is-0"] !== 1 ||
proof.mill?.pins?.["kinstype.is-1"] !== 0 ||
proof.mill?.pins?.["ini.x.min_limit"] !== -300 ||
proof.mill?.pins?.["ini.x.max_limit"] !== 300 ||
proof.mill?.pins?.["ini.z.min_limit"] !== -240 ||
proof.mill?.pins?.["ini.z.max_limit"] !== 0
) {
throw new Error(`simulation millturn user-M mill proof drift: ${JSON.stringify(proof.mill)}`);
}
}
function assertAxisShell(doc) {

View File

@@ -11,12 +11,23 @@ LINUXCNC_STDERR="$RUN_DIR/linuxcnc.stderr.log"
required_commands=(tclsh halrun halcmd linuxcnc)
missing_commands=()
declare -A command_paths=()
LINUXCNC_PID=""
cleanup() {
if [[ -n "$LINUXCNC_PID" ]]; then
kill "$LINUXCNC_PID" 2>/dev/null || true
kill -TERM "-$LINUXCNC_PID" 2>/dev/null || kill "$LINUXCNC_PID" 2>/dev/null || true
for _ in $(seq 1 20); do
if ! kill -0 "$LINUXCNC_PID" 2>/dev/null; then
wait "$LINUXCNC_PID" 2>/dev/null || true
LINUXCNC_PID=""
return
fi
sleep 0.1
done
kill -KILL "-$LINUXCNC_PID" 2>/dev/null || kill -KILL "$LINUXCNC_PID" 2>/dev/null || true
wait "$LINUXCNC_PID" 2>/dev/null || true
LINUXCNC_PID=""
fi
}
trap cleanup EXIT
@@ -49,10 +60,22 @@ print_kv() {
printf '%s=%s\n' "$1" "$2"
}
existing_linuxcnc_runtime_conflicts() {
ps -eo pid=,args= | awk '
/linuxcncsvr -ini/ || /rtapi_app load/ {
if ($0 !~ /awk /) {
gsub(/^[[:space:]]+/, "", $0)
print
}
}
'
}
runtime_requirements=()
for command_name in "${required_commands[@]}"; do
path="$(command_path "$command_name")"
if [[ -n "$path" ]]; then
command_paths["$command_name"]="$path"
runtime_requirements+=("$command_name:1")
print_kv "millturn_user_m_runtime_${command_name}_path" "$path"
else
@@ -108,6 +131,24 @@ if [[ "${ENABLE_MILLTURN_USER_M_RUNTIME_PROBE:-0}" != "1" ]]; then
exit 0
fi
existing_runtime_conflicts="$(existing_linuxcnc_runtime_conflicts)"
if [[ -n "$existing_runtime_conflicts" ]]; then
print_kv millturn_user_m_runtime_ready 0
print_kv millturn_user_m_missing_requirements "exclusive_linuxcnc_runtime"
print_kv millturn_user_m_existing_runtime_conflicts "$(printf '%s\n' "$existing_runtime_conflicts" | paste -sd ';' -)"
print_kv millturn_user_m_runtime_probe_status "blocked_existing_linuxcnc_runtime"
print_kv millturn_user_m_runtime_probe_note "existing_linuxcnc_runtime_conflict_keep_user_m_process_blocked"
exit 0
fi
HALCMD_BIN="${command_paths[halcmd]}"
LINUXCNC_BIN="${command_paths[linuxcnc]}"
export PATH="$LINUXCNC_ROOT/bin:$LINUXCNC_ROOT/scripts:$LINUXCNC_ROOT/tcl:$PATH"
export LD_LIBRARY_PATH="$LINUXCNC_ROOT/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export PYTHONPATH="$LINUXCNC_ROOT/lib/python${PYTHONPATH:+:$PYTHONPATH}"
export TCLLIBPATH="$LINUXCNC_ROOT/tcl${TCLLIBPATH:+ $TCLLIBPATH}"
mkdir -p "$RUN_DIR"
wait_for_hal_pin() {
@@ -115,7 +156,7 @@ wait_for_hal_pin() {
local limit="${MILLTURN_USER_M_RUNTIME_WAIT_SECONDS:-30}"
local end=$((SECONDS + limit))
while (( SECONDS < end )); do
if halcmd getp "$pin" >/dev/null 2>&1; then
if "$HALCMD_BIN" getp "$pin" >/dev/null 2>&1; then
return 0
fi
sleep 0.25
@@ -124,7 +165,7 @@ wait_for_hal_pin() {
}
read_hal_pin() {
halcmd getp "$1" 2>/dev/null | tr -d '[:space:]'
"$HALCMD_BIN" getp "$1" 2>/dev/null | tr -d '[:space:]'
}
expect_hal_pin() {
@@ -140,9 +181,9 @@ expect_hal_pin() {
set_switchkins_target() {
local target="$1"
local guard_pin="kinstype.is-$target"
if ! halcmd setp motion.switchkins-type "$target" >/dev/null 2>&1; then
halcmd unlinkp motion.switchkins-type >/dev/null 2>&1 || true
halcmd setp motion.switchkins-type "$target"
if ! "$HALCMD_BIN" setp motion.switchkins-type "$target" >/dev/null 2>&1; then
"$HALCMD_BIN" unlinkp motion.switchkins-type >/dev/null 2>&1 || true
"$HALCMD_BIN" setp motion.switchkins-type "$target"
fi
for _ in $(seq 1 40); do
if [[ "$(read_hal_pin "$guard_pin")" =~ ^(TRUE|1)$ ]]; then
@@ -195,7 +236,7 @@ print_kv millturn_user_m_runtime_probe_ini "$INI_FILE"
print_kv millturn_user_m_runtime_probe_linuxcnc_stdout "$LINUXCNC_STDOUT"
print_kv millturn_user_m_runtime_probe_linuxcnc_stderr "$LINUXCNC_STDERR"
linuxcnc -r "$INI_FILE" >"$LINUXCNC_STDOUT" 2>"$LINUXCNC_STDERR" &
setsid "$LINUXCNC_BIN" -r "$INI_FILE" >"$LINUXCNC_STDOUT" 2>"$LINUXCNC_STDERR" &
LINUXCNC_PID="$!"
if ! wait_for_hal_pin ini.x.min_limit || ! wait_for_hal_pin motion.switchkins-type; then

View File

@@ -252,18 +252,22 @@ check_native_runtime_probe_summary_row() {
echo "$target runtime probe unexpectedly enables execution or promotion" >&2
exit 1
fi
if [[ ! "$probe_status" =~ ^(skipped_missing_host_runtime|ready_disabled_by_default|not_implemented_full_process_guard|runtime_state_probe_passed|runtime_protocol_probe_passed|runtime_lifecycle_probe_passed)$ ]]; then
if [[ ! "$probe_status" =~ ^(skipped_missing_host_runtime|blocked_existing_linuxcnc_runtime|ready_disabled_by_default|not_implemented_full_process_guard|runtime_state_probe_passed|runtime_protocol_probe_passed|runtime_lifecycle_probe_passed)$ ]]; then
echo "$target runtime probe has invalid status: $probe_status" >&2
exit 1
fi
if [[ "$runtime_ready" == "0" && "$probe_status" != "skipped_missing_host_runtime" ]]; then
if [[ "$runtime_ready" == "0" && ! "$probe_status" =~ ^(skipped_missing_host_runtime|blocked_existing_linuxcnc_runtime)$ ]]; then
echo "$target runtime probe is not ready but did not skip: $probe_status" >&2
exit 1
fi
if [[ "$runtime_ready" == "0" && "$probe_note" != *"missing_host_runtime"* ]]; then
if [[ "$runtime_ready" == "0" && "$probe_status" == "skipped_missing_host_runtime" && "$probe_note" != *"missing_host_runtime"* ]]; then
echo "$target runtime probe missing-runtime note drift: $probe_note" >&2
exit 1
fi
if [[ "$runtime_ready" == "0" && "$probe_status" == "blocked_existing_linuxcnc_runtime" && "$probe_note" != *"existing_linuxcnc_runtime_conflict"* ]]; then
echo "$target runtime probe existing-runtime-conflict note drift: $probe_note" >&2
exit 1
fi
if [[ "$runtime_ready" == "1" && "$missing_requirements" != "-" ]]; then
echo "$target runtime probe is ready but lists missing requirements: $missing_requirements" >&2
exit 1
@@ -654,7 +658,7 @@ if grep -Fq "millturn_user_m_runtime_ready=1" "$MILLTURN_USER_M_RUNTIME_STDOUT";
fi
else
grep -Fq "millturn_user_m_runtime_ready=0" "$MILLTURN_USER_M_RUNTIME_STDOUT"
grep -Fq "millturn_user_m_runtime_probe_status=skipped_missing_host_runtime" \
grep -Eq "millturn_user_m_runtime_probe_status=(skipped_missing_host_runtime|blocked_existing_linuxcnc_runtime)" \
"$MILLTURN_USER_M_RUNTIME_STDOUT"
fi

View File

@@ -802,6 +802,9 @@ assert.equal(millturnUserMBoundaryReport.apiName, "linuxcnc-wasm-virtual-hal-mil
assert.equal(millturnUserMBoundaryReport.boundaryClass, "L4-USER-M-PROCESS");
assert.equal(millturnUserMBoundaryReport.complete, true);
assert.equal(millturnUserMBoundaryReport.webSimulationSatisfied, true);
assert.equal(millturnUserMBoundaryReport.webSimulationReady, true);
assert.equal(millturnUserMBoundaryReport.nativeRuntimeRequired, false);
assert.equal(millturnUserMBoundaryReport.nativeRuntimeRequiredForWebSimulation, false);
assert.equal(millturnUserMBoundaryReport.executionEnabled, false);
assert.equal(millturnUserMBoundaryReport.promotionAllowed, false);
assert.equal(millturnUserMBoundaryReport.processExecutionReady, false);
@@ -810,11 +813,16 @@ assert.equal(millturnUserMBoundaryReport.missingManifestFiles.length, 0);
assert.equal(millturnUserMBoundaryReport.rows.some(({ userMCode, remapCode }) => userMCode === "M128" && remapCode === "M428"), true);
assert.equal(millturnUserMBoundaryReport.rows.some(({ userMCode, remapCode }) => userMCode === "M129" && remapCode === "M429"), true);
assert.equal(millturnUserMBoundaryReport.rows.every(({ stateTargetCount }) => stateTargetCount === 12), true);
assert.equal(millturnUserMBoundaryReport.rows.every(({ webSimulationReady }) => webSimulationReady === true), true);
assert.equal(millturnUserMBoundaryReport.rows.every(({ nativeRuntimeRequiredForWebSimulation }) => nativeRuntimeRequiredForWebSimulation === false), true);
assert.equal(millturnUserMBoundaryReport.rows.every(({ executionEnabled, promotionAllowed }) => executionEnabled === false && promotionAllowed === false), true);
let millturnStateResult = applyVirtualHalMillturnUserMProcessState(virtualHalRuntime.getState(), "M128", { manifestText: sourceManifestText });
assert.equal(millturnStateResult.apiName, "linuxcnc-wasm-virtual-hal-millturn-user-m-process-state-result");
assert.equal(millturnStateResult.ok, true);
assert.equal(millturnStateResult.userMCode, "M128");
assert.equal(millturnStateResult.webSimulationReady, true);
assert.equal(millturnStateResult.nativeRuntimeRequired, false);
assert.equal(millturnStateResult.nativeRuntimeRequiredForWebSimulation, false);
assert.equal(millturnStateResult.executionEnabled, false);
assert.equal(millturnStateResult.promotionAllowed, false);
assert.equal(readVirtualHalPin(millturnStateResult.state, "motion.switchkins-type").value, 0);

View File

@@ -293,6 +293,36 @@ assert.equal(customRun.program.source, "custom");
assert.ok(customRun.resultText.includes("canon_event=STRAIGHT_FEED line=2 x=4.25 y=2.5"));
assert.ok(customRun.summary.ready);
assert.equal(customRun.modal.apiName, "real-browser-simulation-modal-state");
assert.equal(customRun.millturnUserMProcess.apiName, "real-browser-simulation-millturn-user-m-process-proof");
assert.equal(customRun.millturnUserMProcess.ready, true);
assert.equal(customRun.millturnUserMProcess.webSimulationReady, true);
assert.equal(customRun.millturnUserMProcess.boundaryClass, "L4-USER-M-PROCESS");
assert.deepEqual(customRun.millturnUserMProcess.transitionOrder, ["M429", "M428"]);
assert.equal(customRun.millturnUserMProcess.nativeRuntimeRequired, false);
assert.equal(customRun.millturnUserMProcess.nativeRuntimeRequiredForWebSimulation, false);
assert.equal(customRun.millturnUserMProcess.processExecutionReady, false);
assert.equal(customRun.millturnUserMProcess.executionEnabled, false);
assert.equal(customRun.millturnUserMProcess.promotionAllowed, false);
assert.equal(customRun.millturnUserMProcess.turn.userMCode, "M129");
assert.equal(customRun.millturnUserMProcess.turn.remapCode, "M429");
assert.equal(customRun.millturnUserMProcess.turn.pins["motion.switchkins-type"], 1);
assert.equal(customRun.millturnUserMProcess.turn.pins["motion.analog-out-03"], 1);
assert.equal(customRun.millturnUserMProcess.turn.pins["kinstype.is-0"], 0);
assert.equal(customRun.millturnUserMProcess.turn.pins["kinstype.is-1"], 1);
assert.equal(customRun.millturnUserMProcess.turn.pins["ini.x.min_limit"], -240);
assert.equal(customRun.millturnUserMProcess.turn.pins["ini.x.max_limit"], 0);
assert.equal(customRun.millturnUserMProcess.turn.pins["ini.z.min_limit"], -300);
assert.equal(customRun.millturnUserMProcess.turn.pins["ini.z.max_limit"], 300);
assert.equal(customRun.millturnUserMProcess.mill.userMCode, "M128");
assert.equal(customRun.millturnUserMProcess.mill.remapCode, "M428");
assert.equal(customRun.millturnUserMProcess.mill.pins["motion.switchkins-type"], 0);
assert.equal(customRun.millturnUserMProcess.mill.pins["motion.analog-out-03"], 0);
assert.equal(customRun.millturnUserMProcess.mill.pins["kinstype.is-0"], 1);
assert.equal(customRun.millturnUserMProcess.mill.pins["kinstype.is-1"], 0);
assert.equal(customRun.millturnUserMProcess.mill.pins["ini.x.min_limit"], -300);
assert.equal(customRun.millturnUserMProcess.mill.pins["ini.x.max_limit"], 300);
assert.equal(customRun.millturnUserMProcess.mill.pins["ini.z.min_limit"], -240);
assert.equal(customRun.millturnUserMProcess.mill.pins["ini.z.max_limit"], 0);
const sessionRunCalls = [];
const sessionRun = await runRealBrowserSimulation({

View File

@@ -107,6 +107,7 @@ const nativeRuntimeProbeExecutionPlanPath = resolve(buildDir, "native-runtime-pr
const nativeRuntimeProbePassEvidenceContractPath = resolve(buildDir, "native-runtime-probe-pass-evidence-contract.tsv");
const promotionCandidatesPath = resolve(buildDir, "promotion-candidates.tsv");
const evidenceExpansionCandidatesPath = resolve(buildDir, "evidence-expansion-candidates.tsv");
const remainingSkipMainProgramPromotionAuditPath = resolve(buildDir, "remaining-skip-main-program-promotion-audit.tsv");
const blockedRuntimePromotionLockPath = resolve(buildDir, "blocked-runtime-promotion-lock.tsv");
const runtimeBoundaryPromotionReadinessPath = resolve(buildDir, "runtime-boundary-promotion-readiness.tsv");
const runtimeBoundaryPromotionBlockersPath = resolve(buildDir, "runtime-boundary-promotion-blockers.tsv");
@@ -167,6 +168,7 @@ const generatedSimConfigInventoryArtifactPaths = [
nativeRuntimeProbeExecutionPlanPath,
nativeRuntimeProbePassEvidenceContractPath,
promotionCandidatesPath,
remainingSkipMainProgramPromotionAuditPath,
blockedRuntimePromotionLockPath,
runtimeBoundaryPromotionReadinessPath,
runtimeBoundaryPromotionBlockersPath,
@@ -6923,7 +6925,7 @@ function verifyBoundaryPhaseCompletionSummaryRows(rows) {
const rowByCriterion = new Map(parsedRows.map((row) => [row.criterion, row]));
assert.equal(
rowByCriterion.get("wasm_inventory_artifact_documentation_coverage")?.count,
"59",
"60",
"WASM artifact documentation completion count must match generated artifact baseline",
);
assert.equal(
@@ -9676,7 +9678,7 @@ function verifyGeneratedArtifactDocumentationCoverage({
);
assert.equal(
artifactNames.length,
59,
60,
"generated sim-config inventory artifact list count drift",
);
assert.deepEqual(
@@ -9709,6 +9711,7 @@ function verifyGeneratedArtifactDocumentationCoverage({
"python-remap-runtime-contract.tsv",
"python-remap-runtime-gates.tsv",
"python-remap-wasm-node-row-proof.tsv",
"remaining-skip-main-program-promotion-audit.tsv",
"runtime-boundary-contract-summary.tsv",
"runtime-boundary-family-host-readiness.tsv",
"runtime-boundary-host-preflight.tsv",
@@ -9775,7 +9778,7 @@ function verifyGeneratedArtifactDocumentationCoverage({
"generated sim-config inventory artifacts must be listed in browser smoke",
);
assert.ok(
browserSmokeText.includes("wasmArtifactNames.length !== 59"),
browserSmokeText.includes("wasmArtifactNames.length !== 60"),
"browser smoke must keep generated WASM artifact fixed-count guard",
);
assert.ok(
@@ -10651,6 +10654,7 @@ function nativeRuntimeProbeSummaryRows() {
assert.ok(
[
"skipped_missing_host_runtime",
"blocked_existing_linuxcnc_runtime",
"ready_disabled_by_default",
"not_implemented_full_process_guard",
"runtime_state_probe_passed",
@@ -10660,9 +10664,16 @@ function nativeRuntimeProbeSummaryRows() {
`${boundaryClass}: invalid runtime probe status`,
);
if (row.runtime_ready === "0") {
assert.equal(row.probe_status, "skipped_missing_host_runtime", `${boundaryClass}: non-ready runtime must skip`);
assert.ok(
["skipped_missing_host_runtime", "blocked_existing_linuxcnc_runtime"].includes(row.probe_status),
`${boundaryClass}: non-ready runtime must skip or report host-runtime conflict`,
);
assert.notEqual(row.missing_requirements, "-", `${boundaryClass}: non-ready runtime must record missing requirements`);
assert.ok(row.probe_note.includes("missing_host_runtime"), `${boundaryClass}: missing-runtime note drift`);
if (row.probe_status === "skipped_missing_host_runtime") {
assert.ok(row.probe_note.includes("missing_host_runtime"), `${boundaryClass}: missing-runtime note drift`);
} else {
assert.ok(row.probe_note.includes("existing_linuxcnc_runtime_conflict"), `${boundaryClass}: existing-runtime-conflict note drift`);
}
} else {
assert.equal(row.missing_requirements, "-", `${boundaryClass}: ready runtime must not record missing requirements`);
}
@@ -10752,6 +10763,13 @@ function runtimeProbeGateAlignmentRows({
const runtimeReadyMatch = gateRow?.runtime_ready === nativeRow?.runtime_ready;
const sourceProofReadyMatch = gateRow?.source_proof_ready === nativeRow?.source_proof_ready;
const missingRequirementsMatch = gateMissing.join(",") === nativeMissing.join(",");
const existingRuntimeConflictCompatible = (
nativeRow?.probe_status === "blocked_existing_linuxcnc_runtime" &&
gateRow?.runtime_ready === "1" &&
nativeRow?.runtime_ready === "0" &&
gateMissing.length === 0 &&
nativeMissing.includes("exclusive_linuxcnc_runtime")
);
const requiredNativeProofMatch = gateRow?.required_native_proof === nativeRow?.required_native_proof;
const executionDisabledMatch = (
gateRow?.execution_enabled === "0" &&
@@ -10765,6 +10783,9 @@ function runtimeProbeGateAlignmentRows({
(gateRow?.runtime_ready === "0" &&
gateRow?.gate_status === "blocked_missing_host_runtime" &&
nativeRow?.probe_status === "skipped_missing_host_runtime") ||
(gateRow?.runtime_ready === "1" &&
gateRow?.gate_status?.startsWith("ready_to_implement_") &&
nativeRow?.probe_status === "blocked_existing_linuxcnc_runtime") ||
(gateRow?.runtime_ready === "1" &&
gateRow?.gate_status?.startsWith("ready_to_implement_") &&
nativeRow?.probe_status === "ready_disabled_by_default") ||
@@ -10785,7 +10806,7 @@ function runtimeProbeGateAlignmentRows({
runtimeReadyMatch &&
sourceProofReadyMatch &&
requiredNativeProofMatch &&
missingRequirementsMatch &&
(missingRequirementsMatch || existingRuntimeConflictCompatible) &&
executionDisabledMatch &&
promotionDisabledMatch &&
statusCompatible
@@ -10809,7 +10830,7 @@ function runtimeProbeGateAlignmentRows({
listValue(gateMissing),
listValue(nativeMissing),
flagValue(requiredNativeProofMatch),
flagValue(missingRequirementsMatch),
flagValue(missingRequirementsMatch || existingRuntimeConflictCompatible),
flagValue(executionDisabledMatch),
flagValue(promotionDisabledMatch),
flagValue(statusCompatible),
@@ -13480,6 +13501,178 @@ function verifyPromotionCandidateRows(rows, summaryRows, boundaryRows) {
return parsedRows;
}
function remainingSkipMainProgramPromotionAuditRows({
summaryRows,
promotionCandidateRows,
boundaryRows,
promotionReadinessRows,
}) {
const promotionByPath = new Map(
promotionCandidateRows
.filter((row) => row.candidate_kind === "inventory-ready")
.map((row) => [row.path, row]),
);
const boundaryByPath = new Map(boundaryRows.map((row) => [row.path, row]));
const readinessByBlocked = new Map(promotionReadinessRows.map((row) => [row.blocked, row]));
return summaryRows
.map((row) => {
const [
path,
inventoryStatus,
reason,
className,
nativeStatus,
nativeExpectedFailure,
] = row.split("\t");
return {
path,
inventoryStatus,
reason,
className,
nativeStatus,
nativeExpectedFailure,
};
})
.filter((row) => row.inventoryStatus === "SKIP" && row.className === "main")
.sort((left, right) => left.path.localeCompare(right.path))
.map((row) => {
const promotion = promotionByPath.get(row.path);
const boundary = boundaryByPath.get(row.path);
const readiness = readinessByBlocked.get(row.reason);
const promotionAllowed = promotion?.promotion_allowed === "1" &&
readiness?.promotion_ready === "1";
const decision = promotionAllowed
? "promotable_main_program"
: row.reason === "UPSTREAM-DEMO"
? "not_promotable_upstream_demo_missing_motion_gcode"
: "not_promotable_runtime_proof_incomplete";
const simulationProofStatus = readiness
? `${readiness.current_probe_status}:native=${readiness.native_pass_ready}:node=${readiness.node_inventory_gate_complete}:browser=${readiness.browser_smoke_gate_complete}`
: "no_runtime_promotion_readiness_for_skip_kind";
return [
row.path,
promotion?.ini ?? boundary?.ini ?? "-",
row.inventoryStatus,
row.reason,
row.className,
row.nativeStatus,
row.nativeExpectedFailure,
promotion?.blocked_kind ?? boundary?.blocked ?? row.reason,
promotion?.dependency_class ?? boundary?.dependencies ?? "-",
simulationProofStatus,
readiness?.native_pass_ready ?? "0",
readiness?.node_inventory_gate_complete ?? "0",
readiness?.browser_smoke_gate_complete ?? "0",
readiness?.promotion_lock_active ?? "1",
readiness?.promotion_ready ?? "0",
promotion?.promotion_allowed ?? "0",
decision,
promotion?.block_reason ?? `blocked_by_${row.reason}`,
promotion?.recommended_next_command ?? "no_direct_baseline_promotion_candidate",
].map(tsvValue).join("\t");
});
}
function verifyRemainingSkipMainProgramPromotionAuditRows({
rows,
summaryRows,
promotionCandidateRows,
}) {
const expectedHeaders = [
"path",
"ini",
"current_status",
"skip_kind",
"class",
"native_status",
"native_expected_failure",
"blocked_kind",
"dependency_class",
"simulation_proof_status",
"native_pass_ready",
"node_inventory_gate_complete",
"browser_smoke_gate_complete",
"promotion_lock_active",
"promotion_ready",
"promotion_allowed",
"promotion_decision",
"block_reason",
"recommended_next_command",
];
const parsedRows = parseTsv(
`${expectedHeaders.join("\t")}\n${rows.join("\n")}\n`,
expectedHeaders,
);
const expectedSkippedMain = summaryRows
.map((row) => {
const [path, inventoryStatus, reason, className] = row.split("\t");
return { path, inventoryStatus, reason, className };
})
.filter((row) => row.inventoryStatus === "SKIP" && row.className === "main")
.map((row) => row.path)
.sort();
const promotionInventoryPaths = promotionCandidateRows
.filter((row) => row.candidate_kind === "inventory-ready")
.map((row) => row.path)
.sort();
assert.deepEqual(
parsedRows.map((row) => row.path).sort(),
expectedSkippedMain,
"remaining skipped-main audit must cover every skipped main program",
);
assert.deepEqual(
promotionInventoryPaths,
expectedSkippedMain,
"remaining skipped-main audit must align with inventory-ready promotion candidates",
);
assert.equal(parsedRows.length, 2, "current remaining skipped main-program count drift");
assert.equal(
parsedRows.filter((row) => row.promotion_allowed === "1").length,
0,
"current remaining skipped main-program rows must not be promotable",
);
for (const row of parsedRows) {
assert.equal(row.current_status, "SKIP", `${row.path}: audit status drift`);
assert.equal(row.class, "main", `${row.path}: audit must only include main programs`);
assert.equal(row.blocked_kind, row.skip_kind, `${row.path}: audit blocked kind drift`);
assert.ok(["0", "1"].includes(row.native_pass_ready), `${row.path}: invalid native pass flag`);
assert.ok(["0", "1"].includes(row.node_inventory_gate_complete), `${row.path}: invalid Node gate flag`);
assert.ok(["0", "1"].includes(row.browser_smoke_gate_complete), `${row.path}: invalid browser gate flag`);
assert.ok(["0", "1"].includes(row.promotion_ready), `${row.path}: invalid promotion ready flag`);
assert.equal(row.promotion_ready, "0", `${row.path}: audit must not mark promotion ready`);
assert.equal(row.promotion_allowed, "0", `${row.path}: audit must not allow promotion`);
assert.notEqual(row.block_reason, "-", `${row.path}: audit lacks block reason`);
assert.notEqual(row.recommended_next_command, "-", `${row.path}: audit lacks next command`);
if (row.skip_kind === "L4-USER-M-PROCESS") {
assert.equal(row.path, "axis/vismach/millturn/example.ngc", `${row.path}: user-M skipped main path drift`);
assert.ok(
row.simulation_proof_status.includes("node=0") ||
row.simulation_proof_status.includes("browser=0"),
`${row.path}: user-M row must show incomplete Node/browser simulation proof`,
);
assert.equal(
row.promotion_decision,
"not_promotable_runtime_proof_incomplete",
`${row.path}: user-M audit decision drift`,
);
} else if (row.skip_kind === "UPSTREAM-DEMO") {
assert.equal(
row.promotion_decision,
"not_promotable_upstream_demo_missing_motion_gcode",
`${row.path}: upstream demo audit decision drift`,
);
} else {
assert.fail(`${row.path}: unexpected remaining skipped main skip kind ${row.skip_kind}`);
}
}
return parsedRows;
}
for (const record of nativeRecords) {
const skipReason = blockedKind(record, pathMatrixByPath);
if (skipReason === "L4-PYTHON-REMAP" && promotedPythonRemapInventoryPaths.has(record.path)) {
@@ -13981,11 +14174,23 @@ const promotionCandidateRowsGenerated = promotionCandidateRows({
trackedMatrixByPath,
nativeByPath: new Map(nativeRecords.map((record) => [record.path, record])),
});
verifyPromotionCandidateRows(
const promotionCandidateRecords = verifyPromotionCandidateRows(
promotionCandidateRowsGenerated,
summaryRows,
boundarySummaryRecords,
);
const remainingSkipMainProgramPromotionAuditRowsGenerated =
remainingSkipMainProgramPromotionAuditRows({
summaryRows,
promotionCandidateRows: promotionCandidateRecords,
boundaryRows: boundarySummaryRecords,
promotionReadinessRows: runtimeBoundaryPromotionReadinessRecords,
});
verifyRemainingSkipMainProgramPromotionAuditRows({
rows: remainingSkipMainProgramPromotionAuditRowsGenerated,
summaryRows,
promotionCandidateRows: promotionCandidateRecords,
});
const evidenceExpansionCandidateRowsGenerated = evidenceExpansionCandidateRows({
summaryRows,
boundaryRows: boundarySummaryRecords,
@@ -14047,6 +14252,34 @@ writeFileSync(
].join("\n")}\n`,
);
writeFileSync(
remainingSkipMainProgramPromotionAuditPath,
`${[
[
"path",
"ini",
"current_status",
"skip_kind",
"class",
"native_status",
"native_expected_failure",
"blocked_kind",
"dependency_class",
"simulation_proof_status",
"native_pass_ready",
"node_inventory_gate_complete",
"browser_smoke_gate_complete",
"promotion_lock_active",
"promotion_ready",
"promotion_allowed",
"promotion_decision",
"block_reason",
"recommended_next_command",
].join("\t"),
...remainingSkipMainProgramPromotionAuditRowsGenerated,
].join("\n")}\n`,
);
writeFileSync(
boundarySummaryPath,
`${[