export { GrepOnlyBackend, CodeGraphBackend, GitNexusBackend, selectBackend, } from './backends/index.js';
export { extractTypeScriptPrisma, } from './adapters/index.js';
import { selectBackend } from './backends/index.js';
import { scopeFilterFacts } from './facts-scope.js';
import { extractTypeScriptPrisma } from './adapters/index.js';
import { extractTypeScriptDrizzle } from './adapters/index.js';
import { formatSuggestionsAsMarkdown } from './community-detection.js';
// Analyse-scope policy — the non-source noise set, shared by the FS walk and
// the `.gitnexusignore` written for gitnexus's index (single source, no drift).
export { renderGitnexusignore, ANALYSE_IGNORE_DIRS, ANALYSE_IGNORE_EXTS, isIgnoredDir, hasIgnoredExt, isAnalyseConfigFile, } from './scope.js';
// Source-version provenance — what tree was analysed (git commit, or null).
export { buildSourceVersion, readGitInfo } from './provenance.js';
// Attach-mode index detection — the analyse front-end uses this to choose
// attach-or-create-standard vs isolate.
export { hasExistingGitnexusIndex } from './backends/gitnexus.js';
// #27-A: method-body walker — per-method facts (assignments, guards,
// calls, throws, returns) for entity classes. Foundation for behavior
// step extraction (#27-B) and lifecycle transition graphs (#28-lifecycle B).
export { walkMethodBodies, } from './method-body-walker.js';
// #27-B: behavior step classifier — turns walker output into ordered
// CandidateStep timeline that the analyse LLM refines into spec
// behaviors[].steps[] blocks.
export { extractCandidateSteps, } from './behavior-step-classifier.js';
/**
 * Run the structural pre-pass on a source directory.
 *
 * Pipeline:
 *   1. Select / receive a backend (`StructuralPrepass`)
 *   2. Initialize + index the source dir
 *   3. Detect which adapters apply (e.g. schema.prisma exists → TS+Prisma)
 *   4. Run each detected adapter to extract framework-specific facts
 *   5. Merge facts into a single SpecVerse-vocabulary `SpecVerseFacts` object
 */
