/**
 * @internal
 * registry-audit.mjs — features-registry audit + stub emitter (C33).
 *
 * Shared module used by:
 *   - `apt-execute` post-step (`apt-tools features-audit --apply-stubs`) —
 *     auto-registers new UI/core/hook/IPC files the subtask added.
 *   - `apt-reviewer` Pass 6 (`apt-tools features-audit` without `--apply-stubs`)
 *     — reports anything the executor missed.
 *
 * Feature kinds (spec §D-4):
 *   - `ui-component`  — packages/ui/src/sections/** /*.tsx
 *   - `core-service`  — packages/core/src/{services,pipeline}/**\/*.ts
 *   - `hook`          — packages/ui/src/hooks/use-*.ts
 *   - `ipc-channel`   — apps/desktop/src/main/ipc-handlers/*.ts
 *
 * Non-matching paths → null, not included in the audit.
 */
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, join, resolve } from 'node:path';
import { atomicWriteJson } from '../util/io.mjs';
/**
 * Map a file path to a feature kind, or null if it doesn't qualify.
 */
export function detectFeatureKind(filepath) {
    if (!filepath || typeof filepath !== 'string')
        return null;
    const norm = filepath.replace(/\\/g, '/').replace(/^\.\//, '');
    // Exclude test files up-front so they never match a feature kind.
    if (/\.(test|spec)\.(ts|tsx)$/.test(norm))
        return null;
    if (/\/__tests__\//.test(norm))
        return null;
    // ui-component: packages/ui/src/sections/**/*.tsx
    if (/^packages\/ui\/src\/sections\/.+\.tsx$/.test(norm))
        return 'ui-component';
    // core-service: packages/core/src/{services,pipeline}/**/*.ts
    if (/^packages\/core\/src\/(services|pipeline)\/.+\.ts$/.test(norm))
        return 'core-service';
    // hook: packages/ui/src/hooks/use-*.ts (also use-*.tsx allowed for hook factories)
    if (/^packages\/ui\/src\/hooks\/use-[A-Za-z0-9-]+\.tsx?$/.test(norm))
        return 'hook';
    // ipc-channel: apps/desktop/src/main/ipc-handlers/*.ts
    if (/^apps\/desktop\/src\/main\/ipc-handlers\/[^/]+\.ts$/.test(norm))
        return 'ipc-channel';
    return null;
}
/**
 * Infer the features-registry "area" that should own a given path. The area
 * is the leaf folder under `sections/`, or a kind-specific pseudo-area when
 * the file isn't under sections/.
 */
export function inferArea(filepath) {
    const norm = filepath.replace(/\\/g, '/').replace(/^\.\//, '');
    const m = /^packages\/ui\/src\/sections\/([^/]+)\//.exec(norm);
    if (m)
        return m[1];
    if (/^packages\/core\/src\/pipeline\//.test(norm))
        return 'pipeline';
    if (/^packages\/core\/src\/services\//.test(norm))
        return 'services';
    if (/^packages\/ui\/src\/hooks\//.test(norm))
        return 'hooks';
    if (/^apps\/desktop\/src\/main\/ipc-handlers\//.test(norm))
        return 'ipc';
    return null;
}
/**
 * Audit a set of diff files against the features registry.
 *
 * @param {object} opts
 * @param {string} opts.projectDir
 * @param {Array<string>} opts.diffFiles  — project-relative file paths
 *
 * @returns {{
 *   new_components: Array<{file, kind, area}>,
 *   unregistered: Array<{file, kind, area}>,
 *   stub_patches: Array<StubPatch>,
 * }}
 *
 * StubPatch shape:
 *   { target: string, area: string, add_components: string[], create_area?: boolean, kind: string }
 */
export function auditFeatureRegistry({ projectDir, diffFiles }) {
    const root = resolve(projectDir);
    const featuresDir = join(root, '.aperant', 'features');
    const existing = existsSync(featuresDir) ? readFeatureArea(featuresDir) : new Map();
    const new_components = [];
    const unregistered = [];
    for (const rawFile of diffFiles || []) {
        const file = String(rawFile).replace(/\\/g, '/').replace(/^\.\//, '');
        const kind = detectFeatureKind(file);
        if (!kind)
            continue;
        const area = inferArea(file);
        if (!area)
            continue;
        const entry = { file, kind, area };
        new_components.push(entry);
        const areaData = existing.get(area);
        const components = Array.isArray(areaData?.components) ? areaData.components : null;
        if (!components || !components.includes(file)) {
            unregistered.push(entry);
        }
    }
    // Group unregistered by area → one stub_patch per area.
    const byArea = new Map();
    for (const u of unregistered) {
        if (!byArea.has(u.area))
            byArea.set(u.area, []);
        byArea.get(u.area).push(u);
    }
    const stub_patches = [];
    for (const [area, entries] of byArea) {
        const target = `.aperant/features/${area}.json`;
        const existingAreaData = existing.get(area);
        stub_patches.push({
            target,
            area,
            add_components: entries.map((e) => e.file),
            create_area: !existingAreaData,
            kind: entries[0].kind, // representative kind for new-area stubs
        });
    }
    return { new_components, unregistered, stub_patches };
}
/**
 * Read each `<area>.json` under `.aperant/features/`. Skips REGISTRY.md and
 * malformed files silently (best-effort — the audit should still run).
 *
 * @returns Map<area, { area, route?, components, p4_sub_features?, last_scanned?, kind? }>
 */
function readFeatureArea(featuresDir) {
    const out = new Map();
    let names;
    try {
        names = readdirSync(featuresDir);
    }
    catch {
        return out;
    }
    for (const name of names) {
        if (!name.endsWith('.json'))
            continue;
        try {
            const data = JSON.parse(readFileSync(join(featuresDir, name), 'utf-8'));
            const area = data?.area || basename(name, '.json');
            out.set(area, data);
        }
        catch {
            // malformed area file — skip
        }
    }
    return out;
}
/**
 * Apply stub patches from `auditFeatureRegistry`. Creates new area files when
 * `create_area: true` and appends new components to existing area files
 * otherwise. Updates REGISTRY.md's total row so the summary reflects the
 * latest counts.
 *
 * @returns Array<{target, applied: boolean, reason?: string}>
 */
export function applyStubPatches(projectDir, stubPatches) {
    const root = resolve(projectDir);
    const featuresDir = join(root, '.aperant', 'features');
    mkdirSync(featuresDir, { recursive: true });
    const applied = [];
    for (const patch of stubPatches) {
        const absTarget = join(root, patch.target);
        if (patch.create_area) {
            const stub = {
                area: patch.area,
                route: null,
                components: patch.add_components,
                kind: patch.kind,
                last_scanned: new Date().toISOString().slice(0, 10),
                p4_sub_features: [],
                auto_generated: true,
            };
            atomicWriteJson(absTarget, stub);
            applied.push({ target: patch.target, applied: true, reason: 'created-new-area' });
            continue;
        }
        // Append to existing area.
        try {
            const data = JSON.parse(readFileSync(absTarget, 'utf-8'));
            const existing = Array.isArray(data.components) ? data.components : [];
            const seen = new Set(existing);
            let added = 0;
            for (const f of patch.add_components) {
                if (!seen.has(f)) {
                    existing.push(f);
                    seen.add(f);
                    added += 1;
                }
            }
            data.components = existing;
            data.last_scanned = new Date().toISOString().slice(0, 10);
            atomicWriteJson(absTarget, data);
            applied.push({
                target: patch.target,
                applied: true,
                reason: `appended ${added} component${added === 1 ? '' : 's'}`,
            });
        }
        catch (err) {
            applied.push({ target: patch.target, applied: false, reason: err.message });
        }
    }
    // Touch REGISTRY.md — rewrite the TOTAL row to reflect new counts.
    try {
        refreshRegistryTotals(featuresDir);
    }
    catch {
        // best-effort — a malformed REGISTRY.md shouldn't fail the whole audit.
    }
    return applied;
}
/**
 * Best-effort REGISTRY.md refresher. Scans every `<area>.json`, computes total
 * components count, and rewrites the `| **TOTAL** | ...` row if it exists.
 *
 * Intentionally narrow — we do NOT touch individual area rows (which carry
 * manually curated metrics like "Wired %"). The spec says auto-update should
 * "re-compute counts"; we treat the TOTAL row as the safe proxy.
 */
function refreshRegistryTotals(featuresDir) {
    const regPath = join(featuresDir, 'REGISTRY.md');
    if (!existsSync(regPath))
        return;
    const text = readFileSync(regPath, 'utf-8');
    const totalComponents = tallyAllComponents(featuresDir);
    const lines = text.split(/\r?\n/);
    let rewrote = false;
    for (let i = 0; i < lines.length; i++) {
        if (/^\|\s*\*\*TOTAL\*\*\s*\|/.test(lines[i])) {
            // Rewrite as a minimal diagnostic row — preserve the shape but update
            // the leading count columns. We don't re-derive wired %.
            lines[i] = `| **TOTAL** | | | **${totalComponents}** | | | | auto-refreshed |`;
            rewrote = true;
            break;
        }
    }
    if (rewrote) {
        writeFileSync(regPath, lines.join('\n'));
    }
}
function tallyAllComponents(featuresDir) {
    let count = 0;
    for (const name of readdirSync(featuresDir)) {
        if (!name.endsWith('.json'))
            continue;
        try {
            const data = JSON.parse(readFileSync(join(featuresDir, name), 'utf-8'));
            count += Array.isArray(data.components) ? data.components.length : 0;
        }
        catch {
            /* ignore */
        }
    }
    return count;
}
//# sourceMappingURL=registry-audit.mjs.map