Skip to content

Latest commit

 

History

History
816 lines (520 loc) · 29.7 KB

File metadata and controls

816 lines (520 loc) · 29.7 KB

TuneForge API

Overview

The desktop backend exposes a local FastAPI API under /api/v1. It is intended for the bundled TuneForge frontend on the same machine and binds to 127.0.0.1 by default.

This document summarizes the current route surface. The generated OpenAPI schema in packages/shared-types/openapi.json remains the implementation source of truth for exact schemas and generated TypeScript types.

Conventions

  • Base path: /api/v1
  • Request and response format: JSON unless an endpoint streams a file.
  • IDs: string identifiers generated by the backend.
  • Timestamps: ISO 8601 strings in API responses.
  • Errors use a structured error object with code, message, and details.

Pagination Contract

Endpoints that expose growable collections use offset pagination. Endpoint-specific follow-ups should add this contract without renaming the existing list field, so generated OpenAPI clients keep a predictable response shape.

Request query parameters:

  • limit - page size, default 50, minimum 1, maximum 200.
  • offset - zero-based row offset, default 0, minimum 0.

Existing search and filter parameters apply to the full matching collection before total, limit, and offset are applied.

Response fields:

  • the existing list field, such as projects or jobs.
  • total - count after search and filters, before pagination.
  • limit - the effective limit used for this response.
  • offset - the effective offset used for this response.
  • has_more - true when offset + <returned count> < total.

Example:

{
  "projects": [],
  "total": 125,
  "limit": 50,
  "offset": 0,
  "has_more": true
}

Paginated endpoints must document their default sort and any supported client-selected sort keys. Sorting must be deterministic across repeated requests: timestamp, status, search-rank, or other non-unique sort keys need a stable tie-breaker, normally the entity's immutable ID. For example, newest-first pages should sort by updated_at DESC, id DESC instead of timestamp alone. If a sort key can be null, the endpoint must also document where null values appear.

Pagination Endpoint Audit

This audit captures the current list-like API surface. Individual endpoint sections later in this document describe current pagination behavior or explicit unpaginated exceptions.

Endpoint or payload List field Pagination decision
GET /api/v1/jobs jobs Paginated growable list with status, project_id, and project-name search filters plus sort_by/sort_order. Default ordering is active-first and deterministic.
GET /api/v1/projects projects Paginated growable list. search filters the full matching collection before pagination. Default ordering is updated_at DESC, id DESC. Desktop Library consumers should lazy-load this list.
GET /api/v1/projects/{project_id}/artifacts artifacts Explicit unpaginated bounded project inventory exception: source audio, generated stems, practice mixes, exports, and cache artifacts. Ordered by created_at DESC; no pagination.
GET /api/v1/projects/{project_id}/sections sections Explicit unpaginated project document/song-structure exception. Sections are bounded by the song arrangement and must stay complete for editing and playback.
GET /api/v1/sync/trusted-peers trusted_peers Explicit unpaginated active manual trust-list exception. Active peers only; bounded by user-paired devices and returned complete for sync UI. Deterministic ordering is display_name case-insensitive, then device_id.
GET /api/v1/beat-backends backends Explicit unpaginated exception: small static capability list, bounded by bundled/local backend implementations.
GET /api/v1/chord-backends backends Explicit unpaginated exception: small static capability list, bounded by bundled/local backend implementations.
GET /api/v1/stem-models models Explicit unpaginated exception: small static capability list, bounded by supported local stem models.
GET /api/v1/sync/metadata projects, artifacts, delete_tombstones Explicit unpaginated sync snapshot exception. The payload is a complete sync inventory used by native sync and reconciliation, not an interactive scroll list. If scale requires chunking, it should be a sync protocol change rather than this generic pagination contract.
GET /api/v1/sync/preflight projects, duplicate_groups, manual_cleanup_guidance Explicit unpaginated diagnostic exception. The response is a complete local-library health report.
GET /api/v1/sync/projects/{project_id}/manifest entity_revisions, artifacts, delete_tombstones Explicit unpaginated manifest exception. The response is an atomic project export unit.
POST /api/v1/sync/reconciliation/plan items, actions Explicit unpaginated planning exception. The response is a complete computed plan for the supplied sync inventory.
POST /api/v1/sync/reconciliation/apply plan.items, plan.actions, results, timing_evidence Explicit unpaginated apply exception. Partial pages would make apply summaries and results incomplete.
Project document payloads, including analysis timing, chords, lyrics, tab imports, and tab apply results nested content arrays and result arrays Explicit unpaginated document exceptions unless a future endpoint exposes them as standalone growable collections. These arrays are part of a single project document or edit result, not top-level list browsing.

