# 05 RUN 功能程序实现详细实施步骤

生成时间：2026-06-22

## 1. 实施目标

本文件把 `working_run` 下 01-04 文档中的原则、前置条件、源码对照和测试结论，整理成可直接执行的程序实现步骤。

目标不是做一个前端 G-code 播放器，而是实现这个链路：

```text
LinuxCNC INI contract
-> profile 与 INI 一致
-> kinematics WASM 与 INI [KINS] 一致
-> machine files 按同一个 INI stage
-> task/HAL runtime 按同一个 machine context 初始化
-> selected G-code 通过 task plan open 打开
-> RUN 发送 LinuxCNC task command
-> taskHalStatusLoop 持续 runCycles/readStatus
-> UI 只消费 task/HAL/motion feedback
```

## 2. 实施边界

必须保持的边界：

```text
1. 不在 UI/store 自行解析 G-code 来推进 activeLine。
2. 不在 UI/store 自造 axisPose、velocity、DTG。
3. 不把 canonical timing estimate 当成真实 task/HAL runtime。
4. 不在真实 RUN 前置失败时退回 fixture playback。
5. RUN、PAUSE、RESUME、STOP 必须映射为 LinuxCNC task command 语义。
6. activeLine、DRO、RTCP、velocity、runtime feedback 必须来自同一个 task/HAL/motion status snapshot。
```

允许保留的 fallback：

```text
fixture-line-playback 只能作为开发演示 fallback。
一旦 taskHalRuntime.loaded === true，真实 RUN 失败必须明确 blocked，不能 fallback 冒充 LinuxCNC runtime。
```

## 3. 主要代码落点

需要实现或调整的文件：

```text
app/src/runtime/linuxcnc-ini-runtime.js
app/src/profiles/index.js
app/src/profiles/xyzac-trt.js
app/src/profiles/xyzbc-trt.js
app/src/runtime/linuxcnc-kinematics-runtime.js
app/src/runtime/linuxcnc-machine-file-staging.js
app/src/runtime/linuxcnc-task-hal-runtime.js
app/src/runtime/linuxcnc-task-hal-worker-client.js
app/src/runtime/linuxcnc-task-hal-worker.js
app/src/state/store.js
app/src/state/linuxcnc-task-policy.js
app/src/runtime/full-execution-boundary.js
app/src/ui/gmoccapy-shell.js
app/package.json
```

需要新增或扩展的测试：

```text
tests/node/verify_run_preconditions.mjs
tests/node/verify_machine_file_staging.mjs
tests/node/verify_linuxcnc_ini_runtime.mjs
tests/node/verify_linuxcnc_kinematics_runtime.mjs
tests/node/verify_linuxcnc_task_hal_runtime.mjs
tests/node/verify_run_feedback_loop.mjs
tests/node/verify_rtcp_store.mjs
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
```

## 4. 阶段 0：建立当前状态基线

先确认当前代码和文档一致，避免在错误前提上继续实现。

执行：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run build
```

基线记录：

```text
1. 哪些测试已通过。
2. 哪些测试失败，失败是否来自外部 linuxcnc source 缺文件、runtime 缺功能，还是 RUN 逻辑本身。
3. 当前 store 中是否已有 validateRunPreconditions、initializeTaskHalSession、runValidatedTaskHalProgramRun、taskHalStatusLoop。
```

通过标准：

```text
已知 smoke 能稳定复现。
失败项有明确归因。
不在没有基线的情况下改 RUN 主流程。
```

## 5. 阶段 1：实现 INI definition contract

目标：让 `RUN` 依赖结构化 INI readiness，而不是散落字符串和 profile 默认值。

### 5.1 扩展 INI parser

文件：

```text
app/src/runtime/linuxcnc-ini-runtime.js
```

实现内容：

```text
1. parseLinuxCncIni(text, options) 必须解析这些 section:
   [EMC]
   [DISPLAY]
   [RS274NGC]
   [KINS]
   [HAL]
   [HALUI]
   [TRAJ]
   [EMCMOT]
   [TASK]
   [EMCIO]
   [AXIS_*]
   [JOINT_*]