export async function runPrepass(sourceDir, options = {}) {
    const start = Date.now();
    let prepass = options.prepass ?? selectBackend(options.backend ?? 'auto', { attach: options.attach });
    await prepass.init(sourceDir);
    await prepass.index();
    // Auto-fallback: when 'auto' was used and the chosen backend (typically
    // GitNexus) finds zero classes after indexing, swap to grep-only.
    // GitNexus needs git working-tree state; small fixtures rsynced into
    // run dirs without .git history produce empty results — but grep-only
    // walks the filesystem directly and works.
    const usedAuto = !options.prepass && (options.backend === 'auto' || options.backend === undefined);
    if (usedAuto && prepass.constructor.name !== 'GrepOnlyBackend') {
        try {
            const probeClasses = await prepass.listClasses();
            if (probeClasses.length === 0) {
                const { GrepOnlyBackend } = await import('./backends/grep-only.js');
                prepass = new GrepOnlyBackend();
                await prepass.init(sourceDir);
                await prepass.index();
            }
        }
        catch {
            // listClasses unsupported by this backend — leave it alone.
        }
    }
    const adaptersRun = [];
    const facts = {
        entities: [],
        relationships: [],
        lifecycles: [],
        meta: {
            backend: prepass.constructor.name,
            backendCapabilities: { ...prepass.capabilities },
            adaptersRun: [],
            sourceDir,
            durationMs: 0,
        },
    };
    // Set when a complete-schema ORM adapter (Prisma / Drizzle) declared the
    // full table set. The typescript-interfaces class-walker is then suppressed
    // from MINTING new entities — it would otherwise promote service/utility
    // classes (scrapers, managers, coordinators) to entities. See the TSI block.
    let completeSchemaOrm = false;
    // Classes the ORM-precedence rule declines to promote as entities but which
    // may hold business logic (scrapers / coordinators that aren't ORM-backed).
    // The candidate-step walker walks these as SERVICES so their methods become
    // service operations instead of being dropped; method-less data shapes
    // produce empty timelines and fall away naturally.
    const suppressedServiceCandidates = [];
    // ── TypeScript + Prisma adapter (high-precision; uses schema.prisma directly) ──
    const prismaSchemas = await prepass.listFiles({ glob: '**/schema.prisma' });
    if (prismaSchemas.length > 0) {
        adaptersRun.push('typescript-prisma');
        const prisma = await extractTypeScriptPrisma(prepass);
        for (const ent of prisma.entities) {
            facts.entities.push({
                name: ent.name,
                filePath: ent.filePath,
                sourceFramework: 'prisma',
                attributes: ent.fields?.map((f) => ({
                    name: f.name,
                    declaredType: f.type,
                    isPrimary: f.isId,
                    isUnique: f.unique,
                    required: f.required,
                    auto: f.auto,
                })),
            });
            if (ent.lifecycleStates && ent.lifecycleField) {
                facts.lifecycles.push({
                    model: ent.name,
                    field: ent.lifecycleField,
                    states: ent.lifecycleStates,
                });
            }
        }
        for (const rel of prisma.relationships) {
            facts.relationships.push({
                from: rel.fromModel,
                to: rel.toModel,
                type: rel.type,
                cascade: rel.cascade,
            });
        }
        if (prisma.entities.length > 0)
            completeSchemaOrm = true;
        // Storage profile from the Prisma datasource provider (postgresql/sqlite/…
        // → relational, mongodb → document) so the deployment-emitter emits a
        // faithful storage type instead of the blanket `document` default.
        if (prisma.storageProfile) {
            facts.meta.storageProfile = prisma.storageProfile;
        }
    }
    // ── TypeScript + Drizzle adapter (high-precision; reads pgTable/… declarations) ──
    // Runs whenever Prisma didn't already supply the schema. Drizzle + Postgres
    // (Neon/Express) is one of the most common TS backend stacks; without this
    // the class-walker promotes scraper/service CLASSES to entities and loses
    // the real ORM tables + column types.
    if (facts.entities.length === 0) {
        const drizzle = await extractTypeScriptDrizzle(prepass);
        if (drizzle.entities.length > 0) {
            adaptersRun.push('typescript-drizzle');
            for (const ent of drizzle.entities) {
                facts.entities.push({
                    name: ent.name,
                    filePath: ent.filePath,
                    sourceFramework: 'drizzle',
                    attributes: ent.fields?.map((f) => ({
                        name: f.name,
                        declaredType: f.type,
                        isPrimary: f.isId,
                        isUnique: f.unique,
                        required: f.required,
                        auto: f.auto,
                    })),
                });
                if (ent.lifecycleStates && ent.lifecycleField) {
                    facts.lifecycles.push({
                        model: ent.name,
                        field: ent.lifecycleField,
                        states: ent.lifecycleStates,
                    });
                }
            }
            for (const rel of drizzle.relationships) {
                facts.relationships.push({
                    from: rel.fromModel,
                    to: rel.toModel,
                    type: rel.type,
                    cascade: rel.cascade,
                });
            }
            completeSchemaOrm = true;
            // Storage-profile signal for the deployment-emitter (relational ORM).
            if (drizzle.dialect) {
                facts.meta.storageProfile = { type: 'relational', provider: drizzle.dialect };
            }
        }
    }
    // ── Generic decorator-based fallback (covers Sequelize / TypeORM / NestJS /
    // Mongoose / JPA-when-TS) — runs only if no specific adapter produced
    // entities. Lower precision than a framework-specific adapter, dramatically
    // better than empty facts. Future: per-framework adapters register ahead
    // of this fallback for higher-fidelity output.
    if (facts.entities.length === 0) {
        const { extractTypeScriptDecorators } = await import('./adapters/typescript-decorators.js');
        const decorators = await extractTypeScriptDecorators(prepass);
        if (decorators.entities.length > 0) {
            adaptersRun.push('typescript-decorators');
            for (const ent of decorators.entities) {
                facts.entities.push({
                    name: ent.name,
                    filePath: ent.filePath,
                    sourceFramework: ent.framework,
                    attributes: ent.attributes?.map((a) => ({
                        name: a.name,
                        declaredType: a.declaredType,
                        isPrimary: a.isPrimary,
                        isUnique: a.isUnique,
                    })),
                });
            }
            for (const rel of decorators.relationships) {
                facts.relationships.push({
                    from: rel.from,
                    to: rel.to,
                    type: rel.type,
                });
            }
            for (const lc of decorators.lifecycles) {
                facts.lifecycles.push({
                    model: lc.model,
                    field: lc.field,
                    states: lc.states,
                    transitions: lc.transitions,
                });
            }
        }
    }
    // ── TypeScript-interfaces walker (Phase C, plan: 2026-05-04
    // ANALYSE-VIA-FAITHFUL-SKELETON-AND-MICROCALLS) — closes the adapter-
    // free shape gap. Runs CORE per user direction (always, not gated on
    // empty entities). Adapter-detected entities take precedence by name;
    // the TS-interface walker fills in any entities that adapters missed.
    // Non-entity declared types are recorded but NOT promoted to
    // facts.entities (TODO #49 logs the future types: schema slot work).
    if (options.extractInterfaces !== false) {
        const { extractTypeScriptInterfaces } = await import('./adapters/typescript-interfaces.js');
        try {
            const tsi = await extractTypeScriptInterfaces(prepass, {
                entityShapeHeuristic: options.entityShapeHeuristic ?? 'liberal',
            });
            if (tsi.entities.length > 0) {
                // Merge: skip entities already present (adapter-detected names win).
                const existingNames = new Set(facts.entities.map((e) => e.name));
                let added = 0;
                for (const ent of tsi.entities) {
                    if (existingNames.has(ent.name))
                        continue;
                    // ── ORM precedence (engines 6.97.0) ──
                    // When a complete-schema ORM adapter (Prisma / Drizzle) declared
                    // the entity set, the class-walker must NOT mint new entities: it
                    // would promote service/utility CLASSES (scrapers, managers,
                    // coordinators) that have no ORM table — the JobHunter
                    // over-extraction. It still contributes relationships/lifecycles
                    // for ORM-declared entities below. Adapter-free codebases (no ORM
                    // schema) keep the original gap-filling behaviour.
                    //
                    // A suppressed class still gets walked as a SERVICE (below) — its
                    // methods (e.g. JobBrowserAutomation's scraping logic) become
                    // service operations rather than being dropped. The candidate-step
                    // walker filters method-less data shapes out naturally.
                    if (completeSchemaOrm) {
                        suppressedServiceCandidates.push({ className: ent.name, filePath: ent.filePath });
                        continue;
                    }
                    facts.entities.push({
                        name: ent.name,
                        filePath: ent.filePath,
                        sourceFramework: 'typescript-interfaces',
                        attributes: ent.fields.map((f) => ({
                            name: f.name,
                            declaredType: f.declaredType,
                            isPrimary: f.name === 'id' || f.name === '_id',
                            required: !f.isOptional,
                        })),
                    });
                    existingNames.add(ent.name);
                    added++;
                }
                if (added > 0)
                    adaptersRun.push('typescript-interfaces');
                // The known-entity set after the merge — used to keep TSI-inferred
                // relationships/lifecycles from pointing at suppressed classes.
                const knownEntity = new Set(facts.entities.map((e) => e.name));
                // Merge relationships (skip duplicates by from+to+type).
                const seenRel = new Set(facts.relationships.map((r) => `${r.from}|${r.to}|${r.type}`));
                for (const r of tsi.relationships) {
                    // Under ORM precedence, drop a relationship touching a non-entity
                    // (a class the precedence rule declined to promote).
                    if (completeSchemaOrm && (!knownEntity.has(r.from) || !knownEntity.has(r.to)))
                        continue;
                    const key = `${r.from}|${r.to}|${r.type}`;
                    if (seenRel.has(key))
                        continue;
                    facts.relationships.push({ from: r.from, to: r.to, type: r.type });
                    seenRel.add(key);
                }
                // Merge lifecycles (engines 6.27.6+) — typescript-interfaces
                // adapter now infers lifecycles from literal-union type aliases
                // (`type Status = "a" | "b" | "c"`) referenced by entity
                // fields. Skip if a decorator-adapter (or earlier walker)
                // already produced a lifecycle for the same model — those are
                // higher-fidelity signals (carry transitions from method bodies).
                const seenLifecycles = new Set(facts.lifecycles.map((l) => l.model));
                for (const lc of tsi.lifecycles) {
                    if (seenLifecycles.has(lc.model))
                        continue;
                    // Under ORM precedence, only attach lifecycles to ORM-declared entities.
                    if (completeSchemaOrm && !knownEntity.has(lc.model))
                        continue;
                    facts.lifecycles.push({
                        model: lc.model,
                        field: lc.field,
                        states: lc.states,
                        transitions: lc.transitions,
                    });
                    seenLifecycles.add(lc.model);
                }
            }
        }
        catch (e) {
            // TS-interfaces walker is best-effort; an error here doesn't
            // stop the rest of the prepass.
        }
    }
    // ── naming-convention FK inference (engines 6.47.0, 2026-05-12) —
    // post-extraction pass over the merged entity set. Walks each entity's
    // attributes for `*_id` / `*Id` patterns and resolves the prefix
    // against known entity names (singular + plural). Closes the gap
    // surfaced on scoremyclays where PowerSync `column.text` carries no
    // FK metadata and the structural adapters produced 0 relationships.
    // Higher-fidelity adapters win via the existing-relationships dedup.
    if (options.inferNamingConventionFks !== false && facts.entities.length > 0) {
        try {
            const { inferNamingConventionFks } = await import('./adapters/naming-convention-fks.js');
            const inferred = inferNamingConventionFks(facts.entities, facts.relationships);
            if (inferred.relationships.length > 0) {
                adaptersRun.push('naming-convention-fks');
                for (const rel of inferred.relationships) {
                    facts.relationships.push({
                        from: rel.from,
                        to: rel.to,
                        type: rel.type,
                    });
                }
            }
        }
        catch (e) {
            // Best-effort like the other inference passes.
        }
    }
    // ── Date-type fidelity (GUI Tier 2) — upgrade String date attributes to
    // DateTime from the SQL DDL (+ *_at / *_date name conventions), so the
    // generated form renders a date picker not a text box. Runs after entity
    // extraction, before the view gate.
    if (options.refineDateTypes !== false && facts.entities.length > 0) {
        try {
            const { refineDateTypes } = await import('./date-type-refinement.js');
            const upgraded = refineDateTypes(facts, sourceDir);
            if (upgraded.length > 0)
                adaptersRun.push('date-type-refinement');
        }
        catch (e) {
            // Best-effort — never abort the prepass.
        }
    }
    // ── #91b view-layer reachability gate — runs after ALL entity extraction
    // + FK inference, before component suggestions. Drops TSI-extracted
    // entities reachable ONLY from frontend-app subtrees (view-layer render /
    // editor / config shapes the #91 presentational gate's conservative scope
    // left behind). No-op unless there's a genuine multi-package
    // frontend/backend split, so single-package apps are untouched.
    if (options.gateViewOnlyEntities !== false && facts.entities.length > 0) {
        try {
            const { gateViewOnlyEntities } = await import('./view-reachability.js');
            const dropped = gateViewOnlyEntities({ facts, sourceDir });
            if (dropped.length > 0) {
                adaptersRun.push('view-reachability-gate');
            }
        }
        catch (e) {
            // Best-effort — a failure here must never abort the prepass.
        }
    }
    // ── Candidate-step extraction (#27-B/C) — runs INDEPENDENTLY of
    // entity detection. Pre-fix this block lived inside the
    // typescript-decorators guard, which meant adapter-less codebases
    // (idle-meta-style: raw mongodb driver, no @Entity decorators)
    // produced ZERO candidate-step timelines even when their
    // controllers/services had rich method bodies. Now the walker is
    // unconditional — runs on every Controller/Service/Handler/Engine/
    // Manager/Evaluator/Adapter/Cache/Provider/Factory class the
    // backend can list.
    if (options.extractCandidateSteps !== false) {
        const { extractCandidateSteps } = await import('./behavior-step-classifier.js');
        const candidateMethods = [];
        const fileSourceCache = new Map();
        const recordAudit = options.recordAuditDecisions !== false;
        const auditClasses = [];
        const targets = [];
        const seenTargetKeys = new Set();
        const addTarget = (t) => {
            const key = `${t.filePath}::${t.className}`;
            if (seenTargetKeys.has(key))
                return;
            seenTargetKeys.add(key);
            targets.push(t);
        };
        // 1. Detected entities (always walked when present — adapters give
        //    us the lifecycle-field hint here).
        for (const ent of facts.entities) {
            addTarget({ className: ent.name, filePath: ent.filePath, isEntity: true });
        }
        // 2. Service-shaped classes by name. Broader than just `*Service` —
        //    real-world codebases use a variety of naming conventions for
        //    classes that hold business logic (idle-meta has CalculationEngine,
        //    FormulaEvaluator, MongoDBAdapter, none of which would match the
        //    old `*Service` filter). The class-DTO / class-utility distinction
        //    is enforced by the classifier itself: methods with no recognised
        //    candidates produce empty timelines and are filtered out
        //    downstream.
        try {
            const allClasses = await prepass.listClasses();
            for (const cls of allClasses) {
                if (!cls.filePath || !cls.name)
                    continue;
                if (/(?:Controller|Service|Handler|Engine|Manager|Evaluator|Adapter|Cache|Provider|Factory|Repository|Store|Resolver|Worker|Job|Listener|Subscriber)$/.test(cls.name)) {
                    addTarget({ className: cls.name, filePath: cls.filePath, isEntity: false });
                }
            }
        }
        catch { /* listClasses may fail; entity-only fallback is fine */ }
        // 3. Classes the ORM-precedence rule suppressed from becoming entities
        //    (e.g. JobBrowserAutomation/CachedJobTitle — scraping coordinators
        //    with no ORM table). Walk them as services so their business logic
        //    surfaces as service operations instead of vanishing. Method-less
        //    data shapes classify to empty timelines and are filtered out below.
        for (const c of suppressedServiceCandidates) {
            addTarget({ className: c.className, filePath: c.filePath, isEntity: false });
        }
        for (const target of targets) {
            let source = fileSourceCache.get(target.filePath);
            if (source === undefined) {
                try {
                    source = await prepass.fileSourceText(target.filePath);
                }
                catch {
                    source = '';
                }
                fileSourceCache.set(target.filePath, source);
            }
            if (!source)
                continue;
            // Lifecycle field hint — only available when an adapter detected
            // it on this entity. Controllers/services don't own state, so
            // this stays undefined for them (assignments don't get classified
            // as `transition` steps).
            const lifecycleField = target.isEntity
                ? facts.lifecycles.find((lc) => lc.model === target.className)?.field
                : undefined;
            const methods = extractCandidateSteps(source, lifecycleField ? { lifecycleField } : {});
            // Per-method audit record + filter pass with reason capture.
            const perMethodDecisions = methods.map((m) => {
                const filter = m.candidates.length === 0
                    ? { kept: false, reason: 'no-candidates-classified' }
                    : evaluateMethodFilter(m);
                return { method: m, filter };
            });
            const populated = perMethodDecisions
                .filter((p) => p.filter.kept)
                .map((p) => p.method);
            if (populated.length > 0) {
                candidateMethods.push({
                    filePath: target.filePath,
                    entityName: target.className,
                    methods: populated,
                });
            }
            if (recordAudit) {
                const methodEntries = perMethodDecisions.map(({ method, filter }) => {
                    const histogram = {};
                    for (const c of method.candidates) {
                        histogram[c.kind] = (histogram[c.kind] ?? 0) + 1;
                    }
                    return {
                        methodName: method.methodName,
                        isAsync: method.isAsync,
                        sourceLineRange: method.sourceLineRange,
                        candidateKindHistogram: histogram,
                        filter: filter,
                    };
                });
                auditClasses.push({
                    className: target.className,
                    filePath: target.filePath,
                    decision: { walked: true, reason: target.isEntity ? 'adapter-detected entity' : 'matches service-shaped name pattern' },
                    methodCount: { raw: methods.length, kept: populated.length },
                    methods: methodEntries,
                });
            }
        }
        // ── Express-routes adapter (engines 6.27.1+) ──
        // Class-based walk above misses HTTP routing for codebases that use
        // Express router files instead of `*Controller` classes (idle-meta:
        // `routes/auth.ts` etc). Each route file becomes a synthetic
        // controller; each `router.<verb>(path, handler)` call becomes one
        // action.
        try {
            const { isExpressRouterFile, parseExpressRoutes, synthesizeControllerClass, deriveControllerName } = await import('./adapters/express-routes.js');
            // listFiles' glob doesn't support brace-expansion ({ts,js,...}),
            // so request `**/routes/**` then filter extensions in code.
            const allRouteFiles = await prepass.listFiles({ glob: '**/routes/**' });
            const candidateRouteFiles = allRouteFiles.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f));
            for (const rf of candidateRouteFiles) {
                let source;
                try {
                    source = await prepass.fileSourceText(rf);
                }
                catch {
                    continue;
                }
                if (!source)
                    continue;
                if (!isExpressRouterFile(source))
                    continue;
                const handlers = parseExpressRoutes(source);
                if (handlers.length === 0)
                    continue;
                const controllerName = deriveControllerName(rf);
                // Skip if a class with this name was already walked (avoids
                // double emission when both a route file `auth.ts` and a class
                // `AuthController` exist in source).
                if (seenTargetKeys.has(`${rf}::${controllerName}`))
                    continue;
                const synthClassBody = synthesizeControllerClass(controllerName, handlers);
                const classifierMethods = extractCandidateSteps(synthClassBody);
                const classifierByName = new Map(classifierMethods.map((m) => [m.methodName, m]));
                // The class-body walker is `this.X`-oriented; HTTP route bodies
                // typically have free-standing calls (`schema.parse(req.body)`,
                // `res.json(...)`). When the classifier finds 0 candidates we
                // still keep the handler — the per-action microcall LLM will
                // author the body from the source-location pointer. Synthesize
                // one minimal candidate so the method shape stays consistent
                // and the filter doesn't drop it.
                const remapped = handlers.map((h) => {
                    const c = classifierByName.get(h.methodName);
                    const candidates = c?.candidates ?? [];
                    const finalCandidates = candidates.length > 0 ? candidates : [{
                            kind: 'call',
                            position: 0,
                            summary: `HTTP ${h.httpVerb.toUpperCase()} ${h.routePath} (LLM-authored body via microcall)`,
                        }];
                    return {
                        methodName: h.methodName,
                        isAsync: h.isAsync,
                        body: c?.body ?? h.body,
                        sourceLineRange: h.sourceLineRange,
                        candidates: finalCandidates,
                    };
                });
                const perMethodDecisions = remapped.map((m) => {
                    // express-routes path always keeps handlers (HTTP routes are
                    // inherently real action surfaces). Skip the standard filter.
                    return { method: m, filter: { kept: true } };
                });
                const populated = perMethodDecisions
                    .filter((p) => p.filter.kept)
                    .map((p) => p.method);
                if (populated.length > 0) {
                    candidateMethods.push({
                        filePath: rf,
                        entityName: controllerName,
                        methods: populated,
                    });
                }
                if (recordAudit) {
                    const methodEntries = perMethodDecisions.map(({ method, filter }) => {
                        const histogram = {};
                        for (const c of method.candidates)
                            histogram[c.kind] = (histogram[c.kind] ?? 0) + 1;
                        return {
                            methodName: method.methodName,
                            isAsync: method.isAsync,
                            sourceLineRange: method.sourceLineRange,
                            candidateKindHistogram: histogram,
                            filter: filter,
                        };
                    });
                    auditClasses.push({
                        className: controllerName,
                        filePath: rf,
                        decision: { walked: true, reason: 'express-routes adapter (synthesized controller from Router())' },
                        methodCount: { raw: remapped.length, kept: populated.length },
                        methods: methodEntries,
                    });
                }
                if (!adaptersRun.includes('express-routes'))
                    adaptersRun.push('express-routes');
            }
        }
        catch { /* express-routes adapter is best-effort; continue */ }
        // ── React/JSX views adapter ──────────────────────────────────────
        // Walk *.tsx / *.jsx files and detect React component declarations
        // (function, arrow, class, default-export). Each becomes one entry
        // in facts.views[]. The skeleton emitter consumes these to populate
        // per-component `views:` blocks. Closes the gap that prior adapters
        // left open: zero JSX awareness, so React/RN apps came out empty.
        try {
            const { isReactViewFile, parseReactViews } = await import('./adapters/react-views.js');
            const allTsxJsx = (await prepass.listFiles({ glob: '**/*.tsx' }))
                .concat(await prepass.listFiles({ glob: '**/*.jsx' }));
            const collectedViews = [];
            const seenView = new Set(); // cross-file dedup by name+path
            for (const vf of allTsxJsx) {
                if (!isReactViewFile(vf))
                    continue;
                let source;
                try {
                    source = await prepass.fileSourceText(vf);
                }
                catch {
                    continue;
                }
                if (!source)
                    continue;
                const result = parseReactViews(vf, source);
                for (const v of result.views) {
                    // #91 / #94 defect 1 — view precision. Context providers and
                    // framework wrappers (`NextAuthProvider`, `<Context.Provider>`)
                    // are infrastructure, never domain views. Drop them so they don't
                    // inflate the `views:` block. Conservative: gate only the
                    // unambiguous infra shapes (provider viewType + Provider/Context
                    // name suffix), leave presentational primitives alone.
                    if (v.viewType === 'provider' || /(?:Provider|Context)$/.test(v.name)) {
                        continue;
                    }
                    const key = `${v.filePath}::${v.name}`;
                    if (seenView.has(key))
                        continue;
                    seenView.add(key);
                    collectedViews.push({
                        name: v.name,
                        filePath: v.filePath,
                        sourceLineRange: v.sourceLineRange,
                        kind: v.kind,
                        isScreen: v.isScreen,
                        ...(v.docstring ? { docstring: v.docstring } : {}),
                        ...(v.viewType ? { viewType: v.viewType } : {}),
                        ...(v.usesComponents && v.usesComponents.length > 0 ? { usesComponents: v.usesComponents } : {}),
                        ...(v.actions && v.actions.length > 0 ? { actions: v.actions } : {}),
                        ...(v.emptyState ? { emptyState: true } : {}),
                    });
                }
            }
            if (collectedViews.length > 0) {
                // Part 2 (#GUI-uplift) — view precision: keep the source's real
                // screens, drop UI primitives (components/ui/…), layout/infra, and
                // sub-components (imported by another non-page component). And when the
                // source renders a Timeline/Calendar over a date-range model with a
                // belongsTo resource, emit a renderable `<Resource>Timeline` view bound
                // to that model (the runtime resolver derives the rest of the binding).
                if (options.refineViews !== false) {
                    try {
                        const { refineViews } = await import('./view-precision.js');
                        const refined = refineViews({ views: collectedViews, entities: facts.entities, relationships: facts.relationships, sourceDir });
                        facts.views = refined.views;
                        if (refined.addedTimeline)
                            adaptersRun.push('view-timeline-inference');
                    }
                    catch {
                        facts.views = collectedViews;
                    }
                }
                else {
                    facts.views = collectedViews;
                }
                if (!adaptersRun.includes('react-views'))
                    adaptersRun.push('react-views');
            }
        }
        catch { /* react-views adapter is best-effort; continue */ }
        if (candidateMethods.length > 0) {
            facts.candidateMethods = candidateMethods;
        }
        if (recordAudit) {
            // Aggregate stats across walker output.
            const stats = {
                classesWalked: auditClasses.length,
                methodsRaw: 0,
                methodsKept: 0,
                methodsByDropReason: {},
                candidateStepLines: 0,
                candidateStepKindHistogram: {},
            };
            for (const cls of auditClasses) {
                stats.methodsRaw += cls.methodCount.raw;
                stats.methodsKept += cls.methodCount.kept;
                for (const m of cls.methods) {
                    if (!m.filter.kept) {
                        stats.methodsByDropReason[m.filter.reason] =
                            (stats.methodsByDropReason[m.filter.reason] ?? 0) + 1;
                    }
                    for (const [kind, count] of Object.entries(m.candidateKindHistogram)) {
                        stats.candidateStepLines += count;
                        stats.candidateStepKindHistogram[kind] =
                            (stats.candidateStepKindHistogram[kind] ?? 0) + count;
                    }
                }
            }
            facts.auditDecisions = {
                schemaVersion: '1.0',
                classes: auditClasses,
                stats,
            };
        }
    }
    // Future framework-specific adapters slot in BEFORE the decorator
    // fallback: typescript-typeorm, typescript-sequelize, typescript-drizzle.
    // Each detects its applicability and produces higher-precision output
    // than the generic fallback.
    facts.meta.adaptersRun = adaptersRun;
    // ── Component suggestions ──
    // Three sources, ordered by AUTHOR-STATED-INTENT precedence:
    //   1. Structural detector (HIGHEST signal) — author-stated boundaries
    //      from sub-package.json files OR apps/ services/ modules/ subdirs.
    //      idle-meta is a 168-file monorepo: apps/idle-clicker-mobile,
    //      apps/mythology-mobile, packages/idle-data, etc. — these dirs ARE
    //      the architectural decomposition.
    //   2. Entity-graph Louvain — clusters the relationship graph when
    //      ORM adapters extracted ≥5 entities. Useful for single-package
    //      apps where structural signal is absent (cal-com, twenty).
    //   3. Empty (LLM falls back to its own judgement).
    //
    // Prior versions (≤6.26.5) ran (1) only when entities < 5 — for
    // monorepos with rich entity counts (idle-meta has 24), it skipped
    // straight to (2) which collapsed everything to 2 generic-named
    // buckets. Structural detection on monorepos is strictly better
    // signal: package names like `idle-clicker-mobile` are author-chosen
    // architectural identifiers; Louvain on a synthetic single-package
    // graph is just clustering noise.
    if (options.suggestComponents !== false) {
        const { detectStructuralComponents } = await import('./structural-detector.js');
        const structural = detectStructuralComponents(sourceDir);
        if (structural.length >= 2) {
            // Assign entities to structural buckets by filePath prefix.
            // Each entity has filePath (from the walker); match against
            // each component's structural.sourceDir. If no prefix matches,
            // the entity falls through to the Unassigned bucket downstream.
            const sortedByDirLen = [...structural].sort((a, b) => (b.structural?.sourceDir?.length ?? 0) - (a.structural?.sourceDir?.length ?? 0));
            for (const ent of facts.entities) {
                const home = sortedByDirLen.find((c) => c.structural?.sourceDir && ent.filePath.startsWith(c.structural.sourceDir));
                if (home)
                    (home.entities ??= []).push(ent.name);
            }
            facts.suggestedComponents = structural;
        }
        else if (prepass.capabilities?.componentClustering && prepass.clusterFiles) {
            // Engines 6.29.5+: prefer pre-computed clustering from a backend
            // that supports it (GitNexus's Leiden communities) over re-running
            // in-process Louvain on the entity-only graph. Higher-fidelity
            // algorithm + richer graph (call-graph not just entity-edges).
            // The 2026-05-04 source-tree-topology change deliberately took this
            // priority for monorepos (where structural >= 2 fires); for single-
            // package codebases (cal-com, twenty, nestjs-billing) the previous
            // fallback to in-process Louvain ignored canonical signal already
            // sitting in the backend's index.
            try {
                const clusters = await prepass.clusterFiles();
                const sanitizedName = (id) => id.replace(/\s+/g, '').replace(/[^A-Za-z0-9_]/g, '') || id;
                const suggestions = [];
                for (const cluster of clusters) {
                    if (cluster.files.length === 0)
                        continue;
                    const fileSet = new Set(cluster.files);
                    const entityNames = facts.entities
                        .filter((e) => fileSet.has(e.filePath))
                        .map((e) => e.name);
                    if (entityNames.length === 0)
                        continue; // skip type-only / non-entity clusters
                    suggestions.push({
                        suggestedName: sanitizedName(cluster.id),
                        entities: entityNames,
                        crossEdges: [],
                        internalEdgeCount: 0,
                    });
                }
                if (suggestions.length > 0) {
                    facts.suggestedComponents = suggestions;
                }
                else if (facts.entities.length >= 5) {
                    // Backend produced no useful clusters (e.g. all type-only) —
                    // fall through to in-process Louvain.
                    const { detectModelCommunities } = await import('./community-detection.js');
                    facts.suggestedComponents = detectModelCommunities(facts.entities, facts.relationships);
                }
            }
            catch {
                // Backend's clusterFiles failed unexpectedly — fall back gracefully.
                if (facts.entities.length >= 5) {
                    const { detectModelCommunities } = await import('./community-detection.js');
                    facts.suggestedComponents = detectModelCommunities(facts.entities, facts.relationships);
                }
            }
        }
        else if (facts.entities.length >= 5) {
            const { detectModelCommunities } = await import('./community-detection.js');
            facts.suggestedComponents = detectModelCommunities(facts.entities, facts.relationships);
        }
        else {
            // Fewer than 5 entities AND no structural signal — leave empty;
            // the skeleton emitter uses the default-Application fallback.
        }
    }
    // ── Per-method fact sheets (TODO #32) ──
    // Gather MethodFactSheet records for entry-point methods only — heuristic:
    // methods on classes whose name ends in `Controller` or `Service`. Capped
    // at `methodFactsCap` (default 30) to keep prompt context bounded.
    // Backends that can't extract method details just return null and we skip
    // — the field comes back as an empty list, no error.
    if (options.gatherMethodFacts !== false) {
        const cap = options.methodFactsCap ?? 30;
        const sheets = [];
        try {
            const methods = await prepass.listMethods();
            const entryPoints = methods.filter((m) => {
                const className = m.qualifiedName.split('.')[0];
                return /Controller$|Service$/.test(className);
            });
            // Stable order — sort by qualifiedName so re-runs produce identical output
            entryPoints.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName));
            for (const method of entryPoints.slice(0, cap)) {
                try {
                    const sheet = await prepass.getMethodDetails(method.qualifiedName);
                    if (sheet)
                        sheets.push(sheet);
                }
                catch {
                    // Skip individual method failures — partial results are still useful.
                }
            }
        }
        catch {
            // Backend doesn't support listMethods deeply — skip silently.
        }
        if (sheets.length > 0)
            facts.methodFactSheets = sheets;
    }
    // ── Engines 6.30.0+ rich-signal pulls ────────────────────────────────
    // Each gated by capability flag — backends that don't support the
    // signal stay silent, adapters check facts.X for presence.
    if (prepass.capabilities?.routeExtraction && prepass.listRoutes) {
        try {
            const routes = await prepass.listRoutes();
            if (routes.length > 0) {
                facts.routes = routes;
                if (!adaptersRun.includes('backend-routes'))
                    adaptersRun.push('backend-routes');
            }
        }
        catch { /* best-effort */ }
    }
    if (prepass.capabilities?.implementsExtraction && prepass.listImplementsRelationships) {
        try {
            const rels = await prepass.listImplementsRelationships();
            if (rels.length > 0) {
                facts.implementsRelationships = rels;
                if (!adaptersRun.includes('backend-implements'))
                    adaptersRun.push('backend-implements');
            }
        }
        catch { /* best-effort */ }
    }
    if (prepass.capabilities?.validationSchemaExtraction && prepass.listConstSchemas) {
        try {
            const schemas = await prepass.listConstSchemas();
            if (schemas.length > 0) {
                facts.constSchemas = schemas;
                if (!adaptersRun.includes('backend-schemas'))
                    adaptersRun.push('backend-schemas');
                // Engines 6.30.2+ — TODO #51 phase 2B: merge Zod field declarations
                // into matching entity attributes. `<EntityName>Schema` const names
                // map to existing entities; we add fields that aren't already
                // declared (don't override TSI / decorator output).
                try {
                    const { mergeZodSchemasIntoEntities } = await import('./adapters/zod-schemas.js');
                    const merged = mergeZodSchemasIntoEntities({
                        schemas: facts.constSchemas,
                        entities: facts.entities,
                    });
                    if (merged.fieldsAdded > 0) {
                        if (!adaptersRun.includes('zod-schemas'))
                            adaptersRun.push('zod-schemas');
                    }
                }
                catch { /* zod-merge is best-effort */ }
            }
        }
        catch { /* best-effort */ }
    }
    if (prepass.capabilities?.processExtraction && prepass.listProcesses) {
        try {
            const procs = await prepass.listProcesses();
            // Filter to non-trivial processes — single-step "processes" are noise.
            const significant = procs.filter((p) => p.stepCount >= 3);
            if (significant.length > 0) {
                facts.processes = significant;
                if (!adaptersRun.includes('backend-processes'))
                    adaptersRun.push('backend-processes');
            }
        }
        catch { /* best-effort */ }
    }
    // ── module-functions adapter (engines 6.48.0, NORMALISE Phase 0) ──
    // Free-function process terminals → synthetic service classes.
    // PowerSync / Drizzle / Supabase-queries-style codebases carry their
    // business logic in modules of exported async functions, not classes.
    // GitNexus's process extraction (above) traces the cross-method flows
    // and records each terminal as `Function:<filePath>:<fnName>`, but
    // the candidate-step extractor only walked CLASSES — so the function
    // terminals were invisible to the skeleton emitter and the resulting
    // spec had 0 controllers / 0 services on these codebases.
    //
    // This block walks facts.processes for function terminals, groups by
    // file, and synthesises one service class per file (same pattern as
    // express-routes). The synthetic body feeds the existing classifier
    // so module-function services get pre-classified timelines.
    //
    // De-overlap: skip a (file, fnName) pair already covered by an entry
    // in facts.candidateMethods (class methods + express-routes synth
    // controllers win).
    if (options.extractCandidateSteps !== false &&
        facts.processes &&
        facts.processes.length > 0) {
        try {
            const { extractCandidateSteps } = await import('./behavior-step-classifier.js');
            const { groupTerminalsByFile, findExportedFunctionBody, synthesizeServiceClass, deriveServiceName, detectClientSideModule, } = await import('./adapters/module-functions.js');
            // Build the "already covered" set from existing candidateMethods.
            const coveredByFile = new Map();
            for (const cm of facts.candidateMethods ?? []) {
                let set = coveredByFile.get(cm.filePath);
                if (!set) {
                    set = new Set();
                    coveredByFile.set(cm.filePath, set);
                }
                for (const m of cm.methods)
                    set.add(m.methodName);
            }
            const coveredServiceNamesByFile = new Map();
            for (const cm of facts.candidateMethods ?? []) {
                let set = coveredServiceNamesByFile.get(cm.filePath);
                if (!set) {
                    set = new Set();
                    coveredServiceNamesByFile.set(cm.filePath, set);
                }
                set.add(cm.entityName);
            }
            const { join: pathJoin, isAbsolute: pathIsAbsolute } = await import('node:path');
            const resolveSourcePath = (p) => pathIsAbsolute(p) ? p : pathJoin(sourceDir, p);
            const fileFileSourceCache = new Map();
            const grouped = groupTerminalsByFile(facts.processes);
            for (const [filePath, fnNames] of grouped) {
                const covered = coveredByFile.get(filePath) ?? new Set();
                const uncovered = [...fnNames].filter((fn) => !covered.has(fn));
                if (uncovered.length === 0)
                    continue;
                let source = fileFileSourceCache.get(filePath);
                if (source === undefined) {
                    // Terminal-id paths from GitNexus are relative to the project
                    // root; resolve against sourceDir before passing to fileSourceText
                    // (the GitNexus backend's implementation is just readFileSync,
                    // which requires absolute paths).
                    try {
                        source = await prepass.fileSourceText(resolveSourcePath(filePath));
                    }
                    catch {
                        source = '';
                    }
                    fileFileSourceCache.set(filePath, source);
                }
                if (!source)
                    continue;
                const functions = [];
                for (const fnName of uncovered) {
                    const found = findExportedFunctionBody(source, fnName);
                    if (!found)
                        continue;
                    functions.push({ ...found, methodName: fnName });
                }
                if (functions.length === 0)
                    continue;
                // Skip client-side modules (React Native, browser SPAs). The
                // module-functions synthesiser turns module-exported functions
                // into backend Service classes with TARGET RUNTIME: Node.js —
                // wrong for RN client storage (AsyncStorage etc.) or browser
                // window/document code. Conservative detection: only strong
                // client-only signals trigger the skip. (2026-05-15 fix —
                // surfaced by idle-meta × marrbox's TokenStorageService
                // STUB002 in the 9-cell comparison matrix.)
                const clientSide = detectClientSideModule(source);
                if (clientSide.client) {
                    if (process.env.SPECVERSE_VERBOSE) {
                        console.error(`   ⏩ Skipped module-functions synthesis for ${filePath}: ` +
                            `client-side signals [${clientSide.signals.join(', ')}]`);
                    }
                    continue;
                }
                const serviceName = deriveServiceName(filePath);
                const existingServices = coveredServiceNamesByFile.get(filePath) ?? new Set();
                if (existingServices.has(serviceName))
                    continue;
                const synthBody = synthesizeServiceClass(serviceName, functions);
                const classifierMethods = extractCandidateSteps(synthBody);
                // Keep methods with at least one classified candidate. Methods
                // where the classifier produced no candidates would emit empty
                // timelines and waste microcall slots downstream.
                const populated = classifierMethods.filter((m) => m.candidates.length > 0);
                if (populated.length === 0)
                    continue;
                if (!facts.candidateMethods)
                    facts.candidateMethods = [];
                facts.candidateMethods.push({
                    filePath,
                    entityName: serviceName,
                    methods: populated,
                });
                if (!adaptersRun.includes('module-functions')) {
                    adaptersRun.push('module-functions');
                }
            }
        }
        catch { /* module-functions is best-effort */ }
    }
    // Engines 6.32.6+ — cross-component imports graph. Walks each
    // component's source files for `import { ... } from '...'` and
    // resolves to the owning component sourceDir; surfaces the consumer
    // → library dependency that the source has but the spec previously
    // didn't model. See `imports-graph.ts` for matching rules.
    // Best-effort: if file reads fail we leave the field unset.
    if ((facts.suggestedComponents?.length ?? 0) > 1) {
        try {
            const { buildImportsByComponentFromBackend } = await import('./imports-graph.js');
            const importsByComponent = await buildImportsByComponentFromBackend(facts, prepass);
            if (Object.keys(importsByComponent).length > 0) {
                facts.importsByComponent = importsByComponent;
                if (!adaptersRun.includes('imports-graph'))
                    adaptersRun.push('imports-graph');
            }
        }
        catch { /* best-effort — imports remain unset */ }
    }
    // V2 Phase 2 (2026-05-08): non-library dependency hints — subprocess,
    // service clients, messaging crossings, managed-SDK init. Runs even
    // for single-component codebases (unlike imports-graph which needs
    // multiple components to find cross-component edges) because a
    // single-component spec can still depend on Stripe / kafka / etc.
    if ((facts.suggestedComponents?.length ?? 0) >= 1) {
        try {
            const { buildNonLibraryImportsByComponentFromBackend } = await import('./imports-graph.js');
            const nonLibraryImportsByComponent = await buildNonLibraryImportsByComponentFromBackend(facts, prepass);
            if (Object.keys(nonLibraryImportsByComponent).length > 0) {
                facts.nonLibraryImportsByComponent = nonLibraryImportsByComponent;
                if (!adaptersRun.includes('non-library-imports'))
                    adaptersRun.push('non-library-imports');
            }
        }
        catch { /* best-effort — hints remain unset */ }
    }
    // Facts-layer scoping (`--scope`) — applied after extraction so it works
    // even when attaching to a full index.
    if (options.scope && options.scope.length > 0) {
        scopeFilterFacts(facts, options.scope, sourceDir);
    }
    facts.meta.durationMs = Date.now() - start;
    return facts;
}
/**
 * Method-shape filter applied to candidate-step output (engines 6.21.3).
 *
 * The realize stack resolves spec behaviors at three levels:
 *
 *   1. INSTANCE GENERATOR — `cured: { create: {}, retrieve: {}, ... }`
 *      shorthand and `attributes:` declarations produce CURVED handlers
 *      and getters/setters automatically. No spec-side step list, no LLM.
 *   2. CONVENTION-BASED — step phrases inside `actions: { X: { steps: [] } }`
 *      match the `step-conventions.ts` library (find / create / update /
 *      delete / transition / emit / call-service / etc.) and compile to
 *      driver-specific code. No LLM.
 *   3. AI — fallback when no convention matches. Generated `*.ai.ts` body.
 *
 * Candidate-step extraction is for LEVELS 2 + 3. Methods that fall into
 * level 1 (single CURVED-shape) or level 0 (pure attribute access) MUST
 * be dropped — they're realize-generated, not spec behaviors.
 *
 *   Level-0 accessor: every candidate is `[return]` (e.g. `getName()`
 *     returning `this.name` — value access only, no business signal).
 *
 *   Level-1 single CURVED: candidates after stripping `[return]` consist
 *     of either (a) one `[find]`/`[find-many]`, or (b) writes of a single
 *     kind (`[create]` × n / `[update]` × n / `[delete]`) plus at most
 *     one preceding `[find]`. No `[validate]` / `[call]` / `[transition]`
 *     / `[emit]` — those are business signals that bump the method into
 *     level-2/3 territory.
 *
 * Returns `true` when the method has business content the LLM should
 * elaborate into `steps:`.
 */
