@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
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node --import tsx
|
|
2
2
|
import {
|
|
3
3
|
AERO_TOOL_PROFILES,
|
|
4
|
-
|
|
5
|
-
backfillAiReferralPathsCommand,
|
|
6
|
-
backfillAnswerMentionsCommand,
|
|
7
|
-
backfillAnswerVisibilityCommand,
|
|
8
|
-
backfillInsightsCommand,
|
|
9
|
-
backfillNormalizedPaths,
|
|
10
|
-
backfillNormalizedPathsCommand,
|
|
11
|
-
backfillSnapshotAttributionCommand,
|
|
12
|
-
backfillTrafficClassificationCommand,
|
|
4
|
+
OPENAI_RETRIEVAL_CONTRACT,
|
|
13
5
|
checkLatestVersionForCli,
|
|
14
6
|
cliRuntimeContext,
|
|
15
7
|
coerceAgentProvider,
|
|
@@ -28,6 +20,11 @@ import {
|
|
|
28
20
|
listAgentProviders,
|
|
29
21
|
notePrintedUpdateAvailable,
|
|
30
22
|
readCachedUpdateAvailable,
|
|
23
|
+
reparseStoredResult,
|
|
24
|
+
reparseStoredResult2,
|
|
25
|
+
reparseStoredResult3,
|
|
26
|
+
reparseStoredResult4,
|
|
27
|
+
reparseStoredResult5,
|
|
31
28
|
setGoogleAuthConfig,
|
|
32
29
|
setTelemetryPreference,
|
|
33
30
|
setTelemetrySource,
|
|
@@ -35,11 +32,11 @@ import {
|
|
|
35
32
|
trackCliCommandFinished,
|
|
36
33
|
trackEvent,
|
|
37
34
|
waitForServerRuntimeStartup
|
|
38
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-MQPBOQUH.js";
|
|
39
36
|
import {
|
|
40
37
|
autoSyncSkills,
|
|
41
38
|
formatAutoSyncNotice
|
|
42
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-3JEUZ2K5.js";
|
|
43
40
|
import {
|
|
44
41
|
configExists,
|
|
45
42
|
createApiClient,
|
|
@@ -56,13 +53,13 @@ import {
|
|
|
56
53
|
saveConfig,
|
|
57
54
|
saveConfigPatch,
|
|
58
55
|
shouldPrintServeSkillsNudge
|
|
59
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-RVELNJAW.js";
|
|
60
57
|
import {
|
|
61
58
|
DEFAULT_DEMO_TRUSTED_PROXIES,
|
|
62
59
|
trustedProxiesProblem,
|
|
63
60
|
trustedProxyProblem
|
|
64
61
|
} from "./chunk-RB2F4D3Z.js";
|
|
65
|
-
import "./chunk-
|
|
62
|
+
import "./chunk-QND5LEAC.js";
|
|
66
63
|
import {
|
|
67
64
|
CliError,
|
|
68
65
|
EXIT_SYSTEM_ERROR,
|
|
@@ -74,22 +71,36 @@ import {
|
|
|
74
71
|
runMeasurementDraftAction,
|
|
75
72
|
systemError,
|
|
76
73
|
usageError
|
|
77
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-FWV6IIYY.js";
|
|
78
75
|
import {
|
|
79
76
|
CLOUDFLARE_WORKER_BINDINGS,
|
|
80
77
|
CLOUDFLARE_WORKER_GENERATED_MARKER,
|
|
81
78
|
CLOUDFLARE_WRANGLER_GENERATED_MARKER,
|
|
82
79
|
DEFAULT_BOT_LIST,
|
|
80
|
+
aiUserFetchEventsHourly,
|
|
83
81
|
apiKeys,
|
|
82
|
+
auditLog,
|
|
83
|
+
backfillProjectAnswerMentions,
|
|
84
|
+
classifyAiUserFetch,
|
|
85
|
+
classifyCrawler,
|
|
86
|
+
competitors,
|
|
87
|
+
crawlerEventsHourly,
|
|
84
88
|
createClient,
|
|
89
|
+
createRunCompetitorResolver,
|
|
85
90
|
dataRepairCompletions,
|
|
91
|
+
determineCitationState,
|
|
92
|
+
gaAiReferrals,
|
|
93
|
+
gaTrafficSnapshots,
|
|
86
94
|
generateWorkerScript,
|
|
87
95
|
generateWranglerToml,
|
|
88
96
|
migrate,
|
|
97
|
+
parseJsonColumn,
|
|
89
98
|
projects,
|
|
90
99
|
queries,
|
|
100
|
+
querySnapshots,
|
|
101
|
+
rawEventSamples,
|
|
91
102
|
runs
|
|
92
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-XODXE6PT.js";
|
|
93
104
|
import {
|
|
94
105
|
AdsDeliverySnapshotStatuses,
|
|
95
106
|
AdsHistoricalCampaignRollupStatuses,
|
|
@@ -102,19 +113,25 @@ import {
|
|
|
102
113
|
CheckScopes,
|
|
103
114
|
CheckStatuses,
|
|
104
115
|
CitationStates,
|
|
116
|
+
DiscoveryPromoteCompetitorSkipReasons,
|
|
105
117
|
FEEDBACK_KINDS,
|
|
106
118
|
GaMoverChangeBases,
|
|
107
119
|
MeasurementEvidenceShapes,
|
|
108
120
|
OUTSTANDING_PROVIDER_BATCH_STATUSES,
|
|
109
121
|
ProviderBatchStatuses,
|
|
122
|
+
ProviderNames,
|
|
110
123
|
READ_ONLY_SCOPE,
|
|
111
124
|
RatioUnits,
|
|
112
125
|
ResearchRunStatuses,
|
|
126
|
+
RetrievalContracts,
|
|
113
127
|
RunKinds,
|
|
114
128
|
RunStatuses,
|
|
115
129
|
RunTriggers,
|
|
130
|
+
TrafficEventConfidences,
|
|
116
131
|
TrafficEventKinds,
|
|
132
|
+
TrafficEvidenceKinds,
|
|
117
133
|
TrafficSeriesGranularities,
|
|
134
|
+
TrafficSourceTypes,
|
|
118
135
|
UNATTRIBUTED_MENTION_REASON,
|
|
119
136
|
URL_PATH_NORMALIZATION_VERSION,
|
|
120
137
|
USER_PASSWORD_MIN_LENGTH,
|
|
@@ -137,13 +154,17 @@ import {
|
|
|
137
154
|
buildModelChangeNotice,
|
|
138
155
|
canonicalizeGtmAccountId,
|
|
139
156
|
canonicalizeGtmResourceSelection,
|
|
157
|
+
computeCompetitorOverlap,
|
|
140
158
|
contentTargetDismissRequestSchema,
|
|
141
159
|
conversionTrackingContractWriteRequestSchema,
|
|
142
160
|
deduplicateResearchQueries,
|
|
143
161
|
describeError,
|
|
162
|
+
determineAnswerMentioned,
|
|
144
163
|
discoveryBucketSchema,
|
|
145
164
|
discoveryCompetitorTypeSchema,
|
|
165
|
+
effectiveBrandNames,
|
|
146
166
|
effectiveDomains,
|
|
167
|
+
extractRecommendedCompetitors,
|
|
147
168
|
factorStatusFromScore,
|
|
148
169
|
feedbackSubmissionSchema,
|
|
149
170
|
formatGbpMetricLabel,
|
|
@@ -172,8 +193,11 @@ import {
|
|
|
172
193
|
measurementQueryTemplateApplyRequestSchema,
|
|
173
194
|
measurementQueryTemplateUpsertRequestSchema,
|
|
174
195
|
measurementQuestionResultQuerySchema,
|
|
196
|
+
normalizeCompetitorAliases,
|
|
197
|
+
normalizeCompetitorDomain,
|
|
175
198
|
normalizeProjectAliases,
|
|
176
199
|
normalizeTelemetryStatus,
|
|
200
|
+
normalizeUrlPath,
|
|
177
201
|
notificationEventSchema,
|
|
178
202
|
parseTrafficAnalyticsPeriod,
|
|
179
203
|
providerDispatchModeSchema,
|
|
@@ -200,7 +224,7 @@ import {
|
|
|
200
224
|
visibilityCompareSelectionSchema,
|
|
201
225
|
visibilityReportRequestSchema,
|
|
202
226
|
winnabilityClassSchema
|
|
203
|
-
} from "./chunk-
|
|
227
|
+
} from "./chunk-YQXQBFHT.js";
|
|
204
228
|
|
|
205
229
|
// src/cli.ts
|
|
206
230
|
import { pathToFileURL } from "url";
|
|
@@ -471,6 +495,19 @@ function ratedAnswersLine({ ratedAnswers, eligibleAnswers, ratedAnswerRate }) {
|
|
|
471
495
|
if (ratedAnswerRate === null || ratedAnswerRate === void 0) return `Rated: Unavailable \xB7 ${eligibleAnswers} eligible ${eligibleAnswers === 1 ? "answer" : "answers"}`;
|
|
472
496
|
return `Rated: ${formatPercent(ratedAnswerRate, RatioUnits.fraction)} \xB7 ${ratedAnswers} of ${eligibleAnswers} ${eligibleAnswers === 1 ? "answer" : "answers"} rated`;
|
|
473
497
|
}
|
|
498
|
+
var CRITICIZED_PROPERTIES_TITLE = "Most criticized properties";
|
|
499
|
+
function printCriticizedProperties(value) {
|
|
500
|
+
const ranked = value.criticizedProperties;
|
|
501
|
+
if (!ranked?.keys.length) return;
|
|
502
|
+
const rows = new Map(value.breakdowns.filter((row) => row.dimension === "property").map((row) => [row.key, row]));
|
|
503
|
+
const listed = ranked.keys.flatMap((key) => rows.get(key) ?? []);
|
|
504
|
+
if (listed.length === 0) return;
|
|
505
|
+
console.log(`${CRITICIZED_PROPERTIES_TITLE}: ${listed.length} of ${ranked.total}`);
|
|
506
|
+
for (const row of listed) {
|
|
507
|
+
const { favorable, mixed, unfavorable } = row.coverage.counts;
|
|
508
|
+
console.log(` ${row.label} (${row.key}): ${favorable} favorable \xB7 ${mixed} mixed \xB7 ${unfavorable} unfavorable \xB7 ${row.coverage.judged} judged`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
474
511
|
function printSummary(value) {
|
|
475
512
|
const label = value.selection.queryClass === "branded" ? "Branded" : "Non-brand";
|
|
476
513
|
console.log(`${label} sentiment: ${value.state}${value.provisional ? " \xB7 provisional" : ""}`);
|
|
@@ -497,6 +534,7 @@ function printSummary(value) {
|
|
|
497
534
|
}
|
|
498
535
|
}
|
|
499
536
|
for (const row of value.breakdowns) if (row.dimension !== "query") console.log(`${row.dimension} ${row.label} \xB7 ${row.queryClass}: ${row.score.favorableDisplay} favorable \xB7 ${row.coverage.judged} of ${row.coverage.selected} judged`);
|
|
537
|
+
printCriticizedProperties(value);
|
|
500
538
|
if (value.queryPage) console.log(`Query rows: ${value.queries.length} of ${value.queryPage.total}`);
|
|
501
539
|
if (value.queryPage?.nextCursor) console.log(`Next query cursor: ${value.queryPage.nextCursor}`);
|
|
502
540
|
}
|
|
@@ -511,7 +549,8 @@ async function configureSentiment(project2, request, format) {
|
|
|
511
549
|
async function showSentimentEvidence(project2, query, format) {
|
|
512
550
|
const value = await createApiClient().getSentimentEvidence(project2, query);
|
|
513
551
|
if (machine(value, format)) return;
|
|
514
|
-
|
|
552
|
+
const outcomes = value.selection.outcome?.length ? ` \xB7 outcomes ${value.selection.outcome.join(", ")}` : "";
|
|
553
|
+
console.log(`${value.selection.queryClass} sentiment evidence: ${value.state} \xB7 definition ${value.selection.evaluationDefinitionId ?? "not measured"}${outcomes}`);
|
|
515
554
|
for (const item of value.items) {
|
|
516
555
|
console.log(`
|
|
517
556
|
${item.subject.displayName} \xB7 ${item.outcome} \xB7 ${item.context.provider} \xB7 run ${item.runId}`);
|
|
@@ -629,12 +668,13 @@ var SENTIMENT_CLI_COMMANDS = [
|
|
|
629
668
|
},
|
|
630
669
|
{
|
|
631
670
|
path: ["sentiment", "evidence"],
|
|
632
|
-
usage: "canonry sentiment evidence <project> [selection options] [--assessment-id <id>] [--cursor <cursor>] [--limit 1..100] [--format json]",
|
|
633
|
-
help: `${readSelectionHelp} Use --assessment-id from a query assessment to read that exact stored verdict; existing scope filters still apply. Keep all selection fields, including assessmentId and the returned evaluationDefinitionId, when following a cursor. JSONL preserves the entire page envelope, including empty state and next cursor.`,
|
|
634
|
-
options: { ...readSelectionOptions, "assessment-id": stringOption(), cursor: stringOption(), limit: stringOption() },
|
|
671
|
+
usage: "canonry sentiment evidence <project> [selection options] [--assessment-id <id>] [--outcome mixed,unfavorable] [--cursor <cursor>] [--limit 1..100] [--format json]",
|
|
672
|
+
help: `${readSelectionHelp} Use --assessment-id from a query assessment to read that exact stored verdict; existing scope filters still apply. --outcome keeps only assessments with those outcomes (comma-separated or repeated, for example --outcome mixed,unfavorable for the criticism behind a branded score or the non-brand exceptions); it never changes how they were scored. Keep all selection fields, including assessmentId, outcome and the returned evaluationDefinitionId, when following a cursor. JSONL preserves the entire page envelope, including empty state and next cursor.`,
|
|
673
|
+
options: { ...readSelectionOptions, "assessment-id": stringOption(), outcome: multiStringOption(), cursor: stringOption(), limit: stringOption() },
|
|
635
674
|
run: (input) => showSentimentEvidence(project(input, "evidence"), parse(sentimentEvidenceRequestSchema, {
|
|
636
675
|
...readSelection(input),
|
|
637
676
|
assessmentId: getString(input.values, "assessment-id"),
|
|
677
|
+
outcome: getStringArray(input.values, "outcome")?.join(","),
|
|
638
678
|
cursor: getString(input.values, "cursor"),
|
|
639
679
|
limit: getString(input.values, "limit")
|
|
640
680
|
}), input.format)
|
|
@@ -734,7 +774,7 @@ function parseDemoTrustedProxies(values) {
|
|
|
734
774
|
async function demoCommand(options) {
|
|
735
775
|
const { host, port } = parseDemoListenOptions(options);
|
|
736
776
|
const trustProxy = parseDemoTrustedProxies(options.trustProxy);
|
|
737
|
-
const { createDemoServer } = await import("./demo-server-
|
|
777
|
+
const { createDemoServer } = await import("./demo-server-BXABH765.js");
|
|
738
778
|
const app = await createDemoServer({ trustProxy });
|
|
739
779
|
let stopping = false;
|
|
740
780
|
const stop = () => {
|
|
@@ -780,6 +820,908 @@ var DEMO_CLI_COMMANDS = [{
|
|
|
780
820
|
}
|
|
781
821
|
}];
|
|
782
822
|
|
|
823
|
+
// src/commands/backfill.ts
|
|
824
|
+
import { setImmediate as yieldToEventLoop } from "timers/promises";
|
|
825
|
+
import { and, asc, desc, eq, gt, inArray, isNull, lte, sql } from "drizzle-orm";
|
|
826
|
+
var SNAPSHOT_BATCH_SIZE = 500;
|
|
827
|
+
var PATH_REPAIR_PAGE_SIZE = 128;
|
|
828
|
+
async function backfillAnswerVisibilityCommand(opts) {
|
|
829
|
+
const config = loadConfig();
|
|
830
|
+
const db = createClient(config.database);
|
|
831
|
+
migrate(db);
|
|
832
|
+
const projectFilter = opts?.project?.trim();
|
|
833
|
+
const isDryRun = opts?.dryRun === true;
|
|
834
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
835
|
+
let examined = 0;
|
|
836
|
+
let updated = 0;
|
|
837
|
+
let wouldUpdate = 0;
|
|
838
|
+
let mentioned = 0;
|
|
839
|
+
let reparsed = 0;
|
|
840
|
+
let providerErrors = 0;
|
|
841
|
+
let retrievalRelabeled = 0;
|
|
842
|
+
if (scopedProjects.length > 0) {
|
|
843
|
+
const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(and(
|
|
844
|
+
eq(runs.kind, RunKinds["answer-visibility"]),
|
|
845
|
+
inArray(runs.projectId, scopedProjects.map((project2) => project2.id))
|
|
846
|
+
)).all() : db.select({ id: runs.id, projectId: runs.projectId, planVersionId: runs.measurementPlanVersionId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
|
|
847
|
+
const planVersionByRun = new Map(runRows.map((run) => [run.id, run.planVersionId]));
|
|
848
|
+
const runIdsByProject = /* @__PURE__ */ new Map();
|
|
849
|
+
for (const run of runRows) {
|
|
850
|
+
const existing = runIdsByProject.get(run.projectId);
|
|
851
|
+
if (existing) existing.push(run.id);
|
|
852
|
+
else runIdsByProject.set(run.projectId, [run.id]);
|
|
853
|
+
}
|
|
854
|
+
for (const project2 of scopedProjects) {
|
|
855
|
+
const competitorsForRun = createRunCompetitorResolver(db, db.select({ domain: competitors.domain, aliases: competitors.aliases }).from(competitors).where(eq(competitors.projectId, project2.id)).all());
|
|
856
|
+
const runIds = runIdsByProject.get(project2.id) ?? [];
|
|
857
|
+
if (runIds.length === 0) continue;
|
|
858
|
+
const projectDomains = effectiveDomains({
|
|
859
|
+
canonicalDomain: project2.canonicalDomain,
|
|
860
|
+
ownedDomains: project2.ownedDomains
|
|
861
|
+
});
|
|
862
|
+
const projectBrandNames = effectiveBrandNames({
|
|
863
|
+
displayName: project2.displayName,
|
|
864
|
+
aliases: project2.aliases
|
|
865
|
+
});
|
|
866
|
+
for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
|
|
867
|
+
const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
|
|
868
|
+
const snapshotRows = db.select({
|
|
869
|
+
id: querySnapshots.id,
|
|
870
|
+
runId: querySnapshots.runId,
|
|
871
|
+
executionId: querySnapshots.measurementExecutionId,
|
|
872
|
+
provider: querySnapshots.provider,
|
|
873
|
+
citationState: querySnapshots.citationState,
|
|
874
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
875
|
+
answerText: querySnapshots.answerText,
|
|
876
|
+
citedDomains: querySnapshots.citedDomains,
|
|
877
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
878
|
+
recommendedCompetitors: querySnapshots.recommendedCompetitors,
|
|
879
|
+
rawResponse: querySnapshots.rawResponse,
|
|
880
|
+
retrievalStatus: querySnapshots.retrievalStatus,
|
|
881
|
+
retrievalContract: querySnapshots.retrievalContract
|
|
882
|
+
}).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
|
|
883
|
+
const pendingUpdates = [];
|
|
884
|
+
for (const snapshot of snapshotRows) {
|
|
885
|
+
examined++;
|
|
886
|
+
const reparsedResult = reparseProviderSnapshot(snapshot.provider, snapshot.rawResponse);
|
|
887
|
+
if (reparsedResult) reparsed++;
|
|
888
|
+
if (reparsedResult?.providerError) providerErrors++;
|
|
889
|
+
const answerText = reparsedResult?.answerText ?? snapshot.answerText ?? "";
|
|
890
|
+
const nextValue = determineAnswerMentioned(answerText, projectBrandNames, projectDomains);
|
|
891
|
+
if (nextValue) mentioned++;
|
|
892
|
+
const nextPatch = {};
|
|
893
|
+
if (snapshot.answerMentioned !== nextValue) {
|
|
894
|
+
nextPatch.answerMentioned = nextValue;
|
|
895
|
+
}
|
|
896
|
+
if ((snapshot.answerText ?? "") !== answerText) {
|
|
897
|
+
nextPatch.answerText = answerText;
|
|
898
|
+
}
|
|
899
|
+
if (reparsedResult) {
|
|
900
|
+
const normalized = {
|
|
901
|
+
provider: snapshot.provider,
|
|
902
|
+
answerText,
|
|
903
|
+
citedDomains: reparsedResult.citedDomains,
|
|
904
|
+
groundingSources: reparsedResult.groundingSources,
|
|
905
|
+
searchQueries: reparsedResult.searchQueries,
|
|
906
|
+
// Only feeds the citation/mention helpers below, which ignore
|
|
907
|
+
// it. Retrieval is written back solely by the narrow OpenAI
|
|
908
|
+
// contract correction after this block.
|
|
909
|
+
retrievalStatus: "unknown"
|
|
910
|
+
};
|
|
911
|
+
const nextCitationState = determineCitationState(normalized, projectDomains);
|
|
912
|
+
const nextCitedDomains = reparsedResult.citedDomains;
|
|
913
|
+
const runCompetitors = competitorsForRun(planVersionByRun.get(snapshot.runId), snapshot.executionId);
|
|
914
|
+
const nextCompetitorOverlap = computeCompetitorOverlap(normalized, runCompetitors.domains, runCompetitors.aliases);
|
|
915
|
+
const nextRecommendedCompetitors = extractRecommendedCompetitors(
|
|
916
|
+
normalized.answerText,
|
|
917
|
+
projectDomains,
|
|
918
|
+
normalized.citedDomains,
|
|
919
|
+
runCompetitors.domains,
|
|
920
|
+
projectBrandNames,
|
|
921
|
+
runCompetitors.aliases
|
|
922
|
+
);
|
|
923
|
+
const nextRawResponse = stringifyStoredSnapshotEnvelope(
|
|
924
|
+
snapshot.rawResponse,
|
|
925
|
+
reparsedResult
|
|
926
|
+
);
|
|
927
|
+
if (snapshot.citationState !== nextCitationState) {
|
|
928
|
+
nextPatch.citationState = nextCitationState;
|
|
929
|
+
}
|
|
930
|
+
if (JSON.stringify(snapshot.citedDomains) !== JSON.stringify(nextCitedDomains)) {
|
|
931
|
+
nextPatch.citedDomains = nextCitedDomains;
|
|
932
|
+
}
|
|
933
|
+
if (JSON.stringify(snapshot.competitorOverlap) !== JSON.stringify(nextCompetitorOverlap)) {
|
|
934
|
+
nextPatch.competitorOverlap = nextCompetitorOverlap;
|
|
935
|
+
}
|
|
936
|
+
if (JSON.stringify(snapshot.recommendedCompetitors) !== JSON.stringify(nextRecommendedCompetitors)) {
|
|
937
|
+
nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
|
|
938
|
+
}
|
|
939
|
+
if (snapshot.rawResponse !== nextRawResponse) {
|
|
940
|
+
nextPatch.rawResponse = nextRawResponse;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const retrievalCorrection = correctStoredOpenAIRetrieval(snapshot, reparsedResult);
|
|
944
|
+
if (retrievalCorrection) {
|
|
945
|
+
retrievalRelabeled++;
|
|
946
|
+
Object.assign(nextPatch, retrievalCorrection);
|
|
947
|
+
}
|
|
948
|
+
if (Object.keys(nextPatch).length > 0) {
|
|
949
|
+
pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (pendingUpdates.length > 0) {
|
|
953
|
+
if (isDryRun) {
|
|
954
|
+
wouldUpdate += pendingUpdates.length;
|
|
955
|
+
} else {
|
|
956
|
+
db.transaction((tx) => {
|
|
957
|
+
for (const update of pendingUpdates) {
|
|
958
|
+
tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
updated += pendingUpdates.length;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const result = {
|
|
968
|
+
project: projectFilter ?? null,
|
|
969
|
+
projects: scopedProjects.length,
|
|
970
|
+
examined,
|
|
971
|
+
updated,
|
|
972
|
+
mentioned,
|
|
973
|
+
reparsed,
|
|
974
|
+
providerErrors,
|
|
975
|
+
retrievalRelabeled
|
|
976
|
+
};
|
|
977
|
+
if (isDryRun) {
|
|
978
|
+
result.dryRun = true;
|
|
979
|
+
result.wouldUpdate = wouldUpdate;
|
|
980
|
+
}
|
|
981
|
+
if (isMachineFormat(opts?.format)) {
|
|
982
|
+
console.log(JSON.stringify(result, null, 2));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
console.log(`Answer visibility backfill ${isDryRun ? "preview" : "complete"}.
|
|
986
|
+
`);
|
|
987
|
+
if (projectFilter) {
|
|
988
|
+
console.log(` Project: ${projectFilter}`);
|
|
989
|
+
}
|
|
990
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
991
|
+
console.log(` Examined: ${examined}`);
|
|
992
|
+
if (isDryRun) {
|
|
993
|
+
console.log(` Would update: ${wouldUpdate}`);
|
|
994
|
+
} else {
|
|
995
|
+
console.log(` Updated: ${updated}`);
|
|
996
|
+
}
|
|
997
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
998
|
+
console.log(` Reparsed: ${reparsed}`);
|
|
999
|
+
console.log(` Errors: ${providerErrors}`);
|
|
1000
|
+
console.log(` Relabeled: ${retrievalRelabeled} (OpenAI retrieval contract)`);
|
|
1001
|
+
if (isDryRun) {
|
|
1002
|
+
console.log(`
|
|
1003
|
+
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function backfillNormalizedPaths(db, opts) {
|
|
1007
|
+
return repairStoredPaths(db, gaTrafficSnapshots, opts);
|
|
1008
|
+
}
|
|
1009
|
+
async function repairStoredPaths(db, table, opts) {
|
|
1010
|
+
const pageSize = opts?.pageSize ?? PATH_REPAIR_PAGE_SIZE;
|
|
1011
|
+
const projectScope = opts?.projectId ? sql`+${table.projectId} = ${opts.projectId}` : void 0;
|
|
1012
|
+
const lastId = db.select({ id: table.id }).from(table).where(projectScope).orderBy(desc(table.id)).limit(1).get()?.id;
|
|
1013
|
+
if (lastId === void 0) return { examined: 0, updated: 0, unchanged: 0 };
|
|
1014
|
+
let cursor;
|
|
1015
|
+
let examined = 0;
|
|
1016
|
+
let updated = 0;
|
|
1017
|
+
for (; ; ) {
|
|
1018
|
+
const rows = db.select({
|
|
1019
|
+
id: table.id,
|
|
1020
|
+
projectId: table.projectId,
|
|
1021
|
+
landingPage: table.landingPage,
|
|
1022
|
+
landingPageNormalized: table.landingPageNormalized
|
|
1023
|
+
}).from(table).where(and(
|
|
1024
|
+
projectScope,
|
|
1025
|
+
lte(table.id, lastId),
|
|
1026
|
+
cursor === void 0 ? void 0 : gt(table.id, cursor)
|
|
1027
|
+
)).orderBy(asc(table.id)).limit(pageSize).all();
|
|
1028
|
+
examined += rows.length;
|
|
1029
|
+
const changes = rows.flatMap((row) => {
|
|
1030
|
+
const next = normalizeUrlPath(row.landingPage);
|
|
1031
|
+
return next === null || row.landingPageNormalized === next ? [] : [{ row, next }];
|
|
1032
|
+
});
|
|
1033
|
+
if (changes.length > 0) {
|
|
1034
|
+
const written = db.transaction((tx) => {
|
|
1035
|
+
let written2 = 0;
|
|
1036
|
+
for (const { row, next } of changes) {
|
|
1037
|
+
written2 += tx.update(table).set({ landingPageNormalized: next }).where(and(
|
|
1038
|
+
eq(table.id, row.id),
|
|
1039
|
+
eq(table.projectId, row.projectId),
|
|
1040
|
+
eq(table.landingPage, row.landingPage),
|
|
1041
|
+
row.landingPageNormalized === null ? isNull(table.landingPageNormalized) : eq(table.landingPageNormalized, row.landingPageNormalized)
|
|
1042
|
+
)).run().changes;
|
|
1043
|
+
}
|
|
1044
|
+
return written2;
|
|
1045
|
+
});
|
|
1046
|
+
updated += written;
|
|
1047
|
+
if (written < changes.length) opts?.onConflict?.();
|
|
1048
|
+
}
|
|
1049
|
+
if (rows.length < pageSize) break;
|
|
1050
|
+
cursor = rows.at(-1).id;
|
|
1051
|
+
if (cursor === lastId) break;
|
|
1052
|
+
await yieldToEventLoop();
|
|
1053
|
+
}
|
|
1054
|
+
return { examined, updated, unchanged: examined - updated };
|
|
1055
|
+
}
|
|
1056
|
+
async function backfillNormalizedPathsCommand(opts) {
|
|
1057
|
+
const config = loadConfig();
|
|
1058
|
+
const db = createClient(config.database);
|
|
1059
|
+
migrate(db);
|
|
1060
|
+
const projectFilter = opts?.project?.trim();
|
|
1061
|
+
let projectId;
|
|
1062
|
+
if (projectFilter) {
|
|
1063
|
+
const project2 = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
|
|
1064
|
+
if (!project2) {
|
|
1065
|
+
const result2 = {
|
|
1066
|
+
project: projectFilter,
|
|
1067
|
+
examined: 0,
|
|
1068
|
+
updated: 0,
|
|
1069
|
+
unchanged: 0
|
|
1070
|
+
};
|
|
1071
|
+
if (isMachineFormat(opts?.format)) {
|
|
1072
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
console.log(`Backfill normalized-paths: project "${projectFilter}" not found.`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
projectId = project2.id;
|
|
1079
|
+
}
|
|
1080
|
+
const { examined, updated, unchanged } = await backfillNormalizedPaths(db, { projectId });
|
|
1081
|
+
const result = {
|
|
1082
|
+
project: projectFilter ?? null,
|
|
1083
|
+
examined,
|
|
1084
|
+
updated,
|
|
1085
|
+
unchanged
|
|
1086
|
+
};
|
|
1087
|
+
if (isMachineFormat(opts?.format)) {
|
|
1088
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
console.log("Normalized-path backfill complete.\n");
|
|
1092
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
1093
|
+
console.log(` Examined: ${examined}`);
|
|
1094
|
+
console.log(` Updated: ${updated}`);
|
|
1095
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
1096
|
+
}
|
|
1097
|
+
function backfillAiReferralPaths(db, opts) {
|
|
1098
|
+
return repairStoredPaths(db, gaAiReferrals, opts);
|
|
1099
|
+
}
|
|
1100
|
+
async function backfillAiReferralPathsCommand(opts) {
|
|
1101
|
+
const config = loadConfig();
|
|
1102
|
+
const db = createClient(config.database);
|
|
1103
|
+
migrate(db);
|
|
1104
|
+
const projectFilter = opts?.project?.trim();
|
|
1105
|
+
let projectId;
|
|
1106
|
+
if (projectFilter) {
|
|
1107
|
+
const project2 = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
|
|
1108
|
+
if (!project2) {
|
|
1109
|
+
const result2 = {
|
|
1110
|
+
project: projectFilter,
|
|
1111
|
+
examined: 0,
|
|
1112
|
+
updated: 0,
|
|
1113
|
+
unchanged: 0
|
|
1114
|
+
};
|
|
1115
|
+
if (isMachineFormat(opts?.format)) {
|
|
1116
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
projectId = project2.id;
|
|
1123
|
+
}
|
|
1124
|
+
const { examined, updated, unchanged } = await backfillAiReferralPaths(db, { projectId });
|
|
1125
|
+
const result = {
|
|
1126
|
+
project: projectFilter ?? null,
|
|
1127
|
+
examined,
|
|
1128
|
+
updated,
|
|
1129
|
+
unchanged
|
|
1130
|
+
};
|
|
1131
|
+
if (isMachineFormat(opts?.format)) {
|
|
1132
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
console.log("AI referral landing-page backfill complete.\n");
|
|
1136
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
1137
|
+
console.log(` Examined: ${examined}`);
|
|
1138
|
+
console.log(` Updated: ${updated}`);
|
|
1139
|
+
console.log(` Unchanged: ${unchanged}`);
|
|
1140
|
+
}
|
|
1141
|
+
async function backfillAnswerMentionsCommand(opts) {
|
|
1142
|
+
const config = loadConfig();
|
|
1143
|
+
const db = createClient(config.database);
|
|
1144
|
+
migrate(db);
|
|
1145
|
+
const projectFilter = opts?.project?.trim();
|
|
1146
|
+
const isDryRun = opts?.dryRun === true;
|
|
1147
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
1148
|
+
let examined = 0;
|
|
1149
|
+
let updated = 0;
|
|
1150
|
+
let wouldUpdate = 0;
|
|
1151
|
+
let mentioned = 0;
|
|
1152
|
+
for (const project2 of scopedProjects) {
|
|
1153
|
+
const result2 = backfillProjectAnswerMentions(db, project2.id, { dryRun: isDryRun });
|
|
1154
|
+
examined += result2.examined;
|
|
1155
|
+
updated += result2.updated;
|
|
1156
|
+
wouldUpdate += result2.wouldUpdate ?? 0;
|
|
1157
|
+
mentioned += result2.mentioned;
|
|
1158
|
+
}
|
|
1159
|
+
const result = {
|
|
1160
|
+
project: projectFilter ?? null,
|
|
1161
|
+
projects: scopedProjects.length,
|
|
1162
|
+
examined,
|
|
1163
|
+
updated,
|
|
1164
|
+
mentioned
|
|
1165
|
+
};
|
|
1166
|
+
if (isDryRun) {
|
|
1167
|
+
result.dryRun = true;
|
|
1168
|
+
result.wouldUpdate = wouldUpdate;
|
|
1169
|
+
}
|
|
1170
|
+
if (isMachineFormat(opts?.format)) {
|
|
1171
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
console.log(`Answer mentions backfill ${isDryRun ? "preview" : "complete"}.
|
|
1175
|
+
`);
|
|
1176
|
+
if (projectFilter) console.log(` Project: ${projectFilter}`);
|
|
1177
|
+
console.log(` Projects: ${scopedProjects.length}`);
|
|
1178
|
+
console.log(` Examined: ${examined}`);
|
|
1179
|
+
if (isDryRun) {
|
|
1180
|
+
console.log(` Would update: ${wouldUpdate}`);
|
|
1181
|
+
} else {
|
|
1182
|
+
console.log(` Updated: ${updated}`);
|
|
1183
|
+
}
|
|
1184
|
+
console.log(` Mentioned: ${mentioned}`);
|
|
1185
|
+
if (isDryRun) {
|
|
1186
|
+
console.log(`
|
|
1187
|
+
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function backfillInsightsCommand(project2, opts) {
|
|
1191
|
+
const { IntelligenceService } = await import("./intelligence-service-EIWYQYVG.js");
|
|
1192
|
+
const config = loadConfig();
|
|
1193
|
+
const db = createClient(config.database);
|
|
1194
|
+
migrate(db);
|
|
1195
|
+
const service = new IntelligenceService(db);
|
|
1196
|
+
const isJson = isMachineFormat(opts?.format);
|
|
1197
|
+
const isDryRun = opts?.dryRun === true;
|
|
1198
|
+
if (!isJson) {
|
|
1199
|
+
const scope = opts?.since ? ` (since ${opts.since})` : "";
|
|
1200
|
+
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
1201
|
+
process.stderr.write(`Backfilling insights for "${project2}"${scope}${mode}...
|
|
1202
|
+
`);
|
|
1203
|
+
}
|
|
1204
|
+
const result = service.backfill(project2, {
|
|
1205
|
+
fromRunId: opts?.fromRun,
|
|
1206
|
+
toRunId: opts?.toRun,
|
|
1207
|
+
since: opts?.since,
|
|
1208
|
+
dryRun: isDryRun
|
|
1209
|
+
}, (info) => {
|
|
1210
|
+
if (!isJson) {
|
|
1211
|
+
process.stderr.write(` [${info.index}/${info.total}] ${info.runId} \u2014 ${info.insights} insights
|
|
1212
|
+
`);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
const output = {
|
|
1216
|
+
project: project2,
|
|
1217
|
+
processed: result.processed,
|
|
1218
|
+
skipped: result.skipped,
|
|
1219
|
+
totalInsights: result.totalInsights
|
|
1220
|
+
};
|
|
1221
|
+
if (result.dryRun) {
|
|
1222
|
+
output.dryRun = true;
|
|
1223
|
+
output.delta = result.delta;
|
|
1224
|
+
}
|
|
1225
|
+
if (isJson) {
|
|
1226
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
console.log(`
|
|
1230
|
+
Backfill ${isDryRun ? "preview" : "complete"}.`);
|
|
1231
|
+
console.log(` Processed: ${result.processed}`);
|
|
1232
|
+
console.log(` Skipped: ${result.skipped}`);
|
|
1233
|
+
console.log(` Insights: ${result.totalInsights}`);
|
|
1234
|
+
if (result.delta) {
|
|
1235
|
+
console.log(` Delta: -${result.delta.wouldDelete} existing +${result.delta.wouldCreate} new (net ${result.delta.netChange >= 0 ? "+" : ""}${result.delta.netChange})`);
|
|
1236
|
+
console.log(` No DB writes performed. Re-run without --dry-run to apply.`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function replayQueryAuditLog(events) {
|
|
1240
|
+
const active = [];
|
|
1241
|
+
for (const ev of events) {
|
|
1242
|
+
let diff;
|
|
1243
|
+
try {
|
|
1244
|
+
diff = ev.diff ? JSON.parse(ev.diff) : {};
|
|
1245
|
+
} catch {
|
|
1246
|
+
continue;
|
|
1247
|
+
}
|
|
1248
|
+
if (ev.action === "keywords.appended" || ev.action === "queries.appended") {
|
|
1249
|
+
const added = Array.isArray(diff.added) ? diff.added : [];
|
|
1250
|
+
for (const q of added) {
|
|
1251
|
+
active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
|
|
1252
|
+
}
|
|
1253
|
+
} else if (ev.action === "keywords.deleted" || ev.action === "queries.deleted") {
|
|
1254
|
+
const deleted = Array.isArray(diff.deleted) ? diff.deleted : [];
|
|
1255
|
+
for (const q of deleted) {
|
|
1256
|
+
for (let i = active.length - 1; i >= 0; i--) {
|
|
1257
|
+
if (active[i].text === q && active[i].deletedAt === null) {
|
|
1258
|
+
active[i].deletedAt = ev.createdAt;
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
} else if (ev.action === "queries.replaced") {
|
|
1264
|
+
const newSet = Array.isArray(diff.queries) ? diff.queries : [];
|
|
1265
|
+
for (const e of active) {
|
|
1266
|
+
if (e.deletedAt === null) e.deletedAt = ev.createdAt;
|
|
1267
|
+
}
|
|
1268
|
+
for (const q of newSet) {
|
|
1269
|
+
active.push({ text: q, addedAt: ev.createdAt, deletedAt: null });
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return active;
|
|
1274
|
+
}
|
|
1275
|
+
function activeQueriesAt(history, t) {
|
|
1276
|
+
return history.filter((e) => e.addedAt <= t && (e.deletedAt === null || e.deletedAt > t)).map((e) => e.text);
|
|
1277
|
+
}
|
|
1278
|
+
var CONTENT_MATCH_STOPWORDS = /* @__PURE__ */ new Set([
|
|
1279
|
+
"the",
|
|
1280
|
+
"a",
|
|
1281
|
+
"an",
|
|
1282
|
+
"and",
|
|
1283
|
+
"or",
|
|
1284
|
+
"but",
|
|
1285
|
+
"is",
|
|
1286
|
+
"are",
|
|
1287
|
+
"was",
|
|
1288
|
+
"were",
|
|
1289
|
+
"be",
|
|
1290
|
+
"been",
|
|
1291
|
+
"being",
|
|
1292
|
+
"have",
|
|
1293
|
+
"has",
|
|
1294
|
+
"had",
|
|
1295
|
+
"do",
|
|
1296
|
+
"does",
|
|
1297
|
+
"did",
|
|
1298
|
+
"will",
|
|
1299
|
+
"would",
|
|
1300
|
+
"could",
|
|
1301
|
+
"should",
|
|
1302
|
+
"may",
|
|
1303
|
+
"might",
|
|
1304
|
+
"can",
|
|
1305
|
+
"this",
|
|
1306
|
+
"that",
|
|
1307
|
+
"these",
|
|
1308
|
+
"those",
|
|
1309
|
+
"i",
|
|
1310
|
+
"you",
|
|
1311
|
+
"he",
|
|
1312
|
+
"she",
|
|
1313
|
+
"it",
|
|
1314
|
+
"we",
|
|
1315
|
+
"they",
|
|
1316
|
+
"what",
|
|
1317
|
+
"which",
|
|
1318
|
+
"who",
|
|
1319
|
+
"how",
|
|
1320
|
+
"why",
|
|
1321
|
+
"when",
|
|
1322
|
+
"where",
|
|
1323
|
+
"with",
|
|
1324
|
+
"for",
|
|
1325
|
+
"from",
|
|
1326
|
+
"of",
|
|
1327
|
+
"in",
|
|
1328
|
+
"on",
|
|
1329
|
+
"at",
|
|
1330
|
+
"to",
|
|
1331
|
+
"by",
|
|
1332
|
+
"as",
|
|
1333
|
+
"best",
|
|
1334
|
+
"most"
|
|
1335
|
+
]);
|
|
1336
|
+
function tokenizeForMatch(s) {
|
|
1337
|
+
return s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((t) => t.length >= 3 && !CONTENT_MATCH_STOPWORDS.has(t));
|
|
1338
|
+
}
|
|
1339
|
+
function contentMatchScore(queryText, answerText) {
|
|
1340
|
+
const queryTokens = [...new Set(tokenizeForMatch(queryText))];
|
|
1341
|
+
if (queryTokens.length === 0) return 0;
|
|
1342
|
+
const answerHead = answerText.slice(0, 300).toLowerCase();
|
|
1343
|
+
const hit = queryTokens.filter((t) => answerHead.includes(t)).length;
|
|
1344
|
+
return hit / queryTokens.length;
|
|
1345
|
+
}
|
|
1346
|
+
async function backfillSnapshotAttributionCommand(opts) {
|
|
1347
|
+
const config = loadConfig();
|
|
1348
|
+
const db = createClient(config.database);
|
|
1349
|
+
migrate(db);
|
|
1350
|
+
const project2 = db.select().from(projects).where(eq(projects.name, opts.project)).get();
|
|
1351
|
+
if (!project2) {
|
|
1352
|
+
throw new Error(`Project "${opts.project}" not found`);
|
|
1353
|
+
}
|
|
1354
|
+
const isJson = isMachineFormat(opts.format);
|
|
1355
|
+
const isDryRun = opts.dryRun === true;
|
|
1356
|
+
if (!isJson) {
|
|
1357
|
+
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
1358
|
+
process.stderr.write(`Recovering orphan snapshot attribution for "${project2.name}"${mode}...
|
|
1359
|
+
`);
|
|
1360
|
+
}
|
|
1361
|
+
const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and(
|
|
1362
|
+
eq(auditLog.projectId, project2.id),
|
|
1363
|
+
inArray(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
|
|
1364
|
+
)).orderBy(auditLog.createdAt).all();
|
|
1365
|
+
const history = replayQueryAuditLog(events);
|
|
1366
|
+
const orphanRuns = db.select({
|
|
1367
|
+
runId: runs.id,
|
|
1368
|
+
createdAt: runs.createdAt,
|
|
1369
|
+
location: runs.location
|
|
1370
|
+
}).from(runs).innerJoin(querySnapshots, eq(querySnapshots.runId, runs.id)).where(and(
|
|
1371
|
+
eq(runs.projectId, project2.id),
|
|
1372
|
+
isNull(querySnapshots.queryId),
|
|
1373
|
+
isNull(querySnapshots.queryText)
|
|
1374
|
+
)).groupBy(runs.id).orderBy(runs.createdAt).all();
|
|
1375
|
+
const result = {
|
|
1376
|
+
project: project2.name,
|
|
1377
|
+
examinedRuns: orphanRuns.length,
|
|
1378
|
+
orphanSnapshots: 0,
|
|
1379
|
+
recoveredByPosition: 0,
|
|
1380
|
+
recoveredByContent: 0,
|
|
1381
|
+
unrecovered: 0,
|
|
1382
|
+
dryRun: isDryRun,
|
|
1383
|
+
perRun: []
|
|
1384
|
+
};
|
|
1385
|
+
const updates = [];
|
|
1386
|
+
for (const run of orphanRuns) {
|
|
1387
|
+
const activeAt = activeQueriesAt(history, run.createdAt);
|
|
1388
|
+
const orphanSnaps = db.select({
|
|
1389
|
+
id: querySnapshots.id,
|
|
1390
|
+
provider: querySnapshots.provider,
|
|
1391
|
+
createdAt: querySnapshots.createdAt,
|
|
1392
|
+
answerText: querySnapshots.answerText
|
|
1393
|
+
}).from(querySnapshots).where(and(
|
|
1394
|
+
eq(querySnapshots.runId, run.runId),
|
|
1395
|
+
isNull(querySnapshots.queryId),
|
|
1396
|
+
isNull(querySnapshots.queryText)
|
|
1397
|
+
)).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
|
|
1398
|
+
const byProvider = /* @__PURE__ */ new Map();
|
|
1399
|
+
for (const s of orphanSnaps) {
|
|
1400
|
+
const arr = byProvider.get(s.provider);
|
|
1401
|
+
if (arr) arr.push(s);
|
|
1402
|
+
else byProvider.set(s.provider, [s]);
|
|
1403
|
+
}
|
|
1404
|
+
let runPosition = 0;
|
|
1405
|
+
let runContent = 0;
|
|
1406
|
+
let runUnrecovered = 0;
|
|
1407
|
+
for (const [, snaps] of byProvider) {
|
|
1408
|
+
if (snaps.length === activeAt.length) {
|
|
1409
|
+
for (let i = 0; i < snaps.length; i++) {
|
|
1410
|
+
updates.push({ id: snaps[i].id, queryText: activeAt[i] });
|
|
1411
|
+
runPosition++;
|
|
1412
|
+
}
|
|
1413
|
+
} else if (snaps.length < activeAt.length) {
|
|
1414
|
+
const LOOKAHEAD_LIMIT = 5;
|
|
1415
|
+
let cidx = 0;
|
|
1416
|
+
for (const snap of snaps) {
|
|
1417
|
+
const answerText = snap.answerText ?? "";
|
|
1418
|
+
let matchedIdx = -1;
|
|
1419
|
+
for (let i = cidx; i < Math.min(activeAt.length, cidx + LOOKAHEAD_LIMIT); i++) {
|
|
1420
|
+
if (contentMatchScore(activeAt[i], answerText) >= 1) {
|
|
1421
|
+
matchedIdx = i;
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (matchedIdx >= 0) {
|
|
1426
|
+
updates.push({ id: snap.id, queryText: activeAt[matchedIdx] });
|
|
1427
|
+
runContent++;
|
|
1428
|
+
cidx = matchedIdx + 1;
|
|
1429
|
+
} else {
|
|
1430
|
+
runUnrecovered++;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
} else {
|
|
1434
|
+
runUnrecovered += snaps.length;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
result.orphanSnapshots += orphanSnaps.length;
|
|
1438
|
+
result.recoveredByPosition += runPosition;
|
|
1439
|
+
result.recoveredByContent += runContent;
|
|
1440
|
+
result.unrecovered += runUnrecovered;
|
|
1441
|
+
result.perRun.push({
|
|
1442
|
+
runId: run.runId,
|
|
1443
|
+
runCreatedAt: run.createdAt,
|
|
1444
|
+
activeQueryCount: activeAt.length,
|
|
1445
|
+
orphanSnaps: orphanSnaps.length,
|
|
1446
|
+
recoveredPosition: runPosition,
|
|
1447
|
+
recoveredContent: runContent,
|
|
1448
|
+
unrecovered: runUnrecovered
|
|
1449
|
+
});
|
|
1450
|
+
if (!isJson) {
|
|
1451
|
+
process.stderr.write(
|
|
1452
|
+
` ${run.createdAt} loc=${run.location ?? "-"} \u2192 ${orphanSnaps.length} orphan; ${runPosition} position-matched, ${runContent} content-matched, ${runUnrecovered} unrecovered
|
|
1453
|
+
`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!isDryRun && updates.length > 0) {
|
|
1458
|
+
db.transaction((tx) => {
|
|
1459
|
+
for (const u of updates) {
|
|
1460
|
+
tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq(querySnapshots.id, u.id)).run();
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (isJson) {
|
|
1465
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
console.log(`
|
|
1469
|
+
Snapshot attribution ${isDryRun ? "preview" : "recovery"} complete.`);
|
|
1470
|
+
console.log(` Examined runs: ${result.examinedRuns}`);
|
|
1471
|
+
console.log(` Orphan snapshots: ${result.orphanSnapshots}`);
|
|
1472
|
+
console.log(` Position-matched: ${result.recoveredByPosition}`);
|
|
1473
|
+
console.log(` Content-matched: ${result.recoveredByContent}`);
|
|
1474
|
+
console.log(` Unrecovered: ${result.unrecovered}`);
|
|
1475
|
+
if (isDryRun) {
|
|
1476
|
+
console.log(`
|
|
1477
|
+
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function correctStoredOpenAIRetrieval(snapshot, reparsed) {
|
|
1481
|
+
if (snapshot.provider !== ProviderNames.openai) return null;
|
|
1482
|
+
if (snapshot.retrievalContract !== RetrievalContracts["native-auto-v1"]) return null;
|
|
1483
|
+
return {
|
|
1484
|
+
retrievalContract: OPENAI_RETRIEVAL_CONTRACT,
|
|
1485
|
+
...reparsed?.retrievalStatus === void 0 ? {} : { retrievalStatus: reparsed.retrievalStatus }
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function reparseProviderSnapshot(provider, rawResponse) {
|
|
1489
|
+
const envelope = parseJsonColumn(rawResponse, {});
|
|
1490
|
+
const apiResponse = resolveStoredApiResponse(envelope);
|
|
1491
|
+
if (!apiResponse) return null;
|
|
1492
|
+
switch (provider) {
|
|
1493
|
+
case ProviderNames.openai:
|
|
1494
|
+
return reparseStoredResult(apiResponse);
|
|
1495
|
+
case ProviderNames.claude:
|
|
1496
|
+
return reparseStoredResult2(apiResponse);
|
|
1497
|
+
case ProviderNames.gemini:
|
|
1498
|
+
return reparseStoredResult3(apiResponse);
|
|
1499
|
+
case ProviderNames.perplexity:
|
|
1500
|
+
return reparseStoredResult4(apiResponse);
|
|
1501
|
+
case ProviderNames.muse:
|
|
1502
|
+
return reparseStoredResult5(apiResponse);
|
|
1503
|
+
default:
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function resolveStoredApiResponse(parsed) {
|
|
1508
|
+
const nested = parsed.apiResponse;
|
|
1509
|
+
if (nested !== null && typeof nested === "object" && !Array.isArray(nested)) {
|
|
1510
|
+
return nested;
|
|
1511
|
+
}
|
|
1512
|
+
if (looksLikeProviderApiResponse(parsed)) {
|
|
1513
|
+
return parsed;
|
|
1514
|
+
}
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
function looksLikeProviderApiResponse(value) {
|
|
1518
|
+
return Array.isArray(value.output) || Array.isArray(value.content) || Array.isArray(value.candidates) || Array.isArray(value.choices);
|
|
1519
|
+
}
|
|
1520
|
+
function stringifyStoredSnapshotEnvelope(rawResponse, reparsed) {
|
|
1521
|
+
const parsed = parseJsonColumn(rawResponse, {});
|
|
1522
|
+
const apiResponse = resolveStoredApiResponse(parsed);
|
|
1523
|
+
const envelope = apiResponse === parsed ? {} : { ...parsed };
|
|
1524
|
+
delete envelope.answerText;
|
|
1525
|
+
delete envelope.citedDomains;
|
|
1526
|
+
delete envelope.competitorOverlap;
|
|
1527
|
+
delete envelope.recommendedCompetitors;
|
|
1528
|
+
delete envelope.providerError;
|
|
1529
|
+
return JSON.stringify({
|
|
1530
|
+
...envelope,
|
|
1531
|
+
groundingSources: reparsed.groundingSources,
|
|
1532
|
+
searchQueries: reparsed.searchQueries,
|
|
1533
|
+
...reparsed.providerError ? { providerError: reparsed.providerError } : {},
|
|
1534
|
+
...apiResponse ? { apiResponse } : {}
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
async function backfillTrafficClassificationCommand(opts) {
|
|
1538
|
+
const config = loadConfig();
|
|
1539
|
+
const db = createClient(config.database);
|
|
1540
|
+
migrate(db);
|
|
1541
|
+
const projectFilter = opts?.project?.trim();
|
|
1542
|
+
const isDryRun = opts?.dryRun === true;
|
|
1543
|
+
const isJson = isMachineFormat(opts?.format);
|
|
1544
|
+
const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
|
|
1545
|
+
if (scopedProjects.length === 0) {
|
|
1546
|
+
if (projectFilter && !isJson) {
|
|
1547
|
+
process.stderr.write(`No project named "${projectFilter}".
|
|
1548
|
+
`);
|
|
1549
|
+
}
|
|
1550
|
+
if (isJson) console.log(JSON.stringify({ project: projectFilter ?? null, examined: 0, reclassified: 0 }));
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (!isJson) {
|
|
1554
|
+
const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
|
|
1555
|
+
const scope = projectFilter ? `"${projectFilter}"` : "all projects";
|
|
1556
|
+
process.stderr.write(`Re-classifying unknown traffic samples for ${scope}${mode}...
|
|
1557
|
+
`);
|
|
1558
|
+
}
|
|
1559
|
+
const projectIds = scopedProjects.map((p) => p.id);
|
|
1560
|
+
const result = {
|
|
1561
|
+
project: projectFilter ?? null,
|
|
1562
|
+
examined: 0,
|
|
1563
|
+
reclassified: 0,
|
|
1564
|
+
unknownBefore: 0,
|
|
1565
|
+
unknownAfter: 0,
|
|
1566
|
+
dryRun: isDryRun,
|
|
1567
|
+
byBot: {}
|
|
1568
|
+
};
|
|
1569
|
+
const unknownCountRow = db.select({ n: sql`count(*)` }).from(rawEventSamples).where(and(
|
|
1570
|
+
eq(rawEventSamples.eventType, "unknown"),
|
|
1571
|
+
inArray(rawEventSamples.projectId, projectIds)
|
|
1572
|
+
)).get();
|
|
1573
|
+
result.unknownBefore = Number(unknownCountRow?.n ?? 0);
|
|
1574
|
+
const unknownSamples = db.select({
|
|
1575
|
+
id: rawEventSamples.id,
|
|
1576
|
+
projectId: rawEventSamples.projectId,
|
|
1577
|
+
sourceId: rawEventSamples.sourceId,
|
|
1578
|
+
ts: rawEventSamples.ts,
|
|
1579
|
+
userAgent: rawEventSamples.userAgent,
|
|
1580
|
+
pathNormalized: rawEventSamples.pathNormalized,
|
|
1581
|
+
status: rawEventSamples.status
|
|
1582
|
+
}).from(rawEventSamples).where(and(
|
|
1583
|
+
eq(rawEventSamples.eventType, "unknown"),
|
|
1584
|
+
inArray(rawEventSamples.projectId, projectIds)
|
|
1585
|
+
)).all();
|
|
1586
|
+
result.examined = unknownSamples.length;
|
|
1587
|
+
if (unknownSamples.length === 0) {
|
|
1588
|
+
result.unknownAfter = result.unknownBefore;
|
|
1589
|
+
emitTrafficClassificationReport(result, isJson);
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1593
|
+
for (const snap of unknownSamples) {
|
|
1594
|
+
if (!snap.userAgent) continue;
|
|
1595
|
+
const probe = {
|
|
1596
|
+
sourceType: TrafficSourceTypes["cloud-run"],
|
|
1597
|
+
evidenceKind: TrafficEvidenceKinds["raw-request"],
|
|
1598
|
+
confidence: TrafficEventConfidences.observed,
|
|
1599
|
+
eventId: snap.id,
|
|
1600
|
+
observedAt: snap.ts,
|
|
1601
|
+
method: "GET",
|
|
1602
|
+
requestUrl: `https://placeholder${snap.pathNormalized}`,
|
|
1603
|
+
host: "placeholder",
|
|
1604
|
+
path: snap.pathNormalized,
|
|
1605
|
+
queryString: null,
|
|
1606
|
+
status: snap.status ?? 200,
|
|
1607
|
+
userAgent: snap.userAgent,
|
|
1608
|
+
remoteIp: null,
|
|
1609
|
+
referer: null,
|
|
1610
|
+
latencyMs: null,
|
|
1611
|
+
requestSizeBytes: null,
|
|
1612
|
+
responseSizeBytes: null,
|
|
1613
|
+
providerResource: { type: "cloud_run_revision", labels: {} },
|
|
1614
|
+
providerLabels: {}
|
|
1615
|
+
};
|
|
1616
|
+
const userFetch = classifyAiUserFetch(probe);
|
|
1617
|
+
const classified = userFetch ?? classifyCrawler(probe);
|
|
1618
|
+
if (!classified) continue;
|
|
1619
|
+
result.reclassified++;
|
|
1620
|
+
result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
|
|
1621
|
+
if (isDryRun) continue;
|
|
1622
|
+
const tsHour = new Date(snap.ts);
|
|
1623
|
+
tsHour.setUTCMinutes(0, 0, 0);
|
|
1624
|
+
const tsHourIso = tsHour.toISOString();
|
|
1625
|
+
const status = snap.status ?? 200;
|
|
1626
|
+
db.transaction((tx) => {
|
|
1627
|
+
tx.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq(rawEventSamples.id, snap.id)).run();
|
|
1628
|
+
if (userFetch) {
|
|
1629
|
+
tx.insert(aiUserFetchEventsHourly).values({
|
|
1630
|
+
projectId: snap.projectId,
|
|
1631
|
+
sourceId: snap.sourceId,
|
|
1632
|
+
tsHour: tsHourIso,
|
|
1633
|
+
botId: userFetch.botId,
|
|
1634
|
+
operator: userFetch.operator,
|
|
1635
|
+
verificationStatus: userFetch.verificationStatus,
|
|
1636
|
+
pathNormalized: snap.pathNormalized,
|
|
1637
|
+
status,
|
|
1638
|
+
hits: 1,
|
|
1639
|
+
sampledUserAgent: snap.userAgent,
|
|
1640
|
+
createdAt: now,
|
|
1641
|
+
updatedAt: now
|
|
1642
|
+
}).onConflictDoUpdate({
|
|
1643
|
+
target: [
|
|
1644
|
+
aiUserFetchEventsHourly.projectId,
|
|
1645
|
+
aiUserFetchEventsHourly.sourceId,
|
|
1646
|
+
aiUserFetchEventsHourly.tsHour,
|
|
1647
|
+
aiUserFetchEventsHourly.botId,
|
|
1648
|
+
aiUserFetchEventsHourly.verificationStatus,
|
|
1649
|
+
aiUserFetchEventsHourly.pathNormalized,
|
|
1650
|
+
aiUserFetchEventsHourly.status
|
|
1651
|
+
],
|
|
1652
|
+
set: {
|
|
1653
|
+
hits: sql`${aiUserFetchEventsHourly.hits} + 1`,
|
|
1654
|
+
updatedAt: now
|
|
1655
|
+
}
|
|
1656
|
+
}).run();
|
|
1657
|
+
} else {
|
|
1658
|
+
tx.insert(crawlerEventsHourly).values({
|
|
1659
|
+
projectId: snap.projectId,
|
|
1660
|
+
sourceId: snap.sourceId,
|
|
1661
|
+
tsHour: tsHourIso,
|
|
1662
|
+
botId: classified.botId,
|
|
1663
|
+
operator: classified.operator,
|
|
1664
|
+
verificationStatus: classified.verificationStatus,
|
|
1665
|
+
pathNormalized: snap.pathNormalized,
|
|
1666
|
+
status,
|
|
1667
|
+
hits: 1,
|
|
1668
|
+
sampledUserAgent: snap.userAgent,
|
|
1669
|
+
createdAt: now,
|
|
1670
|
+
updatedAt: now
|
|
1671
|
+
}).onConflictDoUpdate({
|
|
1672
|
+
target: [
|
|
1673
|
+
crawlerEventsHourly.projectId,
|
|
1674
|
+
crawlerEventsHourly.sourceId,
|
|
1675
|
+
crawlerEventsHourly.tsHour,
|
|
1676
|
+
crawlerEventsHourly.botId,
|
|
1677
|
+
crawlerEventsHourly.verificationStatus,
|
|
1678
|
+
crawlerEventsHourly.pathNormalized,
|
|
1679
|
+
crawlerEventsHourly.status
|
|
1680
|
+
],
|
|
1681
|
+
set: {
|
|
1682
|
+
hits: sql`${crawlerEventsHourly.hits} + 1`,
|
|
1683
|
+
updatedAt: now
|
|
1684
|
+
}
|
|
1685
|
+
}).run();
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
if (!isDryRun) {
|
|
1690
|
+
const afterRow = db.select({ n: sql`count(*)` }).from(rawEventSamples).where(and(
|
|
1691
|
+
eq(rawEventSamples.eventType, "unknown"),
|
|
1692
|
+
inArray(rawEventSamples.projectId, projectIds)
|
|
1693
|
+
)).get();
|
|
1694
|
+
result.unknownAfter = Number(afterRow?.n ?? 0);
|
|
1695
|
+
} else {
|
|
1696
|
+
result.unknownAfter = result.unknownBefore - result.reclassified;
|
|
1697
|
+
}
|
|
1698
|
+
emitTrafficClassificationReport(result, isJson);
|
|
1699
|
+
}
|
|
1700
|
+
function emitTrafficClassificationReport(result, isJson) {
|
|
1701
|
+
if (isJson) {
|
|
1702
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const verb = result.dryRun ? "Would re-classify" : "Re-classified";
|
|
1706
|
+
console.log(`
|
|
1707
|
+
Traffic classification ${result.dryRun ? "preview" : "recovery"} complete.`);
|
|
1708
|
+
console.log(` Examined unknowns: ${result.examined}`);
|
|
1709
|
+
console.log(` ${verb}: ${result.reclassified}`);
|
|
1710
|
+
console.log(` Unknown before: ${result.unknownBefore}`);
|
|
1711
|
+
console.log(` Unknown after: ${result.unknownAfter}`);
|
|
1712
|
+
if (result.reclassified > 0) {
|
|
1713
|
+
console.log(` By bot:`);
|
|
1714
|
+
const sorted = Object.entries(result.byBot).sort(([, a], [, b]) => b - a);
|
|
1715
|
+
for (const [bot, count] of sorted) {
|
|
1716
|
+
console.log(` ${count.toString().padStart(5)} ${bot}`);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (result.dryRun) {
|
|
1720
|
+
console.log(`
|
|
1721
|
+
No DB writes performed. Re-run without --dry-run to apply.`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
783
1725
|
// src/cli-commands/backfill.ts
|
|
784
1726
|
var BACKFILL_CLI_COMMANDS = [
|
|
785
1727
|
{
|
|
@@ -2317,10 +3259,16 @@ async function discoverPromotePreview(project2, sessionId, opts) {
|
|
|
2317
3259
|
if (preview.suggestedCompetitors.length > 0) {
|
|
2318
3260
|
console.log(` Suggested new competitors:`);
|
|
2319
3261
|
for (const c of preview.suggestedCompetitors) {
|
|
2320
|
-
console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})`);
|
|
3262
|
+
console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})${mergedHostsNote(c)}`);
|
|
2321
3263
|
}
|
|
2322
3264
|
console.log(" Only direct-competitor is promoted by default \u2014 pass --competitor-types to include other types.");
|
|
2323
3265
|
}
|
|
3266
|
+
const skippedCompetitors = preview.skippedCompetitors ?? [];
|
|
3267
|
+
const held = skippedCompetitors.filter((c) => c.reason !== DiscoveryPromoteCompetitorSkipReasons["already-tracked"]);
|
|
3268
|
+
if (held.length > 0) {
|
|
3269
|
+
console.log(` Competitors a promote leaves out:`);
|
|
3270
|
+
for (const c of held) console.log(` ! ${c.domain} (${c.reason}): ${c.message}`);
|
|
3271
|
+
}
|
|
2324
3272
|
console.log(`
|
|
2325
3273
|
Run \`canonry discover promote ${project2} ${sessionId}\` to merge cited + aspirational queries.`);
|
|
2326
3274
|
console.log(" Add `--bucket wasted-surface` only when off-ICP competitor gaps should be tracked.");
|
|
@@ -2337,15 +3285,24 @@ async function discoverPromote(project2, sessionId, opts) {
|
|
|
2337
3285
|
return;
|
|
2338
3286
|
}
|
|
2339
3287
|
const { promoted, skipped } = result;
|
|
3288
|
+
const competitorDetails = result.competitorDetails;
|
|
3289
|
+
const promotedCompetitors = competitorDetails?.promoted ?? promoted.competitors.map((domain) => ({ domain }));
|
|
3290
|
+
const held = (competitorDetails?.skipped ?? []).filter((c) => c.reason !== DiscoveryPromoteCompetitorSkipReasons["already-tracked"]);
|
|
2340
3291
|
console.log(`Promoted discovery session ${sessionId} into "${project2}":`);
|
|
2341
3292
|
console.log(` Queries: ${promoted.queries.length} added, ${skipped.queries.length} already tracked`);
|
|
2342
3293
|
for (const q of promoted.queries) console.log(` + ${q}`);
|
|
2343
|
-
console.log(` Competitors: ${promoted.competitors.length} added, ${skipped.competitors.length} already tracked`);
|
|
2344
|
-
for (const c of
|
|
3294
|
+
console.log(` Competitors: ${promoted.competitors.length} added, ${skipped.competitors.length - held.length} already tracked, ${held.length} left out`);
|
|
3295
|
+
for (const c of promotedCompetitors) console.log(` + ${c.domain}${mergedHostsNote(c)}`);
|
|
3296
|
+
for (const c of held) console.log(` ! ${c.domain} (${c.reason}): ${c.message}`);
|
|
2345
3297
|
if (promoted.queries.length === 0 && promoted.competitors.length === 0) {
|
|
2346
3298
|
console.log(` Nothing new \u2014 the project's basket already covers this session.`);
|
|
2347
3299
|
}
|
|
2348
3300
|
}
|
|
3301
|
+
function mergedHostsNote(competitor) {
|
|
3302
|
+
const sources = competitor.sources ?? [];
|
|
3303
|
+
const hosts = sources.map((source) => source.domain).filter((host) => host !== competitor.domain);
|
|
3304
|
+
return hosts.length > 0 ? ` (from ${sources.map((source) => source.domain).join(", ")})` : "";
|
|
3305
|
+
}
|
|
2349
3306
|
function printSessionDetail(session) {
|
|
2350
3307
|
console.log(`Discovery session: ${session.id}`);
|
|
2351
3308
|
console.log(` Status: ${session.status}`);
|
|
@@ -8133,21 +9090,28 @@ var TRAFFIC_CLI_COMMANDS = [
|
|
|
8133
9090
|
function getClient10() {
|
|
8134
9091
|
return createApiClient();
|
|
8135
9092
|
}
|
|
8136
|
-
async function addCompetitors(project2, domains, format) {
|
|
9093
|
+
async function addCompetitors(project2, domains, format, aliases = []) {
|
|
8137
9094
|
const client = getClient10();
|
|
8138
9095
|
const existing = await client.listCompetitors(project2);
|
|
8139
9096
|
const existingDomains = existing.map((c) => c.domain);
|
|
8140
9097
|
const existingSet = new Set(existingDomains);
|
|
8141
|
-
const requested = new Set(uniqueStrings(domains));
|
|
8142
|
-
const current = await client.appendCompetitors(
|
|
9098
|
+
const requested = new Set(uniqueStrings(domains).map((domain) => normalizeCompetitorDomain(domain.trim())));
|
|
9099
|
+
const current = await client.appendCompetitors(
|
|
9100
|
+
project2,
|
|
9101
|
+
aliases.length > 0 ? domains.map((domain) => ({ domain, aliases })) : domains
|
|
9102
|
+
);
|
|
8143
9103
|
const currentDomains = current.map((c) => c.domain);
|
|
8144
9104
|
const addedDomains = currentDomains.filter((domain) => requested.has(domain) && !existingSet.has(domain));
|
|
9105
|
+
const aliasTarget = aliases.length > 0 ? normalizeCompetitorDomain(domains[0].trim()) : null;
|
|
9106
|
+
const aliasRow = aliasTarget ? current.find((c) => normalizeCompetitorDomain(c.domain) === aliasTarget) : void 0;
|
|
8145
9107
|
if (isMachineFormat(format)) {
|
|
8146
9108
|
console.log(JSON.stringify({
|
|
8147
9109
|
project: project2,
|
|
8148
9110
|
domains: currentDomains,
|
|
8149
9111
|
addedDomains,
|
|
8150
|
-
addedCount: addedDomains.length
|
|
9112
|
+
addedCount: addedDomains.length,
|
|
9113
|
+
// The aliased competitor as the API returns it (`CompetitorDto`).
|
|
9114
|
+
...aliasRow ? { competitor: aliasRow } : {}
|
|
8151
9115
|
}, null, 2));
|
|
8152
9116
|
return;
|
|
8153
9117
|
}
|
|
@@ -8156,15 +9120,75 @@ async function addCompetitors(project2, domains, format) {
|
|
|
8156
9120
|
} else {
|
|
8157
9121
|
console.log(`Added ${addedDomains.length} competitor(s) to "${project2}".`);
|
|
8158
9122
|
}
|
|
9123
|
+
if (aliasRow) console.log(`Aliases for ${aliasRow.domain}: ${formatAliases(storedAliases(aliasRow))}`);
|
|
9124
|
+
}
|
|
9125
|
+
function storedAliases(competitor) {
|
|
9126
|
+
return competitor.aliases ?? [];
|
|
9127
|
+
}
|
|
9128
|
+
function formatAliases(aliases) {
|
|
9129
|
+
return aliases.length > 0 ? aliases.join(", ") : "(none)";
|
|
9130
|
+
}
|
|
9131
|
+
function sameAliasName(a, b) {
|
|
9132
|
+
return a.trim().toLowerCase() === b.trim().toLowerCase();
|
|
9133
|
+
}
|
|
9134
|
+
async function competitorAliases(project2, domain, options) {
|
|
9135
|
+
const client = getClient10();
|
|
9136
|
+
let result;
|
|
9137
|
+
if (options.set !== void 0 || options.clear === true) {
|
|
9138
|
+
result = await client.setCompetitorAliases(project2, domain, options.clear ? [] : options.set ?? []);
|
|
9139
|
+
} else {
|
|
9140
|
+
const target = normalizeCompetitorDomain(domain.trim());
|
|
9141
|
+
const matches = (await client.listCompetitors(project2)).filter((c) => normalizeCompetitorDomain(c.domain) === target);
|
|
9142
|
+
if (matches.length > 1) {
|
|
9143
|
+
const rows = [...matches].sort((a, b) => a.domain.localeCompare(b.domain));
|
|
9144
|
+
const aliases = normalizeCompetitorAliases(rows.flatMap(storedAliases));
|
|
9145
|
+
const addAgain = `canonry competitor add ${project2} ${target}${aliases.map((alias) => ` --alias ${JSON.stringify(alias)}`).join("")}`;
|
|
9146
|
+
const stored = `${matches.length} rows (${rows.map((c) => c.domain).join(", ")})`;
|
|
9147
|
+
throw new CliError({
|
|
9148
|
+
code: "VALIDATION_ERROR",
|
|
9149
|
+
message: `Competitor ${target} is stored as ${stored}; remove the competitor (every row goes) and add it again: ${addAgain}`,
|
|
9150
|
+
displayMessage: `Error: competitor ${target} is stored as ${stored}. Remove it with: canonry competitor remove ${project2} ${target}, then add it again with: ${addAgain}`,
|
|
9151
|
+
details: { project: project2, domain: target, matches: rows.map((c) => ({ id: c.id, domain: c.domain })), aliases }
|
|
9152
|
+
});
|
|
9153
|
+
}
|
|
9154
|
+
const current = matches.at(0);
|
|
9155
|
+
if (!current) {
|
|
9156
|
+
throw new CliError({
|
|
9157
|
+
code: "NOT_FOUND",
|
|
9158
|
+
message: `Competitor "${target}" is not tracked by project "${project2}"`,
|
|
9159
|
+
displayMessage: `Error: competitor "${target}" is not tracked by "${project2}". Add it with: canonry competitor add ${project2} ${target}`,
|
|
9160
|
+
details: { project: project2, domain: target }
|
|
9161
|
+
});
|
|
9162
|
+
}
|
|
9163
|
+
const removed = options.remove ?? [];
|
|
9164
|
+
const added = options.add ?? [];
|
|
9165
|
+
if (added.length === 0 && removed.length === 0) {
|
|
9166
|
+
result = current;
|
|
9167
|
+
} else if (removed.length === 0) {
|
|
9168
|
+
const listed = await client.appendCompetitors(project2, [{ domain: current.domain, aliases: added }]);
|
|
9169
|
+
result = listed.find((c) => c.domain === current.domain) ?? current;
|
|
9170
|
+
} else {
|
|
9171
|
+
const next = [
|
|
9172
|
+
...storedAliases(current).filter((alias) => !removed.some((name) => sameAliasName(name, alias))),
|
|
9173
|
+
...added
|
|
9174
|
+
];
|
|
9175
|
+
result = await client.setCompetitorAliases(project2, current.domain, next);
|
|
9176
|
+
}
|
|
9177
|
+
}
|
|
9178
|
+
if (isMachineFormat(options.format)) {
|
|
9179
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9180
|
+
return;
|
|
9181
|
+
}
|
|
9182
|
+
console.log(`Aliases for ${result.domain}: ${formatAliases(storedAliases(result))}`);
|
|
8159
9183
|
}
|
|
8160
9184
|
async function removeCompetitors(project2, domains, format) {
|
|
8161
9185
|
const client = getClient10();
|
|
8162
9186
|
const existing = await client.listCompetitors(project2);
|
|
8163
9187
|
const existingDomains = existing.map((c) => c.domain);
|
|
8164
|
-
const requested = new Set(uniqueStrings(domains));
|
|
9188
|
+
const requested = new Set(uniqueStrings(domains).map((domain) => normalizeCompetitorDomain(domain.trim())));
|
|
8165
9189
|
const current = await client.deleteCompetitors(project2, domains);
|
|
8166
9190
|
const currentSet = new Set(current.map((c) => c.domain));
|
|
8167
|
-
const removedDomains = existingDomains.filter((domain) => requested.has(domain) && !currentSet.has(domain));
|
|
9191
|
+
const removedDomains = existingDomains.filter((domain) => requested.has(normalizeCompetitorDomain(domain)) && !currentSet.has(domain));
|
|
8168
9192
|
if (isMachineFormat(format)) {
|
|
8169
9193
|
console.log(JSON.stringify({
|
|
8170
9194
|
project: project2,
|
|
@@ -8203,7 +9227,8 @@ async function listCompetitors(project2, format) {
|
|
|
8203
9227
|
console.log(`Competitors for "${project2}" (${comps.length}):
|
|
8204
9228
|
`);
|
|
8205
9229
|
for (const c of comps) {
|
|
8206
|
-
|
|
9230
|
+
const aliases = storedAliases(c);
|
|
9231
|
+
console.log(aliases.length > 0 ? ` ${c.domain} (aliases: ${aliases.join(", ")})` : ` ${c.domain}`);
|
|
8207
9232
|
}
|
|
8208
9233
|
}
|
|
8209
9234
|
async function showCompetitorLandscape(project2, options) {
|
|
@@ -8293,6 +9318,8 @@ function printLandscapeRows(heading, rows, showSampleCount = false, context, que
|
|
|
8293
9318
|
}
|
|
8294
9319
|
|
|
8295
9320
|
// src/cli-commands/competitor.ts
|
|
9321
|
+
var ADD_USAGE = "canonry competitor add <project> <domain...> [--alias <name>]... [--format json]";
|
|
9322
|
+
var ALIASES_USAGE = "canonry competitor aliases <project> <domain> [--set <name>]... [--add <name>]... [--remove <name>]... [--clear] [--format json]";
|
|
8296
9323
|
var LANDSCAPE_USAGE = "canonry competitor landscape <project> [--window 7d|30d|90d|all] [--group-key <key>|--scope all-markets] [--by-model] [--provider <provider> [--model <id>]] [--query-class all|branded|non-brand] [--location <label>] [--run-id <id>] [--format json|jsonl]";
|
|
8297
9324
|
function parseLandscapeScope(value) {
|
|
8298
9325
|
if (value === void 0 || value === "") return void 0;
|
|
@@ -8305,20 +9332,63 @@ function parseLandscapeScope(value) {
|
|
|
8305
9332
|
var COMPETITOR_CLI_COMMANDS = [
|
|
8306
9333
|
{
|
|
8307
9334
|
path: ["competitor", "add"],
|
|
8308
|
-
usage:
|
|
9335
|
+
usage: ADD_USAGE,
|
|
9336
|
+
options: {
|
|
9337
|
+
alias: multiStringOption()
|
|
9338
|
+
},
|
|
8309
9339
|
run: async (input) => {
|
|
8310
|
-
const project2 = requireProject(input, "competitor.add",
|
|
9340
|
+
const project2 = requireProject(input, "competitor.add", ADD_USAGE);
|
|
8311
9341
|
const domains = input.positionals.slice(1);
|
|
8312
9342
|
if (domains.length === 0) {
|
|
8313
|
-
throw usageError(
|
|
9343
|
+
throw usageError(`Error: project name and at least one domain required
|
|
9344
|
+
Usage: ${ADD_USAGE}`, {
|
|
8314
9345
|
message: "project name and at least one domain required",
|
|
8315
9346
|
details: {
|
|
8316
9347
|
command: "competitor.add",
|
|
8317
|
-
usage:
|
|
9348
|
+
usage: ADD_USAGE
|
|
8318
9349
|
}
|
|
8319
9350
|
});
|
|
8320
9351
|
}
|
|
8321
|
-
|
|
9352
|
+
const aliases = getStringArray(input.values, "alias") ?? [];
|
|
9353
|
+
if (aliases.length > 0 && domains.length !== 1) {
|
|
9354
|
+
throw usageError(`Error: --alias names one competitor, so pass exactly one domain
|
|
9355
|
+
Usage: ${ADD_USAGE}`, {
|
|
9356
|
+
message: "--alias names one competitor, so pass exactly one domain",
|
|
9357
|
+
details: { command: "competitor.add", usage: ADD_USAGE }
|
|
9358
|
+
});
|
|
9359
|
+
}
|
|
9360
|
+
await addCompetitors(project2, domains, input.format, aliases);
|
|
9361
|
+
}
|
|
9362
|
+
},
|
|
9363
|
+
{
|
|
9364
|
+
path: ["competitor", "aliases"],
|
|
9365
|
+
usage: ALIASES_USAGE,
|
|
9366
|
+
options: {
|
|
9367
|
+
set: multiStringOption(),
|
|
9368
|
+
add: multiStringOption(),
|
|
9369
|
+
remove: multiStringOption(),
|
|
9370
|
+
clear: { type: "boolean" }
|
|
9371
|
+
},
|
|
9372
|
+
run: async (input) => {
|
|
9373
|
+
const project2 = requireProject(input, "competitor.aliases", ALIASES_USAGE);
|
|
9374
|
+
const domain = requirePositional(input, 1, {
|
|
9375
|
+
command: "competitor.aliases",
|
|
9376
|
+
usage: ALIASES_USAGE,
|
|
9377
|
+
message: "competitor domain is required"
|
|
9378
|
+
});
|
|
9379
|
+
const set = getStringArray(input.values, "set");
|
|
9380
|
+
const add = getStringArray(input.values, "add");
|
|
9381
|
+
const remove = getStringArray(input.values, "remove");
|
|
9382
|
+
const clear = getBoolean(input.values, "clear");
|
|
9383
|
+
const exclusive = [set !== void 0, clear].filter(Boolean).length;
|
|
9384
|
+
if (exclusive > 1 || exclusive === 1 && (add !== void 0 || remove !== void 0)) {
|
|
9385
|
+
throw usageError(`Error: --set and --clear replace the whole list, so they cannot be combined with each other or with --add/--remove
|
|
9386
|
+
Usage: ${ALIASES_USAGE}`, {
|
|
9387
|
+
message: "--set and --clear cannot be combined with each other or with --add/--remove",
|
|
9388
|
+
details: { command: "competitor.aliases", usage: ALIASES_USAGE }
|
|
9389
|
+
});
|
|
9390
|
+
}
|
|
9391
|
+
await competitorAliases(project2, domain, { set, add, remove, clear, format: input.format });
|
|
8322
9392
|
}
|
|
8323
9393
|
},
|
|
8324
9394
|
{
|
|
@@ -8413,12 +9483,12 @@ var COMPETITOR_CLI_COMMANDS = [
|
|
|
8413
9483
|
},
|
|
8414
9484
|
{
|
|
8415
9485
|
path: ["competitor"],
|
|
8416
|
-
usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
|
|
9486
|
+
usage: "canonry competitor <add|aliases|remove|delete|list|landscape> <project> [args]",
|
|
8417
9487
|
run: async (input) => {
|
|
8418
9488
|
unknownSubcommand(input.positionals[0], {
|
|
8419
9489
|
command: "competitor",
|
|
8420
|
-
usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
|
|
8421
|
-
available: ["add", "remove", "delete", "list", "landscape"]
|
|
9490
|
+
usage: "canonry competitor <add|aliases|remove|delete|list|landscape> <project> [args]",
|
|
9491
|
+
available: ["add", "aliases", "remove", "delete", "list", "landscape"]
|
|
8422
9492
|
});
|
|
8423
9493
|
}
|
|
8424
9494
|
}
|
|
@@ -11953,10 +13023,10 @@ Brand Gap Analysis`);
|
|
|
11953
13023
|
console.log(`
|
|
11954
13024
|
Opportunity Gaps (competitors cited, you're not):`);
|
|
11955
13025
|
for (const q of data.gap) {
|
|
11956
|
-
const
|
|
13026
|
+
const competitors2 = q.competitorsCiting.join(", ");
|
|
11957
13027
|
const cons = q.consistency.totalRuns > 0 ? ` [cited ${q.consistency.citedRuns}/${q.consistency.totalRuns} runs]` : "";
|
|
11958
13028
|
console.log(` \u2022 ${q.query}${cons}`);
|
|
11959
|
-
console.log(` Competitors: ${
|
|
13029
|
+
console.log(` Competitors: ${competitors2}`);
|
|
11960
13030
|
}
|
|
11961
13031
|
}
|
|
11962
13032
|
if (data.cited.length > 0) {
|
|
@@ -14631,8 +15701,8 @@ function renderQueries(pdf, report) {
|
|
|
14631
15701
|
for (const result of queryResult.providerResults) {
|
|
14632
15702
|
const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
|
|
14633
15703
|
const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
|
|
14634
|
-
const
|
|
14635
|
-
const line = `${result.displayName}: ${status}${accuracy}${
|
|
15704
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
|
|
15705
|
+
const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
|
|
14636
15706
|
pdf.bullet(line);
|
|
14637
15707
|
if (result.error) {
|
|
14638
15708
|
pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
|
|
@@ -14781,8 +15851,8 @@ function formatSnapshotMarkdown(report) {
|
|
|
14781
15851
|
const mentioned = result.mentioned ? "Yes" : "No";
|
|
14782
15852
|
const cited = result.cited ? "Yes" : "No";
|
|
14783
15853
|
const accuracy = result.describedAccurately === "not-mentioned" ? "-" : result.describedAccurately;
|
|
14784
|
-
const
|
|
14785
|
-
lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${
|
|
15854
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
|
|
15855
|
+
lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
|
|
14786
15856
|
}
|
|
14787
15857
|
lines.push("");
|
|
14788
15858
|
}
|
|
@@ -15081,7 +16151,7 @@ function renderHuman2(overview) {
|
|
|
15081
16151
|
citationMovement,
|
|
15082
16152
|
mentionMovement,
|
|
15083
16153
|
movementComparison,
|
|
15084
|
-
competitors,
|
|
16154
|
+
competitors: competitors2,
|
|
15085
16155
|
providerScores,
|
|
15086
16156
|
attentionItems,
|
|
15087
16157
|
runHistory,
|
|
@@ -15110,11 +16180,11 @@ function renderHuman2(overview) {
|
|
|
15110
16180
|
printScore("Index coverage ", scores.indexCoverage);
|
|
15111
16181
|
printScore("Competitor press.", scores.competitorPressure);
|
|
15112
16182
|
printScore("Run status ", scores.runStatus);
|
|
15113
|
-
const
|
|
15114
|
-
if (overview.sentiment?.configured &&
|
|
16183
|
+
const brandedSentiment = overview.sentiment?.branded;
|
|
16184
|
+
if (overview.sentiment?.configured && brandedSentiment && brandedSentiment.coverage.judged > 0 && brandedSentiment.score.favorableRate !== null) {
|
|
15115
16185
|
console.log(`
|
|
15116
|
-
|
|
15117
|
-
console.log(` ${
|
|
16186
|
+
Sentiment: ${brandedSentiment.score.favorableDisplay} favorable \xB7 branded queries`);
|
|
16187
|
+
console.log(` ${brandedSentiment.coverage.counts.favorable} favorable / ${brandedSentiment.coverage.judged} judged${brandedSentiment.provisional ? " \xB7 provisional" : ""}`);
|
|
15118
16188
|
}
|
|
15119
16189
|
console.log(`
|
|
15120
16190
|
Queries cited: ${queryCounts.citedQueries}/${queryCounts.totalQueries} (${formatPercent(queryCounts.citedRate)})`);
|
|
@@ -15144,9 +16214,9 @@ function renderHuman2(overview) {
|
|
|
15144
16214
|
console.log(`
|
|
15145
16215
|
Transitions since ${transitions.since}: +${transitions.gained} gained, -${transitions.lost} lost, ${transitions.emerging} emerging`);
|
|
15146
16216
|
}
|
|
15147
|
-
if (
|
|
16217
|
+
if (competitors2.length > 0) {
|
|
15148
16218
|
console.log("\n Competitors:");
|
|
15149
|
-
for (const c of
|
|
16219
|
+
for (const c of competitors2) {
|
|
15150
16220
|
console.log(` ${c.domain.padEnd(28)} ${c.citationCount}/${c.totalQueries} ${c.pressureLabel}`);
|
|
15151
16221
|
}
|
|
15152
16222
|
}
|
|
@@ -15196,13 +16266,13 @@ function printMentionShareBreakdown(mentionShare) {
|
|
|
15196
16266
|
const ranking = legacyCompatible.ranking ?? [];
|
|
15197
16267
|
const project2 = ranking.find((row) => row.kind === "project");
|
|
15198
16268
|
if (!project2) return;
|
|
15199
|
-
const
|
|
16269
|
+
const competitors2 = ranking.filter((row) => row.kind === "competitor");
|
|
15200
16270
|
console.log(` you${" ".repeat(28)} ${project2.mentionSnapshots} mentions (${formatPercent(project2.share)} of combined)`);
|
|
15201
|
-
for (const row of
|
|
16271
|
+
for (const row of competitors2.slice(0, 3)) {
|
|
15202
16272
|
console.log(` ${(row.domain ?? "").padEnd(30)} ${row.mentionSnapshots} mentions (${formatPercent(row.share)} of combined)`);
|
|
15203
16273
|
}
|
|
15204
|
-
if (
|
|
15205
|
-
console.log(` + ${
|
|
16274
|
+
if (competitors2.length > 3) {
|
|
16275
|
+
console.log(` + ${competitors2.length - 3} more competitor${competitors2.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
|
|
15206
16276
|
}
|
|
15207
16277
|
}
|
|
15208
16278
|
|
|
@@ -15795,7 +16865,7 @@ var CONTENT_CLI_COMMANDS = [
|
|
|
15795
16865
|
import crypto2 from "crypto";
|
|
15796
16866
|
import path8 from "path";
|
|
15797
16867
|
import { isDeepStrictEqual } from "util";
|
|
15798
|
-
import { eq } from "drizzle-orm";
|
|
16868
|
+
import { eq as eq2 } from "drizzle-orm";
|
|
15799
16869
|
function persistedValue(value) {
|
|
15800
16870
|
if (Array.isArray(value)) return value.map(persistedValue);
|
|
15801
16871
|
if (value && typeof value === "object") {
|
|
@@ -15855,7 +16925,7 @@ async function bootstrapCommand(opts) {
|
|
|
15855
16925
|
projectId: apiKeys.projectId,
|
|
15856
16926
|
lastUsedAt: apiKeys.lastUsedAt,
|
|
15857
16927
|
revokedAt: apiKeys.revokedAt
|
|
15858
|
-
}).from(apiKeys).where(
|
|
16928
|
+
}).from(apiKeys).where(eq2(apiKeys.name, "default")).all();
|
|
15859
16929
|
const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
|
|
15860
16930
|
if (existingDefault) {
|
|
15861
16931
|
const rotating = existingDefault.keyHash !== keyHash;
|
|
@@ -15868,13 +16938,13 @@ async function bootstrapCommand(opts) {
|
|
|
15868
16938
|
projectId: null,
|
|
15869
16939
|
...rotating ? { lastUsedAt: null } : {},
|
|
15870
16940
|
revokedAt: null
|
|
15871
|
-
}).where(
|
|
16941
|
+
}).where(eq2(apiKeys.id, existingDefault.id)).run();
|
|
15872
16942
|
changed = true;
|
|
15873
16943
|
}
|
|
15874
16944
|
for (const duplicate of existingDefaults) {
|
|
15875
16945
|
if (duplicate.id === existingDefault.id) continue;
|
|
15876
16946
|
if (duplicate.revokedAt === null) {
|
|
15877
|
-
tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(
|
|
16947
|
+
tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq2(apiKeys.id, duplicate.id)).run();
|
|
15878
16948
|
changed = true;
|
|
15879
16949
|
}
|
|
15880
16950
|
}
|
|
@@ -16525,7 +17595,7 @@ function encodeLegacySetupState(state) {
|
|
|
16525
17595
|
}
|
|
16526
17596
|
|
|
16527
17597
|
// src/commands/serve.ts
|
|
16528
|
-
import { and, asc, eq as
|
|
17598
|
+
import { and as and2, asc as asc2, eq as eq4, ne } from "drizzle-orm";
|
|
16529
17599
|
|
|
16530
17600
|
// src/server-shutdown.ts
|
|
16531
17601
|
async function closeWithIdleSweep(app, intervalMs = 250) {
|
|
@@ -16539,7 +17609,7 @@ async function closeWithIdleSweep(app, intervalMs = 250) {
|
|
|
16539
17609
|
}
|
|
16540
17610
|
|
|
16541
17611
|
// src/startup-path-repairs.ts
|
|
16542
|
-
import { eq as
|
|
17612
|
+
import { eq as eq3, lt } from "drizzle-orm";
|
|
16543
17613
|
var STARTUP_REPAIR_PAGE_SIZE = 4096;
|
|
16544
17614
|
function repairNormalizedPathsOnStartup(db) {
|
|
16545
17615
|
return repairPathsOnce(db, "ga-traffic-paths", backfillNormalizedPaths);
|
|
@@ -16548,7 +17618,7 @@ function repairAiReferralPathsOnStartup(db) {
|
|
|
16548
17618
|
return repairPathsOnce(db, "ga-ai-referral-paths", backfillAiReferralPaths);
|
|
16549
17619
|
}
|
|
16550
17620
|
async function repairPathsOnce(db, name, repair) {
|
|
16551
|
-
const completed = db.select({ version: dataRepairCompletions.version }).from(dataRepairCompletions).where(
|
|
17621
|
+
const completed = db.select({ version: dataRepairCompletions.version }).from(dataRepairCompletions).where(eq3(dataRepairCompletions.name, name)).get();
|
|
16552
17622
|
if (completed && completed.version >= URL_PATH_NORMALIZATION_VERSION) return null;
|
|
16553
17623
|
let conflicted = false;
|
|
16554
17624
|
const result = await repair(db, { pageSize: STARTUP_REPAIR_PAGE_SIZE, onConflict: () => {
|
|
@@ -16769,15 +17839,15 @@ function readServeOpenState(db) {
|
|
|
16769
17839
|
id: projects.id,
|
|
16770
17840
|
name: projects.name,
|
|
16771
17841
|
createdAt: projects.createdAt
|
|
16772
|
-
}).from(projects).orderBy(
|
|
17842
|
+
}).from(projects).orderBy(asc2(projects.createdAt), asc2(projects.name)).all();
|
|
16773
17843
|
if (rows.length === 0) return { projectCount: 0, hasSiteAudit: false };
|
|
16774
17844
|
let scanned;
|
|
16775
17845
|
try {
|
|
16776
17846
|
const audits = db.select({
|
|
16777
17847
|
projectId: runs.projectId,
|
|
16778
17848
|
status: runs.status
|
|
16779
|
-
}).from(runs).where(
|
|
16780
|
-
|
|
17849
|
+
}).from(runs).where(and2(
|
|
17850
|
+
eq4(runs.kind, RunKinds["site-audit"]),
|
|
16781
17851
|
ne(runs.trigger, RunTriggers.probe)
|
|
16782
17852
|
)).all();
|
|
16783
17853
|
scanned = new Set(
|
|
@@ -20576,7 +21646,7 @@ Page Health:
|
|
|
20576
21646
|
Projects:
|
|
20577
21647
|
project Create, update, list, show, delete projects
|
|
20578
21648
|
query Add, replace, remove, list, import, generate queries
|
|
20579
|
-
competitor Add, remove, list competitors
|
|
21649
|
+
competitor Add, remove, list competitors and their aliases
|
|
20580
21650
|
|
|
20581
21651
|
Monitoring:
|
|
20582
21652
|
run Trigger visibility sweeps
|