/**
 * AgentScanner -- Discovery engine for AI agent definitions across platforms.
 *
 * Scans project directories to detect agent configurations from Claude Code,
 * OpenAI, Cursor, and Gemini platforms. Determines governance status and
 * capabilities for each discovered agent.
 *
 * GATE 9: T-146
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../util/log.js';
const log = createLogger('agent-scanner');
/**
 * Governance markers that indicate an agent has governance controls applied.
 * Presence of these in agent content or project structure raises governance status.
 */
const GOVERNANCE_MARKERS = [
    'PERMISSIONS.md',
    'governance-runtime',
    'deny-by-default',
    'tamper-detection',
    'SHA-256',
    'capability-bundle',
    'cycle-lock',
    'approval-queue',
    'audit-log',
    '@connexum/ai-governance',
];
/**
 * Directories at the top level of a scanned project that are NEVER agents —
 * common build / VCS / dependency / docs paths. Skipping these early avoids
 * walking into massive trees like node_modules during vendor-neutral scan.
 */
const NON_AGENT_DIRS = new Set([
    'node_modules',
    'dist',
    'build',
    'out',
    'target',
    '__pycache__',
    '.next',
    '.cache',
    '.venv',
    'venv',
    'env',
    'coverage',
    'tests',
    'test',
    'docs',
    'scripts',
    'public',
    'assets',
    'site',
]);
/**
 * Recognized LLM / agent-orchestration SDK import patterns. If a source file
 * in a candidate directory imports any of these, the directory counts as an
 * agent for the vendor-neutral scanner. The list deliberately covers the
 * top frameworks customers actually deploy — it is NOT exhaustive, and new
 * frameworks should be added as customer demand surfaces.
 *
 * Patterns are case-insensitive and match common JS/TS import + Python
 * `import` / `from x import y` shapes.
 */
const LLM_SDK_IMPORT_PATTERN = new RegExp([
    // Anthropic
    '@anthropic-ai/sdk',
    '@anthropic-ai/claude',
    '\\banthropic\\b',
    // OpenAI
    '@openai/sdk',
    '\\bopenai\\b',
    // Bedrock / Azure / Vertex
    '@aws-sdk/client-bedrock',
    'boto3.*bedrock',
    '@azure/openai',
    'azure-ai-inference',
    '@google-cloud/vertexai',
    'google-genai',
    'google-generativeai',
    // Orchestration frameworks
    'langchain',
    'langgraph',
    'langchain_anthropic',
    'langchain_openai',
    'crewai',
    'autogen',
    'pyautogen',
    'pydantic_ai',
    'llama_index',
    'llama-index',
    'haystack',
    'semantic_kernel',
    'semantic-kernel',
    // Open-source local-LLM runners
    'huggingface_hub',
    'transformers',
    'replicate',
    // Connexum's own SDK
    '@connexum/ai-governance',
    '@connexum/typescript-sdk',
    'connexum_governance',
].join('|'), 'i');
/**
 * Patterns that indicate agent capabilities from file content.
 */
