Files
pikasTech-HWLAB/internal/mvp-gate/summary.mjs
T
2026-05-24 02:31:36 +00:00

549 lines
19 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { DEV_ENDPOINT } from "../protocol/index.mjs";
export { DEV_ENDPOINT };
export const DEFAULT_GATE_REPORT_SOURCE = "source:mvp-gate-summary";
export const DEFAULT_M5_PLAN_PATH = "fixtures/mvp/m5-e2e/dry-run-plan.json";
export const M3_WORKBENCH_TOPOLOGY = Object.freeze({
projectId: "prj_m3_hardware_loop",
projectStatus: "blocked",
gateways: Object.freeze([
Object.freeze({
gatewaySessionId: "gws_gwsimu_1",
serviceId: "hwlab-gateway-simu",
gatewayId: "gwsimu_1",
status: "connected",
environment: "dev",
endpoint: null
}),
Object.freeze({
gatewaySessionId: "gws_gwsimu_2",
serviceId: "hwlab-gateway-simu",
gatewayId: "gwsimu_2",
status: "connected",
environment: "dev",
endpoint: null
})
]),
boxResources: Object.freeze([
Object.freeze({
resourceId: "res_boxsimu_1",
gatewaySessionId: "gws_gwsimu_1",
boxId: "boxsimu_1",
resourceType: "simulator_endpoint",
name: "BOX-SIMU 1 source DO1",
state: "available",
environment: "dev"
}),
Object.freeze({
resourceId: "res_boxsimu_2",
gatewaySessionId: "gws_gwsimu_2",
boxId: "boxsimu_2",
resourceType: "simulator_endpoint",
name: "BOX-SIMU 2 target DI1",
state: "available",
environment: "dev"
})
]),
patchPanel: Object.freeze({
patchPanelStatusId: "pps_m3_hardware_loop",
serviceId: "hwlab-patch-panel",
state: "blocked",
environment: "dev",
activeConnectionCount: 1,
activeConnections: Object.freeze([
Object.freeze({
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
})
])
})
});
function readJsonFile(repoRoot, relativePath) {
const absolutePath = path.resolve(repoRoot, relativePath);
const raw = fs.readFileSync(absolutePath, "utf8");
return JSON.parse(raw);
}
function defaultMvpGateReport() {
return {
reportVersion: "v1",
issue: "pikasTech/HWLAB#39",
taskId: "dev-mvp-gate-source-summary",
commitId: "source-summary",
acceptanceLevel: "dev_mvp_gate",
devOnly: true,
prodDisabled: true,
gateStatus: "blocked",
sourceContract: {
status: "pass",
documents: [
"docs/dev-gate-report.md",
"docs/dev-acceptance-matrix.md",
"docs/m0-contract-audit.md",
"docs/m1-local-smoke.md",
"docs/m2-dev-deploy-smoke.md",
"docs/m3-hardware-loop.md",
"docs/m4-agent-loop.md",
"docs/m5-mvp-e2e.md",
"docs/runtime-boundary-guard.md",
"docs/topology-constraints.md",
"docs/artifact-catalog.md",
"docs/schema-drift-map.md",
"protocol/README.md",
"protocol/audit.md",
"protocol/evidence-chain.md",
"protocol/mvp-e2e-contract.md"
],
summary: "The DEV gate is anchored to the frozen matrix, gate contract, and M0-M5 milestone source docs; progress and acceptance evidence live in GitHub issue or PR comments, not repository report files."
},
validationCommands: [
"node scripts/repo-reports-guard.mjs",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
],
localSmoke: {
status: "pass",
commands: [
"node scripts/validate-contract.mjs",
"node scripts/validate-artifact-catalog.mjs",
"node scripts/m1-contract-smoke.mjs",
"node scripts/m2-dev-deploy-smoke.mjs --dry-run"
],
evidence: [
"Source contracts remain parseable.",
"MVP dry-run fixtures remain local/source only.",
"Repository report directory is forbidden by repo guard."
],
summary: "M0-M2 contract and smoke checks remain local/source scoped and do not create repository report files."
},
dryRun: {
status: "pass",
commands: [
"node tools/hwlab-cli/bin/hwlab-cli.mjs health",
"node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run"
],
evidence: [
"Dry-run uses fixtures and performs no DEV/PROD mutations.",
"Dry-run evidence is not live acceptance."
],
summary: "The M5 orchestration dry-run is fixture-only and does not establish a live DEV gate."
},
devPreconditions: {
status: "blocked",
classification: "runtime durable adapter; skills injection; hardware loop",
requirements: [
"Current DEV route and health checks must be rerun when needed and summarized in GitHub.",
"Runtime durable adapter proves postgres schema, migration, and read-query readiness before M4/M5 can promote.",
"Agent manager, skills service, and worker jobs expose explicit skill commit/version before M4/M5 can promote.",
"M3 hardware loop proves box-simu-1 DO1 to box-simu-2 DI1 via patch-panel.",
"Keep the run DEV-only and avoid PROD, heavy e2e, secrets, restarts, or UniDesk substitution."
],
commands: [
"curl -fsS --max-time 8 http://74.48.78.17:16666/",
"curl -fsS --max-time 8 http://74.48.78.17:16667/health/live",
"curl -fsS --max-time 8 http://74.48.78.17:16667/health",
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production",
"node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
],
evidence: [
"Repository report files were removed and no longer qualify as current evidence.",
"Live DEV acceptance must be represented in #7, the owning issue, daily brief, or PR comments."
],
summary: "Live MVP remains blocked until current DEV checks clear runtime durability, skills injection, and M3 hardware-loop blockers outside repository report files."
},
blockers: [
{
type: "runtime_blocker",
scope: "runtime-durable-adapter",
status: "open",
sourceIssue: "pikasTech/HWLAB#164",
classification: "runtime_durable_adapter_query_blocked",
summary:
"cloud-api durable runtime adapter readiness is blocked by runtime_durable_adapter_query_blocked; it must prove schema, migration, and read-query durability before M4/M5 promotion."
},
{
type: "agent_blocker",
scope: "skills-commit-version-injection",
status: "open",
sourceIssue: "pikasTech/HWLAB#164",
classification: "skills injection",
summary: "DEV skills injection must expose explicit skills commit and version across manager, skills, and worker paths."
},
{
type: "runtime_blocker",
scope: "m3-hardware-loop-runtime",
status: "open",
sourceIssue: "pikasTech/HWLAB#38",
classification: "hardware loop",
summary: "Live M3 must prove the patch-panel trusted loop with operation, trace, audit, and evidence identifiers."
}
],
milestones: [
{
id: "M0",
status: "pass",
commands: ["node scripts/validate-contract.mjs"],
evidence: ["Source contracts are parseable."],
summary: "Frozen contracts remain source-ready."
},
{
id: "M1",
status: "pass",
commands: ["node scripts/m1-contract-smoke.mjs"],
evidence: ["Local skeleton smoke remains local-only."],
summary: "The local M1 contract smoke passes."
},
{
id: "M2",
status: "pass",
commands: ["node scripts/m2-dev-deploy-smoke.mjs --dry-run"],
evidence: ["Deploy smoke remains fixture-only."],
summary: "The deploy smoke validates source contracts without live acceptance."
},
{
id: "M3",
status: "blocked",
commands: ["node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"],
evidence: ["Repository report snapshots were removed; current live trusted-loop proof is required."],
summary: "Active DEV-LIVE M3 remains blocked by hardware loop topology until the trusted loop is proven.",
blocker: "hardware loop"
},
{
id: "M4",
status: "blocked",
commands: ["node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"],
evidence: ["Runtime durable adapter readiness and skills injection must be current live facts."],
summary: "Active DEV-LIVE M4 remains blocked by runtime durable adapter readiness and skills commit/version injection.",
blocker: "runtime_durable_adapter_query_blocked; skills injection"
},
{
id: "M5",
status: "blocked",
commands: ["node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run"],
evidence: ["M5 dry-run remains fixture-only; live acceptance must be posted in GitHub."],
summary: "The M5 orchestration is green in dry-run mode, but active DEV-LIVE acceptance remains blocked.",
blocker: "runtime durable adapter; skills injection; hardware loop"
}
]
};
}
function countByServiceId(items) {
return new Map(items.map((item) => [item.serviceId, item]));
}
function requireArray(value, label) {
if (!Array.isArray(value)) {
throw new Error(`${label} must be an array`);
}
return value;
}
function assertRequiredMvpGate(report, plan) {
if (report.devOnly !== true || report.prodDisabled !== true) {
throw new Error("MVP gate report must be DEV-only and PROD-disabled");
}
if (report.gateStatus !== "blocked") {
throw new Error("MVP gate report must not claim live DEV pass in this MVP fixture");
}
if (plan.mode !== "dry-run" || plan.environment !== "dev" || plan.endpoint !== DEV_ENDPOINT) {
throw new Error("M5 plan must stay dry-run, dev, and pinned to the frozen DEV endpoint");
}
const milestones = requireArray(report.milestones, "report.milestones");
for (const milestone of ["M0", "M1", "M2", "M3", "M4", "M5"]) {
if (!milestones.some((item) => item.id === milestone)) {
throw new Error(`MVP gate report missing milestone ${milestone}`);
}
}
const artifacts = requireArray(plan.artifacts, "plan.artifacts");
const health = requireArray(plan.health, "plan.health");
const gatewaySessions = requireArray(plan.gatewaySessions, "plan.gatewaySessions");
const boxResources = requireArray(plan.boxResources, "plan.boxResources");
const evidenceRecords = requireArray(plan.evidenceRecords, "plan.evidenceRecords");
const operations = requireArray(plan.operations, "plan.operations");
const byService = countByServiceId(artifacts);
for (const serviceId of [
"hwlab-cloud-web",
"hwlab-cloud-api",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-cli"
]) {
if (!byService.has(serviceId)) {
throw new Error(`M5 plan missing artifact for ${serviceId}`);
}
}
if (gatewaySessions.length < 2) {
throw new Error("M5 plan must include two gateway sessions");
}
if (boxResources.length < 2) {
throw new Error("M5 plan must include two box resources");
}
if (!plan.patchPanelStatus?.patchPanelStatusId) {
throw new Error("M5 plan missing patch-panel status evidence");
}
if (!plan.agentSession?.agentSessionId || !plan.workerSession?.workerSessionId) {
throw new Error("M5 plan missing agent and worker session evidence");
}
if (evidenceRecords.length < 2 || operations.length < 2) {
throw new Error("M5 plan must include direct and agent evidence records");
}
}
function summarizeArtifact(artifact) {
return {
serviceId: artifact.serviceId,
commitId: artifact.commitId,
image: artifact.image,
tag: artifact.tag,
digest: artifact.digest,
digestNotApplicableReason: artifact.digestNotApplicableReason ?? null,
buildSource: artifact.buildSource,
deployEnv: artifact.deployEnv,
healthTimestamp: artifact.healthTimestamp
};
}
function summarizeHealth(health) {
return {
healthId: health.healthId,
serviceId: health.serviceId,
component: health.component,
status: health.status,
deployEnv: health.deployEnv,
endpoint: health.endpoint ?? null,
observedBy: health.observedBy,
dryRun: health.dryRun === true,
healthTimestamp: health.healthTimestamp
};
}
function summarizeMilestone(milestone) {
return {
id: milestone.id,
status: milestone.status,
commandCount: milestone.commands?.length ?? 0,
evidenceCount: milestone.evidence?.length ?? 0,
summary: milestone.summary
};
}
function summarizeBlocker(blocker) {
return {
type: blocker.type,
scope: blocker.scope,
status: blocker.status,
summary: blocker.summary
};
}
function summarizeStep(step) {
return {
id: step.id,
order: step.order,
title: step.title,
kind: step.kind,
requires: step.requires,
inputs: step.inputs,
outputs: step.outputs,
acceptanceStepIds: step.acceptanceStepIds
};
}
function summarizeGatewaySession(session) {
return {
gatewaySessionId: session.gatewaySessionId,
serviceId: session.serviceId,
gatewayId: session.gatewayId,
status: session.status,
environment: session.environment,
endpoint: session.endpoint ?? null
};
}
function summarizeBoxResource(resource) {
return {
resourceId: resource.resourceId,
gatewaySessionId: resource.gatewaySessionId,
boxId: resource.boxId,
resourceType: resource.resourceType,
name: resource.name,
state: resource.state,
environment: resource.environment
};
}
function summarizeEvidence(record) {
return {
evidenceId: record.evidenceId,
operationId: record.operationId,
agentSessionId: record.agentSessionId ?? null,
workerSessionId: record.workerSessionId ?? null,
kind: record.kind,
uri: record.uri,
sha256: record.sha256,
serviceId: record.serviceId,
environment: record.environment,
traceId: record.metadata?.traceId ?? null,
dryRun: record.metadata?.dryRun === true,
createdAt: record.createdAt ?? null
};
}
function summarizeAuditEvent(event) {
return {
auditId: event.auditId,
traceId: event.traceId,
action: event.action,
targetType: event.targetType,
targetId: event.targetId,
projectId: event.projectId ?? null,
gatewaySessionId: event.gatewaySessionId ?? null,
operationId: event.operationId ?? null,
workerSessionId: event.workerSessionId ?? null,
serviceId: event.serviceId,
environment: event.environment,
outcome: event.outcome ?? null,
dryRun: event.metadata?.dryRun === true,
occurredAt: event.occurredAt
};
}
function clone(value) {
return JSON.parse(JSON.stringify(value));
}
export function buildMvpGateSummary({ report, plan }) {
assertRequiredMvpGate(report, plan);
const artifacts = plan.artifacts.map(summarizeArtifact);
const health = plan.health.map(summarizeHealth);
const milestones = report.milestones.map(summarizeMilestone);
const blockers = report.blockers.map(summarizeBlocker);
const steps = plan.steps.map(summarizeStep).sort((a, b) => a.order - b.order);
const blocked = blockers.some((blocker) => blocker.status === "open") || report.gateStatus === "blocked";
return {
generatedFrom: {
gateReport: DEFAULT_GATE_REPORT_SOURCE,
m5Plan: DEFAULT_M5_PLAN_PATH
},
issue: "pikasTech/HWLAB#56",
supports: ["pikasTech/HWLAB#7", "pikasTech/HWLAB#9"],
gateStatus: report.gateStatus,
blocked,
acceptanceLevel: report.acceptanceLevel,
reportCommitId: report.commitId,
dryRunOnly: true,
devOnly: report.devOnly === true,
prodDisabled: report.prodDisabled === true,
environment: plan.environment,
endpoint: plan.endpoint,
namespace: "hwlab-dev",
sourceSummary: report.sourceContract.summary,
localSmoke: report.localSmoke,
dryRun: report.dryRun,
devPreconditions: report.devPreconditions,
blockers,
milestones,
artifactCount: artifacts.length,
healthCount: health.length,
gatewaySessionCount: plan.gatewaySessions.length,
boxResourceCount: plan.boxResources.length,
patchPanelCount: 1,
operationCount: plan.operations.length,
evidenceCount: plan.evidenceRecords.length,
traceCount: plan.traceEvents.length,
auditCount: plan.auditEvents.length,
cleanupCount: plan.cleanup.length,
artifacts,
health,
topology: clone(M3_WORKBENCH_TOPOLOGY),
m5DryRunTopology: {
projectId: plan.project.projectId,
projectStatus: plan.project.status,
gateways: plan.gatewaySessions.map(summarizeGatewaySession),
boxResources: plan.boxResources.map(summarizeBoxResource),
patchPanel: {
patchPanelStatusId: plan.patchPanelStatus.patchPanelStatusId,
serviceId: plan.patchPanelStatus.serviceId,
state: plan.patchPanelStatus.state,
environment: plan.patchPanelStatus.environment,
activeConnectionCount: plan.patchPanelStatus.activeConnections.length,
activeConnections: plan.patchPanelStatus.activeConnections
}
},
agent: {
agentSessionId: plan.agentSession.agentSessionId,
agentServiceId: plan.agentSession.serviceId,
agentStatus: plan.agentSession.status,
workerSessionId: plan.workerSession.workerSessionId,
workerServiceId: plan.workerSession.serviceId,
workerStatus: plan.workerSession.status,
projectId: plan.agentSession.projectId,
environment: plan.agentSession.environment,
cleanupId: plan.agentSession.metadata.cleanupId
},
operations: plan.operations.map((operation) => ({
operationId: operation.operationId,
requestedBy: operation.requestedBy,
status: operation.status,
resourceId: operation.resourceId,
capabilityId: operation.capabilityId,
agentSessionId: operation.agentSessionId ?? null,
workerSessionId: operation.workerSessionId ?? null,
environment: operation.environment,
dryRun: operation.input?.dryRun === true,
requestedAt: operation.requestedAt ?? null,
updatedAt: operation.updatedAt ?? null
})),
auditEvents: plan.auditEvents.map(summarizeAuditEvent),
evidenceRecords: plan.evidenceRecords.map(summarizeEvidence),
steps,
realDevGate: {
sourcePlanCommand: plan.realDevGate.command,
command: `${plan.realDevGate.command} --live --confirm-dev --confirmed-non-production`,
requiresAllBlockersClear: plan.realDevGate.requiresAllBlockersClear,
requiresDryRunPass: plan.realDevGate.requiresDryRunPass,
requiresArtifactObservability: plan.realDevGate.requiresArtifactObservability,
requiresD601RouteObservation: plan.realDevGate.requiresD601RouteObservation,
requiresHumanApprovalForRealDev: plan.realDevGate.requiresHumanApprovalForRealDev
},
safety: {
mode: plan.mode,
allowNetwork: plan.safety.allowNetwork,
allowDeploy: plan.safety.allowDeploy,
allowBrowserE2E: plan.safety.allowBrowserE2E,
allowSecrets: plan.safety.allowSecrets,
prohibitedActions: plan.safety.prohibitedActions
}
};
}
export function loadMvpGateSummary(repoRoot, options = {}) {
const reportPath = options.reportPath ?? null;
const planPath = options.planPath ?? DEFAULT_M5_PLAN_PATH;
const report = reportPath ? readJsonFile(repoRoot, reportPath) : defaultMvpGateReport();
const plan = readJsonFile(repoRoot, planPath);
const summary = buildMvpGateSummary({ report, plan });
summary.generatedFrom = {
gateReport: reportPath ?? DEFAULT_GATE_REPORT_SOURCE,
m5Plan: planPath
};
return summary;
}