import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates"; import type { ErrorCode } from "./error"; export const RELEASE_GATE_SCHEMA = 4 as const; export type ParityStatus = "COMPLETE" | "BLOCKED"; export type ReleaseClass = "LOCAL_EXACT" | "LOCAL_BOUNDED" | "SERVER" | "EXCLUDED"; export type ReleaseStatus = "READY" | "BLOCKED"; export interface ParityFamilyEvidenceIR { id: string; name: string; parityStatus: ParityStatus; releaseClass: ReleaseClass; releaseStatus: ReleaseStatus; roadmapStatus: "completed" | "in_progress" | "planned"; completedSlices: string[]; blockedSlices: string[]; excludedSlices: string[]; v1RequiredSlices: string[]; v1ExcludedSlices: string[]; acceptance: string[]; dependencies: string[]; } export interface ReleaseEvidenceIR { browser: { chromium: boolean }; runtime: { offline: boolean; workerRestart: boolean; opfsRecovery: boolean }; performance: { geometry1M: boolean; geometry10M: boolean; texture4K: boolean; texture8K: boolean; longMedia: boolean; simulationCache: boolean }; faults: { oom: boolean; deviceLoss: boolean; networkInterrupt: boolean; malformedBlend: boolean; zipBomb: boolean }; provenance: { license: boolean; sbom: boolean; sourceOffer: boolean; deterministicPackage: boolean }; records: ReleaseEvidenceRecordIR[]; } export interface ReleaseEvidenceRecordIR { id: string; fields: string[]; command: string; exitCode: 0; durationMs: number; output: string; artifactSha256: string[] } export interface ReleaseManifestIR { schemaVersion: typeof RELEASE_GATE_SCHEMA; source: string; sourceSha256: string; generatedAt: string; families: ParityFamilyEvidenceIR[]; evidence: ReleaseEvidenceIR } export interface ReleaseGateEvaluationIR { status: "READY" | "BLOCKED"; issueCodes: ErrorCode[]; missing: string[] } export class ReleaseGateValidationError extends Error { readonly code: ErrorCode; constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "ReleaseGateValidationError"; this.code = code; } } const PARITY_STATUSES = new Set(["COMPLETE", "BLOCKED"]); const RELEASE_CLASSES = new Set(["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "EXCLUDED"]); const RELEASE_STATUSES = new Set(["READY", "BLOCKED"]); function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function text(value: unknown, name: string, maximum = 256): string { if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`); return value; } function bool(value: unknown, name: string): boolean { if (typeof value !== "boolean") throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} must be boolean`); return value; } function strings(value: unknown, name: string, maximum = 100_000): string[] { if (!Array.isArray(value) || value.length > maximum || value.some((item) => typeof item !== "string" || item.length === 0 || item.length > 256)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`); return [...value] as string[]; } function utcTimestamp(value: unknown, name: string): string { const result = text(value, name, 128); const date = new Date(result); if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(result) || !Number.isFinite(date.getTime()) || date.toISOString() !== result) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} must be a canonical UTC timestamp`); return result; } function parseEvidence(value: unknown): ReleaseEvidenceIR { if (!record(value) || !record(value.browser) || !record(value.runtime) || !record(value.performance) || !record(value.faults) || !record(value.provenance)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "Release evidence groups are missing"); const group = (name: string, keys: readonly string[]): Record => { const item = value[name]; if (!record(item)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `evidence.${name} is invalid`); return Object.fromEntries(keys.map((key) => [key, bool(item[key], `evidence.${name}.${key}`)])); }; if (!Array.isArray(value.records) || value.records.length > 1024) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "evidence.records is invalid"); const recordIds = new Set(); const records = value.records.map((item, index): ReleaseEvidenceRecordIR => { const name = `evidence.records[${index}]`; if (!record(item)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`); const id = text(item.id, `${name}.id`); if (recordIds.has(id)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Duplicate evidence record ${id}`); recordIds.add(id); const fields = strings(item.fields, `${name}.fields`, 64); if (new Set(fields).size !== fields.length || fields.some((field) => !/^(browser|runtime|performance|faults|provenance)\.[A-Za-z0-9]+$/.test(field))) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.fields is invalid`); if (item.exitCode !== 0 || typeof item.durationMs !== "number" || !Number.isSafeInteger(item.durationMs) || item.durationMs < 0 || item.durationMs > 86_400_000) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} did not complete successfully`); const artifactSha256 = strings(item.artifactSha256, `${name}.artifactSha256`, 1024); if ((fields.length > 0 && artifactSha256.length === 0) || new Set(artifactSha256).size !== artifactSha256.length || artifactSha256.some((digest) => !/^[a-f0-9]{64}$/.test(digest))) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.artifactSha256 is missing, duplicate or invalid`); return { id, fields, command: text(item.command, `${name}.command`, 2048), exitCode: 0, durationMs: item.durationMs, output: text(item.output, `${name}.output`, 4096), artifactSha256 }; }); const parsed = { browser: group("browser", ["chromium"]) as ReleaseEvidenceIR["browser"], runtime: group("runtime", ["offline", "workerRestart", "opfsRecovery"]) as ReleaseEvidenceIR["runtime"], performance: group("performance", ["geometry1M", "geometry10M", "texture4K", "texture8K", "longMedia", "simulationCache"]) as ReleaseEvidenceIR["performance"], faults: group("faults", ["oom", "deviceLoss", "networkInterrupt", "malformedBlend", "zipBomb"]) as ReleaseEvidenceIR["faults"], provenance: group("provenance", ["license", "sbom", "sourceOffer", "deterministicPackage"]) as ReleaseEvidenceIR["provenance"], records }; const knownFields = new Map(); for (const [groupName, groupValues] of Object.entries(parsed).filter(([name]) => name !== "records") as Array<[string, Record]>) for (const [key, enabled] of Object.entries(groupValues)) knownFields.set(`${groupName}.${key}`, enabled); for (const evidenceRecord of records) for (const field of evidenceRecord.fields) if (knownFields.get(field) !== true) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Evidence record ${evidenceRecord.id} binds unknown or disabled field ${field}`); for (const [groupName, groupValues] of Object.entries(parsed).filter(([name]) => name !== "records") as Array<[string, Record]>) { for (const [key, enabled] of Object.entries(groupValues)) if (enabled && !records.some((item) => item.fields.includes(`${groupName}.${key}`))) throw new ReleaseGateValidationError("RELEASE_EVIDENCE_MISSING", `Enabled evidence ${groupName}.${key} has no successful record`); } return parsed; } function assertDependencies(families: readonly ParityFamilyEvidenceIR[]): void { const byId = new Map(families.map((family) => [family.id, family])); const active = new Set(); const complete = new Set(); const visit = (id: string): void => { if (active.has(id)) throw new ReleaseGateValidationError("RELEASE_DEPENDENCY_CYCLE", `Release dependency cycle includes ${id}`); if (complete.has(id)) return; const family = byId.get(id); if (!family) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Missing release dependency ${id}`); active.add(id); family.dependencies.forEach(visit); active.delete(id); complete.add(id); }; families.forEach((family) => visit(family.id)); } export function parseReleaseManifest(value: unknown): ReleaseManifestIR { if (!record(value) || value.schemaVersion !== RELEASE_GATE_SCHEMA || !Array.isArray(value.families)) throw new ReleaseGateValidationError("PROTOCOL_MISMATCH", "Unsupported release manifest schema"); const ids = new Set(); const families = value.families.map((item, index): ParityFamilyEvidenceIR => { const name = `families[${index}]`; if (!record(item) || !PARITY_STATUSES.has(item.parityStatus as ParityStatus) || !RELEASE_CLASSES.has(item.releaseClass as ReleaseClass) || !RELEASE_STATUSES.has(item.releaseStatus as ReleaseStatus) || !["completed", "in_progress", "planned"].includes(item.roadmapStatus as string)) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} is invalid`); } const id = text(item.id, `${name}.id`); if (ids.has(id)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `Duplicate family ${id}`); ids.add(id); const completedSlices = strings(item.completedSlices, `${name}.completedSlices`); const blockedSlices = strings(item.blockedSlices, `${name}.blockedSlices`); const excludedSlices = strings(item.excludedSlices ?? [], `${name}.excludedSlices`); const v1RequiredSlices = strings(item.v1RequiredSlices, `${name}.v1RequiredSlices`); const v1ExcludedSlices = strings(item.v1ExcludedSlices, `${name}.v1ExcludedSlices`); const declared = [...completedSlices, ...blockedSlices, ...excludedSlices]; if (new Set(declared).size !== declared.length) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} declares a slice in more than one parity state`); if (v1RequiredSlices.length === 0 || new Set(v1RequiredSlices).size !== v1RequiredSlices.length || new Set(v1ExcludedSlices).size !== v1ExcludedSlices.length) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} has invalid V1 slices`); } const declaredStates = new Map(declared.map((slice) => [slice, completedSlices.includes(slice) ? "completed" : blockedSlices.includes(slice) ? "blocked" : "excluded"])); if (v1RequiredSlices.some((slice) => !declaredStates.has(slice)) || v1ExcludedSlices.some((slice) => !declaredStates.has(slice))) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} references an undeclared V1 slice`); } if (v1RequiredSlices.some((slice) => v1ExcludedSlices.includes(slice)) || v1ExcludedSlices.some((slice) => declaredStates.get(slice) === "completed")) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} overlaps required/excluded V1 slices or excludes a completed slice`); } const blockedRequired = v1RequiredSlices.filter((slice) => declaredStates.get(slice) !== "completed"); if ((item.releaseStatus === "READY" && blockedRequired.length > 0) || (item.releaseStatus === "BLOCKED" && blockedRequired.length === 0)) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.releaseStatus disagrees with its V1 required slices`); } if (item.parityStatus === "COMPLETE" && (blockedSlices.length > 0 || excludedSlices.length > 0)) { throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name}.parityStatus cannot be COMPLETE with unresolved slices`); } const acceptance = strings(item.acceptance, `${name}.acceptance`); if (item.releaseStatus === "READY" && acceptance.length === 0) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} has no V1 acceptance command`); return { id, name: text(item.name, `${name}.name`), parityStatus: item.parityStatus as ParityStatus, releaseClass: item.releaseClass as ReleaseClass, releaseStatus: item.releaseStatus as ReleaseStatus, roadmapStatus: item.roadmapStatus as ParityFamilyEvidenceIR["roadmapStatus"], completedSlices, blockedSlices, excludedSlices, v1RequiredSlices, v1ExcludedSlices, acceptance, dependencies: strings(item.dependencies, `${name}.dependencies`), }; }); assertDependencies(families); const sourceSha256 = text(value.sourceSha256, "sourceSha256", 64); if (!/^[a-f0-9]{64}$/.test(sourceSha256)) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", "sourceSha256 is invalid"); return { schemaVersion: RELEASE_GATE_SCHEMA, source: text(value.source, "source", 2048), sourceSha256, generatedAt: utcTimestamp(value.generatedAt, "generatedAt"), families, evidence: parseEvidence(value.evidence) }; } export function evaluateReleaseManifest(value: unknown): ReleaseGateEvaluationIR { const manifest = parseReleaseManifest(value); const missing: string[] = []; const issueCodes: ErrorCode[] = []; const add = (path: string, code: ErrorCode): void => { missing.push(path); if (!issueCodes.includes(code)) issueCodes.push(code); }; manifest.families.forEach((family) => { if (family.releaseStatus !== "BLOCKED") return; for (const slice of family.v1RequiredSlices.filter((item) => !family.completedSlices.includes(item))) { add(`family.${family.id}.${slice}`, "RELEASE_EVIDENCE_MISSING"); } }); (Object.entries(manifest.evidence.browser) as [string, boolean][]).forEach(([key, ok]) => { if (!ok) add(`browser.${key}`, "RELEASE_TEST_CHANNEL_MISSING"); }); (Object.entries(manifest.evidence.runtime) as [string, boolean][]).forEach(([key, ok]) => { if (!ok) add(`runtime.${key}`, "RELEASE_EVIDENCE_MISSING"); }); (Object.entries(manifest.evidence.performance) as [string, boolean][]).forEach(([key, ok]) => { if (!ok) add(`performance.${key}`, "RELEASE_PERFORMANCE_MISSING"); }); (Object.entries(manifest.evidence.faults) as [string, boolean][]).forEach(([key, ok]) => { if (!ok) add(`faults.${key}`, "RELEASE_FAULT_EVIDENCE_MISSING"); }); (Object.entries(manifest.evidence.provenance) as [string, boolean][]).forEach(([key, ok]) => { if (!ok) add(`provenance.${key}`, "RELEASE_PROVENANCE_MISSING"); }); return { status: missing.length === 0 ? "READY" : "BLOCKED", issueCodes, missing: missing.sort() }; } export function gateRelease(value: unknown): CapabilityGateResult { const evaluation = evaluateReleaseManifest(value); if (evaluation.status === "READY") return readyGate("N-026", "RELEASE"); return blockedGate("N-026", "RELEASE", evaluation.issueCodes.map((code) => capabilityIssue(code, `Release evidence is missing: ${evaluation.missing.filter((path) => path.includes(code === "RELEASE_TEST_CHANNEL_MISSING" ? "browser" : code === "RELEASE_PERFORMANCE_MISSING" ? "performance" : code === "RELEASE_FAULT_EVIDENCE_MISSING" ? "faults" : code === "RELEASE_PROVENANCE_MISSING" ? "provenance" : "family" )).join(", ") || code}`))); } export function serializeReleaseManifest(value: unknown): string { const manifest = parseReleaseManifest(value); return JSON.stringify({ ...manifest, families: [...manifest.families].sort((a, b) => a.id.localeCompare(b.id)), generatedAt: manifest.generatedAt }, null, 2); }