/**
 * Quint generator — emits a per-spec `.qnt` file that asserts lifecycle
 * reachability and behavior precondition validity. v1 of TODO #31.
 *
 * v1 scope (deliberately narrow — see
 * `specverse-self/docs/plans/2026-04-30-DETERMINISTIC-VERIFICATION-GATES.md`):
 *   - For each model with a `lifecycles:` block, generate a Quint state
 *     machine asserting:
 *       1. States are unique within the lifecycle.
 *       2. Transitions reference declared states only.
 *       3. There is at least one initial state (no incoming transition,
 *          or explicitly named in the spec).
 *       4. Behaviors with `requires:` clauses naming a state reference
 *          a real state.
 *
 * Out of scope for v1:
 *   - Cross-model invariants (e.g. Order belongs to Customer).
 *   - Full behavior pre/post encoding as transition guards.
 *   - Quint-driven simulation (`run` actions with assertions).
 *
 * Output is a single Quint module per spec with one sub-module per model
 * that has a lifecycle. The runner subprocess (`./runner.ts`) executes
 * `quint typecheck` against the generated file; reachability/transition
 * errors come back as line-numbered Quint diagnostics.
 */
/**
 * Generate a Quint specification from a SpecVerse spec. Walks every
 * component's models, collects lifecycle declarations, and emits Quint
 * `module` text encoding the four v1 invariants.
 *
 * The spec parameter accepts either the parsed AST (with components: array)
 * or an inferred-spec object (flat models map). The function probes shape
 * and adapts.
 */
