fluncle 0.234.0 → 0.236.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 +89 -16
  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.234.0".trim() ? "0.234.0".trim() : "0.1.0";
625
+ currentVersion = "0.236.0".trim() ? "0.236.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.234.0".trim() ? "0.234.0".trim() : "0.1.0";
2338
+ currentVersion2 = "0.236.0".trim() ? "0.236.0".trim() : "0.1.0";
2339
2339
  });
2340
2340
 
2341
2341
  // ../../packages/registry/src/index.ts
@@ -3059,6 +3059,28 @@ var init_src = __esm(() => {
3059
3059
  url: `${SITE}/robots.txt`,
3060
3060
  weights: { web: "tertiary" }
3061
3061
  },
3062
+ {
3063
+ apiFormat: "text/plain",
3064
+ exposedContent: [
3065
+ "RFC 9116 security.txt — where to send a vulnerability report, in the one place a researcher looks first"
3066
+ ],
3067
+ kind: "discovery",
3068
+ name: "discovery.security-txt",
3069
+ operatorNotes: "A static file, apps/web/public/.well-known/security.txt. It carries an `Expires` field the RFC requires a responder to keep in the future — when it lapses, a reporter reads the contact as stale. No probeConfig, exactly like robots.txt and llms.txt: a static asset the Worker never computes.",
3070
+ route: "/.well-known/security.txt",
3071
+ url: `${SITE}/.well-known/security.txt`,
3072
+ weights: { web: "tertiary" }
3073
+ },
3074
+ {
3075
+ apiFormat: "text/plain",
3076
+ exposedContent: ["humans.txt — who made this, who is thanked, and what it is built with"],
3077
+ kind: "discovery",
3078
+ name: "discovery.humans",
3079
+ operatorNotes: "A static file, apps/web/public/humans.txt — the human counterpart to robots.txt. Machine-fetched by convention, never browsed, which is why it sits under `discovery` beside robots.txt rather than as a web route.",
3080
+ route: "/humans.txt",
3081
+ url: `${SITE}/humans.txt`,
3082
+ weights: { web: "tertiary" }
3083
+ },
3062
3084
  {
3063
3085
  apiFormat: "text/markdown",
3064
3086
  discoveryUrl: `${SITE}/.well-known/api-catalog`,
@@ -3875,7 +3897,7 @@ var init_src = __esm(() => {
3875
3897
  ],
3876
3898
  kind: "cron",
3877
3899
  name: "cron.reconcile-hub-counts",
3878
- operatorNotes: "04:10 Amsterdam (after the 04:00 reach snapshot, before the 04:40 demand tick), a rave-02 host systemd timer (docs/agents/hermes/reconcile-hub-counts-timer/). The SELF-HEALING BACKSTOP under keystone 2's maintained hub counts: they are moved as DELTAS by every edge-writing path (recompute-from-truth measured 27,400 ms at 150k hosted vs ~200 ms for the delta form), and a maintained counter drifts silently — a missed write path, a non-atomic bulk op, or an out-of-band write (the catalogue-prune skill deletes tracks straight out of the database). Keystone 2's own rollout proved it on day one: the deploy-window skew left 44 artists / 3 albums / 1 label wrong until a manual reconcile. A bare trigger (the funnel-snapshot shape): fires the AGENT-tier reconcile_hub_counts op once and the Worker runs two statements per table a grouped `UPDATE FROM ( GROUP BY fk)` guarded by a counts-DIFFER predicate (so rowsAffected IS the corrected-row count) plus a zero-truth pass for an entity whose last track vanished. The artists source is PINNED to `track_artists JOIN tracks` so orphaned edges never count. Idempotent; zero LLM tokens; no new secret. The corrected-row numbers are the operator's DRIFT AUDIT — journalctl -u fluncle-reconcile-hub-counts.service | grep AUDIT. Source: docs/agents/hermes/scripts/reconcile-hub-counts.*.",
3900
+ operatorNotes: "04:10 Amsterdam (after the 04:00 reach snapshot, before the 04:40 demand tick), a rave-02 host systemd timer (docs/agents/hermes/reconcile-hub-counts-timer/). The SELF-HEALING BACKSTOP under keystone 2's maintained hub counts: they are moved as DELTAS by every edge-writing path (recompute-from-truth measured 27,400 ms at 150k hosted vs ~200 ms for the delta form), and a maintained counter drifts silently — a missed write path, a non-atomic bulk op, or an out-of-band write (the catalogue-prune skill deletes tracks straight out of the database). Keystone 2's own rollout proved it on day one: the deploy-window skew left 44 artists / 3 albums / 1 label wrong until a manual reconcile. Walks the AGENT-tier reconcile_hub_counts op in bounded windows, one admitted database phase each: the Worker reads bounded keyset pages of entity rows beside their truth (never an aggregate inside a write transaction) and rewrites only disagreeing rows as compare-and-set point writes, so a concurrent maintained delta is never overwritten. The artists source is PINNED to `track_artists JOIN tracks` so orphaned edges never count. Idempotent; zero LLM tokens; no new secret. The corrected-row numbers are the operator's DRIFT AUDIT — journalctl -u fluncle-reconcile-hub-counts.service | grep AUDIT. Source: docs/agents/hermes/scripts/reconcile-hub-counts.*.",
3879
3901
  probeConfig: {
3880
3902
  cadenceMs: 24 * 60 * MINUTE_MS,
3881
3903
  cronName: "fluncle-reconcile-hub-counts",
@@ -10693,7 +10715,10 @@ var init_admin_artists = __esm(() => {
10693
10715
  path: "/admin/artists/rank",
10694
10716
  summary: "One tick of the similar-artists sweep (artist centroids + top-K edges)",
10695
10717
  tags: ["Admin"]
10696
- }).input(object({ limit: number3().int().min(1).max(1000).default(50) })).output(object({
10718
+ }).input(object({
10719
+ countRemaining: boolean3().default(false),
10720
+ limit: number3().int().min(1).max(1000).default(50)
10721
+ })).output(object({
10697
10722
  ok: literal(true),
10698
10723
  summary: object({
10699
10724
  centroidsComputed: number2(),
@@ -11408,7 +11433,7 @@ var init_admin_bios = __esm(() => {
11408
11433
  });
11409
11434
 
11410
11435
  // ../../packages/contracts/src/orpc/admin-catalogue.ts
11411
- var CatalogueLensSchema, CapturePriorityReasonSchema, CatalogueMatchSchema, CatalogueTrackItemSchema, CatalogueSummarySchema, listCatalogueTracks, rankCatalogue, recordDemand, clearWrongAudio, requeueUnmatchedCaptures, requeueAnchor, flagWrongAudio, forceCapture, certifyTrack, setTrackDismissed, CaptureVerifyItemSchema, listUnverifiedCaptures, verifyCapture, CrawlPassSchema, CrawlPhaseInitializationSchema, CrawlPhaseInputSchema, CrawlPhaseOutputSchema, CrawlStatusSchema, crawlCatalogue, 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, 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, SpotifyAnchorBreakerStateSchema, getSpotifyAnchorBreaker, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
11436
+ var CatalogueLensSchema, CapturePriorityReasonSchema, CatalogueMatchSchema, CatalogueTrackItemSchema, CatalogueSummarySchema, listCatalogueTracks, rankCatalogue, recordDemand, clearWrongAudio, requeueUnmatchedCaptures, requeueAnchor, requeueIsrcRecovery, flagWrongAudio, forceCapture, certifyTrack, setTrackDismissed, CaptureVerifyItemSchema, listUnverifiedCaptures, verifyCapture, CrawlPassSchema, CrawlPhaseInitializationSchema, CrawlPhaseInputSchema, CrawlPhaseOutputSchema, CrawlStatusSchema, crawlCatalogue, 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, 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, SpotifyAnchorBreakerStateSchema, getSpotifyAnchorBreaker, resetSpotifyAnchorBreaker, AppleBreakerStateSchema, resetAppleBreaker, adminCatalogueContract;
11412
11437
  var init_admin_catalogue = __esm(() => {
11413
11438
  init_dist3();
11414
11439
  init_zod();
@@ -11537,6 +11562,21 @@ var init_admin_catalogue = __esm(() => {
11537
11562
  summary: "Clear named rows' anchor re-ask backoff so the next tick retries them (operator)",
11538
11563
  tags: ["Admin"]
11539
11564
  }).input(object({ trackIds: array(string2().min(1)).min(1).max(250) })).output(object({ ok: literal(true), requeued: number2() }));
11565
+ requeueIsrcRecovery = oc.route({
11566
+ method: "POST",
11567
+ operationId: "requeueIsrcRecovery",
11568
+ path: "/admin/catalogue/isrc-recovery/requeue",
11569
+ summary: "Clear Deezer-empty ISRC-recovery stamps from a named window (operator)",
11570
+ tags: ["Admin"]
11571
+ }).input(object({
11572
+ dryRun: boolean2().default(true),
11573
+ since: string2().regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z)?$/, "since must be an ISO date (2026-09-09) or instant (2026-09-09T00:00:00.000Z)")
11574
+ })).output(object({
11575
+ dryRun: boolean2(),
11576
+ matched: number2(),
11577
+ ok: literal(true),
11578
+ requeued: number2()
11579
+ }));
11540
11580
  flagWrongAudio = oc.route({
11541
11581
  method: "POST",
11542
11582
  operationId: "flagWrongAudio",
@@ -11909,6 +11949,7 @@ var init_admin_catalogue = __esm(() => {
11909
11949
  rank_catalogue: rankCatalogue,
11910
11950
  record_demand: recordDemand,
11911
11951
  requeue_anchor: requeueAnchor,
11952
+ requeue_isrc_recovery: requeueIsrcRecovery,
11912
11953
  requeue_unmatched_captures: requeueUnmatchedCaptures,
11913
11954
  reset_apple_breaker: resetAppleBreaker,
11914
11955
  reset_spotify_anchor_breaker: resetSpotifyAnchorBreaker,
@@ -12282,7 +12323,8 @@ var init_admin_projections = __esm(() => {
12282
12323
  processed: CountSchema,
12283
12324
  scheduled: CountSchema,
12284
12325
  status: ProjectionStatusSchema.optional(),
12285
- target: ProjectionTargetSchema
12326
+ target: ProjectionTargetSchema,
12327
+ trackSourceMarkersPending: boolean2().optional()
12286
12328
  }));
12287
12329
  setProjectionCutover = oc.route({
12288
12330
  method: "PUT",
@@ -13226,7 +13268,7 @@ var init_admin_operation_receipts = __esm(() => {
13226
13268
  });
13227
13269
 
13228
13270
  // ../../packages/contracts/src/orpc/admin-health.ts
13229
- var HEALTH_SNAPSHOT_PRODUCER_MAX = 64, HEALTH_SNAPSHOT_PRODUCER_PATTERN, ServiceHealthStatusSchema, HealthCheckSchema, recordHealth, adminHealthContract;
13271
+ var HEALTH_SNAPSHOT_PRODUCER_MAX = 64, HEALTH_SNAPSHOT_PRODUCER_PATTERN, HEALTH_SNAPSHOT_CHECKS_MAX = 128, HEALTH_SNAPSHOT_SERVICE_MAX = 64, ServiceHealthStatusSchema, HealthCheckSchema, recordHealth, adminHealthContract;
13230
13272
  var init_admin_health = __esm(() => {
13231
13273
  init_dist3();
13232
13274
  init_zod();
@@ -13236,7 +13278,7 @@ var init_admin_health = __esm(() => {
13236
13278
  HealthCheckSchema = object({
13237
13279
  latencyMs: number2().int().nullable(),
13238
13280
  message: string2().nullable(),
13239
- service: string2().min(1),
13281
+ service: string2().min(1).max(HEALTH_SNAPSHOT_SERVICE_MAX),
13240
13282
  status: ServiceHealthStatusSchema,
13241
13283
  transitioned: boolean2()
13242
13284
  }).meta({ id: "HealthCheck" });
@@ -13248,7 +13290,7 @@ var init_admin_health = __esm(() => {
13248
13290
  tags: ["Admin"]
13249
13291
  }).input(object({
13250
13292
  at: string2().max(64).datetime({ offset: true }),
13251
- checks: array(HealthCheckSchema),
13293
+ checks: array(HealthCheckSchema).max(HEALTH_SNAPSHOT_CHECKS_MAX),
13252
13294
  operationKey: string2().min(1).max(OPERATION_RECEIPT_KEY_MAX).regex(OPERATION_RECEIPT_KEY_PATTERN).optional(),
13253
13295
  producer: string2().max(HEALTH_SNAPSHOT_PRODUCER_MAX).regex(HEALTH_SNAPSHOT_PRODUCER_PATTERN).optional(),
13254
13296
  requestDigest: string2().regex(OPERATION_RECEIPT_REQUEST_DIGEST_PATTERN).optional()
@@ -13263,24 +13305,34 @@ var init_admin_health = __esm(() => {
13263
13305
  });
13264
13306
 
13265
13307
  // ../../packages/contracts/src/orpc/admin-hub-counts.ts
13266
- var HubCountsTableResultSchema, reconcileHubCounts, adminHubCountsContract;
13308
+ var HubCountsTableResultSchema, HubCountsReconcileCursorSchema, reconcileHubCounts, adminHubCountsContract;
13267
13309
  var init_admin_hub_counts = __esm(() => {
13268
13310
  init_dist3();
13269
13311
  init_zod();
13270
13312
  HubCountsTableResultSchema = object({
13271
- corrected: number2()
13313
+ corrected: number2(),
13314
+ deferred: number2()
13272
13315
  }).meta({ id: "HubCountsTableResult" });
13316
+ HubCountsReconcileCursorSchema = object({
13317
+ afterId: string2().min(1).max(512).nullable(),
13318
+ table: _enum(["labels", "albums", "artists"])
13319
+ }).meta({ id: "HubCountsReconcileCursor" });
13273
13320
  reconcileHubCounts = oc.route({
13274
13321
  method: "POST",
13275
13322
  operationId: "reconcileHubCounts",
13276
13323
  path: "/admin/hub-counts/reconcile",
13277
13324
  summary: "Reconcile the maintained hub counts against truth and report the corrected rows",
13278
13325
  tags: ["Admin"]
13279
- }).input(object({})).output(object({
13326
+ }).input(object({
13327
+ cursor: HubCountsReconcileCursorSchema.optional(),
13328
+ pageLimit: number2().int().min(1).max(20).optional()
13329
+ })).output(object({
13280
13330
  albums: HubCountsTableResultSchema,
13281
13331
  artists: HubCountsTableResultSchema,
13282
13332
  labels: HubCountsTableResultSchema,
13333
+ next: HubCountsReconcileCursorSchema.nullable(),
13283
13334
  ok: literal(true),
13335
+ pages: number2(),
13284
13336
  tookMs: number2()
13285
13337
  }));
13286
13338
  adminHubCountsContract = {
@@ -17199,10 +17251,10 @@ async function resolveDeezerSearch(track) {
17199
17251
  return;
17200
17252
  }
17201
17253
  try {
17202
- const query = `artist:"${artist}" track:"${stripVersionSuffix(track.title.trim())}"`;
17254
+ const query = `${artist} ${stripVersionSuffix(track.title.trim())}`;
17203
17255
  const response = await fetch(`https://api.deezer.com/search?q=${encodeURIComponent(query)}`);
17204
17256
  const body = await response.json();
17205
- const hit = (body.data ?? []).find((item) => item.preview && versionMatches(track.title, item.title ?? "") && baseTitleMatches(track.title, item.title ?? "") && durationAgrees(track.durationMs, item.duration));
17257
+ const hit = (body.data ?? []).find((item) => item.preview && normalize(item.artist?.name ?? "").includes(normalize(artist)) && versionMatches(track.title, item.title ?? "") && baseTitleMatches(track.title, item.title ?? "") && durationAgrees(track.durationMs, item.duration));
17206
17258
  return downloadPreview(hit?.preview, "deezer:search");
17207
17259
  } catch {
17208
17260
  return;
@@ -18474,6 +18526,9 @@ async function requeueAnchorCommand(trackIds) {
18474
18526
  trackIds
18475
18527
  });
18476
18528
  }
18529
+ async function requeueIsrcRecoveryCommand(input) {
18530
+ return adminApiPost("/api/v1/admin/catalogue/isrc-recovery/requeue", input);
18531
+ }
18477
18532
  async function forceCaptureCommand(trackId) {
18478
18533
  return adminApiPost("/api/v1/admin/catalogue/force-capture", {
18479
18534
  trackId
@@ -18601,7 +18656,10 @@ async function resolveArtistCommand(artistId) {
18601
18656
  }
18602
18657
  async function rankArtistsCommand(options) {
18603
18658
  const limit = options.limit ? Number.parseInt(options.limit, 10) : undefined;
18604
- const response = await adminApiPost("/api/v1/admin/artists/rank", limit ? { limit } : {});
18659
+ const response = await adminApiPost("/api/v1/admin/artists/rank", {
18660
+ ...limit ? { limit } : {},
18661
+ ...options.countRemaining ? { countRemaining: true } : {}
18662
+ });
18605
18663
  return { summary: response.summary };
18606
18664
  }
18607
18665
  async function backfillArtistImagesCommand(limit, dryRun, cursor) {
@@ -22127,6 +22185,18 @@ JSON field reference:
22127
22185
  }
22128
22186
  console.log(`Re-queued ${requeued} of ${trackIds.length} row(s) \u2014 the next anchor tick picks them up at their real priority.`);
22129
22187
  });
22188
+ catalogue.command("requeue-isrc-recovery").description("Clear Deezer-empty ISRC-recovery stamps from a window (dry-run; --apply to take)").requiredOption("--since <iso>", "Inclusive lower bound on the stamp (2026-09-09)").option("--apply", "Actually clear the stamps (default is a dry-run count)", false).option("--json", "Print JSON", false).action(async (options) => {
22189
+ await Promise.resolve().then(() => init_admin_catalogue2());
22190
+ const result = await requeueIsrcRecoveryCommand({
22191
+ dryRun: options.apply !== true,
22192
+ since: options.since
22193
+ });
22194
+ if (options.json) {
22195
+ printJson({ ok: true, ...result });
22196
+ return;
22197
+ }
22198
+ console.log(result.dryRun ? `DRY RUN \u2014 ${result.matched} row(s) stamped Deezer-empty since ${options.since}. Re-run with --apply to clear them.` : `Cleared ${result.requeued} of ${result.matched} row(s) \u2014 the next isrc-recovery tick asks them again.`);
22199
+ });
22130
22200
  catalogue.command("requeue-unmatched").description("Re-queue terminal-unmatched captures after a matcher improvement (operator)").option("--json", "Print JSON", false).action(async (options) => {
22131
22201
  await Promise.resolve().then(() => init_admin_catalogue2());
22132
22202
  const { requeued, skippedVetoed } = await requeueUnmatchedCommand();
@@ -24529,7 +24599,10 @@ async function runGalaxies(slug, options, commands) {
24529
24599
  }
24530
24600
  }
24531
24601
  async function runArtistsRank(options, rankArtistsCommand) {
24532
- const { summary } = await rankArtistsCommand({ limit: options.limit });
24602
+ const { summary } = await rankArtistsCommand({
24603
+ countRemaining: !options.json,
24604
+ limit: options.limit
24605
+ });
24533
24606
  if (options.json) {
24534
24607
  printJson({ ok: true, summary });
24535
24608
  return;
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.234.0"
34
+ "version": "0.236.0"
35
35
  }