Unify referenced result validation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-21 12:58:16 +08:00
parent 4870e8a577
commit cb298f2099
8 changed files with 753 additions and 89 deletions
+223
View File
@@ -0,0 +1,223 @@
import { config } from "../config.js";
import { readJsonFile } from "../utils/fileStore.js";
import {
type ResultReferenceKind,
type ResultReferenceRecord,
type ResultReferenceSource,
type RetrievalContext,
RESULT_REFERENCE_KIND,
type ResultReferenceStore,
} from "./store.js";
type ResolveOptions = {
expectedKind?: ResultReferenceKind;
maxItems?: number;
};
type RegisterResultReferenceInput = {
actorKey: string;
clientSessionId: string;
data: unknown;
kind: ResultReferenceKind;
projectId?: string;
projectKey: string;
schemaVersion: number;
sessionId: string;
source: ResultReferenceSource;
traceId: string;
};
export type RenderJunctionPayload = {
node_area_map: Record<string, string>;
area_ids?: string[];
area_colors?: Record<string, string>;
};
export class ResultReferenceResolver {
constructor(private readonly store: ResultReferenceStore) {}
async register(input: RegisterResultReferenceInput) {
const normalizedData = normalizeDataForKind(
input.kind,
input.data,
input.schemaVersion,
);
if (!normalizedData) {
throw new Error(`invalid payload for result ref kind '${input.kind}'`);
}
return this.store.store({
actorKey: input.actorKey,
clientSessionId: input.clientSessionId,
data: normalizedData,
kind: input.kind,
projectId: input.projectId,
projectKey: input.projectKey,
schemaVersion: input.schemaVersion,
sessionId: input.sessionId,
source: input.source,
traceId: input.traceId,
});
}
async registerRenderPayloadFile(
filePath: string,
input: Omit<RegisterResultReferenceInput, "data" | "kind" | "schemaVersion">,
) {
const raw = await readJsonFile<unknown>(filePath);
if (raw === null) {
throw new Error(`render payload file not found: ${filePath}`);
}
const payload = extractRenderJunctionPayload(raw);
if (!payload) {
throw new Error("render payload file does not contain a valid junction render payload");
}
return this.register({
...input,
data: payload,
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
schemaVersion: 1,
});
}
async getAuthorized(resultRef: string, context: RetrievalContext, options: ResolveOptions = {}) {
const record = await this.getResolvedRecord(resultRef, context, options);
if (!record) {
return null;
}
return {
ok: true,
result_ref: record.resultRef,
result_size_bytes: record.sizeBytes,
stored_at: record.createdAt,
data: projectData(record.data, options.maxItems ?? config.RESULT_REF_MAX_RETRIEVAL_ITEMS),
preview: record.preview,
kind: record.kind,
schema_version: record.schemaVersion,
source: record.source,
};
}
async getFullAuthorized(
resultRef: string,
context: RetrievalContext,
options: ResolveOptions = {},
) {
const record = await this.getResolvedRecord(resultRef, context, options);
if (!record) {
return null;
}
return {
ok: true,
result_ref: record.resultRef,
result_size_bytes: record.sizeBytes,
stored_at: record.createdAt,
data: record.data,
preview: record.preview,
kind: record.kind,
schema_version: record.schemaVersion,
source: record.source,
};
}
private async getResolvedRecord(
resultRef: string,
context: RetrievalContext,
options: ResolveOptions,
): Promise<ResultReferenceRecord | null> {
const record = await this.store.getAuthorizedRecord(resultRef, context);
if (!record) {
return null;
}
if (options.expectedKind && record.kind !== options.expectedKind) {
return null;
}
const normalizedData = normalizeDataForKind(
record.kind,
record.data,
record.schemaVersion,
);
if (!normalizedData) {
return null;
}
return {
...record,
data: normalizedData,
};
}
}
export const extractRenderJunctionPayload = (
value: unknown,
): RenderJunctionPayload | null => {
const candidate = unwrapReferencePayload(value);
if (!candidate || !isRecord(candidate.node_area_map)) {
return null;
}
const nodeAreaMap = normalizeStringRecord(candidate.node_area_map);
if (Object.keys(nodeAreaMap).length === 0) {
return null;
}
const areaIds = Array.isArray(candidate.area_ids)
? candidate.area_ids.map((entry) => String(entry).trim()).filter(Boolean)
: undefined;
const areaColors = isRecord(candidate.area_colors)
? normalizeStringRecord(candidate.area_colors)
: undefined;
return {
node_area_map: nodeAreaMap,
...(areaIds && areaIds.length > 0 ? { area_ids: areaIds } : {}),
...(areaColors && Object.keys(areaColors).length > 0
? { area_colors: areaColors }
: {}),
};
};
const normalizeDataForKind = (
kind: ResultReferenceKind,
data: unknown,
schemaVersion: number,
): unknown | null => {
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
return null;
}
if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) {
return extractRenderJunctionPayload(data);
}
return data;
};
const unwrapReferencePayload = (value: unknown): Record<string, unknown> | null => {
if (!isRecord(value)) {
return null;
}
if ("data" in value && value.data !== undefined && value.data !== null) {
return isRecord(value.data) ? value.data : null;
}
return value;
};
const normalizeStringRecord = (value: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(value)
.map(([key, entry]) => [String(key), String(entry ?? "").trim()])
.filter(([, entry]) => entry.length > 0),
);
const projectData = (data: unknown, maxItems: number) => {
if (Array.isArray(data)) {
return data.slice(0, maxItems);
}
if (isRecord(data)) {
return Object.fromEntries(Object.entries(data).slice(0, maxItems));
}
return data;
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);