Add bounded context governance and task plan

This commit is contained in:
2026-08-20 15:15:16 +08:00
parent 2c814634c3
commit d4dec05c37
9 changed files with 2659 additions and 0 deletions

View File

@@ -0,0 +1,214 @@
# LinuxCNC Web/WASM 上下文治理与最小任务目录
**状态**:执行规范 v1.0
**适用基线**:LinuxCNC `2.10.0~pre0`;实际版本由 context lock 冻结
**生成器**:[`scripts/context_packet.py`](../scripts/context_packet.py)
**测试**:[`scripts/test_context_packet.py`](../scripts/test_context_packet.py)
本文件只解决两个问题:任务如何拆到可以持续接续的大小,以及一次任务交接究竟允许携带什么上下文。架构、功能范围、源文件关联和发布门禁分别以[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[源程序关联目录](linuxcnc-web-wasm-source-map.md)、[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)和[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)为准。本文件不复制这些文档的内容。
## 1. 不变量
1. 一个任务只有一个主工作流、一个里程碑、一个可验证结果和一个回滚点。跨三个以上工作流必须拆分。
2. 一个上下文包只有固定八段:`header`、`baseline`、`scope`、`contracts`、`evidence`、`risks`、`next`、`forbidden`。禁止追加 `history`、聊天记录或完整日志段。
3. 默认 profile 为 `feature`(4,096 tokens/16 KiB);里程碑任务才使用 `milestone`(8,192 tokens/32 KiB)。`doc` 和 `contract` 只在确有需要时使用。
4. `total_bytes` 同时限制序列化包和选中证据负载:`context_bytes = packet_bytes + selected_evidence_bytes`。`evidence_bytes` 是独立的证据总量上限,不能用文件数量替代。
5. 证据在生成阶段筛选。可选证据被排除时必须记录稳定 `reason_code`;必需证据无法容纳时生成失败,不输出部分成功包。
6. 交接只携带最新包、context lock 摘要、受影响 manifest ID 和可重跑命令。完整日志、golden 和差异报告只通过路径、大小和哈希引用。
### 1.1 包的分层读取
任务执行者按以下顺序读取,读到足够完成任务即停止:
| 层 | 内容 | 何时读取 | 预算归属 |
|---|---|---|---:|
| L0 | `context_id`、源码/工具链/schema 摘要、任务账本状态 | 每次开始和恢复 | `baseline` |
| L1 | 本任务目标、非目标、直接契约、受影响 manifest | 每次执行 | `header`/`scope`/`contracts` |
| L2 | 通过/失败测试摘要、差异首点、回滚点、下一步 | 执行中或交接 | `evidence`/`risks`/`next` |
| L3 | 完整日志、源码全文、golden 原件 | 只有需要定位时按路径读取 | 不进入包 |
L3 证据候选必须先给出 `path`、`bytes`、`sha256`、`kind`、`priority`、`required`;生成器按照必需、优先级、路径排序。读取清单不能包含未经过预算选择的路径。
## 2. 最小任务契约
任务描述必须能在一屏内完成审查,并使用以下字段。字段值是摘要或引用,不复制大段背景:
```yaml
id: INV-06a
title: 生成上下文包骨架
owner: WS-0
milestone: M0
profile: feature
objective: 固定八段包和稳定 packet_id
non_goals: [证据内容读取, manifest 扫描]
inputs: [context-lock.json, task-summary.json]
contracts: [context_packet.v1]
outputs: [packet.json]
tests: [test_context_packet.test_skeleton]
rollback: 删除生成物,保留输入锁
next: INV-06b
```
`objective` 必须是可观察结果;`non_goals` 防止任务吸收邻近工作;`inputs/contracts` 只列直接依赖;`outputs/tests` 必须能由 CI 重跑;`next` 最多三项。任务完成还需更新 manifest 状态和证据索引,但不把这些历史复制回包内。
## 3. 最小任务目录
下面的任务是执行单元,不是新的功能范围。原有大任务保留为父任务;实现时使用子任务 ID。每个子任务只拥有一条主要产物和一组直接测试。
### 3.1 M0:基线、清单和参考格式
| ID | 单一结果 | 直接输入 | 依赖 | Profile |
|---|---|---|---|---|
| INV-01a | 文档能力来源索引 | docs、source map | 无 | doc |
| INV-01b | 源码导出符号索引 | `src/emc/**`、`src/hal/**` | INV-01a | feature |
| INV-01c | 配置资源索引 | `configs/sim/**` | INV-01a | feature |
| INV-01d | 测试用例索引 | `tests/**`、`unit_tests/**` | INV-01a | feature |
| INV-02a | 规范化 source ID 和哈希 | INV-01a..d | contract |
| INV-02b | 依赖闭包和孤儿报告 | 规范化来源 | INV-02a | feature |
| INV-03a | context lock 文件 | 版本、工具链、schema | INV-02a | contract |
| INV-03b | context lock 校验器 | context lock | INV-03a | doc |
| INV-04a | native runner 输入接口 | sim 配置、测试索引 | INV-01c,d | contract |
| INV-04b | golden JSONL schema | runner 输入接口 | INV-04a | contract |
| INV-04c | 首个 divergent tick 报告 | native 输出 | INV-04b | feature |
| INV-05a | source→feature→task 图 | manifest、协议 | INV-02b | feature |
| INV-05b | 差异报告字段校验 | golden、manifest | INV-04b、INV-05a | doc |
| INV-06a | 固定八段包骨架 | context lock、任务摘要 | INV-03a | feature |
| INV-06b | 证据预算选择 | 候选证据清单 | INV-06a | feature |
| INV-06c | 排除原因和必需项失败 | 预算选择器 | INV-06b | doc |
### 3.2 M1:WASM 核心和最小运行闭环
| ID | 单一结果 | 直接输入 | 依赖 | Profile |
|---|---|---|---|---|
| CORE-01a | 独立构建目录和入口 | 编译锁 | INV-03a | contract |
| CORE-01b | 最小 posemath 编译 | posemath 源文件 | CORE-01a | feature |
| CORE-01c | TP/解释器最小链接 | TP、rs274 依赖 | CORE-01b | feature |
| CORE-02a | `sim_create/destroy` ABI | ABI 草案 | CORE-01a | contract |
| CORE-02b | `sim_load` 工作区入口 | INI/G-code 路径 | CORE-02a、FS-01a | feature |
| CORE-02c | `sim_step` 固定 tick | 状态结构 | CORE-02a | feature |
| CORE-02d | snapshot 导出/导入 | snapshot schema | CORE-02c | contract |
| CORE-02e | 错误码和行号映射 | rs274 错误 | CORE-01c | doc |
| RT-01a | 单会话 tick 队列 | `sim_step` | CORE-02c | feature |
| RT-01b | pause/abort/单步状态机 | tick 队列 | RT-01a | feature |
| FS-01a | 虚拟 POSIX 路径规则 | workspace 约束 | INV-02b | contract |
| FS-01b | open/read/write/stat | POSIX 规则 | FS-01a | feature |
| COM-01a | message envelope/schema | ABI 和错误码 | CORE-02a | contract |
| COM-01b | 命令 ack/result 路由 | envelope | COM-01a、RT-01a | feature |
| FE-01a | AppShell 和只读状态页 | protocol mock | COM-01a | doc |
| FE-01b | WorkerClient 超时/取消 | ack/result | COM-01b | feature |
### 3.3 M2-M3:核心行为、设备和恢复
| ID | 单一结果 | 直接输入 | 依赖 | Profile |
|---|---|---|---|---|
| CORE-03a | 基础 G/M 解释 golden | rs274 源和样例 | CORE-01c、INV-04b | feature |
| CORE-03b | O-word/参数/循环 | 解释器状态 | CORE-03a | feature |
| CORE-03c | remap 调用边界 | remap API | CORE-03b、PY-01a | contract |
| CORE-04a | 直线/圆弧 canonical | TP 源 | CORE-01c | feature |
| CORE-04b | 速度/加速度/拐角 | TP 配置 | CORE-04a | feature |
| CORE-05a | NML 命令/状态最小映射 | NML 消息清单 | CORE-02a、COM-01a | contract |
| CORE-05b | Task 状态快照 | NML 映射 | CORE-05a、CORE-02d | feature |
| CORE-06a | homing/软限位 | motion 状态 | CORE-02c | feature |
| CORE-06b | following error/急停 | motion 状态 | CORE-06a | feature |
| CORE-07a | trivkins 姿态 | kinematics ABI | CORE-02d | feature |
| CORE-07b | 非 trivkins 能力注册 | kinematics 清单 | CORE-07a | feature |
| CORE-08a | 工具表解析和快照 | tooldata 源和样例 | CORE-02b、FS-01b | feature |
| CORE-08b | 主轴/换刀/IO 状态 | iotask 消息 | CORE-05a、CORE-08a | feature |
| RT-02a | HAL pin/signal 图 | HAL 语法 | CORE-02c | feature |
| RT-02b | thread/function/addf 顺序 | HAL 图 | RT-02a | feature |
| RT-03a | 主轴/编码器/探针模型 | 虚拟设备 schema | RT-02b | feature |
| RT-03b | 限位/换刀故障注入 | 设备模型 | RT-03a | feature |
| RT-04a | ClassicLadder 解析 | ladder 输入 | RT-02a | feature |
| RT-04b | scan/寄存器/IO 顺序 | ladder 模型 | RT-04a、RT-02b | feature |
| FS-02a | journal prepare/commit | POSIX 工作区 | FS-01b | contract |
| FS-02b | 损坏记录和配额错误 | journal | FS-02a | feature |
| FS-03a | checkpoint 基线 | snapshot ABI | CORE-02d、FS-02a | feature |
| FS-03b | 增量重放和迁移 | checkpoint/journal | FS-03a | feature |
| PY-01a | Pyodide `linuxcnc/hal` 模块 | Python API 清单 | CORE-03a、FS-01b | feature |
| PY-01b | 资源/权限错误 | 虚拟模块 | PY-01a | doc |
| PY-02a | remap/filter 生命周期 | Python 回调契约 | CORE-03c、PY-01a | feature |
| PY-02b | VCP pin 回调和隔离 | VCP descriptor | PY-02a、RT-02a | feature |
| COM-02a | TaskLedger prepare/commit | operation schema | COM-01b、FS-02a | contract |
| COM-02b | 确认丢失对账 | TaskLedger | COM-02a | feature |
| COM-03a | 完整快照发布 | snapshot schema | CORE-02d、COM-01b | contract |
| COM-03b | 轨迹/HAL 流背压 | stream schema | COM-03a、RT-02b | feature |
| COM-04a | NML→Worker 命令映射 | NML 映射 | CORE-05a、COM-01a | contract |
| COM-04b | 状态/错误顺序 golden | message bus | COM-04a、INV-04b | feature |
| COM-05a | 能力/限制注册表 | 全量 manifest | INV-02b、CORE-07b | contract |
| COM-05b | 未支持能力解释 | 能力注册表 | COM-05a | doc |
### 3.4 M4-M5:前端、回归和发布
| ID | 单一结果 | 直接输入 | 依赖 | Profile |
|---|---|---|---|---|
| FE-03a | 工程选择/租约 | workspace API | FS-03b、COM-02a | feature |
| FE-03b | 刷新/Worker 重启恢复向导 | recovery events | FE-03a、COM-02b | feature |
| FE-04a | 编辑器诊断和当前行 | interpreter events | CORE-03a、COM-01b | feature |
| FE-04b | 轨迹预览与 golden 差异 | trajectory stream | FE-04a、COM-03b | feature |
| FE-05a | MDI/run/pause/abort | command schema | RT-01b、FE-01b | feature |
| FE-05b | jog/home/override 释放保护 | motion commands | FE-05a、CORE-06a | feature |
| FE-06a | 工具/IO/报警面板 | device events | RT-03b | feature |
| FE-06b | 主轴/换刀/探针恢复 | IO panel | FE-06a、CORE-08b | feature |
| FE-07a | HAL pin 图和命令控制台 | HAL snapshot | RT-02a、COM-03a | feature |
| FE-07b | scope/Ladder/日志回放 | sampled streams | FE-07a、RT-04b | feature |
| FE-08a | 轨迹和姿态非空渲染 | snapshot/trajectory | CORE-04a、CORE-07a | feature |
| FE-08b | 毛坯、坐标和最终姿态 | render state | FE-08a、COM-03b | feature |
| FE-09a | VCP descriptor 权限 | Python/VCP schema | PY-01a、COM-01a | contract |
| FE-09b | VCP 生命周期和异常隔离 | VCP runtime | FE-09a、PY-02b | feature |
| FE-10a | 虚拟列表和消息统计 | UI streams | FE-04b、FE-07b | feature |
| FE-10b | 帧预算和录制回放 | performance events | FE-10a、FE-08b | doc |
| COM-06a | 通讯/离线/租约 E2E | protocol + workspace | COM-02b、FE-03b | feature |
| COM-06b | 背压/未知消息/重启矩阵 | protocol streams | COM-03b、COM-06a | feature |
| REG-01a | 单入口 configs/sim 回归 | manifest、native runner | INV-02b、INV-04a | milestone |
| REG-01b | 全入口差异汇总 | 单入口报告 | REG-01a | feature |
| REG-02a | 失败样本保留和分类 | tests/unit_tests | INV-04a | feature |
| REG-03a | 断网/崩溃/配额矩阵 | FS-02b、FS-03b | feature |
| REG-03b | 回放终点一致性 | 连续/恢复 golden | REG-03a | feature |
| REG-04a | 浏览器通讯和离线 | Playwright | FE-03b、COM-02b | feature |
| REG-04b | 性能 P95/P99 报告 | 浏览器矩阵 | REG-04a | doc |
| REL-01a | 许可证和兼容报告 | 全量 manifest | REG-01b、REG-02a | doc |
| REL-01b | 迁移/回滚/离线包 | 发布候选产物 | REL-01a、REG-03b | milestone |
父任务只有在所有直接子任务完成且证据链闭合后才能更新状态。子任务之间不得共享未版本化的临时结构;需要共享时先建立 contract 子任务。
## 4. 交接和恢复
### 4.1 正常交接
1. 从 context lock 和父任务摘要生成新的 `packet_id`;只填本子任务的八段。
2. 用候选证据的实际字节数调用生成器。生成前就排除 `E1`-`E5`,输出后只接受 `budget.packet_bytes <= byte_limit` 且 `packet_bytes + selected_evidence_bytes <= byte_limit`。
3. 完成一步就更新 `evidence`、`risks`、`next`;不追加旧包全文。
4. 提交正文只写 `Context-ID`、`Context-Packet`、子任务 ID、测试命令和证据路径。
### 4.2 工具失败或确认丢失
以 `operation_id` 查询任务账本:`verified` 步骤只引用原证据,`unfinished` 从最近 checkpoint 继续,`unknown` 先对账。无法对账时标记 `blocked`,保留包和产物,不重放可能产生副作用的操作。
### 4.3 拆分停止线
出现任一条件立即拆分,而不是扩大 profile:上下文包超过 profile 80%、候选证据超过证据预算 80%、任务同时修改两个独立契约、测试需要读取三个以上大日志、或 `next` 不能写成三项以内的动作。拆分后的父任务只保留依赖图和摘要。
## 5. CI 检查
CI 在生成阶段执行以下检查,校验器只做复核:
```text
packet JSON 可解析
固定八段存在且无额外段
packet_id/context_id 与任务账本一致
budget.packet_bytes == UTF-8 序列化字节数
budget.packet_bytes <= byte_limit
budget.packet_bytes + budget.selected_evidence_bytes <= byte_limit
selected_evidence_bytes <= evidence_byte_limit
selected_evidence_files <= evidence_file_limit
所有排除项都有 E1-E5 reason_code
required 证据无法容纳时命令失败且不写输出
```
最小回归命令:
```bash
python3 -m unittest discover -s scripts -p 'test_*.py' -v
```
本规范的目标不是把所有信息压进一个更大的包,而是让每个任务只携带完成当前结果所需的最小、可重建、可审计上下文。

View File

@@ -0,0 +1,313 @@
# LinuxCNC Web/WASM 仿真项目执行与交付策略
**文档状态**:Draft v0.1
**适用范围**:本仓库的纯 Web/WASM 仿真移植、浏览器运行时、OPFS 工程存储、覆盖矩阵和发布物
**固定基线**:LinuxCNC `2.10.0~pre0`(以 `linuxcnc/VERSION` 和锁定的源码提交为准)
**关联文档**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)、[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)、[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)、[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)、[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)、[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)
本文件是项目执行的治理入口。技术路线回答“做什么以及采用什么架构”,覆盖矩阵回答“逐项覆盖什么以及如何验收”,本文件回答“谁在什么固定上下文中交付、如何在断网或失败后接续、什么证据才能过门禁”。三份文档必须以同一基线版本更新;若规则冲突,以本文件的交付门禁和覆盖矩阵的逐项验收要求为准。
## 1. 治理缺口与补强决定
| 已有内容 | 缺口 | 本策略的控制措施 | 证据位置 |
|---|---|---|---|
| 技术路线已有 A-E 阶段 | 没有阶段退出条件、依赖关系和返工规则 | 为每个里程碑定义入口、出口、证据和阻断条件;未过门禁不得进入下一阶段 | 第 6、7 节 |
| 覆盖矩阵要求 100% | `sim_status`、`web_status` 没有统一枚举,差异可能被静默接受 | 固定状态机和缺失项 CI 门禁;`REAL-EXCLUDED` 与未实现严格分离 | 第 3.4 节;覆盖矩阵第 1、13 节 |
| OPFS 已有 journal 思路 | 没有断网、标签页挂起、Worker 崩溃和配额不足的接续协议 | 本地优先、事务提交、checkpoint、重放和幂等命令协议;恢复结果必须可验证 | 第 5 节 |
| 固定 tick 和 golden 已提出 | 没有明确“同一上下文”的构成,难以复现差异 | 建立 context lock,提交和 golden 必须携带锁定摘要 | 第 3 节 |
| 仿真与真实硬件边界已说明 | 任务容易跨越 WASM、Worker、UI 和存储责任边界 | 以工作流和接口为边界分工;跨边界改动必须有契约变更记录 | 第 4 节 |
| 有原生/WASM 对照目标 | 没有统一的变更、回滚和发布证据包 | 采用小步、单主题提交;发布只消费完整证据包和签名的 manifest | 第 3.3、7 节 |
治理原则:基线可重建、仿真结果可重复、持久数据可恢复、差异可定位、排除项可识别。任何“先合并再补测试”或“离线时先静默丢弃”的做法都不满足本策略。
## 2. 文档与决策层级
文档关系固定如下:
```text
执行与交付策略(本文件)
├─ 技术路线:架构、范围、阶段 A-E 和风险决策
├─ 覆盖矩阵:功能来源、manifest 条目、测试和逐项验收
├─ 源程序关联目录:功能到源文件、配置、符号和测试的反向索引
├─ 前端与通信设计:UI 工作流、Worker 协议、状态契约、任务和 FE/COM 里程碑
└─ 总控执行手册:端到端依赖、统一任务目录、场景验收和当前实施状态
```
架构、范围或阶段发生变化时,先更新技术路线;功能条目、标记、测试或差异状态发生变化时,先更新覆盖矩阵;两者都需要同步更新本文件中的里程碑或门禁时,必须在同一变更包中完成。文档链接、基线版本和状态枚举由文档一致性检查自动校验。
### 2.1 全功能移植闭环
“全部功能”按可观察的仿真能力定义,而不是按成功编译的源码行数定义。功能全集由以下来源的并集规范化得到:
```text
F = docs capabilities
∪ exported/configurable behavior in src/emc, src/hal, classicladder, usr_intf and Python APIs
∪ configs/sim/** resources and workflows
∪ tests/** and unit_tests/** cases
∪ Web UI, VCP, visualization and persistence workflows
```
M0 必须冻结来源扫描和规范化规则;每个来源项至少映射到一个 manifest 条目,每个 manifest 条目必须反向列出来源、依赖、输入、输出、错误、仿真替代方案、测试和 oracle。来源项没有映射、manifest 条目没有测试、依赖没有递归闭包或新增源码未触发 manifest 变化时,CI 直接阻断。一个来源可以拆成多个功能条目,但不能把多个未验证功能合并成一个笼统的“全覆盖”。
每个功能条目都要通过六个覆盖维度:正常行为、边界/资源限制、错误和报警、暂停/恢复/急停、持久化/刷新/离线恢复、UI/API/脚本工作流;需要与其他域交互的条目还必须进入关键路径或 pairwise 交互测试。`REAL-EXCLUDED` 只允许用于真实硬件、硬实时和物理安全;任何 `configs/sim` 依赖都必须映射为可观测的 `SIM-CORE`、`SIM-ADAPTER`、`SIM-DEVICE`、`WEB-EQUIV` 或 `PY-WASM` 行,不能用排除项掩盖仿真缺口。
完整性证明按“来源扫描 → manifest 闭包 → 实现映射 → 多维测试 → 原生/WASM 差异 → 发布签署”顺序生成。[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)是来源扫描和反向索引的规范入口;发布报告必须同时给出已接受条目、排除条目、阻断条目、孤儿来源和未覆盖交互;只给出一个百分比不能作为全功能声明。
## 3. 固定上下文与提交策略
### 3.1 Context lock
每个可复现构建、golden 集和发布候选都绑定一个不可变的 context lock。锁至少包含以下字段,实际格式可为 `context-lock.json` 或 CI 产物:
| 字段 | 要求 |
|---|---|
| `context_id` | 全局唯一,例如 `linuxcnc-2.10.0-pre0-wasm-c1` |
| `linuxcnc_version`、`linuxcnc_source_sha` | 与 `linuxcnc/VERSION` 和源码提交一致 |
| `roadmap_revision`、`coverage_revision` | 两份治理文档的提交摘要或版本号 |
| `emscripten_version`、`compiler_flags` | 包括优化、浮点、异常、线程和文件系统选项 |
| `python_runtime` | Pyodide/CPython-WASM 版本及包锁摘要 |
| `schema_versions` | C ABI、命令、事件、快照和工程格式版本 |
| `browser_matrix` | 浏览器、版本范围、操作系统和是否跨源隔离 |
| `test_manifest_sha`、`golden_manifest_sha` | 输入清单及参考结果的内容摘要 |
锁定后,不能在同一 `context_id` 内悄悄升级编译器、Python、浏览器基线、schema 或 LinuxCNC 源码。升级必须创建新的 `context_id`,重新生成受影响的 golden,并在迁移说明中列出差异。
### 3.2 固定大小的任务上下文包
“固定上下文”指每次任务交接和每次提交都重新装载一个固定格式、固定预算的上下文包,而不是把整个历史对话、全部日志或已完成阶段重新带入。上下文包是可丢弃的工作索引;完整源码、日志、golden 和差异报告留在仓库或 CI 产物中,只通过路径和摘要引用。
上下文包使用固定的八段结构,逻辑预算为 **8,192 tokens,UTF-8 序列化不超过 32 KiB**。实现上由 [`scripts/context_packet.py`](../scripts/context_packet.py) 在选择证据时执行预算;校验器不能成为第一次发现超限的地方。每段有上限,缺省内容用 `none`,不得追加自由历史段:
| 段 | 固定内容 | 建议上限 |
|---|---|---:|
| `header` | `context_id`、`packet_id`、任务和里程碑 | 512 tokens |
| `baseline` | 源码/工具链/schema/golden 摘要 | 1,024 tokens |
| `scope` | 主工作流、目标、非目标、受影响 manifest ID | 1,024 tokens |
| `contracts` | 仅列出本任务读写的 ABI、事件、文件和状态契约 | 1,536 tokens |
| `evidence` | 已通过测试、失败测试和产物路径/哈希 | 1,536 tokens |
| `risks` | 未解决风险、阻断原因和回滚点 | 1,024 tokens |
| `next` | 下一步最多三项可执行动作 | 768 tokens |
| `forbidden` | 本任务明确不得触碰的边界 | 768 tokens |
包的 `packet_id` 由 `context_id`、任务 ID 和修订号组成;修订只替换对应段,不复制旧包。超过任一段上限或总预算时,必须先拆分任务、生成摘要或把内容移到可寻址产物,不能扩大上下文预算。每次交接从最新包重建,旧包只保留哈希和归档链接,因此移植从阶段 A 深入阶段 E 时,上下文大小不变。
#### 生成阶段的预算算法
证据选择必须在写入输出文件前完成,按以下顺序执行:
1. 加载固定上下文骨架并计算 `skeleton_bytes`;骨架超过 `byte_limit` 立即失败。
2. 将候选证据规范化为 `path`、`bytes`、`sha256`、`kind`、`priority`、`required`,按必需、优先级、路径排序,保证选择确定性。
3. 逐项检查:单项证据字节预算、证据总字节预算、证据文件数,以及把该项加入 `selected` 后的完整上下文剩余空间;完整上下文按 `packet_bytes + selected_evidence_bytes` 计算,不能只计算 JSON 清单。
4. 可选项无法容纳时不写入包,记录稳定排除码;必需项无法容纳时直接失败,不生成部分成功包。
5. 生成最终预算元数据后再次序列化校验;若审计清单本身使包超限,从最低优先级可选项回退并以 `E5=context_remaining_bytes_insufficient` 记录。若只剩必需项仍超限,失败。
排除原因使用稳定机器码,避免长文本侵占上下文:`E1 duplicate_path`、`E2 evidence_item_bytes_exceed_budget`、`E3 evidence_file_count_exceed_budget`、`E4 evidence_bytes_exceed_budget`、`E5 context_remaining_bytes_insufficient`。输出必须包含选中证据字节数、选中/排除文件数、证据预算、总包预算、最终包字节数和排除码;CI 同时检查 `packet_bytes <= byte_limit` 和 `packet_bytes + selected_evidence_bytes <= byte_limit`,而不是先写超限包再报告错误。
上下文包的最小机器可读形状如下;CI 应检查字段齐全、段顺序、预算、`context_id` 一致性和引用产物存在:
```yaml
context_id: linuxcnc-2.10.0-pre0-wasm-c1
packet_id: M2-WS1-interpreter-r07
task: "[WS-1][M2] ..."
header: { milestone: M2, owner_role: runtime }
baseline: { source_sha: "...", schema: { abi: 1, snapshot: 1 } }
scope: { include: ["coverage/..."], exclude: ["WS-5"], manifest: ["..."] }
contracts: ["sim_step", "snapshot.v1"]
evidence: { passed: ["..."], failed: ["..."], artifacts: ["..."] }
risks: ["..." ]
next: ["...", "...", "..."]
forbidden: ["real hardware", "host process", "unversioned schema"]
budget: { token_limit: 8192, byte_limit: 32768 }
```
### 3.3 提交和证据包
提交按一个可审查主题组织:运行时、持久化、组件、UI、测试或文档只能有一个主主题;跨主题改动拆成有依赖顺序的提交。每个提交或合并请求必须声明:
```text
Context-ID: <context_id>
Context-Packet: <packet_id>
Scope: <workstream>/<milestone>
Contract-Change: none | <ABI/schema/format/compatibility id>
Evidence: <test command or artifact paths>
Offline-Impact: none | cached-start | mid-run | recovery
Coverage: <manifest ids or paths>
Context-Budget: 8192 tokens / 32768 bytes
```
合并请求的证据包至少包括变更摘要、受影响 manifest 条目、测试日志、golden/difference 摘要、性能或配额影响、回滚方式和文档链接。提交正文只保存上下文包和产物摘要,不复制完整日志或历史讨论。没有 `Context-ID`、`Context-Packet`、预算声明、证据路径或覆盖条目的提交只能作为草稿,不能进入受保护分支。
### 3.4 覆盖状态机
`sim_status` 和 `web_status` 使用同一组值:
`planned` → `in_progress` → `implemented` → `verified` → `accepted`。
允许从任何非终态回到 `blocked`;`blocked` 必须填写阻断原因、责任工作流、下一步和复核日期。`REAL-EXCLUDED` 是终态,但只允许用于覆盖矩阵定义的真实硬件、硬实时和安全排除项。`accepted` 必须同时具备实现、自动测试、差异报告和 context lock;没有这些证据不得通过文字描述或 UI 状态伪装成已支持。
### 3.5 变更分类和审查
* **C0 文档/注释**:不改变契约;文档链接和术语检查通过即可。
* **C1 行为实现**:必须更新 manifest、golden 或差异说明,并运行受影响功能域测试。
* **C2 契约/格式**:改变 ABI、消息、快照或工程格式;必须先提交迁移方案、双读/版本策略和兼容测试。
* **C3 基线/工具链**:改变 LinuxCNC、Emscripten、Python 或浏览器基线;必须新建 context lock,完整重跑基线回归。
C2/C3 变更需要运行时、测试/兼容性和存储(若受影响)三个责任角色共同审查。任何改动都不得把真实硬件能力从 `REAL-EXCLUDED` 改成普通覆盖项而不修改产品边界和安全说明。
## 4. 工作流拆分与边界
工作流以可交付接口划分,避免按目录切割导致责任重叠。角色是责任类别,不预设具体人员。
| ID | 责任工作流 | 交付物 | 依赖 | 明确不负责 |
|---|---|---|---|---|
| WS-0 | 基线与清单 | context lock、来源扫描、`coverage/manifest.jsonl`、golden 索引 | 无 | 运行时实现 |
| WS-1 | WASM 核心与 ABI | `sim_*` 生命周期、解释器/TP/kinematics 适配、版本化 ABI | WS-0 | UI、OPFS 事务 |
| WS-2 | Worker 调度与虚拟设备 | 固定 tick、HAL、IO、故障模型、事件序列 | WS-1 | 浏览器布局和用户文件选择器 |
| WS-3 | Workspace/OPFS | 虚拟 POSIX、journal、checkpoint、迁移、配额诊断 | WS-0;消费 WS-1 schema | 运动语义和 UI 状态管理 |
| WS-4 | Python/插件兼容 | Pyodide 虚拟模块、remap/filter/VCP 权限和生命周期 | WS-1、WS-3 | 宿主进程、网络和设备访问 |
| WS-5 | Web UI/可视化 | Worker 客户端、控制台、3D、工具和配置工作流 | WS-2、WS-3 | 改写核心算法和持久化协议 |
| WS-6 | 参考运行器与测试 | 原生/WASM golden、浏览器 E2E、故障注入、差异报告 | WS-0;随各工作流增量接入 | 为失败测试降级或删除样本 |
| WS-7 | 发布与文档 | 兼容报告、许可证清单、迁移说明、离线包和发布证据 | WS-0..6 | 绕过门禁发布 |
跨工作流接口只通过已版本化的 ABI、命令/事件 schema、快照 schema、工程格式和 manifest 交换;WS-2/WS-5 的具体接口、FE/COM 任务和 FE-M0..M5 退出条件见[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)。若需要直接读取另一个工作流的内部结构,应先提出契约变更(C2),而不是复制内部状态。每个任务必须标注一个主工作流和一个里程碑;一个任务同时改动三个以上工作流时,默认拆分。
## 5. 稳定性、断网与接续
### 5.1 本地优先原则
已缓存的应用壳、WASM、Pyodide、样例和工程可以在无网络时启动;仿真核心、Worker、OPFS 和 UI 主流程不得把网络可用性当作前置条件。网络只用于可选更新、协作或远程调试。断网期间禁止自动覆盖已提交版本,禁止删除唯一可恢复的 checkpoint,禁止把失败写入标记为成功。
### 5.2 故障分类与策略
| 故障 | 运行策略 | 接续依据 | 验收结果 |
|---|---|---|---|
| 启动时无网 | 从 Service Worker 缓存启动;缺少资源则显示明确缺件 | cache manifest、context lock | 已缓存版本可启动;未缓存依赖可诊断 |
| 运行中断网 | 仿真继续按固定 tick 运行;本地命令和日志照常提交 | OPFS journal、`seq`、`simulation_tick` | 重载页面后恢复最近已提交状态 |
| 标签页挂起/Worker 重启 | 在安全 checkpoint 重启并重放增量事件 | checkpoint 基线 + event log | 重放终点与连续运行一致 |
| 写入中止/崩溃 | 只接受完整 commit;清理临时文件或回滚 | prepare/commit、哈希和版本索引 | 不出现半文件,不损坏旧版本 |
| 配额不足 | 停止非关键 cache/日志写入,保留工程和最近 checkpoint | 配额诊断、版本保留策略 | 已提交工程可读;用户得到导出/清理动作 |
| 重新联网 | 不自动重放重复命令;按摘要对账可选远程数据 | `context_id`、`seq`、`tick`、状态哈希 | 对账成功或明确冲突,不静默合并 |
### 5.3 命令、事件和快照接续协议
每条控制命令带单调递增 `seq`、`context_id`、客户端 `session_id` 和幂等键;每个事件和快照带 `seq`(最后应用的命令)、`simulation_tick`、`schema_version` 和状态哈希。恢复流程固定为:
1. Worker 读取并校验最新完整 checkpoint 和 journal;只接受哈希、版本和路径均匹配的记录。
2. 以 checkpoint 的 tick 为起点按序重放事件;遇到缺号、重复或未知 schema 时停止并报告,不猜测补齐。
3. 发布 `recovered` 事件,包含恢复到的 tick、最后提交序号、丢弃的未提交记录和差异摘要。
4. UI 只有在收到 `recovered` 后才允许继续发送依赖运行态的命令;重复提交使用幂等键返回原结果。
快照采用“完整基线 + 有界增量”策略:增量达到大小、数量或时间上限就合并为新基线。UI 偏好与仿真状态分离;恢复仿真不得依赖尚未提交的 UI 偏好。连续运行与恢复运行必须在相同 `context_id`、输入文件哈希和故障注入种子下得到相同终点。
自动接续使用以下状态机,不依赖用户手动刷新或重复点击:
```text
online_ready
└─ 网络/资源不可用 ─▶ offline_degraded
└─ 本地资源完整 ─▶ local_continuation
└─ online 事件 ─▶ reconnecting
├─ 摘要一致 ─▶ reconciled ─▶ online_ready
└─ 摘要冲突 ─▶ conflict(暂停远程同步,保留本地版本)
```
`reconnecting` 使用指数退避和上限,不重复执行已确认的 `seq`;`local_continuation` 只把命令写入本地 journal,`reconciled` 后按序发送未确认摘要。出现 `conflict` 时不能自动覆盖任一版本,必须保留两个可导出的版本和差异报告。网络恢复不是仿真恢复的前提:即使永远不重新联网,已缓存资源和 OPFS 工程仍应按本地策略完成运行、暂停、快照和导出。
### 5.4 稳定性指标
每个里程碑都记录以下指标,不以平均值掩盖长尾:Worker 周期 overrun、命令确认延迟、快照吞吐、checkpoint 写入耗时、恢复耗时、journal 未完成事务数、OPFS 错误率和 UI 主线程阻塞时间。指标必须按浏览器矩阵和场景分组;性能回归超过基线阈值时阻止发布,除非有带期限的书面例外。
### 5.5 高效执行策略
效率优化不能改变固定 tick、事件顺序或恢复结果。执行和构建按以下规则收敛:
* **按影响图测试**:提交前只运行受影响 manifest 的快速测试;里程碑运行整个功能域;M5 运行全量来源、golden、浏览器和故障矩阵。影响图由来源、契约和测试 ID 生成,不能由提交者手工缩小。
* **按 context 缓存**:构建、解析和轨迹缓存的键至少包含 `context_id`、源码摘要、工具链摘要和输入文件摘要;不同 context 不共享可能改变语义的缓存。
* **并行与串行分界**:独立 manifest、浏览器和 golden 任务可并行;同一仿真会话内的 tick、命令、HAL 和 OPFS 提交必须串行。并行只是缩短验证时间,不得引入非确定性。
* **冷热路径分离**:仿真周期只处理内存中的热点状态;OPFS、日志压缩、差异计算和完整快照在有界批次中运行。任何后台工作都必须有队列上限和取消点,不能拖慢周期或无限积压。
* **预算先测后优化**:M1 建立参考场景的周期耗时、P95/P99 Worker 延迟、内存、快照吞吐、恢复时间和 UI 帧时间基线;后续提交默认不得超过基线 20%,否则必须附 profiling、替代方案和期限明确的例外。
性能优化提交必须同时携带确定性回放结果和资源指标;只有速度变快但无法证明状态、错误和恢复一致的实现不得合并。
### 5.6 问答、工具或执行失败后的接续
断网之外,还必须处理请求超时、回答传输中断、工具调用返回失败、工具已执行但确认消息丢失、结果校验失败和 Worker 崩溃。系统不能把“没有收到回答”当成“没有执行”,也不能把“回答失败”当成“任务失败且需要从头开始”。每个任务使用持久化任务账本记录可恢复边界:
| 字段 | 含义 |
|---|---|
| `operation_id` | 一次任务或工具操作的稳定幂等键 |
| `packet_id`、`context_id` | 恢复时必须重新装载的固定上下文 |
| `phase`、`step_id` | 当前里程碑、工作流和步骤;步骤必须可重入 |
| `intent_hash` | 输入、目标和契约的摘要,防止用新任务误恢复旧任务 |
| `prepare_ref`、`commit_ref` | 操作前后 journal/产物引用 |
| `last_verified_step` | 已验证完成的最后步骤,不以“已发送”代替“已完成” |
| `status` | `running`、`retrying`、`recovering`、`awaiting_reconcile`、`completed` 或 `blocked` |
每个有副作用的步骤遵循 `prepare → execute → verify → commit`:先写入 prepare 记录,再以 `operation_id` 调用;收到结果后校验状态哈希、产物和契约,最后写 commit。重试前先查询 `operation_id` 是否已有 commit:已有则直接复用结果,只有明确没有 commit 才能再次执行。执行完成但确认丢失时进入 `awaiting_reconcile`,通过状态查询或产物哈希判定结果,禁止盲目重放。
恢复流程固定为:
1. 重新装载 context lock、最新固定大小上下文包和任务账本,检查 `intent_hash` 是否匹配。
2. 扫描 prepare/commit、工具产物、Worker 事件和 OPFS journal,建立每个 `step_id` 的 `unknown`、`verified` 或 `unfinished` 判定。
3. 已 `verified` 的步骤只引用原证据;`unfinished` 步骤从最近 checkpoint 继续;`unknown` 步骤先查询和对账,不能猜测。
4. 每完成一个步骤立即更新 `last_verified_step` 和上下文包的 `evidence/risks/next` 段;不追加历史对话,因此恢复前后仍受 8,192 tokens/32 KiB 预算约束。
5. 所有步骤完成后写入 `completed` commit,并生成可重跑的证据索引;无法安全判定时保留本地版本,进入 `blocked` 或 `awaiting_reconcile`,不得静默标记成功。
网络和回答通道都恢复后,自动接续只发送未确认的操作摘要,使用指数退避和有限重试;重复响应、重复事件和迟到响应按 `operation_id` 丢弃或关联到原结果。该机制保证在本地已提交边界内可继续执行;对外部服务不可查询、数据已发生不可逆变化或上下文摘要冲突的情况,只能保证不丢失本地证据并明确要求对账,不能伪造“恰好执行一次”的结论。
## 6. 里程碑、依赖和退出条件
技术路线的阶段 A-E 映射到以下可审查里程碑。里程碑不是日期承诺,而是证据包边界。
| 里程碑 | 对应路线 | 关键交付 | 退出条件 |
|---|---|---|---|
| M0 基线锁定 | A | context lock、来源/测试清单、状态枚举、责任分工 | 清单可重建;缺项 CI 失败;文档链接和基线一致 |
| M1 运行时骨架 | A | 单 Worker、固定 tick、ABI、最小 OPFS journal、Python 启动 | create/load/step/snapshot/recover ABI 测试通过;无网加载缓存样例 |
| M2 核心行为 | B | G/M/O、TP、坐标、补偿、kinematics、原生 golden | 受影响核心 manifest 全部 `verified`;首个 divergent tick 可定位 |
| M3 虚拟设备与持久化 | C | HAL/PLC/IO、数字孪生、事务恢复、断网接续 | 故障注入和恢复矩阵通过;配额不足不损坏已提交版本 |
| M4 Web 工作流 | D | 控制台、3D、VCP/配置工具、导入导出、离线 PWA | 关键 UI 工作流在浏览器矩阵通过;键盘/触摸和刷新恢复有证据 |
| M5 发布候选 | E | 全量覆盖、兼容/许可证报告、迁移工具、离线包 | 所有可计入条目 `accepted`,排除项可识别,发布门禁全绿 |
里程碑依赖单向推进:M2 不能绕过 M1,M3 不能把未版本化的核心状态写入 OPFS,M4 不能以 UI mock 替代 M2/M3 的真实状态,M5 不接受“后续补齐”的覆盖条目。发现契约或基线变化时,回退到受影响的最早里程碑并重新生成证据。
## 7. CI、合并与发布门禁
### 7.1 合并门禁
每个变更包至少通过:文档链接和术语检查、格式/静态检查、受影响单测、manifest 完整性检查、原生/WASM 差异测试(适用时)、离线影响场景(适用时)、`context_packet.py` 生成器测试、上下文包字段/大小检查和证据包字段检查。任何新增来源、配置、组件、G/M/O 功能或 UI 工作流没有 manifest 条目时 CI 失败。上下文包超过 8,192 tokens 或 32 KiB、出现未定义段、把旧包全文嵌入新包,均直接阻止合并。生成阶段不能产生超限包;验证阶段只能复核,不能负责裁剪。
### 7.2 里程碑门禁
* **G0 基线门禁**:context lock 可重建,依赖和生成器版本锁定。
* **G1 行为门禁**:核心状态、canonical 输出、错误码和关键轨迹对照通过;容差写入报告。
* **G2 持久化门禁**:刷新、Worker 重启、半提交、损坏记录、迁移和配额不足场景通过。
* **G3 离线门禁**:断网启动、中途断网、标签页挂起、重新联网对账、请求超时、回答中断、工具结果丢失和 Worker 重启场景通过;没有静默丢命令、重复副作用或跳过步骤。
* **G4 覆盖门禁**:所有 `SIM-*`、`WEB-EQUIV`、`PY-WASM` 条目达到 `accepted`;`REAL-EXCLUDED` 均有明确错误和说明。
* **G5 发布门禁**:浏览器矩阵、性能预算、安全/权限审查、许可证清单、迁移和回滚演练全部有签名证据。
门禁失败时,发布候选保持原 context lock 不变;修复可以追加提交,但若改变契约或基线,必须创建新锁并重新跑受影响门禁。任何例外都必须记录范围、风险、补救责任人和失效日期,不能永久豁免。
## 8. 任务编写与追踪规则
任务标题使用 `[WS-x][Mx]` 前缀,正文包含目标、非目标、输入契约、输出证据、依赖、风险、回滚和受影响 manifest。父任务必须按[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)拆成单结果子任务;执行者只读取该子任务及其直接依赖,不把整份路线、覆盖矩阵或父任务历史装入上下文。任务启动时创建一个固定大小的上下文包;每次交接只更新 `evidence`、`risks` 和 `next` 等对应段,并递增 `packet_id`,不把历史对话或旧包拼接进来。证据候选必须交给 `scripts/context_packet.py` 预筛选,禁止手工把完整日志塞进包。任务完成不是“代码已写”,而是对应条目达到约定状态并附上可重跑命令。阻塞任务保留原上下文和失败日志的摘要及哈希;不得通过删除样本、降低容差或把 `in_progress` 改成 `REAL-EXCLUDED` 来清除阻塞。
最小任务拆分建议与技术路线第 11 节一致:先做 WS-0 的清单和 golden 格式,再做 WS-1/WS-2 的 `sim_step` 与快照,随后做 WS-3 的 WorkspaceRuntime 和恢复测试,最后接入 UI 和全量回归。每个垂直切片都必须同时包含一个正常路径、一个错误路径和一个恢复路径,避免只交付“能运行一次”的演示。
## 9. 里程碑检查清单
里程碑评审前,负责人逐项确认:
1. context lock、源码/工具链版本和文档 revision 一致。
2. 每个任务上下文包均为固定八段结构,未超过 8,192 tokens/32 KiB,旧包只以哈希引用。
3. 新增或变更的 manifest 条目、测试 ID、golden 和差异报告互相可追溯。
4. 正常、边界、错误、暂停/恢复、断网和回答/工具失败场景均有自动化结果。
5. OPFS 已提交版本、临时文件、journal、任务账本和 checkpoint 状态可解释且可恢复。
6. 失败重试使用幂等键,已确认步骤不会重复执行,未确认步骤不会被跳过。
7. 真实硬件排除项未被误报为仿真支持,仿真数字孪生仍覆盖所需可观察行为。
8. 性能、配额、浏览器兼容和许可证风险已记录,未关闭的风险有责任人和期限。
9. 变更可按证据包中的步骤回滚,并且回滚后旧 context lock 仍可重建。
通过后,将评审记录、门禁结果和 context lock 摘要作为里程碑附件;未通过则保持里程碑开放,不发布“部分完成”的兼容声明。
## 10. 与现有路线的落地关系
技术路线第 8 节的测试层级和第 9 节的阶段 A-E 由本文件第 6、7 节转化为可执行退出条件;覆盖矩阵第 1、2、13 节的状态、manifest、完整性和覆盖率要求由本文件第 2.1、3.4、7 节落实。OPFS 的目录、事务和快照设计以技术路线第 5 节为实现约束,并以本文件第 5 节的故障矩阵作为验收补充。浏览器 UI、Worker 通信、快照、背压、前端任务和 FE-M0..M5 以[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)为实现约束。固定上下文包和提交字段由本文件第 3.2、3.3 节统一,任何后续文档若新增阶段、状态或门禁,必须在这里登记并同步关联文档。

