/**
 * SpecVerse Code Realization
 *
 * Main entry point for code generation (realization) from SpecVerse specifications.
 * Transforms minimal specs into production-ready implementations.
 */
// Types (TemplateContext, etc.)
export * from './types/index.js';
// Utilities
export * from './utils/index.js';
// Generators
export * from './generators/index.js';
export { InstanceFactoryLibrary, createDefaultLibrary } from './library/library.js';
export { createResolver } from './library/resolver.js';
// Code generator
export { createCodeGenerator } from './engines/code-generator.js';
export { normalizeAttributeRelationshipCollisions, resolveOneModel, } from './normalize-collisions.js';
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs';
import { dirname, join, basename } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
import { createDefaultLibrary } from './library/library.js';
import { createResolver } from './library/resolver.js';
import { createCodeGenerator } from './engines/code-generator.js';
import { loadManifest } from './utils/manifest-loader.js';
/** List every factory name registered in the library, sorted by category
 *  then name. Used by the fail-loud "unknown factories" error to show the
 *  user what IS available so they can substitute. */
function listKnownFactoryNames(library) {
    const all = library.find({});
    return all
        .map((f) => ({ name: f.name, category: f.category ?? 'unknown' }))
        .sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
}
/**
 * Walk a manifest and collect every implementation type name it references,
 * with the source path(s) where each name appears (defaultMappings.<cat>,
 * capabilityMappings[i].implementation, instanceMappings[i].instanceFactory).
 * Used by initialize() to produce a single fail-loud error for all unknowns.
 */
