// engine/platform-engineering/idp-detector.ts — SPEC-048
// Detects signals of Internal Developer Platform need and calculates DX score.
import { access, readFile } from 'node:fs/promises';
import { join } from 'node:path';
// DX score weights (total = 10)
const DX_WEIGHTS = {
    readmeGettingStarted: 2,
    standardScripts: 2,
    devCompose: 2,
    ciConfigured: 2,
    observabilityBasic: 2,
};
// IDP signals: directories indicating multi-team monorepo structure
const MONOREPO_DIRS = ['packages', 'apps', 'services', 'libs', 'modules'];
// IaC files that indicate platform engineering context
const IAC_FILES = [
    'terraform.tf',
    'main.tf',
    'Pulumi.yaml',
    'cdk.json',
    'serverless.yml',
    'serverless.yaml',
];
async function fileExists(path) {
    try {
        await access(path);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Counts monorepo subdirectories at the given project path.
 * Returns the count of matching top-level MONOREPO_DIRS that have subdirectories.
 */
async function countMonorepoSubdirs(projectPath) {
    let found = 0;
    for (const dir of MONOREPO_DIRS) {
        const dirPath = join(projectPath, dir);
        const exists = await fileExists(dirPath);
        if (exists) {
            found++;
        }
    }
    return found;
}
/**
 * Detects whether the project has CODEOWNERS with multiple team entries.
 */
async function detectMultipleTeamsInCodeowners(projectPath) {
    const paths = [
        join(projectPath, 'CODEOWNERS'),
        join(projectPath, '.github', 'CODEOWNERS'),
        join(projectPath, 'docs', 'CODEOWNERS'),
    ];
    for (const p of paths) {
        try {
            const content = await readFile(p, 'utf-8');
            // Count unique team entries (@org/team-name patterns)
            const teamMatches = content.match(/@[\w-]+\/[\w-]+/g) ?? [];
            const uniqueTeams = new Set(teamMatches);
            if (uniqueTeams.size >= 3) {
                return true;
            }
        }
        catch {
            // file not found or not readable
        }
    }
    return false;
}
/**
 * Detects whether the project has IaC files (terraform, pulumi, cdk).
 */
async function detectIaCFiles(projectPath) {
    for (const file of IAC_FILES) {
        if (await fileExists(join(projectPath, file))) {
            return true;
        }
    }
    // Check for terraform directory
    if (await fileExists(join(projectPath, 'terraform'))) {
        return true;
    }
    if (await fileExists(join(projectPath, 'infra'))) {
        return true;
    }
    return false;
}
/**
 * Detects distinct tech stacks from language indicators in the project.
 * Returns the number of different languages detected.
 */
async function countDistinctLanguageIndicators(projectPath) {
    const languageFiles = {
        'package.json': 'javascript/typescript',
        'go.mod': 'go',
        'Cargo.toml': 'rust',
        'pom.xml': 'java',
        'build.gradle': 'java/kotlin',
        'requirements.txt': 'python',
        'pyproject.toml': 'python',
        Gemfile: 'ruby',
        'composer.json': 'php',
    };
    const detected = new Set();
    for (const [file, lang] of Object.entries(languageFiles)) {
        if (await fileExists(join(projectPath, file))) {
            detected.add(lang);
        }
    }
    return detected.size;
}
/**
 * Determines the team scale based on project signals.
 */
export async function detectTeamScale(projectPath) {
    const [monorepoCount, multipleTeams, languageCount, hasIaC] = await Promise.all([
        countMonorepoSubdirs(projectPath),
        detectMultipleTeamsInCodeowners(projectPath),
        countDistinctLanguageIndicators(projectPath),
        detectIaCFiles(projectPath),
    ]);
    // Platform Engineering Candidate signals:
    // - monorepo with >= 3 subdirectories, OR
    // - CODEOWNERS with >= 3 teams, OR
    // - >= 3 distinct language stacks
    const isPlatformCandidate = monorepoCount >= 3 || multipleTeams || languageCount >= 3 || (monorepoCount >= 2 && hasIaC);
    if (isPlatformCandidate) {
        return 'platform-engineering-candidate';
    }
    // Multi-team: 2 monorepo dirs or IaC files or 2 languages
    if (monorepoCount >= 2 || (hasIaC && languageCount >= 2)) {
        return 'multi-team';
    }
    // Small team: any monorepo dir or IaC
    if (monorepoCount >= 1 || hasIaC) {
        return 'small-team';
    }
    return 'solo';
}
/**
 * Calculates the Developer Experience score (0–10) for the project.
 * Checks for: README Getting Started, standard scripts, docker-compose, CI, observability.
 */
export async function calculateDXScore(projectPath) {
    const [hasReadme, hasScripts, hasCompose, hasCi, hasObs] = await Promise.all([
        checkReadmeGettingStarted(projectPath),
        checkStandardScripts(projectPath),
        checkDevCompose(projectPath),
        checkCiConfigured(projectPath),
        checkObservabilityBasic(projectPath),
    ]);
    const breakdown = {
        readmeGettingStarted: hasReadme,
        standardScripts: hasScripts,
        devCompose: hasCompose,
        ciConfigured: hasCi,
        observabilityBasic: hasObs,
    };
    const score = (hasReadme ? DX_WEIGHTS.readmeGettingStarted : 0) +
        (hasScripts ? DX_WEIGHTS.standardScripts : 0) +
        (hasCompose ? DX_WEIGHTS.devCompose : 0) +
        (hasCi ? DX_WEIGHTS.ciConfigured : 0) +
        (hasObs ? DX_WEIGHTS.observabilityBasic : 0);
    const gaps = [];
    if (!hasReadme) {
        gaps.push({
            area: 'documentation',
            severity: 'warning',
            message: 'README does not have a "Getting Started" section',
            fix: 'Add a "Getting Started" or "Quick Start" section to README.md with local setup steps',
        });
    }
    if (!hasScripts) {
        gaps.push({
            area: 'scripts',
            severity: 'warning',
            message: 'No standard scripts found (Makefile or scripts/ directory)',
            fix: 'Add a Makefile or scripts/ directory with standard commands: setup, dev, test, build',
        });
    }
    if (!hasCompose) {
        gaps.push({
            area: 'dev-environment',
            severity: 'warning',
            message: 'No docker-compose.yml for local development environment',
            fix: 'Add docker-compose.yml to enable one-command local environment setup',
        });
    }
    if (!hasCi) {
        gaps.push({
            area: 'ci',
            severity: 'warning',
            message: 'No CI/CD configuration detected',
            fix: 'Add CI configuration (.github/workflows/, .gitlab-ci.yml, etc.)',
        });
    }
    if (!hasObs) {
        gaps.push({
            area: 'observability',
            severity: 'info',
            message: 'No basic observability configuration detected',
            fix: 'Add health check endpoint, structured logging, and basic metrics (Prometheus/OTel)',
        });
    }
    const platformEngineeringCandidate = score <= 6;
    return { score, breakdown, platformEngineeringCandidate, gaps };
}
async function checkReadmeGettingStarted(projectPath) {
    const readmePaths = ['README.md', 'README.MD', 'readme.md', 'Readme.md'];
    for (const name of readmePaths) {
        try {
            const content = await readFile(join(projectPath, name), 'utf-8');
            const lower = content.toLowerCase();
            return (lower.includes('getting started') ||
                lower.includes('quick start') ||
                lower.includes('quickstart') ||
                lower.includes('installation') ||
                lower.includes('how to run'));
        }
        catch {
            // try next
        }
    }
    return false;
}
async function checkStandardScripts(projectPath) {
    const hasMakefile = await fileExists(join(projectPath, 'Makefile'));
    const hasScriptsDir = await fileExists(join(projectPath, 'scripts'));
    return hasMakefile || hasScriptsDir;
}
async function checkDevCompose(projectPath) {
    return ((await fileExists(join(projectPath, 'docker-compose.yml'))) ||
        (await fileExists(join(projectPath, 'docker-compose.yaml'))) ||
        (await fileExists(join(projectPath, 'compose.yml'))) ||
        (await fileExists(join(projectPath, 'compose.yaml'))));
}
async function checkCiConfigured(projectPath) {
    return ((await fileExists(join(projectPath, '.github', 'workflows'))) ||
        (await fileExists(join(projectPath, '.gitlab-ci.yml'))) ||
        (await fileExists(join(projectPath, '.circleci'))) ||
        (await fileExists(join(projectPath, 'Jenkinsfile'))) ||
        (await fileExists(join(projectPath, '.travis.yml'))));
}
async function checkObservabilityBasic(projectPath) {
    // Check for common observability config files
    const obsFiles = [
        'prometheus.yml',
        'prometheus.yaml',
        'otel-collector-config.yml',
        'opentelemetry.js',
        'opentelemetry.ts',
        'monitoring',
        'observability',
    ];
    for (const file of obsFiles) {
        if (await fileExists(join(projectPath, file))) {
            return true;
        }
    }
    return false;
}
//# sourceMappingURL=idp-detector.js.map