From 35ecb5e038b0b06962151f3f1751b72e1d9f6c73 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 18:28:19 +0000 Subject: [PATCH] feat: add M5 DEV gate aggregator v2 --- docs/dev-evidence-blocker-aggregator.md | 56 +- docs/operator-runbook.md | 28 + package.json | 2 +- .../dev-gate/dev-m5-gate-aggregator-v2.json | 1262 +++++++++++++++++ reports/dev-gate/dev-m5-gate-aggregator-v2.md | 81 ++ scripts/dev-evidence-blocker-aggregator.mjs | 301 +--- .../src/dev-evidence-blocker-aggregator.mjs | 838 +++++++++++ scripts/validate-dev-gate-report.mjs | 257 +++- 8 files changed, 2538 insertions(+), 287 deletions(-) create mode 100644 reports/dev-gate/dev-m5-gate-aggregator-v2.json create mode 100644 reports/dev-gate/dev-m5-gate-aggregator-v2.md create mode 100644 scripts/src/dev-evidence-blocker-aggregator.mjs diff --git a/docs/dev-evidence-blocker-aggregator.md b/docs/dev-evidence-blocker-aggregator.md index 019ec851..c425b437 100644 --- a/docs/dev-evidence-blocker-aggregator.md +++ b/docs/dev-evidence-blocker-aggregator.md @@ -1,8 +1,9 @@ # DEV Evidence / Blocker Aggregator -This is the lightweight report-only entry point for `pikasTech/HWLAB#41`. It -helps a commander decide whether the DEV acceptance path is green, blocked, or -ready for another repair round across `#33-#39`, `#7`, and `#31`. +This is the lightweight report-only entry point for `pikasTech/HWLAB#41` and +the M5 DEV gate aggregator v2 for `pikasTech/HWLAB#58`. It helps a commander +decide whether the DEV acceptance path is green, blocked, or ready for another +repair round across `#33-#39`, `#46`, `#7`, and the MVP DoD in `#9`. Run from the repository root: @@ -16,6 +17,17 @@ For a short validation response: node scripts/dev-evidence-blocker-aggregator.mjs --check ``` +To refresh the committed v2 machine and human reports: + +```sh +node scripts/dev-evidence-blocker-aggregator.mjs --write-report +``` + +The generated files are: + +- `reports/dev-gate/dev-m5-gate-aggregator-v2.json` +- `reports/dev-gate/dev-m5-gate-aggregator-v2.md` + ## Source Levels The aggregator keeps evidence levels separate so fixture output cannot be @@ -42,27 +54,37 @@ promoted to live DEV evidence: ## Blocker Posture -If GitHub issues are not readable in the runner, the report marks -`issue-read` as blocked and uses only committed repository artifacts plus the -task prompt. This keeps #33-#39 status visible without treating issue comments -as silently available. +The v2 report reads committed report fixtures only: + +- `reports/dev-gate/dev-preflight-report.json` +- `reports/dev-gate/dev-deploy-report.json` +- `reports/dev-gate/dev-artifacts.json` +- `reports/dev-gate/dev-edge-health.json` +- `reports/dev-gate/dev-m3-hardware-loop.json` +- `reports/dev-gate/dev-m4-agent-loop.json` +- `reports/dev-gate/dev-mvp-gate-report.json` +- `reports/d601-k3s-readonly-observability.json` The report is blocked until real DEV-LIVE evidence is attached for the DEV route and downstream HWLAB runtime services. Current source, local, and dry-run contracts are useful readiness signals, but they are not live DEV proof. -## Next-Round Buckets +## Next-Round Order -The JSON output groups recommended next actions into: +The JSON output groups recommended next actions in dependency order: -- `artifact/base image`: real publish or provenance evidence for frozen HWLAB - service IDs. -- `edge/network`: read-only DEV route observations for ingress, edge, frp, and - D601 router. -- `runtime/health`: DEV health JSON for cloud, gateway/simulator, - patch-panel, agent, worker, skills, and CLI surfaces. -- `M3/M4 live`: narrow authorized DEV-only loop evidence with trace, audit, - operation, session, evidence, and cleanup IDs. +1. `base-image`: preload or tag an approved Node 20 DEV builder image, then + rerun the base-image and artifact publish preflights. +2. `artifact-publish`, `artifact-catalog`, `ghcr`: publish every frozen HWLAB + service artifact and record immutable registry digests for the selected + `origin/main` commit. +3. `d601-k3s`, `kubectl`, kubeconfig/maintenance bridge: restore read-only + hwlab-dev cluster observability without printing Secrets. +4. `cloud-api-db`: prove DEV cloud-api DB env and health readiness with + redacted values only. +5. `dev-edge`, `dev-edge-health`, `dev-ingress-health`: repair + `:6667`/frp/edge/router reachability, then rerun read-only DEV edge health + before M3/M4/M5 live loops. ## Boundaries diff --git a/docs/operator-runbook.md b/docs/operator-runbook.md index 8255fc38..1f969b02 100644 --- a/docs/operator-runbook.md +++ b/docs/operator-runbook.md @@ -64,6 +64,34 @@ but it must include: - evidence, audit, trace, operation, agent session, worker session, gateway session, and project IDs when the phase touches an MVP loop. +## DEV Gate Aggregator v2 + +Use `reports/dev-gate/dev-m5-gate-aggregator-v2.json` and +`reports/dev-gate/dev-m5-gate-aggregator-v2.md` as the current report-only M5 +gate summary for `pikasTech/HWLAB#58`. Refresh them only from committed +fixtures: + +```sh +node scripts/dev-evidence-blocker-aggregator.mjs --write-report +node scripts/validate-dev-gate-report.mjs +``` + +The aggregator separates `SOURCE`, `LOCAL`, `DRY-RUN`, `DEV-LIVE`, and +`BLOCKED`. It must never promote source, local, dry-run, contract-only, +not-run, or failed-live evidence into `DEV-LIVE`. + +Current unblock order: + +1. Provide an approved Node 20 DEV builder base image for artifact publish. +2. Publish every frozen HWLAB service artifact and record immutable registry + digests for the selected `origin/main` commit. +3. Restore read-only D601 `hwlab-dev` k3s observability with `kubectl` or an + approved kubeconfig/maintenance bridge, without reading Secrets. +4. Prove DEV `hwlab-cloud-api` DB env and health readiness with redacted values + only. +5. Repair the `:6667`/`frp`/edge/router path and rerun read-only DEV edge + health before any M3, M4, or M5 live loop. + ## Phase Gates Run phases in order. A later phase may start only after every earlier required diff --git a/package.json b/package.json index c8e0dcb3..12658531 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/validate-contract.mjs", - "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", "l2:smoke": "node scripts/l2-runtime-contract-smoke.mjs", diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.json b/reports/dev-gate/dev-m5-gate-aggregator-v2.json new file mode 100644 index 00000000..035f3f0d --- /dev/null +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.json @@ -0,0 +1,1262 @@ +{ + "$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": "pikasTech/HWLAB#58", + "supports": [ + "pikasTech/HWLAB#7", + "pikasTech/HWLAB#9", + "pikasTech/HWLAB#31", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#39", + "pikasTech/HWLAB#46" + ], + "generatedAt": "2026-05-21T18:27:07.237Z", + "generatedFromCommit": "1e9591a6611e", + "environment": "dev", + "endpoint": "http://74.48.78.17:6667", + "devOnly": true, + "prodDisabled": true, + "safety": { + "reportOnly": true, + "noDeploy": true, + "noProd": true, + "noSecretRead": true, + "noRuntimeRestart": true, + "noLiveProbe": true, + "noHeavyE2E": true, + "noUniDeskRuntimeSubstitute": true + }, + "sourceReports": { + "devPreflight": { + "path": "reports/dev-gate/dev-preflight-report.json", + "issue": "pikasTech/HWLAB#34", + "taskId": "dev-gate-preflight", + "status": "blocked", + "commitId": "1a2efd4915b2" + }, + "devDeploy": { + "path": "reports/dev-gate/dev-deploy-report.json", + "issue": "pikasTech/HWLAB#33", + "taskId": "dev-deploy-apply", + "status": "blocked", + "commitId": "9b47ebe49f70" + }, + "devArtifacts": { + "path": "reports/dev-gate/dev-artifacts.json", + "issue": "pikasTech/HWLAB#35", + "taskId": "dev-artifact-publish", + "status": "blocked", + "commitId": "7e0ccb0" + }, + "devEdgeHealth": { + "path": "reports/dev-gate/dev-edge-health.json", + "issue": "pikasTech/HWLAB#36", + "taskId": "dev-edge-health", + "status": "not_run", + "commitId": "9b47ebe49f70" + }, + "devM3Hardware": { + "path": "reports/dev-gate/dev-m3-hardware-loop.json", + "issue": "pikasTech/HWLAB#38", + "taskId": "dev-m3-hardware-loop", + "status": "blocked", + "commitId": "70bb9168ead2" + }, + "devM4Agent": { + "path": "reports/dev-gate/dev-m4-agent-loop.json", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "status": "blocked", + "commitId": "e9e71a7" + }, + "devM5Gate": { + "path": "reports/dev-gate/dev-mvp-gate-report.json", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "status": "blocked", + "commitId": "6eb4c199b0c6" + }, + "d601Observability": { + "path": "reports/d601-k3s-readonly-observability.json", + "issue": "pikasTech/HWLAB#46", + "taskId": "d601-k3s-readonly-observability", + "status": "blocked", + "commitId": "unknown" + } + }, + "overall": { + "status": "blocked", + "green": false, + "reason": "SOURCE, LOCAL, and DRY-RUN evidence exists, but DEV-LIVE acceptance is blocked by artifact, observability, DB, edge/frp, and loop evidence gaps." + }, + "dod": { + "status": "blocked", + "green": false, + "checks": [ + { + "id": "m0-source-contract", + "status": "pass", + "evidenceLevel": "SOURCE", + "summary": "M0 contract checks are source-level evidence only." + }, + { + "id": "m1-local-smoke", + "status": "pass", + "evidenceLevel": "LOCAL", + "summary": "M1 local smoke is not a live DEV substitute." + }, + { + "id": "artifact-publish-digests", + "status": "blocked", + "evidenceLevel": "BLOCKED", + "summary": "artifactState=contract-skeleton, ciPublished=false, registryVerified=false, sha256=0, not_published=13" + }, + { + "id": "d601-k3s-observability", + "status": "blocked", + "evidenceLevel": "BLOCKED", + "summary": "D601 runner lacks kubectl/k3s/kubeconfig observability for hwlab-dev." + }, + { + "id": "dev-edge-frp-6667", + "status": "blocked", + "evidenceLevel": "BLOCKED", + "summary": "No committed report proves live HTTP 200/JSON on http://74.48.78.17:6667." + }, + { + "id": "cloud-api-db-ready", + "status": "blocked", + "evidenceLevel": "BLOCKED", + "summary": "Manifest-level DB env exists, but live DB health readiness is still blocked/missing." + }, + { + "id": "m5-mvp-dev-live", + "status": "blocked", + "evidenceLevel": "BLOCKED", + "summary": "M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed." + } + ] + }, + "milestones": [ + { + "id": "M0", + "status": "pass", + "highestVisibleLevel": "SOURCE", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 1, + "blockerCount": 0, + "summary": "contract source is available; highest visible level is SOURCE; status is pass." + }, + { + "id": "M1", + "status": "pass", + "highestVisibleLevel": "LOCAL", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 1, + "blockerCount": 0, + "summary": "local smoke is available; highest visible level is LOCAL; status is pass." + }, + { + "id": "M2", + "status": "blocked", + "highestVisibleLevel": "DRY-RUN", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 5, + "blockerCount": 18, + "summary": "deploy/runtime readiness is blocked before live DEV; highest visible level is DRY-RUN; status is blocked." + }, + { + "id": "M3", + "status": "blocked", + "highestVisibleLevel": "LOCAL", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 3, + "blockerCount": 10, + "summary": "hardware loop has source/local shape but no live operation; highest visible level is LOCAL; status is blocked." + }, + { + "id": "M4", + "status": "blocked", + "highestVisibleLevel": "LOCAL", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 3, + "blockerCount": 5, + "summary": "agent loop has local smoke but live preflight is blocked; highest visible level is LOCAL; status is blocked." + }, + { + "id": "M5", + "status": "blocked", + "highestVisibleLevel": "DRY-RUN", + "liveEvidence": "missing_or_blocked", + "evidenceCount": 2, + "blockerCount": 12, + "summary": "dry-run is green but live MVP gate is blocked; highest visible level is DRY-RUN; status is blocked." + } + ], + "evidence": [ + { + "milestone": "M0", + "issue": "pikasTech/HWLAB#31", + "level": "SOURCE", + "status": "pass", + "category": "contract", + "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", + "level": "LOCAL", + "status": "pass", + "category": "local-smoke", + "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." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#34", + "taskId": "dev-gate-preflight", + "reportPath": "reports/dev-gate/dev-preflight-report.json", + "commitId": "1a2efd4915b2", + "level": "SOURCE", + "status": "pass", + "category": "deploy-manifest", + "commands": [ + "node --check scripts/dev-gate-preflight.mjs", + "node --check scripts/src/dev-gate-preflight.mjs", + "node --check scripts/refresh-artifact-catalog.mjs", + "node scripts/dev-gate-preflight.mjs", + "node --check scripts/validate-dev-gate-report.mjs", + "node scripts/validate-dev-gate-report.mjs" + ], + "evidence": [ + "source-contract-static=pass", + "artifact catalog state=contract-skeleton", + "catalog commit=1a2efd4" + ], + "summary": "Deploy manifests, FRP/master-edge contracts, and safety boundary are source-readable and scoped to hwlab-dev." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#33", + "taskId": "dev-deploy-apply", + "reportPath": "reports/dev-gate/dev-deploy-report.json", + "commitId": "9b47ebe49f70", + "level": "DRY-RUN", + "status": "blocked", + "category": "deploy-apply", + "commands": [ + "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run", + "node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked" + ], + "evidence": [ + "artifact services checked: 13", + "expected artifact commit: 24eb3bf", + "namespace: hwlab-dev", + "workloads planned: 13", + "kubectl executor: missing", + "live health: not_run" + ], + "summary": "DEV apply is blocked before mutation." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#35", + "taskId": "dev-artifact-publish", + "reportPath": "reports/dev-gate/dev-artifacts.json", + "commitId": "7e0ccb0", + "level": "DRY-RUN", + "status": "blocked", + "category": "artifact-publish", + "commands": [ + "node --check scripts/dev-artifact-publish.mjs", + "node --check scripts/preflight-dev-base-image.mjs", + "node --check scripts/src/dev-base-image-preflight.mjs", + "node scripts/preflight-dev-base-image.mjs", + "node scripts/dev-artifact-publish.mjs --preflight --no-report", + "node --check scripts/validate-dev-gate-report.mjs", + "node scripts/validate-dev-gate-report.mjs" + ], + "evidence": [ + "services=13", + "baseImagePreflight=blocked", + "published=0/13" + ], + "summary": "Artifact publish preflight exists but is blocked before any real image publish." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#36", + "taskId": "dev-edge-health", + "reportPath": "reports/dev-gate/dev-edge-health.json", + "commitId": "9b47ebe49f70", + "level": "SOURCE", + "status": "not_run", + "category": "edge-frp-contract", + "commands": [ + "node --check scripts/validate-dev-gate-report.mjs", + "node scripts/validate-dev-gate-report.mjs", + "node --check scripts/dev-edge-health-smoke.mjs", + "node --check scripts/src/dev-edge-health-smoke-lib.mjs", + "node scripts/dev-edge-health-smoke.mjs --live --write-report" + ], + "evidence": [ + "mode=contract-only", + "classification=not_run", + "cloudApiDb=ready" + ], + "summary": "Committed edge report is contract-only/not-run; it must not be counted as DEV-LIVE." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#46", + "taskId": "d601-k3s-readonly-observability", + "reportPath": "reports/d601-k3s-readonly-observability.json", + "commitId": "unknown", + "level": "BLOCKED", + "status": "blocked", + "category": "d601-observability", + "commands": [ + "node --check scripts/d601-k3s-readonly-observability.mjs", + "node --check scripts/src/d601-k3s-readonly-observability.mjs", + "node scripts/d601-k3s-readonly-observability.mjs" + ], + "evidence": [ + "kubectl=missing", + "k3s=missing", + "clusterReadable=no" + ], + "summary": "D601 k3s observability is blocked; cluster state has not been read." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#38", + "taskId": "dev-m3-hardware-loop", + "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", + "commitId": "70bb9168ead2", + "level": "SOURCE", + "status": "manifest-ready", + "category": "hardware-loop-cardinality", + "commands": [ + "node scripts/validate-dev-m3-cardinality.mjs" + ], + "evidence": [ + "deploy-skeleton-m3-cardinality=manifest-ready" + ], + "summary": "Static DEV manifest cardinality declares two box simulators, two gateway simulators, and one patch panel." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "commitId": "6eb4c199b0c6", + "level": "LOCAL", + "status": "pass", + "category": "hardware-loop-local", + "commands": [ + "node scripts/m3-hardware-loop-smoke.mjs" + ], + "evidence": [ + "M3 hardware loop smoke passed", + "topology: 2 box simulators, 2 gateway simulators, 1 patch panel", + "wiring: box-simu-1 DO1 -> box-simu-2 DI1 via hwlab-patch-panel" + ], + "summary": "The local hardware trusted loop smoke passes." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#38", + "taskId": "dev-m3-hardware-loop", + "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", + "commitId": "70bb9168ead2", + "level": "BLOCKED", + "status": "not_run", + "category": "hardware-loop-live", + "commands": [ + "node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production" + ], + "evidence": [ + "operationId=not_observed", + "traceId=not_observed", + "auditId=not_observed", + "evidenceId=not_observed" + ], + "summary": "No live do.write -> di.read operation was attempted because DEV ingress is blocked." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "commitId": "e9e71a7", + "level": "LOCAL", + "status": "pass", + "category": "agent-loop-local", + "commands": [ + "node scripts/m4-agent-loop-smoke.mjs" + ], + "evidence": [ + "Local runtime skeleton smoke confirms create/start/trace/finish/cleanup coverage.", + "Workspace isolation and explicit skills commit wiring are covered by fixture assertions." + ], + "summary": "Local contract smoke passes on the repo fixture." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "commitId": "e9e71a7", + "level": "DRY-RUN", + "status": "not_run", + "category": "agent-loop-dry-run", + "commands": [ + "node scripts/dev-m4-agent-loop-smoke.mjs --dry-run" + ], + "evidence": [ + "No dry-run output is attached to the live preflight report." + ], + "summary": "The live preflight report does not attempt a dry-run agent run." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "commitId": "e9e71a7", + "level": "BLOCKED", + "status": "blocked", + "category": "agent-loop-live-preflight", + "commands": [ + "node scripts/dev-m4-agent-loop-smoke.mjs --live --confirm-dev --confirmed-non-production" + ], + "evidence": [ + "No live DEV observation was recorded." + ], + "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + }, + { + "milestone": "M5", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "commitId": "6eb4c199b0c6", + "level": "DRY-RUN", + "status": "pass", + "category": "mvp-e2e-dry-run", + "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", + "node scripts/m5-mvp-e2e-dry-run.mjs" + ], + "evidence": [ + "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2", + "hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls", + "M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls" + ], + "summary": "The M5 orchestration dry-run is green, but it remains fixture-only and does not establish a live DEV gate." + }, + { + "milestone": "M5", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "commitId": "6eb4c199b0c6", + "level": "BLOCKED", + "status": "blocked", + "category": "mvp-e2e-live", + "commands": [ + "curl -fsS --max-time 5 http://74.48.78.17:6667/health", + "curl -fsS --max-time 5 http://74.48.78.17:6667/live" + ], + "evidence": [ + "curl exit 7 for /health: could not connect to 74.48.78.17 port 6667", + "curl exit 7 for /live: could not connect to 74.48.78.17 port 6667" + ], + "summary": "The frozen DEV endpoint is not reachable from this runner, so no live DEV gate can be claimed yet." + } + ], + "levels": { + "SOURCE": [ + { + "milestone": "M0", + "issue": "pikasTech/HWLAB#31", + "status": "pass", + "category": "contract", + "summary": "Frozen service IDs, JSON-RPC, audit, topology, evidence, and DEV-only deploy contracts are source-ready." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#34", + "taskId": "dev-gate-preflight", + "status": "pass", + "category": "deploy-manifest", + "reportPath": "reports/dev-gate/dev-preflight-report.json", + "summary": "Deploy manifests, FRP/master-edge contracts, and safety boundary are source-readable and scoped to hwlab-dev." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#36", + "taskId": "dev-edge-health", + "status": "not_run", + "category": "edge-frp-contract", + "reportPath": "reports/dev-gate/dev-edge-health.json", + "summary": "Committed edge report is contract-only/not-run; it must not be counted as DEV-LIVE." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#38", + "taskId": "dev-m3-hardware-loop", + "status": "manifest-ready", + "category": "hardware-loop-cardinality", + "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", + "summary": "Static DEV manifest cardinality declares two box simulators, two gateway simulators, and one patch panel." + } + ], + "LOCAL": [ + { + "milestone": "M1", + "issue": "pikasTech/HWLAB#7", + "status": "pass", + "category": "local-smoke", + "summary": "Local skeleton smoke covers cloud API, simulators, patch-panel routing, CLI dry-run boundary, and no DEV/PROD mutation." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "status": "pass", + "category": "hardware-loop-local", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "summary": "The local hardware trusted loop smoke passes." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "status": "pass", + "category": "agent-loop-local", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "summary": "Local contract smoke passes on the repo fixture." + } + ], + "DRY-RUN": [ + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#33", + "taskId": "dev-deploy-apply", + "status": "blocked", + "category": "deploy-apply", + "reportPath": "reports/dev-gate/dev-deploy-report.json", + "summary": "DEV apply is blocked before mutation." + }, + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#35", + "taskId": "dev-artifact-publish", + "status": "blocked", + "category": "artifact-publish", + "reportPath": "reports/dev-gate/dev-artifacts.json", + "summary": "Artifact publish preflight exists but is blocked before any real image publish." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "status": "not_run", + "category": "agent-loop-dry-run", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "summary": "The live preflight report does not attempt a dry-run agent run." + }, + { + "milestone": "M5", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "status": "pass", + "category": "mvp-e2e-dry-run", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "summary": "The M5 orchestration dry-run is green, but it remains fixture-only and does not establish a live DEV gate." + } + ], + "DEV-LIVE": [], + "BLOCKED": [ + { + "milestone": "M2", + "issue": "pikasTech/HWLAB#46", + "taskId": "d601-k3s-readonly-observability", + "status": "blocked", + "category": "d601-observability", + "reportPath": "reports/d601-k3s-readonly-observability.json", + "summary": "D601 k3s observability is blocked; cluster state has not been read." + }, + { + "milestone": "M3", + "issue": "pikasTech/HWLAB#38", + "taskId": "dev-m3-hardware-loop", + "status": "not_run", + "category": "hardware-loop-live", + "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", + "summary": "No live do.write -> di.read operation was attempted because DEV ingress is blocked." + }, + { + "milestone": "M4", + "issue": "pikasTech/HWLAB#37", + "taskId": "dev-m4-agent-loop", + "status": "blocked", + "category": "agent-loop-live-preflight", + "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", + "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + }, + { + "milestone": "M5", + "issue": "pikasTech/HWLAB#39", + "taskId": "dev-mvp-gate-report", + "status": "blocked", + "category": "mvp-e2e-live", + "reportPath": "reports/dev-gate/dev-mvp-gate-report.json", + "summary": "The frozen DEV endpoint is not reachable from this runner, so no live DEV gate can be claimed yet." + }, + { + "priority": "P0", + "order": 1, + "type": "environment_blocker", + "scope": "base-image", + "sourceIssue": "pikasTech/HWLAB#35", + "source": "reports/dev-gate/dev-artifacts.json", + "summary": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "nextTask": "Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes." + }, + { + "priority": "P1", + "order": 3, + "type": "observability_blocker", + "scope": "artifact-publish", + "sourceIssue": "pikasTech/HWLAB#33", + "source": "reports/dev-gate/dev-deploy-report.json", + "summary": "DEV artifact catalog has no CI publish, registry verification, or registry digests", + "nextTask": "Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence." + }, + { + "priority": "P1", + "order": 3, + "type": "observability_blocker", + "scope": "artifact-source-commit", + "sourceIssue": "pikasTech/HWLAB#33", + "source": "reports/dev-gate/dev-deploy-report.json", + "summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 9b47ebe49f70", + "nextTask": "Regenerate deploy/deploy.json and deploy/artifact-catalog.dev.json for the source commit that will be promoted." + }, + { + "priority": "P1", + "order": 3, + "type": "runtime_blocker", + "scope": "artifact-catalog", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "deploy/artifact-catalog.dev.json does not prove published artifacts for origin/main 1a2efd4; ciPublished=false, registryVerified=false, not_published=13.", + "nextTask": "Run the DEV artifact publish workflow, then record only real sha256 digests with `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`; if publish is still blocked, keep not_published via `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`." + }, + { + "priority": "P1", + "order": 3, + "type": "runtime_blocker", + "scope": "dev-artifact-publish", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 1a2efd4; current status is blocked with 0/13 published.", + "nextTask": "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests." + }, + { + "priority": "P1", + "order": 3, + "type": "runtime_blocker", + "scope": "ghcr", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "This preflight could not verify GHCR manifests for the DEV catalog images without reading credentials.", + "nextTask": "Publish public DEV images or provide a non-secret registry evidence artifact with immutable digests for each HWLAB service." + }, + { + "priority": "P1", + "order": 4, + "type": "environment_blocker", + "scope": "d601-k3s-client-binary", + "sourceIssue": "pikasTech/HWLAB#46", + "source": "reports/d601-k3s-readonly-observability.json", + "summary": "Neither kubectl nor k3s is installed in this runner PATH.", + "nextTask": "Install kubectl in the D601 Code Queue runner image, or mount an approved k3s kubectl client path for read-only hwlab-dev observation." + }, + { + "priority": "P1", + "order": 4, + "type": "environment_blocker", + "scope": "d601-kubeconfig-path", + "sourceIssue": "pikasTech/HWLAB#46", + "source": "reports/d601-k3s-readonly-observability.json", + "summary": "No readable KUBECONFIG/default k3s kubeconfig path was found by metadata checks.", + "nextTask": "Mount a read-only kubeconfig for hwlab-dev, or document the approved k3s local kubeconfig path without exposing token material." + }, + { + "priority": "P1", + "order": 4, + "type": "environment_blocker", + "scope": "d601-maintenance-ssh-bridge", + "sourceIssue": "pikasTech/HWLAB#46", + "source": "reports/d601-k3s-readonly-observability.json", + "summary": "No SSH maintenance bridge was detected from ssh binary plus known environment variables or ~/.ssh/config metadata.", + "nextTask": "Provide a documented read-only maintenance bridge variable or config path if kubectl cannot be mounted directly in the runner." + }, + { + "priority": "P1", + "order": 4, + "type": "environment_blocker", + "scope": "kubectl", + "sourceIssue": "pikasTech/HWLAB#33", + "source": "reports/dev-gate/dev-deploy-report.json", + "summary": "kubectl is not installed in the runner", + "nextTask": "Provide a kubectl client configured for the D601 DEV k3s context without exposing token or secret values." + }, + { + "priority": "P1", + "order": 4, + "type": "environment_blocker", + "scope": "d601-k3s", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "D601 runner lacks kubectl and no default kubeconfig was used by this preflight.", + "nextTask": "Provide a read-only kubectl/kubeconfig path for the real D601 hwlab-dev k3s cluster, then rerun this preflight." + }, + { + "priority": "P1", + "order": 5, + "type": "runtime_blocker", + "scope": "cloud-api-db", + "sourceIssue": "pikasTech/HWLAB#33", + "source": "reports/dev-gate/dev-deploy-report.json", + "summary": "cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE", + "nextTask": "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values." + }, + { + "priority": "P1", + "order": 5, + "type": "runtime_blocker", + "scope": "cloud-api-db-health-gate", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE.", + "nextTask": "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value." + }, + { + "priority": "P2", + "order": 6, + "type": "network_blocker", + "scope": "dev-edge", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "http://74.48.78.17:6667/health/live is not reachable from this runner.", + "nextTask": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe." + }, + { + "priority": "P2", + "order": 6, + "type": "network_blocker", + "scope": "dev-edge-health", + "sourceIssue": "pikasTech/HWLAB#34", + "source": "reports/dev-gate/dev-preflight-report.json", + "summary": "live network probes require --live", + "nextTask": "Repair the master frps/public DEV edge route and rerun the read-only DEV edge health smoke before real deployment." + }, + { + "priority": "P2", + "order": 6, + "type": "network_blocker", + "scope": "dev-ingress-health", + "sourceIssue": "pikasTech/HWLAB#38", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "summary": "Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached." + }, + { + "priority": "P2", + "order": 6, + "type": "network_blocker", + "scope": "devPreconditions", + "sourceIssue": "pikasTech/HWLAB#37", + "source": "reports/dev-gate/dev-m4-agent-loop.json", + "summary": "connect ECONNREFUSED 74.48.78.17:6667" + }, + { + "priority": "P2", + "order": 6, + "type": "network_blocker", + "scope": "devPreconditions", + "sourceIssue": "pikasTech/HWLAB#39", + "source": "reports/dev-gate/dev-mvp-gate-report.json", + "summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing." + } + ] + }, + "blockers": [ + { + "id": "dev-artifact-publish:base-image", + "priority": "P0", + "type": "environment_blocker", + "scope": "base-image", + "status": "open", + "source": "reports/dev-gate/dev-artifacts.json", + "sourceIssue": "pikasTech/HWLAB#35", + "summary": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "nextTask": "Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes.", + "unblockOrder": 1, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33" + ], + "rationale": "Artifact publishing cannot start until an approved Node 20 DEV builder base image is available." + }, + { + "id": "dev-deploy-apply:artifact-publish", + "priority": "P1", + "type": "observability_blocker", + "scope": "artifact-publish", + "status": "open", + "source": "reports/dev-gate/dev-deploy-report.json", + "sourceIssue": "pikasTech/HWLAB#33", + "summary": "DEV artifact catalog has no CI publish, registry verification, or registry digests", + "nextTask": "Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence.", + "unblockOrder": 3, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." + }, + { + "id": "dev-deploy-apply:artifact-source-commit", + "priority": "P1", + "type": "observability_blocker", + "scope": "artifact-source-commit", + "status": "open", + "source": "reports/dev-gate/dev-deploy-report.json", + "sourceIssue": "pikasTech/HWLAB#33", + "summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 9b47ebe49f70", + "nextTask": "Regenerate deploy/deploy.json and deploy/artifact-catalog.dev.json for the source commit that will be promoted.", + "unblockOrder": 3, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." + }, + { + "id": "dev-gate-preflight:artifact-catalog", + "priority": "P1", + "type": "runtime_blocker", + "scope": "artifact-catalog", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "deploy/artifact-catalog.dev.json does not prove published artifacts for origin/main 1a2efd4; ciPublished=false, registryVerified=false, not_published=13.", + "nextTask": "Run the DEV artifact publish workflow, then record only real sha256 digests with `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`; if publish is still blocked, keep not_published via `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`.", + "unblockOrder": 3, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." + }, + { + "id": "dev-gate-preflight:dev-artifact-publish", + "priority": "P1", + "type": "runtime_blocker", + "scope": "dev-artifact-publish", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 1a2efd4; current status is blocked with 0/13 published.", + "nextTask": "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests.", + "unblockOrder": 3, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." + }, + { + "id": "dev-gate-preflight:ghcr", + "priority": "P1", + "type": "runtime_blocker", + "scope": "ghcr", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "This preflight could not verify GHCR manifests for the DEV catalog images without reading credentials.", + "nextTask": "Publish public DEV images or provide a non-secret registry evidence artifact with immutable digests for each HWLAB service.", + "unblockOrder": 3, + "unblocks": [ + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." + }, + { + "id": "d601-k3s-readonly-observability:d601-k3s-client-binary", + "priority": "P1", + "type": "environment_blocker", + "scope": "d601-k3s-client-binary", + "status": "open", + "source": "reports/d601-k3s-readonly-observability.json", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "Neither kubectl nor k3s is installed in this runner PATH.", + "nextTask": "Install kubectl in the D601 Code Queue runner image, or mount an approved k3s kubectl client path for read-only hwlab-dev observation.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + }, + { + "id": "d601-k3s-readonly-observability:d601-kubeconfig-path", + "priority": "P1", + "type": "environment_blocker", + "scope": "d601-kubeconfig-path", + "status": "open", + "source": "reports/d601-k3s-readonly-observability.json", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "No readable KUBECONFIG/default k3s kubeconfig path was found by metadata checks.", + "nextTask": "Mount a read-only kubeconfig for hwlab-dev, or document the approved k3s local kubeconfig path without exposing token material.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + }, + { + "id": "d601-k3s-readonly-observability:d601-maintenance-ssh-bridge", + "priority": "P1", + "type": "environment_blocker", + "scope": "d601-maintenance-ssh-bridge", + "status": "open", + "source": "reports/d601-k3s-readonly-observability.json", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "No SSH maintenance bridge was detected from ssh binary plus known environment variables or ~/.ssh/config metadata.", + "nextTask": "Provide a documented read-only maintenance bridge variable or config path if kubectl cannot be mounted directly in the runner.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + }, + { + "id": "dev-deploy-apply:kubectl", + "priority": "P1", + "type": "environment_blocker", + "scope": "kubectl", + "status": "open", + "source": "reports/dev-gate/dev-deploy-report.json", + "sourceIssue": "pikasTech/HWLAB#33", + "summary": "kubectl is not installed in the runner", + "nextTask": "Provide a kubectl client configured for the D601 DEV k3s context without exposing token or secret values.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + }, + { + "id": "dev-gate-preflight:d601-k3s", + "priority": "P1", + "type": "environment_blocker", + "scope": "d601-k3s", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "D601 runner lacks kubectl and no default kubeconfig was used by this preflight.", + "nextTask": "Provide a read-only kubectl/kubeconfig path for the real D601 hwlab-dev k3s cluster, then rerun this preflight.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + }, + { + "id": "dev-deploy-apply:cloud-api-db", + "priority": "P1", + "type": "runtime_blocker", + "scope": "cloud-api-db", + "status": "open", + "source": "reports/dev-gate/dev-deploy-report.json", + "sourceIssue": "pikasTech/HWLAB#33", + "summary": "cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE", + "nextTask": "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values.", + "unblockOrder": 5, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding." + }, + { + "id": "dev-gate-preflight:cloud-api-db-health-gate", + "priority": "P1", + "type": "runtime_blocker", + "scope": "cloud-api-db-health-gate", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE.", + "nextTask": "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value.", + "unblockOrder": 5, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#39" + ], + "rationale": "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding." + }, + { + "id": "dev-gate-preflight:dev-edge", + "priority": "P2", + "type": "network_blocker", + "scope": "dev-edge", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "http://74.48.78.17:6667/health/live is not reachable from this runner.", + "nextTask": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe.", + "unblockOrder": 6, + "unblocks": [ + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected." + }, + { + "id": "dev-gate-preflight:dev-edge-health", + "priority": "P2", + "type": "network_blocker", + "scope": "dev-edge-health", + "status": "open", + "source": "reports/dev-gate/dev-preflight-report.json", + "sourceIssue": "pikasTech/HWLAB#34", + "summary": "live network probes require --live", + "nextTask": "Repair the master frps/public DEV edge route and rerun the read-only DEV edge health smoke before real deployment.", + "unblockOrder": 6, + "unblocks": [ + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected." + }, + { + "id": "dev-m3-hardware-loop:dev-ingress-health", + "priority": "P2", + "type": "network_blocker", + "scope": "dev-ingress-health", + "status": "open", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "sourceIssue": "pikasTech/HWLAB#38", + "summary": "Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached.", + "unblockOrder": 6, + "unblocks": [ + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected." + }, + { + "id": "dev-m4-agent-loop:devPreconditions", + "priority": "P2", + "type": "network_blocker", + "scope": "devPreconditions", + "status": "open", + "source": "reports/dev-gate/dev-m4-agent-loop.json", + "sourceIssue": "pikasTech/HWLAB#37", + "summary": "connect ECONNREFUSED 74.48.78.17:6667", + "unblockOrder": 6, + "unblocks": [ + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected." + }, + { + "id": "dev-mvp-gate-report:devPreconditions", + "priority": "P2", + "type": "network_blocker", + "scope": "devPreconditions", + "status": "open", + "source": "reports/dev-gate/dev-mvp-gate-report.json", + "sourceIssue": "pikasTech/HWLAB#39", + "summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing.", + "unblockOrder": 6, + "unblocks": [ + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected." + } + ], + "nextSteps": [ + { + "order": 1, + "blockerOrder": 1, + "priority": "P0", + "scopes": [ + "base-image" + ], + "sourceIssues": [ + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#35" + ], + "rationale": "Artifact publishing cannot start until an approved Node 20 DEV builder base image is available.", + "action": "Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes.", + "evidenceRequired": "Base-image preflight status=ready with approved Node 20 builder base and no UniDesk/runtime substitute." + }, + { + "order": 2, + "blockerOrder": 3, + "priority": "P1", + "scopes": [ + "artifact-publish", + "artifact-source-commit", + "artifact-catalog", + "dev-artifact-publish", + "ghcr" + ], + "sourceIssues": [ + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#39" + ], + "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests.", + "action": "Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence.", + "evidenceRequired": "Artifact publish report with ciPublished=true, registryVerified=true, and sha256 digest for each frozen service ID." + }, + { + "order": 3, + "blockerOrder": 4, + "priority": "P1", + "scopes": [ + "d601-k3s-client-binary", + "d601-kubeconfig-path", + "d601-maintenance-ssh-bridge", + "kubectl", + "d601-k3s" + ], + "sourceIssues": [ + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#46" + ], + "rationale": "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance.", + "action": "Install kubectl in the D601 Code Queue runner image, or mount an approved k3s kubectl client path for read-only hwlab-dev observation.", + "evidenceRequired": "Read-only kubectl/k3s report proving pods/services/configmaps are observable in hwlab-dev without reading Secrets." + }, + { + "order": 4, + "blockerOrder": 5, + "priority": "P1", + "scopes": [ + "cloud-api-db", + "cloud-api-db-health-gate" + ], + "sourceIssues": [ + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#39" + ], + "rationale": "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding.", + "action": "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values.", + "evidenceRequired": "Cloud API health/live output showing DB env ready and redacted secret references, without secret material." + }, + { + "order": 5, + "blockerOrder": 6, + "priority": "P2", + "scopes": [ + "dev-edge", + "dev-edge-health", + "dev-ingress-health", + "devPreconditions" + ], + "sourceIssues": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#39" + ], + "rationale": "The :6667/frp path must be reachable before any DEV-LIVE M3, M4, or M5 evidence can be collected.", + "action": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe.", + "evidenceRequired": "Read-only DEV route observation for :6667/frp/edge/router with HWLAB service identity and artifact identity." + } + ], + "validationCommands": [ + "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" + ] +} diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.md b/reports/dev-gate/dev-m5-gate-aggregator-v2.md new file mode 100644 index 00000000..b070d005 --- /dev/null +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.md @@ -0,0 +1,81 @@ +# HWLAB M5 DEV Gate Aggregator v2 + +Status: blocked +Generated from: `1e9591a6611e` +Scope: DEV only, report-only + +## Summary + +SOURCE, LOCAL, and DRY-RUN evidence exists, but DEV-LIVE acceptance is blocked by artifact, observability, DB, edge/frp, and loop evidence gaps. + +## Milestones + +| Milestone | Status | Highest visible level | Live evidence | Summary | +| --- | --- | --- | --- | --- | +| M0 | pass | SOURCE | missing_or_blocked | contract source is available; highest visible level is SOURCE; status is pass. | +| M1 | pass | LOCAL | missing_or_blocked | local smoke is available; highest visible level is LOCAL; status is pass. | +| M2 | blocked | DRY-RUN | missing_or_blocked | deploy/runtime readiness is blocked before live DEV; highest visible level is DRY-RUN; status is blocked. | +| M3 | blocked | LOCAL | missing_or_blocked | hardware loop has source/local shape but no live operation; highest visible level is LOCAL; status is blocked. | +| M4 | blocked | LOCAL | missing_or_blocked | agent loop has local smoke but live preflight is blocked; highest visible level is LOCAL; status is blocked. | +| M5 | blocked | DRY-RUN | missing_or_blocked | dry-run is green but live MVP gate is blocked; highest visible level is DRY-RUN; status is blocked. | + +## #9 DoD Checks + +| Check | Status | Evidence level | Summary | +| --- | --- | --- | --- | +| m0-source-contract | pass | SOURCE | M0 contract checks are source-level evidence only. | +| m1-local-smoke | pass | LOCAL | M1 local smoke is not a live DEV substitute. | +| artifact-publish-digests | blocked | BLOCKED | artifactState=contract-skeleton, ciPublished=false, registryVerified=false, sha256=0, not_published=13 | +| d601-k3s-observability | blocked | BLOCKED | D601 runner lacks kubectl/k3s/kubeconfig observability for hwlab-dev. | +| dev-edge-frp-6667 | blocked | BLOCKED | No committed report proves live HTTP 200/JSON on http://74.48.78.17:6667. | +| cloud-api-db-ready | blocked | BLOCKED | Manifest-level DB env exists, but live DB health readiness is still blocked/missing. | +| m5-mvp-dev-live | blocked | BLOCKED | M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed. | + +## Blockers + +| Priority | Order | Type | Scope | Summary | +| --- | ---: | --- | --- | --- | +| P0 | 1 | environment_blocker | base-image | HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. | +| P1 | 3 | observability_blocker | artifact-publish | DEV artifact catalog has no CI publish, registry verification, or registry digests | +| P1 | 3 | observability_blocker | artifact-source-commit | deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 9b47ebe49f70 | +| P1 | 3 | runtime_blocker | artifact-catalog | deploy/artifact-catalog.dev.json does not prove published artifacts for origin/main 1a2efd4; ciPublished=false, registryVerified=false, not_published=13. | +| P1 | 3 | runtime_blocker | dev-artifact-publish | reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 1a2efd4; current status is blocked with 0/13 published. | +| P1 | 3 | runtime_blocker | ghcr | This preflight could not verify GHCR manifests for the DEV catalog images without reading credentials. | +| P1 | 4 | environment_blocker | d601-k3s-client-binary | Neither kubectl nor k3s is installed in this runner PATH. | +| P1 | 4 | environment_blocker | d601-kubeconfig-path | No readable KUBECONFIG/default k3s kubeconfig path was found by metadata checks. | +| P1 | 4 | environment_blocker | d601-maintenance-ssh-bridge | No SSH maintenance bridge was detected from ssh binary plus known environment variables or ~/.ssh/config metadata. | +| P1 | 4 | environment_blocker | kubectl | kubectl is not installed in the runner | +| P1 | 4 | environment_blocker | d601-k3s | D601 runner lacks kubectl and no default kubeconfig was used by this preflight. | +| P1 | 5 | runtime_blocker | cloud-api-db | cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE | +| P1 | 5 | runtime_blocker | cloud-api-db-health-gate | cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE. | +| P2 | 6 | network_blocker | dev-edge | http://74.48.78.17:6667/health/live is not reachable from this runner. | +| P2 | 6 | network_blocker | dev-edge-health | live network probes require --live | +| P2 | 6 | network_blocker | dev-ingress-health | Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached. | +| P2 | 6 | network_blocker | devPreconditions | connect ECONNREFUSED 74.48.78.17:6667 | +| P2 | 6 | network_blocker | devPreconditions | http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing. | + +## Next Unblock Order + +1. Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes. + Evidence required: Base-image preflight status=ready with approved Node 20 builder base and no UniDesk/runtime substitute. +2. Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence. + Evidence required: Artifact publish report with ciPublished=true, registryVerified=true, and sha256 digest for each frozen service ID. +3. Install kubectl in the D601 Code Queue runner image, or mount an approved k3s kubectl client path for read-only hwlab-dev observation. + Evidence required: Read-only kubectl/k3s report proving pods/services/configmaps are observable in hwlab-dev without reading Secrets. +4. Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values. + Evidence required: Cloud API health/live output showing DB env ready and redacted secret references, without secret material. +5. Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe. + Evidence required: Read-only DEV route observation for :6667/frp/edge/router with HWLAB service identity and artifact identity. + +## Validation + +- `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` + +## 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. diff --git a/scripts/dev-evidence-blocker-aggregator.mjs b/scripts/dev-evidence-blocker-aggregator.mjs index 53c93f0d..2408e3d6 100644 --- a/scripts/dev-evidence-blocker-aggregator.mjs +++ b/scripts/dev-evidence-blocker-aggregator.mjs @@ -1,281 +1,46 @@ #!/usr/bin/env node -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { access, readdir, readFile } from "node:fs/promises"; -import { constants as fsConstants } from "node:fs"; +import { writeFile } 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"; +import { + buildReport, + formatCheckSummary, + renderMarkdown, + repoRoot +} from "./src/dev-evidence-blocker-aggregator.mjs"; -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const levels = ["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]; -const blockerTypes = ["contract_blocker", "environment_blocker", "network_blocker", "runtime_blocker", "agent_blocker", "observability_blocker", "safety_blocker"]; -const requiredArtifactFields = ["serviceId", "commitId", "image", "tag", "digest", "buildSource", "deployEnv", "healthTimestamp"]; -const issue = (id) => `pikasTech/HWLAB${id}`; -const allSupportedIssues = ["#33", "#34", "#35", "#36", "#37", "#38", "#39", "#7", "#31"].map(issue); - -const contracts = [ - contract("M0", "SOURCE", "artifact/base image", ["#7", "#31"], ["docs/m0-contract-audit.md", "protocol/schemas/evidence-record.schema.json", "protocol/evidence-chain.md", "protocol/examples/m0-contract/service-ids.json", "scripts/validate-m0-contract.mjs", "scripts/validate-evidence-chain.mjs"], ["node scripts/validate-m0-contract.mjs", "node scripts/validate-evidence-chain.mjs"], "Source contracts freeze service IDs, JSON-RPC, audit, topology, evidence records, and deploy shape."), - contract("M1", "LOCAL", "runtime/health", ["#7", "#31"], ["docs/m1-local-smoke.md", "fixtures/mvp/runtime.json", "scripts/m1-contract-smoke.mjs"], ["node scripts/m1-contract-smoke.mjs"], "Local smoke covers cloud-api, simulators, patch-panel routing, CLI dry-run boundary, and no DEV/PROD mutation."), - contract("M2", "DRY-RUN", "edge/network", ["#7", "#31", "#33", "#34", "#35", "#39"], ["docs/m2-dev-deploy-smoke.md", "docs/dev-acceptance-checklist.json", "fixtures/dev-deploy-smoke/dev-deploy-smoke.json", "deploy/artifact-catalog.dev.json", "scripts/m2-dev-deploy-smoke.mjs", "scripts/validate-artifact-catalog.mjs", "scripts/validate-runtime-boundary.mjs"], ["node scripts/m2-dev-deploy-smoke.mjs --dry-run", "node scripts/validate-artifact-catalog.mjs", "node scripts/validate-runtime-boundary.mjs"], "Dry-run contract covers edge proxy, frp, D601 router, cloud surface, agent runtime, sim, and patch-panel route phases."), - contract("M3", "LOCAL", "M3/M4 live", ["#7", "#31", "#36", "#39"], ["docs/m3-hardware-loop.md", "fixtures/mvp/m3-hardware-loop/topology.json", "scripts/m3-hardware-loop-smoke.mjs"], ["node scripts/m3-hardware-loop-smoke.mjs"], "Local hardware trusted loop fixture proves patch-panel-owned box-simu-1 DO1 to box-simu-2 DI1 propagation."), - contract("M4", "LOCAL", "M3/M4 live", ["#7", "#31", "#37", "#39"], ["docs/m4-agent-loop.md", "fixtures/mvp/m4-agent-loop/runtime.json", "fixtures/mvp/m4-agent-loop/evidence/m4-agent-loop-plan.md", "scripts/m4-agent-loop-smoke.mjs"], ["node scripts/m4-agent-loop-smoke.mjs"], "Local agent loop fixture covers session lifecycle, workspace isolation, skills commit wiring, evidence, and cleanup."), - contract("M5", "DRY-RUN", "runtime/health", ["#7", "#31", "#38", "#39"], ["docs/m5-mvp-e2e.md", "protocol/mvp-e2e-contract.md", "fixtures/mvp/m5-e2e/dry-run-plan.json", "scripts/m5-mvp-e2e-dry-run.mjs"], ["node scripts/m5-mvp-e2e-dry-run.mjs"], "Dry-run orchestration checks the M0-M4 dependency graph, MVP sequence, health, trace, audit, evidence, and cleanup contracts.") -]; - -const visibleParallelResults = [ - parallel("#33", "BLOCKED", "Prompt says #33-#39 are parallel real DEV workstreams. No issue-specific committed DEV-LIVE result is visible in this checkout."), - parallel("#34", "DRY-RUN", "Repository M2 dry-run and artifact contracts can support deploy/artifact review, but this runner cannot read the issue result itself."), - parallel("#35", "DRY-RUN", "Repository DEV route fixtures cover edge, frp, and D601 router contracts as DRY-RUN evidence only."), - parallel("#36", "LOCAL", "Repository M3 topology fixture and smoke script provide LOCAL evidence only; no live DEV M3 loop output is attached."), - parallel("#37", "LOCAL", "Repository M4 runtime fixture and smoke script provide LOCAL evidence only; no live DEV M4 agent run output is attached."), - parallel("#38", "DRY-RUN", "Repository M5 dry-run plan validates orchestration readiness, not a live DEV MVP e2e run."), - parallel("#39", "BLOCKED", "No repository-local DEV-LIVE gate result is visible to this aggregator.") -]; - -const nextRoundRecommendations = [ - next("artifact/base image", "high", ["#33", "#35"], "Publish or attach real DEV artifact provenance for each frozen service ID: commitId, image, immutable tag, digest, buildSource, deployEnv, and healthTimestamp."), - next("edge/network", "high", ["#35", "#39"], "Attach read-only DEV route observations for public ingress, master edge proxy, frp, and D601 hwlab-dev/hwlab-router without deploying, restarting, or reading secrets."), - next("runtime/health", "high", ["#34", "#35", "#38", "#39"], "Attach current DEV health JSON for cloud API/web, gateway or simulator, box simulator, patch panel, agent manager, worker, skills, and CLI, all tied to HWLAB service IDs."), - next("M3/M4 live", "medium", ["#36", "#37", "#38"], "After DEV runtime smoke is green, run the smallest authorized DEV-only M3/M4 loop and attach trace, audit, evidence, operation, session, and cleanup IDs.") -]; - -function contract(milestone, source, category, supports, paths, commands, summary) { - return { - milestone, - source, - status: source === "SOURCE" ? "contract_available" : `${source.toLowerCase().replace("-", "_")}_contract_available`, - category, - supports: supports.map(issue), - paths, - commands, - summary - }; -} - -function parallel(issue, source, visibleResult) { - return { - issue: `pikasTech/HWLAB${issue}`, - issueRead: "blocked", - visibleResult, - source - }; -} - -function next(category, priority, owners, recommendation) { - return { category, priority, owners, recommendation }; -} - -async function readJSON(relativePath) { - const raw = await readFile(path.join(repoRoot, relativePath), "utf8"); - return JSON.parse(raw); -} - -async function pathExists(relativePath) { - try { - await access(path.join(repoRoot, relativePath), fsConstants.R_OK); - return true; - } catch { - return false; - } -} - -function gitCommit() { - try { - return execFileSync("git", ["rev-parse", "--short=12", "HEAD"], { - cwd: repoRoot, - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"] - }).trim(); - } catch { - return "unknown"; - } -} - -async function assertStaticContracts() { - const checklist = await readJSON("docs/dev-acceptance-checklist.json"); - assert.equal(checklist.environment, ENVIRONMENT_DEV, "DEV checklist environment"); - assert.equal(checklist.endpoint, DEV_ENDPOINT, "DEV checklist endpoint"); - assert.deepEqual(checklist.artifactObservabilityFields, requiredArtifactFields); - - const catalog = await readJSON("deploy/artifact-catalog.dev.json"); - assert.equal(catalog.environment, ENVIRONMENT_DEV, "artifact catalog environment"); - assert.equal(catalog.endpoint, DEV_ENDPOINT, "artifact catalog endpoint"); - assert.equal(catalog.publish.ciPublished, false, "skeleton catalog must not claim CI publish"); - assert.equal(catalog.publish.registryVerified, false, "skeleton catalog must not claim registry verification"); - assert.deepEqual(catalog.services.map((service) => service.serviceId).sort(), [...SERVICE_IDS].sort()); - - const m2Fixture = await readJSON("fixtures/dev-deploy-smoke/dev-deploy-smoke.json"); - assert.equal(m2Fixture.environment, ENVIRONMENT_DEV, "M2 fixture environment"); - assert.equal(m2Fixture.endpoint, DEV_ENDPOINT, "M2 fixture endpoint"); - assert.equal(m2Fixture.defaultMode, "dry-run", "M2 fixture default mode"); - - const runtime = await readJSON("fixtures/mvp/runtime.json"); - assert.equal(runtime.endpoints.dev, DEV_ENDPOINT, "MVP runtime endpoint"); - - const m5Plan = await readJSON("fixtures/mvp/m5-e2e/dry-run-plan.json"); - assert.equal(m5Plan.mode, "dry-run", "M5 plan mode"); - assert.equal(m5Plan.environment, ENVIRONMENT_DEV, "M5 plan environment"); - assert.equal(m5Plan.endpoint, DEV_ENDPOINT, "M5 plan endpoint"); - assert.equal(m5Plan.safety.allowNetwork, false, "M5 dry-run network gate"); - assert.equal(m5Plan.safety.allowDeploy, false, "M5 dry-run deploy gate"); - assert.equal(m5Plan.safety.allowSecrets, false, "M5 dry-run secret gate"); -} - -async function collectContracts() { - return Promise.all( - contracts.map(async (entry) => { - const paths = await Promise.all(entry.paths.map(async (item) => ({ path: item, exists: await pathExists(item) }))); - const missing = paths.filter((item) => !item.exists).map((item) => item.path); - return { - ...entry, - status: missing.length === 0 ? entry.status : "blocked_missing_contract_file", - paths, - missing - }; - }) - ); -} - -async function collectDevGateReports() { - const entries = await readdir(path.join(repoRoot, "reports/dev-gate"), { withFileTypes: true }); - const reports = []; - for (const entry of entries) { - if (!entry.isFile() || !entry.name.endsWith(".json")) continue; - const reportPath = path.join("reports/dev-gate", entry.name); - const report = await readJSON(reportPath); - reports.push({ - path: reportPath, - issue: report.issue, - taskId: report.taskId, - acceptanceLevel: report.acceptanceLevel, - status: "contract_available", - blockers: Array.isArray(report.blockers) ? report.blockers.length : 0, - notes: report.notes ?? "" - }); - } - return reports.sort((a, b) => a.path.localeCompare(b.path)); -} - -function buildBlockers(collectedContracts) { - const blockers = [ - blocker("observability_blocker", "issue-read", "GitHub issue state is not readable in this runner; visible #33-#39 results are limited to repository artifacts and prompt context."), - blocker("network_blocker", "DEV-LIVE", "No repository-local artifact can be treated as live DEV evidence for ingress, edge, frp, D601 router, or downstream HWLAB services."), - blocker("observability_blocker", "artifact-provenance", "The checked artifact catalog is a contract skeleton; it explicitly does not claim real CI publish, registry verification, or deployment."), - blocker("runtime_blocker", "M3/M4 DEV loops", "M3 and M4 currently have local contract smoke evidence only; live DEV trace, audit, operation, evidence, and cleanup IDs are not attached.") - ]; - for (const item of collectedContracts) { - if (item.missing.length > 0) { - blockers.push(blocker("contract_blocker", item.milestone, `${item.milestone} is missing repository contract file(s): ${item.missing.join(", ")}.`)); - } - } - return blockers; -} - -function blocker(type, scope, summary) { - return { type, scope, status: "open", source: "BLOCKED", summary }; -} - -function groupBySource(collectedContracts, devLive, blockers) { - const grouped = Object.fromEntries(levels.map((level) => [level, []])); - for (const item of collectedContracts) { - grouped[item.source].push({ - milestone: item.milestone, - supports: item.supports, - status: item.status, - category: item.category, - summary: item.summary - }); - } - grouped["DEV-LIVE"].push(...devLive); - grouped.BLOCKED.push(...blockers); - return grouped; -} - -function validateOutput(output) { - assert.equal(output.environment, ENVIRONMENT_DEV, "output environment"); - assert.equal(output.endpoint, DEV_ENDPOINT, "output endpoint"); - assert.equal(output.devOnly, true, "output devOnly"); - assert.equal(output.prodDisabled, true, "output prodDisabled"); - assert.deepEqual(Object.keys(output.levels), levels, "output levels"); - assert.ok(output.blockers.length > 0, "aggregator must not report green without DEV-LIVE evidence"); - for (const item of output.blockers) { - assert.ok(blockerTypes.includes(item.type), `unknown blocker type ${item.type}`); - assert.equal(item.status, "open", "aggregator blockers remain open until live evidence is attached"); - } -} - -async function buildReport() { - await assertStaticContracts(); - const evidenceContracts = await collectContracts(); - const blockers = buildBlockers(evidenceContracts); - const devLive = [{ - milestone: "M2-M5", - issue: "pikasTech/HWLAB#39", - status: "not_available", - category: "DEV-LIVE", - summary: "No live DEV evidence was read or generated by this report-only aggregator." - }]; - const output = { - reportVersion: "v1", - taskId: "dev-evidence-blocker-aggregator", - issue: "pikasTech/HWLAB#41", - supports: allSupportedIssues, - generatedFromCommit: gitCommit(), - environment: ENVIRONMENT_DEV, - endpoint: DEV_ENDPOINT, - devOnly: true, - prodDisabled: true, - safety: { - reportOnly: true, - noLiveOperation: true, - noDeploy: true, - noProd: true, - noSecretRead: true, - noHeavyE2E: true, - noBrowserTest: true - }, - issueRead: { - status: "blocked", - reason: "GitHub issue data is not available from this runner; this report uses repository files and prompt context only." - }, - overall: { - status: "blocked", - green: false, - reason: "SOURCE, LOCAL, and DRY-RUN contracts are visible, but DEV-LIVE evidence and issue reads are blocked." - }, - evidenceContracts, - devGateReports: await collectDevGateReports(), - visibleParallelResults, - levels: groupBySource(evidenceContracts, devLive, blockers), - blockers, - nextRoundRecommendations, - validationCommands: [ - "node --check scripts/dev-evidence-blocker-aggregator.mjs", - "node scripts/dev-evidence-blocker-aggregator.mjs --check", - "node scripts/dev-evidence-blocker-aggregator.mjs --pretty" - ] - }; - validateOutput(output); - return output; +function hasFlag(name) { + return process.argv.includes(name); } try { - const output = await buildReport(); - if (process.argv.includes("--check")) { - process.stdout.write(JSON.stringify({ - ok: true, - taskId: output.taskId, - status: output.overall.status, - blockers: output.blockers.length, - sourceLevels: levels - }) + "\n"); + const report = await buildReport(); + const wroteReports = hasFlag("--write-report"); + + if (wroteReports) { + const jsonPath = path.join(repoRoot, "reports/dev-gate/dev-m5-gate-aggregator-v2.json"); + const markdownPath = path.join(repoRoot, "reports/dev-gate/dev-m5-gate-aggregator-v2.md"); + await writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`); + await writeFile(markdownPath, renderMarkdown(report)); + } + + if (hasFlag("--check")) { + process.stdout.write(`${JSON.stringify(formatCheckSummary(report))}\n`); + } else if (hasFlag("--markdown")) { + process.stdout.write(renderMarkdown(report)); + } else if (wroteReports) { + process.stdout.write(`${JSON.stringify({ + ...formatCheckSummary(report), + wrote: [ + "reports/dev-gate/dev-m5-gate-aggregator-v2.json", + "reports/dev-gate/dev-m5-gate-aggregator-v2.md" + ] + })}\n`); } else { - process.stdout.write(JSON.stringify(output, null, process.argv.includes("--pretty") ? 2 : 0) + "\n"); + process.stdout.write(`${JSON.stringify(report, null, hasFlag("--pretty") ? 2 : 0)}\n`); } } catch (error) { const message = error instanceof Error ? error.message : String(error); - process.stderr.write(JSON.stringify({ ok: false, error: message }) + "\n"); + process.stderr.write(`${JSON.stringify({ ok: false, error: message })}\n`); process.exitCode = 1; } diff --git a/scripts/src/dev-evidence-blocker-aggregator.mjs b/scripts/src/dev-evidence-blocker-aggregator.mjs new file mode 100644 index 00000000..12780889 --- /dev/null +++ b/scripts/src/dev-evidence-blocker-aggregator.mjs @@ -0,0 +1,838 @@ +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 (blocker.scope.includes("edge") || blocker.scope.includes("ingress") || blocker.summary.includes("6667")) { + return { + ...blocker, + priority: "P2", + unblockOrder: 6, + unblocks: [issue(36), issue(38), issue(37), issue(39)], + rationale: "The :6667/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 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; + + 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: "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: "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 ?? []) { + blockers.push(normalizeBlocker(blocker, report)); + } + for (const blocker of report.artifactPublish?.blockers ?? []) { + blockers.push(normalizeBlocker(blocker, report)); + } + for (const blocker of report.devDeployApply?.remainingBlockers ?? []) { + 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 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; + + 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-6667", + status: hasBlocker(blockers, (blocker) => blocker.scope.includes("edge") || blocker.scope.includes("ingress") || blocker.summary.includes("6667")) ? "blocked" : "pass", + evidenceLevel: "BLOCKED", + summary: "No committed report proves live HTTP 200/JSON on http://74.48.78.17:6667." + }, + { + 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")) 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")) return "Read-only DEV route observation for :6667/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, and DRY-RUN evidence exists, but DEV-LIVE acceptance is blocked by artifact, observability, DB, edge/frp, 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 + }; +} + +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. +`; +} diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index c059d0d5..36d2280d 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -18,7 +18,8 @@ const issueFamily = Object.freeze({ DEV_M3_HARDWARE_LOOP: "pikasTech/HWLAB#38", DEV_M5_GATE_REPORT: "pikasTech/HWLAB#39", DEV_BASE_IMAGE_PREFLIGHT: "pikasTech/HWLAB#40", - DEV_EVIDENCE_BLOCKER_AGGREGATOR: "pikasTech/HWLAB#41" + DEV_EVIDENCE_BLOCKER_AGGREGATOR: "pikasTech/HWLAB#41", + DEV_M5_GATE_AGGREGATOR_V2: "pikasTech/HWLAB#58" }); const allowedIssues = new Set([ contractIssue, @@ -26,10 +27,19 @@ const allowedIssues = new Set([ ]); const requiredPreflightIssue = issueFamily.DEV_GATE_PREFLIGHT; const requiredMilestoneIds = ["M0", "M1", "M2", "M3", "M4", "M5"]; +const evidenceLevels = new Set(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]); const requiredValidationCommands = [ "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ]; +const requiredAggregatorV2Commands = [ + "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 requiredDevDeployApplySupports = [ "pikasTech/HWLAB#50", "pikasTech/HWLAB#7", @@ -277,6 +287,10 @@ async function validateReport(relativePath) { await validatePreflightReport(relativePath, report); return; } + if (report.reportKind === "dev-m5-gate-aggregator") { + await validateAggregatorV2Report(relativePath, report); + return; + } const label = relativePath; assertObject(report, label); @@ -1091,6 +1105,247 @@ async function validatePreflightReport(relativePath, report) { } } +function assertEvidenceLevel(value, label) { + assertString(value, label); + assert.ok(evidenceLevels.has(value), `${label} must be one of ${Array.from(evidenceLevels).join(", ")}`); +} + +function assertBoolean(value, label) { + assert.equal(typeof value, "boolean", `${label} must be a boolean`); +} + +async function validateAggregatorV2Report(relativePath, report) { + const label = relativePath; + + assertObject(report, label); + for (const field of [ + "$schema", + "$id", + "reportVersion", + "reportKind", + "issue", + "supports", + "generatedAt", + "generatedFromCommit", + "environment", + "endpoint", + "devOnly", + "prodDisabled", + "safety", + "sourceReports", + "overall", + "dod", + "milestones", + "evidence", + "levels", + "blockers", + "nextSteps", + "validationCommands" + ]) { + assert.ok(Object.hasOwn(report, field), `${label} missing ${field}`); + } + + assertString(report.$schema, `${label}.$schema`); + assertString(report.$id, `${label}.$id`); + assert.equal(report.reportVersion, "v2", `${label}.reportVersion`); + assert.equal(report.reportKind, "dev-m5-gate-aggregator", `${label}.reportKind`); + assert.equal(report.issue, issueFamily.DEV_M5_GATE_AGGREGATOR_V2, `${label}.issue`); + assertTimestamp(report.generatedAt, `${label}.generatedAt`); + assert.match(report.generatedFromCommit, /^([a-f0-9]{7,40}|unknown)$/, `${label}.generatedFromCommit`); + assert.equal(report.environment, "dev", `${label}.environment`); + assert.equal(report.endpoint, "http://74.48.78.17:6667", `${label}.endpoint`); + assert.equal(report.devOnly, true, `${label}.devOnly`); + assert.equal(report.prodDisabled, true, `${label}.prodDisabled`); + + assertStringArray(report.supports, `${label}.supports`, { minLength: 10 }); + for (const requiredIssue of [ + "pikasTech/HWLAB#7", + "pikasTech/HWLAB#9", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#35", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#37", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#39", + "pikasTech/HWLAB#46" + ]) { + assert.ok(report.supports.includes(requiredIssue), `${label}.supports missing ${requiredIssue}`); + } + + assertObject(report.safety, `${label}.safety`); + for (const field of [ + "reportOnly", + "noDeploy", + "noProd", + "noSecretRead", + "noRuntimeRestart", + "noLiveProbe", + "noHeavyE2E", + "noUniDeskRuntimeSubstitute" + ]) { + assert.equal(report.safety[field], true, `${label}.safety.${field}`); + } + + assertObject(report.sourceReports, `${label}.sourceReports`); + for (const requiredReport of [ + "devPreflight", + "devDeploy", + "devArtifacts", + "devEdgeHealth", + "devM3Hardware", + "devM4Agent", + "devM5Gate", + "d601Observability" + ]) { + assert.ok(Object.hasOwn(report.sourceReports, requiredReport), `${label}.sourceReports missing ${requiredReport}`); + const sourceReport = report.sourceReports[requiredReport]; + assertObject(sourceReport, `${label}.sourceReports.${requiredReport}`); + for (const field of ["path", "issue", "taskId", "status", "commitId"]) { + assert.ok(Object.hasOwn(sourceReport, field), `${label}.sourceReports.${requiredReport} missing ${field}`); + assertString(sourceReport[field], `${label}.sourceReports.${requiredReport}.${field}`); + } + assertRepoRelativePath(sourceReport.path, `${label}.sourceReports.${requiredReport}.path`); + } + + assertObject(report.overall, `${label}.overall`); + assert.ok(["green", "blocked"].includes(report.overall.status), `${label}.overall.status`); + assertBoolean(report.overall.green, `${label}.overall.green`); + assertString(report.overall.reason, `${label}.overall.reason`); + assert.equal(report.overall.green, report.overall.status === "green", `${label}.overall.green must match status`); + + assertObject(report.dod, `${label}.dod`); + assert.ok(["green", "blocked"].includes(report.dod.status), `${label}.dod.status`); + assertBoolean(report.dod.green, `${label}.dod.green`); + assertArray(report.dod.checks, `${label}.dod.checks`); + assert.ok(report.dod.checks.length >= 7, `${label}.dod.checks must cover #9 DoD gates`); + assertUnique(report.dod.checks.map((check) => check.id), `${label}.dod.checks`); + for (const [index, check] of report.dod.checks.entries()) { + const checkLabel = `${label}.dod.checks[${index}]`; + assertObject(check, checkLabel); + for (const field of ["id", "status", "evidenceLevel", "summary"]) { + assert.ok(Object.hasOwn(check, field), `${checkLabel} missing ${field}`); + } + assert.match(check.id, /^[a-z][a-z0-9-]*$/, `${checkLabel}.id`); + assert.ok(["pass", "blocked"].includes(check.status), `${checkLabel}.status`); + assertEvidenceLevel(check.evidenceLevel, `${checkLabel}.evidenceLevel`); + assertString(check.summary, `${checkLabel}.summary`); + } + + assertArray(report.milestones, `${label}.milestones`); + assert.deepEqual( + report.milestones.map((milestone) => milestone.id), + requiredMilestoneIds, + `${label}.milestones must cover M0-M5 in order` + ); + for (const [index, milestone] of report.milestones.entries()) { + const milestoneLabel = `${label}.milestones[${index}]`; + assertObject(milestone, milestoneLabel); + for (const field of ["id", "status", "highestVisibleLevel", "liveEvidence", "evidenceCount", "blockerCount", "summary"]) { + assert.ok(Object.hasOwn(milestone, field), `${milestoneLabel} missing ${field}`); + } + assertStatus(milestone.status, `${milestoneLabel}.status`); + assertEvidenceLevel(milestone.highestVisibleLevel, `${milestoneLabel}.highestVisibleLevel`); + assert.ok(["pass", "missing_or_blocked"].includes(milestone.liveEvidence), `${milestoneLabel}.liveEvidence`); + assert.ok(Number.isInteger(milestone.evidenceCount), `${milestoneLabel}.evidenceCount`); + assert.ok(Number.isInteger(milestone.blockerCount), `${milestoneLabel}.blockerCount`); + assertString(milestone.summary, `${milestoneLabel}.summary`); + } + + assertArray(report.evidence, `${label}.evidence`); + assert.ok(report.evidence.length >= 10, `${label}.evidence must include M0-M5 entries`); + for (const [index, evidence] of report.evidence.entries()) { + const evidenceLabel = `${label}.evidence[${index}]`; + assertObject(evidence, evidenceLabel); + for (const field of ["milestone", "issue", "level", "status", "category", "summary"]) { + assert.ok(Object.hasOwn(evidence, field), `${evidenceLabel} missing ${field}`); + } + assert.match(evidence.milestone, /^M[0-5]$/, `${evidenceLabel}.milestone`); + assertString(evidence.issue, `${evidenceLabel}.issue`); + assertEvidenceLevel(evidence.level, `${evidenceLabel}.level`); + assertString(evidence.status, `${evidenceLabel}.status`); + assertString(evidence.category, `${evidenceLabel}.category`); + assertString(evidence.summary, `${evidenceLabel}.summary`); + if (Object.hasOwn(evidence, "reportPath")) { + assertRepoRelativePath(evidence.reportPath, `${evidenceLabel}.reportPath`); + } + if (Object.hasOwn(evidence, "commands")) { + assertStringArray(evidence.commands, `${evidenceLabel}.commands`); + } + if (Object.hasOwn(evidence, "evidence")) { + assertStringArray(evidence.evidence, `${evidenceLabel}.evidence`); + } + if (Object.hasOwn(evidence, "sources")) { + assertStringArray(evidence.sources, `${evidenceLabel}.sources`); + } + } + + assertObject(report.levels, `${label}.levels`); + assert.deepEqual(Object.keys(report.levels), Array.from(evidenceLevels), `${label}.levels keys`); + for (const level of evidenceLevels) { + assertArray(report.levels[level], `${label}.levels.${level}`); + } + + assertArray(report.blockers, `${label}.blockers`); + assert.ok(report.blockers.length >= 1, `${label}.blockers must not be empty while blocked`); + for (const [index, blocker] of report.blockers.entries()) { + const blockerLabel = `${label}.blockers[${index}]`; + assertObject(blocker, blockerLabel); + for (const field of ["id", "priority", "type", "scope", "status", "source", "sourceIssue", "summary", "unblockOrder", "unblocks", "rationale"]) { + assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`); + } + assertString(blocker.id, `${blockerLabel}.id`); + assert.match(blocker.priority, /^P[0-3]$/, `${blockerLabel}.priority`); + assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type`); + assertString(blocker.scope, `${blockerLabel}.scope`); + assert.equal(blocker.status, "open", `${blockerLabel}.status`); + assertRepoRelativePath(blocker.source, `${blockerLabel}.source`); + assertString(blocker.sourceIssue, `${blockerLabel}.sourceIssue`); + assertString(blocker.summary, `${blockerLabel}.summary`); + assert.ok(Number.isInteger(blocker.unblockOrder), `${blockerLabel}.unblockOrder`); + assertStringArray(blocker.unblocks, `${blockerLabel}.unblocks`, { minLength: 1 }); + assertString(blocker.rationale, `${blockerLabel}.rationale`); + if (Object.hasOwn(blocker, "nextTask")) { + assertString(blocker.nextTask, `${blockerLabel}.nextTask`); + } + } + + assertArray(report.nextSteps, `${label}.nextSteps`); + assert.ok(report.nextSteps.length >= 5, `${label}.nextSteps must contain blocker order`); + assert.deepEqual( + report.nextSteps.map((step) => step.order), + [...report.nextSteps].map((step) => step.order).sort((a, b) => a - b), + `${label}.nextSteps must be sorted by order` + ); + for (const [index, step] of report.nextSteps.entries()) { + const stepLabel = `${label}.nextSteps[${index}]`; + assertObject(step, stepLabel); + for (const field of ["order", "priority", "scopes", "sourceIssues", "rationale", "action", "evidenceRequired"]) { + assert.ok(Object.hasOwn(step, field), `${stepLabel} missing ${field}`); + } + assert.ok(Number.isInteger(step.order), `${stepLabel}.order`); + if (Object.hasOwn(step, "blockerOrder")) { + assert.ok(Number.isInteger(step.blockerOrder), `${stepLabel}.blockerOrder`); + } + assert.match(step.priority, /^P[0-3]$/, `${stepLabel}.priority`); + assertStringArray(step.scopes, `${stepLabel}.scopes`, { minLength: 1 }); + assertStringArray(step.sourceIssues, `${stepLabel}.sourceIssues`, { minLength: 1 }); + assertString(step.rationale, `${stepLabel}.rationale`); + assertString(step.action, `${stepLabel}.action`); + assertString(step.evidenceRequired, `${stepLabel}.evidenceRequired`); + } + + assertStringArray(report.validationCommands, `${label}.validationCommands`, { + minLength: requiredAggregatorV2Commands.length + }); + for (const requiredCommand of requiredAggregatorV2Commands) { + assert.ok( + report.validationCommands.includes(requiredCommand), + `${label}.validationCommands missing ${requiredCommand}` + ); + } +} + async function main() { const reportFiles = await collectReportFiles(); assert.ok(reportFiles.length >= 1, "expected at least one dev-gate report JSON file");