@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.
- package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +3 -2
- package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +1 -1
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +31 -3
- package/assets/assets/{AuditHistoryPanel-Duvyi8vx.js → AuditHistoryPanel-DP92khTY.js} +1 -1
- package/assets/assets/{BacklinksPage-CY5AllQu.js → BacklinksPage-BI_9t1iI.js} +1 -1
- package/assets/assets/{HistoryPage-C0Gr0gcj.js → HistoryPage-tDE6Nv0J.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-DrAB6VKX.js → MeasurementPropertyPage-Y2F334Qs.js} +1 -1
- package/assets/assets/ProjectPage-DW581TNr.js +8 -0
- package/assets/assets/{RunsPage-DMxOrtfl.js → RunsPage-Bl_c9-vA.js} +1 -1
- package/assets/assets/{SettingsPage-sxiUNt2A.js → SettingsPage-DQ5_Q2rc.js} +1 -1
- package/assets/assets/{SiteHealthSection--6tZe3hM.js → SiteHealthSection-DdZruU72.js} +3 -3
- package/assets/assets/{TrafficPage-CvSQDNTi.js → TrafficPage-CROectSE.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-BaYeIBmc.js → TrafficSourceDetailPage-Bthvvwg5.js} +1 -1
- package/assets/assets/{extract-error-message-76IWhqQF.js → extract-error-message-DOLCGQMa.js} +1 -1
- package/assets/assets/{index-BlrH2zRW.js → index-CCOcEGdz.js} +21 -21
- package/assets/assets/{react-sigma_core.esm.min-CkAcKW2k.js → react-sigma_core.esm.min-TWiq-VWU.js} +1 -1
- package/assets/assets/{v2-overview-adapter-DnEE4E9w.js → v2-overview-adapter-CxmV639A.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-EQBYK77W.js → chunk-3JEUZ2K5.js} +3 -3
- package/dist/{chunk-BYSSJEL7.js → chunk-FWV6IIYY.js} +30 -9
- package/dist/{chunk-UIDHHZH3.js → chunk-MQPBOQUH.js} +292 -1266
- package/dist/{chunk-SVTVBPIT.js → chunk-QND5LEAC.js} +2 -2
- package/dist/{chunk-FDBQOQLP.js → chunk-RVELNJAW.js} +29 -2
- package/dist/{chunk-RCQJI3UX.js → chunk-XODXE6PT.js} +3190 -2721
- package/dist/{chunk-TY3CJKKR.js → chunk-YQXQBFHT.js} +5916 -5416
- package/dist/cli.js +1113 -59
- package/dist/{demo-server-GWRYDPJC.js → demo-server-BXABH765.js} +6 -6
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-3L2665AK.js → intelligence-service-EIWYQYVG.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +13 -13
- 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
|
-
|
|
5
|
-
backfillAiReferralPathsCommand,
|
|
6
|
-
backfillAnswerMentionsCommand,
|
|
7
|
-
backfillAnswerVisibilityCommand,
|
|
8
|
-
backfillInsightsCommand,
|
|
9
|
-
backfillNormalizedPaths,
|
|
10
|
-
backfillNormalizedPathsCommand,
|
|
11
|
-
backfillSnapshotAttributionCommand,
|
|
12
|
-
backfillTrafficClassificationCommand,
|
|
4
|
+
OPENAI_RETRIEVAL_CONTRACT,
|
|
13
5
|
checkLatestVersionForCli,
|
|
14
6
|
cliRuntimeContext,
|
|
15
7
|
coerceAgentProvider,
|
|
@@ -28,6 +20,11 @@ import {
|
|
|
28
20
|
listAgentProviders,
|
|
29
21
|
notePrintedUpdateAvailable,
|
|
30
22
|
readCachedUpdateAvailable,
|
|
23
|
+
reparseStoredResult,
|
|
24
|
+
reparseStoredResult2,
|
|
25
|
+
reparseStoredResult3,
|
|
26
|
+
reparseStoredResult4,
|
|
27
|
+
reparseStoredResult5,
|
|
31
28
|
setGoogleAuthConfig,
|
|
32
29
|
setTelemetryPreference,
|
|
33
30
|
setTelemetrySource,
|
|
@@ -35,11 +32,11 @@ import {
|
|
|
35
32
|
trackCliCommandFinished,
|
|
36
33
|
trackEvent,
|
|
37
34
|
waitForServerRuntimeStartup
|
|
38
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-MQPBOQUH.js";
|
|
39
36
|
import {
|
|
40
37
|
autoSyncSkills,
|
|
41
38
|
formatAutoSyncNotice
|
|
42
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-3JEUZ2K5.js";
|
|
43
40
|
import {
|
|
44
41
|
configExists,
|
|
45
42
|
createApiClient,
|
|
@@ -56,13 +53,13 @@ import {
|
|
|
56
53
|
saveConfig,
|
|
57
54
|
saveConfigPatch,
|
|
58
55
|
shouldPrintServeSkillsNudge
|
|
59
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-RVELNJAW.js";
|
|
60
57
|
import {
|
|
61
58
|
DEFAULT_DEMO_TRUSTED_PROXIES,
|
|
62
59
|
trustedProxiesProblem,
|
|
63
60
|
trustedProxyProblem
|
|
64
61
|
} from "./chunk-RB2F4D3Z.js";
|
|
65
|
-
import "./chunk-
|
|
62
|
+
import "./chunk-QND5LEAC.js";
|
|
66
63
|
import {
|
|
67
64
|
CliError,
|
|
68
65
|
EXIT_SYSTEM_ERROR,
|
|
@@ -74,22 +71,36 @@ import {
|
|
|
74
71
|
runMeasurementDraftAction,
|
|
75
72
|
systemError,
|
|
76
73
|
usageError
|
|
77
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-FWV6IIYY.js";
|
|
78
75
|
import {
|
|
79
76
|
CLOUDFLARE_WORKER_BINDINGS,
|
|
80
77
|
CLOUDFLARE_WORKER_GENERATED_MARKER,
|
|
81
78
|
CLOUDFLARE_WRANGLER_GENERATED_MARKER,
|
|
82
79
|
DEFAULT_BOT_LIST,
|
|
80
|
+
aiUserFetchEventsHourly,
|
|
83
81
|
apiKeys,
|
|
82
|
+
auditLog,
|
|
83
|
+
backfillProjectAnswerMentions,
|
|
84
|
+
classifyAiUserFetch,
|
|
85
|
+
classifyCrawler,
|
|
86
|
+
competitors,
|
|
87
|
+
crawlerEventsHourly,
|
|
84
88
|
createClient,
|
|
89
|
+
createRunCompetitorResolver,
|
|
85
90
|
dataRepairCompletions,
|
|
91
|
+
determineCitationState,
|
|
92
|
+
gaAiReferrals,
|
|
93
|
+
gaTrafficSnapshots,
|
|
86
94
|
generateWorkerScript,
|
|
87
95
|
generateWranglerToml,
|
|
88
96
|
migrate,
|
|
97
|
+
parseJsonColumn,
|
|
89
98
|
projects,
|
|
90
99
|
queries,
|
|
100
|
+
querySnapshots,
|
|
101
|
+
rawEventSamples,
|
|
91
102
|
runs
|
|
92
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-XODXE6PT.js";
|
|
93
104
|
import {
|
|
94
105
|
AdsDeliverySnapshotStatuses,
|
|
95
106
|
AdsHistoricalCampaignRollupStatuses,
|
|
@@ -102,19 +113,25 @@ import {
|
|
|
102
113
|
CheckScopes,
|
|
103
114
|
CheckStatuses,
|
|
104
115
|
CitationStates,
|
|
116
|
+
DiscoveryPromoteCompetitorSkipReasons,
|
|
105
117
|
FEEDBACK_KINDS,
|
|
106
118
|
GaMoverChangeBases,
|
|
107
119
|
MeasurementEvidenceShapes,
|
|
108
120
|
OUTSTANDING_PROVIDER_BATCH_STATUSES,
|
|
109
121
|
ProviderBatchStatuses,
|
|
122
|
+
ProviderNames,
|
|
110
123
|
READ_ONLY_SCOPE,
|
|
111
124
|
RatioUnits,
|
|
112
125
|
ResearchRunStatuses,
|
|
126
|
+
RetrievalContracts,
|
|
113
127
|
RunKinds,
|
|
114
128
|
RunStatuses,
|
|
115
129
|
RunTriggers,
|
|
130
|
+
TrafficEventConfidences,
|
|
116
131
|
TrafficEventKinds,
|
|
132
|
+
TrafficEvidenceKinds,
|
|
117
133
|
TrafficSeriesGranularities,
|
|
134
|
+
TrafficSourceTypes,
|
|
118
135
|
UNATTRIBUTED_MENTION_REASON,
|
|
119
136
|
URL_PATH_NORMALIZATION_VERSION,
|
|
120
137
|
USER_PASSWORD_MIN_LENGTH,
|
|
@@ -137,13 +154,17 @@ import {
|
|
|
137
154
|
buildModelChangeNotice,
|
|
138
155
|
canonicalizeGtmAccountId,
|
|
139
156
|
canonicalizeGtmResourceSelection,
|
|
157
|
+
computeCompetitorOverlap,
|
|
140
158
|
contentTargetDismissRequestSchema,
|
|
141
159
|
conversionTrackingContractWriteRequestSchema,
|
|
142
160
|
deduplicateResearchQueries,
|
|
143
161
|
describeError,
|
|
162
|
+
determineAnswerMentioned,
|
|
144
163
|
discoveryBucketSchema,
|
|
145
164
|
discoveryCompetitorTypeSchema,
|
|
165
|
+
effectiveBrandNames,
|
|
146
166
|
effectiveDomains,
|
|
167
|
+
extractRecommendedCompetitors,
|
|
147
168
|
factorStatusFromScore,
|
|
148
169
|
feedbackSubmissionSchema,
|
|
149
170
|
formatGbpMetricLabel,
|
|
@@ -172,8 +193,11 @@ import {
|
|
|
172
193
|
measurementQueryTemplateApplyRequestSchema,
|
|
173
194
|
measurementQueryTemplateUpsertRequestSchema,
|
|
174
195
|
measurementQuestionResultQuerySchema,
|
|
196
|
+
normalizeCompetitorAliases,
|
|
197
|
+
normalizeCompetitorDomain,
|
|
175
198
|
normalizeProjectAliases,
|
|
176
199
|
normalizeTelemetryStatus,
|
|
200
|
+
normalizeUrlPath,
|
|
177
201
|
notificationEventSchema,
|
|
178
202
|
parseTrafficAnalyticsPeriod,
|
|
179
203
|
providerDispatchModeSchema,
|
|
@@ -200,7 +224,7 @@ import {
|
|
|
200
224
|
visibilityCompareSelectionSchema,
|
|
201
225
|
visibilityReportRequestSchema,
|
|
202
226
|
winnabilityClassSchema
|
|
203
|
-
} from "./chunk-
|
|
227
|
+
} from "./chunk-YQXQBFHT.js";
|
|
204
228
|
|
|
205
229
|
// src/cli.ts
|
|
206
230
|
import { pathToFileURL } from "url";
|
|
@@ -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-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
9335
|
+
usage: ADD_USAGE,
|
|
9336
|
+
options: {
|
|
9337
|
+
alias: multiStringOption()
|
|
9338
|
+
},
|
|
8325
9339
|
run: async (input) => {
|
|
8326
|
-
const project2 = requireProject(input, "competitor.add",
|
|
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(
|
|
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:
|
|
9348
|
+
usage: ADD_USAGE
|
|
8334
9349
|
}
|
|
8335
9350
|
});
|
|
8336
9351
|
}
|
|
8337
|
-
|
|
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
|
|
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: ${
|
|
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
|
|
14651
|
-
const line = `${result.displayName}: ${status}${accuracy}${
|
|
15704
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? `; recommended instead: ${result.recommendedCompetitors.join(", ")}` : "";
|
|
15705
|
+
const line = `${result.displayName}: ${status}${accuracy}${competitors2}`;
|
|
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
|
|
14801
|
-
lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${
|
|
15854
|
+
const competitors2 = result.recommendedCompetitors.length > 0 ? result.recommendedCompetitors.join(", ") : "-";
|
|
15855
|
+
lines.push(`| ${result.displayName} | ${mentioned} | ${cited} | ${accuracy} | ${competitors2} |`);
|
|
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 (
|
|
16217
|
+
if (competitors2.length > 0) {
|
|
15164
16218
|
console.log("\n Competitors:");
|
|
15165
|
-
for (const c of
|
|
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
|
|
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
|
|
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 (
|
|
15221
|
-
console.log(` + ${
|
|
16274
|
+
if (competitors2.length > 3) {
|
|
16275
|
+
console.log(` + ${competitors2.length - 3} more competitor${competitors2.length - 3 === 1 ? "" : "s"} (--format json for full breakdown)`);
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
16796
|
-
|
|
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
|