import type { ErrorCode } from "./error"; export const LIBRARY_NEGATIVE_CASE_SCHEMA = 1 as const; export interface LibraryNegativeLibraryIR { libraryId: string; dependencyIds: string[]; } export interface LibraryNegativeDataBlockIR { dataBlockId: string; sourceLibraryId: string; } export interface LibraryNegativeCrossReferenceIR { fromLibraryId: string; toLibraryId: string; } export interface LibraryNegativeReloadIR { sourceLibraryId: string; generation: number; } export interface LibraryNegativeInputIR { schemaVersion: typeof LIBRARY_NEGATIVE_CASE_SCHEMA; libraries: LibraryNegativeLibraryIR[]; dataBlocks: LibraryNegativeDataBlockIR[]; crossReferences: LibraryNegativeCrossReferenceIR[]; reloads: LibraryNegativeReloadIR[]; } export interface LibraryNegativeValidationIR { status: "VALID"; } export class LibraryNegativeValidationError extends Error { readonly code: ErrorCode; readonly path?: string; constructor(code: ErrorCode, message: string, path?: string) { super(`${code}: ${message}`); this.name = "LibraryNegativeValidationError"; this.code = code; this.path = path; } } const LIBRARY_ID = /^library:[a-f0-9]{64}$/; const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; const MAX_ENTRIES = 10_000; function record(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); return value as Record; } function exactKeys(value: Record, expected: readonly string[], path: string): void { const actual = Object.keys(value).sort(); const allowed = [...expected].sort(); if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); } function libraryId(value: unknown, path: string): string { if (typeof value !== "string" || !LIBRARY_ID.test(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); return value; } function dataBlockId(value: unknown, path: string): string { if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path); return value; } function integer(value: unknown, path: string): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); return value; } function parseLibrary(value: unknown, path: string): LibraryNegativeLibraryIR { const item = record(value, path); exactKeys(item, ["libraryId", "dependencyIds"], path); if (!Array.isArray(item.dependencyIds) || item.dependencyIds.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path}.dependencyIds exceeds its bound`, path); return { libraryId: libraryId(item.libraryId, `${path}.libraryId`), dependencyIds: item.dependencyIds.map((dependency, index) => libraryId(dependency, `${path}.dependencyIds[${index}]`)) }; } function parseDataBlock(value: unknown, path: string): LibraryNegativeDataBlockIR { const item = record(value, path); exactKeys(item, ["dataBlockId", "sourceLibraryId"], path); return { dataBlockId: dataBlockId(item.dataBlockId, `${path}.dataBlockId`), sourceLibraryId: libraryId(item.sourceLibraryId, `${path}.sourceLibraryId`) }; } function parseCrossReference(value: unknown, path: string): LibraryNegativeCrossReferenceIR { const item = record(value, path); exactKeys(item, ["fromLibraryId", "toLibraryId"], path); return { fromLibraryId: libraryId(item.fromLibraryId, `${path}.fromLibraryId`), toLibraryId: libraryId(item.toLibraryId, `${path}.toLibraryId`) }; } function parseReload(value: unknown, path: string): LibraryNegativeReloadIR { const item = record(value, path); exactKeys(item, ["sourceLibraryId", "generation"], path); return { sourceLibraryId: libraryId(item.sourceLibraryId, `${path}.sourceLibraryId`), generation: integer(item.generation, `${path}.generation`) }; } export function parseLibraryNegativeInput(value: unknown): LibraryNegativeInputIR { const input = record(value, "input"); exactKeys(input, ["schemaVersion", "libraries", "dataBlocks", "crossReferences", "reloads"], "input"); if (input.schemaVersion !== LIBRARY_NEGATIVE_CASE_SCHEMA) throw new LibraryNegativeValidationError("PROTOCOL_MISMATCH", "Unsupported library negative-case schema", "schemaVersion"); const libraries = input.libraries; const dataBlocks = input.dataBlocks; const crossReferences = input.crossReferences; const reloads = input.reloads; if (!Array.isArray(libraries) || libraries.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.libraries exceeds its bound", "input.libraries"); if (!Array.isArray(dataBlocks) || dataBlocks.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.dataBlocks exceeds its bound", "input.dataBlocks"); if (!Array.isArray(crossReferences) || crossReferences.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.crossReferences exceeds its bound", "input.crossReferences"); if (!Array.isArray(reloads) || reloads.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.reloads exceeds its bound", "input.reloads"); return { schemaVersion: LIBRARY_NEGATIVE_CASE_SCHEMA, libraries: libraries.map((item, index) => parseLibrary(item, `libraries[${index}]`)), dataBlocks: dataBlocks.map((item, index) => parseDataBlock(item, `dataBlocks[${index}]`)), crossReferences: crossReferences.map((item, index) => parseCrossReference(item, `crossReferences[${index}]`)), reloads: reloads.map((item, index) => parseReload(item, `reloads[${index}]`)), }; } function assertNoCycle(nodes: Set, edges: Map, label: string): void { const active = new Set(); const complete = new Set(); const visit = (node: string): void => { if (active.has(node)) throw new LibraryNegativeValidationError("LIBRARY_DEPENDENCY_CYCLE", `${label} contains a cycle at ${node}`, label); if (complete.has(node)) return; active.add(node); for (const dependency of edges.get(node) ?? []) { if (!nodes.has(dependency)) throw new LibraryNegativeValidationError("ASSET_MANIFEST_INVALID", `${label} references a missing library ${dependency}`, label); visit(dependency); } active.delete(node); complete.add(node); }; for (const node of nodes) visit(node); } export function validateLibraryNegativeInput(value: unknown): LibraryNegativeValidationIR { const input = parseLibraryNegativeInput(value); const libraries = new Set(input.libraries.map((item) => item.libraryId)); if (libraries.size !== input.libraries.length) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "duplicate library ID", "libraries"); const libraryEdges = new Map(input.libraries.map((item) => [item.libraryId, item.dependencyIds])); assertNoCycle(libraries, libraryEdges, "library dependencies"); const crossEdges = new Map(); for (const item of input.crossReferences) { if (!libraries.has(item.fromLibraryId) || !libraries.has(item.toLibraryId)) throw new LibraryNegativeValidationError("ASSET_MANIFEST_INVALID", "cross-library reference names a missing library", "crossReferences"); crossEdges.set(item.fromLibraryId, [...(crossEdges.get(item.fromLibraryId) ?? []), item.toLibraryId]); } assertNoCycle(libraries, crossEdges, "cross-library references"); const dataBlocks = new Set(); for (const item of input.dataBlocks) { if (!libraries.has(item.sourceLibraryId)) throw new LibraryNegativeValidationError("ASSET_SOURCE_HASH_MISMATCH", "data-block source library is missing", "dataBlocks"); if (dataBlocks.has(item.dataBlockId)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `duplicate data-block ID ${item.dataBlockId}`, "dataBlocks"); dataBlocks.add(item.dataBlockId); } const reloads = new Set(); for (const item of input.reloads) { if (!libraries.has(item.sourceLibraryId)) throw new LibraryNegativeValidationError("ASSET_SOURCE_HASH_MISMATCH", "reload source library is missing", "reloads"); const identity = `${item.sourceLibraryId}:${item.generation}`; if (reloads.has(identity)) throw new LibraryNegativeValidationError("REVISION_CONFLICT", `duplicate reload ${identity}`, "reloads"); reloads.add(identity); } return { status: "VALID" }; }