完成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

@@ -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,
`${[