/**
 * Manifest-emitter — engines 6.29.0+.
 *
 * Deterministic replacement for the LLM-driven manifest call in the
 * analyse pipeline. The previous `runManifestCall` shipped detected
 * adapters + a stub `importsList` to an LLM and asked it to pattern-
 * match-and-lookup factory names — a job that doesn't need a model.
 * The LLM was unreliable (output had to be normalized for `specVersion`,
 * occasionally hallucinated factory names that don't exist), and one
 * more LLM call adds latency and cost without any decision quality the
 * source signals don't already give us.
 *
 * What `analyse` knows after prepass:
 *   - `meta.adaptersRun` — which detection adapters fired
 *   - `entities[].sourceFramework` — per-entity framework signal
 *   - `views[]` — presence of React/JSX
 *
 * What the realize engine needs:
 *   - A mapping from logical capability names (`api.rest`, `storage.X`,
 *     `ui.framework`, `auth.jwt`) to factory names from the engines
 *     instance-factory registry.
 *
 * The mapping IS a lookup table. This module IS that lookup table.
 *
 * V2 (engines 6.32.2+) — every emitted factory name MUST resolve in
 * the registered InstanceFactoryLibrary. The previous hand-curated
 * mapping referenced names that don't exist (`ExpressAPI`, `NestJSAPI`,
 * `MongoDBDriver`, `ReactRuntime`, `ReactNativeApp`) — realize then
 * left every model with `factory: unknown` and produced no code.
 *
 * Current mapping (registered factory names only — see
 * `libs/instance-factories/<category>/<name>.yaml`):
 *   - api.rest          → FastifyAPI                  (only HTTP factory shipped)
 *   - storage.database  → PostgreSQL15 | MongoDBNativeDriver
 *   - orm.schema/client → PrismaORM                   (when prisma signal present)
 *   - service.controller/business/crud → PrismaServices
 *   - app.frontend      → ReactAppRuntime             (no RN factory exists yet)
 *   - messaging.events  → EventEmitter
 *   - validation.runtime → ZodValidation
 *   - app.entrypoint    → GenericApp
 *   - project.scaffold  → GenericScaffold
 * Edge cases (drizzle, knex, vue, svelte, react-native, etc.) fall
 * through to the closest valid default and the user hand-edits.
 */
import { RegistryClient } from '../registry/client/registry-client.js';
import { VIEW_RENDERER_REGISTRY } from '../realize/view-renderers.js';
/**
 * Emit a deterministic manifest from prepass facts. The output is a
 * complete top-level YAML document (with specVersion) suitable for
 * writing to `manifests/implementation.yaml`.
 */
/**
 * Engines 6.32.2+ — registry of factory names actually present in the
 * shipped instance-factory library (`engines/libs/instance-factories/`).
 * Source: `find libs/instance-factories -name '*.yaml' | xargs grep '^name:'`.
 *
 * Manifest emission MUST pick from this set; emitting a non-existent
 * name leaves every realize-time model with `factory: unknown` and
 * produces no code. The previous emitter referenced ExpressAPI /
 * NestJSAPI / MongoDBDriver / ReactNativeApp / ReactRuntime — none of
 * which exist — and bored-stiff realize would silently emit nothing.
 *
 * Hand-edit consumers can still write any factory name they like in
 * the manifest; this guard only protects auto-emitted manifests from
 * the analyse pipeline.
 */
