Complete V1 performance and OOM release gates
This commit is contained in:
@@ -1,9 +1,25 @@
|
||||
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
|
||||
import type { ErrorCode } from "./error";
|
||||
|
||||
export const RELEASE_GATE_SCHEMA = 3 as const;
|
||||
export type ParityStatus = "LOCAL_EXACT" | "LOCAL_BOUNDED" | "SERVER" | "BLOCKED";
|
||||
export interface ParityFamilyEvidenceIR { id: string; name: string; status: ParityStatus; roadmapStatus: "completed" | "in_progress" | "planned"; completedSlices: string[]; blockedSlices: string[]; excludedSlices: string[]; acceptance: string[]; dependencies: string[] }
|
||||
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 };
|
||||
@@ -21,7 +37,9 @@ export class ReleaseGateValidationError extends Error {
|
||||
constructor(code: ErrorCode, message: string) { super(`${code}: ${message}`); this.name = "ReleaseGateValidationError"; this.code = code; }
|
||||
}
|
||||
|
||||
const STATUSES = new Set<ParityStatus>(["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "BLOCKED"]);
|
||||
const PARITY_STATUSES = new Set<ParityStatus>(["COMPLETE", "BLOCKED"]);
|
||||
const RELEASE_CLASSES = new Set<ReleaseClass>(["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "EXCLUDED"]);
|
||||
const RELEASE_STATUSES = new Set<ReleaseStatus>(["READY", "BLOCKED"]);
|
||||
function record(value: unknown): value is Record<string, unknown> { 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; }
|
||||
@@ -59,7 +77,59 @@ function assertDependencies(families: readonly ParityFamilyEvidenceIR[]): void {
|
||||
|
||||
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<string>(); const families = value.families.map((item, index): ParityFamilyEvidenceIR => { const name = `families[${index}]`; if (!record(item) || !STATUSES.has(item.status as ParityStatus) || !["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 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 state`); if (item.status !== "BLOCKED" && completedSlices.length === 0) throw new ReleaseGateValidationError("RELEASE_MANIFEST_INVALID", `${name} must declare completed slices`); return { id, name: text(item.name, `${name}.name`), status: item.status as ParityStatus, roadmapStatus: item.roadmapStatus as ParityFamilyEvidenceIR["roadmapStatus"], completedSlices, blockedSlices, excludedSlices, acceptance: strings(item.acceptance, `${name}.acceptance`), dependencies: strings(item.dependencies, `${name}.dependencies`) }; });
|
||||
const ids = new Set<string>();
|
||||
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) };
|
||||
@@ -68,7 +138,12 @@ export function parseReleaseManifest(value: unknown): ReleaseManifestIR {
|
||||
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.status === "BLOCKED") add(`family.${family.id}`, "RELEASE_EVIDENCE_MISSING"); if (family.status !== "BLOCKED" && family.acceptance.length === 0) add(`family.${family.id}.acceptance`, "RELEASE_EVIDENCE_MISSING"); });
|
||||
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"); });
|
||||
|
||||
Reference in New Issue
Block a user