fluncle 0.263.0 → 0.265.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 +1401 -15
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -380,6 +380,12 @@ var require_main = __commonJS(function(exports, module) {
380
380
  });
381
381
 
382
382
  // src/env.ts
383
+ var exports_env = {};
384
+ __export(exports_env, {
385
+ getApiBaseUrl: () => getApiBaseUrl,
386
+ loadEnv: () => loadEnv,
387
+ readOptionalEnv: () => readOptionalEnv
388
+ });
383
389
  import { homedir } from "node:os";
384
390
  import { join } from "node:path";
385
391
  function loadEnv(keys) {
@@ -399,6 +405,10 @@ function loadEnv(keys) {
399
405
  }
400
406
  return result;
401
407
  }
408
+ function readOptionalEnv(key) {
409
+ loadConfig();
410
+ return process.env[key] || undefined;
411
+ }
402
412
  function getApiBaseUrl() {
403
413
  loadConfig();
404
414
  return (process.env.FLUNCLE_API_BASE_URL ?? "https://www.fluncle.com").replace(/\/+$/, "");
@@ -622,7 +632,7 @@ function parseVersion(version) {
622
632
  var currentVersion;
623
633
  var init_version = __esm(() => {
624
634
  init_output();
625
- currentVersion = "0.263.0".trim() ? "0.263.0".trim() : "0.1.0";
635
+ currentVersion = "0.265.0".trim() ? "0.265.0".trim() : "0.1.0";
626
636
  });
627
637
 
628
638
  // src/update-notifier.ts
@@ -2335,12 +2345,12 @@ function parseVersion2(version) {
2335
2345
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2336
2346
  var init_version2 = __esm(() => {
2337
2347
  init_output();
2338
- currentVersion2 = "0.263.0".trim() ? "0.263.0".trim() : "0.1.0";
2348
+ currentVersion2 = "0.265.0".trim() ? "0.265.0".trim() : "0.1.0";
2339
2349
  });
2340
2350
 
2341
2351
  // ../../packages/registry/src/index.ts
2342
2352
  function liveSurfaces() {
2343
- return SURFACES.filter((surface) => surface.pending !== true);
2353
+ return SURFACES.filter((surface) => surface.pending !== true && surface.dormant === undefined);
2344
2354
  }
2345
2355
  var SITE = "https://www.fluncle.com", PROBE_CADENCE_MS, PROBE_TIMEOUT_MS, MINUTE_MS, SURFACES;
2346
2356
  var init_src = __esm(() => {
@@ -3524,12 +3534,13 @@ var init_src = __esm(() => {
3524
3534
  weights: { status: "hidden" }
3525
3535
  },
3526
3536
  {
3537
+ dormant: "the iOS app has no users, and the mirror's embedded-replica sync of the main database halves the primary's write capacity while it runs",
3527
3538
  exposedContent: [
3528
3539
  "mirror the anchored, explicitly allowlisted public catalogue into the shared read-only device replica"
3529
3540
  ],
3530
3541
  kind: "cron",
3531
3542
  name: "cron.device-mirror",
3532
- operatorNotes: "hourly, run by a host systemd timer (docs/agents/hermes/device-mirror-timer/). Explicitly synchronizes one restart-safe local embedded replica, materializes the anchored track IDs once, derives and validates one allowlisted generation locally, then stages bounded changes and atomically cuts the complete verified generation into the shared device database. Sync, derivation, upload, validation, or cutover failure leaves the previous generation visible; rebuilding the target database remains forbidden because it would reset the libSQL replication log and force every device to bootstrap again. A schema-version mismatch stops for an operator migration. Zero LLM tokens. Source: docs/agents/hermes/scripts/device-mirror.*.",
3543
+ operatorNotes: "DORMANT: parked by docs/agents/hermes/device-mirror-timer/DORMANT, which keeps the installer from enabling its timer and keeps the healthcheck, /status, and the run-ledger roster from expecting it; revival steps are in that directory's README. When live: hourly, run by a host systemd timer (docs/agents/hermes/device-mirror-timer/). Explicitly synchronizes one restart-safe local embedded replica, materializes the anchored track IDs once, derives and validates one allowlisted generation locally, then stages bounded changes and atomically cuts the complete verified generation into the shared device database. Sync, derivation, upload, validation, or cutover failure leaves the previous generation visible; rebuilding the target database remains forbidden because it would reset the libSQL replication log and force every device to bootstrap again. A schema-version mismatch stops for an operator migration. Zero LLM tokens. Source: docs/agents/hermes/scripts/device-mirror.*.",
3533
3544
  probeConfig: {
3534
3545
  cadenceMs: 60 * MINUTE_MS,
3535
3546
  cronName: "fluncle-device-mirror",
@@ -3663,11 +3674,11 @@ var init_src = __esm(() => {
3663
3674
  {
3664
3675
  command: "fluncle admin tracks context --queue",
3665
3676
  exposedContent: [
3666
- "Firecrawl facts → distilled context_note + a Texture: line (Worker-side Haiku)"
3677
+ "Firecrawl facts → distilled context_note + a Texture: line (Worker-side OpenRouter distill)"
3667
3678
  ],
3668
3679
  kind: "cron",
3669
3680
  name: "cron.context-note",
3670
- operatorNotes: "every 5m. --no-agent trigger; the Worker does the Firecrawl + Haiku distill. Zero on-box tokens.",
3681
+ operatorNotes: "every 5m. --no-agent trigger; the Worker does the Firecrawl + OpenRouter distill. Zero on-box tokens.",
3671
3682
  probeConfig: { cadenceMs: 5 * MINUTE_MS, cronName: "fluncle-context-note", kind: "cron" },
3672
3683
  statusDescription: "distills the facts behind each finding",
3673
3684
  title: "Context notes",
@@ -3723,6 +3734,23 @@ var init_src = __esm(() => {
3723
3734
  title: "Label triage gate",
3724
3735
  weights: { status: "hidden" }
3725
3736
  },
3737
+ {
3738
+ exposedContent: [
3739
+ "nightly label-outlier review list — every album and label-less single of embedded catalogue tracks scored against the rest of its own label, the far ones flagged onto /admin/label-outliers (--no-agent)"
3740
+ ],
3741
+ kind: "cron",
3742
+ name: "cron.label-outliers",
3743
+ operatorNotes: "05:30 Amsterdam daily, retry slot 06:30. Reads its scoring inputs (embedded catalogue tracks with their raw vector bytes, artist credits, album Discogs styles) from the AGENT-tier list_label_outlier_inputs op in keyset pages of 1,000, each page its own admitted heavy-read phase, into a local file it scores and deletes (about 51 pages and 280 MB a night at 50k tracks; an admission yield mid-walk leaves the payload unstarted for the retry slot). It scores each album's mean cosine to its label's leave-it-out centroid as a robust z against that label's own spread (labels under 8 albums use the whole catalogue), and keeps the ones at z <= -4 whose artists have fewer than 3 typical tracks elsewhere and whose album Discogs does not file under Drum n Bass or Jungle. A corpus under 10,000 usable vectors or under 95% usable, or more than 2,000 flags, fails loudly and writes nothing. It then fires the AGENT-tier record_label_outliers op in its own admitted phase; the Worker refuses a duplicated, partial, or collapsed run (fewer than 80% of the live embedded catalogue), replaces the stored list, and returns every visible unit not yet announced. The sweep posts one Discord summary and only then acknowledges them by unit and fingerprint (acknowledge_label_outlier_alerts), so a lost response re-alerts next run. Dismissing is operator tier (set_label_outliers_dismissed) and holds until the album's tracks change. Zero LLM tokens. Source: docs/agents/hermes/scripts/label-outliers*.ts. See docs/catalogue-crawler.md.",
3744
+ probeConfig: {
3745
+ cadenceMs: 24 * 60 * MINUTE_MS,
3746
+ cronName: "fluncle-label-outliers",
3747
+ kind: "cron",
3748
+ schedule: { time: "05:30", tz: "Europe/Amsterdam" }
3749
+ },
3750
+ statusDescription: "flags albums that sound unlike their label",
3751
+ title: "Label outliers",
3752
+ weights: { status: "hidden" }
3753
+ },
3726
3754
  {
3727
3755
  command: "fluncle admin albums describe --queue",
3728
3756
  exposedContent: [
@@ -3874,7 +3902,7 @@ var init_src = __esm(() => {
3874
3902
  ],
3875
3903
  kind: "cron",
3876
3904
  name: "cron.healthcheck",
3877
- operatorNotes: "every 10m, run by a rave-02 host systemd timer (docs/agents/hermes/healthcheck-timer/) — decoupled from the Hermes cron gateway so the prober isn't starved by the scheduler it monitors. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads.",
3905
+ operatorNotes: "every 10m, run by a rave-02 host systemd timer (docs/agents/hermes/healthcheck-timer/) — decoupled from the Hermes cron gateway so the prober isn't starved by the scheduler it monitors. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads, and makes one record_database_write_probe call per tick that feeds database admission's write-latency guardrail.",
3878
3906
  probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-healthcheck", kind: "cron" },
3879
3907
  statusDescription: "the prober behind this very page",
3880
3908
  title: "Healthcheck prober",
@@ -4020,6 +4048,21 @@ var init_src = __esm(() => {
4020
4048
  title: "Funnel snapshot",
4021
4049
  weights: { status: "hidden" }
4022
4050
  },
4051
+ {
4052
+ exposedContent: [
4053
+ "Turso usage and overage reading — the organisation's current-cycle usage priced against the plan and projected to the reset, stored in the telemetry database for the /admin/costs Turso panel (--no-agent)"
4054
+ ],
4055
+ kind: "cron",
4056
+ name: "cron.turso-usage",
4057
+ operatorNotes: "Every 6 hours (OnUnitActiveSec), no retry slot: the counters are cumulative, so the next tick loses nothing. Reads the Turso Platform API (organization usage, database list, subscription, upcoming invoice) with TURSO_PLATFORM_API_TOKEN + TURSO_PLATFORM_ORG from the sweep secrets file, then fires the AGENT-tier record_turso_usage op. The Worker prices the reading with the versioned table in apps/web/src/lib/turso-pricing.ts (list rates, so an upper bound), projects the UTC-month cycle from the recent daily run-rate, stores it in the telemetry database, and claims a (cycle, level) alert row when the projected overage reaches the turso_usage_alert_threshold_usd setting (default $50) or twice it. The sweep posts one Discord message and only then acknowledges the levels (acknowledge_turso_usage_alerts), so a lost post re-sends next run. Missing credentials fail the run by name. Zero LLM tokens. Source: docs/agents/hermes/scripts/turso-usage-sweep.*. See docs/agents/hermes/turso-usage-timer/README.md.",
4058
+ operatorOnly: "the database bill is an operator concern; the job still reports to the prober, the run ledger, and the /admin/costs Turso panel, never to the public status board",
4059
+ probeConfig: {
4060
+ cadenceMs: 6 * 60 * MINUTE_MS,
4061
+ cronName: "fluncle-turso-usage",
4062
+ kind: "cron"
4063
+ },
4064
+ weights: {}
4065
+ },
4023
4066
  {
4024
4067
  exposedContent: [
4025
4068
  "daily per-post social-metrics snapshot — appends each published post's Postiz reach (views/likes/comments/…) into an append-only ledger, one row per post per day (--no-agent)"
@@ -4039,7 +4082,7 @@ var init_src = __esm(() => {
4039
4082
  },
4040
4083
  {
4041
4084
  exposedContent: [
4042
- "nightly codebase audit — one domain/night on a 7-day rotation; opens a PR the reviewer merges (claude -p, subscription auth)"
4085
+ "nightly codebase audit — one domain/night on an 8-domain rotation; opens a PR the reviewer merges (claude -p, subscription auth)"
4043
4086
  ],
4044
4087
  kind: "cron",
4045
4088
  name: "cron.audit",
@@ -4476,6 +4519,7 @@ var init_admin_attention = __esm(() => {
4476
4519
  "attach-cues": "cues",
4477
4520
  "bio-review": "bio gate",
4478
4521
  "capture-suspect": "capture check",
4522
+ "crawl-hold": "held release",
4479
4523
  distribute: "distribute",
4480
4524
  "drip-empty": "clip drip",
4481
4525
  "label-review": "label",
@@ -10193,7 +10237,7 @@ var init_zod = __esm(() => {
10193
10237
  });
10194
10238
 
10195
10239
  // ../../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, AnchorResolveResultSchema, AnchorCommitResultSchema, prepareAnchor, AnchorPhaseBatchItemFailureSchema, prepareAnchorBatch, resolveAnchorCandidate, commitAnchor, commitAnchorBatch, getAnchorReceipt, getAnchorPaidToken, cancelAnchorPaidResult, resolveAnchorPaidResult, resolveAnchor, resolveAnchorReview, setAnchorSearch, setAnchorApify, CaptureBudgetStateSchema, getCaptureBudget, setCaptureBudget, AnchorApifyBudgetSchema, getAnchorApifyBudget, setAnchorApifyBudget, SpotifyAnchorBreakerStateSchema, AnchorRungFlagsSchema, getSpotifyAnchorBreaker, LabelReleasesBudgetSchema, getLabelReleasesBudget, setLabelReleasesBudget, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
10240
+ 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, AnchorResolveResultSchema, AnchorCommitResultSchema, prepareAnchor, AnchorPhaseBatchItemFailureSchema, prepareAnchorBatch, resolveAnchorCandidate, commitAnchor, commitAnchorBatch, getAnchorReceipt, getAnchorPaidToken, cancelAnchorPaidResult, resolveAnchorPaidResult, resolveAnchor, resolveAnchorReview, CrawlHoldReasonSchema, CrawlHoldStateSchema, CrawlHoldSchema, listCrawlHolds, resolveCrawlHold, setAnchorSearch, setAnchorApify, CaptureBudgetStateSchema, getCaptureBudget, setCaptureBudget, AnchorApifyBudgetSchema, getAnchorApifyBudget, setAnchorApifyBudget, SpotifyAnchorBreakerStateSchema, AnchorRungFlagsSchema, getSpotifyAnchorBreaker, LabelReleasesBudgetSchema, getLabelReleasesBudget, setLabelReleasesBudget, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
10197
10241
  var init_admin_catalogue = __esm(() => {
10198
10242
  init_dist3();
10199
10243
  init_zod();
@@ -10222,6 +10266,7 @@ var init_admin_catalogue = __esm(() => {
10222
10266
  hasCapturedAudio: boolean2(),
10223
10267
  hasPreview: boolean2(),
10224
10268
  hiddenFromPublic: boolean2(),
10269
+ hiddenReason: _enum(["long_form", "spoken_word"]).nullable(),
10225
10270
  isrc: string2().nullable(),
10226
10271
  key: string2().nullable(),
10227
10272
  label: string2().nullable(),
@@ -10429,6 +10474,7 @@ var init_admin_catalogue = __esm(() => {
10429
10474
  seedsRearmed: number2(),
10430
10475
  tracksAllowedIn: number2().optional(),
10431
10476
  tracksFound: number2(),
10477
+ tracksHeldImplausible: number2().optional(),
10432
10478
  tracksSkipped: number2(),
10433
10479
  tracksSkippedArtistRule: number2().optional(),
10434
10480
  tracksSkippedHeld: number2().optional(),
@@ -10494,7 +10540,8 @@ var init_admin_catalogue = __esm(() => {
10494
10540
  limit: number2().int().min(1).max(MAX_CRAWL_PREPARE_LIMIT).default(2),
10495
10541
  maxHop: number2().int().min(0).max(3).default(2),
10496
10542
  phase: literal("prepare"),
10497
- sampleStorableRepair: boolean2().optional()
10543
+ sampleStorableRepair: boolean2().optional(),
10544
+ skipFrontierPendingCount: boolean2().optional()
10498
10545
  }),
10499
10546
  object({
10500
10547
  phase: literal("fetch"),
@@ -10519,7 +10566,7 @@ var init_admin_catalogue = __esm(() => {
10519
10566
  object({
10520
10567
  boxFetch: boolean2(),
10521
10568
  capabilities: CrawlPhaseCapabilitiesSchema.optional(),
10522
- frontierPending: number2(),
10569
+ frontierPending: number2().optional(),
10523
10570
  initialization: CrawlPhaseInitializationSchema,
10524
10571
  items: array(object({
10525
10572
  fetchPlan: CrawlFetchPlanSchema,
@@ -10863,6 +10910,48 @@ var init_admin_catalogue = __esm(() => {
10863
10910
  spotifyTrackId: string2().optional()
10864
10911
  })
10865
10912
  }));
10913
+ CrawlHoldReasonSchema = _enum(["before_founding", "before_label_era"]).meta({ id: "CrawlHoldReason" });
10914
+ CrawlHoldStateSchema = _enum(["held", "kept_out", "released"]).meta({ id: "CrawlHoldState" });
10915
+ CrawlHoldSchema = object({
10916
+ artists: array(string2()),
10917
+ createdAt: string2(),
10918
+ labelId: string2(),
10919
+ labelName: string2().nullable(),
10920
+ labelSlug: string2().nullable(),
10921
+ reason: CrawlHoldReasonSchema,
10922
+ releaseDate: string2().nullable(),
10923
+ releaseMbid: string2(),
10924
+ releaseTitle: string2().nullable(),
10925
+ state: CrawlHoldStateSchema,
10926
+ thresholdYear: number2().int(),
10927
+ trackCount: number2().int()
10928
+ }).meta({ id: "CrawlHold" });
10929
+ listCrawlHolds = oc.route({
10930
+ method: "GET",
10931
+ operationId: "listCrawlHolds",
10932
+ path: "/admin/catalogue/holds",
10933
+ summary: "Releases the crawl held back because their label credit looks implausible",
10934
+ tags: ["Admin"]
10935
+ }).input(object({
10936
+ cursor: string2().min(1).max(512).optional(),
10937
+ limit: number3().int().min(1).max(100).optional(),
10938
+ state: CrawlHoldStateSchema.optional()
10939
+ })).output(object({
10940
+ holds: array(CrawlHoldSchema),
10941
+ nextCursor: string2().optional(),
10942
+ ok: literal(true),
10943
+ total: number2().int()
10944
+ }));
10945
+ resolveCrawlHold = oc.route({
10946
+ method: "POST",
10947
+ operationId: "resolveCrawlHold",
10948
+ path: "/admin/catalogue/holds/{releaseMbid}/resolve",
10949
+ summary: "Rule on a held release: store it on the next crawl tick, or keep it out (operator)",
10950
+ tags: ["Admin"]
10951
+ }).input(object({
10952
+ decision: _enum(["keep_out", "store"]),
10953
+ releaseMbid: string2().min(1)
10954
+ })).output(object({ ok: literal(true), state: CrawlHoldStateSchema }));
10866
10955
  setAnchorSearch = oc.route({
10867
10956
  method: "PUT",
10868
10957
  operationId: "setAnchorSearch",
@@ -11018,6 +11107,7 @@ var init_admin_catalogue = __esm(() => {
11018
11107
  get_pipeline: getPipeline,
11019
11108
  get_spotify_anchor_breaker: getSpotifyAnchorBreaker,
11020
11109
  list_catalogue_tracks: listCatalogueTracks,
11110
+ list_crawl_holds: listCrawlHolds,
11021
11111
  list_unverified_captures: listUnverifiedCaptures,
11022
11112
  prepare_anchor: prepareAnchor,
11023
11113
  prepare_anchor_batch: prepareAnchorBatch,
@@ -11033,6 +11123,7 @@ var init_admin_catalogue = __esm(() => {
11033
11123
  resolve_anchor_candidate: resolveAnchorCandidate,
11034
11124
  resolve_anchor_paid_result: resolveAnchorPaidResult,
11035
11125
  resolve_anchor_review: resolveAnchorReview,
11126
+ resolve_crawl_hold: resolveCrawlHold,
11036
11127
  set_anchor_apify: setAnchorApify,
11037
11128
  set_anchor_apify_budget: setAnchorApifyBudget,
11038
11129
  set_anchor_search: setAnchorSearch,
@@ -11664,6 +11755,7 @@ var init_admin_attention2 = __esm(() => {
11664
11755
  "attach-cues",
11665
11756
  "bio-review",
11666
11757
  "capture-suspect",
11758
+ "crawl-hold",
11667
11759
  "distribute",
11668
11760
  "drip-empty",
11669
11761
  "label-review",
@@ -12411,7 +12503,7 @@ var init_admin_costs = __esm(() => {
12411
12503
  });
12412
12504
 
12413
12505
  // ../../packages/contracts/src/orpc/admin-database-admission.ts
12414
- var DatabaseAdmissionActionSchema, DatabaseAdmissionLaneSchema, DatabaseAdmissionOutcomeSchema, DatabaseAdmissionYieldReasonSchema, DatabaseAdmissionInputSchema, DatabaseAdmissionResponseSchema, coordinateDatabaseAdmission, adminDatabaseAdmissionContract;
12506
+ var DatabaseAdmissionActionSchema, DatabaseAdmissionLaneSchema, DatabaseAdmissionOutcomeSchema, DatabaseAdmissionYieldReasonSchema, DatabaseAdmissionInputSchema, DatabaseAdmissionResponseSchema, coordinateDatabaseAdmission, DatabaseWriteProbeOutcomeSchema, DatabaseWriteProbeResponseSchema, recordDatabaseWriteProbe, adminDatabaseAdmissionContract;
12415
12507
  var init_admin_database_admission = __esm(() => {
12416
12508
  init_dist3();
12417
12509
  init_zod();
@@ -12430,12 +12522,15 @@ var init_admin_database_admission = __esm(() => {
12430
12522
  "database-health",
12431
12523
  "direct-read-latency",
12432
12524
  "public-latency",
12433
- "queue"
12525
+ "queue",
12526
+ "write-latency"
12434
12527
  ]);
12435
12528
  DatabaseAdmissionInputSchema = object({
12436
12529
  action: DatabaseAdmissionActionSchema,
12437
12530
  fencingToken: number2().int().positive().optional(),
12531
+ notAfterMs: number2().int().nonnegative().optional(),
12438
12532
  owner: string2().min(1).max(64).regex(/^[a-z0-9][a-z0-9.-]*$/),
12533
+ protocolVersion: literal(2).optional(),
12439
12534
  runId: string2().min(1).max(96).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/)
12440
12535
  }).refine((input) => input.action === "acquire" || input.action === "cancel" || input.fencingToken !== undefined, "heartbeat and release require a fencing token");
12441
12536
  DatabaseAdmissionResponseSchema = object({
@@ -12447,10 +12542,12 @@ var init_admin_database_admission = __esm(() => {
12447
12542
  holdMs: number2().int().nonnegative(),
12448
12543
  lane: DatabaseAdmissionLaneSchema,
12449
12544
  leaseExpiresAtMs: number2().int().nonnegative().nullable(),
12545
+ leaseRemainingMs: number2().int().nonnegative().nullable(),
12450
12546
  operationId: string2().min(1).max(64),
12451
12547
  outcome: DatabaseAdmissionOutcomeSchema,
12452
12548
  queueAgeMs: number2().int().nonnegative(),
12453
12549
  recovered: boolean2(),
12550
+ retryAfterMs: number2().int().nonnegative().nullable(),
12454
12551
  waitMs: number2().int().nonnegative(),
12455
12552
  yieldReason: DatabaseAdmissionYieldReasonSchema.nullable()
12456
12553
  });
@@ -12461,8 +12558,23 @@ var init_admin_database_admission = __esm(() => {
12461
12558
  summary: "Coordinate one registry-classified recurring database operation",
12462
12559
  tags: ["Admin"]
12463
12560
  }).input(DatabaseAdmissionInputSchema).output(DatabaseAdmissionResponseSchema);
12561
+ DatabaseWriteProbeOutcomeSchema = _enum(["failed", "ok", "slow", "stalled"]);
12562
+ DatabaseWriteProbeResponseSchema = object({
12563
+ latencyMs: number2().int().nonnegative().nullable(),
12564
+ ok: literal(true),
12565
+ outcome: DatabaseWriteProbeOutcomeSchema,
12566
+ recorded: boolean2()
12567
+ });
12568
+ recordDatabaseWriteProbe = oc.route({
12569
+ method: "POST",
12570
+ operationId: "recordDatabaseWriteProbe",
12571
+ path: "/admin/database-admission/write-probe",
12572
+ summary: "Time one bounded write to the primary database and record it for admission",
12573
+ tags: ["Admin"]
12574
+ }).input(object({})).output(DatabaseWriteProbeResponseSchema);
12464
12575
  adminDatabaseAdmissionContract = {
12465
- coordinate_database_admission: coordinateDatabaseAdmission
12576
+ coordinate_database_admission: coordinateDatabaseAdmission,
12577
+ record_database_write_probe: recordDatabaseWriteProbe
12466
12578
  };
12467
12579
  });
12468
12580
 
@@ -13776,6 +13888,142 @@ var init_admin_hub_counts = __esm(() => {
13776
13888
  };
13777
13889
  });
13778
13890
 
13891
+ // ../../packages/contracts/src/orpc/admin-label-outliers.ts
13892
+ var MAX_RECORDED_LABEL_OUTLIERS = 2000, MAX_LABEL_OUTLIER_DISMISSALS = 500, LabelOutlierReferenceSchema, RecordedLabelOutlierSchema, LabelOutlierRunSchema, AlertedLabelOutlierSchema, PendingLabelOutlierAlertSchema, recordLabelOutliers, acknowledgeLabelOutlierAlerts, LabelOutlierTrackSchema, LabelOutlierItemSchema, listLabelOutliers, setLabelOutliersDismissed, LabelOutlierInputTrackSchema, LabelOutlierInputAlbumSchema, listLabelOutlierInputs, adminLabelOutliersContract;
13893
+ var init_admin_label_outliers = __esm(() => {
13894
+ init_dist3();
13895
+ init_zod();
13896
+ LabelOutlierReferenceSchema = _enum(["catalogue", "label"]);
13897
+ RecordedLabelOutlierSchema = object({
13898
+ albumId: string2().min(1).max(256).nullable(),
13899
+ artistSupport: number2().int().min(0),
13900
+ fingerprint: string2().min(1).max(128),
13901
+ labelId: string2().min(1).max(256).nullable(),
13902
+ reference: LabelOutlierReferenceSchema,
13903
+ referenceMedian: number2(),
13904
+ score: number2(),
13905
+ singleTrackId: string2().min(1).max(256).nullable(),
13906
+ trackCount: number2().int().min(1),
13907
+ unitId: string2().min(1).max(256),
13908
+ z: number2()
13909
+ }).meta({ id: "RecordedLabelOutlier" });
13910
+ LabelOutlierRunSchema = object({
13911
+ flagged: number2().int().min(0),
13912
+ labelsScored: number2().int().min(0),
13913
+ ranAt: string2(),
13914
+ replicaSyncedAt: string2().nullable(),
13915
+ tracksScored: number2().int().min(0),
13916
+ unitsScored: number2().int().min(0)
13917
+ }).meta({ id: "LabelOutlierRun" });
13918
+ AlertedLabelOutlierSchema = object({
13919
+ fingerprint: string2().min(1).max(128),
13920
+ unitId: string2().min(1).max(256)
13921
+ }).meta({ id: "AlertedLabelOutlier" });
13922
+ PendingLabelOutlierAlertSchema = object({
13923
+ albumName: string2().nullable(),
13924
+ labelName: string2().nullable(),
13925
+ title: string2(),
13926
+ unitId: string2()
13927
+ }).meta({ id: "PendingLabelOutlierAlert" });
13928
+ recordLabelOutliers = oc.route({
13929
+ method: "PUT",
13930
+ operationId: "recordLabelOutliers",
13931
+ path: "/admin/label-outliers",
13932
+ summary: "Replace the nightly label-outlier review list with the box's scored set",
13933
+ tags: ["Admin"]
13934
+ }).input(object({
13935
+ labelsScored: number2().int().min(0),
13936
+ outliers: array(RecordedLabelOutlierSchema).max(MAX_RECORDED_LABEL_OUTLIERS),
13937
+ replicaSyncedAt: string2().nullable(),
13938
+ totalFlagged: number2().int().min(0),
13939
+ tracksScored: number2().int().min(0),
13940
+ unitsScored: number2().int().min(0)
13941
+ })).output(object({
13942
+ flagged: number2().int(),
13943
+ ok: literal(true),
13944
+ pendingAlertUnits: array(AlertedLabelOutlierSchema),
13945
+ pendingAlerts: array(PendingLabelOutlierAlertSchema),
13946
+ removed: number2().int()
13947
+ }));
13948
+ acknowledgeLabelOutlierAlerts = oc.route({
13949
+ method: "PUT",
13950
+ operationId: "acknowledgeLabelOutlierAlerts",
13951
+ path: "/admin/label-outliers/alerts",
13952
+ summary: "Mark flagged label outliers as announced, once their Discord summary has landed",
13953
+ tags: ["Admin"]
13954
+ }).input(object({
13955
+ units: array(AlertedLabelOutlierSchema).min(1).max(MAX_RECORDED_LABEL_OUTLIERS)
13956
+ })).output(object({ acknowledged: number2().int(), ok: literal(true) }));
13957
+ LabelOutlierTrackSchema = object({
13958
+ artists: array(object({ name: string2(), slug: string2() })),
13959
+ title: string2(),
13960
+ trackId: string2()
13961
+ }).meta({ id: "LabelOutlierTrack" });
13962
+ LabelOutlierItemSchema = object({
13963
+ album: object({ id: string2(), name: string2(), slug: string2() }).nullable(),
13964
+ artistSupport: number2(),
13965
+ discogsStyles: array(string2()),
13966
+ dismissedAt: string2().nullable(),
13967
+ firstFlaggedAt: string2(),
13968
+ label: object({ id: string2(), name: string2(), slug: string2() }).nullable(),
13969
+ reference: LabelOutlierReferenceSchema,
13970
+ referenceMedian: number2(),
13971
+ score: number2(),
13972
+ trackCount: number2(),
13973
+ tracks: array(LabelOutlierTrackSchema),
13974
+ unitId: string2(),
13975
+ z: number2()
13976
+ }).meta({ id: "LabelOutlierItem" });
13977
+ listLabelOutliers = oc.route({
13978
+ method: "GET",
13979
+ operationId: "listLabelOutliers",
13980
+ path: "/admin/label-outliers",
13981
+ summary: "The label-outlier review list: albums and singles that sound unlike their label",
13982
+ tags: ["Admin"]
13983
+ }).output(object({
13984
+ items: array(LabelOutlierItemSchema),
13985
+ lastRun: LabelOutlierRunSchema.nullable(),
13986
+ ok: literal(true)
13987
+ }));
13988
+ setLabelOutliersDismissed = oc.route({
13989
+ method: "PUT",
13990
+ operationId: "setLabelOutliersDismissed",
13991
+ path: "/admin/label-outliers/dismissed",
13992
+ summary: "Mark flagged label outliers as looking fine, or put them back on the list (operator)",
13993
+ tags: ["Admin"]
13994
+ }).input(object({
13995
+ dismissed: boolean2(),
13996
+ unitIds: array(string2().min(1).max(256)).min(1).max(MAX_LABEL_OUTLIER_DISMISSALS)
13997
+ })).output(object({ changed: number2().int(), ok: literal(true) }));
13998
+ LabelOutlierInputTrackSchema = object({
13999
+ albumId: string2().nullable(),
14000
+ artistIds: array(string2()),
14001
+ embeddingBase64: string2(),
14002
+ labelId: string2().nullable(),
14003
+ trackId: string2()
14004
+ }).meta({ id: "LabelOutlierInputTrack" });
14005
+ LabelOutlierInputAlbumSchema = object({ discogsStyles: string2(), id: string2() }).meta({ id: "LabelOutlierInputAlbum" });
14006
+ listLabelOutlierInputs = oc.route({
14007
+ method: "GET",
14008
+ operationId: "listLabelOutlierInputs",
14009
+ path: "/admin/label-outliers/inputs",
14010
+ summary: "One keyset page of the label-outlier scoring inputs: embedded catalogue tracks with their raw vector bytes, artist credits, and album Discogs styles",
14011
+ tags: ["Admin"]
14012
+ }).input(object({ cursor: string2().max(512).optional(), limit: string2().optional() })).output(object({
14013
+ albums: array(LabelOutlierInputAlbumSchema),
14014
+ nextCursor: string2().nullable(),
14015
+ ok: literal(true),
14016
+ tracks: array(LabelOutlierInputTrackSchema)
14017
+ }));
14018
+ adminLabelOutliersContract = {
14019
+ acknowledge_label_outlier_alerts: acknowledgeLabelOutlierAlerts,
14020
+ list_label_outlier_inputs: listLabelOutlierInputs,
14021
+ list_label_outliers: listLabelOutliers,
14022
+ record_label_outliers: recordLabelOutliers,
14023
+ set_label_outliers_dismissed: setLabelOutliersDismissed
14024
+ };
14025
+ });
14026
+
13779
14027
  // ../../packages/contracts/src/orpc/admin-labels.ts
13780
14028
  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;
13781
14029
  var init_admin_labels = __esm(() => {
@@ -15824,6 +16072,145 @@ var init_admin_telemetry2 = __esm(() => {
15824
16072
  };
15825
16073
  });
15826
16074
 
16075
+ // ../../packages/contracts/src/orpc/admin-turso-usage.ts
16076
+ var MAX_TURSO_USAGE_DATABASES = 500, TURSO_USAGE_THRESHOLD_MIN_USD = 1, TURSO_USAGE_THRESHOLD_MAX_USD = 1e5, Count, CycleKey, TursoUsageResourceKeySchema, TursoUsageRateBasisSchema, TursoUsageTotalsSchema, TursoDatabaseUsageSchema, TursoUsageResourceSchema, TursoAttributedDatabaseSchema, TursoUsageSnapshotSchema, TursoUsageHistoryDaySchema, TursoUsageAlertSchema, TursoUsageAlertKeySchema, recordTursoUsage, getTursoUsage, acknowledgeTursoUsageAlerts, setTursoUsageThreshold, adminTursoUsageContract;
16077
+ var init_admin_turso_usage = __esm(() => {
16078
+ init_dist3();
16079
+ init_zod();
16080
+ Count = number2().int().min(0);
16081
+ CycleKey = string2().regex(/^\d{4}-\d{2}$/);
16082
+ TursoUsageResourceKeySchema = _enum([
16083
+ "rowsRead",
16084
+ "rowsWritten",
16085
+ "storage",
16086
+ "embeddedSyncs"
16087
+ ]);
16088
+ TursoUsageRateBasisSchema = _enum(["recent", "cycle-to-date", "none"]);
16089
+ TursoUsageTotalsSchema = object({
16090
+ bytesSynced: Count,
16091
+ rowsRead: Count,
16092
+ rowsWritten: Count,
16093
+ storageBytes: Count
16094
+ }).meta({ id: "TursoUsageTotals" });
16095
+ TursoDatabaseUsageSchema = TursoUsageTotalsSchema.extend({
16096
+ name: string2().min(1).max(256)
16097
+ }).meta({ id: "TursoDatabaseUsage" });
16098
+ TursoUsageResourceSchema = object({
16099
+ dailyRate: number2().min(0),
16100
+ included: number2().min(0),
16101
+ key: TursoUsageResourceKeySchema,
16102
+ overageUsd: number2().min(0),
16103
+ projectedOverageUsd: number2().min(0),
16104
+ projectedUsed: number2().min(0),
16105
+ unitSize: number2().positive(),
16106
+ usdPerUnit: number2().min(0),
16107
+ used: number2().min(0)
16108
+ }).meta({ id: "TursoUsageResource" });
16109
+ TursoAttributedDatabaseSchema = TursoDatabaseUsageSchema.extend({
16110
+ attributedOverageUsd: number2().min(0)
16111
+ }).meta({ id: "TursoAttributedDatabase" });
16112
+ TursoUsageSnapshotSchema = object({
16113
+ baseUsd: number2().min(0).nullable(),
16114
+ cycle: CycleKey,
16115
+ cycleEnd: string2(),
16116
+ cycleStart: string2(),
16117
+ databases: array(TursoAttributedDatabaseSchema),
16118
+ observedAt: string2(),
16119
+ overageUsd: number2().min(0),
16120
+ overagesEnabled: boolean2(),
16121
+ plan: string2(),
16122
+ priceSource: string2(),
16123
+ priceTableVersion: string2(),
16124
+ priced: boolean2(),
16125
+ projectedBillUsd: number2().min(0).nullable(),
16126
+ projectedOverageUsd: number2().min(0),
16127
+ rateBasis: TursoUsageRateBasisSchema,
16128
+ rateWindowHours: number2().min(0).nullable(),
16129
+ resources: array(TursoUsageResourceSchema),
16130
+ timeline: string2().nullable(),
16131
+ upcomingInvoiceUsd: number2().min(0).nullable()
16132
+ }).meta({ id: "TursoUsageSnapshot" });
16133
+ TursoUsageHistoryDaySchema = object({
16134
+ bytesSynced: Count,
16135
+ day: string2(),
16136
+ overageUsd: number2().min(0),
16137
+ projectedOverageUsd: number2().min(0),
16138
+ rowsRead: Count,
16139
+ rowsWritten: Count,
16140
+ storageBytes: Count
16141
+ }).meta({ id: "TursoUsageHistoryDay" });
16142
+ TursoUsageAlertSchema = object({
16143
+ cycle: CycleKey,
16144
+ deliveredAt: string2().nullable(),
16145
+ levelUsd: number2().positive(),
16146
+ projectedOverageUsd: number2().min(0),
16147
+ raisedAt: string2()
16148
+ }).meta({ id: "TursoUsageAlert" });
16149
+ TursoUsageAlertKeySchema = object({
16150
+ cycle: CycleKey,
16151
+ levelUsd: number2().positive()
16152
+ }).meta({ id: "TursoUsageAlertKey" });
16153
+ recordTursoUsage = oc.route({
16154
+ method: "POST",
16155
+ operationId: "recordTursoUsage",
16156
+ path: "/admin/costs/turso-usage",
16157
+ summary: "Record one Turso platform usage reading and price it against the plan",
16158
+ tags: ["Admin"]
16159
+ }).input(object({
16160
+ databases: array(TursoDatabaseUsageSchema).max(MAX_TURSO_USAGE_DATABASES),
16161
+ observedAt: string2().min(1).max(64),
16162
+ plan: object({
16163
+ name: string2().min(1).max(64),
16164
+ overages: boolean2(),
16165
+ timeline: string2().max(32).nullable()
16166
+ }),
16167
+ upcomingInvoiceUsd: number2().min(0).nullable(),
16168
+ usage: TursoUsageTotalsSchema
16169
+ })).output(object({
16170
+ ok: literal(true),
16171
+ pendingAlerts: array(TursoUsageAlertSchema),
16172
+ snapshot: TursoUsageSnapshotSchema,
16173
+ stored: boolean2(),
16174
+ thresholdUsd: number2().positive()
16175
+ }));
16176
+ getTursoUsage = oc.route({
16177
+ method: "GET",
16178
+ operationId: "getTursoUsage",
16179
+ path: "/admin/costs/turso-usage",
16180
+ summary: "The latest priced Turso usage reading, its per-day history, and this cycle's alerts (operator)",
16181
+ tags: ["Admin"]
16182
+ }).output(object({
16183
+ alerts: array(TursoUsageAlertSchema),
16184
+ available: boolean2(),
16185
+ history: array(TursoUsageHistoryDaySchema),
16186
+ latest: TursoUsageSnapshotSchema.nullable(),
16187
+ ok: literal(true),
16188
+ thresholdUsd: number2().positive()
16189
+ }));
16190
+ acknowledgeTursoUsageAlerts = oc.route({
16191
+ method: "PUT",
16192
+ operationId: "acknowledgeTursoUsageAlerts",
16193
+ path: "/admin/costs/turso-usage/alerts",
16194
+ summary: "Mark Turso overage alerts as delivered, once their Discord post has landed",
16195
+ tags: ["Admin"]
16196
+ }).input(object({ alerts: array(TursoUsageAlertKeySchema).min(1).max(20) })).output(object({ acknowledged: number2().int().min(0), ok: literal(true) }));
16197
+ setTursoUsageThreshold = oc.route({
16198
+ method: "PUT",
16199
+ operationId: "setTursoUsageThreshold",
16200
+ path: "/admin/costs/turso-usage/threshold",
16201
+ summary: "Set the projected Turso overage that raises a Discord alert (operator)",
16202
+ tags: ["Admin"]
16203
+ }).input(object({
16204
+ thresholdUsd: number2().min(TURSO_USAGE_THRESHOLD_MIN_USD).max(TURSO_USAGE_THRESHOLD_MAX_USD)
16205
+ })).output(object({ ok: literal(true), thresholdUsd: number2().positive() }));
16206
+ adminTursoUsageContract = {
16207
+ acknowledge_turso_usage_alerts: acknowledgeTursoUsageAlerts,
16208
+ get_turso_usage: getTursoUsage,
16209
+ record_turso_usage: recordTursoUsage,
16210
+ set_turso_usage_threshold: setTursoUsageThreshold
16211
+ };
16212
+ });
16213
+
15827
16214
  // ../../packages/contracts/src/orpc/admin-twitch.ts
15828
16215
  var recordLiveState, adminTwitchContract;
15829
16216
  var init_admin_twitch = __esm(() => {
@@ -16282,6 +16669,7 @@ var init_me_recs = __esm(() => {
16282
16669
  label: string2().optional(),
16283
16670
  logId: string2(),
16284
16671
  note: string2().optional(),
16672
+ previewable: boolean2(),
16285
16673
  similarity: number2(),
16286
16674
  spotifyUri: string2().optional(),
16287
16675
  spotifyUrl: string2().optional(),
@@ -16367,6 +16755,7 @@ var init_me_saved = __esm(() => {
16367
16755
  imageUrl: string2().optional(),
16368
16756
  logId: string2().optional(),
16369
16757
  note: string2().optional(),
16758
+ previewable: boolean2(),
16370
16759
  savedAt: string2(),
16371
16760
  title: string2(),
16372
16761
  trackId: string2()
@@ -17236,6 +17625,7 @@ var init_orpc = __esm(() => {
17236
17625
  init_admin_funnel();
17237
17626
  init_admin_health();
17238
17627
  init_admin_hub_counts();
17628
+ init_admin_label_outliers();
17239
17629
  init_admin_labels();
17240
17630
  init_admin_logbook();
17241
17631
  init_admin_migrations();
@@ -17250,6 +17640,7 @@ var init_orpc = __esm(() => {
17250
17640
  init_admin_tokens();
17251
17641
  init_admin_tracks();
17252
17642
  init_admin_telemetry2();
17643
+ init_admin_turso_usage();
17253
17644
  init_admin_twitch();
17254
17645
  init_admin_users();
17255
17646
  init_devices();
@@ -17282,6 +17673,7 @@ var init_orpc = __esm(() => {
17282
17673
  init_mix();
17283
17674
  init_galaxies2();
17284
17675
  init_graph();
17676
+ init_admin_label_outliers();
17285
17677
  init_admin_galaxies();
17286
17678
  init_admin_albums();
17287
17679
  init_admin_artifacts();
@@ -17299,6 +17691,7 @@ var init_orpc = __esm(() => {
17299
17691
  init_admin_notes();
17300
17692
  init_admin_observations();
17301
17693
  init_admin_costs();
17694
+ init_admin_turso_usage();
17302
17695
  init_admin_database_admission();
17303
17696
  init_admin_telemetry2();
17304
17697
  init_admin_health();
@@ -17367,6 +17760,7 @@ var init_orpc = __esm(() => {
17367
17760
  ...adminHealthContract,
17368
17761
  ...adminHubCountsContract,
17369
17762
  ...followDigestContract,
17763
+ ...adminLabelOutliersContract,
17370
17764
  ...adminLabelsContract,
17371
17765
  ...adminLogbookContract,
17372
17766
  ...adminMigrationsContract,
@@ -17381,6 +17775,7 @@ var init_orpc = __esm(() => {
17381
17775
  ...adminTokensContract,
17382
17776
  ...adminTracksContract,
17383
17777
  ...adminTelemetryContract,
17778
+ ...adminTursoUsageContract,
17384
17779
  ...adminTwitchContract,
17385
17780
  ...adminUsersContract,
17386
17781
  ...devicesContract,
@@ -17490,6 +17885,7 @@ async function advanceProjectionCommand(input) {
17490
17885
  let scheduled = response.scheduled;
17491
17886
  let wallStopped = false;
17492
17887
  let rebuildRowsWalked = response.rebuildRowsWalked ?? 0;
17888
+ let rebuildReported = response.rebuildStaleFamilies !== undefined;
17493
17889
  while (!response.complete && steps < maxSteps) {
17494
17890
  if (wallMs !== undefined && now() - startedAt >= wallMs) {
17495
17891
  wallStopped = true;
@@ -17500,8 +17896,9 @@ async function advanceProjectionCommand(input) {
17500
17896
  processed += response.processed;
17501
17897
  scheduled += response.scheduled;
17502
17898
  rebuildRowsWalked += response.rebuildRowsWalked ?? 0;
17899
+ rebuildReported ||= response.rebuildStaleFamilies !== undefined;
17503
17900
  }
17504
- const rebuild = response.rebuildStaleFamilies === undefined ? {} : { rebuildRowsWalked, rebuildStaleFamilies: response.rebuildStaleFamilies };
17901
+ const rebuild = rebuildReported ? { rebuildRowsWalked, rebuildStaleFamilies: response.rebuildStaleFamilies ?? 0 } : {};
17505
17902
  if (!includeTerminalStatus) {
17506
17903
  const { status: _status, ...withoutStatus } = response;
17507
17904
  return { ...withoutStatus, ...rebuild, processed, scheduled, steps, wallStopped };
@@ -19806,6 +20203,974 @@ var init_admin_covers = __esm(() => {
19806
20203
  init_api();
19807
20204
  });
19808
20205
 
20206
+ // src/evidence-http.ts
20207
+ import { createHash } from "node:crypto";
20208
+ import { mkdir as mkdir2, readFile as readFile2, rename, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
20209
+ import { homedir as homedir4 } from "node:os";
20210
+ import { join as join6 } from "node:path";
20211
+ function defaultCacheDir() {
20212
+ const base = process.env.XDG_CACHE_HOME || join6(homedir4(), ".cache");
20213
+ return join6(base, "fluncle", "label-evidence");
20214
+ }
20215
+ function createEvidenceHttp(overrides = {}) {
20216
+ return {
20217
+ cacheDir: defaultCacheDir(),
20218
+ cacheTtlMs: 7 * 24 * 60 * 60 * 1000,
20219
+ fetch: (url, init) => globalThis.fetch(url, init),
20220
+ now: () => Date.now(),
20221
+ policies: SOURCE_POLICIES,
20222
+ random: Math.random,
20223
+ refresh: false,
20224
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
20225
+ stats: { cacheHits: 0, requests: 0 },
20226
+ ...overrides
20227
+ };
20228
+ }
20229
+ function cachePath(http, source, key) {
20230
+ const digest = createHash("sha256").update(key).digest("hex");
20231
+ return join6(http.cacheDir, source, `${digest}.json`);
20232
+ }
20233
+ async function readCache2(http, source, key) {
20234
+ if (http.refresh) {
20235
+ return null;
20236
+ }
20237
+ try {
20238
+ const entry = JSON.parse(await readFile2(cachePath(http, source, key), "utf8"));
20239
+ if (entry.key !== key || http.now() - entry.fetchedAt > http.cacheTtlMs) {
20240
+ return null;
20241
+ }
20242
+ return entry.text;
20243
+ } catch {
20244
+ return null;
20245
+ }
20246
+ }
20247
+ async function writeCache2(http, source, key, text) {
20248
+ const path = cachePath(http, source, key);
20249
+ const temp = `${path}.${process.pid}.${Math.floor(http.random() * 1e9)}.tmp`;
20250
+ try {
20251
+ await mkdir2(join6(http.cacheDir, source), { recursive: true });
20252
+ await writeFile2(temp, JSON.stringify({ fetchedAt: http.now(), key, text }));
20253
+ await rename(temp, path);
20254
+ } catch {
20255
+ await rm(temp, { force: true }).catch(() => {
20256
+ return;
20257
+ });
20258
+ }
20259
+ }
20260
+ async function withSlotLock(http, source, critical) {
20261
+ const dir = join6(http.cacheDir, "slots");
20262
+ await mkdir2(dir, { recursive: true });
20263
+ const lock = join6(dir, `${source}.lock`);
20264
+ for (;; ) {
20265
+ try {
20266
+ await mkdir2(lock);
20267
+ break;
20268
+ } catch (error) {
20269
+ if (error.code !== "EEXIST") {
20270
+ throw error;
20271
+ }
20272
+ const held = await stat(lock).catch(() => null);
20273
+ if (held && Date.now() - held.mtimeMs > LOCK_STALE_MS) {
20274
+ const evicted = `${lock}.stale.${process.pid}.${Math.floor(Math.random() * 1e9)}`;
20275
+ const won = await rename(lock, evicted).then(() => true, () => false);
20276
+ if (won) {
20277
+ await rm(evicted, { force: true, recursive: true });
20278
+ }
20279
+ continue;
20280
+ }
20281
+ await new Promise((resolve) => setTimeout(resolve, 15 + Math.floor(Math.random() * 35)));
20282
+ }
20283
+ }
20284
+ try {
20285
+ return await critical(join6(dir, `${source}.next`));
20286
+ } finally {
20287
+ await rm(lock, { force: true, recursive: true });
20288
+ }
20289
+ }
20290
+ async function readNextSlot(slotFile) {
20291
+ const raw = await readFile2(slotFile, "utf8").catch(() => "0");
20292
+ const value = Number(raw);
20293
+ return Number.isFinite(value) ? value : 0;
20294
+ }
20295
+ async function reserveSlot(http, source) {
20296
+ const interval = http.policies[source].intervalMs;
20297
+ const slotAt = await withSlotLock(http, source, async (slotFile) => {
20298
+ const now = http.now();
20299
+ const slot = Math.max(now, await readNextSlot(slotFile));
20300
+ await writeFile2(slotFile, String(slot + interval));
20301
+ return slot;
20302
+ });
20303
+ const wait = slotAt - http.now();
20304
+ if (wait > 0) {
20305
+ await http.sleep(wait);
20306
+ }
20307
+ }
20308
+ async function pushSlotBack(http, source, until) {
20309
+ await withSlotLock(http, source, async (slotFile) => {
20310
+ if (await readNextSlot(slotFile) < until) {
20311
+ await writeFile2(slotFile, String(until));
20312
+ }
20313
+ });
20314
+ }
20315
+ function parseRetryAfter(value, now) {
20316
+ if (!value) {
20317
+ return null;
20318
+ }
20319
+ const seconds = Number(value);
20320
+ if (Number.isFinite(seconds)) {
20321
+ return Math.max(0, seconds * 1000);
20322
+ }
20323
+ const at = Date.parse(value);
20324
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
20325
+ }
20326
+ function isRetryableStatus(status) {
20327
+ return status === 429 || status >= 500 && status !== 501;
20328
+ }
20329
+ function failureKind(status) {
20330
+ if (status === 404) {
20331
+ return "not_found";
20332
+ }
20333
+ return status === 429 || status === 503 ? "rate_limited" : "http";
20334
+ }
20335
+ async function performWithTimeout(perform, timeoutMs) {
20336
+ const controller = new AbortController;
20337
+ let timer;
20338
+ const timeout = new Promise((resolve) => {
20339
+ timer = setTimeout(() => {
20340
+ controller.abort();
20341
+ resolve({ timedOut: true });
20342
+ }, timeoutMs);
20343
+ });
20344
+ try {
20345
+ return await Promise.race([
20346
+ perform(controller.signal).then((response) => ({ response }), (error) => controller.signal.aborted ? { timedOut: true } : { error }),
20347
+ timeout
20348
+ ]);
20349
+ } finally {
20350
+ clearTimeout(timer);
20351
+ }
20352
+ }
20353
+ async function fetchEvidenceText(http, source, cacheKey, perform, accept = () => true) {
20354
+ const hit = await readCache2(http, source, cacheKey);
20355
+ if (hit !== null && accept(hit)) {
20356
+ http.stats.cacheHits += 1;
20357
+ return { cached: true, text: hit };
20358
+ }
20359
+ const policy = http.policies[source];
20360
+ let last;
20361
+ for (let attempt = 1;attempt <= policy.attempts; attempt += 1) {
20362
+ await reserveSlot(http, source);
20363
+ http.stats.requests += 1;
20364
+ const outcome = await performWithTimeout(perform, policy.timeoutMs);
20365
+ let retryAfterMs = null;
20366
+ if ("response" in outcome) {
20367
+ const { response } = outcome;
20368
+ const success = response.status >= 200 && response.status < 300;
20369
+ if (success && accept(response.text)) {
20370
+ await writeCache2(http, source, cacheKey, response.text);
20371
+ return { cached: false, text: response.text };
20372
+ }
20373
+ if (success) {
20374
+ last = new EvidenceFetchError("invalid", `${source} answered HTTP ${response.status} with a body that is not the expected data`, attempt, response.status);
20375
+ } else {
20376
+ last = new EvidenceFetchError(failureKind(response.status), `${source} answered HTTP ${response.status}`, attempt, response.status);
20377
+ if (!isRetryableStatus(response.status)) {
20378
+ throw last;
20379
+ }
20380
+ retryAfterMs = parseRetryAfter(response.headers.get("retry-after"), http.now());
20381
+ if (retryAfterMs !== null) {
20382
+ await pushSlotBack(http, source, http.now() + Math.min(retryAfterMs, policy.maxBackoffMs));
20383
+ }
20384
+ if (retryAfterMs !== null && retryAfterMs > policy.maxBackoffMs) {
20385
+ throw new EvidenceFetchError("rate_limited", `${source} asked to wait ${Math.ceil(retryAfterMs / 1000)} s, past the ${policy.maxBackoffMs / 1000} s limit`, attempt, response.status);
20386
+ }
20387
+ }
20388
+ } else if ("timedOut" in outcome) {
20389
+ last = new EvidenceFetchError("timeout", `${source} did not answer within ${policy.timeoutMs} ms`, attempt);
20390
+ } else {
20391
+ const reason = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
20392
+ last = new EvidenceFetchError("network", `${source} request failed: ${reason}`, attempt);
20393
+ }
20394
+ if (attempt === policy.attempts) {
20395
+ break;
20396
+ }
20397
+ const exponential = policy.intervalMs * 2 ** attempt;
20398
+ const backoff = Math.min(policy.maxBackoffMs, Math.max(retryAfterMs ?? 0, exponential) + Math.floor(http.random() * 250));
20399
+ await http.sleep(backoff);
20400
+ }
20401
+ throw new EvidenceFetchError(last?.kind ?? "network", `${last?.message ?? `${source} request failed`} (gave up after ${policy.attempts} attempts)`, policy.attempts, last?.status);
20402
+ }
20403
+ function isJson(text) {
20404
+ try {
20405
+ JSON.parse(text);
20406
+ return true;
20407
+ } catch {
20408
+ return false;
20409
+ }
20410
+ }
20411
+ async function fetchEvidenceJson(http, source, url, headers = {}) {
20412
+ const { cached, text } = await fetchEvidenceText(http, source, `GET ${url}`, async (signal) => {
20413
+ const response = await http.fetch(url, {
20414
+ headers: { Accept: "application/json", "User-Agent": EVIDENCE_USER_AGENT, ...headers },
20415
+ signal
20416
+ });
20417
+ return { headers: response.headers, status: response.status, text: await response.text() };
20418
+ }, isJson);
20419
+ return { cached, data: JSON.parse(text) };
20420
+ }
20421
+ var SOURCE_POLICIES, EVIDENCE_USER_AGENT = "FluncleLabelTriage/1.0 ( https://www.fluncle.com )", EvidenceFetchError, LOCK_STALE_MS = 1e4;
20422
+ var init_evidence_http = __esm(() => {
20423
+ SOURCE_POLICIES = {
20424
+ apple: { attempts: 3, intervalMs: 3000, maxBackoffMs: 30000, timeoutMs: 1e4 },
20425
+ beatport: { attempts: 2, intervalMs: 1000, maxBackoffMs: 30000, timeoutMs: 75000 },
20426
+ discogs: { attempts: 3, intervalMs: 2500, maxBackoffMs: 30000, timeoutMs: 15000 },
20427
+ musicbrainz: { attempts: 4, intervalMs: 1100, maxBackoffMs: 30000, timeoutMs: 20000 }
20428
+ };
20429
+ EvidenceFetchError = class EvidenceFetchError extends Error {
20430
+ attempts;
20431
+ kind;
20432
+ status;
20433
+ constructor(kind, message, attempts, status) {
20434
+ super(message);
20435
+ this.name = "EvidenceFetchError";
20436
+ this.kind = kind;
20437
+ this.attempts = attempts;
20438
+ this.status = status;
20439
+ }
20440
+ };
20441
+ });
20442
+
20443
+ // src/commands/admin-labels.ts
20444
+ var exports_admin_labels = {};
20445
+ __export(exports_admin_labels, {
20446
+ resolveLabel: () => resolveLabel2
20447
+ });
20448
+ async function resolveLabel2(slugOrId) {
20449
+ const { labels } = await adminApiGet("/api/v1/admin/labels");
20450
+ const match = labels.find((label) => label.slug === slugOrId || label.id === slugOrId);
20451
+ if (!match) {
20452
+ throw new Error(`No label with slug or id '${slugOrId}' — check \`fluncle labels\``);
20453
+ }
20454
+ return match;
20455
+ }
20456
+ var init_admin_labels3 = __esm(() => {
20457
+ init_api();
20458
+ init_admin_artists3();
20459
+ });
20460
+
20461
+ // src/commands/admin-label-evidence.ts
20462
+ import { spawn } from "node:child_process";
20463
+ function parseCensusPages(value) {
20464
+ const pages = Number(value);
20465
+ if (!Number.isInteger(pages) || pages < 1 || pages > MAX_CENSUS_PAGES) {
20466
+ throw new Error(`Pass --census-pages 1-${MAX_CENSUS_PAGES}`);
20467
+ }
20468
+ return pages;
20469
+ }
20470
+ function parseEvidenceSources(value) {
20471
+ if (value === undefined || value.trim() === "") {
20472
+ return [...EVIDENCE_SOURCES];
20473
+ }
20474
+ const wanted = value.split(",").map((entry) => entry.trim().toLowerCase());
20475
+ const unknown = wanted.filter((entry) => !EVIDENCE_SOURCES.includes(entry));
20476
+ if (unknown.length > 0) {
20477
+ throw new Error(`Unknown source(s): ${unknown.join(", ")} — pick from ${EVIDENCE_SOURCES.join(", ")}`);
20478
+ }
20479
+ return EVIDENCE_SOURCES.filter((source) => wanted.includes(source));
20480
+ }
20481
+ function failureOf(error, request) {
20482
+ if (error instanceof EvidenceFetchError) {
20483
+ return {
20484
+ attempts: error.attempts,
20485
+ kind: error.kind,
20486
+ message: error.message,
20487
+ request,
20488
+ ...error.status === undefined ? {} : { status: error.status }
20489
+ };
20490
+ }
20491
+ return {
20492
+ kind: "parse",
20493
+ message: error instanceof Error ? error.message : String(error),
20494
+ request
20495
+ };
20496
+ }
20497
+ function statusFrom(parts, failures) {
20498
+ if (failures === 0) {
20499
+ return "ok";
20500
+ }
20501
+ return failures < parts ? "partial" : "error";
20502
+ }
20503
+ function tally(names) {
20504
+ const counts = new Map;
20505
+ for (const name of names) {
20506
+ counts.set(name, (counts.get(name) ?? 0) + 1);
20507
+ }
20508
+ return [...counts.entries()].map(([name, count]) => ({ count, name })).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
20509
+ }
20510
+ function firstCredit(credit) {
20511
+ const head = credit?.[0];
20512
+ const mbid = head?.artist?.id;
20513
+ if (!mbid) {
20514
+ return null;
20515
+ }
20516
+ return { mbid, name: head?.artist?.name ?? head?.name ?? mbid };
20517
+ }
20518
+ function crawlRuleCredit(credit) {
20519
+ for (const entry of credit ?? []) {
20520
+ const name = entry.artist?.name ?? entry.name;
20521
+ const mbid = entry.artist?.id;
20522
+ if (name && mbid && mbid !== VARIOUS_ARTISTS_MBID) {
20523
+ return { mbid, name };
20524
+ }
20525
+ }
20526
+ return null;
20527
+ }
20528
+ function creditString(credit) {
20529
+ return (credit ?? []).map((entry) => entry.name ?? entry.artist?.name ?? "").join(" / ");
20530
+ }
20531
+ function linkOf(links, host) {
20532
+ return links.find((link) => link.url.includes(host))?.url ?? null;
20533
+ }
20534
+ function toLabelFacts(payload, mbid) {
20535
+ const relations = payload.relations ?? [];
20536
+ return {
20537
+ area: payload.area?.name ?? null,
20538
+ country: payload.country ?? null,
20539
+ disambiguation: payload.disambiguation || null,
20540
+ genres: (payload.genres ?? []).flatMap((genre) => genre.name ? [genre.name] : []),
20541
+ labelCode: payload["label-code"] ?? null,
20542
+ labelRelations: relations.flatMap((relation) => relation.label?.id ? [
20543
+ {
20544
+ direction: relation.direction ?? "forward",
20545
+ mbid: relation.label.id,
20546
+ name: relation.label.name ?? relation.label.id,
20547
+ type: relation.type ?? "unknown"
20548
+ }
20549
+ ] : []),
20550
+ lifeSpan: {
20551
+ begin: payload["life-span"]?.begin ?? null,
20552
+ end: payload["life-span"]?.end ?? null,
20553
+ ended: payload["life-span"]?.ended === true
20554
+ },
20555
+ links: relations.flatMap((relation) => relation.url?.resource ? [{ type: relation.type ?? "unknown", url: relation.url.resource }] : []),
20556
+ mbid: payload.id ?? mbid,
20557
+ name: payload.name ?? mbid,
20558
+ tags: (payload.tags ?? []).flatMap((tag) => tag.name ? [tag.name] : []),
20559
+ type: payload.type ?? null
20560
+ };
20561
+ }
20562
+ function toReleaseFacts(browse, mbid) {
20563
+ const releases = browse.releases ?? [];
20564
+ const byArtist = new Map;
20565
+ let variousArtistsReleases = 0;
20566
+ const samples = releases.map((release) => {
20567
+ const head = firstCredit(release["artist-credit"]);
20568
+ if (head?.mbid === VARIOUS_ARTISTS_MBID) {
20569
+ variousArtistsReleases += 1;
20570
+ } else if (head) {
20571
+ const entry = byArtist.get(head.mbid) ?? { mbid: head.mbid, name: head.name, releases: 0 };
20572
+ entry.releases += 1;
20573
+ byArtist.set(head.mbid, entry);
20574
+ }
20575
+ const info = release["label-info"]?.find((entry) => entry.label?.id === mbid);
20576
+ const formats = [
20577
+ ...new Set((release.media ?? []).flatMap((m) => m.format ? [m.format] : []))
20578
+ ];
20579
+ return {
20580
+ artist: creditString(release["artist-credit"]),
20581
+ barcode: release.barcode || null,
20582
+ catno: info?.["catalog-number"] ?? null,
20583
+ date: release.date || null,
20584
+ format: formats.length > 0 ? formats.join(" + ") : null,
20585
+ mbid: release.id,
20586
+ status: release.status ?? null,
20587
+ title: release.title ?? ""
20588
+ };
20589
+ });
20590
+ return {
20591
+ firstCreditArtists: [...byArtist.values()].sort((a, b) => b.releases - a.releases || a.name.localeCompare(b.name)).slice(0, FIRST_CREDIT_ARTISTS),
20592
+ releaseCount: browse["release-count"] ?? releases.length,
20593
+ releases: samples.sort((a, b) => (b.date ?? "").localeCompare(a.date ?? "")).slice(0, RELEASE_SAMPLE),
20594
+ releasesRead: releases.length,
20595
+ variousArtistsReleases
20596
+ };
20597
+ }
20598
+ async function runCensus(deps, mbid, maxPages) {
20599
+ const credits = new Map;
20600
+ const seen = new Set;
20601
+ let offset = 0;
20602
+ let pagesFetched = 0;
20603
+ let releaseCount = 0;
20604
+ let allCached = true;
20605
+ let failure;
20606
+ let uncredited = 0;
20607
+ while (pagesFetched < maxPages) {
20608
+ const url = `${MB_ROOT}/release?label=${mbid}&inc=artist-credits+recordings&limit=100&offset=${offset}&fmt=json`;
20609
+ let page;
20610
+ try {
20611
+ page = await fetchEvidenceJson(deps.http, "musicbrainz", url);
20612
+ } catch (error) {
20613
+ if (pagesFetched === 0) {
20614
+ throw error;
20615
+ }
20616
+ failure = failureOf(error, url);
20617
+ break;
20618
+ }
20619
+ const { cached, data } = page;
20620
+ const releases = data.releases ?? [];
20621
+ allCached &&= cached;
20622
+ pagesFetched += 1;
20623
+ releaseCount = data["release-count"] ?? releaseCount;
20624
+ for (const release of releases) {
20625
+ for (const medium of release.media ?? []) {
20626
+ for (const track of medium.tracks ?? []) {
20627
+ const recordingId = track.recording?.id;
20628
+ if (!recordingId || seen.has(recordingId)) {
20629
+ continue;
20630
+ }
20631
+ seen.add(recordingId);
20632
+ const head = crawlRuleCredit(track.recording?.["artist-credit"] ?? release["artist-credit"]);
20633
+ if (!head) {
20634
+ uncredited += 1;
20635
+ continue;
20636
+ }
20637
+ const entry = credits.get(head.mbid) ?? {
20638
+ artistMbid: head.mbid,
20639
+ artistName: head.name,
20640
+ recordings: 0
20641
+ };
20642
+ entry.recordings += 1;
20643
+ credits.set(head.mbid, entry);
20644
+ }
20645
+ }
20646
+ }
20647
+ offset += releases.length;
20648
+ if (releases.length === 0 || offset >= releaseCount) {
20649
+ break;
20650
+ }
20651
+ }
20652
+ const sampled = offset < releaseCount;
20653
+ const firstCredits = [...credits.values()].sort((a, b) => b.recordings - a.recordings || a.artistName.localeCompare(b.artistName));
20654
+ return {
20655
+ cached: allCached,
20656
+ ...failure ? { failure } : {},
20657
+ census: {
20658
+ caveat: sampled ? `sampled: first ${offset} of ${releaseCount} releases${failure ? " (a later page failed)" : ""}` : null,
20659
+ firstCredits,
20660
+ pagesFetched,
20661
+ recordingsCounted: firstCredits.reduce((sum, entry) => sum + entry.recordings, uncredited),
20662
+ releaseCount,
20663
+ releasesRead: offset,
20664
+ sampled,
20665
+ uncreditedRecordings: uncredited
20666
+ }
20667
+ };
20668
+ }
20669
+ async function gatherMusicBrainz(deps, mbid, options) {
20670
+ const data = {};
20671
+ const errors = [];
20672
+ let parts = 2;
20673
+ let allCached = true;
20674
+ const labelUrl = `${MB_ROOT}/label/${mbid}?inc=aliases+label-rels+url-rels+tags+genres&fmt=json`;
20675
+ try {
20676
+ const { cached, data: payload } = await fetchEvidenceJson(deps.http, "musicbrainz", labelUrl);
20677
+ allCached &&= cached;
20678
+ data.label = toLabelFacts(payload, mbid);
20679
+ } catch (error) {
20680
+ errors.push(failureOf(error, labelUrl));
20681
+ }
20682
+ const releasesUrl = `${MB_ROOT}/release?label=${mbid}&inc=artist-credits+labels+media&limit=100&fmt=json`;
20683
+ try {
20684
+ const { cached, data: browse } = await fetchEvidenceJson(deps.http, "musicbrainz", releasesUrl);
20685
+ allCached &&= cached;
20686
+ data.releases = toReleaseFacts(browse, mbid);
20687
+ } catch (error) {
20688
+ errors.push(failureOf(error, releasesUrl));
20689
+ }
20690
+ if (options.census) {
20691
+ parts += 1;
20692
+ try {
20693
+ const { cached, census, failure } = await runCensus(deps, mbid, options.censusPages);
20694
+ allCached &&= cached;
20695
+ data.census = census;
20696
+ if (failure) {
20697
+ errors.push(failure);
20698
+ }
20699
+ } catch (error) {
20700
+ errors.push(failureOf(error, `${MB_ROOT}/release?label=${mbid}&inc=recordings (census)`));
20701
+ }
20702
+ }
20703
+ return {
20704
+ cached: allCached,
20705
+ data,
20706
+ ...errors.length > 0 ? { errors } : {},
20707
+ status: statusFrom(parts, errors.length)
20708
+ };
20709
+ }
20710
+ function discogsLabelIdFrom(url) {
20711
+ const match = url ? /discogs\.com\/(?:[a-z]{2}\/)?label\/(\d+)/i.exec(url) : null;
20712
+ return match?.[1] ? Number(match[1]) : null;
20713
+ }
20714
+ function beatportLabelUrlFrom(url) {
20715
+ const match = url ? /beatport\.com\/label\/([^/?#]+)\/(\d+)/i.exec(url) : null;
20716
+ return match?.[1] && match[2] ? `https://www.beatport.com/label/${match[1]}/${match[2]}` : null;
20717
+ }
20718
+ function discogsHeaders(deps) {
20719
+ return deps.discogsToken ? { Authorization: `Discogs token=${deps.discogsToken}` } : {};
20720
+ }
20721
+ async function discogsCandidates(deps, name) {
20722
+ const searchUrl = `${DISCOGS_ROOT}/database/search?type=label&q=${encodeURIComponent(name)}&per_page=5`;
20723
+ try {
20724
+ const { cached, data } = await fetchEvidenceJson(deps.http, "discogs", searchUrl, discogsHeaders(deps));
20725
+ const candidates = (data.results ?? []).flatMap((result) => result.id ? [
20726
+ {
20727
+ id: result.id,
20728
+ title: result.title ?? "",
20729
+ url: `https://www.discogs.com${result.uri ?? `/label/${result.id}`}`
20730
+ }
20731
+ ] : []);
20732
+ return {
20733
+ cached,
20734
+ data: { candidates },
20735
+ note: "The MusicBrainz label has no Discogs link. Candidates are unverified name matches: confirm the identity before citing any of them.",
20736
+ status: "no_link"
20737
+ };
20738
+ } catch (error) {
20739
+ return {
20740
+ errors: [failureOf(error, searchUrl)],
20741
+ note: "The MusicBrainz label has no Discogs link.",
20742
+ status: "no_link"
20743
+ };
20744
+ }
20745
+ }
20746
+ function toDiscogsLabel(payload, labelId, fallbackName) {
20747
+ const sublabels = (payload.sublabels ?? []).flatMap((entry) => entry.name ? [entry.name] : []);
20748
+ const parent = payload.parent_label;
20749
+ return {
20750
+ id: payload.id ?? labelId,
20751
+ name: payload.name ?? fallbackName,
20752
+ parentLabel: parent?.id === undefined ? null : { id: parent.id, name: parent.name ?? "" },
20753
+ profile: (payload.profile ?? "").slice(0, PROFILE_CHARS),
20754
+ sublabelCount: sublabels.length,
20755
+ sublabels: sublabels.slice(0, 30),
20756
+ url: payload.uri ?? `https://www.discogs.com/label/${labelId}`,
20757
+ urls: payload.urls ?? []
20758
+ };
20759
+ }
20760
+ function toDiscogsReleases(payload) {
20761
+ const releases = payload.releases ?? [];
20762
+ const ids = releases.flatMap((release) => {
20763
+ const id = release.type === "master" ? release.main_release : release.id;
20764
+ return typeof id === "number" ? [id] : [];
20765
+ });
20766
+ return {
20767
+ facts: {
20768
+ releaseCount: payload.pagination?.items ?? releases.length,
20769
+ sample: releases.slice(0, DISCOGS_RELEASE_SAMPLE).map((release) => ({
20770
+ artist: release.artist ?? "",
20771
+ catno: release.catno ?? null,
20772
+ format: release.format ?? null,
20773
+ title: release.title ?? "",
20774
+ year: release.year || null
20775
+ })),
20776
+ topArtists: tally(releases.map((release) => release.artist ?? "")).filter((entry) => entry.name !== "").slice(0, 15).map((entry) => ({ name: entry.name, releases: entry.count }))
20777
+ },
20778
+ ids
20779
+ };
20780
+ }
20781
+ async function gatherDiscogs(deps, mb) {
20782
+ const labelFacts = mb.label;
20783
+ if (!labelFacts) {
20784
+ return {
20785
+ note: "MusicBrainz label unavailable, so no Discogs link to follow",
20786
+ status: "skipped"
20787
+ };
20788
+ }
20789
+ const labelId = discogsLabelIdFrom(linkOf(labelFacts.links, "discogs.com"));
20790
+ if (labelId === null) {
20791
+ return discogsCandidates(deps, labelFacts.name);
20792
+ }
20793
+ const headers = discogsHeaders(deps);
20794
+ const data = {};
20795
+ const errors = [];
20796
+ const releaseIds = new Set;
20797
+ let allCached = true;
20798
+ const labelUrl = `${DISCOGS_ROOT}/labels/${labelId}`;
20799
+ try {
20800
+ const { cached, data: payload } = await fetchEvidenceJson(deps.http, "discogs", labelUrl, headers);
20801
+ allCached &&= cached;
20802
+ data.label = toDiscogsLabel(payload, labelId, labelFacts.name);
20803
+ } catch (error) {
20804
+ errors.push(failureOf(error, labelUrl));
20805
+ }
20806
+ const releasesUrl = `${DISCOGS_ROOT}/labels/${labelId}/releases?per_page=100&page=1`;
20807
+ try {
20808
+ const { cached, data: payload } = await fetchEvidenceJson(deps.http, "discogs", releasesUrl, headers);
20809
+ allCached &&= cached;
20810
+ const { facts, ids } = toDiscogsReleases(payload);
20811
+ data.releases = facts;
20812
+ for (const id of ids) {
20813
+ releaseIds.add(id);
20814
+ }
20815
+ } catch (error) {
20816
+ errors.push(failureOf(error, releasesUrl));
20817
+ }
20818
+ if (releaseIds.size > 0) {
20819
+ const styled = await readDiscogsStyles(deps, releaseIds, data.label?.name ?? labelFacts.name);
20820
+ allCached &&= styled.cached;
20821
+ errors.push(...styled.errors);
20822
+ data.styles = styled.styles;
20823
+ }
20824
+ return {
20825
+ cached: allCached,
20826
+ data,
20827
+ ...errors.length > 0 ? { errors } : {},
20828
+ status: discogsStatus(data, errors)
20829
+ };
20830
+ }
20831
+ function discogsStatus(data, errors) {
20832
+ if (Object.keys(data).length === 0) {
20833
+ return "error";
20834
+ }
20835
+ return errors.length > 0 ? "partial" : "ok";
20836
+ }
20837
+ function discogsSearchName(name) {
20838
+ return name.replace(/\s*\(\d+\)$/, "").replace(/^[^\p{L}\p{N}]+/u, "");
20839
+ }
20840
+ async function readDiscogsStyles(deps, releaseIds, labelName) {
20841
+ const headers = discogsHeaders(deps);
20842
+ const styled = new Map;
20843
+ const errors = [];
20844
+ let allCached = true;
20845
+ let viaSearch = 0;
20846
+ const searchUrl = `${DISCOGS_ROOT}/database/search?type=release&label=${encodeURIComponent(discogsSearchName(labelName))}&per_page=100`;
20847
+ try {
20848
+ const { cached, data } = await fetchEvidenceJson(deps.http, "discogs", searchUrl, headers);
20849
+ allCached &&= cached;
20850
+ for (const result of data.results ?? []) {
20851
+ if (typeof result.id === "number" && releaseIds.has(result.id) && !styled.has(result.id)) {
20852
+ styled.set(result.id, { genres: result.genre ?? [], styles: result.style ?? [] });
20853
+ }
20854
+ }
20855
+ viaSearch = styled.size;
20856
+ } catch (error) {
20857
+ errors.push(failureOf(error, searchUrl));
20858
+ }
20859
+ const target = Math.min(DISCOGS_STYLE_TARGET, releaseIds.size);
20860
+ const unread = [...releaseIds].filter((id) => !styled.has(id));
20861
+ let reads = 0;
20862
+ for (const id of unread) {
20863
+ if (styled.size >= target || reads >= DISCOGS_RELEASE_READS) {
20864
+ break;
20865
+ }
20866
+ reads += 1;
20867
+ const releaseUrl = `${DISCOGS_ROOT}/releases/${id}`;
20868
+ try {
20869
+ const { cached, data } = await fetchEvidenceJson(deps.http, "discogs", releaseUrl, headers);
20870
+ allCached &&= cached;
20871
+ styled.set(id, { genres: data.genres ?? [], styles: data.styles ?? [] });
20872
+ } catch (error) {
20873
+ errors.push(failureOf(error, releaseUrl));
20874
+ }
20875
+ }
20876
+ const rows = [...styled.values()];
20877
+ return {
20878
+ cached: allCached,
20879
+ errors,
20880
+ styles: {
20881
+ genres: tally(rows.flatMap((row) => row.genres)),
20882
+ labelReleasesListed: releaseIds.size,
20883
+ releasesRead: rows.length,
20884
+ styles: tally(rows.flatMap((row) => row.styles)).slice(0, 25),
20885
+ viaRelease: rows.length - viaSearch,
20886
+ viaSearch
20887
+ }
20888
+ };
20889
+ }
20890
+ function facetCounts(rows) {
20891
+ return (rows ?? []).flatMap((row) => row.name && typeof row.count === "number" ? [{ count: row.count, name: row.name }] : []).sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
20892
+ }
20893
+ function parseBeatportTracksPage(html) {
20894
+ const island = /<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/.exec(html);
20895
+ if (!island?.[1]) {
20896
+ return null;
20897
+ }
20898
+ let queries;
20899
+ try {
20900
+ const parsed = JSON.parse(island[1]);
20901
+ queries = parsed.props?.pageProps?.dehydratedState?.queries ?? [];
20902
+ } catch {
20903
+ return null;
20904
+ }
20905
+ const tracks = queries.find((query) => query.queryKey?.[0] === "tracks" && query.state?.data?.facets?.fields)?.state?.data;
20906
+ if (!tracks) {
20907
+ return null;
20908
+ }
20909
+ const fields = tracks.facets?.fields;
20910
+ const genres = facetCounts(fields?.genre);
20911
+ const total = genres.reduce((sum, genre) => sum + genre.count, 0);
20912
+ return {
20913
+ genres: genres.map((genre) => ({
20914
+ ...genre,
20915
+ share: total > 0 ? Math.round(genre.count / total * 1000) / 1000 : 0
20916
+ })),
20917
+ subGenres: facetCounts(fields?.sub_genre),
20918
+ topArtists: facetCounts(fields?.artists).slice(0, 15),
20919
+ trackCount: tracks.count ?? total
20920
+ };
20921
+ }
20922
+ async function gatherBeatport(deps, mb) {
20923
+ if (!mb.label) {
20924
+ return {
20925
+ note: "MusicBrainz label unavailable, so no Beatport link to follow",
20926
+ status: "skipped"
20927
+ };
20928
+ }
20929
+ const labelUrl = beatportLabelUrlFrom(linkOf(mb.label.links, "beatport.com"));
20930
+ if (labelUrl === null) {
20931
+ return { note: "The MusicBrainz label has no Beatport link.", status: "no_link" };
20932
+ }
20933
+ if (deps.scraper === null) {
20934
+ return {
20935
+ note: "Beatport needs Firecrawl: set FIRECRAWL_API_KEY or install the firecrawl CLI.",
20936
+ status: "not_configured"
20937
+ };
20938
+ }
20939
+ const scraper = deps.scraper;
20940
+ const tracksUrl = `${labelUrl}/tracks`;
20941
+ try {
20942
+ const { cached, text } = await fetchEvidenceText(deps.http, "beatport", `SCRAPE ${tracksUrl}`, (signal) => scraper(tracksUrl, signal), (html) => parseBeatportTracksPage(html) !== null);
20943
+ const facts = parseBeatportTracksPage(text);
20944
+ if (facts === null) {
20945
+ return {
20946
+ errors: [
20947
+ {
20948
+ kind: "parse",
20949
+ message: "The Beatport page carried no tracks facet (a missing label or a layout change)",
20950
+ request: tracksUrl
20951
+ }
20952
+ ],
20953
+ status: "error"
20954
+ };
20955
+ }
20956
+ return { cached, data: { ...facts, labelUrl, tracksUrl }, status: "ok" };
20957
+ } catch (error) {
20958
+ return { errors: [failureOf(error, tracksUrl)], status: "error" };
20959
+ }
20960
+ }
20961
+ async function gatherApple(deps, mb) {
20962
+ if (!mb.releases) {
20963
+ return {
20964
+ note: "MusicBrainz releases unavailable, so no barcodes to look up",
20965
+ status: "skipped"
20966
+ };
20967
+ }
20968
+ const seen = new Set;
20969
+ const targets = mb.releases.releases.filter((release) => {
20970
+ if (!release.barcode || seen.has(release.barcode)) {
20971
+ return false;
20972
+ }
20973
+ seen.add(release.barcode);
20974
+ return true;
20975
+ });
20976
+ if (targets.length === 0) {
20977
+ return { note: "No sampled MusicBrainz release carries a barcode.", status: "no_link" };
20978
+ }
20979
+ const releases = [];
20980
+ const errors = [];
20981
+ let allCached = true;
20982
+ for (const target of targets.slice(0, APPLE_LOOKUPS)) {
20983
+ const barcode = target.barcode ?? "";
20984
+ const url = `https://itunes.apple.com/lookup?upc=${encodeURIComponent(barcode)}&entity=album`;
20985
+ try {
20986
+ const { cached, data } = await fetchEvidenceJson(deps.http, "apple", url);
20987
+ allCached &&= cached;
20988
+ const hit = (data.results ?? []).find((result) => result.wrapperType === "collection");
20989
+ releases.push(hit ? {
20990
+ artistName: hit.artistName,
20991
+ barcode,
20992
+ collectionName: hit.collectionName,
20993
+ copyright: hit.copyright,
20994
+ found: true,
20995
+ mbTitle: target.title,
20996
+ primaryGenreName: hit.primaryGenreName
20997
+ } : { barcode, found: false, mbTitle: target.title });
20998
+ } catch (error) {
20999
+ errors.push(failureOf(error, url));
21000
+ }
21001
+ }
21002
+ const looked = Math.min(targets.length, APPLE_LOOKUPS);
21003
+ return {
21004
+ cached: allCached,
21005
+ data: {
21006
+ found: releases.filter((release) => release.found).length,
21007
+ genres: tally(releases.flatMap((release) => release.primaryGenreName ?? [])),
21008
+ looked,
21009
+ releases
21010
+ },
21011
+ ...errors.length > 0 ? { errors } : {},
21012
+ status: statusFrom(looked, errors.length)
21013
+ };
21014
+ }
21015
+ function skipped() {
21016
+ return { note: "Not requested (--sources).", status: "skipped" };
21017
+ }
21018
+ async function resolveTarget(input, deps) {
21019
+ const trimmed = input.trim();
21020
+ if (MBID_PATTERN3.test(trimmed)) {
21021
+ return { input: trimmed, mbLabelId: trimmed.toLowerCase() };
21022
+ }
21023
+ const label = await deps.resolveLabel(trimmed);
21024
+ if (!label.mbLabelId) {
21025
+ throw new Error(`Label '${label.slug}' has no MusicBrainz identity (mb_label_id), so nothing says which same-named label to research. Resolve it by hand first (docs/label-entity.md).`);
21026
+ }
21027
+ return {
21028
+ id: label.id,
21029
+ input: trimmed,
21030
+ mbLabelId: label.mbLabelId.toLowerCase(),
21031
+ name: label.name,
21032
+ seedState: label.seedState,
21033
+ slug: label.slug
21034
+ };
21035
+ }
21036
+ async function gatherLabelEvidence(input, options, deps) {
21037
+ const label = await resolveTarget(input, deps);
21038
+ const wants = (source) => options.sources.includes(source);
21039
+ const needsMb = wants("musicbrainz") || wants("discogs") || wants("beatport") || wants("apple");
21040
+ const musicbrainz = needsMb ? await gatherMusicBrainz(deps, label.mbLabelId, options) : skipped();
21041
+ const mb = musicbrainz.data ?? {};
21042
+ const [discogs, beatport, apple] = await Promise.all([
21043
+ wants("discogs") ? gatherDiscogs(deps, mb) : Promise.resolve(skipped()),
21044
+ wants("beatport") ? gatherBeatport(deps, mb) : Promise.resolve(skipped()),
21045
+ wants("apple") ? gatherApple(deps, mb) : Promise.resolve(skipped())
21046
+ ]);
21047
+ const sources = { apple, beatport, discogs, musicbrainz };
21048
+ const reports = Object.entries(sources);
21049
+ const errors = reports.flatMap(([source, report]) => (report.errors ?? []).map((failure) => ({ ...failure, source })));
21050
+ const requested = reports.filter(([source]) => wants(source));
21051
+ const answered = requested.filter(([, report]) => ["no_link", "ok", "partial"].includes(report.status));
21052
+ return {
21053
+ errors,
21054
+ fetchedAt: new Date(deps.http.now()).toISOString(),
21055
+ label: { ...label, name: label.name ?? mb.label?.name },
21056
+ ok: answered.length > 0,
21057
+ partial: answered.length < requested.length || errors.length > 0,
21058
+ requests: { cacheHits: deps.http.stats.cacheHits, network: deps.http.stats.requests },
21059
+ sources
21060
+ };
21061
+ }
21062
+ function firecrawlApiScraper(apiKey, http) {
21063
+ return async (url, signal) => {
21064
+ const response = await http.fetch(FIRECRAWL_SCRAPE_URL, {
21065
+ body: JSON.stringify({ formats: ["rawHtml"], url }),
21066
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
21067
+ method: "POST",
21068
+ signal
21069
+ });
21070
+ if (!response.ok) {
21071
+ return { headers: response.headers, status: response.status, text: await response.text() };
21072
+ }
21073
+ const payload = await response.json();
21074
+ return {
21075
+ headers: response.headers,
21076
+ status: payload.data?.metadata?.statusCode ?? 200,
21077
+ text: payload.data?.rawHtml ?? ""
21078
+ };
21079
+ };
21080
+ }
21081
+ function firecrawlCliScraper(binary) {
21082
+ return (url, signal) => new Promise((resolve, reject) => {
21083
+ const child = spawn(binary, ["scrape", "-f", "rawHtml", url], {
21084
+ signal,
21085
+ stdio: ["ignore", "pipe", "pipe"]
21086
+ });
21087
+ const out = [];
21088
+ const err = [];
21089
+ child.stdout.on("data", (chunk) => out.push(chunk));
21090
+ child.stderr.on("data", (chunk) => err.push(chunk));
21091
+ child.on("error", reject);
21092
+ child.on("close", (code) => {
21093
+ if (code === 0) {
21094
+ resolve({
21095
+ headers: { get: () => null },
21096
+ status: 200,
21097
+ text: Buffer.concat(out).toString("utf8")
21098
+ });
21099
+ return;
21100
+ }
21101
+ const tail = Buffer.concat(err).toString("utf8").trim().split(`
21102
+ `).slice(-2).join(" ");
21103
+ reject(new Error(`firecrawl CLI exited ${code}: ${tail}`));
21104
+ });
21105
+ });
21106
+ }
21107
+ function defaultBeatportScraper(env, http) {
21108
+ if (env.firecrawlApiKey) {
21109
+ return firecrawlApiScraper(env.firecrawlApiKey, http);
21110
+ }
21111
+ return env.firecrawlBinary ? firecrawlCliScraper(env.firecrawlBinary) : null;
21112
+ }
21113
+ async function labelEvidenceCommand(input, options) {
21114
+ const [{ readOptionalEnv }, { resolveLabel }] = await Promise.all([
21115
+ Promise.resolve().then(() => (init_env(), exports_env)),
21116
+ Promise.resolve().then(() => (init_admin_labels3(), exports_admin_labels))
21117
+ ]);
21118
+ const http = createEvidenceHttp({ refresh: options.refresh });
21119
+ const discogsToken = readOptionalEnv("DISCOGS_USER_TOKEN");
21120
+ if (discogsToken) {
21121
+ http.policies = { ...http.policies, discogs: { ...http.policies.discogs, intervalMs: 1100 } };
21122
+ }
21123
+ return gatherLabelEvidence(input, options, {
21124
+ discogsToken,
21125
+ http,
21126
+ resolveLabel,
21127
+ scraper: defaultBeatportScraper({
21128
+ firecrawlApiKey: readOptionalEnv("FIRECRAWL_API_KEY"),
21129
+ firecrawlBinary: Bun.which("firecrawl")
21130
+ }, http)
21131
+ });
21132
+ }
21133
+ function countLine(counts, limit) {
21134
+ return counts.slice(0, limit).map((entry) => `${entry.name} ${entry.count}`).join(", ");
21135
+ }
21136
+ function labelEvidenceLines(evidence) {
21137
+ const { apple, beatport, discogs, musicbrainz } = evidence.sources;
21138
+ const mb = musicbrainz.data;
21139
+ const name = evidence.label.name ?? evidence.label.mbLabelId;
21140
+ const lines = [`${name} (${evidence.label.mbLabelId})`];
21141
+ const mbParts = [];
21142
+ if (mb?.label) {
21143
+ mbParts.push([mb.label.type, mb.label.area, mb.label.lifeSpan.begin].filter(Boolean).join(", "));
21144
+ }
21145
+ if (mb?.releases) {
21146
+ const top = mb.releases.firstCreditArtists.slice(0, 6).map((artist) => `${artist.name} ${artist.releases}`).join(", ");
21147
+ mbParts.push(`${mb.releases.releaseCount} releases; first credits: ${top || "none"}`);
21148
+ }
21149
+ if (mb?.census) {
21150
+ mbParts.push(`census ${mb.census.recordingsCounted} recordings over ${mb.census.releasesRead} releases${mb.census.caveat ? ` (${mb.census.caveat})` : ""}`);
21151
+ }
21152
+ lines.push(` musicbrainz: ${musicbrainz.status}${mbParts.length ? ` — ${mbParts.join("; ")}` : ""}`);
21153
+ const dg = discogs.data;
21154
+ const dgDetail = dg?.styles ? `styles: ${countLine(dg.styles.styles, 6)} (${dg.styles.releasesRead} releases)` : dg?.candidates ? `unverified candidates: ${dg.candidates.map((c) => c.title).join(", ") || "none"}` : discogs.note;
21155
+ lines.push(` discogs: ${discogs.status}${dgDetail ? ` — ${dgDetail}` : ""}`);
21156
+ const bp = beatport.data;
21157
+ const bpDetail = bp ? `${bp.trackCount} tracks; genres: ${bp.genres.slice(0, 5).map((genre) => `${genre.name} ${Math.round(genre.share * 100)}%`).join(", ")}` : beatport.note;
21158
+ lines.push(` beatport: ${beatport.status}${bpDetail ? ` — ${bpDetail}` : ""}`);
21159
+ const ap = apple.data;
21160
+ const apDetail = ap ? `${ap.found}/${ap.looked} found; genres: ${countLine(ap.genres, 4) || "none"}` : apple.note;
21161
+ lines.push(` apple: ${apple.status}${apDetail ? ` — ${apDetail}` : ""}`);
21162
+ for (const failure of evidence.errors) {
21163
+ lines.push(` ! ${failure.source}: ${failure.message}`);
21164
+ }
21165
+ return lines;
21166
+ }
21167
+ var MBID_PATTERN3, MB_ROOT = "https://musicbrainz.org/ws/2", DISCOGS_ROOT = "https://api.discogs.com", FIRECRAWL_SCRAPE_URL = "https://api.firecrawl.dev/v2/scrape", VARIOUS_ARTISTS_MBID = "89ad4ac3-39f7-470e-963a-56509c546377", RELEASE_SAMPLE = 12, FIRST_CREDIT_ARTISTS = 25, APPLE_LOOKUPS = 5, DISCOGS_RELEASE_SAMPLE = 8, DISCOGS_STYLE_TARGET = 10, DISCOGS_RELEASE_READS = 8, PROFILE_CHARS = 800, EVIDENCE_SOURCES, MAX_CENSUS_PAGES = 5;
21168
+ var init_admin_label_evidence = __esm(() => {
21169
+ init_evidence_http();
21170
+ MBID_PATTERN3 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
21171
+ EVIDENCE_SOURCES = ["musicbrainz", "discogs", "beatport", "apple"];
21172
+ });
21173
+
19809
21174
  // src/commands/admin-albums.ts
19810
21175
  async function describeAlbumCommand(slug, options) {
19811
21176
  return adminApiPost(`/api/v1/admin/albums/${encodeURIComponent(slug)}/bio`, buildBioBody2(options));
@@ -23651,6 +25016,25 @@ JSON field reference:
23651
25016
  console.log(`${label.slug} ${label.seedState} ${looked ?? ""}`.trimEnd());
23652
25017
  }
23653
25018
  });
25019
+ labels.command("evidence").description("Gather a label's MusicBrainz, Discogs, Beatport and Apple evidence for triage (cached, rate-limited)").argument("<label>", "The label's MusicBrainz MBID, slug, or lbl_\u2026 id").option("--census", "Also count FIRST credits per recording over the label's releases", false).option("--census-pages <n>", "MusicBrainz pages the census may read, 1-5", "5").option("--sources <list>", "Comma list of musicbrainz,discogs,beatport,apple (default all)").option("--refresh", "Ignore cached responses and fetch fresh", false).option("--json", "Print JSON", false).action(async (input, options) => {
25020
+ await Promise.resolve().then(() => init_admin_label_evidence());
25021
+ const censusPages = parseCensusPages(options.censusPages);
25022
+ const evidence = await labelEvidenceCommand(input, {
25023
+ census: options.census,
25024
+ censusPages,
25025
+ refresh: options.refresh,
25026
+ sources: parseEvidenceSources(options.sources)
25027
+ });
25028
+ if (!evidence.ok) {
25029
+ process.exitCode = 1;
25030
+ }
25031
+ if (options.json) {
25032
+ printJson(evidence);
25033
+ return;
25034
+ }
25035
+ console.log(labelEvidenceLines(evidence).join(`
25036
+ `));
25037
+ });
23654
25038
  labels.command("triage").description("Record what a triage round found for a label (agent; never rules)").argument("<slug>", "The label slug").requiredOption("--payload <file>", "JSON file holding the round's finding for this label").option("--json", "Print JSON", false).action(async (slug, options) => {
23655
25039
  const payload = JSON.parse(readFileSync6(options.payload, "utf8"));
23656
25040
  await Promise.resolve().then(() => init_admin_labels2());
@@ -26639,6 +28023,7 @@ var stringOptions = new Set([
26639
28023
  "--bpm-confidence",
26640
28024
  "--bpm-source",
26641
28025
  "--calls",
28026
+ "--census-pages",
26642
28027
  "--composition",
26643
28028
  "--consumer-digest",
26644
28029
  "--consumer-item-count",
@@ -26717,6 +28102,7 @@ var stringOptions = new Set([
26717
28102
  "--since",
26718
28103
  "--soundcloud-url",
26719
28104
  "--source",
28105
+ "--sources",
26720
28106
  "--status",
26721
28107
  "--stale-before",
26722
28108
  "--stream",