fluncle 0.185.0 → 0.186.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 +78 -2
  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.185.0".trim() ? "0.185.0".trim() : "0.1.0";
564
+ currentVersion = "0.186.0".trim() ? "0.186.0".trim() : "0.1.0";
565
565
  });
566
566
 
567
567
  // src/update-notifier.ts
@@ -2442,7 +2442,7 @@ function parseVersion2(version) {
2442
2442
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2443
2443
  var init_version2 = __esm(() => {
2444
2444
  init_output();
2445
- currentVersion2 = "0.185.0".trim() ? "0.185.0".trim() : "0.1.0";
2445
+ currentVersion2 = "0.186.0".trim() ? "0.186.0".trim() : "0.1.0";
2446
2446
  });
2447
2447
 
2448
2448
  // ../../packages/registry/src/index.ts
@@ -3457,6 +3457,19 @@ var init_src = __esm(() => {
3457
3457
  title: "Artist edges",
3458
3458
  weights: { status: "hidden" }
3459
3459
  },
3460
+ {
3461
+ command: "fluncle admin backfills artist-credits",
3462
+ exposedContent: [
3463
+ "mint identity-true artists from MusicBrainz credits for slice 0's zero-matched residual → track_artists edges"
3464
+ ],
3465
+ kind: "cron",
3466
+ name: "cron.artist-credits",
3467
+ operatorNotes: "every 5m, run by a rave-02 HOST systemd timer (docs/agents/hermes/artist-credits-timer/). The MB CREDIT SWEEP (RFC artist-primary-capture, slice 1b): the sibling that completes what `backfill_artist_edges` (slice 0) could not. Slice 0's name-fold left a ~14.3k ZERO-MATCHED residual (a track it stamped but wrote no edge — no credited name folded to an existing identity), and those are exactly the tracks capture-authorization (slice 1) cannot reason about, since it matches BY IDENTITY through the `track_artists` graph. This sweep picks up that residual: for each zero-matched track carrying a MusicBrainz recording identity (`mb_recording_id`, or the `mb_<recording-mbid>` PK a crawler-born row carries), ONE paced `/recording/<mbid>?inc=artist-credits` lookup through the shared MusicBrainz client names its credited artists WITH their MB artist ids, and each resolves down a three-rung ladder: an EXACT `mbid` match, else an ADOPT (the credit name folds unambiguously onto an existing artist with no mbid — the common case, since the residual is dominated by compound credit strings like 'Sub Focus & Dimension' whose members Fluncle already holds as Spotify-keyed rows; adopting `coalesce`s the mbid onto that row instead of minting a duplicate, the split-identity guard), else a MINT of a fresh identity-true row (`mintArtistByMbid` — a real MBID is identity, the licence slice 0 lacked). Fail-closed on any ambiguity (a shared fold, or a fold whose row carries a different mbid) → mint, never a wrong merge. Then the `track_artists` edges are written. A zero-matched track with NO MB identity is TERMINALLY SKIPPED (stamped, never retried). METADATA / GRAPH IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_recording_mbids` precedent). Worker-paced (the box holds no MusicBrainz budget): a SMALL bounded batch per tick (each row is one ~1.1s MB call), 1 req/s, circuit-broken on a throttle, with a 60s response budget that pauses mid-page and resumes on the next request. The `tracks` row carries the durable per-row reliability state — its OWN `artist_credits_backfilled_at` stamp, DISTINCT from slice 0's `artist_edges_backfilled_at` (never disturbed). The ~14.3k residual drains at ~40/tick over ~a day and a half. Zero LLM tokens. Source: docs/agents/hermes/scripts/artist-credits-sweep.*. See docs/artist-relationship.md.",
3468
+ probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-artist-credits", kind: "cron" },
3469
+ statusDescription: "mints artists from MusicBrainz credits for the residual",
3470
+ title: "Artist credits",
3471
+ weights: { status: "hidden" }
3472
+ },
3460
3473
  {
3461
3474
  command: "fluncle admin backfills label-lineage",
3462
3475
  exposedContent: [
@@ -4382,6 +4395,7 @@ __export(exports_admin_tracks, {
4382
4395
  backfillLastfmCommand: () => backfillLastfmCommand,
4383
4396
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4384
4397
  backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
4398
+ backfillArtistCreditsCommand: () => backfillArtistCreditsCommand,
4385
4399
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4386
4400
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
4387
4401
  });
@@ -4581,6 +4595,13 @@ async function backfillArtistEdgesCommand(limit, dryRun, cursor) {
4581
4595
  }
4582
4596
  return adminApiPost(`/api/admin/backfill/artist-edges?${params.toString()}`);
4583
4597
  }
4598
+ async function backfillArtistCreditsCommand(limit, dryRun, cursor) {
4599
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4600
+ if (cursor) {
4601
+ params.set("cursor", cursor);
4602
+ }
4603
+ return adminApiPost(`/api/admin/backfill/artist-credits?${params.toString()}`);
4604
+ }
4584
4605
  async function vehiclesCommand(limit) {
4585
4606
  const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
4586
4607
  return tracks.map((track) => ({
@@ -10014,6 +10035,10 @@ function addAdminCommands(program2) {
10014
10035
  const { backfillArtistEdgesCommand: backfillArtistEdgesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10015
10036
  await runBackfillArtistEdges(options, backfillArtistEdgesCommand2);
10016
10037
  });
10038
+ backfill.command("artist-credits").description("Mint identity-true artists from MusicBrainz credits for the zero-matched residual").option("--dry-run", "Report the eligible worklist without any vendor call or write", false).option("--limit <limit>", "Max worklist rows to visit", "40").option("--json", "Print JSON", false).action(async (options) => {
10039
+ const { backfillArtistCreditsCommand: backfillArtistCreditsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10040
+ await runBackfillArtistCredits(options, backfillArtistCreditsCommand2);
10041
+ });
10017
10042
  backfill.command("cover-masters").description("Resolve owned \u22641200\xB2 cover masters (album/artist) into R2").option("--kind <kind>", "Which entity to drain: album | artist", "album").option("--dry-run", "Report the eligible worklist without any fetch or write", false).option("--retry-none", "First re-queue a bounded batch of terminal none rows to pending, then run the pass", false).option("--limit <limit>", "Max entities to process", "50").option("--json", "Print JSON", false).action(async (options) => {
10018
10043
  const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
10019
10044
  await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
@@ -11061,6 +11086,57 @@ async function runBackfillArtistEdges(options, backfillArtistEdgesCommand2) {
11061
11086
  const verb = dryRun ? "Would write" : "Wrote";
11062
11087
  console.log(`${verb} ${edgesWritten} track_artists edge(s) over ${scanned} track(s): ${fullyMatched.length} fully matched, ${partiallyMatched.length} partially, ${zeroMatched.length} with none; ${unmatchedNames} credited name(s) left unmatched.`);
11063
11088
  }
11089
+ async function runBackfillArtistCredits(options, backfillArtistCreditsCommand2) {
11090
+ const limit = parseListLimit(options.limit) ?? 40;
11091
+ let cursor;
11092
+ let dryRun = options.dryRun;
11093
+ let scanned = 0;
11094
+ let mintedArtists = 0;
11095
+ let matchedArtists = 0;
11096
+ let adoptedArtists = 0;
11097
+ let edgesWritten = 0;
11098
+ let skippedNoIdentity = 0;
11099
+ let throttled = false;
11100
+ while (scanned < limit) {
11101
+ const remaining = limit - scanned;
11102
+ const result = await backfillArtistCreditsCommand2(remaining, options.dryRun, cursor);
11103
+ dryRun = result.dryRun;
11104
+ scanned += result.scanned;
11105
+ mintedArtists += result.mintedArtists;
11106
+ matchedArtists += result.matchedArtists;
11107
+ adoptedArtists += result.adoptedArtists;
11108
+ edgesWritten += result.edgesWritten;
11109
+ skippedNoIdentity += result.skippedNoIdentity;
11110
+ if (!options.json) {
11111
+ const verb2 = result.dryRun ? "would visit" : "visited";
11112
+ console.log(` \u2026${verb2} ${result.scanned}; minted ${result.mintedArtists}, adopted ${result.adoptedArtists}, matched ${result.matchedArtists} artist(s); wrote ${result.edgesWritten} edge(s); ${result.skippedNoIdentity} without a MusicBrainz identity`);
11113
+ }
11114
+ if (result.rateLimited) {
11115
+ throttled = true;
11116
+ break;
11117
+ }
11118
+ if (result.nextCursor === null) {
11119
+ break;
11120
+ }
11121
+ cursor = result.nextCursor;
11122
+ }
11123
+ if (options.json) {
11124
+ printJson({
11125
+ adoptedArtists,
11126
+ dryRun,
11127
+ edgesWritten,
11128
+ matchedArtists,
11129
+ mintedArtists,
11130
+ ok: true,
11131
+ rateLimited: throttled,
11132
+ scanned,
11133
+ skippedNoIdentity
11134
+ });
11135
+ return;
11136
+ }
11137
+ const verb = dryRun ? "Would visit" : "Visited";
11138
+ console.log(`${verb} ${scanned} track(s): minted ${mintedArtists}, adopted ${adoptedArtists}, matched ${matchedArtists} artist(s); wrote ${edgesWritten} track_artists edge(s); ${skippedNoIdentity} carried no MusicBrainz identity.`);
11139
+ }
11064
11140
  async function runArtistResolveQueue(options, listArtistsCommand2) {
11065
11141
  const limit = parseListLimit(options.limit);
11066
11142
  const result = await listArtistsCommand2(limit ?? 50);
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.185.0"
34
+ "version": "0.186.0"
35
35
  }