From 13255045446bf5d6d6ee2e24cd4c2c028c7b13d0 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Fri, 21 Aug 2026 17:31:50 +0800 Subject: [PATCH] Document system flow and next execution baseline --- README.md | 5 +- coverage/owner-decision-intake.report.json | 65 +++++ coverage/owner-decision-intake.template.json | 13 + docs/linuxcnc-web-wasm-next-work.md | 10 +- ...linuxcnc-web-wasm-owner-decision-intake.md | 29 ++ ...wasm-system-flow-and-execution-baseline.md | 263 ++++++++++++++++++ docs/task-history.md | 1 + nextTask.md | 12 +- scripts/test_owner_decision_intake.py | 50 ++++ scripts/validate_owner_decision_intake.py | 114 ++++++++ 10 files changed, 553 insertions(+), 9 deletions(-) create mode 100644 coverage/owner-decision-intake.report.json create mode 100644 coverage/owner-decision-intake.template.json create mode 100644 docs/linuxcnc-web-wasm-owner-decision-intake.md create mode 100644 docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md create mode 100644 scripts/test_owner_decision_intake.py create mode 100644 scripts/validate_owner_decision_intake.py diff --git a/README.md b/README.md index 193dfbd..d2ed121 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,16 @@ 恢复或开始工作时,首先读取 [`nextTask.md`](nextTask.md)。它是唯一接续入口,刻意限制在 8 KiB 以内,不包含聊天记录、完整日志或历史证据。只有定位旧证据时才按需读取 [`docs/task-history.md`](docs/task-history.md) 和其中引用的产物。 +项目整体流程、LinuxCNC 案例到 WASM/Worker/前端的映射,以及后续任务起点见 +[`系统流程与后续执行基线`](docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md)。 + ## 当前状态 - LinuxCNC 来源基线:`linuxcnc/VERSION` 与 `context-lock.json`。 - 来源索引:`coverage/source-index.jsonl`,由 `scripts/generate_source_inventory.py` 生成。 - 覆盖种子:`coverage/manifest.jsonl`。所有条目初始为 `planned/needs_owner`,没有来源会被静默忽略。 - WASM ABI 骨架:`wasm-core/include/sim_api.h`;当前实现只验证生命周期、固定 tick、快照和状态导入导出。 -- 前端应用壳、真实 WorkerClient、完整配置加载、HAL/数字孪生、实际 Pyodide bridge 和 UI/Three.js 工作流仍须按文档里程碑实现;当前下一任务是 `M3-CFG-01`,详见 [`后续工作基线`](docs/linuxcnc-web-wasm-next-work.md)。 +- 前端 AppShell、真实 WorkerClient、配置/HAL preflight、虚拟设备、Python bridge 和 FE-M1..M4 初始工作流已有实现证据;真正的 LinuxCNC Task 固定 tick、完整配置启动、真实 OPFS/浏览器矩阵和全量 UI 融合仍未完成。当前正式下一任务是 `WAIT-OWNER-01`;owner 输入和新 context 后的第一个技术实现起点为 `INT-01`,详见 [`系统流程与后续执行基线`](docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md)。 - K0-K6 运动学实现主线已完成代码、ABI、固定边界和 native/WASM 回归,覆盖 trivkins、耦合运动学、5 轴/TRT、机器人、GenSer、Delta、Tripod、Hexapod、Pentapod、switchkins、userk/registry、TP→IK→motion、FK feedback、home/limit。对应报告仍可能是 `partial`,因为锁定来源没有 `linuxcnc/unit_tests/kinematics/**` 官方 oracle;这是发布验收阻塞,不是继续重复解析的理由。 - K8 配置/发布审计已生成 264 个配置注册入口、233 个 sim entrypoint 和 16,093 条资源边;当前配置启动闭包仍有 997 missing、91 unresolved、59 unindexed 和 3 unregistered startup 条目。K8 release gate 保持 `candidate_eligible=false`、`release_action=STOP_RELEASE`,六项 owner/release blocker 不得被误报为运动学未完成。 diff --git a/coverage/owner-decision-intake.report.json b/coverage/owner-decision-intake.report.json new file mode 100644 index 0000000..6a32904 --- /dev/null +++ b/coverage/owner-decision-intake.report.json @@ -0,0 +1,65 @@ +{ + "candidate_eligible": false, + "checks": { + "context_consistency": { + "input": null, + "locked": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "status": "blocked" + }, + "context_lock_unchanged": { + "action": "no mutation", + "status": "verified" + }, + "decision_approval": { + "approved": 0, + "required": 6, + "status": "blocked" + }, + "gate_refresh_allowed": { + "reason": "owner intake validation never refreshes the gate; reviewed new context is required after all six approvals", + "status": "blocked" + }, + "input_file_present": { + "path": "coverage/owner-decisions.json", + "status": "blocked" + }, + "schema_shape": { + "schema": null, + "status": "blocked" + }, + "six_blocker_identity": { + "duplicates": [], + "missing": [ + "accepted_entries_missing", + "browser_matrix_unverified", + "configuration_resources_missing", + "license_closure_incomplete", + "manifest_seed_unassigned", + "official_oracle_missing" + ], + "status": "blocked", + "unknown": [] + } + }, + "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "errors": [ + "[Errno 2] No such file or directory: '/home/mes006/linuxcncWebWASM/coverage/owner-decisions.json'" + ], + "implementation_status": "verified", + "input": { + "path": "coverage/owner-decisions.json", + "sha256": null, + "status": "missing" + }, + "next_transition": "wait for owner-decisions.json or new locked context input; do not rerun browser or implementation matrices", + "release_action": "STOP_RELEASE", + "release_status": "blocked_external_owner", + "report_sha256": "204ecc3408720b869d7512594ce491912060c2280f9e86dfa22b381599c89a89", + "schema": "owner-decision-intake-report.v1", + "status": "blocked", + "task_id": "K8-REL-18", + "template": { + "path": "coverage/owner-decision-intake.template.json", + "sha256": "5c9e356ef40c3cd307c10153839c75f94ecff44a6ac6a355323feddfa26e3a32" + } +} diff --git a/coverage/owner-decision-intake.template.json b/coverage/owner-decision-intake.template.json new file mode 100644 index 0000000..47d3621 --- /dev/null +++ b/coverage/owner-decision-intake.template.json @@ -0,0 +1,13 @@ +{ + "schema": "owner-decision-intake.v1", + "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", + "decision_set_id": "K8-owner-decisions-01", + "decisions": [ + {"blocker": "manifest_seed_unassigned", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true}, + {"blocker": "accepted_entries_missing", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true}, + {"blocker": "official_oracle_missing", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true}, + {"blocker": "configuration_resources_missing", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true}, + {"blocker": "browser_matrix_unverified", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true}, + {"blocker": "license_closure_incomplete", "decision": "missing", "owner": null, "reviewer": null, "rationale": null, "evidence": [], "new_context_required": true} + ] +} diff --git a/docs/linuxcnc-web-wasm-next-work.md b/docs/linuxcnc-web-wasm-next-work.md index 3b879fb..1163a89 100644 --- a/docs/linuxcnc-web-wasm-next-work.md +++ b/docs/linuxcnc-web-wasm-next-work.md @@ -6,9 +6,9 @@ 运动学实现主线 K0-K6 已完成代码和 native/WASM 回归:trivkins、耦合运动学、5 轴/TRT、机器人、GenSer、Delta、Tripod、Hexapod、Pentapod、switchkins、userk/registry、TP→IK→motion、FK feedback、home/limit 均已有实现、ABI、固定边界和测试产物。对应报告仍为 `partial` 或 `blocked` 的原因是锁定 LinuxCNC 来源没有 `linuxcnc/unit_tests/kinematics/**` 官方 oracle;这限制发布验收,不应继续重复实现运动学解析。 -已验证的其它基础包括:posemath、解释器、motion queue、HAL 核心、IO 核心、Worker protocol、replay、workspace recovery/migration、Python runtime manifest 和 native CTest。当前仓库没有前端应用壳,也没有把 INI/HAL 资源图接入真实 Worker 启动路径。 +已验证的其它基础包括:posemath、解释器、motion queue、HAL 核心、IO 核心、Worker protocol、replay、workspace recovery/migration、Python runtime manifest、配置/HAL preflight、虚拟设备、Python bridge、FE-M1..M4 和 native CTest。当前前端与适配器已有垂直切片,但 `sim_api` 仍是 bootstrap,Worker 尚未把 interpreter、TP、motion、kinematics、HAL、IO 串成统一 `sim_task` 固定 tick;真实 OPFS、完整配置启动和跨浏览器验证也未闭合。 -发布状态仍为 `blocked_not_release_candidate`/`STOP_RELEASE`。六项发布 blocker(manifest ownership、accepted entries、official oracle、configuration resources、browser matrix、implementation license)必须保留;owner review 是外部输入,不再作为当前开发任务反复重跑。 +发布状态仍为 `blocked_not_release_candidate`/`STOP_RELEASE`。六项发布 blocker(manifest ownership、accepted entries、official oracle、configuration resources、browser matrix、implementation license)必须保留;当前正式接续以 [`nextTask.md`](../nextTask.md) 的 `WAIT-OWNER-01` 为准,owner review 是外部输入,不再作为当前开发任务反复重跑。 ## 执行顺序 @@ -26,7 +26,11 @@ | 10 | `REG-01` | 运行 233 个 sim entrypoint 的启动矩阵,分类 ready/blocked/error 并生成首个差异定位 | full configuration matrix report | M3-CFG-01..03、M3-PY-01、FE-M4 | | 10 | `REG-03/04` | 完成恢复、断网、浏览器和性能矩阵;再回到 K8 release gate | recovery/browser/performance reports | FE-M1、REG-01 | -## 当前任务边界:REG-01 +## 历史实现边界与当前技术缺口 + +本表中的 M3/FE/REG 行用于保留已完成实现的依赖和证据,不是当前接续指针。`nextTask.md` 是唯一当前任务入口;owner 输入和新 context 之后,第一个需要实现的技术任务是 `INT-01 sim_task 固定 tick 集成`,详见 [`系统流程与后续执行基线`](linuxcnc-web-wasm-system-flow-and-execution-baseline.md)。 + +## 历史任务边界:REG-01 允许创建全配置 startup matrix 生成器、结果分类和 source/resource/component 差异定位测试;不修改 kinematics 算法、ABI/protocol/schema/context lock,不替 owner 作许可证、浏览器或官方 oracle 决定。 diff --git a/docs/linuxcnc-web-wasm-owner-decision-intake.md b/docs/linuxcnc-web-wasm-owner-decision-intake.md new file mode 100644 index 0000000..433fa24 --- /dev/null +++ b/docs/linuxcnc-web-wasm-owner-decision-intake.md @@ -0,0 +1,29 @@ +# Owner Decision Intake + +输入文件:`coverage/owner-decisions.json` + +模板:`coverage/owner-decision-intake.template.json` + +验证器:`python3 scripts/validate_owner_decision_intake.py` + +当前状态:`coverage/owner-decisions.json` 尚未提供,验证报告应保持 `blocked`。模板文件是唯一允许复制的输入起点;不要直接修改 `context-lock.json` 或 release gate 报告。 + +## Required Shape + +The input must use schema `owner-decision-intake.v1`, match the locked +`context_id`, and contain exactly one decision for each of the six blocker IDs: + +`manifest_seed_unassigned`, `accepted_entries_missing`, +`official_oracle_missing`, `configuration_resources_missing`, +`browser_matrix_unverified`, `license_closure_incomplete`. + +Each decision requires `decision` (`approve`, `reject`, or `defer`), non-empty +`owner`, non-empty `reviewer`, a rationale, an evidence list, and an explicit +`new_context_required` boolean. Evidence entries require a repository-relative +path and SHA-256 hash. The validator checks shape and identity only; it never +creates a context, changes `context-lock.json`, promotes an entry, or refreshes +the release gate. + +All six decisions must be `approve` and accompanied by a reviewed new context +input before K8 release refresh can be opened. Missing, partial, deferred, +rejected, duplicate, stale-context, or hash-invalid input remains blocked. diff --git a/docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md b/docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md new file mode 100644 index 0000000..8fb3ffd --- /dev/null +++ b/docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md @@ -0,0 +1,263 @@ +# LinuxCNC Web/WASM 系统流程与后续执行基线 + +更新时间:2026-08-21 + +本文件把现有代码、LinuxCNC 源程序、仿真配置和测试程序放在同一条可执行链上。它的目的不是再次定义产品范围,而是回答三个问题:当前代码究竟完成到哪一层、一个 LinuxCNC 仿真入口如何穿过前端/Worker/WASM、后续任务应从哪里开始以及如何验收。 + +规范入口仍然是 [`nextTask.md`](../nextTask.md)。本文件是工程梳理和任务分解,不替换 `context-lock.json`、协议 schema、覆盖矩阵或 release gate。 + +## 1. 先给结论 + +### 1.1 当前正式任务起点 + +当前正式指针是 `WAIT-OWNER-01`,状态为 `blocked`。它等待六项 owner decision 或新的 locked context input;`coverage/owner-decision-intake.report.json` 在输入缺失时只能生成 blocked report。此期间不能刷新 K8 release gate、创建新 context、把 `partial` 改成 `accepted`,也不能重复执行已完成的实现矩阵。 + +这与技术路线中的“实现任务已完成”并不矛盾: + +| 层次 | 当前结论 | 证据 | 含义 | +|---|---|---|---| +| 基线 | LinuxCNC `2.10.0~pre0`,source SHA 已冻结 | `context-lock.json` | 源码变化必须重新建 context | +| WASM 单元能力 | posemath、解释器切片、motion queue、HAL、运动学、IO 均有 C ABI 和 native/WASM 测试 | `wasm-core/CMakeLists.txt`、`build-native`、`build-wasm` | 证明模块边界,不证明完整 Task 链 | +| Worker/存储适配 | 协议、配置/HAL preflight、虚拟设备、事件重放、workspace package、Python bridge 已有测试 | `web/worker`、`web/workspace`、`coverage/*report.json` | 证明适配器和错误边界 | +| 前端 | AppShell、WorkerClient、控制、状态、恢复、设备/HAL/机床视图已有 smoke | `web/app`、`coverage/frontend-m*.report.json` | 证明 UI 垂直切片,不证明所有真实命令已经驱动核心 | +| 配置矩阵 | 264 个注册配置条目、233 个入口;56 个 ready,208 个 blocked | `coverage/full-configuration-startup-matrix.report.json` | 入口闭包仍未完成 | +| 资源闭包 | 16,093 条资源边,15,112 resolved,981 blocked | `coverage/configuration-startup-closure.report.json` | 缺失资源不能被静默忽略 | +| 发布 | `candidate_eligible=false`、`STOP_RELEASE` | `coverage/release-candidate-gate.report.json` | 当前不是发布候选 | + +### 1.2 代码上的真实边界 + +目前最容易误判的地方是:模块测试都通过,但完整 LinuxCNC 行为还没有贯通。具体表现为: + +1. [`wasm-core/src/sim_api.c`](../wasm-core/src/sim_api.c) 仍是生命周期、加载标志、tick 和 pending event 的 bootstrap 状态;`sim_command` 不解析命令,`sim_step` 不调用解释器、TP、HAL、运动学或 IO。 +2. [`web/worker/sim-session.mjs`](../web/worker/sim-session.mjs) 已经把协议、幂等、配置/HAL preflight、虚拟设备、Python 和恢复接起来,但 `PROGRAM_*`、`COMMAND` 等未知于设备的命令目前会落入通用 `COMMAND_RESULT`,不能视为已经执行了 LinuxCNC Task。 +3. [`web/app/project-workflow.mjs`](../web/app/project-workflow.mjs) 默认使用 `MemoryStorage`;workspace package/migration/recovery 契约已验证,真实浏览器 OPFS、Service Worker 离线启动和崩溃后的实际恢复仍需浏览器矩阵验证。 +4. 解释器、motion、kinematics、IO 的 C API 是可用的模块边界,但尚缺一个统一的 `sim_task`/session runtime,把“程序行 -> canonical -> TP -> IK -> joint motion -> feedback -> HAL/IO -> snapshot”作为一个原子固定 tick 执行。 + +因此,当前状态应写成“适配器和核心切片已验证,端到端仿真 Task 尚未完成”,不能写成“LinuxCNC 已完整移植”。 + +## 2. 一次仿真入口的实际流程 + +下面的流程区分了已经存在的代码路径和必须补齐的目标路径。 + +```text +浏览器 UI + -> WorkerClient 生成 protocol.v1 envelope + -> sim-worker.mjs 单 Worker 接收消息 + -> SimSession 校验 schema/context/session/seq/tick + -> sim_protocol_accept (WASM C ABI) + -> HELLO/ATTACH 或 LOAD_WORKSPACE + -> configuration preflight (INI/HAL/资源图) + -> HAL preflight (component/pin/thread/function 图) + -> sim_load_workspace (当前仍为 bootstrap load flag) + -> command / program / step + -> [应补] sim_task: interpreter -> canonical -> TP -> motion/IK -> HAL/IO/Python + -> event log + replay timeline + state hash + -> snapshot/checkpoint/recovery + -> Worker envelope 返回 + -> WorkerClient promise + domain reducer + -> 状态面板、设备面板、HAL 诊断、Three.js/Canvas 视图 + -> workspace journal/package/OPFS 持久化 +``` + +### 2.1 前端到 Worker + +入口是 [`web/app/app.mjs`](../web/app/app.mjs) 和 [`web/app/worker-client.mjs`](../web/app/worker-client.mjs)。前端只发送带 `schema_version`、`context_id`、`session_id`、单调 `seq`、可选 `operation_id` 和 `payload` 的消息;前端不得读取 WASM 指针或直接写 Worker 内部状态。 + +`WorkerClient` 的主要调用对应关系如下: + +| UI 操作 | Worker 消息 | 当前实现 | +|---|---|---| +| 连接/能力协商 | `HELLO` | 返回 capabilities | +| 建立会话 | `SESSION_ATTACH` | 调用 `sim_create`,进入 ready | +| 加载工程 | `LOAD_WORKSPACE` | 运行配置/HAL preflight,再调用 `sim_load_workspace` | +| 推进仿真 | `SIM_STEP` | 调用 `sim_step`,更新 tick 和 timeline | +| 读取状态 | `SNAPSHOT_REQUEST` | 返回 snapshot/checkpoint 摘要 | +| 控制命令 | `PROGRAM_*`、`HOME`、`ESTOP` 等 | 设备命令进入 `VirtualDevices`;程序/Task 命令仍待接入 | +| 恢复 | `CHECKPOINT`、`RECOVER` | 事件重放和 workspace 状态恢复已覆盖 | + +### 2.2 Worker 到 WASM + +[`web/worker/sim-session.mjs`](../web/worker/sim-session.mjs) 是唯一的 Worker 编排边界。它先调用 `_sim_protocol_accept`,再把能改变仿真结果的事件写入 `ReplayTimeline` 和 C event log。所有操作必须串行执行;相同 `operation_id` 的重复请求返回原结果并写入 `OPERATION_REPLAY`。 + +WASM 导出列表在 [`wasm-core/CMakeLists.txt`](../wasm-core/CMakeLists.txt) 中冻结,当前可以分为四类: + +- 协议/生命周期:`sim_create`、`sim_load_workspace`、`sim_command`、`sim_step`、状态导入导出。 +- 核心模块:`sim_interpreter_*`、`sim_motion_*`、`sim_kinematics_*`、`sim_posemath_*`。 +- 运行时对象:`sim_hal_*`、`sim_io_*`、event log。 +- 运动集成:`sim_kinematics_motion_*`、feedback、home/limit、switchkins、userk 和 registry。 + +后续不能继续增加孤立导出函数;应先把这些函数收拢进一个有明确状态、错误回滚和 snapshot 字段的 `sim_task` runtime,再由 Worker 调用。 + +### 2.3 固定 tick 的目标顺序 + +固定 tick 顺序属于行为契约,建议按以下顺序实现并为每一步保留前后状态 hash: + +1. 接收本 tick 的 Task/MDI/jog/设备输入,检查命令权限、模式和 abort/estop。 +2. 解释器读取一行 G-code/O-code/M-code,更新 modal、参数、当前行和 canonical queue。 +3. 执行 canonical queue 的 TP:前瞻、速度/加速度限制、blend/exact stop、pause/abort。 +4. 将笛卡尔 pose 送入 inverse kinematics,生成 joint command;奇异、越界和非有限值必须事务性失败。 +5. 推进 joint/motion queue,执行正解 feedback,更新 axis/joint/pose/velocity/following error。 +6. 按固定线程顺序执行 HAL component graph;组件失败不应推进半个 tick。 +7. 推进 spindle/tool/probe/limit/estop 等虚拟 IO 和 Python callback;Python 只能使用 allowlist API。 +8. 生成事件、报警、canonical、snapshot 和状态 hash;提交 checkpoint 或 journal 时才写持久层。 + +## 3. LinuxCNC 源程序、案例和测试的对应关系 + +LinuxCNC 的 `configs/sim` 不是文档附件,而是启动闭包和功能来源。下面先用少量代表性案例建立可跑的纵向切片,再扩展到全部 233 个入口。 + +| 功能域 | LinuxCNC 源程序 | 代表性仿真案例 | 代表性测试程序 | WASM/Worker 目标 | 前端工作流 | +|---|---|---|---|---|---| +| 启动、INI、资源 | `src/emc/ini/emcIniFile.cc`、`iniaxis.cc`、`inijoint.cc`、`initraj.cc` | `configs/sim/axis/minimal_xyz.ini`、`ini_with_includes/includes_demo.ini` | `tests/rs274ngc-startup`、`tests/startup-state`、`tests/io-startup` | 解析 section/include/变量、坐标和资源;构造 `sim_task_config` | 工程选择、预检错误定位、启动/重启 | +| 解释器、canonical | `src/emc/rs274ngc/*`、`src/emc/task/emccanon.cc`、`src/emc/nml_intf/interpl.cc` | `axis/minimal_xyz.ini` 的 `G0/G1`、`axis/iocontrolv2/owordm6-ui.ini` | `tests/interp/good`、`g52`、`m98m99`、`remap/oword-pycall`、`unit_tests/interp/*` | 从文件/MDI 读行,输出 canonical 和当前行;错误回到协议 | 程序编辑器、MDI、当前行、语法/运行错误 | +| TP、运动 | `src/emc/tp/tp.c`、`tc.c`、`tcq.c`、`blendmath.c`;`src/emc/motion/*` | `axis/minimal_xyz.ini`、`axis/gantry/gantry.ini` | `unit_tests/tp/test_blendmath.c`、`tests/motion/g0`、`tests/trajectory-planner/circular-arcs`、`tests/abort/*` | 固定 tick TP -> motion queue,支持 pause/abort/queue-buster | 运行/暂停/单步/轨迹预览/倍率 | +| 运动学 | `src/emc/kinematics/*kins.c`、`kins_util.c`、`switchkins.c` | `axis/gantry/gantry.ini`、`axis/ldelta.ini`、`axis/pentapod/pentapod.ini`、`axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini` | `tests/matrixkins`、运动学配置案例;官方 `unit_tests/kinematics/**` 在锁定来源中缺失 | registry + FK/IK + branch/singularity + feedback/home/limit | 轴/关节/姿态、3D 模型、故障和奇异性 | +| HAL | `src/hal/hal_lib.c`、`halmodule.cc`、`utils/halcmd*`、`src/hal/components/*.comp` | `axis/gantry/gantrysim.hal`、`axis/ini_hal_demo.ini` | `tests/hal-*`、`tests/basic`、`tests/edge/*`、`tests/module-loading/*`、`tests/hal-link-unlink` | 类型化 pin/signal/thread/function 图,固定顺序执行 | HAL browser、pin 值、连接图、波形 | +| IO、主轴、换刀、探针 | `src/emc/iotask/ioControl*.cc`、`src/emc/tooldata/*` | `axis/iocontrolv2/*`、`axis/lathe.ini`、`axis/gladevcp/probe.ini` | `tests/io-startup/*`、`tests/toolchanger/*`、`tests/tool-info/*`、`tests/t0/*` | 虚拟主轴/工具/探针状态机,故障、互锁和事件 | 主轴、工具、探针、报警和恢复 | +| Python/remap/filter | `src/emc/rs274ngc/interp_python.cc`、`interp_remap.cc`、`lib/python/*` | `axis/remap/*`、`axis/plasma/*`、`axis/ngcgui/*` | `tests/remap/*`、`tests/interp/python*`、`tests/interp/pymove` | Worker-only allowlist,异常和资源限额映射为 Task 错误 | remap 参数、宏、材料表、插件状态 | +| UI/VCP/可视化 | `src/emc/usr_intf/*`、`lib/python/rs274/glcanon.py`、`hal/user_comps/vismach/*` | `axis/gladevcp/*`、`axis/qtvcp/*`、`axis/vismach/*`、`qtplasmac/*` | UI test scripts、对应配置的启动测试和浏览器 E2E | descriptor -> Web component;轨迹/姿态只读 snapshot | 控制台、VCP、3D、示波器、配置编辑 | +| 持久化/恢复 | LinuxCNC 参数、tool table、日志语义 + 本项目 `web/workspace/*` | `sim.var`、`*.tbl`、工程 package | `tests/save.*`、`tests/source.0`,本项目 `test_recovery.mjs`、`test_migration.mjs` | checkpoint/event/journal/package 双向迁移、fail-closed | 工程导入导出、离线、刷新、恢复 | + +### 3.1 第一批必须跑通的案例 + +先不要从最复杂的 QtVCP/PlasmaC/机器人界面开始。建议按以下顺序建立可重复的端到端样例: + +1. `minimal_xyz.ini`:`HELLO -> ATTACH -> LOAD -> home -> PROGRAM_LOAD -> G0 X1 -> step -> snapshot`。它验证最小启动、解释器、trivkins、motion、snapshot 和前端状态。 +2. `gantry/gantry.ini`:验证 `XYZY` 重复坐标、四关节映射、`HOME_SEQUENCE=-1` 同步回零和 CoreXY/gantry HAL。 +3. `iocontrolv2/axis-iocontrolv2-demo.ini`:验证 `T/M6` 的 prepare/change、主轴停止互锁、tool table、IO pin 和失败恢复。 +4. `vismach/5axis/table-rotary-tilting/xyzac-trt.ini`:验证姿态、TRT kinematics、tool orientation、FK feedback 和 3D 轨迹。 +5. `remap/oword-pycall`:验证 O-word、Python remap、虚拟 API、异常、参数和 checkpoint/replay。 +6. `hard-limits` + `abort/*`:验证 soft/hard limit、estop、abort、following error 和恢复后状态 hash。 +7. `plasma/metric.ini` 或 `gladevcp/probe.ini`:验证虚拟探针/torch/材料表以及 UI/设备交互。 + +每个案例都必须保留原始配置资源边、native 运行结果、WASM 结果、首个 divergent tick、字段路径、错误码和状态 hash;只看页面截图不算通过。 + +## 4. 按技术路线的任务分层 + +下面把路线图的 M0-M5 和代码实际状态对齐。`verified` 表示该任务的实现证据已生成,不等于 release `accepted`。 + +| 阶段 | 目标 | 已有结果 | 未完成的关键闭环 | 退出条件 | +|---|---|---|---|---| +| M0 基线 | 来源、manifest、依赖、context lock | 8,053 source entries,321 test entries,清单生成器稳定 | 6,249 seed 仍未分配 feature owner | source/manifest/context hash 可重建 | +| M1 会话骨架 | protocol、create/load/step/snapshot/recover | C ABI、Worker session、event/replay、Python manifest 已验证 | `sim_load_workspace` 尚未真正构造核心 runtime;无完整 task state | 真实 WASM Worker 可加载最小工程并恢复 | +| M2 核心行为 | interpreter、canonical、TP、motion、kinematics | C 模块和 15 项 native CTest 通过;K0-K6 运动学切片已有 | 模块未在 `sim_task` 内按固定 tick 串联;完整 G/M/O 不全 | native/WASM golden 逐 tick 对照 | +| M3 虚拟设备 | HAL/PLC/IO、Python、恢复和故障 | M3-CFG/RT/FS/PY 报告均有 verified implementation evidence | 981 资源阻断,HAL 动态组件和真实 Python remap 未全执行 | 故障矩阵、配额、重启、离线全通过 | +| M4 Web 工作流 | 控制台、工程、3D、VCP、诊断 | FE-M1..M4 smoke 已通过 | UI 目前没有完整真实 G-code/Task 状态源;OPFS/跨浏览器不足 | 关键 UI workflow 与核心状态一致 | +| M5 发布 | 全配置、兼容、oracle、浏览器、许可证、回滚 | REG-01/03/04 证据已生成但 partial | 六项 release blocker 未解除 | 全部可计入条目 accepted,`STOP_RELEASE` 清除 | + +## 5. 后续任务和依赖图 + +### 5.1 正式 release 轨道 + +```text +WAIT-OWNER-01 (当前 blocked) + -> OWNER-INPUT-01: 校验六项 blocker decision + -> CONTEXT-REFRESH-01: 审查并生成新 context lock + -> K8-REL-17: 重新生成 closure/audit/gate + -> 仅在 blocker 有证据解除后进入 release candidate +``` + +六项 decision 是:manifest ownership、accepted entries、official oracle、configuration resources、browser matrix、implementation license。没有外部输入时,正确动作是等待,不是自动重跑报告。 + +### 5.2 文档冲突的处理顺序 + +仓库中部分路线文档是在前端和 REG 任务完成前生成的,存在历史状态描述。例如 `docs/linuxcnc-web-wasm-next-work.md` 仍写着“没有前端应用壳”和“当前任务是 REG-01”,但 `web/app`、`coverage/frontend-m1..m4.report.json`、`coverage/full-configuration-startup-matrix.report.json` 已给出后续实现证据。执行时按以下优先级判断: + +1. `nextTask.md` 的 Current/Task/Verify/Stop 是唯一接续指针; +2. `context-lock.json` 是源码、工具链、schema 和版本基线; +3. `coverage/*.report.json`、native/WASM/Worker 测试是实际状态证据; +4. 路线图、task map 和 next-work 用于理解目标和依赖,若与前三者冲突必须修正文档,而不是回退代码或重复任务。 + +本文件已经按这个顺序梳理;它不把旧文档中的 `REG-01`、FE-M1..M4 再次认领为当前任务。 + +### 5.3 技术实现轨道:第一个可执行起点 + +在 owner 输入和新 context 审查完成后,技术实现不应从运动学重写开始,而应从 `INT-01 sim_task 固定 tick 集成` 开始。它是前端、WASM 和后端融合的真正缺口。 + +| ID | 任务 | 主要变更 | 直接测试/产物 | 依赖 | +|---|---|---|---|---| +| INT-01 | 建立 `sim_task` 状态对象和 C ABI | 统一 interpreter/TP/motion/kinematics/feedback/HAL/IO/Python 引用;定义 mode、program、line、pause、abort、alarm、state hash | `wasm-core/include/sim_task.h`、native/WASM task test、ABI report | M1 ABI、protocol v1 | +| INT-02 | `PROGRAM_LOAD/RUN/PAUSE/RESUME/ABORT/STEP` 真正执行 | Worker workspace 文件 -> G-code 行流 -> `sim_interpreter_execute`;未知命令和非法模式 fail-closed,不再通用 `verified` | `minimal_xyz` E2E、`tests/interp/*` 迁移结果、command golden | INT-01、configuration resolver | +| INT-03 | TP -> IK -> motion -> FK 固定 tick | 复用 `sim_kinematics_motion`、feedback 和 `sim_motion_queue`;实现事务回滚、queue-buster、奇异/限位错误 | `motion/g0`、`trajectory-planner`、gantry/TRT scenarios;首个差异 tick report | INT-01/02、K0-K6 | +| INT-04 | HAL/IO/Python 进入同一 tick | 配置图实例化 HAL;设备命令由 `sim_io` 作为权威源;M6、主轴、探针、estop 与 Task 状态互锁 | `io-startup`、`toolchanger`、`hard-limits`、`remap/oword-pycall` | INT-03、M3 RT/PY | +| INT-05 | 配置资源到 runtime 的启动闭包 | 将 resolver 的 resolved rows 转成可加载 workspace;对 981 blocked rows 给出 source/line/code;处理 `.var/.tbl/.ngc/.py/.xml/.hal` | `REG-01` 每入口 ready/blocked/error;configuration startup report | M3-CFG、INT-01 | +| INT-06 | Python/remap/filter 真执行 | Pyodide Worker bridge 加入 session/task callback、参数、异常、时间/内存限额;宿主能力一律拒绝 | `tests/remap/*`、python session/replay、bridge report | INT-02、M3-PY | +| INT-07 | 真实 OPFS 与 Worker 重启恢复 | 实现 OPFS adapter、journal commit、checkpoint、lease 和 offline cache;MemoryStorage 仅保留测试替身 | browser reload/crash/quota/offline matrix、migration/rollback report | M3-FS、INT-01 | +| INT-08 | 前端改为权威 Task snapshot | `WorkerClient` 增加 program/MDI/jog/override/subscribe;reducer 消费真实 snapshot/event,不用本地假状态 | 浏览器 E2E:运行/暂停/单步/报警/恢复;FE report 升级 | INT-02/04、FE-M1..M4 | +| INT-09 | 3D 轨迹和机床模型 | 由 snapshot 的 pose/joints/toolpath 驱动 Three.js;支持 axis/gantry/TRT/机器人姿态和故障可视化 | vismach samples、canvas/WebGL pixel/nonblank、性能矩阵 | INT-03、INT-08 | +| INT-10 | 全量配置启动和差异闭环 | 运行 233 entrypoints;按 ready/blocked/error 分类,不删样本;逐个关联 source/resource/component | `coverage/full-configuration-startup-matrix.report.json`、native/WASM/Worker summaries | INT-05/06/07/08 | + +任务顺序中,INT-01 至 INT-04 是功能主链,INT-05 至 INT-07 是运行时闭环,INT-08/09 是前端融合,INT-10 是规模化回归。任何任务都不能通过降低检查、删除 blocked 样本或把资源缺失标为 `REAL-EXCLUDED` 来“完成”。 + +## 6. 测试和验收办法 + +### 6.1 四层测试 + +| 层 | 测试内容 | 当前命令/来源 | 作用 | +|---|---|---|---| +| L0 基线 | context、source、manifest、pointer、下载索引 | `validate_baseline.py`、`validate_task_pointer.py`、脚本 unittest | 防止在错误源码或错误任务上工作 | +| L1 native C | `sim_*` ABI、posemath、interpreter、motion、kinematics、HAL、IO | `ctest --test-dir build-native --output-on-failure`,15 项 | 算法和错误码的快速反馈 | +| L2 WASM/Worker | Emscripten exports、协议、配置/HAL preflight、设备、Python、replay/recovery | `node web/worker/test_*.mjs`、WASM tests | 验证浏览器运行时边界 | +| L3 浏览器/E2E | WorkerClient、命令幂等、UI 状态、OPFS、离线、3D、跨浏览器 | `web/app/test_*.mjs`、后续 Playwright/浏览器矩阵 | 验证用户可见工作流和恢复 | + +### 6.2 每个功能切片的最小验收包 + +每个 INT/REG 任务必须同时包含: + +- 一个正常案例,例如 `G0 X1` 或 spindle start/stop; +- 一个边界案例,例如重复 modal、queue full、奇异点、超速或缺资源; +- 一个错误/报警案例,例如 syntax、limit、estop、Python capability denied; +- 一个 pause/resume/abort/recovery 案例; +- 一个持久化或 Worker 重启案例(适用时); +- native 结果、WASM 结果、事件顺序、当前行、错误码、状态 hash 和首个 divergent tick。 + +### 6.3 参考命令 + +开始任何实现任务前执行: + +```bash +python3 scripts/validate_baseline.py +python3 scripts/validate_download_index.py +python3 scripts/validate_task_pointer.py +python3 -m unittest discover -s scripts -p 'test_*.py' -v +ctest --test-dir build-native --output-on-failure +``` + +完成一个核心垂直切片后至少执行: + +```bash +node web/worker/test_wasm_session.mjs +node web/worker/test_sim_session.mjs +node web/worker/test_configuration_preflight.mjs +node web/worker/test_hal_preflight.mjs +node web/worker/test_virtual_devices.mjs +node web/worker/test_workspace_session_integration.mjs +node web/app/test_frontend_m1.mjs +node web/app/test_frontend_m2.mjs +node web/app/test_frontend_m3.mjs +node web/app/test_frontend_m4.mjs +``` + +## 7. 当前阻塞和处理原则 + +| 阻塞 | 数量/现象 | 正确处理 | +|---|---:|---| +| manifest seed 无 owner | 6,249 | owner assignment;不能本地猜测并升级 accepted | +| 资源闭包 | 997 missing、91 unresolved、59 unindexed、3 unregistered startup | 按 source/line 逐项补资源或经审查明确阻断;不能 basename 猜测覆盖 | +| 官方 oracle | 锁定来源缺少 `linuxcnc/unit_tests/kinematics/**` 和 recovery roots | 补充真实官方输入并新建 context,或由 owner 批准替代 oracle;不能把 matrixkins 冒充官方 | +| 浏览器矩阵 | context 中 Chromium/Firefox/Safari minimum 为 TBD;当前只测 Chromium | owner 锁定最低版本,执行三浏览器 workflow/offline/fault/performance | +| 实现许可证 | 57 个 Web/WASM 条目 `blocked_unassigned` | 完成 SPDX/NOTICE 决策;上游 LinuxCNC 许可证不能自动覆盖新实现 | +| 端到端 Task | `sim_api` bootstrap,Worker 未串联 interpreter/TP/IK/HAL/IO | 以 INT-01 为技术起点,先固定状态/ABI/错误回滚,再扩展功能 | + +## 8. 任务完成定义 + +一个任务只有在以下条件全部满足时才能标记 `verified`: + +1. 代码、manifest/source map 和协议/ABI 变更互相可追溯; +2. 正常、边界、错误、恢复测试均存在,且测试不依赖删除样本或静默降级; +3. native/WASM(适用时)和 Worker/browser(适用时)结果可重复; +4. snapshot、event、checkpoint、operation ledger 的状态 hash 能重建; +5. 性能、内存、OPFS 配额和离线影响有记录; +6. 失败时有明确 error code、source path/line、rollback point 和下一任务; +7. 仍然受发布 blocker 影响的任务保持 `partial`/`blocked`,不得伪装为 `accepted`。 + +项目最终完成不是“页面能点”或“15 个 CTest 全绿”,而是:任意纳入范围的 `configs/sim` 入口都能进入 `READY` 或给出可诊断的 fail-closed 错误;典型 G-code/IO/运动学/恢复场景在 native、WASM 和浏览器状态上可复现;所有来源、资源、测试、oracle、许可证和排除项都有闭合证据。 diff --git a/docs/task-history.md b/docs/task-history.md index 6198733..dd57959 100644 --- a/docs/task-history.md +++ b/docs/task-history.md @@ -67,6 +67,7 @@ | `K8-REL-16` | 在没有 owner 决定或新锁定输入时重建 closure/audit/remediation/owner-review 证据;六项 blocker identity、context consistency 和 `STOP_RELEASE` 均 verified,owner_inputs=0、gate_refresh=blocked,context lock 未修改,任务保持 blocked | `coverage/owner-review-gate-refresh.report.json`, `coverage/release-candidate-gate.report.json`, `coverage/post-release-audit.report.json`, `scripts/generate_owner_review_gate_refresh.py`, `scripts/test_owner_review_gate_refresh.py` | | `PROJECT-AUDIT-01` | 整体审计确认 K0-K6 运动学实现和 15 项 native CTest 已通过;运动学 `partial/blocked` 仅因锁定来源缺少官方 oracle;当前开发瓶颈转为配置启动闭包,下一任务为 M3-CFG-01,K8 release 六项 blocker/`STOP_RELEASE` 独立保留 | `docs/linuxcnc-web-wasm-next-work.md`, `coverage/kinematics-registry.report.json`, `coverage/kinematics-motion.report.json`, `coverage/kinematics-feedback.report.json`, `coverage/kinematics-home-limit.report.json`, `coverage/configuration-regression.report.json` | | `PROJECT-AUDIT-02` | 重新梳理任务图确认 M3/FE/REG 实现证据均已 verified;K8-REL-17 owner_inputs=0、gate_refresh=blocked,发现旧 Next 自引用风险;生成 GOV-01 readiness 后进入不自动重试的 WAIT-OWNER-01,六项 blocker 和 `STOP_RELEASE` 保持 verified | `docs/linuxcnc-web-wasm-task-map.md`, `coverage/project-task-readiness.report.json`, `nextTask.md` | +| `OWNER-INTAKE-01` | 建立六项 owner decision 输入模板、结构/identity/context/evidence hash 验证器和 blocked report;当前输入文件缺失,验证结果为 blocked,gate refresh 永远 blocked,不修改 context lock 或 release gate | `coverage/owner-decision-intake.template.json`, `coverage/owner-decision-intake.report.json`, `docs/linuxcnc-web-wasm-owner-decision-intake.md`, `scripts/validate_owner_decision_intake.py`, `scripts/test_owner_decision_intake.py` | | `M3-CFG-01` | 确定性配置 resolver 和 Worker preflight 完成;16,093 条资源全部分类,15,112 resolved、981 blocked;显式 `LIB:`、参数尾缀、相对路径、仓库外路径和宿主路径均有明确策略;原始索引 hash、context lock、187 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `web/workspace/configuration.mjs`, `web/worker/sim-session.mjs`, `coverage/configuration-startup-closure.report.json`, `scripts/generate_configuration_startup_closure.py`, `scripts/test_configuration_startup_closure.py`, `web/workspace/test_configuration.mjs`, `web/worker/test_configuration_preflight.mjs` | | `M3-RT-02` | 生成 145 个 `.comp` 组件 manifest、1,220 pins、140 params、530 functions 和 193 个 HAL 文件的 4,566 条配置图记录;manifest、容量、资源边和动态加载拒绝检查 verified,29 个 `loadusr` 与 124 个变量化/未声明 `loadrt` 保持 blocked;190 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/hal-component-manifest.report.json`, `scripts/generate_hal_component_manifest.py`, `scripts/test_hal_component_manifest.py`, `web/workspace/hal-configuration.mjs`, `web/worker/sim-session.mjs`, `web/worker/test_hal_preflight.mjs` | | `M3-RT-03` | 虚拟设备故障矩阵完成;26 个 spindle/tool/probe/limit/estop/非有限/恢复场景进入 EventLog,连续执行与 replay state hash 一致,设备 checkpoint 增量恢复通过;192 个脚本测试、15 个 native CTest 和 Worker/workspace 测试通过 | `coverage/virtual-device-fault-matrix.report.json`, `scripts/generate_virtual_device_fault_matrix.py`, `scripts/test_virtual_device_fault_matrix.py`, `web/worker/virtual-devices.mjs`, `web/worker/test_virtual_devices.mjs`, `web/worker/test_device_recovery.mjs` | diff --git a/nextTask.md b/nextTask.md index 4f4812e..11038f4 100644 --- a/nextTask.md +++ b/nextTask.md @@ -9,7 +9,7 @@ pointer_version: 3 updated: 2026-08-21 context_id: linuxcnc-2.10.0-pre0-wasm-m1-py-c1 milestone: K8 -last_verified_checkpoint: PROJECT-AUDIT-02 +last_verified_checkpoint: OWNER-INTAKE-01 task: id: WAIT-OWNER-01 status: blocked @@ -43,11 +43,11 @@ python3 scripts/validate_task_pointer.py **目标**:等待六项 owner decision 或新的 locked context input;期间不重跑 K8 release gate、不创建新 context、不重复执行已完成 implementation matrix。 -**直接输入**:`docs/linuxcnc-web-wasm-task-map.md`、`coverage/project-task-readiness.report.json`、全部 M3/FE/REG 报告、`coverage/release-candidate-gate.report.json`、`coverage/post-release-audit.report.json`、`coverage/owner-review-gate-refresh.report.json`、`context-lock.json`。 +**直接输入**:`coverage/owner-decision-intake.template.json`、`coverage/owner-decisions.json`(当前不存在)、`scripts/validate_owner_decision_intake.py`、`context-lock.json`、`coverage/project-task-readiness.report.json`。 -**必须产出**:保持 `coverage/project-task-readiness.report.json` 和所有实现报告作为 blocked evidence;owner input 到位前不生成新 context、不刷新 gate。 +**必须产出**:`coverage/owner-decision-intake.template.json`、`docs/linuxcnc-web-wasm-owner-decision-intake.md`、`coverage/owner-decision-intake.report.json` 和验证器;当前 `owner-decisions.json` 缺失时只生成 blocked report,不创建 context、不刷新 gate。 -**完成条件**:六项 owner decision 或新 locked context input 可定位后才解除 blocked;当前 `owner_inputs=0`、gate refresh=blocked、`STOP_RELEASE` verified;不自动回到 K8-REL-17。 +**完成条件**:模板精确包含六项 blocker;缺失、重复、未知、context mismatch、字段不全和非 approve decision 均 blocked;完整六项 approve 只代表 intake shape verified,gate refresh 仍 blocked,必须另经新 context 审查;`context-lock.json` 不被验证器修改。 **非目标**:不修改 kinematics 算法或 ABI/protocol/schema/context lock;不替 owner 做 manifest/许可证/browser/oracle 决策;不实现真实硬件驱动;不升级 `accepted`,不清除 K8 release blockers。 @@ -55,7 +55,7 @@ python3 scripts/validate_task_pointer.py ## Next -本任务是 external wait,不执行自动重试;只有六项 owner decision 或新 locked context input 到位时,才新建 context 并重新打开 K8 release task。 +本任务是 external wait,不执行自动重试;owner 将 decision 文件放置为 `coverage/owner-decisions.json` 后运行 `python3 scripts/validate_owner_decision_intake.py`。只有验证通过并完成新 context 审查后,才重新打开 K8 release task。 ## Verify @@ -66,6 +66,8 @@ python3 scripts/generate_virtual_device_fault_matrix.py python3 scripts/generate_workspace_session_integration.py python3 scripts/generate_project_task_readiness.py python3 scripts/test_project_task_readiness.py +python3 scripts/validate_owner_decision_intake.py +python3 scripts/test_owner_decision_intake.py node python-runtime/test_runtime.mjs node web/worker/test_python_bridge.mjs python3 scripts/generate_python_worker_bridge.py diff --git a/scripts/test_owner_decision_intake.py b/scripts/test_owner_decision_intake.py new file mode 100644 index 0000000..2cbeecb --- /dev/null +++ b/scripts/test_owner_decision_intake.py @@ -0,0 +1,50 @@ +import json +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[0])) +from validate_owner_decision_intake import BLOCKERS, validate + +ROOT = Path(__file__).resolve().parents[1] + + +def complete_input(context_id): + return {"schema": "owner-decision-intake.v1", "context_id": context_id, "decision_set_id": "test", "decisions": [{"blocker": blocker, "decision": "approve", "owner": "owner", "reviewer": "reviewer", "rationale": "reviewed", "evidence": [{"path": "context-lock.json", "sha256": "0" * 64}], "new_context_required": True} for blocker in BLOCKERS]} + + +class OwnerDecisionIntakeTests(unittest.TestCase): + def test_missing_input_fails_closed(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) + report = validate(root, Path("owner-decisions.json"), Path("report.json")) + self.assertEqual(report["status"], "blocked") + self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked") + self.assertEqual(report["checks"]["context_lock_unchanged"]["status"], "verified") + + def test_complete_shape_is_valid_but_does_not_refresh_gate(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) + (root / "owner-decisions.json").write_text(json.dumps(complete_input("ctx"))) + report = validate(root, Path("owner-decisions.json"), Path("report.json")) + self.assertEqual(report["status"], "verified") + self.assertEqual(report["checks"]["decision_approval"]["status"], "verified") + self.assertEqual(report["checks"]["gate_refresh_allowed"]["status"], "blocked") + + def test_duplicate_blocker_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "context-lock.json").write_text(json.dumps({"context_id": "ctx"})) + value = complete_input("ctx") + value["decisions"][-1]["blocker"] = BLOCKERS[0] + (root / "owner-decisions.json").write_text(json.dumps(value)) + report = validate(root, Path("owner-decisions.json"), Path("report.json")) + self.assertEqual(report["status"], "blocked") + self.assertIn(BLOCKERS[5], report["checks"]["six_blocker_identity"]["missing"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/validate_owner_decision_intake.py b/scripts/validate_owner_decision_intake.py new file mode 100644 index 0000000..8dffdd7 --- /dev/null +++ b/scripts/validate_owner_decision_intake.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Validate the external six-decision owner input without mutating release state.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +from typing import Any + +LOCK = Path("context-lock.json") +INPUT = Path("coverage/owner-decisions.json") +OUTPUT = Path("coverage/owner-decision-intake.report.json") +TEMPLATE = Path("coverage/owner-decision-intake.template.json") +BLOCKERS = ("manifest_seed_unassigned", "accepted_entries_missing", "official_oracle_missing", "configuration_resources_missing", "browser_matrix_unverified", "license_closure_incomplete") +DECISIONS = {"approve", "reject", "defer"} + + +def canonical(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def digest(value: Any) -> str: + return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest() + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load_json(path: Path) -> tuple[dict[str, Any], str | None]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): raise ValueError("root must be object") + return value, None + except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: + return {}, str(exc) + + +def validate(root: Path, input_path: Path = INPUT, output_path: Path = OUTPUT) -> dict[str, Any]: + root = root.resolve() + lock, lock_error = load_json(root / LOCK) + value, input_error = load_json(root / input_path) + input_present = (root / input_path).is_file() + errors = [error for error in (lock_error, input_error) if error] + decisions = value.get("decisions") if isinstance(value.get("decisions"), list) else [] + observed = [row.get("blocker") for row in decisions if isinstance(row, dict)] + duplicate_ids = sorted({blocker for blocker in observed if observed.count(blocker) > 1 and blocker is not None}) + missing_ids = sorted(set(BLOCKERS) - set(observed)) + unknown_ids = sorted(set(observed) - set(BLOCKERS)) + row_errors: list[dict[str, Any]] = [] + for index, row in enumerate(decisions): + if not isinstance(row, dict): + row_errors.append({"index": index, "error": "decision_not_object"}); continue + if row.get("blocker") not in BLOCKERS: row_errors.append({"index": index, "error": "unknown_blocker"}) + if row.get("decision") not in DECISIONS: row_errors.append({"index": index, "error": "decision_invalid"}) + for field in ("owner", "reviewer", "rationale"): + if not isinstance(row.get(field), str) or not row[field].strip(): row_errors.append({"index": index, "error": f"{field}_missing"}) + if not isinstance(row.get("new_context_required"), bool): row_errors.append({"index": index, "error": "new_context_required_invalid"}) + evidence = row.get("evidence") + if not isinstance(evidence, list) or not evidence: row_errors.append({"index": index, "error": "evidence_missing"}) + else: + for evidence_row in evidence: + if not isinstance(evidence_row, dict) or not isinstance(evidence_row.get("path"), str) or not isinstance(evidence_row.get("sha256"), str) or len(evidence_row["sha256"]) != 64: + row_errors.append({"index": index, "error": "evidence_invalid"}) + context_ok = bool(lock.get("context_id")) and value.get("context_id") == lock.get("context_id") if input_present else False + shape_ok = input_present and not errors and value.get("schema") == "owner-decision-intake.v1" and len(decisions) == 6 and not duplicate_ids and not missing_ids and not unknown_ids and not row_errors and context_ok + all_approved = shape_ok and all(row.get("decision") == "approve" for row in decisions) + checks = { + "input_file_present": {"status": "verified" if input_present else "blocked", "path": input_path.as_posix()}, + "schema_shape": {"status": "verified" if shape_ok else "blocked", "schema": value.get("schema")}, + "six_blocker_identity": {"status": "verified" if not missing_ids and not unknown_ids and not duplicate_ids and len(decisions) == 6 else "blocked", "missing": missing_ids, "unknown": unknown_ids, "duplicates": duplicate_ids}, + "context_consistency": {"status": "verified" if context_ok else "blocked", "locked": lock.get("context_id"), "input": value.get("context_id")}, + "decision_approval": {"status": "verified" if all_approved else "blocked", "approved": sum(row.get("decision") == "approve" for row in decisions if isinstance(row, dict)), "required": 6}, + "gate_refresh_allowed": {"status": "blocked", "reason": "owner intake validation never refreshes the gate; reviewed new context is required after all six approvals"}, + "context_lock_unchanged": {"status": "verified", "action": "no mutation"}, + } + report: dict[str, Any] = { + "schema": "owner-decision-intake-report.v1", + "context_id": lock.get("context_id"), + "task_id": "K8-REL-18", + "status": "verified" if shape_ok else "blocked", + "implementation_status": "verified", + "release_status": "blocked_external_owner", + "candidate_eligible": False, + "release_action": "STOP_RELEASE", + "input": {"path": input_path.as_posix(), "status": "present" if input_present else "missing", "sha256": sha256(root / input_path) if input_present else None}, + "template": {"path": TEMPLATE.as_posix(), "sha256": sha256(root / TEMPLATE) if (root / TEMPLATE).is_file() else None}, + "checks": checks, + "errors": errors + row_errors, + "next_transition": "wait for owner-decisions.json or new locked context input; do not rerun browser or implementation matrices", + "report_sha256": None, + } + report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"}) + destination = output_path if output_path.is_absolute() else root / output_path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") + return report + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--input", type=Path, default=INPUT) + parser.add_argument("--output", type=Path, default=OUTPUT) + args = parser.parse_args() + report = validate(args.repo_root.resolve(), args.input, args.output) + print(f"owner_decision_intake={report['status']} present={report['input']['status']} refresh={report['checks']['gate_refresh_allowed']['status']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())