export function generateQuintFromSpec(spec) {
    const allModels = [];
    // Two shapes to support: AST (components array) and flat (models map).
    if (Array.isArray(spec?.components)) {
        for (const comp of spec.components) {
            const models = normalizeModels(comp.models);
            allModels.push(...models);
        }
    }
    else if (spec?.components && typeof spec.components === 'object') {
        for (const comp of Object.values(spec.components)) {
            const models = normalizeModels(comp.models);
            allModels.push(...models);
        }
    }
    else if (spec?.models) {
        allModels.push(...normalizeModels(spec.models));
    }
    const modelsWithLifecycles = allModels.filter((m) => {
        const lc = m.lifecycles;
        if (!lc)
            return false;
        if (Array.isArray(lc))
            return lc.length > 0;
        return Object.keys(lc).length > 0;
    });
    // Cross-model index — every declared model name across all components,
    // PLUS every type name imported via `import:` blocks (e.g. `Address`
    // imported from `@specverse/primitives`). v2 cross-model invariants
    // check `relationship.target` against this combined set.
    const declaredModelNames = new Set(allModels.map((m) => m.name));
    if (Array.isArray(spec?.components)) {
        for (const comp of spec.components)
            addImportedNames(comp, declaredModelNames);
    }
    else if (spec?.components && typeof spec.components === 'object') {
        for (const comp of Object.values(spec.components))
            addImportedNames(comp, declaredModelNames);
    }
    else {
        addImportedNames(spec, declaredModelNames);
    }
    // Cross-model relationship validity (v2). Check every model's
    // relationships, not just those with lifecycles — referential integrity
    // is a property of the spec as a whole.
    const crossModelViolations = [];
    for (const model of allModels) {
        const rels = normalizeRelationships(model.relationships);
        for (const rel of rels) {
            if (!rel.target)
                continue;
            if (!declaredModelNames.has(rel.target)) {
                crossModelViolations.push({
                    modelName: model.name,
                    lifecycleName: '',
                    kind: 'relationship-target-unknown',
                    message: `${model.name}.${rel.name || '(unnamed)'}: relationship targets "${rel.target}" but no model with that name is declared (${declaredModelNames.size} models declared)`,
                });
            }
        }
    }
    if (modelsWithLifecycles.length === 0 && crossModelViolations.length === 0) {
        return { content: '', modelCount: 0, behaviorPreconditionCount: 0, skipped: true, skipReason: 'no models with lifecycles and no relationship issues', violations: [] };
    }
    const lines = [];
    lines.push('// Auto-generated by @specverse/engines/inference/quint-gen');
    lines.push('// v2 — lifecycle reachability + transition validity +');
    lines.push('//      behavior-requires + behavior-ensures validity +');
    lines.push('//      cross-model relationship-target validity');
    lines.push('// See docs/plans/2026-04-30-DETERMINISTIC-VERIFICATION-GATES.md');
    lines.push('');
    // Cross-model section in the Quint module — declares the universe of
    // model names so future quint-run extensions can use it as a Set.
    if (declaredModelNames.size > 0) {
        lines.push('module Models {');
        lines.push(`  pure val modelNames: Set[str] = Set(${[...declaredModelNames].map((n) => `"${n}"`).join(', ')})`);
        lines.push('}');
        lines.push('');
    }
    let behaviorPreconditionCount = 0;
    const violations = [...crossModelViolations];
    for (const model of modelsWithLifecycles) {
        const lifecycles = normalizeLifecycles(model.lifecycles);
        for (const [lcName, lc] of Object.entries(lifecycles)) {
            const states = collectStates(lc);
            if (states.length === 0) {
                violations.push({
                    modelName: model.name,
                    lifecycleName: lcName,
                    kind: 'no-states-declared',
                    message: `${model.name}.${lcName}: no states declared (need either flow: "a -> b" or states: [...] + transitions: {...})`,
                });
                continue;
            }
            // Value-level invariant checks — Quint's typecheck only validates types,
            // not predicate values, so these run in JS as the actual gate.
            const stateSet = new Set(states);
            // Duplicate detection — only fires when states[] was authored
            // explicitly (lc.type === 'structured', or no flow field). When the
            // parser derived states from a `flow:` chain (lc.type === 'shorthand'),
            // duplicates indicate intentional cyclic state machines (e.g.
            // `flow: active -> disabled -> active` for a re-activation cycle) and
            // must NOT be flagged.
            const flowDerived = lc.type === 'shorthand' || (typeof lc.flow === 'string' && !Array.isArray(lc.transitions));
            if (Array.isArray(lc.states) && !flowDerived) {
                const seen = new Set();
                const dupes = [];
                for (const s of lc.states) {
                    if (seen.has(s) && !dupes.includes(s))
                        dupes.push(s);
                    seen.add(s);
                }
                if (dupes.length > 0) {
                    violations.push({
                        modelName: model.name,
                        lifecycleName: lcName,
                        kind: 'states-not-unique',
                        message: `${model.name}.${lcName}: duplicate states declared: ${dupes.join(', ')}`,
                    });
                }
            }
            const transitionsRaw = collectTransitions(lc);
            for (const t of transitionsRaw) {
                if (!stateSet.has(t.from)) {
                    violations.push({
                        modelName: model.name,
                        lifecycleName: lcName,
                        kind: 'transition-references-unknown-state',
                        message: `${model.name}.${lcName}.transitions.${t.name}: source state "${t.from}" not declared (states: [${states.join(', ')}])`,
                    });
                }
                if (!stateSet.has(t.to)) {
                    violations.push({
                        modelName: model.name,
                        lifecycleName: lcName,
                        kind: 'transition-references-unknown-state',
                        message: `${model.name}.${lcName}.transitions.${t.name}: target state "${t.to}" not declared (states: [${states.join(', ')}])`,
                    });
                }
            }
            const moduleId = sanitizeIdent(`${model.name}_${lcName}`);
            lines.push(`module ${moduleId} {`);
            lines.push(`  // States declared for ${model.name}.${lcName}`);
            lines.push(`  pure val states: Set[str] = Set(${states.map((s) => `"${s}"`).join(', ')})`);
            lines.push('');
            // Invariant 1 — states are unique. Implied by Set construction; we
            // additionally check that the COUNT matches the literal-list length so
            // duplicates in the source spec surface as a typecheck failure.
            lines.push(`  pure val statesUnique: bool = states.size() == ${states.length}`);
            lines.push('');
            // Invariant 2 — transitions reference declared states only.
            const transitions = collectTransitions(lc);
            if (transitions.length > 0) {
                const transitionsExpr = transitions
                    .map(({ name, from, to }) => `("${name}", "${from}", "${to}")`)
                    .join(', ');
                lines.push(`  pure val transitions: Set[(str, str, str)] = Set(${transitionsExpr})`);
                lines.push(`  pure val transitionStatesValid: bool = transitions.forall(t => states.contains(t._2) and states.contains(t._3))`);
                lines.push('');
            }
            else {
                lines.push('  pure val transitions: Set[(str, str, str)] = Set()');
                lines.push('  pure val transitionStatesValid: bool = true');
                lines.push('');
            }
            // Invariant 3 — initial state exists. Heuristic: take the first state
            // in `flow:` declarations, OR any state with no incoming transition.
            const initialState = inferInitial(states, transitions);
            if (initialState) {
                lines.push(`  pure val initialState: str = "${initialState}"`);
                lines.push(`  pure val initialIsDeclared: bool = states.contains(initialState)`);
            }
            else {
                lines.push('  pure val initialIsDeclared: bool = states.size() > 0');
            }
            lines.push('');
            // Invariant 4 — behavior preconditions referencing this lifecycle's
            // states must reference real states. We extract state references from
            // `requires:` clauses heuristically (look for any of the lifecycle's
            // state names appearing as words in the requires text).
            // The Quint module records the references for documentation; the
            // VALUE-level check happens here.
            const behaviorRefs = collectBehaviorStateReferences(model.behaviors, states);
            if (behaviorRefs.declared.length > 0) {
                behaviorPreconditionCount += behaviorRefs.declared.length;
                const refsExpr = behaviorRefs.declared.map((r) => `"${r}"`).join(', ');
                lines.push(`  pure val behaviorRequiredStates: Set[str] = Set(${refsExpr})`);
                lines.push(`  pure val behaviorRequiresValid: bool = behaviorRequiredStates.forall(s => states.contains(s))`);
                lines.push('');
            }
            // behavior-requires that name a NON-state token (e.g. "Booking is in
            // confirmd") get flagged here. Heuristic: any token that LOOKS like
            // a state name (matches the case style of declared states) but isn't
            // in stateSet is a likely typo.
            for (const unknown of behaviorRefs.unknown) {
                violations.push({
                    modelName: model.name,
                    lifecycleName: lcName,
                    kind: 'behavior-requires-unknown-state',
                    message: `${model.name}.behaviors: requires-clause names state "${unknown}" not in ${lcName} (declared: [${states.join(', ')}])`,
                });
            }
            // v2 — behavior-ensures cross-check. Same pattern as requires:
            // postconditions naming a state must reference a declared state.
            // Catches typos in `ensures: ["status is now confimed"]` etc.
            const ensuresRefs = collectBehaviorStateReferencesByField(model.behaviors, states, 'ensures');
            if (ensuresRefs.declared.length > 0) {
                const refsExpr = ensuresRefs.declared.map((r) => `"${r}"`).join(', ');
                lines.push(`  pure val behaviorEnsuredStates: Set[str] = Set(${refsExpr})`);
                lines.push(`  pure val behaviorEnsuresValid: bool = behaviorEnsuredStates.forall(s => states.contains(s))`);
                lines.push('');
            }
            for (const unknown of ensuresRefs.unknown) {
                violations.push({
                    modelName: model.name,
                    lifecycleName: lcName,
                    kind: 'behavior-ensures-unknown-state',
                    message: `${model.name}.behaviors: ensures-clause names state "${unknown}" not in ${lcName} (declared: [${states.join(', ')}])`,
                });
            }
            lines.push('}');
            lines.push('');
        }
    }
    return {
        content: lines.join('\n'),
        modelCount: modelsWithLifecycles.length,
        behaviorPreconditionCount,
        skipped: false,
        violations,
    };
}
// ── helpers ──
function normalizeModels(modelsRaw) {
    if (!modelsRaw)
        return [];
    if (Array.isArray(modelsRaw))
        return modelsRaw;
    return Object.entries(modelsRaw).map(([name, body]) => ({ name, ...body }));
}
function normalizeLifecycles(lc) {
    if (Array.isArray(lc)) {
        const out = {};
        for (const item of lc) {
            if (item.name)
                out[item.name] = item;
        }
        return out;
    }
    return lc;
}
function collectStates(lc) {
    // States come from EXPLICIT declarations only — `states: [...]` array OR
    // `flow:` chain. Transition targets are NOT inferred into the state set:
    // a typo in a transition target should fail the transitionStatesValid
    // invariant, not get silently auto-added as a "real" state.
    const set = new Set();
    if (Array.isArray(lc.states)) {
        for (const s of lc.states)
            set.add(s);
    }
    if (typeof lc.flow === 'string') {
        for (const part of lc.flow.split('->').map((s) => s.trim()).filter(Boolean)) {
            set.add(part);
        }
    }
    return [...set];
}
function collectTransitions(lc) {
    const out = [];
    if (lc.transitions && typeof lc.transitions === 'object') {
        for (const [name, arrow] of Object.entries(lc.transitions)) {
            // Two shapes accepted:
            //   author shorthand: transitions: { ship: "pending -> shipped" }
            //   parsed AST form:  transitions: { ship: { from: "pending", to: "shipped" } }
            if (typeof arrow === 'string') {
                const [from, to] = arrow.split('->').map((s) => s.trim());
                if (from && to)
                    out.push({ name, from, to });
            }
            else if (arrow && typeof arrow === 'object') {
                const a = arrow;
                const from = typeof a.from === 'string' ? a.from.trim() : '';
                const to = typeof a.to === 'string' ? a.to.trim() : '';
                if (from && to)
                    out.push({ name, from, to });
            }
        }
    }
    if (typeof lc.flow === 'string') {
        const states = lc.flow.split('->').map((s) => s.trim()).filter(Boolean);
        for (let i = 0; i < states.length - 1; i++) {
            out.push({ name: `step${i + 1}`, from: states[i], to: states[i + 1] });
        }
    }
    return out;
}
function inferInitial(states, transitions) {
    if (states.length === 0)
        return null;
    const targets = new Set(transitions.map((t) => t.to));
    const sources = states.filter((s) => !targets.has(s));
    if (sources.length === 1)
        return sources[0];
    return states[0]; // fallback
}
function collectBehaviorStateReferences(behaviors, states) {
    return collectBehaviorStateReferencesByField(behaviors, states, 'requires');
}
/**
 * Walk every behavior's `requires` OR `ensures` field (v2) and return any
 * state-like tokens found, classified into "declared" (exists in states[])
 * vs "unknown" (looks like a state name but doesn't match — likely typo).
 *
 * Heuristic phrase patterns capture state names after common keyword
 * positions ("is in X" / "transitions to X" / "is now X" — the latter
 * particularly relevant for ensures-clauses).
 */
