@canonry/canonry 7.13.0 → 7.15.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 (34) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +19 -9
  2. package/assets/assets/AuditHistoryPanel-Cl9tUvSZ.js +1 -0
  3. package/assets/assets/{BacklinksPage-BQxB_eFt.js → BacklinksPage-CTeTJ9dh.js} +1 -1
  4. package/assets/assets/{HistoryPage-DgGUM2tr.js → HistoryPage-DpOoXr2P.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-Cl91vUu5.js → MeasurementPropertyPage-DxXsgM25.js} +1 -1
  6. package/assets/assets/ProjectPage-By9arjLJ.js +8 -0
  7. package/assets/assets/{RunsPage-BtjVuurK.js → RunsPage-CfDi7I1z.js} +1 -1
  8. package/assets/assets/SettingsPage-BHDb1apK.js +1 -0
  9. package/assets/assets/{SiteHealthSection-DMHWsfM4.js → SiteHealthSection-DQZyy4P_.js} +3 -3
  10. package/assets/assets/{TrafficPage-Bf0NkbIR.js → TrafficPage-DX_vN2RL.js} +2 -2
  11. package/assets/assets/{TrafficSourceDetailPage-BtwOdTAO.js → TrafficSourceDetailPage-Cg92zrtZ.js} +1 -1
  12. package/assets/assets/{extract-error-message-Cyc59Xi7.js → extract-error-message-BKxVCbov.js} +1 -1
  13. package/assets/assets/index-DoQJETc6.js +86 -0
  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-BhqZcSgh.js} +1 -1
  16. package/assets/assets/{v2-overview-adapter-CLF10Pij.js → v2-overview-adapter-DZ5YsE_G.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-GQMSJEMZ.js → chunk-7HJYGDRW.js} +384 -57
  19. package/dist/{chunk-AU7OUDWR.js → chunk-KOBK4I5N.js} +5677 -5412
  20. package/dist/{chunk-BTHEFJCI.js → chunk-LC7GMHOL.js} +6 -6
  21. package/dist/{chunk-GBH7N3VZ.js → chunk-N2MRW2DH.js} +93 -31
  22. package/dist/{chunk-7I64KEGM.js → chunk-SG6T3DEP.js} +2 -2
  23. package/dist/{chunk-SM6ZZAPK.js → chunk-SGQBPTWQ.js} +2 -2
  24. package/dist/{chunk-EPSHIH63.js → chunk-YFCKHEKM.js} +3 -3
  25. package/dist/cli.js +1339 -1240
  26. package/dist/{demo-server-WJUA4EIG.js → demo-server-YWGUGZMI.js} +4 -4
  27. package/dist/index.js +6 -6
  28. package/dist/{intelligence-service-DJRH4Z3D.js → intelligence-service-7RKUUR7O.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
  34. package/assets/assets/index-DR_kNdE4.js +0 -86
@@ -30,6 +30,7 @@ import {
30
30
  AdsOperationStepStates,
31
31
  AdsReconcileStrategies,
32
32
  AdsReviewStatuses,
33
+ AiEngineLeadRateUnavailableReasons,
33
34
  AiReferralTrafficClasses,
34
35
  AppError,
35
36
  BacklinkSources,
@@ -223,7 +224,9 @@ import {
223
224
  agentProvidersResponseDtoSchema,
224
225
  aggregateHarvestedQueries,
225
226
  aggregateSentiment,
227
+ aiEngineForReferralSource,
226
228
  aiReferralClassCounts,
229
+ aiReferralEngineLabel,
227
230
  alreadyExists,
228
231
  answerProseForMentions,
229
232
  apiKeyDtoSchema,
@@ -465,6 +468,7 @@ import {
465
468
  internalError,
466
469
  intersectScopes,
467
470
  isBrowserProvider,
471
+ isGa4AiAssistantChannel,
468
472
  isLocationRedirectStatus,
469
473
  isNegativeReviewRating,
470
474
  isProviderAccountFailure,
@@ -685,6 +689,7 @@ import {
685
689
  resultsExportDtoSchema,
686
690
  retryAfterDelayMs,
687
691
  roundRatio,
692
+ runAdmissionDtoSchema,
688
693
  runCompletenessDtoSchema,
689
694
  runDetailDtoSchema,
690
695
  runDtoSchema,
@@ -820,7 +825,7 @@ import {
820
825
  wordpressSchemaDeployResultDtoSchema,
821
826
  wordpressSchemaStatusResultDtoSchema,
822
827
  wordpressStatusDtoSchema
823
- } from "./chunk-AU7OUDWR.js";
828
+ } from "./chunk-KOBK4I5N.js";
824
829
 
825
830
  // src/intelligence-service.ts
826
831
  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 +1279,12 @@ var runs = sqliteTable("runs", {
1274
1279
  * lose them; folded into `error` and cleared when the run finalizes.
1275
1280
  */
1276
1281
  pendingProviderErrors: text("pending_provider_errors", { mode: "json" }).$type(),
1282
+ /**
1283
+ * Providers this run does not call because each keeps failing on its
1284
+ * account, with the failure streak that decided it, frozen at queue time
1285
+ * (`providerAccountAdmission`). Null when it skips none.
1286
+ */
1287
+ skippedProviders: text("skipped_providers", { mode: "json" }).$type(),
1277
1288
  createdAt: text("created_at").notNull()
1278
1289
  }, (table) => [
1279
1290
  index("idx_runs_project").on(table.projectId),
@@ -8322,6 +8333,16 @@ var MIGRATION_VERSIONS = [
8322
8333
  `ALTER TABLE dashboard_sessions ADD COLUMN api_key_hash TEXT NOT NULL DEFAULT ''`,
8323
8334
  `ALTER TABLE dashboard_sessions ADD COLUMN password_fingerprint TEXT`
8324
8335
  ]
8336
+ },
8337
+ {
8338
+ // Providers a run does not call because each keeps failing on its
8339
+ // account, frozen at queue time. Nullable with no default: every existing
8340
+ // run, and every run an older writer queues, skips none.
8341
+ version: 175,
8342
+ name: "runs-skipped-providers",
8343
+ statements: [
8344
+ `ALTER TABLE runs ADD COLUMN skipped_providers TEXT`
8345
+ ]
8325
8346
  }
8326
8347
  ];
8327
8348
  var AERO_LEDGER_REBUILDS = [
@@ -17405,7 +17426,11 @@ function sliceFor(plan, params) {
17405
17426
  }
17406
17427
  function assertMeasurementRunStampable(db, params) {
17407
17428
  const stamp = measurementStamp(db, params);
17408
- resolveQueueDispatch(db, params, stamp);
17429
+ const dispatch = resolveQueueDispatch(db, params, stamp);
17430
+ if (params.dispatchMode === ProviderDispatchModes.batch) {
17431
+ const admission = queueProviderAdmission(db, params, stamp, params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString());
17432
+ dispatchAfterProviderAdmission(params, dispatch, admission);
17433
+ }
17409
17434
  }
17410
17435
  function resolveQueueDispatch(tx, params, stamp) {
17411
17436
  const trigger = stamp?.scope ? RunTriggers.probe : params.trigger ?? RunTriggers.manual;
@@ -17427,6 +17452,30 @@ function resolveQueueDispatch(tx, params, stamp) {
17427
17452
  }
17428
17453
  return resolution;
17429
17454
  }
17455
+ function dispatchAfterProviderAdmission(params, dispatch, admission) {
17456
+ if (admission.refused) return dispatch;
17457
+ const skippedProviders = admission.skipped;
17458
+ const modes = Object.fromEntries(Object.entries(dispatch.modes).filter(([provider]) => !(provider in skippedProviders)));
17459
+ if (params.dispatchMode === ProviderDispatchModes.batch && Object.keys(modes).length === 0) {
17460
+ throw validationError(
17461
+ `${batchDispatchRefusalMessage(dispatch.ineligible)} Held back after repeated account failures: ${Object.keys(skippedProviders).join(", ")}. Pass force to call those providers now.`,
17462
+ { ineligible: dispatch.ineligible, skippedProviders }
17463
+ );
17464
+ }
17465
+ return { ...dispatch, modes };
17466
+ }
17467
+ function queueProviderAdmission(db, params, stamp, now) {
17468
+ return (params.kind ?? RunKinds["answer-visibility"]) === RunKinds["answer-visibility"] ? providerAccountAdmission(db, {
17469
+ projectId: params.projectId,
17470
+ trigger: stamp?.scope ? RunTriggers.probe : params.trigger ?? RunTriggers.manual,
17471
+ force: params.force ?? false,
17472
+ now,
17473
+ providers: () => providersARunWouldCall(
17474
+ stamp?.identity.providers ?? providerRoster(db, params),
17475
+ params.runnableProviders
17476
+ )
17477
+ }) : { skipped: {} };
17478
+ }
17430
17479
  function providersARunWouldCall(roster, runnable) {
17431
17480
  const listed = normalizeProviders(roster);
17432
17481
  if (runnable == null) return listed;
@@ -17447,63 +17496,124 @@ function providerSettingsChangeCouldFix(action, diff) {
17447
17496
  return false;
17448
17497
  }
17449
17498
  }
17450
- function providerAccountRefusal(db, params) {
17451
- if (params.force || params.trigger === RunTriggers.probe) return null;
17499
+ function heldProviderAccounts(db, params) {
17500
+ const held = /* @__PURE__ */ new Map();
17452
17501
  const recent = db.select({
17453
17502
  id: runs.id,
17454
17503
  status: runs.status,
17455
17504
  error: runs.error,
17456
17505
  createdAt: runs.createdAt,
17457
- finishedAt: runs.finishedAt
17506
+ finishedAt: runs.finishedAt,
17507
+ skippedProviders: runs.skippedProviders
17458
17508
  }).from(runs).where(and11(
17459
17509
  eq17(runs.projectId, params.projectId),
17460
17510
  eq17(runs.kind, RunKinds["answer-visibility"]),
17461
17511
  inArray6(runs.status, [RunStatuses.completed, RunStatuses.partial, RunStatuses.failed, RunStatuses.cancelled])
17462
17512
  )).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;
17513
+ const entries = recent.flatMap((run) => Object.values(run.providers));
17514
+ const withAccountFailure = recent.filter((run) => Object.values(run.providers).some((entry) => !entry.skipped && isProviderAccountFailure(entry.code)));
17515
+ if (withAccountFailure.length < PROVIDER_ACCOUNT_FAILURE_STREAK && !entries.some((entry) => entry.skipped)) return held;
17465
17516
  const calling = normalizeProviders(params.providers());
17466
- if (calling.length === 0) return null;
17517
+ if (calling.length === 0) return held;
17467
17518
  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();
17519
+ const finished = (run) => Date.parse(run.finishedAt ?? run.createdAt);
17520
+ const now = Date.parse(params.now);
17469
17521
  for (const provider of calling) {
17470
17522
  let count4 = 0;
17471
- let newest2 = null;
17523
+ let newest = null;
17472
17524
  let oldest = null;
17473
17525
  let code = null;
17526
+ let vouched = null;
17527
+ let ended = false;
17474
17528
  for (const run of recent) {
17475
17529
  const entry = run.providers[provider];
17476
17530
  if (!entry) {
17477
- if (run.status !== RunStatuses.failed && answered2(run.id, provider)) return null;
17531
+ if (run.status !== RunStatuses.failed && answered2(run.id, provider)) {
17532
+ ended = true;
17533
+ break;
17534
+ }
17535
+ continue;
17536
+ }
17537
+ if (entry.skipped) {
17538
+ vouched = run.skippedProviders?.[provider] ?? null;
17539
+ if (vouched) break;
17478
17540
  continue;
17479
17541
  }
17480
- if (!isProviderAccountFailure(entry.code)) return null;
17481
- newest2 ??= run;
17542
+ if (!isProviderAccountFailure(entry.code)) {
17543
+ ended = true;
17544
+ break;
17545
+ }
17546
+ newest ??= run;
17482
17547
  code ??= entry.code;
17483
17548
  oldest = run;
17484
17549
  count4 += 1;
17485
17550
  if (count4 === PROVIDER_ACCOUNT_FAILURE_STREAK) break;
17486
17551
  }
17487
- if (count4 < PROVIDER_ACCOUNT_FAILURE_STREAK) return null;
17488
- streaks.set(provider, { newest: newest2, oldest, code });
17552
+ if (ended) continue;
17553
+ if (count4 < PROVIDER_ACCOUNT_FAILURE_STREAK && !vouched) continue;
17554
+ const retryAfterMs = newest ? finished(newest) + PROVIDER_ACCOUNT_RETRY_HOURS * 36e5 : Date.parse(vouched.retryAfter);
17555
+ if (!(now < retryAfterMs)) continue;
17556
+ held.set(provider, {
17557
+ code: code ?? vouched.code,
17558
+ consecutiveRuns: PROVIDER_ACCOUNT_FAILURE_STREAK,
17559
+ // A vouched streak began before the runs read here.
17560
+ since: vouched ? vouched.since : oldest.createdAt,
17561
+ latestRunId: newest?.id ?? vouched.latestRunId,
17562
+ retryAfter: new Date(retryAfterMs).toISOString()
17563
+ });
17564
+ }
17565
+ if (held.size === 0) return held;
17566
+ const since = [...held.values()].map((streak) => streak.since).sort()[0];
17567
+ 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();
17568
+ for (const change of settingsChanges) {
17569
+ const streak = held.get(change.entityId ?? "");
17570
+ if (streak && change.createdAt > streak.since && providerSettingsChangeCouldFix(change.action, change.diff)) {
17571
+ held.delete(change.entityId);
17572
+ }
17489
17573
  }
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;
17574
+ return held;
17575
+ }
17576
+ function providerAccountFailures(held) {
17577
+ const streaks = [...held.values()];
17578
+ const newest = [...streaks].sort((a, b) => b.retryAfter.localeCompare(a.retryAfter))[0];
17501
17579
  return {
17502
17580
  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]))
17581
+ latestRunId: newest.latestRunId,
17582
+ since: streaks.map((streak) => streak.since).sort()[0],
17583
+ // The first provider called again lets a run through.
17584
+ retryAfter: streaks.map((streak) => streak.retryAfter).sort()[0],
17585
+ providers: Object.fromEntries([...held].map(([provider, streak]) => [provider, streak.code]))
17586
+ };
17587
+ }
17588
+ function providerAccountAdmission(db, params) {
17589
+ if (params.force || params.trigger === RunTriggers.probe) return { skipped: {} };
17590
+ let calling = null;
17591
+ const providers = () => calling ??= normalizeProviders(params.providers());
17592
+ const held = heldProviderAccounts(db, { projectId: params.projectId, now: params.now, providers });
17593
+ if (held.size === 0) return { skipped: {} };
17594
+ if (providers().every((provider) => held.has(provider))) return { refused: providerAccountFailures(held) };
17595
+ return { skipped: Object.fromEntries(held) };
17596
+ }
17597
+ function sweepProviders(db, projectId, runnable, selection) {
17598
+ const planProviders = activeRevisionProviders(db, projectId);
17599
+ if (planProviders.length > 0) return providersARunWouldCall(planProviders, runnable);
17600
+ 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();
17601
+ const project = db.select({ providers: projects.providers }).from(projects).where(eq17(projects.id, projectId)).get();
17602
+ return providersARunWouldCall(resolveRunProviderSelection({
17603
+ requestedProviders: selection === RunTriggers.scheduled && schedule?.enabled ? schedule.providers : null,
17604
+ projectProviders: project?.providers ?? [],
17605
+ runnableProviders: runnable
17606
+ }), runnable);
17607
+ }
17608
+ function runAdmissionState(db, params) {
17609
+ let calling = null;
17610
+ const providers = () => calling ??= sweepProviders(db, params.projectId, params.runnableProviders, params.selection ?? RunTriggers.scheduled);
17611
+ const held = heldProviderAccounts(db, { projectId: params.projectId, now: params.now, providers });
17612
+ const refused = held.size > 0 && providers().every((provider) => held.has(provider));
17613
+ return {
17614
+ refused,
17615
+ retryAfter: refused ? providerAccountFailures(held).retryAfter : null,
17616
+ providers: Object.fromEntries(held)
17507
17617
  };
17508
17618
  }
17509
17619
  function providersFailingError(projectName, failures) {
@@ -17513,6 +17623,20 @@ function providersFailingError(projectName, failures) {
17513
17623
  { projectName, ...failures }
17514
17624
  );
17515
17625
  }
17626
+ var RUN_REFUSED_AUDIT_ACTION = "run.refused";
17627
+ function recordRunRefusal(db, projectId, failures, audit) {
17628
+ 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();
17629
+ if (last && parseJsonColumn(last.diff, {}).latestRunId === failures.latestRunId) return false;
17630
+ writeAuditLog(db, {
17631
+ projectId,
17632
+ actor: audit.actor,
17633
+ action: RUN_REFUSED_AUDIT_ACTION,
17634
+ entityType: audit.entityType,
17635
+ entityId: audit.entityId,
17636
+ diff: { code: "PROVIDERS_FAILING", ...failures }
17637
+ });
17638
+ return true;
17639
+ }
17516
17640
  function queueRunIfProjectIdle(db, params) {
17517
17641
  const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
17518
17642
  const kind = params.kind ?? "answer-visibility";
@@ -17553,17 +17677,14 @@ function queueRunIfProjectIdle(db, params) {
17553
17677
  }
17554
17678
  const current = activeRun();
17555
17679
  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 };
17680
+ const admission = queueProviderAdmission(tx, params, stamp, createdAt);
17681
+ if (admission.refused) {
17682
+ const refusalRecorded = params.auditRefusal ? recordRunRefusal(tx, params.projectId, admission.refused, params.auditRefusal) : false;
17683
+ return { conflict: false, refused: admission.refused, refusalRecorded };
17684
+ }
17685
+ const skippedProviders = admission.skipped;
17686
+ const admittedDispatch = dispatchAfterProviderAdmission(params, dispatch, admission);
17687
+ const dispatchModes = admittedDispatch.modes;
17567
17688
  const scoped = params.queries != null || stamp?.scope != null;
17568
17689
  const basket = scoped ? null : ensureCurrentQueryBasketRevision(tx, params.projectId, createdAt);
17569
17690
  tx.insert(runs).values({
@@ -17587,10 +17708,11 @@ function queueRunIfProjectIdle(db, params) {
17587
17708
  measurementManifest: stamp?.manifest ?? null,
17588
17709
  measurementScope: stamp?.scope ?? null,
17589
17710
  measurementExecutionIdentity: stamp?.identity ?? null,
17590
- providerDispatchModes: Object.keys(dispatch.modes).length > 0 ? dispatch.modes : null,
17711
+ providerDispatchModes: Object.keys(dispatchModes).length > 0 ? dispatchModes : null,
17712
+ skippedProviders: Object.keys(skippedProviders).length > 0 ? skippedProviders : null,
17591
17713
  createdAt
17592
17714
  }).run();
17593
- return { conflict: false, runId, dispatch };
17715
+ return { conflict: false, runId, dispatch: admittedDispatch, skippedProviders };
17594
17716
  });
17595
17717
  }
17596
17718
 
@@ -21005,7 +21127,7 @@ async function runRoutes(app, opts) {
21005
21127
  if (activeRun) {
21006
21128
  return { conflict: true, activeRunId: activeRun.id };
21007
21129
  }
21008
- const refused = providerAccountRefusal(tx, {
21130
+ const admission = providerAccountAdmission(tx, {
21009
21131
  projectId: project.id,
21010
21132
  trigger,
21011
21133
  force: body.force ?? false,
@@ -21020,7 +21142,8 @@ async function runRoutes(app, opts) {
21020
21142
  return providersARunWouldCall(roster, runnable);
21021
21143
  }
21022
21144
  });
21023
- if (refused) return { conflict: false, refused };
21145
+ if (admission.refused) return { conflict: false, refused: admission.refused };
21146
+ const skippedProviders = Object.keys(admission.skipped).length > 0 ? admission.skipped : null;
21024
21147
  const inserted = [];
21025
21148
  for (const loc of projectLocations) {
21026
21149
  const runId2 = crypto16.randomUUID();
@@ -21032,6 +21155,7 @@ async function runRoutes(app, opts) {
21032
21155
  trigger,
21033
21156
  location: loc.label,
21034
21157
  queries: queriesColumn,
21158
+ skippedProviders,
21035
21159
  createdAt: now
21036
21160
  }).run();
21037
21161
  inserted.push({ runId: runId2, loc });
@@ -21110,14 +21234,36 @@ async function runRoutes(app, opts) {
21110
21234
  const countRow = app.db.select({ count: sql11`count(*)` }).from(runs).where(and21(eq28(runs.projectId, project.id), notProbeRun())).get();
21111
21235
  const totalRuns = countRow?.count ?? 0;
21112
21236
  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();
21237
+ const admissionParams = {
21238
+ projectId: project.id,
21239
+ now: (/* @__PURE__ */ new Date()).toISOString(),
21240
+ runnableProviders: opts.getRunnableProviderNames?.()
21241
+ };
21242
+ const admission = runAdmissionState(app.db, admissionParams);
21243
+ const manualAdmission = runAdmissionState(app.db, { ...admissionParams, selection: RunTriggers.manual });
21113
21244
  if (!latestRun) {
21114
- return reply.send({ totalRuns: 0, run: null });
21245
+ return reply.send({ totalRuns: 0, run: null, admission, manualAdmission });
21115
21246
  }
21116
21247
  return reply.send({
21117
21248
  totalRuns,
21118
- run: loadRunDetail(app, latestRun)
21249
+ run: loadRunDetail(app, latestRun),
21250
+ admission,
21251
+ manualAdmission
21119
21252
  });
21120
21253
  });
21254
+ app.get("/projects/:name/run-admission", async (request, reply) => {
21255
+ const selection = request.query.selection ?? RunTriggers.scheduled;
21256
+ if (selection !== RunTriggers.manual && selection !== RunTriggers.scheduled) {
21257
+ throw validationError('"selection" must be one of: manual, scheduled');
21258
+ }
21259
+ const project = resolveProject(app.db, request.params.name);
21260
+ return reply.send(runAdmissionState(app.db, {
21261
+ projectId: project.id,
21262
+ now: (/* @__PURE__ */ new Date()).toISOString(),
21263
+ runnableProviders: opts.getRunnableProviderNames?.(),
21264
+ selection
21265
+ }));
21266
+ });
21121
21267
  app.get("/runs", async (request, reply) => {
21122
21268
  const limit = parseListLimit(request.query.limit, 500, 5e3);
21123
21269
  const since = parseListSince(request.query.since);
@@ -21210,6 +21356,7 @@ async function runRoutes(app, opts) {
21210
21356
  for (const entry of eligible) {
21211
21357
  try {
21212
21358
  assertMeasurementRunStampable(app.db, {
21359
+ createdAt: now,
21213
21360
  projectId: entry.project.id,
21214
21361
  kind,
21215
21362
  trigger: "manual",
@@ -21218,7 +21365,8 @@ async function runRoutes(app, opts) {
21218
21365
  runnableProviders: runnableProviderNames,
21219
21366
  providerModels: opts.getEffectiveProviderModels?.(),
21220
21367
  dispatchMode,
21221
- batchEligibleProviders
21368
+ batchEligibleProviders,
21369
+ force: force ?? false
21222
21370
  });
21223
21371
  dispatchable.push(entry);
21224
21372
  } catch (error) {
@@ -21434,6 +21582,8 @@ function formatRun(row) {
21434
21582
  // Only plan runs can batch, so only they carry the frozen modes.
21435
21583
  dispatchModes: row.providerDispatchModes ?? {}
21436
21584
  } : {},
21585
+ // Only on a run that skipped a provider, so other runs read as before.
21586
+ ...row.skippedProviders && Object.keys(row.skippedProviders).length > 0 ? { skippedProviders: row.skippedProviders } : {},
21437
21587
  createdAt: row.createdAt
21438
21588
  };
21439
21589
  }
@@ -34087,6 +34237,128 @@ function clickPeriods(periods, clicks, impressions) {
34087
34237
  impressions: impressions[index2] ?? 0
34088
34238
  }));
34089
34239
  }
34240
+ function compareCodeUnits(left, right) {
34241
+ if (left < right) return -1;
34242
+ return left > right ? 1 : 0;
34243
+ }
34244
+ function aiEngineLeadPeriods(periods, eventCounts, sessionCounts, leadRateAvailable) {
34245
+ return periods.map((period2, index2) => {
34246
+ const eventCount = eventCounts[index2] ?? 0;
34247
+ const sessions = sessionCounts[index2] ?? 0;
34248
+ return {
34249
+ ...period2,
34250
+ eventCount,
34251
+ sessions,
34252
+ leadRate: leadRateAvailable && sessions > 0 ? roundRatio(eventCount / sessions, RatioUnits.fraction) : null
34253
+ };
34254
+ });
34255
+ }
34256
+ var UNATTRIBUTED_AI = "(unattributed)";
34257
+ function buildAiEngineLeadClass(input) {
34258
+ const { periods, leadRateAvailable } = input;
34259
+ const sourcesByKey = /* @__PURE__ */ new Map();
34260
+ for (const row of [...input.leads, ...input.sessions]) {
34261
+ const sources = sourcesByKey.get(row.key) ?? /* @__PURE__ */ new Set();
34262
+ sources.add(row.source);
34263
+ sourcesByKey.set(row.key, sources);
34264
+ }
34265
+ const leadsByKey = aggregateByKey(input.leads, periods, (row) => row.key, (row) => row.value);
34266
+ const sessionsByKey = aggregateByKey(input.sessions, periods, (row) => row.key, (row) => row.value);
34267
+ const zeros = Array(periods.length).fill(0);
34268
+ const sum = (values) => values.reduce((total, value) => total + value, 0);
34269
+ const countsFor = (key) => ({
34270
+ leads: leadsByKey.get(key) ?? zeros,
34271
+ sessions: sessionsByKey.get(key) ?? zeros
34272
+ });
34273
+ const sourcesFor = (key) => [...sourcesByKey.get(key) ?? []].sort(compareCodeUnits);
34274
+ const engineRows = [...sourcesByKey.keys()].filter((key) => key !== UNATTRIBUTED_AI).map((engine) => ({ engine, ...countsFor(engine) })).sort((left, right) => (right.leads.at(-1) ?? 0) - (left.leads.at(-1) ?? 0) || sum(right.leads) - sum(left.leads) || (right.sessions.at(-1) ?? 0) - (left.sessions.at(-1) ?? 0) || sum(right.sessions) - sum(left.sessions) || left.engine.localeCompare(right.engine));
34275
+ const unattributed = countsFor(UNATTRIBUTED_AI);
34276
+ const allRows = [...engineRows, unattributed];
34277
+ const totalLeads = periods.map((_, index2) => sum(allRows.map((row) => row.leads[index2] ?? 0)));
34278
+ const totalSessions = periods.map((_, index2) => sum(allRows.map((row) => row.sessions[index2] ?? 0)));
34279
+ return {
34280
+ periods: aiEngineLeadPeriods(periods, totalLeads, totalSessions, leadRateAvailable),
34281
+ engines: engineRows.map((row) => ({
34282
+ engine: row.engine,
34283
+ label: aiReferralEngineLabel(row.engine),
34284
+ sources: sourcesFor(row.engine),
34285
+ periods: aiEngineLeadPeriods(periods, row.leads, row.sessions, leadRateAvailable)
34286
+ })),
34287
+ unattributed: {
34288
+ sources: sourcesFor(UNATTRIBUTED_AI),
34289
+ periods: aiEngineLeadPeriods(periods, unattributed.leads, unattributed.sessions, leadRateAvailable)
34290
+ }
34291
+ };
34292
+ }
34293
+ function attributeAiEngineRows(input) {
34294
+ const engineBySource = /* @__PURE__ */ new Map();
34295
+ const engineOf = (source) => {
34296
+ let engine = engineBySource.get(source);
34297
+ if (engine === void 0) {
34298
+ engine = aiEngineForReferralSource(source);
34299
+ engineBySource.set(source, engine);
34300
+ }
34301
+ return engine;
34302
+ };
34303
+ let paidOnlyByLandingPage = false;
34304
+ const attribute = (rows, valueOf) => rows.flatMap((row) => {
34305
+ const engine = engineOf(row.source);
34306
+ if (engine === null && !isGa4AiAssistantChannel(row.channelGroup)) return [];
34307
+ const evidence = { source: row.source, medium: row.medium, channelGroup: row.channelGroup };
34308
+ const withLandingPage = classifyAiReferralTrafficClass({ ...evidence, landingPage: row.landingPage });
34309
+ const trafficClass = input.landingPageEvidence ? withLandingPage : classifyAiReferralTrafficClass({ ...evidence, landingPage: null });
34310
+ if (trafficClass !== withLandingPage) paidOnlyByLandingPage = true;
34311
+ return [{
34312
+ date: row.date,
34313
+ source: row.source,
34314
+ key: engine ?? UNATTRIBUTED_AI,
34315
+ trafficClass,
34316
+ value: valueOf(row)
34317
+ }];
34318
+ });
34319
+ const leads = attribute(input.leads, (row) => row.eventCount);
34320
+ const sessions = attribute(input.sessions, (row) => row.sessions);
34321
+ return { leads, sessions, paidOnlyByLandingPage };
34322
+ }
34323
+ function buildAiEngineLeads(input) {
34324
+ const leadRateAvailable = input.leadRateUnavailableReason === null;
34325
+ const classBlock = (trafficClass) => buildAiEngineLeadClass({
34326
+ periods: input.periods,
34327
+ leads: input.leads.filter((row) => row.trafficClass === trafficClass),
34328
+ sessions: input.sessions.filter((row) => row.trafficClass === trafficClass),
34329
+ leadRateAvailable
34330
+ });
34331
+ return {
34332
+ leadRateAvailable,
34333
+ leadRateUnavailableReason: input.leadRateUnavailableReason,
34334
+ organic: classBlock(AiReferralTrafficClasses.organic),
34335
+ paid: classBlock(AiReferralTrafficClasses.paid)
34336
+ };
34337
+ }
34338
+ function latestDate(rows) {
34339
+ let latest = null;
34340
+ for (const row of rows) {
34341
+ if (latest === null || row.date > latest) latest = row.date;
34342
+ }
34343
+ return latest;
34344
+ }
34345
+ function hasLeadDayWithoutSessions(acquisitionRows, leadRows) {
34346
+ const acquisitionDates = new Set(acquisitionRows.map((row) => row.date));
34347
+ return leadRows.some((row) => !acquisitionDates.has(row.date));
34348
+ }
34349
+ function aiEngineLeadRateUnavailableReason(input) {
34350
+ if (!input.hasData) return AiEngineLeadRateUnavailableReasons["no-data"];
34351
+ if (input.acquisitionStatus !== "ready" || input.leadStatus !== "ready") {
34352
+ return AiEngineLeadRateUnavailableReasons["sync-not-ready"];
34353
+ }
34354
+ if (input.channelLeadsUnfiltered) return AiEngineLeadRateUnavailableReasons["channel-leads-unfiltered"];
34355
+ if (input.paidOnlyByLandingPage) return AiEngineLeadRateUnavailableReasons["paid-split-needs-landing-page"];
34356
+ if (input.latestLeadDate !== null && (input.latestAcquisitionDate === null || input.latestAcquisitionDate < input.latestLeadDate)) {
34357
+ return AiEngineLeadRateUnavailableReasons["sessions-behind-leads"];
34358
+ }
34359
+ if (input.leadDayWithoutSessions) return AiEngineLeadRateUnavailableReasons["sessions-missing-on-lead-days"];
34360
+ return null;
34361
+ }
34090
34362
  function engagementPeriods(periods, rows) {
34091
34363
  return periods.map((period2) => {
34092
34364
  const inPeriod = rows.filter((row) => row.date >= period2.startDate && row.date <= period2.endDate);
@@ -34303,6 +34575,28 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
34303
34575
  (row) => row.channelGroup,
34304
34576
  (row) => row.eventCount
34305
34577
  );
34578
+ const filtersNarrowSessions = parsedHostScope.data === "marketing" || pathPrefix !== null && pathPrefix !== "/";
34579
+ const hasAiEngineLeadData = gaAnchor !== null && hasLeadTimeline;
34580
+ const aiEngineRows = attributeAiEngineRows({
34581
+ leads: hasAiEngineLeadData ? leads : [],
34582
+ sessions: hasAiEngineLeadData ? acquisition : [],
34583
+ landingPageEvidence: attributionScope !== "channel"
34584
+ });
34585
+ const aiEngineLeads = buildAiEngineLeads({
34586
+ periods: hasAiEngineLeadData ? gaPeriods : [],
34587
+ leads: aiEngineRows.leads,
34588
+ sessions: aiEngineRows.sessions,
34589
+ leadRateUnavailableReason: aiEngineLeadRateUnavailableReason({
34590
+ hasData: hasAiEngineLeadData,
34591
+ acquisitionStatus: state?.acquisitionStatus,
34592
+ leadStatus: state?.leadStatus,
34593
+ channelLeadsUnfiltered: attributionScope === "channel" && filtersNarrowSessions,
34594
+ paidOnlyByLandingPage: aiEngineRows.paidOnlyByLandingPage,
34595
+ latestAcquisitionDate: latestDate(acquisitionRows),
34596
+ latestLeadDate: latestDate(leadRows),
34597
+ leadDayWithoutSessions: hasLeadDayWithoutSessions(acquisitionRows, leadRows)
34598
+ })
34599
+ });
34306
34600
  const propertyClicks = aggregateByKey(
34307
34601
  propertyRows,
34308
34602
  gscPeriods,
@@ -34407,7 +34701,8 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
34407
34701
  channels: rankEntries(leadChannels).map(([channelGroup, values]) => ({
34408
34702
  channelGroup,
34409
34703
  periods: eventPeriods(gaPeriods, values)
34410
- }))
34704
+ })),
34705
+ aiEngines: aiEngineLeads
34411
34706
  },
34412
34707
  engagement: {
34413
34708
  status: engagementAnchor === null ? "unavailable" : "ready",
@@ -36404,7 +36699,14 @@ async function compositeRoutes(app, options) {
36404
36699
  const previousVisRunGroup = visRunGroups[1] ?? [];
36405
36700
  const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
36406
36701
  const latestRunRow = allRuns[0] ?? null;
36407
- const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
36702
+ const admissionParams = {
36703
+ projectId: project.id,
36704
+ now: (/* @__PURE__ */ new Date()).toISOString(),
36705
+ runnableProviders: options.getRunnableProviderNames?.()
36706
+ };
36707
+ const admission = runAdmissionState(app.db, admissionParams);
36708
+ const manualAdmission = runAdmissionState(app.db, { ...admissionParams, selection: RunTriggers.manual });
36709
+ const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow), admission, manualAdmission } : { totalRuns: 0, run: null, admission, manualAdmission };
36408
36710
  const healthRow = app.db.select().from(healthSnapshots).where(eq50(healthSnapshots.projectId, project.id)).orderBy(desc24(healthSnapshots.createdAt)).limit(1).get();
36409
36711
  const health = healthRow ? mapHealthRow2(healthRow) : null;
36410
36712
  const insightRows = app.db.select().from(insights).where(eq50(insights.projectId, project.id)).orderBy(desc24(insights.createdAt)).all();
@@ -36644,6 +36946,7 @@ function summarizeRun(run) {
36644
36946
  startedAt: run.startedAt,
36645
36947
  finishedAt: run.finishedAt,
36646
36948
  error: parseRunError(run.error),
36949
+ ...run.skippedProviders && Object.keys(run.skippedProviders).length > 0 ? { skippedProviders: run.skippedProviders } : {},
36647
36950
  createdAt: run.createdAt
36648
36951
  };
36649
36952
  }
@@ -38068,6 +38371,7 @@ var SCHEMA_TABLE = {
38068
38371
  IntegrationSettingsSummaryDto: integrationSettingsSummaryDtoSchema,
38069
38372
  KeywordDto: keywordDtoSchema,
38070
38373
  LatestProjectRunDto: latestProjectRunDtoSchema,
38374
+ RunAdmissionDto: runAdmissionDtoSchema,
38071
38375
  LocationContext: locationContextSchema,
38072
38376
  MeasurementDiscoveryRequest: measurementDiscoveryRequestSchema,
38073
38377
  MeasurementDiscoveryResponse: measurementDiscoveryResponseSchema,
@@ -40544,7 +40848,7 @@ var routeCatalog = [
40544
40848
  dispatchMode: dispatchModeRequestSchema,
40545
40849
  force: {
40546
40850
  type: "boolean",
40547
- description: "Queue the run even when it would be refused with PROVIDERS_FAILING. Admission only; never stored."
40851
+ 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
40852
  }
40549
40853
  }
40550
40854
  }
@@ -40552,12 +40856,15 @@ var routeCatalog = [
40552
40856
  }
40553
40857
  },
40554
40858
  responses: {
40555
- 201: jsonResponse("Run queued.", "RunDto"),
40859
+ 201: jsonResponse(
40860
+ `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.`,
40861
+ "RunDto"
40862
+ ),
40556
40863
  400: errorResponse(
40557
40864
  '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
40865
  ),
40559
40866
  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.`
40867
+ `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
40868
  ),
40562
40869
  409: errorResponse("Run already in progress."),
40563
40870
  503: errorResponse("No runnable answer provider is configured.")
@@ -40577,12 +40884,33 @@ var routeCatalog = [
40577
40884
  method: "get",
40578
40885
  path: "/api/v1/projects/{name}/runs/latest",
40579
40886
  summary: "Get the latest project run",
40887
+ 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
40888
  tags: ["runs"],
40581
40889
  parameters: [nameParameter],
40582
40890
  responses: {
40583
40891
  200: jsonResponse("Latest run returned.", "LatestProjectRunDto")
40584
40892
  }
40585
40893
  },
40894
+ {
40895
+ method: "get",
40896
+ path: "/api/v1/projects/{name}/run-admission",
40897
+ summary: "Get whether the next sweep would be admitted",
40898
+ 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\`.`,
40899
+ tags: ["runs"],
40900
+ parameters: [
40901
+ nameParameter,
40902
+ {
40903
+ name: "selection",
40904
+ in: "query",
40905
+ 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.",
40906
+ schema: { type: "string", enum: ["manual", "scheduled"], default: "scheduled" }
40907
+ }
40908
+ ],
40909
+ responses: {
40910
+ 200: jsonResponse("Run admission returned.", "RunAdmissionDto"),
40911
+ 400: errorResponse("Invalid admission selection.")
40912
+ }
40913
+ },
40586
40914
  {
40587
40915
  method: "get",
40588
40916
  path: "/api/v1/runs",
@@ -40617,7 +40945,7 @@ var routeCatalog = [
40617
40945
  dispatchMode: dispatchModeRequestSchema,
40618
40946
  force: {
40619
40947
  type: "boolean",
40620
- description: "Queue projects that would be refused with PROVIDERS_FAILING. Admission only; never stored."
40948
+ 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
40949
  }
40622
40950
  }
40623
40951
  }
@@ -76149,7 +76477,6 @@ function verifyIpForRuleDecision(ip, ruleId) {
76149
76477
  }
76150
76478
 
76151
76479
  // ../integration-traffic/src/rules.ts
76152
- var LEGACY_CHATGPT_DOMAIN = "chat.openai.com";
76153
76480
  var SELF_TRAFFIC_USER_AGENT_PATTERNS = [
76154
76481
  /AINYC-AEO-Audit\//i
76155
76482
  ];
@@ -76532,7 +76859,7 @@ var DEFAULT_AI_CRAWLER_USER_AGENT_SUBSTRINGS = [
76532
76859
  ];
76533
76860
  var DEFAULT_AI_REFERRER_RULES = [
76534
76861
  { domain: AI_ENGINE_DOMAINS.chatgpt, operator: "OpenAI", product: "ChatGPT" },
76535
- { domain: LEGACY_CHATGPT_DOMAIN, operator: "OpenAI", product: "ChatGPT" },
76862
+ { domain: AI_ENGINE_DOMAINS.chatgptLegacy, operator: "OpenAI", product: "ChatGPT" },
76536
76863
  { domain: AI_ENGINE_DOMAINS.perplexity, operator: "Perplexity", product: "Perplexity" },
76537
76864
  { domain: AI_ENGINE_DOMAINS.claude, operator: "Anthropic", product: "Claude" },
76538
76865
  { domain: AI_ENGINE_DOMAINS.gemini, operator: "Google", product: "Gemini" },
@@ -89646,7 +89973,7 @@ async function apiRoutes(app, opts) {
89646
89973
  await api.register(citationRoutes);
89647
89974
  await api.register(visibilityStatsRoutes);
89648
89975
  await api.register(resultsExportRoutes);
89649
- await api.register(compositeRoutes, { sentiment: opts.sentiment });
89976
+ await api.register(compositeRoutes, { sentiment: opts.sentiment, getRunnableProviderNames: opts.getRunnableProviderNames });
89650
89977
  await api.register(contentRoutes, {
89651
89978
  explainContentRecommendation: opts.explainContentRecommendation,
89652
89979
  briefContentRecommendation: opts.briefContentRecommendation,