Example error response:

{
  "error": {
    "code": "PROJECT_NOT_FOUND",
    "message": "Project not found.",
    "details": {}
  }
}

Shared Shapes

Job

Jobs represent asynchronous work such as analysis, chords, lyrics, transforms, stems, previews, and exports.

Important fields:

  • id
  • project_id
  • type
  • status
  • progress
  • source_artifact_id
  • chord_backend
  • chord_source
  • error_message
  • runtime_device
  • created_at
  • updated_at

Artifact

Artifacts represent generated or imported files associated with a project.

Important fields:

  • id
  • project_id
  • type
  • format
  • path
  • size_bytes
  • generated_by
  • can_delete
  • can_regenerate
  • metadata
  • created_at

Health

Get health

GET /api/v1/health

Returns backend name, legacy backend git ref in version, backend/frontend package versions and git refs, status, API base URL, data root, default export format, and preview format. Build git refs use the packaged build metadata when available, otherwise local development resolves them with git describe --tags --long --dirty --always.

Beat Backends

List beat backends

GET /api/v1/beat-backends

Returns available beat analysis backends. Built-in Beat Analysis is always expected to be available. Advanced Beat Analysis may be unavailable when optional desktop-only dependencies are not installed.

Chord Backends

List chord backends

GET /api/v1/chord-backends

Returns available chord backends and capability metadata. Built-in chords are always expected to be available. Advanced Chords may be unavailable when optional desktop-only dependencies are not installed or the runtime platform is mobile.

Stem Models

List stem models

GET /api/v1/stem-models

Returns supported stem models and availability metadata. Labels are Default (6 stems model) for htdemucs_6s and 2 stems model for htdemucs_ft.

Sync

The sync API exposes local sync metadata, identity, and trust management for the bundled desktop app. FastAPI remains bound to loopback; LAN transport, peer discovery, QR scanning, and file transfer belong to the separate native sync layer.

device_id is the stable identity for one TuneForge install. sync_group_id identifies the sync group that trusted devices may share. Display names are editable convenience labels and must not be treated as identity.

Get local sync identity

GET /api/v1/sync/identity

Returns the local sync identity, creating it if it does not exist yet.

Response wrapper:

  • identity

Identity fields:

  • device_id
  • sync_group_id
  • display_name - nullable convenience label.
  • public_key
  • created_at - nullable when the service returns an identity DTO without persistence metadata.
  • updated_at - nullable when the service returns an identity DTO without persistence metadata.

Update local sync identity

PATCH /api/v1/sync/identity

Updates the local identity display name.

Request fields:

  • display_name

Response wrapper:

  • identity

Changing display_name does not change device_id, sync_group_id, or public identity material.

Create sync pairing offer

POST /api/v1/sync/pairing/offers

Creates a short-lived manual pairing offer for copy/paste style pairing. The endpoint does not start LAN discovery, expose FastAPI beyond loopback, or create QR UI.

Request fields:

  • endpoint_hints - optional advisory peer endpoints, default [].
  • ttl_seconds - pairing offer lifetime in seconds, default 600, maximum 3600.

Response wrapper:

  • pairing_offer

Pairing offer fields:

  • payload
  • expires_at
  • ttl_seconds

The payload is the portable value another device submits inside the payload field of POST /api/v1/sync/trusted-peers. For this backend-first slice, a peer trust request must carry the pairing_offer_id and pairing_secret from a pending local offer so this install can verify the manual exchange before storing trust. It includes:

  • protocol_version
  • pairing_offer_id
  • sync_group_id
  • device_id
  • display_name
  • public_key
  • endpoint_hints
  • pairing_secret
  • expires_at
  • signature

Pairing payloads are signed by the source device's private identity key so copied payloads are tamper-evident. Endpoint hints are advisory and do not authenticate a peer. The receiving install must still explicitly trust the payload before accepting manifests, revisions, tombstones, or artifact bytes from that device. The payload submitted to this endpoint must reference a locally issued pairing_offer_id; that local offer must still be unused, unexpired, and match the payload's pairing_secret.

