fluncle 0.179.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 +4 -97
  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.179.0".trim() ? "0.179.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.179.0".trim() ? "0.179.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",
@@ -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.179.0"
34
+ "version": "0.180.0"
35
35
  }