const CONTAINER_OP = /^this\.[a-zA-Z_$][\w$]*\.(get|set|has|delete|clear|size|values|keys|entries|push|pop|shift|unshift|add|remove|find|filter|map|forEach|some|every|reduce|includes|indexOf|sort|slice|splice|concat|join|length|of|from|isArray|fromEntries)$/;
const SELF_DELEGATION = /^this\.[a-zA-Z_$][\w$]*$/;
function evaluateMethodFilter(method) {
    const c = method.candidates;
    // Level-0: nothing but value returns (`getName() { return this.name }`).
    if (c.length > 0 && c.every((s) => s.kind === 'return')) {
        return { kept: false, reason: 'level-0-accessor' };
    }
    const nonReturn = c.filter((s) => s.kind !== 'return');
    if (nonReturn.length === 0)
        return { kept: false, reason: 'level-0-accessor' };
    // Strong business signals — domain semantics that the realize stack
    // can NOT auto-generate from CURVED conventions or attribute decls.
    const hasStrong = nonReturn.some((s) => s.kind === 'validate' || s.kind === 'emit' || s.kind === 'transition');
    if (hasStrong)
        return { kept: true, reason: 'kept-business-action-strong-signal' };
    // `[call]` candidates split into:
    //   - external — call to a non-self, non-container-op receiver. These
    //     are real cross-entity / cross-service coordination.
    //   - container-op — `this.<field>.{get,set,has,...}` is collection
    //     access; the field is logically an `attributes:` declaration and
    //     the access is auto-generated. Equivalent to level-0.
    //   - self-delegation — `this.helper(...)` is a single-target wrapper;
    //     the actual logic lives in `helper`, which is its own method.
    //     Surfacing the wrapper without the target adds noise.
    const externalCalls = nonReturn.filter((s) => s.kind === 'call' &&
        !!s.callTarget &&
        !CONTAINER_OP.test(s.callTarget) &&
        !SELF_DELEGATION.test(s.callTarget)).length;
    const writeKinds = new Set(nonReturn
        .filter((s) => s.kind === 'create' || s.kind === 'update' || s.kind === 'delete')
        .map((s) => s.kind));
    const findCount = nonReturn.filter((s) => s.kind === 'find' || s.kind === 'find-many').length;
    // Single CURVED-shape (no externals): level-1 instance generator —
    // use `cured: { retrieve|create|update|delete: {} }` shorthand.
    if (writeKinds.size === 0 && findCount === 1 && externalCalls === 0) {
        return { kept: false, reason: 'level-1-curved-retrieve' };
    }
    if (writeKinds.size === 1 && findCount <= 1 && externalCalls === 0) {
        return { kept: false, reason: 'level-1-curved-create-update-delete' };
    }
    // Pure container/self-call wrapper (no CURVED, no external) — level-0.
    if (writeKinds.size === 0 && findCount === 0 && externalCalls === 0) {
        return { kept: false, reason: 'container-op-or-self-delegation-only' };
    }
    // Composite write ops or 2+ finds — multi-step action.
    if (writeKinds.size >= 2 || findCount >= 2) {
        return { kept: true, reason: 'kept-multi-step-composition' };
    }
    // Multiple distinct external calls — cross-entity coordination.
    if (externalCalls >= 2) {
        return { kept: true, reason: 'kept-multiple-external-calls' };
    }
    // Single external call only counts as business if there's other shape
    // to it (≥3 total non-return candidates). One-line delegations drop.
    if (externalCalls === 1 && nonReturn.length >= 3) {
        return { kept: true, reason: 'kept-multi-step-composition' };
    }
    return { kept: false, reason: 'container-op-or-self-delegation-only' };
}
function isMethodWorthSurfacing(method) {
    return evaluateMethodFilter(method).kept;
}
/**
 * Format a `SpecVerseFacts` object as a markdown block suitable for
 * injection into the analyse prompt's user template (mirrors the format
 * from the existing manually-extracted facts files used in §5/§6
 * experiments).
 */
