fluncle 0.181.0 → 0.182.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 +75 -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.181.0".trim() ? "0.181.0".trim() : "0.1.0";
564
+ currentVersion = "0.182.0".trim() ? "0.182.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.181.0".trim() ? "0.181.0".trim() : "0.1.0";
2263
+ currentVersion2 = "0.182.0".trim() ? "0.182.0".trim() : "0.1.0";
2264
2264
  });
2265
2265
 
2266
2266
  // ../../packages/registry/src/index.ts
@@ -3212,6 +3212,19 @@ var init_src = __esm(() => {
3212
3212
  title: "Recording MBIDs",
3213
3213
  weights: { status: "hidden" }
3214
3214
  },
3215
+ {
3216
+ command: "fluncle admin backfills artist-edges",
3217
+ exposedContent: [
3218
+ "fold each edge-less track's artists_json names onto existing artist identities → track_artists edges"
3219
+ ],
3220
+ kind: "cron",
3221
+ name: "cron.artist-edges",
3222
+ operatorNotes: "every 60m, run by a rave-02 HOST systemd timer (docs/agents/hermes/artist-edges-timer/). The track_artists GRAPH BACKFILL (RFC artist-primary-capture, slice 0): the graph is crawl-era-only (born 2026-07-15) — only ~12.3k of ~37.5k tracks carry edges — so this folds each edge-less track's `artists_json` NAMES onto EXISTING `artists` identities and writes the `track_artists` edges, making the graph as full as honest matching allows (slice 1's identity-keyed capture authorization reads it). The matcher is IDENTITY-HONEST: each name matches by exact case-insensitive fold, then via `artist_aliases` (kind='name', status auto|confirmed — the search resolver's alias semantics); a fold two distinct identities share is ambiguous and matches nothing (fail-closed). It MINTS NOTHING — a bare name is not enough identity to create an entity — and reports the UNMATCHED RESIDUAL (credited names with no identity), which decides whether a later paced MusicBrainz credit-sweep is worth running. METADATA / GRAPH IDENTITY ONLY — it certifies nothing and publishes nothing (agent tier, the `backfill_recording_mbids` precedent). Worker-paced with NO vendor call (pure DB set-based matching, so no rate limit / circuit breaker): one bounded batch per tick folds the whole ~1.8k-row artist+alias corpus into one in-memory map and matches each track batch against it. The `tracks` row carries the durable reliability state (the `artist_edges_backfilled_at` stamp on EVERY visited row — matched, partial, or zero — so the worklist drains and a re-run is a no-op). New tracks are minted WITH edges (publish path + crawler link), so this catches history up and drains in a handful of ticks. Zero LLM tokens. Source: docs/agents/hermes/scripts/artist-edges-sweep.*. See docs/artist-relationship.md.",
3223
+ probeConfig: { cadenceMs: 60 * MINUTE_MS, cronName: "fluncle-artist-edges", kind: "cron" },
3224
+ statusDescription: "folds artists_json names onto artist identities",
3225
+ title: "Artist edges",
3226
+ weights: { status: "hidden" }
3227
+ },
3215
3228
  {
3216
3229
  command: "fluncle admin backfills label-lineage",
3217
3230
  exposedContent: [
@@ -4136,6 +4149,7 @@ __export(exports_admin_tracks, {
4136
4149
  backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
4137
4150
  backfillLastfmCommand: () => backfillLastfmCommand,
4138
4151
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4152
+ backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
4139
4153
  backfillAppleMusicCommand: () => backfillAppleMusicCommand,
4140
4154
  backfillAppleCatalogueCommand: () => backfillAppleCatalogueCommand
4141
4155
  });
@@ -4328,6 +4342,13 @@ async function backfillRecordingMbidsCommand(limit, dryRun, cursor) {
4328
4342
  }
4329
4343
  return adminApiPost(`/api/admin/backfill/recording-mbids?${params.toString()}`);
4330
4344
  }
4345
+ async function backfillArtistEdgesCommand(limit, dryRun, cursor) {
4346
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4347
+ if (cursor) {
4348
+ params.set("cursor", cursor);
4349
+ }
4350
+ return adminApiPost(`/api/admin/backfill/artist-edges?${params.toString()}`);
4351
+ }
4331
4352
  async function vehiclesCommand(limit) {
4332
4353
  const tracks = await fetchAdminTracks({ hasVideo: true, max: limit, order: "desc" });
4333
4354
  return tracks.map((track) => ({
@@ -9741,6 +9762,10 @@ function addAdminCommands(program2) {
9741
9762
  const { backfillRecordingMbidsCommand: backfillRecordingMbidsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9742
9763
  await runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2);
9743
9764
  });
9765
+ backfill.command("artist-edges").description("Fold artists_json names onto existing artist identities \u2192 track_artists").option("--dry-run", "Report the classification without writing edges or stamps", false).option("--limit <limit>", "Max tracks to visit", "200").option("--json", "Print JSON", false).action(async (options) => {
9766
+ const { backfillArtistEdgesCommand: backfillArtistEdgesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
9767
+ await runBackfillArtistEdges(options, backfillArtistEdgesCommand2);
9768
+ });
9744
9769
  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) => {
9745
9770
  const { backfillCoverMastersCommand: backfillCoverMastersCommand2 } = await Promise.resolve().then(() => (init_admin_covers(), exports_admin_covers));
9746
9771
  await runBackfillCoverMasters(options, backfillCoverMastersCommand2);
@@ -10740,6 +10765,54 @@ async function runBackfillRecordingMbids(options, backfillRecordingMbidsCommand2
10740
10765
  process.exitCode = 1;
10741
10766
  }
10742
10767
  }
10768
+ async function runBackfillArtistEdges(options, backfillArtistEdgesCommand2) {
10769
+ const limit = parseListLimit(options.limit) ?? 200;
10770
+ const fullyMatched = [];
10771
+ const partiallyMatched = [];
10772
+ const zeroMatched = [];
10773
+ let cursor;
10774
+ let dryRun = options.dryRun;
10775
+ let edgesWritten = 0;
10776
+ let scanned = 0;
10777
+ let unmatchedNames = 0;
10778
+ while (scanned < limit) {
10779
+ const remaining = limit - scanned;
10780
+ const result = await backfillArtistEdgesCommand2(remaining, options.dryRun, cursor);
10781
+ dryRun = result.dryRun;
10782
+ scanned += result.scanned;
10783
+ edgesWritten += result.edgesWritten;
10784
+ unmatchedNames += result.unmatchedNames;
10785
+ fullyMatched.push(...result.fullyMatched);
10786
+ partiallyMatched.push(...result.partiallyMatched);
10787
+ zeroMatched.push(...result.zeroMatched);
10788
+ if (!options.json) {
10789
+ const verb2 = result.dryRun ? "would write" : "wrote";
10790
+ console.log(` \u2026visited ${result.scanned}; ${verb2} ${result.edgesWritten} edge(s); ${result.fullyMatchedCount} full, ${result.partiallyMatchedCount} partial, ${result.zeroMatchedCount} zero; ${result.unmatchedNames} name(s) unmatched`);
10791
+ }
10792
+ if (result.nextCursor === null) {
10793
+ break;
10794
+ }
10795
+ cursor = result.nextCursor;
10796
+ }
10797
+ if (options.json) {
10798
+ printJson({
10799
+ dryRun,
10800
+ edgesWritten,
10801
+ fullyMatched,
10802
+ fullyMatchedCount: fullyMatched.length,
10803
+ ok: true,
10804
+ partiallyMatched,
10805
+ partiallyMatchedCount: partiallyMatched.length,
10806
+ scanned,
10807
+ unmatchedNames,
10808
+ zeroMatched,
10809
+ zeroMatchedCount: zeroMatched.length
10810
+ });
10811
+ return;
10812
+ }
10813
+ const verb = dryRun ? "Would write" : "Wrote";
10814
+ 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.`);
10815
+ }
10743
10816
  async function runArtistResolveQueue(options, listArtistsCommand2) {
10744
10817
  const limit = parseListLimit(options.limit);
10745
10818
  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.181.0"
34
+ "version": "0.182.0"
35
35
  }