157 lines
7.4 KiB
TypeScript
157 lines
7.4 KiB
TypeScript
import type { ErrorCode } from "./error";
|
|
|
|
export const LIBRARY_OVERRIDE_WRITER_SCHEMA = 1 as const;
|
|
export const LIBRARY_OVERRIDE_WRITER_OPERATION = "SET_M12_OVERRIDE_VALUE" as const;
|
|
export const LIBRARY_OVERRIDE_PROPERTY_PATH = '["m12_override_value"]' as const;
|
|
|
|
export interface OverrideWriterStateIR {
|
|
schemaVersion: typeof LIBRARY_OVERRIDE_WRITER_SCHEMA;
|
|
revision: number;
|
|
localDataBlockId: string;
|
|
referenceSourceDataBlockId: string;
|
|
hierarchyRootDataBlockId: string;
|
|
owner: "LOCAL_OVERRIDE";
|
|
readOnly: false;
|
|
referenceReadOnly: true;
|
|
propertyPath: typeof LIBRARY_OVERRIDE_PROPERTY_PATH;
|
|
value: number;
|
|
}
|
|
|
|
export interface OverrideWriterRequestIR {
|
|
schemaVersion: typeof LIBRARY_OVERRIDE_WRITER_SCHEMA;
|
|
operation: typeof LIBRARY_OVERRIDE_WRITER_OPERATION;
|
|
baseRevision: number;
|
|
localDataBlockId: string;
|
|
referenceSourceDataBlockId: string;
|
|
hierarchyRootDataBlockId: string;
|
|
owner: "LOCAL_OVERRIDE";
|
|
readOnly: false;
|
|
referenceReadOnly: true;
|
|
propertyPath: typeof LIBRARY_OVERRIDE_PROPERTY_PATH;
|
|
value: number;
|
|
}
|
|
|
|
export interface OverrideWriterDecisionIR {
|
|
status: "APPLIED" | "BLOCKED";
|
|
code: ErrorCode | null;
|
|
state: OverrideWriterStateIR;
|
|
}
|
|
|
|
export class OverrideWriterValidationError extends Error {
|
|
readonly code: ErrorCode;
|
|
readonly path?: string;
|
|
|
|
constructor(code: ErrorCode, message: string, path?: string) {
|
|
super(`${code}: ${message}`);
|
|
this.name = "OverrideWriterValidationError";
|
|
this.code = code;
|
|
this.path = path;
|
|
}
|
|
}
|
|
|
|
const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/;
|
|
const MAX_VALUE = 1_000_000;
|
|
|
|
function record(value: unknown, path: string): Record<string, unknown> {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path);
|
|
}
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function exactKeys(value: Record<string, unknown>, 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 OverrideWriterValidationError("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 OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function dataBlockId(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) {
|
|
throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function valueNumber(value: unknown, path: string): number {
|
|
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > MAX_VALUE) {
|
|
throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} is outside the bounded float range`, path);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function parseOverrideWriterState(value: unknown): OverrideWriterStateIR {
|
|
const state = record(value, "state");
|
|
exactKeys(state, ["schemaVersion", "revision", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly", "propertyPath", "value"], "state");
|
|
if (state.schemaVersion !== LIBRARY_OVERRIDE_WRITER_SCHEMA) throw new OverrideWriterValidationError("PROTOCOL_MISMATCH", "Unsupported override writer state schema", "schemaVersion");
|
|
if (state.owner !== "LOCAL_OVERRIDE" || state.readOnly !== false || state.referenceReadOnly !== true) throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", "state ownership semantics are invalid", "owner");
|
|
if (state.propertyPath !== LIBRARY_OVERRIDE_PROPERTY_PATH) throw new OverrideWriterValidationError("EDITOR_WRITER_UNAVAILABLE", "state property path is not the verified writer path", "propertyPath");
|
|
return {
|
|
schemaVersion: LIBRARY_OVERRIDE_WRITER_SCHEMA,
|
|
revision: integer(state.revision, "revision"),
|
|
localDataBlockId: dataBlockId(state.localDataBlockId, "localDataBlockId"),
|
|
referenceSourceDataBlockId: dataBlockId(state.referenceSourceDataBlockId, "referenceSourceDataBlockId"),
|
|
hierarchyRootDataBlockId: dataBlockId(state.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"),
|
|
owner: "LOCAL_OVERRIDE",
|
|
readOnly: false,
|
|
referenceReadOnly: true,
|
|
propertyPath: LIBRARY_OVERRIDE_PROPERTY_PATH,
|
|
value: valueNumber(state.value, "value"),
|
|
};
|
|
}
|
|
|
|
export function parseOverrideWriterRequest(value: unknown): OverrideWriterRequestIR {
|
|
const request = record(value, "request");
|
|
exactKeys(request, ["schemaVersion", "operation", "baseRevision", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly", "propertyPath", "value"], "request");
|
|
if (request.schemaVersion !== LIBRARY_OVERRIDE_WRITER_SCHEMA) throw new OverrideWriterValidationError("PROTOCOL_MISMATCH", "Unsupported override writer request schema", "schemaVersion");
|
|
if (request.operation !== LIBRARY_OVERRIDE_WRITER_OPERATION) throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", "override writer operation is invalid", "operation");
|
|
if (request.owner !== "LOCAL_OVERRIDE" || request.readOnly !== false || request.referenceReadOnly !== true) throw new OverrideWriterValidationError("LINKED_DATA_MUTATION_BLOCKED", "override writer must retain local override ownership", "owner");
|
|
if (request.propertyPath !== LIBRARY_OVERRIDE_PROPERTY_PATH) throw new OverrideWriterValidationError("EDITOR_WRITER_UNAVAILABLE", "only the verified override property is writable", "propertyPath");
|
|
return {
|
|
schemaVersion: LIBRARY_OVERRIDE_WRITER_SCHEMA,
|
|
operation: LIBRARY_OVERRIDE_WRITER_OPERATION,
|
|
baseRevision: integer(request.baseRevision, "baseRevision"),
|
|
localDataBlockId: dataBlockId(request.localDataBlockId, "localDataBlockId"),
|
|
referenceSourceDataBlockId: dataBlockId(request.referenceSourceDataBlockId, "referenceSourceDataBlockId"),
|
|
hierarchyRootDataBlockId: dataBlockId(request.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"),
|
|
owner: "LOCAL_OVERRIDE",
|
|
readOnly: false,
|
|
referenceReadOnly: true,
|
|
propertyPath: LIBRARY_OVERRIDE_PROPERTY_PATH,
|
|
value: valueNumber(request.value, "value"),
|
|
};
|
|
}
|
|
|
|
function blocked(state: OverrideWriterStateIR, code: ErrorCode): OverrideWriterDecisionIR {
|
|
return { status: "BLOCKED", code, state: { ...state } };
|
|
}
|
|
|
|
export function applyOverrideWriter(stateValue: unknown, requestValue: unknown): OverrideWriterDecisionIR {
|
|
const state = parseOverrideWriterState(stateValue);
|
|
let request: OverrideWriterRequestIR;
|
|
try {
|
|
request = parseOverrideWriterRequest(requestValue);
|
|
}
|
|
catch (error) {
|
|
const code = error instanceof OverrideWriterValidationError ? error.code : "TASK_VALIDATION_FAILED";
|
|
return blocked(state, code);
|
|
}
|
|
if (request.baseRevision !== state.revision) return blocked(state, "REVISION_CONFLICT");
|
|
if (request.localDataBlockId !== state.localDataBlockId || request.referenceSourceDataBlockId !== state.referenceSourceDataBlockId || request.hierarchyRootDataBlockId !== state.hierarchyRootDataBlockId) {
|
|
return blocked(state, "ASSET_SOURCE_HASH_MISMATCH");
|
|
}
|
|
return {
|
|
status: "APPLIED",
|
|
code: null,
|
|
state: { ...state, revision: state.revision + 1, value: request.value },
|
|
};
|
|
}
|