import { z } from 'zod';
import { DEFAULT_ENTITY_BOUNDS, EntityKindSchema } from '../code/entity/index.js';
import { resolveWithinRepo } from '../code/source.js';
import { CodeDocKindSchema } from '../code/doc.js';
import { formatCodeDocs } from '../code/output.js';
import { ENTITY_CONTEXT_DEFAULT_BUDGET, ENTITY_HISTORY_DEFAULT_LIMIT, ENTITY_HISTORY_MAX_LIMIT, GitRangeModeSchema, OccEntityImpactScopeSchema, } from '../code/entity/api-types.js';
import { formatAnchorCapture, formatEntityBlame, formatEntityCochange, formatEntityContext, formatEntityDiff, formatEntityHistory, formatEntityHotspots, formatEntityImpact, formatEntityList, formatEntityStatus, } from '../code/entity/output.js';
import { SourceInlineBlockSchema, SourceInlineRangeSchema } from '../code/source-inline.js';
/**
 * The thirteen `occ_*` MCP tools.
 *
 * Inputs are OCC's own Zod schemas, so an MCP client is validated by exactly
 * the rules the CLI applies — the published JSON Schema is generated from them
 * and cannot drift. Outputs are declared as *loose* envelopes: the status
 * discriminant a client must switch on is pinned, while the payload passes
 * through unvalidated. That is deliberate. The SDK rejects a call whose
 * `structuredContent` fails its declared `outputSchema`, so a strict mirror of
 * the result interfaces would turn any additive OCC change into a hard error
 * for every already-shipped client. The diff receipt adds contradiction-only
 * cross-field checks, never closed-world equality. The authoritative
 * field-by-field contract is the exported TypeScript type named in each tool's
 * description.
 */
/** Tool names, exported so tests and docs enumerate the same list the server registers. */
export const OCC_MCP_TOOL_NAMES = [
    'occ_doc',
    'occ_pattern',
    'occ_entities',
    'occ_hotspots',
    'occ_cochange',
    'occ_diff',
    'occ_impact',
    'occ_blame',
    'occ_log',
    'occ_context',
    'occ_status',
    'occ_source',
    'occ_anchor',
];
export const MCP_RESPONSE_FORMATS = ['concise', 'detailed'];
/**
 * Executable argument examples shared by the advertised descriptions and the
 * schema contract test. Keeping the objects here makes a stale example fail
 * against the same Zod input shape that rejects a real MCP request.
 */
export const OCC_MCP_TOOL_EXAMPLES = {
    occ_doc: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        { label: 'partial', arguments: { name: 'buildRepoMap', relativePath: 'src/code/map.ts' } },
        {
            label: 'full',
            arguments: {
                name: 'getSession',
                relativePath: 'src/code/store.ts',
                kind: 'function',
                containerName: 'CodeIndexStore',
                tokenizer: 'o200k_base',
                response_format: 'concise',
            },
        },
    ],
    occ_pattern: [
        { label: 'minimal', arguments: { language: 'typescript', pattern: 'JSON.parse($A)' } },
        {
            label: 'partial',
            arguments: { language: 'typescript', pattern: 'JSON.parse($A)', relativePath: 'src', limit: 20 },
        },
        {
            label: 'full',
            arguments: {
                language: 'typescript',
                rule: { matches: 'json-parse' },
                utils: { 'json-parse': { pattern: 'JSON.parse($A)' } },
                relativePath: 'src',
                limit: 50,
                response_format: 'concise',
            },
        },
    ],
    occ_entities: [
        { label: 'minimal', arguments: {} },
        { label: 'partial', arguments: { query: 'Cache', relativePath: 'src' } },
        {
            label: 'full',
            arguments: {
                query: 'Cache',
                kind: 'class',
                relativePath: 'src',
                limit: 25,
                includeTests: false,
                response_format: 'concise',
            },
        },
    ],
    occ_hotspots: [
        { label: 'minimal', arguments: {} },
        {
            label: 'full',
            arguments: {
                relativePath: 'src',
                limit: 10,
                since: '2026-01-01',
                pageRank: true,
                response_format: 'concise',
            },
        },
    ],
    occ_cochange: [
        { label: 'minimal', arguments: {} },
        { label: 'partial', arguments: { name: 'buildRepoMap', relativePath: 'src/code/map.ts' } },
        {
            label: 'full',
            arguments: {
                name: 'buildRepoMap',
                relativePath: 'src/code/map.ts',
                kind: 'function',
                limit: 10,
                minCount: 2,
                minConfidence: 0.5,
                maxCommitEntities: 250,
                response_format: 'concise',
            },
        },
    ],
    occ_diff: [
        { label: 'minimal', arguments: {} },
        { label: 'partial', arguments: { base: 'main', obligations: true } },
        {
            label: 'full',
            arguments: {
                base: 'main',
                head: 'HEAD',
                rangeMode: 'exact',
                relativePath: 'src/code/map.ts',
                maxEntities: 500,
                formats: false,
                obligations: true,
                response_format: 'concise',
            },
        },
    ],
    occ_impact: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        { label: 'partial', arguments: { name: 'buildRepoMap', scope: 'tests' } },
        {
            label: 'full',
            arguments: {
                name: 'buildRepoMap',
                relativePath: 'src/code/map.ts',
                kind: 'function',
                scope: 'all',
                depth: 3,
                maxEntities: 250,
                inline_context: 2,
                response_format: 'concise',
            },
        },
    ],
    occ_blame: [
        { label: 'minimal', arguments: { relativePath: 'src/code/map.ts' } },
        {
            label: 'full',
            arguments: {
                relativePath: 'src/code/map.ts',
                ref: 'HEAD~1',
                walkBack: false,
                response_format: 'concise',
            },
        },
    ],
    occ_log: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        {
            label: 'full',
            arguments: {
                name: 'buildRepoMap',
                relativePath: 'src/code/map.ts',
                kind: 'function',
                limit: 10,
                ref: 'HEAD~1',
                walkBack: false,
                response_format: 'concise',
            },
        },
    ],
    occ_context: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        { label: 'partial', arguments: { name: 'buildRepoMap', include: ['deps'], tokenBudget: 4000 } },
        {
            label: 'full',
            arguments: {
                name: 'buildRepoMap',
                relativePath: 'src/code/map.ts',
                kind: 'function',
                tokenBudget: 8000,
                hops: 2,
                include: ['deps', 'dependents'],
                tokenizer: 'o200k_base',
                inline_context: 2,
                response_format: 'concise',
            },
        },
    ],
    occ_status: [
        { label: 'minimal', arguments: {} },
        { label: 'partial', arguments: { workingTree: false, response_format: 'concise' } },
    ],
    occ_source: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        { label: 'partial', arguments: { name: 'buildRepoMap', relativePath: 'src/code/map.ts' } },
        {
            label: 'full',
            arguments: {
                name: 'buildRepoMap',
                relativePath: 'src/code/map.ts',
                tokenizer: 'o200k_base',
                response_format: 'concise',
            },
        },
    ],
    occ_anchor: [
        { label: 'minimal', arguments: { name: 'buildRepoMap' } },
        { label: 'partial', arguments: { name: 'buildRepoMap', relativePath: 'src/code/map.ts' } },
        {
            label: 'full',
            arguments: {
                lineageId: 'ent1:0123456789abcdef0123456789abcdef',
                response_format: 'concise',
            },
        },
    ],
};
/** Append schema-checked examples to one tool's human-readable MCP contract. */
export function withOccMcpToolExamples(name, description) {
    const examples = OCC_MCP_TOOL_EXAMPLES[name].map(example => `- ${example.label}: ${JSON.stringify(example.arguments)}`);
    return `${description}\n\nExample arguments:\n${examples.join('\n')}`;
}
// ---------------------------------------------------------------------------
// Shared input fragments
// ---------------------------------------------------------------------------
const relativePathIn = z.string().min(1).describe('Repository-relative path. Paths outside the root are rejected.');
const kindIn = EntityKindSchema.describe('Disambiguate by entity kind when a name is not unique.');
const refIn = z.string().min(1).describe('Inspect the entity as of this git ref instead of the working tree.');
const inlineContextIn = z
    .number()
    .int()
    .nonnegative()
    .optional()
    .describe('Verified source lines to include on either side of each returned reference. Default 0.');