export function formatFactsAsMarkdown(facts) {
    const lines = [];
    lines.push('# Structural facts extracted from source via Tree-sitter + per-framework adapters');
    lines.push('');
    lines.push('These facts are deterministic — extracted from AST analysis');
    lines.push('and source-pattern matching, NOT LLM judgment. Treat as ground');
    lines.push('truth for what symbols/files exist; verify SpecVerse semantics');
    lines.push('against the source where you can.');
    lines.push('');
    lines.push(`Backend: ${facts.meta.backend} | Adapters: ${facts.meta.adaptersRun.join(', ') || '(none)'}`);
    lines.push('');
    if (facts.entities.length > 0) {
        lines.push('## Entities (model candidates)');
        lines.push('');
        for (const ent of facts.entities) {
            lines.push(`- **${ent.name}** — declared in ${ent.filePath} (via ${ent.sourceFramework})`);
        }
        lines.push('');
    }
    if (facts.relationships.length > 0) {
        lines.push('## Relationships');
        lines.push('');
        for (const r of facts.relationships) {
            const cascade = r.cascade ? ' cascade' : '';
            lines.push(`- ${r.from} ${r.type} ${r.to}${cascade}`);
        }
        lines.push('');
    }
    if (facts.lifecycles.length > 0) {
        lines.push('## Lifecycle states (extracted from source enums + field declarations)');
        lines.push('');
        lines.push('These are the canonical lifecycle declarations found in source. Use these state names literally; do NOT invent generic synonyms (e.g. `pending` when source says `planning`).');
        lines.push('');
        for (const lc of facts.lifecycles) {
            lines.push(`- **${lc.model}.${lc.field}**: states = [${lc.states.join(', ')}]`);
        }
        lines.push('');
    }
    if (facts.suggestedComponents && facts.suggestedComponents.length > 0) {
        lines.push(formatSuggestionsAsMarkdown(facts.suggestedComponents));
    }
    return lines.join('\n');
}
/**
 * Format the per-method fact-sheet list as a markdown block suitable for
 * injection into the verify-analyse prompt as `methodFacts`. Each method
 * gets a compact entry showing its signature + the deterministic facts
 * (calls / emits / dbWrites / externalCalls / throws / asyncBoundaries /
 * branchPoints) that the LLM can cross-check the analysed `steps:`
 * against. Body text is excluded — keeps the block small enough to fit
 * inside one verify-analyse call without paginating.
 *
 * TODO #32 — surfaces what `runPrepass` already computes but didn't expose.
 */
