@ainyc/canonry 4.25.0 → 4.26.1

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.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-C4scWriC.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-BrlkSJ58.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-rPok6yk8.css">
17
17
  </head>
18
18
  <body>
@@ -18,7 +18,7 @@ import {
18
18
  trafficConnectCloudRunRequestSchema,
19
19
  trafficConnectWordpressRequestSchema,
20
20
  trafficEventKindSchema
21
- } from "./chunk-CRQMGNPH.js";
21
+ } from "./chunk-HVW665A4.js";
22
22
 
23
23
  // src/config.ts
24
24
  import fs from "fs";
@@ -1954,7 +1954,7 @@ var canonryMcpTools = [
1954
1954
  defineTool({
1955
1955
  name: "canonry_run_trigger",
1956
1956
  title: "Trigger run",
1957
- description: "Trigger an answer-visibility run for a Canonry project.",
1957
+ description: "Trigger an answer-visibility run for a Canonry project. Pass request.queries[] to scope the sweep to a subset of the project's tracked queries; omit for a full sweep.",
1958
1958
  access: "write",
1959
1959
  tier: "core",
1960
1960
  inputSchema: runTriggerInputSchema,
@@ -105,6 +105,7 @@ var runTriggerRequestSchema = z2.object({
105
105
  kind: z2.literal(RunKinds["answer-visibility"]).optional(),
106
106
  trigger: z2.literal(RunTriggers.manual).optional(),
107
107
  providers: z2.array(providerNameSchema).optional(),
108
+ queries: z2.array(z2.string().min(1)).min(1).optional(),
108
109
  location: z2.string().min(1).optional(),
109
110
  allLocations: z2.boolean().optional(),
110
111
  noLocation: z2.boolean().optional()
@@ -131,6 +132,7 @@ var runDtoSchema = z2.object({
131
132
  status: runStatusSchema,
132
133
  trigger: runTriggerSchema.default("manual"),
133
134
  location: z2.string().nullable().optional(),
135
+ queries: z2.array(z2.string()).nullable().optional(),
134
136
  startedAt: z2.string().nullable().optional(),
135
137
  finishedAt: z2.string().nullable().optional(),
136
138
  error: runErrorSchema.nullable().optional(),
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-A7HQ6X43.js";
8
+ } from "./chunk-2FAEQ56I.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -68,7 +68,7 @@ import {
68
68
  schedules,
69
69
  trafficSources,
70
70
  usageCounters
71
- } from "./chunk-IS65IYNZ.js";
71
+ } from "./chunk-PN24DAGC.js";
72
72
  import {
73
73
  AGENT_MEMORY_VALUE_MAX_BYTES,
74
74
  AGENT_PROVIDER_IDS,
@@ -169,7 +169,7 @@ import {
169
169
  visibilityStateFromAnswerMentioned,
170
170
  windowCutoff,
171
171
  wordpressEnvSchema
172
- } from "./chunk-CRQMGNPH.js";
172
+ } from "./chunk-HVW665A4.js";
173
173
 
174
174
  // src/telemetry.ts
175
175
  import crypto from "crypto";
@@ -1155,6 +1155,7 @@ function queueRunIfProjectIdle(db, params) {
1155
1155
  status: "queued",
1156
1156
  trigger,
1157
1157
  location: params.location ?? null,
1158
+ queries: params.queries ?? null,
1158
1159
  createdAt
1159
1160
  }).run();
1160
1161
  return { conflict: false, runId };
@@ -1185,6 +1186,20 @@ async function runRoutes(app, opts) {
1185
1186
  rawProviders.splice(0, rawProviders.length, ...normalized);
1186
1187
  }
1187
1188
  const providers = rawProviders?.length ? rawProviders : void 0;
1189
+ let scopedQueries = null;
1190
+ if (body.queries?.length) {
1191
+ const trackedRows = app.db.select({ query: queries.query }).from(queries).where(eq7(queries.projectId, project.id)).all();
1192
+ const tracked = new Set(trackedRows.map((r) => r.query));
1193
+ const missing = body.queries.filter((q) => !tracked.has(q));
1194
+ if (missing.length) {
1195
+ throw validationError(`Queries not tracked on project "${project.name}": ${missing.join(", ")}`, {
1196
+ missing,
1197
+ tracked: [...tracked]
1198
+ });
1199
+ }
1200
+ scopedQueries = body.queries;
1201
+ }
1202
+ const queriesColumn = scopedQueries ? JSON.stringify(scopedQueries) : null;
1188
1203
  let resolvedLocation;
1189
1204
  const projectLocations = parseJsonColumn(project.locations, []);
1190
1205
  if (body.noLocation) {
@@ -1217,6 +1232,7 @@ async function runRoutes(app, opts) {
1217
1232
  status: "queued",
1218
1233
  trigger,
1219
1234
  location: loc.label,
1235
+ queries: queriesColumn,
1220
1236
  createdAt: now
1221
1237
  }).run();
1222
1238
  newRuns.push({ runId: runId2, loc });
@@ -1244,7 +1260,8 @@ async function runRoutes(app, opts) {
1244
1260
  kind,
1245
1261
  projectId: project.id,
1246
1262
  trigger,
1247
- location: locationLabel
1263
+ location: locationLabel,
1264
+ queries: queriesColumn
1248
1265
  });
1249
1266
  if (queueResult.conflict) throw runInProgress(project.name);
1250
1267
  const runId = queueResult.runId;
@@ -1272,7 +1289,7 @@ async function runRoutes(app, opts) {
1272
1289
  const project = resolveProject(app.db, request.params.name);
1273
1290
  const countRow = app.db.select({ count: sql2`count(*)` }).from(runs).where(eq7(runs.projectId, project.id)).get();
1274
1291
  const totalRuns = countRow?.count ?? 0;
1275
- const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(1).get();
1292
+ const latestRun = app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt), desc(runs.id)).limit(1).get();
1276
1293
  if (!latestRun) {
1277
1294
  return reply.send({ totalRuns: 0, run: null });
1278
1295
  }
@@ -1390,6 +1407,7 @@ function formatRun(row) {
1390
1407
  status: row.status,
1391
1408
  trigger: row.trigger,
1392
1409
  location: row.location,
1410
+ queries: parseJsonColumn(row.queries, null),
1393
1411
  startedAt: row.startedAt,
1394
1412
  finishedAt: row.finishedAt,
1395
1413
  error: parseRunError(row.error),
@@ -2136,7 +2154,8 @@ async function historyRoutes(app) {
2136
2154
  transition,
2137
2155
  answerMentioned: snap.answerMentioned,
2138
2156
  visibilityState: snap.answerMentioned ? "visible" : "not-visible",
2139
- visibilityTransition
2157
+ visibilityTransition,
2158
+ location: snap.location
2140
2159
  };
2141
2160
  });
