import type { ErrorCode } from "./error"; export const LIBRARY_LINKED_MISSING_SCHEMA = 1 as const; export const LINKED_MISSING_OPERATION = "MARK_MISSING" as const; export interface MissingLibraryPlaceholderIR { kind: "MISSING_LIBRARY"; sourceLibraryId: string; dataBlockIds: string[]; } export interface LinkedLibraryReferenceIR { sourceLibraryId: string; sourceLocator: string; sourceSha256: string; sourceGeneration: number; sourceRevision: number; dataBlockIds: string[]; status: "AVAILABLE" | "MISSING"; placeholder: MissingLibraryPlaceholderIR | null; } export interface LinkedMissingStateIR { schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA; references: LinkedLibraryReferenceIR[]; } export interface LinkedMissingRequestIR { schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA; operation: typeof LINKED_MISSING_OPERATION; sourceLibraryId: string; sourceLocator: string; sourceSha256: string; expectedGeneration: number; expectedRevision: number; } export interface LinkedMissingDecisionIR { status: "MARKED" | "STALE"; code: ErrorCode | null; sourceLibraryId: string; state: LinkedMissingStateIR; } export class LinkedMissingValidationError extends Error { readonly code: ErrorCode; readonly path?: string; constructor(code: ErrorCode, message: string, path?: string) { super(`${code}: ${message}`); this.name = "LinkedMissingValidationError"; this.code = code; this.path = path; } } const LIBRARY_ID = /^library:[a-f0-9]{64}$/; const SHA256 = /^[a-f0-9]{64}$/; const SOURCE_LOCATOR = /^[^\u0000\r\n]{1,4096}$/; const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; const MAX_REFERENCES = 10_000; const MAX_DATA_BLOCKS = 256; function record(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new LinkedMissingValidationError("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 LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); } } function integer(value: unknown, path: string): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); } return value; } function libraryId(value: unknown, path: string): string { if (typeof value !== "string" || !LIBRARY_ID.test(value)) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); } return value; } function digest(value: unknown, path: string): string { if (typeof value !== "string" || !SHA256.test(value)) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a lowercase SHA-256 digest`, path); } return value; } function locator(value: unknown, path: string): string { if (typeof value !== "string" || !SOURCE_LOCATOR.test(value)) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside the source locator budget`, path); } return value; } function dataBlockIds(value: unknown, path: string): string[] { if (!Array.isArray(value) || value.length === 0 || value.length > MAX_DATA_BLOCKS || value.some((item) => typeof item !== "string" || !DATA_BLOCK_ID.test(item))) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside its bounded ID list`, path); } const result = [...value] as string[]; if (new Set(result).size !== result.length) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} contains duplicate IDs`, path); } return result; } function parsePlaceholder(value: unknown, path: string): MissingLibraryPlaceholderIR | null { if (value === null) return null; const placeholder = record(value, path); exactKeys(placeholder, ["kind", "sourceLibraryId", "dataBlockIds"], path); if (placeholder.kind !== "MISSING_LIBRARY") { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.kind is invalid`, `${path}.kind`); } return { kind: "MISSING_LIBRARY", sourceLibraryId: libraryId(placeholder.sourceLibraryId, `${path}.sourceLibraryId`), dataBlockIds: dataBlockIds(placeholder.dataBlockIds, `${path}.dataBlockIds`), }; } export function parseLinkedLibraryReference(value: unknown, path = "reference"): LinkedLibraryReferenceIR { const reference = record(value, path); exactKeys(reference, ["sourceLibraryId", "sourceLocator", "sourceSha256", "sourceGeneration", "sourceRevision", "dataBlockIds", "status", "placeholder"], path); const sourceLibraryId = libraryId(reference.sourceLibraryId, `${path}.sourceLibraryId`); const sourceLocator = locator(reference.sourceLocator, `${path}.sourceLocator`); const sourceSha256 = digest(reference.sourceSha256, `${path}.sourceSha256`); const sourceGeneration = integer(reference.sourceGeneration, `${path}.sourceGeneration`); const sourceRevision = integer(reference.sourceRevision, `${path}.sourceRevision`); const ids = dataBlockIds(reference.dataBlockIds, `${path}.dataBlockIds`); if (reference.status !== "AVAILABLE" && reference.status !== "MISSING") { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.status is invalid`, `${path}.status`); } const placeholder = parsePlaceholder(reference.placeholder, `${path}.placeholder`); if (reference.status === "AVAILABLE" && placeholder !== null) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} available reference cannot have a placeholder`, path); } if (reference.status === "MISSING" && (placeholder === null || placeholder.sourceLibraryId !== sourceLibraryId || placeholder.dataBlockIds.length !== ids.length || placeholder.dataBlockIds.some((id, index) => id !== ids[index]))) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} missing placeholder must preserve the source IDs`, path); } return { sourceLibraryId, sourceLocator, sourceSha256, sourceGeneration, sourceRevision, dataBlockIds: ids, status: reference.status, placeholder }; } export function parseLinkedMissingState(value: unknown): LinkedMissingStateIR { const state = record(value, "state"); exactKeys(state, ["schemaVersion", "references"], "state"); if (state.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) { throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library state schema", "schemaVersion"); } if (!Array.isArray(state.references) || state.references.length > MAX_REFERENCES) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state.references exceeds its bounded range", "references"); } const references = state.references.map((item, index) => parseLinkedLibraryReference(item, `state.references[${index}]`)); const identities = references.map((item) => `${item.sourceLibraryId}:${item.sourceGeneration}`); if (new Set(identities).size !== identities.length) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state contains duplicate source generations", "references"); } return { schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, references }; } export function parseLinkedMissingRequest(value: unknown): LinkedMissingRequestIR { const request = record(value, "request"); exactKeys(request, ["schemaVersion", "operation", "sourceLibraryId", "sourceLocator", "sourceSha256", "expectedGeneration", "expectedRevision"], "request"); if (request.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) { throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library request schema", "schemaVersion"); } if (request.operation !== LINKED_MISSING_OPERATION) { throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "missing-library operation is invalid", "operation"); } return { schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, operation: LINKED_MISSING_OPERATION, sourceLibraryId: libraryId(request.sourceLibraryId, "request.sourceLibraryId"), sourceLocator: locator(request.sourceLocator, "request.sourceLocator"), sourceSha256: digest(request.sourceSha256, "request.sourceSha256"), expectedGeneration: integer(request.expectedGeneration, "request.expectedGeneration"), expectedRevision: integer(request.expectedRevision, "request.expectedRevision"), }; } function cloneState(state: LinkedMissingStateIR): LinkedMissingStateIR { return { schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, references: state.references.map((reference) => ({ ...reference, dataBlockIds: [...reference.dataBlockIds], placeholder: reference.placeholder === null ? null : { ...reference.placeholder, dataBlockIds: [...reference.placeholder.dataBlockIds], }, })), }; } export function markLinkedLibraryMissing(stateValue: unknown, requestValue: unknown): LinkedMissingDecisionIR { const state = parseLinkedMissingState(stateValue); const request = parseLinkedMissingRequest(requestValue); const index = state.references.findIndex((reference) => reference.sourceLibraryId === request.sourceLibraryId && reference.sourceGeneration === request.expectedGeneration, ); if (index === -1 || state.references[index].sourceRevision !== request.expectedRevision) { return { status: "STALE", code: "REVISION_CONFLICT", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) }; } const current = state.references[index]; if (current.sourceLocator !== request.sourceLocator || current.sourceSha256 !== request.sourceSha256) { return { status: "STALE", code: "ASSET_SOURCE_HASH_MISMATCH", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) }; } const missing: LinkedLibraryReferenceIR = { ...current, status: "MISSING", placeholder: { kind: "MISSING_LIBRARY", sourceLibraryId: current.sourceLibraryId, dataBlockIds: [...current.dataBlockIds] }, dataBlockIds: [...current.dataBlockIds], }; const references = state.references.map((reference, itemIndex) => itemIndex === index ? missing : reference); return { status: "MARKED", code: null, sourceLibraryId: request.sourceLibraryId, state: { schemaVersion: 1, references: references.map((reference) => ({ ...reference, dataBlockIds: [...reference.dataBlockIds], placeholder: reference.placeholder === null ? null : { ...reference.placeholder, dataBlockIds: [...reference.placeholder.dataBlockIds] } })) } }; }