/**
 * @fileoverview Lint rules for Zod schema validation: type checking, `.describe()` presence,
 * and JSON Schema serializability.
 * Covers MCP spec rules T3-T5 and framework convention for field descriptions.
 * @module src/linter/rules/schema-rules
 */
import { toJSONSchema } from 'zod/v4/core';
/**
 * Checks that a schema is a ZodObject (required for tool inputSchema).
 * Spec: T3-T4 — inputSchema MUST be a JSON Schema object with type: "object".
 */
export function checkIsZodObject(schema, fieldName, definitionType, definitionName) {
    if (!isZodObject(schema)) {
        return {
            rule: 'schema-is-object',
            severity: 'error',
            message: `${definitionType} '${definitionName}' ${fieldName} must be a z.object(). ` +
                'MCP spec requires inputSchema to have type: "object".',
            definitionType,
            definitionName,
        };
    }
    return null;
}
/**
 * Checks that all fields in a ZodObject have `.describe()` set, recursing into
 * nested objects, array element types, and union/discriminatedUnion variants.
 * Framework convention: every field the LLM reads should carry a description.
 *
 * Path syntax in diagnostic messages:
 *   - `.key` for object properties
 *   - `[]` for array element types
 *   - `|<i>` for union / discriminatedUnion variant at index i
 */
export function checkFieldDescriptions(schema, fieldName, definitionType, definitionName) {
    if (!isZodObject(schema))
        return [];
    const diagnostics = [];
    const shape = schema.shape;
    for (const [key, field] of Object.entries(shape)) {
        walkField(field, `${fieldName}.${key}`, diagnostics, definitionType, definitionName);
    }
    return diagnostics;
}
/**
 * Emits a diagnostic when the field lacks a description, then recurses into
 * compound types (object, array, union) so inner fields get the same check.
 * A described container does NOT suppress checks on its children — each level
 * is evaluated independently because LLMs read the flattened JSON Schema.
 */
function walkField(field, path, diagnostics, definitionType, definitionName) {
    if (!hasDescription(field)) {
        diagnostics.push({
            rule: 'describe-on-fields',
            severity: 'warning',
            message: `${definitionType} '${definitionName}' ${path} is missing .describe(). ` +
                'Add .describe() to improve LLM tool-use quality.',
            definitionType,
            definitionName,
        });
    }
    recurseIntoCompound(field, path, diagnostics, definitionType, definitionName);
}
/**
 * Strips optional/nullable/default/readonly/nonoptional wrappers to find the
 * core type, then recurses into object shapes, array elements, and union
 * options. Non-compound cores (primitives, literals) terminate recursion.
 * Primitive array elements are skipped — array-level describe is sufficient.
 */
function recurseIntoCompound(field, path, diagnostics, definitionType, definitionName) {
    const core = unwrapWrappers(field);
    if (!core || typeof core !== 'object')
        return;
    const def = core._zod?.def;
    if (!def)
        return;
    if (def.type === 'object') {
        const shape = core.shape;
        for (const [key, inner] of Object.entries(shape)) {
            walkField(inner, `${path}.${key}`, diagnostics, definitionType, definitionName);
        }
        return;
    }
    if (def.type === 'array') {
        const element = def.element;
        if (element && isCompound(element)) {
            walkField(element, `${path}[]`, diagnostics, definitionType, definitionName);
        }
        return;
    }
    if (def.type === 'union') {
        const options = def.options;
        if (Array.isArray(options)) {
            options.forEach((option, i) => {
                // Skip z.literal(...) variants — structural markers (e.g. form-client
                // blank-tolerance sentinels like z.literal('')) carry no independent
                // semantic content. The outer union describe is sufficient; a describe
                // on the literal variant would ship to JSON Schema as clutter.
                if (isLiteralVariant(option))
                    return;
                walkField(option, `${path}|${i}`, diagnostics, definitionType, definitionName);
            });
        }
    }
}
/** True when the (unwrapped) field is a `z.literal(...)` — not a compound variant. */
function isLiteralVariant(field) {
    return getCoreDefType(field) === 'literal';
}
/** Recursively strips optional/nullable/default/readonly/nonoptional wrappers. */
export function unwrapWrappers(field) {
    if (!field || typeof field !== 'object')
        return field;
    const def = field._zod?.def;
    if (!def)
        return field;
    const wrapperTypes = new Set(['optional', 'nullable', 'default', 'readonly', 'nonoptional']);
    if (def.type && wrapperTypes.has(def.type) && def.innerType) {
        return unwrapWrappers(def.innerType);
    }
    return field;
}
/** True if the (unwrapped) field is an object, array, or union — a compound type worth recursing into. */
function isCompound(field) {
    const type = getCoreDefType(field);
    return type === 'object' || type === 'array' || type === 'union';
}
/** Unwrap optional/nullable/default and return the Zod 4 `_zod.def.type` discriminator. */
export function getCoreDefType(field) {
    const core = unwrapWrappers(field);
    if (!core || typeof core !== 'object')
        return;
    return core._zod?.def?.type;
}
/**
 * Checks that a Zod schema can be converted to JSON Schema.
 * The MCP SDK serializes schemas via `toJSONSchema()` when handling `tools/list`.
 * Types like `z.custom()`, `z.date()`, `z.transform()`, etc. throw at serialization
 * time, causing a hard runtime failure for any client that enumerates tools.
 */
export function checkSchemaSerializable(schema, fieldName, definitionType, definitionName) {
    if (!isZodObject(schema))
        return null;
    try {
        toJSONSchema(schema);
        return null;
    }
    catch (err) {
        const message = err instanceof Error ? err.message : 'Schema contains non-serializable types';
        return {
            rule: 'schema-serializable',
            severity: 'error',
            message: `${definitionType} '${definitionName}' ${fieldName} cannot be converted to JSON Schema: ${message}. ` +
                'Replace non-serializable types (z.custom(), z.date(), z.transform(), z.bigint(), etc.) with structural Zod types.',
            definitionType,
            definitionName,
        };
    }
}
/** Runtime check for ZodObject via Zod 4's `_zod.def.type`. */
export function isZodObject(value) {
    if (!value || typeof value !== 'object')
        return false;
    const zod = value._zod;
    return zod?.def?.type === 'object';
}
/** Reads a ZodObject's raw shape, defensively across Zod 4 / legacy internals. */
export function objectShape(schema) {
    if (!schema || typeof schema !== 'object')
        return;
    const s = schema;
    const shape = s.shape ?? s._zod?.def?.shape ?? s._def?.shape;
    return shape && typeof shape === 'object' ? shape : undefined;
}
/** Reads the top-level field names of a ZodObject, defensively across Zod 4 / legacy shapes. */
export function objectShapeKeys(schema) {
    const shape = objectShape(schema);
    return shape ? Object.keys(shape) : [];
}
/**
 * Checks whether a Zod schema (possibly wrapped in optional/nullable/default)
 * has a `.describe()` set at any level.
 */
function hasDescription(field) {
    if (!field || typeof field !== 'object')
        return false;
    const f = field;
    // Direct description on this schema
    if (typeof f.description === 'string' && f.description.length > 0)
        return true;
    // Walk through wrappers (optional, nullable, default, etc.)
    const inner = f._zod?.def?.innerType;
    if (inner)
        return hasDescription(inner);
    return false;
}
//# sourceMappingURL=schema-rules.js.map