2. 返回结构必须包含:
   machineName
   profileId
   kinematics.name
   kinematicsModuleId
   kinematicsParameters.sparm
   kinematicsParameters.joints
   traj.coordinates
   traj.linearUnits
   traj.angularUnits
   rs274ngc.halPinVars
   rs274ngc.remaps
   rs274ngc.subroutinePath
   rs274ngc.parameterFile
   hal.halui
   hal.halFiles
   hal.postguiHalFiles
   hal.halcmd
   hal.switchkinsSignal
   halui.mdiCommands
   emcmot.module
   emcmot.servoPeriodNs
   task.module
   task.cycleTimeSeconds
   emcio.toolTable
   axisLimits
   jointConfig
   validation.ready
   validation.errors

3. validateIniConfig() 必须校验:
   required sections 存在
   COORDINATES 是 XYZAC 或 XYZBC
   KINEMATICS 与 profile 一致
   JOINTS = 5
   JOINT_0..JOINT_4 完整
   AXIS_* 与 COORDINATES 一致
   M428/M429/M430 remap 完整
   HAL_PIN_VARS = 1
   motion.analog-out-03 => motion.switchkins-type HALCMD 存在
   SERVO_PERIOD 存在
   CYCLE_TIME 存在
   TOOL_TABLE 存在
```

注意：

```text
[DISPLAY] GEOMETRY 和 JOG_AXES 只能用于 UI 显示，不能覆盖 [TRAJ] COORDINATES。
[KINS] sparm=identityfirst 决定 switchkins type 0 是 identity。
xyzac-trt 和 xyzbc-trt 的 axis/joint contract 不能混用。
```

### 5.2 应用 INI 到 profile

文件：

```text
app/src/runtime/linuxcnc-ini-runtime.js
app/src/profiles/index.js
app/src/profiles/xyzac-trt.js
app/src/profiles/xyzbc-trt.js
```

实现内容：

```text
1. applyIniConfigToProfile(profile, iniConfig) 只接受 validation.ready=true 的 INI。
2. profile.machineName 来自 [EMC] MACHINE。
3. profile.traj 来自 [TRAJ]。
4. profile.kinematicsModuleId 来自 [KINS] KINEMATICS 推导结果。
5. profile.axisLimits 来自 [AXIS_*]。
6. profile.jointConfig 来自 [JOINT_*]。
7. profile.rs274ngc 来自 [RS274NGC]。
8. profile.hal 来自 [HAL]。
9. profile.emcmot/task/emcio 保存到 profile 或 linuxCncIniConfig，供 runtime/session 使用。
```

阻断行为：

```text
INI validation 不 ready 时:
state.iniConfigReadiness.ready=false
operatorMessage=LinuxCNC INI contract invalid
RUN gate 必须返回 run blocked: LinuxCNC INI contract invalid
```

### 5.3 测试

扩展：

```text
tests/node/verify_linuxcnc_ini_runtime.mjs
tests/node/verify_run_preconditions.mjs
```

断言：

```text
1. xyzac-trt.ini 解析出 XYZAC、xyzac-trt-kins、JOINTS=5、AXIS_X/Y/Z/A/C。
2. xyzbc-trt.ini 解析出 XYZBC、xyzbc-trt-kins、JOINTS=5、AXIS_X/Y/Z/B/C。
3. M428/M429/M430 remap 完整。
4. HAL switchkins signal 完整。
5. 删除任一必填 section 后 validation.ready=false。
6. 删除 HAL_PIN_VARS 后 RUN 被阻断。
7. xyzac 使用 xyzbc tool table 或 parameter file 时 validation.ready=false。
```

## 6. 阶段 2：绑定 kinematics runtime

目标：运动学 runtime 必须来自 INI [KINS]，不能只靠当前 UI profile 猜测。

文件：

```text
app/src/runtime/linuxcnc-kinematics-runtime.js
wasm-port/runtime/sdk/src/linuxcnc-kinematics.js
app/src/state/store.js
```

实现内容：

```text
1. load kinematics runtime 时只接受 iniConfig.kinematicsModuleId。
2. state.kinematicsRuntimeReadiness.moduleId 必须等于 profile.kinematicsModuleId。
3. switchKinematics(type) 后 readback 的 switchkinsType 必须进入 rtcpFrame metadata。
4. rtcpFrame.sourceMode 必须是 source-derived-kinematics-wasm。
5. frame metadata 必须记录:
   profileId
   iniPath
   kinematicsModuleId
   switchkinsType
   coordinates
