Files
minstrel/docs/superpowers/specs/2026-04-29-m5a-lidarr-design.md
T
bvandeusen 17406eebee docs(spec): add M5a Lidarr connection + search/add design
M5a is the foundation slice of M5 (Lidarr integration + quarantine).
Decomposed into M5a/M5b/M5c during brainstorming on 2026-04-29:

- M5a (this spec) — Lidarr connection + search/add + admin shell
- M5b — Quarantine workflow (per-user soft-hide, admin resolution)
- M5c — Radio suggested-additions (out-of-library MBIDs surfaced)

Each ships as its own PR with its own brainstorm/spec/plan cycle.

Key decisions captured:
- Permissions: search-all, add-admin via request queue
- Config: DB-only, Settings UI is the only entry point (no YAML)
- Settings shape: dedicated /admin/* route group, hard route gate
- Search UX: standalone /discover route, all three granularities
- Lifecycle: library scan as source of truth + 5-min reconciler worker
- Quality profile/root folder: default + per-add admin override
- UI lands at FabledSword design system bar (forest-teal accent;
  Moss/Bronze/Oxblood action buttons; per project_design_system memory)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 14:25:08 -04:00

26 KiB

M5a — Lidarr connection + search/add proxy + admin shell

Status: Draft for review · 2026-04-29

Sub-plan of: M5 (Lidarr integration + quarantine workflow). M5 was decomposed into three slices during brainstorming on 2026-04-29:

  • M5a (this spec) — Lidarr connection + search/add + admin shell. Foundation; ships first.
  • M5b — Quarantine workflow (per-user soft-hide, admin resolution UI). Depends on M5a only for the admin shell.
  • M5c — Radio "suggested additions" (out-of-library MBIDs surfaced in /api/radio responses; SPA add affordance). Depends on M5a's lidarr_requests table and add path.

Each ships as its own PR with its own brainstorm/spec/plan cycle.

1. Goal

Connect Minstrel to a household Lidarr instance, give every user a search-and-request workflow at /discover, and give admins a moderation queue at /admin/requests. Approved requests fire Lidarr adds synchronously; a background reconciler matches the resulting downloaded tracks back to the originating request when the next library scan picks them up.

This slice does NOT introduce quarantine, soft-hide, or radio suggested-additions — those are M5b and M5c.

2. Goals and non-goals

Goals

  • Operator can connect, configure, test, and disconnect a Lidarr instance from /admin/integrations without editing YAML or restarting the server.
  • Any authenticated user can search Lidarr at artist / album / track granularity from /discover.
  • Any authenticated user can submit an add request, which gets persisted to lidarr_requests with status pending.
  • Admin can review pending requests at /admin/requests, approve (with optional per-request override of quality profile / root folder) or reject (with optional note).
  • Approved requests trigger a synchronous Lidarr add and a library scan.
  • A background reconciler worker matches approved requests to newly scanned tracks and transitions them to completed.
  • Hard route gating on /admin/* — non-admin users redirected before any admin content loads.

Non-goals (this slice)

  • Quarantine workflow, soft-hide on tracks, admin "delete via Lidarr" path. → M5b.
  • Radio "suggested additions" surfacing out-of-library similar tracks. → M5c.
  • Webhook ingestion from Lidarr (push notifications on download complete). → optional follow-up; the polling reconciler is sufficient for v1.
  • "Pending too long" failure detection / requestor notification on stalled adds. → open question, deferred.
  • Self-service password reset, OIDC, or any other identity work. → orthogonal.
  • Per-user Lidarr accounts. Lidarr is a single household instance.

3. Architecture

New Go packages

  • internal/lidarr/ — HTTP client for Lidarr's v1 API. Mirrors internal/scrobble/listenbrainz/ shape: Client struct with BaseURL, APIKey, HTTP fields. Methods: LookupArtist(ctx, query), LookupAlbum(ctx, query), LookupTrack(ctx, query), AddArtist(ctx, params), AddAlbum(ctx, params), ListQualityProfiles(ctx), ListRootFolders(ctx), Ping(ctx). Returns typed structs; never leaks raw JSON to callers.

  • internal/lidarrconfig/ — singleton config service. Reads/writes the lidarr_config row, exposes Get(ctx) (*Config, error) returning a typed struct, Save(ctx, *Config) error. The Get method also handles the "config not yet set" case by returning a zero-value Config{Enabled: false} so callers can branch cleanly.

  • internal/lidarrrequests/ — request lifecycle service:

    • ServiceCreate(ctx, userID, params), ListPending(ctx), ListByStatus(ctx, status), ListForUser(ctx, userID), Approve(ctx, requestID, adminID, overrides), Reject(ctx, requestID, adminID, notes), Cancel(ctx, requestID, userID). Approve calls lidarr.Client.Add* synchronously and triggers a library scan via the existing scanner package.
    • Reconciler — background worker analogous to internal/similarity.Worker. Run(ctx) loop with tick interval (default 5 min) calls tickOnce(ctx), which:
      1. SELECTs lidarr_requests WHERE status = 'approved' with row limits.
      2. For each, joins against tracks/albums/artists by MBID hierarchy.
      3. Transitions matched rows to completed, sets matched_*_id, completed_at.

Wiring

  • cmd/minstrel/main.go gains a third worker spin-up (alongside the scrobble and similarity workers). The reconciler skips its work when lidarr_config.enabled = false.
  • New handler files: internal/api/lidarr.go (search proxy), internal/api/requests.go (user-facing endpoints), internal/api/admin/lidarr.go (config CRUD + profiles/folders lookups), internal/api/admin/requests.go (approval queue).
  • New middleware: RequireAdmin — checks the user resolved by RequireUser has is_admin = true; 403s with {"error":"not_authorized"} otherwise. Mounted on the /api/admin/* route group.

Data flow — happy path

  1. User opens /discover, types query, SPA hits GET /api/lidarr/search?q=…&kind=artist|album|track.
  2. Handler invokes the appropriate lidarr.Client.Lookup*, normalizes the response, returns JSON.
  3. User clicks "Request" → POST /api/requests with {kind, lidarr_artist_mbid, lidarr_album_mbid?, lidarr_track_mbid?, artist_name, album_title?, track_title?}.
  4. Handler validates the kind→fields invariant, creates a lidarr_requests row with status pending, returns 201.
  5. Admin opens /admin/requests, SPA hits GET /api/admin/requests?status=pending.
  6. Admin clicks "Approve" → POST /api/admin/requests/:id/approve with optional {quality_profile_id, root_folder_path}.
  7. Handler snapshots the chosen values into the row, calls lidarr.Client.AddArtist|AddAlbum, transitions to approved, fires scan trigger. Returns 200 (or surfaces Lidarr error in 4xx/5xx).
  8. Reconciler worker on next tick (≤5 min) sees the approved row, joins against tracks, finds the new track, transitions to completed with matched_track_id set.
  9. Requester's /requests page shows status completed with a "Listen" link to the now-playable track.

SPA route gating

  • /admin/* route group has a +layout.svelte (or +layout.ts) guard: if currentUser.is_admin === false, calls goto('/') before child routes load. Page-level gate, not content gating — the operator's instruction.
  • Same pattern as the existing auth gate; small extension of the existing layout machinery.

4. Schema

Migration 0010_lidarr in two files (up.sql, down.sql).

lidarr_config (singleton)

CREATE TABLE lidarr_config (
    id                          smallint PRIMARY KEY DEFAULT 1 CHECK (id = 1),
    enabled                     boolean  NOT NULL DEFAULT false,
    base_url                    text,
    api_key                     text,
    default_quality_profile_id  int,
    default_root_folder_path    text,
    created_at                  timestamptz NOT NULL DEFAULT now(),
    updated_at                  timestamptz NOT NULL DEFAULT now()
);

INSERT INTO lidarr_config (id, enabled) VALUES (1, false);

The CHECK (id = 1) plus seed row enforces "exactly one row, ever." The Settings UI shows "Connect Lidarr" instead of "Lidarr is connected" when enabled=false or base_url IS NULL.

lidarr_requests

CREATE TYPE lidarr_request_status AS ENUM (
    'pending', 'approved', 'rejected', 'completed', 'failed'
);
CREATE TYPE lidarr_request_kind AS ENUM ('artist', 'album', 'track');

CREATE TABLE lidarr_requests (
    id                         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id                    uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    status                     lidarr_request_status NOT NULL DEFAULT 'pending',
    kind                       lidarr_request_kind   NOT NULL,

    lidarr_artist_mbid         text NOT NULL,
    lidarr_album_mbid          text,
    lidarr_track_mbid          text,
    artist_name                text NOT NULL,
    album_title                text,
    track_title                text,

    quality_profile_id         int,
    root_folder_path           text,

    decided_at                 timestamptz,
    decided_by                 uuid REFERENCES users(id) ON DELETE SET NULL,
    notes                      text,

    completed_at               timestamptz,
    matched_track_id           uuid REFERENCES tracks(id) ON DELETE SET NULL,
    matched_album_id           uuid REFERENCES albums(id) ON DELETE SET NULL,
    matched_artist_id          uuid REFERENCES artists(id) ON DELETE SET NULL,

    requested_at               timestamptz NOT NULL DEFAULT now(),
    updated_at                 timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX lidarr_requests_user_id_idx     ON lidarr_requests (user_id);
CREATE INDEX lidarr_requests_status_idx      ON lidarr_requests (status);
CREATE INDEX lidarr_requests_artist_mbid_idx ON lidarr_requests (lidarr_artist_mbid);
CREATE INDEX lidarr_requests_album_mbid_idx  ON lidarr_requests (lidarr_album_mbid)
  WHERE lidarr_album_mbid IS NOT NULL;

Shape notes:

  • Three matched-* FKs (one per kind) instead of polymorphic — clean SQL, ON DELETE SET NULL keeps the historical audit even if the matched track gets removed later.
  • Track-kind requests still set lidarr_album_mbid (because that's what Lidarr actually adds); lidarr_track_mbid is preserved for "I requested this song specifically" display.
  • quality_profile_id and root_folder_path are NULL until decision time. When admin approves, the override (or the config default) gets snapshotted into the row.
  • failed is reserved in the enum for a future reconciler timeout (e.g., "approved >7 days ago, no match"), but no reconciler logic transitions to failed in this slice. It's a placeholder for an obvious follow-up.

Indexes rationale:

  • user_id/requests page scan ("show my requests").
  • statusWHERE status='pending' for admin queue, WHERE status='approved' for reconciler.
  • lidarr_artist_mbid / lidarr_album_mbid — reconciler joins against tracks, albums by MBID.

Down migration

Drops in reverse: indexes → table → enum types → singleton row deletion (table goes anyway).

5. API surface

All endpoints under /api/*, JSON request/response, {error: "<code>", message: "<human>"} envelope on errors. /api/admin/* group passes through RequireAdmin middleware (403 with not_authorized for non-admin tokens).

User-facing (any authenticated user)

Method Path Behavior
GET /api/lidarr/search?q=&kind=artist|album|track Proxies Lidarr lookup. Returns normalized list `[{mbid, name
POST /api/requests Create a request. Body: {kind, lidarr_artist_mbid, lidarr_album_mbid?, lidarr_track_mbid?, artist_name, album_title?, track_title?}. Server validates kind→required-fields invariant. Returns 201 {request}.
GET /api/requests List the caller's own requests (any status). Ordered by requested_at desc. Pagination: ?limit=&before= cursor.
GET /api/requests/:id Single request detail. 404 if not caller's own and caller is not admin.
DELETE /api/requests/:id Cancel a still-pending request the caller created. 409 request_not_pending if status != pending.

Admin-only

Method Path Behavior
GET /api/admin/lidarr/config Returns current config. api_key masked as "***" when set, null when unset.
PUT /api/admin/lidarr/config Body: {base_url, api_key, default_quality_profile_id, default_root_folder_path, enabled}. api_key: empty string = leave saved value unchanged; non-empty = update. URL validated.
POST /api/admin/lidarr/test Body: {base_url?, api_key?}. Each field independently falls back to the saved value when absent or empty. Calls Client.Ping. Always returns 200 with envelope {ok: true, version: "..."} or {ok: false, error: "..."} — never an HTTP-level error envelope, so the SPA can render results uniformly.
GET /api/admin/lidarr/quality-profiles Proxies Lidarr's quality profile list — populates the Settings dropdown.
GET /api/admin/lidarr/root-folders Proxies Lidarr's root folder list — populates the Settings dropdown.
GET /api/admin/requests?status=pending|approved|rejected|completed|failed&limit= Admin's queue view. Default status=pending.
POST /api/admin/requests/:id/approve Body: {quality_profile_id?, root_folder_path?} (override fields; absent = use config default). Snapshots chosen values, calls `Client.AddArtist
POST /api/admin/requests/:id/reject Body: {notes?}. Sets status rejected, records decided_* and notes.

Error codes

lidarr_disabled, lidarr_unreachable, lidarr_auth_failed, lidarr_lookup_failed, mbid_required, request_not_pending, request_not_found, not_authorized.

6. UI surfaces

All four screens land at the FabledSword design system bar (memory: project_design_system.md). Tokens are referenced by name; concrete values live in the design-system memory. Mockups produced during brainstorming live in .superpowers/brainstorm/<session>/content/ (gitignored) — discover-fs-v2.html, admin-integrations.html, admin-requests.html, user-requests.html.

/discover (user-facing)

Search input at top, kind tabs (Artists / Albums / Tracks), card grid of results. Card state derives from the search response's in_library and requested flags:

  • Kept (in_library=true) — wins over requested. Disabled ghost button "In library", "Kept" pill in the badge slot (forest-teal at 15% opacity bg).
  • Requested (in_library=false && requested=true) — disabled ghost button "Requested", subtitle line shows "awaiting review" for pending requests, "downloading" for approved.
  • Requestable (in_library=false && requested=false) — Moss Request button with plus icon.

Card layout discipline:

  • Card body is display: flex; flex-direction: column. Inside, a .text block has min-height reserving title + meta + badge-row even when fields are absent. Actions block uses margin-top: auto so the button always anchors to the bottom of the card.
  • .badge-row reserves 22px regardless of badge presence — title sits at the same Y across cards.
  • Grid align-items: stretch keeps cards on the same row equal-height.

Implementation: <DiscoverResultCard> Svelte component with props {kind, artistName, albumTitle?, trackTitle?, imageUrl?, state: 'requestable'|'kept'|'requested', onRequest}. Reused for any future "list of music things" surface.

Track-kind result requests open a confirmation modal: "Requesting Track X will add the album Album Y. Continue?" Confirm = Moss, Cancel = Bronze. Disclosure is explicit, not silent.

/admin shell + sidebar

/admin/* routes share +layout.svelte:

  • Role gate: if (!currentUser.is_admin) goto('/') before child routes load.
  • 220px sidebar with Iron card surface. Active item: 12% accent-tinted background, 2px forest-teal left strip ("you are here").
  • Nav items (this slice): Overview · Integrations · Requests · Quarantine (placeholder for M5b) · Users (future) · Library (future).

Lucide icons at 16px, 1px stroke. Sidebar text: Vellum default, Parchment on active.

/admin/integrations

Lidarr panel (single section in this slice; designed to host more integrations later):

  • Header status pill: "Lidarr · connected" (Moss-tinted) when enabled && reachable, "unset" (Pewter ghost) otherwise.
  • Form rows: Base URL · API key (masked) · Default quality profile (dropdown) · Default root folder (dropdown).
  • Action row: Save changes = Moss, Test connection = Pewter ghost, Disconnect = Oxblood + trash icon (right-aligned, separated).
  • Inputs on Obsidian (inset feel), 0.5px Pewter borders, 8px radius, focus = box-shadow: 0 0 0 2px var(--fs-accent) (no layout shift).

A second placeholder section for "MusicBrainz overrides" with status unset foreshadows the panel's role as the integration hub. Not implemented in this slice.

/admin/requests

Tabbed list (Pending / Approved / Completed / Rejected). Tab counts as accent-tinted pills. Default tab Pending with badge showing count.

Row anatomy: 56px album-art square (Slate fallback when Lidarr returns no cover) · meta-row with kind pill + "requested by alice · 2h ago" small caps · title in Parchment · meta in Vellum · action cluster: Override (Pewter ghost, opens modal) → Approve (Moss + check icon) → Reject (Bronze + ✕ icon).

Override modal: collapsed-by-default override of quality_profile_id and root_folder_path for the specific approval. Most approvals click "Approve" without opening this.

Track-kind row's meta line spells out "Approving will add the album Geogaddi" — explicit disclosure of the album-promotion behavior.

/requests (user's own)

Single panel listing the caller's requests, ordered by requested_at desc. Row anatomy mirrors /admin/requests but action cluster is reduced:

  • Pending → Cancel (Pewter ghost + ✕ icon).
  • Approved → no actions, "Approved · downloading" status pill (Info-tinted).
  • Completed → "Listen" link in forest-teal (the page's only brand-moment), navigates to the matched track.
  • Rejected → no actions, admin's note rendered as Vellum meta when present.

Status pills use the doc's semantic palette (Warning · Info · Moss/Success · Error). Voice rule applied: "Kept" instead of "Completed", "Set aside" instead of "Rejected", "Awaiting review" instead of "Pending review."

7. Error handling

Lidarr unreachable / auth-failed

  • Client.* methods return typed errors: lidarr.ErrUnreachable, lidarr.ErrAuthFailed, lidarr.ErrLookupFailed.
  • Search proxy translates to 503 lidarr_unreachable or 401 lidarr_auth_failed — the SPA shows a callout: "Lidarr is unreachable right now. Try again, or check Settings → Integrations." (Voice rule: this is an error/waiting moment, gets the flavored register.)
  • Admin approval handler same pattern: returns the error to admin so they can retry without losing the request. The request stays pending if the Lidarr call fails — never advances to approved without confirmation Lidarr accepted the add.

Test connection from Settings

  • POST /api/admin/lidarr/test always returns 200 with {ok: bool, error?: string, version?: string} — never an error envelope. Lets the SPA always render the result without parsing HTTP-level errors.

Reconciler

  • Worker errors logged at WARN, never propagated. A failing tick doesn't stop the worker — next tick retries.
  • If lidarr_config.enabled = false, worker silently no-ops each tick.
  • Postgres unavailability is a global concern; reconciler errors with the same backoff pattern as internal/similarity.Worker.

Form validation

  • Settings: URL must parse, API key must be non-empty if enabled=true. Quality profile + root folder must be present and known to Lidarr (validated against Client.ListQualityProfiles / ListRootFolders).
  • Request creation: kind→required-MBID-fields invariant enforced server-side. SPA validates client-side first to avoid round-trips.

8. Testing

Unit tests

  • internal/lidarr/ — table-driven request/response parsing tests against canned fixtures (real Lidarr response samples committed under internal/lidarr/testdata/). Covers happy path + auth-failure + 5xx + bad-JSON for each method.
  • internal/lidarrconfig/Get returns sensible zero-value when row says enabled=false; Save updates updated_at.
  • internal/lidarrrequests.Service — pure-logic tests: kind→required-fields validation, status-transition validation (can only approve pending, can only cancel pending, etc.).

Integration tests (gated on MINSTREL_TEST_DATABASE_URL)

  • Reconciler — seeds an approved request + a matching track row, runs tickOnce, asserts status transitions to completed with correct matched_*_id. Covers:
    • artist-kind matched by lidarr_artist_mbid against artists.mbid
    • album-kind matched by lidarr_album_mbid against albums.mbid
    • track-kind matched by lidarr_album_mbid (track-promoted) against albums.mbid
    • no match (no transition)
    • already-completed row not re-processed
    • lidarr_config.enabled=false short-circuits to no-op

HTTP tests (handler level)

  • internal/api/lidarr.go — search proxy with stubbed Client: 200 happy path, 503 disabled, 503 unreachable, 401 auth-failed.
  • internal/api/requests.go — create with valid + invalid kind/MBID combinations; list-mine returns only caller's rows; cancel pending vs cancel non-pending; cross-user 404.
  • internal/api/admin/lidarr.go — config GET masks api_key; PUT empty-string preserves api_key; test-connection always returns 200 envelope.
  • internal/api/admin/requests.go — approve fires Lidarr stub, transitions row, captures defaults from config; approve with override snapshots override values; reject without notes works; non-admin 403 across the board.

Frontend tests (vitest)

  • <DiscoverResultCard> — renders three states correctly; calls onRequest only in requestable state; reserved-slot CSS verified by computed-style assertion (badge-row min-height, button anchored).
  • /discover page — debounce search input; renders results from store; transitions state on request submit; track-kind opens confirmation modal; modal Confirm calls API, modal Cancel does not.
  • /admin/requests page — tab switch refetches; approve fires API and removes row from pending; override modal returns chosen values to approve handler; admin-only redirect verified at layout level.
  • /admin/integrations panel — empty-state copy; test-connection updates status pill; Disconnect requires confirmation.
  • /requests page — status pills render with correct semantic class; Cancel works on pending; "Listen" link only renders on completed rows.

Coverage target

  • internal/lidarr/ ≥ 80%
  • internal/lidarrrequests/ ≥ 80%
  • internal/api/lidarr.go, internal/api/requests.go, internal/api/admin/* — handler coverage measured combined ≥ 70% (matches current api package threshold)

9. Decisions ledger

# Decision Rationale
1 Decompose M5 into M5a / M5b / M5c Matches the M4 cadence; smaller PRs, faster review, clearer scope per slice
2 Permissions: search-all, add-admin (request queue) Operator's call — "browse-and-suggest workflow lets non-trusted household members participate without giving them library-write access"
3 Config storage: DB-only, Settings UI as the entry point Operator's product principle — "no one wants to configure yamls; this is a finished product, not a project" (memory: project_product_not_project.md)
4 Settings shape: dedicated /admin/* route group with hard route gate Per-app product surface for admin actions; redirect at layout-level, never load admin content for non-admin (operator's instruction)
5 Search UX: standalone /discover route (option B), not inline-in-search Operator preference — explicit "I want to add music" surface, separate from local-library search
6 Granularity: artist + album + track Operator preference — track-kind resolves to album-kind under the hood (Lidarr's monitor unit is album), explicit modal disclosure rather than silent expansion
7 Lifecycle detection: library scan as source of truth Reuses existing scanner; reconciler is a 5-min worker; webhook is a clean follow-up if latency matters
8 Quality profile / root folder: default + per-add override Default covers >90% of approvals; override is the escape hatch. Modal is collapsed-by-default
9 Approve fires Lidarr synchronously, reconcile asynchronously (Approach 1) Admin gets immediate "Lidarr accepted/rejected" feedback; reconciliation has to be async because downloads take minutes-to-hours
10 New RequireAdmin middleware on /api/admin/* route group Centralized auth check; SPA route gate is UX, server middleware is the security boundary
11 UI lands at FabledSword design system bar (memory: project_design_system.md) Operator's quality bar — "no more scaffolding-feel UI" (memory: project_ui_quality.md); accent only for brand-moments, Moss/Bronze/Oxblood for actions
12 Track-kind disclosure modal Lidarr can't fetch a single track without its album; explicit "this will add the album" beats silently expanding the request
13 lidarr_requests.failed status reserved but not transitioned in this slice Foreshadows a "stalled approval" timeout follow-up; not v1

10. Out of scope (this slice)

Tracked in the M5 milestone for later sub-plans:

  • Quarantine workflowlidarr_quarantine table, soft-hide on tracks, admin "delete via Lidarr" path, /admin/quarantine page. → M5b.
  • Radio suggested-additions/api/radio response includes a separate field for out-of-library MBIDs from track_similarity; SPA shows "Would you add these?" affordance with inline request submission. → M5c.
  • Lidarr webhook ingestion — push notifications on download complete; near-real-time status updates on /requests. → cheap follow-up after M5c.
  • Failed-request timeout — reconciler transitions long-stuck approved requests to failed with operator-facing diagnostics. → operational tightening; not v1.
  • Admin requestor notifications — toast/badge when a user logs in if their request was approved/completed/rejected. → polish, slot into Fable #349 (UI polish pass) or earlier if it becomes friction.

11. Open questions

  • Album cover art proxying — Lidarr returns image URLs that point at MusicBrainz/Cover Art Archive. The SPA could fetch directly (extra origins, CORS), or Minstrel could proxy them through /api/cover-art?lidarr=.... Decision deferred to plan time — start with direct fetch, add proxy if CORS bites.
  • Search debounce / cache — Lidarr's lookup endpoint is the rate-limit-sensitive one. SPA debounce of 250ms + 60s server-side LRU cache on (query, kind) is the conservative starting point. Tunable.
  • failed status promotion — at what time threshold does approvedfailed? Suggest 7 days, but no logic for it ships in this slice.