@canonry/canonry 7.1.1 → 7.3.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.
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -2
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +6 -4
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +47 -4
- package/assets/assets/{AuditHistoryPanel-CZWzgyrD.js → AuditHistoryPanel-DP92khTY.js} +1 -1
- package/assets/assets/{BacklinksPage-D23A-ahd.js → BacklinksPage-BI_9t1iI.js} +1 -1
- package/assets/assets/{HistoryPage-BKGVxmNz.js → HistoryPage-tDE6Nv0J.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-BSxn6_RB.js → MeasurementPropertyPage-Y2F334Qs.js} +1 -1
- package/assets/assets/ProjectPage-DW581TNr.js +8 -0
- package/assets/assets/{RunsPage-CeBBjLSE.js → RunsPage-Bl_c9-vA.js} +1 -1
- package/assets/assets/{SettingsPage-Jp5Yp4cz.js → SettingsPage-DQ5_Q2rc.js} +1 -1
- package/assets/assets/{SiteHealthSection-DzVegBeN.js → SiteHealthSection-DdZruU72.js} +3 -3
- package/assets/assets/{TrafficPage-B66AYwWk.js → TrafficPage-CROectSE.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CXtq1yES.js → TrafficSourceDetailPage-Bthvvwg5.js} +1 -1
- package/assets/assets/{extract-error-message-BVa0lOze.js → extract-error-message-DOLCGQMa.js} +1 -1
- package/assets/assets/index-CCOcEGdz.js +86 -0
- package/assets/assets/index-HWSQgVum.css +1 -0
- package/assets/assets/{react-sigma_core.esm.min-60drsJa6.js → react-sigma_core.esm.min-TWiq-VWU.js} +1 -1
- package/assets/assets/{v2-overview-adapter-CLUi2Lkv.js → v2-overview-adapter-CxmV639A.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-IAMECH2N.js → chunk-3JEUZ2K5.js} +3 -3
- package/dist/{chunk-2OQ6D26U.js → chunk-FWV6IIYY.js} +37 -12
- package/dist/{chunk-22R5E5CY.js → chunk-MQPBOQUH.js} +292 -1266
- package/dist/{chunk-U2S5ABOR.js → chunk-QND5LEAC.js} +2 -2
- package/dist/{chunk-MTSFSXCG.js → chunk-RVELNJAW.js} +29 -2
- package/dist/{chunk-VDX3LYKL.js → chunk-XODXE6PT.js} +3205 -2729
- package/dist/{chunk-YGHPT5B2.js → chunk-YQXQBFHT.js} +5936 -5414
- package/dist/cli.js +1137 -67
- package/dist/{demo-server-TIMYQLXS.js → demo-server-BXABH765.js} +6 -6
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-YCMRTVCI.js → intelligence-service-EIWYQYVG.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +13 -13
- package/assets/assets/ProjectPage-D9W8G_1m.js +0 -8
- package/assets/assets/index-CMe_-fAB.css +0 -1
- package/assets/assets/index-DuhS0WLM.js +0 -86
|
@@ -20,21 +20,20 @@ import {
|
|
|
20
20
|
saveConfig,
|
|
21
21
|
saveConfigPatch,
|
|
22
22
|
sentimentInstallReadiness
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-RVELNJAW.js";
|
|
24
24
|
import {
|
|
25
25
|
computeFactorAverages,
|
|
26
26
|
deleteSiteCrawlGraphLayout,
|
|
27
27
|
persistSiteCrawlGraphLayout,
|
|
28
28
|
prepareSiteCrawlGraphLayout,
|
|
29
29
|
recordedSharePct
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-QND5LEAC.js";
|
|
31
31
|
import {
|
|
32
32
|
CanonryMcpToolNames,
|
|
33
33
|
CliError,
|
|
34
34
|
PACKAGE_VERSION,
|
|
35
|
-
canonryMcpTools
|
|
36
|
-
|
|
37
|
-
} from "./chunk-2OQ6D26U.js";
|
|
35
|
+
canonryMcpTools
|
|
36
|
+
} from "./chunk-FWV6IIYY.js";
|
|
38
37
|
import {
|
|
39
38
|
CC_CACHE_DIR,
|
|
40
39
|
DUCKDB_SPEC,
|
|
@@ -59,12 +58,12 @@ import {
|
|
|
59
58
|
agentMemory,
|
|
60
59
|
agentSessions,
|
|
61
60
|
agentToolEvents,
|
|
62
|
-
aiUserFetchEventsHourly,
|
|
63
61
|
anyUsersExist,
|
|
64
62
|
apiKeys,
|
|
65
63
|
apiRoutes,
|
|
66
64
|
assessConversionTrackingIntegrity,
|
|
67
65
|
auditLog,
|
|
66
|
+
backfillProjectAnswerMentions,
|
|
68
67
|
backlinkDomains,
|
|
69
68
|
backlinkSummaries,
|
|
70
69
|
bingCoverageSnapshots,
|
|
@@ -74,16 +73,11 @@ import {
|
|
|
74
73
|
captureSimpleMeasurementDefinition,
|
|
75
74
|
ccReleasePaths,
|
|
76
75
|
ccReleaseSyncs,
|
|
77
|
-
classifyAiUserFetch,
|
|
78
|
-
classifyCrawler,
|
|
79
76
|
classifyReviewsError,
|
|
80
77
|
competitors,
|
|
81
78
|
computeCitedCompetitorDomains,
|
|
82
|
-
computeCompetitorOverlap,
|
|
83
79
|
countAttributes,
|
|
84
80
|
countPopulatedGroups,
|
|
85
|
-
crawlerEventsHourly,
|
|
86
|
-
createClient,
|
|
87
81
|
createCredentialChecker,
|
|
88
82
|
createFastifyLogger,
|
|
89
83
|
createLogger,
|
|
@@ -99,12 +93,9 @@ import {
|
|
|
99
93
|
exchangeCode,
|
|
100
94
|
executeDiscovery,
|
|
101
95
|
extractLegacyCredentials,
|
|
102
|
-
extractRecommendedCompetitors,
|
|
103
96
|
fetchDailyMetrics,
|
|
104
97
|
fetchSearchAnalytics,
|
|
105
98
|
formatStorefrontAddress,
|
|
106
|
-
gaAiReferrals,
|
|
107
|
-
gaTrafficSnapshots,
|
|
108
99
|
gbpAttributesSnapshots,
|
|
109
100
|
gbpDailyMetrics,
|
|
110
101
|
gbpKeywordImpressions,
|
|
@@ -158,7 +149,6 @@ import {
|
|
|
158
149
|
measurementRunCompleteness,
|
|
159
150
|
measurementRunSlotState,
|
|
160
151
|
measurementSlotKey,
|
|
161
|
-
migrate,
|
|
162
152
|
newerFullSweep,
|
|
163
153
|
nextRunFromCron,
|
|
164
154
|
nextRunFromSchedule,
|
|
@@ -175,7 +165,6 @@ import {
|
|
|
175
165
|
queryBacklinks,
|
|
176
166
|
querySnapshots,
|
|
177
167
|
queueRunIfProjectIdle,
|
|
178
|
-
rawEventSamples,
|
|
179
168
|
readInstalledVersion,
|
|
180
169
|
readNegativeReviewMaxStars,
|
|
181
170
|
recordSentimentCompletion,
|
|
@@ -216,7 +205,7 @@ import {
|
|
|
216
205
|
siteLivenessState,
|
|
217
206
|
toAlertView,
|
|
218
207
|
usageCounters
|
|
219
|
-
} from "./chunk-
|
|
208
|
+
} from "./chunk-XODXE6PT.js";
|
|
220
209
|
import {
|
|
221
210
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
222
211
|
AGENT_NONE,
|
|
@@ -257,7 +246,6 @@ import {
|
|
|
257
246
|
ProviderBatchStatuses,
|
|
258
247
|
ProviderBatchSubmitError,
|
|
259
248
|
ProviderDispatchModes,
|
|
260
|
-
ProviderNames,
|
|
261
249
|
RATIO_UNIT_META_KEY,
|
|
262
250
|
ROUTE_CAPABLE_CITED_URL_PROVIDERS,
|
|
263
251
|
RUN_FILL_PROVIDER_BREAKER,
|
|
@@ -275,10 +263,6 @@ import {
|
|
|
275
263
|
SentimentOutcomes,
|
|
276
264
|
SiteHealthLinkClassificationSources,
|
|
277
265
|
SnapshotProviderModes,
|
|
278
|
-
TrafficEventConfidences,
|
|
279
|
-
TrafficEventKinds,
|
|
280
|
-
TrafficEvidenceKinds,
|
|
281
|
-
TrafficSourceTypes,
|
|
282
266
|
UserRoles,
|
|
283
267
|
VERTEX_AI_SEARCH_PROXY_DOMAIN,
|
|
284
268
|
actionConfidenceLabel,
|
|
@@ -315,6 +299,7 @@ import {
|
|
|
315
299
|
competitorLabelFromDomain,
|
|
316
300
|
compileBrandAliases,
|
|
317
301
|
computeBacklinkSummaryMetrics,
|
|
302
|
+
computeCompetitorOverlap,
|
|
318
303
|
contentActionLabel,
|
|
319
304
|
contentBriefDtoSchema,
|
|
320
305
|
createTemplateLinkDetectionTally,
|
|
@@ -334,6 +319,7 @@ import {
|
|
|
334
319
|
embedClientConfigForRequest,
|
|
335
320
|
embedProjectTabsError,
|
|
336
321
|
extractProviderHttpStatus,
|
|
322
|
+
extractRecommendedCompetitors,
|
|
337
323
|
factorStatusFromScore,
|
|
338
324
|
filterCapturedCitedUrls,
|
|
339
325
|
forbidden,
|
|
@@ -376,6 +362,7 @@ import {
|
|
|
376
362
|
missingDependency,
|
|
377
363
|
nextScheduleUpdatedAt,
|
|
378
364
|
normalizeAgentSlug,
|
|
365
|
+
normalizeCompetitorAliases,
|
|
379
366
|
normalizeIdTokens,
|
|
380
367
|
normalizeMeasurementExecutionQueryText,
|
|
381
368
|
normalizeProjectDomain,
|
|
@@ -424,7 +411,7 @@ import {
|
|
|
424
411
|
visibilityReportRequestSchema,
|
|
425
412
|
winnabilityClassLabel,
|
|
426
413
|
withRetry
|
|
427
|
-
} from "./chunk-
|
|
414
|
+
} from "./chunk-YQXQBFHT.js";
|
|
428
415
|
|
|
429
416
|
// src/runtime-context.ts
|
|
430
417
|
function cliRuntimeContext(env = process.env, stdio = process) {
|
|
@@ -3554,7 +3541,7 @@ import crypto29 from "crypto";
|
|
|
3554
3541
|
import fs9 from "fs";
|
|
3555
3542
|
import path8 from "path";
|
|
3556
3543
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3557
|
-
import { and as
|
|
3544
|
+
import { and as and23, eq as eq29 } from "drizzle-orm";
|
|
3558
3545
|
|
|
3559
3546
|
// ../integration-typesafe/src/classifier.ts
|
|
3560
3547
|
import { createHash } from "crypto";
|
|
@@ -4048,7 +4035,7 @@ function isVertexConfig(config) {
|
|
|
4048
4035
|
function resolveModel(config) {
|
|
4049
4036
|
return config.model || DEFAULT_MODEL;
|
|
4050
4037
|
}
|
|
4051
|
-
function
|
|
4038
|
+
function createClient(config) {
|
|
4052
4039
|
const httpOptions = config.baseUrl ? { httpOptions: { baseUrl: config.baseUrl } } : {};
|
|
4053
4040
|
if (isVertexConfig(config)) {
|
|
4054
4041
|
return new GoogleGenAI({
|
|
@@ -4090,7 +4077,7 @@ async function healthcheck(config) {
|
|
|
4090
4077
|
if (!validation.ok) return validation;
|
|
4091
4078
|
try {
|
|
4092
4079
|
const model = resolveModel(config);
|
|
4093
|
-
const client =
|
|
4080
|
+
const client = createClient(config);
|
|
4094
4081
|
const result2 = await withRetry2(
|
|
4095
4082
|
() => client.models.generateContent({
|
|
4096
4083
|
model,
|
|
@@ -4127,7 +4114,7 @@ function buildTrackedQueryRequest(input) {
|
|
|
4127
4114
|
async function executeTrackedQuery(input) {
|
|
4128
4115
|
const model = resolveModel(input.config);
|
|
4129
4116
|
const request = buildTrackedQueryRequest(input).body;
|
|
4130
|
-
const client =
|
|
4117
|
+
const client = createClient(input.config);
|
|
4131
4118
|
let result2;
|
|
4132
4119
|
try {
|
|
4133
4120
|
result2 = await withRetry2(
|
|
@@ -4315,7 +4302,7 @@ function extractDomainFromUri(uri) {
|
|
|
4315
4302
|
}
|
|
4316
4303
|
async function generateText(prompt, config) {
|
|
4317
4304
|
const model = resolveModel(config);
|
|
4318
|
-
const client =
|
|
4305
|
+
const client = createClient(config);
|
|
4319
4306
|
const result2 = await withRetry2(
|
|
4320
4307
|
() => client.models.generateContent({
|
|
4321
4308
|
model,
|
|
@@ -4383,7 +4370,7 @@ function extractEmbeddingVectors(response, expectedLength) {
|
|
|
4383
4370
|
function listModels(config, signal) {
|
|
4384
4371
|
return withRetry(async () => {
|
|
4385
4372
|
signal.throwIfAborted();
|
|
4386
|
-
const client =
|
|
4373
|
+
const client = createClient(config);
|
|
4387
4374
|
const page = await client.models.list({ config: { pageSize: 100, queryBase: true, abortSignal: signal, httpOptions: { timeout: 2500 } } });
|
|
4388
4375
|
const models = [];
|
|
4389
4376
|
let seen = 0;
|
|
@@ -4553,7 +4540,7 @@ async function withRetry3(fn, options = {}) {
|
|
|
4553
4540
|
// ../provider-openai/src/normalize.ts
|
|
4554
4541
|
var DEFAULT_MODEL2 = "gpt-5.4";
|
|
4555
4542
|
var OPENAI_RETRIEVAL_CONTRACT = RetrievalContracts["search-required-v1"];
|
|
4556
|
-
function
|
|
4543
|
+
function createClient2(config) {
|
|
4557
4544
|
return new OpenAI({
|
|
4558
4545
|
apiKey: config.apiKey,
|
|
4559
4546
|
...config.baseUrl ? { baseURL: config.baseUrl } : {}
|
|
@@ -4574,7 +4561,7 @@ async function healthcheck2(config) {
|
|
|
4574
4561
|
const validation = validateConfig2(config);
|
|
4575
4562
|
if (!validation.ok) return validation;
|
|
4576
4563
|
try {
|
|
4577
|
-
const client =
|
|
4564
|
+
const client = createClient2(config);
|
|
4578
4565
|
const response = await withRetry3(
|
|
4579
4566
|
() => client.responses.create({
|
|
4580
4567
|
model: config.model ?? DEFAULT_MODEL2,
|
|
@@ -4622,7 +4609,7 @@ function buildTrackedQueryRequest2(input) {
|
|
|
4622
4609
|
async function executeTrackedQuery2(input) {
|
|
4623
4610
|
const model = input.config.model ?? DEFAULT_MODEL2;
|
|
4624
4611
|
const params = buildTrackedQueryRequest2(input).body;
|
|
4625
|
-
const client =
|
|
4612
|
+
const client = createClient2(input.config);
|
|
4626
4613
|
let response;
|
|
4627
4614
|
try {
|
|
4628
4615
|
response = await withRetry3(() => client.responses.create(params));
|
|
@@ -4812,7 +4799,7 @@ function extractDomainFromUri2(uri) {
|
|
|
4812
4799
|
}
|
|
4813
4800
|
async function generateText2(prompt, config) {
|
|
4814
4801
|
const model = config.model ?? DEFAULT_MODEL2;
|
|
4815
|
-
const client =
|
|
4802
|
+
const client = createClient2(config);
|
|
4816
4803
|
const response = await withRetry3(
|
|
4817
4804
|
() => client.responses.create({
|
|
4818
4805
|
model,
|
|
@@ -6556,7 +6543,7 @@ async function healthcheck5(config) {
|
|
|
6556
6543
|
if (!validation.ok) return validation;
|
|
6557
6544
|
const model = resolveModel3(config.model);
|
|
6558
6545
|
try {
|
|
6559
|
-
const client =
|
|
6546
|
+
const client = createClient3(config.apiKey);
|
|
6560
6547
|
const response = await withRetry6(
|
|
6561
6548
|
() => postAgent(client, { ...agentSelection(model), input: 'Say "ok"' })
|
|
6562
6549
|
);
|
|
@@ -6585,7 +6572,7 @@ function buildTrackedQueryRequest5(input) {
|
|
|
6585
6572
|
}
|
|
6586
6573
|
async function executeTrackedQuery5(input) {
|
|
6587
6574
|
const model = resolveModel3(input.config.model);
|
|
6588
|
-
const client =
|
|
6575
|
+
const client = createClient3(input.config.apiKey);
|
|
6589
6576
|
const { body } = buildTrackedQueryRequest5(input);
|
|
6590
6577
|
try {
|
|
6591
6578
|
const rawResponse = await withRetry6(() => postAgent(client, body));
|
|
@@ -6640,7 +6627,7 @@ function reparseStoredResult4(rawResponse) {
|
|
|
6640
6627
|
if (agent) return parseAgentResponse(agent);
|
|
6641
6628
|
return parseSonarResponse(rawResponse);
|
|
6642
6629
|
}
|
|
6643
|
-
function
|
|
6630
|
+
function createClient3(apiKey) {
|
|
6644
6631
|
return new OpenAI4({ apiKey, baseURL: BASE_URL });
|
|
6645
6632
|
}
|
|
6646
6633
|
function postAgent(client, body) {
|
|
@@ -6854,7 +6841,7 @@ function extractDomainFromUri4(uri) {
|
|
|
6854
6841
|
}
|
|
6855
6842
|
async function generateText5(prompt, config) {
|
|
6856
6843
|
const model = resolveModel3(config.model);
|
|
6857
|
-
const client =
|
|
6844
|
+
const client = createClient3(config.apiKey);
|
|
6858
6845
|
const response = await withRetry6(() => postAgent(client, { ...agentSelection(model), input: prompt }));
|
|
6859
6846
|
assertUsableResponse(response);
|
|
6860
6847
|
return extractAgentAnswerText(response);
|
|
@@ -6974,7 +6961,7 @@ var MUSE_SPARK_MODEL = /^muse-spark-[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
6974
6961
|
var MUSE_SELF_DOMAINS = [AI_ENGINE_DOMAINS.metaAi];
|
|
6975
6962
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
6976
6963
|
var RETRYABLE_RESPONSE_ERROR_CODES = /* @__PURE__ */ new Set(["rate_limit_exceeded", "server_error"]);
|
|
6977
|
-
function
|
|
6964
|
+
function createClient4(config) {
|
|
6978
6965
|
return new OpenAI5({
|
|
6979
6966
|
apiKey: config.apiKey,
|
|
6980
6967
|
baseURL: config.baseUrl || MUSE_BASE_URL,
|
|
@@ -7022,7 +7009,7 @@ function statusMessage(rawResponse) {
|
|
|
7022
7009
|
return `Meta Model API response status: ${status}${detail ? ` (${detail})` : ""}`;
|
|
7023
7010
|
}
|
|
7024
7011
|
function createResponse(config, body) {
|
|
7025
|
-
const client =
|
|
7012
|
+
const client = createClient4(config);
|
|
7026
7013
|
return retryRequest(async () => {
|
|
7027
7014
|
const rawResponse = responseToRecord5(await client.responses.create(body));
|
|
7028
7015
|
if (rawResponse.status !== "completed" && rawResponse.status !== "incomplete") {
|
|
@@ -7882,7 +7869,7 @@ function nonBlank(value) {
|
|
|
7882
7869
|
return trimmed ? trimmed : void 0;
|
|
7883
7870
|
}
|
|
7884
7871
|
function resolveBuildCommit(env = process.env) {
|
|
7885
|
-
const embedded = true ? "
|
|
7872
|
+
const embedded = true ? "254a7fb5e66f080e53bad263c955da797653b6b0" : void 0;
|
|
7886
7873
|
return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
|
|
7887
7874
|
}
|
|
7888
7875
|
function resolveInstanceIdentity(env = process.env) {
|
|
@@ -8304,13 +8291,17 @@ function runOutcome(inserted, providerErrors, planShortfall) {
|
|
|
8304
8291
|
error: someFailed ? serializeRunError(buildRunErrorFromMessages(providerErrors)) : null
|
|
8305
8292
|
};
|
|
8306
8293
|
}
|
|
8294
|
+
function competitorIdentityKey(rows) {
|
|
8295
|
+
return JSON.stringify(rows.map((row) => ({ domain: row.domain, aliases: normalizeCompetitorAliases(row.aliases) })).sort((left, right) => left.domain.localeCompare(right.domain)));
|
|
8296
|
+
}
|
|
8307
8297
|
function runRecordingContext(db, input) {
|
|
8308
|
-
const resolveCompetitors = createRunCompetitorResolver(db, input.
|
|
8298
|
+
const resolveCompetitors = createRunCompetitorResolver(db, input.competitors);
|
|
8309
8299
|
return {
|
|
8310
8300
|
runId: input.runId,
|
|
8311
8301
|
allDomains: effectiveDomains({ canonicalDomain: input.project.canonicalDomain, ownedDomains: input.project.ownedDomains }),
|
|
8312
8302
|
competitorsFor: (executionId) => resolveCompetitors(input.measurementPlanVersionId, executionId),
|
|
8313
|
-
allBrandNames: effectiveBrandNames({ displayName: input.project.displayName, aliases: input.project.aliases })
|
|
8303
|
+
allBrandNames: effectiveBrandNames({ displayName: input.project.displayName, aliases: input.project.aliases }),
|
|
8304
|
+
competitorIdentity: competitorIdentityKey(input.competitors)
|
|
8314
8305
|
};
|
|
8315
8306
|
}
|
|
8316
8307
|
function resolvePlanExecution(run, projectQueries) {
|
|
@@ -8510,6 +8501,7 @@ var JobRunner = class {
|
|
|
8510
8501
|
let planExecution = null;
|
|
8511
8502
|
let runTrigger;
|
|
8512
8503
|
let canonicalDomain;
|
|
8504
|
+
let recording;
|
|
8513
8505
|
const providerDispatchCounts = /* @__PURE__ */ new Map();
|
|
8514
8506
|
const providerReservations = /* @__PURE__ */ new Map();
|
|
8515
8507
|
const batchRowIds = [];
|
|
@@ -8580,13 +8572,14 @@ var JobRunner = class {
|
|
|
8580
8572
|
log2.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
8581
8573
|
const projectCompetitors = this.db.select().from(competitors).where(eq3(competitors.projectId, projectId)).all();
|
|
8582
8574
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
8583
|
-
|
|
8575
|
+
recording = runRecordingContext(this.db, {
|
|
8584
8576
|
runId,
|
|
8585
8577
|
measurementPlanVersionId: existingRun.measurementPlanVersionId,
|
|
8586
8578
|
project,
|
|
8587
|
-
|
|
8579
|
+
competitors: projectCompetitors
|
|
8588
8580
|
});
|
|
8589
8581
|
const { allDomains, allBrandNames } = recording;
|
|
8582
|
+
const { aliases: competitorAliases } = recording.competitorsFor(null);
|
|
8590
8583
|
const executionContext = {
|
|
8591
8584
|
providerCount: activeProviders.length,
|
|
8592
8585
|
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
@@ -8676,13 +8669,14 @@ var JobRunner = class {
|
|
|
8676
8669
|
allBrandNames,
|
|
8677
8670
|
allDomains
|
|
8678
8671
|
);
|
|
8679
|
-
const overlap = computeCompetitorOverlap(normalized, competitorDomains);
|
|
8672
|
+
const overlap = computeCompetitorOverlap(normalized, competitorDomains, competitorAliases);
|
|
8680
8673
|
const extractedCompetitors = extractRecommendedCompetitors(
|
|
8681
8674
|
normalized.answerText,
|
|
8682
8675
|
allDomains,
|
|
8683
8676
|
normalized.citedDomains,
|
|
8684
8677
|
competitorDomains,
|
|
8685
|
-
allBrandNames
|
|
8678
|
+
allBrandNames,
|
|
8679
|
+
competitorAliases
|
|
8686
8680
|
);
|
|
8687
8681
|
const answerLocation = runLocation && isSearchLocationIgnored(providerName, normalized.retrievalStatus) ? { location: null, requestedContext: runLocation, supportedContext: { status: "ignored" } } : { location: runLocation?.label ?? null };
|
|
8688
8682
|
let screenshotRelPath = null;
|
|
@@ -8809,12 +8803,15 @@ var JobRunner = class {
|
|
|
8809
8803
|
provider: adapter.name,
|
|
8810
8804
|
requestedModel: config.model ?? null
|
|
8811
8805
|
})),
|
|
8812
|
-
//
|
|
8813
|
-
//
|
|
8814
|
-
//
|
|
8806
|
+
// Freeze the exact identity we actually dispatched with (domain label
|
|
8807
|
+
// plus curated aliases) so later reporting never borrows renamed or
|
|
8808
|
+
// newly added competitors, or later alias edits, from live project
|
|
8809
|
+
// state. A competitor with no curated alias freezes exactly as it did
|
|
8810
|
+
// before aliases existed, so its definition checksum is unchanged.
|
|
8815
8811
|
competitors: projectCompetitors.map((competitor) => {
|
|
8816
8812
|
const label = competitorLabelFromDomain(competitor.domain);
|
|
8817
|
-
|
|
8813
|
+
const curated = normalizeCompetitorAliases(competitor.aliases).filter((alias) => alias.toLowerCase() !== label.toLowerCase());
|
|
8814
|
+
return { domain: competitor.domain, label, aliases: [label, ...curated] };
|
|
8818
8815
|
}),
|
|
8819
8816
|
queries: projectQueries.map((query) => ({
|
|
8820
8817
|
queryId: query.id,
|
|
@@ -8875,6 +8872,7 @@ var JobRunner = class {
|
|
|
8875
8872
|
}
|
|
8876
8873
|
}
|
|
8877
8874
|
providerCallEnd = Date.now();
|
|
8875
|
+
this.reconcileRunCompetitorFields(recording, projectId);
|
|
8878
8876
|
this.throwIfRunCancelled(runId);
|
|
8879
8877
|
if (planExecution) {
|
|
8880
8878
|
for (const [provider, units] of planExecution.unitsByProvider) {
|
|
@@ -8912,6 +8910,7 @@ var JobRunner = class {
|
|
|
8912
8910
|
});
|
|
8913
8911
|
if (!finalized && this.isRunCancelled(runId)) throw new RunCancelledError(runId);
|
|
8914
8912
|
} catch (err) {
|
|
8913
|
+
if (recording) this.reconcileRunCompetitorFields(recording, projectId);
|
|
8915
8914
|
const executionContext = {
|
|
8916
8915
|
providerCount: activeProviders.length,
|
|
8917
8916
|
providers: activeProviders.map((provider) => provider.adapter.name),
|
|
@@ -9552,6 +9551,8 @@ var JobRunner = class {
|
|
|
9552
9551
|
if (!(failure2 instanceof RunCancelledError)) throw failure2;
|
|
9553
9552
|
this.markBatchesCancelled([batch.id], "Cancelled because its run is no longer running.");
|
|
9554
9553
|
return { kind: "cancelled" };
|
|
9554
|
+
} finally {
|
|
9555
|
+
this.reconcileRunCompetitorFields(ctx, batch.projectId);
|
|
9555
9556
|
}
|
|
9556
9557
|
return this.completeBatchIngest(batch);
|
|
9557
9558
|
}
|
|
@@ -9723,12 +9724,13 @@ var JobRunner = class {
|
|
|
9723
9724
|
let filled = 0;
|
|
9724
9725
|
let fatal = null;
|
|
9725
9726
|
let superseded = false;
|
|
9727
|
+
let recording;
|
|
9726
9728
|
try {
|
|
9727
9729
|
const run = this.getRunState(runId);
|
|
9728
9730
|
if (!run) throw new Error(`Run ${runId} not found`);
|
|
9729
9731
|
if (run.status !== "partial") throw new Error(`Run ${runId} is ${run.status}; only a partial run can be filled`);
|
|
9730
9732
|
const runCreatedAt = this.db.select({ createdAt: runs.createdAt }).from(runs).where(eq3(runs.id, runId)).get().createdAt;
|
|
9731
|
-
|
|
9733
|
+
recording = this.buildRunRecordingContext(runId, projectId);
|
|
9732
9734
|
const projectQueries = this.db.select().from(queries).where(eq3(queries.projectId, projectId)).all();
|
|
9733
9735
|
const plan = resolvePlanExecution(run, projectQueries);
|
|
9734
9736
|
if (!plan) throw new Error(`Run ${runId} did not measure a published plan`);
|
|
@@ -9841,6 +9843,7 @@ var JobRunner = class {
|
|
|
9841
9843
|
fatal = describeError(err);
|
|
9842
9844
|
log2.error("fill.failed", { fillId, runId, error: fatal });
|
|
9843
9845
|
} finally {
|
|
9846
|
+
if (recording) this.reconcileRunCompetitorFields(recording, projectId);
|
|
9844
9847
|
this.flushProviderUsage(providerDispatchCounts, providerReservations);
|
|
9845
9848
|
}
|
|
9846
9849
|
const outcome = this.finalizeRunFill({ fillId, runId, filled, fatal, providerErrors, superseded });
|
|
@@ -10095,9 +10098,31 @@ var JobRunner = class {
|
|
|
10095
10098
|
runId,
|
|
10096
10099
|
measurementPlanVersionId: run.measurementPlanVersionId,
|
|
10097
10100
|
project,
|
|
10098
|
-
|
|
10101
|
+
competitors: projectCompetitors
|
|
10099
10102
|
});
|
|
10100
10103
|
}
|
|
10104
|
+
/**
|
|
10105
|
+
* Rescore a run's stored competitor columns when the project's competitors
|
|
10106
|
+
* changed after `recording` read them.
|
|
10107
|
+
*
|
|
10108
|
+
* Saving a competitor alias rescores the stored answers once
|
|
10109
|
+
* (`onCompetitorAliasesChanged`). A writer that read the names before the
|
|
10110
|
+
* edit keeps scoring with them, so an answer it stores afterwards would
|
|
10111
|
+
* disagree with an identical one the edit rescored. Each writer calls this
|
|
10112
|
+
* after its last write: when its names are no longer current, the whole run
|
|
10113
|
+
* gets the same competitor-only pass the edit ran. A failure is logged and
|
|
10114
|
+
* never fails the writer.
|
|
10115
|
+
*/
|
|
10116
|
+
reconcileRunCompetitorFields(recording, projectId) {
|
|
10117
|
+
try {
|
|
10118
|
+
const current = this.db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq3(competitors.projectId, projectId)).all();
|
|
10119
|
+
if (competitorIdentityKey(current) === recording.competitorIdentity) return;
|
|
10120
|
+
const result2 = backfillProjectAnswerMentions(this.db, projectId, { competitorFieldsOnly: true, runId: recording.runId });
|
|
10121
|
+
log2.info("run.competitor-fields-rescored", { runId: recording.runId, projectId, ...result2 });
|
|
10122
|
+
} catch (err) {
|
|
10123
|
+
log2.error("run.competitor-fields-rescore-failed", { runId: recording.runId, projectId, error: describeError(err) });
|
|
10124
|
+
}
|
|
10125
|
+
}
|
|
10101
10126
|
incrementUsage(scope, metric, count3) {
|
|
10102
10127
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10103
10128
|
const period = now.slice(0, 10);
|
|
@@ -13001,8 +13026,7 @@ function buildDefaultDeps(registry) {
|
|
|
13001
13026
|
cfg
|
|
13002
13027
|
);
|
|
13003
13028
|
const normalized = adapter.normalizeResult(raw);
|
|
13004
|
-
const
|
|
13005
|
-
const isCited = normalized.citedDomains.some((d) => canonical.has(d.toLowerCase()));
|
|
13029
|
+
const isCited = normalized.citedDomains.some((d) => hostMatchesAnyDomain(d, input.project.canonicalDomains));
|
|
13006
13030
|
const answerMentioned = determineAnswerMentioned(
|
|
13007
13031
|
normalized.answerText,
|
|
13008
13032
|
input.project.brandNames ?? [],
|
|
@@ -14214,1020 +14238,6 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
14214
14238
|
}
|
|
14215
14239
|
}
|
|
14216
14240
|
|
|
14217
|
-
// src/commands/backfill.ts
|
|
14218
|
-
import { setImmediate as yieldToEventLoop2 } from "timers/promises";
|
|
14219
|
-
import { and as and15, asc as asc4, desc as desc8, eq as eq17, gt as gt3, inArray as inArray7, isNull as isNull2, lte as lte2, sql as sql7 } from "drizzle-orm";
|
|
14220
|
-
var SNAPSHOT_BATCH_SIZE = 500;
|
|
14221
|
-
var PATH_REPAIR_PAGE_SIZE = 128;
|
|
14222
|
-
async function backfillAnswerVisibilityCommand(opts) {
|
|
14223
|
-
const config = loadConfig();
|
|
14224
|
-
const db = createClient(config.database);
|
|
14225
|
-
migrate(db);
|
|
14226
|
-
const projectFilter = opts?.project?.trim();
|
|
14227
|
-
const isDryRun = opts?.dryRun === true;
|
|
14228
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
14229
|
-
let examined = 0;
|
|
14230
|
-
let updated = 0;
|
|
14231
|
-
let wouldUpdate = 0;
|
|
14232
|
-
let mentioned = 0;
|
|
14233
|
-
let reparsed = 0;
|
|
14234
|
-
let providerErrors = 0;
|
|
14235
|
-
let retrievalRelabeled = 0;
|
|
14236
|
-
if (scopedProjects.length > 0) {
|
|
14237
|
-
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and15(
|
|
14238
|
-
eq17(runs.kind, RunKinds["answer-visibility"]),
|
|
14239
|
-
inArray7(runs.projectId, scopedProjects.map((project) => project.id))
|
|
14240
|
-
)).all() : db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(eq17(runs.kind, RunKinds["answer-visibility"])).all();
|
|
14241
|
-
const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
|
|
14242
|
-
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
14243
|
-
for (const run of runRows) {
|
|
14244
|
-
const existing = runIdsByProject.get(run.projectId);
|
|
14245
|
-
if (existing) existing.push(run.id);
|
|
14246
|
-
else runIdsByProject.set(run.projectId, [run.id]);
|
|
14247
|
-
}
|
|
14248
|
-
for (const project of scopedProjects) {
|
|
14249
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, project.id)).all().map((row) => row.domain);
|
|
14250
|
-
const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
|
|
14251
|
-
const runIds = runIdsByProject.get(project.id) ?? [];
|
|
14252
|
-
if (runIds.length === 0) continue;
|
|
14253
|
-
const projectDomains = effectiveDomains({
|
|
14254
|
-
canonicalDomain: project.canonicalDomain,
|
|
14255
|
-
ownedDomains: project.ownedDomains
|
|
14256
|
-
});
|
|
14257
|
-
const projectBrandNames = effectiveBrandNames({
|
|
14258
|
-
displayName: project.displayName,
|
|
14259
|
-
aliases: project.aliases
|
|
14260
|
-
});
|
|
14261
|
-
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
14262
|
-
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
14263
|
-
const snapshotRows = db.select({
|
|
14264
|
-
id: querySnapshots.id,
|
|
14265
|
-
runId: querySnapshots.runId,
|
|
14266
|
-
executionId: querySnapshots.measurementExecutionId,
|
|
14267
|
-
provider: querySnapshots.provider,
|
|
14268
|
-
citationState: querySnapshots.citationState,
|
|
14269
|
-
answerMentioned: querySnapshots.answerMentioned,
|
|
14270
|
-
answerText: querySnapshots.answerText,
|
|
14271
|
-
citedDomains: querySnapshots.citedDomains,
|
|
14272
|
-
competitorOverlap: querySnapshots.competitorOverlap,
|
|
14273
|
-
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
14274
|
-
rawResponse: querySnapshots.rawResponse,
|
|
14275
|
-
retrievalStatus: querySnapshots.retrievalStatus,
|
|
14276
|
-
retrievalContract: querySnapshots.retrievalContract
|
|
14277
|
-
}).from(querySnapshots).where(inArray7(querySnapshots.runId, batchRunIds)).all();
|
|
14278
|
-
const pendingUpdates = [];
|
|
14279
|
-
for (const snapshot of snapshotRows) {
|
|
14280
|
-
examined++;
|
|
14281
|
-
const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
|
|
14282
|
-
if (reparsedResult) reparsed++;
|
|
14283
|
-
if (reparsedResult?.providerError) providerErrors++;
|
|
14284
|
-
const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
|
|
14285
|
-
const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
14286
|
-
if (nextValue) mentioned++;
|
|
14287
|
-
const nextPatch = {};
|
|
14288
|
-
if (snapshot.answerMentioned !== nextValue) {
|
|
14289
|
-
nextPatch.answerMentioned = nextValue;
|
|
14290
|
-
}
|
|
14291
|
-
if ((snapshot.answerText ?? "") !== answerText) {
|
|
14292
|
-
nextPatch.answerText = answerText;
|
|
14293
|
-
}
|
|
14294
|
-
if (reparsedResult) {
|
|
14295
|
-
const normalized = {
|
|
14296
|
-
provider: snapshot.provider,
|
|
14297
|
-
answerText,
|
|
14298
|
-
citedDomains: reparsedResult.citedDomains,
|
|
14299
|
-
groundingSources: reparsedResult.groundingSources,
|
|
14300
|
-
searchQueries: reparsedResult.searchQueries,
|
|
14301
|
-
// Only feeds the citation/mention helpers below, which ignore
|
|
14302
|
-
// it. Retrieval is written back solely by the narrow OpenAI
|
|
14303
|
-
// contract correction after this block.
|
|
14304
|
-
retrievalStatus: "unknown"
|
|
14305
|
-
};
|
|
14306
|
-
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
14307
|
-
const nextCitedDomains = reparsedResult.citedDomains;
|
|
14308
|
-
const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId), snapshot.executionId);
|
|
14309
|
-
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
|
|
14310
|
-
const nextRecommendedCompetitors = extractRecommendedCompetitors(
|
|
14311
|
-
normalized.answerText,
|
|
14312
|
-
projectDomains,
|
|
14313
|
-
normalized.citedDomains,
|
|
14314
|
-
runCompetitors.domains,
|
|
14315
|
-
projectBrandNames,
|
|
14316
|
-
runCompetitors.aliases
|
|
14317
|
-
);
|
|
14318
|
-
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
14319
|
-
snapshot.rawResponse,
|
|
14320
|
-
reparsedResult
|
|
14321
|
-
);
|
|
14322
|
-
if (snapshot.citationState !== nextCitationState) {
|
|
14323
|
-
nextPatch.citationState = nextCitationState;
|
|
14324
|
-
}
|
|
14325
|
-
if (JSON.stringify(snapshot.citedDomains) !== JSON.stringify(nextCitedDomains)) {
|
|
14326
|
-
nextPatch.citedDomains = nextCitedDomains;
|
|
14327
|
-
}
|
|
14328
|
-
if (JSON.stringify(snapshot.competitorOverlap) !== JSON.stringify(nextCompetitorOverlap)) {
|
|
14329
|
-
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
14330
|
-
}
|
|
14331
|
-
if (JSON.stringify(snapshot.recommendedCompetitors) !== JSON.stringify(nextRecommendedCompetitors)) {
|
|
14332
|
-
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
14333
|
-
}
|
|
14334
|
-
if (snapshot.rawResponse !== nextRawResponse) {
|
|
14335
|
-
nextPatch.rawResponse = nextRawResponse;
|
|
14336
|
-
}
|
|
14337
|
-
}
|
|
14338
|
-
const retrievalCorrection = correctStoredOpenAIRetrieval(snapshot, reparsedResult);
|
|
14339
|
-
if (retrievalCorrection) {
|
|
14340
|
-
retrievalRelabeled++;
|
|
14341
|
-
Object.assign(nextPatch, retrievalCorrection);
|
|
14342
|
-
}
|
|
14343
|
-
if (Object.keys(nextPatch).length > 0) {
|
|
14344
|
-
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
14345
|
-
}
|
|
14346
|
-
}
|
|
14347
|
-
if (pendingUpdates.length > 0) {
|
|
14348
|
-
if (isDryRun) {
|
|
14349
|
-
wouldUpdate += pendingUpdates.length;
|
|
14350
|
-
} else {
|
|
14351
|
-
db.transaction((tx) => {
|
|
14352
|
-
for (const update of pendingUpdates) {
|
|
14353
|
-
tx.update(querySnapshots).set(update.patch).where(eq17(querySnapshots.id, update.id)).run();
|
|
14354
|
-
}
|
|
14355
|
-
});
|
|
14356
|
-
updated += pendingUpdates.length;
|
|
14357
|
-
}
|
|
14358
|
-
}
|
|
14359
|
-
}
|
|
14360
|
-
}
|
|
14361
|
-
}
|
|
14362
|
-
const result2 = {
|
|
14363
|
-
project: projectFilter ?? null,
|
|
14364
|
-
projects: scopedProjects.length,
|
|
14365
|
-
examined,
|
|
14366
|
-
updated,
|
|
14367
|
-
mentioned,
|
|
14368
|
-
reparsed,
|
|
14369
|
-
providerErrors,
|
|
14370
|
-
retrievalRelabeled
|
|
14371
|
-
};
|
|
14372
|
-
if (isDryRun) {
|
|
14373
|
-
result2.dryRun = true;
|
|
14374
|
-
result2.wouldUpdate = wouldUpdate;
|
|
14375
|
-
}
|
|
14376
|
-
if (isMachineFormat(opts?.format)) {
|
|
14377
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
14378
|
-
return;
|
|
14379
|
-
}
|
|
14380
|
-
console.log(`Answer visibility backfill ${isDryRun ? "preview" : "complete"}.
|
|
14381
|
-
`);
|
|
14382
|
-
if (projectFilter) {
|
|
14383
|
-
console.log(` Project: ${projectFilter}`);
|
|
14384
|
-
}
|
|
14385
|
-
console.log(` Projects: ${scopedProjects.length}`);
|
|
14386
|
-
console.log(` Examined: ${examined}`);
|
|
14387
|
-
if (isDryRun) {
|
|
14388
|
-
console.log(` Would update: ${wouldUpdate}`);
|
|
14389
|
-
} else {
|
|
14390
|
-
console.log(` Updated: ${updated}`);
|
|
14391
|
-
}
|
|
14392
|
-
console.log(` Mentioned: ${mentioned}`);
|
|
14393
|
-
console.log(` Reparsed: ${reparsed}`);
|
|
14394
|
-
console.log(` Errors: ${providerErrors}`);
|
|
14395
|
-
console.log(` Relabeled: ${retrievalRelabeled} (OpenAI retrieval contract)`);
|
|
14396
|
-
if (isDryRun) {
|
|
14397
|
-
console.log(`
|
|
14398
|
-
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
14399
|
-
}
|
|
14400
|
-
}
|
|
14401
|
-
function backfillNormalizedPaths(db, opts) {
|
|
14402
|
-
return repairStoredPaths(db, gaTrafficSnapshots, opts);
|
|
14403
|
-
}
|
|
14404
|
-
async function repairStoredPaths(db, table, opts) {
|
|
14405
|
-
const pageSize = opts?.pageSize ?? PATH_REPAIR_PAGE_SIZE;
|
|
14406
|
-
const projectScope = opts?.projectId ? sql7`+${table.projectId} = ${opts.projectId}` : void 0;
|
|
14407
|
-
const lastId = db.select({ id: table.id }).from(table).where(projectScope).orderBy(desc8(table.id)).limit(1).get()?.id;
|
|
14408
|
-
if (lastId === void 0) return { examined: 0, updated: 0, unchanged: 0 };
|
|
14409
|
-
let cursor;
|
|
14410
|
-
let examined = 0;
|
|
14411
|
-
let updated = 0;
|
|
14412
|
-
for (; ; ) {
|
|
14413
|
-
const rows = db.select({
|
|
14414
|
-
id: table.id,
|
|
14415
|
-
projectId: table.projectId,
|
|
14416
|
-
landingPage: table.landingPage,
|
|
14417
|
-
landingPageNormalized: table.landingPageNormalized
|
|
14418
|
-
}).from(table).where(and15(
|
|
14419
|
-
projectScope,
|
|
14420
|
-
lte2(table.id, lastId),
|
|
14421
|
-
cursor === void 0 ? void 0 : gt3(table.id, cursor)
|
|
14422
|
-
)).orderBy(asc4(table.id)).limit(pageSize).all();
|
|
14423
|
-
examined += rows.length;
|
|
14424
|
-
const changes = rows.flatMap((row) => {
|
|
14425
|
-
const next = normalizeUrlPath(row.landingPage);
|
|
14426
|
-
return next === null || row.landingPageNormalized === next ? [] : [{ row, next }];
|
|
14427
|
-
});
|
|
14428
|
-
if (changes.length > 0) {
|
|
14429
|
-
const written = db.transaction((tx) => {
|
|
14430
|
-
let written2 = 0;
|
|
14431
|
-
for (const { row, next } of changes) {
|
|
14432
|
-
written2 += tx.update(table).set({ landingPageNormalized: next }).where(and15(
|
|
14433
|
-
eq17(table.id, row.id),
|
|
14434
|
-
eq17(table.projectId, row.projectId),
|
|
14435
|
-
eq17(table.landingPage, row.landingPage),
|
|
14436
|
-
row.landingPageNormalized === null ? isNull2(table.landingPageNormalized) : eq17(table.landingPageNormalized, row.landingPageNormalized)
|
|
14437
|
-
)).run().changes;
|
|
14438
|
-
}
|
|
14439
|
-
return written2;
|
|
14440
|
-
});
|
|
14441
|
-
updated += written;
|
|
14442
|
-
if (written < changes.length) opts?.onConflict?.();
|
|
14443
|
-
}
|
|
14444
|
-
if (rows.length < pageSize) break;
|
|
14445
|
-
cursor = rows.at(-1).id;
|
|
14446
|
-
if (cursor === lastId) break;
|
|
14447
|
-
await yieldToEventLoop2();
|
|
14448
|
-
}
|
|
14449
|
-
return { examined, updated, unchanged: examined - updated };
|
|
14450
|
-
}
|
|
14451
|
-
async function backfillNormalizedPathsCommand(opts) {
|
|
14452
|
-
const config = loadConfig();
|
|
14453
|
-
const db = createClient(config.database);
|
|
14454
|
-
migrate(db);
|
|
14455
|
-
const projectFilter = opts?.project?.trim();
|
|
14456
|
-
let projectId;
|
|
14457
|
-
if (projectFilter) {
|
|
14458
|
-
const project = db.select({ id: projects.id }).from(projects).where(eq17(projects.name, projectFilter)).get();
|
|
14459
|
-
if (!project) {
|
|
14460
|
-
const result3 = {
|
|
14461
|
-
project: projectFilter,
|
|
14462
|
-
examined: 0,
|
|
14463
|
-
updated: 0,
|
|
14464
|
-
unchanged: 0
|
|
14465
|
-
};
|
|
14466
|
-
if (isMachineFormat(opts?.format)) {
|
|
14467
|
-
console.log(JSON.stringify(result3, null, 2));
|
|
14468
|
-
return;
|
|
14469
|
-
}
|
|
14470
|
-
console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
|
|
14471
|
-
return;
|
|
14472
|
-
}
|
|
14473
|
-
projectId = project.id;
|
|
14474
|
-
}
|
|
14475
|
-
const { examined, updated, unchanged } = await backfillNormalizedPaths(db, { projectId });
|
|
14476
|
-
const result2 = {
|
|
14477
|
-
project: projectFilter ?? null,
|
|
14478
|
-
examined,
|
|
14479
|
-
updated,
|
|
14480
|
-
unchanged
|
|
14481
|
-
};
|
|
14482
|
-
if (isMachineFormat(opts?.format)) {
|
|
14483
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
14484
|
-
return;
|
|
14485
|
-
}
|
|
14486
|
-
console.log("Normalized-path backfill complete.\n");
|
|
14487
|
-
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
14488
|
-
console.log(` Examined: ${examined}`);
|
|
14489
|
-
console.log(` Updated: ${updated}`);
|
|
14490
|
-
console.log(` Unchanged: ${unchanged}`);
|
|
14491
|
-
}
|
|
14492
|
-
function backfillAiReferralPaths(db, opts) {
|
|
14493
|
-
return repairStoredPaths(db, gaAiReferrals, opts);
|
|
14494
|
-
}
|
|
14495
|
-
async function backfillAiReferralPathsCommand(opts) {
|
|
14496
|
-
const config = loadConfig();
|
|
14497
|
-
const db = createClient(config.database);
|
|
14498
|
-
migrate(db);
|
|
14499
|
-
const projectFilter = opts?.project?.trim();
|
|
14500
|
-
let projectId;
|
|
14501
|
-
if (projectFilter) {
|
|
14502
|
-
const project = db.select({ id: projects.id }).from(projects).where(eq17(projects.name, projectFilter)).get();
|
|
14503
|
-
if (!project) {
|
|
14504
|
-
const result3 = {
|
|
14505
|
-
project: projectFilter,
|
|
14506
|
-
examined: 0,
|
|
14507
|
-
updated: 0,
|
|
14508
|
-
unchanged: 0
|
|
14509
|
-
};
|
|
14510
|
-
if (isMachineFormat(opts?.format)) {
|
|
14511
|
-
console.log(JSON.stringify(result3, null, 2));
|
|
14512
|
-
return;
|
|
14513
|
-
}
|
|
14514
|
-
console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
|
|
14515
|
-
return;
|
|
14516
|
-
}
|
|
14517
|
-
projectId = project.id;
|
|
14518
|
-
}
|
|
14519
|
-
const { examined, updated, unchanged } = await backfillAiReferralPaths(db, { projectId });
|
|
14520
|
-
const result2 = {
|
|
14521
|
-
project: projectFilter ?? null,
|
|
14522
|
-
examined,
|
|
14523
|
-
updated,
|
|
14524
|
-
unchanged
|
|
14525
|
-
};
|
|
14526
|
-
if (isMachineFormat(opts?.format)) {
|
|
14527
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
14528
|
-
return;
|
|
14529
|
-
}
|
|
14530
|
-
console.log("AI referral landing-page backfill complete.\n");
|
|
14531
|
-
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
14532
|
-
console.log(` Examined: ${examined}`);
|
|
14533
|
-
console.log(` Updated: ${updated}`);
|
|
14534
|
-
console.log(` Unchanged: ${unchanged}`);
|
|
14535
|
-
}
|
|
14536
|
-
function backfillProjectAnswerMentions(db, projectId, opts) {
|
|
14537
|
-
const isDryRun = opts?.dryRun === true;
|
|
14538
|
-
const project = db.select().from(projects).where(eq17(projects.id, projectId)).get();
|
|
14539
|
-
if (!project) return { examined: 0, updated: 0, mentioned: 0 };
|
|
14540
|
-
const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq17(competitors.projectId, projectId)).all().map((row) => row.domain);
|
|
14541
|
-
const runRows = db.select({ id: runs.id, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and15(eq17(runs.kind, RunKinds["answer-visibility"]), eq17(runs.projectId, projectId))).all();
|
|
14542
|
-
const runIds = runRows.map((r) => r.id);
|
|
14543
|
-
const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
|
|
14544
|
-
const competitorsForRun = createRunCompetitorResolver(db, competitorDomains);
|
|
14545
|
-
let examined = 0;
|
|
14546
|
-
let updated = 0;
|
|
14547
|
-
let wouldUpdate = 0;
|
|
14548
|
-
let mentioned = 0;
|
|
14549
|
-
if (runIds.length === 0) {
|
|
14550
|
-
return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
|
|
14551
|
-
}
|
|
14552
|
-
const projectDomains = effectiveDomains({
|
|
14553
|
-
canonicalDomain: project.canonicalDomain,
|
|
14554
|
-
ownedDomains: project.ownedDomains
|
|
14555
|
-
});
|
|
14556
|
-
const projectBrandNames = effectiveBrandNames({
|
|
14557
|
-
displayName: project.displayName,
|
|
14558
|
-
aliases: project.aliases
|
|
14559
|
-
});
|
|
14560
|
-
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
14561
|
-
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
14562
|
-
const snapshotRows = db.select({
|
|
14563
|
-
id: querySnapshots.id,
|
|
14564
|
-
runId: querySnapshots.runId,
|
|
14565
|
-
executionId: querySnapshots.measurementExecutionId,
|
|
14566
|
-
provider: querySnapshots.provider,
|
|
14567
|
-
answerMentioned: querySnapshots.answerMentioned,
|
|
14568
|
-
answerText: querySnapshots.answerText,
|
|
14569
|
-
citedDomains: querySnapshots.citedDomains,
|
|
14570
|
-
competitorOverlap: querySnapshots.competitorOverlap,
|
|
14571
|
-
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
14572
|
-
rawResponse: querySnapshots.rawResponse
|
|
14573
|
-
}).from(querySnapshots).where(inArray7(querySnapshots.runId, batchRunIds)).all();
|
|
14574
|
-
const pendingUpdates = [];
|
|
14575
|
-
for (const snapshot of snapshotRows) {
|
|
14576
|
-
examined++;
|
|
14577
|
-
const answerText = snapshot.answerText ?? "";
|
|
14578
|
-
const nextAnswerMentioned = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
14579
|
-
if (nextAnswerMentioned) mentioned++;
|
|
14580
|
-
const citedDomains = snapshot.citedDomains;
|
|
14581
|
-
const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
|
|
14582
|
-
const normalized = {
|
|
14583
|
-
provider: snapshot.provider,
|
|
14584
|
-
answerText,
|
|
14585
|
-
citedDomains,
|
|
14586
|
-
groundingSources,
|
|
14587
|
-
// Only feeds the competitor helpers, which ignore it. This pass never
|
|
14588
|
-
// observes or writes retrieval.
|
|
14589
|
-
retrievalStatus: "unknown",
|
|
14590
|
-
searchQueries: []
|
|
14591
|
-
};
|
|
14592
|
-
const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId), snapshot.executionId);
|
|
14593
|
-
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
|
|
14594
|
-
const nextRecommendedCompetitors = extractRecommendedCompetitors(
|
|
14595
|
-
answerText,
|
|
14596
|
-
projectDomains,
|
|
14597
|
-
citedDomains,
|
|
14598
|
-
runCompetitors.domains,
|
|
14599
|
-
projectBrandNames,
|
|
14600
|
-
runCompetitors.aliases
|
|
14601
|
-
);
|
|
14602
|
-
const nextPatch = {};
|
|
14603
|
-
if (snapshot.answerMentioned !== nextAnswerMentioned) {
|
|
14604
|
-
nextPatch.answerMentioned = nextAnswerMentioned;
|
|
14605
|
-
}
|
|
14606
|
-
if (JSON.stringify(snapshot.competitorOverlap) !== JSON.stringify(nextCompetitorOverlap)) {
|
|
14607
|
-
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
14608
|
-
}
|
|
14609
|
-
if (JSON.stringify(snapshot.recommendedCompetitors) !== JSON.stringify(nextRecommendedCompetitors)) {
|
|
14610
|
-
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
14611
|
-
}
|
|
14612
|
-
if (Object.keys(nextPatch).length > 0) {
|
|
14613
|
-
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
14614
|
-
}
|
|
14615
|
-
}
|
|
14616
|
-
if (pendingUpdates.length > 0) {
|
|
14617
|
-
if (isDryRun) {
|
|
14618
|
-
wouldUpdate += pendingUpdates.length;
|
|
14619
|
-
} else {
|
|
14620
|
-
db.transaction((tx) => {
|
|
14621
|
-
for (const update of pendingUpdates) {
|
|
14622
|
-
tx.update(querySnapshots).set(update.patch).where(eq17(querySnapshots.id, update.id)).run();
|
|
14623
|
-
}
|
|
14624
|
-
});
|
|
14625
|
-
updated += pendingUpdates.length;
|
|
14626
|
-
}
|
|
14627
|
-
}
|
|
14628
|
-
}
|
|
14629
|
-
return isDryRun ? { examined, updated, wouldUpdate, mentioned } : { examined, updated, mentioned };
|
|
14630
|
-
}
|
|
14631
|
-
async function backfillAnswerMentionsCommand(opts) {
|
|
14632
|
-
const config = loadConfig();
|
|
14633
|
-
const db = createClient(config.database);
|
|
14634
|
-
migrate(db);
|
|
14635
|
-
const projectFilter = opts?.project?.trim();
|
|
14636
|
-
const isDryRun = opts?.dryRun === true;
|
|
14637
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
14638
|
-
let examined = 0;
|
|
14639
|
-
let updated = 0;
|
|
14640
|
-
let wouldUpdate = 0;
|
|
14641
|
-
let mentioned = 0;
|
|
14642
|
-
for (const project of scopedProjects) {
|
|
14643
|
-
const result3 = backfillProjectAnswerMentions(db, project.id, { dryRun: isDryRun });
|
|
14644
|
-
examined += result3.examined;
|
|
14645
|
-
updated += result3.updated;
|
|
14646
|
-
wouldUpdate += result3.wouldUpdate ?? 0;
|
|
14647
|
-
mentioned += result3.mentioned;
|
|
14648
|
-
}
|
|
14649
|
-
const result2 = {
|
|
14650
|
-
project: projectFilter ?? null,
|
|
14651
|
-
projects: scopedProjects.length,
|
|
14652
|
-
examined,
|
|
14653
|
-
updated,
|
|
14654
|
-
mentioned
|
|
14655
|
-
};
|
|
14656
|
-
if (isDryRun) {
|
|
14657
|
-
result2.dryRun = true;
|
|
14658
|
-
result2.wouldUpdate = wouldUpdate;
|
|
14659
|
-
}
|
|
14660
|
-
if (isMachineFormat(opts?.format)) {
|
|
14661
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
14662
|
-
return;
|
|
14663
|
-
}
|
|
14664
|
-
console.log(`Answer mentions backfill ${isDryRun ? "preview" : "complete"}.
|
|
14665
|
-
`);
|
|
14666
|
-
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
14667
|
-
console.log(` Projects: ${scopedProjects.length}`);
|
|
14668
|
-
console.log(` Examined: ${examined}`);
|
|
14669
|
-
if (isDryRun) {
|
|
14670
|
-
console.log(` Would update: ${wouldUpdate}`);
|
|
14671
|
-
} else {
|
|
14672
|
-
console.log(` Updated: ${updated}`);
|
|
14673
|
-
}
|
|
14674
|
-
console.log(` Mentioned: ${mentioned}`);
|
|
14675
|
-
if (isDryRun) {
|
|
14676
|
-
console.log(`
|
|
14677
|
-
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
14678
|
-
}
|
|
14679
|
-
}
|
|
14680
|
-
function readStoredGroundingSources(rawResponse) {
|
|
14681
|
-
const envelope = parseJsonColumn(rawResponse, {});
|
|
14682
|
-
const sources = envelope.groundingSources;
|
|
14683
|
-
if (!Array.isArray(sources)) return [];
|
|
14684
|
-
const result2 = [];
|
|
14685
|
-
for (const source of sources) {
|
|
14686
|
-
if (source && typeof source === "object") {
|
|
14687
|
-
const uri = source.uri;
|
|
14688
|
-
const title = source.title;
|
|
14689
|
-
if (typeof uri === "string") {
|
|
14690
|
-
result2.push({ uri, title: typeof title === "string" ? title : "" });
|
|
14691
|
-
}
|
|
14692
|
-
}
|
|
14693
|
-
}
|
|
14694
|
-
return result2;
|
|
14695
|
-
}
|
|
14696
|
-
async function backfillInsightsCommand(project, opts) {
|
|
14697
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-YCMRTVCI.js");
|
|
14698
|
-
const config = loadConfig();
|
|
14699
|
-
const db = createClient(config.database);
|
|
14700
|
-
migrate(db);
|
|
14701
|
-
const service = new IntelligenceService2(db);
|
|
14702
|
-
const isJson = isMachineFormat(opts?.format);
|
|
14703
|
-
const isDryRun = opts?.dryRun === true;
|
|
14704
|
-
if (!isJson) {
|
|
14705
|
-
const scope = opts?.since ? ` (since ${opts.since})` : "";
|
|
14706
|
-
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
14707
|
-
process.stderr.write(`Backfilling insights for "${project}"${scope}${mode}...
|
|
14708
|
-
`);
|
|
14709
|
-
}
|
|
14710
|
-
const result2 = service.backfill(project, {
|
|
14711
|
-
fromRunId: opts?.fromRun,
|
|
14712
|
-
toRunId: opts?.toRun,
|
|
14713
|
-
since: opts?.since,
|
|
14714
|
-
dryRun: isDryRun
|
|
14715
|
-
}, (info) => {
|
|
14716
|
-
if (!isJson) {
|
|
14717
|
-
process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
|
|
14718
|
-
`);
|
|
14719
|
-
}
|
|
14720
|
-
});
|
|
14721
|
-
const output = {
|
|
14722
|
-
project,
|
|
14723
|
-
processed: result2.processed,
|
|
14724
|
-
skipped: result2.skipped,
|
|
14725
|
-
totalInsights: result2.totalInsights
|
|
14726
|
-
};
|
|
14727
|
-
if (result2.dryRun) {
|
|
14728
|
-
output.dryRun = true;
|
|
14729
|
-
output.delta = result2.delta;
|
|
14730
|
-
}
|
|
14731
|
-
if (isJson) {
|
|
14732
|
-
console.log(JSON.stringify(output, null, 2));
|
|
14733
|
-
return;
|
|
14734
|
-
}
|
|
14735
|
-
console.log(`
|
|
14736
|
-
Backfill ${isDryRun ? "preview" : "complete"}.`);
|
|
14737
|
-
console.log(` Processed: ${result2.processed}`);
|
|
14738
|
-
console.log(` Skipped: ${result2.skipped}`);
|
|
14739
|
-
console.log(` Insights: ${result2.totalInsights}`);
|
|
14740
|
-
if (result2.delta) {
|
|
14741
|
-
console.log(` Delta: -${result2.delta.wouldDelete} existing +${result2.delta.wouldCreate} new (net ${result2.delta.netChange >= 0 ? "+" : ""}${result2.delta.netChange})`);
|
|
14742
|
-
console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
|
|
14743
|
-
}
|
|
14744
|
-
}
|
|
14745
|
-
function replayQueryAuditLog(events) {
|
|
14746
|
-
const active = [];
|
|
14747
|
-
for (const ev of events) {
|
|
14748
|
-
let diff;
|
|
14749
|
-
try {
|
|
14750
|
-
diff = ev.diff ? JSON.parse(ev.diff) : {};
|
|
14751
|
-
} catch {
|
|
14752
|
-
continue;
|
|
14753
|
-
}
|
|
14754
|
-
if (ev.action === "keywords.appended" || ev.action === "queries.appended") {
|
|
14755
|
-
const added = Array.isArray(diff.added) ? diff.added : [];
|
|
14756
|
-
for (const q of added) {
|
|
14757
|
-
active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
|
|
14758
|
-
}
|
|
14759
|
-
} else if (ev.action === "keywords.deleted" || ev.action === "queries.deleted") {
|
|
14760
|
-
const deleted = Array.isArray(diff.deleted) ? diff.deleted : [];
|
|
14761
|
-
for (const q of deleted) {
|
|
14762
|
-
for (let i = active.length - 1; i >= 0; i--) {
|
|
14763
|
-
if (active[i].text === q && active[i].deletedAt === null) {
|
|
14764
|
-
active[i].deletedAt = ev.createdAt;
|
|
14765
|
-
break;
|
|
14766
|
-
}
|
|
14767
|
-
}
|
|
14768
|
-
}
|
|
14769
|
-
} else if (ev.action === "queries.replaced") {
|
|
14770
|
-
const newSet = Array.isArray(diff.queries) ? diff.queries : [];
|
|
14771
|
-
for (const e of active) {
|
|
14772
|
-
if (e.deletedAt === null) e.deletedAt = ev.createdAt;
|
|
14773
|
-
}
|
|
14774
|
-
for (const q of newSet) {
|
|
14775
|
-
active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
|
|
14776
|
-
}
|
|
14777
|
-
}
|
|
14778
|
-
}
|
|
14779
|
-
return active;
|
|
14780
|
-
}
|
|
14781
|
-
function activeQueriesAt(history, t) {
|
|
14782
|
-
return history.filter((e) => e.addedAt <= t && (e.deletedAt === null || e.deletedAt > t)).map((e) => e.text);
|
|
14783
|
-
}
|
|
14784
|
-
var CONTENT_MATCH_STOPWORDS = /* @__PURE__ */ new Set([
|
|
14785
|
-
"the",
|
|
14786
|
-
"a",
|
|
14787
|
-
"an",
|
|
14788
|
-
"and",
|
|
14789
|
-
"or",
|
|
14790
|
-
"but",
|
|
14791
|
-
"is",
|
|
14792
|
-
"are",
|
|
14793
|
-
"was",
|
|
14794
|
-
"were",
|
|
14795
|
-
"be",
|
|
14796
|
-
"been",
|
|
14797
|
-
"being",
|
|
14798
|
-
"have",
|
|
14799
|
-
"has",
|
|
14800
|
-
"had",
|
|
14801
|
-
"do",
|
|
14802
|
-
"does",
|
|
14803
|
-
"did",
|
|
14804
|
-
"will",
|
|
14805
|
-
"would",
|
|
14806
|
-
"could",
|
|
14807
|
-
"should",
|
|
14808
|
-
"may",
|
|
14809
|
-
"might",
|
|
14810
|
-
"can",
|
|
14811
|
-
"this",
|
|
14812
|
-
"that",
|
|
14813
|
-
"these",
|
|
14814
|
-
"those",
|
|
14815
|
-
"i",
|
|
14816
|
-
"you",
|
|
14817
|
-
"he",
|
|
14818
|
-
"she",
|
|
14819
|
-
"it",
|
|
14820
|
-
"we",
|
|
14821
|
-
"they",
|
|
14822
|
-
"what",
|
|
14823
|
-
"which",
|
|
14824
|
-
"who",
|
|
14825
|
-
"how",
|
|
14826
|
-
"why",
|
|
14827
|
-
"when",
|
|
14828
|
-
"where",
|
|
14829
|
-
"with",
|
|
14830
|
-
"for",
|
|
14831
|
-
"from",
|
|
14832
|
-
"of",
|
|
14833
|
-
"in",
|
|
14834
|
-
"on",
|
|
14835
|
-
"at",
|
|
14836
|
-
"to",
|
|
14837
|
-
"by",
|
|
14838
|
-
"as",
|
|
14839
|
-
"best",
|
|
14840
|
-
"most"
|
|
14841
|
-
]);
|
|
14842
|
-
function tokenizeForMatch(s) {
|
|
14843
|
-
return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !CONTENT_MATCH_STOPWORDS.has(t));
|
|
14844
|
-
}
|
|
14845
|
-
function contentMatchScore(queryText, answerText) {
|
|
14846
|
-
const queryTokens = [...new Set(tokenizeForMatch(queryText))];
|
|
14847
|
-
if (queryTokens.length === 0) return 0;
|
|
14848
|
-
const answerHead = answerText.slice(0, 300).toLowerCase();
|
|
14849
|
-
const hit = queryTokens.filter((t) => answerHead.includes(t)).length;
|
|
14850
|
-
return hit / queryTokens.length;
|
|
14851
|
-
}
|
|
14852
|
-
async function backfillSnapshotAttributionCommand(opts) {
|
|
14853
|
-
const config = loadConfig();
|
|
14854
|
-
const db = createClient(config.database);
|
|
14855
|
-
migrate(db);
|
|
14856
|
-
const project = db.select().from(projects).where(eq17(projects.name, opts.project)).get();
|
|
14857
|
-
if (!project) {
|
|
14858
|
-
throw new Error(`Project "${opts.project}" not found`);
|
|
14859
|
-
}
|
|
14860
|
-
const isJson = isMachineFormat(opts.format);
|
|
14861
|
-
const isDryRun = opts.dryRun === true;
|
|
14862
|
-
if (!isJson) {
|
|
14863
|
-
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
14864
|
-
process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
|
|
14865
|
-
`);
|
|
14866
|
-
}
|
|
14867
|
-
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and15(
|
|
14868
|
-
eq17(auditLog.projectId, project.id),
|
|
14869
|
-
inArray7(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
|
|
14870
|
-
)).orderBy(auditLog.createdAt).all();
|
|
14871
|
-
const history = replayQueryAuditLog(events);
|
|
14872
|
-
const orphanRuns = db.select({
|
|
14873
|
-
runId: runs.id,
|
|
14874
|
-
createdAt: runs.createdAt,
|
|
14875
|
-
location: runs.location
|
|
14876
|
-
}).from(runs).innerJoin(querySnapshots, eq17(querySnapshots.runId, runs.id)).where(and15(
|
|
14877
|
-
eq17(runs.projectId, project.id),
|
|
14878
|
-
isNull2(querySnapshots.queryId),
|
|
14879
|
-
isNull2(querySnapshots.queryText)
|
|
14880
|
-
)).groupBy(runs.id).orderBy(runs.createdAt).all();
|
|
14881
|
-
const result2 = {
|
|
14882
|
-
project: project.name,
|
|
14883
|
-
examinedRuns: orphanRuns.length,
|
|
14884
|
-
orphanSnapshots: 0,
|
|
14885
|
-
recoveredByPosition: 0,
|
|
14886
|
-
recoveredByContent: 0,
|
|
14887
|
-
unrecovered: 0,
|
|
14888
|
-
dryRun: isDryRun,
|
|
14889
|
-
perRun: []
|
|
14890
|
-
};
|
|
14891
|
-
const updates = [];
|
|
14892
|
-
for (const run of orphanRuns) {
|
|
14893
|
-
const activeAt = activeQueriesAt(history, run.createdAt);
|
|
14894
|
-
const orphanSnaps = db.select({
|
|
14895
|
-
id: querySnapshots.id,
|
|
14896
|
-
provider: querySnapshots.provider,
|
|
14897
|
-
createdAt: querySnapshots.createdAt,
|
|
14898
|
-
answerText: querySnapshots.answerText
|
|
14899
|
-
}).from(querySnapshots).where(and15(
|
|
14900
|
-
eq17(querySnapshots.runId, run.runId),
|
|
14901
|
-
isNull2(querySnapshots.queryId),
|
|
14902
|
-
isNull2(querySnapshots.queryText)
|
|
14903
|
-
)).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
|
|
14904
|
-
const byProvider = /* @__PURE__ */ new Map();
|
|
14905
|
-
for (const s of orphanSnaps) {
|
|
14906
|
-
const arr = byProvider.get(s.provider);
|
|
14907
|
-
if (arr) arr.push(s);
|
|
14908
|
-
else byProvider.set(s.provider, [s]);
|
|
14909
|
-
}
|
|
14910
|
-
let runPosition = 0;
|
|
14911
|
-
let runContent = 0;
|
|
14912
|
-
let runUnrecovered = 0;
|
|
14913
|
-
for (const [, snaps] of byProvider) {
|
|
14914
|
-
if (snaps.length === activeAt.length) {
|
|
14915
|
-
for (let i = 0; i < snaps.length; i++) {
|
|
14916
|
-
updates.push({ id: snaps[i].id, queryText: activeAt[i] });
|
|
14917
|
-
runPosition++;
|
|
14918
|
-
}
|
|
14919
|
-
} else if (snaps.length < activeAt.length) {
|
|
14920
|
-
const LOOKAHEAD_LIMIT = 5;
|
|
14921
|
-
let cidx = 0;
|
|
14922
|
-
for (const snap of snaps) {
|
|
14923
|
-
const answerText = snap.answerText ?? "";
|
|
14924
|
-
let matchedIdx = -1;
|
|
14925
|
-
for (let i = cidx; i < Math.min(activeAt.length, cidx + LOOKAHEAD_LIMIT); i++) {
|
|
14926
|
-
if (contentMatchScore(activeAt[i], answerText) >= 1) {
|
|
14927
|
-
matchedIdx = i;
|
|
14928
|
-
break;
|
|
14929
|
-
}
|
|
14930
|
-
}
|
|
14931
|
-
if (matchedIdx >= 0) {
|
|
14932
|
-
updates.push({ id: snap.id, queryText: activeAt[matchedIdx] });
|
|
14933
|
-
runContent++;
|
|
14934
|
-
cidx = matchedIdx + 1;
|
|
14935
|
-
} else {
|
|
14936
|
-
runUnrecovered++;
|
|
14937
|
-
}
|
|
14938
|
-
}
|
|
14939
|
-
} else {
|
|
14940
|
-
runUnrecovered += snaps.length;
|
|
14941
|
-
}
|
|
14942
|
-
}
|
|
14943
|
-
result2.orphanSnapshots += orphanSnaps.length;
|
|
14944
|
-
result2.recoveredByPosition += runPosition;
|
|
14945
|
-
result2.recoveredByContent += runContent;
|
|
14946
|
-
result2.unrecovered += runUnrecovered;
|
|
14947
|
-
result2.perRun.push({
|
|
14948
|
-
runId: run.runId,
|
|
14949
|
-
runCreatedAt: run.createdAt,
|
|
14950
|
-
activeQueryCount: activeAt.length,
|
|
14951
|
-
orphanSnaps: orphanSnaps.length,
|
|
14952
|
-
recoveredPosition: runPosition,
|
|
14953
|
-
recoveredContent: runContent,
|
|
14954
|
-
unrecovered: runUnrecovered
|
|
14955
|
-
});
|
|
14956
|
-
if (!isJson) {
|
|
14957
|
-
process.stderr.write(
|
|
14958
|
-
` ${run.createdAt} loc=${run.location ?? "-"} \u2192 ${orphanSnaps.length} orphan; ${runPosition} position-matched, ${runContent} content-matched, ${runUnrecovered} unrecovered
|
|
14959
|
-
`
|
|
14960
|
-
);
|
|
14961
|
-
}
|
|
14962
|
-
}
|
|
14963
|
-
if (!isDryRun && updates.length > 0) {
|
|
14964
|
-
db.transaction((tx) => {
|
|
14965
|
-
for (const u of updates) {
|
|
14966
|
-
tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq17(querySnapshots.id, u.id)).run();
|
|
14967
|
-
}
|
|
14968
|
-
});
|
|
14969
|
-
}
|
|
14970
|
-
if (isJson) {
|
|
14971
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
14972
|
-
return;
|
|
14973
|
-
}
|
|
14974
|
-
console.log(`
|
|
14975
|
-
Snapshot attribution ${isDryRun ? "preview" : "recovery"} complete.`);
|
|
14976
|
-
console.log(` Examined runs: ${result2.examinedRuns}`);
|
|
14977
|
-
console.log(` Orphan snapshots: ${result2.orphanSnapshots}`);
|
|
14978
|
-
console.log(` Position-matched: ${result2.recoveredByPosition}`);
|
|
14979
|
-
console.log(` Content-matched: ${result2.recoveredByContent}`);
|
|
14980
|
-
console.log(` Unrecovered: ${result2.unrecovered}`);
|
|
14981
|
-
if (isDryRun) {
|
|
14982
|
-
console.log(`
|
|
14983
|
-
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
14984
|
-
}
|
|
14985
|
-
}
|
|
14986
|
-
function correctStoredOpenAIRetrieval(snapshot, reparsed) {
|
|
14987
|
-
if (snapshot.provider !== ProviderNames.openai) return null;
|
|
14988
|
-
if (snapshot.retrievalContract !== RetrievalContracts["native-auto-v1"]) return null;
|
|
14989
|
-
return {
|
|
14990
|
-
retrievalContract: OPENAI_RETRIEVAL_CONTRACT,
|
|
14991
|
-
...reparsed?.retrievalStatus === void 0 ? {} : { retrievalStatus: reparsed.retrievalStatus }
|
|
14992
|
-
};
|
|
14993
|
-
}
|
|
14994
|
-
function reparseProviderSnapshot(provider, rawResponse) {
|
|
14995
|
-
const envelope = parseJsonColumn(rawResponse, {});
|
|
14996
|
-
const apiResponse = resolveStoredApiResponse(envelope);
|
|
14997
|
-
if (!apiResponse) return null;
|
|
14998
|
-
switch (provider) {
|
|
14999
|
-
case ProviderNames.openai:
|
|
15000
|
-
return reparseStoredResult2(apiResponse);
|
|
15001
|
-
case ProviderNames.claude:
|
|
15002
|
-
return reparseStoredResult3(apiResponse);
|
|
15003
|
-
case ProviderNames.gemini:
|
|
15004
|
-
return reparseStoredResult(apiResponse);
|
|
15005
|
-
case ProviderNames.perplexity:
|
|
15006
|
-
return reparseStoredResult4(apiResponse);
|
|
15007
|
-
case ProviderNames.muse:
|
|
15008
|
-
return reparseStoredResult5(apiResponse);
|
|
15009
|
-
default:
|
|
15010
|
-
return null;
|
|
15011
|
-
}
|
|
15012
|
-
}
|
|
15013
|
-
function resolveStoredApiResponse(parsed) {
|
|
15014
|
-
const nested = parsed.apiResponse;
|
|
15015
|
-
if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
15016
|
-
return nested;
|
|
15017
|
-
}
|
|
15018
|
-
if (looksLikeProviderApiResponse(parsed)) {
|
|
15019
|
-
return parsed;
|
|
15020
|
-
}
|
|
15021
|
-
return null;
|
|
15022
|
-
}
|
|
15023
|
-
function looksLikeProviderApiResponse(value) {
|
|
15024
|
-
return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
|
|
15025
|
-
}
|
|
15026
|
-
function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
|
|
15027
|
-
const parsed = parseJsonColumn(rawResponse, {});
|
|
15028
|
-
const apiResponse = resolveStoredApiResponse(parsed);
|
|
15029
|
-
const envelope = apiResponse === parsed ? {} : { ...parsed };
|
|
15030
|
-
delete envelope.answerText;
|
|
15031
|
-
delete envelope.citedDomains;
|
|
15032
|
-
delete envelope.competitorOverlap;
|
|
15033
|
-
delete envelope.recommendedCompetitors;
|
|
15034
|
-
delete envelope.providerError;
|
|
15035
|
-
return JSON.stringify({
|
|
15036
|
-
...envelope,
|
|
15037
|
-
groundingSources: reparsed.groundingSources,
|
|
15038
|
-
searchQueries: reparsed.searchQueries,
|
|
15039
|
-
...reparsed.providerError ? { providerError: reparsed.providerError } : {},
|
|
15040
|
-
...apiResponse ? { apiResponse } : {}
|
|
15041
|
-
});
|
|
15042
|
-
}
|
|
15043
|
-
async function backfillTrafficClassificationCommand(opts) {
|
|
15044
|
-
const config = loadConfig();
|
|
15045
|
-
const db = createClient(config.database);
|
|
15046
|
-
migrate(db);
|
|
15047
|
-
const projectFilter = opts?.project?.trim();
|
|
15048
|
-
const isDryRun = opts?.dryRun === true;
|
|
15049
|
-
const isJson = isMachineFormat(opts?.format);
|
|
15050
|
-
const scopedProjects = projectFilter ? db.select().from(projects).where(eq17(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
15051
|
-
if (scopedProjects.length === 0) {
|
|
15052
|
-
if (projectFilter && !isJson) {
|
|
15053
|
-
process.stderr.write(`No project named "${projectFilter}".
|
|
15054
|
-
`);
|
|
15055
|
-
}
|
|
15056
|
-
if (isJson) console.log(JSON.stringify({ project: projectFilter ?? null, examined: 0, reclassified: 0 }));
|
|
15057
|
-
return;
|
|
15058
|
-
}
|
|
15059
|
-
if (!isJson) {
|
|
15060
|
-
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
15061
|
-
const scope = projectFilter ? `"${projectFilter}"` : "all projects";
|
|
15062
|
-
process.stderr.write(`Re-classifying unknown traffic samples for ${scope}${mode}...
|
|
15063
|
-
`);
|
|
15064
|
-
}
|
|
15065
|
-
const projectIds = scopedProjects.map((p) => p.id);
|
|
15066
|
-
const result2 = {
|
|
15067
|
-
project: projectFilter ?? null,
|
|
15068
|
-
examined: 0,
|
|
15069
|
-
reclassified: 0,
|
|
15070
|
-
unknownBefore: 0,
|
|
15071
|
-
unknownAfter: 0,
|
|
15072
|
-
dryRun: isDryRun,
|
|
15073
|
-
byBot: {}
|
|
15074
|
-
};
|
|
15075
|
-
const unknownCountRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and15(
|
|
15076
|
-
eq17(rawEventSamples.eventType, "unknown"),
|
|
15077
|
-
inArray7(rawEventSamples.projectId, projectIds)
|
|
15078
|
-
)).get();
|
|
15079
|
-
result2.unknownBefore = Number(unknownCountRow?.n ?? 0);
|
|
15080
|
-
const unknownSamples = db.select({
|
|
15081
|
-
id: rawEventSamples.id,
|
|
15082
|
-
projectId: rawEventSamples.projectId,
|
|
15083
|
-
sourceId: rawEventSamples.sourceId,
|
|
15084
|
-
ts: rawEventSamples.ts,
|
|
15085
|
-
userAgent: rawEventSamples.userAgent,
|
|
15086
|
-
pathNormalized: rawEventSamples.pathNormalized,
|
|
15087
|
-
status: rawEventSamples.status
|
|
15088
|
-
}).from(rawEventSamples).where(and15(
|
|
15089
|
-
eq17(rawEventSamples.eventType, "unknown"),
|
|
15090
|
-
inArray7(rawEventSamples.projectId, projectIds)
|
|
15091
|
-
)).all();
|
|
15092
|
-
result2.examined = unknownSamples.length;
|
|
15093
|
-
if (unknownSamples.length === 0) {
|
|
15094
|
-
result2.unknownAfter = result2.unknownBefore;
|
|
15095
|
-
emitTrafficClassificationReport(result2, isJson);
|
|
15096
|
-
return;
|
|
15097
|
-
}
|
|
15098
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15099
|
-
for (const snap of unknownSamples) {
|
|
15100
|
-
if (!snap.userAgent) continue;
|
|
15101
|
-
const probe = {
|
|
15102
|
-
sourceType: TrafficSourceTypes["cloud-run"],
|
|
15103
|
-
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
15104
|
-
confidence: TrafficEventConfidences.observed,
|
|
15105
|
-
eventId: snap.id,
|
|
15106
|
-
observedAt: snap.ts,
|
|
15107
|
-
method: "GET",
|
|
15108
|
-
requestUrl: `https://placeholder${snap.pathNormalized}`,
|
|
15109
|
-
host: "placeholder",
|
|
15110
|
-
path: snap.pathNormalized,
|
|
15111
|
-
queryString: null,
|
|
15112
|
-
status: snap.status ?? 200,
|
|
15113
|
-
userAgent: snap.userAgent,
|
|
15114
|
-
remoteIp: null,
|
|
15115
|
-
referer: null,
|
|
15116
|
-
latencyMs: null,
|
|
15117
|
-
requestSizeBytes: null,
|
|
15118
|
-
responseSizeBytes: null,
|
|
15119
|
-
providerResource: { type: "cloud_run_revision", labels: {} },
|
|
15120
|
-
providerLabels: {}
|
|
15121
|
-
};
|
|
15122
|
-
const userFetch = classifyAiUserFetch(probe);
|
|
15123
|
-
const classified = userFetch ?? classifyCrawler(probe);
|
|
15124
|
-
if (!classified) continue;
|
|
15125
|
-
result2.reclassified++;
|
|
15126
|
-
result2.byBot[classified.botId] = (result2.byBot[classified.botId] ?? 0) + 1;
|
|
15127
|
-
if (isDryRun) continue;
|
|
15128
|
-
const tsHour = new Date(snap.ts);
|
|
15129
|
-
tsHour.setUTCMinutes(0, 0, 0);
|
|
15130
|
-
const tsHourIso = tsHour.toISOString();
|
|
15131
|
-
const status = snap.status ?? 200;
|
|
15132
|
-
db.transaction((tx) => {
|
|
15133
|
-
tx.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq17(rawEventSamples.id, snap.id)).run();
|
|
15134
|
-
if (userFetch) {
|
|
15135
|
-
tx.insert(aiUserFetchEventsHourly).values({
|
|
15136
|
-
projectId: snap.projectId,
|
|
15137
|
-
sourceId: snap.sourceId,
|
|
15138
|
-
tsHour: tsHourIso,
|
|
15139
|
-
botId: userFetch.botId,
|
|
15140
|
-
operator: userFetch.operator,
|
|
15141
|
-
verificationStatus: userFetch.verificationStatus,
|
|
15142
|
-
pathNormalized: snap.pathNormalized,
|
|
15143
|
-
status,
|
|
15144
|
-
hits: 1,
|
|
15145
|
-
sampledUserAgent: snap.userAgent,
|
|
15146
|
-
createdAt: now,
|
|
15147
|
-
updatedAt: now
|
|
15148
|
-
}).onConflictDoUpdate({
|
|
15149
|
-
target: [
|
|
15150
|
-
aiUserFetchEventsHourly.projectId,
|
|
15151
|
-
aiUserFetchEventsHourly.sourceId,
|
|
15152
|
-
aiUserFetchEventsHourly.tsHour,
|
|
15153
|
-
aiUserFetchEventsHourly.botId,
|
|
15154
|
-
aiUserFetchEventsHourly.verificationStatus,
|
|
15155
|
-
aiUserFetchEventsHourly.pathNormalized,
|
|
15156
|
-
aiUserFetchEventsHourly.status
|
|
15157
|
-
],
|
|
15158
|
-
set: {
|
|
15159
|
-
hits: sql7`${aiUserFetchEventsHourly.hits} + 1`,
|
|
15160
|
-
updatedAt: now
|
|
15161
|
-
}
|
|
15162
|
-
}).run();
|
|
15163
|
-
} else {
|
|
15164
|
-
tx.insert(crawlerEventsHourly).values({
|
|
15165
|
-
projectId: snap.projectId,
|
|
15166
|
-
sourceId: snap.sourceId,
|
|
15167
|
-
tsHour: tsHourIso,
|
|
15168
|
-
botId: classified.botId,
|
|
15169
|
-
operator: classified.operator,
|
|
15170
|
-
verificationStatus: classified.verificationStatus,
|
|
15171
|
-
pathNormalized: snap.pathNormalized,
|
|
15172
|
-
status,
|
|
15173
|
-
hits: 1,
|
|
15174
|
-
sampledUserAgent: snap.userAgent,
|
|
15175
|
-
createdAt: now,
|
|
15176
|
-
updatedAt: now
|
|
15177
|
-
}).onConflictDoUpdate({
|
|
15178
|
-
target: [
|
|
15179
|
-
crawlerEventsHourly.projectId,
|
|
15180
|
-
crawlerEventsHourly.sourceId,
|
|
15181
|
-
crawlerEventsHourly.tsHour,
|
|
15182
|
-
crawlerEventsHourly.botId,
|
|
15183
|
-
crawlerEventsHourly.verificationStatus,
|
|
15184
|
-
crawlerEventsHourly.pathNormalized,
|
|
15185
|
-
crawlerEventsHourly.status
|
|
15186
|
-
],
|
|
15187
|
-
set: {
|
|
15188
|
-
hits: sql7`${crawlerEventsHourly.hits} + 1`,
|
|
15189
|
-
updatedAt: now
|
|
15190
|
-
}
|
|
15191
|
-
}).run();
|
|
15192
|
-
}
|
|
15193
|
-
});
|
|
15194
|
-
}
|
|
15195
|
-
if (!isDryRun) {
|
|
15196
|
-
const afterRow = db.select({ n: sql7`count(*)` }).from(rawEventSamples).where(and15(
|
|
15197
|
-
eq17(rawEventSamples.eventType, "unknown"),
|
|
15198
|
-
inArray7(rawEventSamples.projectId, projectIds)
|
|
15199
|
-
)).get();
|
|
15200
|
-
result2.unknownAfter = Number(afterRow?.n ?? 0);
|
|
15201
|
-
} else {
|
|
15202
|
-
result2.unknownAfter = result2.unknownBefore - result2.reclassified;
|
|
15203
|
-
}
|
|
15204
|
-
emitTrafficClassificationReport(result2, isJson);
|
|
15205
|
-
}
|
|
15206
|
-
function emitTrafficClassificationReport(result2, isJson) {
|
|
15207
|
-
if (isJson) {
|
|
15208
|
-
console.log(JSON.stringify(result2, null, 2));
|
|
15209
|
-
return;
|
|
15210
|
-
}
|
|
15211
|
-
const verb = result2.dryRun ? "Would re-classify" : "Re-classified";
|
|
15212
|
-
console.log(`
|
|
15213
|
-
Traffic classification ${result2.dryRun ? "preview" : "recovery"} complete.`);
|
|
15214
|
-
console.log(` Examined unknowns: ${result2.examined}`);
|
|
15215
|
-
console.log(` ${verb}: ${result2.reclassified}`);
|
|
15216
|
-
console.log(` Unknown before: ${result2.unknownBefore}`);
|
|
15217
|
-
console.log(` Unknown after: ${result2.unknownAfter}`);
|
|
15218
|
-
if (result2.reclassified > 0) {
|
|
15219
|
-
console.log(` By bot:`);
|
|
15220
|
-
const sorted = Object.entries(result2.byBot).sort(([, a], [, b]) => b - a);
|
|
15221
|
-
for (const [bot, count3] of sorted) {
|
|
15222
|
-
console.log(` ${count3.toString().padStart(5)} ${bot}`);
|
|
15223
|
-
}
|
|
15224
|
-
}
|
|
15225
|
-
if (result2.dryRun) {
|
|
15226
|
-
console.log(`
|
|
15227
|
-
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
15228
|
-
}
|
|
15229
|
-
}
|
|
15230
|
-
|
|
15231
14241
|
// src/provider-registry.ts
|
|
15232
14242
|
var ProviderRegistry = class {
|
|
15233
14243
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -15343,7 +14353,7 @@ function createProviderModelCatalog(registry) {
|
|
|
15343
14353
|
// src/scheduler.ts
|
|
15344
14354
|
import crypto21 from "crypto";
|
|
15345
14355
|
import cron from "node-cron";
|
|
15346
|
-
import { and as
|
|
14356
|
+
import { and as and15, eq as eq17, inArray as inArray7, notExists, sql as sql7 } from "drizzle-orm";
|
|
15347
14357
|
var log15 = createLogger("Scheduler");
|
|
15348
14358
|
var DEFAULT_HEALTH_CRON = "0 */6 * * *";
|
|
15349
14359
|
function ensureDefaultHealthSchedule(db, projectId, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
@@ -15394,9 +14404,9 @@ var Scheduler = class {
|
|
|
15394
14404
|
*/
|
|
15395
14405
|
ensureHealthSchedules() {
|
|
15396
14406
|
const projectsWithoutHealth = this.db.select({ id: projects.id }).from(projects).where(notExists(
|
|
15397
|
-
this.db.select({ one:
|
|
15398
|
-
|
|
15399
|
-
|
|
14407
|
+
this.db.select({ one: sql7`1` }).from(schedules).where(and15(
|
|
14408
|
+
eq17(schedules.projectId, projects.id),
|
|
14409
|
+
eq17(schedules.kind, SchedulableRunKinds.doctor)
|
|
15400
14410
|
))
|
|
15401
14411
|
)).all();
|
|
15402
14412
|
if (projectsWithoutHealth.length === 0) return;
|
|
@@ -15439,7 +14449,7 @@ var Scheduler = class {
|
|
|
15439
14449
|
start() {
|
|
15440
14450
|
this.ensureHealthSchedules();
|
|
15441
14451
|
this.ensureQueryBaskets();
|
|
15442
|
-
const allSchedules = this.db.select().from(schedules).where(
|
|
14452
|
+
const allSchedules = this.db.select().from(schedules).where(eq17(schedules.enabled, true)).all();
|
|
15443
14453
|
for (const schedule of allSchedules) {
|
|
15444
14454
|
const missedRunAt = schedule.nextRunAt;
|
|
15445
14455
|
const recurrence = scheduleRecurrence(schedule);
|
|
@@ -15473,7 +14483,7 @@ var Scheduler = class {
|
|
|
15473
14483
|
this.stopTask(key, existing, "Stopped");
|
|
15474
14484
|
this.tasks.delete(key);
|
|
15475
14485
|
}
|
|
15476
|
-
const schedule = this.db.select().from(schedules).where(
|
|
14486
|
+
const schedule = this.db.select().from(schedules).where(and15(eq17(schedules.projectId, projectId), eq17(schedules.kind, kind))).get();
|
|
15477
14487
|
if (schedule && schedule.enabled) {
|
|
15478
14488
|
this.registerCronTask(schedule);
|
|
15479
14489
|
}
|
|
@@ -15501,12 +14511,12 @@ var Scheduler = class {
|
|
|
15501
14511
|
/** Runtime timing writes must not restore a version already issued to a settings caller. */
|
|
15502
14512
|
updateScheduleTiming(scheduleId, timing) {
|
|
15503
14513
|
this.db.transaction((tx) => {
|
|
15504
|
-
const current = tx.select({ updatedAt: schedules.updatedAt }).from(schedules).where(
|
|
14514
|
+
const current = tx.select({ updatedAt: schedules.updatedAt }).from(schedules).where(eq17(schedules.id, scheduleId)).get();
|
|
15505
14515
|
if (!current) return;
|
|
15506
14516
|
tx.update(schedules).set({
|
|
15507
14517
|
...timing,
|
|
15508
14518
|
updatedAt: nextScheduleUpdatedAt(current.updatedAt)
|
|
15509
|
-
}).where(
|
|
14519
|
+
}).where(eq17(schedules.id, scheduleId)).run();
|
|
15510
14520
|
});
|
|
15511
14521
|
}
|
|
15512
14522
|
registerCronTask(schedule, options = {}) {
|
|
@@ -15567,7 +14577,7 @@ var Scheduler = class {
|
|
|
15567
14577
|
};
|
|
15568
14578
|
const tick = () => {
|
|
15569
14579
|
if (stopped) return;
|
|
15570
|
-
const current = this.db.select().from(schedules).where(
|
|
14580
|
+
const current = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
|
|
15571
14581
|
if (!current || !current.enabled || !scheduleRecurrence(current)) {
|
|
15572
14582
|
this.remove(projectId, kind);
|
|
15573
14583
|
return;
|
|
@@ -15585,7 +14595,7 @@ var Scheduler = class {
|
|
|
15585
14595
|
this.triggerRun(scheduleId, projectId, kind);
|
|
15586
14596
|
}
|
|
15587
14597
|
}
|
|
15588
|
-
arm(this.db.select({ nextRunAt: schedules.nextRunAt }).from(schedules).where(
|
|
14598
|
+
arm(this.db.select({ nextRunAt: schedules.nextRunAt }).from(schedules).where(eq17(schedules.id, scheduleId)).get()?.nextRunAt);
|
|
15589
14599
|
};
|
|
15590
14600
|
this.tasks.set(key, task);
|
|
15591
14601
|
let registeredNextRunAt = schedule.nextRunAt;
|
|
@@ -15600,7 +14610,7 @@ var Scheduler = class {
|
|
|
15600
14610
|
/** Atomically advance a due callback-only calendar occurrence before dispatch. */
|
|
15601
14611
|
claimCalendarOccurrence(schedule, dueAt, now) {
|
|
15602
14612
|
return this.db.transaction((tx) => {
|
|
15603
|
-
const current = tx.select().from(schedules).where(
|
|
14613
|
+
const current = tx.select().from(schedules).where(eq17(schedules.id, schedule.id)).get();
|
|
15604
14614
|
if (!current || !current.enabled || current.updatedAt !== schedule.updatedAt || current.nextRunAt !== dueAt) return false;
|
|
15605
14615
|
const recurrence = scheduleRecurrence(current);
|
|
15606
14616
|
if (!recurrence) return false;
|
|
@@ -15609,11 +14619,11 @@ var Scheduler = class {
|
|
|
15609
14619
|
const result2 = tx.update(schedules).set({
|
|
15610
14620
|
nextRunAt,
|
|
15611
14621
|
updatedAt: nextScheduleUpdatedAt(current.updatedAt)
|
|
15612
|
-
}).where(
|
|
15613
|
-
|
|
15614
|
-
|
|
15615
|
-
|
|
15616
|
-
|
|
14622
|
+
}).where(and15(
|
|
14623
|
+
eq17(schedules.id, current.id),
|
|
14624
|
+
eq17(schedules.enabled, true),
|
|
14625
|
+
eq17(schedules.nextRunAt, dueAt),
|
|
14626
|
+
eq17(schedules.updatedAt, schedule.updatedAt)
|
|
15617
14627
|
)).run();
|
|
15618
14628
|
return result2.changes === 1;
|
|
15619
14629
|
});
|
|
@@ -15621,7 +14631,7 @@ var Scheduler = class {
|
|
|
15621
14631
|
triggerRun(scheduleId, projectId, kind, claimedOccurrence) {
|
|
15622
14632
|
try {
|
|
15623
14633
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15624
|
-
const currentSchedule = this.db.select().from(schedules).where(
|
|
14634
|
+
const currentSchedule = this.db.select().from(schedules).where(eq17(schedules.id, scheduleId)).get();
|
|
15625
14635
|
if (!currentSchedule || !currentSchedule.enabled) {
|
|
15626
14636
|
log15.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
|
|
15627
14637
|
this.remove(projectId, kind);
|
|
@@ -15637,7 +14647,7 @@ var Scheduler = class {
|
|
|
15637
14647
|
timezone: currentSchedule.timezone,
|
|
15638
14648
|
recurrence
|
|
15639
14649
|
});
|
|
15640
|
-
const project = this.db.select().from(projects).where(
|
|
14650
|
+
const project = this.db.select().from(projects).where(eq17(projects.id, projectId)).get();
|
|
15641
14651
|
if (!project) {
|
|
15642
14652
|
log15.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
|
|
15643
14653
|
this.remove(projectId, kind);
|
|
@@ -15688,10 +14698,10 @@ var Scheduler = class {
|
|
|
15688
14698
|
log15.warn("ads-sync.no-callback", { scheduleId, projectId, msg: "host did not register onAdsSyncRequested" });
|
|
15689
14699
|
return;
|
|
15690
14700
|
}
|
|
15691
|
-
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(
|
|
15692
|
-
|
|
15693
|
-
|
|
15694
|
-
|
|
14701
|
+
const activeAdsRun = this.db.select({ id: runs.id }).from(runs).where(and15(
|
|
14702
|
+
eq17(runs.projectId, projectId),
|
|
14703
|
+
eq17(runs.kind, RunKinds["ads-sync"]),
|
|
14704
|
+
inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
15695
14705
|
)).get();
|
|
15696
14706
|
if (activeAdsRun) {
|
|
15697
14707
|
log15.info("ads-sync.skipped-active", { projectName: project.name, activeRunId: activeAdsRun.id });
|
|
@@ -15759,10 +14769,10 @@ var Scheduler = class {
|
|
|
15759
14769
|
log15.warn("site-audit.no-callback", { scheduleId, projectId, msg: "host did not register onSiteAuditRequested" });
|
|
15760
14770
|
return;
|
|
15761
14771
|
}
|
|
15762
|
-
const active = this.db.select({ id: runs.id }).from(runs).where(
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
14772
|
+
const active = this.db.select({ id: runs.id }).from(runs).where(and15(
|
|
14773
|
+
eq17(runs.projectId, projectId),
|
|
14774
|
+
eq17(runs.kind, RunKinds["site-audit"]),
|
|
14775
|
+
inArray7(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
15766
14776
|
)).get();
|
|
15767
14777
|
if (active) {
|
|
15768
14778
|
log15.info("site-audit.skipped-active", { projectName: project.name, activeRunId: active.id });
|
|
@@ -15912,7 +14922,7 @@ function startSiteLivenessLoop(deps, intervalMs = SITE_LIVENESS_INTERVAL_MS) {
|
|
|
15912
14922
|
}
|
|
15913
14923
|
|
|
15914
14924
|
// src/provider-batch-poller.ts
|
|
15915
|
-
import { and as
|
|
14925
|
+
import { and as and16, eq as eq18, inArray as inArray8, isNotNull as isNotNull2, isNull as isNull2, notExists as notExists2, sql as sql8 } from "drizzle-orm";
|
|
15916
14926
|
var log17 = createLogger("ProviderBatchPoller");
|
|
15917
14927
|
var PROVIDER_BATCH_POLL_INITIAL_MS = 3e4;
|
|
15918
14928
|
var PROVIDER_BATCH_POLL_MAX_MS = 10 * 6e4;
|
|
@@ -15929,7 +14939,7 @@ var ProviderBatchPoller = class {
|
|
|
15929
14939
|
/** One pass: advance every outstanding batch that is due, then finalize what that settled. */
|
|
15930
14940
|
async tick() {
|
|
15931
14941
|
const { db } = this.deps;
|
|
15932
|
-
const rows = db.select().from(providerBatches).where(
|
|
14942
|
+
const rows = db.select().from(providerBatches).where(inArray8(providerBatches.status, [...OUTSTANDING_PROVIDER_BATCH_STATUSES])).all();
|
|
15933
14943
|
const live = new Set(rows.map((row) => row.id));
|
|
15934
14944
|
for (const id of this.schedule.keys()) if (!live.has(id)) this.schedule.delete(id);
|
|
15935
14945
|
const settledRuns = /* @__PURE__ */ new Map();
|
|
@@ -15955,7 +14965,7 @@ var ProviderBatchPoller = class {
|
|
|
15955
14965
|
* outstanding (ingested or cancelled), so its run may now finalize.
|
|
15956
14966
|
*/
|
|
15957
14967
|
async advance(row) {
|
|
15958
|
-
const runStatus = this.deps.db.select({ status: runs.status }).from(runs).where(
|
|
14968
|
+
const runStatus = this.deps.db.select({ status: runs.status }).from(runs).where(eq18(runs.id, row.runId)).get()?.status;
|
|
15959
14969
|
if (runStatus !== RunStatuses.running) {
|
|
15960
14970
|
await this.deps.runner.abandonProviderBatches([row.id], "Cancelled because its run is no longer running.");
|
|
15961
14971
|
this.schedule.delete(row.id);
|
|
@@ -15972,7 +14982,7 @@ var ProviderBatchPoller = class {
|
|
|
15972
14982
|
status = poll.status;
|
|
15973
14983
|
if (poll.status === "ended") {
|
|
15974
14984
|
const endedAt = poll.endedAt ?? new Date(this.now()).toISOString();
|
|
15975
|
-
const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(
|
|
14985
|
+
const ended = this.deps.db.update(providerBatches).set({ status: ProviderBatchStatuses.ended, endedAt, resultsExpireAt: poll.resultsExpireAt ?? null, updatedAt: endedAt }).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
|
|
15976
14986
|
this.schedule.delete(row.id);
|
|
15977
14987
|
return ended ? this.ingest({ ...row, status: ProviderBatchStatuses.ended, resultsExpireAt: poll.resultsExpireAt ?? null }) : false;
|
|
15978
14988
|
}
|
|
@@ -15996,10 +15006,10 @@ var ProviderBatchPoller = class {
|
|
|
15996
15006
|
const at = new Date(now).toISOString();
|
|
15997
15007
|
if (row.cancelRequestedAt === null) {
|
|
15998
15008
|
if (now < Date.parse(row.deadlineAt)) return false;
|
|
15999
|
-
const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
15009
|
+
const claimed = this.deps.db.update(providerBatches).set({ cancelRequestedAt: at, updatedAt: at }).where(and16(
|
|
15010
|
+
eq18(providerBatches.id, row.id),
|
|
15011
|
+
eq18(providerBatches.status, ProviderBatchStatuses.submitted),
|
|
15012
|
+
isNull2(providerBatches.cancelRequestedAt)
|
|
16003
15013
|
)).run().changes === 1;
|
|
16004
15014
|
if (!claimed) return false;
|
|
16005
15015
|
log17.warn("batch.deadline-passed", { batchId: row.id, runId: row.runId, provider: row.provider, deadlineAt: row.deadlineAt });
|
|
@@ -16017,7 +15027,7 @@ var ProviderBatchPoller = class {
|
|
|
16017
15027
|
status: ProviderBatchStatuses.cancelled,
|
|
16018
15028
|
error: `The provider batch did not end within an hour of being cancelled at its deadline; its ${row.requestCount} answer(s) were not recorded.`,
|
|
16019
15029
|
updatedAt: at
|
|
16020
|
-
}).where(
|
|
15030
|
+
}).where(and16(eq18(providerBatches.id, row.id), eq18(providerBatches.status, ProviderBatchStatuses.submitted))).run().changes === 1;
|
|
16021
15031
|
this.schedule.delete(row.id);
|
|
16022
15032
|
if (gaveUp) log17.warn("batch.abandoned-after-deadline", { batchId: row.id, runId: row.runId, provider: row.provider });
|
|
16023
15033
|
return gaveUp;
|
|
@@ -16057,10 +15067,10 @@ var ProviderBatchPoller = class {
|
|
|
16057
15067
|
/** Runs the sweep handed off whose batches have all settled. */
|
|
16058
15068
|
finalizableRuns() {
|
|
16059
15069
|
const holding = [ProviderBatchStatuses.submitting, ...OUTSTANDING_PROVIDER_BATCH_STATUSES];
|
|
16060
|
-
return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(
|
|
16061
|
-
|
|
15070
|
+
return this.deps.db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and16(
|
|
15071
|
+
eq18(runs.status, RunStatuses.running),
|
|
16062
15072
|
isNotNull2(runs.pendingProviderErrors),
|
|
16063
|
-
notExists2(this.deps.db.select({ one:
|
|
15073
|
+
notExists2(this.deps.db.select({ one: sql8`1` }).from(providerBatches).where(and16(eq18(providerBatches.runId, runs.id), inArray8(providerBatches.status, holding))))
|
|
16064
15074
|
)).all();
|
|
16065
15075
|
}
|
|
16066
15076
|
};
|
|
@@ -16111,7 +15121,7 @@ async function refreshAllIntegrations(client, projectName, now = /* @__PURE__ */
|
|
|
16111
15121
|
}
|
|
16112
15122
|
|
|
16113
15123
|
// src/notifier.ts
|
|
16114
|
-
import { eq as
|
|
15124
|
+
import { eq as eq19, desc as desc8, and as and17, inArray as inArray9, or } from "drizzle-orm";
|
|
16115
15125
|
import crypto22 from "crypto";
|
|
16116
15126
|
var log19 = createLogger("Notifier");
|
|
16117
15127
|
var MATERIAL_MAGNITUDE_DELTA = 20;
|
|
@@ -16144,18 +15154,18 @@ var Notifier = class {
|
|
|
16144
15154
|
/** Called after a run completes (success, partial, or failed). */
|
|
16145
15155
|
async onRunCompleted(runId, projectId, opts) {
|
|
16146
15156
|
log19.info("run.completed", { runId, projectId, origin: opts?.origin ?? "sweep" });
|
|
16147
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15157
|
+
const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
16148
15158
|
if (notifs.length === 0) {
|
|
16149
15159
|
log19.info("notifications.none-enabled", { projectId });
|
|
16150
15160
|
return;
|
|
16151
15161
|
}
|
|
16152
15162
|
log19.info("notifications.found", { projectId, count: notifs.length });
|
|
16153
|
-
const run = this.db.select().from(runs).where(
|
|
15163
|
+
const run = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
|
|
16154
15164
|
if (!run) {
|
|
16155
15165
|
log19.error("run.not-found", { runId, msg: "skipping notification dispatch" });
|
|
16156
15166
|
return;
|
|
16157
15167
|
}
|
|
16158
|
-
const project = this.db.select().from(projects).where(
|
|
15168
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
16159
15169
|
if (!project) {
|
|
16160
15170
|
log19.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
|
|
16161
15171
|
return;
|
|
@@ -16208,7 +15218,7 @@ var Notifier = class {
|
|
|
16208
15218
|
* inferred from the absence of a webhook.
|
|
16209
15219
|
*/
|
|
16210
15220
|
async onHealthChecked(projectId, report) {
|
|
16211
|
-
const project = this.db.select().from(projects).where(
|
|
15221
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
16212
15222
|
if (!project) {
|
|
16213
15223
|
log19.error("project.not-found", { projectId, msg: "skipping health notification" });
|
|
16214
15224
|
return null;
|
|
@@ -16241,7 +15251,7 @@ var Notifier = class {
|
|
|
16241
15251
|
const status = noSignal ? "warn" : worst ? worst.status : "ok";
|
|
16242
15252
|
const code = noSignal ? "health.no-signal" : worst?.code ?? "health.ok";
|
|
16243
15253
|
const summary = noSignal ? `No health check produced a result (${healthChecks.length} skipped) \u2014 health is unknown, not confirmed.` : worst?.summary ?? `All ${graded.length} health check(s) passing.`;
|
|
16244
|
-
const previous = this.db.select().from(doctorHealthState).where(
|
|
15254
|
+
const previous = this.db.select().from(doctorHealthState).where(eq19(doctorHealthState.projectId, projectId)).get();
|
|
16245
15255
|
const previousStatus = previous?.status ?? null;
|
|
16246
15256
|
const failingSignature = failing.map((c) => `${c.status}:${c.code}`).join(",");
|
|
16247
15257
|
let event = null;
|
|
@@ -16259,13 +15269,13 @@ var Notifier = class {
|
|
|
16259
15269
|
if (previous === void 0) {
|
|
16260
15270
|
this.db.insert(doctorHealthState).values({ ...observation, notifiedAt: null }).run();
|
|
16261
15271
|
} else {
|
|
16262
|
-
this.db.update(doctorHealthState).set(observation).where(
|
|
15272
|
+
this.db.update(doctorHealthState).set(observation).where(eq19(doctorHealthState.projectId, projectId)).run();
|
|
16263
15273
|
}
|
|
16264
15274
|
if (!event) {
|
|
16265
15275
|
log19.info("health.unchanged", { projectId, status, code });
|
|
16266
15276
|
return null;
|
|
16267
15277
|
}
|
|
16268
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15278
|
+
const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
16269
15279
|
const payload = {
|
|
16270
15280
|
source: "canonry",
|
|
16271
15281
|
event,
|
|
@@ -16288,7 +15298,7 @@ var Notifier = class {
|
|
|
16288
15298
|
if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
|
|
16289
15299
|
}
|
|
16290
15300
|
if (delivered > 0) {
|
|
16291
|
-
this.db.update(doctorHealthState).set({ notifiedAt: now }).where(
|
|
15301
|
+
this.db.update(doctorHealthState).set({ notifiedAt: now }).where(eq19(doctorHealthState.projectId, projectId)).run();
|
|
16292
15302
|
}
|
|
16293
15303
|
log19.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
|
|
16294
15304
|
return event;
|
|
@@ -16309,12 +15319,12 @@ var Notifier = class {
|
|
|
16309
15319
|
async onSiteLivenessChecked(projectId, result2) {
|
|
16310
15320
|
const { check, checkedAt } = result2;
|
|
16311
15321
|
if (check.status !== "ok" && check.status !== "fail") return null;
|
|
16312
|
-
const project = this.db.select().from(projects).where(
|
|
15322
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
16313
15323
|
if (!project) {
|
|
16314
15324
|
log19.error("project.not-found", { projectId, msg: "skipping site liveness notification" });
|
|
16315
15325
|
return null;
|
|
16316
15326
|
}
|
|
16317
|
-
const previous = this.db.select().from(siteLivenessState).where(
|
|
15327
|
+
const previous = this.db.select().from(siteLivenessState).where(eq19(siteLivenessState.projectId, projectId)).get();
|
|
16318
15328
|
const sincePrevious = previous ? Date.parse(checkedAt) - Date.parse(previous.checkedAt) : Number.NaN;
|
|
16319
15329
|
const countsAsRepeat = Number.isFinite(sincePrevious) && sincePrevious >= SITE_LIVENESS_MIN_PASS_GAP_MS && sincePrevious <= SITE_LIVENESS_MAX_PASS_GAP_MS;
|
|
16320
15330
|
const failures = check.status === "fail" ? countsAsRepeat ? (previous?.consecutiveFailures ?? 0) + 1 : 1 : 0;
|
|
@@ -16334,10 +15344,10 @@ var Notifier = class {
|
|
|
16334
15344
|
if (previous === void 0) {
|
|
16335
15345
|
this.db.insert(siteLivenessState).values(observation).run();
|
|
16336
15346
|
} else {
|
|
16337
|
-
this.db.update(siteLivenessState).set(observation).where(
|
|
15347
|
+
this.db.update(siteLivenessState).set(observation).where(eq19(siteLivenessState.projectId, projectId)).run();
|
|
16338
15348
|
}
|
|
16339
15349
|
if (!event) return null;
|
|
16340
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15350
|
+
const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
16341
15351
|
const payload = {
|
|
16342
15352
|
source: "canonry",
|
|
16343
15353
|
event,
|
|
@@ -16363,7 +15373,7 @@ var Notifier = class {
|
|
|
16363
15373
|
if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
|
|
16364
15374
|
}
|
|
16365
15375
|
if (event === "health.degraded" && delivered > 0) {
|
|
16366
|
-
this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(
|
|
15376
|
+
this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq19(siteLivenessState.projectId, projectId)).run();
|
|
16367
15377
|
}
|
|
16368
15378
|
log19.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
|
|
16369
15379
|
return event;
|
|
@@ -16386,7 +15396,7 @@ var Notifier = class {
|
|
|
16386
15396
|
*/
|
|
16387
15397
|
isNewInsightNews(projectId, insight) {
|
|
16388
15398
|
const key = insightNotifyKey(projectId, insight);
|
|
16389
|
-
const previous = this.db.select().from(insightNotifyState).where(
|
|
15399
|
+
const previous = this.db.select().from(insightNotifyState).where(eq19(insightNotifyState.key, key)).get();
|
|
16390
15400
|
if (!previous) return true;
|
|
16391
15401
|
if (severityRank(insight.severity) > severityRank(previous.severity)) return true;
|
|
16392
15402
|
const magnitude = insightMagnitude(insight);
|
|
@@ -16408,9 +15418,9 @@ var Notifier = class {
|
|
|
16408
15418
|
magnitude: insightMagnitude(insight),
|
|
16409
15419
|
notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
16410
15420
|
};
|
|
16411
|
-
const existing = this.db.select().from(insightNotifyState).where(
|
|
15421
|
+
const existing = this.db.select().from(insightNotifyState).where(eq19(insightNotifyState.key, key)).get();
|
|
16412
15422
|
if (existing) {
|
|
16413
|
-
this.db.update(insightNotifyState).set(row).where(
|
|
15423
|
+
this.db.update(insightNotifyState).set(row).where(eq19(insightNotifyState.key, key)).run();
|
|
16414
15424
|
} else {
|
|
16415
15425
|
this.db.insert(insightNotifyState).values(row).run();
|
|
16416
15426
|
}
|
|
@@ -16424,11 +15434,11 @@ var Notifier = class {
|
|
|
16424
15434
|
if (criticalInsights.length > 0) insightEvents.push("insight.critical");
|
|
16425
15435
|
if (highInsights.length > 0) insightEvents.push("insight.high");
|
|
16426
15436
|
if (insightEvents.length === 0) return;
|
|
16427
|
-
const notifs = this.db.select().from(notifications).where(
|
|
15437
|
+
const notifs = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled);
|
|
16428
15438
|
if (notifs.length === 0) return;
|
|
16429
|
-
const run = this.db.select().from(runs).where(
|
|
15439
|
+
const run = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
|
|
16430
15440
|
if (!run) return;
|
|
16431
|
-
const project = this.db.select().from(projects).where(
|
|
15441
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
16432
15442
|
if (!project) return;
|
|
16433
15443
|
for (const notif of notifs) {
|
|
16434
15444
|
const config = notif.config;
|
|
@@ -16490,10 +15500,10 @@ var Notifier = class {
|
|
|
16490
15500
|
}
|
|
16491
15501
|
}
|
|
16492
15502
|
async dispatchQueuedReviewAlerts(projectId) {
|
|
16493
|
-
const pendingReviews = this.db.select().from(gbpReviews).where(
|
|
16494
|
-
const pendingRatings = this.db.select().from(gbpReviewRatings).where(
|
|
15503
|
+
const pendingReviews = this.db.select().from(gbpReviews).where(and17(eq19(gbpReviews.projectId, projectId), eq19(gbpReviews.alertState, "pending"))).all();
|
|
15504
|
+
const pendingRatings = this.db.select().from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.alertState, "pending"))).all();
|
|
16495
15505
|
if (pendingReviews.length === 0 && pendingRatings.length === 0) return { reviews: 0, ratingDrops: 0, delivered: 0 };
|
|
16496
|
-
const project = this.db.select().from(projects).where(
|
|
15506
|
+
const project = this.db.select().from(projects).where(eq19(projects.id, projectId)).get();
|
|
16497
15507
|
if (!project) return { reviews: 0, ratingDrops: 0, delivered: 0 };
|
|
16498
15508
|
const now = /* @__PURE__ */ new Date();
|
|
16499
15509
|
const nowIso = now.toISOString();
|
|
@@ -16504,10 +15514,10 @@ var Notifier = class {
|
|
|
16504
15514
|
displayName: gbpLocations.displayName,
|
|
16505
15515
|
mapsUri: gbpLocations.mapsUri,
|
|
16506
15516
|
reviewsAccess: gbpLocations.reviewsAccess
|
|
16507
|
-
}).from(gbpLocations).where(
|
|
15517
|
+
}).from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).all().map((loc) => [loc.locationName, loc])
|
|
16508
15518
|
);
|
|
16509
15519
|
const v4HasWorked = new Set(
|
|
16510
|
-
this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(
|
|
15520
|
+
this.db.select({ locationName: gbpReviewRatings.locationName }).from(gbpReviewRatings).where(and17(eq19(gbpReviewRatings.projectId, projectId), eq19(gbpReviewRatings.origin, "gbp"))).all().map((row) => row.locationName)
|
|
16511
15521
|
);
|
|
16512
15522
|
const coveredByBusinessProfile = (locationName) => businessProfileReviewsCover(locations.get(locationName)?.reviewsAccess ?? null, v4HasWorked.has(locationName));
|
|
16513
15523
|
const locationRef = (locationName) => {
|
|
@@ -16516,11 +15526,11 @@ var Notifier = class {
|
|
|
16516
15526
|
};
|
|
16517
15527
|
const setReviewState = (ids, alertState) => {
|
|
16518
15528
|
if (ids.length === 0) return;
|
|
16519
|
-
this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(
|
|
15529
|
+
this.db.update(gbpReviews).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviews.id, ids)).run();
|
|
16520
15530
|
};
|
|
16521
15531
|
const setRatingState = (ids, alertState) => {
|
|
16522
15532
|
if (ids.length === 0) return;
|
|
16523
|
-
this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(
|
|
15533
|
+
this.db.update(gbpReviewRatings).set({ alertState, alertStateAt: nowIso }).where(inArray9(gbpReviewRatings.id, ids)).run();
|
|
16524
15534
|
};
|
|
16525
15535
|
const reviewsToSend = [];
|
|
16526
15536
|
for (const row of pendingReviews) {
|
|
@@ -16536,7 +15546,7 @@ var Notifier = class {
|
|
|
16536
15546
|
else if (row.rating === null || row.previousRating === null) setRatingState([row.id], "none");
|
|
16537
15547
|
else ratingsToSend.push(row);
|
|
16538
15548
|
}
|
|
16539
|
-
const enabled = this.db.select().from(notifications).where(
|
|
15549
|
+
const enabled = this.db.select().from(notifications).where(eq19(notifications.projectId, projectId)).all().filter((n) => n.enabled && n.config.url);
|
|
16540
15550
|
const subscribedTo = (event) => enabled.filter((n) => (n.config.events ?? []).includes(event));
|
|
16541
15551
|
const projectRef = { name: project.name, canonicalDomain: project.canonicalDomain };
|
|
16542
15552
|
const dashboardUrl = `${this.serverUrl}/projects/${project.name}`;
|
|
@@ -16611,7 +15621,7 @@ var Notifier = class {
|
|
|
16611
15621
|
return { reviews: reviewsToSend.length, ratingDrops: ratingsToSend.length, delivered };
|
|
16612
15622
|
}
|
|
16613
15623
|
computeTransitions(runId, projectId) {
|
|
16614
|
-
const thisRun = this.db.select().from(runs).where(
|
|
15624
|
+
const thisRun = this.db.select().from(runs).where(eq19(runs.id, runId)).get();
|
|
16615
15625
|
if (!thisRun) return [];
|
|
16616
15626
|
const completeness = measurementRunCompleteness(this.db, runId);
|
|
16617
15627
|
if (completeness.planned && !completeness.complete) {
|
|
@@ -16622,10 +15632,10 @@ var Notifier = class {
|
|
|
16622
15632
|
});
|
|
16623
15633
|
return [];
|
|
16624
15634
|
}
|
|
16625
|
-
const groupSiblings = this.db.select().from(runs).where(
|
|
16626
|
-
|
|
16627
|
-
|
|
16628
|
-
|
|
15635
|
+
const groupSiblings = this.db.select().from(runs).where(and17(
|
|
15636
|
+
eq19(runs.projectId, projectId),
|
|
15637
|
+
eq19(runs.kind, thisRun.kind),
|
|
15638
|
+
eq19(runs.createdAt, thisRun.createdAt),
|
|
16629
15639
|
notProbeRun()
|
|
16630
15640
|
)).all();
|
|
16631
15641
|
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
@@ -16642,20 +15652,20 @@ var Notifier = class {
|
|
|
16642
15652
|
return candidate.id > best.id ? candidate : best;
|
|
16643
15653
|
});
|
|
16644
15654
|
if (winner.id !== runId) return [];
|
|
16645
|
-
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
15655
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq19(projects.id, projectId)).get();
|
|
16646
15656
|
const locationCount = Math.max(
|
|
16647
15657
|
1,
|
|
16648
15658
|
(projectLocations?.locations ?? []).length
|
|
16649
15659
|
);
|
|
16650
15660
|
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
16651
15661
|
const recentRuns = this.db.select().from(runs).where(
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
|
|
16655
|
-
or(
|
|
15662
|
+
and17(
|
|
15663
|
+
eq19(runs.projectId, projectId),
|
|
15664
|
+
eq19(runs.kind, thisRun.kind),
|
|
15665
|
+
or(eq19(runs.status, "completed"), eq19(runs.status, "partial")),
|
|
16656
15666
|
notProbeRun()
|
|
16657
15667
|
)
|
|
16658
|
-
).orderBy(
|
|
15668
|
+
).orderBy(desc8(runs.createdAt), desc8(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
16659
15669
|
const groups = groupRunsByCreatedAt(recentRuns);
|
|
16660
15670
|
const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
|
|
16661
15671
|
if (currentGroupIdx < 0) return [];
|
|
@@ -16670,13 +15680,13 @@ var Notifier = class {
|
|
|
16670
15680
|
provider: querySnapshots.provider,
|
|
16671
15681
|
location: querySnapshots.location,
|
|
16672
15682
|
citationState: querySnapshots.citationState
|
|
16673
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
15683
|
+
}).from(querySnapshots).leftJoin(queries, eq19(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
|
|
16674
15684
|
const previousSnapshots = this.db.select({
|
|
16675
15685
|
queryId: querySnapshots.queryId,
|
|
16676
15686
|
provider: querySnapshots.provider,
|
|
16677
15687
|
location: querySnapshots.location,
|
|
16678
15688
|
citationState: querySnapshots.citationState
|
|
16679
|
-
}).from(querySnapshots).where(
|
|
15689
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
|
|
16680
15690
|
const prevMap = /* @__PURE__ */ new Map();
|
|
16681
15691
|
for (const s of previousSnapshots) {
|
|
16682
15692
|
if (s.queryId == null) continue;
|
|
@@ -16755,7 +15765,7 @@ var Notifier = class {
|
|
|
16755
15765
|
};
|
|
16756
15766
|
|
|
16757
15767
|
// src/run-coordinator.ts
|
|
16758
|
-
import { eq as
|
|
15768
|
+
import { eq as eq20 } from "drizzle-orm";
|
|
16759
15769
|
var log20 = createLogger("RunCoordinator");
|
|
16760
15770
|
var RunCoordinator = class {
|
|
16761
15771
|
constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
|
|
@@ -16773,7 +15783,7 @@ var RunCoordinator = class {
|
|
|
16773
15783
|
/** Host wakeup only; durable completion receipts remain the delivery authority. */
|
|
16774
15784
|
onSentimentCompleted;
|
|
16775
15785
|
async onRunCompleted(runId, projectId, opts) {
|
|
16776
|
-
const runRow = this.db.select().from(runs).where(
|
|
15786
|
+
const runRow = this.db.select().from(runs).where(eq20(runs.id, runId)).get();
|
|
16777
15787
|
const kind = runRow?.kind ?? RunKinds["answer-visibility"];
|
|
16778
15788
|
if (runRow?.trigger === RunTriggers.probe) {
|
|
16779
15789
|
log20.info("probe.skip-side-effects", { runId, projectId, kind });
|
|
@@ -16876,7 +15886,7 @@ var RunCoordinator = class {
|
|
|
16876
15886
|
* so the Aero queue is never starved of a follow-up.
|
|
16877
15887
|
*/
|
|
16878
15888
|
buildDiscoveryAeroContext(runId, projectId, status, error) {
|
|
16879
|
-
const session = this.db.select().from(discoverySessions).where(
|
|
15889
|
+
const session = this.db.select().from(discoverySessions).where(eq20(discoverySessions.runId, runId)).get();
|
|
16880
15890
|
const competitorMap = session ? session.competitorMap : [];
|
|
16881
15891
|
return {
|
|
16882
15892
|
kind: RunKinds["aeo-discover-probe"],
|
|
@@ -16919,7 +15929,7 @@ function analysisResultFromInsights(insights2) {
|
|
|
16919
15929
|
|
|
16920
15930
|
// src/agent/session-registry.ts
|
|
16921
15931
|
import crypto26 from "crypto";
|
|
16922
|
-
import { eq as
|
|
15932
|
+
import { eq as eq23 } from "drizzle-orm";
|
|
16923
15933
|
|
|
16924
15934
|
// src/agent/session.ts
|
|
16925
15935
|
import fs8 from "fs";
|
|
@@ -18879,7 +17889,7 @@ async function loadExternalMcpTools(servers, opts = {}) {
|
|
|
18879
17889
|
|
|
18880
17890
|
// src/agent/memory-store.ts
|
|
18881
17891
|
import crypto25 from "crypto";
|
|
18882
|
-
import { and as
|
|
17892
|
+
import { and as and18, desc as desc9, eq as eq21, like, sql as sql9, or as or2 } from "drizzle-orm";
|
|
18883
17893
|
var COMPACTION_KEY_PREFIX = "compaction:";
|
|
18884
17894
|
var COMPACTION_NOTES_PER_SESSION = 3;
|
|
18885
17895
|
function rowToDto(row) {
|
|
@@ -18893,7 +17903,7 @@ function rowToDto(row) {
|
|
|
18893
17903
|
};
|
|
18894
17904
|
}
|
|
18895
17905
|
function listMemoryEntries(db, projectId, opts = {}) {
|
|
18896
|
-
const query = db.select().from(agentMemory).where(
|
|
17906
|
+
const query = db.select().from(agentMemory).where(eq21(agentMemory.projectId, projectId)).orderBy(desc9(agentMemory.updatedAt));
|
|
18897
17907
|
const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
|
|
18898
17908
|
return rows.map(rowToDto);
|
|
18899
17909
|
}
|
|
@@ -18924,22 +17934,22 @@ function upsertMemoryEntry(db, args) {
|
|
|
18924
17934
|
updatedAt: now
|
|
18925
17935
|
}
|
|
18926
17936
|
}).run();
|
|
18927
|
-
const row = db.select().from(agentMemory).where(
|
|
17937
|
+
const row = db.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, args.key))).get();
|
|
18928
17938
|
if (!row) throw new Error("memory upsert produced no row");
|
|
18929
17939
|
return rowToDto(row);
|
|
18930
17940
|
}
|
|
18931
17941
|
function deleteMemoryEntry(db, projectId, key) {
|
|
18932
|
-
const result2 = db.delete(agentMemory).where(
|
|
17942
|
+
const result2 = db.delete(agentMemory).where(and18(eq21(agentMemory.projectId, projectId), eq21(agentMemory.key, key))).run();
|
|
18933
17943
|
const changes = result2.changes ?? 0;
|
|
18934
17944
|
return changes > 0;
|
|
18935
17945
|
}
|
|
18936
17946
|
function loadRecentForHydrate(db, projectId, limit, sessionId) {
|
|
18937
17947
|
if (sessionId === void 0) return listMemoryEntries(db, projectId, { limit });
|
|
18938
17948
|
const sessionPrefix = `compaction:${sessionId ?? ""}:`;
|
|
18939
|
-
return db.select().from(agentMemory).where(
|
|
18940
|
-
|
|
18941
|
-
or2(
|
|
18942
|
-
)).orderBy(
|
|
17949
|
+
return db.select().from(agentMemory).where(and18(
|
|
17950
|
+
eq21(agentMemory.projectId, projectId),
|
|
17951
|
+
or2(sql9`substr(${agentMemory.key}, 1, ${COMPACTION_KEY_PREFIX.length}) <> ${COMPACTION_KEY_PREFIX}`, sessionId ? sql9`substr(${agentMemory.key}, 1, ${sessionPrefix.length}) = ${sessionPrefix}` : void 0)
|
|
17952
|
+
)).orderBy(desc9(agentMemory.updatedAt)).limit(limit).all().map(rowToDto);
|
|
18943
17953
|
}
|
|
18944
17954
|
function writeCompactionNote(db, args) {
|
|
18945
17955
|
if (Buffer.byteLength(args.summary, "utf8") > AGENT_MEMORY_VALUE_MAX_BYTES) {
|
|
@@ -18963,16 +17973,16 @@ function writeCompactionNote(db, args) {
|
|
|
18963
17973
|
}).run();
|
|
18964
17974
|
const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
|
|
18965
17975
|
const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
|
|
18966
|
-
|
|
18967
|
-
|
|
17976
|
+
and18(
|
|
17977
|
+
eq21(agentMemory.projectId, args.projectId),
|
|
18968
17978
|
like(agentMemory.key, `${sessionPrefix}%`)
|
|
18969
17979
|
)
|
|
18970
|
-
).orderBy(
|
|
17980
|
+
).orderBy(desc9(agentMemory.updatedAt)).all();
|
|
18971
17981
|
const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
|
|
18972
17982
|
if (stale.length > 0) {
|
|
18973
|
-
tx.delete(agentMemory).where(
|
|
17983
|
+
tx.delete(agentMemory).where(sql9`${agentMemory.id} IN (${sql9.join(stale.map((s) => sql9`${s}`), sql9`, `)})`).run();
|
|
18974
17984
|
}
|
|
18975
|
-
const row = tx.select().from(agentMemory).where(
|
|
17985
|
+
const row = tx.select().from(agentMemory).where(and18(eq21(agentMemory.projectId, args.projectId), eq21(agentMemory.key, key))).get();
|
|
18976
17986
|
if (row) inserted = rowToDto(row);
|
|
18977
17987
|
});
|
|
18978
17988
|
if (!inserted) throw new Error("compaction note write produced no row");
|
|
@@ -18980,7 +17990,7 @@ function writeCompactionNote(db, args) {
|
|
|
18980
17990
|
}
|
|
18981
17991
|
|
|
18982
17992
|
// src/agent/project-shape.ts
|
|
18983
|
-
import { and as
|
|
17993
|
+
import { and as and19, count as count2, eq as eq22 } from "drizzle-orm";
|
|
18984
17994
|
var ADVANCED_TOOLS = [
|
|
18985
17995
|
"canonry_measurement_portfolio_summary",
|
|
18986
17996
|
"canonry_measurement_overview",
|
|
@@ -19006,10 +18016,10 @@ var ADVANCED_ROUTES = [
|
|
|
19006
18016
|
];
|
|
19007
18017
|
function aeroProjectShape(db, projectId) {
|
|
19008
18018
|
try {
|
|
19009
|
-
const pointer = db.select().from(measurementPlans).where(
|
|
19010
|
-
const version = pointer ? db.select().from(measurementPlanVersions).where(
|
|
19011
|
-
|
|
19012
|
-
|
|
18019
|
+
const pointer = db.select().from(measurementPlans).where(eq22(measurementPlans.projectId, projectId)).get();
|
|
18020
|
+
const version = pointer ? db.select().from(measurementPlanVersions).where(and19(
|
|
18021
|
+
eq22(measurementPlanVersions.projectId, projectId),
|
|
18022
|
+
eq22(measurementPlanVersions.id, pointer.activeVersionId)
|
|
19013
18023
|
)).get() : void 0;
|
|
19014
18024
|
if (version) {
|
|
19015
18025
|
const plan = JSON.parse(version.canonicalJson);
|
|
@@ -19036,7 +18046,7 @@ canonry_visibility_report describes the whole project, not one Property. canonry
|
|
|
19036
18046
|
pinned: ADVANCED_TOOLS
|
|
19037
18047
|
};
|
|
19038
18048
|
}
|
|
19039
|
-
const tracked = db.select({ total: count2() }).from(queries).where(
|
|
18049
|
+
const tracked = db.select({ total: count2() }).from(queries).where(eq22(queries.projectId, projectId)).get()?.total ?? 0;
|
|
19040
18050
|
return {
|
|
19041
18051
|
prompt: `
|
|
19042
18052
|
|
|
@@ -19292,7 +18302,7 @@ var SessionRegistry = class {
|
|
|
19292
18302
|
modelProvider: effectiveProvider,
|
|
19293
18303
|
modelId: effectiveModelId,
|
|
19294
18304
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19295
|
-
}).where(
|
|
18305
|
+
}).where(eq23(agentSessions.projectId, projectId)).run();
|
|
19296
18306
|
}
|
|
19297
18307
|
const agent2 = createAeroSession({
|
|
19298
18308
|
projectName,
|
|
@@ -19588,7 +18598,7 @@ ${lines.join("\n")}
|
|
|
19588
18598
|
modelProvider: nextProvider,
|
|
19589
18599
|
modelId: nextModelId,
|
|
19590
18600
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
19591
|
-
}).where(
|
|
18601
|
+
}).where(eq23(agentSessions.projectId, projectId)).run();
|
|
19592
18602
|
}
|
|
19593
18603
|
/** Snapshot pending work without consuming it before an atomic conversation switch. */
|
|
19594
18604
|
prepareConversationChange(projectName) {
|
|
@@ -19763,11 +18773,11 @@ ${lines.join("\n")}
|
|
|
19763
18773
|
return id;
|
|
19764
18774
|
}
|
|
19765
18775
|
tryResolveProjectId(projectName) {
|
|
19766
|
-
const row = this.opts.db.select({ id: projects.id }).from(projects).where(
|
|
18776
|
+
const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq23(projects.name, projectName)).get();
|
|
19767
18777
|
return row?.id;
|
|
19768
18778
|
}
|
|
19769
18779
|
loadRow(projectId) {
|
|
19770
|
-
const row = this.opts.db.select().from(agentSessions).where(
|
|
18780
|
+
const row = this.opts.db.select().from(agentSessions).where(eq23(agentSessions.projectId, projectId)).get();
|
|
19771
18781
|
return row ?? null;
|
|
19772
18782
|
}
|
|
19773
18783
|
insertRow(params) {
|
|
@@ -19786,7 +18796,7 @@ ${lines.join("\n")}
|
|
|
19786
18796
|
}
|
|
19787
18797
|
updateRow(projectId, patch) {
|
|
19788
18798
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
19789
|
-
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(
|
|
18799
|
+
this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq23(agentSessions.projectId, projectId)).run();
|
|
19790
18800
|
}
|
|
19791
18801
|
/**
|
|
19792
18802
|
* Store a refreshed prompt snapshot without touching `updatedAt`, which the
|
|
@@ -19795,13 +18805,13 @@ ${lines.join("\n")}
|
|
|
19795
18805
|
* recently used right after an upgrade.
|
|
19796
18806
|
*/
|
|
19797
18807
|
persistPromptSnapshot(projectId, systemPrompt) {
|
|
19798
|
-
this.opts.db.update(agentSessions).set({ systemPrompt }).where(
|
|
18808
|
+
this.opts.db.update(agentSessions).set({ systemPrompt }).where(eq23(agentSessions.projectId, projectId)).run();
|
|
19799
18809
|
}
|
|
19800
18810
|
};
|
|
19801
18811
|
|
|
19802
18812
|
// src/agent/viewer-sessions.ts
|
|
19803
18813
|
import crypto27 from "crypto";
|
|
19804
|
-
import { and as
|
|
18814
|
+
import { and as and20, eq as eq24, isNotNull as isNotNull3, like as like2 } from "drizzle-orm";
|
|
19805
18815
|
var AERO_VIEWER_EXCLUDED_MCP_TOOLS = /* @__PURE__ */ new Set([
|
|
19806
18816
|
CanonryMcpToolNames.canonry_ads_account,
|
|
19807
18817
|
CanonryMcpToolNames.canonry_ads_geo_search,
|
|
@@ -19977,10 +18987,10 @@ var ViewerAeroSessions = class {
|
|
|
19977
18987
|
*/
|
|
19978
18988
|
removeKey(id) {
|
|
19979
18989
|
try {
|
|
19980
|
-
this.opts.db.delete(apiKeys).where(
|
|
18990
|
+
this.opts.db.delete(apiKeys).where(eq24(apiKeys.id, id)).run();
|
|
19981
18991
|
} catch {
|
|
19982
18992
|
try {
|
|
19983
|
-
this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(
|
|
18993
|
+
this.opts.db.update(apiKeys).set({ revokedAt: new Date(this.now()).toISOString() }).where(eq24(apiKeys.id, id)).run();
|
|
19984
18994
|
} catch {
|
|
19985
18995
|
}
|
|
19986
18996
|
}
|
|
@@ -19992,7 +19002,7 @@ var ViewerAeroSessions = class {
|
|
|
19992
19002
|
*/
|
|
19993
19003
|
revokeOrphanedKeys() {
|
|
19994
19004
|
try {
|
|
19995
|
-
this.opts.db.delete(apiKeys).where(
|
|
19005
|
+
this.opts.db.delete(apiKeys).where(and20(like2(apiKeys.name, `${VIEWER_AERO_KEY_PREFIX}%`), isNotNull3(apiKeys.delegatedUserId))).run();
|
|
19996
19006
|
} catch {
|
|
19997
19007
|
}
|
|
19998
19008
|
}
|
|
@@ -20008,7 +19018,7 @@ var ViewerAeroSessions = class {
|
|
|
20008
19018
|
|
|
20009
19019
|
// src/mcp-http.ts
|
|
20010
19020
|
import crypto28 from "crypto";
|
|
20011
|
-
import { eq as
|
|
19021
|
+
import { eq as eq25 } from "drizzle-orm";
|
|
20012
19022
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20013
19023
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
20014
19024
|
var SESSION_IDLE_MS = 10 * 60 * 1e3;
|
|
@@ -20029,7 +19039,7 @@ function mintSessionKey(db, scopes, userId) {
|
|
|
20029
19039
|
}
|
|
20030
19040
|
function revokeSessionKey(db, id) {
|
|
20031
19041
|
try {
|
|
20032
|
-
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
19042
|
+
db.update(apiKeys).set({ revokedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq25(apiKeys.id, id)).run();
|
|
20033
19043
|
} catch {
|
|
20034
19044
|
}
|
|
20035
19045
|
}
|
|
@@ -20212,7 +19222,7 @@ function registerMcpHttpRoutes(scope, opts) {
|
|
|
20212
19222
|
}
|
|
20213
19223
|
|
|
20214
19224
|
// src/agent/agent-routes.ts
|
|
20215
|
-
import { eq as
|
|
19225
|
+
import { eq as eq26 } from "drizzle-orm";
|
|
20216
19226
|
function viewerAeroCaller(request, opts) {
|
|
20217
19227
|
if (!opts.viewerSessions) return null;
|
|
20218
19228
|
const principal = request.principal;
|
|
@@ -20242,7 +19252,7 @@ function viewerTranscript(opts, projectName, userId) {
|
|
|
20242
19252
|
};
|
|
20243
19253
|
}
|
|
20244
19254
|
function resolveProject(db, name) {
|
|
20245
|
-
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(
|
|
19255
|
+
const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq26(projects.name, name)).get();
|
|
20246
19256
|
if (!row) throw notFound("project", name);
|
|
20247
19257
|
return row;
|
|
20248
19258
|
}
|
|
@@ -20355,7 +19365,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
20355
19365
|
}
|
|
20356
19366
|
requireInstanceAdministrator(request);
|
|
20357
19367
|
const project = resolveProject(opts.db, request.params.name);
|
|
20358
|
-
const row = opts.db.select().from(agentSessions).where(
|
|
19368
|
+
const row = opts.db.select().from(agentSessions).where(eq26(agentSessions.projectId, project.id)).get();
|
|
20359
19369
|
if (!row) {
|
|
20360
19370
|
return { conversationId: null, messages: [], modelProvider: null, modelId: null, updatedAt: null };
|
|
20361
19371
|
}
|
|
@@ -20395,7 +19405,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
20395
19405
|
const project = resolveProject(opts.db, request.params.name);
|
|
20396
19406
|
if (opts.sessionRegistry.isBusy(project.name)) throw agentBusy(project.name);
|
|
20397
19407
|
opts.sessionRegistry.reset(project.name);
|
|
20398
|
-
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
19408
|
+
opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq26(agentSessions.projectId, project.id)).run();
|
|
20399
19409
|
return { status: "reset" };
|
|
20400
19410
|
}
|
|
20401
19411
|
);
|
|
@@ -20419,7 +19429,7 @@ function registerAgentRoutes(app, opts) {
|
|
|
20419
19429
|
return streamViewerTurn(opts, project, viewerId, body, reply);
|
|
20420
19430
|
}
|
|
20421
19431
|
if (body.conversationId !== void 0) {
|
|
20422
|
-
const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(
|
|
19432
|
+
const current = opts.db.select({ id: agentSessions.id }).from(agentSessions).where(eq26(agentSessions.projectId, project.id)).get();
|
|
20423
19433
|
if ((current?.id ?? null) !== body.conversationId) throw validationError("The active conversation changed. Reload the conversation before sending.");
|
|
20424
19434
|
}
|
|
20425
19435
|
const promptText = body.prompt;
|
|
@@ -21356,20 +20366,20 @@ function clipText(value, length) {
|
|
|
21356
20366
|
}
|
|
21357
20367
|
|
|
21358
20368
|
// src/research-runner.ts
|
|
21359
|
-
import { and as
|
|
20369
|
+
import { and as and21, eq as eq27, inArray as inArray10, sql as sql10 } from "drizzle-orm";
|
|
21360
20370
|
var unfinishedResearchQueryStatuses = [ResearchQueryStatuses.queued, ResearchQueryStatuses.running];
|
|
21361
20371
|
function finalResearchRunStatus(completed, failed) {
|
|
21362
20372
|
if (failed === 0) return ResearchRunStatuses.completed;
|
|
21363
20373
|
return completed > 0 ? ResearchRunStatuses.partial : ResearchRunStatuses.failed;
|
|
21364
20374
|
}
|
|
21365
20375
|
async function executeResearchRun(db, registry, runId, projectId) {
|
|
21366
|
-
const run = db.select().from(researchRuns).where(
|
|
20376
|
+
const run = db.select().from(researchRuns).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.projectId, projectId))).get();
|
|
21367
20377
|
if (!run || run.status !== ResearchRunStatuses.queued) return;
|
|
21368
|
-
const project = db.select().from(projects).where(
|
|
20378
|
+
const project = db.select().from(projects).where(eq27(projects.id, projectId)).get();
|
|
21369
20379
|
if (!project) return;
|
|
21370
20380
|
const provider = registry.get(run.provider);
|
|
21371
20381
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21372
|
-
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(
|
|
20382
|
+
const claim = db.update(researchRuns).set({ status: ResearchRunStatuses.running, startedAt: now }).where(and21(eq27(researchRuns.id, runId), eq27(researchRuns.status, ResearchRunStatuses.queued))).run();
|
|
21373
20383
|
if (claim.changes !== 1) return;
|
|
21374
20384
|
let reserved = 0;
|
|
21375
20385
|
let dispatched = 0;
|
|
@@ -21393,8 +20403,10 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
21393
20403
|
}
|
|
21394
20404
|
reserved = run.totalQueries;
|
|
21395
20405
|
reservation = { scope, period };
|
|
21396
|
-
const rows = db.select().from(researchRunQueries).where(
|
|
21397
|
-
const
|
|
20406
|
+
const rows = db.select().from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).orderBy(researchRunQueries.position).all();
|
|
20407
|
+
const competitorRows = db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq27(competitors.projectId, projectId)).all();
|
|
20408
|
+
const competitorDomains = competitorRows.map((row) => row.domain);
|
|
20409
|
+
const competitorAliases = new Map(competitorRows.filter((row) => row.aliases.length > 0).map((row) => [row.domain, row.aliases]));
|
|
21398
20410
|
const domains = effectiveDomains(project);
|
|
21399
20411
|
const brands = effectiveBrandNames(project);
|
|
21400
20412
|
const config = { ...provider.config, model: run.resolvedModel };
|
|
@@ -21402,7 +20414,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
21402
20414
|
await mapWithConcurrency(rows, Math.max(1, provider.config.quotaPolicy.maxConcurrency), async (row) => {
|
|
21403
20415
|
try {
|
|
21404
20416
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
21405
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(
|
|
20417
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.running, startedAt }).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.queued))).run();
|
|
21406
20418
|
const raw = await gate.run(async () => {
|
|
21407
20419
|
dispatched++;
|
|
21408
20420
|
return provider.adapter.executeTrackedQuery({ query: row.queryText, canonicalDomains: domains, competitorDomains, ...run.location ? { location: run.location } : {} }, config);
|
|
@@ -21413,7 +20425,8 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
21413
20425
|
domains,
|
|
21414
20426
|
normalized.citedDomains,
|
|
21415
20427
|
competitorDomains,
|
|
21416
|
-
brands
|
|
20428
|
+
brands,
|
|
20429
|
+
competitorAliases
|
|
21417
20430
|
);
|
|
21418
20431
|
const citedCompetitorDomains = computeCitedCompetitorDomains(normalized.citedDomains, competitorDomains);
|
|
21419
20432
|
const completed = db.update(researchRunQueries).set({
|
|
@@ -21429,7 +20442,7 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
21429
20442
|
citationState: determineCitationState(normalized, domains),
|
|
21430
20443
|
rawResponse: raw.rawResponse,
|
|
21431
20444
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21432
|
-
}).where(
|
|
20445
|
+
}).where(and21(eq27(researchRunQueries.id, row.id), eq27(researchRunQueries.status, ResearchQueryStatuses.running))).run();
|
|
21433
20446
|
if (completed.changes === 1) incrementResearchProgress(db, runId, "completedQueries");
|
|
21434
20447
|
} catch (error) {
|
|
21435
20448
|
try {
|
|
@@ -21453,18 +20466,18 @@ async function executeResearchRun(db, registry, runId, projectId) {
|
|
|
21453
20466
|
}
|
|
21454
20467
|
}
|
|
21455
20468
|
function incrementResearchProgress(db, runId, column) {
|
|
21456
|
-
db.update(researchRuns).set({ [column]:
|
|
20469
|
+
db.update(researchRuns).set({ [column]: sql10`${researchRuns[column]} + 1` }).where(eq27(researchRuns.id, runId)).run();
|
|
21457
20470
|
}
|
|
21458
20471
|
function markResearchQueryFailed(db, runId, queryId, error) {
|
|
21459
20472
|
const message = describeError(error);
|
|
21460
|
-
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
20473
|
+
const failed = db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error: message, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.id, queryId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
21461
20474
|
if (failed.changes === 1) incrementResearchProgress(db, runId, "failedQueries");
|
|
21462
20475
|
}
|
|
21463
20476
|
function markUnfinishedResearchQueriesFailed(db, runId, error) {
|
|
21464
|
-
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
20477
|
+
db.update(researchRunQueries).set({ status: ResearchQueryStatuses.failed, error, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(and21(eq27(researchRunQueries.researchRunId, runId), inArray10(researchRunQueries.status, unfinishedResearchQueryStatuses))).run();
|
|
21465
20478
|
}
|
|
21466
20479
|
function finalizeResearchRun(db, runId, fatalError) {
|
|
21467
|
-
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(
|
|
20480
|
+
const rows = db.select({ status: researchRunQueries.status }).from(researchRunQueries).where(eq27(researchRunQueries.researchRunId, runId)).all();
|
|
21468
20481
|
const completed = rows.filter((row) => row.status === ResearchQueryStatuses.completed).length;
|
|
21469
20482
|
const failed = rows.filter((row) => row.status === ResearchQueryStatuses.failed).length;
|
|
21470
20483
|
db.update(researchRuns).set({
|
|
@@ -21473,11 +20486,11 @@ function finalizeResearchRun(db, runId, fatalError) {
|
|
|
21473
20486
|
failedQueries: failed,
|
|
21474
20487
|
error: fatalError ?? (failed === rows.length ? "Every research query failed." : null),
|
|
21475
20488
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21476
|
-
}).where(
|
|
20489
|
+
}).where(eq27(researchRuns.id, runId)).run();
|
|
21477
20490
|
}
|
|
21478
20491
|
|
|
21479
20492
|
// src/google-marketing-sync.ts
|
|
21480
|
-
import { and as
|
|
20493
|
+
import { and as and22, eq as eq28, isNull as isNull3 } from "drizzle-orm";
|
|
21481
20494
|
function safeErrorMessage(error) {
|
|
21482
20495
|
const raw = describeError(error);
|
|
21483
20496
|
return raw.replace(/(["']?authorization["']?\s*[:=]\s*["']?bearer\s+)[^\s,"'}]+/gi, "$1[redacted]").replace(/(["']?(?:developer|access|refresh|client)[_-]?(?:token|secret)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, "$1[redacted]").replace(/[\r\n\t]+/g, " ").slice(0, 1e3);
|
|
@@ -21500,10 +20513,10 @@ function assertSnapshotContext(metadata2, expected) {
|
|
|
21500
20513
|
}
|
|
21501
20514
|
}
|
|
21502
20515
|
function requireSyncRun(db, runId, projectId, kind) {
|
|
21503
|
-
const run = db.select().from(runs).where(
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
20516
|
+
const run = db.select().from(runs).where(and22(
|
|
20517
|
+
eq28(runs.id, runId),
|
|
20518
|
+
eq28(runs.projectId, projectId),
|
|
20519
|
+
eq28(runs.kind, kind)
|
|
21507
20520
|
)).get();
|
|
21508
20521
|
if (!run) throw new Error(`Run ${runId} is not a ${kind} run for this project.`);
|
|
21509
20522
|
if (run.status !== RunStatuses.queued) {
|
|
@@ -21516,19 +20529,19 @@ function markFailed(db, runId, projectId, kind, expectedStatus, error) {
|
|
|
21516
20529
|
status: RunStatuses.failed,
|
|
21517
20530
|
error: serializeRunError({ message: safeErrorMessage(error) }),
|
|
21518
20531
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21519
|
-
}).where(
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
20532
|
+
}).where(and22(
|
|
20533
|
+
eq28(runs.id, runId),
|
|
20534
|
+
eq28(runs.projectId, projectId),
|
|
20535
|
+
eq28(runs.kind, kind),
|
|
20536
|
+
eq28(runs.status, expectedStatus)
|
|
21524
20537
|
)).run();
|
|
21525
20538
|
}
|
|
21526
20539
|
function claimSyncRun(db, runId, projectId, kind, startedAt) {
|
|
21527
|
-
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(
|
|
21528
|
-
|
|
21529
|
-
|
|
21530
|
-
|
|
21531
|
-
|
|
20540
|
+
const claim = db.update(runs).set({ status: RunStatuses.running, startedAt, error: null }).where(and22(
|
|
20541
|
+
eq28(runs.id, runId),
|
|
20542
|
+
eq28(runs.projectId, projectId),
|
|
20543
|
+
eq28(runs.kind, kind),
|
|
20544
|
+
eq28(runs.status, RunStatuses.queued)
|
|
21532
20545
|
)).run();
|
|
21533
20546
|
return claim.changes === 1;
|
|
21534
20547
|
}
|
|
@@ -21536,9 +20549,9 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
21536
20549
|
let claimed = false;
|
|
21537
20550
|
try {
|
|
21538
20551
|
requireSyncRun(db, runId, projectId, RunKinds["google-ads-sync"]);
|
|
21539
|
-
const project = db.select().from(projects).where(
|
|
20552
|
+
const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
|
|
21540
20553
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
21541
|
-
const connection = db.select().from(googleAdsConnections).where(
|
|
20554
|
+
const connection = db.select().from(googleAdsConnections).where(eq28(googleAdsConnections.projectId, projectId)).get();
|
|
21542
20555
|
if (!connection?.selectedCustomerId) {
|
|
21543
20556
|
throw new Error("Google Ads needs a selected customer before it can sync.");
|
|
21544
20557
|
}
|
|
@@ -21589,11 +20602,11 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
21589
20602
|
status: RunStatuses.completed,
|
|
21590
20603
|
finishedAt,
|
|
21591
20604
|
error: null
|
|
21592
|
-
}).where(
|
|
21593
|
-
|
|
21594
|
-
|
|
21595
|
-
|
|
21596
|
-
|
|
20605
|
+
}).where(and22(
|
|
20606
|
+
eq28(runs.id, runId),
|
|
20607
|
+
eq28(runs.projectId, projectId),
|
|
20608
|
+
eq28(runs.kind, RunKinds["google-ads-sync"]),
|
|
20609
|
+
eq28(runs.status, RunStatuses.running)
|
|
21597
20610
|
)).run();
|
|
21598
20611
|
if (terminalClaim.changes !== 1) {
|
|
21599
20612
|
throw new Error(`Run ${runId} was no longer running when Google Ads evidence was ready.`);
|
|
@@ -21610,12 +20623,12 @@ async function executeGoogleAdsMarketingSync(db, runtime, runId, projectId) {
|
|
|
21610
20623
|
lastMetricsSnapshotAt: result2.metrics?.metadata.capturedAt ?? connection.lastMetricsSnapshotAt,
|
|
21611
20624
|
lastMetricsSnapshotId: result2.metrics?.metadata.id ?? connection.lastMetricsSnapshotId,
|
|
21612
20625
|
updatedAt: finishedAt
|
|
21613
|
-
}).where(
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
connection.selectedLoginCustomerId === null ?
|
|
21618
|
-
|
|
20626
|
+
}).where(and22(
|
|
20627
|
+
eq28(googleAdsConnections.id, connection.id),
|
|
20628
|
+
eq28(googleAdsConnections.projectId, projectId),
|
|
20629
|
+
eq28(googleAdsConnections.selectedCustomerId, connection.selectedCustomerId),
|
|
20630
|
+
connection.selectedLoginCustomerId === null ? isNull3(googleAdsConnections.selectedLoginCustomerId) : eq28(googleAdsConnections.selectedLoginCustomerId, connection.selectedLoginCustomerId),
|
|
20631
|
+
eq28(googleAdsConnections.selectionGeneration, connection.selectionGeneration)
|
|
21619
20632
|
)).run();
|
|
21620
20633
|
if (connectionClaim.changes !== 1) {
|
|
21621
20634
|
throw new Error("Google Ads selection changed while its evidence was being collected.");
|
|
@@ -21642,9 +20655,9 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
21642
20655
|
let claimed = false;
|
|
21643
20656
|
try {
|
|
21644
20657
|
requireSyncRun(db, runId, projectId, RunKinds["gtm-sync"]);
|
|
21645
|
-
const project = db.select().from(projects).where(
|
|
20658
|
+
const project = db.select().from(projects).where(eq28(projects.id, projectId)).get();
|
|
21646
20659
|
if (!project) throw new Error(`Project ${projectId} was not found.`);
|
|
21647
|
-
const connection = db.select().from(gtmConnections).where(
|
|
20660
|
+
const connection = db.select().from(gtmConnections).where(eq28(gtmConnections.projectId, projectId)).get();
|
|
21648
20661
|
if (!connection?.selectedAccountId || !connection.selectedContainerId) {
|
|
21649
20662
|
throw new Error("GTM needs a selected account and container before it can sync.");
|
|
21650
20663
|
}
|
|
@@ -21679,11 +20692,11 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
21679
20692
|
status: RunStatuses.completed,
|
|
21680
20693
|
finishedAt,
|
|
21681
20694
|
error: null
|
|
21682
|
-
}).where(
|
|
21683
|
-
|
|
21684
|
-
|
|
21685
|
-
|
|
21686
|
-
|
|
20695
|
+
}).where(and22(
|
|
20696
|
+
eq28(runs.id, runId),
|
|
20697
|
+
eq28(runs.projectId, projectId),
|
|
20698
|
+
eq28(runs.kind, RunKinds["gtm-sync"]),
|
|
20699
|
+
eq28(runs.status, RunStatuses.running)
|
|
21687
20700
|
)).run();
|
|
21688
20701
|
if (terminalClaim.changes !== 1) {
|
|
21689
20702
|
throw new Error(`Run ${runId} was no longer running when GTM evidence was ready.`);
|
|
@@ -21697,13 +20710,13 @@ async function executeGtmMarketingSync(db, runtime, runId, projectId) {
|
|
|
21697
20710
|
lastSnapshotAt: snapshot.metadata.capturedAt,
|
|
21698
20711
|
lastSnapshotId: snapshot.metadata.id,
|
|
21699
20712
|
updatedAt: finishedAt
|
|
21700
|
-
}).where(
|
|
21701
|
-
|
|
21702
|
-
|
|
21703
|
-
|
|
21704
|
-
|
|
21705
|
-
connection.selectedWorkspaceId === null ?
|
|
21706
|
-
|
|
20713
|
+
}).where(and22(
|
|
20714
|
+
eq28(gtmConnections.id, connection.id),
|
|
20715
|
+
eq28(gtmConnections.projectId, projectId),
|
|
20716
|
+
eq28(gtmConnections.selectedAccountId, connection.selectedAccountId),
|
|
20717
|
+
eq28(gtmConnections.selectedContainerId, connection.selectedContainerId),
|
|
20718
|
+
connection.selectedWorkspaceId === null ? isNull3(gtmConnections.selectedWorkspaceId) : eq28(gtmConnections.selectedWorkspaceId, connection.selectedWorkspaceId),
|
|
20719
|
+
eq28(gtmConnections.selectionGeneration, connection.selectionGeneration)
|
|
21707
20720
|
)).run();
|
|
21708
20721
|
if (connectionClaim.changes !== 1) {
|
|
21709
20722
|
throw new Error("GTM selection changed while its evidence was being collected.");
|
|
@@ -22206,7 +21219,7 @@ async function createServer(opts) {
|
|
|
22206
21219
|
async (ctx) => {
|
|
22207
21220
|
if (!sessionRegistry) return;
|
|
22208
21221
|
if (!agentProactive) return;
|
|
22209
|
-
const project = opts.db.select({ name: projects.name }).from(projects).where(
|
|
21222
|
+
const project = opts.db.select({ name: projects.name }).from(projects).where(eq29(projects.id, ctx.projectId)).get();
|
|
22210
21223
|
if (!project) return;
|
|
22211
21224
|
let content;
|
|
22212
21225
|
if (ctx.kind === RunKinds["aeo-discover-probe"]) {
|
|
@@ -22665,7 +21678,7 @@ async function createServer(opts) {
|
|
|
22665
21678
|
void (async () => {
|
|
22666
21679
|
try {
|
|
22667
21680
|
const report = await schedulerClient.runDoctor({ project: projectName, checkIds: scheduledHealthCheckIds() });
|
|
22668
|
-
const project = opts.db.select().from(projects).where(
|
|
21681
|
+
const project = opts.db.select().from(projects).where(eq29(projects.name, projectName)).get();
|
|
22669
21682
|
if (!project) {
|
|
22670
21683
|
app.log.warn({ projectName }, "doctor schedule fired for an unknown project");
|
|
22671
21684
|
return;
|
|
@@ -22693,9 +21706,9 @@ async function createServer(opts) {
|
|
|
22693
21706
|
});
|
|
22694
21707
|
if (!probed) return;
|
|
22695
21708
|
const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(
|
|
22696
|
-
|
|
22697
|
-
|
|
22698
|
-
|
|
21709
|
+
and23(
|
|
21710
|
+
eq29(ccReleaseSyncs.release, probed.release),
|
|
21711
|
+
eq29(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
|
|
22699
21712
|
)
|
|
22700
21713
|
).limit(1).get();
|
|
22701
21714
|
if (alreadySynced) {
|
|
@@ -22965,7 +21978,7 @@ async function createServer(opts) {
|
|
|
22965
21978
|
})();
|
|
22966
21979
|
if (opts.config.apiKey) {
|
|
22967
21980
|
const keyHash = hashApiKey2(opts.config.apiKey);
|
|
22968
|
-
const existing = opts.db.select().from(apiKeys).where(
|
|
21981
|
+
const existing = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, keyHash)).get();
|
|
22969
21982
|
if (!existing) {
|
|
22970
21983
|
const prefix = opts.config.apiKey.slice(0, 12);
|
|
22971
21984
|
opts.db.insert(apiKeys).values({
|
|
@@ -23017,7 +22030,7 @@ async function createServer(opts) {
|
|
|
23017
22030
|
};
|
|
23018
22031
|
const getDefaultApiKey = () => {
|
|
23019
22032
|
if (!opts.config.apiKey) return void 0;
|
|
23020
|
-
return opts.db.select().from(apiKeys).where(
|
|
22033
|
+
return opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(opts.config.apiKey))).get();
|
|
23021
22034
|
};
|
|
23022
22035
|
const createPasswordSession = (reply) => {
|
|
23023
22036
|
const key = getDefaultApiKey();
|
|
@@ -23041,7 +22054,7 @@ async function createServer(opts) {
|
|
|
23041
22054
|
if (!header) return false;
|
|
23042
22055
|
const parts = header.split(" ");
|
|
23043
22056
|
if (parts.length !== 2 || parts[0] !== "Bearer") return false;
|
|
23044
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
22057
|
+
const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(parts[1]))).get();
|
|
23045
22058
|
return Boolean(key && !key.revokedAt);
|
|
23046
22059
|
};
|
|
23047
22060
|
const namedAccountsInUse = () => anyUsersExist(opts.db);
|
|
@@ -23136,12 +22149,12 @@ async function createServer(opts) {
|
|
|
23136
22149
|
return reply.send({ authenticated: true });
|
|
23137
22150
|
}
|
|
23138
22151
|
if (apiKey) {
|
|
23139
|
-
const key = opts.db.select().from(apiKeys).where(
|
|
22152
|
+
const key = opts.db.select().from(apiKeys).where(eq29(apiKeys.keyHash, hashApiKey2(apiKey))).get();
|
|
23140
22153
|
if (!key || key.revokedAt) {
|
|
23141
22154
|
const err2 = authInvalid();
|
|
23142
22155
|
return reply.status(err2.statusCode).send(err2.toJSON());
|
|
23143
22156
|
}
|
|
23144
|
-
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(
|
|
22157
|
+
opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq29(apiKeys.id, key.id)).run();
|
|
23145
22158
|
const sessionId = createSession(key.id);
|
|
23146
22159
|
reply.header(
|
|
23147
22160
|
"set-cookie",
|
|
@@ -23344,8 +22357,8 @@ async function createServer(opts) {
|
|
|
23344
22357
|
getAgentPin: () => describeAgentPin(opts.config),
|
|
23345
22358
|
getGoogleMarketingDoctorInput: (ctx) => {
|
|
23346
22359
|
if (!ctx.project) return null;
|
|
23347
|
-
const googleAds = opts.db.select().from(googleAdsConnections).where(
|
|
23348
|
-
const gtm = opts.db.select().from(gtmConnections).where(
|
|
22360
|
+
const googleAds = opts.db.select().from(googleAdsConnections).where(eq29(googleAdsConnections.projectId, ctx.project.id)).get();
|
|
22361
|
+
const gtm = opts.db.select().from(gtmConnections).where(eq29(gtmConnections.projectId, ctx.project.id)).get();
|
|
23349
22362
|
const googleAdsCredential = getGoogleAdsConnection(opts.config, ctx.project.id);
|
|
23350
22363
|
const gtmCredential = getGtmConnection(opts.config, ctx.project.id);
|
|
23351
22364
|
const googleAdsConnected = Boolean(googleAdsCredential?.accessToken);
|
|
@@ -23596,7 +22609,7 @@ async function createServer(opts) {
|
|
|
23596
22609
|
...inspectOpts,
|
|
23597
22610
|
config: opts.config
|
|
23598
22611
|
}).then(() => {
|
|
23599
|
-
const finished = opts.db.select({ status: runs.status }).from(runs).where(
|
|
22612
|
+
const finished = opts.db.select({ status: runs.status }).from(runs).where(eq29(runs.id, runId)).get();
|
|
23600
22613
|
if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
|
|
23601
22614
|
return maybeRefreshGscCoverage(
|
|
23602
22615
|
opts.db,
|
|
@@ -23799,6 +22812,22 @@ async function createServer(opts) {
|
|
|
23799
22812
|
}
|
|
23800
22813
|
});
|
|
23801
22814
|
},
|
|
22815
|
+
onCompetitorAliasesChanged: (projectId, projectName) => {
|
|
22816
|
+
setImmediate(() => {
|
|
22817
|
+
try {
|
|
22818
|
+
const result2 = backfillProjectAnswerMentions(opts.db, projectId, { competitorFieldsOnly: true });
|
|
22819
|
+
app.log.info(
|
|
22820
|
+
{ projectId, projectName, ...result2 },
|
|
22821
|
+
"competitor aliases changed: recomputed competitor fields on historical snapshots"
|
|
22822
|
+
);
|
|
22823
|
+
} catch (err) {
|
|
22824
|
+
app.log.error(
|
|
22825
|
+
{ err, projectId, projectName },
|
|
22826
|
+
"competitor-alias-triggered backfill failed"
|
|
22827
|
+
);
|
|
22828
|
+
}
|
|
22829
|
+
});
|
|
22830
|
+
},
|
|
23802
22831
|
operatorApiKeyIds,
|
|
23803
22832
|
listOperationalLogs: (query) => operationalLogs.list(query),
|
|
23804
22833
|
getTelemetryStatus,
|
|
@@ -24079,7 +23108,7 @@ async function createServer(opts) {
|
|
|
24079
23108
|
notify: (projectId, result2) => notifier.onSiteLivenessChecked(projectId, result2)
|
|
24080
23109
|
});
|
|
24081
23110
|
stopProviderBatchPoller = startProviderBatchPoller({ db: opts.db, registry, runner: jobRunner });
|
|
24082
|
-
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(
|
|
23111
|
+
for (const run of opts.db.select({ id: researchRuns.id, projectId: researchRuns.projectId }).from(researchRuns).where(eq29(researchRuns.status, ResearchRunStatuses.queued)).all()) {
|
|
24083
23112
|
dispatchResearchRun(run.id, run.projectId);
|
|
24084
23113
|
}
|
|
24085
23114
|
runtimeStartupSettled = true;
|
|
@@ -24169,15 +23198,12 @@ export {
|
|
|
24169
23198
|
detectAndTrackUpgrade,
|
|
24170
23199
|
trackEvent,
|
|
24171
23200
|
setTelemetryPreference,
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24175
|
-
|
|
24176
|
-
|
|
24177
|
-
|
|
24178
|
-
backfillInsightsCommand,
|
|
24179
|
-
backfillSnapshotAttributionCommand,
|
|
24180
|
-
backfillTrafficClassificationCommand,
|
|
23201
|
+
OPENAI_RETRIEVAL_CONTRACT,
|
|
23202
|
+
reparseStoredResult2 as reparseStoredResult,
|
|
23203
|
+
reparseStoredResult3 as reparseStoredResult2,
|
|
23204
|
+
reparseStoredResult as reparseStoredResult3,
|
|
23205
|
+
reparseStoredResult4,
|
|
23206
|
+
reparseStoredResult5,
|
|
24181
23207
|
getCloudflareTrafficConnectionBySourceId,
|
|
24182
23208
|
setGoogleAuthConfig,
|
|
24183
23209
|
formatAuditFactorScore,
|