UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

845 lines • 47.4 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. import { formatSuccess, formatDeletion } from "@withpica/mcp-utils"; import { buildIntegrityPressure } from "./integrity-hints.js"; /** * Canonical write-schema for `releases`. Every property below must * correspond to a real column on `public.releases`. Keep this list * tight — reintroducing a phantom field is the exact regression * ADR-174 Phase 2 exists to prevent. */ const RELEASE_WRITE_PROPERTIES = { title: { type: "string", description: "Release title", }, release_date: { type: "string", description: "Release date (YYYY-MM-DD)", }, release_type: { type: "string", description: "Release type", enum: ["album", "ep", "single", "compilation"], }, pre_release_date: { type: "string", description: "Pre-release / announcement date (YYYY-MM-DD)", }, upc: { type: "string", description: "Universal Product Code (UPC/EAN barcode), as a string. Stored in one canonical form per GTIN: a 13-digit EAN that starts with 0 is stored as its 12-digit UPC-A (0198002851000 → 198002851000 — the same barcode, not a lost digit); any other 13-digit EAN is kept as given. pica_releases_query `upc` accepts either form.", }, catalog_number: { type: "string", description: "Label catalog number", }, label_organization_id: { type: "string", description: "organisations.id of the label (must be an org with org_type='label'). " + "Resolve a label name to an ID via pica_labels_query first — never pass a free-text name.", }, parent_label: { type: "string", description: "Free-text parent/umbrella label name (e.g. 'Integrated Music Rights (UK)'). " + "Use label_organization_id when the label exists as an org; parent_label is the " + "plain-text fallback for a label you are not modelling as an organisation.", }, distributor_organization_id: { type: "string", description: "organisations.id of the distributor. Resolve a distributor name via pica_search_all first.", }, artwork_url: { type: "string", description: "Album artwork URL", }, spotify_album_id: { type: "string", description: "Canonical Spotify album id (bare id, e.g. 6QaVfG1pHYl1z15ZxkvVDW). " + "The SINGLE stored Spotify column — preferred over spotify_album_uri/" + "spotify_url; when only the uri/url is supplied the server derives " + "this id automatically. The UPC backfill fires only on a WRITE to this " + "field: a release that already carries a spotify_album_id but no UPC " + "is backfilled by passing the same id again through pica_releases_update.", }, spotify_album_uri: { type: "string", description: "Spotify album URI (e.g. spotify:album:…). Legacy input NAME — the " + "server extracts the album id and stores ONLY the canonical " + "spotify_album_id.", }, spotify_url: { type: "string", description: "Spotify album web URL. Legacy input NAME — extracted to the " + "canonical spotify_album_id, never stored.", }, apple_music_url: { type: "string", description: "Apple Music album URL", }, other_urls: { type: "object", description: "DSP URLs keyed by service name (e.g. { tidal: '…', deezer: '…' })", additionalProperties: { type: "string" }, }, notes: { type: "string", description: "Internal notes", }, availability: { type: "string", description: "Release availability: 'available' (live) or 'withdrawn' (was out, now taken down). " + "Setting 'withdrawn' makes the work read as 'previously released' in /inspect; " + "restoring to 'available' clears the withdrawal.", enum: ["available", "withdrawn"], }, }; export class ReleasesTools { pica; constructor(pica) { this.pica = pica; } getTools() { return [ // Tier 1 (MCP tool-surface consolidation, ADR-149 query/inspect // pattern): pica_releases_query/_inspect are the canonical read pair, // mirroring pica_recordings_query/_inspect. The old pica_releases_list // and pica_releases_get names were silent aliases of this pair until // mcp-server 2.99.0, which removed them. { definition: { name: "pica_releases_query", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Search/list releases — albums, EPs, and singles with release dates, labels, distributors, and UPCs. " + "Filter to locate a specific release instead of paging the whole catalogue: `query` (title substring), `upc` (exact), or `spotify_album_id` (exact — answers 'does a release for this Spotify album already exist?'). Omit all filters to list everything. " + "→ then: pica_releases_inspect (details for a specific release)", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Case-insensitive substring match on release title (e.g. 'paradise').", }, upc: { type: "string", description: "UPC match (the release dedup key). Either form finds the release — a 12-digit UPC-A and its 13-digit 0-prefixed EAN are the same barcode.", }, spotify_album_id: { type: "string", description: "Exact spotify_album_id match — check whether a release for a given Spotify album already exists before importing.", }, limit: { type: "number", minimum: 1, maximum: 200, description: "Max results to return (default 50)", }, offset: { type: "integer", description: "Pagination offset — number of releases to skip (default 0). Pass the next_offset value from the previous response to fetch the next page.", }, release_date_from: { type: "string", description: "Only releases with release_date on/after this ISO date (YYYY-MM-DD). For 'released in 2013' pass release_date_from=2013-01-01 & release_date_to=2013-12-31.", }, release_date_to: { type: "string", description: "Only releases with release_date on/before this ISO date (YYYY-MM-DD).", }, }, }, }, executor: this.listReleases.bind(this), }, { definition: { name: "pica_releases_inspect", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Get release details including tracks. " + "Prefer pica_releases_query when finding a release by title, UPC, or Spotify album id rather than reading one by ID.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", // id/release_id are interchangeable entry points — see // pica_recordings_inspect. required:[] + requiredOneOf lets // pica_execute still hard-reject empty/stripped args while // accepting either name. requiredOneOf: [["id", "release_id"]], inputSchema: { type: "object", properties: { id: { type: "string", description: "Release ID" }, release_id: { type: "string", description: "Release ID (alias for id — write tools use release_id).", }, }, required: [], }, }, executor: this.getRelease.bind(this), }, { definition: { name: "pica_releases_create", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Create a new release — set title, dates, type, catalog number, " + "label/distributor organisation IDs, artwork, DSP URLs, and notes. " + "For label_organization_id use pica_labels_query first to resolve a name to an ID " + "(freetext label names are not accepted — the label must be an organisation that already exists). " + "After several mutations, surface a pica_dashboard_briefing to the user. " + "Prefer pica_releases_create_with_tracks when the tracklist is already known; this one creates the release only.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: RELEASE_WRITE_PROPERTIES, required: ["title"], }, }, executor: this.createRelease.bind(this), }, { definition: { name: "pica_releases_update", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Update release details — any field in the release write schema. " + "For label_organization_id use pica_labels_query first to resolve a name to an ID. " + "Prefer pica_releases_create when no release exists yet; this one edits an existing release.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Release ID" }, ...RELEASE_WRITE_PROPERTIES, }, required: ["id"], }, }, executor: this.updateRelease.bind(this), }, // ADR-173: release-track primitives + compound workflow { definition: { name: "pica_releases_attach_track", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Attach track(s) to a release. Three ways to call it: (1) single track by " + "recording_id and/or work_id, with track_number; (2) single track by external " + "identifier — identifier_type + value (isrc/spotify_url/spotify_track_uri/" + "youtube_video_id/musicbrainz_recording_id/mlc_recording_id), resolved to an " + "EXISTING internal recording id already in your catalog, with track_number. This " + "identifier form is NOT for importing from a Spotify/Deezer URL or extracting " + "metadata/ISRCs — for that use pica_import_streaming_link; (3) batch — items[], each " + "{ recording_id?, work_id?, track_number, disc_number? }, attached in one call. " + "At least one of recording_id or work_id must be provided for the single-track " + "forms. Idempotent on (release_id, disc_number, track_number) — a second call at " + "the same position updates the existing row rather than creating a duplicate. " + "→ then: pica_releases_list_tracks (verify tracklist), pica_releases_reorder_tracks (change order)", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, recording_id: { type: "string", description: "Recording ID (optional — at least one of recording_id or work_id must be provided)", }, work_id: { type: "string", description: "Work ID (optional — omit for licensed-masters releases where no composition record exists)", }, track_number: { type: "number", description: "Position on disc (1-based). Required for the single-track forms; ignored when items[] is passed.", }, disc_number: { type: "number", description: "Disc number (default 1)", }, identifier_type: { type: "string", enum: [ "isrc", "spotify_url", "spotify_track_uri", "youtube_video_id", "musicbrainz_recording_id", "mlc_recording_id", ], description: "Type of external identifier to resolve", }, value: { type: "string", description: "The external identifier value (used with identifier_type).", }, items: { type: "array", description: "Batch mode: attach many tracks in one call. Each item: " + "{ recording_id?, work_id?, track_number, disc_number? }.", items: { type: "object", properties: { recording_id: { type: "string" }, work_id: { type: "string" }, track_number: { type: "number" }, disc_number: { type: "number" }, }, required: ["track_number"], }, }, dry_run: { type: "boolean", description: "Preview a batch (items[]) attach without writing. When true, " + "returns { dry_run: true, would_affect, target_count } instead " + "of attaching any tracks. Only applies to items[] batch mode.", }, }, required: ["release_id"], }, previewMode: "dry_run", }, executor: this.attachTrack.bind(this), }, { definition: { name: "pica_releases_list_tracks", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "List tracks on a release in (disc_number, track_number) order with " + "inlined title, artist, ISRC, and duration from the linked recording and work. " + "→ then: pica_releases_reorder_tracks (change order), pica_recordings_inspect (deep-dive a track)", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, }, required: ["release_id"], }, }, executor: this.listTracks.bind(this), }, { definition: { name: "pica_releases_detach_track", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Remove a track from a release by position. Soft-confirmation: call without " + "confirm:true to see a preview of the row that would be removed, then call " + "again with confirm:true to execute. " + "To SWAP a track's position with another rather than remove it, use " + "pica_releases_move_track — its destination must already be occupied.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, track_number: { type: "number", description: "Position on disc (1-based)", }, disc_number: { type: "number", description: "Disc number (default 1)", }, confirm: { type: "boolean", description: "Set to true to execute the detach after reviewing the preview", }, }, required: ["release_id", "track_number"], }, }, executor: this.detachTrack.bind(this), }, { definition: { name: "pica_releases_reorder_tracks", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Reorder tracks on a release atomically. Pass the full new order as an array " + "of { track_id, track_number, disc_number } — the entire reorder commits or " + "rolls back as one transaction. Duplicate (disc_number, track_number) pairs " + "in the payload are rejected before any write. " + "Prefer pica_releases_move_track when swapping just two positions; this one rewrites the whole order.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, new_order: { type: "array", description: "New ordering of tracks. Every existing track should appear here.", items: { type: "object", properties: { track_id: { type: "string", description: "release_tracks.id", }, track_number: { type: "number" }, disc_number: { type: "number", description: "Default 1", }, }, required: ["track_id", "track_number"], }, }, }, required: ["release_id", "new_order"], }, }, executor: this.reorderTracks.bind(this), }, { definition: { name: "pica_releases_attach_recording_with_work", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Attach a recording to a release at a specific position, automatically " + "pulling the recording's linked work (if any) onto the same track row. " + "Saves agents the recordings.work_id lookup that pica_releases_attach_track " + "would otherwise require. Idempotent on (release_id, disc_number, track_number).", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, recording_id: { type: "string", description: "Recording ID" }, track_number: { type: "number", description: "Position on disc (1-based)", }, disc_number: { type: "number", description: "Disc number (default 1)", }, }, required: ["release_id", "recording_id", "track_number"], }, }, executor: this.attachRecordingWithWork.bind(this), }, // ADR-175: release workflow tools — compound operations on the primitives { definition: { name: "pica_releases_move_track", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Swap two track positions on a release atomically. Internally reads " + "both rows and writes them with exchanged (disc_number, track_number) " + "in one transaction. For the rare full re-sequence, use " + "pica_releases_reorder_tracks instead.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Release ID" }, from: { type: "object", description: "Source position", properties: { track_number: { type: "number" }, disc_number: { type: "number", description: "Default 1" }, }, required: ["track_number"], }, to: { type: "object", description: "Destination position (must be occupied — this is a swap, not a slide)", properties: { track_number: { type: "number" }, disc_number: { type: "number", description: "Default 1" }, }, required: ["track_number"], }, }, required: ["release_id", "from", "to"], }, }, executor: this.moveTrack.bind(this), }, { definition: { name: "pica_releases_create_with_tracks", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "write", description: "Create a release and its tracks in a single atomic transaction. " + "Any failure — an unknown recording or work, an identifier lookup " + "miss, an invalid track entry — rolls the entire release back. No " + "partial releases. Each " + "track entry can pass recording_id/work_id directly OR pass an " + "identifier_lookup object which the tool resolves server-side " + "(same semantics as pica_releases_attach_track with " + "identifier_type + identifier_value). " + "Use this when importing a new album — use pica_releases_attach_track " + "with items[] when the release already exists.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release: { type: "object", description: "Release fields (same shape as pica_releases_create). " + "title is required; release_type defaults to 'single'.", properties: { title: { type: "string", description: "Release title" }, release_type: { type: "string", enum: ["album", "ep", "single", "compilation"], description: "Release type (default 'single')", }, release_date: { type: "string", description: "Release date (YYYY-MM-DD)", }, pre_release_date: { type: "string", description: "Pre-release / announcement date (YYYY-MM-DD)", }, upc: { type: "string", description: "UPC / EAN barcode. A 13-digit EAN starting with 0 is stored as its 12-digit UPC-A (same barcode).", }, catalog_number: { type: "string", description: "Label catalog number", }, label_organization_id: { type: "string", description: "organisations.id of the label — resolve via pica_labels_query first.", }, parent_label: { type: "string", description: "Free-text parent/umbrella label name (fallback to the label_organization_id FK).", }, distributor_organization_id: { type: "string", description: "organisations.id of the distributor.", }, artwork_url: { type: "string", description: "Album artwork URL", }, spotify_album_id: { type: "string", description: "Canonical Spotify album id (derived from the uri/url when omitted)", }, spotify_album_uri: { type: "string" }, spotify_url: { type: "string" }, apple_music_url: { type: "string" }, other_urls: { type: "object", additionalProperties: { type: "string" }, }, notes: { type: "string" }, }, required: ["title"], }, tracks: { type: "array", description: "Track entries. Each needs track_number plus one of: " + "recording_id, work_id, or identifier_lookup.", items: { type: "object", properties: { recording_id: { type: "string" }, work_id: { type: "string" }, identifier_lookup: { type: "object", description: "Resolve a recording by external identifier in " + "the caller's org before inserting the track. " + "Misses and collisions abort the whole create.", properties: { identifier_type: { type: "string", enum: [ "isrc", "spotify_url", "spotify_track_uri", "youtube_video_id", "musicbrainz_recording_id", "mlc_recording_id", ], }, value: { type: "string" }, }, required: ["identifier_type", "value"], }, track_number: { type: "number", description: "Position on disc (1-based)", }, disc_number: { type: "number", description: "Disc number (default 1)", }, }, required: ["track_number"], }, }, }, required: ["release", "tracks"], }, }, executor: this.createWithTracks.bind(this), }, { definition: { name: "pica_releases_completeness_check", scope: "read:catalog", audiences: ["stdio", "http", "public"], tier: "read", description: "Run the release-scoped completeness diligence report. Pass a " + "release_id for a single-release report, or omit for a scan of " + "every release in your catalog. Returns a list of " + "issues per release with severity (critical/warning/suggestion) " + "and a recovery hint pointing at the right tool. Issues also " + "surface in pica_catalog_diligence; this tool gives release-" + "level granularity. " + "For the tracklist itself rather than what is missing from it, use pica_releases_list_tracks.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Specialist context; not yet covered by the Creator Question Atlas — pending telemetry-driven addition per ADR-226 Phase 4 substrate writes.", inputSchema: { type: "object", properties: { release_id: { type: "string", description: "Optional — a single release to check. Omit to scan " + "every release in your catalog.", }, }, }, }, executor: this.completenessCheck.bind(this), }, { definition: { name: "pica_releases_delete", scope: "write:catalog", audiences: ["stdio", "http", "public"], tier: "destructive", description: "Delete a release. If the release still has tracks attached, or " + "has active share links, the delete is REFUSED and the error lists " + "exactly what to clear first (releases cannot be archived — there " + "is no soft-delete). Detach the tracks (pica_releases_detach_track) " + "and revoke the share links, then retry.", workflows: "infrastructure", vernacular_kind: "specialist", vernacular_reason: "Destructive operation; invoked by exact name after explicit user confirmation in a specialist flow.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Release ID", }, }, required: ["id"], }, }, executor: this.deleteRelease.bind(this), }, ]; } async deleteRelease(args) { const outcome = await this.pica.releases.delete(args.id); return formatDeletion("release", args.id, outcome); } async attachTrack(args) { // Superset dispatch — subsumed pica_releases_bulk_attach_tracks and // pica_releases_attach_track_by_identifier (both names removed in // mcp-server 2.99.0; their executors went with them). if (Array.isArray(args.items)) { if (args.dry_run === true) { return formatSuccess(`Dry run — would attach ${args.items.length} track(s) to release ${args.release_id}`, { dry_run: true, would_affect: { release_id: args.release_id, tracks: args.items }, target_count: args.items.length, }); } const tracks = await this.pica.releases.bulkAttachTracks(args.release_id, args.items); return formatSuccess(`Bulk attached ${args.items.length} tracks`, tracks); } if (args.identifier_type && args.value) { const result = await this.pica.releases.attachTrackByIdentifier(args.release_id, { identifier_type: args.identifier_type, value: args.value, track_number: args.track_number, disc_number: args.disc_number, }); return formatSuccess("Track attached by identifier", result); } if (typeof args.track_number !== "number") { throw new Error("attach_track requires track_number for a single track, or items[] for a batch"); } const track = await this.pica.releases.attachTrack(args.release_id, { recording_id: args.recording_id ?? null, work_id: args.work_id ?? null, track_number: args.track_number, disc_number: args.disc_number, }); return formatSuccess("Track attached to release", track); } async listTracks(args) { const tracks = await this.pica.releases.listTracks(args.release_id); return formatSuccess("Release tracks retrieved", tracks); } async detachTrack(args) { const result = await this.pica.releases.detachTrack(args.release_id, { track_number: args.track_number, disc_number: args.disc_number, confirm: args.confirm, }); // The SDK's request() already unwraps the route's {success, data} // envelope, so this tool receives {confirmed, preview|removed} directly. // The old `result?.data?.confirmed` check was ALWAYS undefined — the tool // rendered the "call again with confirm:true" preview instruction even // after the delete had executed (trust bug on a destructive op; // ops_issue 0137b1b8, user_feedback e6320d08). const confirmed = result?.confirmed === true; const message = confirmed ? "Track detached from release" : "Preview of track to be detached — call again with confirm:true to execute"; return formatSuccess(message, result); } async reorderTracks(args) { const tracks = await this.pica.releases.reorderTracks(args.release_id, args.new_order); return formatSuccess("Release tracks reordered", tracks); } async attachRecordingWithWork(args) { const track = await this.pica.releases.attachRecordingWithWork(args.release_id, { recording_id: args.recording_id, track_number: args.track_number, disc_number: args.disc_number, }); return formatSuccess("Recording attached to release with work", track); } async listReleases(args) { // The schema's documented default. Without it the route read every // release in one response. const limit = typeof args.limit === "number" ? args.limit : 50; const offset = typeof args.offset === "number" ? args.offset : 0; // listPage carries the route's real total and has_more (list() returned // only the page, so has_more was guessed from "the page came back full" // and an exactly-full last page sent agents to an empty one — row-cap T3). const page = await this.pica.releases.listPage({ limit, offset, query: args.query, upc: args.upc, spotify_album_id: args.spotify_album_id, release_date_from: args.release_date_from, release_date_to: args.release_date_to, }); const releases = Array.isArray(page?.data) ? page.data : []; const count = releases.length; const hasMore = page.hasMore; const nextOffset = hasMore ? offset + count : null; const from = offset + 1; const to = offset + count; const showingMsg = count === 0 ? "No releases found" : page.total === null ? // A title search past its match cap: the server cannot count. `showing ${from}–${to} of more matches than can be counted — narrow the query${hasMore ? `, or pass offset=${nextOffset} for more (if that page is empty, you've reached the end)` : ""}` : `showing ${from}–${to} of ${page.total}${hasMore ? `; pass offset=${nextOffset} for more` : ""}`; const envelope = { summary: showingMsg, count, total: page.total, offset, has_more: hasMore, next_offset: nextOffset, releases, }; const payload = JSON.stringify(envelope, null, 2); return { content: [{ type: "text", text: payload }], structuredContent: envelope, }; } async getRelease(args) { const releaseId = args.id ?? args.release_id; const release = await this.pica.releases.get(releaseId); return formatSuccess("Release retrieved", release); } async createRelease(args) { const { id: _id, ...data } = args; const release = await this.pica.releases.create(data); // Registration integrity floor (spec 2026-07-09, Task 9) — assess the // just-created release and attach both the rendered hints and the raw // gap block. Best-effort: never blocks or fails the create. // NOTE (Task 12): release-grain assessment is a documented v1 no-op — // the block renders empty by design (no release-grain gap code exists // yet; see assessEntity's release branch in lib/services/integrity-floor.ts). const pressure = await buildIntegrityPressure(this.pica, "release", release.id); return formatSuccess("Release created", { ...release, integrity: pressure.integrity }, pressure.hints); } async updateRelease(args) { const { id, ...updates } = args; const release = await this.pica.releases.update(id, updates); // Registration integrity floor (Task 9) — same pressure rendering as // createRelease. // NOTE (Task 12): release-grain assessment is a documented v1 no-op — // the block renders empty by design (see createRelease's note above). const pressure = await buildIntegrityPressure(this.pica, "release", release.id); return formatSuccess("Release updated", { ...release, integrity: pressure.integrity }, pressure.hints); } // ADR-175 workflow executors async moveTrack(args) { const tracks = await this.pica.releases.moveTrack(args.release_id, args.from, args.to); return formatSuccess("Tracks swapped", tracks); } async createWithTracks(args) { const result = await this.pica.releases.createWithTracks({ release: args.release, tracks: args.tracks, }); const count = Array.isArray(args.tracks) ? args.tracks.length : 0; return formatSuccess(`Release created with ${count} tracks`, result); } async completenessCheck(args) { const report = await this.pica.releases.completenessCheck(args.release_id ? { release_id: args.release_id } : undefined); return formatSuccess("Release completeness report", report); } } //# sourceMappingURL=releases.js.map