const CAPABILITY_PATTERNS = [
    { pattern: /\bBash\b/i, capability: 'bash' },
    { pattern: /\bWebSearch\b/i, capability: 'web-search' },
    { pattern: /\bWebFetch\b/i, capability: 'web-fetch' },
    { pattern: /\bPlaywright\b|mcp__playwright/i, capability: 'browser' },
    { pattern: /\bRead\b.*file|file.*\bRead\b/i, capability: 'file-read' },
    { pattern: /\bEdit\b.*file|file.*\bEdit\b|Write.*file/i, capability: 'file-write' },
    { pattern: /\bgit\b/i, capability: 'git' },
    { pattern: /\bAPI\b.*key|api[_-]?key/i, capability: 'api-keys' },
    { pattern: /\bdeploy\b/i, capability: 'deploy' },
    { pattern: /\bexecute\b.*code|code.*\bexecute\b/i, capability: 'code-execution' },
];
export class AgentScanner {
    /**
     * Scan a project directory for agent definitions across all supported platforms.
     *
     * Default behaviour: treats `projectDir` as the agent itself. The vendor-
     * neutral detector inspects ONLY `projectDir` — it does not walk subdirs.
     * Pass `{ monorepo: true }` to opt into the legacy subdir-walk behaviour
     * for fleet uploads.
     */
    scan(projectDir, options = {}) {
        const resolvedDir = path.resolve(projectDir);
        const monorepo = options.monorepo === true;
        log.info(`Scanning project directory: ${resolvedDir} (mode=${monorepo ? 'monorepo' : 'directed'})`);
        const agents = [];
        // Check for governance infrastructure in the project
        const hasGovernanceInfra = this.detectGovernanceInfrastructure(resolvedDir);
        // Scan each platform
        agents.push(...this.scanClaudeCode(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanOpenAI(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanCursor(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanGemini(resolvedDir, hasGovernanceInfra));
        agents.push(...this.scanCustomAgents(resolvedDir, hasGovernanceInfra, agents, monorepo));
        const stats = this.computeStats(agents);
        log.info(`Scan complete: ${stats.total} agents found`, {
            governed: stats.governed,
            partial: stats.partial,
            ungoverned: stats.ungoverned,
        });
        return {
            agents,
            projectDir: resolvedDir,
            scannedAt: new Date().toISOString(),
            stats,
        };
    }
    /**
     * Detect whether the project has governance infrastructure installed.
     */
    detectGovernanceInfrastructure(projectDir) {
        const markers = new Set();
        // Check for .claude/permissions/ directory
        const permDir = path.join(projectDir, '.claude', 'permissions');
        if (fs.existsSync(permDir)) {
            markers.add('permissions-dir');
        }
        // Check for governance config files
        const governanceFiles = [
            path.join(projectDir, '.claude', 'governance.json'),
            path.join(projectDir, '.governance', 'config.json'),
            path.join(projectDir, 'governance.config.json'),
        ];
        for (const f of governanceFiles) {
            if (fs.existsSync(f)) {
                markers.add('governance-config');
                break;
            }
        }
        // Check for hooks directory
        const hooksDir = path.join(projectDir, '.claude', 'hooks');
        if (fs.existsSync(hooksDir)) {
            markers.add('hooks');
        }
        // Check CLAUDE.md for governance references
        const claudeMd = path.join(projectDir, 'CLAUDE.md');
        if (fs.existsSync(claudeMd)) {
            try {
                const content = fs.readFileSync(claudeMd, 'utf-8');
                for (const marker of GOVERNANCE_MARKERS) {
                    if (content.includes(marker)) {
                        markers.add(`claude-md:${marker}`);
                    }
                }
            }
            catch {
                // Skip unreadable files
            }
        }
        return markers;
    }
    /**
     * Scan for Claude Code agent definitions.
     */
    scanClaudeCode(projectDir, infraMarkers) {
        const agents = [];
        // 1. Scan .claude/agents/*.md
        const agentsDir = path.join(projectDir, '.claude', 'agents');
        if (fs.existsSync(agentsDir)) {
            try {
                const files = fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
                for (const file of files) {
                    const filePath = path.join(agentsDir, file);
                    const name = path.basename(file, '.md');
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    const governanceStatus = this.assessGovernanceStatus(content, infraMarkers, name, projectDir);
                    agents.push({
                        name,
                        platform: 'claude',
                        filePath,
                        governanceStatus,
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read agents directory: ${agentsDir}`);
            }
        }
        // 2. Check CLAUDE.md for inline agent definitions
        const claudeMd = path.join(projectDir, 'CLAUDE.md');
        if (fs.existsSync(claudeMd)) {
            const content = this.safeReadFile(claudeMd);
            if (content && this.containsAgentDefinitions(content)) {
                const capabilities = this.extractCapabilities(content);
                const governanceStatus = this.assessGovernanceStatus(content, infraMarkers, 'claude-md', projectDir);
                agents.push({
                    name: 'claude-md-config',
                    platform: 'claude',
                    filePath: claudeMd,
                    governanceStatus,
                    capabilities,
                });
            }
        }
        // 3. Check .claude/settings.json
        const settingsJson = path.join(projectDir, '.claude', 'settings.json');
        if (fs.existsSync(settingsJson)) {
            const content = this.safeReadFile(settingsJson);
            if (content) {
                try {
                    const settings = JSON.parse(content);
                    if (settings.agents || settings.permissions || settings.tools) {
                        const capabilities = this.extractSettingsCapabilities(settings);
                        agents.push({
                            name: 'claude-settings',
                            platform: 'claude',
                            filePath: settingsJson,
                            governanceStatus: infraMarkers.size > 2 ? 'partial' : 'ungoverned',
                            capabilities,
                        });
                    }
                }
                catch {
                    // Invalid JSON, skip
                }
            }
        }
        return agents;
    }
    /**
     * Scan for OpenAI agent definitions.
     */
    scanOpenAI(projectDir, infraMarkers) {
        const agents = [];
        // Check .openai/ directory
        const openaiDir = path.join(projectDir, '.openai');
        if (fs.existsSync(openaiDir)) {
            try {
                const files = fs.readdirSync(openaiDir).filter(f => f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml'));
                for (const file of files) {
                    const filePath = path.join(openaiDir, file);
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name,
                        platform: 'openai',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read OpenAI directory: ${openaiDir}`);
            }
        }
        // Check openai.json in project root
        const openaiJson = path.join(projectDir, 'openai.json');
        if (fs.existsSync(openaiJson)) {
            const content = this.safeReadFile(openaiJson);
            const capabilities = this.extractCapabilities(content);
            agents.push({
                name: 'openai-config',
                platform: 'openai',
                filePath: openaiJson,
                governanceStatus: 'ungoverned',
                capabilities,
            });
        }
        return agents;
    }
    /**
     * Scan for Cursor agent definitions.
     */
    scanCursor(projectDir, infraMarkers) {
        const agents = [];
        // Check .cursor/ directory
        const cursorDir = path.join(projectDir, '.cursor');
        if (fs.existsSync(cursorDir)) {
            try {
                const files = fs.readdirSync(cursorDir).filter(f => f.endsWith('.json') || f.endsWith('.md') || f === 'rules');
                for (const file of files) {
                    const filePath = path.join(cursorDir, file);
                    // Skip if it's a directory (like rules/)
                    if (fs.statSync(filePath).isDirectory())
                        continue;
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name: `cursor-${name}`,
                        platform: 'cursor',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read Cursor directory: ${cursorDir}`);
            }
        }
        // Check .cursorrules in project root
        const cursorRules = path.join(projectDir, '.cursorrules');
        if (fs.existsSync(cursorRules)) {
            const content = this.safeReadFile(cursorRules);
            const capabilities = this.extractCapabilities(content);
            agents.push({
                name: 'cursorrules',
                platform: 'cursor',
                filePath: cursorRules,
                governanceStatus: 'ungoverned',
                capabilities,
            });
        }
        return agents;
    }
    /**
     * Scan for Gemini agent definitions.
     */
    scanGemini(projectDir, infraMarkers) {
        const agents = [];
        // Check .gemini/ directory
        const geminiDir = path.join(projectDir, '.gemini');
        if (fs.existsSync(geminiDir)) {
            try {
                const files = fs.readdirSync(geminiDir).filter(f => f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md'));
                for (const file of files) {
                    const filePath = path.join(geminiDir, file);
                    if (fs.statSync(filePath).isDirectory())
                        continue;
                    const name = path.basename(file, path.extname(file));
                    const content = this.safeReadFile(filePath);
                    const capabilities = this.extractCapabilities(content);
                    agents.push({
                        name: `gemini-${name}`,
                        platform: 'gemini',
                        filePath,
                        governanceStatus: 'ungoverned',
                        capabilities,
                    });
                }
            }
            catch {
                log.warn(`Failed to read Gemini directory: ${geminiDir}`);
            }
        }
        return agents;
    }
    /**
     * Scan for vendor-neutral agent definitions across any framework.
     *
     * Customers deploy AI agents in many shapes — LangChain, AutoGen, CrewAI,
     * OpenAI Assistants, native Anthropic SDK, native OpenAI SDK, custom
     * Python / TypeScript / JavaScript, framework-specific conventions like
     * Claude Code's `.claude/agents/*.md`, etc. The platform-specific detectors
     * above cover the well-known IDE/SDK paths; this detector handles the
     * generic case: a directory that "looks like an agent" by any of these
     * signals.
     *
     * Detection (any ONE signal makes a directory eligible):
     *   1. Source-file LLM-SDK import — a `.py`, `.ts`, `.js`, or `.mjs` file
     *      that imports one of the recognized LLM/orchestration SDKs.
     *   2. Manifest declaration — a `manifest.json` / `agent.yaml` / equivalent
     *      with an `agent_name` or `name`+`framework` field.
     *   3. Identity-doc + co-located source — an `IDENTITY.md` (or `AGENT.md`)
     *      alongside a source file, regardless of framework.
     *
     * De-duplication: if a directory was already discovered as a platform-
     * specific agent (Claude / OpenAI / Cursor / Gemini), this detector skips
     * it to avoid double-counting.
     *
     * The `platform` field on returned agents is set to `'custom'` — naming
     * the actual framework (LangChain, CrewAI, etc.) requires a follow-on
     * scanner extension; the current call-site cares mainly about "an agent
     * exists here" + extracted capabilities.
     */
    scanCustomAgents(projectDir, infraMarkers, alreadyDiscovered, monorepo) {
        const agents = [];
        // De-dupe: build a set of absolute directories already claimed by
        // platform-specific detectors. We skip any dir already represented.
        const claimedDirs = new Set();
        for (const d of alreadyDiscovered) {
            claimedDirs.add(path.dirname(d.filePath));
        }
        /**
         * Convert a `CustomAgentDetection` into a `DiscoveredAgent` row.
         */
        const buildAgent = (detection) => {
            // Capabilities: union of source-import patterns + manifest tools +
            // PERMISSIONS.md ALLOWED entries (any subset present).
            const capabilities = new Set();
            if (detection.sourceContent) {
                for (const cap of this.extractCapabilities(detection.sourceContent)) {
                    capabilities.add(cap);
                }
            }
            for (const t of detection.manifestTools)
                capabilities.add(t);
            for (const t of detection.permissionsAllowed)
                capabilities.add(t);
            // Governance status: same heuristic the platform-specific paths use.
            // A PERMISSIONS.md with deny-by-default phrasing + governance infra
            // markers ⇒ 'governed'; PERMISSIONS.md alone ⇒ 'partial'; nothing ⇒
            // 'ungoverned'.
            let governanceStatus = 'ungoverned';
            if (detection.hasPermissionsDoc) {
                const isDenyByDefault = /DENY[-_ ]BY[-_ ]DEFAULT/i.test(detection.permissionsContent ?? '');
                if (isDenyByDefault && infraMarkers.size > 0) {
                    governanceStatus = 'governed';
                }
                else {
                    governanceStatus = 'partial';
                }
            }
            return {
                name: detection.name,
                platform: 'custom',
                filePath: detection.anchorPath,
                governanceStatus,
                capabilities: Array.from(capabilities),
            };
        };
        // ---------------------------------------------------------------------
        // Direction-aware (default) — Thomas 2026-05-20:
        //
        //   "The scanner should only work after you're being directed to the
        //    folder that holds or the project that holds the agent."
        //
        // The customer points the scanner at a specific folder. That folder IS
        // the agent. No subdir walking — that surfaces false positives from
        // `tests/`, `examples/`, `node_modules/`, etc.
        // ---------------------------------------------------------------------
        if (!monorepo) {
            if (claimedDirs.has(projectDir)) {
                // Platform-specific detector already claimed the root — nothing to add.
                return agents;
            }
            const detection = this.detectCustomAgent(projectDir);
            if (detection)
                agents.push(buildAgent(detection));
            return agents;
        }
        // ---------------------------------------------------------------------
        // Monorepo / fleet mode (opt-in via { monorepo: true }) — legacy behaviour
        //
        // Walks one level of subdirs, skipping build / vendor / docs trees.
        // ---------------------------------------------------------------------
        let entries;
        try {
            entries = fs.readdirSync(projectDir);
        }
        catch {
            return agents;
        }
        for (const entry of entries) {
            const subdir = path.join(projectDir, entry);
            let isDir = false;
            try {
                isDir = fs.statSync(subdir).isDirectory();
            }
            catch {
                continue;
            }
            if (!isDir)
                continue;
            if (entry.startsWith('.') || NON_AGENT_DIRS.has(entry))
                continue;
            if (claimedDirs.has(subdir))
                continue;
            const detection = this.detectCustomAgent(subdir);
            if (!detection)
                continue;
            agents.push(buildAgent(detection));
        }
        return agents;
    }
    /**
     * Determine whether a given directory contains an agent definition by
     * any of the vendor-neutral signals. Returns the metadata needed to
     * construct a `DiscoveredAgent`, or `null` if no signal matched.
     */
    detectCustomAgent(dir) {
        let entries;
        try {
            entries = fs.readdirSync(dir);
        }
        catch {
            return null;
        }
        // --- Manifest scan ---
        let manifestPath = null;
        let manifest = {};
        for (const m of ['manifest.json', 'agent.json', 'agent.yaml', 'agent.yml']) {
            const candidate = path.join(dir, m);
            if (fs.existsSync(candidate)) {
                manifestPath = candidate;
                if (m === 'manifest.json' || m === 'agent.json') {
                    try {
                        const raw = this.safeReadFile(candidate);
                        if (raw)
                            manifest = JSON.parse(raw);
                    }
                    catch {
                        manifest = {};
                    }
                }
                break;
            }
        }
        const manifestDeclaresAgent = manifestPath !== null &&
            (typeof manifest.agent_name === 'string' ||
                typeof manifest.name === 'string' ||
                typeof manifest.framework === 'string');
        // --- Identity doc scan ---
        let identityPath = null;
        for (const id of ['IDENTITY.md', 'AGENT.md', 'agent.md', 'identity.md']) {
            const candidate = path.join(dir, id);
            if (fs.existsSync(candidate)) {
                identityPath = candidate;
                break;
            }
        }
        // --- Source-file LLM-SDK scan ---
        // Look at top-level source files only (no deep recursion — that would
        // hit node_modules pollution). For each .py / .ts / .js / .mjs file
        // present at the root of the dir, peek at its contents for any of the
        // recognized SDK imports.
        let sourcePath = null;
        let sourceContent = '';
        for (const file of entries) {
            const lower = file.toLowerCase();
            if (!/\.(py|ts|tsx|js|jsx|mjs)$/.test(lower))
                continue;
            const candidate = path.join(dir, file);
            try {
                if (!fs.statSync(candidate).isFile())
                    continue;
            }
            catch {
                continue;
            }
            const content = this.safeReadFile(candidate);
            if (!content)
                continue;
            if (LLM_SDK_IMPORT_PATTERN.test(content)) {
                sourcePath = candidate;
                sourceContent = content;
                break;
            }
            // First source file is recorded as fallback even without SDK signal
            // if a manifest or identity doc independently declares this is an agent.
            if (!sourcePath) {
                sourcePath = candidate;
                sourceContent = content;
            }
        }
        const hasSdkImport = sourceContent.length > 0 && LLM_SDK_IMPORT_PATTERN.test(sourceContent);
        const hasIdentityWithSource = identityPath !== null && sourcePath !== null;
        // Decision: any one of the three signals is sufficient.
        const eligible = hasSdkImport || manifestDeclaresAgent || hasIdentityWithSource;
        if (!eligible)
            return null;
        // --- Name resolution ---
        const manifestName = typeof manifest.agent_name === 'string'
            ? manifest.agent_name
            : typeof manifest.name === 'string'
                ? manifest.name
                : null;
        const name = manifestName || path.basename(dir);
        // --- Manifest tools array (string or {name} objects) ---
        const manifestTools = [];
        if (Array.isArray(manifest.tools)) {
            for (const t of manifest.tools) {
                if (typeof t === 'string')
                    manifestTools.push(t);
                else if (t && typeof t === 'object' && 'name' in t &&
                    typeof t.name === 'string') {
                    manifestTools.push(t.name);
                }
            }
        }
        // --- Permissions doc ---
        const permissionsPath = path.join(dir, 'PERMISSIONS.md');
        const hasPermissionsDoc = fs.existsSync(permissionsPath);
        const permissionsContent = hasPermissionsDoc
            ? this.safeReadFile(permissionsPath)
            : '';
        // Extract ALLOWED + APPROVAL_REQUIRED tool names from PERMISSIONS.md
        // Format: `- tool_name: ALLOWED` / `- tool_name: APPROVAL_REQUIRED`
        const permissionsAllowed = [];
        if (permissionsContent) {
            const re = /^\s*[-*]\s*([a-z][a-z0-9_-]*)\s*:\s*(ALLOWED|APPROVAL_REQUIRED)\b/gim;
            let m;
            while ((m = re.exec(permissionsContent)) !== null) {
                permissionsAllowed.push(m[1]);
            }
        }
        // Anchor path: prefer identity doc > manifest > source. This is the
        // "primary file" surfaced in the dashboard's discovered-agent row.
        const anchorPath = identityPath ?? manifestPath ?? sourcePath ?? dir;
        return {
            name,
            anchorPath,
            sourcePath,
            sourceContent,
            manifestPath,
            manifestTools,
            hasPermissionsDoc,
            permissionsContent,
            permissionsAllowed,
        };
    }
    /**
     * Determine governance status for a Claude Code agent.
     */
    assessGovernanceStatus(content, infraMarkers, agentName, projectDir) {
        let agentScore = 0; // Score from agent-specific evidence
        let infraScore = 0; // Score from project-level infrastructure
        // Check for agent-specific permissions file (strong signal)
        const permFile = path.join(projectDir, '.claude', 'permissions', `${agentName}.md`);
        if (fs.existsSync(permFile)) {
            agentScore += 4;
        }
        // Check content for governance markers
        for (const marker of GOVERNANCE_MARKERS) {
            if (content.toLowerCase().includes(marker.toLowerCase())) {
                agentScore += 1;
            }
        }
        // Infrastructure markers provide partial credit only
        if (infraMarkers.has('permissions-dir'))
            infraScore += 1;
        if (infraMarkers.has('governance-config'))
            infraScore += 1;
        if (infraMarkers.has('hooks'))
            infraScore += 1;
        // Agent-specific evidence is required for elevated governance status.
        // Infrastructure alone cannot classify an agent as governed or partial.
        const totalScore = agentScore + infraScore;
        if (agentScore >= 3 && totalScore >= 5)
            return 'governed';
        if (agentScore >= 1 && totalScore >= 3)
            return 'partial';
        return 'ungoverned';
    }
    /**
     * Extract capabilities from agent file content.
     */
    extractCapabilities(content) {
        const capabilities = [];
        for (const { pattern, capability } of CAPABILITY_PATTERNS) {
            if (pattern.test(content)) {
                capabilities.push(capability);
            }
        }
        return [...new Set(capabilities)];
    }
    /**
     * Extract capabilities from a parsed settings JSON object.
     */
    extractSettingsCapabilities(settings) {
        const capabilities = [];
        if (settings.tools && Array.isArray(settings.tools)) {
            capabilities.push(...settings.tools.map((t) => typeof t === 'string' ? t : String(t)));
        }
        if (settings.allowedTools && Array.isArray(settings.allowedTools)) {
            capabilities.push(...settings.allowedTools.map((t) => typeof t === 'string' ? t : String(t)));
        }
        return [...new Set(capabilities)];
    }
    /**
     * Check if CLAUDE.md content contains agent-like definitions.
     */
    containsAgentDefinitions(content) {
        const agentPatterns = [
            /agent[s]?\s*:/i,
            /## Agent/i,
            /role\s*:/i,
            /capabilities\s*:/i,
            /tools\s*:/i,
            /permissions\s*:/i,
        ];
        let matchCount = 0;
        for (const pattern of agentPatterns) {
            if (pattern.test(content))
                matchCount++;
        }
        return matchCount >= 2;
    }
    /**
     * Safely read a file, returning empty string on failure.
     */
    safeReadFile(filePath) {
        try {
            return fs.readFileSync(filePath, 'utf-8');
        }
        catch {
            return '';
        }
    }
    /**
     * Compute aggregate statistics from discovered agents.
     */
    computeStats(agents) {
        return {
            total: agents.length,
            governed: agents.filter(a => a.governanceStatus === 'governed').length,
            partial: agents.filter(a => a.governanceStatus === 'partial').length,
            ungoverned: agents.filter(a => a.governanceStatus === 'ungoverned').length,
        };
    }
}
// DA-MED-NEW-1 (P1H-01b): re-export the manifest-integrity rule class so
// consumers of `@connexum/ai-governance/scanner` can run the MANI rules
// without reaching into an internal path.
export { evaluateManifest, evaluateManifests, } from './manifest-integrity.js';
//# sourceMappingURL=index.js.map