List trusted peers

GET /api/v1/sync/trusted-peers

Returns active peers this install explicitly trusts. Revoked peers are not returned by this list.

Response wrapper:

  • trusted_peers

Trusted peer fields:

  • device_id
  • sync_group_id
  • display_name
  • public_key
  • endpoint_hints
  • trusted_at
  • revoked_at
  • updated_at

Trust is explicit and non-transitive. Joining the same sync_group_id, discovering a device, or hearing about a peer from another trusted device does not automatically trust that peer.

Trust peer from pairing payload

POST /api/v1/sync/trusted-peers

Stores trust for one peer from a manual pairing payload.

Request fields:

  • payload - pairing payload copied from another device.
  • adopt_sync_group - whether this install should adopt the payload's sync_group_id, default false.

Response wrapper:

  • trusted_peer

Trusting a peer records its device_id, public identity, display name, endpoint hints, and sync group. The peer can later be revoked without deleting local projects or changing this install's own device_id. Standalone payloads that do not reference a local pending offer are rejected.

Update trusted peer endpoint hints

PATCH /api/v1/sync/trusted-peers/{device_id}/endpoint-hints

Replaces advisory endpoint hints for an active trusted peer.

Request fields:

  • endpoint_hints - advisory peer endpoints. Values are trimmed and empty values are rejected.

Response wrapper:

  • trusted_peer

Unknown or revoked peers return 404.

Revoke trusted peer

DELETE /api/v1/sync/trusted-peers/{device_id}

Revokes trust for the peer with the matching device_id.

Response wrapper:

  • trusted_peer

Revocation stops this install from accepting future sync material from that peer unless it is explicitly paired again. Revocation is local to this install and does not revoke trust transitively on any other device.

Run sync preflight

GET /api/v1/sync/preflight

Checks the local library before multi-device sync is enabled. The endpoint returns HTTP 200 for both passing and failing preflight results because failures are actionable diagnostics rather than request errors.

The response includes:

  • ok - overall sync readiness. This is true when library checks pass. Pending or running jobs are diagnostic state, not a hard sync blocker.
  • library_ok - library-only readiness, matching the previous ok behavior.
  • project status counts
  • per-project sync identity status
  • duplicate source-hash groups
  • job_state - active job diagnostics for sync startup and backend responsiveness failures.
  • manual cleanup guidance

job_state includes state (ready or busy), running_job_count, pending_job_count, blocking_job_count, blocking_job_counts, blocking_jobs, blocking_jobs_truncated, and guidance. Pending and running jobs do not block sync by themselves; they are included so the UI can explain backend-busy or endpoint-timeout failures. blocking_jobs is capped at 20 entries and exposes only sync-safe fields: id, project_id, project_name, type, status, progress, started_at, and updated_at. It does not expose job payloads, file paths, audio data, or endpoint details.

Project status values are ready, missing_source_hash, invalid_source_hash, duplicate_source_hash, and noncanonical_project_id. Canonical project IDs use proj_sha256_<full_source_sha256>, while project storage directories use a shorter derived key such as proj_<first_24_sha256_hex>. Preflight uses a stored projects.source_sha256 when present. If that field is missing, preflight only recovers it from an explicit app-managed original-byte copy and otherwise reports missing_source_hash; source_path is provenance only and is not hashed during recovery. This endpoint is sync-specific; general project responses do not expose source hashes.

Get sync metadata

GET /api/v1/sync/metadata

Returns sync-safe project and artifact metadata so sync clients do not need to read the local SQLite database directly. The endpoint does not expose absolute local file paths.

Project fields:

  • project_id
  • display_name
  • source_key_override
  • source_sha256
  • duration_seconds
  • sample_rate
  • channels
  • created_at
  • updated_at

Artifact fields:

  • artifact_id
  • project_id
  • type
  • format
  • relative_path
  • content_sha256
  • size_bytes
  • generated_by
  • can_delete
  • can_regenerate
  • cache_key
  • metadata
  • created_at

Delete tombstone fields:

  • tombstone_id
  • sync_group_id
  • project_id
  • target_type
  • target_id
  • author_device_id
  • deleted_at
  • prior_metadata
  • created_at
  • updated_at

