2026-09-21 11:23:26 -07:00
|
|
|
import type { WorkspaceRevision } from "./types.js";
|
|
|
|
|
import type { CapabilityValidationIssue } from "./validation.js";
|
2026-09-22 13:16:23 -07:00
|
|
|
import { moduleContract } from "./modules.js";
|
2026-09-21 11:23:26 -07:00
|
|
|
|
|
|
|
|
/** Presentation metadata has no graph authority and cannot name arbitrary module URLs. */
|
|
|
|
|
export function validatePresentations(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
|
|
|
|
|
const issues: CapabilityValidationIssue[] = [];
|
|
|
|
|
const fail = (path: string, message: string) => issues.push({ code: "invalid-presentation", path, message });
|
|
|
|
|
const interfaces = new Map(workspace.interfaceImports.map((entry) => [entry.revisionId, entry]));
|
|
|
|
|
const packages = new Map(workspace.packageImports.map((entry) => [entry.revisionId, entry]));
|
|
|
|
|
const text = (value: string, path: string) => {
|
|
|
|
|
if (!value.trim()) fail(path, "Presentation identities and names must be nonempty");
|
|
|
|
|
};
|
|
|
|
|
const subject = (id: string, path: string) => {
|
|
|
|
|
const contract = interfaces.get(id as WorkspaceRevision["interfaceImports"][number]["revisionId"]);
|
|
|
|
|
if (!contract || contract.template) fail(path, `Expected an imported closed subject interface: ${id}`);
|
|
|
|
|
};
|
|
|
|
|
for (const [index, pkg] of workspace.packageImports.entries()) {
|
2026-09-21 15:24:05 -07:00
|
|
|
for (const [alias, id] of Object.entries(pkg.clientInterfaces ?? {})) {
|
|
|
|
|
text(alias, `packageImports[${index}].clientInterfaces`);
|
|
|
|
|
subject(id, `packageImports[${index}].clientInterfaces.${alias}`);
|
|
|
|
|
}
|
2026-09-21 11:23:26 -07:00
|
|
|
}
|
|
|
|
|
const ids = new Set<string>(),
|
|
|
|
|
names = new Set<string>();
|
|
|
|
|
for (const [index, presentation] of (workspace.presentations ?? []).entries()) {
|
|
|
|
|
const path = `presentations[${index}]`;
|
|
|
|
|
text(presentation.id, `${path}.id`);
|
|
|
|
|
text(presentation.displayName, `${path}.displayName`);
|
|
|
|
|
if (ids.has(presentation.id) || names.has(presentation.displayName))
|
|
|
|
|
fail(path, "Duplicate presentation identity or name");
|
|
|
|
|
ids.add(presentation.id);
|
|
|
|
|
names.add(presentation.displayName);
|
|
|
|
|
subject(presentation.subject, `${path}.subject`);
|
2026-09-22 13:16:23 -07:00
|
|
|
const module = packages
|
2026-09-21 11:23:26 -07:00
|
|
|
.get(presentation.packageRevisionId)
|
2026-09-22 13:16:23 -07:00
|
|
|
?.modules?.find((entry) => entry.id === presentation.componentId);
|
|
|
|
|
if (module) {
|
|
|
|
|
const contract = moduleContract(workspace.interfaceImports, module);
|
|
|
|
|
if (contract?.contract !== "org.quixos.react.subject-only/1")
|
|
|
|
|
fail(`${path}.componentId`, "Standalone presentation requires a subject-only module contract");
|
|
|
|
|
if (contract?.subject?.kind !== "interface" || contract.subject.interfaceRevisionId !== presentation.subject)
|
|
|
|
|
fail(`${path}.subject`, "Component requires its exact declared subject interface");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
fail(`${path}.componentId`, "Presentation names no installed package component");
|
2026-09-21 11:23:26 -07:00
|
|
|
}
|
|
|
|
|
return issues;
|
|
|
|
|
}
|