View File

@@ -0,0 +1,452 @@
# LinuxCNC Web/WASM 仿真前端与通信详细设计
**文档状态**:Draft v0.1
**适用基线**:LinuxCNC `2.10.0~pre0`;浏览器运行时、WASM 核心、Pyodide、OPFS 和 Web UI 采用同一 `context_id`
**上层路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**源码关联**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**执行治理**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**总控执行**:[移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
本文件把“浏览器前端”和“LinuxCNC 仿真核心之间的边界”定义为可实现、可测试、可版本化的契约。前端不直接调用 WASM 内部指针、不直接写 OPFS、不直接改变仿真状态;所有命令、状态、事件、日志、快照和恢复操作都通过 Worker 通信层完成。LinuxCNC 的解释器、TP、motion、HAL、IO、Python 和数字孪生是状态权威;前端是命令发起者、状态投影者和用户工作流编排者。
## 1. 设计目标与不可变原则
### 1.1 目标
1. 覆盖 LinuxCNC 仿真用户的完整工作流:工程选择、配置加载、G-code 编辑/预览/运行、MDI、jog、home、override、主轴、换刀、冷却、探针、报警、HAL、ClassicLadder、工具表、参数、日志、快照、恢复、导入导出和离线运行。
2. 前端在 Chromium、Firefox、Safari 支持范围内保持一致的命令语义和状态显示;不要求桌面 GUI 像素级复制,但必须保持工作流、状态、错误和结果等价。
3. 主线程只处理输入、布局、可视化和状态投影;WASM、Python、HAL 周期、文件系统、journal、快照和恢复全部在 Worker 侧执行。
4. 通信协议可录制、回放、限流、取消、重试和恢复;断网、标签页挂起、Worker 重启、回答中断或工具结果丢失不得产生重复副作用或跳过步骤。
5. UI 功能条目与 LinuxCNC 源文件、配置入口、测试和 oracle 互相可追溯,纳入覆盖矩阵和发布门禁。
### 1.2 不可变原则
| 原则 | 约束 |
|---|---|
| Worker 权威 | 前端显示的运行状态必须来自 Worker 快照或事件;前端本地乐观状态只能标记为 `pending`,不能当作仿真已执行 |
| 单写者 | 一个仿真会话只允许一个 Worker 实例写入 WASM 状态和 OPFS journal;多标签页只能观察或显式取得租约 |
| 固定顺序 | 命令按 `seq` 排序,WASM step、HAL thread、事件和 OPFS commit 在会话内串行 |
| 版本化边界 | 命令、事件、快照、能力、工程格式和错误码均带 `schema_version`;未知版本拒绝而不猜测 |
| 可重放 | 任何影响仿真结果的命令都可记录输入、tick、结果和状态哈希;调试 UI 不得改变执行顺序 |
| 本地优先 | 网络不可用不影响已缓存资源和 OPFS 工程的本地仿真;远程同步是可选功能 |
| 安全排除 | 不把宿主路径、shell、真实设备、原始 socket、动态库或任意 Python 宿主能力暴露给前端插件 |
## 2. 技术路线与实现方法
### 2.1 前端技术栈
```text
React + TypeScript
├─ AppShell / Router / PermissionBoundary
├─ Domain stores(session、program、motion、hal、io、workspace、recovery)
├─ WorkerClient(postMessage、序列、超时、重试和协议校验)
├─ Command UX(命令表单、pending、ack、result、错误和取消)
├─ Editors(G-code、INI、HAL、工具表、ClassicLadder、VCP schema)
├─ Machine UI(状态、MDI、jog、home、override、报警、工具和 I/O)
├─ Visualization(Three.js/WebGL/WebGPU 适配、轨迹、坐标、毛坯和模型)
└─ Diagnostics(HAL graph/scope、事件、日志、差异和能力报告)
│ structured clone / transferable ArrayBuffer
▼
Dedicated sim.worker.ts
├─ ProtocolGateway / TaskLedger
├─ SessionManager / CapabilityRegistry
├─ SimScheduler / LinuxCncCompat
├─ WasmRuntime / PythonRuntime
├─ WorkspaceRuntime / OpfsJournal
├─ SnapshotManager / RecoveryManager
└─ StatePublisher / BackpressureController
```
推荐采用 TypeScript 严格模式、运行时 schema 校验(例如手工生成的校验器或项目选定的 schema 工具)、结构化日志和可测试的纯 reducer。React 组件不能直接依赖 `postMessage`;所有消息经过 `WorkerClient`,所有 UI 状态经过 domain store。Three.js 只消费轨迹/姿态快照,不读取 WASM 内存。
### 2.2 分层责任
| 层 | 责任 | 允许访问 | 禁止访问 |
|---|---|---|---|
| View/UI | 用户输入、布局、无障碍、键盘/触摸、状态投影 | domain store、WorkerClient 的只读结果 | WASM、OPFS、网络写入、直接修改 authoritative state |
| Domain store | 规范化状态、reducer、派生选择器、pending/error 状态 | 已校验 Worker 消息 | 猜测未收到的状态、直接持久化 |
| WorkerClient | 握手、序列、超时、取消、重试、消息校验、传输统计 | `postMessage`、transferable buffer | 修改仿真语义、跳过 ack、吞掉错误 |
| ProtocolGateway | schema、权限、幂等键、命令路由和能力检查 | Worker 内模块 | 未版本化命令、跨会话写入 |
| SimRuntime | WASM、Python、HAL、IO、固定 tick、状态快照 | Worker 内 WASM/虚拟 FS | DOM、真实硬件、宿主命令 |
| WorkspaceRuntime | OPFS、journal、checkpoint、导入导出、配额 | Worker OPFS API | UI 线程同步句柄、未提交覆盖 |
| StatePublisher | 快照、事件、轨迹、采样、背压、丢弃策略 | Worker 状态 | 改变核心状态或重排事件 |
### 2.3 状态管理方法
Worker 发布三种数据:
1. **权威快照**:完整或增量的当前状态,带 `simulation_tick` 和状态哈希;UI 刷新、重连和恢复以此为基准。
2. **领域事件**:报警、错误、模式变化、换刀、主轴到速、探针触发、HAL 变化、恢复进度和命令结果;事件不可替代缺失的权威快照。
3. **高频流**:轨迹点、HAL sample、位置/速度/电流等,可按订阅和背压丢弃中间样本;流末端必须有序号和时间范围,不能丢失最终状态。
前端 store 保存 `authoritative`、`pending`、`last_error`、`last_verified_tick` 和 `stream_cursor`。任何本地编辑先进入 draft;保存成功后才更新 committed 版本。仿真状态、工程文件状态和 UI 偏好分开存储。
## 3. 前端功能设计
### 3.1 应用壳和导航
| 功能 | 前端行为 | 通信依赖 |
|---|---|---|
| 启动/能力检查 | 显示浏览器、WASM、Pyodide、OPFS、WebGL/WebGPU、跨源隔离和缓存状态 | `HELLO`、`CAPABILITIES` |
| 会话选择 | 打开、创建、复制、重命名、导入、导出工程;显示租约和最近快照 | `WORKSPACE_LIST`、`SESSION_CREATE`、`PROJECT_*` |
| 运行态导航 | 控制台、程序、预览、HAL、PLC、工具、配置、日志和差异视图 | `STATE_SNAPSHOT`、领域订阅 |
| 多视图同步 | 一个主控视图,其他视图只读订阅;切换不重启仿真 | `SUBSCRIBE`、`SNAPSHOT_REQUEST` |
| 权限边界 | 插件和 Python VCP 只能访问 manifest 授权的 pins、命令和文件 | `CAPABILITIES`、`PLUGIN_*` |
### 3.2 程序、MDI 和编辑器
- G-code 编辑器:语法高亮、行号、错误定位、程序资源树、搜索、跳转行、保存草稿和版本比较。
- 程序预览:解析状态、轨迹、刀具、坐标系、毛坯、当前行和首个差异 tick;预览必须明确“仅解析”或“已运行”。
- 程序控制:加载、运行、暂停、单步、可选停止、恢复、从行运行、重启、清空队列和中止。
- MDI:输入、历史、状态、返回结果、队列破坏和错误行;提交后显示 `pending → ack → result`。
- 宏/remap:显示调用栈、参数、返回值、Python 异常、资源限制和 `real_hardware_excluded` 错误。
### 3.3 手动操作和机床控制
- 模式:manual、mdi、auto、teleop、joint、world、emergency stop、off、on、homing。
- Jog:轴/关节选择、连续/增量、速度、方向、按下/释放、键盘/触控/手轮事件去抖和释放保护。
- Home/limit:单轴和全部回零、序列、共享开关、锁定转台、软限位/硬限位和跟随误差报警。
- Override:feed、spindle、rapid、adaptive、feed hold、feed inhibit、spindle brake 和急停;每个变更显示来源、范围和确认结果。
- 主轴/IO:方向、转速、定向、齿轮、冷却、润滑、换刀、探针、等离子 torch/arc-ok、激光功率和故障注入。
### 3.4 HAL、PLC 和诊断
- HAL browser:组件、pin、signal、param、类型、方向、线程、函数、连接和当前值。
- HAL command console:`loadrt`、`addf`、`setp`、`sets`、`net`、`unlinkp`、`show`、`save`、`source` 的受控子集。
- HAL scope/streamer:订阅通道、采样频率、触发、缩放、暂停、导出、回放和丢样本标记。
- ClassicLadder:工程加载/保存、section、symbol、变量、梯形图、顺序图、扫描周期、虚拟寄存器和调试状态。
- 诊断:事件时间线、报警确认、配置错误、Worker overrun、OPFS 错误、配额、能力报告和原生/WASM 差异。
### 3.5 工程、工具和配置
- 工程文件树:INI/HAL/TBL/VAR/NGC/PY/XML/TCL/CLP、模型和材料表;显示版本、哈希、未保存变更和冲突。
- 配置编辑器:section/键类型、include 图、HALFILE 顺序、资源引用、能力映射、警告和仿真排除项。
- 工具表:pocket/tool、几何、磨损、角度、方向、数据库查询、备份、迁移和换刀预览。
- 快照/恢复:运行态 checkpoint、事件增量、恢复点、差异和回滚;UI 不得把未提交 draft 当作已恢复。
- 导入/导出:工程包、manifest、golden、日志和兼容报告;导入先校验哈希、版本、路径和权限。
### 3.6 可视化与响应式设计
- 三栏工作布局:左侧工程/程序资源,中部程序与 3D 主视图,右侧状态/报警/工具面板;窄屏切换为标签页和底部控制栏。
- 关键状态使用稳定区域:模式、急停、运行/暂停、当前行、坐标、进给、主轴、报警和恢复状态不能因消息刷新跳动。
- 轨迹数据使用 transferable `ArrayBuffer`;渲染线程只处理最新可绘制帧,中间帧可丢弃并显示采样间隙。
- 所有控制支持键盘和触摸;急停、停止、释放 jog 等高风险操作需要可见状态和二次确认策略。
- 前端不使用“看起来完成”的乐观动画;命令尚未 `verified` 时显示 pending,错误必须保留来源和操作 ID。
## 4. 前端与 LinuxCNC 通信协议
### 4.1 通信通道
```text
UI thread
└─ WorkerClient.postMessage()
├─ control channel: command/ack/result/error/recovery
├─ state channel: snapshot/event/alarm/capability
└─ stream channel: trajectory/HAL samples/log chunks (transferable buffers)
```
第一版使用 Dedicated Worker 的 `postMessage` 和结构化克隆;大块轨迹、日志和快照使用 transferable `ArrayBuffer`。SharedArrayBuffer 只作为后续性能优化,必须保持相同命令顺序和结果。通信不使用真实 NML socket;NML 消息类型和状态语义在 `LinuxCncCompat` 中映射到版本化 message bus。
### 4.2 通用消息信封
所有消息都包含以下字段;事件和流也必须带 `seq` 和 `simulation_tick`,不能使用裸 JSON:
```ts
type MessageEnvelope<T> = {
schema_version: 1;
kind: string;
context_id: string;
session_id: string;
seq: number;
operation_id?: string;
simulation_tick?: number;
sent_at_monotonic_ms?: number;
payload: T;
};
```
`seq` 在会话内单调递增;`operation_id` 对所有有副作用命令必填;`sent_at_monotonic_ms` 只用于诊断,不参与仿真结果。消息大小、数组长度、字符串长度、订阅数量和命令速率均由 capability/limits 下发并由 Worker 再次校验。
### 4.3 握手和能力协商
```text
UI -> HELLO { client_version, supported_schema_versions, requested_features }
Worker -> CAPABILITIES { context_id, abi, snapshot, commands, streams, limits, exclusions }
UI -> SESSION_ATTACH { session_id?, project_id, lease_token? }
Worker -> SESSION_READY { session_id, state_hash, simulation_tick, subscriptions }
```
握手失败不得降级到未版本化协议。`CAPABILITIES` 必须列出支持的 G/M/O、HAL 组件、kinematics、Python 模块、UI 插件、最大程序大小、最大快照、可用流和 `REAL-EXCLUDED` 能力。前端按能力隐藏或禁用控制,并显示原因,不把缺失能力伪装成运行时错误。
### 4.4 命令分类
| 命令族 | 示例 | 副作用 | 处理要求 |
|---|---|---:|---|
| 生命周期 | `SESSION_CREATE`、`LOAD_PROJECT`、`START`、`STOP`、`RESET`、`DESTROY` | 是 | prepare/verify/commit;租约和状态机检查 |
| 程序 | `PROGRAM_LOAD`、`PROGRAM_RUN`、`PROGRAM_PAUSE`、`PROGRAM_RESUME`、`PROGRAM_ABORT`、`PROGRAM_STEP`、`PROGRAM_SEEK` | 是 | 记录程序哈希、当前行和 tick;不允许跳过队列语义 |
| MDI/运动 | `MDI_EXECUTE`、`JOG_START`、`JOG_STOP`、`HOME`、`SET_MODE` | 是 | jog 必须有 stop/释放;重复 `operation_id` 返回原结果 |
| 倍率/IO | `SET_OVERRIDE`、`SPINDLE_COMMAND`、`COOLANT_COMMAND`、`TOOL_PREPARE`、`TOOL_CHANGE`、`PROBE_CONFIG` | 是 | 能力、范围、模式和虚拟设备状态校验 |
| HAL/PLC | `HAL_COMMAND`、`HAL_SET`、`HAL_SUBSCRIBE`、`PLC_START/STOP` | 是 | 受控白名单、pin 类型检查、周期和权限检查 |
| 工作区 | `PROJECT_SAVE`、`IMPORT`、`EXPORT`、`CHECKPOINT`、`RECOVER` | 是 | OPFS journal;结果带版本和哈希 |
| 查询/诊断 | `SNAPSHOT_REQUEST`、`HAL_READ`、`LOG_QUERY`、`DIFF_QUERY`、`CAPABILITIES` | 否/只读 | 可取消、可重复;不改变仿真状态 |
| 控制 | `SUBSCRIBE`、`UNSUBSCRIBE`、`CANCEL`、`PING` | 订阅有状态 | 明确 cursor、采样率、背压和取消结果 |
每条命令经过 `validate → authorize → ledger_prepare → execute → verify → ledger_commit → ack/result`。`ACK` 只表示 Worker 已接收和排队,不表示仿真完成;UI 只有收到 `COMMAND_RESULT` 或明确的 `ERROR` 才结束 pending。
### 4.5 状态、事件和流
| 消息 | 频率 | 内容 | 丢失策略 |
|---|---:|---|---|
| `STATE_SNAPSHOT` | 20-60 Hz 或请求 | task/motion/interpreter/IO/HAL/tool/Python/alarms/恢复状态 | 不可丢最终快照;按 cursor 重取 |
| `EVENT`/`ALARM` | 事件驱动 | 状态变化、错误、换刀、探针、主轴、配置和恢复事件 | 不可静默丢;持久化 event log,缺号触发恢复 |
| `TRAJECTORY_CHUNK` | 订阅 | tick 范围、坐标/姿态、刀具、采样和差异标记 | 可丢中间块;保留范围和 gap,最终位置来自快照 |
| `HAL_SAMPLE` | 订阅 | 通道定义、采样值、tick、丢样本数 | 可按背压降采样;触发和边界样本不可丢 |
| `LOG_CHUNK` | 请求/批量 | 级别、来源、操作 ID、错误码和文本 | 可滚动;错误和恢复日志不可丢 |
| `RECOVERY_STATUS` | 恢复期间 | checkpoint、重放、对账、冲突、可继续状态 | 不可丢,UI 必须阻塞依赖状态的命令 |
### 4.6 典型消息
```json
{
"schema_version": 1,
"kind": "MDI_EXECUTE",
"context_id": "linuxcnc-2.10.0-pre0-wasm-c1",
"session_id": "s-42",
"seq": 184,
"operation_id": "op-184-9fd1",
"payload": { "line": "G0 X10 Y5", "source": "mdi", "queue_policy": "queue_buster" }
}
```
```json
{
"schema_version": 1,
"kind": "COMMAND_RESULT",
"context_id": "linuxcnc-2.10.0-pre0-wasm-c1",
"session_id": "s-42",
"seq": 184,
"operation_id": "op-184-9fd1",
"simulation_tick": 93210,
"payload": {
"status": "verified",
"state_hash": "sha256:...",
"canonical_events": 1,
"error": null,
"ledger_commit": "journal/184.commit"
}
}
```
### 4.7 错误、取消和超时
错误码分为 `protocol_*`、`capability_*`、`config_*`、`interpreter_*`、`motion_*`、`hal_*`、`python_*`、`workspace_*`、`recovery_*` 和 `real_hardware_excluded`。错误必须包含 `code`、`message_key`、参数、来源文件/行、`operation_id`、`simulation_tick`、可重试性和建议动作。
取消分为 `requested`、`accepted`、`draining`、`cancelled`、`too_late`;已经提交的有副作用命令不能回滚成“未执行”。超时只表示客户端尚未收到确认,WorkerClient 必须进入 `awaiting_reconcile` 并查询账本/状态,而不是直接重发。
### 4.8 背压、流控和性能预算
1. 控制命令使用有界队列;超过上限返回 `protocol_backpressure`,不丢弃已经接收的副作用命令。
2. 快照发布采用“最新优先”;事件、报警、恢复状态和命令结果采用“顺序优先”,不可用快照降采样替代。
3. 轨迹和 HAL 流按订阅者独立限速;Worker 返回 `cursor`、`sample_period`、`dropped_count` 和 tick 范围。
4. 单次消息、单工程、单快照、单流和订阅数量上限在 `CAPABILITIES.limits` 中声明,并在 Worker 和 UI 双重检查。
5. 前端只渲染当前可见窗口和最近快照;日志、轨迹和波形使用虚拟列表/分块,不把无限历史放入 React state。
6. M1 建立命令确认 P95/P99、快照延迟、流吞吐、UI 主线程阻塞和 Worker overrun 基线;超过预算必须阻止里程碑。
### 4.9 LinuxCNC 源程序和通信映射
ProtocolGateway 不能直接调用零散 LinuxCNC 函数;每个命令族先映射到 `LinuxCncCompat` 的稳定接口,再由兼容层访问对应源码状态机。以下关联必须写入 manifest 的 `source_files`、`source_symbols`、`protocol_messages` 和 `state_fields`:
| 前端通信域 | LinuxCNC 源程序 | Worker/WASM 适配接口 | 前端消息/状态 |
|---|---|---|---|
| 生命周期和 Task | `linuxcnc/src/emc/task/emctaskmain.cc`、`taskclass.cc`、`taskintf.cc`、`emctask.cc` | `SimTask.create/load/start/stop/reset/destroy` | `SESSION_*`、`START/STOP/RESET`、`task.state` |
| 程序和解释器 | `linuxcnc/src/emc/rs274ngc/interp_read.cc`、`interp_execute.cc`、`interp_queue.cc`、`interp_convert.cc`、`modal_state.cc` | `Interpreter.load/run/step/abort/seek` | `PROGRAM_*`、`interpreter.*`、`canonical.*` |
| MDI 和 canonical | `linuxcnc/src/emc/task/emccanon.cc`、`linuxcnc/src/emc/nml_intf/interpl.cc`、`linuxcnc/src/emc/rs274ngc/interp_queue.cc` | `SimTask.mdi`、`CanonicalQueue` | `MDI_EXECUTE`、`command_result`、当前行/队列 |
| Motion/jog/home/limit | `linuxcnc/src/emc/motion/command.c`、`control.c`、`homing.c`、`motion.c`、`motion_struct.h` | `SimMotion.command/jog/home/snapshot` | `JOG_*`、`HOME`、`SET_MODE`、`motion.*`、`homing.*` |
| TP 和轨迹 | `linuxcnc/src/emc/tp/tp.c`、`tc.c`、`tcq.c`、`blendmath.c`、`spherical_arc.c` | `TrajectoryPlanner.step/readChunk` | `TRAJECTORY_CHUNK`、位置/速度/加速度、queue depth |
| Kinematics/姿态 | `linuxcnc/src/emc/kinematics/*kins.c`、`*kins.cc`、`kins_util.c`、`trtfuncs.c` | `Kinematics.forward/inverse/select` | `kinematics.*`、joint/axis/pose、singularity error |
| IO/主轴/换刀/冷却 | `linuxcnc/src/emc/iotask/ioControl.cc`、`ioControl_v2.cc`;`linuxcnc/src/emc/tooldata/**` | `SimIo.command/snapshot`、`ToolService` | `SPINDLE_*`、`TOOL_*`、`COOLANT_*`、`io.*`、`tool.*` |
| HAL/halui | `linuxcnc/src/hal/hal_lib.c`、`hal.h`、`hal_priv.h`、`halmodule.cc`;`linuxcnc/src/emc/usr_intf/halui.cc` | `SimHal.execute/read/subscribe` | `HAL_COMMAND/SET/SUBSCRIBE`、`hal.*`、`HAL_SAMPLE` |
| ClassicLadder | `linuxcnc/src/hal/classicladder/classicladder.c`、`calc.c`、`calc_sequential.c`、`files_project.c`、`module_hal.c` | `SimPlc.load/start/stop/step` | `PLC_*`、PLC section/variable/scan state |
| NML 状态/命令/错误 | `linuxcnc/src/emc/nml_intf/emc.cc`、`emc.hh`、`emc_nml.hh`;`linuxcnc/src/libnml/nml/stat_msg.cc`、`cmd_msg.cc` | `LinuxCncCompat.translateCommand/publishStatus` | envelope、`STATE_SNAPSHOT`、`EVENT`、`ALARM` |
| Python/remap/VCP | `linuxcnc/src/emc/pythonplugin/python_plugin.cc`;`linuxcnc/src/emc/rs274ngc/interp_python.cc`、`interp_remap.cc`;`linuxcnc/lib/python/hal.py`、`qtvcp/**`、`gladevcp/**` | `PythonRuntime.invoke`、虚拟 `linuxcnc/hal` 模块 | `PLUGIN_*`、Python result/error、VCP pin/event |
| 工程和参数 | `linuxcnc/src/libnml/inifile/**`、`linuxcnc/src/emc/ini/**`、`linuxcnc/src/emc/tooldata/**` | `WorkspaceRuntime`、`ConfigService` | `PROJECT_*`、`IMPORT/EXPORT`、配置/工具/参数状态 |
前端所需但 LinuxCNC 原生状态中不存在的字段,例如 `pending`、`operation_id`、`lease_token`、`recovery_progress` 和 `dropped_count`,属于 Web 适配状态,必须明确标记为 `SIM-ADAPTER`,不能写回 LinuxCNC 核心结构。
### 4.10 快照状态契约
快照按领域分区,允许通过位图和版本号发送增量;增量必须基于已确认的 `base_snapshot_id`,基线缺失时前端请求完整快照:
```ts
type SimSnapshotV1 = {
snapshot_id: string;
base_snapshot_id?: string;
full: boolean;
simulation_tick: number;
state_hash: string;
task: { state: string; mode: string; interp_state: string; current_line: number };
motion: { axes: number[]; joints: number[]; velocity: number[]; homed: boolean[]; limits: number };
coordinates: { machine: number[]; relative: number[]; g5x: number; g92: number[]; tool_offset: number[] };
spindle: Array<{ enabled: boolean; direction: number; commanded: number; actual: number; at_speed: boolean }>;
io: { coolant: number; lube: boolean; digital: number[]; analog: number[]; probe: boolean };
tool: { current: number; prepared: number; pocket: number; change_state: string };
overrides: { feed: number; rapid: number; spindle: number[]; adaptive: number; feed_hold: boolean };
hal: { graph_revision: number; changed_values?: ArrayBuffer };
alarms: Array<{ id: string; severity: string; code: string; acknowledged: boolean }>;
recovery: { status: string; last_verified_seq: number; checkpoint_tick: number };
};
```
实际编码可以是生成的二进制 schema,但字段语义必须保持。浮点数组的轴顺序、单位、NaN/Infinity 策略、位掩码和空值语义必须在 schema 中固定;前端不能根据数组长度猜测机床配置。
### 4.11 Worker 到 WASM C ABI 调用规则
| Worker 操作 | C ABI | 调用规则 | 结果 |
|---|---|---|---|
| 创建核心 | `sim_create(config, len)` | 握手和配置校验后调用一次;失败则不创建会话 | 错误码、能力基础和初始状态 |
| 挂载工程 | `sim_load_workspace(root_path)` | WorkspaceRuntime 完成 journal 恢复和 `/workspace` 挂载后调用 | 配置诊断、资源图和可启动状态 |
| 发送命令 | `sim_command(cmd, len)` | ProtocolGateway 编码已校验命令;每个 `operation_id` 最多进入核心一次 | 入队/拒绝错误;最终结果从事件或快照确认 |
| 推进仿真 | `sim_step(cycles)` | 仅 SimScheduler 调用;UI 不得直接指定墙钟结果 | 新 tick、事件和可发布状态 |
| 读取快照 | `sim_read_snapshot(out, capacity)` | 先查询/预分配容量;复制到 Worker 所有的 buffer,再转移给 UI | `STATE_SNAPSHOT` 或容量错误 |
| 读取事件 | `sim_read_event(out, capacity)` | 循环读取到空或有界批次上限;保持核心顺序 | `EVENT`、`ALARM`、`COMMAND_RESULT` |
| 导出运行态 | `sim_export_state(out, capacity)` | 在安全 tick 暂停写入,生成带 schema/context 的 checkpoint | WorkspaceRuntime prepare/commit |
| 导入运行态 | `sim_import_state(data, len)` | 校验 context、schema、工程哈希后导入;导入失败保留旧实例 | `RECOVERY_STATUS` 和完整快照 |
| 销毁核心 | `sim_destroy()` | 关闭订阅、flush journal、关闭句柄后调用;幂等 | `SESSION_DESTROYED` |
WASM 内存由 `WasmRuntime` 独占。输入 buffer 在调用返回前有效;输出在下一次核心调用前复制到 Worker buffer;TypeScript 不长期保存 WASM heap view。C ABI 返回值统一映射为版本化错误码,禁止用异常字符串作为协议。`sim_step`、`sim_command`、状态导入导出和销毁不能并发执行。
## 5. 生命周期、恢复和离线通信
### 5.1 会话生命周期
```text
new → handshaking → attaching → loading → ready
ready → running ↔ paused
running/paused → stopping → stopped
任何状态 → recovering → ready | blocked
任何状态 → destroying → destroyed
```
状态转换由 Worker 权威决定。UI 可以请求转换,但不能本地伪造转换。每次转换发送 `STATE_TRANSITION` 事件并写入任务账本;非法转换返回 `state_transition_invalid`。
### 5.2 刷新、Worker 重启和回答中断
Worker 启动后先发送 `RECOVERY_REQUIRED` 或 `SESSION_READY`。UI 在收到 `RECOVERY_STATUS=verified` 前禁用依赖运行态的运行、jog、MDI 和工具命令,只允许恢复、导出、查看日志和取消。`WorkerClient` 维护未完成操作表;重连时按 `operation_id` 查询结果,已 commit 的操作只关联原结果,未 commit 的操作从 checkpoint 继续。
### 5.3 断网和重新联网
前端的网络状态不能决定本地仿真状态。Service Worker 缓存应用壳、WASM、Pyodide、协议 schema 和样例;Worker 继续使用 OPFS journal、快照和本地命令。重新联网只执行缓存更新或可选同步,必须先比较 `context_id`、工程哈希、最后 `seq`、tick 和状态哈希;冲突时保留本地版本并显示差异。
### 5.4 多标签页与租约
默认一个工程只能有一个写入会话。主标签页持有 `lease_token` 和 heartbeat;其他标签页以只读模式订阅快照。主标签页崩溃后,候选标签页读取 lease expiry、校验 checkpoint 和 journal,再通过 `SESSION_CLAIM` 取得新租约。两个写者不允许同时运行;无法确定租约时进入 `blocked` 而不是并发写入。
## 6. 安全、权限和兼容性
- Worker 对命令、文件、组件、Python 模块和插件能力做白名单检查;UI 隐藏按钮不是安全边界。
- 工程路径统一规范化为 `/workspace` 相对路径,拒绝 `..`、绝对宿主路径、符号链接和未授权扩展名。
- Python remap、filter 和 VCP 只访问虚拟 `linuxcnc`、`hal`、INI、参数、时间和 I/O API;`fork/exec/subprocess/threading/socket/设备` 返回明确错误。
- 所有外部输入(导入工程、G-code、HAL、XML/UI、插件)先解析、限制资源、记录来源哈希,再提交给 Worker。
- 前端显示 `REAL-EXCLUDED` 时必须说明“不连接真实硬件、不提供安全保证”,禁止使用“在线”“已连接”等误导状态。
- schema/ABI/工程格式升级采用新版本号、双读迁移和 context lock;旧版本不能静默解释新字段。
## 7. 详细实施任务
任务标题统一使用 `[WS-x][Mx]`,每个任务绑定 source map/coverage manifest 条目、固定上下文包和测试证据。
### WS-5 前端与可视化任务
| 任务 ID | 任务 | 交付物 | 依赖 | 验收 |
|---|---|---|---|---|
| FE-01 | 应用壳、路由、响应式布局和主题变量 | AppShell、导航、布局断点、无障碍基础 | 无 | 首次启动、窄屏、键盘和只读模式通过 |
| FE-02 | WorkerClient 和协议校验 | 信封类型、schema 校验、序列、超时、取消、重试 | COM-01 | 非法/未知消息拒绝;重复操作不重复副作用 |
| FE-03 | 会话/工程工作流 | 工程选择、租约、导入导出、恢复向导 | WS-3、COM-02 | 刷新、Worker 重启、无网、冲突恢复通过 |
| FE-04 | 程序编辑器和预览 | G-code 编辑、诊断、轨迹预览、当前行和差异 | WS-1、COM-02 | 语法/运行错误定位,预览与 golden 一致 |
| FE-05 | 控制台和手动操作 | MDI、run/pause/step/abort、jog/home/override | WS-2、COM-02 | 模式状态机、释放保护、命令结果完整 |
| FE-06 | 工具、IO、报警和状态面板 | 工具表、主轴、冷却、换刀、探针、报警 | WS-2、WS-3 | 正常/故障/恢复工作流通过 |
| FE-07 | HAL/PLC/诊断界面 | pin 图、命令控制台、scope、Ladder、日志 | WS-2、WS-4 | 订阅、触发、回放和权限限制通过 |
| FE-08 | Three.js 机床和毛坯 | 模型、姿态、轨迹、坐标、切削层 | FE-04、COM-03 | 非空渲染、采样背压、最终姿态一致 |
| FE-09 | VCP/插件 schema 运行时 | XML/descriptor 转 Web Component、权限和生命周期 | WS-4、COM-02 | 样例面板、pin 回调、异常隔离通过 |
| FE-10 | 前端性能和可观测性 | 虚拟列表、帧预算、消息统计、录制回放 | FE-01..09 | P95/P99 和主线程预算满足门禁 |
### WS-2/COM 通信与状态任务
| 任务 ID | 任务 | 交付物 | 依赖 | 验收 |
|---|---|---|---|---|
| COM-01 | schema 和 C/TS 类型生成 | `protocol.v1`、命令/事件/错误/能力定义 | WS-0 | schema 版本锁定,未知字段策略明确 |
| COM-02 | ProtocolGateway/TaskLedger | 路由、权限、幂等、prepare/commit、恢复 | WS-1、WS-3 | 超时、确认丢失、重复响应和重启恢复通过 |
| COM-03 | SnapshotPublisher/流控 | 快照、事件、轨迹、HAL sample、cursor、背压 | WS-2 | 事件不丢、流可降采样、最终快照可重取 |
| COM-04 | LinuxCncCompat message bus | NML command/status/error 到 Worker 消息映射 | WS-1 | 消息类型、顺序、错误和状态 golden 对照 |
| COM-05 | 能力/限制注册表 | G/M/O、组件、kinematics、Python、UI、limits | WS-0、WS-1..4 | UI 正确启用/禁用,排除项可解释 |
| COM-06 | 浏览器和故障矩阵 | Playwright 通讯、离线、租约、刷新、背压测试 | COM-01..05 | Chromium/Firefox/Safari 目标矩阵通过 |
### 7.3 实施批次和依赖顺序
| 批次 | 并行任务 | 串行依赖/冻结点 | 可演示结果 |
|---|---|---|---|
| B0 规范和基线 | COM-01、COM-05、FE-01 | 冻结 `protocol.v1`、能力 ID、错误码和 UI 信息架构 | mock Worker 可完成握手、能力显示和错误页 |
| B1 最小通信闭环 | COM-02、COM-04、FE-02 | 必须先有 `sim_create/load/command/step/snapshot` ABI | 加载最小工程、发送命令、显示权威快照 |
| B2 核心操作 | FE-04、FE-05、COM-03 | interpreter/motion 状态字段冻结 | G-code、MDI、jog、home、override、轨迹闭环 |
| B3 持久化恢复 | FE-03、COM-06 的恢复子集 | WorkspaceRuntime、TaskLedger、checkpoint schema 冻结 | 刷新、Worker 重启、断网和确认丢失自动接续 |
| B4 设备和工具 | FE-06、FE-07 | HAL/IO/tool/PLC schema 和虚拟设备能力冻结 | 主轴、换刀、探针、报警、HAL/PLC 工作流 |
| B5 可视化和插件 | FE-08、FE-09 | 轨迹/姿态流和插件权限冻结 | Three.js 机床、毛坯、VCP/QtVCP 等价工作流 |
| B6 稳定发布 | FE-10、COM-06 全矩阵 | 全量 manifest、浏览器和性能门禁 | 可离线发布候选、兼容/差异/性能报告 |
B0-B2 可以使用只读 mock/golden 驱动 UI 开发,但到对应里程碑退出时必须切换真实 Worker/WASM 状态;mock 不能作为功能完成证据。B3 以后不得修改 `protocol.v1` 语义,只能增加向后兼容字段;破坏性变更必须创建 `protocol.v2` 并回退到 FE-M0 评审。
## 8. 里程碑和退出条件
| 里程碑 | 目标 | 关键输出 | 必须通过的退出条件 |
|---|---|---|---|
| FE-M0 协议冻结 | 定义前端、Worker、WASM 和 OPFS 边界 | `protocol.v1`、能力模型、错误码、UI 信息架构、source/coverage 关联 | 所有消息有 envelope/schema;命令权限和状态转换无歧义 |
| FE-M1 可启动垂直切片 | 工程加载、状态显示、最小 G-code 运行 | AppShell、WorkerClient、`create/load/step/snapshot`、最小 3 轴页面 | 浏览器可加载缓存样例;命令 ack/result、快照和错误完整 |
| FE-M2 操作闭环 | 程序、MDI、jog、home、override、报警 | 控制台、程序编辑/预览、手动操作、状态面板 | 正常/边界/错误/暂停恢复与原生 golden 对照通过 |
| FE-M3 工程和恢复 | OPFS、快照、断网、重启、租约 | 工程树、导入导出、TaskLedger、恢复 UI、离线 PWA | 半提交、配额不足、确认丢失、标签页抢占和冲突均不丢数据 |
| FE-M4 全部仿真工作流 | HAL/PLC/工具/IO/Python/VCP/3D | scope、Ladder、工具、数字孪生、插件和机床视图 | 覆盖矩阵所有 Web/API 条目有 source/test/oracle,关键交互通过 |
| FE-M5 发布候选 | 稳定、离线、高效和可诊断 | 浏览器报告、性能报告、兼容报告、迁移/回滚和离线包 | G0-G5 全绿;无孤儿来源、无未覆盖 UI 工作流、无静默降级 |
每个里程碑都必须附固定 context lock、上下文包、源程序关联 diff、manifest diff、测试日志、golden/difference、性能指标和回滚步骤。发现协议、状态或源文件范围变化时,回退到最早受影响的 FE/COM 里程碑重新验证。
FE 里程碑与项目里程碑映射如下:FE-M0 属于 M0,FE-M1 属于 M1,FE-M2 属于 M2,FE-M3 属于 M3,FE-M4 属于 M4,FE-M5 属于 M5。项目里程碑未通过时,对应 FE 里程碑不能单独签署完成。
## 9. 前端验收与覆盖矩阵绑定
前端功能不得脱离 LinuxCNC 功能条目单独宣布完成。每个前端任务至少绑定:
```text
feature_id → source_files/source_symbols
→ config_inputs
→ worker command/event schema
→ UI workflow test
→ native/WASM oracle or Web-equivalent oracle
→ recovery/offline test
→ context_id + evidence artifact
```
前端与 LinuxCNC 通信条目应使用 `WEB-EQUIV` 或 `SIM-ADAPTER`;Python/VCP 回调使用 `PY-WASM`;虚拟主轴、探针、换刀、等离子、激光、编码器和限位使用 `SIM-DEVICE`。真实设备、真实实时性和安全认证继续使用 `REAL-EXCLUDED`,但其仿真可观察行为必须由数字孪生条目覆盖。
只有以下条件全部满足,才能声明“前端仿真工作流完成”:
1. 能力协商正确反映当前构建和配置,不显示未实现功能。
2. 命令从 UI 到 Worker 到 LinuxCNC 兼容层的顺序、参数、错误和结果可追踪。
3. 快照、事件、流和最终状态在正常、暂停、恢复、断网和 Worker 重启后保持一致。
4. 每个操作具备 pending/ack/result/error 状态,超时先对账,重试不重复副作用。
5. UI、API、Python、HAL 和 OPFS 证据都进入 manifest,且无孤儿来源或未覆盖交互。
## 10. 详细测试矩阵
| 测试层 | 重点用例 | 自动化方式 | 阻断条件 |
|---|---|---|---|
| Schema/类型 | 全部命令、事件、快照、未知版本、缺字段、超限数据 | TS 单测 + C/TS 编解码 round-trip | 任一消息无法双向解析或错误码不稳定 |
| WorkerClient | seq、operation_id、ack/result、timeout、cancel、重复/迟到响应 | 假 Worker、确定性时钟 | 重复副作用、pending 泄漏或错误被吞掉 |
| ProtocolGateway | 权限、状态机、能力、prepare/commit、幂等 | Worker 单测 + WASM stub | 未授权命令进入核心或账本不一致 |
| C ABI | create/load/command/step/snapshot/event/export/import/destroy | Node/WASM harness 和浏览器 Worker | 内存越界、顺序变化、错误映射不一致 |
| Domain store | 完整/增量快照、事件乱序、流 gap、恢复覆盖 | reducer/property tests | UI 状态无法由消息历史重建 |
| UI 工作流 | 工程、程序、MDI、jog、home、override、工具、HAL、PLC、报警 | Playwright + 页面对象 | 任一覆盖条目没有正常/错误/恢复路径 |
| 可视化 | 轨迹、姿态、坐标、毛坯、resize、隐藏/恢复 | Playwright screenshot + canvas pixel checks | 空画布、姿态错误、遮挡或动态内容改变布局 |
| 离线/恢复 | 首次缓存、启动无网、中途断网、刷新、Worker 崩溃、租约转移 | Playwright context/service-worker fault injection | 已提交数据丢失、重复命令、无法继续 |
| 性能/背压 | 大 G-code、多 HAL pin、高采样、长日志、快速模式 | browser benchmark + trace | P95/P99 超预算、无界队列、主线程长任务 |
| 浏览器兼容 | Chromium、Firefox、Safari 支持矩阵 | CI/browser farm 或锁定本地环境 | 必选工作流在任一目标浏览器失败 |
每个测试输出 `context_id`、浏览器、协议版本、工程哈希、最后 seq/tick、状态哈希和产物路径。视觉截图只验证布局和渲染,不能替代 LinuxCNC 状态和轨迹 golden。

View File

@@ -0,0 +1,242 @@
# LinuxCNC Web/WASM 仿真移植总控执行手册
**文档状态**:Draft v0.1
**用途**:统一功能完整性、任务执行、依赖、测试、接续和里程碑验收
**固定基线**:LinuxCNC `2.10.0~pre0`,以 `linuxcnc/VERSION` 和 context lock 为准
**架构路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**源码目录**:[功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**前端/通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**治理规则**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**最小任务与上下文**:[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)
本手册是五份设计文档的总控入口。它不替代各专项文档,而是规定它们如何组合、谁负责什么、任务如何进入执行、依赖如何推进、失败如何恢复以及什么证据才能声明移植完成。
## 1. 当前审计结论
当前工作区已经形成完整的设计基线:技术路线、源码关联、覆盖矩阵、前端通信设计和执行治理均已建立,并且互相有链接。当前工作区尚未出现 `wasm-core/`、`web/`、`tests/golden/`、`tests/browser/` 等实际 Web/WASM 运行时代码目录,因此当前状态只能标记为:
```text
设计基线:已建立
源码/功能清单:规则已建立,待生成机器 manifest
运行时实现:未开始或尚未进入当前工作区
前端实现:未开始或尚未进入当前工作区
全功能验收:未开始,不得声明已完成
```
这不是缺陷,而是交付边界:后续任务必须从 M0/WS-0 开始,先生成可重建的来源和覆盖清单,再实现运行时代码。任何演示页面、mock Worker 或单一三轴样例都不能把项目状态提前改成“全功能完成”。
上下文治理的可执行入口是 `scripts/context_packet.py`,测试是 `scripts/test_context_packet.py`,任务拆分和分层读取以[上下文治理与最小任务目录](linuxcnc-web-wasm-context-governance.md)为准。当前仓库没有运行时实现,但上下文生成和预算测试已经可以独立运行;后续 CI 应在任何任务证据包生成前调用该脚本。
## 2. 唯一追踪链
所有工作都必须沿下面的链路登记,任何断点都会阻止对应条目进入 `accepted`:
```text
LinuxCNC source/config/test
↓ source_id + source_sha
feature_id / capability_id
↓ dependencies + protocol_messages + state_fields
WASM/Worker/UI task_id
↓ implementation commit + fixed context packet
test_id / scenario_id / oracle_id
↓ logs + golden + difference + performance evidence
milestone_id + gate_id + release report
```
### 2.1 记录归属
| 记录 | 规范入口 | 解决的问题 | 不允许替代它的东西 |
|---|---|---|---|
| 架构和范围 | 技术路线 | 做什么、边界在哪里 | 单个任务说明 |
| 源文件和符号 | 源程序关联目录 | LinuxCNC 哪些文件必须审查/适配 | 目录级“已处理” |
| 功能和覆盖 | 覆盖矩阵/manifest | 每项行为如何验收 | 演示成功截图 |
| UI 和通信 | 前端通信设计 | 前端、Worker、WASM 如何协作 | 直接跨层调用 |
| 执行和门禁 | 执行与交付策略 | 上下文、恢复、提交和发布规则 | 临时口头约定 |
| 证据 | CI/发布产物 | 结果能否复现、定位和回滚 | “本地跑过”描述 |
## 3. 总体分工和责任边界
角色是责任类别,不预设具体人员;每个变更必须有一个 DRI(直接负责人)和至少一个独立审查角色。
| 责任域 | 主责任(DRI) | 复核角色 | 主要产物 | 不得越界 |
|---|---|---|---|---|
| 基线与清单 | WS-0/兼容性 | 发布/测试 | source map、manifest、context lock | 不直接实现功能 |
| 核心行为 | WS-1/WASM | 原生 LinuxCNC/测试 | C ABI、解释器、TP、motion、kinematics | 不把 UI 预期当行为定义 |
| Worker/虚拟设备 | WS-2/运行时 | 核心/持久化 | scheduler、HAL、IO、故障模型、事件 | 不直接改 React 状态 |
| Workspace/恢复 | WS-3/存储 | 运行时/安全 | POSIX 适配、journal、checkpoint、迁移 | 不改变运动算法 |
| Python/插件 | WS-4/脚本兼容 | 安全/核心 | Pyodide 模块、remap、VCP 权限 | 不暴露宿主能力 |
| 前端/可视化 | WS-5/FE 任务 | 通信/UX/测试 | UI、WorkerClient、3D、配置工作流 | 不读取 WASM 指针或直接写 OPFS |
| 参考与回归 | WS-6/测试 | 各域 DRI | native runner、WASM golden、浏览器/故障矩阵 | 不删除失败样本来变绿 |
| 发布/文档 | WS-7/发布 | 全部域 | 兼容报告、许可证、离线包、回滚包 | 不绕过门禁 |
协议、快照、工程格式、能力模型和错误码是跨域契约。任何一个域需要读取另一个域的内部结构时,必须先创建 C2 契约变更;禁止通过复制结构或共享未版本化对象解决问题。
## 4. 依赖图和并行策略
```text
M0 / WS-0 基线、source map、manifest、context lock
├─ WS-6 原生参考运行器和 golden 格式
├─ WS-1 WASM 核心、C ABI、解释器、TP、motion、kinematics
│ └─ WS-2 Worker scheduler、HAL、IO、虚拟设备
│ ├─ WS-3 Workspace/OPFS 恢复集成
│ ├─ WS-4 Python/remap/VCP 集成
│ └─ WS-5 前端/可视化集成
└─ COM-01/05 协议和能力模型
└─ COM-02/03/04 Worker 通信、账本、快照和 NML 映射
└─ FE-01..10 前端工作流
↓
WS-6 全量回归 → WS-7 发布
```
可并行:来源扫描、协议类型、原生 runner、AppShell、静态 UI 原型和文档检查。必须串行:同一仿真会话的 tick/命令/HAL/OPFS 写入,协议语义冻结后的破坏性变更,恢复账本重放,以及同一 golden 的生成和验收。并行只能减少验证时间,不能改变仿真事件顺序。
## 5. 统一任务目录
专项文档中的 FE/COM 任务继续作为前端和通信的详细任务;核心、存储、测试和发布任务使用以下统一编号。
### 5.1 WS-0 基线和清单
| ID | 任务 | 输入 | 输出/完成证据 |
|---|---|---|---|
| INV-01 | 扫描文档、源码、符号、配置、组件和测试 | source map、LinuxCNC 基线 | 来源索引、扫描器版本、排除规则 |
| INV-02 | 生成 `coverage/manifest.jsonl` | 来源索引、coverage 规则 | 每个来源的 feature、依赖、oracle、测试和状态 |
| INV-03 | 建立 context lock | 源码/编译器/Python/schema/浏览器 | 可重建锁文件和摘要 |
| INV-04 | 建立原生参考 runner | configs/sim、tests、unit_tests | native 状态/canonical/HAL/IO/golden 格式 |
| INV-05 | 建立影响图和差异报告格式 | manifest、协议、测试 | source→feature→task→test→evidence 索引 |
| INV-06 | 固定上下文包生成器和预算测试 | 任务摘要、候选证据 | 预算前移选择、稳定排除码、必需项失败和生成器单测 |
### 5.2 WS-1 WASM 核心和 ABI
| ID | 任务 | 输入 | 输出/完成证据 |
|---|---|---|---|
| CORE-01 | 独立 CMake/Emscripten 工程 | source map、编译锁 | `wasm-core` 可重复构建 |
| CORE-02 | C ABI 和错误码 | frontend communication design | `sim_create/load/command/step/snapshot/event/export/import/destroy` |
| CORE-03 | rs274ngc/参数/O-code/remap | `src/emc/rs274ngc/**` | 解释器 golden 和错误定位 |
| CORE-04 | TP/posemath/几何 | `src/emc/tp/**`、posemath | canonical、轨迹和容差报告 |
| CORE-05 | Task/NML/状态 | `src/emc/task/**`、nml_intf、libnml | message bus 和状态快照 |
| CORE-06 | Motion/homing/limit | `src/emc/motion/**` | jog、home、limit、following error |
| CORE-07 | Kinematics 全集 | `src/emc/kinematics/**` | 静态模块、能力注册、姿态 golden |
| CORE-08 | Tooldata/IO 基础 | `src/emc/tooldata/**`、iotask | 工具、主轴、换刀、IO 状态 |
### 5.3 WS-2/WS-3/WS-4 运行时集成
| ID | 任务 | 依赖 | 输出/完成证据 |
|---|---|---|---|
| RT-01 | 固定 tick scheduler、批量 step、暂停/单步 | CORE-02/04/05 | 1x/快速/单步结果一致 |
| RT-02 | HAL 对象、thread/function、组件 manifest | CORE-05 | `.comp` 全量索引和 HAL golden |
| RT-03 | 虚拟轴/编码器/主轴/探针/限位/换刀 | RT-02、CORE-06/08 | 正常和故障注入可录制 |
| RT-04 | ClassicLadder/虚拟寄存器 | RT-02 | ladder/scan/IO 交互结果 |
| FS-01 | WasmFS/POSIX 工作区挂载 | INV-02、CORE-03 | open/read/write/stat/readdir 等价测试 |
| FS-02 | OPFS journal/事务/配额 | FS-01 | prepare/commit、损坏和配额恢复 |
| FS-03 | checkpoint/增量重放/迁移 | FS-02、CORE-02 | 连续与恢复终点相同 |
| PY-01 | Pyodide 虚拟 linuxcnc/hal/INI | CORE-03、FS-01 | API/异常/资源限制对照 |
| PY-02 | remap/filter/VCP 生命周期和权限 | PY-01、RT-02 | Python、HAL、UI 回调闭环 |
| COM-01..06 | 协议、账本、快照、NML、能力和浏览器测试 | CORE-02、RT-01、FS-02 | 详见前端通信设计 |
### 5.4 WS-5/WS-6/WS-7 交付
| ID | 任务 | 依赖 | 输出/完成证据 |
|---|---|---|---|
| FE-01..10 | UI、WorkerClient、程序、控制台、HAL/PLC、3D、插件和性能 | COM、RT、FS | 详见前端通信设计 |
| REG-01 | 全量 configs/sim 运行矩阵 | INV-02、CORE/RT/PY | 每个入口有结果和差异 |
| REG-02 | 全量 tests/unit_tests 对照 | INV-04、WASM runner | 通过/排除/阻断逐项报告 |
| REG-03 | 断网、崩溃、回答失败、租约和恢复矩阵 | FS-02/03、COM-02 | 无重复副作用、无静默丢失 |
| REG-04 | 浏览器/性能/视觉矩阵 | FE、COM-03 | Chromium/Firefox/Safari、P95/P99、截图/像素 |
| REL-01 | 兼容、许可证、迁移、回滚和离线包 | 全部 REG | 发布候选证据包和签名报告 |
## 6. 里程碑总表
| 里程碑 | 入口条件 | 必须完成 | 退出证据 | 失败动作 |
|---|---|---|---|---|
| M0 基线锁定 | 版本和范围确认 | INV-01..05、状态枚举、责任人 | source/manifest/context lock 可重建,零孤儿规则生效 | 返回清单阶段,不写运行时代码 |
| M1 运行时骨架 | M0 通过 | CORE-01/02、RT-01、FS-01、COM-01/02 最小集 | create/load/command/step/snapshot/recover、离线缓存启动 | 固定 ABI/schema,修复后重跑 M1 |
| M2 核心行为 | M1 通过 | CORE-03..08、native/WASM runner、解释/轨迹/运动 golden | 核心 manifest `verified`,首个差异 tick 可定位 | 回退到对应核心任务,不进入 UI 全量 |
| M3 虚拟设备和恢复 | M2 通过 | RT-02..04、FS-02/03、PY-01/02、COM-03..05 | HAL/PLC/IO/故障/配额/重启/断网矩阵通过 | 阻断仿真会话发布,保留本地证据 |
| M4 前端工作流 | M3 通过 | FE-01..10、COM-06、关键 UI/3D/配置工作流 | UI 条目有协议、源文件、测试、oracle 和恢复证据 | 禁止 mock 冒充完成,回退 FE/COM |
| M5 发布候选 | M0..M4 通过 | REG-01..04、REL-01 | G0-G5 全绿、全计入条目 `accepted`、排除项可识别 | 维持候选冻结,创建修复 context 或回退 |
里程碑退出不按日期判断,而按证据判断。契约或基线变化必须回退到最早受影响里程碑;不能在 M5 以“后续补齐”方式接受未完成条目。
## 7. 端到端场景验收
| 场景 | 覆盖链 | 必须验证 |
|---|---|---|
| S1 最小三轴启动 | INI→WASM→Worker→UI→OPFS | 能力协商、配置错误、初始快照、停止/销毁 |
| S2 G-code 正常运行 | rs274ngc→canonical→TP→motion→轨迹/UI | 当前行、模态、坐标、轨迹、最终状态和 golden |
| S3 MDI/jog/home | UI command→Task/Motion→HAL/IO | pending/ack/result、释放保护、模式非法转换 |
| S4 主轴/换刀/探针 | iotask/tooldata→虚拟设备→HAL→UI | 正常、不到速、失败、超时、恢复 |
| S5 Python/remap/VCP | Python plugin→HAL/Task→UI | 参数/异常/权限/资源限制和状态哈希 |
| S6 ClassicLadder/HAL | component/PLC→thread→IO→scope | 扫描顺序、pin graph、触发、回放和故障 |
| S7 断网继续 | Service Worker→Worker→OPFS | 无网启动、中途断网、离线保存、重新联网对账 |
| S8 崩溃/回答失败 | TaskLedger→checkpoint→replay | 已确认不重复、未确认不跳过、无法对账时阻断 |
| S9 多浏览器/大负载 | FE/COM→流控→渲染 | P95/P99、背压、内存、UI 长任务和最终状态 |
每个场景产出一个固定大小上下文包、运行日志摘要、source/feature/task/test 索引、状态哈希和可重跑命令。只通过截图或人工点击成功的场景不算验收证据。
## 8. 任务执行规程
### 8.1 开始前
1. 从最小任务目录选择一个单结果子任务,读取最新 context lock 和固定八段上下文包;确认 `context_id`、子任务、主工作流和里程碑一致。上下文包由 `scripts/context_packet.py` 生成,不能手工拼接。
2. 读取 source map/coverage manifest 中的受影响条目,确认输入、依赖、协议、oracle 和非目标。
3. 写明预计变更类型 C0-C3、失败恢复点、回滚提交和测试命令。
4. 若任务跨越三个以上工作流,先拆分;若需要修改协议或格式,先提交 C2 方案。
### 8.2 执行中
1. 先添加失败测试或差异记录,再实现正常路径;不要先删除/放宽测试。
2. 运行受影响图的快速测试,同时保留完整证据路径;独立测试可并行,同一会话必须串行。
3. 每个副作用操作遵循 prepare/execute/verify/commit;回答或工具失败先查账本和产物,不盲目重做。
4. 只更新固定上下文包的对应段,旧历史以摘要/哈希引用;超预算就拆任务。
### 8.3 完成时
任务只有在代码、manifest 状态、自动测试、差异报告、性能/配额影响和文档引用都更新后才能标记完成。合并请求必须包含 `Context-ID`、`Context-Packet`、Scope、Contract-Change、Evidence、Offline-Impact、Coverage 和预算声明。
## 9. 统一完成定义
### 功能完成
- 源程序关联目录中没有孤儿来源、未解析符号或未闭合依赖。
- 所有可计入功能条目达到 `accepted`;每项有实现、测试、oracle、差异和 context lock。
- 正常、边界、错误、暂停/恢复、持久化/离线和适用交互维度全部通过。
- `REAL-EXCLUDED` 只用于真实硬件、实时内核和安全认证,且配置检查返回明确错误;仿真可观察行为仍有数字孪生实现。
### 任务完成
- DRI、审查人、依赖、非目标、回滚点和固定上下文包齐全。
- 未确认操作可安全恢复;重复提交不造成重复副作用;失败原因和下一步可执行。
- 测试、日志、golden、差异、性能和文档链接均可重跑。
### 发布完成
- G0-G5 全部通过;浏览器、离线、恢复、性能、安全、许可证和迁移报告齐全。
- 发布包包含 WASM、Pyodide、Service Worker、协议 schema、source/coverage/golden manifest、兼容报告、排除项清单和回滚包。
- 任何孤儿来源、未覆盖交互、无 oracle、静默降级、半提交数据或超预算上下文都会阻止发布。
## 10. 风险账本和停止线
| 风险 | 触发条件 | 立即动作 | 恢复条件 |
|---|---|---|---|
| 来源遗漏 | source map 与 manifest 数量/哈希不一致 | 停止实现合并,回到 M0 | 重新扫描、补关联、重生成影响图 |
| 核心差异 | 首个 divergent tick、错误码或状态字段不一致 | 冻结受影响域,保留 golden | 定位源文件/编译差异并重跑 |
| 协议漂移 | UI/Worker/C ABI schema 不一致 | 拒绝握手,不能自动降级 | 新版本契约、迁移和双向测试 |
| 数据损坏 | journal 无 commit、哈希不匹配、配额失败 | 保留旧版本,进入恢复/导出 | 完整 commit 或明确阻断 |
| 非确定性 | 同输入同 context 结果不同 | 禁止性能优化合并 | 固定顺序、种子、线程和时钟后重跑 |
| 性能退化 | P95/P99 或 UI 长任务超预算 | 阻止里程碑 | profiling、优化和回归证据 |
| 任务失联 | 固定上下文缺失、无 DRI、无下一步 | 任务标记 blocked | 补齐上下文和负责人,不复制历史 |
## 11. 文档维护规则
新增 LinuxCNC 源文件、配置、G/M/O、HAL 组件、Python API、UI 工作流、协议消息或测试时,必须同时检查:
1. 源程序关联目录是否新增 `source_files/source_symbols/config_inputs`。
2. 覆盖矩阵是否新增 `feature_id`、状态、测试维度、oracle 和差异字段。
3. 前端通信设计是否新增命令、事件、快照字段、能力或 UI 工作流。
4. 执行策略是否需要新增工作流、任务、门禁、恢复场景或上下文锁字段。
5. 技术路线是否需要更新依赖、阶段、风险或代码组织。
6. 若任务新增证据类型,生成器是否为其提供字节大小、优先级、必需性和稳定排除码;是否增加“证据字节预算”和“完整包剩余空间”测试。
文档检查必须验证本地链接、编号、状态枚举、manifest 字段、任务引用和里程碑映射。文档通过不代表代码完成;代码通过也不能绕过文档和 manifest 关联。

View File

@@ -0,0 +1,386 @@
# LinuxCNC 纯 Web/WASM 仿真全功能覆盖矩阵(第一版)
**对应路线**:[LinuxCNC 纯 Web/WASM 仿真机床技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**执行治理**:[LinuxCNC Web/WASM 仿真项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**源码关联**:[LinuxCNC Web/WASM 仿真功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**前端与通信**:[LinuxCNC Web/WASM 仿真前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**总控执行**:[LinuxCNC Web/WASM 仿真移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
**目标**:覆盖 LinuxCNC 面向仿真机床的全部用户可见功能
**范围**:只做仿真,不接真实机床,不实现真实硬件访问
**基线**:仓库当前 LinuxCNC `2.10.0~pre0`
## 1. 覆盖规则
本矩阵不是功能愿望清单,而是验收清单。每一行都必须拥有实现、测试和差异报告;只有标记为 `REAL-EXCLUDED` 的真实硬件/物理安全项目不进入仿真覆盖率。任务拆分、固定 context lock、上下文包预算、断网接续和发布门禁遵循[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md);端到端场景和任务依赖以[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)为准;本矩阵只维护功能条目及其证据索引。
| 标记 | 含义 | 验收要求 |
|---|---|---|
| `SIM-CORE` | 直接复用 LinuxCNC C/C++ 核心 | 原生与 WASM golden 对照 |
| `SIM-ADAPTER` | 进程、NML、文件、时钟等兼容层 | API、状态和错误码对照 |
| `SIM-DEVICE` | 硬件功能的虚拟数字孪生 | pin、时序、故障、报警和状态可录制 |
| `WEB-EQUIV` | 桌面 GUI/工具在 Web 中等价实现 | 工作流和配置结果对照,不要求像素一致 |
| `PY-WASM` | Python/remap/过滤器在 Pyodide 中执行 | 受控宿主 API、异常和返回值对照 |
| `REAL-EXCLUDED` | 真实硬件、物理安全和实时内核 | 不实现,但必须明确报错和文档说明 |
完成定义:所有 `SIM-*`、`WEB-EQUIV`、`PY-WASM` 项在正常、边界、错误、暂停/恢复、持久化/离线和适用的跨域交互维度达到 100% 通过;`REAL-EXCLUDED` 项在配置检查中全部被识别且不会误报为已连接。单一百分比不能替代来源闭包、维度闭包和交互闭包。
条目状态使用执行策略定义的 `planned`、`in_progress`、`implemented`、`verified`、`accepted`、`blocked` 和 `REAL-EXCLUDED`。`accepted` 必须有 context lock、自动测试和差异报告;状态缺失、来源新增但无 manifest 项、或上下文包超预算均为 CI 阻断。
## 2. 功能来源和清单生成
覆盖清单必须从以下仓库来源自动生成,避免只依赖人工整理:
各功能域的实际源文件、配置入口和测试入口维护在[源程序关联目录](linuxcnc-web-wasm-source-map.md);本节定义扫描全集,两者共同构成 manifest 的来源合同。
| 来源 | 覆盖内容 |
|---|---|
| `linuxcnc/docs/src/Master_Documentation.adoc` | 用户、配置、HAL、ClassicLadder、运动、remap、GUI、G-code 和 Python 接口;Hardware Drivers/Examples 只生成排除项 |
| `linuxcnc/configs/sim/**` | 全部仿真 INI/HAL/TBL/NGC/PY/XML/TCL 配置和样例 |
| `linuxcnc/tests/**` | 启动、解释器、运动、HAL、IO、remap、工具、回零、限位和 UI 测试 |
| `linuxcnc/unit_tests/**` | TP、解释器和数学单元测试 |
| `linuxcnc/src/emc/**` | task、io、motion、kinematics、interpreter、tooldata、NML 接口 |
| `linuxcnc/src/hal/components/**` | 全部 `.comp`、C 组件、仿真组件、测试组件和组件模板 |
| `linuxcnc/src/hal/classicladder/**` | 梯形图、顺序功能图、变量、定时器、计数器、Modbus 逻辑和工程文件 |
| `linuxcnc/src/emc/usr_intf/**`、`lib/python/**` | AXIS、Gmoccapy、Touchy、Gscreen、QtVCP、PyVCP、GladeVCP、Vismach 等界面和脚本 |
构建脚本生成 `coverage/manifest.jsonl`,字段至少包括:`id`、`source`、`source_sha`、`domain`、`kind`、`dependencies`、`inputs`、`outputs`、`errors`、`sim_status`、`web_status`、`test_id`、`oracle`、`golden`、`difference`、`coverage_dimensions`、`context_id`、`exclusion_reason`。`source` 必须可定位到文档锚点、源码符号、配置文件或测试用例;`dependencies` 必须递归闭包;`oracle` 必须说明原生对照、规范结果或明确的 Web 等价判定。任何新增源码、文档功能或仿真配置没有 manifest 项时,CI 失败。
### 2.1 完整性闭包和防遗漏检查
每次生成 manifest 都执行以下闭包检查,并将结果作为构建产物:
1. 对文档标题、G/M/O/Python/HAL/API 名称、导出符号、`.comp`、ClassicLadder 对象、`configs/sim/**` 资源和测试入口做规范化扫描,记录来源总数和来源摘要。
2. 计算来源到 manifest 的反向索引;任何孤儿来源、重复 ID、空依赖、无测试、无 oracle 或状态字段缺失都使 CI 失败。
3. 对每个条目展开依赖图,确认依赖条目存在且没有未登记的真实硬件路径;仿真配置引用的排除功能必须有对应数字孪生条目。
4. 为每个条目生成正常、边界、错误、暂停/恢复、持久化/离线测试维度;G-code、HAL、IO、Python、UI 和存储之间的关键交互生成 pairwise/关键路径用例。
5. 比较原生参考运行器和 WASM 结果,保存首个差异 tick、状态/错误字段和容差;没有可比 oracle 的条目只能是 `blocked`,不能是 `accepted`。
6. 发布前生成完整性报告:已接受、进行中、阻断、排除、孤儿来源、未覆盖维度和未覆盖交互分别列出;任何非零的未覆盖或孤儿项阻止全功能声明。
## 3. 启动、工程和配置
| 功能 | Web/WASM 实现 | 标记 |
|---|---|---|
| LinuxCNC 启动、初始化、退出和重启 | Worker 内 `create/load/start/stop/reset/destroy` 状态机 | `SIM-ADAPTER` |
| 配置选择器和 sample 配置 | OPFS 工程浏览器;完整导入 `configs/sim/**` | `WEB-EQUIV` |
| INI 注释、section、变量和 include | 兼容解析、相对路径和循环检测 | `SIM-CORE` |
| `[EMC]`、`[DISPLAY]`、`[FILTER]`、`[RS274NGC]` | 仿真相关字段读取、校验、警告和 Web 配置编辑;桌面进程字段识别后映射或排除 | `SIM-ADAPTER` |
| `[EMCMOT]`、`[TASK]`、`[HAL]`、`[HALUI]`、`[APPLICATIONS]` | 仿真字段映射到虚拟运行时;外部宿主应用不执行 | `SIM-ADAPTER` |
| `[TRAJ]`、`[KINS]`、`[AXIS_*]`、`[JOINT_*]`、`[SPINDLE_*]`、`[EMCIO]` | 轴、关节、主轴、IO、回零和运动参数全部生效 | `SIM-ADAPTER` |
| 多入口 INI、include 目录和资源查找 | `/workspace` 虚拟 POSIX 路径 + OPFS | `SIM-ADAPTER` |
| HAL twopass、HALFILE 顺序、参数展开 | 解析、排序、重复加载和错误语义对照 | `SIM-ADAPTER` |
| HAL 命令 `loadrt/addf/setp/net/sets/ newsig/linksp/linkps` | Worker 内命令执行和状态回放 | `SIM-CORE` |
| 参数、变量、`.var`、tool table、tool database | 内存运行态 + OPFS 事务提交 | `SIM-ADAPTER` |
| 工程复制、删除、重命名、导入、导出 | OPFS journal + ZIP/目录导入导出 | `WEB-EQUIV` |
| 启动错误、配置警告、资源缺失 | 行号、路径、错误码和恢复建议 | `SIM-ADAPTER` |
## 4. G-code、O-code、M-code 和解释器
### 4.1 单词、模式和程序结构
| 功能 | 覆盖内容 | 标记 |
|---|---|---|
| 行语法 | `%`、序号 `N`、注释、空行、大小写、空格、行尾 | `SIM-CORE` |
| 轴/关节词 | `X Y Z A B C U V W`、额外关节和旋转轴 | `SIM-CORE` |
| 速度/主轴/刀具 | `F S T H D Q I J K L P R E` 等全部合法词 | `SIM-CORE` |
| 参数 | `#1..#5399`、命名参数、系统参数、数组、间接寻址 | `SIM-CORE` |
| O-code | sub、call、return、do/while、if/elseif/else、break、continue、repeat、call file、返回值、错误 | `SIM-CORE` |
| Fanuc 风格 | `M98/M99`、编号程序和参数传递 | `SIM-CORE` |
| 读前瞻/队列破坏 | MDI、M-code、输入等待、暂停、remap 的队列语义 | `SIM-ADAPTER` |
| 程序控制 | run、pause、optional stop、single block、step、abort、resume、restart、line seek | `WEB-EQUIV` |
| 错误 | 语法、表达式、参数、路径、运动学和运行时错误 | `SIM-CORE` |
### 4.2 G-code 全量列表
以下代码全部纳入解析、状态、canonical 输出和运动结果测试:
| 类别 | 代码 |
|---|---|
| 运动与停留 | `G0 G1 G2 G3 G4` |
| 样条/NURBS | `G5 G5.1 G5.2 G5.3` |
| 车床模式 | `G7 G8` |
| 参数/坐标/刀具写入 | `G10 L0 L1 L2 L10 L11 L20` |
| 平面 | `G17 G17.1 G18 G18.1 G19 G19.1` |
| 单位与预定义点 | `G20 G21 G28 G28.1 G30 G30.1` |
| 同步/探针 | `G33 G33.1 G38.2 G38.3 G38.4 G38.5` |
| 刀补 | `G40 G41 G41.1 G42 G42.1 G43 G43.1 G43.2 G49` |
| 坐标系 | `G52 G53 G54 G55 G56 G57 G58 G59 G59.1 G59.2 G59.3` |
| 轨迹控制 | `G61 G61.1 G64` |
| 车床循环 | `G70 G71 G71.1 G71.2 G72 G72.1 G72.2 G74 G76` |
| 铣削/钻孔循环 | `G73 G80 G81 G82 G83 G84 G85 G86 G87 G88 G89` |
| 距离/弧距离 | `G90 G90.1 G91 G91.1` |
| G92 | `G92 G92.1 G92.2 G92.3` |
| 进给与主轴模式 | `G93 G94 G95 G96 G97 G98 G99` |
### 4.3 M-code 全量列表
| 类别 | 代码及仿真行为 |
|---|---|
| 暂停/结束 | `M0 M1 M2 M30 M60`;由 Web 控制面板和虚拟托盘/换盘事件驱动 |
| 主轴 | `M3 M4 M5 M19`;虚拟主轴转速、方向、定向和到速状态 |
| 换刀 | `M6 M61`;虚拟手动/随机/非随机换刀、准备/完成状态和失败注入 |
| 冷却 | `M7 M8 M9`;虚拟雾/洪水/主轴冷却 pin 和状态 |
| 倍率 | `M48 M49 M50 M51 M52 M53`;进给、主轴、adaptive、feed stop |
| IO | `M62 M63 M64 M65 M66 M67 M68`;同步/立即数字和模拟 IO、输入等待和超时 |
| 模态 | `M70 M71 M72 M73`;保存、失效、恢复和自动恢复模态状态 |
| 子程序 | `M98 M99`;程序调用、返回和选择性模态恢复 |
| 用户命令 | `M100-M199`;映射为 OPFS 工程内白名单 Python/NGC 虚拟动作,不执行宿主命令 |
### 4.4 Remap、Python 和过滤器
| 功能 | Web/WASM 实现 | 标记 |
|---|---|---|
| 内置 remap prolog/epilog | 解释器内 C++ ABI 复用 | `SIM-CORE` |
| NGC remap | 虚拟文件工作区中的 O-code 执行 | `SIM-ADAPTER` |
| Embedded Python remap | Pyodide Worker,暴露 linuxcnc/hal/INI/参数虚拟模块 | `PY-WASM` |
| `T/M6/M61/S/F` remap | 完整参数、返回值、异常和顺序对照 | `PY-WASM` |
| 自定义 G/M code | 工程 manifest 注册和安全限额 | `PY-WASM` |
| `[FILTER]` 程序过滤器 | 在 Pyodide 中执行 `.py/.pyc` 的纯计算/文本过滤部分 | `PY-WASM` |
| CAD/CAM 外部程序 | 不执行宿主程序;提供内置转换插件接口和错误状态 | `WEB-EQUIV` |
| Python `subprocess/threading/socket/设备` | 虚拟 API 返回 LinuxCNC 约定错误 | `REAL-EXCLUDED` |
## 5. 运动、轨迹、轴、关节和运动学
| 功能域 | 全量覆盖项 | 标记 |
|---|---|---|
| TP | 直线、圆弧、球面弧、样条、NURBS、前瞻、blend、exact path、exact stop、速度/加速度/jerk 限制 | `SIM-CORE` |
| 运行模式 | coordinated、teleop、free/joint jog、MDI、单步、暂停、恢复、abort、queue-buster | `SIM-ADAPTER` |
| 轴/关节 | X/Y/Z/A/B/C/U/V/W、额外关节、随机/非随机配置、轴-关节映射 | `SIM-CORE` |
| 限制 | soft limit、hard limit、following error、backlash、offset limit、速度/加速度限制 | `SIM-ADAPTER` |
| 回零 | search/latch/final velocity、index、shared switch、offset、sequence、volatile、immediate、locking indexer、inhibit | `SIM-DEVICE` |
| 坐标 | machine/world/relative/display、G5x、G52、G92、rotation、scale、external offset、moveoff | `SIM-CORE` |
| 补偿 | cutter radius、tool length、dynamic tool length、additional tool offset、wear、螺距误差 | `SIM-CORE` |
| 主轴同步 | threading、G33/G33.1、rigid tapping、CSS、gear、orient、multiple spindle | `SIM-DEVICE` |
| 控制环 | PID、feed/velocity/position loop、encoder、stepgen、adaptive feed | `SIM-CORE` |
| 运动学模块 | `trivkins`、`5axiskins`、`corexykins`、`cubic`、`genhexkins`、`genserkins`、`lineardelta`、`maxkins`、`pentakins`、`rosekins`、`rotarydelta`、`rotatekins`、`scarakins`、`scorbot`、`tripod`、`xyzac-trt`、`xyzbc-trt`、`switchkins`、`ugenser/userkfuncs` | `SIM-CORE` |
| DH/机器人 | DH 参数、PUMA、SCARA、六足、Delta、Pentapod、Scorbot 和自定义静态 kinematics | `SIM-CORE` |
| 用户 kinematics | 构建期静态 WASM ABI、热切换由仿真状态机完成 | `SIM-ADAPTER` |
| 坐标/姿态显示 | joints、axes、pose、forward/inverse、tool orientation 和 singularity 错误 | `WEB-EQUIV` |
## 6. 机床类型和数字孪生
| 仿真机床 | 覆盖内容 | 标记 |
|---|---|---|
| 3/4/5/6/7/8/9 轴铣床 | 轴、关节、刀具、冷却、换刀、探针、坐标系和轨迹 | `SIM-DEVICE` |
| 车床 | G7/G8、G70/G71/G72/G76、主轴 CSS、刀尖半径、背刀塔、Fanuc Y、双主轴 | `SIM-DEVICE` |
| Gantry/CoreXY | 双关节、耦合、gantry HAL、同步误差和回零 | `SIM-DEVICE` |
| 机器人/Delta/Hexapod | 全部内置 kinematics、关节限位、姿态和模型 | `SIM-DEVICE` |
| 等离子 | torch enable、height control、arc-ok、ohmic、pierce、cut recovery、material table、qtplasmac 行为 | `SIM-DEVICE` |
| 激光 | laser power、PWM/模拟输出、动态功率和轨迹同步 | `SIM-DEVICE` |
| 泡沫切割 | 双平面/双 Z、foam kinematics、丝热功率和同步 | `SIM-DEVICE` |
| 探针/自动对刀 | probe input、触发位置、失败/超时、tool sensor 和宏 | `SIM-DEVICE` |
| 3D 打印/挤出 | 虚拟挤出轴、温度/风扇/材料状态(若 LinuxCNC 仿真配置启用) | `SIM-DEVICE` |
| 毛坯和切削 | 轨迹核心与可视化切削层分离;heightfield/voxel/SDF 按机床类型选择 | `WEB-EQUIV` |
| 故障注入 | 限位、跟随误差、探针不触发、主轴不到速、换刀失败、IO 超时、存储失败 | `SIM-DEVICE` |
## 7. HAL 全量和仿真 I/O
### 7.1 HAL 运行时契约
| 功能 | 覆盖内容 | 标记 |
|---|---|---|
| HAL 对象 | component、pin、signal、parameter、owner、type、direction、value、export/unexport | `SIM-CORE` |
| HAL 命令 | `loadrt`、`addf`、`start/stop`、`setp`、`sets`、`net`、`linksp`、`linkps`、`unlinkp`、`show`、`save`、`source` | `SIM-CORE` |
| HAL 线程 | base/servo/thread 周期、顺序、浮点标志、超时和 overrun 事件 | `SIM-ADAPTER` |
| `.comp` ABI | 全部组件模板的参数、pin、function、personality 和 lifecycle | `SIM-CORE` |
| HAL UI | `halui` 全 pin、模式、jog、home、abort、feed/spindle override 和消息 | `SIM-CORE` |
| HAL scope | 采样、触发、通道、缩放、保存和回放 | `WEB-EQUIV` |
| HAL show/meter | pin 图、实时值、类型和连接关系 | `WEB-EQUIV` |
### 7.2 组件 manifest
`linuxcnc/src/hal/components/**` 下的每个 `.comp`、C 组件和测试组件都必须在 manifest 中出现。按类别验收:
- 数学/转换:abs、scale、sum、mult、div、min/max、near、hypot、lincurve、lut、modmath、bit/float/s32/s64/u32/u64 转换。
- 逻辑/选择:and/or/not、mux、demux、select、logic、flipflop、toggle、tristate、oneshot、edge、multiswitch、multiclick。
- 时序/滤波:time、timedelay、timedelta、ton、tof、updown、counter、sample-hold、lowpass、ilowpass、biquad、filter-kalman、ddt、integ、deadzone。
- 运动/控制:PID、limit、limit-axis、limit1/2/3、blend、simple_tp、tpcomp、feedcomp、moveoff、gantry、corexy、millturn、orient、spindle、spindle-monitor、stepgen、encoder、encoder-ratio。
- 信号/诊断:message、watchdog、safety-latch、estop-latch、histobins、latencybins、sampler、streamer、lcd、matrix-kb。
- 设备/仿真:sim-axis-hardware、sim-encoder、sim-home-switch、sim-matrix-kb、sim-parport、sim-spindle、sphereprobe、probe、raster、laserpower、plasmac、thc/thcud、ohmic、charge-pump。
- 测试和用户模板:constant、test、thread、userkins、usercomp、所有 `*.comp` personality 和示例工程。
### 7.3 通用虚拟机床部件
Web 仿真不移植具体硬件驱动,而由稳定的通用模型为仿真配置提供输入输出:
| 通用模型 | 仿真能力 |
|---|---|
| 虚拟数字/模拟 I/O | pin 值、同步/立即变化、等待、延迟、噪声和故障注入 |
| 虚拟寄存器组 | 为 ClassicLadder/仿真 PLC 提供本地寄存器,不实现真实 Modbus transport |
| 虚拟 MPG/手轮/键盘 | Pointer/键盘/触控事件映射为脉冲和 HAL pin |
| 伺服/编码器 | 位置环、量化、噪声、丢脉冲、跟随误差模型 |
| 电机/主轴/冷却/阀 | 可配置惯性、加减速、到速、温度/压力/状态数字孪生 |
真实并口、GPIO、PCI、Mesa/HostMot2/FPGA、串口、VFD、Modbus transport、现场总线和协议工具统一为 `REAL-EXCLUDED`。
## 8. ClassicLadder、Modbus 和 PLC
| 功能 | 覆盖内容 | 标记 |
|---|---|---|
| 工程文件 | `.clp`、sections、symbols、variables、project load/save | `SIM-ADAPTER` |
| Ladder 对象 | contacts、coils、set/reset、timers、IEC timers、monostable、counters、compare、assignment、jump/call | `SIM-CORE` |
| 顺序控制 | GRAFCET steps/transitions/actions、状态和扫描顺序 | `SIM-CORE` |
| 扫描周期 | 虚拟 PLC thread、周期、overrun 和暂停恢复 | `SIM-ADAPTER` |
| Modbus 逻辑 | 使用虚拟寄存器、延迟和错误状态验证 Ladder 逻辑;不实现真实 serial/TCP transport | `SIM-DEVICE` |
| ClassicLadder UI | Web sections、ladder editor、variable/symbol/config/debug windows | `WEB-EQUIV` |
| PLC 与 HAL | pin 绑定、消息、工具换刀、E-stop、冷却和顺序动作 | `SIM-CORE` |
## 9. 工具、IO、主轴、换刀和探针
| 功能 | 覆盖内容 | 标记 |
|---|---|---|
| 工具表 | pocket/tool、X/Y/Z/A/B/C/U/V/W、diameter、front/rear angle、orientation、wear、offset | `SIM-CORE` |
| 工具数据库 | `db_program`、JSON/文本数据库、查询、更新、备份和迁移 | `SIM-ADAPTER` |
| 换刀 | manual、random、non-random、carousel、prepare/change、M6 remap、tool sensor、失败恢复 | `SIM-DEVICE` |
| IO control v1/v2 | tool prepare/change、coolant、lube、spindle brake、mist/flood、aux outputs | `SIM-DEVICE` |
| 主轴 | 多主轴、方向、速度、加减速、CSS、orient、gear、encoder、到速和故障 | `SIM-DEVICE` |
| 进给 | G93/G94/G95、feed hold、override、adaptive、feed stop、feed inhibit | `SIM-CORE` |
| 探针 | G38.x、probe input、位置锁存、宏、失败/超时和对刀 | `SIM-DEVICE` |
| 位置日志 | motion logger、采样、导出、回放和差异 | `WEB-EQUIV` |
## 10. GUI、VCP、可视化和配置工具
所有桌面界面不要求像素级复制,但必须覆盖其仿真工作流、状态、控制项、配置项和脚本接口。
组件信息架构、命令 pending/ack/result、通信信封、快照字段、事件/高频流、背压、错误/取消、离线恢复、多标签页租约、详细任务和 FE-M0..M5 退出条件见[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)。本节条目必须绑定该文档中的 UI 工作流和协议消息,不能只以页面存在作为验收。
| LinuxCNC 界面/工具 | Web 等价实现 |
|---|---|
| AXIS/Gremlin | 主控制台、程序、预览、3D 视图、状态、MDI、jog、override、报警、HAL 面板 |
| Gmoccapy/Gscreen | 工业面板、主题、消息、宏按钮、偏好、键盘和触控布局 |
| Touchy/QtTouchy | 触控操作、轴/关节 jog、home、MDI、程序控制和软键 |
| QtDragon/QtVCP | screen/widget schema、动态属性、signals/slots、tabs、custom widget、Vismach |
| QtPlasmaC | 等离子流程、材料表、切割参数、探高、arc-ok、暂停/恢复和日志 |
| PyVCP/GladeVCP | XML/descriptor 转 Web Component,全部 widget 属性、pin 和回调 |
| PanelUI/MDRO | 面板、数字输出、override、模式和用户按键 |
| NGCGUI/PyNGCGUI | 子程序参数表单、预览、生成/加载/运行和参数持久化 |
| HALShow/HALScope/HALMeter | pin 树、连接图、波形、触发、缩放、保存和回放 |
| Vismach | WebGL/WebGPU 机床模型、层级、关节、刀具和姿态 |
| 仿真配置编辑器 | 生成兼容的仿真 INI/HAL/工程 manifest;Stepconf/Pncconf 的驱动、板卡、引脚和时序配置排除 |
| Tooledit/Tool Database | 工具表、数据库查询、编辑、导入导出和换刀状态 |
| Image-to-G-code/NGC tools | 浏览器内纯计算插件;文件通过 OPFS/用户导入,不调用宿主程序 |
| 国际化/主题/快捷键 | 语言资源、主题变量、可配置快捷键和触摸交互 |
## 11. API、远程接口和脚本接口
| 接口 | 仿真实现 | 标记 |
|---|---|---|
| NML command/status/error | Worker 内 message bus,保留消息类型、顺序、状态和错误语义 | `SIM-ADAPTER` |
| `linuxcnc.command/stat/error` | TypeScript API + 可选 Pyodide Python binding | `PY-WASM` |
| `hal`/`hal_glib`/`pyhal` | 虚拟 pin/signal/param、监听、周期回调和类型转换 | `PY-WASM` |
| `gstat`/position logger | 状态快照、流式订阅、OPFS 日志和回放 | `WEB-EQUIV` |
| `linuxcncrsh`/远程命令 | 同源 Worker loopback;可选 WebSocket 仅连接当前仿真会话 | `SIM-ADAPTER` |
| `halcmd`/`haltcl`/`halrun` | Web 控制台命令解析;脚本只作用于当前虚拟工程 | `WEB-EQUIV` |
| MQTT/消息发布 | 本地事件总线和可选浏览器 WebSocket mock,不访问真实设备 | `SIM-DEVICE` |
| UI 插件/自定义面板 | manifest、权限、版本、生命周期和 OPFS 资源 | `WEB-EQUIV` |
### 11.1 诊断、开发和运维工具
| LinuxCNC 工具/能力 | 仿真实现 | 标记 |
|---|---|---|
| `linuxcnc_info` | 输出浏览器、WASM、Worker、OPFS、配置、组件和仿真能力报告 | `WEB-EQUIV` |
| `inivar`/INI 检查 | Web 配置查看、类型转换、include 展开和错误定位 | `WEB-EQUIV` |
| `lintini` | Worker 内静态检查全部 INI/HAL/资源引用 | `WEB-EQUIV` |
| `halcompile`/`modcompile` | 构建流水线中编译 `.comp`;Web 编辑器提供语法/接口检查 | `SIM-ADAPTER` |
| `runtests`/测试选择器 | 浏览器内按 manifest 运行 golden、回归和故障测试 | `WEB-EQUIV` |
| 仿真性能诊断 | 提供批处理耗时、Worker 延迟和 UI 帧率;`latency-test` 的内核实时延迟测试排除 | `WEB-EQUIV` |
| `debuglevel`/调试标志 | 虚拟运行时动态日志级别和 OPFS 日志 | `SIM-ADAPTER` |
| `tool_watch`/tool data monitor | 工具、换刀、数据库和快照监视器 | `WEB-EQUIV` |
| `motion-logger`/`halsampler`/`halstreamer` | 采样、导出、回放和差异可视化 | `WEB-EQUIV` |
| `axis-remote`/`schedrmt`/`linuxcnclcd`/`emclcd` | 同源命令/状态通道和虚拟显示终端 | `SIM-ADAPTER` |
| `linuxcncsvr`/远程 NML 服务 | 仅提供同源仿真会话服务;不监听宿主真实端口 | `SIM-ADAPTER` |
| `decode_dmesg`/系统诊断 | 虚拟设备和 WASM 运行时日志;无宿主内核日志 | `REAL-EXCLUDED` |
## 12. OPFS、快照和可重复仿真
| 功能 | 覆盖要求 | 标记 |
|---|---|---|
| 工程目录 | INI/HAL/TBL/VAR/NGC/PY/XML/TCL/CLP/模型/材料/日志全部可保存 | `SIM-ADAPTER` |
| 文件语义 | open/read/write/stat/readdir/rename/unlink/fsync、相对路径、大小写和错误 | `SIM-ADAPTER` |
| 事务 | prepare/commit、临时文件、SHA-256、原子替换、版本索引 | `SIM-ADAPTER` |
| 恢复 | 刷新、崩溃、半提交、版本迁移、损坏文件和回滚 | `SIM-ADAPTER` |
| 快照 | 任务、解释器、TP、运动、HAL、IO、Python、工具、参数和 UI 状态 | `SIM-ADAPTER` |
| 回放 | 从 tick/事件恢复后与连续运行结果一致 | `SIM-CORE` |
| 任务接续账本 | 请求超时、回答/工具结果丢失、重复响应、Worker 重启;按 `operation_id`、prepare/commit 和最后已验证步骤恢复 | `SIM-ADAPTER` |
| 配额 | estimate、persist、配额不足、清理 cache、导出备份 | `WEB-EQUIV` |
| 离线 | Service Worker 缓存 WASM、Pyodide、样例和 UI;无网加载已有工程 | `WEB-EQUIV` |
## 13. 测试覆盖模型
### 13.1 Golden 对照
原生 LinuxCNC 和 WASM 对每个 manifest 项输出:
- 解释器状态、当前行、模态、参数和返回值
- canonical motion、每 tick 位置/速度/加速度/跟随误差
- 轴/关节/姿态、运动学错误和限位状态
- HAL pin/signal/param、线程顺序和组件输出
- 主轴、工具、探针、换刀、冷却、等离子/激光/泡沫 IO
- Python/remap 输出、异常、日志和事件
- UI 命令、报警、状态和快照恢复结果
### 13.2 样本集合
1. `configs/sim/**` 下全部 INI 和关联资源,而不是只抽取 10 个样例。
2. `tests/**` 下全部可运行仿真测试;只依赖真实硬件、内核实时或驱动的测试登记为排除项。
3. `unit_tests/interp`、`unit_tests/tp`、posemath 和 kinematics 单元测试。
4. 每个 G/M/O 代码至少一个正常、边界、错误和暂停/恢复样例。
5. 每个 HAL 组件至少一个 pin 类型、周期、复位和异常样例。
6. 每种机床类型至少一个完整工程、模型、程序、工具表、IO 和故障回放。
7. 每个 Web UI 工作流覆盖首次启动、刷新恢复、离线、导入导出和键盘/触摸路径。
8. 每个可恢复任务至少覆盖一次请求失败、确认丢失、幂等重试和无法对账的阻断路径。
### 13.3 覆盖率门槛
- 覆盖率门槛与发布门禁 G4/G5 同步;来源、依赖、维度和交互闭包以及具体任务上下文和证据包格式见[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)第 2.1、3、7 节。
- `SIM-CORE`、`SIM-ADAPTER`、`SIM-DEVICE`、`WEB-EQUIV`、`PY-WASM`:100% manifest 条目有实现和自动测试。
- 每个条目的正常、边界、错误、暂停/恢复、持久化/离线及适用交互维度均有通过记录;缺失任一维度不得为 `accepted`。
- 关键状态和错误的原生/WASM 对照通过率:100%。
- 浮点误差按算法定义容差,默认机床单位目标 `1e-6`。
- OPFS 已提交数据在刷新、Worker 重启和模拟崩溃后可恢复。
- 任何 manifest 缺失、未测试或静默降级都阻止发布。
- 任何孤儿来源、未闭合依赖、无 oracle、未覆盖交互或未解释的差异都阻止发布。
- 前端通信条目必须同时有消息 schema、LinuxCNC 源程序映射、UI 工作流、错误/取消、背压和离线恢复测试;详细字段和 FE/COM 任务见[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)。
- `REAL-EXCLUDED` 不计入仿真覆盖率,但配置检查必须返回明确 `real_hardware_excluded`。
## 14. 实施顺序
本节的阶段 A-E 只描述功能交付顺序;对应的 M0-M5 里程碑、工作流边界、固定上下文和返工规则见[执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)第 4、6、8 节。
### 阶段 A:全量清单和运行时骨架
- 生成 manifest,锁定所有文档、源码、配置和测试输入。
- 建立单 Worker、固定 tick、OPFS journal、快照和版本化 ABI。
- 建立 Python/Pyodide Worker 和虚拟 `linuxcnc/hal/gstat` 模块。
### 阶段 B:核心解释和运动全覆盖
- 移植全部 G/M/O、remap、参数、坐标系、补偿、TP、motion、全部 kinematics。
- 完成 mill/lathe/gantry/robot/delta/pentapod/foam/plasma/laser/probe 数字孪生。
### 阶段 C:HAL、PLC、IO 和虚拟机床部件
- 生成并编译全部 HAL 组件 manifest。
- 完成 ClassicLadder、虚拟寄存器、主轴/换刀/探针/编码器/stepgen/限位/故障模型。
- 完成 halcmd/haltcl/halui/HALScope/HALShow。
### 阶段 D:全部 Web UI 和配置工具
- 完成 AXIS、Gmoccapy、Touchy、Gscreen、QtVCP、QtDragon、QtPlasmaC、VCP、Vismach、NGCGUI 等仿真操作能力的 Web 等价工作流和仿真配置编辑器。
- 完成工程导入、导出、离线、国际化、主题和插件权限。
### 阶段 E:全量回归和发布
- 全部 configs/tests/golden/浏览器矩阵跑通。
- 生成版本化差异报告、兼容报告、许可证清单和工程迁移工具。
## 15. 明确排除项
以下不是仿真机床功能,因此不实现,也不能通过浏览器 API 绕过:
- 真实电机、驱动器、主轴、刀具、冷却、气路、液压和工件。
- 真实 Mesa/FPGA、并口、GPIO、PCI、串口、USB、Modbus 设备和现场总线。
- PREEMPT_RT、RTAI、Xenomai、内核模块、CPU 绑定、硬实时和物理步进脉冲。
- 真实急停、安全继电器、门锁、限位开关和人员/设备安全认证。
- 任意宿主路径、shell、fork/exec、子进程、设备文件和网络原始套接字。
排除项的仿真等价行为必须由第 6~9 节的数字孪生提供;例如“主轴不到速”要模拟成可配置状态和故障,而不是把主轴驱动标为“不支持”。

View File

@@ -0,0 +1,111 @@
# LinuxCNC Web/WASM 仿真功能与源程序关联目录
**文档状态**:Draft v0.1
**适用基线**:LinuxCNC `2.10.0~pre0`;源码根目录为 `linuxcnc/`
**上层路线**:[技术路线](linuxcnc-web-wasm-technical-roadmap.md)
**覆盖验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**执行治理**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**前端与通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**总控执行**:[移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
本目录把“功能”与 LinuxCNC 的实际源程序、配置、样例和测试绑定。表中的路径是当前仓库中的真实路径;`*`、`**` 通配范围必须由 manifest 生成器展开为逐文件条目,不能把通配符本身当成完成证据。源码文件发生增删、重命名或符号变化时,来源扫描必须更新 manifest 并阻断未关联的提交。
## 1. 关联契约
每个功能条目至少包含以下关联字段:
| 字段 | 要求 |
|---|---|
| `feature_id` | 稳定的功能 ID,不随文件重命名改变 |
| `domain`、`capability` | 功能域和用户可观察能力 |
| `source_files` | 仓库相对路径,展开到具体文件并记录 `source_sha` |
| `source_symbols` | 从 C/C++/C/Python/组件文件生成的符号或入口;无法解析时标记 `blocked` |
| `config_inputs` | `configs/sim/**` 中实际启用该功能的 INI/HAL/NGC/PY/XML/TCL/CLP 文件 |
| `adaptation` | `SIM-CORE`、`SIM-ADAPTER`、`SIM-DEVICE`、`WEB-EQUIV`、`PY-WASM` 或 `REAL-EXCLUDED` |
| `test_sources`、`test_id` | 原生测试、单测、浏览器测试和故障/恢复测试 |
| `oracle` | 原生 golden、规范输出、状态机不变量或明确 Web 等价判定 |
| `status`、`difference` | 实施状态、首个差异位置、容差和差异说明 |
| `protocol_messages`、`state_fields` | 前端命令、事件、快照字段和 LinuxCNC 状态的映射;详细契约见前端与通信设计 |
源文件关联只说明“必须审查和适配的来源”,不表示该文件可直接编译进 WASM。每个条目仍须按[覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)的正常、边界、错误、暂停/恢复、持久化/离线和交互维度验收。
## 2. 核心行为源程序映射
| 功能域 | LinuxCNC 源程序/文件范围 | Web/WASM 处理 | 首批测试来源 |
|---|---|---|---|
| INI、配置和 HAL 初始化 | `linuxcnc/src/libnml/inifile/inifile.cc`、`inifile.hh`、`inivar.cc`;`linuxcnc/src/emc/ini/emcIniFile.cc`、`iniaxis.cc`、`inijoint.cc`、`inispindle.cc`、`initraj.cc`、`inihal.cc` | 复用解析规则;路径、include、变量和错误映射到 `/workspace` | `linuxcnc/configs/sim/**` 及其关联启动/配置测试 |
| RS-274/G-code 读取 | `linuxcnc/src/emc/rs274ngc/interp_read.cc`、`interp_check.cc`、`interp_setup.cc`、`interp_base.cc`、`rs274ngc.hh` | 优先编译核心;输入和诊断通过稳定 ABI 暴露 | `linuxcnc/unit_tests/interp/**`;`linuxcnc/tests/interp/**` |
| G/M/O 执行和 canonical 输出 | `linuxcnc/src/emc/rs274ngc/interp_execute.cc`、`interp_convert.cc`、`interp_cycles.cc`、`interp_o_word.cc`、`interp_queue.cc`、`interp_write.cc`、`canonmodule.cc` | 复用解释器状态和 canonical;队列接入 `sim_task` | `linuxcnc/tests/m70-m73/**`、`linuxcnc/tests/mdi-*`、全部 G/M/O manifest |
| Remap、Python 和命名参数 | `linuxcnc/src/emc/rs274ngc/interp_remap.cc`、`interp_python.cc`、`interp_namedparams.cc`、`pyparamclass.cc`、`pyinterp1.cc`、`pyarrays.cc`;`linuxcnc/src/emc/pythonplugin/python_plugin.cc`、`python_plugin.hh` | Pyodide/CPython-WASM 虚拟模块;禁止宿主进程、线程、设备和任意路径 | `linuxcnc/configs/sim/**/python/**`、`tests/**/remap.py`、`unit_tests/interp/**` |
| 模态、单位和坐标状态 | `linuxcnc/src/emc/rs274ngc/modal_state.cc`、`modal_state.hh`、`interp_parameter_def.hh`、`units.h`、`interp_inverse.cc` | 状态快照版本化;与原生模态 golden 对照 | `linuxcnc/unit_tests/interp/**`;坐标/单位仿真配置 |
| 轨迹规划和队列 | `linuxcnc/src/emc/tp/tp.c`、`tp.h`、`tp_types.h`、`tc.c`、`tcq.c`、`tcq.h`、`blendmath.c`、`spherical_arc.c`、`spherical_arc.h` | 固定 tick 驱动;保留前瞻、blend、exact path/stop 和容差 | `linuxcnc/unit_tests/tp/**`;`linuxcnc/tests/motion-logger/**` |
| Task、canonical 和 NML | `linuxcnc/src/emc/task/emctaskmain.cc`、`taskclass.cc`、`taskintf.cc`、`emccanon.cc`;`linuxcnc/src/emc/nml_intf/emc.cc`、`emc.hh`、`emc_nml.hh`、`interpl.cc`;`linuxcnc/src/libnml/nml/nml.cc`、`stat_msg.cc`、`cmd_msg.cc` | 单 Worker `sim_task` 和 message bus;删除 fork/进程边界但保留状态、命令和错误语义 | `linuxcnc/tests/mdi-*`、`linuxcnc/tests/abort/**`、`linuxcnc/tests/linuxcncrsh-*` |
| NML/CMS/共享内存边界 | `linuxcnc/src/libnml/cms/**`、`linuxcnc/src/libnml/buffer/**`、`linuxcnc/src/libnml/nml/**` | 进程间通信改为 Worker message bus;共享内存改为进程内结构和版本化快照 | NML、linuxcncrsh、状态/命令 golden |
| RTAPI、OS 接口和时钟 | `linuxcnc/src/rtapi/**`;`linuxcnc/src/libnml/os_intf/timer.cc`、`_timer.c`、`sem.cc`、`shm.cc`;`linuxcnc/src/emc/linuxcnc.h` | 固定逻辑时钟、虚拟同步原语和受控内存;内核实时、CPU 绑定和设备访问标记为 `REAL-EXCLUDED` | `linuxcnc/tests/lowlevel/**`、`linuxcnc/tests/rtapi-shmem/**`(可移植部分) |
| Motion、回零和限位 | `linuxcnc/src/emc/motion/motion.c`、`control.c`、`command.c`、`homing.c`、`emcmotglb.c`、`emcmotutil.c`、`simple_tp.c`、`motion_struct.h`、`homing.h` | `sim_motion` 状态机;虚拟编码器、开关、跟随误差和故障注入 | `linuxcnc/tests/hard-limits/**`、`linuxcnc/tests/abort/**`、`linuxcnc/tests/limit3/**` |
| Kinematics | `linuxcnc/src/emc/kinematics/*kins.c`、`*kins.cc`、`kins_util.c`、`genserfuncs.c`、`trtfuncs.c`、`switchkins.c`;关键文件包括 `trivkins.c`、`5axiskins.c`、`genhexkins.c`、`genserkins.c`、`lineardeltakins.c`、`rotarydeltakins.c`、`corexykins.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c` | 静态 WASM 模块和 manifest 注册;不使用宿主动态库 | `linuxcnc/configs/sim/axis/gantry/**`、`linuxcnc/configs/sim/axis/foam/**`、`linuxcnc/configs/sim/axis/ldelta*`、机器人样例;运动学单测 |
| Posemath 和几何 | `linuxcnc/src/libnml/posemath/posemath.cc`、`posemath.h`、`gomath.c`、`gomath.h`、`sincos.c`、`gotypes.h` | 原样编译或等价 ABI;固定浮点编译选项 | posemath 相关单测、TP/kinematics golden |
| Tooldata、工具表和数据库 | `linuxcnc/src/emc/tooldata/tooldata_common.cc`、`tooldata_nml.cc`、`tooldata_db.cc`、`tooldata_mmap.cc`、`tool_mmap_read.cc`、`tooldata.hh` | 内存工具服务 + OPFS 事务;兼容 pocket、offset、wear 和数据库迁移 | `linuxcnc/tests/mdi-queue/**`、`linuxcnc/configs/sim/**/*.tbl` |
| IO、换刀、主轴和冷却 | `linuxcnc/src/emc/iotask/ioControl.cc`、`ioControl_v2.cc`;`linuxcnc/src/emc/tooldata/**` | 虚拟 IO、主轴、换刀、探针和故障模型;真实协议排除 | `linuxcnc/configs/sim/axis/iocontrolv2/**`、`linuxcnc/configs/sim/axis/lathe_multispindle/**`、换刀测试 |
## 3. HAL、PLC 和数字孪生映射
| 功能域 | LinuxCNC 源程序/文件范围 | Web/WASM 处理 | 首批测试来源 |
|---|---|---|---|
| HAL 核心对象和生命周期 | `linuxcnc/src/hal/hal_lib.c`、`hal.h`、`hal_priv.h`、`halmodule.cc` | pin/signal/param/component 图和生命周期;固定线程顺序 | `linuxcnc/tests/hal-*`、`linuxcnc/tests/basic/**` |
| halcmd/halrun/Scope | `linuxcnc/src/hal/utils/halcmd.c`、`halcmd_commands.cc`、`halcmd_main.c`、`halsh.c`、`scope.c`、`scope_rt.c`、`meter.c` | Web 命令控制台、波形采样和回放;真实 PCI 工具排除 | `linuxcnc/tests/halrun-*`、`linuxcnc/tests/threads.*`、`linuxcnc/tests/mux/**` |
| `.comp` 编译和组件 ABI | `linuxcnc/src/hal/utils/halcompile.g`;`linuxcnc/src/hal/components/*.comp`、`*.c` | 构建期静态组件 manifest;每个 `.comp` 展开为独立条目 | `linuxcnc/tests/symbols.*`、组件同名测试和 `configs/sim/**` |
| 虚拟轴/主轴/编码器/回零 | `linuxcnc/src/hal/components/sim_axis_hardware.comp`、`sim_spindle.comp`、`sim_encoder.c`、`sim_home_switch.comp`、`sim_parport.comp`、`sim_matrix_kb.comp` | 通用数字孪生;输入、延迟、噪声和故障可录制 | `configs/sim/**`、`tests/hard-limits/**`、`tests/edge/**` |
| 运动和控制组件 | `linuxcnc/src/hal/components/stepgen.c`、`encoder.c`、`encoder_ratio.c`、`pid.c`、`simple_tp.comp`、`gantry.comp`、`corexy_by_hal.comp`、`spindle.comp`、`spindle_monitor.comp` | 固定周期组件和类型转换;与 HAL golden 对照 | `linuxcnc/tests/limit3/**`、`linuxcnc/tests/biquad/**`、组件测试 |
| 探针、等离子和激光 | `linuxcnc/src/hal/components/sphereprobe.comp`、`plasmac.comp`、`laserpower.comp`、`homecomp.comp`、`thc.comp`、`thcud.comp`、`ohmic.comp` | 虚拟触发、arc-ok、torch、功率和超时故障模型 | `configs/sim/axis/laser/**`、probe/PlasmaC 样例 |
| ClassicLadder | `linuxcnc/src/hal/classicladder/*.c`、`*.h`、`projects_examples/*.clp`,重点为 `classicladder.c`、`calc.c`、`calc_sequential.c`、`files.c`、`files_project.c`、`protocol_modbus_master.c` | PLC 扫描、梯形图、顺序控制和虚拟寄存器;真实串口/TCP Modbus 排除 | ClassicLadder 工程样例、`configs/sim/**` 中 `.clp` |
## 4. Python、UI 和可视化映射
| 功能域 | LinuxCNC 源程序/文件范围 | Web/WASM 处理 | 首批测试来源 |
|---|---|---|---|
| Python HAL/LinuxCNC API | `linuxcnc/lib/python/hal.py`、`hal_glib.py`、`pyhal.py`、`tooldb.py`、`linuxcnc_util.py`、`rs274/**` | 虚拟模块和受控回调;API、异常和返回值对照 | `configs/sim/**/**/*.py`、Python remap/filter tests |
| AXIS/Gremlin 路径显示 | `linuxcnc/src/emc/usr_intf/axis/scripts/axis.py`、`axis-remote.py`、`mdi.py`、`hal_manualtoolchange.py`;`gremlin/gremlin.py`、`qt5_graphics.py`;`linuxcnc/lib/python/rs274/glcanon.py` | Web 控制台、3D 轨迹和程序工作流;不复制 GTK 窗口 | AXIS 仿真配置、UI tests、路径 golden |
| Gmoccapy/Gscreen/Touchy | `linuxcnc/src/emc/usr_intf/gmoccapy/*.py`、`*.glade`;`gscreen/*.py`、`*.glade`;`touchy/*.py`、`*.glade` | 状态、触控、jog、MDI、报警和偏好 Web 等价 | `configs/sim/**` 面板样例、浏览器 E2E |
| QtVCP/PyVCP/GladeVCP | `linuxcnc/src/emc/usr_intf/qtvcp/qtvcp.py`;`linuxcnc/lib/python/qtvcp/**`、`gladevcp/**`、`pyvcp_widgets.py`、`pyngcgui.py`、`vcpparse.py` | XML/descriptor 转 Web Component schema;保留 pin、属性、回调和权限 | `configs/sim/axis/gladevcp/**`、`halui_pyvcp/**`、QtVCP samples |
| Vismach/机床模型 | `linuxcnc/src/hal/user_comps/vismach/*.py`、`linuxcnc/lib/python/vismach.py` | Three.js/WebGPU 模型、姿态和刀具层 | vismach/5-axis/机器人仿真配置 |
| PlasmaC、NGCGUI 和工具界面 | `linuxcnc/src/emc/usr_intf/qtplasmac/*.py`;`linuxcnc/lib/python/plasmac/**`、`pyngcgui.py` | 等离子流程、材料表、参数表单和预览 Web 等价 | `configs/sim/axis/laser/**`、相关 qtplasmac/NGCGUI 样例 |
Stepconf/Pncconf 的真实板卡、引脚和实时配置源文件仍需进入 manifest,但标记为 `REAL-EXCLUDED`;其中可生成纯仿真 INI/HAL 的部分必须拆出并按 `WEB-EQUIV` 验收,不能整体排除。
## 5. 仿真配置与测试来源映射
配置文件不是“示例附件”,而是功能来源。manifest 生成器必须递归扫描 `linuxcnc/configs/sim/**`,为每个入口 INI 建立资源图,至少关联其 HAL、TBL、VAR、NGC、PY、XML、TCL、UI、模型和材料文件。当前重点入口包括:
| 仿真能力 | 配置入口/资源范围 |
|---|---|
| 最小三轴、坐标和 INI include | `linuxcnc/configs/sim/axis/minimal_xyz.ini`、`axis/ini_with_includes/**` |
| Gantry、额外关节、锁定转台 | `axis/gantry/**`、`axis/extrajoints/**`、`axis/ja_tests/**` |
| 车床、Fanuc Y、多主轴 | `axis/lathe.ini`、`axis/lathe-fanucy/**`、`axis/lathe_multispindle/**` |
| Foam/Delta/机器人/多轴 | `axis/foam/**`、`axis/ldelta*`、`**/*kins*`、`axis/*gui*` |
| Laser/Plasma/探针 | `axis/laser/**`、`axis/gladevcp/probe.*`、所有包含 `plasmac`/`probe` 的 sim 入口 |
| IOControl v2、换刀和 O-word | `axis/iocontrolv2/**`、所有 `toolchange`、`oword`、`m6` 入口 |
| HAL UI、Scope、VCP 和面板 | `axis/halui_pyvcp/**`、`axis/gladevcp/**`、所有 `*.halshow`、`*.halscope`、`*.xml` |
测试来源按功能反向绑定:`linuxcnc/tests/**` 的每个可运行测试、`linuxcnc/unit_tests/**` 的每个测试目标和配置入口都必须至少出现在一个 manifest 条目的 `test_sources` 或 `config_inputs` 中。只依赖真实硬件、实时内核或宿主驱动的测试登记为 `REAL-EXCLUDED`,并写明对应的仿真替代测试。
### 5.1 构建和链接来源
构建文件也是源程序关联的一部分。以下文件必须扫描并绑定到对应的编译目标、组件 manifest 和排除理由:
| 构建来源 | 关联要求 |
|---|---|
| `linuxcnc/meson.build`、`linuxcnc/src/meson.build` | 记录原生目标、子目录和源文件集合;WASM 目标以独立 CMake/Emscripten manifest 对照 |
| `linuxcnc/src/**/meson.build`、`linuxcnc/src/**/Submakefile` | 记录每个库、模块、组件和测试目标的实际输入;源文件未被任何目标引用时标记为孤儿候选 |
| `linuxcnc/src/hal/components/*.comp` 的生成规则 | 记录 `.comp` 到生成 C/ABI 的映射和编译参数;生成文件不能替代原始 `.comp` 来源 |
| `linuxcnc/unit_tests/**/meson.build`、`linuxcnc/tests/**/test.sh` | 记录测试入口、预期结果和需要的仿真资源;测试脚本失败或不可移植时必须登记替代测试 |
## 6. 生成、审查和变更规则
1. M0 扫描源码、文档、配置和测试,生成带 `source_sha` 的初始索引;扫描器版本和排除规则写入 context lock。
2. 领域负责人把索引项归并为稳定 `feature_id`,保留一对多和多对一关系,不删除原始来源引用。
3. C/C++/C/Python/组件符号由固定版本的解析器或 `compile_commands.json` 生成;解析失败的项为 `blocked`,不得手工填充“已覆盖”。
4. 文件新增、删除、重命名、构建清单变化或配置入口变化都必须触发 manifest diff;差异没有对应任务和测试时 CI 失败。
5. 发布使用锁定的 source map、coverage manifest、golden manifest 和 context lock;source map 与 coverage manifest 的数量、哈希和反向索引必须一致。
source map 是移植的导航和审计入口,不以目录级“已处理”替代文件级和功能级证据。只有源文件关联、实现、测试、差异和排除理由全部闭合,功能条目才允许进入 `accepted`。

View File

@@ -0,0 +1,473 @@
# LinuxCNC 纯 Web/WASM 仿真机床技术路线(第一版)
**文档状态**:Draft v0.2
**目标产品**:浏览器内、离线可运行的 LinuxCNC 仿真机床
**代码基线**:本仓库当前 LinuxCNC `2.10.0~pre0`(以 `linuxcnc/VERSION` 为准)
**范围**:只做仿真,不连接真实机床,不通过网络控制硬件
**兼容目标**:以 LinuxCNC 仿真模式为行为基线,覆盖全部仿真功能并给出浏览器内等价实现
**执行治理**:[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)
**逐项验收**:[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)
**源码关联**:[仿真功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)
**前端与通信**:[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)
**总控执行**:[仿真移植总控执行手册](linuxcnc-web-wasm-integration-playbook.md)
本路线定义架构、范围、风险和阶段 A-E;任务上下文预算、提交格式、断网接续、里程碑退出条件和发布门禁统一由[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)约束;功能到 LinuxCNC 实际源文件、配置和测试的关联以[源程序关联目录](linuxcnc-web-wasm-source-map.md)为入口;浏览器 UI、Worker 协议、状态契约、前端任务和 FE-M0..M5 里程碑以[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)为准;端到端依赖、任务目录、场景验收和当前实施状态以[总控执行手册](linuxcnc-web-wasm-integration-playbook.md)为准。
## 1. 摘要
本项目不是把完整 LinuxCNC 的 Linux 进程、实时内核和桌面 GUI 原样塞进浏览器,而是将 LinuxCNC 面向仿真用户的全部行为拆成可移植核心,并为浏览器补齐进程、文件系统、时钟、HAL、虚拟 I/O、GUI、Python 和配置运行时。真实机床、真实硬件驱动和安全控制链路全部排除;原本由硬件产生的可观察信号,在浏览器中由可配置的数字孪生设备产生。
最终形态如下:
```text
Web UI(React + TypeScript)
├─ G-code 编辑器、MDI、Jog、倍率、报警、配置
├─ Three.js 机床/刀具/轨迹/毛坯显示
└─ OPFS 工程、配置、工具表、日志和快照
│ postMessage
▼
Dedicated Web Worker
├─ Emscripten WASM LinuxCNC 仿真核心
├─ Pyodide/Python 仿真运行时(remap、过滤器、VCP 脚本)
├─ POSIX 兼容工作区(WasmFS/适配层)
├─ 虚拟时钟、任务/运动/I/O 调度
├─ 虚拟 HAL 和全部仿真组件
└─ OPFS 同步访问、事务日志和恢复
```
浏览器主线程只负责界面和渲染;解释、轨迹计算、配置读取、HAL 周期、OPFS 同步访问和 Python 调用调度都在 Worker 侧完成。仿真默认单线程、串行、确定性;Python remap/过滤器可以在同一仿真 Worker 的协作运行时中执行,也可以拆成受控 Python Worker,但不能引入不可复现的后台线程。WASM pthread 只作为性能优化构建,不能改变仿真语义。
## 2. 目标与边界
### 2.1 必须达到的目标
1. 在 Chromium、Firefox、Safari 的支持范围内运行纯仿真;主流程不依赖 Linux、Node.js 或服务器 API。
2. 能加载 LinuxCNC 仿真配置、G-code、子程序、宏、工具表、INI 和 HAL 配置,并在浏览器内得到明确的兼容结果。
3. 对 LinuxCNC 的全部仿真功能做可重复回归:解释器、轨迹规划、全部内置运动学、模态状态、坐标系、补偿、宏、remap、HAL、ClassicLadder、虚拟工具/主轴/探针/换刀/限位、车床/等离子/激光/泡沫/机器人等仿真配置,以及所有仿真操作界面能力。
4. 使用 OPFS 持久化工程工作区,支持关闭页面后恢复、崩溃恢复、版本迁移、导入导出和配额诊断。
5. 真实硬件功能明确排除;除真实硬件访问外,不允许以“不支持”跳过任何 LinuxCNC 仿真功能。行为有差异的功能必须有机器可读的兼容状态和用户可见的差异说明。
6. 通过录制/回放的确定性仿真,能够对照原生 LinuxCNC 生成差异报告。
### 2.2 明确不做的事情
- 不连接串口、USB、PCI、并口、Mesa、Ethernet、GPIO 或任何真实硬件。
- 不提供真实机床安全保证,不替代急停、限位、安全继电器或认证控制器。
- 不执行任意 `fork`、`exec`、shell 命令、系统脚本或宿主机路径操作。
- 不连接真实机床或真实硬件;硬件相关 LinuxCNC 功能只实现仿真数字孪生和故障模型。
- Python 解释器属于仿真必选运行时;使用 Worker 内的 Pyodide/CPython WASM,并以虚拟文件、虚拟时钟和虚拟 I/O 替代宿主能力。
- 不承诺浏览器的墙钟、Worker 调度或后台标签页具有 Linux PREEMPT_RT 的实时保证。
### 2.3 “仿真全覆盖”的定义
“仿真全覆盖”不是承诺每一行 LinuxCNC 源码都可直接编译,而是对 LinuxCNC 发布版中所有可由仿真机床观察、配置、调用或回放的行为逐项建立以下状态:
- **复用**:原生 C/C++ 核心可在 WASM 编译,行为直接复用。
- **等价实现**:Linux/进程/硬件依赖不可复用,但在浏览器内提供同等仿真语义。
- **兼容适配**:保留配置或 API 形状,内部转换为 Web 运行时模型。
- **仿真器**:用通用虚拟机床部件、虚拟 I/O 或虚拟时钟复现仿真配置依赖的行为,不复刻真实设备驱动和现场协议。
- **Web 等价**:用 Web UI、Web Worker、WebGL/WebGPU 或浏览器 API 提供同等用户能力。
- **排除**:真实硬件访问、真实实时内核、安全认证和物理机床输出;排除项不计入仿真覆盖率,但必须在兼容报告中明确标注。
每个功能在 [仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md) 中记录:来源文档/源码、输入、输出、错误码、仿真替代方案、参考测试、当前状态和差异说明。覆盖率只统计 `SIM-*`、`WEB-EQUIV` 和 `PY-WASM` 项,目标是 100%;`REAL-EXCLUDED` 单独统计,不得伪装为已支持。
## 3. 基于当前仓库的复用分析
当前仓库的 `linuxcnc/configs/sim` 已提供大量无特殊硬件的仿真配置,是移植和回归的首批样本。仓库中的核心目录与路线映射如下:
本节只给出架构级目录映射;逐文件范围、功能入口、配置资源和测试来源见[仿真功能与源程序关联目录](linuxcnc-web-wasm-source-map.md)。manifest 生成器必须把目录和通配范围展开为带 `source_sha` 的具体文件,禁止用目录级状态替代文件级证据。
| 当前源码 | Web/WASM 处理 | 说明 |
|---|---|---|
| `src/emc/rs274ngc` | 优先原样编译并做 ABI 封装 | G-code 解释、宏、循环、模态和 canonical 输出 |
| `src/emc/tp` | 原样复用,接入确定性周期 | 前瞻、速度规划、圆弧、球面弧和轨迹队列 |
| `src/libnml/posemath` | 原样复用 | 位姿、矩阵、坐标变换和几何计算 |
| `src/emc/kinematics` | 原样复用/按配置选择 | trivkins、5 轴、机器人、六足等运动学 |
| `src/libnml/inifile` | 编译或等价解析 | 兼容 INI;前端另提供 JSON 配置视图 |
| `src/emc/motion` | 迁移到 `sim_motion` | 保留状态机和运动语义,去除设备/共享内存边界 |
| `src/emc/task` | 拆分为单进程 `sim_task` | 删除 fork、信号、NML 服务和外部命令 |
| `src/emc/iotask` | 迁移为虚拟 I/O 任务 | 主轴、冷却、换刀、探针、限位和消息 |
| `src/hal` | 迁移 HAL 数据模型和调度 | pin、signal、param、thread、function、组件生命周期 |
| `src/hal/components` | 全量移植/重写 | 全部核心组件、仿真组件、测试组件和用户组件模板进入组件 manifest |
| `src/hal/drivers` | 不移植 | 真实设备驱动整体排除;仿真配置所需行为由 `sim_*` 和通用虚拟机床组件提供 |
| `src/emc/usr_intf/*` | 全部重写为 Web UI | 不移植 GTK、Qt、Tk、OpenGL 桌面窗口 |
| Python plugin/remap | Pyodide/CPython WASM + 虚拟宿主 API | 兼容解释器、remap、过滤器、VCP 脚本和 Python HAL API;禁止宿主进程/线程/设备访问 |
当前 [meson.build](../linuxcnc/meson.build) 依赖 Boost.Python、Python 2 和 `libdl` 等宿主环境;WASM 工程使用独立 CMake/Emscripten 目标,避免破坏原生构建。
Python 运行时采用 Pyodide/CPython-WASM,放在 Worker 中执行 remap、过滤器、VCP 回调、工具数据库脚本和 Python HAL API;LinuxCNC 的 `linuxcnc`、`hal`、`gstat` 等模块由仿真桥提供。Pyodide 支持在 Web Worker 中运行,但浏览器环境不提供可直接复用的宿主子进程、线程、原始套接字和设备访问,因此这些调用必须映射为虚拟文件、虚拟时钟、虚拟事件或明确的 `real_hardware_excluded` 错误。[Pyodide Web Worker](https://pyodide.org/en/latest/usage/webworker.html)、[Pyodide WASM constraints](https://pyodide.org/en/latest/usage/wasm-constraints.html)
## 4. 总体运行时架构
### 4.1 Worker 分层
```text
sim.worker.ts
├─ CommandRouter 命令序列、取消、错误和权限边界
├─ TaskLedger operation_id、步骤账本、幂等重试和回答/工具失败接续
├─ WorkspaceRuntime OPFS、工作区挂载、日志和恢复
├─ WasmRuntime Emscripten 模块生命周期和 C ABI
├─ LinuxCncCompat INI/HAL/NML/任务/IO 兼容层
├─ SimScheduler 固定步长、倍速、单步、暂停和回放
└─ SnapshotPublisher 二进制状态快照和事件流
```
主线程通过结构化消息发送低频控制命令;状态使用固定二进制 schema 或可选 JSON 调试模式。所有命令有单调递增 `seq` 和稳定 `operation_id`,所有快照有 `simulation_tick` 和 `schema_version`。任务账本在执行前写 prepare、验证后写 commit;回答或工具确认丢失时先对账,不盲目重放。
通信信封、握手/能力协商、命令族、快照字段、事件与高频流、背压、错误/取消、租约、离线接续和 UI 工作流的详细契约见[前端与 LinuxCNC 通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)。
### 4.2 WASM 边界
WASM 核心只暴露稳定 C ABI,不把 C++ 类布局、指针或 STL 容器暴露给 TypeScript:
```c
int sim_create(const uint8_t *config, uint32_t config_len);
int sim_load_workspace(const char *root_path);
int sim_command(const uint8_t *cmd, uint32_t cmd_len);
int sim_step(uint32_t cycles);
uint32_t sim_read_snapshot(uint8_t *out, uint32_t capacity);
uint32_t sim_read_event(uint8_t *out, uint32_t capacity);
int sim_export_state(uint8_t *out, uint32_t capacity);
int sim_import_state(const uint8_t *data, uint32_t len);
void sim_destroy(void);
```
建议使用命令和事件的版本化二进制结构;JSON 只作为调试、日志和外部工程文件格式。
### 4.3 仿真时钟
LinuxCNC 的实时周期在浏览器中改写为确定性仿真时钟:
1. 逻辑周期默认 1 ms,允许配置为 0.25/0.5/1/2/5 ms。
2. `sim_step(n)` 批量推进 n 个周期,不读取墙钟决定运动结果。
3. Worker 每次运行有限批次,例如 5~20 ms 逻辑时间,然后发布快照并让出事件循环。
4. `1x` 模式按墙钟节流,快速模式只按计算能力推进,单步模式一次推进一个周期或一个解释块。
5. 快照频率与仿真周期解耦,默认 20~60 Hz;轨迹点可按采样策略压缩。
6. 暂停、复位、急停、断点和回放都基于 tick,不依赖 `setInterval(1)` 的准确性。
## 5. OPFS 工程持久化方案
### 5.1 选择原则
OPFS 是按源隔离、浏览器管理、默认不向用户可见的持久化文件系统;需要安全上下文(HTTPS),并可在 Worker 使用。其同步访问句柄只能用于 Worker,适合承接 LinuxCNC 大量同步 `fopen/read/write` 语义。[MDN OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)
OPFS 受浏览器配额和存储淘汰策略约束,因此不能假设无限磁盘;应用启动时调用 `navigator.storage.estimate()`,重要工程可请求 `navigator.storage.persist()`,并提供显式导出为 ZIP/目录的备份能力。[MDN Storage Quotas](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria)
### 5.2 目录布局
```text
opfs:/linuxcnc-web/
format.json # 工作区格式版本和应用版本
projects/
<project-id>/
project.json # 工程元数据、机床类型、单位、入口 INI
config/
*.ini *.hal *.tbl *.var *.ngc
subroutines/ # O-word、M-code 和用户宏
components/ # 允许的静态 HAL 组件配置
programs/ # 用户 G-code
snapshots/ # 可恢复仿真状态
logs/ # 事件、报警、轨迹差异和崩溃日志
journal/ # 写前日志、提交标记和恢复信息
cache/ # 可删除的预解析/轨迹缓存
samples/ # 随应用发布的 configs/sim 样本
migrations/ # 格式迁移标记
```
路径必须是 POSIX 风格、大小写敏感且经过规范化;禁止 `..`、绝对宿主路径和符号链接语义。LinuxCNC 看到的是 `/workspace` 下的虚拟工作区,而不是浏览器真实路径。
### 5.3 两层文件系统模型
为了兼容同步 C/C++ 文件访问,不让 LinuxCNC 核心直接调用异步 DOM API,采用两层模型:
```text
WASM 进程内工作树(同步、低延迟)
└─ WasmFS/MEMFS 或定制 POSIX 适配层
▲ 读写同步 / 批量刷新
▼
Worker OPFS 适配器(FileSystemSyncAccessHandle)
└─ OPFS 持久文件、journal、快照、配额和恢复
```
实现路线分两步:
1. **第一阶段**:使用 Emscripten WasmFS/虚拟文件系统,Worker 启动时加载工作区索引和热点文件;保存时通过 OPFS 适配器批量写入。WasmFS 是稳定但仍非全部旧 FS API 等价的方案,需要针对 LinuxCNC 使用到的文件调用做测试。[Emscripten File System API](https://emscripten.org/docs/api_reference/Filesystem-API.html)
2. **第二阶段**:为 `open/read/write/stat/readdir/rename/unlink/fsync` 建立最小 POSIX 适配层,普通文件通过 OPFS 同步句柄按块读写,目录和元数据通过索引维护;不把 OPFS 文件句柄暴露给 WASM。
OPFS 的 `createSyncAccessHandle()` 本身是异步获取,获取完成后 `read/write/truncate/flush/close` 可同步调用;因此所有句柄建立、恢复和关闭都由 Worker 的 `WorkspaceRuntime` 串行管理。[MDN OPFS Worker Access](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system#manipulating_the_opfs_from_a_web_worker)
### 5.4 事务、一致性和恢复
每次工程写入按以下顺序执行:
1. 生成 `journal/<seq>.prepare`,记录目标路径、旧版本、内容哈希和新版本。
2. 写入临时文件 `.<name>.tmp.<seq>`,分块写入并 `flush()`。
3. 校验长度和 SHA-256,写入 `journal/<seq>.commit`。
4. 原子替换目标文件,更新 `project.json` 的索引版本。
5. 删除临时文件和已完成日志。
启动恢复时扫描 `prepare/commit`:只有 commit 且校验通过的事务才生效;未完成事务回滚到旧版本。快照使用“完整基线 + 增量事件”策略,定期合并,避免每个 tick 写 OPFS。
### 5.5 数据策略
| 数据 | 存储 | 写入策略 |
|---|---|---|
| 当前工程配置 | OPFS | 显式保存 + 防抖自动保存 |
| G-code/宏/子程序 | OPFS | 编辑器保存时事务提交 |
| 工具表/参数/变量 | OPFS | 运行关键点和退出前提交 |
| 仿真快照 | OPFS | 用户手动/周期性 checkpoint |
| 事件和报警日志 | OPFS | 批量追加,达到大小后滚动 |
| 轨迹缓存 | OPFS `cache/` | 可删除,哈希命中才复用 |
| UI 偏好 | IndexedDB 或 OPFS 小文件 | 与工程数据分离 |
用户可见文件使用浏览器 File System Access API 导入/导出;OPFS 本身不承诺与用户文件系统一一对应,不能把 OPFS 路径显示成真实磁盘路径。
## 6. LinuxCNC 全功能对照路线
以下是功能域摘要;完整条目、代码清单、组件清单、配置样本和测试要求以[仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md)为准。每一项都必须进入自动化兼容矩阵;“等价”表示在纯仿真语义下达到相同用户可见结果。开发阶段 P0/P1/P2 只表示交付顺序,不表示可以永久缺失功能。
本节表格中的“全覆盖”是目标范围标签,不是当前实现状态。只有覆盖矩阵完成来源、依赖、测试维度、交互路径和原生/WASM oracle 闭包,并达到 `accepted`,才可在兼容报告中声明该功能已完成。
### 6.1 启动、配置和文件
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| INI 文件、include、变量替换 | 虚拟工作区解析;路径限制和错误码保持一致 | P0 |
| HAL 文件、`loadrt/addf/setp/net` | 解析器 + 静态组件注册 + pin/signal 图构建 | P0 |
| 工具表 `.tbl` | 兼容解析、编辑、校验和持久化 | P0 |
| 参数/变量文件 | 进程内参数区 + OPFS 提交 | P0 |
| G-code 主程序/子程序 | 工作区相对路径解析、递归深度限制 | P0 |
| M-code 文件 | 白名单虚拟动作;禁止外部程序 | P0 |
| 配置选择器 | Web 工程选择器和 `configs/sim` 样本导入 | P0 |
| 环境变量、宿主路径、权限 | 映射为受控 workspace 变量;不暴露宿主环境 | P1 |
| LinuxCNC 启动/退出生命周期 | `sim_create/load/start/stop/destroy` 状态机 | P0 |
### 6.2 G-code 解释器和程序执行
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| RS-274 基础 G/M 代码 | 复用 `rs274ngc`,canonical 输出接 `sim_task` | P0 |
| O-word、宏、命名参数、表达式 | 原生核心优先;禁止未审计宿主回调 | P0 |
| 子程序、循环、条件、递归 | 虚拟工作区路径和资源限额 | P0 |
| 模态组、单位、平面、绝对/增量 | 对照原生状态快照 | P0 |
| G54-G59、G52、G92、旋转/缩放 | 复用解释器和 posemath,逐项差分测试 | P0 |
| 刀具长度/半径补偿 | 复用解释器/TP,覆盖 mill、lathe、5 轴和数字孪生设备 | 全覆盖 |
| 固定循环、螺纹、同步运动 | 按 canonical 输出逐项验收 | 全覆盖 |
| Remap | C++ ABI + Pyodide Python 运行时,覆盖 prolog/epilog、NGC、Python 和队列语义 | 全覆盖 |
| MDI、单步、断点、队列清空 | Worker 命令状态机 | P0 |
| 程序暂停/恢复/中止 | tick 和解释器状态快照恢复 | P0 |
| 语法错误、运行时错误、报警 | 统一错误码 + 行号 + 来源文件 | P0 |
### 6.3 轨迹规划、运动学和坐标
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| `tp` 前瞻、速度/加速度、拐角融合 | 原样复用,固定 dt 驱动 | P0 |
| 直线、圆弧、球面弧、NURBS | 复用 TP/几何库,轨迹容差对表 | 全覆盖 |
| 关节/笛卡尔坐标 | `sim_motion` 状态模型 | P0 |
| trivkins | 默认机床 | P0 |
| 全部内置及示例 kinematics | 编译为静态模块,由配置选择并建立姿态/限位 golden | 全覆盖 |
| 用户运动学模块 | 构建期静态 WASM ABI;以 manifest 注册,不允许宿主动态库 | 全覆盖 |
| 软限位、硬限位、回零 | 虚拟开关和确定性限位模型 | P0 |
| 外部偏置、跟随误差、螺距补偿 | 进程内仿真参数和测试向量 | 全覆盖 |
| 反向时运动学/姿态限制 | 与原生 kinematics 对照 | 全覆盖 |
### 6.4 HAL 和仿真 I/O
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| pin、signal、parameter、component | 内存图模型,名称和类型保持兼容 | P0 |
| HAL thread/function/addf | 固定周期调度器中的有序函数列表 | P0 |
| 全部逻辑、数学、滤波、PID、计数器和测试组件 | `.comp` 转静态 WASM 组件并由 manifest 验收 | 全覆盖 |
| `sim_axis_hardware`、`sim_spindle`、`sim_encoder` | 直接作为虚拟设备模型 | P0 |
| 探针、限位、回零、换刀 | 可脚本化虚拟 I/O,状态可录制 | P0 |
| 主轴加减速、定向、齿轮/同步 | 虚拟主轴模型 + TP 事件 + 故障注入 | 全覆盖 |
| ClassicLadder | 完整解析/执行;UI 用 Web 梯形图视图 | 全覆盖 |
| 用户 `.comp` | 构建期组件 ABI 和 manifest;无宿主动态库 | 全覆盖 |
| 真实 HAL 驱动 | 不编译、不模拟具体设备协议;仅识别配置并返回 `real_hardware_excluded` | 排除 |
| HAL scope/示波器 | Worker 采样环 + Web 图表 + 触发/回放 | 全覆盖 |
### 6.5 Task、IO、状态和消息
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| emctask 状态机 | `sim_task` 单进程状态机 | P0 |
| emcIO/tooldata | 内存服务 + OPFS 工具表 | P0 |
| NML command/status/error | Worker 内命令/状态/事件总线;保留消息语义 | P0 |
| 共享内存 | 进程内结构体;UI 使用快照,不暴露指针 | P0 |
| 信号、日志、报警 | 事件 schema + OPFS 滚动日志 | P0 |
| `fork/exec/system` | 虚拟 M-code 白名单 | 不适用/受限 |
| 退出、复位、急停 | 明确定义状态转换和恢复点 | P0 |
### 6.6 用户界面和可视化
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| AXIS | Web 主界面:程序、状态、预览、控制和报警 | P0 |
| Gmoccapy/Touchy | 响应式触控布局和可配置面板 | 全覆盖 |
| QtVCP/GladeVCP/PyVCP | Web Component + JSON schema + Python 回调桥;不解析桌面窗口 | 全覆盖 |
| Gremlin/路径预览 | Three.js 轨迹和坐标可视化 | P0 |
| 机床模型/刀具/毛坯 | glTF + Three.js/WebGPU;切削层独立于运动核心 | 全覆盖 |
| HALShow/HALScope | Web pin 图和波形查看器 | 全覆盖 |
| 仿真配置编辑器 | Web 编辑器生成仿真 JSON/INI/HAL;Stepconf/Pncconf 的真实硬件配置部分排除 | 全覆盖 |
| 国际化、主题、快捷键 | TypeScript UI 层实现 | 全覆盖 |
### 6.7 宿主和实时相关能力
| LinuxCNC 能力 | Web/WASM 路线 | 初始状态 |
|---|---|---|
| PREEMPT_RT/RTAI/Xenomai | 不移植;用确定性逻辑时钟代替 | 不适用 |
| Linux pthread/优先级/CPU 绑定 | 用确定性 Worker 调度等价;可选 pthread 只做性能优化 | 仿真等价 |
| `mmap`、PCI、`ioctl`、`/proc` | 虚拟内存/诊断对象;不访问宿主 | 不适用 |
| 动态库 `dlopen` | 构建期静态链接或组件注册表 | 受限 |
| gettext/locale | 编译资源 + Web i18n;数值解析保持 LinuxCNC 规则 | 全覆盖 |
| OpenGL 桌面上下文 | Three.js WebGL/WebGPU | 等价实现 |
## 7. 代码组织和构建
建议不改动 LinuxCNC 原生构建,在仓库根增加独立目标:
```text
wasm-core/
include/sim_api.h
src/core/sim_engine.cc
src/core/sim_task.cc
src/core/sim_motion.cc
src/core/sim_scheduler.cc
src/core/sim_hal.cc
src/core/sim_io.cc
src/core/sim_nml.cc
src/core/sim_fs.cc
src/compat/ # 对 LinuxCNC 源码的最小补丁/适配
CMakeLists.txt
web/
src/protocol/ # 版本化命令、事件、快照、错误和能力 schema
src/client/WorkerClient.ts # 握手、序列、超时、取消、重试和对账
src/workers/sim.worker.ts
src/workers/ProtocolGateway.ts
src/workers/TaskLedger.ts
src/workers/SnapshotPublisher.ts
src/engine/WasmEngine.ts # C ABI 内存所有权和错误映射
src/state/ # session/program/motion/HAL/IO/recovery stores
src/workspace/OpfsWorkspace.ts
src/viewer/ # Three.js 机床、轨迹、坐标和毛坯
src/ui/ # 控制台、程序、HAL、PLC、工具和配置工作流
tests/
golden/ # 原生 LinuxCNC 参考快照
wasm/ # WASM 结果
protocol/ # C/TS round-trip、schema、幂等和背压
browser/ # Playwright + OPFS + 离线/恢复/视觉场景
scripts/
build-wasm.sh
generate-compat-matrix.sh
```
推荐 Emscripten/CMake 配置:
```text
emcmake cmake -S wasm-core -B build-wasm -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm
```
模块输出使用 `MODULARIZE=1`、`EXPORT_ES6=1`、`ENVIRONMENT=web,worker`。OPFS 同步句柄位于专用 Worker,因此不能依赖 `PROXY_TO_WORKER` 这类把入口转发到不兼容文件系统路径的模式;OPFS/WasmFS 组合要单独做浏览器测试。WasmFS 在 Emscripten 中仍不是旧 JS FS 的完全替代,必须固定版本并锁定实际使用的 API。
第一版默认单线程构建;若启用 pthread/共享 WASM 内存,需要 HTTPS、`Cross-Origin-Opener-Policy: same-origin` 和 `Cross-Origin-Embedder-Policy: require-corp`/`credentialless`。OPFS 本身不要求线程,但共享内存和高精度计时器需要跨源隔离。[Emscripten Pthreads](https://emscripten.org/docs/porting/pthreads.html)、[MDN SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)
## 8. 测试和一致性验证
### 8.1 参考基线
对同一个工程和 G-code,原生 LinuxCNC 与 WASM 分别输出:
- 解释器状态和当前行
- canonical motion 命令
- 每周期位置、速度、加速度和跟随误差
- 模态、坐标系、刀具、参数和变量
- HAL pin/signal/parameter 快照
- 报警、错误、消息和退出状态
测试结果保存为带版本的 JSONL/二进制 golden 文件,差异报告按功能域和首个 divergent tick 定位。
### 8.2 测试层级
1. **原生核心单测**:复用 `unit_tests/interp`、`unit_tests/tp`,补充边界、NaN、单位和浮点容差。
2. **WASM ABI 测试**:创建/加载/命令/周期/快照/快照恢复/错误码。
3. **功能对表测试**:覆盖 `configs/sim` 中的仿真 INI/HAL/G-code 样本,逐项登记兼容状态。
4. **确定性测试**:相同输入运行 1x、快速、暂停恢复、单步后结果一致。
5. **OPFS 测试**:首次创建、断电/异常中止模拟、事务恢复、配额不足、版本迁移、并发打开和句柄关闭。
6. **浏览器 E2E**:Playwright 测试 Worker、刷新恢复、离线 PWA、导入导出和 WebGL 画面。
7. **性能测试**:程序行数、HAL 组件数量、周期耗时、Worker 延迟、快照吞吐和 OPFS 写入。
8. **任务接续测试**:请求超时、回答传输中断、工具结果丢失、重复响应、Worker 重启和任务账本恢复;验证已确认步骤不重复、未确认步骤不跳过。
### 8.3 验收门槛
- 交付过程必须遵循[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)的 context lock、固定大小上下文包、断网恢复和 G0-G5 门禁;门禁证据作为本路线验收附件。
- [仿真全功能覆盖矩阵](linuxcnc-web-wasm-simulation-coverage.md) 中所有 `SIM-*`、`WEB-EQUIV`、`PY-WASM` 条目均有实现、自动测试和差异报告,覆盖率 100%。
- 来源扫描与 manifest 闭包无孤儿项;每个条目覆盖正常、边界、错误、暂停/恢复、持久化/离线和适用交互维度。
- 每个仿真配置、导出符号、HAL 组件、G/M/O/Python/API 和 Web 工作流均能追溯到实现、测试和 oracle;未登记或无法对照的功能阻止发布。
- 原生/WASM 终点和轨迹误差在定义的机床单位容差内,默认目标 `1e-6`,特殊算法单独声明。
- 强制刷新后最近一次已提交工程和快照可恢复;未提交事务不产生半文件。
- OPFS 配额不足时不丢失已提交版本,并显示可操作错误。
- 请求或工具失败后,任务可从最近已验证步骤继续;同一 `operation_id` 不产生重复副作用,无法对账时保留证据并进入明确阻断状态。
- 主线程在普通 3 轴场景保持可用,WASM 计算不阻塞 UI。
- 所有真实硬件排除项均返回 `real_hardware_excluded`;仿真配置依赖的通用虚拟主轴、编码器、探针、限位和 I/O 必须可用。真实 Modbus、GPIO、Mesa 或 VFD 协议不属于仿真覆盖范围。
## 9. 分阶段实施路线
本节的阶段 A-E 映射到执行策略中的 M0-M5;阶段退出条件、任务边界和返工规则以[项目执行与交付策略](linuxcnc-web-wasm-execution-strategy.md)第 4、6、7 节为准。
### 阶段 A:全量清单和运行时骨架
- 固定 LinuxCNC 版本和编译补丁清单。
- 生成覆盖 manifest,锁定所有文档、源码、仿真配置和测试输入。
- 建立单 Worker、固定 tick、OPFS journal、快照、版本化 ABI 和 Python/WASM 运行时。
- 建立原生 LinuxCNC 参考运行器和全量 golden 产物格式。
### 阶段 B:核心解释和运动全覆盖
- 移植全部 G/M/O、remap、参数、坐标系、补偿、TP、motion 和全部内置 kinematics。
- 完成 mill、lathe、gantry、robot、delta、pentapod、foam、plasma、laser 和 probe 虚拟机床模型。
### 阶段 C:HAL、PLC、IO 和虚拟机床部件
- 生成并编译全部 HAL 组件 manifest。
- 完成 ClassicLadder、虚拟寄存器、主轴/换刀/探针/编码器/stepgen/限位/故障模型。
- 完成 halcmd、haltcl、halui、HALScope、HALShow、Python HAL API。
### 阶段 D:全部 Web UI 和配置工具
- 完成 AXIS、Gmoccapy、Touchy、Gscreen、QtVCP、QtDragon、QtPlasmaC、VCP、Vismach、NGCGUI 等仿真操作能力的 Web 等价工作流,以及独立仿真配置编辑器。
- 完成工程导入导出、离线、国际化、主题和插件权限。
### 阶段 E:全量回归和发布
- 跑通全部 `configs/sim`、`tests`、`unit_tests`、golden 和浏览器矩阵。
- 生成版本化差异报告、兼容报告、许可证清单和工程迁移工具。
## 10. 主要风险与决策
| 风险 | 影响 | 决策/缓解 |
|---|---|---|
| LinuxCNC 代码含 Python、动态库和 Linux API | 无法直接链接 | 核心/兼容层分离;Pyodide 提供仿真必需 Python,宿主能力替换为虚拟 API |
| 浏览器不提供硬实时 | 运动结果不可依赖墙钟 | 固定 tick、批量 step、确定性回放 |
| OPFS 有配额和淘汰 | 用户数据可能不可用 | journal、版本保留、配额提示、导出备份、可选 persist |
| WasmFS/OPFS API 版本差异 | 构建或运行时回归 | 锁定 Emscripten 版本,封装适配器,浏览器矩阵测试 |
| 全量 HAL/组件工作量大 | 项目周期失控 | manifest 驱动、自动生成组件适配层和全量配置/测试回归 |
| 浮点和算法差异 | 原生/WASM 轨迹不一致 | 固定编译选项、黄金快照、定义逐算法容差 |
| 浏览器清理存储 | 工程丢失 | OPFS 不是唯一备份,提供工程包导出和恢复检查 |
## 11. 第一批可执行任务
1. 增加 `wasm-core` 最小 CMake 工程,先只编译 posemath、TP 和解释器依赖的最小源集。
2. 定义 `sim_api.h`、命令/事件/快照 schema 和错误码。
3. 由 `configs/sim/**`、`tests/**`、`unit_tests/**` 和文档/源码 G/M/O、HAL、Python、API 清单生成全量 manifest 和 golden 集;不以抽样集代替来源、维度或交互覆盖。
4. 实现 Worker 内 `WorkspaceRuntime`:OPFS 目录初始化、句柄池、journal、恢复和配额查询。
5. 实现 `sim_step()` 固定周期和 `sim_read_snapshot()`。
6. 建立原生 LinuxCNC 参考运行器,自动生成 canonical/状态 golden 文件。
7. 完成第一个浏览器 E2E:加载样例、运行、暂停、刷新、恢复、导出工程。
8. 按[前端与通信详细设计](linuxcnc-web-wasm-frontend-communication-design.md)完成 COM-01/COM-02 和 FE-01/FE-02,冻结 `protocol.v1`、能力模型、错误码、WorkerClient 和 TaskLedger 最小闭环。
## 12. 结论
纯 Web/WASM 仿真版本应把 LinuxCNC 看作“运动和解释行为的参考实现”,把 Linux 进程、实时内核、共享内存、文件系统、HAL 组件和桌面 UI 分别替换为浏览器内等价运行时。OPFS 负责长期工程数据和恢复,不直接承担每个仿真周期的随机写入;Worker 内的同步工作区负责 LinuxCNC 兼容文件语义;固定逻辑时钟负责可重复运动。
按本路线实施,交付顺序从运行时骨架、核心运动、HAL/数字孪生到 Web UI,但最终验收对象始终是覆盖矩阵中的全部仿真功能。三轴只是最早的垂直切片,不是产品能力边界;发布前必须完成完整仿真覆盖和真实硬件排除校验。

315
scripts/context_packet.py Normal file
View File

@@ -0,0 +1,315 @@
#!/usr/bin/env python3
"""Generate bounded, auditable task context packets.
The selector applies byte and count limits before writing a packet. It never
creates an over-budget packet for a later validator to reject.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
PROFILES = {
"doc": {"tokens": 2048, "bytes": 8192, "evidence_bytes": 2048, "files": 8},
"feature": {"tokens": 4096, "bytes": 16384, "evidence_bytes": 6144, "files": 16},
"contract": {"tokens": 6144, "bytes": 24576, "evidence_bytes": 10240, "files": 24},
"milestone": {"tokens": 8192, "bytes": 32768, "evidence_bytes": 14336, "files": 32},
}
SEGMENTS = ("header", "baseline", "scope", "contracts", "evidence", "risks", "next", "forbidden")
# Compact codes keep the audit trail inside the packet budget. The human
# explanation is stable and can be rendered by the reader without repeating
# large strings in every packet.
EXCLUSION_REASONS = {
"E1": "duplicate_path",
"E2": "evidence_item_bytes_exceed_budget",
"E3": "evidence_file_count_exceed_budget",
"E4": "evidence_bytes_exceed_budget",
"E5": "context_remaining_bytes_insufficient",
}
class PacketError(ValueError):
"""Raised when a packet cannot be generated without violating a budget."""
@dataclass(frozen=True)
class Candidate:
path: str
size: int
sha256: str | None
kind: str
priority: int
required: bool
def _json_bytes(value: Any) -> bytes:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _candidate(raw: dict[str, Any], base_dir: Path) -> Candidate:
path_value = str(raw.get("path", ""))
if not path_value:
raise PacketError("candidate is missing path")
path = Path(path_value)
if "size" in raw:
size = int(raw["size"])
else:
file_path = path if path.is_absolute() else base_dir / path
try:
size = file_path.stat().st_size
except OSError as exc:
raise PacketError(f"cannot stat evidence {path_value}: {exc}") from exc
if size < 0:
raise PacketError(f"evidence size must be non-negative: {path_value}")
sha256 = raw.get("sha256")
if sha256 is None and raw.get("hash_file", False):
file_path = path if path.is_absolute() else base_dir / path
sha256 = _sha256_file(file_path)
return Candidate(
path=path_value,
size=size,
sha256=str(sha256) if sha256 is not None else None,
kind=str(raw.get("kind", "artifact")),
priority=int(raw.get("priority", 0)),
required=bool(raw.get("required", False)),
)
def _evidence_item(candidate: Candidate) -> dict[str, Any]:
item: dict[str, Any] = {"path": candidate.path, "bytes": candidate.size, "kind": candidate.kind}
if candidate.sha256:
item["sha256"] = candidate.sha256
return item
def _packet_bytes(packet: dict[str, Any]) -> int:
return len(_json_bytes(packet))
def _context_bytes(packet: dict[str, Any], evidence_used: int) -> int:
"""Return the bytes consumed by the envelope and referenced evidence.
Evidence is referenced by path in the JSON envelope, but the referenced
payload is read into the task context by the consumer. Counting both
here prevents a packet that is small on disk from expanding past the
context limit when it is loaded.
"""
return _packet_bytes(packet) + evidence_used
def _finalize_packet_size(packet: dict[str, Any]) -> int:
"""Store the self-consistent serialized size in the budget metadata."""
packet["budget"]["packet_bytes"] = 0
for _ in range(4):
packet_bytes = _packet_bytes(packet)
if packet["budget"]["packet_bytes"] == packet_bytes:
return packet_bytes
packet["budget"]["packet_bytes"] = packet_bytes
raise PacketError("could not stabilize serialized packet size")
def _base_packet(input_data: dict[str, Any], context_id: str, packet_id: str) -> dict[str, Any]:
packet: dict[str, Any] = {
"context_id": context_id,
"packet_id": packet_id,
"task": str(input_data.get("task", "")),
"header": dict(input_data.get("header", {})),
"baseline": dict(input_data.get("baseline", {})),
"scope": dict(input_data.get("scope", {})),
"contracts": list(input_data.get("contracts", [])),
"evidence": {"selected": [], "excluded": []},
"risks": list(input_data.get("risks", [])),
"next": list(input_data.get("next", []))[:3],
"forbidden": list(input_data.get("forbidden", [])),
}
return packet
def _budget_metadata(limits: dict[str, int], selected: list[dict[str, Any]], excluded: list[dict[str, Any]], evidence_used: int) -> dict[str, Any]:
return {
"profile": limits["profile"],
"token_limit": limits["tokens"],
"byte_limit": limits["bytes"],
"evidence_byte_limit": limits["evidence_bytes"],
"evidence_file_limit": limits["files"],
"selected_evidence_bytes": evidence_used,
"selected_evidence_files": len(selected),
"excluded_evidence_files": len(excluded),
# Reserve the complete decimal width of the configured limit while
# selecting. This keeps the selection check conservative.
"packet_bytes": limits["bytes"],
}
def generate_packet(
input_data: dict[str, Any],
*,
context_id: str,
packet_id: str,
profile: str = "feature",
total_bytes: int | None = None,
evidence_bytes: int | None = None,
max_files: int | None = None,
base_dir: Path = Path("."),
) -> dict[str, Any]:
"""Select evidence under all budgets and return a packet within its limit."""
if profile not in PROFILES:
raise PacketError(f"unknown profile: {profile}")
limits = dict(PROFILES[profile])
limits["profile"] = profile
if total_bytes is not None:
limits["bytes"] = int(total_bytes)
if evidence_bytes is not None:
limits["evidence_bytes"] = int(evidence_bytes)
if max_files is not None:
limits["files"] = int(max_files)
if any(limits[key] < 0 for key in ("tokens", "bytes", "evidence_bytes", "files")):
raise PacketError("budgets must be non-negative")
raw_candidates = input_data.get("candidates", [])
if not isinstance(raw_candidates, list):
raise PacketError("candidates must be a list")
candidates = []
for raw in raw_candidates:
if not isinstance(raw, dict):
raise PacketError("each candidate must be an object")
candidates.append(_candidate(raw, base_dir))
candidates.sort(key=lambda item: (-int(item.required), -item.priority, item.path))
packet = _base_packet(input_data, context_id, packet_id)
skeleton_bytes = _packet_bytes(packet)
if skeleton_bytes > limits["bytes"]:
raise PacketError(
f"context skeleton exceeds byte budget ({skeleton_bytes}>{limits['bytes']}); "
"reduce task metadata or use a larger profile"
)
selected: list[dict[str, Any]] = []
excluded: list[dict[str, Any]] = []
evidence_used = 0
seen: set[str] = set()
for candidate in candidates:
if candidate.path in seen:
excluded.append({"path": candidate.path, "reason_code": "E1"})
continue
seen.add(candidate.path)
item = _evidence_item(candidate)
reason: str | None = None
if candidate.size > limits["evidence_bytes"]:
reason = "E2"
elif len(selected) >= limits["files"]:
reason = "E3"
elif evidence_used + candidate.size > limits["evidence_bytes"]:
reason = "E4"
else:
trial = dict(packet)
trial_selected = selected + [item]
trial["evidence"] = {"selected": trial_selected, "excluded": excluded}
trial["budget"] = _budget_metadata(
limits, trial_selected, excluded, evidence_used + candidate.size
)
# This check is intentionally performed before selection is committed.
trial_context_bytes = _context_bytes(trial, evidence_used + candidate.size)
if trial_context_bytes > limits["bytes"]:
reason = "E5"
if reason:
excluded.append({"path": candidate.path, "reason_code": reason})
if candidate.required:
raise PacketError(f"required evidence cannot fit ({reason}): {candidate.path}")
continue
selected.append(item)
evidence_used += candidate.size
# The exclusion audit trail is part of the packet, so it can consume the
# remaining context space. Remove optional selections until the complete
# packet (selected evidence, exclusions and budget metadata) fits.
while True:
packet["evidence"] = {"selected": selected, "excluded": excluded}
packet["budget"] = _budget_metadata(limits, selected, excluded, evidence_used)
packet_bytes = _finalize_packet_size(packet)
context_bytes = packet_bytes + evidence_used
if packet_bytes <= limits["bytes"] and context_bytes <= limits["bytes"]:
break
removable_index = next(
(index for index in range(len(selected) - 1, -1, -1)
if not any(item["path"] == selected[index]["path"] and candidate.required
for candidate in candidates)),
None,
)
if removable_index is None:
raise PacketError("context skeleton and required evidence exceed byte budget")
removed = selected.pop(removable_index)
evidence_used -= int(removed["bytes"])
excluded.append({
"path": removed["path"],
"reason_code": "E5",
})
return packet
def _load(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
value = json.load(handle)
if not isinstance(value, dict):
raise PacketError("input must be a JSON object")
return value
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", required=True, type=Path, help="candidate/context JSON")
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--context-id", required=True)
parser.add_argument("--packet-id", required=True)
parser.add_argument("--profile", choices=sorted(PROFILES), default="feature")
parser.add_argument("--base-dir", type=Path, default=Path("."))
parser.add_argument("--total-bytes", type=int)
parser.add_argument("--evidence-bytes", type=int)
parser.add_argument("--max-files", type=int)
args = parser.parse_args(list(argv) if argv is not None else None)
try:
packet = generate_packet(
_load(args.input),
context_id=args.context_id,
packet_id=args.packet_id,
profile=args.profile,
total_bytes=args.total_bytes,
evidence_bytes=args.evidence_bytes,
max_files=args.max_files,
base_dir=args.base_dir,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
payload = _json_bytes(packet)
# Do not expose a partially written packet to a concurrent validator.
temporary = args.output.with_name(f".{args.output.name}.tmp")
temporary.write_bytes(payload)
if len(payload) != packet["budget"]["packet_bytes"]:
raise PacketError("serialized packet size changed before write")
temporary.replace(args.output)
except (PacketError, OSError, json.JSONDecodeError) as exc:
print(f"context packet generation failed: {exc}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
import json
import subprocess
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from context_packet import PacketError, generate_packet
class ContextPacketTests(unittest.TestCase):
def base(self, candidates):
return {
"task": "bounded task",
"header": {"milestone": "M1"},
"baseline": {"source_sha": "abc"},
"scope": {"include": ["feature/1"]},
"contracts": ["protocol.v1"],
"risks": [],
"next": ["run test"],
"forbidden": ["real hardware"],
"candidates": candidates,
}
def test_filters_by_evidence_bytes_before_final_write(self):
packet = generate_packet(
self.base([
{"path": "small.log", "size": 6, "priority": 2},
{"path": "large.log", "size": 7, "priority": 1},
]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=10,
)
self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["small.log"])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E4")
self.assertLessEqual(packet["budget"]["packet_bytes"], 4096)
def test_filters_by_remaining_context_space(self):
packet = generate_packet(
self.base([{"path": "evidence.log", "size": 1, "priority": 1}]),
context_id="c1",
packet_id="p1",
total_bytes=550,
evidence_bytes=100,
)
self.assertEqual(packet["evidence"]["selected"], [])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E5")
self.assertLessEqual(packet["budget"]["packet_bytes"], 550)
def test_evidence_payload_counts_against_total_context_budget(self):
packet = generate_packet(
self.base([{"path": "payload.bin", "size": 40, "priority": 1}]),
context_id="c1",
packet_id="p1",
total_bytes=570,
evidence_bytes=100,
)
self.assertEqual(packet["evidence"]["selected"], [])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E5")
self.assertLessEqual(
packet["budget"]["packet_bytes"]
+ packet["budget"]["selected_evidence_bytes"],
570,
)
self.assertEqual(
packet["budget"]["packet_bytes"],
len(json.dumps(packet, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")),
)
def test_skeleton_over_budget_fails_before_selection(self):
with self.assertRaisesRegex(PacketError, "context skeleton exceeds byte budget"):
generate_packet(
self.base([]),
context_id="c1",
packet_id="p1",
total_bytes=100,
evidence_bytes=100,
)
def test_required_item_fails_without_writing_an_over_budget_packet(self):
with self.assertRaisesRegex(PacketError, "required evidence cannot fit"):
generate_packet(
self.base([{"path": "required.log", "size": 101, "required": True}]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=100,
)
def test_required_item_fails_when_payload_consumes_remaining_context(self):
with self.assertRaisesRegex(PacketError, r"required evidence cannot fit \(E5\)"):
generate_packet(
self.base([{"path": "required.log", "size": 40, "required": True}]),
context_id="c1",
packet_id="p1",
total_bytes=570,
evidence_bytes=100,
)
def test_selection_is_deterministic_and_count_is_enforced(self):
packet = generate_packet(
self.base([
{"path": "z.log", "size": 1},
{"path": "a.log", "size": 1},
{"path": "required.log", "size": 1, "required": True},
]),
context_id="c1",
packet_id="p1",
total_bytes=4096,
evidence_bytes=100,
max_files=2,
)
self.assertEqual([item["path"] for item in packet["evidence"]["selected"]], ["required.log", "a.log"])
self.assertEqual(packet["evidence"]["excluded"][0]["reason_code"], "E3")
def test_cli_does_not_replace_existing_output_on_generation_failure(self):
with TemporaryDirectory() as directory:
root = Path(directory)
input_path = root / "input.json"
output_path = root / "packet.json"
input_path.write_text(
json.dumps(self.base([{"path": "required.log", "size": 101, "required": True}]))
)
output_path.write_text("previous packet")
result = subprocess.run(
[
"python3",
"scripts/context_packet.py",
"--input",
str(input_path),
"--output",
str(output_path),
"--context-id",
"c1",
"--packet-id",
"p1",
"--total-bytes",
"4096",
"--evidence-bytes",
"100",
],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 2)
self.assertEqual(output_path.read_text(), "previous packet")
if __name__ == "__main__":
unittest.main()