Skip to content

plans: cross-cache-identity architecture pivot decision record#800

Merged
jakebromberg merged 1 commit into
mainfrom
plans/cross-cache-identity-architecture-pivot
May 10, 2026
Merged

plans: cross-cache-identity architecture pivot decision record#800
jakebromberg merged 1 commit into
mainfrom
plans/cross-cache-identity-architecture-pivot

Conversation

@jakebromberg
Copy link
Copy Markdown
Member

Summary

Decision record capturing the architectural reasoning behind pivoting cross-cache-identity §4 step 2. The pivot moves Backend out of the metadata-resolution business: LML composes identity end-to-end via a new bulk-resolve endpoint; Backend stores LML's verdict locally for read performance via HTTP only, no cross-DB reads.

This PR is the decision record itself. It does not execute the pivot. Wave 1 (closures + reframes) follows in separate PRs/issue actions linked from this doc.

Why now

While building sub-PR 2.1 (the S2 LML reader), two findings made the cost of the original architecture concrete:

  1. The 2.2 spike (plans: §4 sub-PR 2.2 spike memo (discogs-cache match score shape) #794) found that flowsheet_match and fuzzy_resolved aren't part of LML's discogs-cache schema — Backend's own scripts materialize them inside LML's PG. So Backend wasn't reading LML's data; it was reading data Backend's scripts wrote into LML's PG.
  2. The S2 design pivot found that LML's entity.reconciliation_log already carries per-row (method, confidence). The original "blanket alias_match 0.85" interim was unnecessary — but reaching this conclusion required Backend to read yet another LML schema (reconciliation_log) directly.

Each new source leg added another LML schema Backend depended on. The architectural debt was compounding fast enough that catching it pre-2.1-merge avoids ~1500 LOC of unwritten work that would have to be undone later.

What the doc covers

Action sequence after this lands

This doc is a precondition for Wave 1. Once it's reviewable:

Each wave's specific actions are listed in the doc.

Review notes

This is a planning artifact, not code. Looking for:

  • Architectural agreement: do you buy "BS = cache, LML = resolution authority, HTTP-only"?
  • Phase ordering: is 1→2→3 (contract → LML impl → BS consumer) the right sequence?
  • Any phase mis-categorized as "later" that should be earlier?
  • Any of the four "Open decisions for execution" you want to lock now vs. resolve during Wave 2?
  • Any reversibility concern I underweighted?

If everything looks right, merging this as-is gives Wave 1 a public URL to cite when closing #797 et al.

Refs #663

Captures the architectural reasoning + 4-wave action sequence before
any artifacts are closed or filed. The pivot moves Backend out of the
metadata-resolution business: LML composes identity end-to-end,
Backend stores LML's verdict via HTTP only.

Sections:
  - Why a pivot (the muddled current state, the two findings during
    sub-PR 2.1 design that made it concrete)
  - What we're going for (Backend = catalog + cache; LML = sole
    resolution authority; HTTP-only contract surface)
  - Backend read performance under the pivot (cache contents stay,
    only the population mechanism changes)
  - What changes in LML (new bulk-resolve endpoint; composition rules
    in Python; release-level matching ports over)
  - Migration phases 0-7
  - Costs and benefits
  - What we keep vs throw away
  - Action plan with parallelization (Wave 1: gh state changes; Wave
    2: new tickets; Wave 3: wiki amendment; Wave 4: close out)
  - Open decisions for execution
  - Reversibility analysis
  - Success criteria

This is a decision record, not yet an implementation. Once Wave 4
lands the pivot is in motion across BS/LML/wxyc-shared/wiki.
@jakebromberg jakebromberg merged commit 126ab34 into main May 10, 2026
5 checks passed
@jakebromberg jakebromberg deleted the plans/cross-cache-identity-architecture-pivot branch May 10, 2026 03:47
jakebromberg added a commit that referenced this pull request May 10, 2026
The pre-pivot plan committed Backend to a five-source backfill (S1=canonical_entity_id, S2=LML entity.identity, S3=flowsheet_match, S4=fuzzy_resolved, S5=semantic-index reconciliation_log). On 2026-05-09 the project pivoted: Backend stops reading LML's PG and consumes a new POST /api/v1/identity/bulk-resolve-libraries endpoint instead.

