@canonry/canonry 7.2.0 → 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.
Files changed (32) hide show
  1. package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -2
  2. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +31 -3
  4. package/assets/assets/{AuditHistoryPanel-Duvyi8vx.js → AuditHistoryPanel-DP92khTY.js} +1 -1
  5. package/assets/assets/{BacklinksPage-CY5AllQu.js → BacklinksPage-BI_9t1iI.js} +1 -1
  6. package/assets/assets/{HistoryPage-C0Gr0gcj.js → HistoryPage-tDE6Nv0J.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-DrAB6VKX.js → MeasurementPropertyPage-Y2F334Qs.js} +1 -1
  8. package/assets/assets/ProjectPage-DW581TNr.js +8 -0
  9. package/assets/assets/{RunsPage-DMxOrtfl.js → RunsPage-Bl_c9-vA.js} +1 -1
  10. package/assets/assets/{SettingsPage-sxiUNt2A.js → SettingsPage-DQ5_Q2rc.js} +1 -1
  11. package/assets/assets/{SiteHealthSection--6tZe3hM.js → SiteHealthSection-DdZruU72.js} +3 -3
  12. package/assets/assets/{TrafficPage-CvSQDNTi.js → TrafficPage-CROectSE.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-BaYeIBmc.js → TrafficSourceDetailPage-Bthvvwg5.js} +1 -1
  14. package/assets/assets/{extract-error-message-76IWhqQF.js → extract-error-message-DOLCGQMa.js} +1 -1
  15. package/assets/assets/{index-BlrH2zRW.js → index-CCOcEGdz.js} +21 -21
  16. package/assets/assets/{react-sigma_core.esm.min-CkAcKW2k.js → react-sigma_core.esm.min-TWiq-VWU.js} +1 -1
  17. package/assets/assets/{v2-overview-adapter-DnEE4E9w.js → v2-overview-adapter-CxmV639A.js} +1 -1
  18. package/assets/index.html +1 -1
  19. package/dist/{chunk-EQBYK77W.js → chunk-3JEUZ2K5.js} +3 -3
  20. package/dist/{chunk-BYSSJEL7.js → chunk-FWV6IIYY.js} +30 -9
  21. package/dist/{chunk-UIDHHZH3.js → chunk-MQPBOQUH.js} +292 -1266
  22. package/dist/{chunk-SVTVBPIT.js → chunk-QND5LEAC.js} +2 -2
  23. package/dist/{chunk-FDBQOQLP.js → chunk-RVELNJAW.js} +29 -2
  24. package/dist/{chunk-RCQJI3UX.js → chunk-XODXE6PT.js} +3190 -2721
  25. package/dist/{chunk-TY3CJKKR.js → chunk-YQXQBFHT.js} +5916 -5416
  26. package/dist/cli.js +1113 -59
  27. package/dist/{demo-server-GWRYDPJC.js → demo-server-BXABH765.js} +6 -6
  28. package/dist/index.js +6 -6
  29. package/dist/{intelligence-service-3L2665AK.js → intelligence-service-EIWYQYVG.js} +2 -2
  30. package/dist/mcp.js +4 -4
  31. package/package.json +13 -13
  32. package/assets/assets/ProjectPage-DSxvvnAd.js +0 -8
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
- backfillAiReferralPaths,
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-UIDHHZH3.js";
35
+ } from "./chunk-MQPBOQUH.js";
39
36
  import {
40
37
  autoSyncSkills,
41
38
  formatAutoSyncNotice
42
- } from "./chunk-EQBYK77W.js";
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-FDBQOQLP.js";
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-SVTVBPIT.js";
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-BYSSJEL7.js";
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-RCQJI3UX.js";
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-TY3CJKKR.js";
227
+ } from "./chunk-YQXQBFHT.js";
204
228
 
205
229
  // src/cli.ts
206
230
  import { pathToFileURL } from "url";
