@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
@@ -139,7 +139,7 @@ import {
139
139
  trafficSeriesGranularitySchema,
140
140
  visibilityCompareSelectionSchema,
141
141
  visibilityReportRequestSchema
142
- } from "./chunk-AU7OUDWR.js";
142
+ } from "./chunk-KOBK4I5N.js";
143
143
 
144
144
  // src/cli-error.ts
145
145
  function isMachineFormat(format) {
@@ -1482,7 +1482,7 @@ var canonryMcpTools = [
1482
1482
  defineTool({
1483
1483
  name: "canonry_project_overview",
1484
1484
  title: "Get project overview (composite)",
1485
- description: `Sentiment headline is sentiment.branded; nonBrand is a separate exception population. pooledOverall and deprecated overall are explicitly pooled compatibility figures and must never be reported as a class-specific headline. One-call summary for "how is project X doing?". Returns independent mention and citation coverage, separate query-level movement for each signal, query-basket comparability with added/removed counts, latest run and health, insights, provider/model breakdowns, competitors, attention items, and recent history. Movement excludes queries not shared by both sweeps. Filterable by location and time window. Prefer this over fanning out to separate read tools for the same question. queryClassScope says which figures pool branded and non-brand: the coverage, scores (except scores.mentionShare) and movement count queries across both classes, so never report them as one class or as answers. The latest run's status and health are not a completeness check: for expected, executed and missing answers read canonry_measurement_data_quality or, for a plan run, canonry_run_completeness. canonry_doctor checks configuration and integrations, not sweeps. SCOPE LIMIT: this returns nothing about Advanced Measurement: no plan, Targets, Properties, groups, or segments. Its silence is not evidence that a project lacks them. When the system prompt has no Project shape line, call canonry_measurement_plan_get before stating that a project has no Properties, no Advanced plan, or cannot break out per-Property performance; use canonry_measurement_portfolio_summary to rank Properties.`,
1485
+ description: `Sentiment headline is sentiment.branded; nonBrand is a separate exception population. pooledOverall and deprecated overall are explicitly pooled compatibility figures and must never be reported as a class-specific headline. One-call summary for "how is project X doing?". Returns independent mention and citation coverage, separate query-level movement for each signal, query-basket comparability with added/removed counts, latest run and health, insights, provider/model breakdowns, competitors, attention items, and recent history. Movement excludes queries not shared by both sweeps. Filterable by location and time window. Prefer this over fanning out to separate read tools for the same question. queryClassScope says which figures pool branded and non-brand: the coverage, scores (except scores.mentionShare) and movement count queries across both classes, so never report them as one class or as answers. latestRun.admission describes the scheduled sweep; latestRun.manualAdmission describes a default full manual sweep. Each says whether its provider scope would be refused or skip providers that keep failing on their accounts. They can differ when the schedule names fewer providers; use manualAdmission before a default manual sweep. The latest run's status and health are not a completeness check: for expected, executed and missing answers read canonry_measurement_data_quality or, for a plan run, canonry_run_completeness. canonry_doctor checks configuration and integrations, not sweeps. SCOPE LIMIT: this returns nothing about Advanced Measurement: no plan, Targets, Properties, groups, or segments. Its silence is not evidence that a project lacks them. When the system prompt has no Project shape line, call canonry_measurement_plan_get before stating that a project has no Properties, no Advanced plan, or cannot break out per-Property performance; use canonry_measurement_portfolio_summary to rank Properties.`,
1486
1486
  access: "read",
1487
1487
  tier: "core",
1488
1488
  inputSchema: z3.object({
@@ -1656,7 +1656,7 @@ var canonryMcpTools = [
1656
1656
  defineTool({
1657
1657
  name: "canonry_runs_latest",
1658
1658
  title: "Get latest project run",
1659
- description: "Get the latest run and total run count for a Canonry project.",
1659
+ description: "Get the latest run and total run count for a Canonry project, plus scheduled admission and default full-sweep manualAdmission: whether each would be refused (PROVIDERS_FAILING) and which providers it would skip because each keeps failing on its account, with each code and retryAfter. They can differ when the schedule names fewer providers. Read manualAdmission before a default manual sweep; admission describes the schedule. A refused scheduled sweep leaves no run, so read admission to tell whether scheduled sweeps are running.",
1660
1660
  access: "read",
1661
1661
  tier: "monitoring",
1662
1662
  inputSchema: projectInputSchema,
@@ -1667,7 +1667,7 @@ var canonryMcpTools = [
1667
1667
  defineTool({
1668
1668
  name: "canonry_run_get",
1669
1669
  title: "Get run",
1670
- description: "Get a Canonry run with its snapshots.",
1670
+ description: "Get a Canonry run with its snapshots. skippedProviders records the providers this run did not call because they kept failing on their accounts; error.providers marks their entries skipped. This is a stored run, not the next sweep's admission: read canonry_runs_latest for scheduled admission and default full-sweep manualAdmission, which can differ when a schedule names fewer providers.",
1671
1671
  access: "read",
1672
1672
  tier: "monitoring",
1673
1673
  inputSchema: runGetInputSchema,
@@ -2280,7 +2280,7 @@ var canonryMcpTools = [
2280
2280
  defineTool({
2281
2281
  name: "canonry_ga_measurement_analysis",
2282
2282
  title: "Analyze GA acquisition and search demand",
2283
- description: "Compare native GA4 channels and lead events with branded/non-brand Search Console demand over fixed 30-day cohorts.",
2283
+ description: "Compare native GA4 channels and lead events (also split by AI engine, with each engine's sessions and lead rate) with branded/non-brand Search Console demand over fixed 30-day cohorts.",
2284
2284
  access: "read",
2285
2285
  tier: "ga",
2286
2286
  inputSchema: gaMeasurementAnalysisInputSchema,
@@ -3182,7 +3182,7 @@ var canonryMcpTools = [
3182
3182
  defineTool({
3183
3183
  name: "canonry_run_trigger",
3184
3184
  title: "Trigger run",
3185
- description: `Trigger an answer-visibility run for a Canonry project. Omit both scoping fields for a full sweep. Pass request.queries[] to measure only some of the project's tracked questions. On a project with a published measurement plan, pass request.measurementScope with at least one group or target to measure one slice of that plan \u2014 a group stands for its member Targets, and only the questions those Targets selected are measured. A scope naming nothing is rejected rather than treated as a full sweep, so omit the field entirely when you want everything. Either kind of slice is recorded as a spot check: it never replaces a full sweep in the dashboard, analytics or report. For verification scenarios (testing whether a provider migration worked, reproducing a regression, sanity-checking after a code change), set request.trigger='probe' \u2014 probe runs write a snapshot you can inspect via canonry_runs_get but are EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use 'probe' whenever you are testing on your own initiative rather than producing data the user/dashboard will consume. request.dispatchMode='batch' sends every provider that can (a full sweep of a published plan, a batch-capable provider enabled in config.yaml, every answer's model frozen) to its asynchronous batch API at the batch price; the rest run sync and the run stays running until the batch ends (up to its deadline, 24h by default). A batch request no provider can honour is refused with each provider's reason. Omit it for the ordinary sync sweep. A run is refused with PROVIDERS_FAILING when every provider it 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). Tell the user to fix the key, access or billing; saving a new key, model or endpoint in the provider's settings lets the next run through at once. A probe (request.trigger='probe') is never refused, and one that succeeds clears the refusal, so use a probe to check a fix. Set request.force=true only when the user asks to run anyway.`,
3185
+ description: `Trigger an answer-visibility run for a Canonry project. Omit both scoping fields for a full sweep. Pass request.queries[] to measure only some of the project's tracked questions. On a project with a published measurement plan, pass request.measurementScope with at least one group or target to measure one slice of that plan \u2014 a group stands for its member Targets, and only the questions those Targets selected are measured. A scope naming nothing is rejected rather than treated as a full sweep, so omit the field entirely when you want everything. Either kind of slice is recorded as a spot check: it never replaces a full sweep in the dashboard, analytics or report. For verification scenarios (testing whether a provider migration worked, reproducing a regression, sanity-checking after a code change), set request.trigger='probe' \u2014 probe runs write a snapshot you can inspect via canonry_runs_get but are EXCLUDED from dashboard, analytics, intelligence, report, and notifications. Use 'probe' whenever you are testing on your own initiative rather than producing data the user/dashboard will consume. request.dispatchMode='batch' sends every provider that can (a full sweep of a published plan, a batch-capable provider enabled in config.yaml, every answer's model frozen) to its asynchronous batch API at the batch price; the rest run sync and the run stays running until the batch ends (up to its deadline, 24h by default). A batch request no provider can honour is refused with each provider's reason. Omit it for the ordinary sync sweep. A run is refused with PROVIDERS_FAILING when every provider it 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). Tell the user to fix the key, access or billing; saving a new key, model or endpoint in the provider's settings lets the next run through at once. A probe (request.trigger='probe') is never refused, and one that succeeds clears the refusal, so use a probe to check a fix. When only some providers keep failing that way, the run is queued without them: skippedProviders on the returned run names each with its code and retryAfter, and the run ends partial. Set request.force=true (call every provider) only when the user asks to run anyway.`,
3186
3186
  access: "write",
3187
3187
  tier: "core",
3188
3188
  inputSchema: runTriggerInputSchema,
@@ -20,20 +20,20 @@ import {
20
20
  saveConfig,
21
21
  saveConfigPatch,
22
22
  sentimentInstallReadiness
23
- } from "./chunk-SM6ZZAPK.js";
23
+ } from "./chunk-SGQBPTWQ.js";
24
24
  import {
25
25
  computeFactorAverages,
26
26
  deleteSiteCrawlGraphLayout,
27
27
  persistSiteCrawlGraphLayout,
28
28
  prepareSiteCrawlGraphLayout,
29
29
  recordedSharePct
30
- } from "./chunk-7I64KEGM.js";
30
+ } from "./chunk-SG6T3DEP.js";
31
31
  import {
32
32
  CanonryMcpToolNames,
33
33
  CliError,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-BTHEFJCI.js";
36
+ } from "./chunk-LC7GMHOL.js";
37
37
  import {
38
38
  CC_CACHE_DIR,
39
39
  DUCKDB_SPEC,
@@ -217,7 +217,7 @@ import {
217
217
  siteLivenessState,
218
218
  toAlertView,
219
219
  usageCounters
220
- } from "./chunk-GQMSJEMZ.js";
220
+ } from "./chunk-7HJYGDRW.js";
221
221
  import {
222
222
  AGENT_MEMORY_VALUE_MAX_BYTES,
223
223
  AGENT_NONE,
@@ -309,7 +309,6 @@ import {
309
309
  canonicalizeGtmContainerId,
310
310
  canonicalizeGtmResourceSelection,
311
311
  classifyProviderErrorMessage,
312
- classifyProviderErrorMessages,
313
312
  classifyTemplateLinkEdge,
314
313
  cloudflareQueueNameSchema,
315
314
  compareSemver,
@@ -378,6 +377,7 @@ import {
378
377
  maskTelemetryAnonymousId,
379
378
  matcherMatchesText,
380
379
  missingDependency,
380
+ mostActionableProviderErrorCode,
381
381
  nextScheduleUpdatedAt,
382
382
  normalizeAgentSlug,
383
383
  normalizeIdTokens,
@@ -428,8 +428,9 @@ import {
428
428
  validationError,
429
429
  visibilityReportRequestSchema,
430
430
  winnabilityClassLabel,
431
- withRetry
432
- } from "./chunk-AU7OUDWR.js";
431
+ withRetry,
432
+ withSkippedProviders
433
+ } from "./chunk-KOBK4I5N.js";
433
434
 
434
435
  // src/runtime-context.ts
435
436
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -8041,7 +8042,7 @@ function nonBlank(value) {
8041
8042
  return trimmed ? trimmed : void 0;
8042
8043
  }
8043
8044
  function resolveBuildCommit(env = process.env) {
8044
- const embedded = true ? "c2de5a23906b2d9fff8c2543a55d52dd9cbcb889" : void 0;
8045
+ const embedded = true ? "e5577fdc9f99f2cc008aeff477aaf284da4532f7" : void 0;
8045
8046
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
8046
8047
  }
8047
8048
  function resolveInstanceIdentity(env = process.env) {
@@ -8123,12 +8124,17 @@ function buildSiteAuditCompletedProps(input) {
8123
8124
  return props;
8124
8125
  }
8125
8126
  var MAX_NESTED_KEYS = 12;
8126
- function buildProviderOutcomeProps(providers, providerErrors) {
8127
- if (providerErrors.size === 0) return {};
8128
- const names = [.../* @__PURE__ */ new Set([...providers, ...providerErrors.keys()])].slice(0, MAX_NESTED_KEYS);
8127
+ function buildProviderOutcomeProps(providers, providerErrors, skippedProviders = []) {
8128
+ if (providerErrors.size === 0 && skippedProviders.length === 0) return {};
8129
+ const skipped = new Set(skippedProviders);
8130
+ const names = [.../* @__PURE__ */ new Set([...providers, ...providerErrors.keys(), ...skipped])].slice(0, MAX_NESTED_KEYS);
8129
8131
  const providerOutcomes = {};
8130
8132
  const providerHttpStatus = {};
8131
8133
  for (const name of names) {
8134
+ if (skipped.has(name)) {
8135
+ providerOutcomes[name] = "skipped";
8136
+ continue;
8137
+ }
8132
8138
  const message = providerErrors.get(name);
8133
8139
  if (message === void 0) {
8134
8140
  providerOutcomes[name] = "ok";
@@ -8140,6 +8146,20 @@ function buildProviderOutcomeProps(providers, providerErrors) {
8140
8146
  }
8141
8147
  return Object.keys(providerHttpStatus).length > 0 ? { providerOutcomes, providerHttpStatus } : { providerOutcomes };
8142
8148
  }
8149
+ function buildRunRefusedProps(input) {
8150
+ const outcomes = Object.entries(input.providers).slice(0, MAX_NESTED_KEYS);
8151
+ const props = {
8152
+ reason: "providers_failing",
8153
+ providerCount: Object.keys(input.providers).length,
8154
+ providers: Object.keys(input.providers),
8155
+ providerOutcomes: Object.fromEntries(outcomes),
8156
+ trigger: input.trigger
8157
+ };
8158
+ const domainHash = hashDomain(input.canonicalDomain ?? null);
8159
+ if (domainHash) props.domainHash = domainHash;
8160
+ if (input.location) props.location = input.location;
8161
+ return props;
8162
+ }
8143
8163
  function runFailureSite(providerCallStart, providerCallEnd) {
8144
8164
  if (providerCallStart === void 0) return "setup";
8145
8165
  return providerCallEnd === void 0 ? "provider_call" : "finalize";
@@ -8455,12 +8475,16 @@ function chunkBatchLines(lines, maxRequests, maxBytes) {
8455
8475
  if (current.length > 0) chunks.push(current);
8456
8476
  return { chunks, oversized };
8457
8477
  }
8458
- function runOutcome(inserted, providerErrors, planShortfall) {
8459
- const someFailed = providerErrors.size > 0 || planShortfall > 0;
8478
+ function calledProviderErrors(providerErrors, skipped) {
8479
+ return new Map([...providerErrors].filter(([provider]) => !(provider in skipped)));
8480
+ }
8481
+ function runOutcome(inserted, providerErrors, planShortfall, skipped) {
8482
+ const errors = calledProviderErrors(providerErrors, skipped);
8483
+ const someFailed = errors.size > 0 || planShortfall > 0 || Object.keys(skipped).length > 0;
8460
8484
  const allFailed = inserted === 0 && someFailed;
8461
8485
  return {
8462
8486
  status: allFailed ? RunStatuses.failed : someFailed ? RunStatuses.partial : RunStatuses.completed,
8463
- error: someFailed ? serializeRunError(buildProviderRunError(providerErrors)) : null
8487
+ error: someFailed ? serializeRunError(withSkippedProviders(buildProviderRunError(errors), skipped)) : null
8464
8488
  };
8465
8489
  }
8466
8490
  function runRecordingContext(db, input) {
@@ -8518,8 +8542,12 @@ function classifyRunAbortReason(message) {
8518
8542
  return void 0;
8519
8543
  }
8520
8544
  var FAILURE_STREAK_LOOKBACK = 50;
8521
- function classifyProviderErrors(errors) {
8522
- return classifyProviderErrorMessages(errors.values());
8545
+ function classifyProviderErrors(errors, skipped) {
8546
+ const codes = [
8547
+ ...[...errors.values()].map(classifyProviderErrorMessage),
8548
+ ...Object.values(skipped).map((streak) => streak.code)
8549
+ ];
8550
+ return codes.length > 0 ? mostActionableProviderErrorCode(codes) : void 0;
8523
8551
  }
8524
8552
  var JobRunner = class {
8525
8553
  /**
@@ -8739,7 +8767,14 @@ var JobRunner = class {
8739
8767
  if (activeProviders.length === 0) {
8740
8768
  throw new Error("No providers configured. Add at least one provider API key.");
8741
8769
  }
8742
- log2.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
8770
+ const skipped = existingRun.skippedProviders ?? {};
8771
+ const calledProviders = activeProviders.filter((provider) => !(provider.adapter.name in skipped));
8772
+ log2.info("run.dispatch", {
8773
+ runId,
8774
+ providerCount: calledProviders.length,
8775
+ providers: calledProviders.map((p) => p.adapter.name),
8776
+ ...Object.keys(skipped).length > 0 ? { skippedProviders: Object.keys(skipped) } : {}
8777
+ });
8743
8778
  const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).orderBy(competitors.domain).all();
8744
8779
  const competitorDomains = projectCompetitors.map((c) => c.domain);
8745
8780
  recording = runRecordingContext(this.db, {
@@ -8761,7 +8796,7 @@ var JobRunner = class {
8761
8796
  };
8762
8797
  const queriesPerProvider = planExecution?.maxUnitsPerProvider ?? projectQueries.length;
8763
8798
  const todayPeriod = getCurrentUsageDay();
8764
- for (const p of activeProviders) {
8799
+ for (const p of calledProviders) {
8765
8800
  const providerScope = `${projectId}:${p.adapter.name}`;
8766
8801
  const limit = p.config.quotaPolicy.maxRequestsPerDay;
8767
8802
  const quota = reserveDailyQueryQuota(this.db, { scope: providerScope, period: todayPeriod, count: queriesPerProvider, limit });
@@ -8773,7 +8808,7 @@ var JobRunner = class {
8773
8808
  providerReservations.set(p.adapter.name, { scope: providerScope, period: todayPeriod, reserved: queriesPerProvider });
8774
8809
  }
8775
8810
  const executionGates = /* @__PURE__ */ new Map();
8776
- for (const provider of activeProviders) {
8811
+ for (const provider of calledProviders) {
8777
8812
  executionGates.set(
8778
8813
  provider.adapter.name,
8779
8814
  getSharedProviderExecutionGate(
@@ -8785,8 +8820,8 @@ var JobRunner = class {
8785
8820
  }
8786
8821
  const providerErrors = /* @__PURE__ */ new Map();
8787
8822
  let totalSnapshotsInserted = 0;
8788
- const apiProviders = activeProviders.filter((p) => !isBrowserProvider(p.adapter.name));
8789
- const browserProviders = activeProviders.filter((p) => isBrowserProvider(p.adapter.name));
8823
+ const apiProviders = calledProviders.filter((p) => !isBrowserProvider(p.adapter.name));
8824
+ const browserProviders = calledProviders.filter((p) => isBrowserProvider(p.adapter.name));
8790
8825
  const processQueryForProvider = async (registeredProvider, q) => {
8791
8826
  const { adapter, config } = registeredProvider;
8792
8827
  const providerName = adapter.name;
@@ -9052,7 +9087,7 @@ var JobRunner = class {
9052
9087
  for (const [provider, units] of planExecution.unitsByProvider) {
9053
9088
  const providerName = provider;
9054
9089
  const dispatched = providerDispatchCounts.get(providerName) ?? 0;
9055
- if (dispatched >= units.length || providerErrors.has(providerName)) continue;
9090
+ if (dispatched >= units.length || providerErrors.has(providerName) || providerName in skipped) continue;
9056
9091
  providerErrors.set(
9057
9092
  providerName,
9058
9093
  `No ${provider} provider was available to this worker, so ${units.length - dispatched} expected measurement(s) did not run.`
@@ -9077,6 +9112,7 @@ var JobRunner = class {
9077
9112
  inserted: totalSnapshotsInserted,
9078
9113
  providerErrors,
9079
9114
  planShortfall,
9115
+ skippedProviders: skipped,
9080
9116
  executionContext,
9081
9117
  startTime,
9082
9118
  phases: buildPhases({ startTime, providerCallStart, providerCallEnd }),
@@ -9181,7 +9217,7 @@ var JobRunner = class {
9181
9217
  */
9182
9218
  finalizeRun(input) {
9183
9219
  const { runId, providerErrors } = input;
9184
- const outcome = runOutcome(input.inserted, providerErrors, input.planShortfall);
9220
+ const outcome = runOutcome(input.inserted, providerErrors, input.planShortfall, input.skippedProviders ?? {});
9185
9221
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
9186
9222
  const won = this.db.transaction((tx) => {
9187
9223
  const changed = tx.update(runs).set({
@@ -9207,8 +9243,10 @@ var JobRunner = class {
9207
9243
  * post-run pipeline.
9208
9244
  */
9209
9245
  reportFinalizedRun(input, finalStatus) {
9210
- const { runId, projectId, providerErrors, executionContext } = input;
9211
- const failureCode = providerErrors.size > 0 ? classifyProviderErrors(providerErrors) : void 0;
9246
+ const { runId, projectId, executionContext } = input;
9247
+ const skippedProviders = input.skippedProviders ?? {};
9248
+ const providerErrors = calledProviderErrors(input.providerErrors, skippedProviders);
9249
+ const failureCode = classifyProviderErrors(providerErrors, skippedProviders);
9212
9250
  const sampling = failureStreakSampling(finalStatus, this.priorFailureStreak(projectId, runId), runId);
9213
9251
  if (sampling.report) {
9214
9252
  trackEvent(
@@ -9225,7 +9263,7 @@ var JobRunner = class {
9225
9263
  phases: input.phases,
9226
9264
  location: executionContext.location
9227
9265
  }),
9228
- ...buildProviderOutcomeProps(executionContext.providers, providerErrors),
9266
+ ...buildProviderOutcomeProps(executionContext.providers, providerErrors, Object.keys(skippedProviders)),
9229
9267
  ...sampling.props
9230
9268
  },
9231
9269
  failureCode ? { errorCode: failureCode } : void 0
@@ -9295,15 +9333,21 @@ var JobRunner = class {
9295
9333
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
9296
9334
  const decided = this.db.transaction((tx) => {
9297
9335
  const txDb = tx;
9298
- const run = txDb.select({ kind: runs.kind, status: runs.status, pendingProviderErrors: runs.pendingProviderErrors }).from(runs).where(and3(eq3(runs.id, runId), eq3(runs.projectId, projectId))).get();
9336
+ const run = txDb.select({
9337
+ kind: runs.kind,
9338
+ status: runs.status,
9339
+ pendingProviderErrors: runs.pendingProviderErrors,
9340
+ skippedProviders: runs.skippedProviders
9341
+ }).from(runs).where(and3(eq3(runs.id, runId), eq3(runs.projectId, projectId))).get();
9299
9342
  if (!run || run.status !== RunStatuses.running || run.pendingProviderErrors === null) return null;
9300
9343
  const holding = txDb.select({ id: providerBatches.id }).from(providerBatches).where(and3(eq3(providerBatches.runId, runId), inArray(providerBatches.status, RUN_HOLDING_BATCH_STATUSES))).limit(1).get();
9301
9344
  if (holding) return null;
9302
9345
  const state = measurementRunSlotState(txDb, runId);
9303
9346
  const inserted = txDb.select({ value: count() }).from(querySnapshots).where(eq3(querySnapshots.runId, runId)).get()?.value ?? 0;
9304
9347
  const providerErrors = this.batchRunProviderErrors(txDb, runId, run.pendingProviderErrors, state.missing);
9348
+ const skippedProviders = run.skippedProviders ?? {};
9305
9349
  const shortfall = state.missing.length + (state.readable ? 0 : 1);
9306
- const outcome = runOutcome(inserted, providerErrors, shortfall);
9350
+ const outcome = runOutcome(inserted, providerErrors, shortfall, skippedProviders);
9307
9351
  const won = txDb.update(runs).set({
9308
9352
  status: outcome.status,
9309
9353
  finishedAt,
@@ -9312,7 +9356,7 @@ var JobRunner = class {
9312
9356
  }).where(and3(eq3(runs.id, runId), eq3(runs.status, RunStatuses.running), isNotNull(runs.pendingProviderErrors))).run().changes === 1;
9313
9357
  if (!won) return null;
9314
9358
  recordSentimentCompletion(tx, { projectId, runId, completionKey: "initial", completedAt: finishedAt });
9315
- return { kind: run.kind, inserted, providerErrors, status: outcome.status };
9359
+ return { kind: run.kind, inserted, providerErrors, skippedProviders, status: outcome.status };
9316
9360
  });
9317
9361
  if (!decided) return false;
9318
9362
  const telemetry = this.batchRunTelemetry(runId, projectId);
@@ -9323,6 +9367,7 @@ var JobRunner = class {
9323
9367
  kind: decided.kind,
9324
9368
  inserted: decided.inserted,
9325
9369
  providerErrors: decided.providerErrors,
9370
+ skippedProviders: decided.skippedProviders,
9326
9371
  executionContext: telemetry.executionContext,
9327
9372
  startTime: telemetry.startTime,
9328
9373
  // The sweep's phase split does not survive the wait (or a restart);
@@ -10373,7 +10418,8 @@ var JobRunner = class {
10373
10418
  measurementPlanVersionId: runs.measurementPlanVersionId,
10374
10419
  measurementManifest: runs.measurementManifest,
10375
10420
  providerDispatchModes: runs.providerDispatchModes,
10376
- pendingProviderErrors: runs.pendingProviderErrors
10421
+ pendingProviderErrors: runs.pendingProviderErrors,
10422
+ skippedProviders: runs.skippedProviders
10377
10423
  }).from(runs).where(eq3(runs.id, runId)).get();
10378
10424
  }
10379
10425
  isRunCancelled(runId) {
@@ -15120,6 +15166,9 @@ var Scheduler = class {
15120
15166
  runnableProviders: this.callbacks.getRunnableProviderNames?.(),
15121
15167
  providerModels: this.callbacks.getEffectiveProviderModels?.(),
15122
15168
  batchEligibleProviders: this.callbacks.getBatchEligibleProviderNames?.() ?? null,
15169
+ // A skipped slot is otherwise invisible: no run row, nothing in the
15170
+ // dashboard. One audit row per refusal keeps it in the history.
15171
+ auditRefusal: { actor: "scheduler", entityType: "schedule", entityId: currentSchedule.id },
15123
15172
  ...claimedOccurrence && recurrence && currentSchedule.nextRunAt === claimedOccurrence ? {
15124
15173
  scheduleClaim: {
15125
15174
  scheduleId: currentSchedule.id,
@@ -15149,6 +15198,14 @@ var Scheduler = class {
15149
15198
  scheduleId: currentSchedule.id,
15150
15199
  ...queueResult.refused
15151
15200
  });
15201
+ if (queueResult.refusalRecorded) {
15202
+ trackEvent("run.aborted", buildRunRefusedProps({
15203
+ providers: queueResult.refused.providers,
15204
+ trigger: RunTriggers.scheduled,
15205
+ canonicalDomain: project.canonicalDomain,
15206
+ location: locationLabel
15207
+ }), { errorCode: "PROVIDERS_FAILING" });
15208
+ }
15152
15209
  if (!claimedOccurrence) this.updateScheduleTiming(currentSchedule.id, { nextRunAt });
15153
15210
  return;
15154
15211
  }
@@ -15169,7 +15226,12 @@ var Scheduler = class {
15169
15226
  lastRunAt: now,
15170
15227
  nextRunAt
15171
15228
  });
15172
- log16.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
15229
+ log16.info("run.triggered", {
15230
+ runId,
15231
+ projectName: project.name,
15232
+ providers: providers ?? "all",
15233
+ ...Object.keys(queueResult.skippedProviders).length > 0 ? { skippedProviders: queueResult.skippedProviders } : {}
15234
+ });
15173
15235
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
15174
15236
  } catch (err) {
15175
15237
  log16.error("trigger.error", { scheduleId, projectId, kind, error: describeError(err) });
@@ -5,7 +5,7 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-GQMSJEMZ.js";
8
+ } from "./chunk-7HJYGDRW.js";
9
9
  import {
10
10
  SITE_CRAWL_GRAPH_MAX_EDGES,
11
11
  SITE_CRAWL_GRAPH_MAX_NODES,
@@ -14,7 +14,7 @@ import {
14
14
  factorStatusFromDistribution,
15
15
  factorStatusFromScore,
16
16
  roundPreservingTotal
17
- } from "./chunk-AU7OUDWR.js";
17
+ } from "./chunk-KOBK4I5N.js";
18
18
 
19
19
  // src/site-audit-factors.ts
20
20
  function recordedSharePct(factor) {
@@ -5,7 +5,7 @@ import {
5
5
  PACKAGE_VERSION,
6
6
  canonryMcpTools,
7
7
  isMachineFormat
8
- } from "./chunk-BTHEFJCI.js";
8
+ } from "./chunk-LC7GMHOL.js";
9
9
  import {
10
10
  AGENT_PROVIDER_IDS,
11
11
  CANONRY_NPM_PACKAGE_URL,
@@ -50,7 +50,7 @@ import {
50
50
  skillsClientSchema,
51
51
  upgradeCaveatFor,
52
52
  upgradeCommandFor
53
- } from "./chunk-AU7OUDWR.js";
53
+ } from "./chunk-KOBK4I5N.js";
54
54
 
55
55
  // src/config.ts
56
56
  import fs from "fs";
@@ -4,14 +4,14 @@ import {
4
4
  installSkills,
5
5
  loadConfigRaw,
6
6
  saveConfigPatch
7
- } from "./chunk-SM6ZZAPK.js";
7
+ } from "./chunk-SGQBPTWQ.js";
8
8
  import {
9
9
  PACKAGE_VERSION
10
- } from "./chunk-BTHEFJCI.js";
10
+ } from "./chunk-LC7GMHOL.js";
11
11
  import {
12
12
  SKILL_MANIFEST_FILENAME,
13
13
  SkillsClients
14
- } from "./chunk-AU7OUDWR.js";
14
+ } from "./chunk-KOBK4I5N.js";
15
15
 
16
16
  // src/skills-autosync.ts
17
17
  import fs from "fs";