fluncle 0.178.0 → 0.180.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 (2) hide show
  1. package/bin/fluncle.mjs +7 -100
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -561,7 +561,7 @@ function parseVersion(version) {
561
561
  var currentVersion;
562
562
  var init_version = __esm(() => {
563
563
  init_output();
564
- currentVersion = "0.178.0".trim() ? "0.178.0".trim() : "0.1.0";
564
+ currentVersion = "0.180.0".trim() ? "0.180.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2260,7 +2260,7 @@ function parseVersion2(version) {
2260
2260
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2261
2261
  var init_version2 = __esm(() => {
2262
2262
  init_output();
2263
- currentVersion2 = "0.178.0".trim() ? "0.178.0".trim() : "0.1.0";
2263
+ currentVersion2 = "0.180.0".trim() ? "0.180.0".trim() : "0.1.0";
2264
2264
  });
2265
2265
 
2266
2266
  // ../../packages/registry/src/index.ts
@@ -3121,13 +3121,12 @@ var init_src = __esm(() => {
3121
3121
  weights: { status: "hidden" }
3122
3122
  },
3123
3123
  {
3124
- command: "fluncle admin backfills label-releases",
3125
3124
  exposedContent: [
3126
- "tap Spotify's fresh releases for the operator's enabled seed labels → day-one uncertified catalogue rows"
3125
+ "tap day-one fresh releases for the operator's enabled seed labels → uncertified catalogue rows"
3127
3126
  ],
3128
3127
  kind: "cron",
3129
3128
  name: "cron.label-releases",
3130
- operatorNotes: "every 24h, run by a rave-02 HOST systemd timer (docs/agents/hermes/label-releases-timer/). The FRESHNESS TAP (D8): MusicBrainz WALKS the graph (cron.crawl) but lags a release ~2 weeks; Spotify has it day one, so this searches each ENABLED seed label's fresh releases (`label:\"<name>\" tag:new`) and mints METADATA-ONLY catalogue rows (a `tracks` row with no `findings` row) with their real release dates closing the /fresh lag cliff. The fuzzy `label:` search is gated on TWO signals before an album mints onto public /fresh: an album's Spotify artist must already be in our archive (`artists.spotify_artist_id` — the primary identity/genre anchor that stops cross-genre homonym junk) AND the album's ℗/© copyright must EXACTLY match the seed label (a brand-new artist's debut is skipped until the MB crawl backfills them). It certifies nothing, publishes nothing, and never widens the graph (no new labels, no artist hops). Deduped against the MB crawl from both directions (Spotify id/uri/ISRC + same-album title fold). Worker-paced, reusing the publish path's Spotify OAuth (no new secret; Apple was a measured 2/99 seed-label dead end). Zero LLM tokens. Source: docs/agents/hermes/scripts/label-releases-sweep.*. See docs/catalogue-crawler.md.",
3129
+ operatorNotes: "every 24h, run by a rave-02 HOST systemd timer (docs/agents/hermes/label-releases-timer/). The FRESHNESS TAP (D8): MusicBrainz WALKS the graph (cron.crawl) but lags a release ~2 weeks; Spotify has it day one, so this mints METADATA-ONLY catalogue rows (a `tracks` row with no `findings` row) for each ENABLED seed label's fresh releases with their real dates — closing the /fresh lag cliff. Runs OFF the official (dev-mode) Spotify app, which is rate-limited to death at its tier and must stay for user paths: the box reads the due seed labels (`list_label_releases_work`), runs an Apify Spotify-scraper actor per label (`label:\"<name>\" tag:new`), maps each result album (inline tracks[] + ISRCs + artists[]) to a candidate, and POSTs them to the agent-tier `backfill_label_releases` op where the WORKER re-runs the FULL gate (the box's verdict is never trusted) and mints. The gate: artist-grounding (an album's Spotify artist already in `artists.spotify_artist_id` — the PRIMARY anchor that stops cross-genre homonym junk, always required) AND, when the actor gives a label signal (album_label/copyright both null in albums-search mode, so grounding-only there), an exact fold-match to the seed name. It certifies nothing, publishes nothing, never widens the graph (no new labels, no artist hops). Deduped against the MB crawl from both directions (Spotify id/uri/ISRC + same-album title fold). Reuses the anchor sweep's APIFY_API_TOKEN (no new secret). Calls the oRPC HTTP endpoints directly (no CLI command the pinned box CLI would lack). Zero LLM tokens. Source: docs/agents/hermes/scripts/label-releases-sweep.*. See docs/catalogue-crawler.md.",
3131
3130
  probeConfig: {
3132
3131
  cadenceMs: 24 * 60 * MINUTE_MS,
3133
3132
  cronName: "fluncle-label-releases",
@@ -3169,13 +3168,13 @@ var init_src = __esm(() => {
3169
3168
  ],
3170
3169
  kind: "cron",
3171
3170
  name: "cron.frontier-refresh",
3172
- operatorNotes: "weekly (Fri 07:00 Amsterdam), run by a rave-02 HOST systemd timer (docs/agents/hermes/frontier-refresh-timer/). E2, the public recommendation machine: every verified user can mint ONE public 'Fluncle's Frontier' playlist on Fluncle's OWN Spotify account (no per-user OAuth), holding THEIR recommendations (the E1 blend); this cron is the Discover-Weekly-style refresh that keeps each one current. The walk + the Spotify item replaces all happen inside the Worker; the sweep just fires one `fluncle admin frontier refresh`. It respects the DEFAULT-DENY `frontier.minting` kill switch (a closed switch touches nothing), skips playlists whose recommendation set is unchanged (a per-row URI-hash mirror guard), and creates no new public authority — every playlist it touches already exists, minted by its own owner. `refresh_frontier_playlists` is AGENT tier, so the box's existing agent-scoped token drives it: NO new secret. Zero LLM tokens. Source: docs/agents/hermes/scripts/frontier-refresh-sweep.*. See docs/the-ear.md § Fluncle's Frontier.",
3171
+ operatorNotes: "every ~15 min, run by a rave-02 HOST systemd timer (docs/agents/hermes/frontier-refresh-timer/). E2, the public recommendation machine: every verified user can mint ONE public 'Fluncle's Frontier' playlist on Fluncle's OWN Spotify account (no per-user OAuth), holding THEIR recommendations (the E1 blend); this cron keeps each one current. It is a PACED, RESUMABLE DRAIN, not a weekly burst: each tick fires one `fluncle admin frontier refresh` that processes only a small BATCH of DUE users inside the Worker (pending mints first, then users whose per-user cursor is older than ~6 days), so the whole crew refreshes ~weekly SPREAD across the day instead of one 07:00 pass that collided with Spotify's shared per-app budget and 429'd live user paths. It consults the shared Spotify budget and stops cleanly when the window is spent (`budgetPaused`), respects the DEFAULT-DENY `frontier.minting` kill switch (a closed switch touches nothing on Spotify), skips playlists whose recommendation set is unchanged (a per-row URI-hash mirror guard), and creates no new public authority — every playlist it touches already exists, minted by its own owner. `refresh_frontier_playlists` is AGENT tier, so the box's existing agent-scoped token drives it: NO new secret. Zero LLM tokens. Source: docs/agents/hermes/scripts/frontier-refresh-sweep.*. See docs/the-ear.md § Fluncle's Frontier.",
3173
3172
  probeConfig: {
3174
- cadenceMs: 7 * 24 * 60 * MINUTE_MS,
3173
+ cadenceMs: 15 * MINUTE_MS,
3175
3174
  cronName: "fluncle-frontier-refresh",
3176
3175
  kind: "cron"
3177
3176
  },
3178
- statusDescription: "refreshes every crew member's Frontier playlist",
3177
+ statusDescription: "refreshes the crew's Frontier playlists, paced",
3179
3178
  title: "Frontier refresh",
3180
3179
  weights: { status: "hidden" }
3181
3180
  },
@@ -4128,7 +4127,6 @@ __export(exports_admin_tracks, {
4128
4127
  captureQueueCommand: () => captureQueueCommand,
4129
4128
  backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
4130
4129
  backfillLastfmCommand: () => backfillLastfmCommand,
4131
- backfillLabelReleasesCommand: () => backfillLabelReleasesCommand,
4132
4130
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4133
4131
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4134
4132
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
@@ -4315,10 +4313,6 @@ async function backfillAppleCatalogueCommand(limit, dryRun) {
4315
4313
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4316
4314
  return adminApiPost(`/api/admin/backfill/apple-catalogue?${params.toString()}`);
4317
4315
  }
4318
- async function backfillLabelReleasesCommand(limit, dryRun) {
4319
- const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4320
- return adminApiPost(`/api/admin/backfill/label-releases?${params.toString()}`);
4321
- }
4322
4316
  async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4323
4317
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4324
4318
  if (cursor) {
@@ -9698,10 +9692,6 @@ function addAdminCommands(program2) {
9698
9692
  const { backfillAppleCatalogueCommand: backfillAppleCatalogueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9699
9693
  await runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2);
9700
9694
  });
9701
- backfill.command("label-releases").description("Tap Spotify's fresh releases for enabled seed labels into catalogue rows").option("--dry-run", "Report the labels that would be probed without any Spotify call", false).option("--limit <limit>", "Max enabled seed labels to probe per pass", "5").option("--json", "Print JSON", false).action(async (options) => {
9702
- const { backfillLabelReleasesCommand: backfillLabelReleasesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9703
- await runBackfillLabelReleases(options, backfillLabelReleasesCommand2);
9704
- });
9705
9695
  backfill.command("artists").description("Back-fill the artist entity (artists + track_artists) for existing findings").option("--dry-run", "Report which findings would be upserted without touching the DB", false).option("--limit <limit>", "Max findings to process", "50").option("--json", "Print JSON", false).action(async (options) => {
9706
9696
  const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9707
9697
  await runBackfillArtists(options, backfillArtistsCommand2);
@@ -10395,89 +10385,6 @@ async function runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2
10395
10385
  process.exitCode = 1;
10396
10386
  }
10397
10387
  }
10398
- async function runBackfillLabelReleases(options, backfillLabelReleasesCommand2) {
10399
- const perPass = parseListLimit(options.limit);
10400
- const newTrackIds = [];
10401
- const labelSlugs = [];
10402
- const failedLabels = [];
10403
- let dryRun = options.dryRun;
10404
- let throttled = false;
10405
- let configured = true;
10406
- let labelsProbed = 0;
10407
- let albumsSeen = 0;
10408
- let albumsMatched = 0;
10409
- let newRows = 0;
10410
- let skippedKnown = 0;
10411
- let skippedUngrounded = 0;
10412
- let failedFetches = 0;
10413
- let fetchCeilingHit = false;
10414
- const MAX_PASSES = 100;
10415
- for (let pass = 0;pass < MAX_PASSES; pass += 1) {
10416
- const result = await backfillLabelReleasesCommand2(perPass, options.dryRun);
10417
- dryRun = result.dryRun;
10418
- configured = result.configured;
10419
- labelsProbed += result.labelsProbed;
10420
- albumsSeen += result.albumsSeen;
10421
- albumsMatched += result.albumsMatched;
10422
- newRows += result.newRows;
10423
- skippedKnown += result.skippedKnown;
10424
- skippedUngrounded += result.skippedUngrounded;
10425
- failedFetches += result.failedFetches;
10426
- fetchCeilingHit = fetchCeilingHit || result.fetchCeilingHit;
10427
- newTrackIds.push(...result.newTrackIds);
10428
- labelSlugs.push(...result.labelSlugs);
10429
- failedLabels.push(...result.failedLabels);
10430
- if (!options.json) {
10431
- const verb2 = result.dryRun ? "would probe" : "probed";
10432
- console.log(` \u2026${verb2} ${result.labelSlugs.length} label(s); ${result.albumsMatched} matched album(s); ${result.newRows} new; ${result.skippedKnown} known; ${result.skippedUngrounded} ungrounded; ${result.failedFetches} fetch-fail; ${result.failedLabels.length} search-fail`);
10433
- }
10434
- if (!result.configured) {
10435
- break;
10436
- }
10437
- if (result.rateLimited) {
10438
- throttled = true;
10439
- break;
10440
- }
10441
- if (result.dryRun) {
10442
- break;
10443
- }
10444
- if (result.labelsProbed === 0) {
10445
- break;
10446
- }
10447
- }
10448
- if (options.json) {
10449
- printSweepJson({
10450
- albumsMatched,
10451
- albumsSeen,
10452
- configured,
10453
- dryRun,
10454
- failedFetches,
10455
- failedLabels,
10456
- fetchCeilingHit,
10457
- labelSlugs,
10458
- labelsProbed,
10459
- newRows,
10460
- newTrackIds,
10461
- rateLimited: throttled,
10462
- skippedKnown,
10463
- skippedUngrounded
10464
- }, 0);
10465
- return;
10466
- }
10467
- if (!configured) {
10468
- console.log("Label-releases tap is a no-op \u2014 the Worker's Spotify grant is gone; reconnect Spotify.");
10469
- return;
10470
- }
10471
- const verb = dryRun ? "Would probe" : "Probed";
10472
- const probedCount = dryRun ? labelSlugs.length : labelsProbed;
10473
- console.log(`${verb} ${probedCount} enabled seed label(s); ${albumsMatched} matched album(s); ${newRows} new catalogue row(s); ${skippedKnown} already known; ${skippedUngrounded} dropped for no known artist; ${failedFetches} fetch-fail; ${failedLabels.length} search-fail.`);
10474
- if (fetchCeilingHit) {
10475
- console.log(" (a pass ended early on the single-fetch ceiling \u2014 resumes next tick)");
10476
- }
10477
- for (const slug of failedLabels) {
10478
- console.log(` search error: ${slug}`);
10479
- }
10480
- }
10481
10388
  async function runBackfillArtists(options, backfillArtistsCommand2) {
10482
10389
  const limit = parseListLimit(options.limit);
10483
10390
  const upserted = [];
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.178.0"
34
+ "version": "0.180.0"
35
35
  }