relative_path is project-root relative when the artifact is stored under the backend-managed project root; otherwise it is null. Operational source_audio artifacts are app-managed WAV files under the project root. Artifact metadata is sanitized recursively before returning and removes local path-bearing keys such as source_path, original_copy_path, playback_path, imported_path, and any metadata key ending in _path. Non-path metadata such as retune/transpose settings, stem_model, and source_artifact_id is preserved. Job internals such as result_artifact_ids_json are not exposed.

Plan sync reconciliation

POST /api/v1/sync/reconciliation/plan

Returns a stateless sync plan comparing the local library with metadata supplied by the native sync layer. The endpoint reads local state but does not write projects, artifacts, revisions, tombstones, staged files, jobs, or conflict records. Clients execute returned actions through the existing import, staging, and future sync flows.

Request fields:

  • remote_library - sync metadata in the same shape returned by GET /api/v1/sync/metadata, plus optional entity_revisions: projects, artifacts, entity_revisions, and optional delete_tombstones.
  • project_manifests - optional list of exported project manifests, default [].
  • peer_inventory - peer entries with device_id, available_content_sha256, and optional small metadata.

Response fields:

  • summary - total_items, total_actions, total_conflicts, and status_counts.
  • items - per-project, artifact, revision, or tombstone decisions with item_type, item_id, optional project_id, status, optional action_type, optional content_sha256, optional chosen_provider_device_id, optional reason, and details.
  • actions - ordered operations with action_type, item_type, item_id, optional project_id, optional content_sha256, optional provider_device_id, optional reason, priority, and details.

Planner statuses are noop, identical_content, missing_local_bytes, remote_available, missing_provider, deleted, and conflicted.

Action types are apply_delete_tombstone, import_project_manifest, import_entity_revision, fetch_artifact_content, import_artifact_manifest, record_conflict, and noop.

Export project sync manifest

GET /api/v1/sync/projects/{project_id}/manifest

Returns a single project manifest for manifest-only sync export. The response is wrapped as project_manifest and includes:

  • schema_version
  • exported_at
  • project
  • entity_revisions
  • artifacts
  • delete_tombstones

Manifest paths are project-root relative and use portable path separators. The response does not expose absolute local source, project, artifact, or app data paths. The project entry's source_sha256 is the identity hash of the original imported source bytes. Artifact entries' content_sha256 values hash the bytes for those specific staged artifact files, including the source_audio artifact. These hashes are separate on purpose: an app-managed normalized WAV source artifact may have different bytes from the original import identity hash. sync_entity_revisions.content_sha256 is also per-entity revision payload content, not project source identity. Projects whose source_audio artifact is not an app-managed readable PCM WAV must be reimported or repaired before manifest export. Receiving peers must verify staged files against the artifact SHA-256 values and must preserve the project source_sha256 as identity metadata before accepting the import. The endpoint exports metadata only. File transfer and LAN peer discovery belong to the native sync layer, not the loopback FastAPI API.

entity_revisions carries durable sync records for project metadata, chords, lyrics, sections, regeneration events, and other editable or generated project entities. Each revision records revision_id, project_id, entity_type, entity_id, revision_type, optional base_revision_id, author_device_id, optional source_artifact_id, content_sha256, state, metadata, payload, created_at, and updated_at.

delete_tombstones carries durable group-delete records for project, artifact, and entity revision targets so offline peers do not resurrect deleted records when they reconnect. Each tombstone records the author device, delete timestamp, target type, target ID, sync group/project context, and prior metadata needed for diagnostics.

Import staged project sync manifest

POST /api/v1/sync/projects/import

Imports a project from a previously exported project manifest plus files that have already been staged on disk by the sync transport.

Request fields:

  • manifest
  • staging_root

staging_root is a local directory containing the staged project files at the relative paths declared in the manifest. During import, the backend requires exactly one source_audio artifact, and that artifact must use format="wav" with a .wav relative path. The backend verifies source and artifact bytes with SHA-256, rewrites accepted paths into this install's backend-managed project root, and persists the project through backend services instead of copying database rows from another device. Original absolute import paths are local provenance only and are not sync-operational inputs.

If the local library already contains the same canonical project or source SHA-256, staged import rejects the duplicate with HTTP 409 instead of creating a second project. The response uses the normal project wrapper shape:

  • project

