/**
 * MCP server `release-tools` — exposes M17 release operations to agents
 * (chat, external MCP clients). Mirrors `m17mcp001`: 5 tools — create, list,
 * show, diff, update. `release_restore` is intentionally absent (decyzja 9 +
 * `m17open01` open #3 — restore is human-initiated only).
 *
 * `release_diff` / `release_show` project the raw L2 shape (`RawDelta` /
 * `SpecSnapshot`) onto a self-contained MCP shape (`MCPReleaseDiff` /
 * `MCPSpecSnapshot`). This is the only consumer of `release-tools` in brief
 * threads; the projection is what makes briefs interpretable after HEAD
 * advances beyond the release pair. REST (`/api/releases/...`) and UI keep
 * consuming the raw L2 shape for render-time `line_diff`.
 */
import { createMcpServer, mcpTool } from '@inharness-ai/agent-adapters';
import { z } from 'zod';
import { DomainError } from '../../services/tags.js';
import { projectReleaseDiff, projectSpecSnapshot } from './projection.js';
const INCLUDE_VALUES = ['pages', 'entities'];
const ENTITY_TYPE_VALUES = ['endpoint', 'dto', 'database-table', 'ui-view', 'ac'];
const DEFAULT_INCLUDE = ['pages', 'entities'];
export function createReleaseToolsServer(deps) {
    const ok = (payload) => ({
        content: [{ type: 'text', text: JSON.stringify(payload) }],
    });
    const fail = (err) => {
        const code = err instanceof DomainError ? err.code : 'INTERNAL';
        const message = err instanceof Error ? err.message : String(err);
        return {
            content: [{ type: 'text', text: JSON.stringify({ error: message, code }) }],
            isError: true,
        };
    };
    const releaseCreate = mcpTool('release_create', 'Create a named release (snapshot of current spec state). Assigns release_id to all unreleased entity_version + page_version rows in one transaction. Always manual — there are no auto-triggers (M17 decyzja 9). Both name (UNIQUE) and description (non-empty) are required.', {
        name: z.string().describe('Release name, must be unique. e.g. "v1.0.0", "pre-launch"'),
        description: z.string().describe('Non-empty intent of the release — surfaced to M18 brief-builder'),
    }, async (args) => {
        try {
            const release = deps.releaseService.createRelease({ name: String(args.name), description: String(args.description) }, 'agent');
            deps.ws.broadcast({ kind: 'release:created', releaseId: release.id, name: release.name });
            // M28: agent-surface parity — same best-effort git commit as the HTTP
            // surface, with the outcome returned in `gitSync` (null when off/no repo).
            const gitSync = await deps.gitService.commitOnRelease(release);
            return ok({ ...release, gitSync });
        }
        catch (err) {
            return fail(err);
        }
    });
    const releaseList = mcpTool('release_list', 'List all releases newest-first. Returns id, name, description, createdBy, createdAt per release.', {}, async () => ok({ releases: deps.releaseService.listReleases() }));
    const releaseShow = mcpTool('release_show', "Show a release's identification surface (release metadata + lists of entity slugs/page paths present at the release). Returns `MCPSpecSnapshot` — IDENTIFICATION only, not full entity/page data. To inspect the data, call `release_diff` (with this release as `to` and any earlier release — or `null` — as `from`). Accepts numeric id or release name. Filters: `include` (defaults to ['pages','entities']) trims the dimensions returned; `entityTypes` restricts entity types. Brief versions (`page_version.kind='brief'`) are excluded from `pages` (L2 invariant).", {
        idOrName: z.union([z.string(), z.number()]).describe('Numeric id or release name'),
        include: z
            .array(z.enum(INCLUDE_VALUES))
            .optional()
            .describe("Filter dimensions. Default ['pages','entities']. Empty array → 400 INVALID_INCLUDE_FILTER."),
        entityTypes: z
            .array(z.enum(ENTITY_TYPE_VALUES))
            .optional()
            .describe("Filter entity types. Default all 5 types. Empty array → 400 INVALID_ENTITY_TYPES_FILTER. Passing this without 'entities' in `include` → 400 CONFLICTING_FILTERS."),
    }, async (args) => {
        try {
            const include = args.include ?? DEFAULT_INCLUDE;
            const entityTypes = args.entityTypes;
            validateFilters(args.include, entityTypes);
            const raw = deps.releaseService.getReleaseSnapshot(args.idOrName);
            return ok(projectSpecSnapshot(raw, { include, entityTypes }));
        }
        catch (err) {
            return fail(err);
        }
    });
    const releaseDiff = mcpTool('release_diff', "Compute a SELF-CONTAINED structured diff between two releases. Each entity carries full `before`/`after` snapshots (per plugin's serializer); each modified section carries full `before`/`after` raw markdown. There is NO `line_diff` and NO drill-down tool (`get_<type>` / `Read pages/...` are not available in this thread) — this single round-trip is everything you need to ground a brief. Pass `from: null` for the initial brief (synthetic empty `from`; all entries become `op:'create'` with `before` omitted). `from === to` returns an empty diff. Filters: `include` (defaults ['pages','entities']) and `entityTypes` (defaults all 5) trim the payload to manage context budget.", {
        fromIdOrName: z
            .union([z.string(), z.number(), z.null()])
            .describe('Earlier release id or name. `null` = initial brief (compare to empty state — all entries become op:create).'),
        toIdOrName: z.union([z.string(), z.number()]).describe('Later release id or name'),
        include: z
            .array(z.enum(INCLUDE_VALUES))
            .optional()
            .describe("Filter dimensions. Default ['pages','entities']. Empty array → 400 INVALID_INCLUDE_FILTER."),
        entityTypes: z
            .array(z.enum(ENTITY_TYPE_VALUES))
            .optional()
            .describe("Filter entity types. Default all 5 types. Empty array → 400 INVALID_ENTITY_TYPES_FILTER. Passing this without 'entities' in `include` → 400 CONFLICTING_FILTERS."),
    }, async (args) => {
        try {
            const include = args.include ?? DEFAULT_INCLUDE;
            const entityTypes = args.entityTypes;
            validateFilters(args.include, entityTypes);
            const fromIdOrName = args.fromIdOrName;
            const toIdOrName = args.toIdOrName;
            const raw = deps.releaseService.getReleaseDiff(fromIdOrName, toIdOrName);
            const toSnap = deps.releaseService.getReleaseSnapshot(toIdOrName);
            const fromSnap = fromIdOrName === null ? null : deps.releaseService.getReleaseSnapshot(fromIdOrName);
            return ok(projectReleaseDiff(raw, fromSnap, toSnap, { include, entityTypes }));
        }
        catch (err) {
            return fail(err);
        }
    });
    const releaseUpdate = mcpTool('release_update', 'Update the LATEST release only — older releases are frozen. Mutates name/description in-place and optionally pulls all unreleased entity_version + page_version rows (release_id IS NULL) into this release. 409 RELEASE_FROZEN if id != MAX(id). 409 RELEASE_NAME_CONFLICT on rename collision.', {
        idOrName: z.union([z.string(), z.number()]).describe('Numeric id or release name'),
        name: z.string().optional().describe('New name (must be unique). Omit to leave unchanged.'),
        description: z.string().optional().describe('New description (non-empty). Omit to leave unchanged.'),
        assignUnreleased: z
            .boolean()
            .optional()
            .describe('When true, assigns all entity_version/page_version rows where release_id IS NULL to this release. No-op when queue is empty.'),
    }, async (args) => {
        try {
            const release = deps.releaseService.updateRelease({
                idOrName: args.idOrName,
                name: args.name,
                description: args.description,
                assignUnreleased: args.assignUnreleased,
            });
            deps.ws.broadcast({ kind: 'release:updated', releaseId: release.id, name: release.name });
            return ok(release);
        }
        catch (err) {
            return fail(err);
        }
    });
    return createMcpServer({
        name: 'release-tools',
        tools: [releaseCreate, releaseList, releaseShow, releaseDiff, releaseUpdate],
    });
}
function validateFilters(include, entityTypes) {
    if (include !== undefined && include.length === 0) {
        throw new DomainError('INVALID_INCLUDE_FILTER', 'include must not be an empty array');
    }
    if (entityTypes !== undefined && entityTypes.length === 0) {
        throw new DomainError('INVALID_ENTITY_TYPES_FILTER', 'entityTypes must not be an empty array');
    }
    if (entityTypes !== undefined) {
        const effectiveInclude = include ?? DEFAULT_INCLUDE;
        if (!effectiveInclude.includes('entities')) {
            throw new DomainError('CONFLICTING_FILTERS', "entityTypes filter requires 'entities' in include");
        }
    }
}
//# sourceMappingURL=index.js.map