@audiofool/moondog 0.1.0 → 0.2.0-beta.3
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.
- package/LICENSE +202 -0
- package/README.md +156 -68
- package/contracts/v1/definitions.schema.json +1 -169
- package/package.json +7 -23
- package/scripts/contract-lib.mjs +0 -7
- package/scripts/contract-semantics.mjs +0 -1904
- package/scripts/moondog.mjs +285 -74
- package/src/core/apple-library-source-status.mjs +0 -16
- package/src/core/apple-projection-domain-services.mjs +253 -53
- package/src/core/application-update.mjs +320 -0
- package/src/core/capability-catalog.mjs +290 -55
- package/src/core/listener-avoids.mjs +101 -0
- package/src/core/listening-now.mjs +108 -0
- package/src/core/listening-profile-domain-services.mjs +189 -41
- package/src/core/listening-upkeep.mjs +31 -0
- package/src/core/moondog-application.mjs +2115 -107
- package/src/core/playback-preferences.mjs +97 -0
- package/src/core/spotify-listening-intent.mjs +124 -0
- package/src/core/synthetic-domain-services.mjs +24 -3
- package/src/core/track-title.mjs +107 -0
- package/src/demo/moondog-demo.mjs +4 -22
- package/src/i18n/index.mjs +285 -0
- package/src/i18n/messages/de.mjs +135 -0
- package/src/i18n/messages/en.mjs +136 -0
- package/src/i18n/messages/es.mjs +135 -0
- package/src/i18n/messages/fr.mjs +135 -0
- package/src/i18n/messages/it.mjs +135 -0
- package/src/i18n/messages/ja.mjs +135 -0
- package/src/i18n/messages/ko.mjs +135 -0
- package/src/i18n/messages/pt.mjs +135 -0
- package/src/i18n/messages/zh-Hant.mjs +135 -0
- package/src/i18n/messages/zh.mjs +135 -0
- package/src/i18n/preferences.mjs +34 -0
- package/src/i18n/screen/es.mjs +967 -0
- package/src/i18n/screen/ja.mjs +967 -0
- package/src/i18n/screen/pt.mjs +967 -0
- package/src/i18n/screen/zh.mjs +967 -0
- package/src/importers/apple-music-library/index.mjs +0 -16
- package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
- package/src/integrations/apple-music/catalog.mjs +166 -14
- package/src/integrations/apple-music/music-app.mjs +232 -0
- package/src/integrations/apple-music/storefront.mjs +28 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
- package/src/integrations/lastfm/history.mjs +221 -0
- package/src/integrations/lastfm/settings.mjs +75 -0
- package/src/integrations/netease/ncm-cli-player.mjs +328 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
- package/src/integrations/public-playlists.mjs +2 -1
- package/src/integrations/spotify/account-data-profile.mjs +14 -6
- package/src/integrations/spotify/authentication.mjs +149 -15
- package/src/integrations/spotify/catalog-resolver.mjs +9 -6
- package/src/integrations/spotify/connection.mjs +2 -1
- package/src/integrations/spotify/credential-store.mjs +4 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
- package/src/integrations/spotify/history-archive.mjs +1 -1
- package/src/integrations/spotify/library-snapshot.mjs +227 -0
- package/src/integrations/spotify/recent-activity.mjs +2 -0
- package/src/integrations/spotify/service.mjs +873 -69
- package/src/integrations/spotify/web-api-client.mjs +410 -127
- package/src/integrations/web/brave-web.mjs +45 -0
- package/src/integrations/web/codex-web.mjs +15 -82
- package/src/integrations/web/deepseek-web.mjs +81 -0
- package/src/integrations/web/web-research.mjs +119 -0
- package/src/integrations/web/web-result.mjs +215 -0
- package/src/integrations/youtube-music/takeout.mjs +3 -1
- package/src/memory/local-memory-store.mjs +542 -57
- package/src/profile/history-import.mjs +42 -6
- package/src/profile/listener-corrections.mjs +22 -0
- package/src/profile/listener-profile-build.mjs +461 -0
- package/src/profile/listener-profile-store.mjs +130 -0
- package/src/profile/listener-profile-verification.mjs +68 -0
- package/src/profile/listener-profile-worker.mjs +179 -0
- package/src/profile/listening-history-store.mjs +640 -17
- package/src/profile/listening-profile-projection.mjs +454 -86
- package/src/profile/local-music-data.mjs +1 -1
- package/src/profile/moondog-suggestions.mjs +184 -0
- package/src/profile/music-providers.mjs +11 -7
- package/src/profile/profile-exploration.mjs +92 -0
- package/src/profile/recorded-measures.mjs +24 -0
- package/src/profile/spotify-archive-taste.mjs +0 -4
- package/src/runtime/pi/agent-runtime.mjs +2536 -692
- package/src/runtime/pi/authentication.mjs +34 -1
- package/src/runtime/pi/configured-runtime.mjs +22 -6
- package/src/runtime/pi/conversation-name.mjs +53 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
- package/src/runtime/pi/model-catalog.mjs +28 -5
- package/src/runtime/pi/model-settings.mjs +202 -0
- package/src/runtime/pi/model-transport.mjs +10 -0
- package/src/runtime/pi/persistent-credential-store.mjs +67 -31
- package/src/runtime/pi/profile-build-runtime.mjs +161 -0
- package/src/runtime/pi/public-model-catalog.mjs +288 -0
- package/src/runtime/pi/runtime-settings.mjs +10 -2
- package/src/runtime/pi/skills/listener-profile/SKILL.md +48 -0
- package/src/runtime/pi/tool-trace.mjs +37 -0
- package/src/surfaces/cli/auth-command.mjs +12 -4
- package/src/surfaces/cli/brand-components.mjs +310 -132
- package/src/surfaces/cli/brand-theme.mjs +51 -28
- package/src/surfaces/cli/command-completions.mjs +58 -42
- package/src/surfaces/cli/format-output.mjs +199 -388
- package/src/surfaces/cli/history-import-view.mjs +292 -139
- package/src/surfaces/cli/import-guides.mjs +2 -0
- package/src/surfaces/cli/listener-profile-command.mjs +92 -0
- package/src/surfaces/cli/listening-menu.mjs +7 -4
- package/src/surfaces/cli/profile-command.mjs +30 -21
- package/src/surfaces/cli/seasons-chart.mjs +87 -0
- package/src/surfaces/cli/spotify-command.mjs +54 -6
- package/src/surfaces/cli/taste-profile-model.mjs +78 -79
- package/src/surfaces/cli/taste-profile-view.mjs +33 -26
- package/src/surfaces/cli/terminal-art.mjs +140 -35
- package/src/surfaces/cli/tui.mjs +1066 -357
- package/src/surfaces/cli/update-command.mjs +32 -0
- package/src/surfaces/cli/web-command.mjs +53 -14
- package/src/surfaces/html/tasteprint.mjs +43 -70
- package/contracts/v1/capability-policy.schema.json +0 -572
- package/contracts/v1/profile-evidence.schema.json +0 -237
- package/contracts/v1/tool-invocation.schema.json +0 -318
- package/contracts/v1/tool-receipt.schema.json +0 -1066
- package/scripts/demo-studio.mjs +0 -58
- package/src/core/apple-data-migration.mjs +0 -50
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
- package/src/demo/moondog-tasteprint-demo.mjs +0 -631
- package/src/integrations/spotify/index.mjs +0 -80
- package/src/surfaces/web/showcase.mjs +0 -272
- package/src/surfaces/web/studio-page.mjs +0 -4148
- package/src/surfaces/web/studio.mjs +0 -2288
package/scripts/demo-studio.mjs
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
MOONDOG_STUDIO_MAXIMUM_ARCHIVES,
|
|
5
|
-
runMoondogStudio,
|
|
6
|
-
} from "../src/surfaces/web/studio.mjs";
|
|
7
|
-
|
|
8
|
-
const usage = `Usage:
|
|
9
|
-
npm run demo:studio
|
|
10
|
-
npm run demo:studio -- --from <spotify-history.zip> [--from <spotify-history.zip>]
|
|
11
|
-
|
|
12
|
-
Without --from, Moondog opens the fictional production-importer tour.
|
|
13
|
-
With one or two explicitly supplied ZIPs, it opens a session-only private profile without installing dependencies or writing persistent profile state.`;
|
|
14
|
-
|
|
15
|
-
function terminalText(value) {
|
|
16
|
-
return String(value ?? "Unknown failure")
|
|
17
|
-
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, " ")
|
|
18
|
-
.replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
|
|
19
|
-
.slice(0, 1_000);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function main() {
|
|
23
|
-
const values = process.argv.slice(2);
|
|
24
|
-
if (values.length === 1 && new Set(["--help", "-h"]).has(values[0])) {
|
|
25
|
-
process.stdout.write(`${usage}\n`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (values.length === 0) {
|
|
29
|
-
await runMoondogStudio({ demoOnly: true });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const archivePaths = [];
|
|
34
|
-
for (let index = 0; index < values.length; index += 2) {
|
|
35
|
-
const option = values[index];
|
|
36
|
-
const archivePath = values[index + 1];
|
|
37
|
-
if (
|
|
38
|
-
option !== "--from" ||
|
|
39
|
-
!archivePath ||
|
|
40
|
-
archivePath.startsWith("--")
|
|
41
|
-
) {
|
|
42
|
-
throw new Error(usage);
|
|
43
|
-
}
|
|
44
|
-
archivePaths.push(archivePath);
|
|
45
|
-
}
|
|
46
|
-
if (
|
|
47
|
-
archivePaths.length < 1 ||
|
|
48
|
-
archivePaths.length > MOONDOG_STUDIO_MAXIMUM_ARCHIVES
|
|
49
|
-
) {
|
|
50
|
-
throw new Error(usage);
|
|
51
|
-
}
|
|
52
|
-
await runMoondogStudio({ archivePaths });
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
main().catch((error) => {
|
|
56
|
-
process.stderr.write(`moondog demo studio: ${terminalText(error?.message)}\n`);
|
|
57
|
-
process.exitCode = 1;
|
|
58
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { cp, lstat, mkdir } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot } from "./apple-library-source-status.mjs";
|
|
5
|
-
import { resolveAppleMusicProjectionPath } from "./apple-projection-domain-services.mjs";
|
|
6
|
-
import { legacyAppleMusicProjectionPath } from "../importers/apple-music-library/index.mjs";
|
|
7
|
-
|
|
8
|
-
async function kind(target) {
|
|
9
|
-
try {
|
|
10
|
-
const metadata = await lstat(target);
|
|
11
|
-
return metadata.isSymbolicLink() ? "link" : metadata.isDirectory() ? "directory" : "file";
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error?.code === "ENOENT") return null;
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function copyOnce(from, to, expected) {
|
|
19
|
-
if ((await kind(from)) !== expected || (await kind(to)) !== null) return false;
|
|
20
|
-
await mkdir(path.dirname(to), { recursive: true, mode: 0o700 });
|
|
21
|
-
await cp(from, to, { recursive: true, errorOnExist: true, force: false, preserveTimestamps: true });
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Earlier versions kept Apple Music data inside the source checkout. Copy it
|
|
27
|
-
* into the default private state directory once, leaving the original in
|
|
28
|
-
* place. Runs with an explicit state or Apple location are never touched, so
|
|
29
|
-
* isolated runs such as tests cannot pull private data into their directories.
|
|
30
|
-
*/
|
|
31
|
-
export async function migrateLegacyAppleMusicData(environment = process.env) {
|
|
32
|
-
const explicit = [
|
|
33
|
-
"MOONDOG_STATE_HOME",
|
|
34
|
-
"MOONDOG_CONFIG_HOME",
|
|
35
|
-
"MOONDOG_APPLE_IMPORTS_ROOT",
|
|
36
|
-
"MOONDOG_APPLE_PROJECTION_PATH",
|
|
37
|
-
].some((name) => environment[name]?.trim());
|
|
38
|
-
if (explicit) return { copied: [] };
|
|
39
|
-
const copied = [];
|
|
40
|
-
if (await copyOnce(legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot(environment), "directory")) {
|
|
41
|
-
copied.push("imports");
|
|
42
|
-
}
|
|
43
|
-
if (await copyOnce(legacyAppleMusicProjectionPath, resolveAppleMusicProjectionPath(environment), "file")) {
|
|
44
|
-
copied.push("projection");
|
|
45
|
-
for (const suffix of ["-wal", "-shm"]) {
|
|
46
|
-
await copyOnce(`${legacyAppleMusicProjectionPath}${suffix}`, `${resolveAppleMusicProjectionPath(environment)}${suffix}`, "file");
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return { copied };
|
|
50
|
-
}
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createListenerCorrection,
|
|
3
|
-
createListenerCorrectionRetraction,
|
|
4
|
-
listenerCorrectionTargetKey,
|
|
5
|
-
} from "../profile/listener-corrections.mjs";
|
|
6
|
-
import { createPublicTasteprintDemoProfile } from "./moondog-tasteprint-demo.mjs";
|
|
7
|
-
|
|
8
|
-
const DEMO_SUBJECT_ID = "70000000-0000-4000-8000-000000000001";
|
|
9
|
-
|
|
10
|
-
function demoCorrectionError(code, message) {
|
|
11
|
-
const error = new Error(message);
|
|
12
|
-
error.name = "ListenerCorrectionError";
|
|
13
|
-
error.code = code;
|
|
14
|
-
return error;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function activeAssertions(records) {
|
|
18
|
-
const retracted = new Set(
|
|
19
|
-
records
|
|
20
|
-
.filter((record) => record.operation === "retract")
|
|
21
|
-
.map((record) => record.retracts_taste_event_id),
|
|
22
|
-
);
|
|
23
|
-
const superseded = new Set(
|
|
24
|
-
records
|
|
25
|
-
.filter((record) => record.operation === "assert")
|
|
26
|
-
.map((record) => record.supersedes_taste_event_id)
|
|
27
|
-
.filter(Boolean),
|
|
28
|
-
);
|
|
29
|
-
return records
|
|
30
|
-
.filter(
|
|
31
|
-
(record) =>
|
|
32
|
-
record.operation === "assert" &&
|
|
33
|
-
!retracted.has(record.taste_event_id) &&
|
|
34
|
-
!superseded.has(record.taste_event_id),
|
|
35
|
-
)
|
|
36
|
-
.sort((left, right) => {
|
|
37
|
-
const time = right.recorded_at.localeCompare(left.recorded_at);
|
|
38
|
-
return time || right.taste_event_id.localeCompare(left.taste_event_id);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function publicCorrection(record) {
|
|
43
|
-
const artistCredit = record.extensions?.["moondog.artist_credit"];
|
|
44
|
-
return {
|
|
45
|
-
correction_id: record.taste_event_id,
|
|
46
|
-
state: "active",
|
|
47
|
-
entity_type: record.target.entity_type,
|
|
48
|
-
label: record.target.label,
|
|
49
|
-
...(artistCredit ? { artist_credit: artistCredit } : {}),
|
|
50
|
-
stance: record.signal_type === "avoidance" ? "avoid" : "like",
|
|
51
|
-
occurred_at: record.occurred_at,
|
|
52
|
-
...(record.note ? { note: record.note } : {}),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function projectedAssertion(record) {
|
|
57
|
-
const correction = publicCorrection(record);
|
|
58
|
-
return {
|
|
59
|
-
correction_id: correction.correction_id,
|
|
60
|
-
evidence_id: correction.correction_id,
|
|
61
|
-
entity_type: correction.entity_type,
|
|
62
|
-
label: correction.label,
|
|
63
|
-
...(correction.artist_credit
|
|
64
|
-
? { artist_credit: correction.artist_credit }
|
|
65
|
-
: {}),
|
|
66
|
-
stance: correction.stance,
|
|
67
|
-
strength: 1,
|
|
68
|
-
asserted_at: correction.occurred_at,
|
|
69
|
-
...(correction.note ? { note: correction.note } : {}),
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizedText(value) {
|
|
74
|
-
return String(value).normalize("NFKC").toLocaleLowerCase("und").trim();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function strongPreferenceAvoided(item, avoids) {
|
|
78
|
-
const label = normalizedText(item.label);
|
|
79
|
-
return avoids.some((avoid) => {
|
|
80
|
-
if (normalizedText(avoid.label) !== label) return false;
|
|
81
|
-
if (avoid.entity_type === "track") return true;
|
|
82
|
-
return /artist/iu.test(item.signal ?? "");
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function behaviorTrackAvoided(item, avoids) {
|
|
87
|
-
const label = normalizedText(item?.label);
|
|
88
|
-
const artist = normalizedText(item?.artist_credit);
|
|
89
|
-
return avoids.some((avoid) => {
|
|
90
|
-
if (avoid.entity_type === "artist") {
|
|
91
|
-
return artist && artist === normalizedText(avoid.label);
|
|
92
|
-
}
|
|
93
|
-
if (avoid.entity_type !== "track" || label !== normalizedText(avoid.label)) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
return !avoid.artist_credit ||
|
|
97
|
-
artist === normalizedText(avoid.artist_credit);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function directPreference(assertion, kind) {
|
|
102
|
-
return {
|
|
103
|
-
label: assertion.artist_credit
|
|
104
|
-
? `${assertion.label} / ${assertion.artist_credit}`
|
|
105
|
-
: assertion.label,
|
|
106
|
-
signal: `Direct ${kind === "demo" ? "demo" : "session"} ${assertion.entity_type} preference`,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export class InteractiveTasteprintDemoSession {
|
|
111
|
-
#records = [];
|
|
112
|
-
#baseProfileFactory;
|
|
113
|
-
#kind;
|
|
114
|
-
|
|
115
|
-
constructor({
|
|
116
|
-
baseProfileFactory = createPublicTasteprintDemoProfile,
|
|
117
|
-
kind = "demo",
|
|
118
|
-
} = {}) {
|
|
119
|
-
if (typeof baseProfileFactory !== "function") {
|
|
120
|
-
throw new TypeError("The interactive demo profile factory is invalid.");
|
|
121
|
-
}
|
|
122
|
-
if (!new Set(["demo", "private_session"]).has(kind)) {
|
|
123
|
-
throw new TypeError("The interactive profile session kind is invalid.");
|
|
124
|
-
}
|
|
125
|
-
this.#baseProfileFactory = baseProfileFactory;
|
|
126
|
-
this.#kind = kind;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
get activeCount() {
|
|
130
|
-
return activeAssertions(this.#records).length;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
corrections() {
|
|
134
|
-
return activeAssertions(this.#records).map(publicCorrection);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
profile() {
|
|
138
|
-
const profile = this.#baseProfileFactory();
|
|
139
|
-
if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
|
|
140
|
-
throw new TypeError("The interactive demo base profile is invalid.");
|
|
141
|
-
}
|
|
142
|
-
const active = activeAssertions(this.#records).map(projectedAssertion);
|
|
143
|
-
const preferences = active.filter((item) => item.stance === "like");
|
|
144
|
-
const avoids = active.filter((item) => item.stance === "avoid");
|
|
145
|
-
const assertions = this.#records.filter(
|
|
146
|
-
(record) => record.operation === "assert",
|
|
147
|
-
).length;
|
|
148
|
-
const retractions = this.#records.filter(
|
|
149
|
-
(record) => record.operation === "retract",
|
|
150
|
-
).length;
|
|
151
|
-
profile.coverage = {
|
|
152
|
-
...profile.coverage,
|
|
153
|
-
listener_assertion_events: assertions,
|
|
154
|
-
active_listener_assertions: active.length,
|
|
155
|
-
listener_retractions: retractions,
|
|
156
|
-
};
|
|
157
|
-
profile.listener_assertions = {
|
|
158
|
-
active,
|
|
159
|
-
preferences,
|
|
160
|
-
avoids,
|
|
161
|
-
retractions,
|
|
162
|
-
};
|
|
163
|
-
profile.strong_preferences = [
|
|
164
|
-
...preferences.map((item) => directPreference(item, this.#kind)),
|
|
165
|
-
...profile.strong_preferences.filter(
|
|
166
|
-
(item) => !strongPreferenceAvoided(item, avoids),
|
|
167
|
-
),
|
|
168
|
-
].slice(0, 10);
|
|
169
|
-
profile.curated_preferences = {
|
|
170
|
-
...profile.curated_preferences,
|
|
171
|
-
avoids: avoids.map((item) => ({
|
|
172
|
-
entity_type: item.entity_type,
|
|
173
|
-
label: item.label,
|
|
174
|
-
...(item.artist_credit
|
|
175
|
-
? { artist_credit: item.artist_credit }
|
|
176
|
-
: {}),
|
|
177
|
-
stance: "avoid",
|
|
178
|
-
})),
|
|
179
|
-
};
|
|
180
|
-
if (profile.listening_behavior) {
|
|
181
|
-
// Refilter every listen-again selection from the original bounded profile.
|
|
182
|
-
// Descriptive history remains evidence even when the listener avoids it.
|
|
183
|
-
// Retraction restores the original candidates without inventing replacements.
|
|
184
|
-
profile.listening_behavior = { ...profile.listening_behavior };
|
|
185
|
-
for (const key of [
|
|
186
|
-
"rediscovery_tracks",
|
|
187
|
-
"time_capsule_tracks",
|
|
188
|
-
"historical_return_tracks",
|
|
189
|
-
"back_to_back_tracks",
|
|
190
|
-
]) {
|
|
191
|
-
const candidates = profile.listening_behavior[key];
|
|
192
|
-
if (Array.isArray(candidates)) {
|
|
193
|
-
profile.listening_behavior[key] = candidates.filter(
|
|
194
|
-
(item) => !behaviorTrackAvoided(item, avoids),
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
profile.limitations = [
|
|
200
|
-
this.#kind === "demo"
|
|
201
|
-
? "Interactive demo corrections exist only in this Studio process and disappear when it stops."
|
|
202
|
-
: "Session-only corrections exist only in this Studio process and disappear when it stops.",
|
|
203
|
-
...profile.limitations,
|
|
204
|
-
].slice(0, 8);
|
|
205
|
-
return profile;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
record({
|
|
209
|
-
entityType,
|
|
210
|
-
label,
|
|
211
|
-
artistCredit,
|
|
212
|
-
stance,
|
|
213
|
-
note,
|
|
214
|
-
occurredAt,
|
|
215
|
-
recordedAt = occurredAt,
|
|
216
|
-
} = {}) {
|
|
217
|
-
const initial = createListenerCorrection({
|
|
218
|
-
subjectId: DEMO_SUBJECT_ID,
|
|
219
|
-
entityType,
|
|
220
|
-
label,
|
|
221
|
-
artistCredit,
|
|
222
|
-
stance,
|
|
223
|
-
note,
|
|
224
|
-
occurredAt,
|
|
225
|
-
recordedAt,
|
|
226
|
-
});
|
|
227
|
-
const previous = activeAssertions(this.#records).find(
|
|
228
|
-
(record) => listenerCorrectionTargetKey(record) === initial.targetKey,
|
|
229
|
-
);
|
|
230
|
-
const value = previous
|
|
231
|
-
? createListenerCorrection({
|
|
232
|
-
subjectId: DEMO_SUBJECT_ID,
|
|
233
|
-
entityType,
|
|
234
|
-
label,
|
|
235
|
-
artistCredit,
|
|
236
|
-
stance,
|
|
237
|
-
note,
|
|
238
|
-
occurredAt,
|
|
239
|
-
recordedAt,
|
|
240
|
-
correctionId: initial.record.taste_event_id,
|
|
241
|
-
supersedesCorrectionId: previous.taste_event_id,
|
|
242
|
-
})
|
|
243
|
-
: initial;
|
|
244
|
-
this.#records.push(value.record);
|
|
245
|
-
return publicCorrection(value.record);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
retract({
|
|
249
|
-
correctionId,
|
|
250
|
-
occurredAt,
|
|
251
|
-
recordedAt = occurredAt,
|
|
252
|
-
} = {}) {
|
|
253
|
-
const assertion = this.#records.find(
|
|
254
|
-
(record) =>
|
|
255
|
-
record.operation === "assert" &&
|
|
256
|
-
record.taste_event_id === correctionId?.toLocaleLowerCase("en-US"),
|
|
257
|
-
);
|
|
258
|
-
if (!assertion) {
|
|
259
|
-
throw demoCorrectionError(
|
|
260
|
-
"listener_correction_not_found",
|
|
261
|
-
this.#kind === "demo"
|
|
262
|
-
? "The demo listener correction was not found."
|
|
263
|
-
: "The session-only listener correction was not found.",
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
if (
|
|
267
|
-
!activeAssertions(this.#records).some(
|
|
268
|
-
(record) => record.taste_event_id === assertion.taste_event_id,
|
|
269
|
-
)
|
|
270
|
-
) {
|
|
271
|
-
throw demoCorrectionError(
|
|
272
|
-
"listener_correction_inactive",
|
|
273
|
-
this.#kind === "demo"
|
|
274
|
-
? "Only an active demo listener correction can be retracted."
|
|
275
|
-
: "Only an active session-only listener correction can be retracted.",
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
const retraction = createListenerCorrectionRetraction({
|
|
279
|
-
subjectId: DEMO_SUBJECT_ID,
|
|
280
|
-
retractsCorrectionId: assertion.taste_event_id,
|
|
281
|
-
occurredAt,
|
|
282
|
-
recordedAt,
|
|
283
|
-
});
|
|
284
|
-
this.#records.push(retraction);
|
|
285
|
-
return {
|
|
286
|
-
state: "retracted",
|
|
287
|
-
correction_id: assertion.taste_event_id,
|
|
288
|
-
retraction_id: retraction.taste_event_id,
|
|
289
|
-
entity_type: assertion.target.entity_type,
|
|
290
|
-
label: assertion.target.label,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
}
|