Files
pikasTech-HWLAB/scripts/src/dev-evidence-blocker-aggregator.mjs
T
2026-05-22 18:06:24 +00:00

1430 lines
61 KiB
JavaScript

import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle, reportIsHistorical, reportLifecycleState } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const activeBrowserRoot = `${DEV_FRONTEND_ENDPOINT}/`;
const activeApiLiveEndpoint = `${DEV_ENDPOINT}/health/live`;
const deprecatedLegacyPublicEndpoints = Object.freeze([
"http://74.48.78.17:6666",
"http://74.48.78.17:6667"
]);
const latestFrontendRevision = "1e8805664970839b72be40c34636b08f6d18b131";
const levels = Object.freeze(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]);
const statusRank = Object.freeze({
pass: 0,
"contract-ready": 0,
"manifest-ready": 0,
ready: 0,
published: 0,
blocked: 1,
failed: 1,
not_run: 2,
not_observed: 2,
unavailable: 2,
not_applicable: 3
});
const blockerTypes = new Set([
"contract_blocker",
"environment_blocker",
"network_blocker",
"runtime_blocker",
"agent_blocker",
"observability_blocker",
"safety_blocker"
]);
const historicalLifecycleSummary =
"Historical report retained for evidence provenance; it is not active green DEV evidence.";
const reportPaths = Object.freeze({
devPreflight: "reports/dev-gate/dev-preflight-report.json",
devDeploy: "reports/dev-gate/dev-deploy-report.json",
devArtifacts: "reports/dev-gate/dev-artifacts.json",
devEdgeHealth: "reports/dev-gate/dev-edge-health.json",
devM2Smoke: "reports/dev-gate/dev-m2-deploy-smoke-active.json",
devM3Hardware: "reports/dev-gate/dev-m3-hardware-loop.json",
devM4Agent: "reports/dev-gate/dev-m4-agent-loop.json",
devM5Gate: "reports/dev-gate/dev-mvp-gate-report.json",
d601Observability: "reports/d601-k3s-readonly-observability.json"
});
const validationCommands = Object.freeze([
"node --check scripts/dev-evidence-blocker-aggregator.mjs",
"node --check scripts/src/dev-evidence-blocker-aggregator.mjs",
"node scripts/dev-evidence-blocker-aggregator.mjs --check",
"node scripts/dev-evidence-blocker-aggregator.mjs --markdown",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
]);
const sourceContracts = Object.freeze([
{
milestone: "M0",
issue: "pikasTech/HWLAB#31",
category: "contract",
level: "SOURCE",
status: "pass",
sources: [
"docs/m0-contract-audit.md",
"protocol/README.md",
"protocol/evidence-chain.md",
"protocol/schemas/evidence-record.schema.json",
"protocol/examples/m0-contract/service-ids.json"
],
commands: [
"node scripts/validate-contract.mjs",
"node scripts/validate-m0-contract.mjs",
"node scripts/validate-evidence-chain.mjs"
],
summary: "Frozen service IDs, JSON-RPC, audit, topology, evidence, and DEV-only deploy contracts are source-ready."
},
{
milestone: "M1",
issue: "pikasTech/HWLAB#7",
category: "local-smoke",
level: "LOCAL",
status: "pass",
sources: [
"docs/m1-local-smoke.md",
"fixtures/mvp/runtime.json",
"scripts/m1-contract-smoke.mjs"
],
commands: ["node scripts/m1-contract-smoke.mjs"],
summary: "Local skeleton smoke covers cloud API, simulators, patch-panel routing, CLI dry-run boundary, and no DEV/PROD mutation."
}
]);
const frontendDevFact = Object.freeze({
revision: latestFrontendRevision,
sources: [
"docs/dev-deploy-apply.md",
"docs/cloud-web-workbench.md",
"web/hwlab-cloud-web/index.html",
"web/hwlab-cloud-web/styles.css"
],
evidence: [
`Cloud Web /health/live accepted revision ${latestFrontendRevision}`,
"Cloud Workbench public browser endpoint is the active frontend route only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
commands: [
"node web/hwlab-cloud-web/scripts/check.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --static"
],
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest accepted DEV frontend fact, but it is frontend-only evidence.`
});
function issue(id) {
return `pikasTech/HWLAB#${id}`;
}
function short(value) {
return typeof value === "string" && value.length > 12 ? value.slice(0, 12) : value ?? "unknown";
}
function oneLine(value) {
return String(value ?? "")
.replace(/\s+/gu, " ")
.trim();
}
function primaryStatus(report) {
return report.status ??
report.gateStatus ??
report.conclusion ??
report.artifactPublish?.status ??
(report.blockers?.length > 0 ? "blocked" : "not_run");
}
function rank(status) {
return statusRank[status] ?? 2;
}
function worstStatus(values) {
const statuses = values.filter(Boolean);
if (statuses.length === 0) return "not_run";
return statuses.sort((a, b) => rank(b) - rank(a))[0];
}
function statusIsPass(status) {
return rank(status) === 0;
}
function statusHasEvidence(status) {
return !["not_run", "not_observed", "unavailable", "not_applicable"].includes(status);
}
async function readJSON(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
}
function gitCommit() {
try {
return execFileSync("git", ["rev-parse", "--short=12", "HEAD"], {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
}).trim();
} catch {
return "unknown";
}
}
function normalizeBlocker(blocker, sourceReport, fallbackPriority = "P3") {
assert.ok(blocker && typeof blocker === "object" && !Array.isArray(blocker), "blocker must be an object");
assert.ok(blockerTypes.has(blocker.type), `unknown blocker type ${blocker.type}`);
const nextTask = oneLine(blocker.nextTask ?? blocker.unblockHint ?? blocker.next ?? "");
return {
id: `${sourceReport.taskId ?? sourceReport.reportKind ?? "report"}:${blocker.scope}`,
priority: fallbackPriority,
type: blocker.type,
scope: blocker.scope,
status: blocker.status ?? "open",
source: sourceReport.path,
sourceIssue: sourceReport.issue,
summary: oneLine(blocker.summary),
...(nextTask ? { nextTask } : {})
};
}
function dedupeBlockers(blockers) {
const byKey = new Map();
for (const blocker of blockers) {
const key = `${blocker.type}:${blocker.scope}:${blocker.summary}`;
const existing = byKey.get(key);
if (!existing) {
byKey.set(key, blocker);
} else if (!existing.nextTask && blocker.nextTask) {
byKey.set(key, { ...existing, nextTask: blocker.nextTask });
} else if (blocker.priority < existing.priority) {
byKey.set(key, { ...blocker, nextTask: blocker.nextTask || existing.nextTask });
}
}
return [...byKey.values()];
}
function hasBlocker(blockers, predicate) {
return blockers.some(predicate);
}
function isRouteTransportBlocker(blocker) {
const scope = blocker.scope ?? "";
return scope === "m2-public-endpoints" ||
scope === "dev-edge" ||
scope === "dev-edge-health" ||
scope === "hwlab-router" ||
scope === "hwlab-tunnel-client" ||
scope === "hwlab-edge-proxy" ||
scope.includes("edge-frp") ||
scope.includes("ingress") ||
scope.includes("frp");
}
function applyPriority(blocker) {
if (blocker.scope === "base-image") {
return {
...blocker,
priority: "P0",
unblockOrder: 1,
unblocks: [issue(35), issue(33)],
rationale: "Artifact publishing cannot start until an approved Node 20 DEV builder base image is available."
};
}
if (
blocker.scope === "hwlab-router" ||
blocker.scope === "hwlab-tunnel-client" ||
blocker.scope === "hwlab-edge-proxy"
) {
return {
...blocker,
priority: "P0",
unblockOrder: 2,
unblocks: [issue(35), issue(36), issue(33)],
rationale: "The DEV route services need real HWLAB runtime entrypoints before image publish, frp, or ingress smoke can be meaningful."
};
}
if (blocker.scope.includes("artifact") || blocker.scope === "ghcr") {
return {
...blocker,
priority: "P1",
unblockOrder: 3,
unblocks: [issue(35), issue(33), issue(39)],
rationale: "The gate cannot promote deploy or runtime observations without immutable image provenance and digests."
};
}
if (
blocker.scope === "kubectl" ||
blocker.scope === "d601-k3s" ||
blocker.scope === "runner-kubeconfig-readonly-gap" ||
blocker.scope === "m3-service-discovery" ||
blocker.scope === "m3-direct-target-missing" ||
blocker.scope.startsWith("d601-")
) {
return {
...blocker,
priority: "P1",
unblockOrder: 4,
unblocks: [issue(34), issue(33), issue(36), issue(38), issue(46), issue(64)],
rationale: "Runner read-only observability must be repaired without treating the runner gap as proof that D601 k3s or public DEV endpoints are unavailable."
};
}
if (blocker.scope === "hwlab-dev-readonly-rbac") {
return {
...blocker,
priority: "P1",
unblockOrder: 4,
unblocks: [issue(34), issue(33), issue(36), issue(38), issue(39)],
rationale: "D601 has a client path, but read-only hwlab-dev pods/services/configmaps must be observable before live acceptance evidence can be trusted; this is not evidence that D601 is globally offline."
};
}
if (blocker.scope.includes("cloud-api-db")) {
return {
...blocker,
priority: "P1",
unblockOrder: 5,
unblocks: [issue(34), issue(33), issue(39)],
rationale: "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding."
};
}
if (blocker.scope === "db-live") {
return {
...blocker,
priority: "P1",
unblockOrder: 5,
unblocks: [issue(37), issue(39)],
rationale: "M4 and M5 cannot claim live agent or MVP evidence until cloud-api /health/live proves DB readiness with redacted live evidence."
};
}
if (isEdgeOrFrpBlocker(blocker)) {
return {
...blocker,
priority: "P2",
unblockOrder: 6,
unblocks: [issue(36), issue(38), issue(37), issue(39)],
rationale: "The :16667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected."
};
}
if (
blocker.scope === "m3-hardware-loop-runtime" ||
blocker.scope === "m3-patch-panel-wiring" ||
blocker.scope === "m3-box-simu-identity" ||
blocker.scope === "m3-gateway-simu-identity"
) {
return {
...blocker,
priority: "P0",
unblockOrder: 6,
unblocks: [issue(38), issue(39), issue(64)],
rationale: "M3 remains blocked until the real DEV hardware trusted loop proves DO1 -> patch-panel -> DI1 with operation, trace, audit, and evidence identifiers."
};
}
return {
...blocker,
priority: blocker.priority ?? "P3",
unblockOrder: 99,
unblocks: [issue(39)],
rationale: "Residual blocker that must be classified before claiming a green DEV gate."
};
}
function isEdgeOrFrpBlocker(blocker) {
return isRouteTransportBlocker(blocker);
}
function sourceEntry(entry) {
return {
milestone: entry.milestone,
issue: entry.issue,
level: entry.level,
status: entry.status,
category: entry.category,
lifecycleState: entry.lifecycleState ?? "active",
sources: entry.sources,
commands: entry.commands,
summary: entry.summary
};
}
function reportEvidence({ milestone, level, report, status, category, summary, evidence = [], commands = [] }) {
const lifecycleState = reportLifecycleState(report);
const historical = lifecycleState === "historical";
return {
milestone,
issue: report.issue,
taskId: report.taskId ?? report.reportKind,
reportPath: report.path,
commitId: short(report.commitId ?? report.target?.commitId),
lifecycleState,
level: historical ? "BLOCKED" : level,
status: historical && status === "pass" ? "blocked" : status,
category,
commands,
evidence,
summary: historical ? `${summary} ${historicalLifecycleSummary}` : summary
};
}
function collectM2Evidence(reports) {
const preflight = reports.devPreflight;
const deploy = reports.devDeploy;
const artifacts = reports.devArtifacts;
const edge = reports.devEdgeHealth;
const m2Smoke = reports.devM2Smoke;
const d601 = reports.d601Observability;
const artifactIdentity = preflight.artifactIdentity;
const edgeLive = hasCurrentLiveEdgeEvidence(edge);
const m2EndpointLive = hasCurrentM2EndpointEvidence(m2Smoke);
return [
{
milestone: "M2",
issue: issue(99),
taskId: "cloud-web-workbench-frontend",
lifecycleState: "active",
level: "DEV-LIVE",
status: "pass",
category: "frontend-dev-revision",
sources: frontendDevFact.sources,
commands: frontendDevFact.commands,
evidence: frontendDevFact.evidence,
summary: frontendDevFact.summary
},
reportEvidence({
milestone: "M2",
level: "SOURCE",
report: preflight,
status: "pass",
category: "deploy-manifest",
evidence: [
"source-contract-static=pass",
`artifact catalog state=${artifactIdentity.artifactCatalog.artifactState}`,
`catalog commit=${artifactIdentity.artifactCatalog.commitId}`
],
commands: preflight.validationCommands,
summary: "Deploy manifests, FRP/master-edge contracts, and safety boundary are source-readable and scoped to hwlab-dev."
}),
reportEvidence({
milestone: "M2",
level: "DRY-RUN",
report: deploy,
status: deploy.dryRun?.status ?? primaryStatus(deploy),
category: "deploy-apply",
evidence: deploy.dryRun?.evidence ?? [],
commands: deploy.dryRun?.commands ?? [],
summary: deploy.dryRun?.summary ?? "DEV apply dry-run report is available."
}),
reportEvidence({
milestone: "M2",
level: "DRY-RUN",
report: artifacts,
status: artifacts.artifactPublish?.status ?? primaryStatus(artifacts),
category: "artifact-publish",
evidence: [
`services=${artifacts.artifactPublish?.services?.length ?? 0}`,
`baseImagePreflight=${artifacts.artifactPublish?.baseImagePreflight?.status ?? "unknown"}`,
`published=${artifacts.artifactPublish?.services?.filter((service) => service.status === "published").length ?? 0}/${SERVICE_IDS.length}`
],
commands: artifacts.validationCommands,
summary: "Artifact publish preflight exists but is blocked before any real image publish."
}),
reportEvidence({
milestone: "M2",
level: m2EndpointLive ? "DEV-LIVE" : "BLOCKED",
report: m2Smoke,
status: m2Smoke.runtimeSmoke?.status ?? primaryStatus(m2Smoke),
category: "public-entrypoints-read-only",
evidence: publicEndpointEvidence(m2Smoke),
commands: m2Smoke.devPreconditions?.commands ?? [],
summary: m2EndpointLive
? "Read-only probes prove the frozen public DEV entrypoints on :16666/:16667 are reachable; this is EDGE/ROUTE evidence only."
: "The active M2 public endpoint smoke does not prove the frozen :16666/:16667 entrypoints."
}),
reportEvidence({
milestone: "M2",
level: edgeLive ? "DEV-LIVE" : "SOURCE",
report: edge,
status: edge.edgeHealth?.status ?? primaryStatus(edge),
category: "edge-frp-contract",
evidence: [
`mode=${edge.edgeHealth?.mode ?? "unknown"}`,
`classification=${edge.edgeHealth?.classification ?? "unknown"}`,
`cloudApiDb=${edge.edgeHealth?.contracts?.deploy?.cloudApiDb?.status ?? "unknown"}`
],
commands: edge.validationCommands,
summary: edgeLive
? "Committed edge report proves read-only public HTTP on :16667 /health and /health/live; remaining edge report blocker is DB readiness, not route/frp reachability."
: "Committed edge report is contract-only/not-run; it must not be counted as DEV-LIVE."
}),
reportEvidence({
milestone: "M2",
level: d601.cluster?.readable === true || d601.d601PublicEndpointsReachable === true ? "DEV-LIVE" : "BLOCKED",
report: d601,
status: d601.conclusion,
category: "d601-observability",
evidence: d601ObservationSummary(d601),
commands: d601.validationCommands,
summary: d601ObservabilitySummary(d601)
})
];
}
function collectM3Evidence(reports) {
const m3 = reports.devM3Hardware;
const m5Milestone = reports.devM5Gate.milestones?.find((item) => item.id === "M3");
const liveSummary = m3TrustedLoopSummary(m3);
return [
reportEvidence({
milestone: "M3",
level: "SOURCE",
report: m3,
status: "manifest-ready",
category: "hardware-loop-cardinality",
evidence: m3.readOnlySupplementalEvidence?.map((item) => `${item.id}=${item.status}`) ?? [],
commands: ["node scripts/validate-dev-m3-cardinality.mjs"],
summary: "Static DEV manifest cardinality declares two box simulators, two gateway simulators, and one patch panel."
}),
reportEvidence({
milestone: "M3",
level: "LOCAL",
report: reports.devM5Gate,
status: m5Milestone?.status ?? "not_run",
category: "hardware-loop-local",
evidence: m5Milestone?.evidence ?? [],
commands: m5Milestone?.commands ?? ["node scripts/m3-hardware-loop-smoke.mjs"],
summary: m5Milestone?.summary ?? "No local M3 smoke evidence was attached."
}),
reportEvidence({
milestone: "M3",
level: statusIsPass(m3.liveOperation?.status) ? "DEV-LIVE" : "BLOCKED",
report: m3,
status: m3.liveOperation?.status ?? "not_run",
category: "hardware-loop-live",
evidence: [
`operationId=${m3.liveOperation?.operationId ?? "not_observed"}`,
`traceId=${m3.liveOperation?.traceId ?? "not_observed"}`,
`auditId=${m3.liveOperation?.auditId ?? "not_observed"}`,
`evidenceId=${m3.liveOperation?.evidenceId ?? "not_observed"}`
],
commands: [
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
],
summary: liveSummary
})
];
}
function m3TrustedLoopSummary(m3Report) {
const operationSummary = m3Report.liveOperation?.summary ?? "No live M3 hardware operation was observed.";
const blockerSummary = m3Report.blockers?.find((blocker) =>
blocker.scope === "m3-hardware-loop-runtime" ||
blocker.scope === "m3-service-discovery" ||
blocker.scope === "m3-direct-target-missing" ||
blocker.scope === "m3-box-simu-identity" ||
blocker.scope === "m3-gateway-simu-identity" ||
blocker.scope === "m3-patch-panel-wiring"
)?.summary;
if (statusIsPass(m3Report.liveOperation?.status) || !blockerSummary) {
return operationSummary;
}
return `${operationSummary} Blocker: ${blockerSummary.replace(/[.。]+$/u, "")}.`;
}
function collectM4Evidence(reports) {
const m4 = reports.devM4Agent;
return [
reportEvidence({
milestone: "M4",
level: "LOCAL",
report: m4,
status: m4.localSmoke?.status ?? "not_run",
category: "agent-loop-local",
evidence: m4.localSmoke?.evidence ?? [],
commands: m4.localSmoke?.commands ?? [],
summary: m4.localSmoke?.summary ?? "No local M4 smoke evidence was attached."
}),
reportEvidence({
milestone: "M4",
level: "DRY-RUN",
report: m4,
status: m4.dryRun?.status ?? "not_run",
category: "agent-loop-dry-run",
evidence: m4.dryRun?.evidence ?? [],
commands: m4.dryRun?.commands ?? [],
summary: m4.dryRun?.summary ?? "No M4 dry-run evidence was attached."
}),
reportEvidence({
milestone: "M4",
level: statusIsPass(m4.livePreflight?.status) ? "DEV-LIVE" : "BLOCKED",
report: m4,
status: m4.livePreflight?.status ?? m4.devPreconditions?.status ?? "not_run",
category: "agent-loop-live-preflight",
evidence: m4.livePreflight?.evidence ?? [],
commands: m4.livePreflight?.commands ?? [],
summary: m4.livePreflight?.summary ?? m4.devPreconditions?.summary ?? "No live M4 observation was recorded."
})
];
}
function collectM5Evidence(reports) {
const m5 = reports.devM5Gate;
return [
reportEvidence({
milestone: "M5",
level: "DRY-RUN",
report: m5,
status: m5.dryRun?.status ?? "not_run",
category: "mvp-e2e-dry-run",
evidence: m5.dryRun?.evidence ?? [],
commands: m5.dryRun?.commands ?? [],
summary: m5.dryRun?.summary ?? "No M5 dry-run evidence was attached."
}),
reportEvidence({
milestone: "M5",
level: statusIsPass(m5.devPreconditions?.status) ? "DEV-LIVE" : "BLOCKED",
report: m5,
status: m5.devPreconditions?.status ?? m5.gateStatus ?? "blocked",
category: "mvp-e2e-live",
evidence: m5.devPreconditions?.evidence ?? [],
commands: m5.devPreconditions?.commands ?? [],
summary: m5.devPreconditions?.summary ?? "No live M5 e2e evidence was attached."
})
];
}
function collectBlockers(reports) {
const blockers = [];
for (const report of Object.values(reports)) {
if (reportIsHistorical(report)) {
blockers.push({
id: `${report.taskId ?? report.reportKind ?? "report"}-historical:${report.path}`,
priority: "P2",
type: "observability_blocker",
scope: `${report.taskId ?? report.reportKind ?? "report"}-historical`,
status: "open",
source: report.path,
sourceIssue: report.issue,
summary: `${report.taskId ?? report.reportKind ?? "Report"} is historical/deprecated and must be refreshed before it can support active DEV evidence. ${historicalLifecycleSummary}`,
nextTask: "Regenerate the report as active DEV evidence or keep the historical snapshot in an archival path."
});
}
for (const blocker of report.blockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
for (const blocker of report.artifactPublish?.blockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
for (const blocker of report.devDeployApply?.remainingBlockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
}
blockers.push(normalizeBlocker(blocker, report));
}
}
const normalized = dedupeBlockers(blockers).map(applyPriority);
return normalized.sort((a, b) => a.unblockOrder - b.unblockOrder || a.id.localeCompare(b.id));
}
function hasCurrentLiveEdgeEvidence(edgeReport) {
const publicHttp = edgeReport?.edgeHealth?.publicHttp ?? [];
return edgeReport?.edgeHealth?.endpoint === DEV_ENDPOINT &&
edgeReport?.edgeHealth?.mode === "live-read-only" &&
publicHttp.some((probe) => probe.url === `${DEV_ENDPOINT}/health` && probe.ok === true && probe.status === 200) &&
publicHttp.some((probe) => probe.url === `${DEV_ENDPOINT}/health/live` && probe.ok === true && probe.status === 200);
}
function hasCurrentM2EndpointEvidence(m2Report) {
const probes = m2Report?.runtimeSmoke?.probes ?? [];
return m2Report?.endpoint === DEV_ENDPOINT &&
m2Report?.frontendEndpoint === DEV_FRONTEND_ENDPOINT &&
m2Report?.runtimeSmoke?.mode === "live-read-only" &&
m2Report?.runtimeSmoke?.status === "pass" &&
probes.some((probe) => probe.url === `${DEV_ENDPOINT}/health` && probe.ok === true && probe.status === 200) &&
probes.some((probe) => probe.url === `${DEV_ENDPOINT}/health/live` && probe.ok === true && probe.status === 200) &&
probes.some((probe) => probe.url === `${DEV_FRONTEND_ENDPOINT}/` && probe.ok === true && probe.status === 200);
}
function publicEndpointEvidence(m2Report) {
return (m2Report?.runtimeSmoke?.probes ?? []).map((probe) => {
const identity = probe.json?.serviceId ?? probe.title ?? "unknown";
const status = probe.json?.status ?? (probe.ok ? "ok" : "failed");
return `${probe.url} -> HTTP ${probe.status ?? "none"} identity=${identity} status=${status}`;
});
}
function d601ObservationSummary(d601) {
return [
`runnerKubeconfigReadable=${d601.runnerKubeconfigReadable === true}`,
`runnerKubeconfigProbeExitCode=${d601.runnerKubeconfigProbeExitCode ?? "unknown"}`,
`runnerKubeconfigProbeStderr=${d601.runnerKubeconfigProbeStderr || "empty"}`,
`d601PublicEndpointsReachable=${d601.d601PublicEndpointsReachable === true}`,
`d601K3sUnavailable=${d601.d601K3sUnavailable === true}`
];
}
function d601ObservabilitySummary(d601) {
if (d601.d601PublicEndpointsReachable === true && d601.runnerKubeconfigReadable === false) {
const clusterNote = d601.cluster?.readable === true
? " Alternate read-only cluster probes are readable."
: "";
return `D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline.${clusterNote} ${d601ObservationSummary(d601).join(", ")}.`;
}
if (d601.cluster?.readable === true) {
return `D601 hwlab-dev cluster is readable from this runner; ${d601ObservationSummary(d601).join(", ")}.`;
}
if (d601.d601K3sUnavailable === true) {
return `Direct read-only probes indicate D601 k3s may be unavailable; ${d601ObservationSummary(d601).join(", ")}.`;
}
return `D601 read-only observability is blocked without proving D601 unavailable; ${d601ObservationSummary(d601).join(", ")}.`;
}
function isStaleLegacyIngressBlocker(blocker, sourceReport, reports) {
const text = `${blocker.scope ?? ""} ${blocker.summary ?? ""}`;
const staleLegacyPort = deprecatedLegacyPublicEndpoints.some((endpoint) => text.includes(endpoint));
const stalePreflightEdgeProbe = sourceReport.path === reportPaths.devPreflight &&
blocker.type === "network_blocker" &&
(blocker.scope === "dev-edge" || blocker.scope === "dev-edge-health") &&
(
text.includes(`${DEV_ENDPOINT}/health/live is not reachable`) ||
text.includes("live network probes require --live")
);
if (!staleLegacyPort && !stalePreflightEdgeProbe) {
return false;
}
const currentEdgeReachable = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth);
return currentEdgeReachable && sourceReport.path !== reportPaths.devM3Hardware &&
sourceReport.path !== reportPaths.devM4Agent &&
sourceReport.path !== reportPaths.devM5Gate;
}
function deriveMilestones(evidence, blockers) {
const evidenceByMilestone = new Map();
for (const item of evidence) {
if (!evidenceByMilestone.has(item.milestone)) {
evidenceByMilestone.set(item.milestone, []);
}
evidenceByMilestone.get(item.milestone).push(item);
}
return ["M0", "M1", "M2", "M3", "M4", "M5"].map((milestone) => {
const items = evidenceByMilestone.get(milestone) ?? [];
const liveItems = items.filter((item) => item.level === "DEV-LIVE");
const highestVisibleLevel = levels.findLast((level) => items.some((item) => item.level === level && level !== "BLOCKED" && statusHasEvidence(item.status))) ?? "BLOCKED";
const hasPassingLive = liveItems.some((item) => statusIsPass(item.status));
const hasBlockedEvidence = items.some((item) => item.level === "BLOCKED" && ["blocked", "failed", "degraded"].includes(item.status));
const milestoneBlockers = blockers.filter((blocker) => {
if (milestone === "M2") return blocker.unblocks.includes(issue(33)) || blocker.unblocks.includes(issue(35)) || blocker.unblocks.includes(issue(36));
if (milestone === "M3") return blocker.unblocks.includes(issue(38));
if (milestone === "M4") return blocker.unblocks.includes(issue(37));
if (milestone === "M5") return blocker.unblocks.includes(issue(39));
return false;
});
const status = milestoneBlockers.length > 0 || hasBlockedEvidence || (liveItems.length > 0 && !hasPassingLive)
? "blocked"
: worstStatus(items.map((item) => item.status));
return {
id: milestone,
status,
highestVisibleLevel,
liveEvidence: hasPassingLive ? "pass" : "missing_or_blocked",
evidenceCount: items.length,
blockerCount: milestoneBlockers.length,
summary: summarizeMilestone(milestone, highestVisibleLevel, status)
};
});
}
function summarizeMilestone(milestone, highestVisibleLevel, status) {
const labels = {
M0: "contract source is available",
M1: "local smoke is available",
M2: "deploy/runtime readiness is blocked before live DEV",
M3: "hardware loop has source/local shape but no live operation",
M4: "agent loop has local smoke but live preflight is blocked",
M5: "dry-run is green but live MVP gate is blocked"
};
return `${labels[milestone]}; highest visible level is ${highestVisibleLevel}; status is ${status}.`;
}
function buildMilestoneLevelClassification(milestones) {
const currentLabels = {
M0: "SOURCE",
M1: "LOCAL",
M2: "DEV-LIVE",
M3: "BLOCKED",
M4: "BLOCKED",
M5: "BLOCKED"
};
const notes = {
M0: "Source contract is green at source level only.",
M1: "Local smoke is green; it is not DEV-LIVE.",
M2: "Current public frontend/API route evidence is DEV-LIVE for route/front-end reachability only.",
M3: "DEV-LIVE hardware trusted loop is blocked; local/source shape is not acceptance.",
M4: "DEV-LIVE agent loop is blocked at DB live readiness; local smoke is not acceptance.",
M5: "Dry-run is green, but bounded DEV-LIVE MVP e2e is blocked."
};
return milestones.map((milestone) => ({
milestone: milestone.id,
currentLevel: currentLabels[milestone.id],
status: milestone.status,
strongestEvidenceLevel: milestone.highestVisibleLevel,
liveEvidence: milestone.liveEvidence,
summary: notes[milestone.id]
}));
}
function groupByLevel(evidence, blockers) {
const grouped = Object.fromEntries(levels.map((level) => [level, []]));
for (const item of evidence) {
grouped[item.level].push({
milestone: item.milestone,
issue: item.issue,
taskId: item.taskId,
lifecycleState: item.lifecycleState,
status: item.status,
category: item.category,
reportPath: item.reportPath,
summary: item.summary
});
}
grouped.BLOCKED.push(
...blockers.map((blocker) => ({
priority: blocker.priority,
order: blocker.unblockOrder,
type: blocker.type,
scope: blocker.scope,
sourceIssue: blocker.sourceIssue,
source: blocker.source,
summary: blocker.summary,
nextTask: blocker.nextTask
}))
);
return grouped;
}
function buildDoD(reports, milestones, blockers) {
const m0 = milestones.find((item) => item.id === "M0");
const m1 = milestones.find((item) => item.id === "M1");
const m5 = milestones.find((item) => item.id === "M5");
const preflight = reports.devPreflight;
const artifactIdentity = preflight.artifactIdentity;
const d601 = reports.d601Observability;
const edgeLive = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth);
const artifactCurrent = artifactIdentity.publishVerified === true &&
artifactIdentity.targetCoverage?.covered !== false &&
artifactIdentity.artifactCatalog?.matchesTarget !== false;
const cloudApiDb = cloudApiDbStatus(reports);
const dbReady = cloudApiDb.ready === true && cloudApiDb.connected === true && cloudApiDb.liveDbEvidence === true;
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
return {
status: blockers.length === 0 && milestones.every((item) => item.status === "pass") ? "green" : "blocked",
green: blockers.length === 0 && milestones.every((item) => item.status === "pass"),
checks: [
{
id: "m0-source-contract",
status: m0?.status === "pass" ? "pass" : "blocked",
evidenceLevel: "SOURCE",
summary: "M0 contract checks are source-level evidence only."
},
{
id: "m1-local-smoke",
status: m1?.status === "pass" ? "pass" : "blocked",
evidenceLevel: "LOCAL",
summary: "M1 local smoke is not a live DEV substitute."
},
{
id: "artifact-publish-digests",
status: artifactCurrent ? "pass" : "blocked",
evidenceLevel: artifactCurrent ? "DEV-LIVE" : "BLOCKED",
summary: `artifactState=${artifactIdentity.artifactCatalog.artifactState}, ciPublished=${artifactIdentity.artifactCatalog.ciPublished}, registryVerified=${artifactIdentity.artifactCatalog.registryVerified}, sha256=${artifactIdentity.artifactCatalog.digestCounts.sha256}, not_published=${artifactIdentity.artifactCatalog.digestCounts.notPublished}, targetCovered=${artifactIdentity.targetCoverage?.covered ?? "unknown"}`
},
{
id: "d601-k3s-observability",
status: d601.runnerKubeconfigReadable === false ? "blocked" : d601.cluster?.readable ? "pass" : "blocked",
evidenceLevel: d601.cluster?.readable || d601.d601PublicEndpointsReachable ? "DEV-LIVE" : "BLOCKED",
summary: d601ObservabilitySummary(d601)
},
{
id: "dev-edge-frp-16667",
status: edgeLive && !hasBlocker(blockers, isRouteTransportBlocker) ? "pass" : "blocked",
evidenceLevel: edgeLive && !hasBlocker(blockers, isRouteTransportBlocker) ? "DEV-LIVE" : "BLOCKED",
summary: edgeLive
? "Committed edge report proves read-only public HTTP on :16667 /health and /health/live; this is route evidence, not DB/M3/M4/M5 acceptance."
: "No committed report proves live HTTP 200/JSON on http://74.48.78.17:16667."
},
{
id: "cloud-api-db-ready",
status: dbReady ? "pass" : "blocked",
evidenceLevel: dbReady ? "DEV-LIVE" : "BLOCKED",
summary: `cloud-api DB status=${cloudApiDb.status}; ready=${cloudApiDb.ready}; connected=${cloudApiDb.connected}; liveDbEvidence=${cloudApiDb.liveDbEvidence}.`
},
{
id: "m3-hardware-trusted-loop",
status: m3Live ? "pass" : "blocked",
evidenceLevel: m3Live ? "DEV-LIVE" : "BLOCKED",
summary: m3Live
? "M3 hardware trusted loop has operation, trace, audit, and evidence identifiers."
: "M3 trusted loop is blocked until res_boxsimu_1:DO1 -> patch-panel -> res_boxsimu_2:DI1 is proven with operation/trace/audit/evidence."
},
{
id: "m4-agent-loop-live",
status: m4Live ? "pass" : "blocked",
evidenceLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
summary: m4Live
? "M4 agent loop live preflight passed."
: "M4 agent loop live path is blocked before accepted agent scheduling/evidence closure."
},
{
id: "m5-mvp-dev-live",
status: m5?.status === "pass" && m5.liveEvidence === "pass" ? "pass" : "blocked",
evidenceLevel: "BLOCKED",
summary: "M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed."
}
]
};
}
function cloudApiDbStatus(reports) {
const db = reports.devEdgeHealth.edgeHealth?.contracts?.deploy?.cloudApiDb ?? {};
return {
status: db.status ?? "unknown",
ready: db.ready === true,
connected: db.connected === true || db.liveConnected === true,
configReady: db.configReady === true,
connectionChecked: db.connectionChecked === true,
liveDbEvidence: db.liveDbEvidence === true
};
}
function buildCurrentDevLayering(reports, blockers) {
const m2EndpointLive = hasCurrentM2EndpointEvidence(reports.devM2Smoke);
const edgeLive = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth);
const cloudDb = cloudApiDbStatus(reports);
const dbReady = cloudDb.ready && cloudDb.connected && cloudDb.liveDbEvidence;
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
const artifactIdentity = reports.devPreflight.artifactIdentity;
const artifactCurrent = artifactIdentity.publishVerified === true &&
artifactIdentity.targetCoverage?.covered !== false &&
artifactIdentity.artifactCatalog?.matchesTarget !== false;
const desired = reports.devDeploy.devDeployApply ?? {};
const artifactSourceStates = artifactSourceStateCounts(reports.devArtifacts);
return {
frontendRevision: {
label: "Frontend DEV revision",
status: "pass",
evidenceLevel: "DEV-LIVE",
summary: `${activeBrowserRoot} serves the accepted Cloud Workbench frontend revision ${latestFrontendRevision}; this is browser/frontend evidence only.`,
evidence: frontendDevFact.evidence,
nextRequired: "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance."
},
edgeRoute: {
label: "EDGE/ROUTE live",
status: m2EndpointLive || edgeLive ? "pass" : "blocked",
evidenceLevel: m2EndpointLive || edgeLive ? "DEV-LIVE" : "BLOCKED",
summary: m2EndpointLive
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned accepted HWLAB DEV responses in the active M2 read-only smoke.`
: `No active read-only public endpoint report proves both ${activeBrowserRoot} and ${activeApiLiveEndpoint}.`,
evidence: publicEndpointEvidence(reports.devM2Smoke),
nextRequired: "Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance."
},
dbLive: {
label: "DB live/degraded",
status: dbReady ? "pass" : "blocked",
evidenceLevel: dbReady ? "DEV-LIVE" : "BLOCKED",
summary: `cloud-api DB status=${cloudDb.status}; configReady=${cloudDb.configReady}; ready=${cloudDb.ready}; connected=${cloudDb.connected}; liveDbEvidence=${cloudDb.liveDbEvidence}.`,
evidence: reports.devM5Gate.devPreconditions?.evidence?.filter((line) => line.includes("/health/live") || line.includes("DB")) ?? [],
nextRequired: "Provide live DB connection evidence through redacted health output; route reachability alone is insufficient."
},
d601RunnerObservability: {
label: "D601 runner observability",
status: reports.d601Observability.runnerKubeconfigReadable === false
? "blocked"
: reports.d601Observability.cluster?.readable === true
? "pass"
: "blocked",
evidenceLevel: reports.d601Observability.cluster?.readable === true ||
reports.d601Observability.d601PublicEndpointsReachable === true
? "DEV-LIVE"
: "BLOCKED",
summary: d601ObservabilitySummary(reports.d601Observability),
evidence: d601ObservationSummary(reports.d601Observability),
nextRequired: "Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired."
},
m3HardwareTrustedLoop: {
label: "M3 hardware trusted loop",
status: m3Live ? "pass" : "blocked",
evidenceLevel: m3Live ? "DEV-LIVE" : "BLOCKED",
summary: m3TrustedLoopSummary(reports.devM3Hardware),
evidence: [
`operationId=${reports.devM3Hardware.liveOperation?.operationId ?? "not_observed"}`,
`traceId=${reports.devM3Hardware.liveOperation?.traceId ?? "not_observed"}`,
`auditId=${reports.devM3Hardware.liveOperation?.auditId ?? "not_observed"}`,
`evidenceId=${reports.devM3Hardware.liveOperation?.evidenceId ?? "not_observed"}`
],
nextRequired: "Only a real DEV res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 observation with operation/trace/audit/evidence can clear M3."
},
m4AgentLoop: {
label: "M4 agent loop",
status: m4Live ? "pass" : "blocked",
evidenceLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
summary: reports.devM4Agent.livePreflight?.summary ?? reports.devM4Agent.devPreconditions?.summary ?? "No live M4 observation was recorded.",
evidence: reports.devM4Agent.livePreflight?.evidence ?? [],
nextRequired: "Do not schedule or claim the agent loop as live until DB live and required runtime/evidence preconditions pass."
},
artifactDesiredStateSource: {
label: "artifact/desired-state source",
status: artifactCurrent && desired.conclusion?.status === "ready" ? "pass" : "blocked",
evidenceLevel: artifactCurrent ? "SOURCE" : "BLOCKED",
summary: `artifact targetCovered=${artifactIdentity.targetCoverage?.covered ?? "unknown"}; artifactSource=${short(artifactIdentity.artifactSource?.commitId)}; target=${short(artifactIdentity.source?.commitId)}; desiredApplyMode=${desired.mode ?? "unknown"}; mutationAttempted=${desired.mutationAttempted === true}.`,
evidence: [
`artifactState=${artifactIdentity.artifactCatalog?.artifactState ?? "unknown"}`,
`sourceStates=${artifactSourceStates.sourcePresent} source-present, ${artifactSourceStates.intentionallyDisabled} intentionally-disabled`,
`desiredState=${desired.conclusion?.status ?? "not_reported"} dry-run-only`
],
nextRequired: "Refresh artifact/source coverage for current origin/main and keep desired-state apply separate from read-only route proof."
}
};
}
function buildMilestoneBlockerClassification(reports) {
const cloudDb = cloudApiDbStatus(reports);
const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status);
const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status);
const m3ServiceDiscoveryBlocked = reports.devM3Hardware.blockers?.some((blocker) =>
blocker.scope === "m3-service-discovery" || blocker.scope === "m3-direct-target-missing"
) === true;
const m3IdentityBlocked = reports.devM3Hardware.blockers?.some((blocker) =>
blocker.scope === "m3-box-simu-identity" || blocker.scope === "m3-gateway-simu-identity"
) === true;
const m3WiringBlocked = reports.devM3Hardware.blockers?.some((blocker) =>
blocker.scope === "m3-patch-panel-wiring"
) === true;
const m3BlockerClass = m3Live
? "cleared"
: m3ServiceDiscoveryBlocked
? "direct-target-missing"
: m3IdentityBlocked
? "direct-target-identity-gap"
: m3WiringBlocked
? "patch-panel-m3-wiring-missing"
: "hardware-loop-runtime";
return [
{
milestone: "M3",
status: m3Live ? "pass" : "blocked",
currentLevel: m3Live ? "DEV-LIVE" : "BLOCKED",
blockerClass: m3BlockerClass,
dependency: m3ServiceDiscoveryBlocked
? "Direct DEV simulator and patch-panel targets must be discoverable before the real trusted loop can be observed."
: m3IdentityBlocked
? "Two distinct live box-simu resources and two distinct gateway-simu identities are required before M3 can run."
: m3WiringBlocked
? "The callable DEV patch-panel must actively carry res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 before the write/read operation can run."
: "Real DEV trusted loop through res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1.",
evidence: [
`operationId=${reports.devM3Hardware.liveOperation?.operationId ?? "not_observed"}`,
`traceId=${reports.devM3Hardware.liveOperation?.traceId ?? "not_observed"}`,
`auditId=${reports.devM3Hardware.liveOperation?.auditId ?? "not_observed"}`,
`evidenceId=${reports.devM3Hardware.liveOperation?.evidenceId ?? "not_observed"}`,
`runnerKubeconfigReadable=${reports.devM3Hardware.d601Observability?.runnerKubeconfigReadable === true}`,
`d601PublicEndpointsReachable=${reports.devM3Hardware.d601Observability?.d601PublicEndpointsReachable === true}`,
`d601K3sUnavailable=${reports.devM3Hardware.d601Observability?.d601K3sUnavailable === true}`,
`directTargetSource=${reports.devM3Hardware.directTargetDiscovery?.source ?? "not_observed"}`,
`directTargetCounts=${JSON.stringify(reports.devM3Hardware.directTargetDiscovery?.counts ?? {})}`,
`patchPanelM3Wiring=${reports.devM3Hardware.directTargetDiscovery?.patchPanelWiring?.hasRequiredConfiguredConnection === true}`
],
nextRequired: m3ServiceDiscoveryBlocked
? "Expose or document callable DEV direct targets for both simulators and the patch-panel, then rerun the read-only M3 preflight."
: m3IdentityBlocked
? "Fix DEV simulator instance identity so two box-simu pods report res_boxsimu_1/res_boxsimu_2 and two gateway-simu pods report distinct gateway identities."
: m3WiringBlocked
? "Load/apply DEV patch-panel wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1, then rerun the bounded DEV M3 smoke."
: "Run the bounded DEV M3 live smoke only after patch-panel topology can prove operation, trace, audit, and evidence IDs.",
nonPromotionReason: "Public frontend, route, and artifact evidence do not prove the required hardware loop."
},
{
milestone: "M4",
status: m4Live ? "pass" : "blocked",
currentLevel: m4Live ? "DEV-LIVE" : "BLOCKED",
blockerClass: m4Live ? "cleared" : "db-live-readiness",
dependency: "Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure.",
evidence: [
`db.status=${cloudDb.status}`,
`db.ready=${cloudDb.ready}`,
`db.connected=${cloudDb.connected}`,
`db.liveDbEvidence=${cloudDb.liveDbEvidence}`
],
nextRequired: "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.",
nonPromotionReason: "Frontend revision and read-only route reachability do not prove DB-backed agent runtime readiness."
},
{
milestone: "M5",
status: "blocked",
currentLevel: "BLOCKED",
blockerClass: "composite-db-m3-m4-live",
dependency: "M5 needs DB live readiness, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage.",
evidence: [
"M5 dry-run is green",
`db.ready=${cloudDb.ready}`,
`m3.live=${m3Live}`,
`m4.live=${m4Live}`
],
nextRequired: "After DB/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations.",
nonPromotionReason: "No frontend, route-only, local, or dry-run evidence is allowed to stand in for bounded DEV-LIVE MVP e2e acceptance."
}
];
}
function artifactSourceStateCounts(artifactReport) {
const counts = { sourcePresent: 0, intentionallyDisabled: 0 };
for (const service of artifactReport.artifactPublish?.services ?? []) {
if (service.sourceState === "source-present") {
counts.sourcePresent += 1;
} else if (service.sourceState === "intentionally-disabled") {
counts.intentionallyDisabled += 1;
}
}
return counts;
}
function buildNextSteps(blockers) {
const byOrder = new Map();
for (const blocker of blockers) {
if (!byOrder.has(blocker.unblockOrder)) {
byOrder.set(blocker.unblockOrder, []);
}
byOrder.get(blocker.unblockOrder).push(blocker);
}
return [...byOrder.entries()]
.sort(([a], [b]) => a - b)
.map(([blockerOrder, items], index) => {
const first = items[0];
return {
order: index + 1,
blockerOrder,
priority: first.priority,
scopes: [...new Set(items.map((item) => item.scope))],
sourceIssues: [...new Set(items.flatMap((item) => [item.sourceIssue, ...item.unblocks].filter(Boolean)))].sort(),
rationale: first.rationale,
action: actionForBlockerOrder(blockerOrder, items),
evidenceRequired: evidenceRequiredFor(first.scope)
};
});
}
function actionForBlockerOrder(blockerOrder, items) {
if (blockerOrder === 2) {
const scopes = items.map((item) => item.scope).sort().join(", ");
return `Add real runtime entrypoints or dedicated Dockerfiles for ${scopes}, then rerun the artifact publish preflight.`;
}
return items.find((item) => item.nextTask)?.nextTask || fallbackAction(items[0].scope);
}
function fallbackAction(scope) {
if (scope === "base-image") return "Preload or tag node:20-bookworm-slim and rerun the base-image and artifact publish preflights.";
if (scope.includes("artifact")) return "Publish DEV artifacts for every frozen HWLAB service and record immutable registry digests.";
if (scope.includes("edge") || scope.includes("ingress") || scope.includes("frp")) return "Repair frp/master-edge/D601 router path and rerun read-only DEV edge health.";
if (scope.includes("cloud-api-db")) return "Configure DEV cloud-api DB env readiness and rerun health/preflight without exposing secrets.";
if (scope === "db-live") return "Repair DEV cloud-api DB live readiness, then rerun the read-only health and M4 preflight reports without exposing secret values.";
if (scope === "m3-patch-panel-wiring") return "Load/apply DEV patch-panel wiring for res_boxsimu_1:DO1 -> res_boxsimu_2:DI1, then rerun the bounded DEV M3 smoke.";
if (scope === "m3-box-simu-identity") return "Fix DEV box-simu instance identity so direct endpoints expose distinct res_boxsimu_1 and res_boxsimu_2 resources.";
if (scope === "m3-gateway-simu-identity") return "Fix DEV gateway-simu instance identity so direct endpoints expose two distinct gateway sessions.";
if (scope === "m3-hardware-loop-runtime") return "Prove the real DEV M3 trusted loop res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, trace, audit, and evidence identifiers.";
if (scope === "runner-kubeconfig-readonly-gap" || scope === "m3-service-discovery" || scope === "m3-direct-target-missing") return "Repair the #46 runner kubeconfig mount/permission or document the approved alternate read-only KUBECONFIG/service-discovery path; do not classify this as D601 global offline.";
if (scope.includes("kubectl") || scope.includes("k3s")) return "Provide read-only kubectl/kubeconfig observability for hwlab-dev.";
return "Resolve the blocker and attach source/local/dry-run/DEV-live evidence at the correct level.";
}
function evidenceRequiredFor(scope) {
if (scope === "base-image") return "Base-image preflight status=ready with approved Node 20 builder base and no UniDesk/runtime substitute.";
if (scope === "hwlab-router" || scope === "hwlab-tunnel-client" || scope === "hwlab-edge-proxy") return "Real repo entrypoints or dedicated Dockerfiles for route services, followed by artifact preflight.";
if (scope.includes("artifact") || scope === "ghcr") return "Artifact publish report with ciPublished=true, registryVerified=true, and sha256 digest for each frozen service ID.";
if (scope.includes("kubectl") || scope.includes("k3s")) return "Read-only kubectl/k3s report proving pods/services/configmaps are observable in hwlab-dev without reading Secrets.";
if (scope.includes("cloud-api-db")) return "Cloud API health/live output showing DB env ready and redacted secret references, without secret material.";
if (scope === "db-live") return "Cloud API /health/live output with ready=true, connected=true, liveDbEvidence=true, and redacted secret references.";
if (scope === "m3-patch-panel-wiring") return "Read-only patch-panel /status and /wiring showing active res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 before a bounded write/read smoke records operation, trace, audit, and evidence IDs.";
if (scope === "m3-box-simu-identity") return "Read-only direct box-simu /health/live and /status output showing distinct res_boxsimu_1 and res_boxsimu_2 resources.";
if (scope === "m3-gateway-simu-identity") return "Read-only direct gateway-simu /health/live and /status output showing two distinct gateway identities/sessions.";
if (scope === "m3-hardware-loop-runtime") return "Operation, trace, audit, and evidence IDs from the real DEV DO1 -> patch-panel -> DI1 trusted loop.";
if (scope === "runner-kubeconfig-readonly-gap" || scope === "m3-service-discovery" || scope === "m3-direct-target-missing") return "Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write.";
if (scope.includes("edge") || scope.includes("ingress") || scope.includes("frp")) return "Read-only DEV route observation for :16667/frp/edge/router with HWLAB service identity and artifact identity.";
return "A committed report with the exact evidence level and command used.";
}
function validateReport(report) {
assert.equal(report.reportVersion, "v2", "reportVersion");
assert.equal(report.issue, issue(58), "issue");
assert.equal(report.environment, ENVIRONMENT_DEV, "environment");
assert.equal(report.endpoint, DEV_ENDPOINT, "endpoint");
assert.equal(report.frontendEndpoint, DEV_FRONTEND_ENDPOINT, "frontendEndpoint");
assert.equal(report.activeEndpoints.frontend, activeBrowserRoot, "activeEndpoints.frontend");
assert.equal(report.activeEndpoints.apiLive, activeApiLiveEndpoint, "activeEndpoints.apiLive");
assert.equal(report.devOnly, true, "devOnly");
assert.equal(report.prodDisabled, true, "prodDisabled");
assert.deepEqual(Object.keys(report.levels), [...levels], "level keys");
assert.equal(report.latestFrontendDevFact.revision, latestFrontendRevision, "latestFrontendDevFact.revision");
assert.equal(report.latestFrontendDevFact.promotesM3M4M5, false, "frontend fact cannot promote M3/M4/M5");
assert.deepEqual(
report.milestoneLevelClassification.map((item) => item.milestone),
["M0", "M1", "M2", "M3", "M4", "M5"],
"milestoneLevelClassification"
);
assert.deepEqual(
report.milestoneBlockerClassification.map((item) => item.milestone),
["M3", "M4", "M5"],
"milestoneBlockerClassification"
);
assert.ok(report.blockers.length >= 1, "must remain blocked without DEV-LIVE evidence");
assert.equal(report.overall.status, "blocked", "overall status");
assert.ok(report.nextSteps.length >= 1, "next steps required");
for (const blocker of report.blockers) {
assert.ok(blockerTypes.has(blocker.type), `unknown blocker type ${blocker.type}`);
assert.match(blocker.priority, /^P[0-3]$/u, `invalid priority ${blocker.priority}`);
assert.equal(blocker.status, "open", "blocker status");
}
}
export async function buildReport() {
const entries = await Promise.all(
Object.entries(reportPaths).map(async ([key, relativePath]) => {
const report = await readJSON(relativePath);
return [key, { ...report, path: relativePath }];
})
);
const reports = Object.fromEntries(entries);
const evidence = [
...sourceContracts.map(sourceEntry),
...collectM2Evidence(reports),
...collectM3Evidence(reports),
...collectM4Evidence(reports),
...collectM5Evidence(reports)
];
const blockers = collectBlockers(reports);
const milestones = deriveMilestones(evidence, blockers);
const milestoneLevelClassification = buildMilestoneLevelClassification(milestones);
const dod = buildDoD(reports, milestones, blockers);
const currentDevLayering = buildCurrentDevLayering(reports, blockers);
const milestoneBlockerClassification = buildMilestoneBlockerClassification(reports);
const nextSteps = buildNextSteps(blockers);
const report = {
"$schema": "https://hwlab.pikastech.local/schemas/dev-m5-gate-aggregator-v2.schema.json",
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-m5-gate-aggregator-v2.json",
reportVersion: "v2",
reportKind: "dev-m5-gate-aggregator",
issue: issue(58),
supports: [issue(7), issue(9), issue(23), issue(26), issue(31), issue(33), issue(34), issue(35), issue(36), issue(37), issue(38), issue(39), issue(46), issue(64)],
generatedAt: new Date().toISOString(),
generatedFromCommit: gitCommit(),
environment: ENVIRONMENT_DEV,
endpoint: DEV_ENDPOINT,
frontendEndpoint: DEV_FRONTEND_ENDPOINT,
activeEndpoints: {
frontend: activeBrowserRoot,
apiLive: activeApiLiveEndpoint
},
deprecatedEndpoints: deprecatedLegacyPublicEndpoints.map((endpoint) => ({
endpoint,
status: "historical/deprecated",
activeGreenEligible: false
})),
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle(
"Current M5 evidence aggregator report; source, local, and dry-run evidence are not DEV-LIVE acceptance."
),
safety: {
reportOnly: true,
noDeploy: true,
noProd: true,
noSecretRead: true,
noRuntimeRestart: true,
noLiveProbe: true,
noHeavyE2E: true,
noUniDeskRuntimeSubstitute: true
},
sourceReports: Object.fromEntries(
Object.entries(reports).map(([key, report]) => [
key,
{
path: report.path,
issue: report.issue,
taskId: report.taskId ?? report.reportKind,
lifecycleState: reportLifecycleState(report),
status: primaryStatus(report),
commitId: short(report.commitId ?? report.target?.commitId)
}
])
),
overall: {
status: dod.status,
green: dod.green,
reason: dod.green
? "All #9 DEV DoD checks are green."
: `Frontend revision ${latestFrontendRevision} and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
},
latestFrontendDevFact: {
...frontendDevFact,
issue: issue(99),
supports: [issue(99), issue(108), issue(78)],
endpoint: activeBrowserRoot,
evidenceLevel: "DEV-LIVE",
promotesM3M4M5: false
},
dod,
currentDevLayering,
milestoneLevelClassification,
milestoneBlockerClassification,
milestones,
evidence,
levels: groupByLevel(evidence, blockers),
blockers,
nextSteps,
validationCommands
};
validateReport(report);
return report;
}
export function formatCheckSummary(report) {
return {
ok: true,
taskId: report.reportKind,
issue: report.issue,
status: report.overall.status,
green: report.overall.green,
blockers: report.blockers.length,
activeEndpoints: report.activeEndpoints,
frontendRevision: report.latestFrontendDevFact.revision,
m3m4m5: Object.fromEntries(
report.milestoneBlockerClassification.map((item) => [item.milestone, item.status])
),
nextStep: report.nextSteps[0]?.action ?? null,
levels: Object.fromEntries(
Object.entries(report.levels).map(([level, entries]) => [level, entries.length])
)
};
}
export function renderMarkdown(report) {
const layeringLines = Object.entries(report.currentDevLayering)
.map(([, layer]) => `| ${layer.label} | ${layer.status} | ${layer.evidenceLevel} | ${layer.summary} | ${layer.nextRequired} |`)
.join("\n");
const milestoneLevelLines = report.milestoneLevelClassification
.map((item) => `| ${item.milestone} | ${item.currentLevel} | ${item.status} | ${item.strongestEvidenceLevel} | ${item.liveEvidence} | ${item.summary} |`)
.join("\n");
const milestoneLines = report.milestones
.map((milestone) => `| ${milestone.id} | ${milestone.status} | ${milestone.highestVisibleLevel} | ${milestone.liveEvidence} | ${milestone.summary} |`)
.join("\n");
const dodLines = report.dod.checks
.map((check) => `| ${check.id} | ${check.status} | ${check.evidenceLevel} | ${check.summary} |`)
.join("\n");
const blockerClassificationLines = report.milestoneBlockerClassification
.map((item) => `| ${item.milestone} | ${item.status} | ${item.currentLevel} | ${item.blockerClass} | ${item.dependency} | ${item.nextRequired} |`)
.join("\n");
const blockerLines = report.blockers
.map((blocker) => `| ${blocker.priority} | ${blocker.unblockOrder} | ${blocker.type} | ${blocker.scope} | ${blocker.summary} |`)
.join("\n");
const nextLines = report.nextSteps
.map((step) => `${step.order}. ${step.action}\n Evidence required: ${step.evidenceRequired}`)
.join("\n");
return `# HWLAB M5 DEV Gate Aggregator v2
Status: ${report.overall.status}
Generated from: \`${report.generatedFromCommit}\`
Scope: DEV only, report-only
Active frontend: \`${report.activeEndpoints.frontend}\`
Active API/live: \`${report.activeEndpoints.apiLive}\`
Deprecated public endpoints: ${report.deprecatedEndpoints.map((item) => `\`${item.endpoint}\``).join(", ")}
## Summary
${report.overall.reason}
## Frontend DEV Fact
The latest accepted #99/#108 frontend DEV fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. This is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence only and does not promote M3, M4, or M5.
## Current DEV Layering
| Layer | Status | Evidence level | Current conclusion | Required next proof |
| --- | --- | --- | --- | --- |
${layeringLines}
## M0-M5 Level Classification
| Milestone | Current classification | Status | Strongest evidence | Live evidence | Summary |
| --- | --- | --- | --- | --- | --- |
${milestoneLevelLines}
## Milestones
| Milestone | Status | Highest visible level | Live evidence | Summary |
| --- | --- | --- | --- | --- |
${milestoneLines}
## #9 DoD Checks
| Check | Status | Evidence level | Summary |
| --- | --- | --- | --- |
${dodLines}
## M3/M4/M5 Blocker Classification
| Milestone | Status | Current level | Blocker class | Dependency | Required next proof |
| --- | --- | --- | --- | --- | --- |
${blockerClassificationLines}
## Blockers
| Priority | Order | Type | Scope | Summary |
| --- | ---: | --- | --- | --- |
${blockerLines}
## Next Unblock Order
${nextLines}
## Validation
${report.validationCommands.map((command) => `- \`${command}\``).join("\n")}
## Boundary
This report reads committed reports and fixtures only. It does not deploy, call DEV, call PROD, read secrets, restart runtime, run heavy e2e, or substitute UniDesk runtime for HWLAB runtime.
`;
}