/**
 * Normalise rule: `cluster-module-functions`.
 *
 * Phase 2 of the 2026-05-13-NORMALISE-PHASE proposal. Cleans up
 * project-prefix noise on services synthesised by the module-functions
 * adapter (Phase 0 prepass change).
 *
 * Trigger pattern: PowerSync / Drizzle / Supabase-queries codebases
 * tend to prefix module functions with a project tag:
 *
 *   export async function smcCreateRound(...)
 *   export async function smcGetRound(...)
 *   export async function smcUpdateRoundStatus(...)
 *
 * The prepass synthesises one service per file. After Phase 0 the
 * faithful spec looks like:
 *
 *   SmcRoundsService:
 *     operations:
 *       smcCreateRound: { ... }
 *       smcGetRound: { ... }
 *       smcUpdateRoundStatus: { ... }
 *
 * After this rule fires:
 *
 *   RoundsService:
 *     operations:
 *       createRound: { ... }
 *       getRound: { ... }
 *       updateRoundStatus: { ... }
 *
 * The `Smc` / `smc` project prefix appears on both the service name
 * (PascalCase) and operation names (camelCase). Stripping it from both
 * removes noise while preserving the rest of the naming so the LLM-
 * authored bodies still reference the right entities.
 *
 * What this rule does NOT do (deferred to later rules):
 *   - Rename service from `RoundsService` to `RoundService` based on
 *     detected target entity (would be `cluster-module-functions`'s
 *     dominant-entity inference, per proposal §5 — separate rule)
 *   - Collapse `RoundsService.getRound` → `Round.retrieve` CURVED
 *     (that's `crud-into-curved`, Rule 2)
 *   - Drop trivial passthrough ops (Rule 3)
 *
 * Conservative invariants:
 *   - Prefix must be 2–3 lowercase letters ending just before an
 *     uppercase letter (camelCase boundary). The 3-char ceiling is the
 *     load-bearing false-positive defence: real project tags are short
 *     non-words (`smc`, `db`, `api`, `iam`); English verbs that share
 *     a camelCase head (`save`, `claim`, `format`, `calculate`,
 *     `invalidate`) are 4+ characters and stay untouched.
 *   - At least ONE operation must share the prefix (no service-name-
 *     only renames — `SaveGameService` doesn't get stripped to
 *     `GameService` just because `Save` appears at the start of the
 *     service name; that's a meaningful word, not a project tag).
 *   - Skip when stripping would produce a name collision.
 *   - Skip when stripping would produce an empty / invalid name.
 *   - Skip when ctx.source === 'hand-authored'.
 */
const MAX_PREFIX_LENGTH = 3;
/**
 * Find the longest lowercase prefix shared by `names`, ending strictly
 * before the first uppercase letter (camelCase boundary). Returns the
 * empty string when no such prefix is found.
 */
function findCamelPrefix(names) {
    if (names.length === 0)
        return '';
    const reference = names[0];
    let candidate = '';
    for (let i = 0; i < reference.length; i++) {
        const c = reference[i];
        if (c >= 'A' && c <= 'Z')
            break;
        if (!(c >= 'a' && c <= 'z'))
            break;
        if (!names.every((n) => n[i] === c))
            break;
        candidate += c;
    }
    return candidate;
}
function stripCamelPrefix(name, prefix) {
    if (!name.startsWith(prefix))
        return name;
    const rest = name.slice(prefix.length);
    if (rest.length === 0)
        return name;
    return rest[0].toLowerCase() + rest.slice(1);
}
function stripPascalPrefix(serviceName, prefix) {
    if (prefix.length === 0)
        return null;
    const pascalPrefix = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase();
    if (!serviceName.startsWith(pascalPrefix))
        return null;
    const rest = serviceName.slice(pascalPrefix.length);
    if (rest.length === 0)
        return null;
    if (!(rest[0] >= 'A' && rest[0] <= 'Z'))
        return null;
    return rest;
}
/**
 * Detect whether the service with `serviceName` is a cluster-cleanup
 * candidate. See file-header doc for the two detection signals.
 */
