fluncle 0.172.0 → 0.174.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 +122 -3
  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.172.0".trim() ? "0.172.0".trim() : "0.1.0";
564
+ currentVersion = "0.174.0".trim() ? "0.174.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2259,7 +2259,7 @@ function parseVersion2(version) {
2259
2259
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2260
2260
  var init_version2 = __esm(() => {
2261
2261
  init_output();
2262
- currentVersion2 = "0.172.0".trim() ? "0.172.0".trim() : "0.1.0";
2262
+ currentVersion2 = "0.174.0".trim() ? "0.174.0".trim() : "0.1.0";
2263
2263
  });
2264
2264
 
2265
2265
  // ../../packages/registry/src/index.ts
@@ -2476,7 +2476,7 @@ var init_src = __esm(() => {
2476
2476
  ],
2477
2477
  kind: "web_route",
2478
2478
  name: "web.tracks",
2479
- operatorNotes: "The top-level track index — the whole archive as one browse list, findings in full voice and the catalogue rows in the unlit register (DESIGN.md). Ordered by tracks.release_date (what came out), never findings.added_at (the Found Rule), keyset-paginated over the tracks_release_date_idx btree so it stays a bounded reverse scan as the catalogue grows (lib/server/tracks-hub.ts). The filter params MIRROR the search vocabulary verbatim (yearMin/yearMax, bpmMin/bpmMax, key, label; galaxy is the one extension). The bare HUB is always indexable + in the sitemap; ANY filter param present flips it to noindex, and the canonical is always the bare /tracks. The INDEX is always-200, so it is HTTP-probeable.",
2479
+ operatorNotes: "The top-level track index — the whole archive as one browse list, findings in full voice and the catalogue rows in the unlit register (DESIGN.md). Ordered by tracks.release_date (what came out), never findings.added_at (the Found Rule), numbered-paginated (?page=N) over the tracks_release_date_idx btree with a quiet YEAR fast lane, so a crawler with no JS walks the whole list (lib/server/tracks-hub.ts). The filter params MIRROR the search vocabulary verbatim (yearMin/yearMax, bpmMin/bpmMax, key, label; galaxy is the one extension). The bare HUB is always indexable + in the sitemap, each page self-canonical; ANY filter param present flips it to noindex, and paged bare URLs stay out of the sitemap. The INDEX is always-200, so it is HTTP-probeable.",
2480
2480
  probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
2481
2481
  route: "/tracks",
2482
2482
  url: `${SITE}/tracks`,
@@ -3118,6 +3118,23 @@ var init_src = __esm(() => {
3118
3118
  title: "Track crawler",
3119
3119
  weights: { status: "hidden" }
3120
3120
  },
3121
+ {
3122
+ command: "fluncle admin backfills apple-releases",
3123
+ exposedContent: [
3124
+ "tap Apple Music's latest releases for the operator's enabled seed labels → day-one uncertified catalogue rows"
3125
+ ],
3126
+ kind: "cron",
3127
+ name: "cron.apple-releases",
3128
+ operatorNotes: "every 24h, run by a rave-02 HOST systemd timer (docs/agents/hermes/apple-releases-timer/). The MusicKit FRESHNESS TAP (D8): MusicBrainz WALKS the graph (cron.crawl) but lags a release ~2 weeks; Apple has it day one, so this taps each ENABLED seed label's latest releases and mints METADATA-ONLY catalogue rows (a `tracks` row with no `findings` row) with their real release dates — closing the /fresh lag cliff. It certifies nothing, publishes nothing, and never widens the graph (no new labels, no artist hops). Deduped against the MB crawl from both directions (ISRC + same-album title fold). Worker-paced (the three APPLE_MUSIC_* secrets live on the Worker); a NO-OP until they are provisioned. Zero LLM tokens. Source: docs/agents/hermes/scripts/apple-releases-sweep.*. See docs/catalogue-crawler.md.",
3129
+ probeConfig: {
3130
+ cadenceMs: 24 * 60 * MINUTE_MS,
3131
+ cronName: "fluncle-apple-releases",
3132
+ kind: "cron"
3133
+ },
3134
+ statusDescription: "taps day-one releases from the enabled labels",
3135
+ title: "Freshness tap",
3136
+ weights: { status: "hidden" }
3137
+ },
3121
3138
  {
3122
3139
  command: "fluncle admin catalogue rank",
3123
3140
  exposedContent: [
@@ -4093,6 +4110,7 @@ __export(exports_admin_tracks, {
4093
4110
  backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
4094
4111
  backfillLastfmCommand: () => backfillLastfmCommand,
4095
4112
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4113
+ backfillAppleReleasesCommand: () => backfillAppleReleasesCommand,
4096
4114
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4097
4115
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
4098
4116
  });
@@ -4278,6 +4296,10 @@ async function backfillAppleCatalogueCommand(limit, dryRun) {
4278
4296
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4279
4297
  return adminApiPost(`/api/admin/backfill/apple-catalogue?${params.toString()}`);
4280
4298
  }
4299
+ async function backfillAppleReleasesCommand(limit, dryRun) {
4300
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4301
+ return adminApiPost(`/api/admin/backfill/apple-releases?${params.toString()}`);
4302
+ }
4281
4303
  async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4282
4304
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4283
4305
  if (cursor) {
@@ -6293,6 +6315,7 @@ var init_admin_reach = __esm(() => {
6293
6315
  var exports_admin_artists = {};
6294
6316
  __export(exports_admin_artists, {
6295
6317
  resolveArtistCommand: () => resolveArtistCommand,
6318
+ rankArtistsCommand: () => rankArtistsCommand,
6296
6319
  listArtistsCommand: () => listArtistsCommand,
6297
6320
  draftArtistBioCommand: () => draftArtistBioCommand,
6298
6321
  describeArtistCommand: () => describeArtistCommand,
@@ -6331,6 +6354,11 @@ async function listArtistsCommand(limit, cursor) {
6331
6354
  async function resolveArtistCommand(artistId) {
6332
6355
  return adminApiPost(`/api/admin/artists/${encodeURIComponent(artistId)}/resolve`);
6333
6356
  }
6357
+ async function rankArtistsCommand(options) {
6358
+ const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
6359
+ const response = await adminApiPost("/api/admin/artists/rank", limit ? { limit } : {});
6360
+ return { summary: response.summary };
6361
+ }
6334
6362
  async function backfillArtistImagesCommand(limit, dryRun, cursor) {
6335
6363
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
6336
6364
  if (cursor) {
@@ -9651,6 +9679,10 @@ function addAdminCommands(program2) {
9651
9679
  const { backfillAppleCatalogueCommand: backfillAppleCatalogueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9652
9680
  await runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2);
9653
9681
  });
9682
+ backfill.command("apple-releases").description("Tap Apple's latest releases for enabled seed labels into catalogue rows").option("--dry-run", "Report the labels that would be probed without any Apple call", false).option("--limit <limit>", "Max enabled seed labels to probe per pass", "5").option("--json", "Print JSON", false).action(async (options) => {
9683
+ const { backfillAppleReleasesCommand: backfillAppleReleasesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9684
+ await runBackfillAppleReleases(options, backfillAppleReleasesCommand2);
9685
+ });
9654
9686
  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) => {
9655
9687
  const { backfillArtistsCommand: backfillArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9656
9688
  await runBackfillArtists(options, backfillArtistsCommand2);
@@ -9693,6 +9725,10 @@ function addAdminCommands(program2) {
9693
9725
  const { describeArtistCommand: describeArtistCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9694
9726
  await runEntityDescribe("artist", slug, options, describeArtistCommand2);
9695
9727
  });
9728
+ artists.command("rank").description("Run one tick of the similar-artists sweep (artist centroids + nearest neighbours)").option("--limit <limit>", "Stale artists per tick (default 100)").option("--json", "Print JSON", false).action(async (options) => {
9729
+ const { rankArtistsCommand: rankArtistsCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9730
+ await runArtistsRank(options, rankArtistsCommand2);
9731
+ });
9696
9732
  artists.command("draft-bio").description("Assemble the Worker-grounded bio prompt for an artist (the sweep's trigger)").argument("<slug>").option("--json", "Print JSON", false).allowExcessArguments().action(async (slug, options) => {
9697
9733
  const { draftArtistBioCommand: draftArtistBioCommand2 } = await Promise.resolve().then(() => (init_admin_artists(), exports_admin_artists));
9698
9734
  await runEntityBioDraft("artist", slug, options, draftArtistBioCommand2);
@@ -10340,6 +10376,80 @@ async function runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2
10340
10376
  process.exitCode = 1;
10341
10377
  }
10342
10378
  }
10379
+ async function runBackfillAppleReleases(options, backfillAppleReleasesCommand2) {
10380
+ const perPass = parseListLimit(options.limit);
10381
+ const newTrackIds = [];
10382
+ const resolvedLabels = [];
10383
+ const unresolvedLabels = [];
10384
+ let dryRun = options.dryRun;
10385
+ let throttled = false;
10386
+ let breakerTripped = false;
10387
+ let configured = true;
10388
+ let labelsProbed = 0;
10389
+ let albumsSeen = 0;
10390
+ let newRows = 0;
10391
+ let skippedKnown = 0;
10392
+ const MAX_PASSES = 100;
10393
+ for (let pass = 0;pass < MAX_PASSES; pass += 1) {
10394
+ const result = await backfillAppleReleasesCommand2(perPass, options.dryRun);
10395
+ dryRun = result.dryRun;
10396
+ configured = result.configured;
10397
+ labelsProbed += result.labelsProbed;
10398
+ albumsSeen += result.albumsSeen;
10399
+ newRows += result.newRows;
10400
+ skippedKnown += result.skippedKnown;
10401
+ newTrackIds.push(...result.newTrackIds);
10402
+ resolvedLabels.push(...result.resolvedLabels);
10403
+ unresolvedLabels.push(...result.unresolvedLabels);
10404
+ if (!options.json) {
10405
+ const verb2 = result.dryRun ? "would probe" : "probed";
10406
+ console.log(` \u2026${verb2} ${result.dryRun ? result.resolvedLabels.length : result.labelsProbed} label(s); ${result.newRows} new; ${result.skippedKnown} known; ${result.unresolvedLabels.length} unresolved`);
10407
+ }
10408
+ if (!result.configured) {
10409
+ break;
10410
+ }
10411
+ if (result.breakerTripped) {
10412
+ breakerTripped = true;
10413
+ break;
10414
+ }
10415
+ if (result.rateLimited) {
10416
+ throttled = true;
10417
+ break;
10418
+ }
10419
+ if (result.dryRun) {
10420
+ break;
10421
+ }
10422
+ if (result.labelsProbed === 0 && result.resolvedLabels.length === 0) {
10423
+ break;
10424
+ }
10425
+ }
10426
+ if (options.json) {
10427
+ printSweepJson({
10428
+ albumsSeen,
10429
+ breakerTripped,
10430
+ configured,
10431
+ dryRun,
10432
+ labelsProbed,
10433
+ newRows,
10434
+ newTrackIds,
10435
+ rateLimited: throttled,
10436
+ resolvedLabels,
10437
+ skippedKnown,
10438
+ unresolvedLabels
10439
+ }, 0);
10440
+ return;
10441
+ }
10442
+ if (!configured) {
10443
+ console.log("Apple releases tap is not configured (the Worker's MusicKit secrets are unset) \u2014 nothing probed.");
10444
+ return;
10445
+ }
10446
+ const verb = dryRun ? "Would probe" : "Probed";
10447
+ const probedCount = dryRun ? resolvedLabels.length : labelsProbed;
10448
+ console.log(`${verb} ${probedCount} enabled seed label(s); ${newRows} new catalogue row(s); ${skippedKnown} already known; ${resolvedLabels.length} resolved; ${unresolvedLabels.length} unresolved.`);
10449
+ for (const slug of unresolvedLabels) {
10450
+ console.log(` no exact Apple label match: ${slug}`);
10451
+ }
10452
+ }
10343
10453
  async function runBackfillArtists(options, backfillArtistsCommand2) {
10344
10454
  const limit = parseListLimit(options.limit);
10345
10455
  const upserted = [];
@@ -11521,6 +11631,15 @@ async function runGalaxies(slug, options, commands) {
11521
11631
  console.log(`${galaxy.name.padEnd(40)} ${String(galaxy.memberCount).padStart(3)} finding${galaxy.memberCount === 1 ? "" : "s"}`);
11522
11632
  }
11523
11633
  }
11634
+ async function runArtistsRank(options, rankArtistsCommand2) {
11635
+ const { summary } = await rankArtistsCommand2({ limit: options.limit });
11636
+ if (options.json) {
11637
+ printJson({ ok: true, summary });
11638
+ return;
11639
+ }
11640
+ const removed = summary.centroidsRemoved > 0 ? ` Purged ${summary.centroidsRemoved} orphan(s).` : "";
11641
+ console.log(`Ranked ${summary.centroidsComputed} artist(s); wrote ${summary.edgesWritten} edges.${removed} ${summary.remaining} still stale.`);
11642
+ }
11524
11643
  async function runCatalogueRank(options, catalogueRankCommand2) {
11525
11644
  const { summary, telescope } = await catalogueRankCommand2({ limit: options.limit });
11526
11645
  if (options.json) {
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.172.0"
34
+ "version": "0.174.0"
35
35
  }