function collectBehaviorStateReferencesByField(behaviors, states, field) {
    if (!behaviors)
        return { declared: [], unknown: [] };
    const map = Array.isArray(behaviors)
        ? Object.fromEntries(behaviors.map((b) => [b.name || '', b]))
        : behaviors;
    const stateSet = new Set(states);
    const declared = new Set();
    const unknown = new Set();
    const phrasePatterns = [
        /\bis\s+(?:in|at|now)\s+(\w+)/gi, // "is in X", "is now X" (ensures-style)
        /\btransitions?\s+(?:to|from)\s+(\w+)/gi,
        /\bafter\s+(\w+)/gi,
        /\bbefore\s+(\w+)/gi,
        /\b(?:in|when)\s+state\s+(\w+)/gi,
        /\bbecomes?\s+(\w+)/gi, // "becomes X" (ensures-style)
        /\bmoves?\s+to\s+(\w+)/gi, // "moves to X"
    ];
    for (const b of Object.values(map)) {
        const clauses = b?.[field];
        if (!clauses)
            continue;
        const list = Array.isArray(clauses) ? clauses : [clauses];
        for (const clause of list) {
            if (typeof clause !== 'string')
                continue;
            for (const pattern of phrasePatterns) {
                let m;
                const re = new RegExp(pattern.source, pattern.flags);
                while ((m = re.exec(clause)) !== null) {
                    const token = m[1];
                    if (!token)
                        continue;
                    if (stateSet.has(token))
                        declared.add(token);
                    else if (looksLikeStateName(token, states))
                        unknown.add(token);
                }
            }
            for (const state of stateSet) {
                const re = new RegExp(`\\b${state.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\b`);
                if (re.test(clause))
                    declared.add(state);
            }
        }
    }
    return { declared: [...declared], unknown: [...unknown] };
}
/** Normalize relationships into a list of {name, type, target}. */
function normalizeRelationships(relsRaw) {
    if (!relsRaw)
        return [];
    if (Array.isArray(relsRaw)) {
        return relsRaw.map((r) => (typeof r === 'string' ? parseRelationshipShorthand('', r) : r)).filter(Boolean);
    }
    return Object.entries(relsRaw)
        .map(([name, body]) => {
        if (typeof body === 'string')
            return parseRelationshipShorthand(name, body);
        return { name, ...body };
    })
        .filter(Boolean);
}
/** Parse "hasMany Booking cascade" / "belongsTo Customer" shorthand into a RelationshipSpec. */
function parseRelationshipShorthand(name, shorthand) {
    const parts = shorthand.trim().split(/\s+/);
    if (parts.length < 2)
        return null;
    const [type, target] = parts;
    if (!['hasMany', 'hasOne', 'belongsTo', 'manyToMany'].includes(type))
        return null;
    return { name, type: type, target };
}
/**
 * Extend the declaredModelNames set with anything declared in this
 * component's `import:` block. Each `import: [{ from, select }]` entry
 * carries a list of type names imported from another package — those
 * names are valid relationship targets even though they aren't local
 * models. Authors write `import:` (singular) in YAML and the parser AST
 * carries it under the same singular `import` field (DP3-aligned).
 */
function addImportedNames(comp, names) {
    const imports = comp?.import;
    if (!Array.isArray(imports))
        return;
    for (const imp of imports) {
        const select = imp?.select;
        if (Array.isArray(select)) {
            for (const item of select) {
                if (typeof item === 'string' && item)
                    names.add(item);
            }
        }
    }
}
/** Heuristic: a candidate string "looks like" a state name when it matches
 *  the case-style of the declared states (snake_case or single-word). */
function looksLikeStateName(token, states) {
    if (states.length === 0)
        return false;
    const allSnakeCase = states.every((s) => /^[a-z][a-z0-9_]*$/.test(s));
    if (allSnakeCase)
        return /^[a-z][a-z0-9_]*$/.test(token);
    return /^[a-z][a-zA-Z0-9_]*$/.test(token);
}
function sanitizeIdent(s) {
    return s.replace(/[^A-Za-z0-9_]/g, '_').replace(/^[0-9]/, '_$&');
}
//# sourceMappingURL=index.js.map