@@ -750,7 +774,7 @@ function parseDemoTrustedProxies(values) {
750
774
  async function demoCommand(options) {
751
775
  const { host, port } = parseDemoListenOptions(options);
752
776
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
753
- const { createDemoServer } = await import("./demo-server-GWRYDPJC.js");
777
+ const { createDemoServer } = await import("./demo-server-BXABH765.js");
754
778
  const app = await createDemoServer({ trustProxy });
755
779
  let stopping = false;
756
780
  const stop = () => {
@@ -796,6 +820,908 @@ var DEMO_CLI_COMMANDS = [{
796
820
  }
797
821
  }];
798
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
+
799
1725
  // src/cli-commands/backfill.ts
800
1726
  var BACKFILL_CLI_COMMANDS = [
801
1727
  {
@@ -2333,10 +3259,16 @@ async function discoverPromotePreview(project2, sessionId, opts) {
2333
3259
  if (preview.suggestedCompetitors.length > 0) {
2334
3260
  console.log(` Suggested new competitors:`);
2335
3261
  for (const c of preview.suggestedCompetitors) {
2336
- console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})`);
3262
+ console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})${mergedHostsNote(c)}`);
2337
3263
  }
2338
3264
  console.log(" Only direct-competitor is promoted by default \u2014 pass --competitor-types to include other types.");
2339
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
+ }
2340
3272
  console.log(`
2341
3273
  Run \`canonry discover promote ${project2} ${sessionId}\` to merge cited + aspirational queries.`);
2342
3274
  console.log(" Add `--bucket wasted-surface` only when off-ICP competitor gaps should be tracked.");
@@ -2353,15 +3285,24 @@ async function discoverPromote(project2, sessionId, opts) {
2353
3285
  return;
2354
3286
  }
2355
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"]);
2356
3291
  console.log(`Promoted discovery session ${sessionId} into "${project2}":`);
2357
3292
  console.log(` Queries: ${promoted.queries.length} added, ${skipped.queries.length} already tracked`);
2358
3293
  for (const q of promoted.queries) console.log(` + ${q}`);
2359
- console.log(` Competitors: ${promoted.competitors.length} added, ${skipped.competitors.length} already tracked`);
2360
- for (const c of promoted.competitors) console.log(` + ${c}`);
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}`);
2361
3297
  if (promoted.queries.length === 0 && promoted.competitors.length === 0) {
2362
3298
  console.log(` Nothing new \u2014 the project's basket already covers this session.`);
2363
3299
  }
