@cronwatch/sdk 0.11.0 → 0.12.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 (104) hide show
  1. package/README.md +4 -1
  2. package/dist/anthropic.d.cts +1 -1
  3. package/dist/anthropic.d.ts +1 -1
  4. package/dist/bugsnag.cjs +1 -1
  5. package/dist/bugsnag.cjs.map +1 -1
  6. package/dist/bugsnag.d.cts +1 -1
  7. package/dist/bugsnag.d.ts +1 -1
  8. package/dist/bugsnag.js +1 -1
  9. package/dist/bugsnag.js.map +1 -1
  10. package/dist/{client-CRMfhylq.d.cts → client-BkPMRkDq.d.cts} +1 -1
  11. package/dist/{client-D2Xk2600.d.ts → client-DySsV5T0.d.ts} +1 -1
  12. package/dist/d1.d.cts +1 -1
  13. package/dist/d1.d.ts +1 -1
  14. package/dist/datadog.cjs +1 -0
  15. package/dist/datadog.cjs.map +1 -1
  16. package/dist/datadog.d.cts +1 -1
  17. package/dist/datadog.d.ts +1 -1
  18. package/dist/datadog.js +1 -0
  19. package/dist/datadog.js.map +1 -1
  20. package/dist/discord.cjs +1 -0
  21. package/dist/discord.cjs.map +1 -1
  22. package/dist/discord.d.cts +1 -1
  23. package/dist/discord.d.ts +1 -1
  24. package/dist/discord.js +1 -0
  25. package/dist/discord.js.map +1 -1
  26. package/dist/{email-BWuUiCkw.d.ts → email-Bx3ehK0O.d.ts} +1 -1
  27. package/dist/{email-Dcqe3k8n.d.cts → email-CD6NO8CV.d.cts} +1 -1
  28. package/dist/honeybadger.cjs +1 -0
  29. package/dist/honeybadger.cjs.map +1 -1
  30. package/dist/honeybadger.d.cts +1 -1
  31. package/dist/honeybadger.d.ts +1 -1
  32. package/dist/honeybadger.js +1 -0
  33. package/dist/honeybadger.js.map +1 -1
  34. package/dist/index.cjs +83 -7
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +5 -5
  37. package/dist/index.d.ts +5 -5
  38. package/dist/index.js +83 -7
  39. package/dist/index.js.map +1 -1
  40. package/dist/mailgun.cjs.map +1 -1
  41. package/dist/mailgun.d.cts +2 -2
  42. package/dist/mailgun.d.ts +2 -2
  43. package/dist/mailgun.js.map +1 -1
  44. package/dist/newrelic.cjs +1 -1
  45. package/dist/newrelic.cjs.map +1 -1
  46. package/dist/newrelic.d.cts +1 -1
  47. package/dist/newrelic.d.ts +1 -1
  48. package/dist/newrelic.js +1 -1
  49. package/dist/newrelic.js.map +1 -1
  50. package/dist/pg-cron.cjs +2 -1
  51. package/dist/pg-cron.cjs.map +1 -1
  52. package/dist/pg-cron.d.cts +2 -2
  53. package/dist/pg-cron.d.ts +2 -2
  54. package/dist/pg-cron.js +2 -1
  55. package/dist/pg-cron.js.map +1 -1
  56. package/dist/postgres.d.cts +1 -1
  57. package/dist/postgres.d.ts +1 -1
  58. package/dist/postmark.cjs.map +1 -1
  59. package/dist/postmark.d.cts +2 -2
  60. package/dist/postmark.d.ts +2 -2
  61. package/dist/postmark.js.map +1 -1
  62. package/dist/resend.cjs.map +1 -1
  63. package/dist/resend.d.cts +2 -2
  64. package/dist/resend.d.ts +2 -2
  65. package/dist/resend.js.map +1 -1
  66. package/dist/rollbar.cjs +1 -1
  67. package/dist/rollbar.cjs.map +1 -1
  68. package/dist/rollbar.d.cts +1 -1
  69. package/dist/rollbar.d.ts +1 -1
  70. package/dist/rollbar.js +1 -1
  71. package/dist/rollbar.js.map +1 -1
  72. package/dist/sendgrid.cjs.map +1 -1
  73. package/dist/sendgrid.d.cts +2 -2
  74. package/dist/sendgrid.d.ts +2 -2
  75. package/dist/sendgrid.js.map +1 -1
  76. package/dist/sentry.cjs +1 -1
  77. package/dist/sentry.cjs.map +1 -1
  78. package/dist/sentry.d.cts +1 -1
  79. package/dist/sentry.d.ts +1 -1
  80. package/dist/sentry.js +1 -1
  81. package/dist/sentry.js.map +1 -1
  82. package/dist/ses.cjs.map +1 -1
  83. package/dist/ses.d.cts +2 -2
  84. package/dist/ses.d.ts +2 -2
  85. package/dist/ses.js.map +1 -1
  86. package/dist/slack.cjs +1 -0
  87. package/dist/slack.cjs.map +1 -1
  88. package/dist/slack.d.cts +1 -1
  89. package/dist/slack.d.ts +1 -1
  90. package/dist/slack.js +1 -0
  91. package/dist/slack.js.map +1 -1
  92. package/dist/sqlite.d.cts +1 -1
  93. package/dist/sqlite.d.ts +1 -1
  94. package/dist/twilio.cjs.map +1 -1
  95. package/dist/twilio.d.cts +1 -1
  96. package/dist/twilio.d.ts +1 -1
  97. package/dist/twilio.js.map +1 -1
  98. package/dist/{types-DLnlUie8.d.cts → types-Bm972Uqj.d.cts} +19 -1
  99. package/dist/{types-DLnlUie8.d.ts → types-Bm972Uqj.d.ts} +19 -1
  100. package/dist/webhook.cjs.map +1 -1
  101. package/dist/webhook.d.cts +1 -1
  102. package/dist/webhook.d.ts +1 -1
  103. package/dist/webhook.js.map +1 -1
  104. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-CRMfhylq.cjs';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-CRMfhylq.cjs';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.cjs';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.cjs';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-BkPMRkDq.cjs';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-BkPMRkDq.cjs';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Bm972Uqj.cjs';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-Bm972Uqj.cjs';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.11.0";
