fix: add dev report lifecycle contract (#85)

Co-authored-by: root <root@lyon.remote>
This commit is contained in:
Lyon
2026-05-22 14:33:10 +08:00
committed by GitHub
parent 1af787f63f
commit db9d6ae6e3
21 changed files with 344 additions and 21 deletions
+21
View File
@@ -10,6 +10,26 @@ blockers stay in one place.
This contract is report-only. It does not authorize a real DEV or PROD
deployment.
## Report Lifecycle
Each committed DEV gate report must carry `reportLifecycle`:
- `state: "active"` means the report belongs to the current DEV gate surface.
Its `activeEndpoint` must be `http://74.48.78.17:16667`, its
`activeBrowserEndpoint` must be `http://74.48.78.17:16666`, and
`deprecatedEndpoint` must be `null`.
- `state: "historical"` means the report is retained only for evidence
provenance. It may name a deprecated endpoint such as
`http://74.48.78.17:6667`, but it must stay blocked/non-green and cannot be
consumed as active acceptance evidence.
- Active reports may preserve a legacy public endpoint only inside an explicit
`runtimeSmoke.legacyPublicEndpoint` object with `status: "deprecated"` and
`activeGreenEligible: false`.
- M3 DEV-LIVE remains narrower than report freshness: it requires
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` in real DEV
plus operation, audit, and evidence identifiers. SOURCE, LOCAL, DRY-RUN,
fixture, and historical report evidence are support/diagnostics only.
## Files
- `reports/dev-gate/dev-gate-report.example.json`: machine-readable example.
@@ -29,6 +49,7 @@ Every report must include:
- `acceptanceLevel`
- `devOnly`
- `prodDisabled`
- `reportLifecycle`
- `sourceContract`
- `validationCommands`
- `localSmoke`
+63
View File
@@ -0,0 +1,63 @@
export const ACTIVE_DEV_PUBLIC_ENDPOINT = "http://74.48.78.17:16667";
export const ACTIVE_DEV_BROWSER_ENDPOINT = "http://74.48.78.17:16666";
export const DEPRECATED_DEV_PUBLIC_ENDPOINT = "http://74.48.78.17:6667";
export const DEPRECATED_RESERVED_PROD_ENDPOINT = "http://74.48.78.17:6666";
export const REPORT_LIFECYCLE_VERSION = "v1";
export const deprecatedPublicEndpoints = Object.freeze([
DEPRECATED_DEV_PUBLIC_ENDPOINT,
DEPRECATED_RESERVED_PROD_ENDPOINT
]);
const deprecatedPublicEndpointPattern = /http:\/\/74\.48\.78\.17:666[67]\b/gu;
export function findDeprecatedPublicEndpoints(text) {
return [...new Set(String(text).match(deprecatedPublicEndpointPattern) ?? [])].sort();
}
export function isDeprecatedEndpointAllowedInActiveReport(report, endpoint) {
const legacyEndpoint = report?.runtimeSmoke?.legacyPublicEndpoint;
return Boolean(
legacyEndpoint &&
legacyEndpoint.endpoint === endpoint &&
legacyEndpoint.status === "deprecated" &&
legacyEndpoint.activeGreenEligible === false
);
}
export function findForbiddenActiveDeprecatedEndpoints(report, raw) {
if (reportIsHistorical(report)) return [];
return findDeprecatedPublicEndpoints(raw).filter(
(endpoint) => !isDeprecatedEndpointAllowedInActiveReport(report, endpoint)
);
}
export function reportLifecycleState(report) {
return report?.reportLifecycle?.state ?? "active";
}
export function reportIsHistorical(report) {
return reportLifecycleState(report) === "historical";
}
export function activeReportLifecycle(summary) {
return {
version: REPORT_LIFECYCLE_VERSION,
state: "active",
activeEndpoint: ACTIVE_DEV_PUBLIC_ENDPOINT,
activeBrowserEndpoint: ACTIVE_DEV_BROWSER_ENDPOINT,
deprecatedEndpoint: null,
summary
};
}
export function historicalReportLifecycle({ activeEndpoint = ACTIVE_DEV_PUBLIC_ENDPOINT, deprecatedEndpoint, summary }) {
return {
version: REPORT_LIFECYCLE_VERSION,
state: "historical",
activeEndpoint,
activeBrowserEndpoint: ACTIVE_DEV_BROWSER_ENDPOINT,
deprecatedEndpoint,
summary
};
}
+9 -1
View File
@@ -1114,5 +1114,13 @@
],
"blockers": []
},
"notes": "DEV-only artifact report. Do not treat runtime placeholder images as real service implementations."
"notes": "DEV-only artifact report. Do not treat runtime placeholder images as real service implementations.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current DEV artifact publish report; it is not a DEV-LIVE runtime substitute."
}
}
+9 -1
View File
@@ -1124,5 +1124,13 @@
}
},
"blockers": [],
"notes": "DEV-only result. No PROD resources, secret values, force push, heavy e2e, browser e2e, or UniDesk runtime substitute were used."
"notes": "DEV-only result. No PROD resources, secret values, force push, heavy e2e, browser e2e, or UniDesk runtime substitute were used.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current DEV deploy apply report; active public API/health endpoint is 16667 and browser endpoint is 16666."
}
}
+8
View File
@@ -752,5 +752,13 @@
],
"nextTask": "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."
}
},
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current DEV edge-health report; deprecated public 6666/6667 endpoints are not valid active evidence."
}
}
@@ -69,5 +69,13 @@
"summary": "No live DEV precondition evidence is attached."
}
],
"notes": "Contract-only example. Do not treat this file as a real DEV gate report."
"notes": "Contract-only example. Do not treat this file as a real DEV gate report.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Active example report for the current DEV gate contract."
}
}
@@ -252,5 +252,13 @@
"summary": "Legacy public :6667 observations are retained only as historical blockers and are not accepted as active green evidence."
}
},
"notes": "M2 active report uses read-only GET probes only; PROD, service restarts, deployments, and script-driven apply flows were not touched."
"notes": "M2 active report uses read-only GET probes only; PROD, service restarts, deployments, and script-driven apply flows were not touched.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current M2 DEV deploy smoke report; legacy public endpoints are deprecated and not active green evidence."
}
}
@@ -195,5 +195,13 @@
"classification": "hardware loop",
"observedAt": "2026-05-22T04:30:50.820Z",
"result": "Live DEV M3 runtime was reachable but did not satisfy the M3 hardware-loop contract."
},
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids."
}
}
+9 -1
View File
@@ -75,5 +75,13 @@
],
"summary": "Blocked at DB live readiness before scheduling a DEV agent task."
},
"notes": "DEV M4 agent loop live preflight report. No secret material is read."
"notes": "DEV M4 agent loop live preflight report. No secret material is read.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current DEV M4 agent-loop preflight report; it does not substitute for M3 DEV-LIVE acceptance."
}
}
@@ -1109,5 +1109,13 @@
"node scripts/dev-evidence-blocker-aggregator.mjs --markdown",
"node --check scripts/validate-dev-gate-report.mjs",
"node scripts/validate-dev-gate-report.mjs"
]
],
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current M5 evidence aggregator report; source, local, and dry-run evidence are not DEV-LIVE acceptance."
}
}
+9 -1
View File
@@ -256,5 +256,13 @@
"status": "not_observed",
"evidence": "No additional blocker class observed in the short DEV-only retest"
}
]
],
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current MVP gate report; M3 DEV-LIVE remains blocked until the patch-panel trusted loop is proven."
}
}
+9 -1
View File
@@ -1553,5 +1553,13 @@
"nextTask": "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."
}
],
"notes": "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed."
"notes": "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed.",
"reportLifecycle": {
"version": "v1",
"state": "active",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": null,
"summary": "Current read-only DEV preflight report; source, local, and dry-run checks are not DEV-LIVE acceptance."
}
}
@@ -0,0 +1,37 @@
{
"$schema": "https://hwlab.pikastech.local/schemas/dev-m3-hardware-loop-report.schema.json",
"$id": "https://hwlab.pikastech.local/reports/dev-gate/historical/dev-m3-hardware-loop-legacy-6667.json",
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#38",
"taskId": "dev-m3-hardware-loop",
"commitId": "historical",
"acceptanceLevel": "dev_m3_hardware_loop",
"devOnly": true,
"prodDisabled": true,
"reportLifecycle": {
"version": "v1",
"state": "historical",
"activeEndpoint": "http://74.48.78.17:16667",
"activeBrowserEndpoint": "http://74.48.78.17:16666",
"deprecatedEndpoint": "http://74.48.78.17:6667",
"summary": "Historical deprecated endpoint snapshot retained only for evidence provenance; not active DEV-LIVE evidence."
},
"status": "blocked",
"generatedAt": "2026-05-22T00:00:00.000Z",
"runtimeTarget": {
"environment": "dev",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"requiredBoxSimulators": 2,
"requiredGatewaySimulators": 2,
"requiredPatchPanels": 1
},
"blockers": [
{
"type": "observability_blocker",
"scope": "deprecated-public-endpoint",
"status": "open",
"summary": "Legacy public :6667 evidence is historical/deprecated and cannot support current M3 DEV-LIVE acceptance."
}
]
}
+2
View File
@@ -9,6 +9,7 @@ import {
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import {
BUILDABLE_IMPLEMENTATION_STATES,
@@ -824,6 +825,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif
acceptanceLevel: "dev_artifact_publish",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current DEV artifact publish report; it is not a DEV-LIVE runtime substitute."),
sourceContract: {
status: fatalBlocked ? "blocked" : "pass",
documents: [
+2
View File
@@ -8,6 +8,7 @@ import { request as httpsRequest } from "node:https";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -566,6 +567,7 @@ function baseReport({ commitId, observedAt }) {
acceptanceLevel: "dev_m3_hardware_loop",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids."),
sourceContract: {
status: "pass",
documents: [
+2
View File
@@ -7,6 +7,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -151,6 +152,7 @@ function buildReport(fixture, evidence, result) {
acceptanceLevel: "dev_m4_agent_loop",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current DEV M4 agent-loop preflight report; it does not substitute for M3 DEV-LIVE acceptance."),
sourceContract,
validationCommands,
localSmoke: {
+2
View File
@@ -10,6 +10,7 @@ import {
DEV_DB_ENV_CONTRACT,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -970,6 +971,7 @@ export async function runDevDeployApply(argv, io = {}) {
acceptanceLevel: "dev_deploy_apply",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current DEV deploy apply report; active public API/health endpoint is 16667 and browser endpoint is 16666."),
status,
generatedAt: new Date().toISOString(),
namespace,
@@ -6,6 +6,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT, summarizeDbContract } from "../../internal/cloud/db-contract.mjs";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -118,6 +119,7 @@ async function createDevGateReport(edgeHealth) {
acceptanceLevel: "dev_edge_health",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current DEV edge-health report; deprecated public 6666/6667 endpoints are not valid active evidence."),
status,
generatedAt: edgeHealth.generatedAt,
namespace,
@@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle, reportIsHistorical, reportLifecycleState } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -33,6 +34,8 @@ const blockerTypes = new Set([
"observability_blocker",
"safety_blocker"
]);
const historicalLifecycleSummary =
"Historical report retained for evidence provenance; it is not active green DEV evidence.";
const reportPaths = Object.freeze({
devPreflight: "reports/dev-gate/dev-preflight-report.json",
@@ -275,6 +278,7 @@ function sourceEntry(entry) {
level: entry.level,
status: entry.status,
category: entry.category,
lifecycleState: entry.lifecycleState ?? "active",
sources: entry.sources,
commands: entry.commands,
summary: entry.summary
@@ -282,18 +286,21 @@ function sourceEntry(entry) {
}
function reportEvidence({ milestone, level, report, status, category, summary, evidence = [], commands = [] }) {
const lifecycleState = reportLifecycleState(report);
const historical = lifecycleState === "historical";
return {
milestone,
issue: report.issue,
taskId: report.taskId ?? report.reportKind,
reportPath: report.path,
commitId: short(report.commitId ?? report.target?.commitId),
level,
status,
lifecycleState,
level: historical ? "BLOCKED" : level,
status: historical && status === "pass" ? "blocked" : status,
category,
commands,
evidence,
summary
summary: historical ? `${summary} ${historicalLifecycleSummary}` : summary
};
}
@@ -485,6 +492,19 @@ function collectM5Evidence(reports) {
function collectBlockers(reports) {
const blockers = [];
for (const report of Object.values(reports)) {
if (reportIsHistorical(report)) {
blockers.push({
id: `${report.taskId ?? report.reportKind ?? "report"}-historical:${report.path}`,
priority: "P2",
type: "observability_blocker",
scope: `${report.taskId ?? report.reportKind ?? "report"}-historical`,
status: "open",
source: report.path,
sourceIssue: report.issue,
summary: `${report.taskId ?? report.reportKind ?? "Report"} is historical/deprecated and must be refreshed before it can support active DEV evidence. ${historicalLifecycleSummary}`,
nextTask: "Regenerate the report as active DEV evidence or keep the historical snapshot in an archival path."
});
}
for (const blocker of report.blockers ?? []) {
if (isStaleLegacyIngressBlocker(blocker, report, reports)) {
continue;
@@ -593,6 +613,7 @@ function groupByLevel(evidence, blockers) {
milestone: item.milestone,
issue: item.issue,
taskId: item.taskId,
lifecycleState: item.lifecycleState,
status: item.status,
category: item.category,
reportPath: item.reportPath,
@@ -780,6 +801,9 @@ export async function buildReport() {
endpoint: DEV_ENDPOINT,
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle(
"Current M5 evidence aggregator report; source, local, and dry-run evidence are not DEV-LIVE acceptance."
),
safety: {
reportOnly: true,
noDeploy: true,
@@ -797,6 +821,7 @@ export async function buildReport() {
path: report.path,
issue: report.issue,
taskId: report.taskId ?? report.reportKind,
lifecycleState: reportLifecycleState(report),
status: primaryStatus(report),
commitId: short(report.commitId ?? report.target?.commitId)
}
+2
View File
@@ -10,6 +10,7 @@ import {
buildDbHealthContract,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
import { probeRegistryCapabilities } from "./registry-capabilities.mjs";
@@ -1095,6 +1096,7 @@ function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIde
mode: "read-only",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current read-only DEV preflight report; source, local, and dry-run checks are not DEV-LIVE acceptance."),
forbiddenActions,
validationCommands: [
"node --check scripts/dev-gate-preflight.mjs",
+95 -10
View File
@@ -5,6 +5,16 @@ import { constants as fsConstants } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
ACTIVE_DEV_BROWSER_ENDPOINT,
ACTIVE_DEV_PUBLIC_ENDPOINT,
REPORT_LIFECYCLE_VERSION,
deprecatedPublicEndpoints,
findDeprecatedPublicEndpoints,
findForbiddenActiveDeprecatedEndpoints,
reportIsHistorical
} from "../internal/dev-report-lifecycle.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const reportsDir = path.join(repoRoot, "reports/dev-gate");
@@ -330,24 +340,101 @@ function assertUnique(values, label) {
assert.equal(new Set(values).size, values.length, `${label} must be unique`);
}
async function readJsonFile(relativePath) {
function assertReportLifecycle(report, raw, label) {
assertObject(report.reportLifecycle, `${label}.reportLifecycle`);
const lifecycle = report.reportLifecycle;
assert.equal(lifecycle.version, REPORT_LIFECYCLE_VERSION, `${label}.reportLifecycle.version`);
assert.ok(["active", "historical"].includes(lifecycle.state), `${label}.reportLifecycle.state`);
assert.equal(lifecycle.activeEndpoint, ACTIVE_DEV_PUBLIC_ENDPOINT, `${label}.reportLifecycle.activeEndpoint`);
assert.equal(lifecycle.activeBrowserEndpoint, ACTIVE_DEV_BROWSER_ENDPOINT, `${label}.reportLifecycle.activeBrowserEndpoint`);
assertString(lifecycle.summary, `${label}.reportLifecycle.summary`);
if (lifecycle.state === "active") {
assert.equal(lifecycle.deprecatedEndpoint, null, `${label}.reportLifecycle.deprecatedEndpoint`);
assert.deepEqual(
findForbiddenActiveDeprecatedEndpoints(report, raw),
[],
`${label} active report must not use deprecated public endpoints as active evidence`
);
return;
}
assert.ok(
deprecatedPublicEndpoints.includes(lifecycle.deprecatedEndpoint),
`${label}.reportLifecycle.deprecatedEndpoint must name a deprecated public endpoint`
);
assert.ok(
lifecycle.summary.toLowerCase().includes("historical") ||
lifecycle.summary.toLowerCase().includes("deprecated"),
`${label}.reportLifecycle.summary must explain the historical/deprecated state`
);
const deprecatedMatches = findDeprecatedPublicEndpoints(raw);
assert.ok(
deprecatedMatches.length === 0 || deprecatedMatches.includes(lifecycle.deprecatedEndpoint),
`${label}.reportLifecycle.deprecatedEndpoint must cover deprecated public endpoint evidence`
);
}
function primaryReportStatus(report) {
return report.status ??
report.gateStatus ??
report.conclusion ??
report.artifactPublish?.status ??
report.summary?.status ??
(Array.isArray(report.blockers) && report.blockers.length > 0 ? "blocked" : "not_run");
}
function validateHistoricalReport(report, label) {
assert.equal(report.devOnly, true, `${label}.devOnly`);
assert.equal(report.prodDisabled, true, `${label}.prodDisabled`);
assertString(report.issue, `${label}.issue`);
assert.ok(allowedIssues.has(report.issue), `${label}.issue must be a known HWLAB DEV gate issue`);
assertString(report.taskId ?? report.reportKind, `${label}.taskId`);
assert.ok(
["blocked", "not_run", "not_applicable", "failed", "degraded"].includes(primaryReportStatus(report)),
`${label} historical report must not present active green status`
);
assert.ok(
Array.isArray(report.blockers) && report.blockers.length >= 1,
`${label}.blockers must explain why the report is historical`
);
assertBlockers(report.blockers, `${label}.blockers`);
}
async function readJsonReport(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
return { report: JSON.parse(raw), raw };
}
async function collectReportFiles() {
if (process.argv.length > 2) {
return process.argv.slice(2).map((entry) => path.relative(repoRoot, path.resolve(process.cwd(), entry)));
}
const entries = await readdir(reportsDir, { withFileTypes: true });
return entries
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
.map((entry) => path.join("reports/dev-gate", entry.name))
.sort();
return collectReportJsonFiles(reportsDir, "reports/dev-gate");
}
async function collectReportJsonFiles(directory, relativeDirectory) {
const entries = await readdir(directory, { withFileTypes: true });
const nested = await Promise.all(entries.map(async (entry) => {
const relativePath = path.join(relativeDirectory, entry.name);
if (entry.isDirectory()) {
return collectReportJsonFiles(path.join(directory, entry.name), relativePath);
}
return entry.isFile() && entry.name.endsWith(".json") ? [relativePath] : [];
}));
return nested.flat().sort();
}
async function validateReport(relativePath) {
const report = await readJsonFile(relativePath);
const { report, raw } = await readJsonReport(relativePath);
const label = relativePath;
assertObject(report, label);
assertReportLifecycle(report, raw, label);
if (reportIsHistorical(report)) {
validateHistoricalReport(report, label);
return;
}
if (report.reportKind === "dev-gate-preflight") {
await validatePreflightReport(relativePath, report);
return;
@@ -357,8 +444,6 @@ async function validateReport(relativePath) {
return;
}
const label = relativePath;
assertObject(report, label);
if (report.issue === requiredDevM3Issue || report.taskId === "dev-m3-hardware-loop") {
await validateDevM3Report(report, label);
return;