2364
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
+ }
2365
3306
  function printSessionDetail(session) {
2366
3307
  console.log(`Discovery session: ${session.id}`);
2367
3308
  console.log(` Status: ${session.status}`);
@@ -8149,21 +9090,28 @@ var TRAFFIC_CLI_COMMANDS = [
8149
9090
  function getClient10() {
8150
9091
  return createApiClient();
8151
9092
  }
8152
- async function addCompetitors(project2, domains, format) {
9093
+ async function addCompetitors(project2, domains, format, aliases = []) {
8153
9094
  const client = getClient10();
8154
9095
  const existing = await client.listCompetitors(project2);
8155
9096
  const existingDomains = existing.map((c) => c.domain);
8156
9097
  const existingSet = new Set(existingDomains);
8157
- const requested = new Set(uniqueStrings(domains));
8158
- const current = await client.appendCompetitors(project2, domains);
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
+ );
8159
9103
  const currentDomains = current.map((c) => c.domain);
8160
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;
8161
9107
  if (isMachineFormat(format)) {
8162
9108
  console.log(JSON.stringify({
8163
9109
  project: project2,
8164
9110
  domains: currentDomains,
8165
9111
  addedDomains,
8166
- addedCount: addedDomains.length
9112
+ addedCount: addedDomains.length,
9113
+ // The aliased competitor as the API returns it (`CompetitorDto`).
9114
+ ...aliasRow ? { competitor: aliasRow } : {}
8167
9115
  }, null, 2));
8168
9116
  return;
8169
9117
  }
@@ -8172,15 +9120,75 @@ async function addCompetitors(project2, domains, format) {
8172
9120
  } else {
8173
9121
  console.log(`Added ${addedDomains.length} competitor(s) to "${project2}".`);
8174
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))}`);
8175
9183
  }
8176
9184
  async function removeCompetitors(project2, domains, format) {
8177
9185
  const client = getClient10();
8178
9186
  const existing = await client.listCompetitors(project2);
8179
9187
  const existingDomains = existing.map((c) => c.domain);
8180
- const requested = new Set(uniqueStrings(domains));
9188
+ const requested = new Set(uniqueStrings(domains).map((domain) => normalizeCompetitorDomain(domain.trim())));
8181
9189
  const current = await client.deleteCompetitors(project2, domains);
8182
9190
  const currentSet = new Set(current.map((c) => c.domain));
8183
- const removedDomains = existingDomains.filter((domain) => requested.has(domain) && !currentSet.has(domain));
9191
+ const removedDomains = existingDomains.filter((domain) => requested.has(normalizeCompetitorDomain(domain)) && !currentSet.has(domain));
8184
9192
  if (isMachineFormat(format)) {
8185
9193
  console.log(JSON.stringify({
8186
9194
  project: project2,
@@ -8219,7 +9227,8 @@ async function listCompetitors(project2, format) {
8219
9227
  console.log(`Competitors for "${project2}" (${comps.length}):
8220
9228
  `);
8221
9229
  for (const c of comps) {
8222
- console.log(` ${c.domain}`);
9230
+ const aliases = storedAliases(c);
9231
+ console.log(aliases.length > 0 ? ` ${c.domain} (aliases: ${aliases.join(", ")})` : ` ${c.domain}`);
8223
9232
  }
8224
9233
  }
8225
9234
  async function showCompetitorLandscape(project2, options) {
@@ -8309,6 +9318,8 @@ function printLandscapeRows(heading, rows, showSampleCount = false, context, que
8309
9318
  }
8310
9319
 
8311
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]";
8312
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]";
8313
9324
  function parseLandscapeScope(value) {
8314
9325
  if (value === void 0 || value === "") return void 0;
@@ -8321,20 +9332,63 @@ function parseLandscapeScope(value) {
8321
9332
  var COMPETITOR_CLI_COMMANDS = [
8322
9333
  {
8323
9334
  path: ["competitor", "add"],
8324
- usage: "canonry competitor add <project> <domain...> [--format json]",
9335
+ usage: ADD_USAGE,
9336
+ options: {
9337
+ alias: multiStringOption()
9338
+ },
8325
9339
  run: async (input) => {
8326
- const project2 = requireProject(input, "competitor.add", "canonry competitor add <project> <domain...> [--format json]");
9340
+ const project2 = requireProject(input, "competitor.add", ADD_USAGE);
8327
9341
  const domains = input.positionals.slice(1);
8328
9342
  if (domains.length === 0) {
8329
- throw usageError("Error: project name and at least one domain required\nUsage: canonry competitor add <project> <domain...> [--format json]", {
9343
+ throw usageError(`Error: project name and at least one domain required
9344
+ Usage: ${ADD_USAGE}`, {
8330
9345
  message: "project name and at least one domain required",
8331
9346
  details: {
8332
9347
  command: "competitor.add",
8333
- usage: "canonry competitor add <project> <domain...> [--format json]"
9348
+ usage: ADD_USAGE
8334
9349
  }
8335
9350
  });
8336
9351
  }
8337
- await addCompetitors(project2, domains, input.format);
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 });
8338
9392
  }
8339
9393
  },
8340
9394
  {
@@ -8429,12 +9483,12 @@ var COMPETITOR_CLI_COMMANDS = [
8429
9483
  },
8430
9484
  {
8431
9485
  path: ["competitor"],
8432
- usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
9486
+ usage: "canonry competitor <add|aliases|remove|delete|list|landscape> <project> [args]",
8433
9487
  run: async (input) => {
8434
9488
  unknownSubcommand(input.positionals[0], {
8435
9489
  command: "competitor",
8436
- usage: "canonry competitor <add|remove|delete|list|landscape> <project> [args]",
8437
- 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"]
8438
9492
  });
8439
9493
  }
8440
9494
  }
@@ -11969,10 +13023,10 @@ Brand Gap Analysis`);
11969
13023
  console.log(`
11970
13024
  Opportunity Gaps (competitors cited, you're not):`);
11971
13025
  for (const q of data.gap) {
11972
- const competitors = q.competitorsCiting.join(", ");
13026
+ const competitors2 = q.competitorsCiting.join(", ");
11973
13027
  const cons = q.consistency.totalRuns > 0 ? ` [cited ${q.consistency.citedRuns}/${q.consistency.totalRuns} runs]` : "";
11974
13028
  console.log(` \u2022 ${q.query}${cons}`);
11975
- console.log(` Competitors: ${competitors}`);
13029
+ console.log(` Competitors: ${competitors2}`);
11976
13030
  }
11977
13031
  }