export function formatMethodFactsAsMarkdown(sheets) {
    if (!sheets || sheets.length === 0)
        return '';
    const lines = [];
    lines.push('# Method-level facts extracted from source');
    lines.push('');
    lines.push('These facts are deterministic — extracted from AST analysis of');
    lines.push('method bodies. Use to cross-check the analysed `behaviors[].steps:`');
    lines.push('against what the source method actually does.');
    lines.push('');
    for (const m of sheets) {
        lines.push(`## ${m.qualifiedName}  (${m.filePath}:${m.startLine}-${m.endLine})`);
        if (m.signature)
            lines.push(`- Signature: \`${m.signature}\``);
        if (m.calls?.length)
            lines.push(`- Calls: ${m.calls.map((c) => c.qualifiedName || c.name).slice(0, 12).join(', ')}${m.calls.length > 12 ? ', …' : ''}`);
        if (m.emits?.length)
            lines.push(`- Emits: ${m.emits.join(', ')}`);
        if (m.dbWrites?.length) {
            const writes = m.dbWrites.map((w) => `${w.entity}.${w.op}${w.field ? `(${w.field})` : ''}`);
            lines.push(`- DbWrites: ${writes.join(', ')}`);
        }
        if (m.externalCalls?.length) {
            const ec = m.externalCalls.map((c) => `${c.kind}${c.target ? ` ${c.target}` : ''}`);
            lines.push(`- ExternalCalls: ${ec.join(', ')}`);
        }
        if (m.throws?.length)
            lines.push(`- Throws: ${m.throws.join(', ')}`);
        if (m.asyncBoundaries?.length) {
            const counts = new Map();
            for (const ab of m.asyncBoundaries)
                counts.set(ab.type, (counts.get(ab.type) || 0) + 1);
            lines.push(`- AsyncBoundaries: ${[...counts.entries()].map(([k, v]) => `${v} ${k}`).join(' + ')}`);
        }
        if (m.branchPoints)
            lines.push(`- BranchPoints: ${m.branchPoints}`);
        lines.push('');
    }
    return lines.join('\n');
}
/**
 * Format the per-method candidate-step timelines (#27-B/C) as a
 * markdown block suitable for prompt injection. Output shape:
 *
 *   # Pre-classified candidate behavior steps
 *   ## EntityName (path/to/file.ts)
 *   ### methodName  (5 candidate steps)
 *   1. [find]      Find Customer by { id }
 *   2. [validate]  Throw NotFoundException
 *   3. [create]    Create Invoice
 *   ...
 *
 * The analyse LLM uses this as a STRUCTURAL ANCHOR — refining the
 * pre-classified timeline into spec `behaviors[].steps[]` blocks
 * rather than summarizing raw method bodies.
 *
 * Returns the empty string when `candidateMethods` is undefined or
 * empty — caller should `if (md) prompt += md` rather than
 * unconditionally embed.
 */
