@canonry/canonry 7.13.0 → 7.14.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 (33) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +14 -7
  2. package/assets/assets/AuditHistoryPanel-gAkqWz9z.js +1 -0
  3. package/assets/assets/{BacklinksPage-BQxB_eFt.js → BacklinksPage-C67DtHo8.js} +1 -1
  4. package/assets/assets/{HistoryPage-DgGUM2tr.js → HistoryPage-BQuW6Z6m.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-Cl91vUu5.js → MeasurementPropertyPage-B1XS6DR0.js} +1 -1
  6. package/assets/assets/ProjectPage-CV_7bOA7.js +8 -0
  7. package/assets/assets/{RunsPage-BtjVuurK.js → RunsPage-CJYihZYC.js} +1 -1
  8. package/assets/assets/SettingsPage-Dd3J2DQE.js +1 -0
  9. package/assets/assets/{SiteHealthSection-DMHWsfM4.js → SiteHealthSection-iktyuzt2.js} +3 -3
  10. package/assets/assets/{TrafficPage-Bf0NkbIR.js → TrafficPage-CWjK7F3j.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-BtwOdTAO.js → TrafficSourceDetailPage-D4_AXQNE.js} +1 -1
  12. package/assets/assets/{extract-error-message-Cyc59Xi7.js → extract-error-message-Cxefvthz.js} +1 -1
  13. package/assets/assets/{index-DR_kNdE4.js → index-BbmiGIM_.js} +27 -27
  14. package/assets/assets/{index-D4QB1Igv.css → index-aBS1f2Bj.css} +1 -1
  15. package/assets/assets/{react-sigma_core.esm.min-CFB4bYg_.js → react-sigma_core.esm.min-DDEPa7AV.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-CLF10Pij.js → v2-overview-adapter-CLxlMdb2.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-BTHEFJCI.js → chunk-4LQ4HKVK.js} +5 -5
  19. package/dist/{chunk-AU7OUDWR.js → chunk-A25FO3Z5.js} +64 -13
  20. package/dist/{chunk-GQMSJEMZ.js → chunk-EZRBFH77.js} +233 -54
  21. package/dist/{chunk-GBH7N3VZ.js → chunk-KXHPQMBP.js} +93 -31
  22. package/dist/{chunk-EPSHIH63.js → chunk-LUQ5G5OO.js} +3 -3
  23. package/dist/{chunk-7I64KEGM.js → chunk-WHVEDCHY.js} +2 -2
  24. package/dist/{chunk-SM6ZZAPK.js → chunk-ZHXOR2LJ.js} +2 -2
  25. package/dist/cli.js +1300 -1244
  26. package/dist/{demo-server-WJUA4EIG.js → demo-server-SIATTXXS.js} +4 -4
  27. package/dist/index.js +6 -6
  28. package/dist/{intelligence-service-DJRH4Z3D.js → intelligence-service-UEEVETII.js} +2 -2
  29. package/dist/mcp.js +4 -4
  30. package/package.json +11 -11
  31. package/assets/assets/AuditHistoryPanel-BYxGn9mS.js +0 -1
  32. package/assets/assets/ProjectPage-B_S900QN.js +0 -8
  33. package/assets/assets/SettingsPage-p0x0ZwGY.js +0 -1
@@ -685,6 +685,7 @@ import {
685
685
  resultsExportDtoSchema,
686
686
  retryAfterDelayMs,
687
687
  roundRatio,
688
+ runAdmissionDtoSchema,
688
689
  runCompletenessDtoSchema,
689
690
  runDetailDtoSchema,
690
691
  runDtoSchema,
@@ -820,7 +821,7 @@ import {
820
821
  wordpressSchemaDeployResultDtoSchema,
821
822
  wordpressSchemaStatusResultDtoSchema,
822
823
  wordpressStatusDtoSchema
823
- } from "./chunk-AU7OUDWR.js";
824
+ } from "./chunk-A25FO3Z5.js";
824
825
 
825
826
  // src/intelligence-service.ts
826
827
  import { eq as eq87, desc as desc39, asc as asc17, and as and73, ne as ne16, or as or21, inArray as inArray35, gte as gte23, lte as lte20, isNull as isNull16, sql as sql32, exists as exists2 } from "drizzle-orm";
