@audiofool/moondog 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.
Files changed (109) hide show
  1. package/README.md +125 -0
  2. package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
  3. package/contracts/v1/capability-policy.schema.json +572 -0
  4. package/contracts/v1/definitions.schema.json +365 -0
  5. package/contracts/v1/library-track-observation.schema.json +138 -0
  6. package/contracts/v1/listening-event.schema.json +111 -0
  7. package/contracts/v1/profile-evidence.schema.json +237 -0
  8. package/contracts/v1/taste-event.schema.json +258 -0
  9. package/contracts/v1/tool-invocation.schema.json +318 -0
  10. package/contracts/v1/tool-receipt.schema.json +1066 -0
  11. package/contracts/v1/track-ref.schema.json +275 -0
  12. package/contracts/v2/profile-evidence.schema.json +284 -0
  13. package/package.json +105 -0
  14. package/scripts/contract-lib.mjs +361 -0
  15. package/scripts/contract-semantics.mjs +2630 -0
  16. package/scripts/demo-studio.mjs +58 -0
  17. package/scripts/moondog +52 -0
  18. package/scripts/moondog.mjs +954 -0
  19. package/src/core/apple-data-migration.mjs +50 -0
  20. package/src/core/apple-library-source-status.mjs +220 -0
  21. package/src/core/apple-projection-domain-services.mjs +2356 -0
  22. package/src/core/capability-catalog.mjs +577 -0
  23. package/src/core/listening-profile-domain-services.mjs +1010 -0
  24. package/src/core/lyric-library.mjs +121 -0
  25. package/src/core/lyric-profile.mjs +50 -0
  26. package/src/core/lyric-service.mjs +41 -0
  27. package/src/core/moondog-application.mjs +1827 -0
  28. package/src/core/state-directory.mjs +19 -0
  29. package/src/core/synthetic-domain-services.mjs +1485 -0
  30. package/src/core/uuid-v5.mjs +42 -0
  31. package/src/demo/fictional-spotify-history.mjs +400 -0
  32. package/src/demo/moondog-demo.mjs +355 -0
  33. package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
  34. package/src/demo/moondog-tasteprint-demo.mjs +631 -0
  35. package/src/importers/apple-music-library/import-batch.mjs +290 -0
  36. package/src/importers/apple-music-library/index.mjs +76 -0
  37. package/src/importers/apple-music-library/normalize.mjs +1126 -0
  38. package/src/importers/apple-music-library/parse-plist.mjs +357 -0
  39. package/src/importers/apple-music-library/projection-records.mjs +557 -0
  40. package/src/importers/apple-music-library/read-batch.mjs +276 -0
  41. package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
  42. package/src/importers/apple-music-library/stable-ids.mjs +19 -0
  43. package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
  44. package/src/integrations/apple-music/catalog.mjs +770 -0
  45. package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
  46. package/src/integrations/listenbrainz/history-file.mjs +560 -0
  47. package/src/integrations/lyrics/lrclib.mjs +55 -0
  48. package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
  49. package/src/integrations/public-playlists.mjs +104 -0
  50. package/src/integrations/spotify/account-data-history.mjs +545 -0
  51. package/src/integrations/spotify/account-data-profile.mjs +918 -0
  52. package/src/integrations/spotify/authentication.mjs +791 -0
  53. package/src/integrations/spotify/catalog-resolver.mjs +657 -0
  54. package/src/integrations/spotify/connection.mjs +125 -0
  55. package/src/integrations/spotify/credential-store.mjs +26 -0
  56. package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
  57. package/src/integrations/spotify/history-archive.mjs +59 -0
  58. package/src/integrations/spotify/index.mjs +80 -0
  59. package/src/integrations/spotify/recent-activity.mjs +198 -0
  60. package/src/integrations/spotify/resolution-cache.mjs +260 -0
  61. package/src/integrations/spotify/service.mjs +713 -0
  62. package/src/integrations/spotify/settings.mjs +89 -0
  63. package/src/integrations/spotify/web-api-client.mjs +761 -0
  64. package/src/integrations/web/codex-web.mjs +263 -0
  65. package/src/integrations/youtube-music/takeout.mjs +143 -0
  66. package/src/memory/local-memory-store.mjs +1705 -0
  67. package/src/memory/reflection-worker.mjs +87 -0
  68. package/src/profile/apple-library-import.mjs +28 -0
  69. package/src/profile/history-import.mjs +163 -0
  70. package/src/profile/listener-corrections.mjs +217 -0
  71. package/src/profile/listening-history-store.mjs +2055 -0
  72. package/src/profile/listening-profile-projection.mjs +2520 -0
  73. package/src/profile/local-music-data.mjs +812 -0
  74. package/src/profile/music-import-bundle.mjs +91 -0
  75. package/src/profile/music-providers.mjs +25 -0
  76. package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
  77. package/src/profile/spotify-archive-taste.mjs +482 -0
  78. package/src/runtime/pi/agent-runtime.mjs +6444 -0
  79. package/src/runtime/pi/authentication.mjs +104 -0
  80. package/src/runtime/pi/configured-runtime.mjs +98 -0
  81. package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
  82. package/src/runtime/pi/model-catalog.mjs +62 -0
  83. package/src/runtime/pi/model-transport.mjs +117 -0
  84. package/src/runtime/pi/persistent-credential-store.mjs +670 -0
  85. package/src/runtime/pi/provider-registry.mjs +74 -0
  86. package/src/runtime/pi/runtime-settings.mjs +81 -0
  87. package/src/surfaces/cli/auth-command.mjs +355 -0
  88. package/src/surfaces/cli/brand-components.mjs +390 -0
  89. package/src/surfaces/cli/brand-theme.mjs +231 -0
  90. package/src/surfaces/cli/catalog-command.mjs +509 -0
  91. package/src/surfaces/cli/command-completions.mjs +113 -0
  92. package/src/surfaces/cli/data-command.mjs +214 -0
  93. package/src/surfaces/cli/format-output.mjs +750 -0
  94. package/src/surfaces/cli/history-import-view.mjs +445 -0
  95. package/src/surfaces/cli/import-guides.mjs +9 -0
  96. package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
  97. package/src/surfaces/cli/listening-menu.mjs +171 -0
  98. package/src/surfaces/cli/profile-command.mjs +290 -0
  99. package/src/surfaces/cli/spotify-command.mjs +849 -0
  100. package/src/surfaces/cli/taste-profile-model.mjs +306 -0
  101. package/src/surfaces/cli/taste-profile-view.mjs +272 -0
  102. package/src/surfaces/cli/terminal-art.mjs +295 -0
  103. package/src/surfaces/cli/tui.mjs +1714 -0
  104. package/src/surfaces/cli/web-command.mjs +52 -0
  105. package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
  106. package/src/surfaces/html/tasteprint.mjs +3575 -0
  107. package/src/surfaces/web/showcase.mjs +272 -0
  108. package/src/surfaces/web/studio-page.mjs +4148 -0
  109. package/src/surfaces/web/studio.mjs +2288 -0
