feat: enhance discovery, vibe sessions, and library enrichment
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled

This commit is contained in:
kami
2026-08-01 14:40:48 +04:00
parent a0c9f42a89
commit 4c48d11e9d
54 changed files with 4136 additions and 521 deletions
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { MIGRATIONS } from './migrations.js';
describe('track release-date migration', () => {
const migration = MIGRATIONS.find(
({ id }) => id === '20260801_track_release_dates_from_albums',
);
it('backfills from albums with a null-safe, deterministic conflict rule', () => {
expect(migration).toBeDefined();
expect(migration!.sql).toContain('ALTER TABLE albums ADD COLUMN IF NOT EXISTS release_date DATE');
expect(migration!.sql).toContain('UPDATE tracks AS t');
expect(migration!.sql).toContain('SET release_date = al.release_date');
expect(migration!.sql).toContain('t.release_date IS DISTINCT FROM al.release_date');
});
it('keeps new tracks and album metadata updates synchronized', () => {
expect(migration!.sql).toContain('BEFORE INSERT OR UPDATE OF album_id, release_date ON tracks');
expect(migration!.sql).toContain('AFTER UPDATE OF release_date ON albums');
expect(migration!.sql).toContain('WHEN (OLD.release_date IS DISTINCT FROM NEW.release_date)');
});
});
+134
View File
@@ -510,4 +510,138 @@ export const MIGRATIONS: Migration[] = [
ON pending_file_deletions(requested_at);
`,
},
{
// System E originally wrote graph_exploration claims without registering it
// in source_trust, so the FK rejected every graph walk. It also lacked a
// durable link from an acquired candidate to the track the scanner created.
id: '20260801_acquisition_pipeline',
sql: `
INSERT INTO source_trust (key, trust, description)
VALUES ('graph_exploration', 0.40,
'Muzick graph traversal provenance for discovery candidates.')
ON CONFLICT (key) DO NOTHING;
ALTER TABLE discovery_candidates
ADD COLUMN IF NOT EXISTS acquired_track_id UUID
REFERENCES tracks(id) ON DELETE SET NULL;
ALTER TABLE discovery_candidates
ADD COLUMN IF NOT EXISTS acquired_at TIMESTAMPTZ;
ALTER TABLE discovery_candidates
ADD COLUMN IF NOT EXISTS acquisition_attempts INTEGER NOT NULL DEFAULT 0;
ALTER TABLE discovery_candidates
ADD COLUMN IF NOT EXISTS last_error TEXT;
CREATE INDEX IF NOT EXISTS idx_discovery_candidates_status
ON discovery_candidates (status, first_seen_at);
`,
},
{
// Separate portrait fetching from structural metadata so an operator can
// re-enrich missing artist images without implicitly enabling MB writes.
id: '20260801_artist_image_enrichment_toggle',
sql: `
INSERT INTO settings (key, value)
VALUES ('enrich_artist_images', 'false')
ON CONFLICT (key) DO NOTHING;
`,
},
{
// Audio analysis v1 stored an arbitrary linear energy scale, which made
// nearly every mastered track look maximally energetic. Track the analysis
// contract and source hash so bounded background jobs can safely refresh
// stale results without repeatedly decoding unchanged files.
id: '20260801_audio_analysis_v2',
sql: `
ALTER TABLE track_audio_features
ADD COLUMN IF NOT EXISTS analysis_version SMALLINT NOT NULL DEFAULT 0;
ALTER TABLE track_audio_features
ADD COLUMN IF NOT EXISTS source_hash TEXT;
ALTER TABLE track_audio_features
ADD COLUMN IF NOT EXISTS analyzed_at TIMESTAMPTZ;
DO $mig$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'track_audio_features'::regclass
AND conname = 'track_audio_features_bpm_range'
) THEN
ALTER TABLE track_audio_features
ADD CONSTRAINT track_audio_features_bpm_range
CHECK (bpm IS NULL OR (bpm >= 30 AND bpm <= 300)) NOT VALID;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'track_audio_features'::regclass
AND conname = 'track_audio_features_energy_range'
) THEN
ALTER TABLE track_audio_features
ADD CONSTRAINT track_audio_features_energy_range
CHECK (energy IS NULL OR (energy >= 0 AND energy <= 1)) NOT VALID;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conrelid = 'track_audio_features'::regclass
AND conname = 'track_audio_features_danceability_range'
) THEN
ALTER TABLE track_audio_features
ADD CONSTRAINT track_audio_features_danceability_range
CHECK (danceability IS NULL OR (danceability >= 0 AND danceability <= 1)) NOT VALID;
END IF;
END $mig$;
`,
},
{
// tracks.release_date is a denormalised copy of the canonical
// MusicBrainz release-group date on albums. Before this migration the
// worker populated albums only, leaving every historical track invisible
// to the novelty generator. The backfill and triggers make the canonical
// album value win deterministically for both old and future rows.
id: '20260801_track_release_dates_from_albums',
sql: `
-- The worker used to create this column lazily, but migration execution
-- must not rely on worker startup order.
ALTER TABLE albums ADD COLUMN IF NOT EXISTS release_date DATE;
-- Backfill only mismatches. IS DISTINCT FROM is null-safe and makes the
-- statement safe to rerun manually for diagnostics/recovery.
UPDATE tracks AS t
SET release_date = al.release_date
FROM albums AS al
WHERE t.album_id = al.id
AND t.release_date IS DISTINCT FROM al.release_date;
CREATE OR REPLACE FUNCTION sync_track_release_date_from_album()
RETURNS TRIGGER AS $mig$
BEGIN
SELECT release_date INTO NEW.release_date
FROM albums
WHERE id = NEW.album_id;
RETURN NEW;
END;
$mig$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_tracks_sync_release_date ON tracks;
CREATE TRIGGER trg_tracks_sync_release_date
BEFORE INSERT OR UPDATE OF album_id, release_date ON tracks
FOR EACH ROW EXECUTE FUNCTION sync_track_release_date_from_album();
CREATE OR REPLACE FUNCTION propagate_album_release_date_to_tracks()
RETURNS TRIGGER AS $mig$
BEGIN
UPDATE tracks
SET release_date = NEW.release_date
WHERE album_id = NEW.id
AND release_date IS DISTINCT FROM NEW.release_date;
RETURN NEW;
END;
$mig$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_albums_propagate_release_date ON albums;
CREATE TRIGGER trg_albums_propagate_release_date
AFTER UPDATE OF release_date ON albums
FOR EACH ROW
WHEN (OLD.release_date IS DISTINCT FROM NEW.release_date)
EXECUTE FUNCTION propagate_album_release_date_to_tracks();
`,
},
];
+81 -1
View File
@@ -130,6 +130,17 @@ CREATE TABLE IF NOT EXISTS tracks (
-- Add normalized columns as generated columns for existing databases where the
-- CREATE TABLE IF NOT EXISTS above was a no-op (column didn't exist before).
-- `albums.release_date` was introduced after the original albums table. It
-- must exist before the track synchronisation trigger below is compiled.
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'albums' AND column_name = 'release_date'
) THEN
ALTER TABLE albums ADD COLUMN release_date DATE;
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
@@ -140,6 +151,49 @@ DO $$ BEGIN
END $$;
CREATE INDEX IF NOT EXISTS idx_tracks_release_date ON tracks (release_date) WHERE release_date IS NOT NULL;
-- An album's MusicBrainz first-release-date is the canonical date for every
-- track on that album. Keep the denormalised tracks.release_date column in
-- lockstep so date-based recommendation queries stay indexable and never have
-- to guess which of two conflicting values is authoritative.
--
-- The trigger intentionally also overwrites a direct tracks.release_date
-- update. There is no per-recording release-date provenance in this schema;
-- accepting an independent track value would silently make novelty results
-- depend on write order. A future per-recording metadata source needs its own
-- canonical/provenance column before changing this rule.
CREATE OR REPLACE FUNCTION sync_track_release_date_from_album()
RETURNS TRIGGER AS $$
BEGIN
SELECT release_date INTO NEW.release_date
FROM albums
WHERE id = NEW.album_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_tracks_sync_release_date ON tracks;
CREATE TRIGGER trg_tracks_sync_release_date
BEFORE INSERT OR UPDATE OF album_id, release_date ON tracks
FOR EACH ROW EXECUTE FUNCTION sync_track_release_date_from_album();
CREATE OR REPLACE FUNCTION propagate_album_release_date_to_tracks()
RETURNS TRIGGER AS $$
BEGIN
UPDATE tracks
SET release_date = NEW.release_date
WHERE album_id = NEW.id
AND release_date IS DISTINCT FROM NEW.release_date;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_albums_propagate_release_date ON albums;
CREATE TRIGGER trg_albums_propagate_release_date
AFTER UPDATE OF release_date ON albums
FOR EACH ROW
WHEN (OLD.release_date IS DISTINCT FROM NEW.release_date)
EXECUTE FUNCTION propagate_album_release_date_to_tracks();
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
@@ -306,12 +360,20 @@ CREATE TABLE IF NOT EXISTS track_audio_features (
key TEXT,
energy REAL,
danceability REAL,
-- Reserved legacy columns: Vibe readers tolerate these as NULL. The local
-- analyzer intentionally does not claim to infer them.
valence REAL,
acousticness REAL,
instrumentalness REAL,
liveness REAL,
valence_score REAL,
tempo REAL
tempo REAL,
analysis_version SMALLINT NOT NULL DEFAULT 0,
source_hash TEXT,
analyzed_at TIMESTAMPTZ,
CONSTRAINT track_audio_features_bpm_range CHECK (bpm IS NULL OR (bpm >= 30 AND bpm <= 300)),
CONSTRAINT track_audio_features_energy_range CHECK (energy IS NULL OR (energy >= 0 AND energy <= 1)),
CONSTRAINT track_audio_features_danceability_range CHECK (danceability IS NULL OR (danceability >= 0 AND danceability <= 1))
);
CREATE TABLE IF NOT EXISTS track_lyrics (
@@ -331,6 +393,10 @@ CREATE TABLE IF NOT EXISTS settings (
);
INSERT INTO settings (key, value) VALUES ('enrich_metadata', 'true') ON CONFLICT (key) DO NOTHING;
-- Artist portraits are independent from structural metadata. Keeping this
-- separate lets an operator re-fill artwork without re-running MusicBrainz
-- canonicalisation across the entire library.
INSERT INTO settings (key, value) VALUES ('enrich_artist_images', 'true') ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('enrich_cover_art', 'true') ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('enrich_genres', 'true') ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value) VALUES ('enrich_lyrics', 'true') ON CONFLICT (key) DO NOTHING;
@@ -355,6 +421,10 @@ INSERT INTO source_trust (key, trust, description) VALUES
('cover_art_archive', 0.85, 'Cover Art Archive, MB-backed.'),
('discogs', 0.75, 'Discogs release/artist credits.'),
('lastfm', 0.50, 'Last.fm tags + similar. Noisy; used as weak signal.'),
-- A first-party record of how a candidate was reached through the graph.
-- This is deliberately separate from Last.fm/MB: it describes the
-- traversal strategy, not a claim made by an external provider.
('graph_exploration', 0.40, 'Muzick graph traversal provenance for discovery candidates.'),
('listener_behavior', 0.40, 'Derived from observed play patterns. User-keyed.'),
('tag', 0.30, 'File-tag-derived via scanner heuristic. Lowest trust.')
ON CONFLICT (key) DO NOTHING;
@@ -487,9 +557,19 @@ CREATE TABLE IF NOT EXISTS discovery_candidates (
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_eval_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'candidate',
-- Filled only after a worker scanned a successfully acquired file. Keeping
-- this FK makes candidate -> local-track provenance auditable and avoids
-- guessing from filename metadata later.
acquired_track_id UUID REFERENCES tracks(id) ON DELETE SET NULL,
acquired_at TIMESTAMPTZ,
acquisition_attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
UNIQUE (source, external_id)
);
CREATE INDEX IF NOT EXISTS idx_discovery_candidates_status
ON discovery_candidates (status, first_seen_at);
-- Probation status for acquired tracks.
DO $$ BEGIN
IF NOT EXISTS (
+28
View File
@@ -16,6 +16,8 @@ export interface Album {
artist_id: string;
title: string;
year?: number | null;
/** Canonical MusicBrainz release-group first-release-date (YYYY-MM-DD). */
release_date?: string | null;
artwork_id?: string | null;
}
@@ -38,6 +40,8 @@ export interface Track {
skip_count: number;
dislike_count: number;
last_played_at?: Date | null;
/** Denormalised from albums.release_date by a database trigger. */
release_date?: string | null;
mtime?: number | null;
source_type: string;
artists?: TrackArtist[];
@@ -136,6 +140,30 @@ export interface ListenerBelief {
last_decayed_at: Date;
}
/**
* Stable pseudo-entity IDs for audio preference buckets. listener_beliefs uses
* UUID entity IDs for every entity type, while audio dimensions are values rather
* than rows in their own table. Keeping the IDs fixed makes these beliefs usable
* by the planner without introducing a second, unbounded vocabulary.
*/
export const AUDIO_PREFERENCE_BUCKETS = {
energy: {
low: '10000000-0000-0000-0000-000000000001',
medium: '10000000-0000-0000-0000-000000000002',
high: '10000000-0000-0000-0000-000000000003',
},
bpm: {
slow: '10000000-0000-0000-0000-000000000011',
medium: '10000000-0000-0000-0000-000000000012',
fast: '10000000-0000-0000-0000-000000000013',
},
valence: {
low: '10000000-0000-0000-0000-000000000021',
neutral: '10000000-0000-0000-0000-000000000022',
high: '10000000-0000-0000-0000-000000000023',
},
} as const;
export interface ClaimEdge {
subjectType: string;
subjectId: string;