Merge pull request #244 from pikasTech/fix/dev-gate-degraded-acceptance-guard-pr

Guard degraded DEV gate acceptance summaries
This commit is contained in:
Lyon
2026-05-23 12:17:57 +08:00
committed by GitHub
10 changed files with 726 additions and 94 deletions
+5 -5
View File
@@ -48,7 +48,7 @@ without promoting the full gate:
| Layer | Meaning |
| --- | --- |
| `Frontend DEV revision` | Latest accepted Cloud Workbench frontend revision on the active `http://74.48.78.17:16666/` browser endpoint. Frontend revision evidence is browser-only and cannot green DB, M3, M4, or M5. |
| `Frontend DEV revision` | Latest Cloud Workbench frontend revision visible on the active `http://74.48.78.17:16666/` browser endpoint. While API health or runtime durability is degraded, this may only be summarized as deployed UI usable in degraded/read-only mode; frontend evidence cannot green DB, M3, M4, or M5. |
| `EDGE/ROUTE live` | `16666` browser and `16667` health route evidence from read-only probes. |
| `DB live/degraded` | Cloud API DB readiness from redacted health evidence; route success and env presence do not imply DB live. A `dns_error`/`dns_resolution_failed` DB probe is a DB live blocker. |
| `Code Agent provider Secret` | #143 provider-backed chat readiness from redacted runtime error and Secret key-presence evidence only; missing provider Secret is independent from DB live. |
@@ -66,9 +66,9 @@ Legacy public `http://74.48.78.17:6666` and
`http://74.48.78.17:6667` are historical/deprecated only and are not active
green evidence.
`latestFrontendDevFact` records the current #99/#108 frontend fact: revision
`1e8805664970839b72be40c34636b08f6d18b131` is accepted on DEV, with
`promotesM3M4M5=false`.
`latestFrontendDevFact` records the current #99/#108 frontend visibility fact:
revision `1e8805664970839b72be40c34636b08f6d18b131` is visible on DEV in
degraded/read-only mode, with `promotesM3M4M5=false`.
## Current Contract Map
@@ -76,7 +76,7 @@ green evidence.
| --- | --- | --- |
| M0 | `SOURCE` | `docs/m0-contract-audit.md`, protocol schemas, evidence chain examples, M0 validator. |
| M1 | `LOCAL` | `docs/m1-local-smoke.md`, `fixtures/mvp/runtime.json`, `scripts/m1-contract-smoke.mjs`. |
| M2 | `DEV-LIVE` for frontend/route only | DEV deploy smoke fixture plus active read-only `16666`/`16667` endpoint smoke and accepted frontend revision. |
| M2 | `DEV-LIVE` for frontend/route only | DEV deploy smoke fixture plus active read-only `16666`/`16667` endpoint smoke and degraded/read-only frontend visibility. |
| M3 | `BLOCKED` | Hardware trusted loop topology fixture, local patch-panel smoke, and the no-write DEV plan exist, but DEV-LIVE operation/trace/audit/evidence IDs for `res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` are missing. |
| M4 | `BLOCKED` | Agent automation loop fixture and local smoke exist, but live preflight is blocked at DB live readiness. |
| M5 | `BLOCKED` | MVP e2e dry-run plan is green, but bounded DEV-LIVE acceptance is blocked by DB/M3/M4 and source/artifact coverage. |
+68 -7
View File
@@ -2,7 +2,7 @@
"$schema": "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json",
"reportVersion": "v1",
"status": "pass",
"status": "degraded",
"issue": "pikasTech/HWLAB#7",
"taskId": "dev-cloud-workbench-live",
"commitId": "unknown",
@@ -15,7 +15,7 @@
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
"summary": "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance."
},
"task": "DC-DCSN-P0-2026-003",
"refs": [
@@ -33,8 +33,8 @@
"mode": "live",
"url": "http://74.48.78.17:16666/",
"generatedAt": "2026-05-22T18:11:35.955Z",
"evidenceLevel": "DEV-LIVE-BROWSER",
"devLive": true,
"evidenceLevel": "DEV-LIVE-BROWSER-DEGRADED",
"devLive": false,
"sourceIdentity": {
"status": "observed",
"source": "git-head",
@@ -53,10 +53,35 @@
"serviceId": "hwlab-cloud-api",
"environment": "dev",
"healthStatus": "degraded",
"ready": false,
"commitId": "c7de474",
"commitSource": "runtime-env",
"imageTag": "c7de474",
"observedAt": "2026-05-22T18:11:24.844Z",
"runtime": {
"durable": false,
"ready": false,
"status": "degraded",
"blocker": "runtime_durable_adapter_query_blocked",
"connection": {
"queryAttempted": true,
"queryResult": "query_blocked"
}
},
"readiness": {
"ready": false,
"status": "degraded",
"durability": {
"ready": false,
"status": "blocked",
"blocker": "runtime_durable_adapter_query_blocked",
"blockedLayer": "durability_query",
"queryResult": "query_blocked"
}
},
"blockerCodes": [
"runtime_durable_adapter_query_blocked"
],
"summary": "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
},
"sourceContract": {
@@ -97,7 +122,7 @@
"summary": "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
},
"devPreconditions": {
"status": "pass",
"status": "degraded",
"requirements": [
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
"Browser DOM exposes the default workbench and core controls.",
@@ -107,7 +132,7 @@
"commands": [
"node scripts/dev-cloud-workbench-smoke.mjs --live --url http://74.48.78.17:16666/ --report reports/dev-gate/dev-cloud-workbench-live.json"
],
"summary": "Deployed 16666 browser journey and same-origin Code Agent chat completed."
"summary": "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance."
},
"endpoints": {
"frontend": "http://74.48.78.17:16666",
@@ -154,6 +179,42 @@
}
}
},
{
"id": "live-api-runtime-readiness",
"status": "degraded",
"summary": "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance.",
"evidence": [
"api.status=degraded",
"ready=false",
"runtime.durable=false",
"runtime.ready=false",
"durability.ready=false",
"runtime_durable_adapter_query_blocked",
"blocker=runtime_durable_adapter_query_blocked"
],
"observations": {
"status": "degraded",
"healthStatus": "degraded",
"ready": false,
"runtimeDurable": false,
"runtimeReady": false,
"durabilityReady": false,
"durableBlocked": true,
"blockerCodes": [
"runtime_durable_adapter_query_blocked"
],
"evidence": [
"api.status=degraded",
"ready=false",
"runtime.durable=false",
"runtime.ready=false",
"durability.ready=false",
"runtime_durable_adapter_query_blocked",
"blocker=runtime_durable_adapter_query_blocked"
],
"summary": "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
}
},
{
"id": "live-code-agent-browser-journey",
"status": "pass",
@@ -241,7 +302,7 @@
"error.code",
"error.missingEnv"
],
"statement": "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
"statement": "Live smoke records deployed UI usable in degraded/read-only mode only; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance and must not be used to infer M3/M4/M5 acceptance."
},
"artifacts": {
"reportPath": "/workspace/hwlab/.worktrees/deployed-workbench-journey-evidence/reports/dev-gate/dev-cloud-workbench-live.json"
+50 -22
View File
@@ -20,8 +20,8 @@
"pikasTech/HWLAB#46",
"pikasTech/HWLAB#64"
],
"generatedAt": "2026-05-22T16:59:52.864Z",
"generatedFromCommit": "f64182b54fe6",
"generatedAt": "2026-05-23T03:16:52.953Z",
"generatedFromCommit": "96e802b2a22c",
"environment": "dev",
"endpoint": "http://74.48.78.17:16667",
"frontendEndpoint": "http://74.48.78.17:16666",
@@ -84,7 +84,7 @@
"taskId": "dev-artifact-publish",
"lifecycleState": "active",
"status": "published",
"commitId": "7e29522"
"commitId": "7de6edd"
},
"devEdgeHealth": {
"path": "reports/dev-gate/dev-edge-health.json",
@@ -138,7 +138,7 @@
"overall": {
"status": "blocked",
"green": false,
"reason": "Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight."
"reason": "Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight."
},
"latestFrontendDevFact": {
"revision": "1e8805664970839b72be40c34636b08f6d18b131",
@@ -149,15 +149,15 @@
"web/hwlab-cloud-web/styles.css"
],
"evidence": [
"Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is the active frontend route only",
"Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
"commands": [
"node web/hwlab-cloud-web/scripts/check.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --static"
],
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence.",
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only.",
"issue": "pikasTech/HWLAB#99",
"supports": [
"pikasTech/HWLAB#99",
@@ -206,7 +206,7 @@
"id": "cloud-api-db-ready",
"status": "blocked",
"evidenceLevel": "BLOCKED",
"summary": "cloud-api DB status=degraded; ready=false; connected=false; liveDbEvidence=false."
"summary": "cloud-api DB status=degraded; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false."
},
{
"id": "m3-hardware-trusted-loop",
@@ -233,10 +233,10 @@
"label": "Frontend DEV revision",
"status": "pass",
"evidenceLevel": "DEV-LIVE",
"summary": "http://74.48.78.17:16666/ serves the accepted Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; this is browser/frontend evidence only.",
"summary": "http://74.48.78.17:16666/ serves the Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only.",
"evidence": [
"Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is the active frontend route only",
"Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
"nextRequired": "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance."
@@ -245,7 +245,7 @@
"label": "EDGE/ROUTE live",
"status": "pass",
"evidenceLevel": "DEV-LIVE",
"summary": "http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned accepted HWLAB DEV responses in the active M2 read-only smoke.",
"summary": "http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned observed HWLAB DEV route responses in the active M2 read-only smoke.",
"evidence": [
"http://74.48.78.17:16667/health -> HTTP 200 identity=hwlab-edge-proxy status=ok",
"http://74.48.78.17:16667/health/live -> HTTP 200 identity=hwlab-cloud-api status=degraded",
@@ -257,7 +257,7 @@
"label": "DB live/degraded",
"status": "blocked",
"evidenceLevel": "BLOCKED",
"summary": "cloud-api DB status=degraded; configReady=true; ready=false; connected=false; liveDbEvidence=false.",
"summary": "cloud-api DB status=degraded; configReady=true; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false.",
"evidence": [
"http://74.48.78.17:16667/health/live -> HTTP 200 serviceId=hwlab-cloud-api status=degraded db.ready=false db.connected=false db.connectionChecked=false",
"dev-edge-health active report -> blocked by DB live readiness, while public 16667 TCP/HTTP and frp control/tunnel health are reachable",
@@ -405,11 +405,12 @@
"status": "blocked",
"currentLevel": "BLOCKED",
"blockerClass": "db-live-readiness",
"dependency": "Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure.",
"dependency": "Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure.",
"evidence": [
"db.status=degraded",
"db.ready=false",
"db.connected=false",
"db.endpointSource=unknown",
"db.liveDbEvidence=false"
],
"nextRequired": "Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass.",
@@ -483,7 +484,7 @@
"highestVisibleLevel": "DRY-RUN",
"liveEvidence": "missing_or_blocked",
"evidenceCount": 2,
"blockerCount": 15,
"blockerCount": 16,
"summary": "dry-run is green but live MVP gate is blocked; highest visible level is DRY-RUN; status is blocked."
}
],
@@ -545,11 +546,11 @@
"node scripts/dev-cloud-workbench-smoke.mjs --static"
],
"evidence": [
"Cloud Web /health/live accepted revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is the active frontend route only",
"Cloud Web /health/live observed revision 1e8805664970839b72be40c34636b08f6d18b131",
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence."
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only."
},
{
"milestone": "M2",
@@ -607,7 +608,7 @@
"issue": "pikasTech/HWLAB#35",
"taskId": "dev-artifact-publish",
"reportPath": "reports/dev-gate/dev-artifacts.json",
"commitId": "7e29522",
"commitId": "7de6edd",
"lifecycleState": "active",
"level": "DRY-RUN",
"status": "published",
@@ -751,7 +752,8 @@
"status": "not_run",
"category": "hardware-loop-live",
"commands": [
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --confirmed-non-production"
"node scripts/dev-m3-hardware-loop-smoke.mjs --dry-run",
"node scripts/dev-m3-hardware-loop-smoke.mjs --live --confirm-dev --expect-non-prod"
],
"evidence": [
"operationId=not_observed",
@@ -996,7 +998,7 @@
"lifecycleState": "active",
"status": "pass",
"category": "frontend-dev-revision",
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest accepted DEV frontend fact, but it is frontend-only evidence."
"summary": "#99/#108 Cloud Workbench revision 1e8805664970839b72be40c34636b08f6d18b131 is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only."
},
{
"milestone": "M2",
@@ -1192,6 +1194,15 @@
"source": "reports/dev-gate/dev-edge-health.json",
"summary": "Code Agent provider Secret hwlab-code-agent-provider/openai-api-key is not present as key-presence evidence; #143 real provider-backed chat remains blocked"
},
{
"priority": "P3",
"order": 99,
"type": "safety_blocker",
"scope": "m3-live-write-authorization",
"sourceIssue": "pikasTech/HWLAB#38",
"source": "reports/dev-gate/dev-m3-hardware-loop.json",
"summary": "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets."
},
{
"priority": "P3",
"order": 99,
@@ -1456,6 +1467,21 @@
],
"rationale": "Residual blocker that must be classified before claiming a green DEV gate."
},
{
"id": "dev-m3-hardware-loop:m3-live-write-authorization",
"priority": "P3",
"type": "safety_blocker",
"scope": "m3-live-write-authorization",
"status": "open",
"source": "reports/dev-gate/dev-m3-hardware-loop.json",
"sourceIssue": "pikasTech/HWLAB#38",
"summary": "Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets.",
"unblockOrder": 99,
"unblocks": [
"pikasTech/HWLAB#39"
],
"rationale": "Residual blocker that must be classified before claiming a green DEV gate."
},
{
"id": "dev-m4-agent-loop:agent-mgr-health",
"priority": "P3",
@@ -1544,7 +1570,7 @@
],
"rationale": "Cloud API DB env and health readiness block runtime health and MVP evidence even if ingress starts responding.",
"action": "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value.",
"evidenceRequired": "Cloud API health/live output showing DB env ready and redacted secret references, without secret material."
"evidenceRequired": "Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material."
},
{
"order": 4,
@@ -1571,12 +1597,14 @@
"priority": "P3",
"scopes": [
"code-agent-provider-secret",
"m3-live-write-authorization",
"agent-mgr-health",
"skills-commit-version-injection"
],
"sourceIssues": [
"pikasTech/HWLAB#36",
"pikasTech/HWLAB#37",
"pikasTech/HWLAB#38",
"pikasTech/HWLAB#39"
],
"rationale": "Residual blocker that must be classified before claiming a green DEV gate.",
+10 -9
View File
@@ -1,7 +1,7 @@
# HWLAB M5 DEV Gate Aggregator v2
Status: blocked
Generated from: `f64182b54fe6`
Generated from: `96e802b2a22c`
Scope: DEV only, report-only
Active frontend: `http://74.48.78.17:16666/`
Active API/live: `http://74.48.78.17:16667/health/live`
@@ -9,19 +9,19 @@ Deprecated public endpoints: `http://74.48.78.17:6666`, `http://74.48.78.17:6667
## Summary
Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.
Frontend revision 1e8805664970839b72be40c34636b08f6d18b131 is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.
## Frontend DEV Fact
The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be40c34636b08f6d18b131` at `http://74.48.78.17:16666/`. This is DEV-LIVE browser/frontend evidence only and does not promote M3, M4, or M5.
The latest #99/#108 frontend DEV visibility fact is revision `1e8805664970839b72be40c34636b08f6d18b131` at `http://74.48.78.17:16666/`. It means the deployed UI is usable in degraded/read-only mode only; this is DEV-LIVE browser/frontend evidence and does not promote M3, M4, or M5.
## Current DEV Layering
| Layer | Status | Evidence level | Current conclusion | Required next proof |
| --- | --- | --- | --- | --- |
| Frontend DEV revision | pass | DEV-LIVE | http://74.48.78.17:16666/ serves the accepted Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; this is browser/frontend evidence only. | Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance. |
| EDGE/ROUTE live | pass | DEV-LIVE | http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned accepted HWLAB DEV responses in the active M2 read-only smoke. | Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance. |
| DB live/degraded | blocked | BLOCKED | cloud-api DB status=degraded; configReady=true; ready=false; connected=false; liveDbEvidence=false. | Provide live DB connection evidence through redacted health output; route reachability alone is insufficient. |
| Frontend DEV revision | pass | DEV-LIVE | http://74.48.78.17:16666/ serves the Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only. | Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance. |
| EDGE/ROUTE live | pass | DEV-LIVE | http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned observed HWLAB DEV route responses in the active M2 read-only smoke. | Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance. |
| DB live/degraded | blocked | BLOCKED | cloud-api DB status=degraded; configReady=true; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false. | Provide live DB connection evidence through redacted health output; route reachability alone is insufficient. |
| D601 runner observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. | Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired. |
| M3 hardware trusted loop | blocked | BLOCKED | No live DEV operation was attempted because read-only direct target checks did not prove the required patch-panel-owned M3 route. Blocker: DEV exposes two box-simu endpoints, but live identities are not the required distinct resources res_boxsimu_1 and res_boxsimu_2; observed resources=res_boxsimu_1. | Only a real DEV res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 observation with operation/trace/audit/evidence can clear M3. |
| M4 agent loop | blocked | BLOCKED | Blocked at DB live readiness before scheduling a DEV agent task. | Do not schedule or claim the agent loop as live until DB live and required runtime/evidence preconditions pass. |
@@ -58,7 +58,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be
| artifact-publish-digests | blocked | BLOCKED | artifactState=contract-skeleton, ciPublished=false, registryVerified=false, sha256=0, not_published=13, targetCovered=false |
| d601-k3s-observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. |
| dev-edge-frp-16667 | pass | DEV-LIVE | Committed edge report proves read-only public HTTP on :16667 /health and /health/live; this is route evidence, not DB/M3/M4/M5 acceptance. |
| cloud-api-db-ready | blocked | BLOCKED | cloud-api DB status=degraded; ready=false; connected=false; liveDbEvidence=false. |
| cloud-api-db-ready | blocked | BLOCKED | cloud-api DB status=degraded; ready=false; connected=false; endpointSource=unknown; liveDbEvidence=false. |
| m3-hardware-trusted-loop | blocked | BLOCKED | M3 trusted loop is blocked until res_boxsimu_1:DO1 -> patch-panel -> res_boxsimu_2:DI1 is proven with operation/trace/audit/evidence. |
| m4-agent-loop-live | blocked | BLOCKED | M4 agent loop live path is blocked before accepted agent scheduling/evidence closure. |
| m5-mvp-dev-live | blocked | BLOCKED | M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed. |
@@ -68,7 +68,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be
| Milestone | Status | Current level | Blocker class | Dependency | Required next proof |
| --- | --- | --- | --- | --- | --- |
| M3 | blocked | BLOCKED | direct-target-identity-gap | Two distinct live box-simu resources and two distinct gateway-simu identities are required before M3 can run. | Fix DEV simulator instance identity so two box-simu pods report res_boxsimu_1/res_boxsimu_2 and two gateway-simu pods report distinct gateway identities. |
| M4 | blocked | BLOCKED | db-live-readiness | Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure. | Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass. |
| M4 | blocked | BLOCKED | db-live-readiness | Cloud API /health/live must report DB ready=true, connected=true, endpointSource=secret-url-host, and liveDbEvidence=true before live agent scheduling/evidence closure. | Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass. |
| M5 | blocked | BLOCKED | composite-db-m3-m4-live | M5 needs DB live readiness, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage. | After DB/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations. |
## Blockers
@@ -89,6 +89,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be
| P0 | 6 | runtime_blocker | m3-patch-panel-wiring | DEV patch-panel is callable, but live wiring does not contain res_boxsimu_1:DO1 -> res_boxsimu_2:DI1; active=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0; configured=res_boxsim_alpha:uart0->res_boxsim_beta:uart0, res_boxsim_alpha:gpio0->res_boxsim_beta:gpio0. |
| P0 | 6 | runtime_blocker | m3-hardware-loop-runtime | Live M3 smoke reached DEV simulators, but patch-panel active DO1 -> DI1 connection is missing. |
| P3 | 99 | agent_blocker | code-agent-provider-secret | Code Agent provider Secret hwlab-code-agent-provider/openai-api-key is not present as key-presence evidence; #143 real provider-backed chat remains blocked |
| P3 | 99 | safety_blocker | m3-live-write-authorization | Plan-only mode is intentionally non-mutating; run the bounded live smoke only after explicit DEV/non-PROD approval and after read-only preconditions identify the exact HWLAB targets. |
| P3 | 99 | agent_blocker | agent-mgr-health | hwlab-agent-mgr /health/live is degraded. |
| P3 | 99 | agent_blocker | skills-commit-version-injection | DEV skills injection is incomplete: missing hwlab-agent-skills.HWLAB_SKILLS_VERSION, worker-dry-run.HWLAB_SKILL_VERSION_FROM_DEV. |
@@ -99,7 +100,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be
2. Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report.
Evidence required: Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write.
3. Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value.
Evidence required: Cloud API health/live output showing DB env ready and redacted secret references, without secret material.
Evidence required: Cloud API health/live output showing DB env ready, endpointSource=secret-url-host, redacted secret references, and no secret material.
4. Fix DEV box-simu instance identity so direct endpoints expose distinct res_boxsimu_1 and res_boxsimu_2 resources.
Evidence required: Read-only direct box-simu /health/live and /status output showing distinct res_boxsimu_1 and res_boxsimu_2 resources.
5. Resolve the blocker and attach source/local/dry-run/DEV-live evidence at the correct level.
@@ -143,6 +143,36 @@ test("Code Agent browser classifier blocks provider 502 response shapes", () =>
assert.equal(classification.blocker, "provider-upstream");
});
test("Code Agent browser classifier treats sanitized 服务受阻 UI as blocked provider evidence", () => {
const summary = sanitizeAgentChatBody({
status: "failed",
provider: "openai-responses",
model: "gpt-5.5",
backend: "hwlab-cloud-api/openai-responses",
traceId: "trc_provider_502",
error: {
code: "provider_unavailable",
message: "OpenAI Responses returned HTTP 502: request rejected",
providerStatus: 502,
missingEnv: []
}
}, { httpStatus: 200 });
const classification = classifyCodeAgentBrowserJourney({
responseSummary: summary,
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "服务受阻",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(classification.status, "blocked");
assert.equal(classification.blocker, "provider-upstream");
});
test("Code Agent readiness classifier blocks completed payloads over any non-2xx HTTP status", () => {
const completedPayload = {
conversationId: "cnv_completed_non_2xx",
+2 -1
View File
@@ -1,5 +1,6 @@
const trustedLiveProviders = new Set(["openai-responses", "codex-cli"]);
const untrustedProviderPattern = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]);
export function classifyCodeAgentChatReadiness(payload, { realDevLive = false, httpStatus = null } = {}) {
const providerBlock = classifyCodeAgentProviderBlock(payload, { httpStatus });
@@ -193,7 +194,7 @@ export function summarizeCodeAgentPayload(payload, { httpStatus = null, traceId
export function classifyCodeAgentBrowserJourney({ responseSummary, httpOk = false, httpStatus = null, ui = null } = {}) {
const providerBlock = classifyCodeAgentProviderBlock(responseSummary, { httpStatus });
const uiCompleted = ui?.agentChatStatus === "DEV-LIVE 回复" || ui?.agentChatStatus === "已回复" || ui?.completedMessageVisible === true;
const uiFailed = ui?.failedMessageVisible === true || ui?.agentChatStatus === "发送失败" || ui?.agentChatStatus === "BLOCKED 凭证缺口";
const uiFailed = ui?.failedMessageVisible === true || blockedCodeAgentUiLabels.has(ui?.agentChatStatus);
const evidence = inspectSanitizedCompletionEvidence(responseSummary);
const backendCompleted =
httpOk &&
+151 -9
View File
@@ -148,6 +148,8 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
"message-evidence-chip"
]);
const blockedCodeAgentUiLabels = Object.freeze(["服务受阻", "BLOCKED 凭证缺口"]);
const forbiddenWritePatterns = Object.freeze([
/callRpc\(\s*["']hardware\./u,
/hardware\.operation\.request/u,
@@ -317,7 +319,7 @@ function runStaticSmoke() {
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider credential blockers and only real completed replies can become DEV-LIVE.", {
blocker: "observability_blocker",
evidence: ["BLOCKED 凭证缺口", "provider_unavailable", "OPENAI_API_KEY", "hwlab-code-agent-provider/openai-api-key", "completed -> dev-live guard"]
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard"]
});
addCheck(checks, blockers, "code-agent-completed-evidence-visible", hasCodeAgentCompletedEvidenceVisibility(files), "Completed Code Agent replies expose provider/model/trace/conversation evidence and reject echo/mock/stub completions.", {
@@ -331,7 +333,7 @@ function runStaticSmoke() {
"DEV-LIVE 回复",
"SOURCE 回复",
"发送失败",
"BLOCKED 凭证缺口",
"服务受阻 or legacy BLOCKED 凭证缺口",
"message-evidence/details/chips"
]
});
@@ -391,6 +393,13 @@ async function runLiveSmoke(args) {
const blockers = [];
const sourceIdentity = observeSourceIdentity();
const runtimeIdentity = await observeLiveRuntimeIdentity(runtime.endpoints.api);
const apiRuntimeReadiness = classifyLiveApiRuntimeReadiness(runtimeIdentity);
addCheck(checks, blockers, "live-api-runtime-readiness", apiRuntimeReadiness.status, apiRuntimeReadiness.summary, {
blocker: "runtime_blocker",
evidence: apiRuntimeReadiness.evidence,
observations: apiRuntimeReadiness
});
const expectedRuntimeIdentity = observeExpectedRuntimeIdentity("hwlab-cloud-api");
const deploymentIdentity = classifyLiveDeploymentIdentity(sourceIdentity, runtimeIdentity, expectedRuntimeIdentity);
addCheck(checks, blockers, "live-runtime-current-main", deploymentIdentity.status, deploymentIdentity.summary, {
@@ -523,14 +532,18 @@ async function runLiveSmoke(args) {
});
}
const status = blockers.length === 0 ? "pass" : "blocked";
const status = blockers.length === 0
? apiRuntimeReadiness.status === "degraded" ? "degraded" : "pass"
: "blocked";
return baseReport({
mode: "live",
url: args.url,
status,
checks,
blockers,
evidenceLevel: status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED",
evidenceLevel: status === "pass"
? "DEV-LIVE-BROWSER"
: status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
devLive: status === "pass",
runtimeIdentity,
sourceIdentity,
@@ -545,7 +558,9 @@ async function runLiveSmoke(args) {
sourceIsDevLive: false,
liveMode: "browser-user-journey-with-code-agent-post",
retainedApiFields: ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv", "error.providerStatus"],
statement: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
statement: status === "degraded"
? "Live smoke records deployed UI usable in degraded/read-only mode only; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance and must not be used to infer M3/M4/M5 acceptance."
: "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance."
}
});
}
@@ -567,6 +582,7 @@ function baseReport({
webAssetIdentity = null
}) {
const liveJourneyPassed = mode === "live" && status === "pass";
const liveJourneyDegraded = mode === "live" && status === "degraded";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-cloud-workbench-live.json",
@@ -584,7 +600,9 @@ function baseReport({
activeEndpoint: runtime.endpoints.api,
activeBrowserEndpoint: runtime.endpoints.frontend,
deprecatedEndpoint: null,
summary: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
summary: liveJourneyDegraded
? "Current deployed browser user journey report for the 16666 Cloud Workbench; deployed UI usable in degraded/read-only mode and does not substitute for M3/M4/M5 DEV-LIVE acceptance."
: "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance."
},
task: "DC-DCSN-P0-2026-003",
refs: [
@@ -643,7 +661,7 @@ function baseReport({
summary: "The journey evidence is collected from the real DEV browser endpoint, not a dry-run fixture."
},
devPreconditions: {
status: liveJourneyPassed ? "pass" : "blocked",
status: liveJourneyPassed ? "pass" : liveJourneyDegraded ? "degraded" : "blocked",
requirements: [
"GET http://74.48.78.17:16666/ serves the Cloud Workbench HTML.",
"Browser DOM exposes the default workbench and core controls.",
@@ -655,6 +673,8 @@ function baseReport({
],
summary: liveJourneyPassed
? "Deployed 16666 browser journey and same-origin Code Agent chat completed."
: liveJourneyDegraded
? "Deployed UI usable in degraded/read-only mode; API health/runtime durability is degraded, so this is not full DEV-LIVE acceptance."
: mode === "live"
? "Deployed browser journey is blocked; see checks and blockers."
: "Static/local fixture evidence does not establish the deployed 16666 browser journey."
@@ -1189,10 +1209,9 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
/codeAgentAvailability/u.test(app) &&
/codeAgentAvailabilityFrom/u.test(app) &&
/latestCompletedAgentMessage/u.test(app) &&
/BLOCKED 凭证缺口/u.test(app) &&
blockedCodeAgentUiLabels.some((label) => app.includes(label)) &&
/provider_unavailable/u.test(app) &&
/OPENAI_API_KEY/u.test(app) &&
/hwlab-code-agent-provider\/openai-api-key/u.test(app) &&
/只有真实 completed 回复才标 DEV-LIVE/u.test(app) &&
/不能因为只有 conversationId 标为开发实况/u.test(app) &&
/isTrustedCodeAgentProvider/u.test(app) &&
@@ -1417,6 +1436,75 @@ function staticSafety() {
};
}
export function classifyLiveApiRuntimeReadiness(runtimeIdentity) {
const healthStatus = sanitizeRuntimeString(runtimeIdentity?.healthStatus);
const runtime = runtimeIdentity?.runtime ?? {};
const readiness = runtimeIdentity?.readiness ?? {};
const durability = readiness?.durability ?? {};
const blockerCodes = Array.isArray(runtimeIdentity?.blockerCodes)
? runtimeIdentity.blockerCodes.map((item) => sanitizeRuntimeString(item))
: [];
const ready = runtimeIdentity?.ready === true || readiness?.ready === true;
const runtimeReady = runtime?.ready === true;
const durabilityReady = durability?.ready === true;
const runtimeDurable = runtime?.durable === true;
const durableBlocked = runtime?.blocker === "runtime_durable_adapter_query_blocked" ||
durability?.blocker === "runtime_durable_adapter_query_blocked" ||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
durability?.blockedLayer === "durability_query" ||
runtime?.connection?.queryResult === "query_blocked" ||
durability?.queryResult === "query_blocked";
const degraded = healthStatus === "degraded" ||
ready === false ||
runtimeReady === false ||
durabilityReady === false ||
runtimeDurable === false ||
durableBlocked;
if (!degraded) {
return {
status: "pass",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`
],
summary: "Live API health and runtime durability are ready for browser journey evidence."
};
}
const blockedEvidence = [
`api.status=${healthStatus}`,
`ready=${ready}`,
`runtime.durable=${runtimeDurable}`,
`runtime.ready=${runtimeReady}`,
`durability.ready=${durabilityReady}`,
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
...blockerCodes.map((code) => `blocker=${code}`)
].filter(Boolean);
return {
status: "degraded",
healthStatus,
ready,
runtimeDurable,
runtimeReady,
durabilityReady,
durableBlocked,
blockerCodes,
evidence: blockedEvidence,
summary: "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
};
}
async function observeLiveRuntimeIdentity(apiEndpoint) {
const endpoint = new URL("/health/live", apiEndpoint).toString();
const controller = new AbortController();
@@ -1442,10 +1530,14 @@ async function observeLiveRuntimeIdentity(apiEndpoint) {
serviceId: sanitizeRuntimeString(body.serviceId),
environment: sanitizeRuntimeString(body.environment),
healthStatus: sanitizeRuntimeString(body.status),
ready: body.ready === true,
commitId: sanitizeRevision(body.commit?.id ?? body.commitId),
commitSource: sanitizeRuntimeString(body.commit?.source),
imageTag: sanitizeRuntimeString(body.image?.tag),
observedAt: sanitizeTimestamp(body.observedAt),
runtime: summarizeRuntimeHealth(body.runtime),
readiness: summarizeReadinessHealth(body.readiness),
blockerCodes: sanitizeStringList(body.blockerCodes),
summary: "Live runtime identity was observed through the existing read-only health endpoint and is not inferred from source git HEAD."
};
} catch (error) {
@@ -1466,15 +1558,65 @@ function notObservedRuntimeIdentity(reason, endpoint = new URL("/health/live", r
serviceId: "not_observed",
environment: "not_observed",
healthStatus: "not_observed",
ready: false,
commitId: "not_observed",
commitSource: "not_observed",
imageTag: "not_observed",
observedAt: new Date().toISOString(),
runtime: {
durable: false,
ready: false,
status: "not_observed",
blocker: "not_observed"
},
readiness: {
ready: false,
status: "not_observed",
durability: {
ready: false,
status: "not_observed",
blocker: "not_observed",
blockedLayer: "not_observed",
queryResult: "not_observed"
}
},
blockerCodes: [],
reason: oneLine(reason),
summary: "Live runtime identity is recorded as not_observed; source identity must not be treated as the deployed runtime revision."
};
}
function summarizeRuntimeHealth(runtimeHealth) {
return {
durable: runtimeHealth?.durable === true,
ready: runtimeHealth?.ready === true,
status: sanitizeRuntimeString(runtimeHealth?.status),
blocker: sanitizeRuntimeString(runtimeHealth?.blocker),
connection: {
queryAttempted: runtimeHealth?.connection?.queryAttempted === true,
queryResult: sanitizeRuntimeString(runtimeHealth?.connection?.queryResult)
}
};
}
function summarizeReadinessHealth(readiness) {
return {
ready: readiness?.ready === true,
status: sanitizeRuntimeString(readiness?.status),
durability: {
ready: readiness?.durability?.ready === true,
status: sanitizeRuntimeString(readiness?.durability?.status),
blocker: sanitizeRuntimeString(readiness?.durability?.blocker),
blockedLayer: sanitizeRuntimeString(readiness?.durability?.blockedLayer),
queryResult: sanitizeRuntimeString(readiness?.durability?.queryResult)
}
};
}
function sanitizeStringList(value) {
return Array.isArray(value) ? value.map((item) => sanitizeRuntimeString(item)) : [];
}
function sanitizeRevision(value) {
if (typeof value !== "string") return "unknown";
const trimmed = value.trim().toLowerCase();
@@ -109,15 +109,15 @@ const frontendDevFact = Object.freeze({
"web/hwlab-cloud-web/styles.css"
],
evidence: [
`Cloud Web /health/live accepted revision ${latestFrontendRevision}`,
"Cloud Workbench public browser endpoint is the active frontend route only",
`Cloud Web /health/live observed revision ${latestFrontendRevision}`,
"Cloud Workbench public browser endpoint is usable in degraded/read-only mode only",
"Frontend load/revision evidence cannot satisfy DB, M3 hardware-loop, M4 agent-loop, or M5 MVP e2e acceptance"
],
commands: [
"node web/hwlab-cloud-web/scripts/check.mjs",
"node scripts/dev-cloud-workbench-smoke.mjs --static"
],
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest accepted DEV frontend fact, but it is frontend-only evidence.`
summary: `#99/#108 Cloud Workbench revision ${latestFrontendRevision} is the latest DEV frontend visibility fact; API/runtime durability remains degraded, so it is degraded/read-only UI evidence only.`
});
function issue(id) {
@@ -949,7 +949,7 @@ function buildCurrentDevLayering(reports, blockers) {
label: "Frontend DEV revision",
status: "pass",
evidenceLevel: "DEV-LIVE",
summary: `${activeBrowserRoot} serves the accepted Cloud Workbench frontend revision ${latestFrontendRevision}; this is browser/frontend evidence only.`,
summary: `${activeBrowserRoot} serves the Cloud Workbench frontend revision ${latestFrontendRevision}; deployed UI is usable in degraded/read-only mode only and this is browser/frontend evidence only.`,
evidence: frontendDevFact.evidence,
nextRequired: "Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance."
},
@@ -958,7 +958,7 @@ function buildCurrentDevLayering(reports, blockers) {
status: m2EndpointLive || edgeLive ? "pass" : "blocked",
evidenceLevel: m2EndpointLive || edgeLive ? "DEV-LIVE" : "BLOCKED",
summary: m2EndpointLive
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned accepted HWLAB DEV responses in the active M2 read-only smoke.`
? `${activeBrowserRoot}, ${DEV_ENDPOINT}/health, and ${activeApiLiveEndpoint} returned observed HWLAB DEV route responses in the active M2 read-only smoke.`
: `No active read-only public endpoint report proves both ${activeBrowserRoot} and ${activeApiLiveEndpoint}.`,
evidence: publicEndpointEvidence(reports.devM2Smoke),
nextRequired: "Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance."
@@ -1299,7 +1299,7 @@ export async function buildReport() {
green: dod.green,
reason: dod.green
? "All #9 DEV DoD checks are green."
: `Frontend revision ${latestFrontendRevision} and EDGE/ROUTE DEV-LIVE evidence exist on :16666/:16667, but M5 remains blocked by artifact source drift, DB live degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
: `Frontend revision ${latestFrontendRevision} is usable in degraded/read-only mode and EDGE/ROUTE DEV-LIVE evidence exists on :16666/:16667, but M5 remains blocked by artifact source drift, API/runtime durability degradation, missing M3 trusted loop operation evidence, and blocked M4 agent-loop preflight.`
},
latestFrontendDevFact: {
...frontendDevFact,
@@ -1382,7 +1382,7 @@ ${report.overall.reason}
## Frontend DEV Fact
The latest accepted #99/#108 frontend DEV fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. This is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence only and does not promote M3, M4, or M5.
The latest #99/#108 frontend DEV visibility fact is revision \`${report.latestFrontendDevFact.revision}\` at \`${report.latestFrontendDevFact.endpoint}\`. It means the deployed UI is usable in degraded/read-only mode only; this is ${report.latestFrontendDevFact.evidenceLevel} browser/frontend evidence and does not promote M3, M4, or M5.
## Current DEV Layering
+187 -9
View File
@@ -21,6 +21,76 @@ function cloneBaseAggregatorReport() {
return JSON.parse(JSON.stringify(baseAggregatorReport));
}
function fullAcceptanceReport() {
const report = cloneBaseReport();
report.status = "pass";
report.evidenceLevel = "DEV-LIVE-BROWSER";
report.devLive = true;
report.devPreconditions.status = "pass";
report.devPreconditions.summary = "Deployed 16666 browser journey and same-origin Code Agent chat completed.";
report.reportLifecycle.summary = "Current deployed browser user journey report for the 16666 Cloud Workbench; it does not substitute for M3/M4/M5 hardware acceptance.";
report.safety.statement = "Live smoke opens the deployed workbench and sends one controlled Code Agent message; it does not call hardware write APIs and must not be used to infer M3 DEV-LIVE hardware-loop acceptance.";
Object.assign(report.runtimeIdentity, {
healthStatus: "ok",
ready: true,
runtime: {
durable: true,
ready: true,
status: "ok",
blocker: "none",
connection: {
queryAttempted: true,
queryResult: "ok"
}
},
readiness: {
ready: true,
status: "ok",
durability: {
ready: true,
status: "ready",
blocker: "none",
blockedLayer: "none",
queryResult: "ok"
}
},
blockerCodes: []
});
const readiness = report.checks.find((check) => check.id === "live-api-runtime-readiness");
if (readiness) {
Object.assign(readiness, {
status: "pass",
summary: "Live API health and runtime durability are ready for browser journey evidence.",
evidence: [
"api.status=ok",
"ready=true",
"runtime.durable=true",
"runtime.ready=true",
"durability.ready=true"
],
observations: {
status: "pass",
healthStatus: "ok",
ready: true,
runtimeDurable: true,
runtimeReady: true,
durabilityReady: true,
durableBlocked: false,
blockerCodes: [],
evidence: [
"api.status=ok",
"ready=true",
"runtime.durable=true",
"runtime.ready=true",
"durability.ready=true"
],
summary: "Live API health and runtime durability are ready for browser journey evidence."
}
});
}
return report;
}
function preflightBlockedReport() {
const report = cloneBaseReport();
report.status = "blocked";
@@ -177,6 +247,51 @@ test("accepts the sanitized deployed workbench evidence contract", async () => {
});
});
test("accepts full workbench acceptance only when API health and runtime durability are ready", async () => {
await withReport(fullAcceptanceReport(), async (reportPath) => {
const result = runValidator(reportPath);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
});
});
test("rejects full workbench acceptance when API health is degraded", async () => {
const report = fullAcceptanceReport();
report.runtimeIdentity.healthStatus = "degraded";
report.checks.find((check) => check.id === "live-api-runtime-readiness").observations.healthStatus = "degraded";
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
});
test("rejects full workbench acceptance when runtime durability query is blocked", async () => {
const report = fullAcceptanceReport();
Object.assign(report.runtimeIdentity.runtime, {
durable: false,
ready: false,
status: "degraded",
blocker: "runtime_durable_adapter_query_blocked",
connection: {
queryAttempted: true,
queryResult: "query_blocked"
}
});
Object.assign(report.runtimeIdentity.readiness.durability, {
ready: false,
status: "blocked",
blocker: "runtime_durable_adapter_query_blocked",
blockedLayer: "durability_query",
queryResult: "query_blocked"
});
report.runtimeIdentity.blockerCodes = ["runtime_durable_adapter_query_blocked"];
await assertRejected(report, /degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted/);
});
test("rejects degraded workbench evidence with full-acceptance wording", async () => {
const report = cloneBaseReport();
report.devPreconditions.summary = "M3/M4/M5 DEV-LIVE accepted.";
report.safety.statement = "Full DEV-LIVE accepted.";
await assertRejected(report, /degraded API\/runtime report must not claim full M3\/M4\/M5 DEV-LIVE acceptance/);
});
test("accepts preflight-blocked workbench evidence when Code Agent POST is marked not_sent", async () => {
await withReport(preflightBlockedReport(), async (reportPath) => {
const result = runValidator(reportPath);
@@ -242,8 +357,7 @@ test("rejects accepted workbench evidence that mixes provider_unavailable blocke
await assertRejected(report, /provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/);
});
test("accepts blocked workbench evidence for provider HTTP 502 without promoting completion", async () => {
const report = cloneBaseReport();
function applyProviderBlockedJourney(report, { uiLabel = "发送失败" } = {}) {
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
report.status = "blocked";
report.evidenceLevel = "BLOCKED";
@@ -277,7 +391,7 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting
"provider=openai-responses",
"model=gpt-5.5",
"traceId=trc_provider_502",
"uiStatus=发送失败",
`uiStatus=${uiLabel}`,
"blocker=provider-upstream"
];
journey.observations.request.status = 200;
@@ -300,7 +414,75 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting
providerStatus: 502,
reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed"
};
journey.observations.ui.agentChatStatus = "发送失败";
journey.observations.ui.agentChatStatus = uiLabel;
journey.observations.ui.inputCleared = false;
journey.observations.ui.completedMessageVisible = false;
journey.observations.ui.failedMessageVisible = true;
journey.observations.networkEvents = [
{
method: "POST",
status: 200,
urlPath: "/v1/agent/chat",
body: journey.observations.response
}
];
return report;
}
test("accepts blocked workbench evidence for provider HTTP 502 without promoting completion", async () => {
const report = applyProviderBlockedJourney(cloneBaseReport());
await withReport(report, async (reportPath) => {
const result = runValidator(reportPath);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
});
});
test("accepts sanitized 服务受阻 provider blocker wording only as blocked evidence", async () => {
const report = applyProviderBlockedJourney(cloneBaseReport(), { uiLabel: "服务受阻" });
await withReport(report, async (reportPath) => {
const result = runValidator(reportPath);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
});
});
test("rejects sanitized 服务受阻 provider blocker evidence promoted to full workbench acceptance", async () => {
const report = fullAcceptanceReport();
const journey = report.checks.find((check) => check.id === "live-code-agent-browser-journey");
journey.status = "pass";
journey.summary = "Incorrectly promoted provider blocker evidence.";
journey.evidence = [
"POST /v1/agent/chat HTTP 200",
"status=failed",
"provider=openai-responses",
"model=gpt-5.5",
"traceId=trc_provider_502",
"uiStatus=服务受阻",
"blocker=provider-upstream"
];
journey.observations.response = {
status: "failed",
provider: "openai-responses",
model: "gpt-5.5",
backend: "hwlab-cloud-api/openai-responses",
traceId: "trc_provider_502",
hasReply: false,
error: {
code: "provider_unavailable",
missingEnv: [],
providerStatus: 502
}
};
journey.observations.classification = {
status: "blocked",
blocker: "provider-upstream",
providerStatus: 502,
reason: "Code Agent HTTP non-2xx status 502 means the upstream response is blocked, not completed"
};
journey.observations.ui.agentChatStatus = "服务受阻";
journey.observations.ui.inputCleared = false;
journey.observations.ui.completedMessageVisible = false;
journey.observations.ui.failedMessageVisible = true;
@@ -313,11 +495,7 @@ test("accepts blocked workbench evidence for provider HTTP 502 without promoting
}
];
await withReport(report, async (reportPath) => {
const result = runValidator(reportPath);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /validated 1 dev-gate report JSON file/);
});
await assertRejected(report, /response\.status|provider_unavailable, OPENAI_API_KEY, or providerStatus blockers/);
});
test("rejects accepted workbench evidence that hides providerStatus 502 in network events", async () => {
+216 -25
View File
@@ -42,6 +42,7 @@ const evidenceLevels = new Set(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCK
const devCloudWorkbenchRuntimeIdentityMaxSkewMs = 15 * 60 * 1000;
const syntheticAcceptedCodeAgentModels = new Set(["gpt-5"]);
const deploymentPreflightCodeAgentErrorCode = "deployment_identity_preflight";
const blockedCodeAgentUiLabels = new Set(["发送失败", "服务受阻", "BLOCKED 凭证缺口"]);
const requiredValidationCommands = [
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
@@ -1351,13 +1352,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(report.devOnly, true, `${label}.devOnly`);
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
assertStatus(report.status, `${label}.status`);
assert.equal(report.status, report.blockers.length === 0 ? "pass" : "blocked", `${label}.status must match blockers`);
assert.equal(
report.blockers.length === 0 ? ["pass", "degraded"].includes(report.status) : report.status === "blocked",
true,
`${label}.status must match blockers`
);
assertTimestamp(report.generatedAt, `${label}.generatedAt`);
assert.equal(report.mode, "live", `${label}.mode`);
assert.equal(report.url, "http://74.48.78.17:16666/", `${label}.url`);
assert.equal(report.evidenceLevel, report.status === "pass" ? "DEV-LIVE-BROWSER" : "BLOCKED", `${label}.evidenceLevel`);
assert.equal(
report.evidenceLevel,
report.status === "pass"
? "DEV-LIVE-BROWSER"
: report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
`${label}.evidenceLevel`
);
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
assertDevCloudWorkbenchIdentity(report, label);
const apiRuntimeReadiness = assertDevCloudWorkbenchApiRuntimeReadiness(report, label);
assertDevCloudWorkbenchEndpoints(report, label);
assertObject(report.sourceContract, `${label}.sourceContract`);
@@ -1424,6 +1436,7 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
for (const requiredCheck of [
"live-http-html",
"live-browser-dom",
"live-api-runtime-readiness",
"live-code-agent-browser-journey"
]) {
assert.ok(checks.has(requiredCheck), `${label}.checks missing ${requiredCheck}`);
@@ -1445,8 +1458,24 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(dom.coreControlsVisible[control], true, `${label}.live-browser-dom.coreControlsVisible.${control}`);
}
const apiRuntimeReadinessCheck = checks.get("live-api-runtime-readiness");
assert.equal(
apiRuntimeReadinessCheck.status,
apiRuntimeReadiness.status,
`${label}.live-api-runtime-readiness.status`
);
assert.deepEqual(
apiRuntimeReadinessCheck.observations,
apiRuntimeReadiness,
`${label}.live-api-runtime-readiness.observations`
);
const journey = checks.get("live-code-agent-browser-journey");
assert.equal(journey.status, report.status, `${label}.live-code-agent-browser-journey.status`);
assert.equal(
journey.status,
report.status === "degraded" ? "pass" : report.status,
`${label}.live-code-agent-browser-journey.status`
);
assertObject(journey.observations, `${label}.live-code-agent-browser-journey.observations`);
assertObject(journey.observations.request, `${label}.live-code-agent-browser-journey.request`);
assert.equal(journey.observations.request.method, "POST", `${label}.live-code-agent-browser-journey.request.method`);
@@ -1486,6 +1515,9 @@ async function validateDevCloudWorkbenchLiveReport(report, label, raw) {
assert.equal(report.safety.liveMode, "browser-user-journey-with-code-agent-post", `${label}.safety.liveMode`);
assertCodeAgentRetainedApiFields(report.safety.retainedApiFields, `${label}.safety.retainedApiFields`);
assertString(report.safety.statement, `${label}.safety.statement`);
if (report.status === "degraded") {
assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness);
}
assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey);
}
@@ -1517,7 +1549,7 @@ function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) {
assertObject(journey.observations.ui, `${label}.live-code-agent-browser-journey.ui`);
assert.ok(
["发送失败", "BLOCKED 凭证缺口"].includes(journey.observations.ui.agentChatStatus),
blockedCodeAgentUiLabels.has(journey.observations.ui.agentChatStatus),
`${label}.live-code-agent-browser-journey.ui.agentChatStatus`
);
assert.equal(journey.observations.ui.completedMessageVisible, false, `${label}.live-code-agent-browser-journey.ui.completedMessageVisible`);
@@ -1537,6 +1569,157 @@ function assertDevCloudWorkbenchCodeAgentBlocked(report, label, journey) {
assertString(report.safety.statement, `${label}.safety.statement`);
}
function assertDevCloudWorkbenchApiRuntimeReadiness(report, label) {
assertObject(report.runtimeIdentity, `${label}.runtimeIdentity`);
if (Object.hasOwn(report.runtimeIdentity, "ready")) {
assertBoolean(report.runtimeIdentity.ready, `${label}.runtimeIdentity.ready`);
}
if (Object.hasOwn(report.runtimeIdentity, "runtime")) {
assertObject(report.runtimeIdentity.runtime, `${label}.runtimeIdentity.runtime`);
for (const field of ["durable", "ready"]) {
if (Object.hasOwn(report.runtimeIdentity.runtime, field)) {
assertBoolean(report.runtimeIdentity.runtime[field], `${label}.runtimeIdentity.runtime.${field}`);
}
}
if (Object.hasOwn(report.runtimeIdentity.runtime, "status")) {
assertString(report.runtimeIdentity.runtime.status, `${label}.runtimeIdentity.runtime.status`);
}
if (Object.hasOwn(report.runtimeIdentity.runtime, "blocker")) {
assertString(report.runtimeIdentity.runtime.blocker, `${label}.runtimeIdentity.runtime.blocker`);
}
}
if (Object.hasOwn(report.runtimeIdentity, "readiness")) {
assertObject(report.runtimeIdentity.readiness, `${label}.runtimeIdentity.readiness`);
if (Object.hasOwn(report.runtimeIdentity.readiness, "ready")) {
assertBoolean(report.runtimeIdentity.readiness.ready, `${label}.runtimeIdentity.readiness.ready`);
}
if (Object.hasOwn(report.runtimeIdentity.readiness, "status")) {
assertString(report.runtimeIdentity.readiness.status, `${label}.runtimeIdentity.readiness.status`);
}
if (Object.hasOwn(report.runtimeIdentity.readiness, "durability")) {
assertObject(report.runtimeIdentity.readiness.durability, `${label}.runtimeIdentity.readiness.durability`);
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, "ready")) {
assertBoolean(report.runtimeIdentity.readiness.durability.ready, `${label}.runtimeIdentity.readiness.durability.ready`);
}
for (const field of ["status", "blocker", "blockedLayer", "queryResult"]) {
if (Object.hasOwn(report.runtimeIdentity.readiness.durability, field)) {
assertString(report.runtimeIdentity.readiness.durability[field], `${label}.runtimeIdentity.readiness.durability.${field}`);
}
}
}
}
if (Object.hasOwn(report.runtimeIdentity, "blockerCodes")) {
assertStringArray(report.runtimeIdentity.blockerCodes, `${label}.runtimeIdentity.blockerCodes`);
}
const runtime = report.runtimeIdentity.runtime ?? {};
const readiness = report.runtimeIdentity.readiness ?? {};
const durability = readiness.durability ?? {};
const blockerCodes = Array.isArray(report.runtimeIdentity.blockerCodes) ? report.runtimeIdentity.blockerCodes : [];
const durableBlocked = runtime.blocker === "runtime_durable_adapter_query_blocked" ||
durability.blocker === "runtime_durable_adapter_query_blocked" ||
blockerCodes.includes("runtime_durable_adapter_query_blocked") ||
durability.blockedLayer === "durability_query" ||
runtime.connection?.queryResult === "query_blocked" ||
durability.queryResult === "query_blocked";
const degraded = report.runtimeIdentity.healthStatus === "degraded" ||
report.runtimeIdentity.ready === false ||
runtime.ready === false ||
durability.ready === false ||
runtime.durable === false ||
durableBlocked;
const status = degraded ? "degraded" : "pass";
const result = {
status,
healthStatus: report.runtimeIdentity.healthStatus,
ready: report.runtimeIdentity.ready === true,
runtimeDurable: runtime.durable === true,
runtimeReady: runtime.ready === true,
durabilityReady: durability.ready === true,
durableBlocked,
blockerCodes,
evidence: [
`api.status=${report.runtimeIdentity.healthStatus}`,
`ready=${report.runtimeIdentity.ready === true}`,
`runtime.durable=${runtime.durable === true}`,
`runtime.ready=${runtime.ready === true}`,
`durability.ready=${durability.ready === true}`,
durableBlocked ? "runtime_durable_adapter_query_blocked" : null,
...blockerCodes.map((code) => `blocker=${code}`)
].filter(Boolean),
summary: degraded
? "Live API is reachable but degraded/read-only; runtime durability is not ready, so this cannot be full DEV-LIVE acceptance."
: "Live API health and runtime durability are ready for browser journey evidence."
};
if (["pass", "degraded"].includes(report.status)) {
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
const generatedAtMs = Date.parse(report.generatedAt);
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
assert.ok(
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
);
}
if (report.status === "pass") {
assert.equal(
degraded,
false,
`${label}.runtimeIdentity degraded API or runtime durability blocker cannot be summarized as full DEV-LIVE accepted`
);
}
if (degraded) {
assert.ok(
["degraded", "blocked"].includes(report.status),
`${label}.status must be degraded or blocked when API health or runtime durability is degraded`
);
assert.equal(report.devLive, false, `${label}.devLive must remain false for degraded/read-only workbench evidence`);
assert.equal(
report.evidenceLevel,
report.status === "degraded" ? "DEV-LIVE-BROWSER-DEGRADED" : "BLOCKED",
`${label}.evidenceLevel`
);
}
return result;
}
function assertDevCloudWorkbenchDegradedSummary(report, label, apiRuntimeReadiness) {
assert.equal(apiRuntimeReadiness.status, "degraded", `${label}.live-api-runtime-readiness.status`);
assert.equal(report.devPreconditions.status, "degraded", `${label}.devPreconditions.status`);
const summaryText = [
report.reportLifecycle.summary,
report.devPreconditions.summary,
report.safety.statement
].join(" ");
assert.match(
summaryText,
/degraded\/read-only mode|degraded\/read-only|read-only mode/u,
`${label} degraded API/runtime report must be summarized as deployed UI usable in degraded/read-only mode`
);
assert.doesNotMatch(
summaryText,
/full DEV-LIVE accepted|full DEV-LIVE acceptance passed|M3\/M4\/M5 DEV-LIVE accepted|M[345]\s+(?:DEV-LIVE\s+)?(?:accepted|acceptance passed|green)/u,
`${label} degraded API/runtime report must not claim full M3/M4/M5 DEV-LIVE acceptance`
);
}
function assertCodeAgentRetainedApiFields(value, label) {
assertArray(value, label);
for (const field of ["status", "provider", "model", "backend", "traceId", "hasReply", "error.code", "error.missingEnv"]) {
@@ -1763,31 +1946,39 @@ function assertFreshDevCloudWorkbenchLiveEvidence(report, label, raw, journey) {
`${label} deployed workbench report must not contain deprecated public endpoint evidence`
);
if (report.status !== "pass") {
if (!["pass", "degraded"].includes(report.status)) {
return;
}
assert.equal(report.devLive, true, `${label}.devLive`);
assert.equal(report.evidenceLevel, "DEV-LIVE-BROWSER", `${label}.evidenceLevel`);
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
assert.equal(report.runtimeIdentity.status, "observed", `${label}.runtimeIdentity.status`);
assert.equal(report.runtimeIdentity.source, "health-live", `${label}.runtimeIdentity.source`);
assert.equal(report.runtimeIdentity.endpoint, `${ACTIVE_DEV_PUBLIC_ENDPOINT}/health/live`, `${label}.runtimeIdentity.endpoint`);
assert.equal(report.runtimeIdentity.serviceId, "hwlab-cloud-api", `${label}.runtimeIdentity.serviceId`);
assert.equal(report.runtimeIdentity.environment, "dev", `${label}.runtimeIdentity.environment`);
assert.match(report.runtimeIdentity.commitId, /^[a-f0-9]{7,40}$/, `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitId, "unknown", `${label}.runtimeIdentity.commitId`);
assert.notEqual(report.runtimeIdentity.commitSource, "source-git-head", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "hard-coded", `${label}.runtimeIdentity.commitSource`);
assert.notEqual(report.runtimeIdentity.commitSource, "literal", `${label}.runtimeIdentity.commitSource`);
const generatedAtMs = Date.parse(report.generatedAt);
const runtimeObservedAtMs = Date.parse(report.runtimeIdentity.observedAt);
assert.ok(
Math.abs(generatedAtMs - runtimeObservedAtMs) <= devCloudWorkbenchRuntimeIdentityMaxSkewMs,
`${label}.runtimeIdentity.observedAt must be fresh relative to generatedAt`
assert.equal(report.devLive, report.status === "pass", `${label}.devLive`);
assert.equal(
report.evidenceLevel,
report.status === "pass" ? "DEV-LIVE-BROWSER" : "DEV-LIVE-BROWSER-DEGRADED",
`${label}.evidenceLevel`
);
assert.equal(report.safety.sourceIsDevLive, false, `${label}.safety.sourceIsDevLive`);
if (report.status === "pass") {
assert.notEqual(report.runtimeIdentity.healthStatus, "degraded", `${label}.runtimeIdentity.healthStatus`);
assert.equal(report.runtimeIdentity.ready, true, `${label}.runtimeIdentity.ready`);
assert.equal(report.runtimeIdentity.runtime?.durable, true, `${label}.runtimeIdentity.runtime.durable`);
assert.equal(report.runtimeIdentity.runtime?.ready, true, `${label}.runtimeIdentity.runtime.ready`);
assert.equal(report.runtimeIdentity.readiness?.durability?.ready, true, `${label}.runtimeIdentity.readiness.durability.ready`);
assert.notEqual(
report.runtimeIdentity.runtime?.blocker,
"runtime_durable_adapter_query_blocked",
`${label}.runtimeIdentity.runtime.blocker`
);
assert.notEqual(
report.runtimeIdentity.readiness?.durability?.blocker,
"runtime_durable_adapter_query_blocked",
`${label}.runtimeIdentity.readiness.durability.blocker`
);
assert.equal(
(report.runtimeIdentity.blockerCodes ?? []).includes("runtime_durable_adapter_query_blocked"),
false,
`${label}.runtimeIdentity.blockerCodes`
);
}
assert.equal(
report.sourceIdentity.reportCommitId === "unknown" || report.sourceIdentity.worktreeState === "clean",
true,