新增 release gate execution manifest API

This commit is contained in:
2026-06-16 17:20:21 +08:00
parent 2f6e8480bf
commit 741afc5103
13 changed files with 443 additions and 21 deletions

View File

@@ -54,6 +54,7 @@ import {
createMachineSessionPersistenceSummary,
createMachineSessionSnapshotPayload,
createProjectReleaseGateActionPlan,
createProjectReleaseGateExecutionManifest,
createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactJsonWorkflow,
@@ -208,12 +209,17 @@ 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.
`createProjectReleaseGateExecutionManifest()` packages the same gate manifest
with caller-provided explicit results and observed output evidence. Each row
includes the command, expected output, matched observed output when present,
evidence kind, and `passed`/`unknown` status so CI or browser dashboards can
render release-gate execution state without scraping shell scripts.
`createProjectReleaseReadinessReport()` returns a machine-readable release
readiness report that embeds the same manifest, result matrix, and action plan
beside the sim-config inventory baseline and blocked runtime families. It is
evidence driven: the default report is `ready: false` until the caller supplies
observed gate output such as `project_release_gate=ok` or explicit gate
results.
beside the gate execution manifest, sim-config inventory baseline, and blocked
runtime families. It is evidence driven: the default report is `ready: false`
until the caller supplies observed gate output such as `project_release_gate=ok`
or explicit gate results.
`createProjectReleaseReadinessSummaryViewModel()` turns the report into stable
status text, a status line, and display rows for dashboards without executing
any gate.
@@ -231,9 +237,9 @@ load that JSON with `parseProjectReleaseReadinessArtifactJson()` and validate
it with `createProjectReleaseReadinessArtifactValidation()` without copying the
host test assertions. The validation result includes `expectedGateCount` and
`expectedGateIds` so external callers can compare an artifact against the
current manifest, plus `gateManifestReady`, `gateResultMatrixReady`, and
`gateActionPlanReady` so stale artifacts that omit the embedded gate evidence
chain fail the same executable gate.
current manifest, plus `gateManifestReady`, `gateExecutionManifestReady`,
`gateResultMatrixReady`, and `gateActionPlanReady` so stale artifacts that omit
the embedded gate evidence chain fail the same executable gate.
`createProjectReleaseReadinessArtifactValidationSummaryViewModel()` turns that
validation result into stable status text and display rows for dashboards or
browser shells without reinterpreting the artifact fields.

View File

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

View File

@@ -98,6 +98,16 @@ function arrayOrEmpty(value) {
return Array.isArray(value) ? value : [];
}
function normalizeObservedOutput(value) {
if (Array.isArray(value)) {
return value.map((output) => String(output));
}
if (typeof value === "string") {
return [value];
}
return [];
}
export function createProjectReleaseGateManifest() {
return {
apiName: "project-release-gate-manifest",
@@ -113,6 +123,53 @@ export function createProjectReleaseGateManifest() {
};
}
export function createProjectReleaseGateExecutionManifest({
gateResults = {},
observedOutputs = [],
gateObservedOutputs = {},
manifest = createProjectReleaseGateManifest(),
} = {}) {
const globalOutputs = normalizeObservedOutput(observedOutputs);
const gates = arrayOrEmpty(manifest?.gates);
const rows = gates.map((gate) => {
const scopedOutputs = [
...normalizeObservedOutput(gateObservedOutputs?.[gate.id]),
...globalOutputs,
];
const matchedOutput = gate.expectedOutput
? scopedOutputs.find((output) => output.includes(gate.expectedOutput)) ?? null
: null;
const explicitPassed = gateResults?.[gate.id] === true;
const passed = explicitPassed || matchedOutput !== null;
return {
id: gate.id,
label: gate.label,
command: gate.command,
expectedOutput: gate.expectedOutput ?? null,
observedOutput: matchedOutput,
observedOutputCount: scopedOutputs.length,
status: passed ? "passed" : "unknown",
passed,
evidence: matchedOutput !== null
? "expected-output"
: (explicitPassed ? "explicit-result" : "missing"),
};
});
return {
apiName: "project-release-gate-execution-manifest",
manifestVersion: 1,
phase: rows.every(({ passed }) => passed) ? "ready" : "waiting",
ready: rows.every(({ passed }) => passed),
gateCount: rows.length,
passedCount: rows.filter(({ passed }) => passed).length,
unknownCount: rows.filter(({ passed }) => !passed).length,
missingGateIds: rows.filter(({ passed }) => !passed).map(({ id }) => id),
manifest,
rows,
};
}
export function createProjectReleaseGateResultMatrix({
gateResults = {},
observedOutputs = [],
@@ -206,6 +263,11 @@ export function createProjectReleaseReadinessReport({
promotedRuntimeFamilies = [],
} = {}) {
const gateManifest = createProjectReleaseGateManifest();
const gateExecutionManifest = createProjectReleaseGateExecutionManifest({
gateResults,
observedOutputs,
manifest: gateManifest,
});
const gateResultMatrix = createProjectReleaseGateResultMatrix({
gateResults,
observedOutputs,
@@ -249,6 +311,7 @@ export function createProjectReleaseReadinessReport({
promotedRuntimeFamilies: promotedFamilies,
promotedBlockedFamilies,
gateManifest,
gateExecutionManifest,
gateResultMatrix,
gateActionPlan,
gates: gateRows,
@@ -358,6 +421,11 @@ export function createProjectReleaseReadinessArtifactValidationSummaryViewModel(
label: "Gate manifest",
value: validation?.gateManifestReady === true ? "ready" : "missing",
},
{
id: "gate-execution-manifest",
label: "Gate execution manifest",
value: validation?.gateExecutionManifestReady === true ? "ready" : "missing",
},
{
id: "gate-result-matrix",
label: "Gate result matrix",
@@ -814,10 +882,12 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
const releaseGate = objectOrEmpty(artifactObject.releaseGate);
const simConfigInventory = objectOrEmpty(artifactObject.simConfigInventory);
const gateManifest = objectOrEmpty(artifactObject.gateManifest);
const gateExecutionManifest = objectOrEmpty(artifactObject.gateExecutionManifest);
const gateResultMatrix = objectOrEmpty(artifactObject.gateResultMatrix);
const gateActionPlan = objectOrEmpty(artifactObject.gateActionPlan);
const gates = arrayOrEmpty(artifactObject.gates);
const manifestGateIds = arrayOrEmpty(gateManifest.gateIds);
const executionRows = arrayOrEmpty(gateExecutionManifest.rows);
const matrixRows = arrayOrEmpty(gateResultMatrix.rows);
const actionPlanCommands = arrayOrEmpty(gateActionPlan.commands);
const rows = arrayOrEmpty(artifactObject.rows);
@@ -834,6 +904,13 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
&& gateResultMatrix.passedCount === REQUIRED_RELEASE_GATES.length
&& gateResultMatrix.unknownCount === 0
&& matrixRows.length === REQUIRED_RELEASE_GATES.length;
const executionManifestReady = gateExecutionManifest.apiName === "project-release-gate-execution-manifest"
&& gateExecutionManifest.manifestVersion === 1
&& gateExecutionManifest.ready === true
&& gateExecutionManifest.passedCount === REQUIRED_RELEASE_GATES.length
&& gateExecutionManifest.unknownCount === 0
&& executionRows.length === REQUIRED_RELEASE_GATES.length
&& executionRows.every(({ command, status }) => typeof command === "string" && status === "passed");
const actionPlanReady = gateActionPlan.apiName === "project-release-gate-action-plan"
&& gateActionPlan.planVersion === 1
&& gateActionPlan.ready === true
@@ -858,6 +935,7 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
: ["blockedRuntimeFamilies"]),
...(arrayOrEmpty(artifactObject.promotedBlockedFamilies).length === 0 ? [] : ["promotedBlockedFamilies"]),
...(manifestReady ? [] : ["gateManifest"]),
...(executionManifestReady ? [] : ["gateExecutionManifest"]),
...(matrixReady ? [] : ["gateResultMatrix"]),
...(actionPlanReady ? [] : ["gateActionPlan"]),
...(gateCountReady ? [] : ["gates.length"]),
@@ -879,6 +957,7 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
expectedGateCount: REQUIRED_RELEASE_GATES.length,
expectedGateIds,
gateManifestReady: manifestReady,
gateExecutionManifestReady: executionManifestReady,
gateResultMatrixReady: matrixReady,
gateActionPlanReady: actionPlanReady,
blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies),
@@ -898,6 +977,11 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
label: "Gate manifest",
value: manifestReady ? "ready" : "missing",
},
{
id: "gate-execution-manifest",
label: "Gate execution manifest",
value: executionManifestReady ? "ready" : "missing",
},
{
id: "gate-result-matrix",
label: "Gate result matrix",