fluncle 0.213.0 → 0.215.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 +225 -22
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -610,7 +610,7 @@ function parseVersion(version) {
610
610
  var currentVersion;
611
611
  var init_version = __esm(() => {
612
612
  init_output();
613
- currentVersion = "0.213.0".trim() ? "0.213.0".trim() : "0.1.0";
613
+ currentVersion = "0.215.0".trim() ? "0.215.0".trim() : "0.1.0";
614
614
  });
615
615
 
616
616
  // src/update-notifier.ts
@@ -2452,7 +2452,7 @@ function parseVersion2(version) {
2452
2452
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2453
2453
  var init_version2 = __esm(() => {
2454
2454
  init_output();
2455
- currentVersion2 = "0.213.0".trim() ? "0.213.0".trim() : "0.1.0";
2455
+ currentVersion2 = "0.215.0".trim() ? "0.215.0".trim() : "0.1.0";
2456
2456
  });
2457
2457
 
2458
2458
  // ../../packages/registry/src/index.ts
@@ -3550,6 +3550,22 @@ var init_src = __esm(() => {
3550
3550
  title: "Spotify anchors",
3551
3551
  weights: { status: "hidden" }
3552
3552
  },
3553
+ {
3554
+ exposedContent: [
3555
+ "mirror the anchored, explicitly allowlisted public catalogue into the shared read-only device replica"
3556
+ ],
3557
+ kind: "cron",
3558
+ name: "cron.device-mirror",
3559
+ operatorNotes: "hourly, run by a host systemd timer (docs/agents/hermes/device-mirror-timer/). Reads one consistent anchored-cut snapshot through a read-only source credential, diffs every whitelisted tuple against the shared device database, then applies dependent-first deletes and parent-first upserts in bounded parameterized batches. The target is mutated IN PLACE: rebuilding it would reset the libSQL replication log and force every device to bootstrap the full database again. A schema-version mismatch stops for an operator migration; the content fingerprint and derived timestamp update only after a verified zero-drift tick. Zero LLM tokens. Source: docs/agents/hermes/scripts/device-mirror.*. See docs/planning/offline-first-mobile-research.md.",
3560
+ probeConfig: {
3561
+ cadenceMs: 60 * MINUTE_MS,
3562
+ cronName: "fluncle-device-mirror",
3563
+ kind: "cron"
3564
+ },
3565
+ statusDescription: "keeps the offline catalogue replica in step",
3566
+ title: "Device catalogue mirror",
3567
+ weights: { status: "hidden" }
3568
+ },
3553
3569
  {
3554
3570
  command: "fluncle admin frontier refresh",
3555
3571
  exposedContent: [
@@ -3781,9 +3797,9 @@ var init_src = __esm(() => {
3781
3797
  weights: { status: "hidden" }
3782
3798
  },
3783
3799
  {
3784
- command: "fluncle admin backfills discogs && fluncle admin backfills lastfm && fluncle admin backfills apple-music && fluncle admin backfills apple-catalogue",
3800
+ command: "fluncle admin backfills discogs && fluncle admin backfills lastfm && fluncle admin backfills apple-music && fluncle admin backfills apple-catalogue && fluncle admin backfills beatport && fluncle admin backfills discogs-facts",
3785
3801
  exposedContent: [
3786
- "Discogs id + Last.fm love + Apple Music link repair, findings then catalogue (--no-agent, Worker HTTP)"
3802
+ "Discogs id + Last.fm love + Apple Music link + Beatport link repair, findings then catalogue, then each record's catalogue number (--no-agent, Worker HTTP)"
3787
3803
  ],
3788
3804
  kind: "cron",
3789
3805
  name: "cron.backfill",
@@ -4478,9 +4494,21 @@ __export(exports_admin_telemetry, {
4478
4494
  });
4479
4495
  async function telemetryCommand(options) {
4480
4496
  const params = new URLSearchParams({ limit: String(options.limit) });
4497
+ if (options.blind === true) {
4498
+ params.set("blind", "true");
4499
+ }
4481
4500
  if (options.cursor !== undefined) {
4482
4501
  params.set("cursor", options.cursor);
4483
4502
  }
4503
+ if (options.liar === true) {
4504
+ params.set("liar", "true");
4505
+ }
4506
+ if (options.missing === true) {
4507
+ params.set("missing", "true");
4508
+ }
4509
+ if (options.missingField !== undefined) {
4510
+ params.set("missingField", options.missingField);
4511
+ }
4484
4512
  if (options.ok !== undefined) {
4485
4513
  params.set("ok", String(options.ok));
4486
4514
  }
@@ -4507,16 +4535,50 @@ function cell(value) {
4507
4535
  function padded(columns, widths) {
4508
4536
  return columns.map((column, index) => column.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
4509
4537
  }
4510
- function telemetryLines(page) {
4538
+ function cadence(expectedIntervalMs) {
4539
+ if (expectedIntervalMs === null) {
4540
+ return "-";
4541
+ }
4542
+ const units = [
4543
+ { label: "d", milliseconds: 24 * 60 * 60 * 1000 },
4544
+ { label: "h", milliseconds: 60 * 60 * 1000 },
4545
+ { label: "m", milliseconds: 60 * 1000 },
4546
+ { label: "s", milliseconds: 1000 }
4547
+ ];
4548
+ for (const unit of units) {
4549
+ if (expectedIntervalMs % unit.milliseconds === 0) {
4550
+ return `${expectedIntervalMs / unit.milliseconds}${unit.label}`;
4551
+ }
4552
+ }
4553
+ return `${expectedIntervalMs}ms`;
4554
+ }
4555
+ function telemetryLines(page, options = {}) {
4511
4556
  if (!page.available) {
4512
4557
  return ["Telemetry database is not configured."];
4513
4558
  }
4514
- if (page.rows.length === 0) {
4559
+ if (options.missing === true || page.missingRoster.length > 0) {
4560
+ if (page.missingRoster.length === 0) {
4561
+ return ["No expected writers are missing."];
4562
+ }
4563
+ const missingHeader = ["MISSING UNIT", "CADENCE"];
4564
+ const missingRows = page.missingRoster.map((entry) => [
4565
+ entry.unit,
4566
+ cadence(entry.expectedIntervalMs)
4567
+ ]);
4568
+ const missingWidths = missingHeader.map((header, index) => Math.max(header.length, ...missingRows.map((row) => row[index]?.length ?? 0)));
4569
+ return [
4570
+ `Expected writers with no run (${missingRows.length})`,
4571
+ padded(missingHeader, missingWidths),
4572
+ ...missingRows.map((row) => padded(row, missingWidths))
4573
+ ];
4574
+ }
4575
+ if (page.rows.length === 0 && page.rollups.length === 0) {
4515
4576
  return ["No run events matched."];
4516
4577
  }
4517
- const rollupHeader = ["UNIT", "RUNS", "LAST", "OK=0", "LIAR", "BLIND"];
4578
+ const rollupHeader = ["UNIT", "CADENCE", "RUNS", "LAST", "OK=0", "LIAR", "BLIND"];
4518
4579
  const rollupRows = page.rollups.map((rollup) => [
4519
4580
  rollup.unit,
4581
+ cadence(rollup.expectedIntervalMs),
4520
4582
  String(rollup.runCount),
4521
4583
  rollup.lastOccurredAt,
4522
4584
  String(rollup.failedCount),
@@ -4547,15 +4609,18 @@ function telemetryLines(page) {
4547
4609
  row.summaryStatus
4548
4610
  ]);
4549
4611
  const runWidths = runHeader.map((header, index) => Math.max(header.length, ...runRows.map((row) => row[index]?.length ?? 0)));
4550
- const lines = [
4551
- `Unit rollups (${page.totalCount} matching runs)`,
4552
- padded(rollupHeader, rollupWidths),
4553
- ...rollupRows.map((row) => padded(row, rollupWidths)),
4554
- "",
4555
- `Runs (${page.rows.length} on this page)`,
4556
- padded(runHeader, runWidths),
4557
- ...runRows.map((row) => padded(row, runWidths))
4558
- ];
4612
+ const lines = [];
4613
+ if (rollupRows.length > 0) {
4614
+ lines.push("Unit rollups (all runs in the selected time/unit window)", padded(rollupHeader, rollupWidths), ...rollupRows.map((row) => padded(row, rollupWidths)));
4615
+ }
4616
+ if (lines.length > 0) {
4617
+ lines.push("");
4618
+ }
4619
+ if (runRows.length === 0) {
4620
+ lines.push("No evidence rows matched.");
4621
+ } else {
4622
+ lines.push(`Evidence rows (${page.rows.length} of ${page.totalCount} matching; this page)`, padded(runHeader, runWidths), ...runRows.map((row) => padded(row, runWidths)));
4623
+ }
4559
4624
  if (page.nextCursor !== null) {
4560
4625
  lines.push("", `Next cursor: ${page.nextCursor}`);
4561
4626
  }
@@ -4663,6 +4728,7 @@ __export(exports_admin_tracks, {
4663
4728
  captureQueueCommand: () => captureQueueCommand,
4664
4729
  backfillRecordingMbidsCommand: () => backfillRecordingMbidsCommand,
4665
4730
  backfillLastfmCommand: () => backfillLastfmCommand,
4731
+ backfillDiscogsFactsCommand: () => backfillDiscogsFactsCommand,
4666
4732
  backfillDiscogsCommand: () => backfillDiscogsCommand,
4667
4733
  backfillBeatportCommand: () => backfillBeatportCommand,
4668
4734
  backfillArtistEdgesCommand: () => backfillArtistEdgesCommand,
@@ -4848,6 +4914,10 @@ async function backfillAppleMusicCommand(limit, dryRun, cursor) {
4848
4914
  }
4849
4915
  return adminApiPost(`/api/v1/admin/backfill/apple-music?${params.toString()}`);
4850
4916
  }
4917
+ async function backfillDiscogsFactsCommand(limit, dryRun) {
4918
+ const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4919
+ return adminApiPost(`/api/v1/admin/backfill/discogs-facts?${params.toString()}`);
4920
+ }
4851
4921
  async function backfillBeatportCommand(limit, dryRun, cursor) {
4852
4922
  const params = new URLSearchParams({ dryRun: String(dryRun), limit: String(limit) });
4853
4923
  if (cursor) {
@@ -9768,7 +9838,19 @@ function addAdminCommands(program2) {
9768
9838
  adminTelemetry.action(() => {
9769
9839
  adminTelemetry.outputHelp();
9770
9840
  });
9771
- adminTelemetry.command("read").description("Run-ledger rows plus per-unit rollups").option("--unit <unit>", "Only this systemd unit").option("--since <iso>", "Only runs at or after this occurred-at instant").option("--until <iso>", "Only runs at or before this occurred-at instant").option("--ok <true|false>", "Filter by the Worker's derived run verdict").option("--limit <limit>", "Rows to show (1-100)", "50").option("--cursor <cursor>", "Opaque cursor from the previous page").option("--json", "Print the lossless 20-column rows as JSON", false).action(async (options) => {
9841
+ adminTelemetry.command("read").description("Run-ledger rows plus per-unit rollups").option("--unit <unit>", "Only this systemd unit").option("--since <iso|age>", "Only runs at or after this instant (ISO, 24h, or 90m)").option("--until <iso>", "Only runs at or before this occurred-at instant").option("--ok <true|false>", "Filter by the Worker's derived run verdict").option("--liar", "Only rows whose claimed ok contradicts the derived verdict").option("--blind", "Only rows missing every work counter").option("--missing-field <name>", "Only rows missing this canonical counter").option("--missing", "List expected writers with no run in the selected window").option("--limit <limit>", "Rows to show (1-100)", "50").option("--cursor <cursor>", "Opaque cursor from the previous page").option("--json", "Print lossless rows, rollups, and missing writers as JSON", false).addHelpText("after", `
9842
+ Evidence and rollups:
9843
+ --ok, --liar, --blind, and --missing-field filter evidence rows only.
9844
+ RUNS always counts every run in the selected --unit/--since/--until window.
9845
+ --missing is a separate roster view; combine it only with --unit/--since/--until.
9846
+ --missing-field names: checked, produced, queue_depth, errors, expected_interval_ms.
9847
+
9848
+ JSON field reference:
9849
+ top-level ok Request acknowledgement only; never a run verdict.
9850
+ rows[].ok Worker's derived verdict for that run.
9851
+ rollups[] Whole-window unit totals, including expectedIntervalMs.
9852
+ missingRoster[] Expected writers with no run in the selected window.
9853
+ `).action(async (options) => {
9772
9854
  const { telemetryCommand: telemetryCommand2 } = await Promise.resolve().then(() => (init_admin_telemetry(), exports_admin_telemetry));
9773
9855
  await runAdminTelemetry(options, telemetryCommand2);
9774
9856
  });
@@ -10409,6 +10491,10 @@ function addAdminCommands(program2) {
10409
10491
  const { backfillDiscogsCommand: backfillDiscogsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10410
10492
  await runBackfillDiscogs(options, backfillDiscogsCommand2);
10411
10493
  });
10494
+ backfill.command("discogs-facts").description("Read album catalogue numbers + styles from already-resolved Discogs releases").option("--dry-run", "Report the eligible albums without reading Discogs or writing", false).option("--limit <limit>", "Max albums to read", "50").option("--json", "Print JSON", false).action(async (options) => {
10495
+ const { backfillDiscogsFactsCommand: backfillDiscogsFactsCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10496
+ await runBackfillDiscogsFacts(options, backfillDiscogsFactsCommand2);
10497
+ });
10412
10498
  backfill.command("apple-music").description("Resolve missing Apple Music URLs for published findings (exact ISRC match)").option("--dry-run", "Report the eligible set without resolving or writing", false).option("--limit <limit>", "Max findings to resolve", "50").option("--json", "Print JSON", false).action(async (options) => {
10413
10499
  const { backfillAppleMusicCommand: backfillAppleMusicCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
10414
10500
  await runBackfillAppleMusic(options, backfillAppleMusicCommand2);
@@ -11206,6 +11292,63 @@ async function runBackfillAppleCatalogue(options, backfillAppleCatalogueCommand2
11206
11292
  process.exitCode = 1;
11207
11293
  }
11208
11294
  }
11295
+ async function runBackfillDiscogsFacts(options, backfillDiscogsFactsCommand2) {
11296
+ const limit = parseListLimit(options.limit);
11297
+ const resolved = [];
11298
+ const none = [];
11299
+ const failed = [];
11300
+ let dryRun = options.dryRun;
11301
+ let throttled = false;
11302
+ let configured = true;
11303
+ while (resolved.length + none.length + failed.length < limit) {
11304
+ const remaining = limit - (resolved.length + none.length + failed.length);
11305
+ const result = await backfillDiscogsFactsCommand2(remaining, options.dryRun);
11306
+ dryRun = result.dryRun;
11307
+ configured = result.configured;
11308
+ resolved.push(...result.resolved);
11309
+ none.push(...result.none);
11310
+ failed.push(...result.failed);
11311
+ if (!options.json) {
11312
+ const verb2 = result.dryRun ? "would read" : "read";
11313
+ console.log(` \u2026${verb2} ${result.resolvedCount} catalogue number(s); ${result.noneCount} with none; ${result.failedCount} failed`);
11314
+ }
11315
+ if (!result.configured) {
11316
+ break;
11317
+ }
11318
+ if (result.rateLimited) {
11319
+ throttled = true;
11320
+ break;
11321
+ }
11322
+ if (result.resolvedCount === 0 && result.noneCount === 0 && result.failedCount === 0) {
11323
+ break;
11324
+ }
11325
+ }
11326
+ if (options.json) {
11327
+ printSweepJson({
11328
+ configured,
11329
+ dryRun,
11330
+ failed,
11331
+ none,
11332
+ noneCount: none.length,
11333
+ rateLimited: throttled,
11334
+ resolved,
11335
+ resolvedCount: resolved.length
11336
+ }, failed.length);
11337
+ return;
11338
+ }
11339
+ if (!configured) {
11340
+ console.log("Discogs facts backfill is not configured (the Worker's DISCOGS_USER_TOKEN is unset) \u2014 nothing read.");
11341
+ return;
11342
+ }
11343
+ const verb = dryRun ? "Would read" : "Read";
11344
+ console.log(`${verb} ${resolved.length} catalogue number(s); ${none.length} release(s) carry none; ${failed.length} failed.`);
11345
+ for (const item of failed) {
11346
+ console.log(` ${item.slug}: ${item.error}`);
11347
+ }
11348
+ if (failed.length > 0) {
11349
+ process.exitCode = 1;
11350
+ }
11351
+ }
11209
11352
  async function runBackfillArtists(options, backfillArtistsCommand2) {
11210
11353
  const limit = parseListLimit(options.limit);
11211
11354
  const upserted = [];
@@ -12677,13 +12820,62 @@ function parseTelemetryOk(value) {
12677
12820
  }
12678
12821
  return value === "true";
12679
12822
  }
12680
- function parseTelemetryTimestamp(value, flag) {
12823
+ var telemetryMissingFields = [
12824
+ "checked",
12825
+ "errors",
12826
+ "expected_interval_ms",
12827
+ "produced",
12828
+ "queue_depth"
12829
+ ];
12830
+ function parseTelemetryMissingField(value) {
12681
12831
  if (value === undefined) {
12682
12832
  return;
12683
12833
  }
12834
+ const field = telemetryMissingFields.find((candidate) => candidate === value);
12835
+ if (field === undefined) {
12836
+ throw new Error(`--missing-field must be one of: ${telemetryMissingFields.join(", ")}`);
12837
+ }
12838
+ return field;
12839
+ }
12840
+ function parseExplicitTelemetryInstant(value) {
12841
+ if (value.length > 64) {
12842
+ return null;
12843
+ }
12844
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.exec(value);
12845
+ if (match === null) {
12846
+ return null;
12847
+ }
12848
+ const year = Number(match[1]);
12849
+ const month = Number(match[2]);
12850
+ const day = Number(match[3]);
12851
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
12852
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
12853
+ const maximumDay = daysInMonth[month - 1];
12854
+ if (maximumDay === undefined || day < 1 || day > maximumDay) {
12855
+ return null;
12856
+ }
12684
12857
  const timestamp = new Date(value);
12685
- if (Number.isNaN(timestamp.getTime())) {
12686
- throw new Error(`${flag} must be an ISO-8601 timestamp`);
12858
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp;
12859
+ }
12860
+ function parseTelemetryTimestamp(value, flag) {
12861
+ if (value === undefined) {
12862
+ return;
12863
+ }
12864
+ const relative = flag === "--since" ? value.match(/^([1-9]\d*)([mhdw])$/) : null;
12865
+ if (relative !== null) {
12866
+ const amount = Number(relative[1]);
12867
+ const unitMs = relative[2] === "w" ? 604800000 : relative[2] === "d" ? 86400000 : relative[2] === "h" ? 3600000 : 60000;
12868
+ const durationMs = amount * unitMs;
12869
+ const maximumDurationMs = 315360000000;
12870
+ if (value.length > 32 || !Number.isSafeInteger(amount) || !Number.isSafeInteger(durationMs) || durationMs > maximumDurationMs) {
12871
+ throw new Error("--since relative age must not exceed 3650d");
12872
+ }
12873
+ return value;
12874
+ }
12875
+ const timestamp = parseExplicitTelemetryInstant(value);
12876
+ if (timestamp === null) {
12877
+ const expected = flag === "--since" ? "an explicit ISO-8601 instant (with Z or offset) or a relative age like 24h or 90m" : "an explicit ISO-8601 instant with Z or offset";
12878
+ throw new Error(`${flag} must be ${expected}`);
12687
12879
  }
12688
12880
  return timestamp.toISOString();
12689
12881
  }
@@ -12755,14 +12947,23 @@ async function runAdminAttention(options, attentionQueueCommand2) {
12755
12947
  `));
12756
12948
  }
12757
12949
  async function runAdminTelemetry(options, telemetryCommand2) {
12950
+ if (options.missing === true && (options.blind === true || options.cursor !== undefined || options.liar === true || options.missingField !== undefined || options.ok !== undefined)) {
12951
+ throw new Error("--missing cannot be combined with --ok, --liar, --blind, --missing-field, or --cursor");
12952
+ }
12758
12953
  const since = parseTelemetryTimestamp(options.since, "--since");
12759
12954
  const until = parseTelemetryTimestamp(options.until, "--until");
12760
- if (since !== undefined && until !== undefined && since > until) {
12955
+ const sinceMs = since === undefined ? Number.NaN : Date.parse(since);
12956
+ const untilMs = until === undefined ? Number.NaN : Date.parse(until);
12957
+ if (Number.isFinite(sinceMs) && Number.isFinite(untilMs) && sinceMs > untilMs) {
12761
12958
  throw new Error("--since must be before or equal to --until");
12762
12959
  }
12763
12960
  const page = await telemetryCommand2({
12961
+ blind: options.blind,
12764
12962
  cursor: options.cursor,
12963
+ liar: options.liar,
12765
12964
  limit: parseTelemetryLimit(options.limit),
12965
+ missing: options.missing,
12966
+ missingField: parseTelemetryMissingField(options.missingField),
12766
12967
  ok: parseTelemetryOk(options.ok),
12767
12968
  since,
12768
12969
  unit: options.unit,
@@ -12773,7 +12974,7 @@ async function runAdminTelemetry(options, telemetryCommand2) {
12773
12974
  return;
12774
12975
  }
12775
12976
  const { telemetryLines: telemetryLines2 } = await Promise.resolve().then(() => (init_admin_telemetry(), exports_admin_telemetry));
12776
- console.log(telemetryLines2(page).join(`
12977
+ console.log(telemetryLines2(page, { missing: options.missing === true }).join(`
12777
12978
  `));
12778
12979
  }
12779
12980
  async function runAdminQueue(options, queueCommand2) {
@@ -13240,6 +13441,7 @@ var stringOptions = new Set([
13240
13441
  "--max-hop",
13241
13442
  "--max-overlap",
13242
13443
  "--metrics",
13444
+ "--missing-field",
13243
13445
  "--mime",
13244
13446
  "--min-phrase-words",
13245
13447
  "--model",
@@ -13323,5 +13525,6 @@ if (__require.main == __require.module) {
13323
13525
  await main();
13324
13526
  }
13325
13527
  export {
13528
+ parseTelemetryTimestamp,
13326
13529
  createProgram
13327
13530
  };
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.213.0"
34
+ "version": "0.215.0"
35
35
  }