11978
13032
  if (data.cited.length > 0) {
@@ -14647,8 +15701,8 @@ function renderQueries(pdf, report) {
14647
15701
  for (const result of queryResult.providerResults) {
14648
15702
  const status = result.error ? "error" : result.mentioned ? result.cited ? "mentioned and cited" : "mentioned" : "not mentioned";
14649
15703
  const accuracy = result.describedAccurately === "not-mentioned" ? "" : `; accuracy: ${result.describedAccurately}`;
14650
- const competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
14651
- const line = `${result.displayName}: ${status}${accuracy}${competitors}`;
15704
+ const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
15705
+ const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
14652
15706
  pdf.bullet(line);
14653
15707
  if (result.error) {
14654
15708
  pdf.paragraph(`Error: ${result.error}`, { size: 9, color: FAIL, lineHeight: 12 });
@@ -14797,8 +15851,8 @@ function formatSnapshotMarkdown(report) {
14797
15851
  const mentioned = result.mentioned ? "Yes" : "No";
14798
15852
  const cited = result.cited ? "Yes" : "No";
14799
15853
  const accuracy = result.describedAccurately === "not-mentioned" ? "-" : result.describedAccurately;
14800
- const competitors = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
14801
- lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors} |`);
15854
+ const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
15855
+ lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
14802
15856
  }
14803
15857
  lines.push("");
14804
15858
  }
@@ -15097,7 +16151,7 @@ function renderHuman2(overview) {
15097
16151
  citationMovement,
15098
16152
  mentionMovement,
15099
16153
  movementComparison,
15100
- competitors,
16154
+ competitors: competitors2,
15101
16155
  providerScores,
15102
16156
  attentionItems,
15103
16157
  runHistory,
@@ -15160,9 +16214,9 @@ function renderHuman2(overview) {
15160
16214
  console.log(`
15161
16215
  Transitions since ${transitions.since}: +${transitions.gained} gained, -${transitions.lost} lost, ${transitions.emerging} emerging`);
15162
16216
  }
15163
- if (competitors.length > 0) {
16217
+ if (competitors2.length > 0) {
15164
16218
  console.log("\n Competitors:");
15165
- for (const c of competitors) {
16219
+ for (const c of competitors2) {
15166
16220
  console.log(` ${c.domain.padEnd(28)} ${c.citationCount}/${c.totalQueries} ${c.pressureLabel}`);
15167
16221
  }
15168
16222
  }
@@ -15212,13 +16266,13 @@ function printMentionShareBreakdown(mentionShare) {
15212
16266
  const ranking = legacyCompatible.ranking ?? [];
15213
16267
  const project2 = ranking.find((row) => row.kind === "project");
15214
16268
  if (!project2) return;
15215
- const competitors = ranking.filter((row) => row.kind === "competitor");
16269
+ const competitors2 = ranking.filter((row) => row.kind === "competitor");
15216
16270
  console.log(` you${" ".repeat(28)} ${project2.mentionSnapshots} mentions (${formatPercent(project2.share)} of combined)`);
15217
- for (const row of competitors.slice(0, 3)) {
16271
+ for (const row of competitors2.slice(0, 3)) {
15218
16272
  console.log(` ${(row.domain ?? "").padEnd(30)} ${row.mentionSnapshots} mentions (${formatPercent(row.share)} of combined)`);
15219
16273
  }
15220
- if (competitors.length > 3) {
15221
- console.log(` + ${competitors.length - 3} more competitor${competitors.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
16274
+ if (competitors2.length > 3) {
16275
+ console.log(` + ${competitors2.length - 3} more competitor${competitors2.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
15222
16276
  }
15223
16277
  }
15224
16278
 
@@ -15811,7 +16865,7 @@ var CONTENT_CLI_COMMANDS = [
15811
16865
  import crypto2 from "crypto";
15812
16866
  import path8 from "path";
15813
16867
  import { isDeepStrictEqual } from "util";
15814
- import { eq } from "drizzle-orm";
16868
+ import { eq as eq2 } from "drizzle-orm";
15815
16869
  function persistedValue(value) {
15816
16870
  if (Array.isArray(value)) return value.map(persistedValue);
15817
16871
  if (value && typeof value === "object") {
@@ -15871,7 +16925,7 @@ async function bootstrapCommand(opts) {
15871
16925
  projectId: apiKeys.projectId,
15872
16926
  lastUsedAt: apiKeys.lastUsedAt,
15873
16927
  revokedAt: apiKeys.revokedAt
15874
- }).from(apiKeys).where(eq(apiKeys.name, "default")).all();
16928
+ }).from(apiKeys).where(eq2(apiKeys.name, "default")).all();
15875
16929
  const existingDefault = existingDefaults.find((key) => key.keyHash === existingConfigKeyHash) ?? existingDefaults.at(0);
15876
16930
  if (existingDefault) {
15877
16931
  const rotating = existingDefault.keyHash !== keyHash;
@@ -15884,13 +16938,13 @@ async function bootstrapCommand(opts) {
15884
16938
  projectId: null,
15885
16939
  ...rotating ? { lastUsedAt: null } : {},
15886
16940
  revokedAt: null
15887
- }).where(eq(apiKeys.id, existingDefault.id)).run();
16941
+ }).where(eq2(apiKeys.id, existingDefault.id)).run();
15888
16942
  changed = true;
15889
16943
  }
15890
16944
  for (const duplicate of existingDefaults) {
15891
16945
  if (duplicate.id === existingDefault.id) continue;
15892
16946
  if (duplicate.revokedAt === null) {
15893
- tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq(apiKeys.id, duplicate.id)).run();
16947
+ tx.update(apiKeys).set({ revokedAt: rotatedAt }).where(eq2(apiKeys.id, duplicate.id)).run();
15894
16948
  changed = true;
15895
16949
  }
15896
16950
  }
@@ -16541,7 +17595,7 @@ function encodeLegacySetupState(state) {
16541
17595
  }
16542
17596
 
16543
17597
  // src/commands/serve.ts
16544
- import { and, asc, eq as eq3, ne } from "drizzle-orm";
17598
+ import { and as and2, asc as asc2, eq as eq4, ne } from "drizzle-orm";
16545
17599
 
16546
17600
  // src/server-shutdown.ts
16547
17601
  async function closeWithIdleSweep(app, intervalMs = 250) {
@@ -16555,7 +17609,7 @@ async function closeWithIdleSweep(app, intervalMs = 250) {
16555
17609
  }
16556
17610
 
16557
17611
  // src/startup-path-repairs.ts
16558
- import { eq as eq2, lt } from "drizzle-orm";
17612
+ import { eq as eq3, lt } from "drizzle-orm";
16559
17613
  var STARTUP_REPAIR_PAGE_SIZE = 4096;
16560
17614
  function repairNormalizedPathsOnStartup(db) {
16561
17615
  return repairPathsOnce(db, "ga-traffic-paths", backfillNormalizedPaths);
@@ -16564,7 +17618,7 @@ function repairAiReferralPathsOnStartup(db) {
16564
17618
  return repairPathsOnce(db, "ga-ai-referral-paths", backfillAiReferralPaths);
16565
17619
  }
16566
17620
  async function repairPathsOnce(db, name, repair) {
16567
- const completed = db.select({ version: dataRepairCompletions.version }).from(dataRepairCompletions).where(eq2(dataRepairCompletions.name, name)).get();
17621
+ const completed = db.select({ version: dataRepairCompletions.version }).from(dataRepairCompletions).where(eq3(dataRepairCompletions.name, name)).get();
16568
17622
  if (completed && completed.version >= URL_PATH_NORMALIZATION_VERSION) return null;
16569
17623
  let conflicted = false;
16570
17624
  const result = await repair(db, { pageSize: STARTUP_REPAIR_PAGE_SIZE, onConflict: () => {
@@ -16785,15 +17839,15 @@ function readServeOpenState(db) {
16785
17839
  id: projects.id,
16786
17840
  name: projects.name,
16787
17841
  createdAt: projects.createdAt
16788
- }).from(projects).orderBy(asc(projects.createdAt), asc(projects.name)).all();
17842
+ }).from(projects).orderBy(asc2(projects.createdAt), asc2(projects.name)).all();
16789
17843
  if (rows.length === 0) return { projectCount: 0, hasSiteAudit: false };
16790
17844
  let scanned;
16791
17845
  try {
16792
17846
  const audits = db.select({
16793
17847
  projectId: runs.projectId,
16794
17848
  status: runs.status
16795
- }).from(runs).where(and(
16796
- eq3(runs.kind, RunKinds["site-audit"]),
17849
+ }).from(runs).where(and2(
17850
+ eq4(runs.kind, RunKinds["site-audit"]),
16797
17851
  ne(runs.trigger, RunTriggers.probe)
16798
17852
  )).all();
16799
17853
  scanned = new Set(
@@ -20592,7 +21646,7 @@ Page Health:
20592
21646
  Projects:
20593
21647
  project Create, update, list, show, delete projects
20594
21648
  query Add, replace, remove, list, import, generate queries
20595
- competitor Add, remove, list competitors
21649
+ competitor Add, remove, list competitors and their aliases
20596
21650
 
20597
21651
  Monitoring:
20598
21652
  run Trigger visibility sweeps