```

RUN gate 增加检查：

```text
kinematicsRuntimeReadiness.loaded === true
kinematicsRuntimeReadiness.moduleId === profile.kinematicsModuleId
rtcpFrame.sourceMode === source-derived-kinematics-wasm
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
```

## 7. 阶段 3：实现 machine-file staging

目标：task/HAL runtime 看到的 INI、HAL、remap、tool table、G-code 必须来自同一个 LinuxCNC machine context。

文件：

```text
app/src/runtime/linuxcnc-machine-file-staging.js
app/src/state/store.js
```

实现内容：

```text
1. stageProfileMachineFiles(profile) 使用 profile.iniPath 和当前 iniText 生成 plan。
2. plan 必须包含:
   profileId
   iniPath
   wasmDir
   wasmIniPath
   wasmProgramPath
   files[]
   taskHalSession

3. save 必须包含:
   files[]
   gcodeSources[]
   gcodeFiles[]
   summary
   storageMode
   storageCapability

4. files[] 至少包含:
   当前 profile INI
   当前 profile tool table
   remap_subs/428remap.ngc
   remap_subs/429remap.ngc
   remap_subs/430remap.ngc
   当前 profile demos 下的 G-code

5. selectMachineFileProgram(plan, save, sourceRel) 只能选择 LinuxCNC vendored five-axis demo G-code。
6. 选择 G-code 后，machineFileStaging.selectedGcodeSourceRel 和 plan.wasmProgramPath 必须同步更新。
```

状态更新：

```text
MACHINE_FILE_STAGING_STARTED
MACHINE_FILE_STAGING_COMPLETE
MACHINE_FILE_STAGING_FAILED
LOAD_LINUXCNC_GCODE_SOURCE
```

失败阻断：

```text
run blocked: LinuxCNC machine files not staged
run blocked: no machine-file G-code opened for task/HAL session
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
```

断言：

```text
1. xyzac-trt stage 后 save.summary.kinds.remap >= 3。
2. gcodeSources 包含 xyzac_switchkins_test_1.ngc。
3. selectedGcodeSourceRel 为空时 RUN 被阻断。
4. LOAD_LINUXCNC_GCODE_SOURCE 后 wasmProgramPath 指向 selected source。
5. xyzac/xyzbc 的 INI、tool table、parameter file 不混用。
```

## 8. 阶段 4：初始化 task/HAL session

目标：在 RUN 前完成等价于 LinuxCNC `EMC_TASK_PLAN_OPEN` 的程序打开动作。

文件：

```text
app/src/runtime/linuxcnc-task-hal-runtime.js
app/src/runtime/linuxcnc-task-hal-worker-client.js
app/src/runtime/linuxcnc-task-hal-worker.js
app/src/state/store.js
```

实现内容：

```text
1. attach taskHalRuntime 后写入 taskHalRuntimeReadiness。
2. initializeTaskHalSession({ openProgram: true }) 执行:
   buildTaskHalSessionFromMachineFiles(...)
   taskHalRuntime.resetSession()
   taskHalRuntime.initSession({ profileId, iniPath, iniText, programPath })
   taskHalRuntime.stageFiles(session.files)
   taskHalRuntime.openProgram(session.programPath)
   dispatch TASK_HAL_SESSION_READY
   readStatus()
   dispatch TASK_HAL_STATUS_APPLIED

3. taskHalSession 必须记录:
   profileId
   iniPath
   programPath
   programSourceRel
   files
   semanticBoundary

4. readStatus() 必须能返回:
   task.file 或 openedProgram.path
   task.mode
   task.state
   task.interpState
   task.nextProgramLine
   task.openedLineCount
   motionStatus.motion
   ui.axisPose
   ui.activeLine
   ui.currentVelocity
   ui.switchkinsType