export function formatCandidateMethodsAsMarkdown(candidateMethods) {
    if (!candidateMethods || candidateMethods.length === 0)
        return '';
    const lines = [];
    lines.push('# Pre-classified candidate behavior steps');
    lines.push('');
    lines.push('Per-method timelines extracted by the deterministic walker +');
    lines.push('classifier (NOT LLM judgment). Each step has a `kind` from a');
    lines.push('fixed vocabulary {find, find-many, create, update, delete,');
    lines.push('transition, emit, validate, call, return} and a one-line summary.');
    lines.push('Use these as structural anchors when emitting `behaviors[].steps[]`:');
    lines.push('');
    lines.push('  - REFINE the pre-classified timeline into spec vocabulary');
    lines.push('    (`[find] Find Customer by { id }` → `"Find Customer by id"`)');
    lines.push('  - MERGE adjacent related steps where appropriate');
    lines.push('    (`[create]` then `[create]` from `repo.create + repo.save`');
    lines.push('    is one create step in spec terms)');
    lines.push('  - FILTER implementation noise — `[call]` to a logger, idempotency');
    lines.push('    short-circuit returns, response serialization are not steps');
    lines.push('  - PRESERVE order — the timeline reflects execution order');
    lines.push('  - DO NOT INVENT — if a behavior step you want to emit is not');
    lines.push('    backed by a candidate here OR in the source body, drop it');
    lines.push('');
    for (const entry of candidateMethods) {
        lines.push(`## ${entry.entityName}  (${entry.filePath})`);
        for (const m of entry.methods) {
            lines.push('');
            lines.push(`### ${m.methodName}${m.isAsync ? ' (async)' : ''}  — ${m.candidates.length} candidate step${m.candidates.length === 1 ? '' : 's'}`);
            m.candidates.forEach((step, idx) => {
                const kind = `[${step.kind}]`.padEnd(12);
                lines.push(`${idx + 1}. ${kind} ${step.summary}`);
            });
        }
        lines.push('');
    }
    return lines.join('\n');
}
//# sourceMappingURL=index.js.map