@@ -1274,6 +1275,12 @@ var runs = sqliteTable("runs", {
1274
1275
  * lose them; folded into `error` and cleared when the run finalizes.
1275
1276
  */
1276
1277
  pendingProviderErrors: text("pending_provider_errors", { mode: "json" }).$type(),
1278
+ /**
1279
+ * Providers this run does not call because each keeps failing on its
1280
+ * account, with the failure streak that decided it, frozen at queue time
1281
+ * (`providerAccountAdmission`). Null when it skips none.
1282
+ */
1283
+ skippedProviders: text("skipped_providers", { mode: "json" }).$type(),
1277
1284
  createdAt: text("created_at").notNull()
1278
1285
  }, (table) => [
1279
1286
  index("idx_runs_project").on(table.projectId),
@@ -8322,6 +8329,16 @@ var MIGRATION_VERSIONS = [
8322
8329
  `ALTER TABLE dashboard_sessions ADD COLUMN api_key_hash TEXT NOT NULL DEFAULT ''`,
8323
8330
  `ALTER TABLE dashboard_sessions ADD COLUMN password_fingerprint TEXT`
8324
8331
  ]
8332
+ },
8333
+ {
8334
+ // Providers a run does not call because each keeps failing on its
8335
+ // account, frozen at queue time. Nullable with no default: every existing
8336
+ // run, and every run an older writer queues, skips none.
8337
+ version: 175,
8338
+ name: "runs-skipped-providers",
8339
+ statements: [
8340
+ `ALTER TABLE runs ADD COLUMN skipped_providers TEXT`
8341
+ ]
8325
8342
  }
8326
8343
  ];