2142
2161
  }
@@ -8003,6 +8022,7 @@ var routeCatalog = [
8003
8022
  kind: stringSchema,
8004
8023
  trigger: stringSchema,
8005
8024
  providers: stringArraySchema,
8025
+ queries: stringArraySchema,
8006
8026
  location: stringSchema,
8007
8027
  allLocations: booleanSchema,
8008
8028
  noLocation: booleanSchema
@@ -10171,8 +10191,8 @@ var routeCatalog = [
10171
10191
  {
10172
10192
  method: "post",
10173
10193
  path: "/api/v1/projects/{name}/traffic/sources/{id}/backfill",
10174
- summary: "Reclassify historical Cloud Run logs for a traffic source",
10175
- description: 'Async one-shot backfill: pulls the last `days` of request logs (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it.',
10194
+ summary: "Reclassify historical traffic-source logs",
10195
+ description: 'Async one-shot backfill: pulls the last `days` of events (clamped server-side to the upstream retention ceiling \u2014 30d for Cloud Logging `_Default`; the WordPress plugin honours the same window via `since`/`until` query params), classifies them with the current rules, and replaces the hourly rollup buckets + sample slice in the window inside one transaction. Returns immediately with `{ runId, status: "running" }`; poll `GET /runs/{id}` for completion. lastSyncedAt only advances forward, so a backfill never undoes incremental sync progress that ran ahead of it. Supported source types: `cloud-run`, `wordpress`.',
10176
10196
  tags: ["traffic"],
10177
10197
  parameters: [
10178
10198
  nameParameter,
@@ -17411,6 +17431,7 @@ async function listWordpressTrafficEvents(options) {
17411
17431
  let cursor = options.cursor;
17412
17432
  let rawEntryCount = 0;
17413
17433
  let skippedEntryCount = 0;
17434
+ let hasMore = false;
17414
17435
  const events = [];
17415
17436
  for (let page = 0; page < maxPages; page += 1) {
17416
17437
  const url = new URL(endpoint);
@@ -17418,6 +17439,12 @@ async function listWordpressTrafficEvents(options) {
17418
17439
  if (cursor !== void 0 && cursor !== "") {
17419
17440
  url.searchParams.set("cursor", cursor);
17420
17441
  }
17442
+ if (options.since !== void 0 && options.since !== "") {
17443
+ url.searchParams.set("since", options.since);
17444
+ }
17445
+ if (options.until !== void 0 && options.until !== "") {
17446
+ url.searchParams.set("until", options.until);
17447
+ }
17421
17448
  const response = await fetch(url, {
17422
17449
  method: "GET",
17423
17450
  headers: {
@@ -17446,6 +17473,7 @@ async function listWordpressTrafficEvents(options) {
17446
17473
  }
17447
17474
  }
17448
17475
  cursor = body.next_cursor ?? void 0;
17476
+ hasMore = Boolean(body.has_more) && Boolean(cursor);
17449
17477
  if (!body.has_more || !cursor) break;
17450
17478
  }
17451
17479
  return {
@@ -17453,6 +17481,7 @@ async function listWordpressTrafficEvents(options) {
17453
17481
  rawEntryCount,
17454
17482
  skippedEntryCount,
17455
17483
  nextCursor: cursor,
17484
+ hasMore,
17456
17485
  endpoint
17457
17486
  };
17458
17487
  }
@@ -17462,6 +17491,8 @@ var DEFAULT_SYNC_WINDOW_MINUTES = 43200;
17462
17491
  var DEFAULT_PAGE_SIZE3 = 1e3;
17463
17492
  var DEFAULT_MAX_PAGES3 = 5;
17464
17493
  var DEFAULT_SAMPLE_LIMIT2 = 100;
17494
+ var DEFAULT_WP_PAGE_SIZE = 500;
17495
+ var DEFAULT_WP_MAX_PAGES = 20;
17465
17496
  var MAX_TRACKED_EVENT_IDS = 1e3;
17466
17497
  var DEFAULT_BACKFILL_DAYS = 30;
17467
17498
  var MAX_BACKFILL_DAYS = 30;
@@ -17503,14 +17534,10 @@ async function runBackfillTask(options) {
17503
17534
  runId,
17504
17535
  project,
17505
17536
  sourceRow,
17506
- gcpProjectId,
17507
- serviceName,
17508
- location,
17509
- credential,
17510
17537
  windowStart,
17511
17538
  windowEnd,
17512
- pullEvents,
17513
- resolveAccessToken: resolveAccessToken2
17539
+ pullForBackfill,
17540
+ pullErrorPrefix
17514
17541
  } = options;
17515
17542
  const markFailed = (msg) => {
17516
17543
  const failedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -17522,33 +17549,11 @@ async function runBackfillTask(options) {
17522
17549
  } catch {
17523
17550
  }
17524
17551
  };
17525
- let accessToken;
17552
+ let allEvents;
17526
17553
  try {
17527
- accessToken = await resolveAccessToken2(credential);
17554
+ allEvents = await pullForBackfill();
17528
17555
  } catch (e) {
17529
- markFailed(`Failed to resolve Cloud Run access token: ${e instanceof Error ? e.message : String(e)}`);
17530
- return;
17531
- }
17532
- const allEvents = [];
17533
- try {
17534
- const page = await pullEvents(accessToken, {
17535
- gcpProjectId,
17536
- serviceName,
17537
- location,
17538
- startTime: windowStart.toISOString(),
17539
- endTime: windowEnd.toISOString(),
17540
- pageSize: DEFAULT_PAGE_SIZE3,
17541
- maxPages: BACKFILL_MAX_PAGES,
17542
- // Backfill is intentionally `firstSync: false`. We don't want desc
17543
- // ordering — the in-memory rollup builder handles any order, and the
17544
- // ring-buffer reseed at the end takes the most-recent IDs from the
17545
- // dedupedEvents anyway.
17546
- firstSync: false,
17547
- orderBy: "timestamp asc"
17548
- });
17549
- allEvents.push(...page.events);
17550
- } catch (e) {
17551
- markFailed(`Cloud Run pull failed: ${e instanceof Error ? e.message : String(e)}`);
17556
+ markFailed(`${pullErrorPrefix}: ${e instanceof Error ? e.message : String(e)}`);
17552
17557
  return;
17553
17558
  }
17554
17559
  if (allEvents.length === 0) {
@@ -17838,33 +17843,12 @@ async function trafficRoutes(app, opts) {
17838
17843
  if (!sourceRow || sourceRow.projectId !== project.id) {
17839
17844
  throw notFound("Traffic source", request.params.id);
17840
17845
  }
17841
- if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"]) {
17846
+ if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
17842
17847
  throw validationError(
17843
- `Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run is supported in v1.`
17848
+ `Sync for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and wordpress are supported in v1.`
17844
17849
  );
17845
17850
  }
17846
- const credentialStore = opts.cloudRunCredentialStore;
17847
- if (!credentialStore) {
17848
- throw validationError("Cloud Run credential storage is not configured for this deployment");
17849
- }
17850
- const credential = credentialStore.getConnection(project.name);
17851
- if (!credential) {
17852
- throw validationError(
17853
- `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
17854
- );
17855
- }
17856
- const config = parseSourceConfig(sourceRow);
17857
- const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
17858
- const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
17859
- const location = config.location ?? credential.location ?? void 0;
17860
- const requestedMinutes = request.body?.sinceMinutes;
17861
- const windowMinutes = Number.isFinite(requestedMinutes) && requestedMinutes && requestedMinutes > 0 ? Math.floor(requestedMinutes) : syncWindowMinutes;
17862
17851
  const windowEnd = /* @__PURE__ */ new Date();
17863
- const requestedStartMs = windowEnd.getTime() - windowMinutes * 6e4;
17864
- const lastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
17865
- const windowStart = new Date(
17866
- Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
17867
- );
17868
17852
  const startedAt = windowEnd.toISOString();
17869
17853
  const syncStartedAtMs = windowEnd.getTime();
17870
17854
  const runId = crypto20.randomUUID();
@@ -17898,32 +17882,100 @@ async function trafficRoutes(app, opts) {
17898
17882
  } catch {
17899
17883
  }
17900
17884
  };
17901
- let accessToken;
17902
- try {
17903
- accessToken = await resolveAccessToken2(credential);
17904
- } catch (e) {
17905
- const msg = e instanceof Error ? e.message : String(e);
17906
- markFailed(msg, "PROVIDER_AUTH");
17907
- throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
17908
- }
17909
- const isFirstSync = !sourceRow.lastSyncedAt;
17910
- let allEvents = [];
17911
- try {
17912
- const page = await pullEvents(accessToken, {
17913
- gcpProjectId,
17914
- serviceName,
17915
- location,
17916
- startTime: windowStart.toISOString(),
17917
- endTime: windowEnd.toISOString(),
17918
- pageSize,
17919
- maxPages,
17920
- firstSync: isFirstSync
17921
- });
17922
- allEvents = page.events;
17923
- } catch (e) {
17924
- const msg = e instanceof Error ? e.message : String(e);
17925
- markFailed(msg, "PROVIDER_PULL");
17926
- throw providerError(`Cloud Run pull failed: ${msg}`);
17885
+ let windowStart;
17886
+ let allEvents;
17887
+ let nextCursor;
17888
+ let auditAction;
17889
+ if (sourceRow.sourceType === TrafficSourceTypes["cloud-run"]) {
17890
+ auditAction = "traffic.cloud-run.synced";
17891
+ const credentialStore = opts.cloudRunCredentialStore;
17892
+ if (!credentialStore) {
17893
+ throw validationError("Cloud Run credential storage is not configured for this deployment");
17894
+ }
17895
+ const credential = credentialStore.getConnection(project.name);
17896
+ if (!credential) {
17897
+ throw validationError(
17898
+ `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
17899
+ );
17900
+ }
17901
+ const config = parseSourceConfig(sourceRow);
17902
+ const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
17903
+ const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
17904
+ const location = config.location ?? credential.location ?? void 0;
17905
+ const requestedMinutes = request.body?.sinceMinutes;
17906
+ const windowMinutes = Number.isFinite(requestedMinutes) && requestedMinutes && requestedMinutes > 0 ? Math.floor(requestedMinutes) : syncWindowMinutes;
17907
+ const requestedStartMs = windowEnd.getTime() - windowMinutes * 6e4;
17908
+ const lastSyncedMs = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt).getTime() : Number.NEGATIVE_INFINITY;
17909
+ windowStart = new Date(
17910
+ Math.min(windowEnd.getTime(), Math.max(requestedStartMs, lastSyncedMs))
17911
+ );
17912
+ let accessToken;
17913
+ try {
17914
+ accessToken = await resolveAccessToken2(credential);
17915
+ } catch (e) {
17916
+ const msg = e instanceof Error ? e.message : String(e);
17917
+ markFailed(msg, "PROVIDER_AUTH");
17918
+ throw providerError(`Failed to resolve Cloud Run access token: ${msg}`);
17919
+ }
17920
+ const isFirstSync = !sourceRow.lastSyncedAt;
17921
+ try {
17922
+ const page = await pullEvents(accessToken, {
17923
+ gcpProjectId,
17924
+ serviceName,
17925
+ location,
17926
+ startTime: windowStart.toISOString(),
17927
+ endTime: windowEnd.toISOString(),
17928
+ pageSize,
17929
+ maxPages,
17930
+ firstSync: isFirstSync
17931
+ });
17932
+ allEvents = page.events;
17933
+ } catch (e) {
17934
+ const msg = e instanceof Error ? e.message : String(e);
17935
+ markFailed(msg, "PROVIDER_PULL");
17936
+ throw providerError(`Cloud Run pull failed: ${msg}`);
17937
+ }
17938
+ } else {
17939
+ auditAction = "traffic.wordpress.synced";
17940
+ const credentialStore = opts.wordpressTrafficCredentialStore;
17941
+ if (!credentialStore) {
17942
+ throw validationError("WordPress traffic credential storage is not configured for this deployment");
17943
+ }
17944
+ const credential = credentialStore.getConnection(project.name);
17945
+ if (!credential) {
17946
+ app.db.delete(runs).where(eq23(runs.id, runId)).run();
17947
+ throw validationError(
17948
+ `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
17949
+ );
17950
+ }
17951
+ windowStart = sourceRow.lastSyncedAt ? new Date(sourceRow.lastSyncedAt) : windowEnd;
17952
+ const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
17953
+ const wpMaxPages = opts.defaultWordpressMaxPages ?? DEFAULT_WP_MAX_PAGES;
17954
+ const collected = [];
17955
+ let cursor = sourceRow.lastCursor ?? void 0;
17956
+ try {
17957
+ for (let page = 0; page < wpMaxPages; page += 1) {
17958
+ const pageResult = await pullWordpressEvents({
17959
+ baseUrl: credential.baseUrl,
17960
+ username: credential.username,
17961
+ applicationPassword: credential.applicationPassword,
17962
+ cursor,
17963
+ pageSize: wpPageSize,
17964
+ maxPages: 1
17965
+ });
17966
+ collected.push(...pageResult.events);
17967
+ const previousCursor = cursor;
17968
+ cursor = pageResult.nextCursor;
17969
+ if (!pageResult.hasMore) break;
17970
+ if (!cursor || cursor === previousCursor) break;
17971
+ }
17972
+ allEvents = collected;
17973
+ nextCursor = cursor;
17974
+ } catch (e) {
17975
+ const msg = e instanceof Error ? e.message : String(e);
17976
+ markFailed(msg, "PROVIDER_PULL");
17977
+ throw providerError(`WordPress pull failed: ${msg}`);
17978
+ }
17927
17979
  }
17928
17980
  let crawlerBucketRows = 0;
17929
17981
  let aiReferralBucketRows = 0;
@@ -18050,7 +18102,7 @@ async function trafficRoutes(app, opts) {
18050
18102
  }).run();
18051
18103
  sampleRows += 1;
18052
18104
  }
18053
- tx.update(trafficSources).set({
18105
+ const sourceUpdate = {
18054
18106
  status: TrafficSourceStatuses.connected,
18055
18107
  // Advance to windowEnd, not finishedAt — events arriving at the
18056
18108
  // source between windowEnd and finishedAt aren't in this pull's
@@ -18060,13 +18112,17 @@ async function trafficRoutes(app, opts) {
18060
18112
  lastError: null,
18061
18113
  lastEventIds: JSON.stringify(nextEventIds),
18062
18114
  updatedAt: finishedAt
18063
- }).where(eq23(trafficSources.id, sourceRow.id)).run();
18115
+ };
18116
+ if (sourceRow.sourceType === TrafficSourceTypes.wordpress) {
18117
+ sourceUpdate.lastCursor = nextCursor ?? null;
18118
+ }
18119
+ tx.update(trafficSources).set(sourceUpdate).where(eq23(trafficSources.id, sourceRow.id)).run();
18064
18120
  tx.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq23(runs.id, runId)).run();
18065
18121
  });
18066
18122
  writeAuditLog(app.db, {
18067
18123
  projectId: project.id,
18068
18124
  actor: "api",
18069
- action: "traffic.cloud-run.synced",
18125
+ action: auditAction,
18070
18126
  entityType: "traffic_source",
18071
18127
  entityId: sourceRow.id
18072
18128
  });
@@ -18104,19 +18160,9 @@ async function trafficRoutes(app, opts) {
18104
18160
  if (!sourceRow || sourceRow.projectId !== project.id) {
18105
18161
  throw notFound("Traffic source", request.params.id);
18106
18162
  }
18107
- if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"]) {
18163
+ if (sourceRow.sourceType !== TrafficSourceTypes["cloud-run"] && sourceRow.sourceType !== TrafficSourceTypes.wordpress) {
18108
18164
  throw validationError(
18109
- `Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run is supported in v1.`
18110
- );
18111
- }
18112
- const credentialStore = opts.cloudRunCredentialStore;
18113
- if (!credentialStore) {
18114
- throw validationError("Cloud Run credential storage is not configured for this deployment");
18115
- }
18116
- const credential = credentialStore.getConnection(project.name);
18117
- if (!credential) {
18118
- throw validationError(
18119
- `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
18165
+ `Backfill for source type "${sourceRow.sourceType}" is not implemented yet \u2014 only cloud-run and wordpress are supported in v1.`
18120
18166
  );
18121
18167
  }
18122
18168
  const requestedDays = request.body?.days ?? DEFAULT_BACKFILL_DAYS;
@@ -18124,13 +18170,86 @@ async function trafficRoutes(app, opts) {
18124
18170
  throw validationError('"days" must be a positive integer');
18125
18171
  }
18126
18172
  const appliedDays = Math.min(requestedDays, MAX_BACKFILL_DAYS);
18127
- const config = parseSourceConfig(sourceRow);
18128
- const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
18129
- const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
18130
- const location = config.location ?? credential.location ?? void 0;
18131
18173
  const windowEnd = /* @__PURE__ */ new Date();
18132
18174
  const windowStart = new Date(windowEnd.getTime() - appliedDays * 864e5);
18133
18175
  windowStart.setUTCMinutes(0, 0, 0);
18176
+ let pullForBackfill;
18177
+ let pullErrorPrefix;
18178
+ if (sourceRow.sourceType === TrafficSourceTypes["cloud-run"]) {
18179
+ const credentialStore = opts.cloudRunCredentialStore;
18180
+ if (!credentialStore) {
18181
+ throw validationError("Cloud Run credential storage is not configured for this deployment");
18182
+ }
18183
+ const credential = credentialStore.getConnection(project.name);
18184
+ if (!credential) {
18185
+ throw validationError(
18186
+ `No Cloud Run credential found for project "${project.name}". Run "canonry traffic connect cloud-run" first.`
18187
+ );
18188
+ }
18189
+ const config = parseSourceConfig(sourceRow);
18190
+ const gcpProjectId = config.gcpProjectId ?? credential.gcpProjectId;
18191
+ const serviceName = config.serviceName ?? credential.serviceName ?? void 0;
18192
+ const location = config.location ?? credential.location ?? void 0;
18193
+ pullErrorPrefix = "Cloud Run pull failed";
18194
+ pullForBackfill = async () => {
18195
+ const accessToken = await resolveAccessToken2(credential);
18196
+ const page = await pullEvents(accessToken, {
18197
+ gcpProjectId,
18198
+ serviceName,
18199
+ location,
18200
+ startTime: windowStart.toISOString(),
18201
+ endTime: windowEnd.toISOString(),
18202
+ pageSize: DEFAULT_PAGE_SIZE3,
18203
+ maxPages: BACKFILL_MAX_PAGES,
18204
+ // Backfill is intentionally `firstSync: false`. We don't want desc
18205
+ // ordering — the in-memory rollup builder handles any order, and the
18206
+ // ring-buffer reseed at the end takes the most-recent IDs from the
18207
+ // dedupedEvents anyway.
18208
+ firstSync: false,
18209
+ orderBy: "timestamp asc"
18210
+ });
18211
+ return page.events;
18212
+ };
18213
+ } else {
18214
+ const credentialStore = opts.wordpressTrafficCredentialStore;
18215
+ if (!credentialStore) {
18216
+ throw validationError("WordPress traffic credential storage is not configured for this deployment");
18217
+ }
18218
+ const credential = credentialStore.getConnection(project.name);
18219
+ if (!credential) {
18220
+ throw validationError(
18221
+ `No WordPress credential found for project "${project.name}". Run "canonry traffic connect wordpress" first.`
18222
+ );
18223
+ }
18224
+ const wpPageSize = opts.defaultWordpressPageSize ?? DEFAULT_WP_PAGE_SIZE;
18225
+ pullErrorPrefix = "WordPress pull failed";
18226
+ pullForBackfill = async () => {
18227
+ const collected = [];
18228
+ const windowStartIso = windowStart.toISOString();
18229
+ const windowEndIso = windowEnd.toISOString();
18230
+ let cursor = void 0;
18231
+ for (let page = 0; page < BACKFILL_MAX_PAGES; page += 1) {
18232
+ const pageResult = await pullWordpressEvents({
18233
+ baseUrl: credential.baseUrl,
18234
+ username: credential.username,
18235
+ applicationPassword: credential.applicationPassword,
18236
+ cursor,
18237
+ pageSize: wpPageSize,
18238
+ // Each call fetches a single page; the for-loop drives
18239
+ // continuation. Matches the WP sync path's pattern.
18240
+ maxPages: 1,
18241
+ since: windowStartIso,
18242
+ until: windowEndIso
18243
+ });
18244
+ collected.push(...pageResult.events);
18245
+ const previousCursor = cursor;
18246
+ cursor = pageResult.nextCursor;
18247
+ if (!pageResult.hasMore) break;
18248
+ if (!cursor || cursor === previousCursor) break;
18249
+ }
18250
+ return collected;
18251
+ };
18252
+ }
18134
18253
  const startedAt = windowEnd.toISOString();
18135
18254
  const runId = crypto20.randomUUID();
18136
18255
  app.db.insert(runs).values({
@@ -18148,15 +18267,10 @@ async function trafficRoutes(app, opts) {
18148
18267
  runId,
18149
18268
  project,
18150
18269
  sourceRow,
18151
- gcpProjectId,
18152
- serviceName,
18153
- location,
18154
- credential,
18155
18270
  windowStart,
18156
18271
  windowEnd,
18157
- appliedDays,
18158
- pullEvents,
18159
- resolveAccessToken: resolveAccessToken2
18272
+ pullForBackfill,
18273
+ pullErrorPrefix
18160
18274
  }).catch(() => {
18161
18275
  });
18162
18276
  const response = {
@@ -22887,7 +23001,8 @@ var JobRunner = class {
22887
23001
  throw new Error("No providers configured. Add at least one provider API key.");
22888
23002
  }
22889
23003
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
22890
- projectQueries = this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
23004
+ const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
23005
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray7(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
22891
23006
  const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
22892
23007
  const competitorDomains = projectCompetitors.map((c) => c.domain);
22893
23008
  const allDomains = effectiveDomains({
@@ -23167,7 +23282,8 @@ var JobRunner = class {
23167
23282
  status: runs.status,
23168
23283
  finishedAt: runs.finishedAt,
23169
23284
  error: runs.error,
23170
- trigger: runs.trigger
23285
+ trigger: runs.trigger,
23286
+ queries: runs.queries
23171
23287
  }).from(runs).where(eq27(runs.id, runId)).get();
23172
23288
  }
23173
23289
  isRunCancelled(runId) {
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-CRQMGNPH.js";
11
+ } from "./chunk-HVW665A4.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -109,6 +109,7 @@ var runs = sqliteTable("runs", {
109
109
  status: text("status").notNull().default("queued"),
110
110
  trigger: text("trigger").notNull().default("manual"),
111
111
  location: text("location"),
112
+ queries: text("queries"),
112
113
  sourceId: text("source_id"),
113
114
  startedAt: text("started_at"),
114
115
  finishedAt: text("finished_at"),
@@ -1792,6 +1793,17 @@ var MIGRATION_VERSIONS = [
1792
1793
  `ALTER TABLE discovery_sessions ADD COLUMN run_id TEXT`,
1793
1794
  `CREATE INDEX IF NOT EXISTS idx_discovery_sessions_run ON discovery_sessions(run_id)`
1794
1795
  ]
1796
+ },
1797
+ {
1798
+ version: 57,
1799
+ name: "runs-scoped-queries",
1800
+ // Persists an optional subset of tracked queries to sweep on a per-run
1801
+ // basis. NULL = full sweep (the default and only behavior pre-v57); a JSON
1802
+ // array of query strings = scope. The job runner reads this to filter the
1803
+ // query fetch via `inArray`.
1804
+ statements: [
1805
+ `ALTER TABLE runs ADD COLUMN queries TEXT`
1806
+ ]
1795
1807
  }
1796
1808
  ];
1797
1809
  function isDuplicateColumnError(err) {