const responseFormatIn = z
    .enum(MCP_RESPONSE_FORMATS)
    .optional()
    .describe('Response projection. Default detailed preserves the complete current contract.');
const responseFormatInputShape = { response_format: responseFormatIn };
export const entitiesInputShape = {
    query: z.string().optional().describe('Substring matched against qualified names. Omit to list everything.'),
    kind: kindIn.optional(),
    relativePath: z
        .string()
        .min(1)
        .optional()
        .describe('Restrict the search to one file, or to a directory to list everything beneath it.'),
    limit: z.number().int().positive().max(500).optional().describe('Maximum entities to return. Default 50.'),
    includeTests: z.boolean().optional().describe('Include entities recognized as tests. Default true.'),
    ...responseFormatInputShape,
};
export const docInputShape = {
    name: z.string().min(1).optional().describe('Symbol name. Use either name or nodeId.'),
    nodeId: z.string().min(1).optional().describe('Exact graph node id. Use either nodeId or name.'),
    relativePath: z.string().min(1).optional().describe('Repository-relative file path to disambiguate a name.'),
    kind: CodeDocKindSchema.optional().describe('Symbol kind to disambiguate a name.'),
    containerName: z.string().min(1).optional().describe('Containing class/interface name to disambiguate a method.'),
    tokenizer: z
        .enum(['heuristic', 'o200k_base', 'cl100k_base'])
        .optional()
        .describe('Token counter used for bodyTokens. Default heuristic.'),
    ...responseFormatInputShape,
};
export const sourceInputShape = {
    name: z.string().min(1).optional().describe('Symbol name. Use either name or nodeId.'),
    nodeId: z.string().min(1).optional().describe('Exact graph node id. Use either nodeId or name.'),
    relativePath: z.string().min(1).optional().describe('Repository-relative file path to disambiguate a name.'),
    tokenizer: z
        .enum(['heuristic', 'o200k_base', 'cl100k_base'])
        .optional()
        .describe('Token counter used for the returned source. Default heuristic.'),
    ...responseFormatInputShape,
};
export const anchorInputShape = {
    name: z.string().min(1).optional().describe('Entity name. Use exactly one of name, lineageId, or nodeId.'),
    lineageId: z
        .string()
        .regex(/^ent\d+:[0-9a-f]{32}$/)
        .optional()
        .describe('Durable lineage id to re-anchor.'),
    nodeId: z.string().min(1).optional().describe('Exact graph node id — the handle concise-mode entity refs carry.'),
    relativePath: z.string().min(1).optional().describe('Repository-relative file path to disambiguate a name.'),
    kind: EntityKindSchema.optional().describe('Entity kind to disambiguate a name.'),
    containerName: z.string().min(1).optional().describe('Containing class/interface name to disambiguate a method.'),
    ...responseFormatInputShape,
};
export const patternInputShape = {
    language: z.string().min(1).describe('Pattern language, e.g. typescript, python, rust, go.'),
    pattern: z
        .string()
        .min(1)
        .optional()
        .describe('ast-grep pattern: code with $META / $$$MULTI metavariables. Use either pattern or rule.'),
    rule: z
        .record(z.string(), z.unknown())
        .optional()
        .describe('Full ast-grep rule object (pattern, kind, regex, inside, has, not, all, any, ...). Use either rule or pattern.'),
    constraints: z.record(z.string(), z.unknown()).optional().describe('Per-metavariable sub-rules.'),
    utils: z.record(z.string(), z.unknown()).optional().describe('Named utility rules referenced by `matches` clauses.'),
    relativePath: z.string().min(1).optional().describe('Restrict matching to one repository file or directory.'),
    limit: z.number().int().positive().max(500).optional().describe('Maximum matches returned. Default 50.'),
    ...responseFormatInputShape,
};
export const hotspotsInputShape = {
    relativePath: z.string().min(1).optional().describe('Restrict ranking to one repository file or directory.'),
    limit: z.number().int().positive().max(500).optional().describe('Maximum rows. Default 20.'),
    since: z.string().min(1).optional().describe('Only count events at or after this ISO 8601 date or timestamp.'),
    pageRank: z.boolean().optional().describe('Blend normalized graph PageRank into the score. Default false.'),
    ...responseFormatInputShape,
};
export const cochangeInputShape = {
    name: z.string().min(1).optional().describe('Entity name. Omit for the top co-change pairs in the repository.'),
    relativePath: z.string().min(1).optional().describe('Repository-relative path used to disambiguate name.'),
    kind: kindIn.optional(),
    limit: z.number().int().positive().max(500).optional().describe('Maximum pairs. Default 20.'),
    minCount: z.number().int().positive().optional().describe('Minimum commits changed together. Default 3.'),
    minConfidence: z.number().min(0).max(1).optional().describe('Minimum cochangeCount / aChangeCount. Default 0.'),
    maxCommitEntities: z
        .number()
        .int()
        .positive()
        .max(5_000)
        .optional()
        .describe('Skip commits changing more code entities than this. Default 500.'),
    ...responseFormatInputShape,
};
export const diffInputShape = {
    base: z.string().min(1).optional().describe('Base ref for the diff range.'),
    head: z.string().min(1).optional().describe('Head ref. Omit to compare against the working tree.'),
    staged: z.boolean().optional().describe('Compare staged changes instead of the working tree.'),
    relativePath: z.string().min(1).optional().describe('Restrict the diff to one file.'),
    maxEntities: z.number().int().positive().max(5000).optional().describe('Stop after this many entity changes.'),
    rangeMode: GitRangeModeSchema.optional().describe('How base and head are compared. "merge-base" (default) reports what head added since it ' +
        'diverged from base; "exact" reports what differs between exactly those two revisions.'),
    formats: z
        .boolean()
        .optional()
        .describe('Include data-format entities (JSON, YAML, TOML, CSV, Markdown headings). Default true.'),
    obligations: z
        .boolean()
        .optional()
        .describe('Attach untested logic gaps, touched seam keys, and the logic/cosmetic split. Default false.'),
    ...responseFormatInputShape,
};
export const impactInputShape = {
    name: z.string().min(1).describe('Entity name, qualified (`Cache.get`) or bare (`get`).'),
    relativePath: z.string().min(1).optional(),
    kind: kindIn.optional(),
    scope: OccEntityImpactScopeSchema.optional().describe('deps, dependents, tests, or all. Default all.'),
    depth: z.number().int().nonnegative().max(50).optional().describe('Graph hops to follow; 0 is unbounded. Default 2.'),
    maxEntities: z.number().int().positive().max(5000).optional().describe('Stop after this many entities. Default 500.'),
    inline_context: inlineContextIn,
    ...responseFormatInputShape,
};
const walkBackIn = z
    .boolean()
    .optional()
    .describe('Search git beyond the indexed commit window for attribution the index does not cover. Default true.');