```

RUN gate 增加检查：

```text
taskHalRuntime.loaded === true
taskHalRuntimeReadiness.taskRuntimeReady === true
taskHalRuntimeReadiness.motionRuntimeReady === true
taskHalRuntimeReadiness.halRuntimeReady === true
taskHalSession.programPath === expectedTaskHalProgramPathForState(state)
task status opened file === taskHalSession.programPath
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
```

## 9. 阶段 5：实现 RUN preconditions gate

目标：所有 RUN 入口都先经过统一 gate。

文件：

```text
app/src/state/store.js
app/src/state/linuxcnc-task-policy.js
```

函数：

```text
validateRunPreconditions(state, options)
expectedTaskHalProgramPathForState(state)
gateLinuxCncTaskAction(state, action)
```

实现顺序：

```text
1. 检查 supported profile: xyzac-trt / xyzbc-trt。
2. 检查 INI loaded + ready。
3. 检查 INI path 与 profile.iniPath 一致。
4. 检查 [TRAJ] COORDINATES 与 profile.traj.coordinates 一致。
5. 检查 [KINS] moduleId 与 profile.kinematicsModuleId 一致。
6. 检查 kinematics runtime loaded/moduleId/frame source。
7. 检查 machine files staged。
8. 检查 selectedGcodeSourceRel 存在。
9. 检查 task/HAL runtime readiness。
10. requireTaskHalSession=true 时检查 taskHalSession.programPath 与 expected program path 一致。
11. 检查 task status:
    task state ON 或 RUN 前可 set ON
    task mode AUTO 或 RUN 前可 set AUTO
    homed/no_force_homing 满足
    opened file 与 session programPath 一致
```

返回结构：

```js
{
  apiName: "web-rtcp-5axis-run-preconditions",
  ok: true,
  profileId,
  iniPath,
  coordinates,
  kinematicsModuleId,
  selectedGcodeSourceRel,
  programPath,
  sourceMode: "linuxcnc-task-motion-hal-wasm",
  semanticBoundary: "linuxcnc_ini_profile_kinematics_task_hal_run_preconditions"
}
```

失败返回：

```js
{
  ok: false,
  operatorMessage: "run blocked: ...",
  profileId,
  iniPath,
  coordinates,
  kinematicsModuleId
}
```

必须覆盖的阻断消息：

```text
run blocked: LinuxCNC INI not loaded
run blocked: LinuxCNC INI contract invalid
run blocked: machine profile and INI path mismatch
run blocked: machine profile and INI coordinates mismatch
run blocked: machine profile and INI kinematics mismatch
run blocked: LinuxCNC kinematics runtime not ready
run blocked: LinuxCNC kinematics module mismatch
run blocked: LinuxCNC kinematics frame not ready
run blocked: LinuxCNC machine files not staged
run blocked: no machine-file G-code opened for task/HAL session
run blocked: task/HAL runtime not ready
run blocked: task/HAL session program mismatch
run blocked: machine is not on
run blocked: machine is not homed
run blocked: task mode is not AUTO
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
```

## 10. 阶段 6：实现 RUN command sequence

目标：RUN 按 LinuxCNC task command 发送，不直接播放 fixture。

文件：

```text
app/src/state/store.js
```

函数：

```text
runValidatedTaskHalProgramRun()
runTaskHalCommandSequence(commands, options)
```

实现步骤：

```text
1. preflight = validateRunPreconditions(state, { requireTaskHalSession: false })。
2. preflight 失败时只设置 operatorMessage，不改 activeLine、不启动 loop。
3. expectedProgramPath = expectedTaskHalProgramPathForState(state)。
4. 如果 taskHalSession 不存在，或 programPath 不等于 expectedProgramPath:
   initializeTaskHalSession({ openProgram: true })
5. ready = validateRunPreconditions(state, { requireTaskHalSession: true })。
6. ready 失败时只设置 operatorMessage，不启动 loop。
7. stopTaskHalStatusLoop("restarted")。
8. 发送:
   EMC_TASK_SET_STATE ON
   EMC_TASK_SET_MODE AUTO
   EMC_TASK_PLAN_RUN line=linuxcncStartLine
