/*
Kernel registry assembly and app-target resolution: build an ActualState from a loaded
app/workspace config, list registered commands/pipelines across the workspace + every
discovered app, and resolve which app(s) a CLI invocation targets.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- The registry builds ActualState from a loaded app — resolution is explicit, not ambient.
- RFC-1147: site-module materialization memoized via getOrBuildSiteModuleStates — one materialization per workspaceRoot instead of 18 dynamic imports per call.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- RFC-1193: bound module hashing to src/os roots with constant fallbacks
- RFC-1231: step 1 — rename supportsAllSites to acceptsAllFlag
Mechanical sweep: the field only ever gated --all argv acceptance; fan-out
follows the parsed selector. Guard renamed assertAllSitesAllowed ->
assertAllFlagAccepted, message updated. 417 declaration sites + type
surfaces (KernelCommandMetadata, ForgeCommandMetadata) in one atomic pass.
- RFC-1327: run-twice idempotency — declarations, command.idempotency.validate, rerun tests, reconcile/ship/abort convergence fixes
ADR-0022, RFC-0303, RFC-0960, RFC-1026, RFC-1038, RFC-1097, RFC-1138, RFC-1144, RFC-1147
*/
import path from "node:path";
import process from "node:process";
import { discoverSiteWorkspaces, loadKernelAppConfig } from "../discovery.ts";
import {
getOrBuildRegistry,
getOrBuildSiteModuleStates,
getOrBuildWorkspaceRegistry,
} from "./registry-cache.ts";
import { buildActualState } from "../../runtime/reconciler.ts";
import { validateDeclarations } from "../../runtime/validate-declarations.ts";
import { siteModuleLoaders } from "../module-map.ts";
import type {
DiscoveredSiteWorkspace,
KernelAppConfig,
SiteWorkspacesListResult,
KernelCommandDefinition,
KernelRegisteredCommandInfo,
} from "@warpgogol/werkstatt-shared/kernel";
import type { ActualState, ModuleExport } from "@warpgogol/werkstatt-shared/kernel";
export interface RegistryLoadOptions {
/** RFC-1309: suppress `[registry]` loading/cache preamble lines on stderr. */
quiet?: boolean;
}
export async function buildRegistry(
config: KernelAppConfig,
options?: RegistryLoadOptions,
): Promise {
const exports: ModuleExport[] = [];
if (config.modules) {
for (const mod of config.modules) {
if (!options?.quiet) process.stderr.write(` [registry] loading module ${mod.name} …\n`);
exports.push(mod);
}
} else if (config.moduleLoaders) {
for (const [moduleName, loader] of Object.entries(config.moduleLoaders)) {
if (!options?.quiet) process.stderr.write(` [registry] loading module ${moduleName} …\n`);
const mod = await loader();
assertModuleExport(mod, moduleName);
exports.push(mod);
}
}
const actualState = buildActualState(exports);
// RFC-1138: pass loaded module names — module-scoped GENERATOR_OWNERSHIP_EXEMPT
// entries are only stale-checked when their owning module was loaded.
validateDeclarations(actualState.commands, {
loadedModules: new Set(exports.map((m) => m.name)),
});
return actualState;
}
/**
* Fail fast when a module loader resolves to something that is not a
* ModuleExport — a renamed/removed export surfaces as `undefined` and would
* otherwise crash later with an opaque TypeError (or silently drop the
* module's whole command surface).
*/
function assertModuleExport(
mod: ModuleExport | undefined | null,
loaderKey: string,
): asserts mod is ModuleExport {
if (
mod === undefined ||
mod === null ||
typeof mod !== "object" ||
typeof mod.name !== "string"
) {
throw new Error(
`Module loader \`${loaderKey}\` did not return a module (got ${
mod === null ? "null" : typeof mod
}) — the loader's import specifier or export name is likely stale.`,
);
}
}
export async function buildRegistryForModule(
config: KernelAppConfig,
moduleName: string,
): Promise {
if (config.moduleLoaders) {
const loader = config.moduleLoaders[moduleName];
if (!loader) {
throw new Error(`No module loader registered for module \`${moduleName}\`.`);
}
const mod = await loader();
assertModuleExport(mod, moduleName);
const actualState = buildActualState([mod]);
return actualState;
} else if (config.modules) {
const mod = config.modules.find((m) => m.name === moduleName);
if (!mod) {
throw new Error(`No module named \`${moduleName}\` in config.`);
}
const actualState = buildActualState([mod]);
return actualState;
}
const emptyState = buildActualState([]);
return emptyState;
}
export async function loadAppRuntime(
workspaceRoot: string,
site: DiscoveredSiteWorkspace,
options?: RegistryLoadOptions,
) {
const config = await loadKernelAppConfig(site, options);
const cacheKey = `site:${site.configPath}`;
const registry = await getOrBuildRegistry(cacheKey, config, options);
return { config, registry };
}
export async function listSiteWorkspaces(workspaceRoot: string): Promise {
const sites = await discoverSiteWorkspaces(workspaceRoot);
return {
workspaceRoot,
sites,
};
}
async function resolveSiteByName(
workspaceRoot: string,
siteName?: string,
): Promise {
const sites = await discoverSiteWorkspaces(workspaceRoot);
if (sites.length === 0) {
return undefined;
}
if (siteName) {
return sites.find((site) => site.name === siteName);
}
const currentWorkingDirectory = path.resolve(process.cwd());
const currentSite = sites.find((site) => {
const resolvedSiteDir = path.resolve(site.directory);
return currentWorkingDirectory.toLowerCase().startsWith(resolvedSiteDir.toLowerCase());
});
if (currentSite) {
return currentSite;
}
if (sites.length === 1) {
return sites[0];
}
return undefined;
}
export async function ensureTargetSites(
workspaceRoot: string,
allSites: boolean,
siteName?: string,
): Promise {
const sites = await discoverSiteWorkspaces(workspaceRoot);
if (allSites) {
return sites.filter((site) => site.configPath);
}
const site = await resolveSiteByName(workspaceRoot, siteName);
return site ? [site] : [];
}
function commandInfo(
command: KernelCommandDefinition,
provider: KernelRegisteredCommandInfo["provider"],
moduleName?: string,
): KernelRegisteredCommandInfo {
return {
name: command.name,
description: command.description,
scope: command.scope,
mutatesState: command.mutatesState,
...(command.execOnReadOnly ? { execOnReadOnly: command.execOnReadOnly } : {}),
...(command.mutatingFlags ? { mutatingFlags: command.mutatingFlags } : {}),
requiresNetwork: command.requiresNetwork,
acceptsAllFlag: command.acceptsAllFlag,
timeoutMs: command.timeoutMs,
expectedDurationMs: command.expectedDurationMs,
longRunning: command.longRunning,
...(moduleName ? { module: moduleName } : {}),
...(command.flags ? { flags: command.flags } : {}),
...(command.reads ? { reads: command.reads } : {}),
...(command.writes ? { writes: command.writes } : {}),
...(command.cacheable !== undefined ? { cacheable: command.cacheable } : {}),
...(command.coverage ? { coverage: command.coverage } : {}),
...(command.coverageNote ? { coverageNote: command.coverageNote } : {}),
...(command.idempotency ? { idempotency: command.idempotency } : {}),
...(command.idempotencyNote ? { idempotencyNote: command.idempotencyNote } : {}),
...(command.gate ? { gate: command.gate } : {}),
...(command.validatesOutputs ? { validatesOutputs: command.validatesOutputs } : {}),
...(command.generates ? { generates: command.generates } : {}),
...(command.modulePath
? { modulePath: command.modulePath, moduleBasePath: deriveModuleBasePath(command.modulePath) }
: {}),
...(command.modulePaths ? { modulePaths: command.modulePaths } : {}),
provider,
};
}
/**
* RFC-1028: Derive the module's src/ or os/ directory from a repo-relative
* modulePath. Given "packages/werkstatt-engine/src/kernel/runtime/registry.ts",
* returns "packages/werkstatt-engine/src" — everything up to and including the
* src/ segment. RFC-1193: `/os/` is a module-root segment alongside `/src/`
* ("packages/forge/os/rfc/rfc.module.ts" → "packages/forge/os"); the first
* occurring segment wins. Returns undefined if neither segment is found.
*/
export function deriveModuleBasePath(modulePath: string): string | undefined {
const srcIndex = modulePath.indexOf("/src/");
const osIndex = modulePath.indexOf("/os/");
if (srcIndex === -1 && osIndex === -1) return undefined;
if (srcIndex !== -1 && (osIndex === -1 || srcIndex < osIndex)) {
return modulePath.slice(0, srcIndex + 4); // include "/src"
}
return modulePath.slice(0, osIndex + 3); // include "/os"
}
/**
* RFC-1147/RFC-1148: the static command surface — workspace registry plus the
* canonical site module set (SITE_MODULE_MAP), enumerated without discovering
* or loading any site instance. Pure function of source data: identical output
* with or without a mission workpiece present.
*
* `siteModules` is injectable for unit tests; production callers omit it and
* get siteModuleLoaders() (the same map kernel.config.template.ts delegates to).
*/
export interface StaticCommandSurface {
commands: KernelRegisteredCommandInfo[];
pipelines: Record;
}
// @ai-invariant The static surface MUST be fail-fast: a throwing module
// loader kills the whole call (a partially-enumerated surface silently
// hides commands — route tolerant scans through loadSurfaceDeclarations
// instead). The surface is a union of the workspace registry AND the
// canonical site-module set — post RFC-1147, consumers resolving commands
// by name must check both (RFC-1157).
export async function listStaticCommandSurface(
workspaceRoot: string,
siteModules?: Record Promise>,
): Promise {
const byKey = new Map();
const pipelines: Record = {};
const wsRegistry = await getOrBuildWorkspaceRegistry(workspaceRoot);
if (wsRegistry) {
for (const [commandName, command] of wsRegistry.commands) {
byKey.set(
`workspace:${commandName}`,
commandInfo(command, "workspace", wsRegistry.commandModules?.get(commandName)),
);
}
for (const [name, decl] of wsRegistry.pipelines) {
pipelines[name] = decl.steps.map((step) => step.command);
}
}
// Production path materializes the canonical site set once per workspaceRoot
// (process-lifetime cache); injected siteModules always materialize fresh.
let siteStates: Map;
if (siteModules === undefined) {
siteStates = await getOrBuildSiteModuleStates(workspaceRoot, siteModuleLoaders());
} else {
siteStates = new Map();
for (const [moduleName, loader] of Object.entries(siteModules)) {
siteStates.set(moduleName, buildActualState([await loader()]));
}
}
for (const [moduleName, actualState] of siteStates) {
for (const [commandName, command] of actualState.commands) {
byKey.set(
`site-module:${commandName}`,
commandInfo(
command,
"site-module",
actualState.commandModules?.get(commandName) ?? moduleName,
),
);
}
for (const [name, decl] of actualState.pipelines) {
if (!(name in pipelines)) pipelines[name] = decl.steps.map((step) => step.command);
}
}
const commands = [...byKey.values()].sort((a, b) => {
const byName = a.name.localeCompare(b.name);
if (byName !== 0) return byName;
return a.provider.localeCompare(b.provider);
});
return { commands, pipelines };
}