@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.
Files changed (35) 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 +6 -4
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +47 -4
  4. package/assets/assets/{AuditHistoryPanel-CZWzgyrD.js → AuditHistoryPanel-DP92khTY.js} +1 -1
  5. package/assets/assets/{BacklinksPage-D23A-ahd.js → BacklinksPage-BI_9t1iI.js} +1 -1
  6. package/assets/assets/{HistoryPage-BKGVxmNz.js → HistoryPage-tDE6Nv0J.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-BSxn6_RB.js → MeasurementPropertyPage-Y2F334Qs.js} +1 -1
  8. package/assets/assets/ProjectPage-DW581TNr.js +8 -0
  9. package/assets/assets/{RunsPage-CeBBjLSE.js → RunsPage-Bl_c9-vA.js} +1 -1
  10. package/assets/assets/{SettingsPage-Jp5Yp4cz.js → SettingsPage-DQ5_Q2rc.js} +1 -1
  11. package/assets/assets/{SiteHealthSection-DzVegBeN.js → SiteHealthSection-DdZruU72.js} +3 -3
  12. package/assets/assets/{TrafficPage-B66AYwWk.js → TrafficPage-CROectSE.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-CXtq1yES.js → TrafficSourceDetailPage-Bthvvwg5.js} +1 -1
  14. package/assets/assets/{extract-error-message-BVa0lOze.js → extract-error-message-DOLCGQMa.js} +1 -1
  15. package/assets/assets/index-CCOcEGdz.js +86 -0
  16. package/assets/assets/index-HWSQgVum.css +1 -0
  17. package/assets/assets/{react-sigma_core.esm.min-60drsJa6.js → react-sigma_core.esm.min-TWiq-VWU.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-CLUi2Lkv.js → v2-overview-adapter-CxmV639A.js} +1 -1
  19. package/assets/index.html +2 -2
  20. package/dist/{chunk-IAMECH2N.js → chunk-3JEUZ2K5.js} +3 -3
  21. package/dist/{chunk-2OQ6D26U.js → chunk-FWV6IIYY.js} +37 -12
  22. package/dist/{chunk-22R5E5CY.js → chunk-MQPBOQUH.js} +292 -1266
  23. package/dist/{chunk-U2S5ABOR.js → chunk-QND5LEAC.js} +2 -2
  24. package/dist/{chunk-MTSFSXCG.js → chunk-RVELNJAW.js} +29 -2
  25. package/dist/{chunk-VDX3LYKL.js → chunk-XODXE6PT.js} +3205 -2729
  26. package/dist/{chunk-YGHPT5B2.js → chunk-YQXQBFHT.js} +5936 -5414
  27. package/dist/cli.js +1137 -67
  28. package/dist/{demo-server-TIMYQLXS.js → demo-server-BXABH765.js} +6 -6
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-YCMRTVCI.js → intelligence-service-EIWYQYVG.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. package/package.json +13 -13
  33. package/assets/assets/ProjectPage-D9W8G_1m.js +0 -8
  34. package/assets/assets/index-CMe_-fAB.css +0 -1
  35. 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
- 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-22R5E5CY.js";
35
+ } from "./chunk-MQPBOQUH.js";
39
36
  import {
40
37
  autoSyncSkills,
41
38
  formatAutoSyncNotice
42
- } from "./chunk-IAMECH2N.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-MTSFSXCG.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-U2S5ABOR.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-2OQ6D26U.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-VDX3LYKL.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-YGHPT5B2.js";
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
- console.log(`${value.selection.queryClass} sentiment evidence: ${value.state} \xB7 definition ${value.selection.evaluationDefinitionId ?? "not measured"}`);
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-TIMYQLXS.js");
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 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}`);
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(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
+ );
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
- console.log(` ${c.domain}`);
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: "canonry competitor add <project> <domain...> [--format json]",
9335
+ usage: ADD_USAGE,
9336
+ options: {
9337
+ alias: multiStringOption()
9338
+ },
8309
9339
  run: async (input) => {
8310
- const project2 = requireProject(input, "competitor.add", "canonry competitor add <project> <domain...> [--format json]");
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("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}`, {
8314
9345
  message: "project name and at least one domain required",
8315
9346
  details: {
8316
9347
  command: "competitor.add",
8317
- usage: "canonry competitor add <project> <domain...> [--format json]"
9348
+ usage: ADD_USAGE
8318
9349
  }
8319
9350
  });
8320
9351
  }
8321
- 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 });
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 competitors = q.competitorsCiting.join(", ");
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: ${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 competitors = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
14635
- 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}`;
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 competitors = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
14785
- 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} |`);
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 overallSentiment = overview.sentiment?.overall;
15114
- if (overview.sentiment?.configured && overallSentiment && overallSentiment.coverage.judged > 0 && overallSentiment.score.favorableRate !== null) {
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
- Overall sentiment: ${overallSentiment.score.favorableDisplay} favorable \xB7 all query classes`);
15117
- console.log(` ${overallSentiment.coverage.counts.favorable} favorable / ${overallSentiment.coverage.judged} judged${overallSentiment.provisional ? " \xB7 provisional" : ""}`);
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 (competitors.length > 0) {
16217
+ if (competitors2.length > 0) {
15148
16218
  console.log("\n Competitors:");
15149
- for (const c of competitors) {
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 competitors = ranking.filter((row) => row.kind === "competitor");
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 competitors.slice(0, 3)) {
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 (competitors.length > 3) {
15205
- 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)`);
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(eq(apiKeys.name, "default")).all();
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(eq(apiKeys.id, existingDefault.id)).run();
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(eq(apiKeys.id, duplicate.id)).run();
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 eq3, ne } from "drizzle-orm";
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 eq2, lt } from "drizzle-orm";
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(eq2(dataRepairCompletions.name, name)).get();
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(asc(projects.createdAt), asc(projects.name)).all();
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(and(
16780
- eq3(runs.kind, RunKinds["site-audit"]),
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