9. runCycles({ taskCycles: initialBatch })
10. readStatus()
11. dispatch TASK_HAL_STATUS_APPLIED
12. 如果 status 仍在 READING，启动 taskHalStatusLoop。
```

重要修正：

```text
普通 RUN 从头运行时 linuxcncStartLine=0。
不要用 activeLine - programStartLine 自动推导 run-from-line。
run-from-line 必须作为独立功能实现，并单独处理 previous modal state、安全提示和 LinuxCNC restart semantics。
```

`runTaskHalCommandSequence()` 要求：

```text
1. allowFixtureSession=false 时绝不创建 fixture task/HAL session。
2. 每条 command 都通过 taskHalRuntime.sendCommand(command)。
3. command 后必须 runCycles/readStatus 一次。
4. status 通过 TASK_HAL_STATUS_APPLIED 统一落入 store。
5. 失败派发 TASK_HAL_COMMAND_FAILED。
```

## 11. 阶段 7：实现 taskHalStatusLoop

目标：RUN 后持续推进 task/HAL runtime，并持续消费真实 status。

文件：

```text
app/src/state/store.js
```

状态：

```js
taskHalStatusLoop: {
  active,
  sequence,
  profileId,
  iniPath,
  kinematicsModuleId,
  tickCount,
  batchSize,
  intervalMs,
  taskPeriodNs,
  servoPeriodNs,
  lastStatusAt,
  lastError,
  stopReason
}
```

函数：

```text
startTaskHalStatusLoop(options)
runTaskHalStatusLoopTick(sequence)
stopTaskHalStatusLoop(reason, options)
shouldContinueTaskHalStatusLoop(state, status)
```

实现步骤：

```text
1. start 时先 stopTaskHalStatusLoop("restarted", notify=false)。
2. sequence 自增，写入 TASK_HAL_STATUS_LOOP_STARTED。
3. setTimeout 调度 runTaskHalStatusLoopTick(sequence)。
4. tick 内校验 loop.active、sequence、taskHalRuntime.loaded。
5. 调用 taskHalRuntime.runCycles({ taskPeriodNs, servoPeriodNs, taskCycles: batchSize })。
6. 调用 taskHalRuntime.readStatus()。
7. 如果 sequence 未过期，dispatch TASK_HAL_STATUS_APPLIED(status, loopSequence)。
8. shouldContinueTaskHalStatusLoop 为 true 时继续 setTimeout。
9. paused/complete/stopped/error 时 stopTaskHalStatusLoop。
```

停止条件：

```text
motion.aborted === true -> stopped
interpState === paused 或 motion.paused === true -> paused
interpState === idle 且 nextProgramLine >= openedLineCount -> complete
sequence 不匹配 -> ignore stale tick
runtime missing -> stopped
```

参数来源：

```text
taskPeriodNs 默认来自 [TASK] CYCLE_TIME。
servoPeriodNs 默认来自 [EMCMOT] SERVO_PERIOD。
batchSize 初始可为 5。
intervalMs 初始可为 25。
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
```

断言：

```text
1. RUN 后 programRuntimeFeedbackHistory 至少 3 条。
2. 每条 sourceMode=linuxcnc-task-motion-hal-wasm。
3. semanticBoundary=linuxcnc_task_motion_hal_wasm_simulation_runtime。
4. taskCycle/servoCycle 单调递增。
5. STOP 后 taskHalStatusLoop.active=false。
```

## 12. 阶段 8：统一 TASK_HAL_STATUS_APPLIED

目标：所有 task/HAL 状态进入 UI 前只走一个 reducer patch。

文件：

```text
app/src/state/store.js
```

函数：

```text
applyTaskHalStatusPatch(state, status, operatorMessage, options)
createTaskHalRuntimeFeedback(state, status, axisPose, activeLine)
resolveTaskHalAxisPose(state, status)
resolveTaskHalKinsType(state, status, activeLine)
rtcpStateFromKinsType(kinsType)
```

必须更新的 state：

```text
taskHalStatus
taskHalExecutionPending
activeLine
axisPose
kinsType
rtcpState
programExecutionSourceMode
machine.powerOn
machine.taskState
machine.mode
machine.interpState
machine.taskPaused
runState
feed.currentVelocity
programRuntimeFeedback
programRuntimeFeedbackHistory
taskHalStatusLoop.tickCount
taskHalStatusLoop.lastStatusAt
```

规则：

```text
1. activeLine 来自 status.ui.activeLine，不从 timer 或 sample index 推导。
2. axisPose 优先来自 status.ui.axisPose。
3. velocity 来自 status.ui.currentVelocity 或 motion current velocity。
4. switchkinsType 来自 status.ui.switchkinsType/HAL/motion，不从 G-code 文本猜测。
5. rtcpState 由 switchkinsType 映射。
6. runtime feedback history 最多保留 100 条。
7. axisPose、velocity、activeLine、DTG、kinsType 必须来自同一个 status。
```

需要重点防止：

```text
task-local 0 值覆盖 HOME/work pose。
canonical preview pose 覆盖 runtime feedback pose。
fixture playback sourceMode 混入 task/HAL history。
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
```

## 13. 阶段 9：实现 STOP / ABORT / PAUSE / RESUME / STEP

目标：控制按钮与 LinuxCNC task command 对齐。

文件：

```text
app/src/state/store.js
app/src/ui/gmoccapy-shell.js
```

行为：

```text
STOP/ABORT:
  stopTaskHalStatusLoop("stopped")
  send EMC_TASK_ABORT
  runCycles/readStatus 一次
  TASK_HAL_STATUS_APPLIED

