1430 lines
61 KiB
JavaScript
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.
|
|
`;
|
|
}
|