feat: add module contracts and discovery
CI / verify (push) Has been cancelled
CI / infrastructure-smoke (push) Has been cancelled

This commit is contained in:
ekklesia
2026-07-12 22:14:01 -05:00
parent d9489384c1
commit 57552a21a4
43 changed files with 2277 additions and 21 deletions
+199
View File
@@ -0,0 +1,199 @@
import { z } from "zod";
import {
capabilityIdSchema,
logicalBucketIdSchema,
moduleIdSchema,
packageRelativePathSchema,
permissionIdSchema,
} from "./identifiers.js";
import { semanticVersionRangeSchema, semanticVersionSchema } from "./versions.js";
const moduleVersionMapSchema = z.record(moduleIdSchema, semanticVersionRangeSchema);
const moduleDependenciesSchema = z
.object({
required: moduleVersionMapSchema.default(() => ({})),
optional: moduleVersionMapSchema.default(() => ({})),
})
.strict()
.default(() => ({ required: {}, optional: {} }));
const capabilityDependenciesSchema = z
.object({
required: z.array(capabilityIdSchema).default(() => []),
optional: z.array(capabilityIdSchema).default(() => []),
})
.strict()
.default(() => ({ required: [], optional: [] }));
const dependenciesSchema = z
.object({
modules: moduleDependenciesSchema,
capabilities: capabilityDependenciesSchema,
})
.strict()
.default(() => ({
modules: { required: {}, optional: {} },
capabilities: { required: [], optional: [] },
}));
export const moduleBucketDeclarationSchema = z
.object({
id: logicalBucketIdSchema,
description: z.string().trim().min(1).max(240).optional(),
})
.strict();
const moduleStorageSchema = z
.object({
buckets: z.array(moduleBucketDeclarationSchema).default(() => []),
})
.strict()
.default(() => ({ buckets: [] }));
const entrypointsSchema = z
.object({
server: packageRelativePathSchema,
schema: packageRelativePathSchema.optional(),
migrations: packageRelativePathSchema.optional(),
jobs: packageRelativePathSchema.optional(),
})
.strict();
function reportDuplicates(
values: readonly string[],
path: readonly (string | number)[],
context: z.RefinementCtx,
): void {
const seen = new Set<string>();
values.forEach((value, index) => {
if (seen.has(value)) {
context.addIssue({
code: "custom",
message: `${value} is declared more than once.`,
path: [...path, index],
});
}
seen.add(value);
});
}
export const moduleManifestSchema = z
.object({
manifestVersion: z.literal(1),
id: moduleIdSchema,
name: z.string().trim().min(1).max(120),
description: z.string().trim().min(1).max(500).optional(),
version: semanticVersionSchema,
compatibility: z
.object({
platform: semanticVersionRangeSchema,
moduleSdk: semanticVersionRangeSchema,
})
.strict(),
dependencies: dependenciesSchema,
capabilities: z.array(capabilityIdSchema).default(() => []),
permissions: z.array(permissionIdSchema).default(() => []),
storage: moduleStorageSchema,
entrypoints: entrypointsSchema,
})
.strict()
.superRefine((manifest, context) => {
reportDuplicates(manifest.capabilities, ["capabilities"], context);
reportDuplicates(manifest.permissions, ["permissions"], context);
reportDuplicates(
manifest.dependencies.capabilities.required,
["dependencies", "capabilities", "required"],
context,
);
reportDuplicates(
manifest.dependencies.capabilities.optional,
["dependencies", "capabilities", "optional"],
context,
);
reportDuplicates(
manifest.storage.buckets.map((bucket) => bucket.id),
["storage", "buckets"],
context,
);
manifest.capabilities.forEach((capability, index) => {
if (!capability.startsWith(`${manifest.id}.`)) {
context.addIssue({
code: "custom",
message: `Capability identifiers owned by this module must begin with ${manifest.id}.`,
path: ["capabilities", index],
});
}
});
manifest.permissions.forEach((permission, index) => {
if (!permission.startsWith(`${manifest.id}.`)) {
context.addIssue({
code: "custom",
message: `Permission identifiers owned by this module must begin with ${manifest.id}.`,
path: ["permissions", index],
});
}
});
for (const dependencyKind of ["required", "optional"] as const) {
if (Object.hasOwn(manifest.dependencies.modules[dependencyKind], manifest.id)) {
context.addIssue({
code: "custom",
message: "A module cannot depend on itself.",
path: ["dependencies", "modules", dependencyKind, manifest.id],
});
}
}
for (const dependencyId of Object.keys(manifest.dependencies.modules.required)) {
if (Object.hasOwn(manifest.dependencies.modules.optional, dependencyId)) {
context.addIssue({
code: "custom",
message: `${dependencyId} cannot be both a required and optional module dependency.`,
path: ["dependencies", "modules", "optional", dependencyId],
});
}
}
const ownedCapabilities = new Set(manifest.capabilities);
const requiredCapabilities = new Set(manifest.dependencies.capabilities.required);
manifest.dependencies.capabilities.required.forEach((capability, index) => {
if (ownedCapabilities.has(capability)) {
context.addIssue({
code: "custom",
message: "A module does not need to depend on a capability it owns.",
path: ["dependencies", "capabilities", "required", index],
});
}
});
manifest.dependencies.capabilities.optional.forEach((capability, index) => {
if (ownedCapabilities.has(capability)) {
context.addIssue({
code: "custom",
message: "A module does not need to depend on a capability it owns.",
path: ["dependencies", "capabilities", "optional", index],
});
}
if (requiredCapabilities.has(capability)) {
context.addIssue({
code: "custom",
message: `${capability} cannot be both a required and optional capability dependency.`,
path: ["dependencies", "capabilities", "optional", index],
});
}
});
});
export function parseModuleManifest(input: unknown): ModuleManifest {
return moduleManifestSchema.parse(input);
}
export function safeParseModuleManifest(input: unknown) {
return moduleManifestSchema.safeParse(input);
}
export type ModuleManifestInput = z.input<typeof moduleManifestSchema>;
export type ModuleManifest = z.output<typeof moduleManifestSchema>;
export type ModuleBucketDeclaration = z.output<typeof moduleBucketDeclarationSchema>;