PAUSE:
  stopTaskHalStatusLoop("paused")
  send EMC_TASK_PLAN_PAUSE
  runCycles/readStatus 一次
  runState=paused

RESUME:
  send EMC_TASK_PLAN_RESUME
  runCycles/readStatus 一次
  如果 interpState 回到 reading，restart taskHalStatusLoop

STEP:
  task/HAL runtime loaded 时不走 fixture sample playback
  runCycles({ taskCycles: 1 })
  readStatus 一次
  TASK_HAL_STATUS_APPLIED
```

测试：

```text
1. RUN 后 PAUSE，loop active=false，runState=paused。
2. RESUME 后 loop 重新 active。
3. STOP 后 loop active=false，motion.aborted/stopped 状态进入 UI。
4. STEP 在 task/HAL loaded 时 sourceMode 仍是 linuxcnc-task-motion-hal-wasm。
```

## 14. 阶段 10：UI 显示与交互

目标：UI 只展示 store 中的 LinuxCNC-owned status，不推导运行状态。

文件：

```text
app/src/ui/gmoccapy-shell.js
app/src/styles/gmoccapy.css
```

需要显示：

```text
1. INI readiness:
   loaded/ready/path
   kinematics
   coordinates
   jointCount/axisCount

2. Machine files:
   staged/error
   file count
   gcode source selected
   storage mode

3. Task/HAL readiness:
   taskRuntimeReady
   motionRuntimeReady
   halRuntimeReady
   profileId
   iniPath

4. Runtime feedback:
   sourceMode
   line
   sample
   taskCycle
   servoCycle
   velocity
   DTG
   switchkinsType

5. Run history:
   最近 10 条 programRuntimeFeedbackHistory。
```

交互要求：

```text
1. 选择 G-code 后必须 initializeTaskHalSession({ openProgram: true }) 或标记 session stale。
2. RUN button 只 dispatch RUN_MACHINE_FILE_PROGRAM / RUN action，不直接操作 line。
3. RUN blocked 时显示 operatorMessage。
4. UI 高亮行读取 state.activeLine。
5. DRO 读取 state.axisPose 或 state.programRuntimeFeedback.axisPose。
6. RTCP canvas 状态读取 state.rtcpState。
```

## 15. 阶段 11：full execution boundary

目标：让系统明确声明当前是不是完整 LinuxCNC task/HAL run，而不是含糊显示 ready。

文件：

```text
app/src/runtime/full-execution-boundary.js
app/src/runtime/native-task-hal-audit.js
app/src/state/store.js
```

实现内容：

```text
1. fullLinuxCncProgramExecutionReady 只在这些条件都满足时为 true:
   INI contract ready
   profile/INI coordinates match
   kinematics runtime ready
   machine files staged
   task runtime ready
   motion runtime ready
   HAL runtime ready
   selected G-code opened

2. hardwareDrive=false。
3. hostRealtimeKernel=false。
4. externalUserMProcessReady=false。
5. semanticBoundary 明确标注:
   linuxcnc_task_motion_hal_wasm_simulation_runtime
```

测试：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_native_task_hal_audit.mjs
```

## 16. 阶段 12：Node smoke 集成

目标：所有关键链路进入 `npm run smoke:node`。

文件：

```text
app/package.json
```

加入或确认：

```json
{
  "scripts": {
    "smoke:node": "node ../tests/node/verify_linuxcnc_kinematics_runtime.mjs && node ../tests/node/verify_linuxcnc_interpreter_runtime.mjs && node ../tests/node/verify_linuxcnc_ini_runtime.mjs && node ../tests/node/verify_run_preconditions.mjs && node ../tests/node/verify_run_feedback_loop.mjs && node ../tests/node/verify_linuxcnc_task_hal_runtime.mjs"
  }
}
```

