Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,258 @@
import { satisfies } from "@suaveplan/api-manifest/dep-graph";
import type { CapabilityId, CapabilityMap } from "@suaveplan/api-types";
import type { CapabilityVersion } from "@suaveplan/api-types/capabilities";
import { CAPABILITY_CODES, HostError } from "../error-codes/error-codes.js";
/**
* Scope of a capability — controls how the host vends it into the per-plugin
* vs per-request context. `host` is process-global, `plugin` is one-per-plugin
* (injected on `PluginContext`), `request` is per-request (injected on
* `RequestContext`, e.g. tenant-scoped DB).
*/
export type CapabilityScope = "host" | "plugin" | "request";
/**
* Registration record for one capability adapter. The `provider` is the
* concrete runtime that vends the capability shape (a logger, a db
* client, etc.); the `disposer` is invoked in reverse-registration order
* on registry teardown.
*/
export interface CapabilityRegistration<
Id extends CapabilityId = CapabilityId,
> {
readonly id: Id;
readonly version: CapabilityVersion;
readonly scope: CapabilityScope;
readonly provider: Id extends keyof CapabilityMap
? CapabilityMap[Id]
: unknown;
readonly disposer?: () => Promise<void> | void;
}
/**
* Author-facing helper. Identity at runtime; exists so `defineCapability(...)`
* provides an inference-friendly entry point for adapter authors (mirrors
* `defineCapability` in api-types' design intent, but the runtime lives here
* per design doc §5.2).
*/
export function defineCapability<const Id extends CapabilityId>(
registration: CapabilityRegistration<Id>
): CapabilityRegistration<Id> {
return registration;
}
const SEMVER_FULL = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
const VALID_SCOPES = new Set<CapabilityScope>(["host", "plugin", "request"]);
/**
* Live registry of every capability adapter wired into a host instance.
*
* Responsibilities:
* - register / lookup / unregister of capability records keyed by `id`;
* - input validation (semver shape, scope enum) at register time;
* - duplicate detection;
* - per-plugin version-compat check via `assertSatisfies` — the host's
* lifecycle driver calls this on every install to enforce the
* plugin's `requiredCapabilities` declarations against the actual
* registered adapter versions;
* - reverse-registration-order disposer invocation on `dispose()`.
*
* The registry is single-instance per host process. The host's bootstrap
* builds it before any plugin is loaded; Stage-4 adapter packages call
* `register(...)` from their own boot hooks.
*/
export class CapabilityRegistry {
private readonly entries = new Map<CapabilityId, CapabilityRegistration>();
/** Registration order — used to compute reverse-disposer order. */
private readonly order: CapabilityId[] = [];
private disposed = false;
/**
* Register a capability. Throws:
* - `CAPABILITY.INVALID_VERSION` when `version` is not a parseable semver;
* - `CAPABILITY.INVALID_SCOPE` when `scope` is outside `host|plugin|request`;
* - `CAPABILITY.DUPLICATE_REGISTRATION` when the same id is registered twice.
*/
public register<Id extends CapabilityId>(
registration: CapabilityRegistration<Id>
): void {
if (this.disposed) {
throw new HostError(
CAPABILITY_CODES.NOT_REGISTERED,
"capability-registry: already disposed",
{ context: { id: registration.id } }
);
}
if (!SEMVER_FULL.test(registration.version)) {
throw new HostError(
CAPABILITY_CODES.INVALID_VERSION,
"capability-registry: version is not valid semver",
{
context: {
id: registration.id,
version: registration.version,
},
}
);
}
if (!VALID_SCOPES.has(registration.scope)) {
throw new HostError(
CAPABILITY_CODES.INVALID_SCOPE,
"capability-registry: scope must be host|plugin|request",
{
context: {
id: registration.id,
scope: registration.scope,
},
}
);
}
if (this.entries.has(registration.id)) {
throw new HostError(
CAPABILITY_CODES.DUPLICATE_REGISTRATION,
"capability-registry: id already registered",
{ context: { id: registration.id } }
);
}
this.entries.set(
registration.id,
registration as CapabilityRegistration
);
this.order.push(registration.id);
}
/** Best-effort lookup. Returns `undefined` when the id is not registered. */
public lookup<Id extends CapabilityId>(
id: Id
): CapabilityRegistration<Id> | undefined {
return this.entries.get(id) as CapabilityRegistration<Id> | undefined;
}
/**
* Strict lookup. Throws `CAPABILITY.NOT_REGISTERED` when the id is not
* present — the lifecycle driver uses this to fail-fast on
* `requiredCapabilities` resolution.
*/
public require<Id extends CapabilityId>(
id: Id
): CapabilityRegistration<Id> {
const entry = this.lookup(id);
if (entry === undefined) {
throw new HostError(
CAPABILITY_CODES.NOT_REGISTERED,
"capability-registry: required capability is not registered",
{ context: { id } }
);
}
return entry;
}
/** Enumeration of every registered id, in registration order. */
public ids(): readonly CapabilityId[] {
return [...this.order];
}
/** Count of currently registered capabilities. */
public size(): number {
return this.entries.size;
}
/** `true` iff `id` is present in the registry. */
public has(id: CapabilityId): boolean {
return this.entries.has(id);
}
/**
* Per-plugin compat check. For every `(id, range)` pair, verifies the
* registered adapter's version satisfies the requested semver range.
*
* Throws:
* - `CAPABILITY.NOT_REGISTERED` when any required id is missing;
* - `CAPABILITY.VERSION_MISMATCH` when the version does not satisfy
* the range OR the range string is unparseable.
*
* The host's lifecycle driver invokes this during the `installing`
* transition, after the manifest has been validated.
*/
public assertSatisfies(ranges: Readonly<Record<string, string>>): void {
for (const [id, range] of Object.entries(ranges)) {
const entry = this.entries.get(id as CapabilityId);
if (entry === undefined) {
throw new HostError(
CAPABILITY_CODES.NOT_REGISTERED,
"capability-registry: required capability missing",
{ context: { id, range } }
);
}
let ok: boolean;
try {
ok = satisfies(entry.version, range);
} catch (cause) {
throw new HostError(
CAPABILITY_CODES.VERSION_MISMATCH,
"capability-registry: requested range is unparseable",
{
cause: cause as Error,
context: {
id,
range,
actual: entry.version,
},
}
);
}
if (!ok) {
throw new HostError(
CAPABILITY_CODES.VERSION_MISMATCH,
"capability-registry: version does not satisfy range",
{
context: {
id,
range,
actual: entry.version,
},
}
);
}
}
}
/**
* Dispose every registered adapter in reverse-registration order.
* Disposer throws are collected and re-raised as a single
* `CAPABILITY.DISPOSER_FAILED` error carrying the list of failing
* ids — one bad disposer does not prevent the others from running.
*
* Idempotent: calling `dispose()` twice is a no-op on the second
* call (returns immediately).
*/
public async dispose(): Promise<void> {
if (this.disposed) return;
this.disposed = true;
const failures: { id: CapabilityId; cause: unknown }[] = [];
for (let i = this.order.length - 1; i >= 0; i--) {
const id = this.order[i] as CapabilityId;
const entry = this.entries.get(id);
if (entry?.disposer !== undefined) {
try {
await entry.disposer();
} catch (cause) {
failures.push({ id, cause });
}
}
}
this.entries.clear();
this.order.length = 0;
if (failures.length > 0) {
throw new HostError(
CAPABILITY_CODES.DISPOSER_FAILED,
"capability-registry: one or more disposers threw",
{
context: {
failed: failures.map((f) => f.id),
},
}
);
}
}
}