Staged import does not enqueue analysis, chord, lyrics, stem, or other generation jobs. Synced projects should import the durable state that was actually exported; future rebuilds are explicit user actions. Imports must reject older manifests that would recreate a project, artifact, or entity revision covered by an accepted delete tombstone. This endpoint also does not expose FastAPI on the LAN. Peer communication should keep using the separate native sync layer while FastAPI remains bound to loopback.

Projects

Import project

POST /api/v1/projects/import

Creates a project from a source path and queues initial analysis, chord, lyrics, and source stem jobs.

Request fields:

  • source_path
  • copy_into_project
  • display_name
  • stem_model

Response: ProjectResponse.

stem_model is optional. Desktop imports send the user's default stem model so automatic source stems match manual stem generation preferences; if omitted, the backend stem model default is used.

source_path records where the user imported the file from on this install. Sync treats it as local provenance, not a durable source of original bytes or an operational sync input. copy_into_project=false is accepted for compatibility, but new imports still create an app-managed operational WAV source artifact under the project root.

If the same source track has already been imported, the endpoint returns HTTP 409 with code DUPLICATE_PROJECT_SOURCE, message This project is already imported with name "{name}"., and details containing:

  • project_id
  • project_name

List projects

GET /api/v1/projects

Optional query:

  • search - filters projects by display name or path before pagination.
  • limit - page size, default 50, minimum 1, maximum 200.
  • offset - zero-based row offset, default 0, minimum 0.

Default ordering is deterministic newest-first: updated_at DESC, id DESC.

Response: ProjectsResponse with projects, total, limit, offset, and has_more.

Get project

GET /api/v1/projects/{project_id}

Response: ProjectResponse.

Update project

PATCH /api/v1/projects/{project_id}

Supported fields:

  • display_name
  • source_key_override

Response: ProjectResponse.

Delete project

DELETE /api/v1/projects/{project_id}

Deletes the project and project-owned data.

Response: DeleteResponse.

Analysis

Start analysis

POST /api/v1/projects/{project_id}/analyze

Queues an analysis job.

Request fields:

  • include_tempo
  • force

Response: JobResponse.

Get analysis

GET /api/v1/projects/{project_id}/analysis

Returns the stored analysis result, or null if no result exists.

Analysis includes key, key confidence, reference tuning, tuning offset, tempo, analysis version, source artifact, and creation time.

Analysis is an unpaginated project document payload. Its timing data is returned with the analysis document so playback and editing consumers do not need lazy-loaded fragments.

Chords

Generate chords

POST /api/v1/projects/{project_id}/chords

Queues chord generation.

Request fields:

  • backend
  • backend_fallback_from
  • force
  • overwrite_user_edits

Response: JobResponse.

Get chords

GET /api/v1/projects/{project_id}/chords

Returns source segments, current timeline segments, backend, source artifact, source kind, user-edit state, metadata, and timestamps. If no chord timeline exists, the response contains an empty timeline.

Chord timelines are unpaginated project document payloads. The source and current timelines stay complete so playback and editing consumers share one coherent chord document.

Lyrics

Generate lyrics

POST /api/v1/projects/{project_id}/lyrics

Queues local lyrics generation.

Request fields:

  • force
  • language_override (optional): null or omitted uses Whisper auto-detection. Supported overrides are none, en, pt, es, fr, de, it, ja, ko, zh, and hi. none marks the project as having no lyrics and does not run Whisper.

Response: JobResponse.

Get lyrics

GET /api/v1/projects/{project_id}/lyrics

Returns source transcript segments, current edited segments, backend, source artifact, model/device metadata, effective language, language override, user-edit state, and timestamps. If no lyrics exist, the response contains empty segment lists.

Lyrics segments are unpaginated project document payloads. Source and edited segments stay complete so lyric editing and playback highlighting do not depend on lazy-loaded fragments.

Update lyrics

PUT /api/v1/projects/{project_id}/lyrics

Persists edited lyric segment text.

Request fields:

  • segments

Each segment currently accepts:

  • text

Response: LyricsResponse.

Sections

List project sections

GET /api/v1/projects/{project_id}/sections

Returns the complete song-structure section list for the project.

Sections are an unpaginated project document payload. They are bounded by the arrangement and stay complete so editing, playback navigation, and tab-import apply results use one shared song-structure view.

Response: SongSectionsResponse.