export const blameInputShape = {
    relativePath: relativePathIn,
    ref: refIn.optional(),
    walkBack: walkBackIn,
    ...responseFormatInputShape,
};
export const logInputShape = {
    name: z.string().min(1).describe('Entity name, qualified or bare.'),
    relativePath: z.string().min(1).optional(),
    kind: kindIn.optional(),
    limit: z
        .number()
        .int()
        .positive()
        .max(ENTITY_HISTORY_MAX_LIMIT)
        .optional()
        .describe(`Maximum history entries, newest first. Default ${ENTITY_HISTORY_DEFAULT_LIMIT}.`),
    ref: refIn.optional(),
    walkBack: walkBackIn,
    ...responseFormatInputShape,
};
export const contextInputShape = {
    name: z.string().min(1).describe('Entity name, qualified or bare.'),
    relativePath: z.string().min(1).optional(),
    kind: kindIn.optional(),
    tokenBudget: z
        .number()
        .int()
        .positive()
        .max(200_000)
        .optional()
        .describe(`Hard ceiling on bundle tokens. Default ${ENTITY_CONTEXT_DEFAULT_BUDGET}.`),
    hops: z.number().int().nonnegative().max(50).optional().describe('Graph hops to follow; 0 is unbounded. Default 0.'),
    include: z
        .array(z.enum(['deps', 'dependents']))
        .nonempty()
        .optional()
        .describe('Directions to bundle: callees, callers, or both. Default both.'),
    tokenizer: z
        .enum(['heuristic', 'o200k_base', 'cl100k_base'])
        .optional()
        .describe('Token counter. Default heuristic.'),
    inline_context: inlineContextIn,
    ...responseFormatInputShape,
};
export const statusInputShape = {
    workingTree: z
        .boolean()
        .optional()
        .describe('Also stat the working tree against the code index manifest. Default true. ' +
        'Set false for a head-only answer that costs a single git call.'),
    ...responseFormatInputShape,
};
/** Zod objects used by the example contract test and real handler inference. */
export const OCC_MCP_INPUT_SCHEMAS = {
    occ_doc: z.object(docInputShape),
    occ_pattern: z.object(patternInputShape),
    occ_entities: z.object(entitiesInputShape),
    occ_hotspots: z.object(hotspotsInputShape),
    occ_cochange: z.object(cochangeInputShape),
    occ_diff: z.object(diffInputShape),
    occ_impact: z.object(impactInputShape),
    occ_blame: z.object(blameInputShape),
    occ_log: z.object(logInputShape),
    occ_context: z.object(contextInputShape),
    occ_status: z.object(statusInputShape),
    occ_source: z.object(sourceInputShape),
    occ_anchor: z.object(anchorInputShape),
};
// ---------------------------------------------------------------------------
// Shared output fragments
// ---------------------------------------------------------------------------
const statusOut = z.enum(['resolved', 'not_found', 'ambiguous', 'error']);
const entityRefOut = z
    .looseObject({
    lineageId: z.string().optional(),
    nodeId: z.string().optional(),
    name: z.string(),
    kind: z.string(),
    relativePath: z.string(),
    startLine: z.number().int(),
    endLine: z.number().int(),
    isExported: z.boolean(),
    isTest: z.boolean(),
})
    .superRefine((ref, context) => {
    if (ref.lineageId === undefined && ref.nodeId === undefined) {
        context.addIssue({ code: 'custom', message: 'An entity reference requires a lineageId or nodeId handle.' });
    }
});
const diagnosticsOut = z.array(z.looseObject({ code: z.string(), message: z.string() }));
const impactEntryOut = z.looseObject({
    entity: entityRefOut,
    distance: z.number().int(),
    via: z.array(z.string()),
    selector: z
        .looseObject({
        file: z.string(),
        title: z.string(),
        suites: z.array(z.string()),
        fullTitle: z.string(),
        framework: z.enum(['vitest', 'jest', 'mocha', 'node:test', 'pytest']).optional(),
        command: z
            .looseObject({
            executable: z.string(),
            args: z.array(z.string()),
            cwd: z.string(),
        })
            .optional(),
    })
        .optional(),
    inlineContext: SourceInlineBlockSchema.optional(),
});
export const entitiesOutputSchema = z.looseObject({
    entities: z.array(entityRefOut),
    count: z.number().int().nonnegative(),
    totalMatches: z.number().int().nonnegative(),
    limit: z.number().int().positive(),
    truncated: z.boolean(),
    honesty: z.looseObject({
        ambiguityCount: z.number().int().nonnegative(),
        evidenceGaps: z.looseObject({
            count: z.number().int().nonnegative(),
            reasons: diagnosticsOut,
        }),
    }),
});
export const patternOutputSchema = z.looseObject({
    status: z.literal('resolved'),
    language: z.string(),
    engine: z.looseObject({ name: z.string(), version: z.string() }).optional(),
    totalMatches: z.number().int().nonnegative(),
    truncated: z.boolean(),
    matches: z.array(z.looseObject({
        relativePath: z.string(),
        kind: z.string(),
        startLine: z.number().int().positive(),
        text: z.string(),
    })),
});
export const docOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'not_found', 'ambiguous', 'unsupported', 'error']),
    signature: z.string().optional(),
    params: z.array(z.string()).optional(),
    docComment: z.string().optional(),
    bodyTokens: z.number().int().nonnegative().optional(),
    tokenizer: z.string().optional(),
    message: z.string().optional(),
});
export const anchorOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'not_found', 'ambiguous', 'error']),
    /**
     * Nested under a literal `anchor` key on purpose: the concise projection
     * protects subtrees by field name, and the captured `expected` hash tuple
     * would otherwise be stripped member-by-member by the concise omit list.
     */
    anchor: z
        .looseObject({
        lineageId: z.string(),
        symbolKey: z.looseObject({ relativePath: z.string(), name: z.string() }),
        baseCommit: z.string().optional(),
        expected: z.looseObject({ bodyHash: z.string() }),
    })
        .optional(),
    freshness: z.looseObject({ fresh: z.boolean() }).optional(),
    warnings: z.array(z.string()).optional(),
    candidates: z.array(z.looseObject({ name: z.string(), relativePath: z.string() })).optional(),
    message: z.string().optional(),
});
export const sourceOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'not_found', 'ambiguous', 'error']),
    source: z.string().optional(),
    path: z.string().optional(),
    relativePath: z.string().optional(),
    span: z
        .looseObject({
        startLine: z.number().int().positive(),
        endLine: z.number().int().positive(),
        exact: z.boolean(),
    })
        .optional(),
    tokens: z.number().int().nonnegative().optional(),
    tokenizer: z.string().optional(),
    candidates: z.array(z.looseObject({ nodeId: z.string(), name: z.string(), path: z.string() })).optional(),
    message: z.string().optional(),
    honesty: z
        .looseObject({
        freshness: z.looseObject({
            fresh: z.boolean(),
            reasons: z.array(z.string()),
            workingTreeMeasured: z.boolean(),
        }),
        truncation: z.looseObject({
            truncated: z.boolean(),
            reasons: diagnosticsOut,
            omitted: z.number().int().nonnegative(),
        }),
        coverage: z.looseObject({
            indexNodesConsidered: z.number().int().nonnegative(),
            candidatesMatched: z.number().int().nonnegative(),
            sourceRead: z.boolean(),
        }),
        ambiguityCount: z.number().int().nonnegative(),
        evidenceGaps: z.looseObject({
            count: z.number().int().nonnegative(),
            reasons: diagnosticsOut,
        }),
    })
        .optional()
        .describe('Present on every source-session outcome; absent only when boundary validation refuses before indexing.'),
});
export const hotspotsOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'error']),
    rows: z
        .array(z.looseObject({
        entity: entityRefOut,
        logicChangeCount: z.number().int().nonnegative(),
        cosmeticChangeCount: z.number().int().nonnegative(),
        lastLogicChange: z.string(),
        ageDays: z.number().int().nonnegative(),
        score: z.number().nonnegative(),
    }))
        .optional(),
    honesty: z.looseObject({ ambiguityCount: z.number().int().nonnegative() }).optional(),
    message: z.string().optional(),
});
export const cochangeOutputSchema = z.looseObject({
    status: statusOut,
    entity: entityRefOut.optional(),
    pairs: z
        .array(z.looseObject({
        a: entityRefOut,
        b: entityRefOut,
        cochangeCount: z.number().int().positive(),
        aChangeCount: z.number().int().positive(),
        confidence: z.number().min(0).max(1),
        lastCoChange: z.string(),
    }))
        .optional(),
    windowCommits: z.number().int().nonnegative().optional(),
    honesty: z.looseObject({ ambiguityCount: z.number().int().nonnegative() }).optional(),
    candidates: z.array(entityRefOut).optional(),
    message: z.string().optional(),
});
const entityChangeTypeOut = z
    .string()
    .describe('Entity change kind. `modified_cosmetic` is decodable legacy-only and is not emitted by current producers.');
