@sriinnu/harmon-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Harmon Core - Session engine, ranking, and adaptation
3
+ */
4
+ export { createEngine } from './engine.js';
5
+ export { rankTracks } from './ranking.js';
6
+ export { fetchCandidates } from './sources.js';
7
+ export { calculateArcModulation } from './arc.js';
8
+ export { checkRecencyPenalty, getRecentPlays, getRecentArtists } from './history.js';
9
+ export type { SessionEngine, EngineConfig, } from './engine.js';
10
+ export type { AudioFeatures, MusicProvider, PlaybackController, TrackWithFeatures, PlayRecord, SessionState, EngineEvent, EventCallback, EngineLogger, RankedTrack, SessionStore, SourcesConfig, } from './types.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErF,YAAY,EACV,aAAa,EACb,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,YAAY,EACZ,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,aAAa,GACd,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Harmon Core - Session engine, ranking, and adaptation
3
+ */
4
+ export { createEngine } from './engine.js';
5
+ export { rankTracks } from './ranking.js';
6
+ export { fetchCandidates } from './sources.js';
7
+ export { calculateArcModulation } from './arc.js';
8
+ export { checkRecencyPenalty, getRecentPlays, getRecentArtists } from './history.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Two-phase track ranking algorithm
3
+ */
4
+ import type { SessionPolicy } from '@sriinnu/harmon-protocol';
5
+ import type { TrackWithFeatures, PlayRecord, RankedTrack } from './types.js';
6
+ /**
7
+ * Two-phase ranking:
8
+ * 1. Hard constraint filtering (pass/fail)
9
+ * 2. Soft weight scoring (0-1 score)
10
+ *
11
+ * @param candidates Candidate tracks to rank
12
+ * @param policy Session policy
13
+ * @param history Play history
14
+ * @param elapsedMs Elapsed session time in milliseconds
15
+ * @returns Ranked tracks sorted by score (descending)
16
+ */
17
+ export declare function rankTracks(candidates: TrackWithFeatures[], policy: SessionPolicy, history: PlayRecord[], elapsedMs: number): Promise<RankedTrack[]>;
18
+ //# sourceMappingURL=ranking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranking.d.ts","sourceRoot":"","sources":["../src/ranking.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI7E;;;;;;;;;;GAUG;AACH,wBAAsB,UAAU,CAC9B,UAAU,EAAE,iBAAiB,EAAE,EAC/B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,UAAU,EAAE,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,WAAW,EAAE,CAAC,CA0BxB"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Two-phase track ranking algorithm
3
+ */
4
+ import { calculateArcModulation } from './arc.js';
5
+ import { checkRecencyPenalty } from './history.js';
6
+ /**
7
+ * Two-phase ranking:
8
+ * 1. Hard constraint filtering (pass/fail)
9
+ * 2. Soft weight scoring (0-1 score)
10
+ *
11
+ * @param candidates Candidate tracks to rank
12
+ * @param policy Session policy
13
+ * @param history Play history
14
+ * @param elapsedMs Elapsed session time in milliseconds
15
+ * @returns Ranked tracks sorted by score (descending)
16
+ */
17
+ export async function rankTracks(candidates, policy, history, elapsedMs) {
18
+ // Phase 1: Hard constraints
19
+ const filtered = candidates.filter(candidate => passesHardConstraints(candidate, policy));
20
+ if (filtered.length === 0) {
21
+ return [];
22
+ }
23
+ // Phase 2: Soft scoring
24
+ const scored = filtered.map(track => {
25
+ const score = calculateSoftScore(track, policy, history, elapsedMs);
26
+ const reason = buildScoreReason(track, score);
27
+ return {
28
+ track,
29
+ score,
30
+ reason,
31
+ };
32
+ });
33
+ // Sort descending by score
34
+ scored.sort((a, b) => b.score - a.score);
35
+ return scored;
36
+ }
37
+ /**
38
+ * Phase 1: Hard constraint filtering
39
+ */
40
+ function passesHardConstraints(track, policy) {
41
+ const hard = policy.hard || {};
42
+ const features = track.features;
43
+ // No vocals check
44
+ if (hard.noVocals && features.instrumentalness < 0.5) {
45
+ return false;
46
+ }
47
+ // Explicit content check
48
+ if (hard.explicit === 'avoid' && track.explicit === true) {
49
+ return false;
50
+ }
51
+ if (hard.explicit === 'require' && track.explicit !== true) {
52
+ return false;
53
+ }
54
+ // Tempo range
55
+ if (hard.tempo) {
56
+ if (hard.tempo.min !== undefined && features.tempo < hard.tempo.min) {
57
+ return false;
58
+ }
59
+ if (hard.tempo.max !== undefined && features.tempo > hard.tempo.max) {
60
+ return false;
61
+ }
62
+ }
63
+ // Energy range
64
+ if (hard.energy) {
65
+ if (hard.energy.min !== undefined && features.energy < hard.energy.min) {
66
+ return false;
67
+ }
68
+ if (hard.energy.max !== undefined && features.energy > hard.energy.max) {
69
+ return false;
70
+ }
71
+ }
72
+ // Instrumentalness minimum
73
+ if (hard.instrumentalnessMin !== undefined) {
74
+ if (features.instrumentalness < hard.instrumentalnessMin) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ }
80
+ /**
81
+ * Phase 2: Soft scoring (0-1)
82
+ */
83
+ function calculateSoftScore(track, policy, history, elapsedMs) {
84
+ const soft = policy.soft || {};
85
+ const weights = soft.weights || {};
86
+ const features = track.features;
87
+ let score = 0;
88
+ let totalWeight = 0;
89
+ // Energy weight (with arc modulation)
90
+ if (typeof weights.energy === 'number') {
91
+ const arcMod = calculateArcModulation(elapsedMs, soft.arc, policy.durationMs);
92
+ const targetEnergy = 0.5 + arcMod; // Base 0.5, modified by arc
93
+ const energyScore = 1 - Math.abs(features.energy - targetEnergy);
94
+ score += weights.energy * energyScore;
95
+ totalWeight += Math.abs(weights.energy);
96
+ }
97
+ // Instrumentalness weight
98
+ if (typeof weights.instrumentalness === 'number') {
99
+ score += weights.instrumentalness * features.instrumentalness;
100
+ totalWeight += Math.abs(weights.instrumentalness);
101
+ }
102
+ // Speechiness weight (usually negative)
103
+ if (typeof weights.speechiness === 'number') {
104
+ const speechScore = weights.speechiness > 0
105
+ ? features.speechiness
106
+ : (1 - features.speechiness);
107
+ score += Math.abs(weights.speechiness) * speechScore;
108
+ totalWeight += Math.abs(weights.speechiness);
109
+ }
110
+ // Valence weight
111
+ if (typeof weights.valence === 'number') {
112
+ score += weights.valence * features.valence;
113
+ totalWeight += Math.abs(weights.valence);
114
+ }
115
+ // Acousticness weight
116
+ if (typeof weights.acousticness === 'number') {
117
+ score += weights.acousticness * features.acousticness;
118
+ totalWeight += Math.abs(weights.acousticness);
119
+ }
120
+ // Tempo weight (normalized to 0-1, assuming 60-180 BPM range)
121
+ if (typeof weights.tempo === 'number') {
122
+ const normalizedTempo = clamp((features.tempo - 60) / 120, 0, 1);
123
+ score += weights.tempo * normalizedTempo;
124
+ totalWeight += Math.abs(weights.tempo);
125
+ }
126
+ // Normalize by total weight
127
+ if (totalWeight > 0) {
128
+ score = score / totalWeight;
129
+ }
130
+ // Clamp to [0,1] BEFORE applying penalty (prevents negative * penalty inversion)
131
+ score = clamp(score, 0, 1);
132
+ // Apply recency penalty
133
+ const recencyPenalty = checkRecencyPenalty(track, history, policy.limits);
134
+ score = score * (1 - recencyPenalty);
135
+ return clamp(score, 0, 1);
136
+ }
137
+ function buildScoreReason(track, score) {
138
+ return `${track.name} scored ${score.toFixed(3)}`;
139
+ }
140
+ function clamp(value, min, max) {
141
+ return Math.min(max, Math.max(min, value));
142
+ }
143
+ //# sourceMappingURL=ranking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranking.js","sourceRoot":"","sources":["../src/ranking.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Candidate fetching from music providers (provider-agnostic)
3
+ */
4
+ import type { MusicProvider, SourcesConfig, TrackWithFeatures, EngineLogger } from './types.js';
5
+ /**
6
+ * Fetch candidate tracks from configured sources
7
+ *
8
+ * @param provider Music provider (Spotify, Apple Music, YouTube Music, etc.)
9
+ * @param sources Sources configuration
10
+ * @param targetCount Target number of candidates
11
+ * @returns Array of tracks with audio features
12
+ */
13
+ export declare function fetchCandidates(provider: MusicProvider, sources: SourcesConfig, targetCount: number, logger?: EngineLogger): Promise<TrackWithFeatures[]>;
14
+ //# sourceMappingURL=sources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAiB,YAAY,EAAE,MAAM,YAAY,CAAC;AAS/G;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,aAAa,EACvB,OAAO,EAAE,aAAa,EACtB,WAAW,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CA6F9B"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Candidate fetching from music providers (provider-agnostic)
3
+ */
4
+ /** Default features for tracks from providers without audio analysis (Apple Music, YouTube Music) */
5
+ const DEFAULT_FEATURES = {
6
+ energy: 0.5, instrumentalness: 0.1, speechiness: 0.1, valence: 0.5,
7
+ acousticness: 0.3, tempo: 120, danceability: 0.5, liveness: 0.2,
8
+ loudness: -8, key: 5, mode: 1, timeSignature: 4,
9
+ };
10
+ /**
11
+ * Fetch candidate tracks from configured sources
12
+ *
13
+ * @param provider Music provider (Spotify, Apple Music, YouTube Music, etc.)
14
+ * @param sources Sources configuration
15
+ * @param targetCount Target number of candidates
16
+ * @returns Array of tracks with audio features
17
+ */
18
+ export async function fetchCandidates(provider, sources, targetCount, logger) {
19
+ const candidates = [];
20
+ const perSource = Math.ceil(targetCount / countActiveSources(sources));
21
+ // Liked/library tracks
22
+ if (sources.likedTracks) {
23
+ const tracks = await fetchSourceTracks('liked tracks', () => provider.getLibraryTracks({ limit: perSource }), logger);
24
+ const withFeatures = await enrichWithFeatures(provider, tracks, logger);
25
+ candidates.push(...withFeatures);
26
+ }
27
+ // Top tracks
28
+ if (sources.topTracks) {
29
+ const tracks = await fetchSourceTracks('top tracks', () => provider.getTopTracks({ limit: perSource }), logger);
30
+ const withFeatures = await enrichWithFeatures(provider, tracks, logger);
31
+ candidates.push(...withFeatures);
32
+ }
33
+ // Recent plays
34
+ if (sources.recentPlays) {
35
+ const tracks = await fetchSourceTracks('recent plays', () => provider.getRecentlyPlayed({ limit: perSource }), logger);
36
+ const withFeatures = await enrichWithFeatures(provider, tracks, logger);
37
+ candidates.push(...withFeatures);
38
+ }
39
+ // Search-seeded sources
40
+ if (sources.searchQueries && sources.searchQueries.length > 0) {
41
+ for (const query of sources.searchQueries.slice(0, 5)) {
42
+ const tracks = await fetchSourceTracks(`search ${query}`, () => provider.search(query, perSource), logger);
43
+ const withFeatures = await enrichWithFeatures(provider, tracks, logger);
44
+ candidates.push(...withFeatures);
45
+ }
46
+ }
47
+ // Seed playlists
48
+ if (sources.seedPlaylists && sources.seedPlaylists.length > 0) {
49
+ const seedPlaylists = sources.seedPlaylists;
50
+ for (const playlistId of sources.seedPlaylists.slice(0, 3)) {
51
+ const tracks = await fetchSourceTracks(`playlist ${playlistId}`, () => provider.getPlaylistTracks(extractId(playlistId), { limit: Math.ceil(perSource / seedPlaylists.length) }), logger);
52
+ const withFeatures = await enrichWithFeatures(provider, tracks, logger);
53
+ candidates.push(...withFeatures);
54
+ }
55
+ }
56
+ // Discovery via recommendations
57
+ if (sources.discovery?.enabled) {
58
+ const discoveryCount = Math.ceil(targetCount * (sources.discovery.ratio || 0.15));
59
+ const seedTracks = candidates.slice(0, 5).map((track) => track.id);
60
+ if (seedTracks.length > 0) {
61
+ const recommendations = await fetchSourceTracks('recommendations', () => provider.getRecommendations({
62
+ seedTrackIds: seedTracks,
63
+ limit: discoveryCount,
64
+ }), logger);
65
+ const withFeatures = await enrichWithFeatures(provider, recommendations, logger);
66
+ candidates.push(...withFeatures);
67
+ }
68
+ }
69
+ // Deduplicate by track ID
70
+ return deduplicateTracks(candidates);
71
+ }
72
+ /**
73
+ * I isolate one provider source failure so one unsupported capability does not
74
+ * wipe out the rest of the candidate collection pass.
75
+ */
76
+ async function fetchSourceTracks(sourceName, loader, logger) {
77
+ try {
78
+ return await loader();
79
+ }
80
+ catch (error) {
81
+ if (!isExplicitlyUnsupportedSourceError(error)) {
82
+ throw error;
83
+ }
84
+ logger?.warn({ source: sourceName, error: error instanceof Error ? error.message : String(error) }, `Skipping source ${sourceName}`);
85
+ return [];
86
+ }
87
+ }
88
+ function isExplicitlyUnsupportedSourceError(error) {
89
+ if (!(error instanceof Error)) {
90
+ return false;
91
+ }
92
+ return /not implemented|not available/i.test(error.message);
93
+ }
94
+ /**
95
+ * Enrich tracks with audio features.
96
+ * Uses positional correspondence — provider.getTrackFeatures returns
97
+ * (AudioFeatures | null)[] with nulls preserved at correct indices.
98
+ */
99
+ async function enrichWithFeatures(provider, tracks, logger) {
100
+ if (tracks.length === 0) {
101
+ return [];
102
+ }
103
+ const trackIds = tracks.map(t => t.id);
104
+ let featureResults = await provider.getTrackFeatures(trackIds);
105
+ // If the provider breaks positional correspondence, I prefer
106
+ // a safe full fallback over silently attaching the wrong features
107
+ // to the wrong tracks.
108
+ if (featureResults.length !== tracks.length) {
109
+ logger?.warn({ expected: tracks.length, received: featureResults.length }, 'Feature result length mismatch — falling back to defaults');
110
+ featureResults = Array.from({ length: tracks.length }, () => null);
111
+ }
112
+ // Match features to tracks by position (nulls preserved).
113
+ // Tracks without features get DEFAULT_FEATURES so non-Spotify providers still work.
114
+ const enriched = [];
115
+ for (let i = 0; i < tracks.length; i++) {
116
+ const feature = featureResults[i];
117
+ enriched.push({
118
+ ...tracks[i],
119
+ features: feature || DEFAULT_FEATURES,
120
+ });
121
+ }
122
+ return enriched;
123
+ }
124
+ /**
125
+ * Remove duplicate tracks by ID
126
+ */
127
+ function deduplicateTracks(tracks) {
128
+ const seen = new Set();
129
+ const unique = [];
130
+ for (const track of tracks) {
131
+ if (!seen.has(track.id)) {
132
+ seen.add(track.id);
133
+ unique.push(track);
134
+ }
135
+ }
136
+ return unique;
137
+ }
138
+ /**
139
+ * Count active sources for distribution
140
+ */
141
+ function countActiveSources(sources) {
142
+ let count = 0;
143
+ if (sources.likedTracks)
144
+ count++;
145
+ if (sources.topTracks)
146
+ count++;
147
+ if (sources.recentPlays)
148
+ count++;
149
+ if (sources.searchQueries && sources.searchQueries.length > 0)
150
+ count++;
151
+ if (sources.seedPlaylists && sources.seedPlaylists.length > 0)
152
+ count++;
153
+ if (sources.discovery?.enabled)
154
+ count++;
155
+ return Math.max(count, 1);
156
+ }
157
+ /**
158
+ * Extract provider ID from URI or URL
159
+ */
160
+ function extractId(uri) {
161
+ // Spotify URIs
162
+ if (uri.startsWith('spotify:')) {
163
+ return uri.split(':').pop() || uri;
164
+ }
165
+ if (uri.startsWith('youtube:playlist:')) {
166
+ return uri.split(':').pop() || uri;
167
+ }
168
+ // Spotify/YouTube URLs
169
+ if (uri.includes('spotify.com/') || uri.includes('youtube.com/') || uri.includes('music.apple.com/')) {
170
+ try {
171
+ const url = new URL(uri);
172
+ const playlistId = url.searchParams.get('list');
173
+ if (playlistId) {
174
+ return playlistId;
175
+ }
176
+ const parts = url.pathname.split('/').filter(Boolean);
177
+ return parts[parts.length - 1] || uri;
178
+ }
179
+ catch {
180
+ const parts = uri.split('/');
181
+ return parts[parts.length - 1].split('?')[0];
182
+ }
183
+ }
184
+ return uri;
185
+ }
186
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Internal types for harmon-core
3
+ */
4
+ import type { TrackInfo, SessionPolicy } from '@sriinnu/harmon-protocol';
5
+ /**
6
+ * Audio features for track ranking.
7
+ * Canonical definition — providers map their features to this shape.
8
+ * Providers that don't support features (YouTube Music) return null.
9
+ */
10
+ export interface AudioFeatures {
11
+ energy: number;
12
+ instrumentalness: number;
13
+ speechiness: number;
14
+ valence: number;
15
+ acousticness: number;
16
+ tempo: number;
17
+ danceability: number;
18
+ liveness: number;
19
+ loudness: number;
20
+ key: number;
21
+ mode: number;
22
+ timeSignature: number;
23
+ }
24
+ /**
25
+ * Extended TrackInfo with audio features attached
26
+ */
27
+ export interface TrackWithFeatures extends TrackInfo {
28
+ features: AudioFeatures;
29
+ }
30
+ /**
31
+ * Provider-agnostic interface for fetching music data.
32
+ * Each music service (Spotify, Apple Music, YouTube Music) implements this.
33
+ */
34
+ export interface MusicProvider {
35
+ readonly name: 'spotify' | 'apple' | 'youtube' | 'local';
36
+ isConnected(): boolean;
37
+ /** Search for tracks */
38
+ search(query: string, limit?: number): Promise<TrackInfo[]>;
39
+ /** Get user's library/saved tracks */
40
+ getLibraryTracks(options?: {
41
+ limit?: number;
42
+ offset?: number;
43
+ }): Promise<TrackInfo[]>;
44
+ /** Get user's top/most-played tracks */
45
+ getTopTracks(options?: {
46
+ limit?: number;
47
+ timeRange?: string;
48
+ }): Promise<TrackInfo[]>;
49
+ /** Get recently played tracks */
50
+ getRecentlyPlayed(options?: {
51
+ limit?: number;
52
+ }): Promise<TrackInfo[]>;
53
+ /** Get tracks from a specific playlist */
54
+ getPlaylistTracks(playlistId: string, options?: {
55
+ limit?: number;
56
+ }): Promise<TrackInfo[]>;
57
+ /** Get recommendations based on seed tracks */
58
+ getRecommendations(options: {
59
+ seedTrackIds?: string[];
60
+ limit?: number;
61
+ }): Promise<TrackInfo[]>;
62
+ /**
63
+ * Get audio features for tracks.
64
+ * Returns array with same length as trackIds. Null for tracks without features.
65
+ */
66
+ getTrackFeatures(trackIds: string[]): Promise<(AudioFeatures | null)[]>;
67
+ }
68
+ /**
69
+ * Provider-agnostic playback controller.
70
+ * Optional methods (seek, volume, etc.) may not be supported by all providers.
71
+ */
72
+ export interface PlaybackController {
73
+ readonly name: 'spotify' | 'apple' | 'youtube' | 'local';
74
+ play(options?: {
75
+ uri?: string;
76
+ trackId?: string;
77
+ }): Promise<void>;
78
+ pause(): Promise<void>;
79
+ next(): Promise<void>;
80
+ previous(): Promise<void>;
81
+ seek?(positionMs: number): Promise<void>;
82
+ setVolume?(volumePercent: number): Promise<void>;
83
+ setShuffle?(state: boolean): Promise<void>;
84
+ setRepeat?(state: 'off' | 'track' | 'context'): Promise<void>;
85
+ getNowPlaying(): Promise<TrackInfo | null>;
86
+ addToQueue(trackUri: string, track?: TrackInfo): Promise<void>;
87
+ }
88
+ /**
89
+ * Track play record in history
90
+ */
91
+ export interface PlayRecord {
92
+ trackId: string;
93
+ artistIds: string[];
94
+ playedAt: number;
95
+ }
96
+ /**
97
+ * Storage contract required by the session engine.
98
+ * I keep this narrow so harmon-core stays decoupled from any concrete store.
99
+ */
100
+ export interface SessionStore {
101
+ createSession(policy: string): Promise<string>;
102
+ endSession(id: string): Promise<void>;
103
+ logEvent(type: string, payload: Record<string, unknown>, sessionId?: string): Promise<string>;
104
+ }
105
+ /**
106
+ * Session state managed by engine
107
+ */
108
+ export interface SessionState {
109
+ id: string;
110
+ policy: SessionPolicy;
111
+ startedAt: number;
112
+ status: 'idle' | 'running' | 'paused';
113
+ history: PlayRecord[];
114
+ currentTrack: TrackInfo | null;
115
+ queuedTracks: TrackInfo[];
116
+ }
117
+ /**
118
+ * Event emitted by engine
119
+ */
120
+ export interface EngineEvent {
121
+ type: 'session.started' | 'session.stopped' | 'session.nudged' | 'track.started' | 'queue.refilled' | 'error';
122
+ payload: Record<string, unknown>;
123
+ }
124
+ /**
125
+ * Event callback type
126
+ */
127
+ export type EventCallback = (event: EngineEvent) => void;
128
+ /**
129
+ * Minimal logger contract for engine internals.
130
+ * Matches the subset of pino that harmon-core actually uses,
131
+ * so the core package stays decoupled from any concrete logger.
132
+ */
133
+ export interface EngineLogger {
134
+ warn(obj: Record<string, unknown>, msg: string): void;
135
+ error(obj: Record<string, unknown>, msg: string): void;
136
+ }
137
+ /**
138
+ * Ranking result with score
139
+ */
140
+ export interface RankedTrack {
141
+ track: TrackWithFeatures;
142
+ score: number;
143
+ reason: string;
144
+ }
145
+ /**
146
+ * Sources configuration for fetching candidates
147
+ */
148
+ export interface SourcesConfig {
149
+ likedTracks?: boolean;
150
+ topTracks?: boolean;
151
+ recentPlays?: boolean;
152
+ searchQueries?: string[];
153
+ seedPlaylists?: string[];
154
+ seedArtists?: string[];
155
+ discovery?: {
156
+ enabled?: boolean;
157
+ ratio?: number;
158
+ };
159
+ }
160
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,SAAS;IAClD,QAAQ,EAAE,aAAa,CAAC;CACzB;AAMD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAEzD,WAAW,IAAI,OAAO,CAAC;IAEvB,wBAAwB;IACxB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAE5D,sCAAsC;IACtC,gBAAgB,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAEtF,wCAAwC;IACxC,YAAY,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAErF,iCAAiC;IACjC,iBAAiB,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAEtE,0CAA0C;IAC1C,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAE1F,+CAA+C;IAC/C,kBAAkB,CAAC,OAAO,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAE/F;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,aAAa,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC;CACzE;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAEzD,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,SAAS,CAAC,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,UAAU,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,SAAS,CAAC,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,aAAa,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAC3C,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChE;AAMD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,EAAE,SAAS,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,SAAS,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,gBAAgB,GAAG,OAAO,CAAC;IAC9G,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;AAEzD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACtD,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACxD;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,iBAAiB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Internal types for harmon-core
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/logo.svg ADDED
@@ -0,0 +1,14 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" fill="none">
2
+ <!-- I am the core mark: a small engine-heart with adjustment arms. -->
3
+ <rect width="64" height="64" rx="16" fill="#F3E7D4"/>
4
+ <g stroke="#171513" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
5
+ <path d="M19 20L23 16"/>
6
+ <path d="M41 20L45 16"/>
7
+ <path d="M15 33H19"/>
8
+ <path d="M45 33H49"/>
9
+ <path d="M22 46L19 50"/>
10
+ <path d="M42 46L45 50"/>
11
+ <path d="M32 21C36 16 45 20 45 27C45 38 32 44 32 44C32 44 19 38 19 27C19 20 28 16 32 21Z"/>
12
+ <circle cx="32" cy="31" r="5"/>
13
+ </g>
14
+ </svg>