新增项目发布 gate action plan API

This commit is contained in:
2026-06-16 10:51:18 +08:00
parent 1d383b475c
commit 9abca15d60
9 changed files with 185 additions and 9 deletions

View File

@@ -9,7 +9,7 @@ turn-by-turn continuation notes and next-batch execution records.
## Current Status ## Current Status
- Active continuation file: `text14.txt` - Active continuation file: `text14.txt`
- Latest completed batch: project release readiness summary view-model API - Latest completed batch: project release gate action-plan API
- Latest relevant commit: use `git log -1 --oneline` after each committed batch - Latest relevant commit: use `git log -1 --oneline` after each committed batch
- Working tree at tracker creation: clean - Working tree at tracker creation: clean
- Required policy: LinuxCNC remains the only CNC semantic source - Required policy: LinuxCNC remains the only CNC semantic source
@@ -43,7 +43,7 @@ The project is complete for the current scope when all of the following are true
| Native nc_files baseline | Stable | Last recorded Layer 1: `total 107`, `pass 101`, `expected_fail 6`, `unexpected_fail 0`. | | Native nc_files baseline | Stable | Last recorded Layer 1: `total 107`, `pass 101`, `expected_fail 6`, `unexpected_fail 0`. |
| Native sim configs baseline | Stable | Last recorded Layer 2: `total 159`, `pass 151`, `expected_fail 8`, `unexpected_fail 0`. | | Native sim configs baseline | Stable | Last recorded Layer 2: `total 159`, `pass 151`, `expected_fail 8`, `unexpected_fail 0`. |
| OPFS/session persistence | Stable for current scope | Browser smoke passes, SDK re-exports OPFS/session helpers including `readMachineSessionReadiness()` and `createMachineSessionPersistenceSummary()`, the UI exposes `getMachineSessionPersistenceSummary()`, and `docs/opfs-session-persistence.md` defines the project-level release gate. | | OPFS/session persistence | Stable for current scope | Browser smoke passes, SDK re-exports OPFS/session helpers including `readMachineSessionReadiness()` and `createMachineSessionPersistenceSummary()`, the UI exposes `getMachineSessionPersistenceSummary()`, and `docs/opfs-session-persistence.md` defines the project-level release gate. |
| SDK/API surface | Stable for current scope | SDK re-exports core, OPFS/session readiness/load helpers, sim-config staging, project release gate manifest/result matrix/readiness summary view-model, project release readiness report/artifact validation plus URL loading workflow, INI panel entry/launcher/control-page/shell/workflow overview helpers, the session readiness workflow report, and the shell API surface inventory with a Node surface smoke. | | SDK/API surface | Stable for current scope | SDK re-exports core, OPFS/session readiness/load helpers, sim-config staging, project release gate manifest/result matrix/action-plan/readiness summary view-model, project release readiness report/artifact validation plus URL loading workflow, INI panel entry/launcher/control-page/shell/workflow overview helpers, the session readiness workflow report, and the shell API surface inventory with a Node surface smoke. |
| Browser/UI workflow | Stable for current scope | Workflow overview embedding mount DOM closure is complete, and INI panel session workflows plus read-only control/shell handoff expose, render, fetch, validate, and report OPFS/session readiness, OPFS/session persistence summary, release readiness artifact validation/rendering, and API surface inventory before loading or reading WASM session state. | | Browser/UI workflow | Stable for current scope | Workflow overview embedding mount DOM closure is complete, and INI panel session workflows plus read-only control/shell handoff expose, render, fetch, validate, and report OPFS/session readiness, OPFS/session persistence summary, release readiness artifact validation/rendering, and API surface inventory before loading or reading WASM session state. |
| Workflow overview embedding | Stable for current scope | Report/display/render, DOM contract/readiness/renderer, mount result/display/render, mount DOM contract/readiness/renderer/wrapper exist. | | Workflow overview embedding | Stable for current scope | Report/display/render, DOM contract/readiness/renderer, mount result/display/render, mount DOM contract/readiness/renderer/wrapper exist. |
| Host/runtime boundary proof | Stable blocked state | Host smoke passes; runtime families remain blocked and `docs/host-runtime-boundary-handoff.md` records host readiness, promotion blockers, dispatch, and evidence gates. | | Host/runtime boundary proof | Stable blocked state | Host smoke passes; runtime families remain blocked and `docs/host-runtime-boundary-handoff.md` records host readiness, promotion blockers, dispatch, and evidence gates. |

View File

