/**
 * Normalise runner — walks the rule manifest, applies each applicable
 * rule's detect/transform pipeline, and returns the transformed spec
 * + an audit trail of what fired.
 *
 * Phase 1a of the 2026-05-13-NORMALISE-PHASE proposal.
 *
 * Contract:
 *   - INPUT: a parsed NormaliseSpec + a NormaliseContext
 *   - OUTPUT: the transformed AST + per-rule result list
 *   - INVARIANT: identity on empty ruleset (no rules → spec returned unchanged)
 *   - INVARIANT: deterministic — same input + same ruleset → same output
 *   - INVARIANT: rules apply in `ordering` ascending; later rules see
 *     the transformed state from earlier rules
 *
 * The runner does NOT deep-clone the spec between rules. Rules MAY
 * mutate in-place (and downstream rules MUST handle the mutated state
 * gracefully). The runner returns the same reference passed in unless
 * a rule explicitly returns a new object.
 */
import { RULES, rulesApplicableTo, } from './normalise-rules.js';
/**
 * Run the normalise pipeline on a parsed spec.
 *
 * Each applicable rule:
 *   1. Has detect() called once
 *   2. If detect returned ≥1 match, transform() is called per match
 *   3. The rule's result is recorded in the returned audit trail
 *
 * Rules that return no matches are recorded as `matchCount: 0`, NOT
 * as skipped — they ran successfully, just had nothing to do. Skipped
 * means the rule was excluded before detect ran (disabled in ctx,
 * `appliesWhen` returned false, etc.).
 */
export function runNormalise(spec, ctx, options = {}) {
    const allRules = options.rules ?? RULES;
    // Build the same applicable set rulesApplicableTo would, but against
    // a custom rule list when one was passed.
    const applicable = options.rules
        ? filterAndSort(allRules, ctx)
        : rulesApplicableTo(ctx);
    // Find the rules that were FILTERED OUT, so we can record them as skipped.
    const applicableIds = new Set(applicable.map((r) => r.id));
    const skipped = allRules
        .filter((r) => !applicableIds.has(r.id))
        .map((r) => ({
        ruleId: r.id,
        matchCount: 0,
        matches: [],
        skipped: true,
        skipReason: skipReasonFor(r, ctx),
    }));
    const fired = [];
    let workingSpec = spec;
    for (const rule of applicable) {
        const matches = rule.detect(workingSpec, ctx);
        for (const match of matches) {
            workingSpec = rule.transform(workingSpec, match, ctx);
        }
        fired.push({
            ruleId: rule.id,
            matchCount: matches.length,
            matches: matches.map((m) => ({ description: m.description })),
            skipped: false,
        });
    }
    const results = [...fired, ...skipped];
    return {
        spec: workingSpec,
        results,
        stats: {
            totalRulesConsidered: allRules.length,
            rulesFired: fired.filter((r) => r.matchCount > 0).length,
            rulesSkipped: skipped.length,
            totalMatches: fired.reduce((sum, r) => sum + r.matchCount, 0),
        },
    };
}
/** Determine why a specific rule was skipped, for audit-log clarity. */
function skipReasonFor(rule, ctx) {
    if (ctx.disabledRules?.has(rule.id))
        return 'disabled in project overrides';
    if (!rule.enabledByDefault && !ctx.enabledRules?.has(rule.id)) {
        return 'opt-in only (not in enabledRules)';
    }
    if (rule.appliesWhen && !rule.appliesWhen(ctx)) {
        return 'appliesWhen returned false';
    }
    return 'unknown';
}
/**
 * Mirror of `rulesApplicableTo` from normalise-rules.ts but against a
 * caller-provided rule list. Kept private so the manifest's
 * `rulesApplicableTo` remains the canonical entry point for the live
 * RULES; this is purely a test/override helper.
 */
function filterAndSort(rules, ctx) {
    const disabled = ctx.disabledRules ?? new Set();
    const enabled = ctx.enabledRules ?? new Set();
    return rules
        .filter((r) => {
        if (disabled.has(r.id))
            return false;
        const enabledByConfig = r.enabledByDefault || enabled.has(r.id);
        if (!enabledByConfig)
            return false;
        if (r.appliesWhen && !r.appliesWhen(ctx))
            return false;
        return true;
    })
        .slice()
        .sort((a, b) => a.ordering - b.ordering);
}
//# sourceMappingURL=runner.js.map