function collectReferencedFactoryNames(manifest) {
    const refs = new Map();
    const add = (name, source) => {
        if (!name || typeof name !== 'string')
            return;
        const list = refs.get(name) ?? [];
        list.push(source);
        refs.set(name, list);
    };
    const inner = manifest?.manifests
        ? Object.values(manifest.manifests)[0]
        : manifest;
    if (!inner)
        return refs;
    if (inner.defaultMappings) {
        for (const [category, name] of Object.entries(inner.defaultMappings)) {
            add(name, `defaultMappings.${category}`);
        }
    }
    if (Array.isArray(inner.capabilityMappings)) {
        for (let i = 0; i < inner.capabilityMappings.length; i++) {
            const m = inner.capabilityMappings[i];
            const impl = m?.implementation ?? m?.instanceFactory;
            add(impl, `capabilityMappings[${i}] (capability: ${m?.capability ?? '?'})`);
        }
    }
    if (Array.isArray(inner.instanceMappings)) {
        for (let i = 0; i < inner.instanceMappings.length; i++) {
            const m = inner.instanceMappings[i];
            add(m?.instanceFactory, `instanceMappings[${i}] (instance: ${m?.instanceName ?? '?'})`);
        }
    }
    return refs;
}
class SpecVerseRealizeEngine {
    name = 'realize';
    version = '3.5.2';
    capabilities = ['realize', 'code-generation', 'manifest-resolution', 'instance-factories'];
    library = null;
    resolver = null;
    codeGenerator = null;
    manifest = null;
    initialized = false;
    async initialize(config) {
        const workingDir = config?.workingDir || process.cwd();
        // Load instance factory library
        this.library = await createDefaultLibrary(workingDir);
        // Load and resolve manifest if provided
        if (config?.manifestPath) {
            this.manifest = loadManifest(config.manifestPath);
            // Pre-flight: validate every implementation type referenced in the
            // manifest against the registered factory library. Fail-loud so users
            // see exactly what's unmapped (rather than silently producing an
            // empty `generated/code/` because tryResolve swallows ResolutionError
            // per-capability further down).
            const referenced = collectReferencedFactoryNames(this.manifest);
            const unknown = [];
            for (const [name, sources] of referenced) {
                if (!this.library.has(name))
                    unknown.push({ name, sources });
            }
            if (unknown.length > 0) {
                // Build the actual list of registered factory names so the user sees
                // exactly what's available instead of a vague "go check the docs".
                const known = listKnownFactoryNames(this.library);
                const lines = [];
                lines.push('Realize cannot proceed — your manifest references implementation type names that');
                lines.push('are not registered SpecVerse instance factories.');
                lines.push('');
                lines.push('Unknown:');
                for (const u of unknown) {
                    lines.push(`  ✗ ${u.name}  (referenced in: ${u.sources.join(', ')})`);
                }
                lines.push('');
                lines.push(`Registered factories (${known.length}):`);
                for (const k of known) {
                    lines.push(`  ✓ ${k.name}  (category: ${k.category})`);
                }
                lines.push('');
                lines.push(`To resolve, either:`);
                lines.push(`  1. Update your manifest to use the registered names above (e.g. `);
                lines.push(`     PostgreSQL15 instead of PostgreSQL, FastifyAPI instead of KoaAPI).`);
                lines.push(`  2. Pair the spec with a known-good manifest from \`spv init --template default\``);
                lines.push(`     (Fastify+Prisma stack).`);
                lines.push(`  3. Install custom instance factories that register the missing names.`);
                throw new Error(lines.join('\n'));
            }
            this.resolver = createResolver(this.library, this.manifest);
        }
        // Create code generator
        this.codeGenerator = createCodeGenerator();
        this.initialized = true;
    }
    getInfo() {
        return { name: this.name, version: this.version, capabilities: this.capabilities };
    }
    resolve(capability) {
        if (!this.resolver)
            throw new Error('Realize engine not initialized with manifest.');
        return this.resolver.resolveCapability(capability);
    }
    async generate(resolved, template, context) {
        if (!this.codeGenerator)
            throw new Error('Realize engine not initialized.');
        return this.codeGenerator.generateFromTemplate(resolved, template, context);
    }
    /**
     * Realize all code from an AI-optimized spec.
     * This is the full pipeline equivalent of the hand-written CLI's `case 'all':`.
     */
    async realizeAll(spec, outputDir) {
        if (!this.initialized || !this.resolver || !this.codeGenerator) {
            throw new Error('Realize engine not initialized. Call initialize() with manifestPath.');
        }
        const files = [];
        const errors = [];
        // Shared LanguageModel for the whole realize run.
        //
        // The claude-cli provider's first call uses `--session-id <uuid>` and
        // every subsequent call on the SAME model instance uses
        // `--resume <uuid>` — claude-cli caches the conversation context
        // (system prompt, prior messages) so the second+ calls see ~75%
        // input-token reduction and a much faster TTFT.
        //
        // Pre-this-wire-up: per-action and per-owner emit hooks each let
        // runPrompt build a FRESH model per call (line 235 of
        // engines/src/ai/prompt-runner.ts: `model ?? resolveModel(...)`),
        // producing a new sessionId per call → every claude-cli invocation
        // was cold. Empirically this added 5-10s of subprocess + cold-cache
        // overhead per call. On per-owner trial v3 (28 sequential calls)
        // that's a meaningful share of the 36-min wall time.
        //
        // Now: build the model ONCE here with a sessionId (resolveModel
        // honours `SPECVERSE_AI_SESSION_ID` env var; otherwise the
        // claude-cli provider generates a fresh uuid internally). Pass
        // this same instance to both per-action and per-owner emit
        // factories; they forward it to `runPrompt({ model })` which
        // bypasses the internal resolveModel call.
        let sharedModel = null;
        try {
            const { resolveModel } = await import('../ai/model-resolver.js');
            sharedModel = resolveModel({
                sessionId: process.env.SPECVERSE_AI_SESSION_ID,
            });
        }
        catch {
            // resolveModel can fail when no AI provider is configured (e.g.
            // SPECVERSE_AI_PROVIDER=stub paths). The downstream emit hooks
            // gracefully fall back to building their own model when
            // sharedModel is null.
        }
        // Pre-realize normalization: rename any attribute that collides with
        // a relationship name on the same model. Mutates spec.models in place
        // so every downstream generator (prisma, mongo, pg, controllers,
        // services, routes, views) sees the canonical renamed shape. Without
        // this, the rename used to live inside prisma's schema-generator only,
        // and non-prisma manifests (e.g. mongo) silently shipped colliding
        // attribute/relationship names downstream. Per-rename warns are emitted
        // by resolveOneModel; this site doesn't add a summary line. See
        // engines/src/realize/normalize-collisions.ts for the why + invariants.
        const { normalizeAttributeRelationshipCollisions } = await import('./normalize-collisions.js');
        normalizeAttributeRelationshipCollisions(spec);
        const allModels = Object.values(spec.models || {});
        // Audit-trail recorder (#audit-trail-2026-05-03 plan, engines 6.22.1+).
        // Forward-logs every matcher dispatch decision (level-2 convention vs
        // level-3 AI fallback) at the moment it happens. Attached before any
        // generator runs; drained at end into realize-decisions.json +
        // realize-coverage.json sidecars.
        const { InMemoryRealizeRecorder } = await import('../audit/realize-recorder.js');
        const auditRecorder = new InMemoryRealizeRecorder();
        // Attach the recorder to the shared step-matching module. Every step-
        // conventions.ts (prisma / mongodb-native / postgres-native) delegates
        // to `matchAgainstConventions` in this shared module, so a single hook
        // captures all matcher decisions across all 6 call sites.
        const sharedStepMatchingPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', '_shared', 'step-matching.ts');
        const sharedDistPath = sharedStepMatchingPath.includes('/libs/')
            ? sharedStepMatchingPath.replace('/libs/', '/dist/libs/').replace(/\.tsx?$/, '.js')
            : sharedStepMatchingPath;
        let detachStepRecorder = () => { };
        try {
            const sharedMod = existsSync(sharedDistPath)
                ? await import(sharedDistPath)
                : await import(sharedStepMatchingPath);
            if (typeof sharedMod.setStepMatchRecorder === 'function') {
                sharedMod.setStepMatchRecorder((rec) => auditRecorder.recordStepResolution({
                    ownerName: rec.ownerName,
                    actionName: rec.actionName,
                    stepOrdinal: rec.stepOrdinal,
                    stepText: rec.stepText,
                    matched: rec.matched,
                    helperMethod: rec.helperMethod,
                    aiFunctionName: rec.aiFunctionName,
                    generatorPhase: 'controller-gen', // pre-scan + ai-gen also flow through; we dedupe in build()
                }));
                detachStepRecorder = () => sharedMod.setStepMatchRecorder(null);
            }
        }
        catch { /* shared module not yet compiled — generation works without recorder */ }
        // L3 verification (Quint guards) is NOT run here. It lives in
        // `validate --verify` so users can opt into it explicitly and run
        // it on any spec — raw or inferred — without also generating code.
        // See engines/src/inference/verification.ts.
        const writeOutput = (output) => {
            // A generator can signal "skip this file" by returning an empty string.
            if (output.code === '')
                return;
            const dir = dirname(output.filePath);
            if (!existsSync(dir))
                mkdirSync(dir, { recursive: true });
            writeFileSync(output.filePath, output.code, 'utf-8');
            files.push(basename(output.filePath));
        };
        // Prefer compiled .js in dist/libs/ over .ts in libs/ (Node v24+ can't strip types from node_modules)
        const resolveGenPath = (tsPath) => {
            // Try dist/libs/ compiled JS first
            if (tsPath.includes('/libs/')) {
                const distJsPath = tsPath.replace('/libs/', '/dist/libs/').replace(/\.tsx?$/, '.js');
                if (existsSync(distJsPath))
                    return distJsPath;
            }
            // Fall back to .js alongside .ts
            const jsPath = tsPath.replace(/\.tsx?$/, '.js');
            if (existsSync(jsPath))
                return jsPath;
            // Fall back to .ts (works in dev, fails in node_modules on Node v24+)
            if (existsSync(tsPath))
                return tsPath;
            return null;
        };
        const tryResolve = (capability) => {
            try {
                return this.resolver.resolveCapability(capability);
            }
            catch {
                return null;
            }
        };
        // 1. ORM Schema
        const ormResolved = tryResolve('orm.schema');
        if (ormResolved?.instanceFactory?.codeTemplates?.schema) {
            try {
                // 53A — thread the resolved storage factory so the Prisma schema
                // emitter can pick the right `provider` for the datasource block.
                // Without this, the manifest's `storage.database: PostgreSQL15`
                // mapping is silently ignored and the schema falls back to SQLite.
                const storageResolved = tryResolve('storage.database');
                const storageFactory = storageResolved?.instanceFactory;
                // #75 — thread the dev environment's DB config so the schema follows
                // the SAME source the backend .env generator uses. A manifest can
                // override the production-grade storage factory for zero-deps dev
                // (e.g. PostgreSQL15 storage + a sqlite environment); without this the
                // schema emits postgres while the .env emits a sqlite url and
                // `prisma db push` fails. The schema-gen prefers this over the storage
                // factory. Resolve the env generically — prefer `development`, else the
                // first environment declaring a database (e.g. a `smoke` env) — and
                // keep this selection identical to backend-env-generator.ts.
                // (this.manifest may be wrapped under `manifests`.)
                const innerManifest = this.manifest?.manifests
                    ? Object.values(this.manifest.manifests)[0]
                    : this.manifest;
                const environments = innerManifest?.configuration?.environments || {};
                const devEnv = environments.development?.database
                    ? environments.development
                    : Object.values(environments).find((e) => e?.database);
                const devDatabase = devEnv?.database;
                const output = await this.codeGenerator.generateFromTemplate(ormResolved, 'schema', { spec, models: allModels, storageFactory, devDatabase }, { outputDir });
                writeOutput(output);
                console.log(`   ✅ ORM schema: ${output.filePath}`);
            }
            catch (e) {
                errors.push(`ORM: ${e.message}`);
            }
        }
        // 1.5 Event infrastructure (types + bus + websocket bridge)
        const eventResolved = tryResolve('messaging.events');
        if (eventResolved?.instanceFactory?.codeTemplates) {
            const eventFiles = [];
            // Generate in dependency order: types first, then bus, then websocket
            const orderedTemplates = ['types', 'bus', 'websocket'].filter(t => eventResolved.instanceFactory.codeTemplates[t]);
            // Also generate any other templates (publisher, subscriber)
            for (const t of Object.keys(eventResolved.instanceFactory.codeTemplates)) {
                if (!orderedTemplates.includes(t))
                    orderedTemplates.push(t);
            }
            for (const templateName of orderedTemplates) {
                try {
                    const output = await this.codeGenerator.generateFromTemplate(eventResolved, templateName, { spec, models: allModels, manifest: this.manifest }, { outputDir });
                    writeOutput(output);
                    eventFiles.push(basename(output.filePath));
                }
                catch (e) {
                    errors.push(`Event ${templateName}: ${e.message}`);
                }
            }
            if (eventFiles.length)
                console.log(`   ✅ Events: ${eventFiles.join(', ')}`);
        }
        // 2. Controllers (per model) — use spec controllers if available, else generate from model
        const ctrlResolved = tryResolve('service.controller');
        if (ctrlResolved?.instanceFactory?.codeTemplates?.controllers) {
            // Build controller lookup from spec
            const specControllers = Array.isArray(spec.controllers)
                ? spec.controllers
                : Object.values(spec.controllers || {});
            const controllerLookup = {};
            for (const c of specControllers)
                controllerLookup[c.name] = c;
            for (const model of allModels) {
                try {
                    const ctrlName = `${model.name}Controller`;
                    const specCtrl = controllerLookup[ctrlName];
                    const controller = specCtrl ? {
                        ...specCtrl,
                        model: specCtrl.modelReference || specCtrl.model || model.name,
                        modelReference: specCtrl.modelReference || specCtrl.model || model.name,
                        cured: specCtrl.cured || { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
                    } : {
                        name: ctrlName,
                        model: model.name,
                        modelReference: model.name,
                        cured: { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
                    };
                    const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'controllers', { spec, model, controller, models: allModels }, { outputDir });
                    writeOutput(output);
                    // Phase 2 — emit `<Model>.guards.ts` ONLY when the model has
                    // declared constraints. Models without constraints skip this
                    // template entirely; the controller still works as before.
                    // The `guards` template is optional in the factory (older
                    // factories without it just don't fire).
                    const modelConstraints = model.constraints;
                    if (Array.isArray(modelConstraints) &&
                        modelConstraints.length > 0 &&
                        ctrlResolved?.instanceFactory?.codeTemplates?.guards) {
                        try {
                            const guardsOutput = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'guards', { spec, model, controller, models: allModels }, { outputDir });
                            writeOutput(guardsOutput);
                        }
                        catch (ge) {
                            errors.push(`Guards ${model.name}: ${ge.message}`);
                        }
                    }
                }
                catch (e) {
                    errors.push(`Controller ${model.name}: ${e.message}`);
                }
            }
            console.log(`   ✅ Controllers: ${allModels.length} controller(s)`);
            // 2.5 AI Behaviors — per-owner LLM emit.
            //
            // One LLM call per controller / service emits the whole
            // `<Owner>.ai.ts` file (one exported `async function` per spec
            // action / operation). The skill-based architecture (engines
            // 6.44.0+) auto-loads the `realize-emit` skill into claude-cli so
            // the LLM has CONSTRAINTS / GUIDANCE / EXAMPLE available without
            // bloating the per-call prompt.
            //
            // Matcher resolution: ORM-specific matchers override the prisma
            // default when MongoDBNativeDriver or PostgresNativeDriver is the
            // bound ORM, so the controller's `aiBehaviors.<actionName>(...)`
            // calls and the emitter's pre-baked convention snippets stay in
            // sync.
            //
            // Per-action emission retired in engines 6.45.0. SPECVERSE_REALIZE_PER_ACTION
            // is no longer read; per-owner has been the default since 6.41.0
            // and accumulated empirical wins through the skill-based v18 trial
            // (29 → 7 TS errors on idle-meta sonnet).
            try {
                // ─── Resolve the matcher + conventions ────────────────────────
                let matcher = undefined;
                let conventions = undefined;
                const ormFactoryName = ctrlResolved?.instanceFactory?.name;
                if (ormFactoryName === 'MongoDBNativeDriver') {
                    const mongoMatcherPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'mongodb-native', 'step-conventions.ts');
                    const mongoMatcherSrc = resolveGenPath(mongoMatcherPath);
                    if (mongoMatcherSrc) {
                        const mongoMod = await import(mongoMatcherSrc);
                        matcher = mongoMod.matchMongoStep;
                        conventions = mongoMod.MONGO_STEP_CONVENTIONS ?? mongoMod.STEP_CONVENTIONS;
                    }
                }
                else if (ormFactoryName === 'PostgresNativeDriver') {
                    const pgMatcherPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'postgres-native', 'step-conventions.ts');
                    const pgMatcherSrc = resolveGenPath(pgMatcherPath);
                    if (pgMatcherSrc) {
                        const pgMod = await import(pgMatcherSrc);
                        matcher = pgMod.matchPgStep;
                        conventions = pgMod.PG_STEP_CONVENTIONS ?? pgMod.STEP_CONVENTIONS;
                    }
                }
                else {
                    // Prisma default — load the convention array; the matcher is
                    // the shared `matchAgainstConventions` walker below.
                    const prismaMatcherPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', 'prisma', 'step-conventions.ts');
                    const prismaMatcherSrc = resolveGenPath(prismaMatcherPath);
                    if (prismaMatcherSrc) {
                        const prismaMod = await import(prismaMatcherSrc);
                        conventions = prismaMod.STEP_CONVENTIONS;
                    }
                }
                // Fall back to the shared matchAgainstConventions walker when
                // the ORM didn't provide its own matcher (prisma default path).
                let sharedMatcher = matcher;
                if (!sharedMatcher && conventions) {
                    const sharedMatcherTsPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'services', 'templates', '_shared', 'step-matching.ts');
                    const sharedMatcherSrc = resolveGenPath(sharedMatcherTsPath);
                    if (sharedMatcherSrc) {
                        const sharedMod = await import(sharedMatcherSrc);
                        sharedMatcher = sharedMod.matchAgainstConventions;
                    }
                }
                // V2 imports → per-owner `availableClients` map for the LLM
                // prompt's `availableClients` slot.
                const { buildAvailableClientsByOwner } = await import('./owner-emit-shared.js');
                const availableClientsByOwner = buildAvailableClientsByOwner(spec);
                // Per-owner emission. The SHARED model is threaded through so
                // every owner call reuses one claude-cli session (first call =
                // --session-id; subsequent = --resume → ~75% input-token
                // savings, warm cache, and skill state persists across calls).
                const { runPerOwnerEmission, buildAvailableCapabilitiesString } = await import('./per-owner-runner.js');
                const { createRealizeOwnerLlmEmit } = await import('./per-owner-emit.js');
                const { createPerActionRecovery } = await import('./per-action-recovery.js');
                const { resolveProviderId: resolveProviderIdForRecovery } = await import('../ai/model-resolver.js');
                // Always-on emission telemetry. Each owner's emit pushes one
                // record capturing the journey through retry tiers (Lever 1
                // structural retry, surgical-retry on parse/rule, exhaustion
                // outcomes). Serialized to `realize-emission.json` after the
                // run so downstream observers can answer "did Lever 1 fire"
                // without parsing logs. Phase 5c of
                // 2026-05-14-PER-ACTION-RECOVERY-TIER.md (Tier 1 slice).
                const emissionTelemetryRecords = [];
                const emissionTelemetry = {
                    record(entry) { emissionTelemetryRecords.push(entry); },
                };
                const perOwnerEmit = createRealizeOwnerLlmEmit({
                    model: sharedModel,
                    telemetry: emissionTelemetry,
                });
                // Tier 2 — per-action recovery (Phase 5a of
                // 2026-05-14-PER-ACTION-RECOVERY-TIER.md). When the whole-owner
                // emit fails, the runner retries the owner action-by-action with
                // full owner context before γ-stubbing. Default-on; the runner
                // honours SPECVERSE_REALIZE_NO_RECOVERY_TIER=1 + the action-count
                // cap. Stub provider has no LLM — skip wiring it then.
                const perActionRecovery = resolveProviderIdForRecovery() === 'stub'
                    ? undefined
                    : createPerActionRecovery({ model: sharedModel });
                const availableCapabilities = buildAvailableCapabilitiesString(this.manifest);
                const ownerResult = await runPerOwnerEmission({
                    spec, outputDir,
                    matcher: sharedMatcher,
                    conventions,
                    perOwnerEmit,
                    perActionRecovery,
                    availableClientsByOwner,
                    availableCapabilities,
                    silent: false,
                });
                if (ownerResult.totals.actionCount > 0) {
                    const llmCount = ownerResult.totals.ownersEmittedByLLM;
                    const tier2Count = ownerResult.totals.ownersRecoveredByTier2;
                    const fallbackCount = ownerResult.totals.ownersFellBack;
                    const tailParts = [];
                    if (llmCount > 0)
                        tailParts.push(`${llmCount} LLM-emitted`);
                    if (tier2Count > 0)
                        tailParts.push(`${tier2Count} via per-action recovery`);
                    if (fallbackCount > 0)
                        tailParts.push(`${fallbackCount} fell back to placeholder`);
                    const tail = tailParts.length > 0 ? ` (${tailParts.join(', ')})` : '';
                    console.log(`   ✅ Per-OWNER AI behaviors: ${ownerResult.totals.actionCount} action(s) across ${ownerResult.totals.ownerCount} owner(s)${tail}`);
                }
                // Write the emission telemetry sidecar. Always-on; structured
                // so future tooling can detect Lever 1 / Lever 2 wins without
                // parsing CLI logs. Sibling to realize-quality.json + post-
                // emit-feedback.json under outputDir.
                try {
                    const totals = {
                        ownerCount: ownerResult.totals.ownerCount,
                        tier1Attempted: emissionTelemetryRecords.length,
                        tier1FirstAttemptSuccess: emissionTelemetryRecords.filter((r) => r.outcome === 'success').length,
                        tier1StructuralRetryFired: emissionTelemetryRecords.filter((r) => r.structuralRetryFired).length,
                        tier1StructuralRetryRecovered: emissionTelemetryRecords.filter((r) => r.outcome === 'structural-retry-recovered').length,
                        tier1ParseRetryFired: emissionTelemetryRecords.filter((r) => r.parseRetryFired).length,
                        tier1ParseRetryRecovered: emissionTelemetryRecords.filter((r) => r.outcome === 'parse-retry-recovered').length,
                        tier1RuleRetryFired: emissionTelemetryRecords.filter((r) => r.ruleRetryFired).length,
                        tier1RuleRetryRecovered: emissionTelemetryRecords.filter((r) => r.outcome === 'rule-retry-recovered').length,
                        tier1Exhausted: emissionTelemetryRecords.filter((r) => r.outcome === 'structural-exhausted'
                            || r.outcome === 'structural-immediate'
                            || r.outcome === 'parse-exhausted'
                            || r.outcome === 'rule-exhausted'
                            || r.outcome === 'empty-block'
                            || r.outcome === 'threw-from-llm').length,
                        tier2Invocations: ownerResult.totals.ownersFellBack + ownerResult.totals.ownersRecoveredByTier2,
                        tier2Recovered: ownerResult.totals.ownersRecoveredByTier2,
                        tier3GammaFallbacks: ownerResult.totals.ownersFellBack,
                    };
                    // engines version from package.json — readFileSync over JSON
                    // import-assertions for Node 20 compat.
                    let engineVersion = 'unknown';
                    try {
                        const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
                        engineVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
                    }
                    catch { /* fall through with 'unknown' */ }
                    const sidecar = {
                        schemaVersion: '1.0',
                        ranAt: new Date().toISOString(),
                        engineVersion,
                        totals,
                        byOwner: emissionTelemetryRecords,
                    };
                    writeFileSync(join(outputDir, 'realize-emission.json'), JSON.stringify(sidecar, null, 2), 'utf8');
                }
                catch (e) {
                    // Telemetry write failure must never crash realize.
                    errors.push(`Realize emission telemetry: ${e?.message ?? String(e)}`);
                }
                // 2.5a Diagnostic sidecar (default-on; opt-out via SPECVERSE_REALIZE_NO_DIAGNOSTICS=1).
                //
                // Phase 1 of 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.
                // Runs the stub-completeness verifier in diagnostic-only mode
                // (separate from the feedback runner), writes a sidecar with
                // per-owner stub breakdown, prints a one-line CLI summary if
                // there's anything to flag. Zero LLM calls, milliseconds runtime.
                if (process.env.SPECVERSE_REALIZE_NO_DIAGNOSTICS !== '1') {
                    try {
                        const { runDiagnostics, formatSummaryLine } = await import('./post-emit-verify/index.js');
                        const summary = await runDiagnostics({
                            outputDir,
                            targetLanguage: 'typescript',
                        });
                        const line = formatSummaryLine(summary);
                        if (line)
                            console.log(line);
                    }
                    catch (e) {
                        // Diagnostic failure must never crash the realize pipeline.
                        errors.push(`Realize diagnostics: ${e?.message ?? String(e)}`);
                    }
                }
                // 2.5b Post-emit feedback (default-on; escape hatch retained).
                //
                // After all per-owner emits complete, run language-specific
                // verifiers (tsc, mypy, etc.) on the realized output and feed
                // any errors back to the LLM for a surgical fix pass. Each
                // verifier runs only when it `applies` to the realized stack;
                // tsc skips when there's no tsconfig.json, etc.
                //
                // Flip history:
                //   6.51.0 — opt-in via SPECVERSE_REALIZE_POST_VERIFY=1
                //   6.52.0 — default-on; opt-out via SPECVERSE_REALIZE_NO_POST_VERIFY=1
                //   6.53.0 — env hatch retired
                //   6.53.2 — escape hatch restored (this version) after
                //            real-npm publish surfaced the need for a safety
                //            valve. Default-on stays; users with a genuine
                //            regression can set SPECVERSE_REALIZE_NO_POST_VERIFY=1
                //            until upstream ships a fix.
                //
                // Empirical evidence at flip time (Phase A corpus harness):
                //   - scoremyclays-ollama: 9 → 0 tsc errors
                //   - idle-meta sonnet: 44 → 3 tsc errors
                //   - nestjs-billing: 11 → 11 (correctly identifies template-
                //     generator errors as unmappable; no spurious re-emits)
                //   - zero regressions across the corpus
                //
                // Env tuning:
                //   SPECVERSE_REALIZE_NO_POST_VERIFY=1        — opt out (escape hatch)
                //   SPECVERSE_REALIZE_POST_VERIFY_PASSES=N    — tune retry count (default 1)
                //   SPECVERSE_REALIZE_POST_VERIFY_MAX_FILES=N — cap LLM spend per pass (default 20)
                //   SPECVERSE_REALIZE_POST_VERIFY_ENABLE=id,…  — opt in to verifiers
                //                                              with enabledByDefault: false
                //                                              (e.g. stub-completeness)
                //
                // Gate: run feedback whenever there are owners AT ALL — not just
                // LLM-emitted ones. Pre-6.58.0 the gate was
                // `ownersEmittedByLLM > 0`, which skipped feedback entirely when
                // EVERY owner γ-fell-back (structural-preservation rejection,
                // parse failure, etc.). That defeated the stub-completeness
                // verifier (engines 6.54.0+) in its single highest-value case:
                // an all-fallback realize is ALL STUB001 γ-stubs, exactly what
                // auto-reemit-with-context (Phase 3) is built to recover. The
                // Phase 3 trial (2026-05-14, nestjs-billing × MarrBox/Ollama)
                // surfaced this: both providers had all owners rejected by
                // structural-preservation → 0 LLM-emitted → feedback skipped →
                // 7 STUB001 stubs shipped unfixed. Now: `ownerCount > 0`. The
                // only true skip is "no behaviors at all" (validate-only
                // backends) — and the stub provider is still skipped below.
                //
                // Stub provider is auto-skipped (no LLM → can't fix).
                if (process.env.SPECVERSE_REALIZE_NO_POST_VERIFY !== '1' && ownerResult.totals.ownerCount > 0) {
                    // Detect stub provider early — there's no LLM, so feedback
                    // can't fire. Stub returns the prompt verbatim which would
                    // appear as a "fix" but is garbage. Log + skip cleanly.
                    const { resolveProviderId } = await import('../ai/model-resolver.js');
                    const providerForFeedback = resolveProviderId();
                    if (providerForFeedback === 'stub') {
                        console.log('   ⏩ Post-emit feedback skipped: stub provider has no LLM');
                    }
                    else {
                        try {
                            const { runPostEmitFeedback, buildLlmReemit, appendSpecGaps, formatSpecGapLine } = await import('./post-emit-verify/index.js');
                            const maxPasses = parseInt(process.env.SPECVERSE_REALIZE_POST_VERIFY_PASSES ?? '1', 10) || 1;
                            const maxFilesPerPass = parseInt(process.env.SPECVERSE_REALIZE_POST_VERIFY_MAX_FILES ?? '20', 10) || 20;
                            const enableCsv = process.env.SPECVERSE_REALIZE_POST_VERIFY_ENABLE ?? '';
                            const enabledOverrides = enableCsv
                                ? new Set(enableCsv.split(',').map((s) => s.trim()).filter(Boolean))
                                : undefined;
                            const { loadContextSnapshot } = await import('./realize-context-snapshot.js');
                            const reemit = buildLlmReemit({
                                model: sharedModel,
                                contextLoader: (ownerName) => loadContextSnapshot(outputDir, ownerName),
                            });
                            const feedbackResult = await runPostEmitFeedback({ outputDir, targetLanguage: 'typescript' }, reemit, { maxPasses, maxFilesPerPass, enabledOverrides });
                            // Phase 2: surface spec gaps (STUB002s the treatment selector
                            // routed away from auto-reemit) into the diagnostic sidecar.
                            if (feedbackResult.specGaps.length > 0) {
                                appendSpecGaps(outputDir, feedbackResult.specGaps);
                                const line = formatSpecGapLine(feedbackResult.specGaps.length);
                                if (line)
                                    console.log(line);
                            }
                            const before = feedbackResult.initialErrors.length;
                            const after = feedbackResult.finalErrors.length;
                            if (before > 0) {
                                console.log(`   ✅ Post-emit feedback: ${before} → ${after} errors after ${feedbackResult.passes.length} pass(es)`);
                                for (const p of feedbackResult.passes) {
                                    const parts = [
                                        `pass ${p.passNumber}: ${p.errorsBefore}→${p.errorsAfter}`,
                                        `attempted=${p.ownersAttempted}`,
                                        `improved=${p.ownersImproved}`,
                                        `unchanged=${p.ownersUnchanged}`,
                                        `rolled-back=${p.ownersRolledBack}`,
                                        `unmappable=${p.ownersUnmappable}`,
                                        `reemit-failed=${p.ownersReemitFailed}`,
                                    ];
                                    console.log(`      ${parts.join(', ')}`);
                                }
                                for (const n of feedbackResult.verifierNotes) {
                                    for (const note of n.notes) {
                                        console.log(`      [${n.verifierId}] ${note}`);
                                    }
                                }
                                // Audit-trail sidecar.
                                try {
                                    const { writeFileSync, mkdirSync } = await import('fs');
                                    const { join } = await import('path');
                                    mkdirSync(outputDir, { recursive: true });
                                    writeFileSync(join(outputDir, 'post-emit-feedback.json'), JSON.stringify({
                                        schemaVersion: '1.0',
                                        ranAt: new Date().toISOString(),
                                        maxPasses,
                                        initialErrorCount: before,
                                        finalErrorCount: after,
                                        passes: feedbackResult.passes,
                                        verifierNotes: feedbackResult.verifierNotes,
                                    }, null, 2));
                                }
                                catch { /* best-effort sidecar */ }
                            }
                        }
                        catch (e) {
                            errors.push(`Post-emit feedback: ${e?.message ?? String(e)}`);
                        }
                    } // end of `else { ... }` (non-stub provider path)
                }
            }
            catch (e) {
                errors.push(`AI Behaviors: ${e.message}`);
            }
        }
        // 2.6 V2 imports — per-component runtime client emission
        //
        // For each component's `import:` block, resolve the `from:` reference via
        // the V2 dependency resolver (4-step chain: local → npm → registry library
        // → registry factory) and dispatch to the right per-runtime emitter
        // (library / service / managed / executable / messaging) keyed by factory
        // v2metadata.topology. This is the wire-up that makes V2 P3's framework
        // actually drive code emission end-to-end.
        //
        // Factory binding (R3 follow-up): for each import the provider's deployment
        // instance is looked up in the spec by `instance.component === resolved.resolvedName`
        // across all 8 instance categories. The first `advertises:` capability is
        // then resolved via manifest.capabilityMappings to an instanceFactory name,
        // which is fetched from the InstanceFactoryLibrary. If the factory has
        // v2metadata, it drives dispatch; otherwise we fall back to a synthetic
        // library-topology factory (in-process default) so consumers without
        // explicit topology bindings still get a typed wrapper.
        //
        // Output: clients/<provider>.ts (and servers/<provider>-server.ts for
        // service topology when serverTemplate is set) under <outputDir>/backend/src/<ConsumerComponent>/.
        //
        // Resolver registry steps (3+4) are stubbed in Round 1; offline mode and
        // local + npm sources work fully.
        //
        // Deliberately no-op when a component has no `import:` entries — V2 is
        // additive; existing specs are unaffected.
        // Helper: walk all deployments in the spec, find the first instance whose
        // `component:` matches the resolved provider name. Searches across the 8
        // instance categories (controllers / services / views / communications /
        // storage / security / infrastructure / monitoring).
        const findDeploymentInstanceForProvider = (providerName) => {
            const deployments = Array.isArray(spec.deployments)
                ? spec.deployments
                : Object.values(spec.deployments ?? {});
            for (const dep of deployments) {
                const instances = dep.instances ?? {};
                for (const categoryKey of Object.keys(instances)) {
                    const category = instances[categoryKey];
                    if (!category || typeof category !== 'object')
                        continue;
                    for (const instanceKey of Object.keys(category)) {
                        const inst = category[instanceKey];
                        if (inst && typeof inst === 'object' && inst.component === providerName) {
                            return inst;
                        }
                    }
                }
            }
            return null;
        };
        // Helper: pick a factory for an import. Priority chain:
        //   1. Resolved provider's deployment instance → first `advertises:` capability
        //      → manifest.capabilityMappings → library.get(instanceFactory).
        //   2. Synthetic library-topology default (the safe in-process fallback).
        const syntheticLibraryFactory = {
            name: 'LibraryRuntimeDefault',
            version: '1.0.0',
            category: 'service',
            capabilities: { provides: [], requires: [] },
            technology: { runtime: 'node', framework: 'typescript', language: 'typescript' },
            v2metadata: { topology: 'library', emitsDeploymentInstance: false },
        };
        // Helper: strip a trailing 'Client' suffix from an emitter-returned
        // specifier so the v2Clients aggregator keys are clean (e.g. specifier
        // 'profitCalculatorClient' → key 'profitCalculator').
        const stripTrailingClient = (specifier) => specifier.endsWith('Client') ? specifier.slice(0, -'Client'.length) : specifier;
        const pickFactoryForImport = (resolved) => {
            const providerName = resolved.resolvedName ?? resolved.fromName;
            if (!providerName)
                return syntheticLibraryFactory;
            const deployInstance = findDeploymentInstanceForProvider(providerName);
            const advertises = deployInstance?.advertises;
            const firstCap = Array.isArray(advertises) ? advertises[0] : undefined;
            if (typeof firstCap !== 'string' || firstCap.length === 0) {
                return syntheticLibraryFactory;
            }
            const mappings = this.manifest?.capabilityMappings ?? this.manifest?.manifests?.[0]?.capabilityMappings ?? [];
            const mapping = (Array.isArray(mappings) ? mappings : []).find((m) => m?.capability === firstCap);
            const factoryName = mapping?.instanceFactory;
            if (typeof factoryName !== 'string' || factoryName.length === 0) {
                return syntheticLibraryFactory;
            }
            // Look up the factory in the library. If it has v2metadata, use it;
            // otherwise fall back to the synthetic default. Pre-V2 factories without
            // v2metadata can't drive topology-specific emitters; the loader's
            // applyV2MetadataDefaults fills v2metadata for canonical factories, so
            // this fallback is the safety net for malformed manifests.
            const factory = this.library?.get?.(factoryName);
            if (factory && factory.v2metadata) {
                return factory;
            }
            return syntheticLibraryFactory;
        };
        try {
            const { createDependencyResolver } = await import('./resolver/index.js');
            const { dispatchRuntimeEmitter, buildDefaultEmitters } = await import('./runtime-emitters/dispatcher.js');
            const dependencyResolver = createDependencyResolver();
            const runtimeEmitters = await buildDefaultEmitters();
            let importsEmitted = 0;
            // V2 P3 R3 follow-up: collect structured deployment-instance
            // suggestions across every consumer component so the run produces
            // a single aggregated v2-deployment-suggestions.yaml at the
            // top-level outputDir. Library-topology emissions don't contribute.
            const allDeploymentSuggestions = [];
            // Iterate spec.components as [name, component] entries so we have
            // the canonical name even when components is a keyed-object whose
            // values omit `.name` (which is what the inference YAML produces
            // — name is the key, not a field on the value). Pre-fix this site
            // read `consumerComp.name ?? 'unknown'`, which silently lumped
            // every V2 import for every component into `backend/src/unknown/clients/`
            // and overwrote them as the loop continued. Same shape mismatch
            // as the resolver fix (engines 6.38.4 defect C), different site.
            const componentEntries = Array.isArray(spec.components)
                ? spec.components.map((c) => [String(c?.name ?? ''), c])
                : Object.entries(spec.components ?? {});
            for (const [entryKey, consumerComp] of componentEntries) {
                const imports = consumerComp.imports ?? consumerComp.import ?? [];
                if (!Array.isArray(imports) || imports.length === 0)
                    continue;
                // Track per-component imports for the aggregator file (clients/index.ts).
                // Each entry: { from: file path (without .js), specifier: client const name }
                const aggregatorEntries = [];
                // Prefer the canonical name field on the component object, fall
                // back to the entry key (the YAML map key), fall back to 'unknown'
                // for malformed inputs.
                const componentName = String(consumerComp.name || entryKey || 'unknown');
                // V2 component dirs live under `backend/src/components/<Name>/`
                // rather than `backend/src/<Name>/` to avoid case-insensitive
                // filesystem collisions with the deterministic backend dirs
                // (`services/`, `controllers/`, `routes/`, `views/`). idle-meta
                // has a component named `Services` which on macOS APFS would
                // resolve to the same path as the lowercase `services/` dir
                // that holds emitted service classes — TS1261 (20 errors on
                // the 2026-05-11 idle-meta stub trial).
                const componentOutputDir = join(outputDir, 'backend', 'src', 'components', componentName);
                for (const importEntry of imports) {
                    try {
                        const fromName = importEntry.from;
                        if (typeof fromName !== 'string' || fromName.length === 0)
                            continue;
                        const resolved = await dependencyResolver.resolve(fromName, importEntry.version, { spec, offline: !!process.env.SPECVERSE_OFFLINE });
                        if (!resolved)
                            continue;
                        const factory = pickFactoryForImport(resolved);
                        const emitter = dispatchRuntimeEmitter(factory, runtimeEmitters);
                        const result = await emitter.emit({
                            consumerComponent: consumerComp,
                            importEntry: {
                                from: fromName,
                                version: importEntry.version,
                                select: importEntry.select ?? [],
                            },
                            resolved,
                            factory,
                            outputDir: componentOutputDir,
                            spec: spec,
                        });
                        for (const file of result.files) {
                            const filePath = join(componentOutputDir, file.path);
                            writeOutput({ filePath, code: file.content });
                            importsEmitted += 1;
                        }
                        // Surface emitter notes (e.g. TODO #58 orphan-skip messages).
                        // Gated behind SPECVERSE_VERBOSE to keep production runs
                        // quiet; the orphan-skip case is expected behaviour, not
                        // a regression, until per-component aggregate emission lands.
                        if (process.env.SPECVERSE_VERBOSE) {
                            for (const note of (result.notes ?? [])) {
                                console.error(`   ⚠️  ${note}`);
                            }
                        }
                        // Collect the consumer-import declarations the emitter returned so
                        // we can write the per-component clients/index.ts aggregator below.
                        for (const consumerImport of (result.imports ?? [])) {
                            aggregatorEntries.push(consumerImport);
                        }
                        // Collect structured deployment-instance suggestion (V2 P3 R3
                        // follow-up). Library emitters omit this field; only service /
                        // managed / executable / messaging contribute.
                        if (result.deploymentSuggestion) {
                            allDeploymentSuggestions.push(result.deploymentSuggestion);
                        }
                    }
                    catch (importErr) {
                        const compName = String(consumerComp.name || entryKey || '<unknown>');
                        const fromName = importEntry.from ?? '<unknown>';
                        errors.push(`V2 import ${compName}.${fromName}: ${importErr.message}`);
                    }
                }
                // Per-component aggregator: clients/index.ts re-exports every V2
                // client emitted for this consumer + a combined `v2Clients` constant
                // so consumers can `import { v2Clients } from './clients'` and spread
                // it into their deps object. This is the cleanest seam between the
                // V2 emission phase and the per-action emitter (L3 P3) without
                // requiring controller-template surgery — the consumer's existing
                // code just imports + spreads.
                if (aggregatorEntries.length > 0) {
                    const lines = [];
                    lines.push('// FACTORY-EMITTED — DO NOT EDIT (R8)');
                    lines.push('// V2 P3 imports aggregator. Re-exports every emitted client + a combined map.');
                    lines.push('// Consumer usage:');
                    lines.push('//   import { v2Clients } from \'./clients\';');
                    lines.push('//   const deps = { ...existing, ...v2Clients };');
                    lines.push('');
                    // Static imports.
                    for (const entry of aggregatorEntries) {
                        const localPath = entry.from.replace(/^\.\/clients\//, './');
                        lines.push(`import { ${entry.specifier} } from '${localPath}.js';`);
                    }
                    lines.push('');
                    // Re-exports.
                    for (const entry of aggregatorEntries) {
                        lines.push(`export { ${entry.specifier} };`);
                    }
                    lines.push('');
                    // Aggregated map keyed by client name with trailing 'Client' stripped
                    // (v2Clients.profitCalculator rather than v2Clients.profitCalculatorClient).
                    lines.push('export const v2Clients = {');
                    for (const entry of aggregatorEntries) {
                        lines.push(`  ${stripTrailingClient(entry.specifier)}: ${entry.specifier},`);
                    }
                    lines.push('} as const;');
                    lines.push('');
                    lines.push('export type V2Clients = typeof v2Clients;');
                    lines.push('');
                    const aggregatorPath = join(componentOutputDir, 'clients', 'index.ts');
                    writeOutput({ filePath: aggregatorPath, code: lines.join('\n') });
                }
            }
            if (importsEmitted > 0) {
                console.log(`   ✅ V2 imports: ${importsEmitted} client file(s) emitted`);
            }
            // V2 P3 R3 follow-up: write v2-deployment-suggestions.yaml when any
            // emitter returned a structured suggestion. Auto-merge into the
            // user's deployments block isn't safe (env-specific values, secrets,
            // scaling parameters), so this is review-then-paste material.
            if (allDeploymentSuggestions.length > 0) {
                const yamlBody = yaml.dump({ suggestions: allDeploymentSuggestions }, { indent: 2, lineWidth: 100, noRefs: true, sortKeys: false });
                const header = [
                    '# FACTORY-EMITTED — DO NOT EDIT (R8)',
                    '# V2 P3 R3 follow-up: deployment-instance suggestions extracted from runtime emitters.',
                    '#',
                    '# Each entry corresponds to one V2 import that produced a non-library client.',
                    '# Merge these into your deployments.<env>.instances.<category> block manually:',
                    '#',
                    '#   deployments:',
                    '#     production:',
                    '#       instances:',
                    '#         <category>:',
                    '#           <key>:',
                    '#             <config keys from this file>',
                    '#',
                    '# Auto-merge is intentionally NOT performed: deployment shape is user-controlled',
                    '# (env-specific values, secrets, scaling parameters). Review each suggestion before',
                    '# pasting; placeholder markers like <url> indicate values you must supply.',
                    '',
                ].join('\n');
                const suggestionsPath = join(outputDir, 'v2-deployment-suggestions.yaml');
                writeOutput({ filePath: suggestionsPath, code: header + yamlBody });
                console.log(`   ✅ V2 deployment suggestions: ${allDeploymentSuggestions.length} entry(ies) → v2-deployment-suggestions.yaml`);
            }
        }
        catch (e) {
            errors.push(`V2 imports phase: ${e.message}`);
        }
        // 3. Services
        const servicesList = Array.isArray(spec.services) ? spec.services : Object.values(spec.services || {});
        if (ctrlResolved?.instanceFactory?.codeTemplates?.services) {
            for (const service of servicesList) {
                try {
                    const svcName = service.name || 'Service';
                    const output = await this.codeGenerator.generateFromTemplate(ctrlResolved, 'services', { spec, service: { name: svcName, ...service } }, { outputDir });
                    writeOutput(output);
                }
                catch (e) {
                    errors.push(`Service: ${e.message}`);
                }
            }
            console.log(`   ✅ Services: ${servicesList.length} service(s)`);
        }
        // 4. Routes (per model) — use spec controllers for endpoint data
        const routeResolved = tryResolve('api.rest');
        if (routeResolved?.instanceFactory?.codeTemplates?.routes) {
            const specControllers2 = Array.isArray(spec.controllers)
                ? spec.controllers
                : Object.values(spec.controllers || {});
            const ctrlLookup2 = {};
            for (const c of specControllers2)
                ctrlLookup2[c.name] = c;
            for (const model of allModels) {
                try {
                    const ctrlName = `${model.name}Controller`;
                    const specCtrl = ctrlLookup2[ctrlName];
                    const controller = specCtrl || {
                        name: ctrlName,
                        model: model.name,
                        modelReference: model.name,
                        cured: { create: {}, retrieve: {}, update: {}, validate: {}, evolve: {}, delete: {} },
                    };
                    const output = await this.codeGenerator.generateFromTemplate(routeResolved, 'routes', { spec, model, controller, models: allModels }, { outputDir });
                    writeOutput(output);
                }
                catch (e) {
                    errors.push(`Route ${model.name}: ${e.message}`);
                }
            }
            console.log(`   ✅ Routes: ${allModels.length} route handler(s)`);
        }
        // 4.5 Service routes — HTTP endpoints for service operations
        //     POST /api/services/{ServiceName}/{operationName}
        //     Mirrors app-demo's dynamic runtime URL shape (see
        //     specverse-app-demo/src/api/http-server.ts:611) so smoke-parity
        //     tests can hit both backends identically.
        if (routeResolved?.instanceFactory?.codeTemplates?.serviceRoutes && servicesList.length > 0) {
            let svcRouteCount = 0;
            for (const service of servicesList) {
                try {
                    const svcName = service.name || 'Service';
                    const output = await this.codeGenerator.generateFromTemplate(routeResolved, 'serviceRoutes', { spec, service: { name: svcName, ...service }, models: allModels }, { outputDir });
                    writeOutput(output);
                    svcRouteCount++;
                }
                catch (e) {
                    errors.push(`Service route ${service?.name}: ${e.message}`);
                }
            }
            if (svcRouteCount > 0)
                console.log(`   ✅ Service routes: ${svcRouteCount} file(s)`);
        }
        // 5. Views, Forms, Hooks
        const viewsResolved = tryResolve('ui.components');
        if (viewsResolved && spec.views) {
            const views = Array.isArray(spec.views) ? spec.views : Object.values(spec.views);
            let viewCount = 0, formCount = 0, hookCount = 0;
            for (const viewData of views) {
                const modelRef = viewData.primaryModel || viewData.model;
                const model = modelRef ? allModels.find((m) => m.name === modelRef) : undefined;
                // View component
                if (viewsResolved.instanceFactory.codeTemplates?.components) {
                    try {
                        const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'components', { spec, view: viewData, model, models: allModels }, { outputDir });
                        writeOutput(output);
                        viewCount++;
                    }
                    catch (e) {
                        errors.push(`View ${viewData?.name || 'unknown'}: ${e.message}`);
                    }
                }
                // Form
                if (model && viewsResolved.instanceFactory.codeTemplates?.forms) {
                    try {
                        const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'forms', { spec, model, view: viewData, models: allModels }, { outputDir });
                        writeOutput(output);
                        formCount++;
                    }
                    catch (e) {
                        errors.push(`Form ${model?.name || 'unknown'}: ${e.message}`);
                    }
                }
                // Hook
                if (model && viewsResolved.instanceFactory.codeTemplates?.hooks) {
                    try {
                        const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'hooks', { spec, model, view: viewData, models: allModels }, { outputDir });
                        writeOutput(output);
                        hookCount++;
                    }
                    catch (e) {
                        errors.push(`Hook ${model?.name || 'unknown'}: ${e.message}`);
                    }
                }
            }
            if (viewCount)
                console.log(`   ✅ Views: ${viewCount} component(s)`);
            if (formCount)
                console.log(`   ✅ Forms: ${formCount} form(s)`);
            if (hookCount)
                console.log(`   ✅ Hooks: ${hookCount} hook(s)`);
        }
        // 6. Types (per model)
        if (viewsResolved?.instanceFactory?.codeTemplates?.types) {
            for (const model of allModels) {
                try {
                    const output = await this.codeGenerator.generateFromTemplate(viewsResolved, 'types', { spec, model, models: allModels }, { outputDir });
                    writeOutput(output);
                }
                catch (e) {
                    errors.push(`Type ${model?.name || 'unknown'}: ${e.message}`);
                }
            }
            console.log(`   ✅ Types: ${allModels.length} model type(s)`);
        }
        // 7. Scaffolding (package.json, tsconfig, etc.)
        //
        // Some scaffold templates are frontend-specific (e.g. tsconfigReact
        // → tsconfig.react.json). For backend-only projects we skip those —
        // dropping a React tsconfig into a project that has no React makes
        // no sense and clutters the output.
        const scaffoldResolved = tryResolve('project.scaffold');
        const hasFrontendForScaffold = !!tryResolve('app.frontend');
        const FRONTEND_ONLY_SCAFFOLDS = new Set(['tsconfigReact']);
        if (scaffoldResolved?.instanceFactory?.codeTemplates) {
            const scaffoldFiles = [];
            for (const [templateName] of Object.entries(scaffoldResolved.instanceFactory.codeTemplates)) {
                if (!hasFrontendForScaffold && FRONTEND_ONLY_SCAFFOLDS.has(templateName))
                    continue;
                try {
                    const output = await this.codeGenerator.generateFromTemplate(scaffoldResolved, templateName, { spec, models: allModels, manifest: this.manifest, instanceFactories: [] }, { outputDir });
                    writeOutput(output);
                    scaffoldFiles.push(basename(output.filePath));
                }
                catch (e) {
                    errors.push(`Scaffold: ${e.message}`);
                }
            }
            if (scaffoldFiles.length)
                console.log(`   ✅ Project scaffolding: ${scaffoldFiles.join(', ')}`);
        }
        // 8. Backend app entry point + Fastify server with route wiring
        const appResolved = tryResolve('app.entrypoint');
        if (appResolved?.instanceFactory?.codeTemplates) {
            const appFiles = [];
            for (const [templateName] of Object.entries(appResolved.instanceFactory.codeTemplates)) {
                try {
                    const output = await this.codeGenerator.generateFromTemplate(appResolved, templateName, { spec, models: allModels, manifest: this.manifest }, { outputDir });
                    writeOutput(output);
                    appFiles.push(basename(output.filePath));
                }
                catch (e) {
                    errors.push(`App ${templateName}: ${e.message}`);
                }
            }
            // Generate Fastify server with auto-wired routes — but only for
            // monorepo layouts. Standalone layouts (backend-only template) get
            // their main.ts from the GenericApp template above (which writes
            // to {backendDir}/src/main.ts where backendDir=.) and emitting the
            // monorepo server-generator output on top would create a dead
            // backend/src/main.ts file alongside the real one.
            const appCfg = appResolved?.configuration || appResolved?.instanceFactory?.configuration || {};
            const appOutputStructure = appCfg.outputStructure || 'monorepo';
            if (appOutputStructure !== 'standalone') {
                try {
                    const serverGenPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'controllers', 'templates', 'fastify', 'server-generator.ts');
                    const serverGen = resolveGenPath(serverGenPath);
                    if (serverGen) {
                        const genPath = serverGen;
                        const { default: generateServer } = await import(genPath);
                        const serverCode = generateServer({ spec, models: allModels, manifest: this.manifest });
                        const serverPath = join(outputDir, 'backend', 'src', 'main.ts');
                        const serverDir = dirname(serverPath);
                        if (!existsSync(serverDir))
                            mkdirSync(serverDir, { recursive: true });
                        writeFileSync(serverPath, serverCode);
                        appFiles.push('main.ts');
                    }
                }
                catch (e) {
                    errors.push(`Server: ${e.message}`);
                }
            }
            if (appFiles.length)
                console.log(`   ✅ Backend application: ${appFiles.join(', ')}`);
        }
        // 9. Frontend app
        const frontendResolved = tryResolve('app.frontend');
        if (frontendResolved?.instanceFactory?.codeTemplates) {
            const frontendFiles = [];
            for (const [templateName] of Object.entries(frontendResolved.instanceFactory.codeTemplates)) {
                try {
                    const output = await this.codeGenerator.generateFromTemplate(frontendResolved, templateName, { spec, models: allModels, views: spec.views, manifest: this.manifest }, { outputDir });
                    writeOutput(output);
                    frontendFiles.push(basename(output.filePath));
                }
                catch (e) {
                    errors.push(`Frontend ${templateName}: ${e.message}`);
                }
            }
            if (frontendFiles.length)
                console.log(`   ✅ Frontend application: ${frontendFiles.join(', ')}`);
            // 9a. Generate shared view utilities and Tailwind config.
            // The frontend factory may declare outputStructure=standalone with
            // frontendDir="." — in that case views/lib live at the output root
            // instead of under `frontend/`.
            const frontendCfg = frontendResolved?.configuration || frontendResolved?.instanceFactory?.configuration || {};
            const frontendOutputStructure = frontendCfg.outputStructure || 'monorepo';
            const frontendRelDir = frontendOutputStructure === 'standalone'
                ? '.'
                : (frontendCfg.frontendDir || 'frontend');
            const frontendDir = frontendRelDir === '.' ? outputDir : join(outputDir, frontendRelDir);
            try {
                // Tailwind config. ReactAppRuntime pulls its component classes from
                // @specverse/runtime's compiled JS so tailwind must scan that dir;
                // ReactAppStarter is standalone (no runtime dep) so the content
                // glob stops at the generated src/.
                const frontendFactoryName = frontendResolved?.instanceFactory?.name || '';
                const needsRuntimeContent = frontendFactoryName === 'ReactAppRuntime';
                const runtimeContentLine = needsRuntimeContent
                    ? `    path.join(path.dirname(require.resolve('@specverse/runtime/package.json')), 'dist/**/*.js'),\n`
                    : '';
                const runtimeContentImports = needsRuntimeContent
                    ? `import path from 'path';\nimport { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n`
                    : '';
                writeFileSync(join(frontendDir, 'tailwind.config.js'), `${runtimeContentImports}/** @type {import('tailwindcss').Config} */
export default {
  darkMode: 'class',
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx}',
${runtimeContentLine}  ],
  theme: { extend: {} },
  plugins: [],
};
`);
                writeFileSync(join(frontendDir, 'postcss.config.js'), `export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};
`);
            }
            catch (e) {
                errors.push(`TailwindConfig: ${e.message}`);
            }
        }
        // 9b. Quint guards are no longer emitted to the generated backend.
        // They are L3 verification assertions about the SPEC, not runtime
        // checks on the user's data — so they run at realize time (see the
        // L3 verification gate at the start of realizeAll) and don't ship
        // to the user's project.
        // 10. CLI commands (Commander.js)
        try {
            const cliDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'cli', 'templates', 'commander');
            const entryGen = join(cliDir, 'cli-entry-generator.ts');
            const cmdGen = join(cliDir, 'command-generator.ts');
            const entryGenResolved = resolveGenPath(entryGen);
            const cmdGenResolved = resolveGenPath(cmdGen);
            if (entryGenResolved && cmdGenResolved) {
                // Extract commands from spec — check each component for a commands section
                const components = spec.components || [];
                const componentList = Array.isArray(components)
                    ? components
                    : Object.entries(components).map(([name, c]) => ({ name, ...c }));
                let commands = [];
                let cliComponentVersion;
                for (const comp of componentList) {
                    if (comp.commands) {
                        commands = Array.isArray(comp.commands)
                            ? comp.commands
                            : Object.entries(comp.commands).map(([name, def]) => ({ name, ...def }));
                        cliComponentVersion = comp.version;
                        break;
                    }
                }
                if (commands.length > 0) {
                    const cliOutputDir = join(outputDir, 'backend', 'src', 'cli');
                    if (!existsSync(cliOutputDir))
                        mkdirSync(cliOutputDir, { recursive: true });
                    // Generate entry point
                    const { default: generateCLIEntry } = await import(entryGenResolved);
                    const entryCode = generateCLIEntry({ spec, commands, componentVersion: cliComponentVersion });
                    const entryPath = join(cliOutputDir, 'index.ts');
                    writeFileSync(entryPath, entryCode);
                    // Generate individual command files
                    const { default: generateCommand } = await import(cmdGenResolved);
                    const cmdOutputDir = join(cliOutputDir, 'commands');
                    if (!existsSync(cmdOutputDir))
                        mkdirSync(cmdOutputDir, { recursive: true });
                    // The root command's subcommands are the actual CLI commands
                    const rootCommand = commands[0];
                    const subcommands = rootCommand.subcommands || {};
                    let cmdCount = 0;
                    for (const [cmdName, cmdDef] of Object.entries(subcommands)) {
                        try {
                            const code = generateCommand({ spec, command: { name: cmdName, ...cmdDef } });
                            writeFileSync(join(cmdOutputDir, `${cmdName}.ts`), code);
                            cmdCount++;
                        }
                        catch (e) {
                            errors.push(`CLI ${cmdName}: ${e.message}`);
                        }
                    }
                    console.log(`   ✅ CLI: ${cmdCount} command(s) generated`);
                    files.push('index.ts', ...Object.keys(subcommands).map(n => `${n}.ts`));
                }
            }
        }
        catch (e) {
            errors.push(`CLI generation: ${e.message}`);
        }
        // 11. Developer tools (VSCode extension, MCP server)
        // Only generate when the manifest maps tools.vscode or tools.mcp capabilities
        try {
            const toolsDir = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'libs', 'instance-factories', 'tools', 'templates');
            // VSCode extension — only if manifest maps tools.vscode
            if (tryResolve('tools.vscode')) {
                const vscodeGen = resolveGenPath(join(toolsDir, 'vscode', 'vscode-extension-generator.ts'));
                if (vscodeGen) {
                    try {
                        const { default: generateVSCodeExtension } = await import(vscodeGen);
                        const result = generateVSCodeExtension({ spec, outputDir, models: allModels });
                        console.log(`   ✅ VSCode extension: ${result}`);
                    }
                    catch (e) {
                        errors.push(`VSCode: ${e.message}`);
                    }
                }
            }
            // MCP server — only if manifest maps tools.mcp
            if (tryResolve('tools.mcp')) {
                const mcpGen = resolveGenPath(join(toolsDir, 'mcp', 'mcp-server-generator.ts'));
                if (mcpGen) {
                    try {
                        const { default: generateMCPServer } = await import(mcpGen);
                        const result = generateMCPServer({ spec, outputDir, models: allModels });
                        console.log(`   ✅ MCP server: ${result}`);
                    }
                    catch (e) {
                        errors.push(`MCP: ${e.message}`);
                    }
                }
            }
        }
        catch (e) {
            errors.push(`Tools: ${e.message}`);
        }
        // 12. Ship assets (templates, examples, schema) from engine packages
        try {
            const assetsCopied = await this.copyAssets(outputDir);
            if (assetsCopied.length > 0) {
                console.log(`   ✅ Assets: ${assetsCopied.join(', ')}`);
            }
        }
        catch (e) {
            errors.push(`Assets: ${e.message}`);
        }
        // 13. UI contract tests (Phase 1 of the inference test framework).
        // Rules live in engines/src/inference/ui-contracts/. Each rule is
        // a pure function (spec) => TestCase[]; the translator renders each
        // rule's output into a single .spec.ts file under
        // generated/code/tests/contract/. A Playwright config and the
        // test:contract npm script are emitted alongside.
        //
        // GATED on the presence of a frontend factory — UI contract tests
        // exercise the frontend, so for backend-only templates we skip the
        // whole block (would otherwise drop tests/contract/ +
        // playwright.contract.config.ts into a project with no UI to test).
        //
        // ALSO gated on the frontend actually using @specverse/runtime: the
        // generated .spec.ts files import the Playwright harness from
        // `@specverse/runtime/test-harness`. The runtime-backed ReactAppRuntime
        // (Factory A) + app-demo have that dep; the standalone ReactAppStarter
        // (Factory B) deliberately does NOT (it ships self-contained code), so
        // the tests could never resolve the harness. Emitting them into a
        // standalone project just drops dead, unrunnable files. Factory B's
        // belongsTo/display resolution is covered by the runtime's own unit
        // tests (resolveEntityDisplayName / getEntityDisplayName), of which the
        // starter emits a port.
        const frontendUsesRuntime = (frontendResolved?.instanceFactory?.name || '') === 'ReactAppRuntime';
        if (frontendResolved && frontendUsesRuntime)
            try {
                const { generateContractTests, normalizeSpec } = await import('../inference/ui-contracts/index.js');
                const normalized = normalizeSpec(spec);
                const contractFiles = generateContractTests(normalized);
                if (contractFiles.length > 0) {
                    const contractDir = join(outputDir, 'tests', 'contract');
                    if (!existsSync(contractDir))
                        mkdirSync(contractDir, { recursive: true });
                    let totalTests = 0;
                    for (const f of contractFiles) {
                        writeFileSync(join(contractDir, f.filename), f.contents, 'utf-8');
                        totalTests += f.testCount;
                    }
                    // Playwright config — written once at the output root. Uses
                    // the tests/contract directory exclusively; behavioral tests
                    // (if any) live under tests/e2e/ and are handled by a
                    // separate playwright.behavioral.config.ts (not auto-generated).
                    //
                    // baseURL: the generation-time default comes from the manifest
                    // (`frontend.baseUrl` or `frontend.devUrl`), falling back to the
                    // vite dev-server default. The emitted config exposes
                    // `PLAYWRIGHT_BASE_URL` (the standard playwright env var name)
                    // as the runtime override — users running contract tests against
                    // a deployed server set that, not a SPECVERSE_-prefixed one.
                    const frontendDefault = this.manifest?.frontend?.baseUrl ??
                        this.manifest?.frontend?.devUrl ??
                        'http://localhost:5173';
                    const playwrightConfigPath = join(outputDir, 'playwright.contract.config.ts');
                    const playwrightConfig = `/**
 * Playwright config for auto-generated UI contract tests.
 * DO NOT EDIT — regenerated every \`spv realize\` run.
 *
 * Behavioral tests (hand-written) live under tests/e2e/ and use a
 * separate playwright.config.ts that you maintain yourself.
 */
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/contract',
  timeout: 30_000,
  expect: { timeout: 10_000 },
  reporter: 'list',
  use: {
    baseURL: process.env.PLAYWRIGHT_BASE_URL || '${frontendDefault}',
    trace: 'on-first-retry',
  },
  projects: [
    {
      name: 'chromium',
      use: { browserName: 'chromium' },
    },
  ],
});
`;
                    writeFileSync(playwrightConfigPath, playwrightConfig, 'utf-8');
                    console.log(`   ✅ UI contract tests: ${contractFiles.length} file(s), ${totalTests} test case(s)`);
                }
            }
            catch (e) {
                errors.push(`UI contract tests: ${e.message}`);
            }
        // 13b. Behavioral scenario tests (API-level use-case contracts).
        // Rules in engines/src/inference/scenario-contracts/ derive multi-entity
        // scenarios (lifecycle walks, cascade integrity, ...) from the spec and
        // emit vitest tests that drive the realized backend's CURVED endpoints via
        // a SELF-CONTAINED harness (consumption model B — emitted into the project,
        // so it needs no @specverse/engines dependency). API-level, so this is
        // GATED on a REST backend (api.rest) and needs no frontend.
        if (routeResolved)
            try {
                const { generateScenarioTests, renderScenarioHarness, HARNESS_FILENAME, normalizeSpec: normalizeForScenarios, } = await import('../inference/scenario-contracts/index.js');
                const scenarioFiles = generateScenarioTests(normalizeForScenarios(spec));
                if (scenarioFiles.length > 0) {
                    const scenarioDir = join(outputDir, 'tests', 'scenario');
                    if (!existsSync(scenarioDir))
                        mkdirSync(scenarioDir, { recursive: true });
                    // The self-contained harness — imported by the generated tests via
                    // ./_harness.js. No external deps.
                    writeFileSync(join(scenarioDir, HARNESS_FILENAME), renderScenarioHarness(), 'utf-8');
                    let totalScenarioTests = 0;
                    for (const f of scenarioFiles) {
                        writeFileSync(join(scenarioDir, f.filename), f.contents, 'utf-8');
                        totalScenarioTests += f.testCount;
                    }
                    const scenarioConfig = `/**
 * Vitest config for auto-generated behavioral scenario tests.
 * DO NOT EDIT — regenerated every \`spv realize\` run.
 *
 * Run with the backend up + reachable:
 *   SPECVERSE_API_URL=http://127.0.0.1:3000 npx vitest run --config vitest.scenario.config.ts
 */
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    include: ['tests/scenario/**/*.scenario.test.ts'],
    testTimeout: 30_000,
  },
});
`;
                    writeFileSync(join(outputDir, 'vitest.scenario.config.ts'), scenarioConfig, 'utf-8');
                    console.log(`   ✅ Scenario tests: ${scenarioFiles.length} file(s), ${totalScenarioTests} test case(s)`);
                }
            }
            catch (e) {
                errors.push(`Scenario tests: ${e.message}`);
            }
        // 14. Implementation conformance checks (Phase 5 of the inference
        // test framework). Static, grep-level checks that verify the
        // current React-Query-based implementation honors the design
        // contract. Runs in milliseconds, no browser needed. When the
        // runtime is rewritten in a different state library, this script
        // gets rewritten too — the design-contract Playwright tests above
        // are the stable layer, these checks are the short-lived ratchet
        // that enforces "no escape hatches in the current implementation".
        //
        // GATED on the presence of a frontend factory, same reason as the
        // UI contract tests block — the conformance checks grep frontend
        // source paths that don't exist in backend-only projects.
        if (frontendResolved)
            try {
                const scriptsDir = join(outputDir, 'scripts');
                if (!existsSync(scriptsDir))
                    mkdirSync(scriptsDir, { recursive: true });
                const conformanceScript = `#!/usr/bin/env node
/**
 * Implementation conformance checks — auto-generated.
 * DO NOT EDIT — regenerated every \`spv realize\` run.
 *
 * Verifies the current React-Query-based runtime implementation
 * doesn't have escape hatches that could violate the design
 * contract:
 *   1. no-raw-fetch-in-views — view components go through apiClient
 *   2. no-bare-usequery-in-views — view components use named hooks
 *   3. websocket-invalidates-cache — WS events trigger cache refetch
 *   4. mutation-hooks-invalidate — mutations invalidate after success
 *
 * Exits 1 on any failure. Fast (grep-level, no browser).
 */
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
let failed = false;

function walk(dir, pattern, exclude = []) {
  const hits = [];
  const visit = (d) => {
    let stat;
    try { stat = statSync(d); } catch { return; }
    if (!stat.isDirectory()) return;
    for (const entry of readdirSync(d)) {
      const p = join(d, entry);
      if (exclude.some(e => p.includes(e))) continue;
      let s;
      try { s = statSync(p); } catch { continue; }
      if (s.isDirectory()) visit(p);
      else if (/\\.(ts|tsx|js|jsx|mjs)$/.test(entry)) {
        const lines = readFileSync(p, 'utf-8').split('\\n');
        lines.forEach((line, i) => {
          if (pattern.test(line)) hits.push({ file: p, line: i + 1, text: line.trim() });
        });
      }
    }
  };
  visit(dir);
  return hits;
}

function report(name, ok, msg, hits) {
  console.log(\`\${ok ? '\\u2713' : '\\u2717'} \${name}\${msg ? ' \\u2014 ' + msg : ''}\`);
  if (!ok) {
    failed = true;
    if (hits) {
      for (const h of hits.slice(0, 5)) {
        console.log(\`    \${h.file}:\${h.line}  \${h.text.slice(0, 120)}\`);
      }
      if (hits.length > 5) console.log(\`    ... and \${hits.length - 5} more\`);
    }
  }
}

const frontendSrc = join(root, 'frontend', 'src');
const componentsDir = join(frontendSrc, 'components');
const pagesDir = join(frontendSrc, 'pages');

// 1. no-raw-fetch-in-views
{
  const hits = [
    ...walk(componentsDir, /\\bfetch\\s*\\(/),
    ...walk(pagesDir, /\\bfetch\\s*\\(/),
  ];
  report(
    'no-raw-fetch-in-views',
    hits.length === 0,
    hits.length === 0 ? '' : \`found \${hits.length} raw fetch() call(s) outside apiClient\`,
    hits
  );
}

// 2. no-bare-usequery-in-views — allow useQuery only inside hooks/useApi
{
  const hits = walk(frontendSrc, /\\buseQuery\\s*\\(/, ['hooks/useApi', 'lib/']);
  report(
    'no-bare-usequery-in-views',
    hits.length === 0,
    hits.length === 0 ? '' : \`found \${hits.length} bare useQuery() call(s) outside useApi\`,
    hits
  );
}

// 3. websocket-invalidates-cache — runtime must wire WS → invalidate
{
  const candidates = [
    join(root, 'node_modules', '@specverse', 'runtime', 'dist', 'runtime', 'views', 'react', 'hooks', 'useEntitySync.js'),
  ];
  let content = null;
  for (const path of candidates) {
    try { content = readFileSync(path, 'utf-8'); break; } catch { /* keep trying */ }
  }
  if (!content) {
    report('websocket-invalidates-cache', false, 'useEntitySync hook not found in @specverse/runtime');
  } else if (!/invalidateEntities/.test(content)) {
    report('websocket-invalidates-cache', false, 'useEntitySync does not call invalidateEntities on event receipt');
  } else {
    report('websocket-invalidates-cache', true, '');
  }
}

// 4. mutation-hooks-invalidate — useApi.ts must invalidate on success
{
  const useApiPath = join(frontendSrc, 'hooks', 'useApi.ts');
  let content = null;
  try { content = readFileSync(useApiPath, 'utf-8'); } catch { /* ignore */ }
  if (!content) {
    report('mutation-hooks-invalidate', false, 'frontend/src/hooks/useApi.ts not found');
  } else if (!/useExecuteOperationMutation/.test(content)) {
    report('mutation-hooks-invalidate', false, 'useExecuteOperationMutation not exported from useApi.ts');
  } else if (!/invalidateQueries/.test(content)) {
    report('mutation-hooks-invalidate', false, 'useApi.ts does not call invalidateQueries on mutation success');
  } else {
    report('mutation-hooks-invalidate', true, '');
  }
}

if (failed) {
  console.log('\\n\\u2717 conformance checks failed');
  process.exit(1);
}
console.log('\\n\\u2713 all conformance checks passed');
`;
                writeFileSync(join(scriptsDir, 'test-conformance.mjs'), conformanceScript, 'utf-8');
                console.log(`   ✅ UI contract conformance: scripts/test-conformance.mjs`);
            }
            catch (e) {
                errors.push(`UI conformance: ${e.message}`);
            }
        console.log(`\n✅ All code generated in: ${outputDir}`);
        if (errors.length) {
            console.warn(`⚠️  ${errors.length} warning(s) during generation`);
            for (const err of errors)
                console.warn(`  ${err}`);
        }
        // Record per-model factory mappings (level-1 attribution). The
        // controller-resolved factory IS the level-1 instance generator that
        // emits getter/setter/CURVED handlers per model.
        try {
            const ormFactoryName = this.resolver?.lastResolved?.['service.controller']?.instanceFactory?.name
                ?? 'unknown';
            for (const model of allModels) {
                auditRecorder.recordModelFactory({
                    modelFqn: model.name,
                    factory: ormFactoryName,
                });
            }
        }
        catch { /* tolerable */ }
        // Drain the recorder. Detach hook so subsequent realize calls in the
        // same process don't accumulate into this run's recorder.
        try {
            detachStepRecorder();
        }
        catch { /* ignore */ }
        try {
            const { decisions, coverage } = auditRecorder.build();
            writeFileSync(join(outputDir, 'realize-decisions.json'), JSON.stringify(decisions, null, 2));
            writeFileSync(join(outputDir, 'realize-coverage.json'), JSON.stringify(coverage, null, 2));
        }
        catch (e) {
            console.warn(`[audit] realize sidecar emit failed: ${e?.message ?? e}`);
        }
        return { files, errors };
    }
    /**
     * Copy static assets (templates, examples, schema) from engine packages
     * into the output directory so the generated project is self-contained.
     */
    async copyAssets(outputDir) {
        const copied = [];
        const copyDir = (src, dest, label) => {
            if (!existsSync(src))
                return;
            if (!existsSync(dest))
                mkdirSync(dest, { recursive: true });
            const copyRecursive = (s, d) => {
                for (const entry of readdirSync(s)) {
                    const srcPath = join(s, entry);
                    const destPath = join(d, entry);
                    if (statSync(srcPath).isDirectory()) {
                        if (!existsSync(destPath))
                            mkdirSync(destPath, { recursive: true });
                        copyRecursive(srcPath, destPath);
                    }
                    else {
                        copyFileSync(srcPath, destPath);
                    }
                }
            };
            copyRecursive(src, dest);
            copied.push(label);
        };
        // Assets live in engines/assets/ (co-located with this package)
        const enginesAssets = this.resolvePackageAssets('@specverse/engines', 'assets');
        if (enginesAssets) {
            copyDir(join(enginesAssets, 'examples'), join(outputDir, 'examples'), 'examples');
            copyDir(join(enginesAssets, 'prompts'), join(outputDir, 'prompts'), 'prompts');
        }
        // Copy composed schema from engine-entities (single source of truth)
        // into <output>/schema/. Was previously at <output>/backend/schema/
        // which broke standalone layouts (creates a dead backend/ dir in
        // backend-only / frontend-only projects). Schema is consumed by the
        // VSCode extension and MCP tooling, neither of which cares whether
        // it sits under backend/ — moving it up one level eliminates the
        // dead-dir problem without breaking any consumers.
        const thisDir = dirname(fileURLToPath(import.meta.url));
        const schemaCandidates = [];
        try {
            const { createRequire } = await import('module');
            const require = createRequire(import.meta.url);
            const entitiesPkg = dirname(require.resolve('@specverse/entities/package.json'));
            schemaCandidates.push(join(entitiesPkg, 'schema', 'SPECVERSE-SCHEMA.json'));
        }
        catch { /* engine-entities not available */ }
        schemaCandidates.push(join(thisDir, '..', 'schema', 'SPECVERSE-SCHEMA.json'), join(thisDir, '../..', 'schema', 'SPECVERSE-SCHEMA.json'));
        for (const schemaFile of schemaCandidates) {
            if (existsSync(schemaFile)) {
                const destSchema = join(outputDir, 'schema');
                if (!existsSync(destSchema))
                    mkdirSync(destSchema, { recursive: true });
                copyFileSync(schemaFile, join(destSchema, 'SPECVERSE-SCHEMA.json'));
                copied.push('schema');
                break;
            }
        }
        return copied;
    }
    resolvePackageAssets(packageName, subdir) {
        const thisDir = dirname(fileURLToPath(import.meta.url));
        const isSelf = packageName === '@specverse/engines' || packageName === '@specverse/engine-realize';
        const candidates = [];
        if (isSelf) {
            // For our own package, look relative to this file
            candidates.push(join(thisDir, '..', subdir));
            candidates.push(join(thisDir, '../..', subdir));
        }
        // Try via node_modules (workspace layout and npm install)
        for (let i = 2; i <= 5; i++) {
            const up = Array(i).fill('..').join('/');
            candidates.push(join(thisDir, up, 'node_modules', ...packageName.split('/'), subdir));
        }
        candidates.push(join(process.cwd(), 'node_modules', ...packageName.split('/'), subdir));
        for (const candidate of candidates) {
            if (existsSync(candidate))
                return candidate;
        }
        return null;
    }
}
export const engine = new SpecVerseRealizeEngine();
export default engine;
export { SpecVerseRealizeEngine };
//# sourceMappingURL=index.js.map