diff --git a/ROADMAP.md b/ROADMAP.md index 811a383..8e3f1e4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -75,7 +75,7 @@ fold but appear nowhere. `shelf.entry` for acquired/sold events; no UI captures them. The event log was designed for exactly this history ("bought 2019 for X, sold 2022 for Y"). -- **Wantlist import (done 2026-07):** `POST /api/discogs/import-wantlist` +- **Wantlist import (done 2026-07):** `discogs.importWantlist` runs the shared import pipeline with `action: "wanted"` (same cap, dedup, enrichment, and failure accounting); IMPORT WANTLIST button on the Discogs card. @@ -139,19 +139,20 @@ architecture track, not a product one. ## Decisions made -| Decision | Why | -| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Extract a generic `atproto` package** (XRPC, identity, auth, repo CRUD) | The generic plumbing is ~80% of a Gleam atproto SDK and none existed; isolating it keeps the app thin and the lib publishable. | -| **Transport-agnostic client** (`xrpc.Client` wraps a `send` fn; server injects `httpc`) | No HTTP dependency in the core, so it compiles on both targets and a JS caller could supply `fetch`. | -| **Generic `repo` CRUD** (collection NSID + row decoder, no lexicon knowledge) | Keeps `atproto` free of `crate` types; the shelf-specific decoder lives in the server. | -| **Slingshot for identity resolution** (`com.bad-example.identity.resolveMiniDoc`) | One call returns did+handle+pds, replacing the appview + `plc.directory` two-hop. | -| **MVU module split of the web app** | A 426-line single file became `model`/`msg`/`update`/`effects`/`view` + thin `main`. | -| **Docker** (multi-stage, `gleam export erlang-shipment`, `erlang:29-slim`, binds `0.0.0.0`) + **Makefile** | Single-service deploy (storage is the PDS, no DB). `0.0.0.0` bind is required or the container is unreachable. | -| **codegen scalar sweep** (`cid-link`->`String`, `bytes`->`BitArray`, `unknown`->`Dynamic` via `internal` helpers) | Removed the last skip (`catalog.edit`). `unknown` round-trips through a `dynamic_to_json` passthrough. | -| **External IDs as a repeatable `externalId` ref** (`provider`+`id`+`url`), replacing scalar `discogsReleaseId`/`mbid` | Many providers per item (Discogs, MusicBrainz, ...) without a `V2` to add one; relaxes `catalog.release.required` to `[title, createdAt]`. | -| **Finalised `required[]` + string limits** while pre-adoption (single-user) | "Required is forever"; every free-text string now carries `maxGraphemes`/`maxLength` (~10:1). The window to do this closes once others adopt the lexicons. | -| **Entry identity = genesis TID + `subject` strongRef** (replaced the client-minted `entryId`; BC break pre-adoption) | Stable linkable at-uris per entry (Constellation backlinks for free), no minted id to thread through, appends validate their genesis via `getRecord`. | -| **Failure posture: best-effort for reads, accounted for writes** (decided 2026-07) | Display reads (handle chip, avatar) may silently degrade. Writes that create immutable records must surface failures (counts in run results, user notices) or fail the item; 429s always propagate to run-level rate-limit handling, never swallowed. | +| Decision | Why | +| --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Extract a generic `atproto` package** (XRPC, identity, auth, repo CRUD) | The generic plumbing is ~80% of a Gleam atproto SDK and none existed; isolating it keeps the app thin and the lib publishable. | +| **Transport-agnostic client** (`xrpc.Client` wraps a `send` fn; server injects `httpc`) | No HTTP dependency in the core, so it compiles on both targets and a JS caller could supply `fetch`. | +| **Generic `repo` CRUD** (collection NSID + row decoder, no lexicon knowledge) | Keeps `atproto` free of `crate` types; the shelf-specific decoder lives in the server. | +| **Slingshot for identity resolution** (`com.bad-example.identity.resolveMiniDoc`) | One call returns did+handle+pds, replacing the appview + `plc.directory` two-hop. | +| **MVU module split of the web app** | A 426-line single file became `model`/`msg`/`update`/`effects`/`view` + thin `main`. | +| **Docker** (multi-stage, `gleam export erlang-shipment`, `erlang:29-slim`, binds `0.0.0.0`) + **Makefile** | Single-service deploy (storage is the PDS, no DB). `0.0.0.0` bind is required or the container is unreachable. | +| **codegen scalar sweep** (`cid-link`->`String`, `bytes`->`BitArray`, `unknown`->`Dynamic` via `internal` helpers) | Removed the last skip (`catalog.edit`). `unknown` round-trips through a `dynamic_to_json` passthrough. | +| **External IDs as a repeatable `externalId` ref** (`provider`+`id`+`url`), replacing scalar `discogsReleaseId`/`mbid` | Many providers per item (Discogs, MusicBrainz, ...) without a `V2` to add one; relaxes `catalog.release.required` to `[title, createdAt]`. | +| **Finalised `required[]` + string limits** while pre-adoption (single-user) | "Required is forever"; every free-text string now carries `maxGraphemes`/`maxLength` (~10:1). The window to do this closes once others adopt the lexicons. | +| **Entry identity = genesis TID + `subject` strongRef** (replaced the client-minted `entryId`; BC break pre-adoption) | Stable linkable at-uris per entry (Constellation backlinks for free), no minted id to thread through, appends validate their genesis via `getRecord`. | +| **Failure posture: best-effort for reads, accounted for writes** (decided 2026-07) | Display reads (handle chip, avatar) may silently degrade. Writes that create immutable records must surface failures (counts in run results, user notices) or fail the item; 429s always propagate to run-level rate-limit handling, never swallowed. | +| **API surface is XRPC** (lexicon-defined query/procedure methods under `dev.mokkenstorm.crate.*`, decided 2026-07) | Consistent with the rest of the stack (everything is lexicon-defined); the index reads become a real appview surface others can consume; method lexicons feed the planned typed-client codegen. GraphQL (e.g. quickslice's) stays internal plumbing if ever used. OAuth redirect flows stay plain routes. | ## Deferred / planned diff --git a/docs/catalog-authority.md b/docs/catalog-authority.md index b9be60b..996b6d2 100644 --- a/docs/catalog-authority.md +++ b/docs/catalog-authority.md @@ -96,7 +96,7 @@ the proposal format anyone can publish against a `subject` catalogRef: ## Serving / display (minimal version built 2026-07) -Built: the per-entry endpoint (`GET /api/shelf/:entryId`) resolves the +Built: the per-entry endpoint (`shelf.getEntry`) resolves the entry's `release` ref via the catalog `Deps` port and returns a `release` object (`artistDisplay`, `genres`, `styles`, `country`, `released`) that the record-detail page renders as chips and detail rows. Best-effort read: diff --git a/docs/catalog-browse.md b/docs/catalog-browse.md index 2ae3180..196ac7c 100644 --- a/docs/catalog-browse.md +++ b/docs/catalog-browse.md @@ -46,10 +46,10 @@ theorizing: Built as sketched: a `known_users` store (memory + Postgres, `did`/ `handle`/`pds` upserted at OAuth-callback time; storing the pds at login -means browse never resolves identities), and `GET /api/browse` fanning out +means browse never resolves identities), and `catalog.listReleases` fanning out public `listRecords` per known user, deduping by discogs external id, with `owned`/`wanted` badges computed against the viewer's crate fold. -`POST /api/browse/add` is the want-it/have-it write: verify-refetch the +`catalog.adoptRelease` is the want-it/have-it write: verify-refetch the release, snapshot from the canonical record, foreign cover bytes copied into the adopter's repo, genesis with `origin: "adoption"` and a cid-pinned ref. @@ -78,7 +78,10 @@ collections, CAR-file backfill for pre-existing records, and a generated GraphQL query API over SQLite/Postgres. It removes the write-the-consumer work but not the run-it-forever obligation: it is self-hosted software, so the liveness contract in point 2 below still has to be signed by a real -deployment. The other microcosm services stay complementary rather than +deployment. Decided 2026-07: the read API stays lexicon-defined XRPC +(`catalog.listReleases`) regardless of the index behind it; if quickslice +is used, its GraphQL is internal plumbing the handler queries server-side, +never a browser-facing surface. The other microcosm services stay complementary rather than substitutes: Constellation is backlink-shaped (adoption counts, not collection enumeration) and [UFOs](https://ufos.microcosm.blue/) exposes per-NSID stats and sample records (useful for noticing foreign `crate.*` @@ -121,7 +124,7 @@ the ref is already known from the index: - **Want it** → genesis with `action: "wanted"`. - **I have this** → genesis with `action: "acquired"`. -Built as sketched: `POST /api/browse/add` verify-refetches the ref before +Built as sketched: `catalog.adoptRelease` verify-refetches the ref before writing (the fan-out can be briefly stale), snapshots from the canonical record, copies foreign cover bytes into the adopter's repo via the shared `covers.copy_cover_by_uri`, and writes the genesis with diff --git a/docs/shelf-events.md b/docs/shelf-events.md index 228ee2a..242cdde 100644 --- a/docs/shelf-events.md +++ b/docs/shelf-events.md @@ -127,22 +127,25 @@ Shows in "history" / "previously owned", not in the current crate. - No `deleted` flag (the mutable soft-delete is gone; `sold`/`dropped` are the soft removal; `deleteRecord` is the hard one). -## API (BFF) +## API (BFF, XRPC methods under `dev.mokkenstorm.crate.`) + +Since 2026-07 the API surface is lexicon-defined XRPC (`/xrpc/`, +queries GET, procedures POST; method lexicons live in `lexicons/`). Reads (the BFF folds): -- `GET /api/shelf` — current owned entries (folded). `?view=wanted|history|all`. -- `GET /api/shelf/:entryId` — one entry's full timeline. -- `GET /api/feed` — raw event stream (yours now; others' later). _(future)_ +- `shelf.listEntries` — current owned entries (folded). `?view=wanted|history|all`. +- `shelf.getEntry?entry=` — one entry's full timeline. +- a feed method for the raw event stream (yours now; others' later). _(future)_ Writes (each appends one immutable event; nothing mutates): -- `POST /api/shelf` — genesis event (`action: wanted|acquired`); its TID becomes the entry id. -- `POST /api/shelf/:entryId` — append an event (`action` + payload in body). -- `DELETE /api/shelf/:entryId` — purge: `deleteRecord` all events for the entry. +- `shelf.addEntry` — genesis event (`action: wanted|acquired`); its TID becomes the entry id. +- `shelf.appendEvent` — append an event (`entryId` + `action` + payload in body). +- `shelf.purgeEntry` — purge: `deleteRecord` all events for the entry. -No `PUT`. "Edit" = append a corrective event. "Remove from crate" = a `sold` / -`dropped` event. +No update method. "Edit" = append a corrective event. "Remove from crate" = a +`sold` / `dropped` event. ## Migration from the former mutable `shelf.item`