const wholeDiffBasisOut = z
    .string()
    .describe('Whole-file evidence basis. V4 admits safe `whitespace-only` and `comment-only-change` rows and refuses directives or unsafe trivia explicitly.');
const wholeDiffFileVerdictOut = z
    .string()
    .describe('File verdict. Current arms are `logic-change`, `cosmetic-change`, and `unknown`; `semantic-no-op` is decodable legacy-only for file rows, and future additive arms pass through this envelope.');
export const diffOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'error']),
    changedFiles: z.array(z.string()),
    changes: z.array(z.looseObject({ changeType: entityChangeTypeOut, confidence: z.number().optional() })),
    truncated: z.boolean(),
    omitted: z.number().int(),
    diagnostics: diagnosticsOut,
    semantic: z.looseObject({
        scope: z.literal('entity-records').optional(),
        verdict: z.enum(['logic-change', 'semantic-no-op', 'unknown']),
        coverage: z.enum(['complete', 'partial', 'unavailable']),
        counts: z.looseObject({
            logic: z.number().int().nonnegative(),
            cosmetic: z.number().int().nonnegative(),
            unknown: z.number().int().nonnegative(),
        }),
    }),
    wholeDiffSemantic: z
        .looseObject({
        scope: z.literal('whole-diff'),
        proof: z.enum(['whole-file-cst-v3', 'whole-file-cst-v4']),
        grammarManifestDigest: z
            .string()
            .regex(/^[0-9a-f]{64}$/u)
            .optional(),
        triviaPolicyDigest: z
            .string()
            .regex(/^[0-9a-f]{64}$/u)
            .optional(),
        verdict: z.enum(['logic-change', 'cosmetic-change', 'semantic-no-op', 'unknown']),
        coverage: z.enum(['complete', 'partial', 'unavailable']),
        counts: z.looseObject({
            logic: z.number().int().nonnegative(),
            noOp: z.number().int().nonnegative(),
            unknown: z.number().int().nonnegative(),
            cosmetic: z.number().int().nonnegative().optional(),
            whitespaceOnly: z.number().int().nonnegative().optional(),
            commentOnly: z.number().int().nonnegative().optional(),
        }),
        files: z.array(z.looseObject({
            relativePath: z.string(),
            previousPath: z.string().optional(),
            status: z.enum(['added', 'modified', 'deleted', 'renamed', 'copied', 'type-changed', 'unmerged', 'unknown']),
            verdict: wholeDiffFileVerdictOut,
            coverage: z.enum(['complete', 'partial', 'unavailable']),
            basis: wholeDiffBasisOut,
            grammar: z.string().optional(),
            grammarDigest: z
                .string()
                .regex(/^[0-9a-f]{64}$/u)
                .optional(),
            grammarManifestDigest: z
                .string()
                .regex(/^[0-9a-f]{64}$/u)
                .optional(),
            triviaPolicyDigest: z
                .string()
                .regex(/^[0-9a-f]{64}$/u)
                .optional(),
            oldMode: z.string().optional(),
            newMode: z.string().optional(),
        })),
        evidenceGaps: z.looseObject({
            unclassifiedFiles: z.array(z.looseObject({
                relativePath: z.string(),
                basis: wholeDiffBasisOut,
            })),
            diagnostics: diagnosticsOut,
        }),
        gitScope: z
            .looseObject({
            range: z.looseObject({
                base: z.string().optional(),
                head: z.string().optional(),
                kind: z.enum(['three-dot', 'two-dot', 'working-tree', 'staged', 'commit']),
                mergeBase: z.string().optional(),
                resolvedBase: z.string().optional(),
                resolvedHead: z.string().optional(),
                describe: z.string(),
            }),
            comparison: z.enum(['merge-base-to-head', 'base-to-head', 'head-to-index', 'head-to-worktree']),
            includedLayers: z.array(z.enum(['committed', 'index', 'worktree'])),
            relativePath: z.string().optional(),
            implicit: z.boolean(),
            indexTreeOid: z.string().optional(),
            untracked: z.enum(['not-applicable', 'excluded-none-observed', 'excluded-present']),
        })
            .optional(),
    })
        .superRefine((receipt, context) => {
        const floors = { logic: 0, noOp: 0, unknown: 0, cosmetic: 0, whitespaceOnly: 0, commentOnly: 0 };
        for (const file of receipt.files) {
            if (file.verdict === 'logic-change')
                floors.logic++;
            else if (file.verdict === 'semantic-no-op')
                floors.noOp++;
            else if (file.verdict === 'unknown')
                floors.unknown++;
            else if (file.verdict === 'cosmetic-change') {
                floors.cosmetic++;
                if (file.basis === 'whitespace-only')
                    floors.whitespaceOnly++;
                else if (file.basis === 'comment-only-change')
                    floors.commentOnly++;
            }
        }
        if (receipt.proof === 'whole-file-cst-v4') {
            if (receipt.grammarManifestDigest === undefined) {
                context.addIssue({
                    code: 'custom',
                    path: ['grammarManifestDigest'],
                    message: 'V4 whole-diff receipts require the grammar manifest digest.',
                });
            }
            if (receipt.triviaPolicyDigest === undefined) {
                context.addIssue({
                    code: 'custom',
                    path: ['triviaPolicyDigest'],
                    message: 'V4 whole-diff receipts require the trivia policy digest.',
                });
            }
            for (const [field, value] of Object.entries({
                cosmetic: receipt.counts.cosmetic,
                whitespaceOnly: receipt.counts.whitespaceOnly,
                commentOnly: receipt.counts.commentOnly,
            })) {
                if (value === undefined) {
                    context.addIssue({
                        code: 'custom',
                        path: ['counts', field],
                        message: `V4 whole-diff receipts require counts.${field}.`,
                    });
                }
            }
            receipt.files.forEach((file, index) => {
                if (file.triviaPolicyDigest === undefined) {
                    context.addIssue({
                        code: 'custom',
                        path: ['files', index, 'triviaPolicyDigest'],
                        message: 'Every V4 file receipt requires the trivia policy digest.',
                    });
                }
                else if (file.triviaPolicyDigest !== receipt.triviaPolicyDigest) {
                    context.addIssue({
                        code: 'custom',
                        path: ['files', index, 'triviaPolicyDigest'],
                        message: 'V4 file and summary trivia policy digests must match.',
                    });
                }
            });
        }
        else {
            if (receipt.verdict === 'cosmetic-change') {
                context.addIssue({
                    code: 'custom',
                    path: ['verdict'],
                    message: 'Only V4 whole-diff receipts may claim the cosmetic-change verdict.',
                });
            }
            receipt.files.forEach((file, index) => {
                if (file.verdict === 'cosmetic-change') {
                    context.addIssue({
                        code: 'custom',
                        path: ['files', index, 'verdict'],
                        message: 'Only V4 whole-diff receipts may carry cosmetic-change file verdicts.',
                    });
                }
            });
            for (const [field, value] of Object.entries({
                cosmetic: receipt.counts.cosmetic,
                whitespaceOnly: receipt.counts.whitespaceOnly,
                commentOnly: receipt.counts.commentOnly,
            })) {
                if ((value ?? 0) > 0) {
                    context.addIssue({
                        code: 'custom',
                        path: ['counts', field],
                        message: `Only V4 whole-diff receipts may report a nonzero counts.${field}.`,
                    });
                }
            }
        }
        if (receipt.counts.logic < floors.logic) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'logic'],
                message: 'Whole-diff logic count is below the floor established by known file verdicts.',
            });
        }
        if (receipt.counts.noOp < floors.noOp) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'noOp'],
                message: 'Whole-diff no-op count is below the floor established by known file verdicts.',
            });
        }
        if (receipt.counts.unknown < floors.unknown) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'unknown'],
                message: 'Whole-diff unknown count is below the floor established by known file verdicts.',
            });
        }
        if ((receipt.counts.cosmetic ?? 0) < floors.cosmetic) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'cosmetic'],
                message: 'Whole-diff cosmetic count is below the floor established by known file verdicts.',
            });
        }
        if ((receipt.counts.whitespaceOnly ?? 0) < floors.whitespaceOnly) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'whitespaceOnly'],
                message: 'Whole-diff whitespace-only count is below the floor established by known file bases.',
            });
        }
        if ((receipt.counts.commentOnly ?? 0) < floors.commentOnly) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'commentOnly'],
                message: 'Whole-diff comment-only count is below the floor established by known file bases.',
            });
        }
        if (receipt.counts.cosmetic !== undefined &&
            (receipt.counts.whitespaceOnly ?? 0) + (receipt.counts.commentOnly ?? 0) > receipt.counts.cosmetic) {
            context.addIssue({
                code: 'custom',
                path: ['counts', 'cosmetic'],
                message: 'Whole-diff cosmetic subcounts cannot exceed the cosmetic total.',
            });
        }
        if (receipt.coverage === 'complete' &&
            (receipt.counts.unknown > 0 ||
                receipt.evidenceGaps.unclassifiedFiles.length > 0 ||
                receipt.evidenceGaps.diagnostics.length > 0)) {
            context.addIssue({
                code: 'custom',
                path: ['coverage'],
                message: 'Complete whole-diff coverage contradicts unknown counts or recorded evidence gaps.',
            });
        }
    })
        .optional(),
    obligations: z
        .looseObject({
        gaps: z.array(z.looseObject({
            entityId: z.string(),
            qualifiedName: z.string(),
            nearestTestedAncestor: z.string().nullable(),
        })),
        seamCrossings: z.array(z.string()),
        split: z.looseObject({
            logic: z.number().int().nonnegative(),
            cosmetic: z.number().int().nonnegative(),
        }),
        coverage: z.enum(['complete', 'partial']).optional(),
        evidenceGaps: z
            .looseObject({
            skippedChanges: z.array(z.looseObject({
                qualifiedName: z.string(),
                relativePath: z.string(),
                reason: z.string(),
            })),
            count: z.number().int().nonnegative(),
        })
            .optional(),
    })
        .optional(),
});
export const impactOutputSchema = z.looseObject({
    status: statusOut,
    entity: entityRefOut.optional(),
    inlineContext: SourceInlineBlockSchema.optional(),
    scope: OccEntityImpactScopeSchema.optional(),
    coverage: z.enum(['full', 'partial']).optional(),
    unreachableEdges: z.number().int().nonnegative().optional(),
    truncated: z.boolean().optional(),
    dependencies: z.array(impactEntryOut).optional(),
    dependents: z.array(impactEntryOut).optional(),
    transitive: z.array(impactEntryOut).optional(),
    tests: z.array(impactEntryOut).optional(),
    candidates: z.array(entityRefOut).optional(),
    message: z.string().optional(),
});
export const blameOutputSchema = z.looseObject({
    status: z.enum(['resolved', 'not_found', 'error']),
    relativePath: z.string().optional(),
    entries: z.array(z.looseObject({ state: z.string() })).optional(),
    message: z.string().optional(),
});
export const logOutputSchema = z.looseObject({
    status: statusOut,
    entity: entityRefOut.optional(),
    entries: z.array(z.looseObject({ commit: z.string(), changeType: z.string() })).optional(),
    truncated: z.boolean().optional(),
    candidates: z.array(entityRefOut).optional(),
    message: z.string().optional(),
});
export const statusOutputSchema = z.looseObject({
    truncated: z.boolean(),
    evidenceComplete: z.boolean(),
    mode: z.string(),
    codeIndexSchemaVersion: z.number().int().positive().optional(),
    parserContract: z.looseObject({ parserName: z.string(), parserVersion: z.number().int().positive() }).optional(),
    parserEvidence: z
        .looseObject({
        totalFiles: z.number().int().nonnegative(),
        provenanceFiles: z.number().int().nonnegative(),
        legacyFiles: z.number().int().nonnegative(),
        parserVersions: z.array(z.number().int().positive()),
        checkerFiles: z.number().int().nonnegative(),
        syntaxFiles: z.number().int().nonnegative(),
        checkerFallbackFiles: z.number().int().nonnegative(),
        recoveredFiles: z.number().int().nonnegative(),
        fallbackFiles: z.number().int().nonnegative(),
        degradedFiles: z.number().int().nonnegative(),
    })
        .optional(),
    commitsIndexed: z.number().int(),
    lineageCount: z.number().int(),
    fresh: z.boolean(),
    reasons: diagnosticsOut,
    diagnostics: diagnosticsOut,
});
export const contextOutputSchema = z.looseObject({
    status: statusOut,
    refused: z.literal(true).optional(),
    reason: z.literal('token-budget-exceeded').optional(),
    required: z.number().int().positive().optional(),
    budget: z.number().int().positive().optional(),
    options: z
        .array(z.looseObject({
        description: z.string(),
        argv: z.array(z.string()),
        projectedTokens: z.number().int().positive(),
    }))
        .optional(),
    entity: entityRefOut.optional(),
    entries: z
        .array(z.looseObject({
        role: z.string(),
        tokens: z.number().int(),
        inlineContext: SourceInlineRangeSchema.optional(),
    }))
        .optional(),
    tokenBudget: z.number().int().optional(),
    tokensUsed: z.number().int().optional(),
    truncated: z.boolean().optional(),
    referenceClosure: z.enum(['closed', 'open', 'unknown']).optional(),
    referenceClosureReasons: z
        .array(z.enum([
        'targets-omitted',
        'unresolved-edges',
        'unreachable-edges',
        'graph-node-unavailable',
        'regex-fallback',
        'truncated-entity',
    ]))
        .optional(),
    edgesResolved: z.number().int().nonnegative().optional(),
    edgesUnresolved: z.number().int().nonnegative().optional(),
    targetsOmitted: z.number().int().nonnegative().optional(),
    unreachableEdges: z.number().int().nonnegative().optional(),
    unresolvedHandles: z
        .array(z.looseObject({
        kind: z.enum(['omitted-target', 'unreachable-target', 'unresolved-edge', 'ambiguous-edge']),
        edgeType: z.enum(['calls', 'imports']),
        name: z.string(),
        nodeId: z.string().optional(),
        relativePath: z.string().optional(),
    }))
        .optional(),
    unresolvedHandlesOmitted: z.number().int().nonnegative().optional(),
    candidates: z.array(entityRefOut).optional(),
    message: z.string().optional(),
});
function asJson(value) {
    return value;
}
/**
 * Confine a client-supplied path to the repository root.
 *
 * The MCP process may be launched by an agent acting on a model's output, so
 * `../../.ssh/id_rsa` is a realistic input rather than a theoretical one. The
 * server applies this *before* it builds a session: refusing an escape must
 * not require indexing a repository first, and the refusal must not depend on
 * anything the session knows.
 */
