83 lines
4.0 KiB
TypeScript
83 lines
4.0 KiB
TypeScript
import type { InterfaceRevision, ModuleInterfaceMember, PackageModule, WorkspaceRevision } from "./types.js";
|
|||
|
|
import type { CapabilityValidationIssue } from "./validation.js";
|
||
|
|
|
||
|
|
/** Public nominal contract lookup; does not resolve an executable artifact or grant access. */
|
||
|
|
export function moduleContract(
|
||
|
|
interfaces: readonly InterfaceRevision[],
|
||
|
|
module: PackageModule,
|
||
|
|
): ModuleInterfaceMember | undefined {
|
||
|
|
const iface = interfaces.find((entry) => entry.revisionId === module.interfaceRevisionId);
|
||
|
|
if (!iface || iface.template) return undefined;
|
||
|
|
const member = iface.members.find((entry) => entry.id === module.memberId);
|
||
|
|
return member?.kind === "module" ? member : undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateModules(workspace: WorkspaceRevision): CapabilityValidationIssue[] {
|
||
|
|
const issues: CapabilityValidationIssue[] = [];
|
||
|
|
const fail = (path: string, message: string) => issues.push({ code: "invalid-module-contract", path, message });
|
||
|
|
for (const iface of workspace.interfaceImports) {
|
||
|
|
for (const member of iface.template?.members ?? iface.members) {
|
||
|
|
if (member.kind !== "module") continue;
|
||
|
|
const path = `${iface.revisionId}.${member.id}`;
|
||
|
|
if (!/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+\/[1-9][0-9]*$/.test(member.contract))
|
||
|
|
fail(path, "Module contracts require a namespaced, versioned identifier");
|
||
|
|
if (member.operations.length) fail(path, "A module member cannot declare callable operations");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const pkg of workspace.packageImports) {
|
||
|
|
const ids = new Set<string>(pkg.exports.map((entry) => entry.id));
|
||
|
|
const names = new Set([
|
||
|
|
...pkg.exports.map((entry) => entry.displayName),
|
||
|
|
...(pkg.genericExports ?? []).map((entry) => entry.displayName),
|
||
|
|
...(pkg.queries ?? []).map((entry) => entry.displayName),
|
||
|
|
...(pkg.queryTemplates ?? []).map((entry) => entry.declaration.displayName),
|
||
|
|
]);
|
||
|
|
for (const module of pkg.modules ?? []) {
|
||
|
|
const path = `${pkg.revisionId}.${module.id}`;
|
||
|
|
if (!module.id.trim() || !module.displayName.trim() || ids.has(module.id) || names.has(module.displayName))
|
||
|
|
fail(path, "Duplicate or empty module identity or package member name");
|
||
|
|
ids.add(module.id);
|
||
|
|
names.add(module.displayName);
|
||
|
|
if (!moduleContract(workspace.interfaceImports, module))
|
||
|
|
fail(path, "Module export must implement a closed imported module member");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (const conformance of workspace.conformances) {
|
||
|
|
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
|
||
|
|
if (!iface) continue;
|
||
|
|
const bound = new Set<string>();
|
||
|
|
for (const binding of conformance.moduleBindings ?? []) {
|
||
|
|
const path = `${conformance.id ?? conformance.atomId}.${binding.memberId}`;
|
||
|
|
const member = iface.members.find((entry) => entry.id === binding.memberId);
|
||
|
|
const pkg = workspace.packageImports.find((entry) => entry.revisionId === binding.packageRevisionId);
|
||
|
|
const module = pkg?.modules?.find((entry) => entry.id === binding.moduleId);
|
||
|
|
if (bound.has(binding.memberId)) fail(path, "Module member is bound more than once");
|
||
|
|
bound.add(binding.memberId);
|
||
|
|
if (
|
||
|
|
member?.kind !== "module" ||
|
||
|
|
!module ||
|
||
|
|
module.interfaceRevisionId !== iface.revisionId ||
|
||
|
|
module.memberId !== member.id
|
||
|
|
) {
|
||
|
|
fail(path, "Module binding must implement this exact nominal interface member");
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const subject = member.subject;
|
||
|
|
if (
|
||
|
|
subject &&
|
||
|
|
!(subject.kind === "atom"
|
||
|
|
? subject.atomId === conformance.atomId
|
||
|
|
: workspace.conformances.some(
|
||
|
|
(entry) =>
|
||
|
|
entry.atomId === conformance.atomId && entry.interfaceRevisionId === subject.interfaceRevisionId,
|
||
|
|
))
|
||
|
|
)
|
||
|
|
fail(path, "Implementing atom does not satisfy the module subject contract");
|
||
|
|
}
|
||
|
|
for (const member of iface.members)
|
||
|
|
if (member.kind === "module" && !bound.has(member.id))
|
||
|
|
fail(`${conformance.id ?? conformance.atomId}.${member.id}`, "Missing module member binding");
|
||
|
|
}
|
||
|
|
return issues;
|
||
|
|
}
|