Replaces the 277-line draft plan with a stub redirecting to the decision record (#800), the wiki amendment (WXYC/wiki#25), and the new tickets (wxyc-shared#103, library-metadata-lookup#272, #802). Captures what survives (substrate from #790, S1 self-migration leg, 2.2 spike memo) and what's discarded (dispatch.ts, lml-provenance-index.ts, resolve-s2.ts).

Wave 4 of the cross-cache-identity architecture pivot.
jakebromberg added a commit to WXYC/wxyc-shared that referenced this pull request May 10, 2026
…identity pivot

Per the cross-cache-identity architecture pivot (WXYC/Backend-Service#800, 2026-05-09), Backend stops composing cross-cache identity from multiple PG schemas; LML becomes the sole composer. This commit locks the HTTP contract for the new seam: POST /api/v1/identity/bulk-resolve-libraries.

Adds 7 schemas (BulkResolveLibrariesRequest, BulkResolveInput, BulkResolveResultKind, BulkResolveProvenanceEntry, BulkResolveTrackIdentity, BulkResolveResult, BulkResolveLibrariesResponse), 1 endpoint, 1 security scheme (LMLBearerAuth, distinct from BearerAuth which is Better Auth JWT). Reuses ReconciledIdentity, IdentitySource, IdentityMethod, CacheStats, ApiErrorResponse for cross-spec consistency.

Request shape is uniform per #103: {library_id, artist_name, album_title}. LML auto-detects V/A from library_id and returns a kind discriminator on the response (single_artist | compilation | unresolved). Provenance always returned (empty array if no source resolved); no optional knobs. Compilation results carry per-track identity in tracks[]; full V/A matcher behaviour ships under WXYC/library-metadata-lookup#271.

info.version bumped 1.1.0 to 1.2.0. No breaking changes (additions only).
jakebromberg added a commit that referenced this pull request May 11, 2026
Implements BS#802 under the post-#800 cross-cache-identity pivot: Backend is now a thin writer; LML is the sole composer of cross-cache identity. The new `jobs/library-identity-consumer/` package consumes LML's `POST /api/v1/identity/bulk-resolve-libraries` endpoint (api.yaml v1.2.0, wxyc-shared#104; deployed via LML#272 / PR #273) and UPSERTs the verdicts into `library_identity` + `library_identity_source` atomically per library_id.

The SELECT predicate picks libraries needing identity refresh: `library.canonical_entity_id IS NOT NULL OR library.id IN (SELECT library_id FROM library_identity WHERE last_verified_at < NOW() - interval '7 days')`. Note: BS#802's body wrote `last_refreshed_at`, but the actual column on `library_identity` is `last_verified_at`; the code uses the real column name.

Batches up to 500 inputs per LML call (LML caps at 1000). For each `BulkResolveResult`: `single_artist` → write per-source rows + main row in `db.transaction()`; `unresolved` → counted, no write; `compilation` → counted as `rows_skipped { compilation }` and deferred to BS#801 (per-track identity writes for V/A rows). A batch-level LML error counts every input as `rows_skipped { lml_error }` and continues — the next run re-picks failed rows via the SELECT predicate, so retry is free.

Sentry-traced metrics (`rows_resolved`, `rows_unresolved`, `rows_skipped`, `lml_total_calls`, `lml_total_latency_ms`) land as attributes on a top-level `library-identity-consumer.run` span. The per-batch LML POST is wrapped in `lml.bulk_resolve_libraries` (`http.client`); LML's `cache_stats` projects onto the same span as `lml.cache.*` attributes (LML#229 pattern). DRY_RUN still calls LML so the resolve/unresolved/error counts are honest predictions; only DB writes are suppressed, and a locked-schema JSON object is emitted on stdout.

Deletes the predecessor `jobs/library-identity-backfill/` and `Dockerfile.library-identity-backfill` in the same change (BS#802 acceptance). The deploy-base.yml matrix discovers targets dynamically via `jobs/$TARGET_APP/package.json`'s `job-type` field, so registering the new one-shot target requires no workflow edit. CLAUDE.md and docs/env-vars.md are updated to reflect the rename and the new env vars (`LIBRARY_METADATA_URL`, `LML_API_KEY`, `STALE_THRESHOLD_DAYS`).

Known scope cuts called out for follow-up: `library_identity` has no main-row destinations for `discogs_artist_id`, `musicbrainz_artist_id`, or `bandcamp_id` from LML's `ReconciledIdentity` payload — those values flow through `library_identity_source.external_id` (text) via provenance rows so no data is dropped, but the main row is a partial denormalised view until a follow-up migration adds artist-id columns (deliberately out of scope here). Compilation handling is BS#801's scope.

Test coverage (34 tests across select / writer / orchestrate): SELECT predicate honors the post-#800 disjunction and env var validation; writer is transactional with per-source-then-main ordering and null-confidence skipping; orchestrator dispatches by kind, counts LML and writer errors without aborting the run, paginates by id-cursor, and emits the locked DRY_RUN JSON schema.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

§4 step 2 sub-PR 2.2a — S3 discogs-cache flowsheet_match reader

1 participant