8327
8344
  var AERO_LEDGER_REBUILDS = [
@@ -17405,7 +17422,11 @@ function sliceFor(plan, params) {
17405
17422
  }
17406
17423
  function assertMeasurementRunStampable(db, params) {
17407
17424
  const stamp = measurementStamp(db, params);
17408
- resolveQueueDispatch(db, params, stamp);
17425
+ const dispatch = resolveQueueDispatch(db, params, stamp);
17426
+ if (params.dispatchMode === ProviderDispatchModes.batch) {
17427
+ const admission = queueProviderAdmission(db, params, stamp, params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString());
17428
+ dispatchAfterProviderAdmission(params, dispatch, admission);
17429
+ }
17409
17430
  }
17410
17431
  function resolveQueueDispatch(tx, params, stamp) {
17411
17432
  const trigger = stamp?.scope ? RunTriggers.probe : params.trigger ?? RunTriggers.manual;
@@ -17427,6 +17448,30 @@ function resolveQueueDispatch(tx, params, stamp) {
17427
17448
  }
17428
17449
  return resolution;
17429
17450
  }
17451
+ function dispatchAfterProviderAdmission(params, dispatch, admission) {
17452
+ if (admission.refused) return dispatch;
17453
+ const skippedProviders = admission.skipped;
17454
+ const modes = Object.fromEntries(Object.entries(dispatch.modes).filter(([provider]) => !(provider in skippedProviders)));
17455
+ if (params.dispatchMode === ProviderDispatchModes.batch && Object.keys(modes).length === 0) {
17456
+ throw validationError(
17457
+ `${batchDispatchRefusalMessage(dispatch.ineligible)} Held back after repeated account failures: ${Object.keys(skippedProviders).join(", ")}. Pass force to call those providers now.`,
17458
+ { ineligible: dispatch.ineligible, skippedProviders }
17459
+ );
17460
+ }
17461
+ return { ...dispatch, modes };
17462
+ }
17463
+ function queueProviderAdmission(db, params, stamp, now) {
17464
+ return (params.kind ?? RunKinds["answer-visibility"]) === RunKinds["answer-visibility"] ? providerAccountAdmission(db, {
17465
+ projectId: params.projectId,
17466
+ trigger: stamp?.scope ? RunTriggers.probe : params.trigger ?? RunTriggers.manual,
17467
+ force: params.force ?? false,
17468
+ now,
17469
+ providers: () => providersARunWouldCall(
17470
+ stamp?.identity.providers ?? providerRoster(db, params),
17471
+ params.runnableProviders
17472
+ )
17473
+ }) : { skipped: {} };
17474
+ }
17430
17475
  function providersARunWouldCall(roster, runnable) {
17431
17476
  const listed = normalizeProviders(roster);
17432
17477
  if (runnable == null) return listed;
@@ -17447,63 +17492,124 @@ function providerSettingsChangeCouldFix(action, diff) {
17447
17492
  return false;
17448
17493
  }
17449
17494
  }
17450
- function providerAccountRefusal(db, params) {
17451
- if (params.force || params.trigger === RunTriggers.probe) return null;
17495
+ function heldProviderAccounts(db, params) {
17496
+ const held = /* @__PURE__ */ new Map();
17452
17497
  const recent = db.select({
17453
17498
  id: runs.id,
17454
17499
  status: runs.status,
17455
17500
  error: runs.error,
17456
17501
  createdAt: runs.createdAt,
17457
- finishedAt: runs.finishedAt
17502
+ finishedAt: runs.finishedAt,
17503
+ skippedProviders: runs.skippedProviders
17458
17504
  }).from(runs).where(and11(
17459
17505
  eq17(runs.projectId, params.projectId),
17460
17506
  eq17(runs.kind, RunKinds["answer-visibility"]),
17461
17507
  inArray6(runs.status, [RunStatuses.completed, RunStatuses.partial, RunStatuses.failed, RunStatuses.cancelled])
17462
17508
  )).orderBy(desc7(runs.createdAt), desc7(runs.id)).limit(PROVIDER_ACCOUNT_LOOKBACK).all().map((run) => ({ ...run, providers: parseRunError(run.error)?.providers ?? {} }));
17463
- const withAccountFailure = recent.filter((run) => Object.values(run.providers).some((entry) => isProviderAccountFailure(entry.code)));
17464
- if (withAccountFailure.length < PROVIDER_ACCOUNT_FAILURE_STREAK) return null;
17509
+ const entries = recent.flatMap((run) => Object.values(run.providers));
17510
+ const withAccountFailure = recent.filter((run) => Object.values(run.providers).some((entry) => !entry.skipped && isProviderAccountFailure(entry.code)));
17511
+ if (withAccountFailure.length < PROVIDER_ACCOUNT_FAILURE_STREAK && !entries.some((entry) => entry.skipped)) return held;
17465
17512
  const calling = normalizeProviders(params.providers());
17466
- if (calling.length === 0) return null;
17513
+ if (calling.length === 0) return held;
17467
17514
  const answered2 = (runId, provider) => db.select({ id: querySnapshots.id }).from(querySnapshots).where(and11(eq17(querySnapshots.runId, runId), eq17(querySnapshots.provider, provider))).limit(1).get() !== void 0;
17468
- const streaks = /* @__PURE__ */ new Map();
17515
+ const finished = (run) => Date.parse(run.finishedAt ?? run.createdAt);
17516
+ const now = Date.parse(params.now);
17469
17517
  for (const provider of calling) {
17470
17518
  let count4 = 0;
17471
- let newest2 = null;
17519
+ let newest = null;
17472
17520
  let oldest = null;
17473
17521
  let code = null;
17522
+ let vouched = null;
17523
+ let ended = false;
17474
17524
  for (const run of recent) {
17475
17525
  const entry = run.providers[provider];
17476
17526
  if (!entry) {
17477
- if (run.status !== RunStatuses.failed && answered2(run.id, provider)) return null;
17527
+ if (run.status !== RunStatuses.failed && answered2(run.id, provider)) {
17528
+ ended = true;
17529
+ break;
17530
+ }
17531
+ continue;
17532
+ }
17533
+ if (entry.skipped) {
17534
+ vouched = run.skippedProviders?.[provider] ?? null;
17535
+ if (vouched) break;
17478
17536
  continue;
17479
17537
  }
17480
- if (!isProviderAccountFailure(entry.code)) return null;
17481
- newest2 ??= run;
17538
+ if (!isProviderAccountFailure(entry.code)) {
17539
+ ended = true;
17540
+ break;
17541
+ }
17542
+ newest ??= run;
17482
17543
  code ??= entry.code;
17483
17544
  oldest = run;
17484
17545
  count4 += 1;
17485
17546
  if (count4 === PROVIDER_ACCOUNT_FAILURE_STREAK) break;
17486
17547
  }
17487
- if (count4 < PROVIDER_ACCOUNT_FAILURE_STREAK) return null;
17488
- streaks.set(provider, { newest: newest2, oldest, code });
17548
+ if (ended) continue;
17549
+ if (count4 < PROVIDER_ACCOUNT_FAILURE_STREAK && !vouched) continue;
17550
+ const retryAfterMs = newest ? finished(newest) + PROVIDER_ACCOUNT_RETRY_HOURS * 36e5 : Date.parse(vouched.retryAfter);
17551
+ if (!(now < retryAfterMs)) continue;
17552
+ held.set(provider, {
17553
+ code: code ?? vouched.code,
17554
+ consecutiveRuns: PROVIDER_ACCOUNT_FAILURE_STREAK,
17555
+ // A vouched streak began before the runs read here.
17556
+ since: vouched ? vouched.since : oldest.createdAt,
17557
+ latestRunId: newest?.id ?? vouched.latestRunId,
17558
+ retryAfter: new Date(retryAfterMs).toISOString()
17559
+ });
17489
17560
  }
17490
- const finished = (run) => Date.parse(run.finishedAt ?? run.createdAt);
17491
- const newest = [...streaks.values()].map((streak) => streak.newest).sort((a, b) => finished(b) - finished(a))[0];
17492
- const retryAfterMs = finished(newest) + PROVIDER_ACCOUNT_RETRY_HOURS * 36e5;
17493
- if (Date.parse(params.now) >= retryAfterMs) return null;
17494
- const since = [...streaks.values()].map((streak) => streak.oldest.createdAt).sort()[0];
17495
- const settingsChanges = db.select({ entityId: auditLog.entityId, action: auditLog.action, diff: auditLog.diff, createdAt: auditLog.createdAt }).from(auditLog).where(and11(eq17(auditLog.entityType, "provider"), inArray6(auditLog.entityId, calling), gt3(auditLog.createdAt, since))).all();
17496
- const fixedSince = settingsChanges.some((change) => {
17497
- const streak = streaks.get(change.entityId ?? "");
17498
- return streak !== void 0 && change.createdAt > streak.oldest.createdAt && providerSettingsChangeCouldFix(change.action, change.diff);
17499
- });
17500
- if (fixedSince) return null;
17561
+ if (held.size === 0) return held;
17562
+ const since = [...held.values()].map((streak) => streak.since).sort()[0];
17563
+ const settingsChanges = db.select({ entityId: auditLog.entityId, action: auditLog.action, diff: auditLog.diff, createdAt: auditLog.createdAt }).from(auditLog).where(and11(eq17(auditLog.entityType, "provider"), inArray6(auditLog.entityId, [...held.keys()]), gt3(auditLog.createdAt, since))).all();
17564
+ for (const change of settingsChanges) {
17565
+ const streak = held.get(change.entityId ?? "");
17566
+ if (streak && change.createdAt > streak.since && providerSettingsChangeCouldFix(change.action, change.diff)) {
17567
+ held.delete(change.entityId);
17568
+ }
17569
+ }
17570
+ return held;
17571
+ }
17572
+ function providerAccountFailures(held) {
17573
+ const streaks = [...held.values()];
17574
+ const newest = [...streaks].sort((a, b) => b.retryAfter.localeCompare(a.retryAfter))[0];
17501
17575
  return {
17502
17576
  consecutiveRuns: PROVIDER_ACCOUNT_FAILURE_STREAK,
17503
- latestRunId: newest.id,
17504
- since,
17505
- retryAfter: new Date(retryAfterMs).toISOString(),
17506
- providers: Object.fromEntries(calling.map((provider) => [provider, streaks.get(provider).code]))
17577
+ latestRunId: newest.latestRunId,
17578
+ since: streaks.map((streak) => streak.since).sort()[0],
17579
+ // The first provider called again lets a run through.
17580
+ retryAfter: streaks.map((streak) => streak.retryAfter).sort()[0],
17581
+ providers: Object.fromEntries([...held].map(([provider, streak]) => [provider, streak.code]))
17582
+ };
17583
+ }
17584
+ function providerAccountAdmission(db, params) {
17585
+ if (params.force || params.trigger === RunTriggers.probe) return { skipped: {} };
17586
+ let calling = null;
17587
+ const providers = () => calling ??= normalizeProviders(params.providers());
17588
+ const held = heldProviderAccounts(db, { projectId: params.projectId, now: params.now, providers });
17589
+ if (held.size === 0) return { skipped: {} };
17590
+ if (providers().every((provider) => held.has(provider))) return { refused: providerAccountFailures(held) };
17591
+ return { skipped: Object.fromEntries(held) };
17592
+ }
17593
+ function sweepProviders(db, projectId, runnable, selection) {
17594
+ const planProviders = activeRevisionProviders(db, projectId);
17595
+ if (planProviders.length > 0) return providersARunWouldCall(planProviders, runnable);
17596
+ const schedule = db.select({ providers: schedules.providers, enabled: schedules.enabled }).from(schedules).where(and11(eq17(schedules.projectId, projectId), eq17(schedules.kind, RunKinds["answer-visibility"]))).get();
17597
+ const project = db.select({ providers: projects.providers }).from(projects).where(eq17(projects.id, projectId)).get();
17598
+ return providersARunWouldCall(resolveRunProviderSelection({
17599
+ requestedProviders: selection === RunTriggers.scheduled && schedule?.enabled ? schedule.providers : null,
17600
+ projectProviders: project?.providers ?? [],
17601
+ runnableProviders: runnable
17602
+ }), runnable);
17603
+ }
17604
+ function runAdmissionState(db, params) {
17605
+ let calling = null;
17606
+ const providers = () => calling ??= sweepProviders(db, params.projectId, params.runnableProviders, params.selection ?? RunTriggers.scheduled);
17607
+ const held = heldProviderAccounts(db, { projectId: params.projectId, now: params.now, providers });
17608
+ const refused = held.size > 0 && providers().every((provider) => held.has(provider));
17609
+ return {
17610
+ refused,
17611
+ retryAfter: refused ? providerAccountFailures(held).retryAfter : null,
17612
+ providers: Object.fromEntries(held)
17507
17613
  };
17508
17614
  }
17509
17615
  function providersFailingError(projectName, failures) {
@@ -17513,6 +17619,20 @@ function providersFailingError(projectName, failures) {
17513
17619
  { projectName, ...failures }
17514
17620
  );
17515
17621
  }
17622
+ var RUN_REFUSED_AUDIT_ACTION = "run.refused";
17623
+ function recordRunRefusal(db, projectId, failures, audit) {
17624
+ const last = db.select({ diff: auditLog.diff }).from(auditLog).where(and11(eq17(auditLog.projectId, projectId), eq17(auditLog.action, RUN_REFUSED_AUDIT_ACTION))).orderBy(desc7(auditLog.createdAt), desc7(auditLog.id)).limit(1).get();
17625
+ if (last && parseJsonColumn(last.diff, {}).latestRunId === failures.latestRunId) return false;
17626
+ writeAuditLog(db, {
17627
+ projectId,
17628
+ actor: audit.actor,
17629
+ action: RUN_REFUSED_AUDIT_ACTION,
17630
+ entityType: audit.entityType,
17631
+ entityId: audit.entityId,
17632
+ diff: { code: "PROVIDERS_FAILING", ...failures }
17633
+ });
17634
+ return true;
17635
+ }
17516
17636
  function queueRunIfProjectIdle(db, params) {
17517
17637
  const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
17518
17638
  const kind = params.kind ?? "answer-visibility";
@@ -17553,17 +17673,14 @@ function queueRunIfProjectIdle(db, params) {
17553
17673
  }
17554
17674
  const current = activeRun();
17555
17675
  if (current) return { conflict: true, activeRunId: current.id };
17556
- const refused = kind === RunKinds["answer-visibility"] ? providerAccountRefusal(tx, {
17557
- projectId: params.projectId,
17558
- trigger: stamp?.scope ? RunTriggers.probe : trigger,
17559
- force: params.force ?? false,
17560
- now: createdAt,
17561
- providers: () => providersARunWouldCall(
17562
- stamp?.identity.providers ?? providerRoster(tx, params),
17563
- params.runnableProviders
17564
- )
17565
- }) : null;
17566
- if (refused) return { conflict: false, refused };
17676
+ const admission = queueProviderAdmission(tx, params, stamp, createdAt);
17677
+ if (admission.refused) {
17678
+ const refusalRecorded = params.auditRefusal ? recordRunRefusal(tx, params.projectId, admission.refused, params.auditRefusal) : false;
17679
+ return { conflict: false, refused: admission.refused, refusalRecorded };
17680
+ }
17681
+ const skippedProviders = admission.skipped;
17682
+ const admittedDispatch = dispatchAfterProviderAdmission(params, dispatch, admission);
17683
+ const dispatchModes = admittedDispatch.modes;
17567
17684
  const scoped = params.queries != null || stamp?.scope != null;
17568
17685
  const basket = scoped ? null : ensureCurrentQueryBasketRevision(tx, params.projectId, createdAt);
17569
17686
  tx.insert(runs).values({
@@ -17587,10 +17704,11 @@ function queueRunIfProjectIdle(db, params) {
17587
17704
  measurementManifest: stamp?.manifest ?? null,
17588
17705
  measurementScope: stamp?.scope ?? null,
17589
17706
  measurementExecutionIdentity: stamp?.identity ?? null,
17590
- providerDispatchModes: Object.keys(dispatch.modes).length > 0 ? dispatch.modes : null,
17707
+ providerDispatchModes: Object.keys(dispatchModes).length > 0 ? dispatchModes : null,
17708
+ skippedProviders: Object.keys(skippedProviders).length > 0 ? skippedProviders : null,
17591
17709
  createdAt
17592
17710
  }).run();
17593
- return { conflict: false, runId, dispatch };
17711
+ return { conflict: false, runId, dispatch: admittedDispatch, skippedProviders };
17594
17712
  });
17595
17713
  }
17596
17714
 
@@ -21005,7 +21123,7 @@ async function runRoutes(app, opts) {
21005
21123
  if (activeRun) {
21006
21124
  return { conflict: true, activeRunId: activeRun.id };
21007
21125
  }
21008
- const refused = providerAccountRefusal(tx, {
21126
+ const admission = providerAccountAdmission(tx, {
21009
21127
  projectId: project.id,
21010
21128
  trigger,
21011
21129
  force: body.force ?? false,
@@ -21020,7 +21138,8 @@ async function runRoutes(app, opts) {
21020
21138
  return providersARunWouldCall(roster, runnable);
21021
21139
  }
21022
21140
  });
21023
- if (refused) return { conflict: false, refused };
21141
+ if (admission.refused) return { conflict: false, refused: admission.refused };
21142
+ const skippedProviders = Object.keys(admission.skipped).length > 0 ? admission.skipped : null;
21024
21143
  const inserted = [];
21025
21144
  for (const loc of projectLocations) {
21026
21145
  const runId2 = crypto16.randomUUID();
@@ -21032,6 +21151,7 @@ async function runRoutes(app, opts) {
21032
21151
  trigger,
21033
21152
  location: loc.label,
21034
21153
  queries: queriesColumn,
21154
+ skippedProviders,
21035
21155
  createdAt: now
21036
21156
  }).run();
21037
21157
  inserted.push({ runId: runId2, loc });
@@ -21110,14 +21230,36 @@ async function runRoutes(app, opts) {
21110
21230
  const countRow = app.db.select({ count: sql11`count(*)` }).from(runs).where(and21(eq28(runs.projectId, project.id), notProbeRun())).get();
21111
21231
  const totalRuns = countRow?.count ?? 0;
21112
21232
  const latestRun = app.db.select().from(runs).where(and21(eq28(runs.projectId, project.id), notProbeRun())).orderBy(desc11(runs.createdAt), desc11(runs.id)).limit(1).get();
21233
+ const admissionParams = {
21234
+ projectId: project.id,
21235
+ now: (/* @__PURE__ */ new Date()).toISOString(),
21236
+ runnableProviders: opts.getRunnableProviderNames?.()
21237
+ };
21238
+ const admission = runAdmissionState(app.db, admissionParams);
21239
+ const manualAdmission = runAdmissionState(app.db, { ...admissionParams, selection: RunTriggers.manual });
21113
21240
  if (!latestRun) {
21114
- return reply.send({ totalRuns: 0, run: null });
21241
+ return reply.send({ totalRuns: 0, run: null, admission, manualAdmission });
21115
21242
  }
21116
21243
  return reply.send({
21117
21244
  totalRuns,
21118
- run: loadRunDetail(app, latestRun)
21245
+ run: loadRunDetail(app, latestRun),
21246
+ admission,
21247
+ manualAdmission
21119
21248
  });
21120
21249
  });
21250
+ app.get("/projects/:name/run-admission", async (request, reply) => {
21251
+ const selection = request.query.selection ?? RunTriggers.scheduled;
21252
+ if (selection !== RunTriggers.manual && selection !== RunTriggers.scheduled) {
21253
+ throw validationError('"selection" must be one of: manual, scheduled');
21254
+ }
21255
+ const project = resolveProject(app.db, request.params.name);
21256
+ return reply.send(runAdmissionState(app.db, {
21257
+ projectId: project.id,
21258
+ now: (/* @__PURE__ */ new Date()).toISOString(),
21259
+ runnableProviders: opts.getRunnableProviderNames?.(),
21260
+ selection
21261
+ }));
21262
+ });
21121
21263
  app.get("/runs", async (request, reply) => {
21122
21264
  const limit = parseListLimit(request.query.limit, 500, 5e3);
21123
21265
  const since = parseListSince(request.query.since);
@@ -21210,6 +21352,7 @@ async function runRoutes(app, opts) {
21210
21352
  for (const entry of eligible) {
21211
21353
  try {
21212
21354
  assertMeasurementRunStampable(app.db, {
21355
+ createdAt: now,
21213
21356
  projectId: entry.project.id,
21214
21357
  kind,
21215
21358
  trigger: "manual",
@@ -21218,7 +21361,8 @@ async function runRoutes(app, opts) {
21218
21361
  runnableProviders: runnableProviderNames,
21219
21362
  providerModels: opts.getEffectiveProviderModels?.(),
21220
21363
  dispatchMode,
21221
- batchEligibleProviders
21364
+ batchEligibleProviders,
21365
+ force: force ?? false
21222
21366
  });
21223
21367
  dispatchable.push(entry);
21224
21368
  } catch (error) {
@@ -21434,6 +21578,8 @@ function formatRun(row) {
21434
21578
  // Only plan runs can batch, so only they carry the frozen modes.
21435
21579
  dispatchModes: row.providerDispatchModes ?? {}
21436
21580
  } : {},
21581
+ // Only on a run that skipped a provider, so other runs read as before.
21582
+ ...row.skippedProviders && Object.keys(row.skippedProviders).length > 0 ? { skippedProviders: row.skippedProviders } : {},
21437
21583
  createdAt: row.createdAt
21438
21584
  };
21439
21585
  }
@@ -36404,7 +36550,14 @@ async function compositeRoutes(app, options) {
36404
36550
  const previousVisRunGroup = visRunGroups[1] ?? [];
36405
36551
  const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
36406
36552
  const latestRunRow = allRuns[0] ?? null;
36407
- const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
36553
+ const admissionParams = {
36554
+ projectId: project.id,
36555
+ now: (/* @__PURE__ */ new Date()).toISOString(),
36556
+ runnableProviders: options.getRunnableProviderNames?.()
36557
+ };
36558
+ const admission = runAdmissionState(app.db, admissionParams);
36559
+ const manualAdmission = runAdmissionState(app.db, { ...admissionParams, selection: RunTriggers.manual });
36560
+ const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow), admission, manualAdmission } : { totalRuns: 0, run: null, admission, manualAdmission };
36408
36561
  const healthRow = app.db.select().from(healthSnapshots).where(eq50(healthSnapshots.projectId, project.id)).orderBy(desc24(healthSnapshots.createdAt)).limit(1).get();
36409
36562
  const health = healthRow ? mapHealthRow2(healthRow) : null;
36410
36563
  const insightRows = app.db.select().from(insights).where(eq50(insights.projectId, project.id)).orderBy(desc24(insights.createdAt)).all();
@@ -36644,6 +36797,7 @@ function summarizeRun(run) {
36644
36797
  startedAt: run.startedAt,
36645
36798
  finishedAt: run.finishedAt,
36646
36799
  error: parseRunError(run.error),
36800
+ ...run.skippedProviders && Object.keys(run.skippedProviders).length > 0 ? { skippedProviders: run.skippedProviders } : {},
36647
36801
  createdAt: run.createdAt
36648
36802
  };
36649
36803
  }
@@ -38068,6 +38222,7 @@ var SCHEMA_TABLE = {
38068
38222
  IntegrationSettingsSummaryDto: integrationSettingsSummaryDtoSchema,
38069
38223
  KeywordDto: keywordDtoSchema,
38070
38224
  LatestProjectRunDto: latestProjectRunDtoSchema,
38225
+ RunAdmissionDto: runAdmissionDtoSchema,
38071
38226
  LocationContext: locationContextSchema,
38072
38227
  MeasurementDiscoveryRequest: measurementDiscoveryRequestSchema,
38073
38228
  MeasurementDiscoveryResponse: measurementDiscoveryResponseSchema,
@@ -40544,7 +40699,7 @@ var routeCatalog = [
40544
40699
  dispatchMode: dispatchModeRequestSchema,
40545
40700
  force: {
40546
40701
  type: "boolean",
40547
- description: "Queue the run even when it would be refused with PROVIDERS_FAILING. Admission only; never stored."
40702
+ description: "Call every provider: queue the run even when it would be refused with PROVIDERS_FAILING, and skip no provider that keeps failing on its account. Admission only; never stored."
40548
40703
  }
40549
40704
  }
40550
40705
  }
@@ -40552,12 +40707,15 @@ var routeCatalog = [
40552
40707
  }
40553
40708
  },
40554
40709
  responses: {
40555
- 201: jsonResponse("Run queued.", "RunDto"),
40710
+ 201: jsonResponse(
40711
+ `Run queued. \`skippedProviders\` names each provider it will not call because that provider failed on its account in each of its last ${PROVIDER_ACCOUNT_FAILURE_STREAK} runs (until its \`retryAfter\`); the run calls the rest and ends partial.`,
40712
+ "RunDto"
40713
+ ),
40556
40714
  400: errorResponse(
40557
40715
  'Invalid request: an untracked query, a measurement scope naming a group/target/question the published plan does not contain, a scope combined with a query list, a per-run location on a plan project, a provider roster the plan was not published for, or `dispatchMode: "batch"` when no provider of the run can batch (`details.ineligible` names each provider\'s reason).'
40558
40716
  ),
40559
40717
  422: errorResponse(
40560
- `NO_QUERIES: the project has no tracked queries. PROVIDERS_FAILING: every provider the run would call failed on its account (rejected key, denied access, no credit) in each of its last ${PROVIDER_ACCOUNT_FAILURE_STREAK} runs. \`details.providers\` names each provider's code and \`details.retryAfter\` when one run is let through again (${PROVIDER_ACCOUNT_RETRY_HOURS}h after the newest failure). Saving a new key, model or endpoint in a provider's settings lets the next run through at once, a probe is never refused, and \`force: true\` overrides.`
40718
+ `NO_QUERIES: the project has no tracked queries. PROVIDERS_FAILING: every provider the run would call failed on its account (rejected key, denied access, no credit) in each of its last ${PROVIDER_ACCOUNT_FAILURE_STREAK} runs. \`details.providers\` names each provider's code and \`details.retryAfter\` when one run is let through again (${PROVIDER_ACCOUNT_RETRY_HOURS}h after a provider's newest failure; the run then calls only the providers due a retry). Saving a new key, model or endpoint in a provider's settings lets the next run through at once, a probe is never refused, and \`force: true\` overrides. When only some providers keep failing, the run is queued without them instead.`
40561
40719
  ),
40562
40720
  409: errorResponse("Run already in progress."),
40563
40721
  503: errorResponse("No runnable answer provider is configured.")
@@ -40577,12 +40735,33 @@ var routeCatalog = [
40577
40735
  method: "get",
40578
40736
  path: "/api/v1/projects/{name}/runs/latest",
40579
40737
  summary: "Get the latest project run",
40738
+ description: "The newest non-probe run, the run count, and `admission`: whether the next full sweep would be refused (PROVIDERS_FAILING) and which providers it would skip because each keeps failing on its account, with their codes and `retryAfter`. `admission` uses the scheduled roster; `manualAdmission` describes an unfiltered manual launch. A scheduled sweep that is refused leaves no run, so read `admission`, not the latest run, to tell.",
40580
40739
  tags: ["runs"],
40581
40740
  parameters: [nameParameter],
40582
40741
  responses: {
40583
40742
  200: jsonResponse("Latest run returned.", "LatestProjectRunDto")
40584
40743
  }
40585
40744
  },
40745
+ {
40746
+ method: "get",
40747
+ path: "/api/v1/projects/{name}/run-admission",
40748
+ summary: "Get whether the next sweep would be admitted",
40749
+ description: `The \`admission\` or \`manualAdmission\` of \`/projects/{name}/runs/latest\` without the latest run: whether the next full sweep would be refused (PROVIDERS_FAILING) and which providers it would skip because each failed on its account in each of its last ${PROVIDER_ACCOUNT_FAILURE_STREAK} runs, with each provider's code and \`retryAfter\`.`,
40750
+ tags: ["runs"],
40751
+ parameters: [
40752
+ nameParameter,
40753
+ {
40754
+ name: "selection",
40755
+ in: "query",
40756
+ description: "Read-selection identity: scheduled (default) uses the enabled schedule's providers; manual uses the unfiltered manual launch's project or instance providers. Both use an Advanced revision's frozen engines.",
40757
+ schema: { type: "string", enum: ["manual", "scheduled"], default: "scheduled" }
40758
+ }
40759
+ ],
40760
+ responses: {
40761
+ 200: jsonResponse("Run admission returned.", "RunAdmissionDto"),
40762
+ 400: errorResponse("Invalid admission selection.")
40763
+ }
40764
+ },
40586
40765
  {
40587
40766
  method: "get",
40588
40767
  path: "/api/v1/runs",
@@ -40617,7 +40796,7 @@ var routeCatalog = [
40617
40796
  dispatchMode: dispatchModeRequestSchema,
40618
40797
  force: {
40619
40798
  type: "boolean",
40620
- description: "Queue projects that would be refused with PROVIDERS_FAILING. Admission only; never stored."
40799
+ description: "Call every provider: queue projects that would be refused with PROVIDERS_FAILING, and skip no provider that keeps failing on its account. Admission only; never stored."
40621
40800
  }
40622
40801
  }
40623
40802
  }
@@ -89646,7 +89825,7 @@ async function apiRoutes(app, opts) {
89646
89825
  await api.register(citationRoutes);
89647
89826
  await api.register(visibilityStatsRoutes);
89648
89827
  await api.register(resultsExportRoutes);
89649
- await api.register(compositeRoutes, { sentiment: opts.sentiment });
89828
+ await api.register(compositeRoutes, { sentiment: opts.sentiment, getRunnableProviderNames: opts.getRunnableProviderNames });
89650
89829
  await api.register(contentRoutes, {
89651
89830
  explainContentRecommendation: opts.explainContentRecommendation,
89652
89831
  briefContentRecommendation: opts.briefContentRecommendation,