14
+ declare const VERSION = "0.12.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as CronwatchOptions, a as Cronwatch } from './client-D2Xk2600.js';
2
- export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-D2Xk2600.js';
3
- import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-DLnlUie8.js';
4
- export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-DLnlUie8.js';
1
+ import { C as CronwatchOptions, a as Cronwatch } from './client-DySsV5T0.js';
2
+ export { b as CronWatch, c as CronWatchOptions, F as FetchHandler, H as HandlerFn, d as HandlerOptions, J as JobContext, e as JobFn, f as JobHandle, R as RecordRunOptions, g as Routes, h as RoutesOptions, i as RunHandle, j as RunOutcome, S as Source, k as SourceHost, l as StartOptions, m as consoleChannel, n as createRoutes, o as custom } from './client-DySsV5T0.js';
3
+ import { S as Store, D as Duration, b as AlertDraft, c as StoredJobDefinition, A as Alert } from './types-Bm972Uqj.js';
4
+ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach, C as ChannelContext, f as CheckResult, g as Condition, E as ExpectRule, h as JobDefinition, i as JobHealth, J as JobOptions, j as JobState, k as JobSummary, R as Run, l as RunStatus, m as SendingAlert, n as StoredJob, T as TriageContext, o as TriageFn } from './types-Bm972Uqj.js';
5
5
 
6
6
  /**
7
7
  * Keeps everything in process memory. The default when no store is given,
@@ -11,7 +11,7 @@ export { a as AlertChannel, d as AlertDetails, e as AlertType, B as BudgetBreach
11
11
  declare function memory(): Store;
12
12
 
13
13
  /** This release of @cronwatch/sdk. scripts/release.mjs keeps it in step with package.json. */
14
- declare const VERSION = "0.11.0";
14
+ declare const VERSION = "0.12.0";
15
15
 