Transforms and Previews

Retune

POST /api/v1/projects/{project_id}/retune

Queues a retune job.

Request fields:

  • exactly one of target_reference_hz or target_cents_offset
  • preview_only
  • output_format

Response: JobResponse.

Transpose

POST /api/v1/projects/{project_id}/transpose

Queues a transpose job.

Request fields:

  • semitones
  • preview_only
  • output_format

Response: JobResponse.

Generate preview

POST /api/v1/projects/{project_id}/preview

Queues a preview job for a retune and/or transpose transform.

Request fields:

  • retune
  • transpose
  • output_format

At least one transform must be provided.

Response: JobResponse.

Stems

Generate stems

POST /api/v1/projects/{project_id}/stems

Queues stem generation for the selected source audio or practice mix.

Request fields:

  • mode
  • stem_model
  • output_format
  • force
  • source_artifact_id
  • chord_backend
  • chord_backend_fallback_from
  • overwrite_chord_edits

Current validation allows mode: "stems" or mode: "two_stem" and output_format: "wav". If stem_model is omitted, mode: "stems" uses the backend default htdemucs_6s; mode: "two_stem" maps to htdemucs_ft for compatibility.

htdemucs_6s creates visible Vocals, Drums, Bass, Guitar, Piano, and Other artifacts. htdemucs_ft creates visible Vocals and Instrumental artifacts.

Response: JobResponse.

Project Artifacts

List project artifacts

GET /api/v1/projects/{project_id}/artifacts

Returns the bounded project inventory for source audio, generated stems, practice mixes, exports, and cache artifacts. Results are ordered by created_at DESC.

Project artifacts are not paginated. They are project-scoped inventory, not a library-level growable browsing list.

Response: ArtifactsResponse.

Delete project artifact

DELETE /api/v1/projects/{project_id}/artifacts/{artifact_id}

Deletes a project artifact when it belongs to the project and can be removed.

Response: DeleteResponse.

Export

Export project artifacts

POST /api/v1/projects/{project_id}/export

Queues an export job for selected project artifacts.

Request fields:

  • artifact_ids
  • mixdown_mode
  • output_format
  • destination_path

At least one artifact ID is required.

Response: JobResponse.

Jobs

List jobs

GET /api/v1/jobs

Returns a paginated list of jobs.

Query parameters:

  • limit - page size, default 50, minimum 1, maximum 200.
  • offset - zero-based row offset, default 0, minimum 0.
  • status - optional repeatable status filter, for example ?status=pending&status=running.
  • project_id - optional project ID filter.
  • search - optional project display name filter. Search is applied before pagination and composes with status and project_id filters.
  • sort_by - optional sort key. Supported values are activity (default), created_at, started_at, updated_at, and status.
  • sort_order - optional asc or desc direction for non-activity sorts. Omit this when sort_by is activity or omitted; sort_order with activity sorting returns 422 INVALID_REQUEST.

Filters and search are applied before sorting, total, and pagination. Pagination is applied after sorting.

Default activity ordering:

  • running jobs first, ordered by started_at when present, otherwise created_at, ascending, then id ascending.
  • pending jobs next, ordered by created_at ascending, then id ascending.
  • terminal jobs (completed, cancelled, failed) next, ordered by completed_at when present, otherwise updated_at, descending, then id descending.
  • unknown statuses last, ordered by updated_at descending, then id descending.

Timestamp ordering:

  • created_at, started_at, and updated_at default to desc; use sort_order=asc for oldest-first.
  • null timestamp values are always last for both asc and desc.
  • Timestamp ties use id ascending as the deterministic tie-breaker.

Status ordering:

  • sort_by=status defaults to asc.
  • asc groups jobs as running, pending, completed, cancelled, failed, then unknown statuses.
  • desc reverses those groups.
  • Jobs inside each status group use the same deterministic tie-breakers as activity ordering for that group.

Response: JobsResponse.

Get job

GET /api/v1/jobs/{job_id}

Response: JobResponse.

Cancel job

POST /api/v1/jobs/{job_id}/cancel

Requests cancellation for a job.

Response: JobResponse.

Artifact Streaming

Stream artifact

GET /api/v1/artifacts/{artifact_id}/stream

Streams the artifact file as an audio response. Returns ARTIFACT_NOT_FOUND if the artifact row or file is missing.