/**
 * Deterministic behavior ↔ method-facts cross-check (TODO #32 v2).
 *
 * Pure-function, no-LLM check that compares analysed `behaviors[].publishes`
 * (and `actions[].publishes`, `operations[].publishes`) against
 * `MethodFactSheet.emits` lists. If a spec behavior claims to publish event
 * X but no source method actually emits X, that's a hallucination — flag it
 * BEFORE the LLM verify call so retry feedback is grounded in source truth.
 *
 * Symmetric check for `dbWrites`: if the source mutates Order but no
 * `ensures:` clause references the mutation, flag it.
 *
 * v1 was LLM-judgment-only via verify-analyse check 8. v2 promotes the
 * objective parts (emit set membership, dbWrite presence) to deterministic
 * gate that runs alongside the LLM call.
 */
/**
 * Run the cross-check. Walks every executable in every component (model
 * behaviors, controller actions, service operations) and compares against
 * the supplied method fact sheets indexed by qualifiedName.
 *
 * Symbol-matching heuristic:
 *   - Behavior `Model.foo` matches MethodFactSheet `ModelController.foo` OR
 *     `ModelService.foo` OR `Model.foo` (most specific wins).
 *   - Action `Controller.foo` matches MethodFactSheet `Controller.foo`.
 *   - Operation `Service.foo` matches MethodFactSheet `Service.foo`.
 *
 * When no matching fact sheet exists, the executable is skipped silently
 * (the source method might have a different name, OR backend didn't extract
 * its details).
 */
export function crossCheckBehaviors(spec, sheets) {
    if (!sheets || sheets.length === 0)
        return [];
    const violations = [];
    // Index sheets by qualifiedName for O(1) lookup
    const sheetByQName = new Map();
    for (const s of sheets)
        sheetByQName.set(s.qualifiedName, s);
    // Walk every component's executables
    const components = normalizeComponents(spec);
    for (const comp of components) {
        // Model behaviors
        for (const [modelName, model] of Object.entries(comp.models || {})) {
            for (const [behaviorName, exec] of Object.entries(model.behaviors || {})) {
                const sheet = findSheetForBehavior(modelName, behaviorName, sheetByQName);
                if (sheet)
                    checkExecutable(modelName, behaviorName, exec, sheet, violations);
            }
        }
        // Controller actions
        for (const [ctrlName, ctrl] of Object.entries(comp.controllers || {})) {
            for (const [actionName, exec] of Object.entries(ctrl.actions || {})) {
                const sheet = sheetByQName.get(`${ctrlName}.${actionName}`);
                if (sheet)
                    checkExecutable(ctrlName, actionName, exec, sheet, violations);
            }
        }
        // Service operations
        for (const [svcName, svc] of Object.entries(comp.services || {})) {
            for (const [opName, exec] of Object.entries(svc.operations || {})) {
                const sheet = sheetByQName.get(`${svcName}.${opName}`);
                if (sheet)
                    checkExecutable(svcName, opName, exec, sheet, violations);
            }
        }
    }
    return violations;
}
/**
 * Format violations as a markdown block for prompt injection. The
 * verify-behavior prompt receives this alongside the per-behavior
 * fact sheet so the LLM sees the deterministic signal up front.
 */
export function formatBehaviorViolationsAsMarkdown(violations) {
    if (violations.length === 0)
        return '';
    const lines = [];
    lines.push('# Deterministic behavior cross-check violations');
    lines.push('');
    lines.push('These were flagged BEFORE the LLM verify call by comparing the');
    lines.push('spec\'s `publishes:` / `ensures:` declarations against the source');
    lines.push('method\'s actual `Emits:` / `DbWrites:` lists. Each one is a');
    lines.push('factual mismatch — fix the spec to match source.');
    lines.push('');
    for (const v of violations) {
        lines.push(`- **[${v.kind}]** \`${v.modelOrService}.${v.behaviorOrOperation}\` — ${v.message}`);
    }
    lines.push('');
    return lines.join('\n');
}
// ── helpers ──
function normalizeComponents(spec) {
    if (Array.isArray(spec?.components))
        return spec.components;
    if (spec?.components && typeof spec.components === 'object') {
        return Object.values(spec.components);
    }
    // flat shape (single-component spec)
    return [spec];
}
/**
 * For a behavior on `Model.foo`, find a fact sheet on `ModelController.foo`
 * OR `ModelService.foo` OR `Model.foo`. Most specific match wins (controller
 * > service > model).
 */
function findSheetForBehavior(modelName, behaviorName, sheets) {
    return (sheets.get(`${modelName}Controller.${behaviorName}`) ||
        sheets.get(`${modelName}Service.${behaviorName}`) ||
        sheets.get(`${modelName}.${behaviorName}`));
}
function checkExecutable(ownerName, execName, exec, sheet, violations) {
    // Check 1: every spec.publishes[E] must appear in source.emits
    const sourceEmits = new Set((sheet.emits || []).map((e) => e.toLowerCase()));
    for (const event of exec.publishes || []) {
        // Match case-insensitively to tolerate stylistic differences
        if (!sourceEmits.has(event.toLowerCase())) {
            // Look for a "fuzzy" match — same root word
            const fuzzyMatch = [...sourceEmits].some((e) => e.includes(event.toLowerCase()) || event.toLowerCase().includes(e));
            if (!fuzzyMatch) {
                violations.push({
                    modelOrService: ownerName,
                    behaviorOrOperation: execName,
                    kind: 'emit-not-in-source',
                    message: `spec publishes [${event}] but source method does NOT emit it (source emits: [${[...sourceEmits].join(', ') || 'nothing'}])`,
                });
            }
        }
    }
    // Check 2: source emits that the spec doesn't publish — soft signal
    // (the spec might be intentionally narrowing).
    for (const sourceEvent of sheet.emits || []) {
        const specPublishes = new Set((exec.publishes || []).map((e) => e.toLowerCase()));
        if (!specPublishes.has(sourceEvent.toLowerCase())) {
            // Only flag when there's no fuzzy match
            const fuzzyMatch = [...specPublishes].some((e) => e.includes(sourceEvent.toLowerCase()) || sourceEvent.toLowerCase().includes(e));
            if (!fuzzyMatch) {
                violations.push({
                    modelOrService: ownerName,
                    behaviorOrOperation: execName,
                    kind: 'source-emit-missing-from-spec',
                    message: `source emits [${sourceEvent}] but spec doesn't list it in publishes (spec publishes: [${[...specPublishes].join(', ') || 'nothing'}])`,
                });
            }
        }
    }
    // Check 3: source dbWrites without an ensures clause referencing the entity
    const ensuresStr = Array.isArray(exec.ensures) ? exec.ensures.join(' ').toLowerCase() : (exec.ensures || '').toLowerCase();
    for (const w of sheet.dbWrites || []) {
        if (!ensuresStr.includes(w.entity.toLowerCase())) {
            violations.push({
                modelOrService: ownerName,
                behaviorOrOperation: execName,
                kind: 'dbwrite-missing-ensures',
                message: `source ${w.op}s ${w.entity} but no ensures-clause mentions ${w.entity} (ensures: ${exec.ensures ? JSON.stringify(exec.ensures) : 'none'})`,
            });
        }
    }
}
//# sourceMappingURL=index.js.map