Files
pikasTech-HWLAB/scripts/src/dev-evidence-blocker-aggregator.mjs
T
Lyon ab11565640 test: refresh HWLAB DEV live evidence
Merge PR #71 after commander review. Rebaselines DEV live evidence to 16666/16667, removes stale edge/frp blocker classification, keeps DB live and M3 patch-panel runtime as real blockers. Complies with HWLAB #78 / DC-DCSN-P0-2026-003 by preserving DEV-LIVE evidence boundaries and not treating Gate/UI as MVP success.
2026-05-22 13:15:31 +08:00

894 lines
33 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 { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
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 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",
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."
}
]);
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 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.startsWith("d601-")) {
return {
...blocker,
priority: "P1",
unblockOrder: 4,
unblocks: [issue(34), issue(33), issue(36), issue(38)],
rationale: "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance."
};
}
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 (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."
};
}
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 blocker.scope.includes("edge") ||
blocker.scope.includes("ingress") ||
blocker.scope.includes("frp") ||
blocker.summary.includes(":16667") ||
blocker.summary.includes("16667") ||
blocker.summary.includes(":6667") ||
blocker.summary.includes("6667");
}
function sourceEntry(entry) {
return {
milestone: entry.milestone,
issue: entry.issue,
level: entry.level,
status: entry.status,
category: entry.category,
sources: entry.sources,
commands: entry.commands,
summary: entry.summary
};
}
function reportEvidence({ milestone, level, report, status, category, summary, evidence = [], commands = [] }) {
return {
milestone,
issue: report.issue,
taskId: report.taskId ?? report.reportKind,
reportPath: report.path,
commitId: short(report.commitId ?? report.target?.commitId),
level,
status,
category,
commands,
evidence,
summary
};
}
function collectM2Evidence(reports) {
const preflight = reports.devPreflight;
const deploy = reports.devDeploy;
const artifacts = reports.devArtifacts;
const edge = reports.devEdgeHealth;
const d601 = reports.d601Observability;
const artifactIdentity = preflight.artifactIdentity;
const edgeLive = hasCurrentLiveEdgeEvidence(edge);
return [
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: 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: "BLOCKED",
report: d601,
status: d601.conclusion,
category: "d601-observability",
evidence: [
`kubectl=${d601.environment?.binaries?.kubectl?.available === true ? "available" : "missing"}`,
`k3s=${d601.environment?.binaries?.k3s?.available === true ? "available" : "missing"}`,
`clusterReadable=${d601.cluster?.readable === true ? "yes" : "no"}`
],
commands: d601.validationCommands,
summary: "D601 k3s observability is blocked; cluster state has not been read."
})
];
}
function collectM3Evidence(reports) {
const m3 = reports.devM3Hardware;
const m5Milestone = reports.devM5Gate.milestones?.find((item) => item.id === "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 --live --confirm-dev --confirmed-non-production"],
summary: m3.liveOperation?.summary ?? "No live M3 hardware operation was observed."
})
];
}
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)) {
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 isStaleLegacyIngressBlocker(blocker, sourceReport, reports) {
const text = `${blocker.scope ?? ""} ${blocker.summary ?? ""}`;
const staleLegacyPort = text.includes("74.48.78.17:6667");
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 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 || (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 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,
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);
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: artifactIdentity.publishVerified ? "pass" : "blocked",
evidenceLevel: artifactIdentity.publishVerified ? "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}`
},
{
id: "d601-k3s-observability",
status: d601.cluster?.readable ? "pass" : "blocked",
evidenceLevel: d601.cluster?.readable ? "DEV-LIVE" : "BLOCKED",
summary: d601.cluster?.readable
? "D601 hwlab-dev cluster was readable."
: "D601 runner lacks kubectl/k3s/kubeconfig observability for hwlab-dev."
},
{
id: "dev-edge-frp-16667",
status: edgeLive && !hasBlocker(blockers, isEdgeOrFrpBlocker) ? "pass" : "blocked",
evidenceLevel: edgeLive && !hasBlocker(blockers, isEdgeOrFrpBlocker) ? "DEV-LIVE" : "BLOCKED",
summary: edgeLive
? "Committed edge report proves read-only public HTTP on :16667 /health and /health/live; old preflight network probe blockers are stale."
: "No committed report proves live HTTP 200/JSON on http://74.48.78.17:16667."
},
{
id: "cloud-api-db-ready",
status: hasBlocker(blockers, (blocker) => blocker.scope.includes("cloud-api-db")) ? "blocked" : "pass",
evidenceLevel: "BLOCKED",
summary: "Manifest-level DB env exists, but live DB health readiness is still blocked/missing."
},
{
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 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.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.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.devOnly, true, "devOnly");
assert.equal(report.prodDisabled, true, "prodDisabled");
assert.deepEqual(Object.keys(report.levels), [...levels], "level keys");
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 dod = buildDoD(reports, milestones, blockers);
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(31), issue(33), issue(34), issue(35), issue(36), issue(37), issue(38), issue(39), issue(46)],
generatedAt: new Date().toISOString(),
generatedFromCommit: gitCommit(),
environment: ENVIRONMENT_DEV,
endpoint: DEV_ENDPOINT,
devOnly: true,
prodDisabled: true,
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,
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."
: "SOURCE, LOCAL, DRY-RUN, and edge DEV-LIVE evidence exists, but acceptance is still blocked by artifact, observability, DB, and loop evidence gaps."
},
dod,
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,
nextStep: report.nextSteps[0]?.action ?? null,
levels: Object.fromEntries(
Object.entries(report.levels).map(([level, entries]) => [level, entries.length])
)
};
}
export function renderMarkdown(report) {
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 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
## Summary
${report.overall.reason}
## Milestones
| Milestone | Status | Highest visible level | Live evidence | Summary |
| --- | --- | --- | --- | --- |
${milestoneLines}
## #9 DoD Checks
| Check | Status | Evidence level | Summary |
| --- | --- | --- | --- |
${dodLines}
## 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.
`;
}