export function guardPath(repoRoot, value) {
    if (value === undefined)
        return {};
    const resolved = resolveWithinRepo(repoRoot, value);
    if (typeof resolved === 'string') {
        const message = `Resolved path "${value}" is outside the repository root.`;
        return {
            text: `Error (path-escape): ${message}`,
            data: { status: 'error', code: 'path-escape', message },
            isError: true,
        };
    }
    return { relativePath: resolved.relativePath };
}
export function isToolOutcome(value) {
    return 'text' in value;
}
function queryFrom(args, relativePath) {
    const query = { name: args.name };
    if (relativePath)
        query.relativePath = relativePath;
    if (args.kind)
        query.kind = args.kind;
    if (args.ref)
        query.ref = args.ref;
    return query;
}
/**
 * Handlers receive the repo-relative path already normalized and confined, so
 * none of them can forget the guard or apply it differently.
 */
export async function runEntities(args, ctx, relativePath) {
    const result = await ctx.session.searchDetailed(args.query ?? '', {
        limit: args.limit,
        kind: args.kind,
        relativePath,
        includeTests: args.includeTests,
    });
    return {
        text: formatEntityList(result.entities),
        data: { ...result, count: result.entities.length },
    };
}
export async function runDoc(args, ctx, relativePath) {
    if ((args.name == null) === (args.nodeId == null)) {
        const message = 'Provide exactly one of name or nodeId.';
        return {
            text: `error: ${message}`,
            data: { status: 'error', message },
            isError: true,
        };
    }
    const query = args.nodeId != null
        ? { nodeId: args.nodeId }
        : {
            name: args.name,
            relativePath,
            kind: args.kind,
            containerName: args.containerName,
        };
    const result = await ctx.session.doc(query, { tokenizer: args.tokenizer });
    return {
        text: formatCodeDocs([result], true),
        data: asJson(result),
    };
}
export async function runSource(args, ctx, relativePath) {
    if ((args.name == null) === (args.nodeId == null)) {
        const message = 'Provide exactly one of name or nodeId.';
        return {
            text: `error: ${message}`,
            data: { status: 'error', message },
            isError: true,
        };
    }
    const query = args.nodeId != null ? { nodeId: args.nodeId } : { name: args.name, file: relativePath };
    const result = await ctx.session.source(query, { tokenizer: args.tokenizer });
    if (result.status !== 'resolved') {
        return { text: `${result.status}: ${result.message}`, data: asJson(result) };
    }
    const location = result.relativePath ?? result.path;
    const footer = `${result.tokens} tokens (${result.tokenizer}); fresh=${result.honesty.freshness.fresh}; coverage=${result.honesty.evidenceGaps.count === 0 ? 'complete' : 'partial'}`;
    return {
        text: `${location}:${result.line}-${result.endLine}\n${result.source}\n${footer}`,
        data: asJson(result),
    };
}
function formatCodePatternText(result) {
    const lines = result.matches.map(match => `${match.relativePath}:${match.startLine}:${match.startColumn}\t${match.kind}\t${match.symbol ?? '-'}\t${match.text}`);
    for (const diagnostic of result.diagnostics)
        lines.push(`note: ${diagnostic}`);
    const queried = result.honesty.filesQueried ?? result.filesParsed;
    const eligible = result.honesty.eligibleFiles ?? result.filesParsed + result.filesSkipped;
    lines.push(`${result.totalMatches} match(es) in ${queried}/${eligible} file(s); coverage=${result.honesty.coverage}`);
    return lines.join('\n');
}
export async function runPattern(args, ctx, relativePath) {
    if ((args.pattern == null) === (args.rule == null)) {
        const message = 'Provide exactly one of pattern or rule.';
        return {
            text: `error: ${message}`,
            data: { status: 'error', message },
            isError: true,
        };
    }
    const result = await ctx.session.pattern({
        language: args.language,
        pattern: args.pattern,
        rule: args.rule,
        constraints: args.constraints,
        utils: args.utils,
        files: relativePath ? [relativePath] : undefined,
        limit: args.limit,
    });
    return {
        text: formatCodePatternText(result),
        data: asJson(result),
    };
}
export async function runHotspots(args, ctx, relativePath) {
    const result = await ctx.session.hotspots({
        relativePath,
        limit: args.limit,
        since: args.since,
        pageRank: args.pageRank,
    });
    return {
        text: formatEntityHotspots(result),
        data: asJson(result),
    };
}
export async function runCochange(args, ctx, relativePath) {
    if (!args.name && (relativePath || args.kind)) {
        const message = 'relativePath and kind require name; omit all three for the repository leaderboard.';
        return {
            text: `Error (invalid-query): ${message}`,
            data: { status: 'error', code: 'invalid-query', message },
            isError: true,
        };
    }
    const query = args.name ? queryFrom({ name: args.name, kind: args.kind }, relativePath) : undefined;
    const result = await ctx.session.cochange(query, {
        limit: args.limit,
        minCount: args.minCount,
        minConfidence: args.minConfidence,
        maxCommitEntities: args.maxCommitEntities,
    });
    return {
        text: formatEntityCochange(result),
        data: asJson(result),
    };
}
export async function runDiff(args, ctx, relativePath) {
    const result = await ctx.session.diff({
        base: args.base,
        head: args.head,
        staged: args.staged,
        relativePath,
        maxEntities: args.maxEntities,
        rangeMode: args.rangeMode,
        formats: args.formats,
        obligations: args.obligations,
    });
    return { text: formatEntityDiff(result), data: asJson(result) };
}
export async function runImpact(args, ctx, relativePath) {
    const result = await ctx.session.impact(queryFrom(args, relativePath), {
        scope: args.scope,
        depth: args.depth,
        maxEntities: args.maxEntities,
        inlineContext: args.inline_context,
    });
    return { text: formatEntityImpact(result), data: asJson(result) };
}
export async function runBlame(args, ctx, relativePath) {
    const result = await ctx.session.blame({
        relativePath: relativePath ?? args.relativePath,
        ref: args.ref,
        walkBack: args.walkBack,
    });
    return { text: formatEntityBlame(result), data: asJson(result) };
}
export async function runLog(args, ctx, relativePath) {
    const result = await ctx.session.history(queryFrom(args, relativePath), {
        limit: args.limit,
        ref: args.ref,
        walkBack: args.walkBack,
    });
    return { text: formatEntityHistory(result), data: asJson(result) };
}
export async function runContext(args, ctx, relativePath) {
    const result = await ctx.session.context(queryFrom(args, relativePath), {
        tokenBudget: args.tokenBudget,
        hops: args.hops,
        tokenizer: args.tokenizer,
        include: args.include,
        inlineContext: args.inline_context,
        budgetFormat: 'json',
    });
    return { text: formatEntityContext(result), data: asJson(result) };
}
/**
 * Capture one entity as a verifiable memory anchor.
 *
 * The `nodeId` arm exists for concise-mode clients: the concise projection
 * collapses entity refs to one stable handle preferring `nodeId`, so this is
 * how such a client turns any ref it holds into a durable anchor. The result
 * nests its payload under `anchor`, which the concise projection protects as
 * a subtree — the hash tuple must survive projection or capture is useless.
 */
