Rebuild local Blender WASM environment and recovery evidence
This commit is contained in:
154
web/protocol/capability-contract.ts
Normal file
154
web/protocol/capability-contract.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import type { ErrorCode } from "./error";
|
||||
|
||||
export const CAPABILITY_CONTRACT_SCHEMA_VERSION = 1 as const;
|
||||
export const CAPABILITY_CONTRACT_ID = "workflow:freeze_capability_error_budget_contract" as const;
|
||||
export const CAPABILITY_CONTRACT_ERROR_CODES = [
|
||||
"PROTOCOL_MISMATCH",
|
||||
"REVISION_CONFLICT",
|
||||
"OPEN_CANCELLED",
|
||||
"CAPABILITY_CONTRACT_INVALID",
|
||||
"CAPABILITY_CONTRACT_BUDGET_EXCEEDED",
|
||||
"CAPABILITY_CONTRACT_UNSUPPORTED",
|
||||
"CAPABILITY_CONTRACT_SCHEMA_HASH_MISMATCH",
|
||||
"CAPABILITY_CONTRACT_MIGRATION_BLOCKED",
|
||||
] as const satisfies readonly ErrorCode[];
|
||||
|
||||
export type CapabilityContractErrorCode = typeof CAPABILITY_CONTRACT_ERROR_CODES[number];
|
||||
|
||||
export interface CapabilityContractError {
|
||||
code: CapabilityContractErrorCode;
|
||||
severity: "info" | "warning" | "error" | "fatal";
|
||||
recoverable: boolean;
|
||||
}
|
||||
|
||||
export interface CapabilityContractIR {
|
||||
schemaVersion: typeof CAPABILITY_CONTRACT_SCHEMA_VERSION;
|
||||
contractId: typeof CAPABILITY_CONTRACT_ID;
|
||||
capability: { level: "L0" | "L1" | "L2"; status: "BOUNDED" | "READY" | "BLOCKED" };
|
||||
revision: { base: number; current: number };
|
||||
budget: { maxErrors: number; maxMessageBytes: number; maxUnknownFields: number };
|
||||
errors: CapabilityContractError[];
|
||||
cancellation: { code: "OPEN_CANCELLED"; preservesRevision: boolean };
|
||||
migration: { currentSchemaVersion: 1; supportedSchemaVersions: number[]; preserveUnknown: boolean; strategy: "RETAIN_UNKNOWN" };
|
||||
schemaHash: string;
|
||||
extensions?: Record<string, unknown>;
|
||||
unknownFields?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CapabilityContractValidationError extends Error {
|
||||
readonly code: CapabilityContractErrorCode;
|
||||
|
||||
constructor(code: CapabilityContractErrorCode, message: string) {
|
||||
super(`${code}: ${message}`);
|
||||
this.name = "CapabilityContractValidationError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function integer(value: unknown, name: string, minimum: number, maximum: number): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `${name} is outside its bounded range`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function text(value: unknown, name: string, maximum: number): string {
|
||||
if (typeof value !== "string" || value.length === 0 || value.length > maximum) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `${name} is invalid`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value: unknown, name: string): string {
|
||||
const result = text(value, name, 64);
|
||||
if (!/^[a-f0-9]{64}$/u.test(result)) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_SCHEMA_HASH_MISMATCH", `${name} is not a SHA-256 digest`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseCapabilityContract(value: unknown): CapabilityContractIR {
|
||||
if (!record(value)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "contract must be an object");
|
||||
const input = value;
|
||||
if (input.schemaVersion !== CAPABILITY_CONTRACT_SCHEMA_VERSION) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_UNSUPPORTED", "unsupported schema version");
|
||||
}
|
||||
if (input.contractId !== CAPABILITY_CONTRACT_ID) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "contractId does not identify this workflow");
|
||||
}
|
||||
if (!record(input.capability) || !["L0", "L1", "L2"].includes(input.capability.level as string) || !["BOUNDED", "READY", "BLOCKED"].includes(input.capability.status as string)) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "capability is invalid");
|
||||
}
|
||||
if (!record(input.revision)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "revision is invalid");
|
||||
const revision = {
|
||||
base: integer(input.revision.base, "revision.base", 0, Number.MAX_SAFE_INTEGER),
|
||||
current: integer(input.revision.current, "revision.current", 0, Number.MAX_SAFE_INTEGER),
|
||||
};
|
||||
if (revision.current < revision.base) throw new CapabilityContractValidationError("REVISION_CONFLICT", "current revision precedes base revision");
|
||||
if (!record(input.budget)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "budget is invalid");
|
||||
const budget = {
|
||||
maxErrors: integer(input.budget.maxErrors, "budget.maxErrors", 1, 128),
|
||||
maxMessageBytes: integer(input.budget.maxMessageBytes, "budget.maxMessageBytes", 32, 4096),
|
||||
maxUnknownFields: integer(input.budget.maxUnknownFields, "budget.maxUnknownFields", 0, 128),
|
||||
};
|
||||
if (!Array.isArray(input.errors) || input.errors.length === 0 || input.errors.length > budget.maxErrors) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "errors exceed the declared budget");
|
||||
}
|
||||
const errorCodes = new Set<string>();
|
||||
const errors = input.errors.map((entry, index) => {
|
||||
if (!record(entry) || !CAPABILITY_CONTRACT_ERROR_CODES.includes(entry.code as CapabilityContractErrorCode) || errorCodes.has(entry.code as string)) {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `errors[${index}] is invalid or duplicated`);
|
||||
}
|
||||
errorCodes.add(entry.code as string);
|
||||
if (!["info", "warning", "error", "fatal"].includes(entry.severity as string) || typeof entry.recoverable !== "boolean") {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", `errors[${index}] has invalid severity or recovery policy`);
|
||||
}
|
||||
return { code: entry.code as CapabilityContractErrorCode, severity: entry.severity as CapabilityContractError["severity"], recoverable: entry.recoverable };
|
||||
});
|
||||
if (!record(input.cancellation) || input.cancellation.code !== "OPEN_CANCELLED" || typeof input.cancellation.preservesRevision !== "boolean") {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "cancellation policy is invalid");
|
||||
}
|
||||
if (!record(input.migration) || input.migration.currentSchemaVersion !== 1 || !Array.isArray(input.migration.supportedSchemaVersions) || !input.migration.supportedSchemaVersions.includes(1) || input.migration.preserveUnknown !== true || input.migration.strategy !== "RETAIN_UNKNOWN") {
|
||||
throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_MIGRATION_BLOCKED", "migration policy must retain unknown data");
|
||||
}
|
||||
const schemaHash = digest(input.schemaHash, "schemaHash");
|
||||
if (input.extensions !== undefined && !record(input.extensions)) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_INVALID", "extensions must be an object");
|
||||
const extensions = input.extensions as Record<string, unknown> | undefined;
|
||||
const knownFields = new Set(["schemaVersion", "contractId", "capability", "revision", "budget", "errors", "cancellation", "migration", "schemaHash", "extensions"]);
|
||||
const unknownFields = Object.fromEntries(Object.entries(input).filter(([key]) => !knownFields.has(key)));
|
||||
if (Object.keys(unknownFields).length > budget.maxUnknownFields) throw new CapabilityContractValidationError("CAPABILITY_CONTRACT_BUDGET_EXCEEDED", "unknown fields exceed the migration budget");
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
contractId: CAPABILITY_CONTRACT_ID,
|
||||
capability: { level: input.capability.level as "L0" | "L1" | "L2", status: input.capability.status as "BOUNDED" | "READY" | "BLOCKED" },
|
||||
revision,
|
||||
budget,
|
||||
errors,
|
||||
cancellation: { code: "OPEN_CANCELLED", preservesRevision: input.cancellation.preservesRevision },
|
||||
migration: { currentSchemaVersion: 1, supportedSchemaVersions: input.migration.supportedSchemaVersions.map((value) => integer(value, "migration.supportedSchemaVersions[]", 1, 1)), preserveUnknown: true, strategy: "RETAIN_UNKNOWN" },
|
||||
schemaHash,
|
||||
...(extensions === undefined ? {} : { extensions }),
|
||||
...(Object.keys(unknownFields).length === 0 ? {} : { unknownFields }),
|
||||
};
|
||||
}
|
||||
|
||||
function stable(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(stable);
|
||||
if (!record(value)) return value;
|
||||
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
||||
}
|
||||
|
||||
/** Canonical bytes covered by schemaHash; unknown extension data is retained. */
|
||||
export function capabilityContractSchemaInput(value: CapabilityContractIR): string {
|
||||
const { schemaHash: _schemaHash, unknownFields: _unknownFields, ...payload } = value;
|
||||
return JSON.stringify(stable(payload));
|
||||
}
|
||||
|
||||
/** Version-1 migration is intentionally lossless for unknown root fields/extensions. */
|
||||
export function migrateCapabilityContract(value: unknown): CapabilityContractIR {
|
||||
return parseCapabilityContract(value);
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import { CorrectiveCommandRegistry } from "./corrective-command-registry";
|
||||
import { CaseSurface, SurfaceReceipt } from "./corrective-case-runner";
|
||||
|
||||
export type CorrectiveFamily = "ASSET_CATALOG" | "MODIFIER" | "MESH";
|
||||
export const ATOMIC_CORRECTIVE_FAMILIES = [
|
||||
"CORE_DNA_RNA", "CORE_MESH", "CORE_OBJECT", "CORE_SCENE_GRAPH", "MATERIAL_RENDER",
|
||||
"MEDIA_IO", "MESH_EDITING", "MODIFIER_SYSTEM", "NODES_GRAPH", "OBJECT_OPERATIONS",
|
||||
"OTHER_OPERATOR", "RIG_ANIMATION", "SCULPT_PAINT", "SIMULATION", "UI_EDITOR",
|
||||
] as const;
|
||||
export const CORRECTIVE_FAMILIES = [...ATOMIC_CORRECTIVE_FAMILIES, "ASSET_CATALOG", "MODIFIER", "MESH"] as const;
|
||||
export type CorrectiveFamily = typeof CORRECTIVE_FAMILIES[number];
|
||||
|
||||
export interface CorrectiveFamilyCase {
|
||||
id: string;
|
||||
@@ -23,21 +29,17 @@ export async function runCorrectiveFamilyCases(cases: readonly CorrectiveFamilyC
|
||||
for (const testCase of cases) {
|
||||
if (!testCase || !CASE_ID.test(testCase.id) || ids.has(testCase.id)) throw new Error(`family case id is invalid or duplicated: ${testCase?.id ?? "unknown"}`);
|
||||
ids.add(testCase.id);
|
||||
if (!["ASSET_CATALOG", "MODIFIER", "MESH"].includes(testCase.family)) throw new Error(`unsupported corrective family: ${testCase.family}`);
|
||||
if (!CORRECTIVE_FAMILIES.includes(testCase.family)) throw new Error(`unsupported corrective family: ${testCase.family}`);
|
||||
if (!(testCase.registry instanceof CorrectiveCommandRegistry)) throw new Error(`family case registry is invalid: ${testCase.id}`);
|
||||
if (!Number.isSafeInteger(testCase.mainRevision) || testCase.mainRevision < 0) throw new Error(`family case revision is invalid: ${testCase.id}`);
|
||||
}
|
||||
return Promise.all(cases.map(async (testCase) => ({ ...testCase, receipt: await testCase.registry.dispatch(testCase.surface, { command: testCase.command, mainRevision: testCase.mainRevision }) })));
|
||||
}
|
||||
|
||||
export function summarizeCorrectiveFamilyResults(results: readonly CorrectiveFamilyResult[]): Record<CorrectiveFamily, { total: number; finished: number; unchanged: number }> {
|
||||
const summary: Record<CorrectiveFamily, { total: number; finished: number; unchanged: number }> = {
|
||||
ASSET_CATALOG: { total: 0, finished: 0, unchanged: 0 },
|
||||
MODIFIER: { total: 0, finished: 0, unchanged: 0 },
|
||||
MESH: { total: 0, finished: 0, unchanged: 0 },
|
||||
};
|
||||
export function summarizeCorrectiveFamilyResults(results: readonly CorrectiveFamilyResult[]): Partial<Record<CorrectiveFamily, { total: number; finished: number; unchanged: number }>> {
|
||||
const summary: Partial<Record<CorrectiveFamily, { total: number; finished: number; unchanged: number }>> = {};
|
||||
for (const result of results) {
|
||||
const bucket = summary[result.family]; bucket.total += 1;
|
||||
const bucket = (summary[result.family] ??= { total: 0, finished: 0, unchanged: 0 }); bucket.total += 1;
|
||||
if (result.receipt.result.status === "FINISHED") bucket.finished += 1;
|
||||
if (result.receipt.result.mainMutation === "UNCHANGED") bucket.unchanged += 1;
|
||||
}
|
||||
|
||||
40
web/protocol/corrective-generic-family-command.ts
Normal file
40
web/protocol/corrective-generic-family-command.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CorrectiveCommandRegistry } from "./corrective-command-registry";
|
||||
import { CorrectiveCaseRunnerError, MainMutationCommand } from "./corrective-case-runner";
|
||||
import { CORRECTIVE_FAMILIES, CorrectiveFamily } from "./corrective-family-runner";
|
||||
import { InMemoryMainStore, runAtomicMutation } from "./corrective-main-transaction";
|
||||
|
||||
export interface GenericFamilyMain {
|
||||
family: CorrectiveFamily;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const VALUE = /^[A-Za-z0-9._:-]{1,128}$/u;
|
||||
|
||||
function payload(command: MainMutationCommand): { family: CorrectiveFamily; value: string } {
|
||||
const input = command.payload;
|
||||
if (typeof input.family !== "string" || !CORRECTIVE_FAMILIES.includes(input.family as CorrectiveFamily) || typeof input.value !== "string" || !VALUE.test(input.value) || Object.keys(input).some((key) => !["family", "value"].includes(key))) {
|
||||
throw new CorrectiveCaseRunnerError("COMMAND_MALFORMED", "family payload must contain only a bounded family and value");
|
||||
}
|
||||
return { family: input.family as CorrectiveFamily, value: input.value };
|
||||
}
|
||||
|
||||
export function createGenericFamilyMain(family: CorrectiveFamily, value = "before", revision = 1, dirty = false): InMemoryMainStore<GenericFamilyMain> {
|
||||
return new InMemoryMainStore({ family, value }, revision, dirty, (state) => ({ ...state }));
|
||||
}
|
||||
|
||||
export function createGenericFamilyRegistry(main: InMemoryMainStore<GenericFamilyMain>): CorrectiveCommandRegistry {
|
||||
return new CorrectiveCommandRegistry().register({
|
||||
commandType: "familySetState",
|
||||
targetKinds: ["DATABLOCK", "SCENE"],
|
||||
authorizations: ["USER_EDIT", "SYSTEM"],
|
||||
execute: async (command) => {
|
||||
const change = payload(command);
|
||||
return runAtomicMutation(main, command, (draft) => {
|
||||
if (draft.family !== change.family) throw new CorrectiveCaseRunnerError("INVALID_CONTEXT", "family does not match Main");
|
||||
const previous = draft.value;
|
||||
draft.value = change.value;
|
||||
return { delta: { family: change.family, previous, value: change.value }, persistence: { dirty: true } };
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -576,6 +576,126 @@ export interface SceneSnapshotIR {
|
||||
editorWorkflowStatus?: "AVAILABLE" | "BLOCKED";
|
||||
scriptSources?: ScriptSourceInventoryIR;
|
||||
scriptSourceStatus?: "AVAILABLE" | "BLOCKED";
|
||||
renderRoutingStatus?: "AVAILABLE" | "BLOCKED";
|
||||
renderRouting?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:render_capability_routing";
|
||||
source: { sha256: string; label?: string };
|
||||
settings: Record<string, unknown> & { sha256: string };
|
||||
build: { version: string; buildSha256: string };
|
||||
routes: Array<{ name: string; target: string; status: "READY" | "BLOCKED"; [key: string]: unknown }>;
|
||||
recovery: { cancellation: "OPEN_CANCELLED"; deviceLoss: "RELEASED"; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
serverJobBindingStatus?: "AVAILABLE" | "BLOCKED";
|
||||
serverJobBinding?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:server_job_result_binding";
|
||||
source: { sha256: string; [key: string]: unknown };
|
||||
settings: { settingsSha256: string; [key: string]: unknown };
|
||||
build: { version: string; buildSha256: string };
|
||||
request: { requestSha256: string; [key: string]: unknown };
|
||||
result: { status: "SUCCEEDED"; resultSha256: string; [key: string]: unknown };
|
||||
lifecycle: { duplicate: "REUSED"; stale: "BLOCKED"; cancel: "CANCELLED"; timeout: "TIMEOUT"; oom: "OOM"; publishedOnFailure: false; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
scriptingPolicyStatus?: "AVAILABLE" | "BLOCKED";
|
||||
scriptingPolicy?: {
|
||||
schemaVersion: 1;
|
||||
policyId: "workflow:scripting_default_deny_audit";
|
||||
defaults: { autorun: false; network: false; addonInstall: false; driverExpressions: false; module: false };
|
||||
scope: { dom: false; hostWorker: false; opfs: false; indexedDB: false; network: false };
|
||||
sandbox: {
|
||||
execution: "DISABLED";
|
||||
cancellation: "SCRIPT_SANDBOX_CANCELLED";
|
||||
dispose: "RELEASED";
|
||||
mainRevisionUnchanged: true;
|
||||
publishedResults: 0;
|
||||
temporaryBytes: 0;
|
||||
};
|
||||
audit: { decision: "DENY"; approvedKey: false; reason: "SCRIPT_SIGNATURE_INVALID" | "SCRIPT_SANDBOX_UNAVAILABLE"; [key: string]: unknown };
|
||||
boundaries: { secretAccess: "BLOCKED"; filesystemEscape: "BLOCKED"; network: "BLOCKED" };
|
||||
budget: { maxSourceBytes: number; maxScripts: number; maxAuditEntries: number };
|
||||
policyHash: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
performanceFaultMatrixStatus?: "AVAILABLE" | "BLOCKED";
|
||||
performanceFaultMatrix?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:performance_fault_recovery_matrix";
|
||||
thresholds: Array<{ id: string; limit: number; status: "PASS"; [key: string]: unknown }>;
|
||||
faults: Record<string, { status: "BLOCKED"; code: string; publishedResults: 0; [key: string]: unknown }>;
|
||||
recovery: { oldRevisionUsable: true; revisionBefore: 1; revisionAfter: 1; resourcesAfter: 0; cache: "QUARANTINED"; workerRestart: "RECOVERED"; [key: string]: unknown };
|
||||
budget: { maxGeometryTriangles: number; maxTextureBytes: number; maxMediaBytes: number; maxCacheBytes: number; [key: string]: unknown };
|
||||
matrixHash: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
releaseCandidateStatus?: "AVAILABLE" | "BLOCKED";
|
||||
releaseCandidate?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:offline_ci_sbom_and_rollback_release";
|
||||
releaseId: string;
|
||||
ci: { quick: { status: "READY"; reportSha256: string }; [key: string]: unknown };
|
||||
gates: { offlineReproducibility: { status: "READY" }; sbom: { status: "READY" }; rollback: { status: "READY" }; [key: string]: unknown };
|
||||
archives: { binary: { bytes: number; sha256: string }; source: { bytes: number; sha256: string }; [key: string]: unknown };
|
||||
sbom: { sha256: string; [key: string]: unknown };
|
||||
rollback: { runbookSha256: string; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
gapInventoryStatus?: "AVAILABLE" | "BLOCKED";
|
||||
gapInventory?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:cluster_full_blender_gap_inventory";
|
||||
runtime: "BLENDER_5_2_INVENTORY";
|
||||
inventorySource: { path: string; sha256: string; entryCount: 6900; [key: string]: unknown };
|
||||
classificationPolicy: { featureParityRequires: string[]; negativeOnlyClasses: string[]; unprovenClass: "METADATA_ONLY"; parityLevel: "L0"; [key: string]: unknown };
|
||||
clusters: Array<{
|
||||
capabilityId: string;
|
||||
family: string;
|
||||
layer: string;
|
||||
entryCount: number;
|
||||
legacyCompleted: number;
|
||||
legacyPending: number;
|
||||
legacyActive: number;
|
||||
targetParityLevel: string;
|
||||
currentParityLevel: "L0";
|
||||
completionClass: "METADATA_ONLY";
|
||||
evidenceStatus: "NOT_RUN";
|
||||
webStatus: "GAP";
|
||||
riskScore: number;
|
||||
riskBand: string;
|
||||
representativeIds: string[];
|
||||
blockedReasons: string[];
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
summary: { inventoryEntries: 6900; clusterCount: 15; unknownEntries: 0; webGapEntries: 6900; currentParityClaims: 0; currentParityLevel: "L0"; [key: string]: unknown };
|
||||
riskRanking: string[];
|
||||
nextTask: "WBV2-P7-002";
|
||||
[key: string]: unknown;
|
||||
};
|
||||
atomicGapQueueStatus?: "AVAILABLE" | "BLOCKED";
|
||||
atomicGapQueue?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:generate_atomic_family_gap_queue";
|
||||
runtime: "BLENDER_5_2_INVENTORY";
|
||||
parent: { task: "WBV2-P7-001"; manifestPath: string; manifestSha256: string; receiptPath: string; receiptSha256: string; [key: string]: unknown };
|
||||
queue: { activeTaskId: string; activeCount: 1; plannedCount: 44; ordering: string; behaviors: string[]; implementationClasses: string[]; [key: string]: unknown };
|
||||
tasks: Array<{ id: string; parityId: string; capabilityId: string; family: string; behavior: string; implementationClass: "LOCAL_BOUNDED"; parentTask: "WBV2-P7-001"; parentReceiptSha256: string; state: "active" | "pending"; ready: boolean; [key: string]: unknown }>;
|
||||
nextTask: "WBV2-P7-003";
|
||||
[key: string]: unknown;
|
||||
};
|
||||
parityReleaseGateStatus?: "AVAILABLE" | "BLOCKED";
|
||||
parityReleaseGate?: {
|
||||
schemaVersion: 1;
|
||||
receiptId: "workflow:full_parity_candidate_release_gate";
|
||||
runtime: "BLENDER_DESKTOP_WASM_MAIN";
|
||||
inputs: Record<string, { path: string; sha256: string; [key: string]: unknown }>;
|
||||
parityClosure: { status: "BLOCKED"; inventoryEntries: 6900; clusterCount: 15; atomicTaskCount: 45; currentParityLevel: "L0"; featureParityClaims: 0; reasonCodes: string[]; [key: string]: unknown };
|
||||
evidenceHealth: { status: "BLOCKED"; historicalEvidencePreserved: true; m12ProvenanceState: "BLOCKED_SOURCE_LOST"; m12Entries: number; reasonCodes: string[]; [key: string]: unknown };
|
||||
recoveryHealth: { status: "READY"; quickCi: "READY"; inventorySaveReopen: "EXACT"; queueSaveReopen: "EXACT"; malformedBlend: "REJECTED_WITHOUT_MAIN_MUTATION"; [key: string]: unknown };
|
||||
releaseDecision: { status: "BLOCKED"; reasonCodes: string[]; nextTask: null; [key: string]: unknown };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
physicsSimulation?: PhysicsSimulationManifestIR;
|
||||
particleSettings?: ParticleSettingsIR[];
|
||||
sounds?: SoundResourceIR[];
|
||||
@@ -1516,6 +1636,209 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
|
||||
parseScriptSourceInventory(value.scriptSources);
|
||||
if (value.scriptSourceStatus !== "AVAILABLE") throw new Error("SceneIR.scriptSourceStatus must be AVAILABLE when scriptSources is present");
|
||||
}
|
||||
if (value.renderRoutingStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.renderRoutingStatus as string)) {
|
||||
throw new Error("SceneIR.renderRoutingStatus is invalid");
|
||||
}
|
||||
if (value.renderRouting !== undefined) {
|
||||
if (!isRecord(value.renderRouting) || value.renderRouting.schemaVersion !== 1 ||
|
||||
value.renderRouting.receiptId !== "workflow:render_capability_routing" ||
|
||||
!isRecord(value.renderRouting.source) || typeof value.renderRouting.source.sha256 !== "string" ||
|
||||
!isRecord(value.renderRouting.settings) || typeof value.renderRouting.settings.sha256 !== "string" ||
|
||||
!isRecord(value.renderRouting.build) || typeof value.renderRouting.build.version !== "string" ||
|
||||
typeof value.renderRouting.build.buildSha256 !== "string" || !Array.isArray(value.renderRouting.routes) ||
|
||||
!isRecord(value.renderRouting.recovery) || value.renderRouting.recovery.cancellation !== "OPEN_CANCELLED" ||
|
||||
value.renderRouting.recovery.deviceLoss !== "RELEASED") {
|
||||
throw new Error("SceneIR.renderRouting is invalid");
|
||||
}
|
||||
if (value.renderRoutingStatus !== "AVAILABLE") throw new Error("SceneIR.renderRoutingStatus must be AVAILABLE when renderRouting is present");
|
||||
}
|
||||
if (value.serverJobBindingStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.serverJobBindingStatus as string)) {
|
||||
throw new Error("SceneIR.serverJobBindingStatus is invalid");
|
||||
}
|
||||
if (value.serverJobBinding !== undefined) {
|
||||
const binding = value.serverJobBinding;
|
||||
if (!isRecord(binding) || binding.schemaVersion !== 1 || binding.receiptId !== "workflow:server_job_result_binding" ||
|
||||
!isRecord(binding.source) || typeof binding.source.sha256 !== "string" ||
|
||||
!isRecord(binding.settings) || typeof binding.settings.settingsSha256 !== "string" ||
|
||||
!isRecord(binding.build) || typeof binding.build.version !== "string" || typeof binding.build.buildSha256 !== "string" ||
|
||||
!isRecord(binding.request) || typeof binding.request.requestSha256 !== "string" ||
|
||||
!isRecord(binding.result) || binding.result.status !== "SUCCEEDED" || typeof binding.result.resultSha256 !== "string" ||
|
||||
!isRecord(binding.lifecycle) || binding.lifecycle.duplicate !== "REUSED" || binding.lifecycle.stale !== "BLOCKED" ||
|
||||
binding.lifecycle.cancel !== "CANCELLED" || binding.lifecycle.timeout !== "TIMEOUT" || binding.lifecycle.oom !== "OOM" ||
|
||||
binding.lifecycle.publishedOnFailure !== false) {
|
||||
throw new Error("SceneIR.serverJobBinding is invalid");
|
||||
}
|
||||
if (value.serverJobBindingStatus !== "AVAILABLE") throw new Error("SceneIR.serverJobBindingStatus must be AVAILABLE when serverJobBinding is present");
|
||||
}
|
||||
if (value.scriptingPolicyStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.scriptingPolicyStatus as string)) {
|
||||
throw new Error("SceneIR.scriptingPolicyStatus is invalid");
|
||||
}
|
||||
if (value.scriptingPolicy !== undefined) {
|
||||
const policy = value.scriptingPolicy;
|
||||
const defaults = isRecord(policy) ? policy.defaults : undefined;
|
||||
const scope = isRecord(policy) ? policy.scope : undefined;
|
||||
const sandbox = isRecord(policy) ? policy.sandbox : undefined;
|
||||
const audit = isRecord(policy) ? policy.audit : undefined;
|
||||
const boundaries = isRecord(policy) ? policy.boundaries : undefined;
|
||||
const budget = isRecord(policy) ? policy.budget : undefined;
|
||||
if (!isRecord(policy) || policy.schemaVersion !== 1 || policy.policyId !== "workflow:scripting_default_deny_audit" ||
|
||||
!isRecord(defaults) || defaults.autorun !== false || defaults.network !== false || defaults.addonInstall !== false ||
|
||||
defaults.driverExpressions !== false || defaults.module !== false || !isRecord(scope) || scope.dom !== false ||
|
||||
scope.hostWorker !== false || scope.opfs !== false || scope.indexedDB !== false || scope.network !== false ||
|
||||
!isRecord(sandbox) || sandbox.execution !== "DISABLED" || sandbox.cancellation !== "SCRIPT_SANDBOX_CANCELLED" ||
|
||||
sandbox.dispose !== "RELEASED" || sandbox.mainRevisionUnchanged !== true || sandbox.publishedResults !== 0 ||
|
||||
sandbox.temporaryBytes !== 0 || !isRecord(audit) || audit.decision !== "DENY" || audit.approvedKey !== false ||
|
||||
!["SCRIPT_SIGNATURE_INVALID", "SCRIPT_SANDBOX_UNAVAILABLE"].includes(audit.reason as string) ||
|
||||
!isRecord(boundaries) || boundaries.secretAccess !== "BLOCKED" || boundaries.filesystemEscape !== "BLOCKED" ||
|
||||
boundaries.network !== "BLOCKED" || !isRecord(budget) || !Number.isSafeInteger(budget.maxSourceBytes) ||
|
||||
(budget.maxSourceBytes as number) < 1 || !Number.isSafeInteger(budget.maxScripts) || (budget.maxScripts as number) < 1 ||
|
||||
!Number.isSafeInteger(budget.maxAuditEntries) || (budget.maxAuditEntries as number) < 1 ||
|
||||
typeof policy.policyHash !== "string" || !/^[a-f0-9]{64}$/u.test(policy.policyHash)) {
|
||||
throw new Error("SceneIR.scriptingPolicy is invalid");
|
||||
}
|
||||
if (value.scriptingPolicyStatus !== "AVAILABLE") throw new Error("SceneIR.scriptingPolicyStatus must be AVAILABLE when scriptingPolicy is present");
|
||||
}
|
||||
if (value.performanceFaultMatrixStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.performanceFaultMatrixStatus as string)) {
|
||||
throw new Error("SceneIR.performanceFaultMatrixStatus is invalid");
|
||||
}
|
||||
if (value.performanceFaultMatrix !== undefined) {
|
||||
const matrix = value.performanceFaultMatrix;
|
||||
const thresholds = isRecord(matrix) ? matrix.thresholds : undefined;
|
||||
const faults = isRecord(matrix) ? matrix.faults : undefined;
|
||||
const recovery = isRecord(matrix) ? matrix.recovery : undefined;
|
||||
const budget = isRecord(matrix) ? matrix.budget : undefined;
|
||||
const thresholdIds = new Set(Array.isArray(thresholds) ? thresholds.map((entry) => isRecord(entry) ? entry.id : undefined) : []);
|
||||
if (!isRecord(matrix) || matrix.schemaVersion !== 1 || matrix.receiptId !== "workflow:performance_fault_recovery_matrix" ||
|
||||
!Array.isArray(thresholds) || thresholds.length < 7 || thresholds.some((entry) => !isRecord(entry) || typeof entry.id !== "string" || entry.status !== "PASS" || !Number.isSafeInteger(entry.limit) || (entry.limit as number) < 1) ||
|
||||
!thresholdIds.has("geometry-100k") || !thresholdIds.has("geometry-1m") || !thresholdIds.has("geometry-10m") || !thresholdIds.has("texture-4k") || !thresholdIds.has("texture-8k") || !thresholdIds.has("long-media") || !thresholdIds.has("simulation-cache") ||
|
||||
!isRecord(faults) || Object.values(faults).some((fault) => !isRecord(fault) || fault.status !== "BLOCKED" || typeof fault.code !== "string" || fault.publishedResults !== 0) ||
|
||||
!isRecord(recovery) || recovery.oldRevisionUsable !== true || recovery.revisionBefore !== 1 || recovery.revisionAfter !== 1 || recovery.resourcesAfter !== 0 || recovery.cache !== "QUARANTINED" || recovery.workerRestart !== "RECOVERED" ||
|
||||
!isRecord(budget) || Object.values(budget).some((limit) => typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1) ||
|
||||
typeof matrix.matrixHash !== "string" || !/^[a-f0-9]{64}$/u.test(matrix.matrixHash)) {
|
||||
throw new Error("SceneIR.performanceFaultMatrix is invalid");
|
||||
}
|
||||
if (value.performanceFaultMatrixStatus !== "AVAILABLE") throw new Error("SceneIR.performanceFaultMatrixStatus must be AVAILABLE when performanceFaultMatrix is present");
|
||||
}
|
||||
if (value.releaseCandidateStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.releaseCandidateStatus as string)) {
|
||||
throw new Error("SceneIR.releaseCandidateStatus is invalid");
|
||||
}
|
||||
if (value.releaseCandidate !== undefined) {
|
||||
const receipt = value.releaseCandidate;
|
||||
const ci = isRecord(receipt) ? receipt.ci : undefined;
|
||||
const gates = isRecord(receipt) ? receipt.gates : undefined;
|
||||
const archives = isRecord(receipt) ? receipt.archives : undefined;
|
||||
const binary = isRecord(archives) ? archives.binary : undefined;
|
||||
const source = isRecord(archives) ? archives.source : undefined;
|
||||
const sbom = isRecord(receipt) ? receipt.sbom : undefined;
|
||||
const rollback = isRecord(receipt) ? receipt.rollback : undefined;
|
||||
if (!isRecord(receipt) || receipt.schemaVersion !== 1 || receipt.receiptId !== "workflow:offline_ci_sbom_and_rollback_release" ||
|
||||
typeof receipt.releaseId !== "string" || !isRecord(ci) || !isRecord(ci.quick) || ci.quick.status !== "READY" ||
|
||||
typeof ci.quick.reportSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(ci.quick.reportSha256) || !isRecord(gates) ||
|
||||
!isRecord(gates.offlineReproducibility) || gates.offlineReproducibility.status !== "READY" || !isRecord(gates.sbom) ||
|
||||
gates.sbom.status !== "READY" || !isRecord(gates.rollback) || gates.rollback.status !== "READY" || !isRecord(binary) ||
|
||||
!Number.isSafeInteger(binary.bytes) || (binary.bytes as number) < 1 || typeof binary.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(binary.sha256) ||
|
||||
!isRecord(source) || !Number.isSafeInteger(source.bytes) || (source.bytes as number) < 1 || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sha256) ||
|
||||
!isRecord(sbom) || typeof sbom.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(sbom.sha256) || !isRecord(rollback) ||
|
||||
typeof rollback.runbookSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(rollback.runbookSha256)) {
|
||||
throw new Error("SceneIR.releaseCandidate is invalid");
|
||||
}
|
||||
if (value.releaseCandidateStatus !== "AVAILABLE") throw new Error("SceneIR.releaseCandidateStatus must be AVAILABLE when releaseCandidate is present");
|
||||
}
|
||||
if (value.gapInventoryStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.gapInventoryStatus as string)) {
|
||||
throw new Error("SceneIR.gapInventoryStatus is invalid");
|
||||
}
|
||||
if (value.gapInventory !== undefined) {
|
||||
const inventory = value.gapInventory;
|
||||
const source = isRecord(inventory) ? inventory.inventorySource : undefined;
|
||||
const policy = isRecord(inventory) ? inventory.classificationPolicy : undefined;
|
||||
const summary = isRecord(inventory) ? inventory.summary : undefined;
|
||||
const clusters = isRecord(inventory) ? inventory.clusters : undefined;
|
||||
if (!isRecord(inventory) || inventory.schemaVersion !== 1 || inventory.receiptId !== "workflow:cluster_full_blender_gap_inventory" ||
|
||||
inventory.runtime !== "BLENDER_5_2_INVENTORY" || !isRecord(source) || typeof source.path !== "string" ||
|
||||
typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(source.sha256) || source.entryCount !== 6900 ||
|
||||
!isRecord(policy) || policy.unprovenClass !== "METADATA_ONLY" || policy.parityLevel !== "L0" ||
|
||||
!Array.isArray(policy.featureParityRequires) || policy.featureParityRequires.length !== 3 ||
|
||||
!Array.isArray(policy.negativeOnlyClasses) || !Array.isArray(clusters) || clusters.length !== 15 ||
|
||||
!isRecord(summary) || summary.inventoryEntries !== 6900 || summary.clusterCount !== 15 || summary.unknownEntries !== 0 ||
|
||||
summary.webGapEntries !== 6900 || summary.currentParityClaims !== 0 || summary.currentParityLevel !== "L0" ||
|
||||
!Array.isArray(inventory.riskRanking) || inventory.riskRanking.length !== 15 || inventory.nextTask !== "WBV2-P7-002") {
|
||||
throw new Error("SceneIR.gapInventory is invalid");
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const cluster of clusters) {
|
||||
if (!isRecord(cluster) || typeof cluster.capabilityId !== "string" || seen.has(cluster.capabilityId) ||
|
||||
!Number.isSafeInteger(cluster.entryCount) || (cluster.entryCount as number) < 1 ||
|
||||
cluster.currentParityLevel !== "L0" || cluster.completionClass !== "METADATA_ONLY" ||
|
||||
cluster.evidenceStatus !== "NOT_RUN" || cluster.webStatus !== "GAP" ||
|
||||
!Number.isSafeInteger(cluster.riskScore) || !Array.isArray(cluster.representativeIds) || cluster.representativeIds.length < 1) {
|
||||
throw new Error("SceneIR.gapInventory cluster is invalid");
|
||||
}
|
||||
seen.add(cluster.capabilityId);
|
||||
}
|
||||
if (seen.size !== inventory.riskRanking.length || inventory.riskRanking.some((id) => typeof id !== "string" || !seen.has(id))) {
|
||||
throw new Error("SceneIR.gapInventory risk ranking is invalid");
|
||||
}
|
||||
if (value.gapInventoryStatus !== "AVAILABLE") throw new Error("SceneIR.gapInventoryStatus must be AVAILABLE when gapInventory is present");
|
||||
}
|
||||
if (value.atomicGapQueueStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.atomicGapQueueStatus as string)) {
|
||||
throw new Error("SceneIR.atomicGapQueueStatus is invalid");
|
||||
}
|
||||
if (value.atomicGapQueue !== undefined) {
|
||||
const queue = value.atomicGapQueue;
|
||||
const parent = isRecord(queue) ? queue.parent : undefined;
|
||||
const details = isRecord(queue) ? queue.queue : undefined;
|
||||
const tasks = isRecord(queue) ? queue.tasks : undefined;
|
||||
if (!isRecord(queue) || queue.schemaVersion !== 1 || queue.receiptId !== "workflow:generate_atomic_family_gap_queue" ||
|
||||
queue.runtime !== "BLENDER_5_2_INVENTORY" || !isRecord(parent) || parent.task !== "WBV2-P7-001" ||
|
||||
typeof parent.manifestPath !== "string" || typeof parent.receiptPath !== "string" ||
|
||||
typeof parent.manifestSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parent.manifestSha256) ||
|
||||
typeof parent.receiptSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parent.receiptSha256) ||
|
||||
!isRecord(details) || details.activeCount !== 1 || details.plannedCount !== 44 ||
|
||||
typeof details.activeTaskId !== "string" || !Array.isArray(details.behaviors) || details.behaviors.length !== 3 ||
|
||||
!Array.isArray(details.implementationClasses) || details.implementationClasses.length !== 1 ||
|
||||
details.implementationClasses[0] !== "LOCAL_BOUNDED" || !Array.isArray(tasks) || tasks.length !== 45 ||
|
||||
queue.nextTask !== "WBV2-P7-003") {
|
||||
throw new Error("SceneIR.atomicGapQueue is invalid");
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
for (const [index, task] of tasks.entries()) {
|
||||
if (!isRecord(task) || typeof task.id !== "string" || ids.has(task.id) ||
|
||||
typeof task.parityId !== "string" || task.parityId !== task.capabilityId ||
|
||||
task.implementationClass !== "LOCAL_BOUNDED" || task.parentTask !== "WBV2-P7-001" ||
|
||||
typeof task.parentReceiptSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(task.parentReceiptSha256) ||
|
||||
task.state !== (index === 0 ? "active" : "pending") || task.ready !== (index === 0)) {
|
||||
throw new Error("SceneIR.atomicGapQueue task is invalid");
|
||||
}
|
||||
ids.add(task.id);
|
||||
}
|
||||
if (details.activeTaskId !== tasks[0].id || value.atomicGapQueueStatus !== "AVAILABLE") {
|
||||
throw new Error("SceneIR.atomicGapQueueStatus must be AVAILABLE for a valid queue");
|
||||
}
|
||||
}
|
||||
if (value.parityReleaseGateStatus !== undefined && !["AVAILABLE", "BLOCKED"].includes(value.parityReleaseGateStatus as string)) {
|
||||
throw new Error("SceneIR.parityReleaseGateStatus is invalid");
|
||||
}
|
||||
if (value.parityReleaseGate !== undefined) {
|
||||
const gate = value.parityReleaseGate;
|
||||
const closure = isRecord(gate) ? gate.parityClosure : undefined;
|
||||
const evidence = isRecord(gate) ? gate.evidenceHealth : undefined;
|
||||
const recovery = isRecord(gate) ? gate.recoveryHealth : undefined;
|
||||
const decision = isRecord(gate) ? gate.releaseDecision : undefined;
|
||||
if (!isRecord(gate) || gate.schemaVersion !== 1 || gate.receiptId !== "workflow:full_parity_candidate_release_gate" ||
|
||||
gate.runtime !== "BLENDER_DESKTOP_WASM_MAIN" || !isRecord(gate.inputs) || Object.keys(gate.inputs).length !== 4 ||
|
||||
Object.values(gate.inputs).some((input) => !isRecord(input) || typeof input.path !== "string" || typeof input.sha256 !== "string" || !/^[a-f0-9]{64}$/u.test(input.sha256)) ||
|
||||
!isRecord(closure) || closure.status !== "BLOCKED" || closure.inventoryEntries !== 6900 || closure.clusterCount !== 15 ||
|
||||
closure.atomicTaskCount !== 45 || closure.currentParityLevel !== "L0" || closure.featureParityClaims !== 0 ||
|
||||
!Array.isArray(closure.reasonCodes) || closure.reasonCodes.length < 1 || !isRecord(evidence) || evidence.status !== "BLOCKED" ||
|
||||
evidence.historicalEvidencePreserved !== true || evidence.m12ProvenanceState !== "BLOCKED_SOURCE_LOST" ||
|
||||
!Number.isSafeInteger(evidence.m12Entries) || (evidence.m12Entries as number) < 1 || !Array.isArray(evidence.reasonCodes) || evidence.reasonCodes.length < 1 ||
|
||||
!isRecord(recovery) || recovery.status !== "READY" || recovery.quickCi !== "READY" || recovery.inventorySaveReopen !== "EXACT" ||
|
||||
recovery.queueSaveReopen !== "EXACT" || recovery.malformedBlend !== "REJECTED_WITHOUT_MAIN_MUTATION" || !isRecord(decision) ||
|
||||
decision.status !== "BLOCKED" || decision.nextTask !== null || !Array.isArray(decision.reasonCodes) || decision.reasonCodes.length < 1) {
|
||||
throw new Error("SceneIR.parityReleaseGate is invalid");
|
||||
}
|
||||
if (value.parityReleaseGateStatus !== "AVAILABLE") throw new Error("SceneIR.parityReleaseGateStatus must be AVAILABLE when parityReleaseGate is present");
|
||||
}
|
||||
if (value.physicsSimulation !== undefined) parsePhysicsSimulationManifest(value.physicsSimulation);
|
||||
if (value.geometryNodeGraphs !== undefined) {
|
||||
const graphs = requireArray(value.geometryNodeGraphs, "geometryNodeGraphs");
|
||||
|
||||
Reference in New Issue
Block a user