fluncle 0.181.0 → 0.183.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.
- package/bin/fluncle.mjs +76 -3
- 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.
|
|
564
|
+
currentVersion = "0.183.0".trim() ? "0.183.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.
|
|
2263
|
+
currentVersion2 = "0.183.0".trim() ? "0.183.0".trim() : "0.1.0";
|
|
2264
2264
|
});
|
|
2265
2265
|
|
|
2266
2266
|
// ../../packages/registry/src/index.ts
|
|
@@ -3134,7 +3134,7 @@ var init_src = __esm(() => {
|
|
|
3134
3134
|
],
|
|
3135
3135
|
kind: "cron",
|
|
3136
3136
|
name: "cron.label-releases",
|
|
3137
|
-
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.
|
|
3137
|
+
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. The WORKER does all of it (`backfill_label_releases`, agent tier): it searches the official Spotify API (`label:\"<name>\" tag:new`), reads each hit as a SINGLE `GET /albums/{id}` then `GET /tracks/{id}` (the batch endpoints are 403 at our tier), and mints. The box sweep is a thin HTTP TRIGGER that POSTs bounded passes with the agent token — no vendor token, no CLI dependency (a pinned box CLI missing a flag broke an earlier run). The gate, both required: artist-grounding (an album's Spotify artist already in `artists.spotify_artist_id` — the PRIMARY anchor that stops cross-genre homonym junk) AND an EXACT fold-match of the seed name in the ℗/© copyright; an album with no release_date is dropped outright (/fresh could never show it). BUDGET: the tap shares the official app's per-app window with the user-facing paths, so it paces itself against the shared call meter and stops at a FRACTION of the window (its own ceiling) — user write paths get the window, the tap takes only slack. Hitting that ceiling ends the pass cleanly and the durable per-label cadence stamps resume it next tick. 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). No vendor spend and zero LLM tokens. Source: docs/agents/hermes/scripts/label-releases-sweep.*. See docs/catalogue-crawler.md.",
|
|
3138
3138
|
probeConfig: {
|
|
3139
3139
|
cadenceMs: 24 * 60 * MINUTE_MS,
|
|
3140
3140
|
cronName: "fluncle-label-releases",
|
|
@@ -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