16
16
  /**
17
17
  * "15m" -> 900000. Accepts a plain number of milliseconds, and compound
package/dist/index.js CHANGED
@@ -326,7 +326,7 @@ function isObject(value) {
326
326
  }
327
327
  function normalizeState(state, job) {
328
328
  if (!isObject(state)) return emptyState(job);
329
- const { sending, ...rest } = state;
329
+ const { sending, underFloor, ...rest } = state;
330
330
  const open = isObject(state.open) ? Object.fromEntries(Object.entries(state.open).filter(([, at]) => typeof at === "number")) : {};
331
331
  const number = (value) => typeof value === "number" ? value : null;
332
332
  const list = (value) => Array.isArray(value) ? value : [];
@@ -339,7 +339,8 @@ function normalizeState(state, job) {
339
339
  lastAlertAt: number(state.lastAlertAt),
340
340
  pendingRecovery: list(state.pendingRecovery).filter((c) => typeof c === "string"),
341
341
  undelivered: list(state.undelivered).filter((alert) => isObject(alert)),
342
- ...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {}
342
+ ...Array.isArray(sending) && sending.length > 0 ? { sending: [...sending] } : {},
343
+ ...Array.isArray(underFloor) && underFloor.some((m) => typeof m === "string") ? { underFloor: underFloor.filter((m) => typeof m === "string") } : {}
343
344
  };
344
345
  }
345
346
  var MAX_UNDELIVERED = 20;
@@ -439,6 +440,26 @@ function budgetBreaches(def, run, history) {
439
440
  }
440
441
  return breaches;
441
442
  }
443
+ function floorBreaches(def, run, history, previous = []) {
444
+ const breaches = [];
445
+ for (const [metric, value] of Object.entries(run.metrics)) {
446
+ const floor = def.floor?.[metric];
447
+ if (floor !== void 0) {
448
+ if (value < floor) breaches.push({ metric, value, limit: floor, basis: "floor" });
449
+ continue;
450
+ }
451
+ if (value > 0) continue;
452
+ if (previous.includes(metric)) {
453
+ breaches.push({ metric, value, limit: 0, basis: "0 or less on the run before too" });
454
+ continue;
455
+ }
456
+ const past = history.filter((r) => r.status === "ok" && typeof r.metrics[metric] === "number").slice(0, BASELINE_WINDOW).map((r) => r.metrics[metric]);
457
+ if (past.length < BASELINE_MIN_RUNS || !past.every((v) => v > 0)) continue;
458
+ const lowest = Math.min(...past);
459
+ breaches.push({ metric, value, limit: lowest, basis: `the last ${past.length} runs all reported more than 0, the lowest ${formatNumber(lowest)}` });
460
+ }
461
+ return breaches;
462
+ }
442
463
  function hasFullBaseline(history) {
443
464
  return history.filter((r) => r.status === "ok").length >= BASELINE_WINDOW;
444
465
  }
@@ -476,6 +497,16 @@ function onRunFinish(def, run, state, history, now) {
476
497
  } else {
477
498
  closeCondition(next, "over_budget");
478
499
  }
500
+ const short = floorBreaches(def, run, history, next.underFloor);
501
+ if (short.length > 0) {
502
+ next.underFloor = short.map((b) => b.metric);
503
+ if (openCondition(next, "under_floor", now)) {
504
+ alerts.push({ type: "under_floor", run, details: { breaches: short } });
505
+ }
506
+ } else {
507
+ delete next.underFloor;
508
+ closeCondition(next, "under_floor");
509
+ }
479
510
  const pending = next.pendingRecovery ?? [];
480
511
  if (pending.length > 0 && openConditions(next).length === 0) {
481
512
  alerts.push({ type: "recovered", run, details: { after: [...pending] } });
@@ -660,6 +691,14 @@ ${out}`);
660
691
  if (run) lines.push(`Started ${when(run.startedAt, now)}.`);
661
692
  break;
662
693
  }
694
+ case "under_floor": {
695
+ title = `${name} fell short`;
696
+ for (const b of draft.details.breaches) {
697
+ lines.push(b.basis === "floor" ? `${b.metric}: ${formatNumber(b.value)}, below the floor of ${formatNumber(b.limit)}.` : `${b.metric}: ${formatNumber(b.value)} (${b.basis}).`);
698
+ }
699
+ if (run) lines.push(`Started ${when(run.startedAt, now)}.`);
700
+ break;
701
+ }
663
702
  case "recovered": {
664
703
  if (draft.details.reason === "unscheduled") {
665
704
  title = `${name} is no longer scheduled`;
@@ -912,6 +951,21 @@ function manifest(base) {
912
951
  });
913
952
  }
914
953
  var APP_JS = `"use strict";
954
+ (function () {
955
+ var root = document.documentElement;
956
+ try {
957
+ var stored = localStorage.getItem("cronwatch-theme");
958
+ if (stored === "light" || stored === "dark") root.setAttribute("data-theme", stored);
959
+ } catch (e) {}
960
+ document.addEventListener("keydown", function (event) {
961
+ if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey || event.code !== "KeyD") return;
962
+ event.preventDefault();
963
+ var shown = root.getAttribute("data-theme") || (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
964
+ var next = shown === "dark" ? "light" : "dark";
965
+ root.setAttribute("data-theme", next);
966
+ try { localStorage.setItem("cronwatch-theme", next); } catch (e) {}
967
+ });
968
+ })();
915
969
  (function () {
916
970
  var script = document.currentScript;
917
971
  if (!script || !("serviceWorker" in navigator)) return;
@@ -1063,7 +1117,7 @@ function toneOf(run, job, now) {
1063
1117
  if (run.status === "failed") return "bad";
1064
1118
  if (run.status === "timeout") return "timeout";
1065
1119
  const latest = job.lastRun?.id === run.id;
1066
- return latest && (job.open.includes("over_budget") || job.open.includes("slow")) ? "warn" : "ok";
1120
+ return latest && (job.open.includes("over_budget") || job.open.includes("under_floor") || job.open.includes("slow")) ? "warn" : "ok";
1067
1121
  }
1068
1122
  function timeoutText(job) {
1069
1123
  return safely(() => formatDuration(timeoutMs(job.definition)), "configured");
@@ -1073,13 +1127,17 @@ function describeRun(run, tone, job, now) {
1073
1127
  if (tone === "running") return `running since ${at}, ${formatDuration(now - run.startedAt)} so far`;
1074
1128
  if (tone === "stuck") return `running since ${at}, past its ${timeoutText(job)} timeout`;
1075
1129
  const took = run.durationMs !== null ? `, took ${formatDuration(run.durationMs)}` : "";
1076
- const extra = tone === "warn" ? job.open.includes("over_budget") ? ", over budget" : ", slow" : "";
1130
+ const extra = tone === "warn" ? job.open.includes("over_budget") ? ", over budget" : job.open.includes("under_floor") ? ", under floor" : ", slow" : "";
1077
1131
  return `${run.status} at ${at}${took}${extra}`;
1078
1132
  }
1079
1133
  function overCeilings(job) {
1080
1134
  const metrics = job.lastRun?.metrics ?? {};
1081
1135
  return Object.entries(job.definition.budget ?? {}).filter(([k, limit]) => (metrics[k] ?? -Infinity) > limit).map(([k]) => k);
1082
1136
  }
1137
+ function underFloors(job) {
1138
+ const floors = job.definition.floor ?? {};
1139
+ return Object.entries(job.lastRun?.metrics ?? {}).filter(([k, v]) => floors[k] !== void 0 ? v < floors[k] : v <= 0).map(([k]) => k);
1140
+ }
1083
1141
  function laneNote(job, missed, now) {
1084
1142
  const last = job.lastRun;
1085
1143
  if (job.silencedUntil !== null && job.silencedUntil > now) return `silenced until ${when2(job.silencedUntil, now)}`;
@@ -1095,6 +1153,10 @@ function laneNote(job, missed, now) {
1095
1153
  const over = overCeilings(job);
1096
1154
  return `went over budget${over.length ? ` on ${over.join(" and ")}` : ""} at ${when2(last.startedAt, now)}`;
1097
1155
  }
1156
+ if (job.open.includes("under_floor") && last) {
1157
+ const under = underFloors(job);
1158
+ return `fell short${under.length ? ` on ${under.join(" and ")}` : ""} at ${when2(last.startedAt, now)}`;
1159
+ }
1098
1160
  if (job.open.includes("slow") && last?.durationMs != null) return `slow: took ${formatDuration(last.durationMs)}`;
1099
1161
  if (job.open.includes("failed")) return "failing";
1100
1162
  if (!last && job.nextExpectedAt !== null) return `no runs yet, first due ${when2(job.nextExpectedAt, now)}`;
@@ -1211,7 +1273,7 @@ function legend() {
1211
1273
  [box("run ok"), "ran"],
1212
1274
  [box("run bad"), "failed"],
1213
1275
  [box("run timeout"), "timed out"],
1214
- [box("run warn"), "over budget or slow"],
1276
+ [box("run warn"), "over budget, under floor or slow"],
1215
1277
  [box("run running"), "running"],
1216
1278
  [box("missed"), "missed"]
1217
1279
  ];
@@ -1288,7 +1350,8 @@ function weekRunsLimit(job, now) {
1288
1350
  var h = escapeHtml;
1289
1351
  var CSS = `
1290
1352
  :root{color-scheme:light dark;--paper:#f4f4f5;--sheet:#fff;--sunk:#fafafa;--rule:#e4e4e7;--rule-2:#d4d4d8;--tick:#909098;--ink:#000;--body:#18181b;--muted:#71717a;--ok:#15803d;--warn:#a16207;--bad:#b91c1c;--serif:"Newsreader",ui-serif,Georgia,Cambria,"Times New Roman",serif;--mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--who:200px}
1291
- @media(prefers-color-scheme:dark){:root{--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
1353
+ @media(prefers-color-scheme:dark){:root:not([data-theme=light]){--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}}
1354
+ :root[data-theme=light]{color-scheme:light}:root[data-theme=dark]{color-scheme:dark;--paper:#09090b;--sheet:#111113;--sunk:#18181b;--rule:#27272a;--rule-2:#3f3f46;--tick:#66666f;--ink:#fff;--body:#e4e4e7;--muted:#a1a1aa;--ok:#4ade80;--warn:#fbbf24;--bad:#f87171}
1292
1355
  *{box-sizing:border-box}html{-webkit-text-size-adjust:100%}
1293
1356
  body{margin:0;background:var(--paper);color:var(--ink);font:400 16px/1.55 var(--serif);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
1294
1357
  a{color:inherit;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:.16em;text-decoration-color:var(--rule-2)}a:hover{text-decoration-color:currentColor}
@@ -1628,6 +1691,7 @@ function jobPage(job, runs, now, base, complete = true) {
1628
1691
  <dt>Timeout</dt><dd>${h(d.timeout ?? "1h")}</dd>
1629
1692
  ${d.maxDuration ? `<dt>Max duration</dt><dd>${h(d.maxDuration)}</dd>` : ""}
1630
1693
  ${d.budget ? `<dt>Budget</dt><dd>${h(Object.entries(d.budget).map(([k, v]) => `${k} \u2264 ${v}`).join(", "))}</dd>` : ""}
1694
+ ${d.floor ? `<dt>Floor</dt><dd>${h(Object.entries(d.floor).map(([k, v]) => `${k} \u2265 ${v}`).join(", "))}</dd>` : ""}
1631
1695
  ${d.expect ? `<dt>Expect</dt><dd>${h(d.expect)}</dd>` : ""}
1632
1696
  ${d.failuresBeforeAlert && d.failuresBeforeAlert > 1 ? `<dt>Alert after</dt><dd>${h(d.failuresBeforeAlert)} consecutive failures</dd>` : ""}
1633
1697
  ${d.tags?.length ? `<dt>Tags</dt><dd>${d.tags.map((t) => h(t)).join(", ")}</dd>` : ""}
@@ -1645,7 +1709,7 @@ function messagePage(title, message, base, signIn = false) {
1645
1709
  }
1646
1710
 
1647
1711
  // src/version.ts
1648
- var VERSION = "0.11.0";
1712
+ var VERSION = "0.12.0";
1649
1713
  var API_VERSION = 1;
1650
1714
 
1651
1715
  // src/routes/index.ts
@@ -2139,6 +2203,18 @@ function validateDefinition(def) {
2139
2203
  }
2140
2204
  }
2141
2205
  }
2206
+ if (def.floor !== void 0) {
2207
+ if (typeof def.floor !== "object" || def.floor === null) throw new Error(`job "${name}": floor must be an object of { metric: floor }`);
2208
+ for (const [metric, floor] of Object.entries(def.floor)) {
2209
+ if (typeof floor !== "number" || !Number.isFinite(floor)) {
2210
+ throw new Error(`job "${name}": floor.${metric} must be a finite number (got ${String(floor)})`);
2211
+ }
2212
+ const ceiling = def.budget?.[metric];
2213
+ if (ceiling !== void 0 && floor > ceiling) {
2214
+ throw new Error(`job "${name}": floor.${metric} (${floor}) is above budget.${metric} (${ceiling}), so every run would alert`);
2215
+ }
2216
+ }
2217
+ }
2142
2218
  if (def.expect !== void 0 && typeof def.expect !== "string" && !(def.expect instanceof RegExp) && typeof def.expect !== "function") {
2143
2219
  throw new Error(`job "${name}": expect must be a string, a RegExp or a function`);
2144
2220
  }