fluncle 0.261.0 → 0.262.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 +79 -10
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -622,7 +622,7 @@ function parseVersion(version) {
622
622
  var currentVersion;
623
623
  var init_version = __esm(() => {
624
624
  init_output();
625
- currentVersion = "0.261.0".trim() ? "0.261.0".trim() : "0.1.0";
625
+ currentVersion = "0.262.0".trim() ? "0.262.0".trim() : "0.1.0";
626
626
  });
627
627
 
628
628
  // src/update-notifier.ts
@@ -2335,7 +2335,7 @@ function parseVersion2(version) {
2335
2335
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2336
2336
  var init_version2 = __esm(() => {
2337
2337
  init_output();
2338
- currentVersion2 = "0.261.0".trim() ? "0.261.0".trim() : "0.1.0";
2338
+ currentVersion2 = "0.262.0".trim() ? "0.262.0".trim() : "0.1.0";
2339
2339
  });
2340
2340
 
2341
2341
  // ../../packages/registry/src/index.ts
@@ -3459,12 +3459,12 @@ var init_src = __esm(() => {
3459
3459
  ],
3460
3460
  kind: "cron",
3461
3461
  name: "cron.label-releases",
3462
- operatorNotes: "daily at 07:20 Amsterdam, run by a Hermes 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.",
3462
+ operatorNotes: "every 15 minutes from 03:00 through 08:45 UTC, run by a Hermes 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.",
3463
3463
  probeConfig: {
3464
3464
  cadenceMs: 24 * 60 * MINUTE_MS,
3465
3465
  cronName: "fluncle-label-releases",
3466
3466
  kind: "cron",
3467
- schedule: { time: "07:20", tz: "Europe/Amsterdam" }
3467
+ schedule: { time: "03:00", tz: "UTC" }
3468
3468
  },
3469
3469
  statusDescription: "taps day-one releases from the enabled labels",
3470
3470
  title: "Freshness tap",
@@ -10193,7 +10193,7 @@ var init_zod = __esm(() => {
10193
10193
  });
10194
10194
 
10195
10195
  // ../../packages/contracts/src/orpc/admin-catalogue.ts
10196
- var CatalogueLensSchema, CapturePriorityReasonSchema, CatalogueMatchSchema, CatalogueTrackItemSchema, CatalogueSummarySchema, listCatalogueTracks, rankCatalogue, recordDemand, clearWrongAudio, requeueUnmatchedCaptures, requeueAnchor, requeueIsrcRecovery, flagWrongAudio, forceCapture, certifyTrack, setTrackDismissed, CaptureVerifyItemSchema, listUnverifiedCaptures, verifyCapture, CrawlPassSchema, CrawlPhaseInitializationSchema, MAX_CRAWL_PREPARE_LIMIT = 6, CrawlFetchPlanSchema, SuppliedCrawlBodySchema, MAX_CRAWL_COMMIT_BATCH, CRAWL_COMMIT_TOKEN_MAX_BYTES, CRAWL_COMMIT_BATCH_MAX_TOTAL_BYTES, CrawlCommitItemSchema, CrawlCommitReceiptSchema, CrawlPhaseCapabilitiesSchema, CrawlPhaseInputSchema, CrawlPhaseOutputSchema, CrawlStatusSchema, CrawlPipelineSummarySchema, PipelineWatchBoundedCountSchema, getPipeline, crawlCatalogue, commitCrawlNodes, getCrawlStatus, ANCHOR_CANDIDATE_LIMIT = 100, ANCHOR_ARTIST_LIMIT = 20, ANCHOR_TEXT_MAX = 300, ANCHOR_ISRC_MAX = 64, ANCHOR_ID_MAX = 64, ANCHOR_URL_MAX = 2048, AnchorCandidateSchema, anchorTrack, recordAnchorFailure, DEEZER_CANDIDATE_LIMIT = 5, DEEZER_TEXT_MAX = 300, DEEZER_ISRC_MAX = 64, DEEZER_TRACK_ID_MAX = 32, DeezerIsrcCandidateSchema, resolveAnchor, resolveAnchorReview, setAnchorSearch, setAnchorApify, CaptureBudgetStateSchema, getCaptureBudget, setCaptureBudget, AnchorApifyBudgetSchema, getAnchorApifyBudget, setAnchorApifyBudget, SpotifyAnchorBreakerStateSchema, AnchorRungFlagsSchema, getSpotifyAnchorBreaker, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
10196
+ var CatalogueLensSchema, CapturePriorityReasonSchema, CatalogueMatchSchema, CatalogueTrackItemSchema, CatalogueSummarySchema, listCatalogueTracks, rankCatalogue, recordDemand, clearWrongAudio, requeueUnmatchedCaptures, requeueAnchor, requeueIsrcRecovery, flagWrongAudio, forceCapture, certifyTrack, setTrackDismissed, CaptureVerifyItemSchema, listUnverifiedCaptures, verifyCapture, CrawlPassSchema, CrawlPhaseInitializationSchema, MAX_CRAWL_PREPARE_LIMIT = 6, CrawlFetchPlanSchema, SuppliedCrawlBodySchema, MAX_CRAWL_COMMIT_BATCH, CRAWL_COMMIT_TOKEN_MAX_BYTES, CRAWL_COMMIT_BATCH_MAX_TOTAL_BYTES, CrawlCommitItemSchema, CrawlCommitReceiptSchema, CrawlPhaseCapabilitiesSchema, CrawlPhaseInputSchema, CrawlPhaseOutputSchema, CrawlStatusSchema, CrawlPipelineSummarySchema, PipelineWatchBoundedCountSchema, getPipeline, crawlCatalogue, commitCrawlNodes, getCrawlStatus, ANCHOR_CANDIDATE_LIMIT = 100, ANCHOR_ARTIST_LIMIT = 20, ANCHOR_TEXT_MAX = 300, ANCHOR_ISRC_MAX = 64, ANCHOR_ID_MAX = 64, ANCHOR_URL_MAX = 2048, AnchorCandidateSchema, anchorTrack, recordAnchorFailure, DEEZER_CANDIDATE_LIMIT = 5, DEEZER_TEXT_MAX = 300, DEEZER_ISRC_MAX = 64, DEEZER_TRACK_ID_MAX = 32, DeezerIsrcCandidateSchema, resolveAnchor, resolveAnchorReview, setAnchorSearch, setAnchorApify, CaptureBudgetStateSchema, getCaptureBudget, setCaptureBudget, AnchorApifyBudgetSchema, getAnchorApifyBudget, setAnchorApifyBudget, SpotifyAnchorBreakerStateSchema, AnchorRungFlagsSchema, getSpotifyAnchorBreaker, LabelReleasesBudgetSchema, getLabelReleasesBudget, setLabelReleasesBudget, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
10197
10197
  var init_admin_catalogue = __esm(() => {
10198
10198
  init_dist3();
10199
10199
  init_zod();
@@ -10826,7 +10826,30 @@ var init_admin_catalogue = __esm(() => {
10826
10826
  path: "/admin/catalogue/anchor/breaker",
10827
10827
  summary: "The Spotify anchor-search throttle breaker: tripped, why, how long left, and the rungs",
10828
10828
  tags: ["Admin"]
10829
- }).input(object({})).output(SpotifyAnchorBreakerStateSchema.extend({ ok: literal(true), rungs: AnchorRungFlagsSchema }));
10829
+ }).input(object({})).output(SpotifyAnchorBreakerStateSchema.extend({
10830
+ ok: literal(true),
10831
+ rungs: AnchorRungFlagsSchema,
10832
+ spotifyDailyCalls: number2().int().nullable()
10833
+ }));
10834
+ LabelReleasesBudgetSchema = object({
10835
+ callsSpent: number2().int().nonnegative(),
10836
+ dailyBudget: number2().int().nonnegative(),
10837
+ spotifyDailyCalls: number2().int().nonnegative()
10838
+ });
10839
+ getLabelReleasesBudget = oc.route({
10840
+ method: "GET",
10841
+ operationId: "getLabelReleasesBudget",
10842
+ path: "/admin/catalogue/label-releases-budget",
10843
+ summary: "Read the label releases tap daily budget and Spotify usage",
10844
+ tags: ["Admin"]
10845
+ }).input(object({})).output(LabelReleasesBudgetSchema.extend({ ok: literal(true) }));
10846
+ setLabelReleasesBudget = oc.route({
10847
+ method: "PUT",
10848
+ operationId: "setLabelReleasesBudget",
10849
+ path: "/admin/catalogue/label-releases-budget",
10850
+ summary: "Set the label releases tap daily budget (operator)",
10851
+ tags: ["Admin"]
10852
+ }).input(object({ dailyBudget: number2().int().min(0).max(1e6) })).output(LabelReleasesBudgetSchema.extend({ ok: literal(true) }));
10830
10853
  resetSpotifyAnchorBreaker = oc.route({
10831
10854
  method: "POST",
10832
10855
  operationId: "resetSpotifyAnchorBreaker",
@@ -10858,6 +10881,7 @@ var init_admin_catalogue = __esm(() => {
10858
10881
  get_anchor_apify_budget: getAnchorApifyBudget,
10859
10882
  get_capture_budget: getCaptureBudget,
10860
10883
  get_crawl_status: getCrawlStatus,
10884
+ get_label_releases_budget: getLabelReleasesBudget,
10861
10885
  get_pipeline: getPipeline,
10862
10886
  get_spotify_anchor_breaker: getSpotifyAnchorBreaker,
10863
10887
  list_catalogue_tracks: listCatalogueTracks,
@@ -10876,6 +10900,7 @@ var init_admin_catalogue = __esm(() => {
10876
10900
  set_anchor_apify_budget: setAnchorApifyBudget,
10877
10901
  set_anchor_search: setAnchorSearch,
10878
10902
  set_capture_budget: setCaptureBudget,
10903
+ set_label_releases_budget: setLabelReleasesBudget,
10879
10904
  set_track_dismissed: setTrackDismissed,
10880
10905
  verify_capture: verifyCapture
10881
10906
  };
@@ -11928,6 +11953,13 @@ var init_admin_backfills = __esm(() => {
11928
11953
  })).output(object({
11929
11954
  albumsMatched: number2(),
11930
11955
  albumsSeen: number2(),
11956
+ blockedReason: _enum([
11957
+ "spotify_breaker",
11958
+ "spotify_budget",
11959
+ "spotify_budget_spent",
11960
+ "spotify_quota",
11961
+ "spotify_throttle"
11962
+ ]).nullable(),
11931
11963
  budgetPaused: boolean2(),
11932
11964
  configured: boolean2(),
11933
11965
  dryRun: boolean2(),
@@ -11935,14 +11967,20 @@ var init_admin_backfills = __esm(() => {
11935
11967
  failedLabels: array(string2()),
11936
11968
  fetchCeilingHit: boolean2(),
11937
11969
  labelSlugs: array(string2()),
11970
+ labelsDue: number2(),
11938
11971
  labelsProbed: number2(),
11972
+ neverChecked: number2(),
11939
11973
  newRows: number2(),
11940
11974
  newTrackIds: array(string2()),
11941
11975
  ok: literal(true),
11976
+ quotaExceeded: boolean2(),
11942
11977
  rateLimited: boolean2(),
11978
+ retryAfterMs: number2(),
11943
11979
  skippedKnown: number2(),
11944
11980
  skippedUndated: number2(),
11945
11981
  skippedUngrounded: number2(),
11982
+ tapDailyBudget: number2().int(),
11983
+ tapDailyCallsSpent: number2().int(),
11946
11984
  tracksSkippedArtistRule: number2().optional()
11947
11985
  }));
11948
11986
  LabelImagesBackfillFailedSchema = object({
@@ -13602,7 +13640,7 @@ var init_admin_hub_counts = __esm(() => {
13602
13640
  });
13603
13641
 
13604
13642
  // ../../packages/contracts/src/orpc/admin-labels.ts
13605
- var LabelSeedStateSchema, LabelTriageVerdictSchema, LabelAdminItemSchema, listLabelsAdmin, updateLabel, listLabelArtistRules, replaceLabelArtistRules, MergeLabelResultSchema, mergeLabel, MB_LABEL_MBID_PATTERN, MintLabelOutcomeSchema, LabelTakeOverResultSchema, mintLabel, LabelAliasSourceSchema, LabelAliasKindSchema, LabelAliasCandidateSchema, listLabelAliases, confirmLabelAlias, rejectLabelAlias, DescribeLabelBodySchema, describeLabel, draftLabelBio, LabelBioWorkItemSchema, listLabelsMissingBio, TriageRuleProposalSchema, RecordLabelTriageBodySchema, recordLabelTriage, adminLabelsContract;
13643
+ var LabelSeedStateSchema, LabelTriageVerdictSchema, LabelAdminItemSchema, listLabelsAdmin, updateLabel, listLabelArtistRules, LABEL_ARTIST_RULES_MAX = 100, LABEL_TRIAGE_RULE_PROPOSALS_MAX, replaceLabelArtistRules, MergeLabelResultSchema, mergeLabel, MB_LABEL_MBID_PATTERN, MintLabelOutcomeSchema, LabelTakeOverResultSchema, mintLabel, LabelAliasSourceSchema, LabelAliasKindSchema, LabelAliasCandidateSchema, listLabelAliases, confirmLabelAlias, rejectLabelAlias, DescribeLabelBodySchema, describeLabel, draftLabelBio, LabelBioWorkItemSchema, listLabelsMissingBio, TriageRuleProposalSchema, RecordLabelTriageBodySchema, recordLabelTriage, adminLabelsContract;
13606
13644
  var init_admin_labels = __esm(() => {
13607
13645
  init_dist3();
13608
13646
  init_zod();
@@ -13655,6 +13693,7 @@ var init_admin_labels = __esm(() => {
13655
13693
  summary: "List one label's artist rules for future catalogue acquisition",
13656
13694
  tags: ["Admin"]
13657
13695
  }).input(object({ id: string2() })).output(object({ ok: literal(true), rules: array(ArtistRuleSchema) }));
13696
+ LABEL_TRIAGE_RULE_PROPOSALS_MAX = LABEL_ARTIST_RULES_MAX * 2;
13658
13697
  replaceLabelArtistRules = oc.route({
13659
13698
  method: "PUT",
13660
13699
  operationId: "replaceLabelArtistRules",
@@ -13663,7 +13702,7 @@ var init_admin_labels = __esm(() => {
13663
13702
  tags: ["Admin"]
13664
13703
  }).input(object({
13665
13704
  id: string2(),
13666
- rules: array(ArtistRuleInputSchema).max(100).superRefine((rules, context) => {
13705
+ rules: array(ArtistRuleInputSchema).max(LABEL_ARTIST_RULES_MAX).superRefine((rules, context) => {
13667
13706
  const seen = new Set;
13668
13707
  for (const [index, rule] of rules.entries()) {
13669
13708
  if (seen.has(rule.artistMbid)) {
@@ -13820,7 +13859,7 @@ var init_admin_labels = __esm(() => {
13820
13859
  reason: string2().optional(),
13821
13860
  residualOffLaneShare: number2().min(0).max(1).optional(),
13822
13861
  roundId: string2(),
13823
- rules: array(TriageRuleProposalSchema).optional(),
13862
+ rules: array(TriageRuleProposalSchema).max(LABEL_TRIAGE_RULE_PROPOSALS_MAX).optional(),
13824
13863
  verdict: LabelTriageVerdictSchema,
13825
13864
  verifyAgrees: boolean2().optional(),
13826
13865
  verifyEvidence: string2().optional()
@@ -15435,7 +15474,7 @@ var init_admin_tracks = __esm(() => {
15435
15474
  debtAware: string2().optional(),
15436
15475
  kind: TrackWorkKindSchema,
15437
15476
  limit: number3().int().min(1).max(250).default(50),
15438
- paidMode: _enum(["quota", "prior"]).optional(),
15477
+ paidMode: _enum(["quota", "prior", "unasked"]).optional(),
15439
15478
  scope: TrackWorkScopeSchema.default("all")
15440
15479
  })).output(object({
15441
15480
  capabilities: TrackWorkCapabilitiesSchema.optional(),
@@ -19297,6 +19336,12 @@ async function requeueAnchorCommand(trackIds) {
19297
19336
  async function requeueIsrcRecoveryCommand(input) {
19298
19337
  return adminApiPost("/api/v1/admin/catalogue/isrc-recovery/requeue", input);
19299
19338
  }
19339
+ async function labelReleasesBudgetCommand() {
19340
+ return adminApiGet("/api/v1/admin/catalogue/label-releases-budget");
19341
+ }
19342
+ async function setLabelReleasesBudgetCommand(dailyBudget) {
19343
+ return adminApiPut("/api/v1/admin/catalogue/label-releases-budget", { dailyBudget });
19344
+ }
19300
19345
  async function anchorApifyBudgetCommand() {
19301
19346
  return adminApiGet("/api/v1/admin/catalogue/anchor/apify-budget");
19302
19347
  }
@@ -23088,6 +23133,29 @@ JSON field reference:
23088
23133
  }
23089
23134
  console.log(`Re-queued ${requeued} of ${trackIds.length} row(s) \u2014 the next anchor tick picks them up at their real priority.`);
23090
23135
  });
23136
+ const labelReleasesBudget = catalogue.command("label-releases-budget").description("Read or set the label releases tap daily Spotify call budget");
23137
+ labelReleasesBudget.option("--json", "Print JSON", false).action(async (options) => {
23138
+ await Promise.resolve().then(() => init_admin_catalogue2());
23139
+ const state = await labelReleasesBudgetCommand();
23140
+ if (options.json) {
23141
+ printJson({ ...state, ok: true });
23142
+ return;
23143
+ }
23144
+ console.log(`LABEL RELEASES BUDGET \u2014 ${state.callsSpent}/${state.dailyBudget} tap calls today \xB7 ${state.spotifyDailyCalls} Spotify calls total`);
23145
+ });
23146
+ labelReleasesBudget.command("set").requiredOption("--calls <count>", "Daily tap Spotify call budget").option("--json", "Print JSON", false).action(async (options) => {
23147
+ await Promise.resolve().then(() => init_admin_catalogue2());
23148
+ const calls = Number(options.calls);
23149
+ if (!Number.isSafeInteger(calls) || calls < 0 || calls > 1e6) {
23150
+ throw new Error("--calls must be an integer from 0 to 1000000");
23151
+ }
23152
+ const state = await setLabelReleasesBudgetCommand(calls);
23153
+ if (options.json) {
23154
+ printJson({ ...state, ok: true });
23155
+ return;
23156
+ }
23157
+ console.log(`LABEL RELEASES BUDGET \u2014 ${state.callsSpent}/${state.dailyBudget} tap calls today \xB7 ${state.spotifyDailyCalls} Spotify calls total`);
23158
+ });
23091
23159
  catalogue.command("anchor-breaker").description("Why the anchor rungs are quiet: the throttle breaker, and which rungs are armed").option("--json", "Print JSON", false).action(async (options) => {
23092
23160
  await Promise.resolve().then(() => init_admin_catalogue2());
23093
23161
  const state = await anchorBreakerCommand();
@@ -26432,6 +26500,7 @@ var stringOptions = new Set([
26432
26500
  "--bpm",
26433
26501
  "--bpm-confidence",
26434
26502
  "--bpm-source",
26503
+ "--calls",
26435
26504
  "--composition",
26436
26505
  "--consumer-digest",
26437
26506
  "--consumer-item-count",
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.261.0"
34
+ "version": "0.262.0"
35
35
  }