@@ -3308,6 +3308,67 @@ ui_node_smokes=ok
继续只做实质能力推进。优先选择可执行 gate、SDK/API surface、或真实 browser workflow 继续只做实质能力推进。优先选择可执行 gate、SDK/API surface、或真实 browser workflow
不要做只整理断言或局部常量化的批次。 不要做只整理断言或局部常量化的批次。
四十五、2026-06-16 继续执行记录project release gate action-plan SDK API
本轮继续按“每批必须产出可调用 API、可验证 workflow、可执行 gate、或真实 UI/browser 能力”推进,
把 project release gate matrix 进一步提升为外部 CI/SDK 可直接消费的 action-plan API。外部工具现在可以
从 observed outputs、explicit gate results 或现成 matrix 得到 pending gate commands、next command、
display rows 和可展示/可执行的 shell script而不需要复制 release gate manifest 顺序或匹配逻辑。
完成内容:
- `runtime/sdk/src/project-release-readiness.js` 新增:
- `createProjectReleaseGateActionPlan()`
- 新 action plan 返回:
- `apiName: project-release-gate-action-plan`
- `planVersion: 1`
- `phase/ready`
- `gateCount/completedCount/pendingCount`
- `completedGateIds/pendingGateIds`
- `nextGateId/nextCommand`
- `commands[]`
- `rows[]`
- `shellScript`
- `createProjectReleaseReadinessReport()` 现在嵌入 `gateActionPlan`,与 `gateManifest` 和
`gateResultMatrix` 使用同一份 evidence
- `runtime/sdk/src/index.js` re-export 新 action-plan API
- `verify_project_release_gate_manifest.mjs` 覆盖 partial matrix action plan、next command、pending
commands 和 ready shellScript
- `verify_sdk_surface.mjs` 覆盖 SDK export、默认 waiting action plan、逐项 full-pass shellScript
- `runtime/sdk/README.md`、`docs/project-release-handoff.md`、`verify_project_release_handoff_docs.mjs`
记录 action-plan API
- `PROJECT_COMPLETION_TRACKER.md` 同步 latest completed batch 和 SDK/API surface 状态;
- 未新增控制按钮;
- 未在 SDK 中执行 shell command
- 未解析 G-code
- 未解释 canonical events
- 未新增 JS CNC 语义;
- 未运行 opt-in runtime probes
- 未 promotion blocked runtime family
- 未改变 LinuxCNC interpreter、tool、parameter、planner、kinematics、remap 或 LinuxCNC-owned
runtime semantics。
验证已通过(先行 targeted
```bash
wasm-port/tests/sdk/node/verify_project_release_gate_manifest.sh
wasm-port/tests/sdk/node/verify_sdk_surface.sh
wasm-port/tests/docs/node/verify_project_release_handoff_docs.sh
```
关键输出:
```text
project_release_gate_manifest_node_smoke=ok
sdk_surface_node_smoke=ok
project_release_handoff_docs_node_smoke=ok
```
下一步工作计划:
继续只做实质能力推进。优先选择可执行 gate、SDK/API surface、或真实 browser workflow
不要做只整理断言或局部常量化的批次。
四十二、2026-06-16 继续执行记录project release gate manifest SDK API/gate 四十二、2026-06-16 继续执行记录project release gate manifest SDK API/gate
本轮继续按“每批必须产出可调用 API、可验证 workflow、可执行 gate、或真实 UI/browser 能力”推进, 本轮继续按“每批必须产出可调用 API、可验证 workflow、可执行 gate、或真实 UI/browser 能力”推进,

View File

@@ -37,10 +37,12 @@ continuation records are in `../text14.txt`.
- CI dashboards and external SDK callers can use - CI dashboards and external SDK callers can use
`createProjectReleaseGateManifest()` to discover required gate commands and `createProjectReleaseGateManifest()` to discover required gate commands and
expected smoke outputs, `createProjectReleaseGateResultMatrix()` to map expected smoke outputs, `createProjectReleaseGateResultMatrix()` to map
observed outputs onto passed/unknown gate rows, and observed outputs onto passed/unknown gate rows,
`createProjectReleaseGateActionPlan()` to derive pending gate commands, the
next command, display rows, and a shell script from that matrix, and
`createProjectReleaseReadinessReport()` for a machine-readable release `createProjectReleaseReadinessReport()` for a machine-readable release
readiness report covering the same manifest, result matrix, sim-config readiness report covering the same manifest, result matrix, action plan,
inventory baseline, and blocked runtime families. sim-config inventory baseline, and blocked runtime families.
`createProjectReleaseReadinessSummaryViewModel()` turns that report into `createProjectReleaseReadinessSummaryViewModel()` turns that report into
stable status text and rows for dashboards. The report helper is evidence stable status text and rows for dashboards. The report helper is evidence
driven and only reports `ready: true` when the caller supplies release gate driven and only reports `ready: true` when the caller supplies release gate

View File

@@ -51,6 +51,7 @@ import {
createMachineSessionPersistenceRenderState, createMachineSessionPersistenceRenderState,
createMachineSessionPersistenceSummary, createMachineSessionPersistenceSummary,
createMachineSessionSnapshotPayload, createMachineSessionSnapshotPayload,
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest, createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix, createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation, createProjectReleaseReadinessArtifactValidation,
@@ -196,11 +197,15 @@ It does not implement or reinterpret LinuxCNC error semantics.
commands, expected smoke outputs, and gate count for CI dashboards and external commands, expected smoke outputs, and gate count for CI dashboards and external
SDK callers. `createProjectReleaseGateResultMatrix()` maps observed gate output SDK callers. `createProjectReleaseGateResultMatrix()` maps observed gate output
and explicit gate results onto that manifest as stable `passed`/`unknown` rows. and explicit gate results onto that manifest as stable `passed`/`unknown` rows.
`createProjectReleaseGateActionPlan()` turns that matrix into pending gate
commands, the next gate command, stable display rows, and a shell script that
external CI can execute or show without duplicating the manifest order.
`createProjectReleaseReadinessReport()` returns a machine-readable release `createProjectReleaseReadinessReport()` returns a machine-readable release
readiness report that embeds the same manifest and result matrix beside the readiness report that embeds the same manifest, result matrix, and action plan
sim-config inventory baseline and blocked runtime families. It is evidence beside the sim-config inventory baseline and blocked runtime families. It is
driven: the default report is `ready: false` until the caller supplies observed evidence driven: the default report is `ready: false` until the caller supplies
gate output such as `project_release_gate=ok` or explicit gate results. observed gate output such as `project_release_gate=ok` or explicit gate
results.
`createProjectReleaseReadinessSummaryViewModel()` turns the report into stable `createProjectReleaseReadinessSummaryViewModel()` turns the report into stable
status text, a status line, and display rows for dashboards without executing status text, a status line, and display rows for dashboards without executing
any gate. any gate.

View File

@@ -1,6 +1,7 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js"; export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js"; export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export { export {
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest, createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix, createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation, createProjectReleaseReadinessArtifactValidation,

View File

@@ -145,6 +145,59 @@ export function createProjectReleaseGateResultMatrix({
}; };
} }
export function createProjectReleaseGateActionPlan({
gateResultMatrix = null,
gateResults = {},
observedOutputs = [],
manifest = createProjectReleaseGateManifest(),
} = {}) {
const matrix = gateResultMatrix && typeof gateResultMatrix === "object"
? gateResultMatrix
: createProjectReleaseGateResultMatrix({ gateResults, observedOutputs, manifest });
const rows = Array.isArray(matrix.rows) ? matrix.rows : [];
const pendingRows = rows.filter(({ passed }) => passed !== true);
const completedRows = rows.filter(({ passed }) => passed === true);
const commands = pendingRows.map((gate, index) => ({
step: index + 1,
gateId: gate.id,
label: gate.label,
command: gate.command,
expectedOutput: gate.expectedOutput ?? null,
}));
const nextGate = commands[0] ?? null;
const shellScript = commands.length > 0
? [
"#!/usr/bin/env bash",
"set -euo pipefail",
"",
...commands.map(({ command }) => command),
"",
].join("\n")
: "# Project release gates are already marked passed.\n";
return {
apiName: "project-release-gate-action-plan",
planVersion: 1,
phase: commands.length === 0 ? "ready" : "waiting",
ready: commands.length === 0,
gateCount: rows.length,
completedCount: completedRows.length,
pendingCount: pendingRows.length,
completedGateIds: completedRows.map(({ id }) => id),
pendingGateIds: pendingRows.map(({ id }) => id),
nextGateId: nextGate?.gateId ?? null,
nextCommand: nextGate?.command ?? null,
commands,
shellScript,
rows: commands.map(({ gateId, label, command, expectedOutput }) => ({
id: gateId,
label,
value: command,
expectedOutput,
})),
};
}
export function createProjectReleaseReadinessReport({ export function createProjectReleaseReadinessReport({
gateResults = {}, gateResults = {},
observedOutputs = [], observedOutputs = [],
@@ -158,6 +211,10 @@ export function createProjectReleaseReadinessReport({
observedOutputs, observedOutputs,
manifest: gateManifest, manifest: gateManifest,
}); });
const gateActionPlan = createProjectReleaseGateActionPlan({
gateResultMatrix,
manifest: gateManifest,
});
const gateRows = gateResultMatrix.rows; const gateRows = gateResultMatrix.rows;
const releaseGatePassed = gateRows.find(({ id }) => id === "project-release-gate")?.passed === true; const releaseGatePassed = gateRows.find(({ id }) => id === "project-release-gate")?.passed === true;
const inventory = { const inventory = {
@@ -193,6 +250,7 @@ export function createProjectReleaseReadinessReport({
promotedBlockedFamilies, promotedBlockedFamilies,
gateManifest, gateManifest,
gateResultMatrix, gateResultMatrix,
gateActionPlan,
gates: gateRows, gates: gateRows,
rows: [ rows: [
{ {

View File

@@ -33,6 +33,7 @@ for (const phrase of [
"runtime/sdk/src/index.js", "runtime/sdk/src/index.js",
"createProjectReleaseGateManifest()", "createProjectReleaseGateManifest()",
"createProjectReleaseGateResultMatrix()", "createProjectReleaseGateResultMatrix()",
"createProjectReleaseGateActionPlan()",
"createProjectReleaseReadinessReport()", "createProjectReleaseReadinessReport()",
"createProjectReleaseReadinessSummaryViewModel()", "createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()", "parseProjectReleaseReadinessArtifactJson()",
@@ -101,6 +102,7 @@ for (const phrase of [
"Project release readiness export", "Project release readiness export",
"createProjectReleaseGateManifest()", "createProjectReleaseGateManifest()",
"createProjectReleaseGateResultMatrix()", "createProjectReleaseGateResultMatrix()",
"createProjectReleaseGateActionPlan()",
"createProjectReleaseReadinessReport()", "createProjectReleaseReadinessReport()",
"createProjectReleaseReadinessSummaryViewModel()", "createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()", "parseProjectReleaseReadinessArtifactJson()",

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest, createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix, createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessReport, createProjectReleaseReadinessReport,
@@ -78,12 +79,47 @@ assert.deepEqual(
], ],
); );
const partialActionPlan = createProjectReleaseGateActionPlan({ gateResultMatrix: partialMatrix });
assert.equal(partialActionPlan.apiName, "project-release-gate-action-plan");
assert.equal(partialActionPlan.planVersion, 1);
assert.equal(partialActionPlan.phase, "waiting");
assert.equal(partialActionPlan.ready, false);
assert.equal(partialActionPlan.gateCount, 10);
assert.equal(partialActionPlan.completedCount, 3);
assert.equal(partialActionPlan.pendingCount, 7);
assert.deepEqual(partialActionPlan.completedGateIds, [
"diff-check",
"vendor-sync",
"release-artifact-url-browser",
]);
assert.deepEqual(partialActionPlan.pendingGateIds, [
"standalone-cnc-semantics",
"interp-wasm",
"sim-config-inventory",
"ini-panel-browser",
"ui-node-smokes",
"host-smokes",
"project-release-gate",
]);
assert.equal(partialActionPlan.nextGateId, "standalone-cnc-semantics");
assert.equal(partialActionPlan.nextCommand, "wasm-port/tools/verify_no_standalone_cnc_semantics.sh");
assert.equal(partialActionPlan.commands.length, 7);
assert.equal(partialActionPlan.commands[0].step, 1);
assert.equal(partialActionPlan.commands[0].gateId, "standalone-cnc-semantics");
assert.equal(partialActionPlan.rows[0].id, "standalone-cnc-semantics");
assert.match(partialActionPlan.shellScript, /^#!\/usr\/bin\/env bash\nset -euo pipefail\n/);
assert.match(partialActionPlan.shellScript, /wasm-port\/tests\/host\/verify_project_release_gate\.sh/);
const report = createProjectReleaseReadinessReport({ const report = createProjectReleaseReadinessReport({
gateResults: Object.fromEntries(manifest.gateIds.map((id) => [id, true])), gateResults: Object.fromEntries(manifest.gateIds.map((id) => [id, true])),
}); });
assert.deepEqual(report.gateManifest, manifest); assert.deepEqual(report.gateManifest, manifest);
assert.equal(report.gateResultMatrix.ready, true); assert.equal(report.gateResultMatrix.ready, true);
assert.equal(report.gateResultMatrix.passedCount, manifest.gateCount); assert.equal(report.gateResultMatrix.passedCount, manifest.gateCount);
assert.equal(report.gateActionPlan.ready, true);
assert.equal(report.gateActionPlan.pendingCount, 0);
assert.equal(report.gateActionPlan.nextCommand, null);
assert.equal(report.gateActionPlan.shellScript, "# Project release gates are already marked passed.\n");
assert.deepEqual(report.gates.map(({ id }) => id), manifest.gateIds); assert.deepEqual(report.gates.map(({ id }) => id), manifest.gateIds);
assert.equal(report.gates.length, manifest.gateCount); assert.equal(report.gates.length, manifest.gateCount);
assert.deepEqual(createProjectReleaseReadinessSummaryViewModel(report), { assert.deepEqual(createProjectReleaseReadinessSummaryViewModel(report), {

View File

@@ -32,6 +32,7 @@ import {
createMachineSessionPersistenceDisplayViewModel, createMachineSessionPersistenceDisplayViewModel,
createMachineSessionPersistenceRenderState, createMachineSessionPersistenceRenderState,
createMachineSessionPersistenceSummary, createMachineSessionPersistenceSummary,
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest, createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix, createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation, createProjectReleaseReadinessArtifactValidation,
@@ -86,6 +87,7 @@ const trackerText = readFileSync(resolve(root, "../PROJECT_COMPLETION_TRACKER.md
const requiredExports = [ const requiredExports = [
["createLinuxCncIniSdk", createLinuxCncIniSdk], ["createLinuxCncIniSdk", createLinuxCncIniSdk],
["createLinuxCncInterpSdk", createLinuxCncInterpSdk], ["createLinuxCncInterpSdk", createLinuxCncInterpSdk],
["createProjectReleaseGateActionPlan", createProjectReleaseGateActionPlan],
["createProjectReleaseGateManifest", createProjectReleaseGateManifest], ["createProjectReleaseGateManifest", createProjectReleaseGateManifest],
["createProjectReleaseGateResultMatrix", createProjectReleaseGateResultMatrix], ["createProjectReleaseGateResultMatrix", createProjectReleaseGateResultMatrix],
["createProjectReleaseReadinessArtifactValidation", createProjectReleaseReadinessArtifactValidation], ["createProjectReleaseReadinessArtifactValidation", createProjectReleaseReadinessArtifactValidation],
@@ -211,6 +213,9 @@ assert.equal(releaseReadinessWaiting.ready, false);
assert.deepEqual(releaseReadinessWaiting.missing, ["project-release-gate"]); assert.deepEqual(releaseReadinessWaiting.missing, ["project-release-gate"]);
assert.equal(releaseReadinessWaiting.simConfigInventory.unexpectedFail, 0); assert.equal(releaseReadinessWaiting.simConfigInventory.unexpectedFail, 0);
assert.deepEqual(releaseReadinessWaiting.promotedBlockedFamilies, []); assert.deepEqual(releaseReadinessWaiting.promotedBlockedFamilies, []);
assert.equal(releaseReadinessWaiting.gateActionPlan.apiName, "project-release-gate-action-plan");
assert.equal(releaseReadinessWaiting.gateActionPlan.pendingCount, 10);
assert.equal(releaseReadinessWaiting.gateActionPlan.nextGateId, "diff-check");
assert.equal( assert.equal(
releaseReadinessWaiting.rows.find(({ id }) => id === "blocked-runtime-families")?.value, releaseReadinessWaiting.rows.find(({ id }) => id === "blocked-runtime-families")?.value,
"L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP", "L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP",
@@ -222,6 +227,12 @@ assert.equal(releaseReadinessReady.ready, true);
assert.equal(releaseReadinessReady.releaseGate.passed, true); assert.equal(releaseReadinessReady.releaseGate.passed, true);
assert.deepEqual(releaseReadinessReady.gateManifest, createProjectReleaseGateManifest()); assert.deepEqual(releaseReadinessReady.gateManifest, createProjectReleaseGateManifest());
assert.equal(releaseReadinessReady.gateResultMatrix.rows.length, 10); assert.equal(releaseReadinessReady.gateResultMatrix.rows.length, 10);
assert.equal(createProjectReleaseGateActionPlan({
gateResultMatrix: releaseReadinessReady.gateResultMatrix,
}).ready, false);
assert.equal(createProjectReleaseGateActionPlan({
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
}).shellScript, "# Project release gates are already marked passed.\n");
assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons"); assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons");
assert.equal( assert.equal(
createProjectReleaseGateResultMatrix({ createProjectReleaseGateResultMatrix({