diff --git a/ROADMAP.md b/ROADMAP.md index 2467b95..2b2d91f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,10 +11,23 @@ scan-to-import, adopt-or-mint catalog promotion with `via @handle` attribution, and the amend flow (own records superseded, foreign ones get `basedOn` + a `catalog.edit` proposal). -Three axes from here. Cross-axis order: design catch-up and the UX-hole -audit first, then data-model completeness, then catalog depth. +Split by horizon from here. **MVP** is the polished single-user crate: +one person maintains their collection end to end, plus interop reads over +their own repo. **v1** is the network layer on top: cross-user reads, +adoption, and the collaborative edit loop. **v2** is the scale work; its +items are event-triggered rather than sequence-triggered, so each keeps +its trigger inline instead of waiting on v1 completing. The engineering +sections further down (decisions, deferred plans) stay +horizon-independent, and the [BFF-thinning +work](./docs/bff-architecture.md) runs alongside as an architecture +track, not a product one. -### Clean design +### MVP: the single-user crate (mostly shipped) + +Cross-axis order within the MVP: design catch-up and the UX-hole audit +first, then data-model completeness, then catalog depth. + +#### Clean design The direction is chosen and prototyped in Figma ("Zine × Greenhouse": parchment, bark ink, hard offset shadows; a design-system page with color, @@ -86,15 +99,13 @@ tokens but not yet the full system. for text on colored fills. A manual theme toggle stays open: it needs a designed home first. -### Complete data model +#### Complete data model -The lexicons are ahead of the app: several fields exist on `shelf.entry` and -`catalog.release` that nothing writes or shows. Concretely, as of today: -`sleeveGrade` is absent even from the web `Entry` type; `notes` and `folder` -render when present but nothing can set them (the server's `annotated` and -`moved` reducer actions have no UI that dispatches them); `price`, -`counterparty`, and `acquiredAt` are wired end to end through lexicon and -fold but appear nowhere. +The lexicons are ahead of the app in a shrinking number of places. Still +unwritable as of today: `notes` renders when present but no UI dispatches +the `annotated` action; `folder` is settable at genesis but the `moved` +action has no UI; sale-side `price`/`counterparty` on removal are +uncaptured; backdated acquisitions await the `acquiredAt` decision below. - **Widen the add/edit surface to the lexicon (done 2026-07, one correction):** `sleeveGrade`, `rating`, `folder`, `price`, and @@ -138,14 +149,13 @@ fold but appear nowhere. the (single, existing) per-release Discogs fetch: artists minted with own-repo dedup and wired into `creditedArtists`; genres minted as vocabulary from genre/style strings; MBIDs on artists done 2026-07 (see - the MusicBrainz bullet above). Still open: cross-user artist - adoption via Constellation, genre externalIds (MB's genre list is the - natural target), any read path, and - graduation past PoC (which means coordinating with teal.fm and similar - first; "required is forever" cuts both ways). Master and label entities - remain unbuilt (reserved in `catalog.edit`). + the MusicBrainz bullet above). Still open MVP-side: genre externalIds + (MB's genre list is the natural target) and any read path. The + cross-user halves (artist adoption via Constellation, graduation past + PoC) live in v1 below. Master and label entities remain unbuilt + (reserved in `catalog.edit`). -### More complete catalog +#### Catalog depth - **Enrich minted releases (mostly done 2026-07).** Minted releases now carry `artistDisplay`, `country`, `released`, `genres`, `styles`, @@ -153,15 +163,34 @@ fold but appear nowhere. per-release Discogs fetch that was already made for cover art. Still stubs: `tracklist`, `labels`, `master`. - **Barcodes as identifiers (capture done 2026-07):** scanned/imported - barcodes land in `catalog.release.identifiers`. The payoff half is - open: making the scan flow match the network catalog first and Discogs - second, which compounds as the catalog grows. -- **Browsable catalog (v1 built 2026-07)** ([spec](./docs/catalog-browse.md)): - `/browse` grid over at-record's known users (fan-out of public - `listRecords`, dedup by Discogs id, owned/wanted badges), want-it / - have-it as one-tap adoption writes. v2 (Jetstream-filtered indexer, - likely quickslice) waits for its trigger: more publishers or a slow - fan-out. + barcodes land in `catalog.release.identifiers`. The payoff half + (network-first scan matching) is v1 below, where the network exists. +- **Universal scanner:** Firefox/Safari barcode scanning via the ZXing + ponyfill; plan and asset plumbing are specced in the scan-to-import + section below. Sequenced after the design/data-model work. + +#### Per-user music-app interop + +Decided 2026-07-11 (full recon in Cross-user / discovery infrastructure +below): interop that only reads the signed-in user's own repo is MVP-side; +anything needing other users' repos is not. + +- **Import-from-teal:** `releaseMbId`, exact, MB-to-Discogs via URL + relationships. +- **Import-from-rocksky:** name-based via the scan flow's similarity + scorer, review-first. +- **Own-listening display** on record detail. + +### v1: the network layer + +Cross-user reads and the collaborative catalog loop. (Naming note: the +browse spec's internal "v1"/"v2" labels predate this roadmap split and are +feature-local.) + +- **Browsable catalog (fan-out version built 2026-07)** + ([spec](./docs/catalog-browse.md)): `/browse` grid over at-record's + known users (fan-out of public `listRecords`, dedup by Discogs id, + owned/wanted badges), want-it / have-it as one-tap adoption writes. - **Adoption counts** via Constellation backlinks: "N people have this", browse sorting, and the answer to the open canonical-selection decision. - **`catalog.edit` inbox:** amending a foreign record files a proposal @@ -170,12 +199,33 @@ fold but appear nowhere. `supersedes`). - **Dedup in browse:** group near-duplicate releases by shared `externalIds` before real canonical convergence exists. - -Longer arc, unchanged: two-way Discogs sync -([spec](./docs/discogs-sync.md)) -> cross-user discovery and social feed -> -native wrap when scan justifies it (sections below). The -[BFF-thinning work](./docs/bff-architecture.md) runs alongside as an -architecture track, not a product one. +- **Network-first scan matching:** the barcode-capture payoff; the scan + flow matches the network catalog first and Discogs second, which + compounds as the catalog grows. +- **Cross-user artist adoption** via Constellation backlinks on the + external-id URL (the same discover-verify-adopt shape release promotion + uses); today artists are own-repo only, so two users minting the same + artist do not converge. +- **Entity graduation past PoC:** promoting `catalog.artist`/`genre` + beyond our namespace-private PoC means coordinating with teal.fm, + rocksky, and similar first; "required is forever" cuts both ways. + +### v2: triggered scale work + +Each item waits on its stated trigger, not on v1 completing. + +- **Browse indexer** (the browse spec's "v2"): Jetstream-filtered indexer, + likely quickslice. Trigger: more publishers or a slow fan-out. +- **Cross-user music-app joins** ("who on rocksky listens to this"): a + multi-lexicon indexer, since Constellation joins at-uris, not string + fields. Same trigger as the browse indexer (decided 2026-07-11). +- **Two-way Discogs sync** ([spec](./docs/discogs-sync.md)). +- **Cross-user discovery and social feed**, downstream of the browse and + adoption layers. +- **Native wrap** (Capacitor; full reasoning in its section below). + Trigger: scan usage justifying the app-store operational cost. +- **Photo-mode scanning.** Blocker rather than trigger: Discogs has no + image-match API, so this waits for an image-recognition backend. ## Decisions made diff --git a/docs/catalog-entities.md b/docs/catalog-entities.md index 3664887..75316f2 100644 --- a/docs/catalog-entities.md +++ b/docs/catalog-entities.md @@ -1,6 +1,7 @@ # Spec: artist + genre entities (proof of concept) -Status: **PoC, Discogs-fed** (lexicons + write path built 2026-07). Every +Status: **PoC, Discogs-fed** (lexicons + write path built 2026-07; +artist MBIDs with dedup-hit backfill added 2026-07). Every mint path (import, scan add, manual add with a Discogs id) now widens the existing per-release Discogs fetch into a `ReleaseDetails` and, before minting the release: mints/reuses `catalog.artist` records (own-repo dedup @@ -63,15 +64,26 @@ the pure, tested `distinct_missing`) and threads through `promotion.adopt_or_mint`, which now returns an `Outcome` carrying the grown dedup dicts and a `MintReport`. +Also built (2026-07): **MBIDs on artists**. The release mint's existing +MusicBrainz lookup returns the release's artist credits alongside the +release id (the barcode search carries `artist-credit` by default; the +Discogs-URL fallback adds `inc=artist-credits` to the same request, so MB +call volume is unchanged). An artist whose name matches a credit (exact +case-insensitive, else the `release_similarity` token score at a 90 +threshold) gets a `provider: "musicbrainz"` externalId: attached at mint +for new artists, backfilled via putRecord for dedup hits that still lack +one. The backfill is best-effort: a non-429 failure is log-only (the +record stays complete and the next credit retries it), while a 429 stops +the run like any other entity write. + Still open: 1. **Cross-user artist adoption** via Constellation backlinks on the external-id URL (the same discover-verify-adopt shape release promotion uses); today artists are own-repo only, so two users minting the same artist do not converge. -2. **MBIDs on artists**: only Discogs ids are captured; MusicBrainz ids - (the actual cross-app key, see above) need a Discogs-to-MB mapping - pass, which pairs with the MBID roadmap item for releases. +2. **Genre externalIds**: genres mint with no external ids; MusicBrainz's + genre list is the natural target. 3. **Genre migration to refs**: releases keep genres as plain strings; migrating to genre refs is a breaking change only worth doing with cross-app agreement.