// engine/workflow-validator/mcp-conventions.ts — SPEC-053
// Audits MCP tool files for naming, Zod .describe(), annotation, and error message conventions.
import { readFileSync } from 'node:fs';
import { glob } from 'glob';
// === Helpers ===
function readFileLines(filePath) {
    try {
        return readFileSync(filePath, 'utf-8').split('\n');
    }
    catch {
        return [];
    }
}
// === C4: Detect Zod schemas missing .describe() on fields ===
function checkMissingDescribe(filePath, lines) {
    const violations = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        // Match Zod field definitions: z.string(), z.number(), z.boolean(), z.array(), z.object()
        // that do NOT have .describe() on the same line and the next line doesn't chain .describe()
        const isZodField = /z\.(string|number|boolean|array|object|enum|record|union)\(/.test(line);
        if (!isZodField) {
            continue;
        }
        const nextLine = lines[i + 1] ?? '';
        const hasDescribeOnLine = line.includes('.describe(');
        const hasDescribeOnNext = nextLine.includes('.describe(');
        // Only flag lines that look like schema field definitions (inside z.object shape)
        // They typically end with a comma or closing paren, indicating field-level assignment
        const isFieldDefinition = /\w+:\s*z\./.test(line);
        if (!isFieldDefinition) {
            continue;
        }
        if (!hasDescribeOnLine && !hasDescribeOnNext) {
            violations.push({
                file: filePath,
                line: i + 1,
                violationType: 'missing-describe',
                suggestion: 'Add .describe("explanation of what this field represents") to every Zod schema field.',
            });
        }
    }
    return violations;
}
// === C5: Detect missing readOnlyHint / destructiveHint annotations ===
function checkMissingAnnotations(filePath, lines) {
    const violations = [];
    const content = lines.join('\n');
    // Check for tool registration patterns (server.tool or similar MCP tool registrations)
    const hasToolRegistration = content.includes('server.tool(') ||
        content.includes('registerTool(') ||
        content.includes('annotations:');
    if (!hasToolRegistration) {
        return violations;
    }
    const hasReadOnlyHint = content.includes('readOnlyHint');
    const hasDestructiveHint = content.includes('destructiveHint');
    if (!hasReadOnlyHint && !hasDestructiveHint) {
        // Find the first tool registration line as the violation location
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i] ?? '';
            if (line.includes('server.tool(') || line.includes('registerTool(')) {
                violations.push({
                    file: filePath,
                    line: i + 1,
                    violationType: 'missing-readonly-hint',
                    suggestion: 'Add annotations: { readOnlyHint: true } for read-only tools or { destructiveHint: true } for tools that modify/delete data.',
                });
                break;
            }
        }
    }
    return violations;
}
// === C6: Detect error messages missing action hint ===
// Patterns that indicate a bare error message without action guidance
const ACTION_HINT_PHRASES = [
    'please',
    'try',
    'check',
    'run',
    'make sure',
    'ensure',
    'use',
    'provide',
    'add',
    'remove',
    'update',
    'install',
    'create',
    'delete',
    'see',
];
function lineHasActionHint(line) {
    const lower = line.toLowerCase();
    return ACTION_HINT_PHRASES.some((phrase) => lower.includes(phrase));
}
function checkMissingActionHint(filePath, lines) {
    const violations = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        // Look for throw new Error(...) or error return patterns
        const isErrorLine = line.includes('throw new Error(') ||
            /content:\s*\[.*text:/.test(line) ||
            line.includes('isError: true');
        if (!isErrorLine) {
            continue;
        }
        // Get the surrounding context (current line + next 2 lines) to check for action hint
        const context = [line, lines[i + 1] ?? '', lines[i + 2] ?? ''].join(' ');
        if (!lineHasActionHint(context)) {
            violations.push({
                file: filePath,
                line: i + 1,
                violationType: 'missing-action-hint',
                suggestion: 'Error messages must explain what failed AND what the user should do next. Format: "<what failed>. <action to take>".',
            });
        }
    }
    return violations;
}
// === Public API ===
export async function auditMcpConventions(toolsDir) {
    const files = await glob('**/*.ts', {
        cwd: toolsDir,
        nodir: true,
        ignore: ['**/*.test.ts', '**/*.d.ts', 'node_modules/**'],
    });
    const allViolations = [];
    for (const relFile of files) {
        const fullPath = `${toolsDir}/${relFile}`;
        const lines = readFileLines(fullPath);
        allViolations.push(...checkMissingDescribe(fullPath, lines));
        allViolations.push(...checkMissingAnnotations(fullPath, lines));
        allViolations.push(...checkMissingActionHint(fullPath, lines));
    }
    return {
        violations: allViolations,
        totalChecked: files.length,
    };
}
//# sourceMappingURL=mcp-conventions.js.map