function detectClusterMatch(componentName, serviceName, serviceBody) {
    const ops = serviceBody.operations;
    if (!ops || typeof ops !== 'object')
        return null;
    const opNames = Object.keys(ops);
    if (opNames.length === 0)
        return null;
    // Signal A — multi-operation common prefix.
    let prefix = '';
    if (opNames.length >= 2) {
        prefix = findCamelPrefix(opNames);
    }
    // Signal B — service-name prefix matched by ≥1 op's camel prefix.
    if ((prefix.length < 2 || prefix.length > MAX_PREFIX_LENGTH) && opNames.length >= 1) {
        for (const op of opNames) {
            const opPrefix = findCamelPrefix([op]);
            if (opPrefix.length < 2 || opPrefix.length > MAX_PREFIX_LENGTH)
                continue;
            if (stripPascalPrefix(serviceName, opPrefix) !== null) {
                prefix = opPrefix;
                break;
            }
        }
    }
    // Length cap: project tags are short. Anything longer is an English
    // verb like `save` / `claim` / `calculate` that we mustn't strip.
    if (prefix.length < 2 || prefix.length > MAX_PREFIX_LENGTH)
        return null;
    // Compute the renames + check for collisions.
    const opRenames = new Map();
    const newNamesSeen = new Set();
    for (const op of opNames) {
        const newName = stripCamelPrefix(op, prefix);
        if (newName === op)
            continue;
        if (newName.length === 0)
            return null;
        if (newNamesSeen.has(newName) || opNames.includes(newName))
            return null;
        opRenames.set(op, newName);
        newNamesSeen.add(newName);
    }
    // Activity requirement: at least one operation must share the prefix.
    // Without this, a service whose NAME happens to begin with a short
    // lowercase word (`SaveGameService`, `IdpFooService`) would get
    // renamed even when no operation evidence supports the prefix being
    // a project tag.
    if (opRenames.size === 0)
        return null;
    const newServiceName = stripPascalPrefix(serviceName, prefix);
    return {
        componentName,
        oldServiceName: serviceName,
        newServiceName: newServiceName ?? serviceName,
        prefix,
        opRenames,
    };
}
function applyClusterMatch(spec, data) {
    const components = spec.components;
    const component = components?.[data.componentName];
    const services = component?.services;
    if (!services)
        return spec;
    const serviceBody = services[data.oldServiceName];
    if (!serviceBody)
        return spec;
    // Rename operations preserving insertion order.
    const oldOps = serviceBody.operations;
    const renamedOps = {};
    for (const [oldKey, value] of Object.entries(oldOps)) {
        const newKey = data.opRenames.get(oldKey) ?? oldKey;
        renamedOps[newKey] = value;
    }
    serviceBody.operations = renamedOps;
    // Rename service key if changed. We need to preserve insertion order
    // here too — rebuild the services object with the renamed key in the
    // original slot.
    if (data.newServiceName !== data.oldServiceName) {
        const rebuilt = {};
        for (const [k, v] of Object.entries(services)) {
            if (k === data.oldServiceName)
                rebuilt[data.newServiceName] = v;
            else
                rebuilt[k] = v;
        }
        component.services = rebuilt;
    }
    return spec;
}
export const CLUSTER_MODULE_FUNCTIONS_RULE = {
    id: 'cluster-module-functions',
    summary: 'Strip project-prefix from synthesised module-function services + their operations (e.g. SmcRoundsService.smcGetRound → RoundsService.getRound).',
    ordering: 0,
    enabledByDefault: true,
    appliesWhen: (ctx) => ctx.source !== 'hand-authored',
    detect: (spec) => {
        const matches = [];
        const components = spec.components;
        if (!components || typeof components !== 'object')
            return matches;
        for (const [componentName, componentBody] of Object.entries(components)) {
            const services = componentBody?.services;
            if (!services || typeof services !== 'object')
                continue;
            for (const [serviceName, serviceBody] of Object.entries(services)) {
                if (!serviceBody || typeof serviceBody !== 'object')
                    continue;
                const data = detectClusterMatch(componentName, serviceName, serviceBody);
                if (!data)
                    continue;
                matches.push({
                    description: `${componentName}.${serviceName}: strip prefix "${data.prefix}" — ${data.opRenames.size} operation(s) renamed${data.newServiceName !== serviceName ? `, service → ${data.newServiceName}` : ''}`,
                    data,
                });
            }
        }
        return matches;
    },
    transform: (spec, match) => {
        return applyClusterMatch(spec, match.data);
    },
};
// Internal helpers exported for unit testing.
export const __internal = {
    findCamelPrefix,
    stripCamelPrefix,
    stripPascalPrefix,
    detectClusterMatch,
};
//# sourceMappingURL=cluster-module-functions.js.map