export async function runAnchor(args, ctx, relativePath) {
    const handles = [args.name, args.lineageId, args.nodeId].filter(value => value != null).length;
    if (handles !== 1) {
        const message = 'Provide exactly one of name, lineageId, or nodeId.';
        return {
            text: `error: ${message}`,
            data: { status: 'error', message },
            isError: true,
        };
    }
    const handle = args.nodeId != null
        ? { nodeId: args.nodeId }
        : args.lineageId != null
            ? { lineageId: args.lineageId }
            : {
                name: args.name,
                ...(relativePath !== undefined ? { relativePath } : {}),
                ...(args.kind !== undefined ? { kind: args.kind } : {}),
                ...(args.containerName !== undefined ? { containerName: args.containerName } : {}),
            };
    const result = await ctx.session.anchor(handle);
    return {
        text: formatAnchorCapture(result),
        data: asJson(result),
    };
}
/**
 * What the index covers, and whether it still describes the repository.
 *
 * The only tool that answers about OCC rather than about the code. Everything
 * else can return an empty result for two incompatible reasons — the repository
 * genuinely has nothing, or the index was starved by its bounds or has gone
 * stale — and this is where a client tells those apart.
 */
export async function runStatus(args, ctx) {
    const status = ctx.session.status();
    const freshness = await ctx.session.freshness({ workingTree: args.workingTree });
    const bounds = ctx.bounds ?? DEFAULT_ENTITY_BOUNDS;
    return {
        text: formatEntityStatus(status, freshness, bounds),
        data: asJson({ ...status, fresh: freshness.fresh, freshness, bounds }),
    };
}
//# sourceMappingURL=tools.js.map