@@ -0,0 +1,1485 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const SYNTHETIC_DOMAIN_LIMITS = Object.freeze({
4
+ searchDefault: 8,
5
+ searchMax: 12,
6
+ searchOffsetMax: 10_000,
7
+ profileDefault: 6,
8
+ profileMax: 10,
9
+ activeCandidateSetsMax: 8,
10
+ playlistTracksMax: 12,
11
+ filterValuesMax: 4,
12
+ labelsMax: 4,
13
+ limitationsMax: 4,
14
+ queryLengthMax: 256,
15
+ shortTextLengthMax: 256,
16
+ longTextLengthMax: 500,
17
+ });
18
+
19
+ export const LIBRARY_SEARCH_FILTER_VALUES = Object.freeze({
20
+ familiarity: Object.freeze(["low", "medium", "high", "unknown"]),
21
+ preferenceSignals: Object.freeze(["loved", "favorited", "rated"]),
22
+ });
23
+
24
+ const familiarityValues = new Set(LIBRARY_SEARCH_FILTER_VALUES.familiarity);
25
+ const preferenceSignalValues = new Set(
26
+ LIBRARY_SEARCH_FILTER_VALUES.preferenceSignals,
27
+ );
28
+ const externalCatalogProviders = new Set(["apple_music", "listenbrainz"]);
29
+ const listenBrainzModes = new Set(["easy", "medium", "hard"]);
30
+ const filterKeys = new Set([
31
+ "artists",
32
+ "genres",
33
+ "familiarity",
34
+ "preferenceSignals",
35
+ ]);
36
+
37
+ const englishTrackCounts = new Map([
38
+ ["one", 1],
39
+ ["two", 2],
40
+ ["three", 3],
41
+ ["four", 4],
42
+ ["five", 5],
43
+ ["six", 6],
44
+ ["seven", 7],
45
+ ["eight", 8],
46
+ ["nine", 9],
47
+ ["ten", 10],
48
+ ["eleven", 11],
49
+ ["twelve", 12],
50
+ ]);
51
+
52
+ const chineseTrackCounts = new Map([
53
+ ["一", 1],
54
+ ["二", 2],
55
+ ["两", 2],
56
+ ["三", 3],
57
+ ["四", 4],
58
+ ["五", 5],
59
+ ["六", 6],
60
+ ["七", 7],
61
+ ["八", 8],
62
+ ["九", 9],
63
+ ["十", 10],
64
+ ["十一", 11],
65
+ ["十二", 12],
66
+ ]);
67
+
68
+ export class DomainServiceError extends Error {
69
+ constructor(code, message) {
70
+ super(message);
71
+ this.name = "DomainServiceError";
72
+ this.code = code;
73
+ }
74
+ }
75
+
76
+ function fail(code, message) {
77
+ throw new DomainServiceError(code, message);
78
+ }
79
+
80
+ function isPlainObject(value) {
81
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
82
+ return false;
83
+ }
84
+ const prototype = Object.getPrototypeOf(value);
85
+ return prototype === Object.prototype || prototype === null;
86
+ }
87
+
88
+ function cleanText(value, maxLength) {
89
+ if (typeof value !== "string") return undefined;
90
+ const cleaned = value
91
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, "")
92
+ .replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
93
+ .replace(/\s+/gu, " ")
94
+ .trim();
95
+ if (cleaned.length === 0) return undefined;
96
+ if (cleaned.length <= maxLength) return cleaned;
97
+ return `${cleaned.slice(0, Math.max(0, maxLength - 3))}...`;
98
+ }
99
+
100
+ function trustedSubjectId(subjectScope) {
101
+ const candidate =
102
+ typeof subjectScope === "string"
103
+ ? subjectScope
104
+ : isPlainObject(subjectScope)
105
+ ? (subjectScope.subjectId ?? subjectScope.subject_id)
106
+ : undefined;
107
+ const subjectId = cleanText(candidate, SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax);
108
+ if (!subjectId) {
109
+ fail(
110
+ "trusted_subject_scope_required",
111
+ "A trusted subject scope is required to construct domain services.",
112
+ );
113
+ }
114
+ return subjectId;
115
+ }
116
+
117
+ function modelArguments(value, allowedKeys) {
118
+ const input = value ?? {};
119
+ if (!isPlainObject(input)) {
120
+ fail("invalid_arguments", "Domain service arguments are invalid.");
121
+ }
122
+ if (Object.keys(input).some((key) => !allowedKeys.has(key))) {
123
+ fail("invalid_arguments", "Domain service arguments are invalid.");
124
+ }
125
+ return input;
126
+ }
127
+
128
+ function requiredInputText(value, maxLength, code) {
129
+ if (typeof value !== "string" || value.length > maxLength) {
130
+ fail(code, "A required domain service text value is invalid.");
131
+ }
132
+ const result = cleanText(value, maxLength);
133
+ if (!result) {
134
+ fail(code, "A required domain service text value is invalid.");
135
+ }
136
+ return result;
137
+ }
138
+
139
+ function searchQueryText(value) {
140
+ if (value === undefined) return "";
141
+ if (
142
+ typeof value !== "string" ||
143
+ value.length > SYNTHETIC_DOMAIN_LIMITS.queryLengthMax
144
+ ) {
145
+ fail("invalid_query", "The library search query is invalid.");
146
+ }
147
+ return cleanText(value, SYNTHETIC_DOMAIN_LIMITS.queryLengthMax) ?? "";
148
+ }
149
+
150
+ function boundedResultCount(value, defaultValue, maximum, code) {
151
+ if (value === undefined) return defaultValue;
152
+ if (!Number.isInteger(value) || value < 1) {
153
+ fail(code, "A domain service result limit is invalid.");
154
+ }
155
+ return Math.min(value, maximum);
156
+ }
157
+
158
+ function boundedOffset(value, maximum, code) {
159
+ const offset = value ?? 0;
160
+ if (!Number.isInteger(offset) || offset < 0 || offset > maximum) {
161
+ fail(code, "A domain service offset is invalid.");
162
+ }
163
+ return offset;
164
+ }
165
+
166
+ function requiredFixtureText(value) {
167
+ const result = cleanText(value, SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax);
168
+ if (!result) {
169
+ fail("synthetic_fixture_invalid", "The synthetic domain fixture is invalid.");
170
+ }
171
+ return result;
172
+ }
173
+
174
+ function safeFixtureText(value, maximum = SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax) {
175
+ return cleanText(value, maximum);
176
+ }
177
+
178
+ function safeStringArray(values, maximum = SYNTHETIC_DOMAIN_LIMITS.labelsMax) {
179
+ if (!Array.isArray(values)) return [];
180
+ const result = [];
181
+ for (const value of values) {
182
+ const item = safeFixtureText(value);
183
+ if (item && !result.includes(item)) result.push(item);
184
+ if (result.length === maximum) break;
185
+ }
186
+ return result;
187
+ }
188
+
189
+ function normalizeInternalTrack(record) {
190
+ if (!isPlainObject(record)) {
191
+ fail("synthetic_fixture_invalid", "The synthetic domain fixture is invalid.");
192
+ }
193
+ const preferenceSignals = safeStringArray(record.observation?.preference_signals)
194
+ .map((value) => value.toLowerCase())
195
+ .filter((value) => preferenceSignalValues.has(value));
196
+ const familiarity = safeFixtureText(record.observation?.familiarity)?.toLowerCase();
197
+ const playCount = record.observation?.play_count;
198
+ const durationMs = record.duration_ms;
199
+
200
+ return {
201
+ trackRefId: requiredFixtureText(record.track_ref_id),
202
+ title: requiredFixtureText(record.title),
203
+ artistCredit: requiredFixtureText(record.artist_credit),
204
+ release: requiredFixtureText(record.release),
205
+ durationMs:
206
+ Number.isInteger(durationMs) && durationMs >= 0 && durationMs <= 86_400_000
207
+ ? durationMs
208
+ : undefined,
209
+ genres: safeStringArray(record.labels?.genres),
210
+ composer: safeFixtureText(record.labels?.composer),
211
+ preferenceSignals,
212
+ familiarity: familiarityValues.has(familiarity) ? familiarity : "unknown",
213
+ playCount:
214
+ Number.isInteger(playCount) && playCount >= 0 ? playCount : undefined,
215
+ searchTerms: safeStringArray(record.search_terms, 16),
216
+ rank: Number.isFinite(record.rank) ? record.rank : 0,
217
+ };
218
+ }
219
+
220
+ function safeTrackResult(track) {
221
+ const labels = { genres: [...track.genres] };
222
+ if (track.composer) labels.composer = track.composer;
223
+
224
+ const familiarity = {
225
+ level: track.familiarity,
226
+ basis:
227
+ track.playCount === undefined
228
+ ? "not_observed"
229
+ : "aggregate_play_count",
230
+ };
231
+ if (track.playCount !== undefined) familiarity.play_count = track.playCount;
232
+
233
+ const result = {
234
+ track_ref_id: track.trackRefId,
235
+ title: track.title,
236
+ artist_credit: track.artistCredit,
237
+ release: track.release,
238
+ labels,
239
+ observation_summary: {
240
+ preference_signals: [...track.preferenceSignals],
241
+ familiarity,
242
+ },
243
+ };
244
+ if (track.durationMs !== undefined) result.duration_ms = track.durationMs;
245
+ if (track.candidateScope === "external_catalog") {
246
+ result.candidate_scope = "external_catalog";
247
+ result.catalog_provider = track.catalogProvider;
248
+ if (track.catalogProvider === "apple_music") {
249
+ result.matched_queries = [...(track.matchedQueries ?? [])];
250
+ }
251
+ if (track.catalogProvider === "listenbrainz") {
252
+ result.discovery_basis = structuredClone(track.discoveryBasis);
253
+ }
254
+ result.knownness = {
255
+ imported_library: "not_found_by_exact_title_artist",
256
+ listening_history: "not_checked",
257
+ };
258
+ if (track.primaryGenre) result.primary_genre = track.primaryGenre;
259
+ if (track.releaseDate) result.release_date = track.releaseDate;
260
+ if (track.catalogUrl) result.catalog_url = track.catalogUrl;
261
+ }
262
+ return result;
263
+ }
264
+
265
+ function normalizeExternalInternalTrack(raw, provider) {
266
+ if (
267
+ !isPlainObject(raw) ||
268
+ raw.candidate_scope !== "external_catalog" ||
269
+ raw.catalog_provider !== provider ||
270
+ !externalCatalogProviders.has(provider)
271
+ ) {
272
+ fail("external_candidate_invalid", "An external catalog candidate is invalid.");
273
+ }
274
+ const internal = {
275
+ trackRefId: requiredInputText(
276
+ raw.track_ref_id,
277
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
278
+ "external_candidate_invalid",
279
+ ),
280
+ title: requiredInputText(
281
+ raw.title,
282
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
283
+ "external_candidate_invalid",
284
+ ),
285
+ artistCredit: requiredInputText(
286
+ raw.artist_credit,
287
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
288
+ "external_candidate_invalid",
289
+ ),
290
+ release: requiredInputText(
291
+ raw.release,
292
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
293
+ "external_candidate_invalid",
294
+ ),
295
+ durationMs:
296
+ Number.isInteger(raw.duration_ms) && raw.duration_ms >= 0
297
+ ? raw.duration_ms
298
+ : undefined,
299
+ genres: safeStringArray(raw.primary_genre ? [raw.primary_genre] : []),
300
+ composer: undefined,
301
+ preferenceSignals: [],
302
+ familiarity: "unknown",
303
+ playCount: undefined,
304
+ searchTerms: [],
305
+ rank: 0,
306
+ candidateScope: "external_catalog",
307
+ catalogProvider: provider,
308
+ primaryGenre: safeFixtureText(raw.primary_genre, 128),
309
+ releaseDate: safeFixtureText(raw.release_date, 10),
310
+ catalogUrl: safeFixtureText(raw.catalog_url, 2_048),
311
+ matchedQueries: safeStringArray(raw.matched_queries, 3),
312
+ discoveryBasis:
313
+ provider === "listenbrainz" &&
314
+ isPlainObject(raw.discovery_basis) &&
315
+ raw.discovery_basis.kind ===
316
+ "listenbrainz_collaborative_artist_similarity" &&
317
+ listenBrainzModes.has(raw.discovery_basis.mode)
318
+ ? {
319
+ kind: "listenbrainz_collaborative_artist_similarity",
320
+ seed_artist: requiredInputText(
321
+ raw.discovery_basis.seed_artist,
322
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
323
+ "external_candidate_invalid",
324
+ ),
325
+ adjacent_artist: requiredInputText(
326
+ raw.discovery_basis.adjacent_artist,
327
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
328
+ "external_candidate_invalid",
329
+ ),
330
+ mode: raw.discovery_basis.mode,
331
+ }
332
+ : undefined,
333
+ };
334
+ if (provider === "listenbrainz" && !internal.discoveryBasis) {
335
+ fail("external_candidate_invalid", "An external catalog candidate is invalid.");
336
+ }
337
+ return internal;
338
+ }
339
+
340
+ function normalizeSearchText(value) {
341
+ return value.normalize("NFKC").toLocaleLowerCase("und");
342
+ }
343
+
344
+ function searchScore(track, tokens) {
345
+ if (tokens.length === 0) return 0;
346
+ const haystack = normalizeSearchText(
347
+ [
348
+ track.title,
349
+ track.artistCredit,
350
+ track.release,
351
+ ...track.genres,
352
+ track.composer,
353
+ ...track.searchTerms,
354
+ ]
355
+ .filter(Boolean)
356
+ .join(" "),
357
+ );
358
+ let score = 0;
359
+ for (const token of tokens) {
360
+ if (haystack.includes(token)) score += 1;
361
+ }
362
+ return score;
363
+ }
364
+
365
+ function normalizeFilterValues(value, allowedValues) {
366
+ if (value === undefined) return [];
367
+ const input = Array.isArray(value) ? value : [value];
368
+ if (input.length > SYNTHETIC_DOMAIN_LIMITS.filterValuesMax) {
369
+ fail("invalid_filters", "Library search filters are invalid.");
370
+ }
371
+ const normalized = input.map((item) =>
372
+ requiredInputText(
373
+ item,
374
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
375
+ "invalid_filters",
376
+ ).toLowerCase(),
377
+ );
378
+ if (allowedValues && normalized.some((item) => !allowedValues.has(item))) {
379
+ fail("invalid_filters", "Library search filters are invalid.");
380
+ }
381
+ return [...new Set(normalized)];
382
+ }
383
+
384
+ function normalizeFilters(value) {
385
+ if (value === undefined) {
386
+ return {
387
+ artists: [],
388
+ genres: [],
389
+ familiarity: [],
390
+ preferenceSignals: [],
391
+ };
392
+ }
393
+ if (!isPlainObject(value) || Object.keys(value).some((key) => !filterKeys.has(key))) {
394
+ fail("invalid_filters", "Library search filters are invalid.");
395
+ }
396
+ return {
397
+ artists: normalizeFilterValues(value.artists),
398
+ genres: normalizeFilterValues(value.genres),
399
+ familiarity: normalizeFilterValues(value.familiarity, familiarityValues),
400
+ preferenceSignals: normalizeFilterValues(
401
+ value.preferenceSignals,
402
+ preferenceSignalValues,
403
+ ),
404
+ };
405
+ }
406
+
407
+ function matchesFilters(track, filters) {
408
+ const artist = normalizeSearchText(track.artistCredit);
409
+ const genres = track.genres.map(normalizeSearchText);
410
+ if (
411
+ filters.artists.length > 0 &&
412
+ !filters.artists.some((item) => artist.includes(normalizeSearchText(item)))
413
+ ) {
414
+ return false;
415
+ }
416
+ if (
417
+ filters.genres.length > 0 &&
418
+ !filters.genres.some((item) => genres.includes(normalizeSearchText(item)))
419
+ ) {
420
+ return false;
421
+ }
422
+ if (
423
+ filters.familiarity.length > 0 &&
424
+ !filters.familiarity.includes(track.familiarity)
425
+ ) {
426
+ return false;
427
+ }
428
+ if (
429
+ filters.preferenceSignals.length > 0 &&
430
+ !filters.preferenceSignals.every((item) =>
431
+ track.preferenceSignals.includes(item),
432
+ )
433
+ ) {
434
+ return false;
435
+ }
436
+ return true;
437
+ }
438
+
439
+ function normalizeProfileItem(item, kind) {
440
+ if (!isPlainObject(item)) return undefined;
441
+ const evidenceId = safeFixtureText(item.evidence_id);
442
+ if (kind === "strong_preference") {
443
+ const label = safeFixtureText(item.label);
444
+ const signal = safeFixtureText(item.signal);
445
+ if (!label || !signal || !evidenceId) return undefined;
446
+ return { label, signal, evidence_id: evidenceId };
447
+ }
448
+ if (kind === "familiarity") {
449
+ const label = safeFixtureText(item.label);
450
+ const level = safeFixtureText(item.level)?.toLowerCase();
451
+ if (!label || !familiarityValues.has(level) || !evidenceId) return undefined;
452
+ const result = { label, level, evidence_id: evidenceId };
453
+ if (Number.isInteger(item.play_count) && item.play_count >= 0) {
454
+ result.play_count = item.play_count;
455
+ }
456
+ return result;
457
+ }
458
+ const name = safeFixtureText(item.name);
459
+ if (!name || !evidenceId) return undefined;
460
+ return { name, evidence_id: evidenceId };
461
+ }
462
+
463
+ function normalizeProfile(profile) {
464
+ const safeItems = (value, kind) =>
465
+ (Array.isArray(value) ? value : [])
466
+ .map((item) => normalizeProfileItem(item, kind))
467
+ .filter(Boolean);
468
+ const coverage = isPlainObject(profile?.coverage) ? profile.coverage : {};
469
+ const safeCount = (value) =>
470
+ Number.isInteger(value) && value >= 0 ? value : 0;
471
+
472
+ return {
473
+ strongPreferences: safeItems(profile?.strong_preferences, "strong_preference"),
474
+ familiarity: safeItems(profile?.familiarity, "familiarity"),
475
+ artistFacets: safeItems(profile?.artist_facets, "facet"),
476
+ genreFacets: safeItems(profile?.genre_facets, "facet"),
477
+ coverage: {
478
+ tracks_observed: safeCount(coverage.tracks_observed),
479
+ loved_or_favorited: safeCount(coverage.loved_or_favorited),
480
+ aggregate_play_count: safeCount(coverage.aggregate_play_count),
481
+ non_computed_rating: safeCount(coverage.non_computed_rating),
482
+ },
483
+ source: {
484
+ kind: "synthetic_library_snapshot",
485
+ captured_at:
486
+ safeFixtureText(profile?.source?.captured_at) ?? "2026-08-25T00:00:00.000Z",
487
+ },
488
+ limitations: safeStringArray(
489
+ profile?.limitations,
490
+ SYNTHETIC_DOMAIN_LIMITS.limitationsMax,
491
+ ),
492
+ };
493
+ }
494
+
495
+ function normalizeEvidence(record) {
496
+ if (!isPlainObject(record)) return undefined;
497
+ const evidenceId = safeFixtureText(record.evidence_id);
498
+ const dimension = safeFixtureText(record.claim?.dimension);
499
+ const value = safeFixtureText(record.claim?.value);
500
+ const direction = safeFixtureText(record.claim?.direction);
501
+ const basisSummary = safeFixtureText(
502
+ record.basis_summary,
503
+ SYNTHETIC_DOMAIN_LIMITS.longTextLengthMax,
504
+ );
505
+ const interpretationLimit = safeFixtureText(
506
+ record.interpretation_limit,
507
+ SYNTHETIC_DOMAIN_LIMITS.longTextLengthMax,
508
+ );
509
+ const kind = safeFixtureText(record.derivation?.kind);
510
+ const name = safeFixtureText(record.derivation?.name);
511
+ const version = safeFixtureText(record.derivation?.version);
512
+ if (
513
+ !evidenceId ||
514
+ !dimension ||
515
+ !value ||
516
+ !direction ||
517
+ !basisSummary ||
518
+ !interpretationLimit ||
519
+ !kind ||
520
+ !name ||
521
+ !version ||
522
+ !Number.isFinite(record.confidence)
523
+ ) {
524
+ return undefined;
525
+ }
526
+ return {
527
+ evidence_id: evidenceId,
528
+ claim: { dimension, value, direction },
529
+ basis_summary: basisSummary,
530
+ derivation: { kind, name, version },
531
+ confidence: Math.max(0, Math.min(record.confidence, 1)),
532
+ interpretation_limit: interpretationLimit,
533
+ };
534
+ }
535
+
536
+ function requestedTrackCount(intent) {
537
+ const numericBeforeUnit = intent.match(
538
+ /(?:^|[^0-9])(\d+)\s*(?:首|tracks?|songs?)/iu,
539
+ );
540
+ if (numericBeforeUnit) return Number.parseInt(numericBeforeUnit[1], 10);
541
+
542
+ const numericAfterUnit = intent.match(
543
+ /(?:tracks?|songs?)\s*[:=]?\s*(\d+)\b/iu,
544
+ );
545
+ if (numericAfterUnit) return Number.parseInt(numericAfterUnit[1], 10);
546
+
547
+ const english = intent.match(
548
+ /\b(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)\s+(?:tracks?|songs?)\b/iu,
549
+ );
550
+ if (english) return englishTrackCounts.get(english[1].toLowerCase());
551
+
552
+ const chinese = intent.match(/([一二两三四五六七八九十]+)\s*首/u);
553
+ if (!chinese) return undefined;
554
+ if (chineseTrackCounts.has(chinese[1])) {
555
+ return chineseTrackCounts.get(chinese[1]);
556
+ }
557
+ const [tensText, onesText] = chinese[1].split("十");
558
+ if (chinese[1].split("十").length !== 2) return undefined;
559
+ const tens = tensText === "" ? 1 : chineseTrackCounts.get(tensText);
560
+ const ones = onesText === "" ? 0 : chineseTrackCounts.get(onesText);
561
+ return tens && ones !== undefined ? tens * 10 + ones : undefined;
562
+ }
563
+
564
+ function assertExternalPlanDiversity(tracks, intent) {
565
+ const externalTracks = tracks.filter(
566
+ (track) => track.candidate_scope === "external_catalog",
567
+ );
568
+ if (externalTracks.length < 2) return;
569
+ const normalizedIntent = normalizeSearchText(intent);
570
+ const releaseCounts = new Map();
571
+ const artistCounts = new Map();
572
+ for (const track of externalTracks) {
573
+ const artist = normalizeSearchText(track.artist_credit);
574
+ const release = normalizeSearchText(track.release);
575
+ const releaseKey = `${artist}|${release}`;
576
+ releaseCounts.set(releaseKey, {
577
+ release,
578
+ count: (releaseCounts.get(releaseKey)?.count ?? 0) + 1,
579
+ });
580
+ artistCounts.set(artist, {
581
+ artist,
582
+ count: (artistCounts.get(artist)?.count ?? 0) + 1,
583
+ });
584
+ }
585
+ if (
586
+ [...releaseCounts.values()].some(
587
+ (group) =>
588
+ group.count > 1 && !normalizedIntent.includes(group.release),
589
+ )
590
+ ) {
591
+ fail(
592
+ "playlist_external_release_concentration",
593
+ "External discovery plans may select only one track per release unless the intent names that release.",
594
+ );
595
+ }
596
+ if (
597
+ [...artistCounts.values()].some(
598
+ (group) =>
599
+ group.count > 2 && !normalizedIntent.includes(group.artist),
600
+ )
601
+ ) {
602
+ fail(
603
+ "playlist_external_artist_concentration",
604
+ "External discovery plans may select at most two tracks per artist unless the intent names that artist.",
605
+ );
606
+ }
607
+ }
608
+
609
+ function createDefaultFixture(subjectId) {
610
+ const evidenceIds = {
611
+ explicitMara: "20000000-0000-4000-8000-000000000001",
612
+ explicitNorthWindow: "20000000-0000-4000-8000-000000000002",
613
+ familiarity: "20000000-0000-4000-8000-000000000003",
614
+ artistFacet: "20000000-0000-4000-8000-000000000004",
615
+ genreFacet: "20000000-0000-4000-8000-000000000005",
616
+ };
617
+ const tracks = [
618
+ {
619
+ track_ref_id: "10000000-0000-4000-8000-000000000001",
620
+ title: "Midnight Lines",
621
+ artist_credit: "Mara Vale",
622
+ release: "Night Transit",
623
+ duration_ms: 278000,
624
+ labels: { genres: ["Ambient", "Electronic"], composer: "Mara Vale" },
625
+ observation: {
626
+ preference_signals: ["loved", "rated"],
627
+ familiarity: "high",
628
+ play_count: 48,
629
+ },
630
+ search_terms: ["night", "drive", "alone", "familiar", "夜晚", "独自", "坐车"],
631
+ rank: 80,
632
+ },
633
+ {
634
+ track_ref_id: "10000000-0000-4000-8000-000000000002",
635
+ title: "Glass Highway",
636
+ artist_credit: "North Window",
637
+ release: "Slow Roads",
638
+ duration_ms: 251000,
639
+ labels: { genres: ["Dream Pop"] },
640
+ observation: {
641
+ preference_signals: ["favorited"],
642
+ familiarity: "high",
643
+ play_count: 35,
644
+ },
645
+ search_terms: ["night", "drive", "familiar", "road", "夜晚", "熟悉"],
646
+ rank: 70,
647
+ },
648
+ {
649
+ track_ref_id: "10000000-0000-4000-8000-000000000003",
650
+ title: "Blue Exit",
651
+ artist_credit: "Kestrel Frame",
652
+ release: "After Hours",
653
+ duration_ms: 234000,
654
+ labels: { genres: ["Jazz", "Electronic"] },
655
+ observation: {
656
+ preference_signals: [],
657
+ familiarity: "medium",
658
+ play_count: 14,
659
+ },
660
+ search_terms: ["night", "drive", "transition", "夜晚", "过渡"],
661
+ rank: 60,
662
+ },
663
+ {
664
+ track_ref_id: "10000000-0000-4000-8000-000000000004",
665
+ title: "Distant Headlights",
666
+ artist_credit: "Mara Vale",
667
+ release: "Unlit Maps",
668
+ duration_ms: 305000,
669
+ labels: { genres: ["Ambient"] },
670
+ observation: {
671
+ preference_signals: ["loved"],
672
+ familiarity: "medium",
673
+ play_count: 8,
674
+ },
675
+ search_terms: ["night", "drive", "alone", "headlights", "夜晚", "独自"],
676
+ rank: 50,
677
+ },
678
+ {
679
+ track_ref_id: "10000000-0000-4000-8000-000000000005",
680
+ title: "Soft Static",
681
+ artist_credit: "Field Arithmetic",
682
+ release: "Low Signal",
683
+ duration_ms: 219000,
684
+ labels: { genres: ["Experimental", "Electronic"] },
685
+ observation: {
686
+ preference_signals: [],
687
+ familiarity: "low",
688
+ play_count: 2,
689
+ },
690
+ search_terms: ["night", "drive", "surprise", "unexpected", "意外", "夜晚"],
691
+ rank: 40,
692
+ },
693
+ {
694
+ track_ref_id: "10000000-0000-4000-8000-000000000006",
695
+ title: "Empty Overpass",
696
+ artist_credit: "Serein Club",
697
+ release: "Nocturne District",
698
+ duration_ms: 337000,
699
+ labels: { genres: ["Post-Rock"] },
700
+ observation: {
701
+ preference_signals: [],
702
+ familiarity: "low",
703
+ play_count: 1,
704
+ },
705
+ search_terms: ["night", "drive", "alone", "surprise", "夜晚", "意外"],
706
+ rank: 30,
707
+ },
708
+ {
709
+ track_ref_id: "10000000-0000-4000-8000-000000000007",
710
+ title: "First Light Behind Us",
711
+ artist_credit: "North Window",
712
+ release: "Slow Roads",
713
+ duration_ms: 264000,
714
+ labels: { genres: ["Dream Pop"] },
715
+ observation: {
716
+ preference_signals: ["favorited"],
717
+ familiarity: "medium",
718
+ play_count: 16,
719
+ },
720
+ search_terms: ["night", "drive", "dawn", "transition", "过渡", "熟悉"],
721
+ rank: 20,
722
+ },
723
+ {
724
+ track_ref_id: "10000000-0000-4000-8000-000000000008",
725
+ title: "Rain on Route Nine",
726
+ artist_credit: "Aster Fold",
727
+ release: "Weather Systems",
728
+ duration_ms: 288000,
729
+ labels: { genres: ["Ambient Jazz"] },
730
+ observation: {
731
+ preference_signals: [],
732
+ familiarity: "unknown",
733
+ },
734
+ search_terms: ["night", "drive", "rain", "surprise", "夜晚", "意外"],
735
+ rank: 10,
736
+ },
737
+ ].map((track, index) => ({
738
+ ...track,
739
+ subject_id: subjectId,
740
+ provider_id: `synthetic-provider-private-${index + 1}`,
741
+ source_path: `/private/synthetic/library/${index + 1}.m4a`,
742
+ raw_observation: { private_snapshot_row: index + 1 },
743
+ }));
744
+
745
+ return {
746
+ tracks,
747
+ profiles: [
748
+ {
749
+ subject_id: subjectId,
750
+ strong_preferences: [
751
+ {
752
+ label: "Mara Vale",
753
+ signal: "loved and non-computed rating",
754
+ evidence_id: evidenceIds.explicitMara,
755
+ },
756
+ {
757
+ label: "North Window",
758
+ signal: "favorited",
759
+ evidence_id: evidenceIds.explicitNorthWindow,
760
+ },
761
+ ],
762
+ familiarity: [
763
+ {
764
+ label: "Midnight Lines",
765
+ level: "high",
766
+ play_count: 48,
767
+ evidence_id: evidenceIds.familiarity,
768
+ },
769
+ {
770
+ label: "Glass Highway",
771
+ level: "high",
772
+ play_count: 35,
773
+ evidence_id: evidenceIds.familiarity,
774
+ },
775
+ {
776
+ label: "Soft Static",
777
+ level: "low",
778
+ play_count: 2,
779
+ evidence_id: evidenceIds.familiarity,
780
+ },
781
+ ],
782
+ artist_facets: [
783
+ { name: "Mara Vale", evidence_id: evidenceIds.artistFacet },
784
+ { name: "North Window", evidence_id: evidenceIds.artistFacet },
785
+ ],
786
+ genre_facets: [
787
+ { name: "Ambient", evidence_id: evidenceIds.genreFacet },
788
+ { name: "Dream Pop", evidence_id: evidenceIds.genreFacet },
789
+ { name: "Electronic", evidence_id: evidenceIds.genreFacet },
790
+ ],
791
+ coverage: {
792
+ tracks_observed: tracks.length,
793
+ loved_or_favorited: 4,
794
+ aggregate_play_count: 7,
795
+ non_computed_rating: 1,
796
+ },
797
+ source: { captured_at: "2026-08-25T00:00:00.000Z" },
798
+ limitations: [
799
+ "The source is one aggregate library snapshot, not complete listening history.",
800
+ "Play count supports familiarity, not liking.",
801
+ "Artist facets are name-only and genre facets use provider labels.",
802
+ "The synthetic projection does not model short-term preference change.",
803
+ ],
804
+ private_profile_blob: { subject_id: subjectId },
805
+ },
806
+ ],
807
+ evidence: [
808
+ {
809
+ subject_id: subjectId,
810
+ evidence_id: evidenceIds.explicitMara,
811
+ claim: {
812
+ dimension: "preference.artist",
813
+ value: "Mara Vale",
814
+ direction: "supports",
815
+ },
816
+ basis_summary:
817
+ "Loved state and a non-computed rating support a strong preference signal.",
818
+ derivation: {
819
+ kind: "rule",
820
+ name: "explicit-library-preference",
821
+ version: "synthetic/1",
822
+ },
823
+ confidence: 0.95,
824
+ interpretation_limit:
825
+ "This supports preference for the observed item or facet, not a timeless personality trait.",
826
+ raw_basis_refs: ["private-observation-1"],
827
+ },
828
+ {
829
+ subject_id: subjectId,
830
+ evidence_id: evidenceIds.explicitNorthWindow,
831
+ claim: {
832
+ dimension: "preference.artist",
833
+ value: "North Window",
834
+ direction: "supports",
835
+ },
836
+ basis_summary: "Favorited state supports a strong preference signal.",
837
+ derivation: {
838
+ kind: "rule",
839
+ name: "explicit-library-preference",
840
+ version: "synthetic/1",
841
+ },
842
+ confidence: 0.9,
843
+ interpretation_limit:
844
+ "Favorited state supports preference but does not establish when or why it formed.",
845
+ },
846
+ {
847
+ subject_id: subjectId,
848
+ evidence_id: evidenceIds.familiarity,
849
+ claim: {
850
+ dimension: "familiarity.track",
851
+ value: "aggregate play count",
852
+ direction: "supports",
853
+ },
854
+ basis_summary:
855
+ "Aggregate play counts distinguish frequently and rarely played library tracks.",
856
+ derivation: {
857
+ kind: "rule",
858
+ name: "aggregate-play-count-familiarity",
859
+ version: "synthetic/1",
860
+ },
861
+ confidence: 0.85,
862
+ interpretation_limit: "Play count supports familiarity, not liking.",
863
+ },
864
+ {
865
+ subject_id: subjectId,
866
+ evidence_id: evidenceIds.artistFacet,
867
+ claim: {
868
+ dimension: "profile.artist_facet",
869
+ value: "name-only artist facets",
870
+ direction: "supports",
871
+ },
872
+ basis_summary:
873
+ "Explicit track signals are grouped by the artist credit string in the snapshot.",
874
+ derivation: {
875
+ kind: "aggregate",
876
+ name: "artist-name-facet",
877
+ version: "synthetic/1",
878
+ },
879
+ confidence: 0.75,
880
+ interpretation_limit:
881
+ "Artist names are labels only and are not resolved external identities.",
882
+ },
883
+ {
884
+ subject_id: subjectId,
885
+ evidence_id: evidenceIds.genreFacet,
886
+ claim: {
887
+ dimension: "profile.genre_facet",
888
+ value: "provider genre labels",
889
+ direction: "supports",
890
+ },
891
+ basis_summary:
892
+ "Genre labels attached to tracks with explicit signals form a bounded facet summary.",
893
+ derivation: {
894
+ kind: "aggregate",
895
+ name: "provider-genre-facet",
896
+ version: "synthetic/1",
897
+ },
898
+ confidence: 0.7,
899
+ interpretation_limit:
900
+ "Provider genre labels are not a normalized genre ontology or an abstract taste score.",
901
+ },
902
+ ],
903
+ };
904
+ }
905
+
906
+ export class SyntheticDomainServices {
907
+ #subjectId;
908
+ #tracks;
909
+ #tracksById;
910
+ #profile;
911
+ #evidenceById;
912
+ #candidateSets = new Map();
913
+ #externalTrackIds = new Set();
914
+
915
+ constructor({ subjectScope, fixture } = {}) {
916
+ this.#subjectId = trustedSubjectId(subjectScope);
917
+ const source = structuredClone(fixture ?? createDefaultFixture(this.#subjectId));
918
+ if (!isPlainObject(source) || !Array.isArray(source.tracks)) {
919
+ fail("synthetic_fixture_invalid", "The synthetic domain fixture is invalid.");
920
+ }
921
+
922
+ this.#tracks = source.tracks
923
+ .filter((record) => record?.subject_id === this.#subjectId)
924
+ .map(normalizeInternalTrack);
925
+ this.#tracksById = new Map();
926
+ for (const track of this.#tracks) {
927
+ if (this.#tracksById.has(track.trackRefId)) {
928
+ fail("synthetic_fixture_invalid", "The synthetic domain fixture is invalid.");
929
+ }
930
+ this.#tracksById.set(track.trackRefId, track);
931
+ }
932
+
933
+ const profiles = Array.isArray(source.profiles)
934
+ ? source.profiles
935
+ : isPlainObject(source.profile)
936
+ ? [source.profile]
937
+ : [];
938
+ const scopedProfile = profiles.find(
939
+ (record) => record?.subject_id === this.#subjectId,
940
+ );
941
+ this.#profile = normalizeProfile(scopedProfile);
942
+
943
+ this.#evidenceById = new Map();
944
+ for (const record of Array.isArray(source.evidence) ? source.evidence : []) {
945
+ if (record?.subject_id !== this.#subjectId) continue;
946
+ const evidence = normalizeEvidence(record);
947
+ if (!evidence) continue;
948
+ if (this.#evidenceById.has(evidence.evidence_id)) {
949
+ fail("synthetic_fixture_invalid", "The synthetic domain fixture is invalid.");
950
+ }
951
+ this.#evidenceById.set(evidence.evidence_id, evidence);
952
+ }
953
+ for (const key of [
954
+ "strongPreferences",
955
+ "familiarity",
956
+ "artistFacets",
957
+ "genreFacets",
958
+ ]) {
959
+ this.#profile[key] = this.#profile[key]
960
+ .filter((item) => this.#evidenceById.has(item.evidence_id))
961
+ .slice(0, SYNTHETIC_DOMAIN_LIMITS.profileMax);
962
+ }
963
+ }
964
+
965
+ status() {
966
+ return {
967
+ state: "ready",
968
+ adapter: "synthetic",
969
+ subject_scope: "trusted_runtime",
970
+ candidate_scope: "prompt_local",
971
+ external_effects: "none",
972
+ };
973
+ }
974
+
975
+ beginPrompt() {
976
+ return this.resetCandidateSets();
977
+ }
978
+
979
+ endPrompt() {
980
+ return this.resetCandidateSets();
981
+ }
982
+
983
+ resetCandidateSets() {
984
+ const invalidated = this.#candidateSets.size;
985
+ this.#candidateSets.clear();
986
+ for (const trackRefId of this.#externalTrackIds) {
987
+ this.#tracksById.delete(trackRefId);
988
+ }
989
+ this.#externalTrackIds.clear();
990
+ return { invalidated_candidate_sets: invalidated };
991
+ }
992
+
993
+ async searchLibrary(argumentsValue) {
994
+ const input = modelArguments(
995
+ argumentsValue,
996
+ new Set(["query", "limit", "offset", "filters"]),
997
+ );
998
+ const query = searchQueryText(input.query);
999
+ const limit = boundedResultCount(
1000
+ input.limit,
1001
+ SYNTHETIC_DOMAIN_LIMITS.searchDefault,
1002
+ SYNTHETIC_DOMAIN_LIMITS.searchMax,
1003
+ "invalid_search_limit",
1004
+ );
1005
+ const offset = boundedOffset(
1006
+ input.offset,
1007
+ SYNTHETIC_DOMAIN_LIMITS.searchOffsetMax,
1008
+ "invalid_search_offset",
1009
+ );
1010
+ const filters = normalizeFilters(input.filters);
1011
+ if (this.#candidateSets.size >= SYNTHETIC_DOMAIN_LIMITS.activeCandidateSetsMax) {
1012
+ fail(
1013
+ "candidate_set_capacity_reached",
1014
+ "The prompt has reached its candidate set limit.",
1015
+ );
1016
+ }
1017
+
1018
+ const tokens = normalizeSearchText(query).split(/\s+/u).filter(Boolean);
1019
+ const matches = this.#tracks
1020
+ .map((track) => ({ track, score: searchScore(track, tokens) }))
1021
+ .filter(
1022
+ ({ track, score }) =>
1023
+ (tokens.length === 0 || score > 0) && matchesFilters(track, filters),
1024
+ )
1025
+ .sort((left, right) => {
1026
+ if (right.score !== left.score) return right.score - left.score;
1027
+ if (right.track.rank !== left.track.rank) return right.track.rank - left.track.rank;
1028
+ return left.track.trackRefId.localeCompare(right.track.trackRefId);
1029
+ });
1030
+ const tracks = matches
1031
+ .slice(offset, offset + limit)
1032
+ .map(({ track }) => safeTrackResult(track));
1033
+ const hasMore = matches.length > offset + tracks.length;
1034
+ const nextOffset = hasMore
1035
+ ? Math.min(offset + tracks.length, SYNTHETIC_DOMAIN_LIMITS.searchOffsetMax)
1036
+ : null;
1037
+
1038
+ let candidateSetId;
1039
+ for (let attempt = 0; attempt < 4; attempt += 1) {
1040
+ const candidate = randomUUID();
1041
+ if (!this.#candidateSets.has(candidate)) {
1042
+ candidateSetId = candidate;
1043
+ break;
1044
+ }
1045
+ }
1046
+ if (!candidateSetId) {
1047
+ fail("candidate_set_unavailable", "A candidate set could not be created.");
1048
+ }
1049
+
1050
+ this.#candidateSets.set(
1051
+ candidateSetId,
1052
+ new Set(tracks.map((track) => track.track_ref_id)),
1053
+ );
1054
+ return {
1055
+ candidate_set_id: candidateSetId,
1056
+ candidate_scope: "private_library",
1057
+ result_count: tracks.length,
1058
+ limit_applied: limit,
1059
+ offset_applied: offset,
1060
+ next_offset: nextOffset === offset ? null : nextOffset,
1061
+ has_more: hasMore && nextOffset !== offset,
1062
+ expires_on: "prompt_end",
1063
+ tracks,
1064
+ };
1065
+ }
1066
+
1067
+ registerExternalCandidateSet({ tracks, source } = {}) {
1068
+ const provider =
1069
+ isPlainObject(source) && externalCatalogProviders.has(source.provider)
1070
+ ? source.provider
1071
+ : null;
1072
+ if (
1073
+ !Array.isArray(tracks) ||
1074
+ tracks.length < 1 ||
1075
+ tracks.length > SYNTHETIC_DOMAIN_LIMITS.playlistTracksMax ||
1076
+ !provider
1077
+ ) {
1078
+ fail("external_candidate_invalid", "External catalog candidates are invalid.");
1079
+ }
1080
+ if (this.#candidateSets.size >= SYNTHETIC_DOMAIN_LIMITS.activeCandidateSetsMax) {
1081
+ fail(
1082
+ "candidate_set_capacity_reached",
1083
+ "The prompt has reached its candidate set limit.",
1084
+ );
1085
+ }
1086
+ const accepted = [];
1087
+ let excludedLibraryMatches = 0;
1088
+ for (const raw of tracks) {
1089
+ if (
1090
+ !isPlainObject(raw) ||
1091
+ raw.candidate_scope !== "external_catalog" ||
1092
+ raw.catalog_provider !== provider
1093
+ ) {
1094
+ fail("external_candidate_invalid", "An external catalog candidate is invalid.");
1095
+ }
1096
+ const internal = normalizeExternalInternalTrack(raw, provider);
1097
+ const libraryMatch = this.#tracks.some(
1098
+ (track) =>
1099
+ normalizeSearchText(track.title) ===
1100
+ normalizeSearchText(internal.title) &&
1101
+ normalizeSearchText(track.artistCredit) ===
1102
+ normalizeSearchText(internal.artistCredit),
1103
+ );
1104
+ if (libraryMatch) {
1105
+ excludedLibraryMatches += 1;
1106
+ continue;
1107
+ }
1108
+ if (this.#tracksById.has(internal.trackRefId)) {
1109
+ fail("external_candidate_invalid", "External catalog candidates are duplicated.");
1110
+ }
1111
+ this.#tracksById.set(internal.trackRefId, internal);
1112
+ this.#externalTrackIds.add(internal.trackRefId);
1113
+ accepted.push(safeTrackResult(internal));
1114
+ }
1115
+ if (accepted.length === 0) {
1116
+ return {
1117
+ candidate_set_id: null,
1118
+ candidate_scope: "external_catalog",
1119
+ result_count: 0,
1120
+ excluded_library_matches: excludedLibraryMatches,
1121
+ expires_on: "prompt_end",
1122
+ source: structuredClone(source),
1123
+ tracks: [],
1124
+ };
1125
+ }
1126
+ const candidateSetId = randomUUID();
1127
+ this.#candidateSets.set(
1128
+ candidateSetId,
1129
+ new Set(accepted.map((track) => track.track_ref_id)),
1130
+ );
1131
+ return {
1132
+ candidate_set_id: candidateSetId,
1133
+ candidate_scope: "external_catalog",
1134
+ result_count: accepted.length,
1135
+ excluded_library_matches: excludedLibraryMatches,
1136
+ expires_on: "prompt_end",
1137
+ source: structuredClone(source),
1138
+ tracks: accepted,
1139
+ };
1140
+ }
1141
+
1142
+ registerRetainedPlaylistCandidateSet({ tracks } = {}) {
1143
+ if (
1144
+ !Array.isArray(tracks) ||
1145
+ tracks.length < 1 ||
1146
+ tracks.length > SYNTHETIC_DOMAIN_LIMITS.playlistTracksMax
1147
+ ) {
1148
+ fail(
1149
+ "retained_candidate_invalid",
1150
+ "Retained playlist candidates are invalid.",
1151
+ );
1152
+ }
1153
+ if (this.#candidateSets.size >= SYNTHETIC_DOMAIN_LIMITS.activeCandidateSetsMax) {
1154
+ fail(
1155
+ "candidate_set_capacity_reached",
1156
+ "The prompt has reached its candidate set limit.",
1157
+ );
1158
+ }
1159
+
1160
+ const prepared = [];
1161
+ const seen = new Set();
1162
+ for (const raw of tracks) {
1163
+ if (!isPlainObject(raw)) {
1164
+ fail(
1165
+ "retained_candidate_invalid",
1166
+ "Retained playlist candidates are invalid.",
1167
+ );
1168
+ }
1169
+ let track;
1170
+ if (raw.candidate_scope === "external_catalog") {
1171
+ const provider = externalCatalogProviders.has(raw.catalog_provider)
1172
+ ? raw.catalog_provider
1173
+ : null;
1174
+ track = normalizeExternalInternalTrack(raw, provider);
1175
+ if (this.#tracksById.has(track.trackRefId)) {
1176
+ fail(
1177
+ "retained_candidate_invalid",
1178
+ "A retained playlist candidate conflicts with a trusted library track.",
1179
+ );
1180
+ }
1181
+ } else {
1182
+ if (
1183
+ raw.candidate_scope !== undefined &&
1184
+ raw.candidate_scope !== "private_library"
1185
+ ) {
1186
+ fail(
1187
+ "retained_candidate_invalid",
1188
+ "Retained playlist candidates are invalid.",
1189
+ );
1190
+ }
1191
+ const trackRefId = requiredInputText(
1192
+ raw.track_ref_id,
1193
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1194
+ "retained_candidate_invalid",
1195
+ );
1196
+ track = this.#tracksById.get(trackRefId);
1197
+ const canonical = track ? safeTrackResult(track) : null;
1198
+ if (
1199
+ !track ||
1200
+ track.candidateScope === "external_catalog" ||
1201
+ raw.title !== canonical.title ||
1202
+ raw.artist_credit !== canonical.artist_credit ||
1203
+ raw.release !== canonical.release
1204
+ ) {
1205
+ fail(
1206
+ "retained_candidate_invalid",
1207
+ "A retained playlist candidate no longer matches the trusted library.",
1208
+ );
1209
+ }
1210
+ }
1211
+ if (seen.has(track.trackRefId)) {
1212
+ fail(
1213
+ "retained_candidate_invalid",
1214
+ "Retained playlist candidates are duplicated.",
1215
+ );
1216
+ }
1217
+ seen.add(track.trackRefId);
1218
+ prepared.push(track);
1219
+ }
1220
+
1221
+ for (const track of prepared) {
1222
+ if (track.candidateScope === "external_catalog") {
1223
+ this.#tracksById.set(track.trackRefId, track);
1224
+ this.#externalTrackIds.add(track.trackRefId);
1225
+ }
1226
+ }
1227
+ const candidateSetId = randomUUID();
1228
+ this.#candidateSets.set(candidateSetId, new Set(seen));
1229
+ const safeTracks = prepared.map(safeTrackResult);
1230
+ const scopes = new Set(
1231
+ safeTracks.map((track) => track.candidate_scope ?? "private_library"),
1232
+ );
1233
+ return {
1234
+ candidate_set_id: candidateSetId,
1235
+ candidate_scope: scopes.size === 1 ? [...scopes][0] : "mixed",
1236
+ result_count: safeTracks.length,
1237
+ expires_on: "prompt_end",
1238
+ source: "retained_validated_playlist",
1239
+ tracks: safeTracks,
1240
+ };
1241
+ }
1242
+
1243
+ getTrustedTracks(trackRefIds) {
1244
+ if (
1245
+ !Array.isArray(trackRefIds) ||
1246
+ trackRefIds.length < 1 ||
1247
+ trackRefIds.length > SYNTHETIC_DOMAIN_LIMITS.playlistTracksMax
1248
+ ) {
1249
+ fail("invalid_track_refs", "Trusted track references are invalid.");
1250
+ }
1251
+ const results = [];
1252
+ for (const trackRefId of trackRefIds) {
1253
+ const cleanedId = requiredInputText(
1254
+ trackRefId,
1255
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1256
+ "invalid_track_refs",
1257
+ );
1258
+ let trusted = false;
1259
+ for (const candidateSet of this.#candidateSets.values()) {
1260
+ if (candidateSet.has(cleanedId)) {
1261
+ trusted = true;
1262
+ break;
1263
+ }
1264
+ }
1265
+ const track = this.#tracksById.get(cleanedId);
1266
+ if (!trusted || !track) {
1267
+ fail(
1268
+ "untrusted_track_ref",
1269
+ "A referenced track was not returned by the trusted candidate sets.",
1270
+ );
1271
+ }
1272
+ results.push(safeTrackResult(track));
1273
+ }
1274
+ return results;
1275
+ }
1276
+
1277
+ async getProfileSummary(argumentsValue) {
1278
+ const input = modelArguments(argumentsValue, new Set(["maxItems"]));
1279
+ const maxItems = boundedResultCount(
1280
+ input.maxItems,
1281
+ SYNTHETIC_DOMAIN_LIMITS.profileDefault,
1282
+ SYNTHETIC_DOMAIN_LIMITS.profileMax,
1283
+ "invalid_profile_limit",
1284
+ );
1285
+ return {
1286
+ profile_version: "profile_projection/0",
1287
+ max_items_applied: maxItems,
1288
+ strong_preferences: structuredClone(
1289
+ this.#profile.strongPreferences.slice(0, maxItems),
1290
+ ),
1291
+ familiarity: structuredClone(this.#profile.familiarity.slice(0, maxItems)),
1292
+ artist_facets: structuredClone(this.#profile.artistFacets.slice(0, maxItems)),
1293
+ genre_facets: structuredClone(this.#profile.genreFacets.slice(0, maxItems)),
1294
+ coverage: structuredClone(this.#profile.coverage),
1295
+ source: structuredClone(this.#profile.source),
1296
+ limitations: [...this.#profile.limitations],
1297
+ };
1298
+ }
1299
+
1300
+ async explainProfileEvidence(argumentsValue) {
1301
+ const input = modelArguments(argumentsValue, new Set(["evidenceId"]));
1302
+ const evidenceId = requiredInputText(
1303
+ input.evidenceId,
1304
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1305
+ "invalid_evidence_id",
1306
+ );
1307
+ const evidence = this.#evidenceById.get(evidenceId);
1308
+ if (!evidence) {
1309
+ fail(
1310
+ "profile_evidence_unavailable",
1311
+ "Profile evidence is unavailable in the trusted subject scope.",
1312
+ );
1313
+ }
1314
+ return structuredClone(evidence);
1315
+ }
1316
+
1317
+ async buildPlaylistPlan(argumentsValue) {
1318
+ const input = modelArguments(
1319
+ argumentsValue,
1320
+ new Set([
1321
+ "intent",
1322
+ "requestedTrackCount",
1323
+ "candidateSetIds",
1324
+ "trackRefs",
1325
+ "orderingNotes",
1326
+ ]),
1327
+ );
1328
+ const intent = requiredInputText(
1329
+ input.intent,
1330
+ SYNTHETIC_DOMAIN_LIMITS.longTextLengthMax,
1331
+ "invalid_playlist_intent",
1332
+ );
1333
+ const orderingRationale = requiredInputText(
1334
+ input.orderingNotes,
1335
+ SYNTHETIC_DOMAIN_LIMITS.longTextLengthMax,
1336
+ "invalid_ordering_notes",
1337
+ );
1338
+ if (
1339
+ !Array.isArray(input.candidateSetIds) ||
1340
+ input.candidateSetIds.length < 1 ||
1341
+ input.candidateSetIds.length > SYNTHETIC_DOMAIN_LIMITS.activeCandidateSetsMax
1342
+ ) {
1343
+ fail("invalid_candidate_sets", "Playlist candidate sets are invalid.");
1344
+ }
1345
+ const candidateSetIds = input.candidateSetIds.map((value) =>
1346
+ requiredInputText(
1347
+ value,
1348
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1349
+ "invalid_candidate_sets",
1350
+ ),
1351
+ );
1352
+ if (new Set(candidateSetIds).size !== candidateSetIds.length) {
1353
+ fail("invalid_candidate_sets", "Playlist candidate sets are invalid.");
1354
+ }
1355
+
1356
+ const trustedTrackIds = new Set();
1357
+ for (const candidateSetId of candidateSetIds) {
1358
+ const candidateSet = this.#candidateSets.get(candidateSetId);
1359
+ if (!candidateSet) {
1360
+ fail(
1361
+ "candidate_set_unavailable",
1362
+ "A playlist candidate set is unavailable or expired.",
1363
+ );
1364
+ }
1365
+ for (const trackRefId of candidateSet) trustedTrackIds.add(trackRefId);
1366
+ }
1367
+
1368
+ if (
1369
+ !Array.isArray(input.trackRefs) ||
1370
+ input.trackRefs.length < 1 ||
1371
+ input.trackRefs.length > SYNTHETIC_DOMAIN_LIMITS.playlistTracksMax
1372
+ ) {
1373
+ fail("invalid_track_refs", "Playlist track references are invalid.");
1374
+ }
1375
+ const selections = input.trackRefs.map((entry) => {
1376
+ const item = modelArguments(
1377
+ entry,
1378
+ new Set(["trackRefId", "selectionReason"]),
1379
+ );
1380
+ return {
1381
+ trackRefId: requiredInputText(
1382
+ item.trackRefId,
1383
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1384
+ "invalid_track_refs",
1385
+ ),
1386
+ selectionReason: requiredInputText(
1387
+ item.selectionReason,
1388
+ SYNTHETIC_DOMAIN_LIMITS.shortTextLengthMax,
1389
+ "invalid_selection_reason",
1390
+ ),
1391
+ };
1392
+ });
1393
+ const selectedIds = selections.map((selection) => selection.trackRefId);
1394
+ if (new Set(selectedIds).size !== selectedIds.length) {
1395
+ fail("duplicate_track_ref", "A playlist plan cannot contain duplicate tracks.");
1396
+ }
1397
+ if (selectedIds.some((trackRefId) => !trustedTrackIds.has(trackRefId))) {
1398
+ fail(
1399
+ "untrusted_track_ref",
1400
+ "A playlist track was not returned by the trusted candidate sets.",
1401
+ );
1402
+ }
1403
+
1404
+ if (
1405
+ !Number.isInteger(input.requestedTrackCount) ||
1406
+ input.requestedTrackCount < 1 ||
1407
+ input.requestedTrackCount > SYNTHETIC_DOMAIN_LIMITS.playlistTracksMax
1408
+ ) {
1409
+ fail(
1410
+ "playlist_intent_count_unsupported",
1411
+ "The requested playlist size is outside the supported range.",
1412
+ );
1413
+ }
1414
+ const inferredRequestedCount = requestedTrackCount(intent);
1415
+ if (
1416
+ inferredRequestedCount !== undefined &&
1417
+ inferredRequestedCount !== input.requestedTrackCount
1418
+ ) {
1419
+ fail(
1420
+ "playlist_track_count_mismatch",
1421
+ "The playlist track count does not match the explicit user intent.",
1422
+ );
1423
+ }
1424
+ const requestedCount = input.requestedTrackCount;
1425
+ if (requestedCount !== selections.length) {
1426
+ fail(
1427
+ "playlist_track_count_mismatch",
1428
+ "The playlist track count does not match the explicit user intent.",
1429
+ );
1430
+ }
1431
+
1432
+ const tracks = selections.map((selection, index) => {
1433
+ const track = this.#tracksById.get(selection.trackRefId);
1434
+ if (!track) {
1435
+ fail(
1436
+ "untrusted_track_ref",
1437
+ "A playlist track was not returned by the trusted candidate sets.",
1438
+ );
1439
+ }
1440
+ const result = {
1441
+ position: index + 1,
1442
+ track_ref_id: track.trackRefId,
1443
+ title: track.title,
1444
+ artist_credit: track.artistCredit,
1445
+ release: track.release,
1446
+ candidate_scope: track.candidateScope ?? "private_library",
1447
+ selection_reason: selection.selectionReason,
1448
+ };
1449
+ if (track.durationMs !== undefined) result.duration_ms = track.durationMs;
1450
+ if (
1451
+ track.candidateScope === "external_catalog" &&
1452
+ track.catalogProvider === "apple_music" &&
1453
+ track.catalogUrl
1454
+ ) {
1455
+ result.public_catalog_reference = {
1456
+ provider: "apple_music",
1457
+ url: track.catalogUrl,
1458
+ };
1459
+ }
1460
+ return result;
1461
+ });
1462
+ const selectedScopes = new Set(
1463
+ tracks.map((track) => track.candidate_scope),
1464
+ );
1465
+ assertExternalPlanDiversity(tracks, intent);
1466
+
1467
+ return {
1468
+ plan_version: "playlist_plan/0",
1469
+ intent,
1470
+ requested_track_count: requestedCount ?? null,
1471
+ track_count: tracks.length,
1472
+ tracks,
1473
+ candidate_scope:
1474
+ selectedScopes.size === 1 ? [...selectedScopes][0] : "mixed",
1475
+ ordering_rationale: orderingRationale,
1476
+ candidate_sets_validated: candidateSetIds.length,
1477
+ persistence: "none",
1478
+ external_effects: "none",
1479
+ };
1480
+ }
1481
+ }
1482
+
1483
+ export function createSyntheticDomainServices(options) {
1484
+ return new SyntheticDomainServices(options);
1485
+ }