实际脚本可以保留现有顺序，但必须覆盖：

```text
INI parser
kinematics runtime
machine-file staging
task/HAL runtime
RUN preconditions
RUN feedback loop
store status patch
full boundary
```

执行：

```bash
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
```

## 17. 阶段 13：浏览器证据采集

目标：证明浏览器 UI 中 RUN 不是一次性 status，也不是 fixture playback。

文件：

```text
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
```

新增 case：

```text
07-run-preconditions-and-feedback
```

浏览器步骤：

```text
1. 打开 app。
2. 等待 state.iniConfigReadiness.loaded === true。
3. 选择 xyzac-trt。
4. 确认 INI/profile/kinematics module 都是 xyzac-trt。
5. 选择 xyzac_switchkins_test_1.ngc。
6. 点击 POWER。
7. 点击 HOME。
8. 点击 AUTO。
9. 点击 RUN。
10. 采集 t=0.2s / 0.5s / 1.0s / 2.0s / 5.0s state。
11. 截图。
```

断言：

```text
1. taskHalStatusLoop.tickCount 增长。
2. programRuntimeFeedbackHistory.length 增长。
3. 每条 feedback sourceMode 是 linuxcnc-task-motion-hal-wasm。
4. activeLine 等于 UI 高亮行。
5. DRO 等于 state.programRuntimeFeedback.axisPose。
6. RTCP canvas 状态与 state.rtcpState 一致。
7. runtime feedback text 显示 taskCycle/servoCycle/line/velocity。
8. 没有 fixture-line-playback sourceMode。
```

输出：

```text
qa/web-rtcp-5axis-site-test/output/run-preconditions-feedback.json
qa/web-rtcp-5axis-site-test/screenshots/run-preconditions-feedback/*.png
```

## 18. 最终验收命令

基础：

```bash
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_ini_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_machine_file_staging.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_preconditions.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_run_feedback_loop.mjs
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
npm --prefix web-rtcp-5axis-sim-plan/app run build
```

完整：

```bash
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
```

## 19. 完成标准

功能完成必须同时满足：

```text
1. RUN 前能明确追溯到一个 LinuxCNC INI。
2. INI definition contract 完整通过。
3. profile/INI/kinematics runtime/moduleId/coordinates 一致。
4. machine-file staging 使用同一 profile 的 INI、HAL、remap、tool table、G-code。
5. selected G-code 已通过 taskHalRuntime.openProgram 打开。
6. machine ON、AUTO、homed/no_force_homing gate 明确。
7. RUN 发送 EMC_TASK_SET_STATE / EMC_TASK_SET_MODE / EMC_TASK_PLAN_RUN。
8. RUN 后 taskHalStatusLoop 持续推进 runCycles/readStatus。
9. activeLine、DRO、velocity、DTG、switchkinsType、RTCP 来自同一个 status snapshot。
10. PAUSE/RESUME/STOP/STEP 行为与 LinuxCNC task command 对齐。
11. Node smoke 通过。
12. 浏览器 JSON/截图证据证明 UI 正在消费 task/HAL/motion feedback。
13. fallback playback 不会冒充 LinuxCNC task/HAL runtime。
```

## 20. 建议实施顺序

推荐实际开发顺序：

```text
1. 补强 INI parser/validation。
2. 补 run_preconditions 测试。
3. 补 machine-file staging 对 xyzac/xyzbc 混用的断言。
4. 补 taskHalSession opened program status 字段。
5. 修正 RUN line=0 与 run-from-line 分离。
6. 扩展 validateRunPreconditions 的 ON/AUTO/homed/opened-file gate。
7. 确认 taskHalStatusLoop 使用 INI CYCLE_TIME/SERVO_PERIOD。
8. 扩展 TASK_HAL_STATUS_APPLIED 的 same-snapshot 断言。
9. 补 PAUSE/RESUME/STOP/STEP 测试。
10. 更新 UI runtime feedback/history。
11. 跑 smoke:node。
12. 补浏览器证据采集。
```

每完成一个阶段都先跑该阶段对应 node 测试，再进入下一阶段。