export const KNOWN_FACTORIES = new Set([
    'CommanderJS', 'DockerKubernetes', 'EventEmitter', 'FastifyAPI',
    'GenericApp', 'GenericScaffold', 'MCPServer', 'MongoDB6',
    'MongoDBNativeDriver', 'PostgresNativeDriver', 'PostgreSQL15',
    'PrismaORM', 'PrismaServices', 'PythonSDK', 'RabbitMQEvents',
    'ReactAppRuntime', 'ReactAppStarter', 'Redis7', 'TypeScriptSDK',
    'VisTimeline', 'TimelineGridCustom', // view.timeline renderer factories
    'KanbanBoard', // view.kanban renderer factory
    'VitestTests', 'VSCodeExtension', 'ZodValidation',
]);
export function emitManifest(input) {
    const adapters = new Set(input.facts.meta?.adaptersRun ?? []);
    const entities = input.facts.entities ?? [];
    const views = input.facts.views ?? [];
    const choices = {};
    const rationale = {};
    // ── api.rest — HTTP framework ─────────────────────────────────────
    // Only FastifyAPI exists in the registry today. Express / Nest signals
    // are recorded in rationale so a human knows the framework the source
    // actually uses, but the auto-emitted choice is FastifyAPI either way.
    if (adapters.has('express-routes')) {
        choices['api.rest'] = 'FastifyAPI';
        rationale['api.rest'] = 'source uses express; auto-mapped to FastifyAPI (only HTTP factory in registry — hand-edit to a real ExpressAPI factory once added)';
    }
    else if (adapters.has('typescript-decorators')) {
        choices['api.rest'] = 'FastifyAPI';
        rationale['api.rest'] = 'source uses NestJS-style decorators; auto-mapped to FastifyAPI (only HTTP factory in registry)';
    }
    else {
        choices['api.rest'] = 'FastifyAPI';
        rationale['api.rest'] = 'no specific framework signal; FastifyAPI is the registry default for HTTP';
    }
    // ── storage.database — relational vs document ─────────────────────
    // Choose between PostgreSQL15 (relational) and MongoDBNativeDriver
    // (document) based on prisma signal. Both are real registered
    // factories. Storage.relational and storage.documents capabilities
    // are NOT what the realize engine asks for — it wants
    // `storage.database` plus an optional `orm.schema` / `orm.client`
    // pair when the source uses an ORM.
    // Document-store detection. No analyse adapter emits a reliable Mongo
    // signal today (the prepass `backend` is gitnexus/grep, not a DB), so an
    // adapter-less / raw-SQL app is relational far more often than document.
    // When a future mongo adapter lands, add it here and the whole stack
    // stays coherent (#94 defect 4).
    const isDocumentStore = adapters.has('mongoose') || adapters.has('mongodb-native');
    if (adapters.has('typescript-prisma')) {
        choices['storage.database'] = 'PostgreSQL15';
        rationale['storage.database'] = 'typescript-prisma adapter detected schema.prisma — pairing with PostgreSQL15';
        choices['orm.schema'] = 'PrismaORM';
        choices['orm.client'] = 'PrismaORM';
        rationale['orm.schema'] = 'prisma signal — PrismaORM for schema + client';
    }
    else if (isDocumentStore) {
        choices['storage.database'] = 'MongoDBNativeDriver';
        rationale['storage.database'] = 'document-store signal — MongoDBNativeDriver (provides its own orm.schema + client)';
    }
    else {
        // Relational default. Prisma is the only fully-working backend, so an
        // adapter-less relational/raw-SQL app realizes most reliably onto the
        // PostgreSQL15 + PrismaORM stack. (Pre-#94 this defaulted to
        // MongoDBNativeDriver, which then mis-paired with PrismaServices — a
        // self-contradictory manifest that compiled to nothing.)
        choices['storage.database'] = 'PostgreSQL15';
        choices['orm.schema'] = 'PrismaORM';
        choices['orm.client'] = 'PrismaORM';
        rationale['storage.database'] = 'no prisma/document signal — defaulting to the relational PostgreSQL15 + PrismaORM stack (Prisma is the fully-supported backend; hand-edit for a document store)';
        rationale['orm.schema'] = 'relational default — PrismaORM for schema + client';
    }
    // ── service.controller / business / crud ──────────────────────────
    // Service factory MUST cohere with the storage choice. A Mongo store
    // pairs with the MongoDBNativeDriver services factory (which provides its
    // own orm.schema/client); every relational store pairs with
    // PrismaServices. Pre-#94 this was hardcoded to PrismaServices even when
    // storage was MongoDBNativeDriver — an incoherent manifest.
    const servicesFactory = choices['storage.database'] === 'MongoDBNativeDriver' ? 'MongoDBNativeDriver' : 'PrismaServices';
    for (const cap of ['service.controller', 'service.business', 'service.crud']) {
        choices[cap] = servicesFactory;
    }
    rationale['service.*'] =
        servicesFactory === 'MongoDBNativeDriver'
            ? 'MongoDBNativeDriver services — paired with the document store'
            : 'PrismaServices — paired with the PostgreSQL15 + PrismaORM relational stack';
    // ── messaging.events ─────────────────────────────────────────────
    choices['messaging.events'] = 'EventEmitter';
    rationale['messaging.events'] = 'in-process EventEmitter is the simplest default — swap for RabbitMQEvents when distribution required';
    // ── validation.runtime ───────────────────────────────────────────
    // Always emit ZodValidation when a `const-schemas` adapter ran (Zod
    // const schemas detected) OR as a low-friction default.
    choices['validation.runtime'] = 'ZodValidation';
    rationale['validation.runtime'] = adapters.has('zod-schemas') || adapters.has('backend-schemas')
        ? 'zod schemas detected in source'
        : 'ZodValidation as low-friction validator default';
    // ── app.entrypoint + project.scaffold (always emit) ──────────────
    choices['app.entrypoint'] = 'GenericApp';
    choices['project.scaffold'] = 'GenericScaffold';
    rationale['app.entrypoint'] = 'GenericApp — single entry-point shell';
    rationale['project.scaffold'] = 'GenericScaffold — root-level project files';
    // ── app.frontend (when React detected) ───────────────────────────
    if (adapters.has('react-views')) {
        const isReactNative = views.some((v) => v.filePath.includes('react-native') || v.filePath.includes('-mobile') || v.isScreen);
        choices['app.frontend'] = 'ReactAppRuntime';
        rationale['app.frontend'] = isReactNative
            ? 'react-views adapter detected RN screens — auto-mapped to ReactAppRuntime (no RN factory in registry yet; emits a web SPA shell instead)'
            : 'react-views adapter detected JSX components — using ReactAppRuntime';
    }
    // ── view.timeline (when a timeline view exists) ──────────────────
    // The renderer for a specialized view is a swappable renderer registered in
    // the view-renderer registry (#view-component-factories). Emit that registry's
    // DEFAULT — by contract an app-demo-compatible, zero-dependency runtime
    // renderer (TimelineGridCustom) that renders in the dynamic interpreter and in
    // generated apps with no extra package. Swap to VisTimeline in the manifest
    // for the interactive vis-timeline renderer.
    if (views.some((v) => String(v.viewType ?? v.type).toLowerCase() === 'timeline')) {
        const tl = VIEW_RENDERER_REGISTRY['view.timeline'];
        choices['view.timeline'] = tl.default;
        rationale['view.timeline'] = `timeline view detected — ${tl.default} (zero-dep custom grid; renders in app-demo + generated apps with no extra dependency). Swap to VisTimeline for the interactive vis-timeline renderer.`;
    }
    // ── view.kanban (when a kanban / board view exists) ───────────────
    // Same swappable-renderer model as view.timeline. Emit the registry's
    // app-demo-compatible default (KanbanBoard — zero-dep, groups records into
    // columns by a lifecycle state / status attribute).
    if (views.some((v) => ['kanban', 'board'].includes(String(v.viewType ?? v.type).toLowerCase()))) {
        const kb = VIEW_RENDERER_REGISTRY['view.kanban'];
        choices['view.kanban'] = kb.default;
        rationale['view.kanban'] = `kanban/board view detected — ${kb.default} (zero-dep custom board; renders in app-demo + generated apps with no extra dependency).`;
    }
    // ── auth.jwt ─────────────────────────────────────────────────────
    // Reuses the api.rest factory — Fastify ships JWT plugin support.
    const entityNames = new Set(entities.map((e) => e.name.toLowerCase()));
    const hasJwtSignal = entityNames.has('refreshtoken')
        || entityNames.has('authsession')
        || entityNames.has('authservice')
        || entityNames.has('apitoken');
    if (hasJwtSignal) {
        choices['auth.jwt'] = choices['api.rest'];
        rationale['auth.jwt'] = 'detected RefreshToken/AuthSession/AuthService entity — JWT path handled by the api.rest factory';
    }
    // ── Final guard: drop any choice that isn't a known factory ──────
    // Known = bundled `KNOWN_FACTORIES` (always-available offline fallback) ∪
    // registry-advertised names (when the caller fetched them via
    // `fetchRegistryFactoryNames`). Belt-and-braces — if a code path emits a
    // name in neither set, log + drop instead of shipping a manifest realize
    // will refuse to use.
    const known = input.registryFactories
        ? new Set([...KNOWN_FACTORIES, ...input.registryFactories])
        : KNOWN_FACTORIES;
    for (const cap of Object.keys(choices)) {
        const name = choices[cap];
        if (!known.has(name)) {
            rationale[cap] = `${rationale[cap] ?? ''} — DROPPED: factory "${name}" not in registry`;
            delete choices[cap];
        }
    }
    // Build the yaml document. Engines 6.32.4+ — emit the array form
    // `capabilityMappings: [{ capability, instanceFactory }, …]` and a
    // `defaultMappings` block. The realize-engine loader (manifest-loader.ts)
    // expects the array form; the previous hashmap form parsed cleanly
    // but resolveCapability() then never matched any factory, leaving
    // every model with `factory: unknown` and no code emitted.
    const specName = input.specName ?? 'AnalysedSpec';
    const lines = [];
    lines.push('specVersion: "1.0"');
    lines.push(`name: ${specName}`);
    lines.push('');
    // defaultMappings — quick fallback the realize engine consults when
    // a capability has no explicit row. Pull from the choice set when we
    // emitted one for the canonical capability.
    const defaultMappings = [
        ['storage', choices['storage.database']],
        ['controller', choices['service.controller']],
        ['service', choices['service.business']],
        ['routing', choices['api.rest']],
        ['orm', choices['orm.schema']],
        ['communication', choices['messaging.events']],
    ];
    const defaults = defaultMappings.filter(([, v]) => v !== undefined);
    if (defaults.length > 0) {
        lines.push('defaultMappings:');
        for (const [k, v] of defaults)
            lines.push(`  ${k}: "${v}"`);
        lines.push('');
    }
    lines.push('capabilityMappings:');
    // Stable ordering — sort keys for reproducibility.
    for (const cap of Object.keys(choices).sort()) {
        lines.push(`  - capability: "${cap}"`);
        lines.push(`    instanceFactory: "${choices[cap]}"`);
    }
    lines.push('');
    return {
        manifestYaml: lines.join('\n'),
        factoryChoices: choices,
        rationale,
    };
}
/**
 * Best-effort fetch of factory names advertised by the registry
 * (`GET /api/libraries?type=factory`, via the shared RegistryClient).
 *
 * Offline-safe: returns an EMPTY set on ANY failure (registry unreachable,
 * timeout, no factory records) and NEVER throws, so callers always fall back
 * to the bundled `KNOWN_FACTORIES`. Uses a short timeout + single attempt to
 * keep it cheap when offline.
 *
 * Intended use: analyse-runner passes the result as
 * `emitManifest({ ..., registryFactories })` (opt-in via
 * `SPECVERSE_REGISTRY_FACTORIES=1`) so published factories aren't dropped by
 * the bundled allow-list. The registry carries no factory records yet, so this
 * returns empty today — the wiring is ready for Phase 4 (factory libraries).
 */
export async function fetchRegistryFactoryNames(client) {
    const c = client ?? new RegistryClient({ retries: 1, timeout: 5000 });
    try {
        const resp = await c.fetchFactories();
        return new Set(resp.results.map((f) => f.name).filter((n) => Boolean(n)));
    }
    catch {
        return new Set();
    }
}
//# sourceMappingURL=manifest-emitter.js.map