@canonry/canonry 4.136.1 → 4.137.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 (29) hide show
  1. package/README.md +2 -1
  2. package/assets/assets/{AuditHistoryPanel-B73VrWNb.js → AuditHistoryPanel-B6GEmlVM.js} +1 -1
  3. package/assets/assets/{BacklinksPage-CLVMg_qx.js → BacklinksPage-Cr3QegNo.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-P81CbvWc.js → ChartPrimitives-rhsGzU1A.js} +1 -1
  5. package/assets/assets/{HistoryPage-IcDqWBYW.js → HistoryPage-B3HrxyPP.js} +1 -1
  6. package/assets/assets/ProjectPage-D3WiL3RI.js +8 -0
  7. package/assets/assets/{RunRow-BTH3-Tml.js → RunRow-MsV1RLPm.js} +1 -1
  8. package/assets/assets/{RunsPage-B0k5wjFN.js → RunsPage-BVlv0j3Y.js} +1 -1
  9. package/assets/assets/{SettingsPage-DGnT3Vdr.js → SettingsPage-Dfd-T4o9.js} +1 -1
  10. package/assets/assets/{TrafficPage-D79PC0bB.js → TrafficPage-Bl1jj73m.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BYEVjfFm.js → TrafficSourceDetailPage-CinQcKmY.js} +1 -1
  12. package/assets/assets/{arrow-left-DCdFYJK-.js → arrow-left-CHU6s_Xn.js} +1 -1
  13. package/assets/assets/extract-error-message-BDdyZgyf.js +1 -0
  14. package/assets/assets/index-BNulljlk.js +213 -0
  15. package/assets/assets/{trash-2-CEz2iXBv.js → trash-2-BE5oH1Tj.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-PIMWEIJ3.js → chunk-7RF5EDUO.js} +3141 -400
  18. package/dist/{chunk-YQ2RXMU2.js → chunk-PBTB3FCN.js} +207 -195
  19. package/dist/{chunk-OESGFFUO.js → chunk-UVWI6VPP.js} +1 -1
  20. package/dist/{chunk-YMF35LN5.js → chunk-XEX5CLQW.js} +163 -133
  21. package/dist/cli.js +147 -33
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-BKLR57RQ.js → intelligence-service-JEFHUGVX.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +9 -9
  27. package/assets/assets/ProjectPage-DTivvVIl.js +0 -8
  28. package/assets/assets/extract-error-message-bM0aUBUI.js +0 -1
  29. package/assets/assets/index-CMuA-uEN.js +0 -210
@@ -248,6 +248,8 @@ import {
248
248
  gscUrlInspectionDtoSchema,
249
249
  hasLocationLabel,
250
250
  healthSnapshotDtoSchema,
251
+ hostMatchesAnyDomain,
252
+ hostMatchesDomain,
251
253
  hostOf,
252
254
  indexingRequestResponseDtoSchema,
253
255
  internalError,
@@ -262,6 +264,8 @@ import {
262
264
  mentionStateFromAnswerMentioned,
263
265
  missingDependency,
264
266
  modelIdsEquivalent,
267
+ noProvider,
268
+ noQueries,
265
269
  normalizeIdTokens,
266
270
  normalizeModelId,
267
271
  normalizeProjectAliases,
@@ -271,6 +275,7 @@ import {
271
275
  notFound,
272
276
  notImplemented,
273
277
  notificationDtoSchema,
278
+ onboardingTelemetryEventSchema,
274
279
  operationInProgress,
275
280
  orderLocationsDefaultFirst,
276
281
  organicEvidenceDtoSchema,
@@ -336,6 +341,10 @@ import {
336
341
  summarizeCheckResults,
337
342
  surfaceClassFromCompetitorType,
338
343
  surfaceClassLabel,
344
+ telemetryEventAcceptedDtoSchema,
345
+ textContainsAnyBrandAlias,
346
+ textContainsBrandAlias,
347
+ textContainsDomain,
339
348
  trafficBackfillResponseSchema,
340
349
  trafficConnectVercelRequestSchema,
341
350
  trafficConnectWordpressRequestSchema,
@@ -368,7 +377,7 @@ import {
368
377
  wordpressSchemaDeployResultDtoSchema,
369
378
  wordpressSchemaStatusResultDtoSchema,
370
379
  wordpressStatusDtoSchema
371
- } from "./chunk-PIMWEIJ3.js";
380
+ } from "./chunk-7RF5EDUO.js";
372
381
 
373
382
  // src/intelligence-service.ts
374
383
  import { eq as eq44, desc as desc21, asc as asc9, and as and35, ne as ne6, or as or10, inArray as inArray15, gte as gte12, lte as lte8 } from "drizzle-orm";
@@ -5466,24 +5475,19 @@ function groupInsights(insights2, keyFn = (i) => JSON.stringify([i.query, i.prov
5466
5475
  var TRANSACTIONAL_RE = /\b(?:buy|price|pricing|cost|hire|near me|services?|agency|consultant|company)\b/i;
5467
5476
  var INFORMATIONAL_RE = /\b(?:what|how|why|when|guide|tutorial|vs|versus|alternatives?|examples?|definition)\b/i;
5468
5477
  var MIN_BRAND_TOKEN_LENGTH = 3;
5469
- function compact(value) {
5470
- return value.toLowerCase().replace(/[^a-z0-9]/g, "");
5471
- }
5472
5478
  function buildBrandTokens(canonicalDomain, brandNames = []) {
5473
5479
  const seen = /* @__PURE__ */ new Set();
5474
- const stem = canonicalDomain.toLowerCase().replace(/\.[a-z]{2,}$/, "");
5475
- const stemCompact = compact(stem);
5480
+ const stemCompact = brandKeyFromText(brandLabelFromDomain(canonicalDomain));
5476
5481
  if (stemCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(stemCompact);
5477
5482
  for (const name of brandNames) {
5478
5483
  if (!name) continue;
5479
- const nameCompact = compact(name);
5484
+ const nameCompact = brandKeyFromText(name);
5480
5485
  if (nameCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(nameCompact);
5481
5486
  }
5482
5487
  return [...seen];
5483
5488
  }
5484
5489
  function categorizeQueryByIntent(query, brandTokens) {
5485
- const compactQuery = compact(query);
5486
- if (brandTokens.length > 0 && brandTokens.some((t) => compactQuery.includes(t))) {
5490
+ if (textContainsAnyBrandAlias(query, brandTokens)) {
5487
5491
  return "brand";
5488
5492
  }
5489
5493
  if (TRANSACTIONAL_RE.test(query)) return "lead-gen";
@@ -5551,12 +5555,7 @@ function buildCitationScorecard(snapshots, queryLookup) {
5551
5555
 
5552
5556
  // ../intelligence/src/domain-matching.ts
5553
5557
  function citedDomainBelongsToProject(citedDomain, projectDomains) {
5554
- const candidate = normalizeProjectDomain(citedDomain);
5555
- for (const domain of projectDomains) {
5556
- const normalized = normalizeProjectDomain(domain);
5557
- if (candidate === normalized || candidate.endsWith(`.${normalized}`)) return true;
5558
- }
5559
- return false;
5558
+ return hostMatchesAnyDomain(citedDomain, projectDomains);
5560
5559
  }
5561
5560
 
5562
5561
  // ../intelligence/src/competitor-landscape.ts
@@ -5578,11 +5577,8 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
5578
5577
  entry.count++;
5579
5578
  if (q) entry.queries.add(q);
5580
5579
  }
5581
- const competitorNorm = normalizeUrlDomain(competitor);
5582
5580
  for (const gs of snap.groundingSources) {
5583
- const host = normalizeUrlDomain(extractHostFromUri(gs.uri));
5584
- if (!host) continue;
5585
- if (host === competitorNorm || host.endsWith(`.${competitorNorm}`)) {
5581
+ if (hostMatchesDomain(gs.uri, competitor)) {
5586
5582
  const entry = competitorMap.get(competitor);
5587
5583
  const pageQueries = entry.pages.get(gs.uri) ?? /* @__PURE__ */ new Set();
5588
5584
  if (q) pageQueries.add(q);
@@ -5616,16 +5612,6 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
5616
5612
  competitorRows.sort((a, b) => b.citationCount - a.citationCount);
5617
5613
  return { projectCitationCount, competitors: competitorRows };
5618
5614
  }
5619
- function normalizeUrlDomain(domain) {
5620
- return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
5621
- }
5622
- function extractHostFromUri(uri) {
5623
- try {
5624
- return new URL(uri).hostname;
5625
- } catch {
5626
- return "";
5627
- }
5628
- }
5629
5615
 
5630
5616
  // ../intelligence/src/mention-landscape.ts
5631
5617
  function buildMentionLandscape(snapshots, competitorDomains, projectBrandNames, projectDomains, queryLookup) {
@@ -6043,10 +6029,9 @@ function buildCompetitorPressureScore(snapshots, competitorDomains, totalTracked
6043
6029
  trend: []
6044
6030
  };
6045
6031
  }
6046
- const competitorSet = new Set(competitorDomains);
6047
6032
  let overlapCount = 0;
6048
6033
  for (const snap of snapshots) {
6049
- if (snap.competitorOverlap.some((d) => competitorSet.has(d))) {
6034
+ if (snap.competitorOverlap.some((domain) => hostMatchesAnyDomain(domain, competitorDomains))) {
6050
6035
  overlapCount++;
6051
6036
  }
6052
6037
  }
@@ -6071,7 +6056,7 @@ function buildOverviewCompetitors(snapshots, competitors2, queryLookup) {
6071
6056
  return competitors2.map((competitor, index2) => {
6072
6057
  const citedQuerySet = /* @__PURE__ */ new Set();
6073
6058
  for (const snap of snapshots) {
6074
- if (snap.competitorOverlap.includes(competitor.domain) || snap.citedDomains.includes(competitor.domain)) {
6059
+ if (snap.competitorOverlap.some((domain) => hostMatchesDomain(domain, competitor.domain)) || snap.citedDomains.some((domain) => hostMatchesDomain(domain, competitor.domain))) {
6075
6060
  if (snap.queryId) citedQuerySet.add(snap.queryId);
6076
6061
  }
6077
6062
  }
@@ -6218,15 +6203,22 @@ function buildMentionShare(snapshots, options) {
6218
6203
  let projectMentionSnapshots = 0;
6219
6204
  let snapshotsWithAnswerText = 0;
6220
6205
  const competitorCounts = /* @__PURE__ */ new Map();
6221
- for (const c of options.competitors) competitorCounts.set(c.domain, 0);
6206
+ const competitorAliases = /* @__PURE__ */ new Map();
6207
+ for (const competitor of options.competitors) {
6208
+ competitorCounts.set(competitor.domain, 0);
6209
+ competitorAliases.set(
6210
+ competitor.domain,
6211
+ competitor.brandTokens.filter((alias) => brandKeyFromText(alias).length >= 3)
6212
+ );
6213
+ }
6222
6214
  for (const snap of snapshots) {
6223
6215
  const text2 = snap.answerText ?? "";
6224
6216
  if (text2.length === 0) continue;
6225
6217
  snapshotsWithAnswerText++;
6226
6218
  if (snap.projectMentioned) projectMentionSnapshots++;
6227
- const answerBrandKey = brandKeyFromText(text2);
6228
6219
  for (const competitor of options.competitors) {
6229
- if (competitorMentioned(text2, answerBrandKey, competitor.brandTokens)) {
6220
+ const aliases = competitorAliases.get(competitor.domain) ?? [];
6221
+ if (textContainsAnyBrandAlias(text2, aliases)) {
6230
6222
  competitorCounts.set(competitor.domain, (competitorCounts.get(competitor.domain) ?? 0) + 1);
6231
6223
  }
6232
6224
  }
@@ -6269,16 +6261,6 @@ function mentionShareTone(score) {
6269
6261
  if (score >= 25) return "caution";
6270
6262
  return "negative";
6271
6263
  }
6272
- function competitorMentioned(text2, answerBrandKey, brandTokens) {
6273
- for (const token of brandTokens) {
6274
- if (token.length < 3) continue;
6275
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6276
- if (new RegExp(`\\b${escaped}\\b`, "i").test(text2)) return true;
6277
- const tokenBrandKey = brandKeyFromText(token);
6278
- if (tokenBrandKey.length >= 3 && answerBrandKey.includes(tokenBrandKey)) return true;
6279
- }
6280
- return false;
6281
- }
6282
6264
  function describe(parts) {
6283
6265
  const { score, projectMentionSnapshots, competitorMentionSnapshots, perCompetitor } = parts;
6284
6266
  if (projectMentionSnapshots === 0 && competitorMentionSnapshots === 0) {
@@ -7991,9 +7973,20 @@ async function runRoutes(app, opts) {
7991
7973
  rawProviders.splice(0, rawProviders.length, ...normalized);
7992
7974
  }
7993
7975
  const providers = rawProviders?.length ? rawProviders : void 0;
7976
+ const shouldPreflight = trigger !== RunTriggers.probe && opts.getRunnableProviderNames !== void 0;
7977
+ const trackedRows = body.queries?.length || shouldPreflight ? app.db.select({ query: queries.query }).from(queries).where(eq8(queries.projectId, project.id)).all() : [];
7978
+ if (shouldPreflight) {
7979
+ const preflightError = answerVisibilityPreflightError({
7980
+ projectName: project.name,
7981
+ projectProviders: project.providers,
7982
+ requestedProviders: providers,
7983
+ runnableProviderNames: opts.getRunnableProviderNames(),
7984
+ trackedQueryCount: trackedRows.length
7985
+ });
7986
+ if (preflightError) throw preflightError;
7987
+ }
7994
7988
  let scopedQueries = null;
7995
7989
  if (body.queries?.length) {
7996
- const trackedRows = app.db.select({ query: queries.query }).from(queries).where(eq8(queries.projectId, project.id)).all();
7997
7990
  const tracked = new Set(trackedRows.map((r) => r.query));
7998
7991
  const missing = body.queries.filter((q) => !tracked.has(q));
7999
7992
  if (missing.length) {
@@ -8157,7 +8150,28 @@ async function runRoutes(app, opts) {
8157
8150
  const providers = rawProviders?.length ? rawProviders : void 0;
8158
8151
  const now = (/* @__PURE__ */ new Date()).toISOString();
8159
8152
  const results = [];
8153
+ const runnableProviderNames = opts.getRunnableProviderNames?.();
8160
8154
  for (const project of allProjects) {
8155
+ if (runnableProviderNames) {
8156
+ const trackedQuery = app.db.select({ id: queries.id }).from(queries).where(eq8(queries.projectId, project.id)).limit(1).get();
8157
+ const preflightError = answerVisibilityPreflightError({
8158
+ projectName: project.name,
8159
+ projectProviders: project.providers,
8160
+ requestedProviders: providers,
8161
+ runnableProviderNames,
8162
+ trackedQueryCount: trackedQuery ? 1 : 0
8163
+ });
8164
+ if (preflightError) {
8165
+ results.push({
8166
+ projectName: project.name,
8167
+ projectId: project.id,
8168
+ status: "error",
8169
+ error: preflightError.message,
8170
+ errorCode: preflightError.code
8171
+ });
8172
+ continue;
8173
+ }
8174
+ }
8161
8175
  const projectLocations = project.locations;
8162
8176
  let resolvedLocation;
8163
8177
  if (project.defaultLocation) {
@@ -8221,6 +8235,26 @@ async function runRoutes(app, opts) {
8221
8235
  return reply.send(loadRunDetail(app, run));
8222
8236
  });
8223
8237
  }
8238
+ function answerVisibilityPreflightError(input) {
8239
+ if (input.trackedQueryCount === 0) {
8240
+ return noQueries(input.projectName);
8241
+ }
8242
+ const availableProviders = normalizeProviderNames(input.runnableProviderNames);
8243
+ const requestedProviders = normalizeProviderNames(input.requestedProviders ?? []);
8244
+ const projectProviders = normalizeProviderNames(input.projectProviders);
8245
+ const selectedProviders = requestedProviders.length > 0 ? requestedProviders : projectProviders.length > 0 ? projectProviders : availableProviders;
8246
+ const available = new Set(availableProviders);
8247
+ const runnableProviders = selectedProviders.filter((provider) => available.has(provider));
8248
+ if (runnableProviders.length > 0) return null;
8249
+ return noProvider(input.projectName, {
8250
+ availableProviders,
8251
+ selectedProviders,
8252
+ selectionSource: requestedProviders.length > 0 ? "request" : projectProviders.length > 0 ? "project" : "instance"
8253
+ });
8254
+ }
8255
+ function normalizeProviderNames(providerNames) {
8256
+ return [...new Set(providerNames.map((name) => name.trim().toLowerCase()).filter(Boolean))];
8257
+ }
8224
8258
  function parseRunTriggerRequest(value) {
8225
8259
  const result = runTriggerRequestSchema.safeParse(value);
8226
8260
  if (result.success) return result.data;
@@ -9779,14 +9813,7 @@ async function analyticsRoutes(app) {
9779
9813
  }
9780
9814
  var ANCHOR_SWEEP_SCAN_LIMIT = 60;
9781
9815
  function isProviderInfraDomain(uri) {
9782
- try {
9783
- const host = new URL(uri).hostname.toLowerCase();
9784
- for (const blocked of AI_PROVIDER_INFRA_DOMAINS) {
9785
- if (host === blocked || host.endsWith(`.${blocked}`)) return true;
9786
- }
9787
- } catch {
9788
- }
9789
- return false;
9816
+ return hostMatchesAnyDomain(uri, AI_PROVIDER_INFRA_DOMAINS);
9790
9817
  }
9791
9818
  function parseGroundingSources(rawResponse) {
9792
9819
  const parsed = parseJsonColumn(rawResponse, {});
@@ -10218,12 +10245,12 @@ import { and as and9, eq as eq13, desc as desc5, inArray as inArray6 } from "dri
10218
10245
  var RECENT_RUNS_WINDOW = 5;
10219
10246
  function loadOrchestratorInput(db, project, locationFilter = void 0) {
10220
10247
  const projectId = project.id;
10221
- const ownDomain = normalizeDomain(project.canonicalDomain);
10248
+ const ownDomain = hostOf(project.canonicalDomain) ?? "";
10222
10249
  const ownedDomains = project.ownedDomains ?? [];
10223
- const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
10250
+ const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map((domain) => hostOf(domain) ?? "")]);
10224
10251
  const trackedQueries = listQueries(db, projectId);
10225
10252
  const candidateQueryStrings = trackedQueries.filter(isBlogShapedQuery);
10226
- const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
10253
+ const trackedCompetitors = listCompetitorDomains(db, projectId).map((domain) => hostOf(domain) ?? "");
10227
10254
  const competitorSet = new Set(trackedCompetitors);
10228
10255
  const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW, locationFilter);
10229
10256
  const latestRunId = recentRunIds[0] ?? "";
@@ -10264,7 +10291,7 @@ function loadOrchestratorInput(db, project, locationFilter = void 0) {
10264
10291
  }
10265
10292
  function loadDomainClasses(db, projectId) {
10266
10293
  const rows = db.select({ domain: domainClassifications.domain, competitorType: domainClassifications.competitorType }).from(domainClassifications).where(eq13(domainClassifications.projectId, projectId)).all();
10267
- return new Map(rows.map((r) => [normalizeDomain(r.domain), r.competitorType]));
10294
+ return new Map(rows.map((r) => [hostOf(r.domain) ?? "", r.competitorType]));
10268
10295
  }
10269
10296
  function buildQueryIntentModifiers(project, locationFilter) {
10270
10297
  if (locationFilter === void 0 || locationFilter === null) return [];
@@ -10483,21 +10510,22 @@ function aggregateCandidate(opts) {
10483
10510
  const isLatestRun = snap.runId === opts.latestRunId;
10484
10511
  const competitorOverlap = snap.competitorOverlap;
10485
10512
  for (const domain of competitorOverlap) {
10486
- const normalized = normalizeDomain(domain);
10487
- if (!opts.competitorSet.has(normalized)) continue;
10488
- competitorTally.set(normalized, (competitorTally.get(normalized) ?? 0) + 1);
10513
+ const normalized = hostOf(domain) ?? "";
10514
+ const competitor = findMatchingDomain(normalized, opts.competitorSet);
10515
+ if (!competitor) continue;
10516
+ competitorTally.set(competitor, (competitorTally.get(competitor) ?? 0) + 1);
10489
10517
  }
10490
10518
  const grounding = extractGroundingSources(snap.rawResponse);
10491
10519
  for (const g of grounding) {
10492
- const domain = normalizeDomain(extractHostFromUri2(g.uri));
10520
+ const domain = hostOf(g.uri) ?? "";
10493
10521
  if (!domain) continue;
10494
- if (opts.ourDomains.has(domain)) {
10522
+ if (findMatchingDomain(domain, opts.ourDomains)) {
10495
10523
  if (isLatestRun) ourCitedInLatestRun = true;
10496
10524
  recordGroundingHit(ourGroundingTally, g, domain, snap.provider);
10497
10525
  continue;
10498
10526
  }
10499
10527
  citedSurfaceTally.set(domain, (citedSurfaceTally.get(domain) ?? 0) + 1);
10500
- if (!opts.competitorSet.has(domain)) continue;
10528
+ if (!findMatchingDomain(domain, opts.competitorSet)) continue;
10501
10529
  recordGroundingHit(competitorGroundingTally, g, domain, snap.provider);
10502
10530
  }
10503
10531
  }
@@ -10519,6 +10547,9 @@ function aggregateCandidate(opts) {
10519
10547
  runsOfHistory: new Set(opts.snapshots.map((s) => s.runId)).size
10520
10548
  };
10521
10549
  }
10550
+ function findMatchingDomain(candidate, domains) {
10551
+ return [...domains].find((domain) => hostMatchesDomain(candidate, domain));
10552
+ }
10522
10553
  function recordGroundingHit(tally, g, domain, provider) {
10523
10554
  const existing = tally.get(g.uri);
10524
10555
  if (existing) {
@@ -10571,16 +10602,6 @@ function extractGroundingSources(rawResponse) {
10571
10602
  }
10572
10603
  return [];
10573
10604
  }
10574
- function extractHostFromUri2(uri) {
10575
- try {
10576
- return new URL(uri).hostname;
10577
- } catch {
10578
- return "";
10579
- }
10580
- }
10581
- function normalizeDomain(domain) {
10582
- return domain.toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
10583
- }
10584
10605
  function extractPath2(url) {
10585
10606
  if (!url) return "";
10586
10607
  const trimmed = url.trim();
@@ -14943,10 +14964,7 @@ function stableUnique(values, normalize) {
14943
14964
  return result;
14944
14965
  }
14945
14966
  function matchesHost(value, marketingHosts) {
14946
- const normalized = normalizeHost(value);
14947
- return marketingHosts.some(
14948
- (candidate) => normalized === candidate || normalized.endsWith(`.${candidate}`)
14949
- );
14967
+ return hostMatchesAnyDomain(value, marketingHosts);
14950
14968
  }
14951
14969
  function normalizeLandingPage(value) {
14952
14970
  return normalizeUrlPath(value) ?? UNKNOWN_LANDING_PAGE;
@@ -15016,10 +15034,10 @@ function buildGaMeasurementAnalysis(db, projectName, options = {}) {
15016
15034
  const conditions = [];
15017
15035
  if (parsedHostScope.data === "marketing") {
15018
15036
  const lowerHost = sql9`lower(${hostColumn})`;
15019
- const normalizedHost2 = sql9`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
15037
+ const normalizedHost = sql9`case when ${lowerHost} like 'www.%' then substr(${lowerHost}, 5) else ${lowerHost} end`;
15020
15038
  conditions.push(or4(...marketingHosts.flatMap((host) => [
15021
- sql9`${normalizedHost2} = ${host}`,
15022
- sql9`${normalizedHost2} like ${`%.${host}`}`
15039
+ sql9`${normalizedHost} = ${host}`,
15040
+ sql9`${normalizedHost} like ${`%.${host}`}`
15023
15041
  ])));
15024
15042
  }
15025
15043
  if (pathPrefix && pathPrefix !== "/") {
@@ -15276,11 +15294,7 @@ var normalizedPath = (value) => {
15276
15294
  return normalized.split("?")[0] || "/";
15277
15295
  };
15278
15296
  var inRange = (date, startDate, endDate) => date >= startDate && date <= endDate;
15279
- var normalizedHost = (value) => value.trim().toLowerCase().replace(/^www\./, "");
15280
- var matchesMarketingHost = (value, marketingHosts) => {
15281
- const hostname = normalizedHost(value);
15282
- return marketingHosts.some((candidate) => hostname === candidate || hostname.endsWith(`.${candidate}`));
15283
- };
15297
+ var matchesMarketingHost = (value, marketingHosts) => hostMatchesAnyDomain(value, marketingHosts);
15284
15298
  var pageMatchesMarketingHost = (value, marketingHosts) => {
15285
15299
  return matchesMarketingHost(hostOf(value) ?? "", marketingHosts);
15286
15300
  };
@@ -15360,7 +15374,9 @@ function buildOrganicEvidence(db, projectName, periodDays) {
15360
15374
  const nativeAcquisition = measurement.acquisition;
15361
15375
  const nativeAcquisitionActive = nativeAcquisition.status !== "never-synced";
15362
15376
  const marketingHosts = measurement.filters.marketingHosts;
15363
- const normalizedMarketingHosts = [...new Set(marketingHosts.map(normalizedHost).filter(Boolean))];
15377
+ const normalizedMarketingHosts = [...new Set(
15378
+ marketingHosts.map((host) => hostOf(host)).filter((host) => host !== null)
15379
+ )];
15364
15380
  const nativeStartDate = nativeAcquisition.periods[0]?.startDate;
15365
15381
  const nativeEndDate = nativeAcquisition.periods.at(-1)?.endDate;
15366
15382
  const gscCoverage = aggregateCoverage(db.select({
@@ -15813,7 +15829,7 @@ async function citationRoutes(app) {
15813
15829
  queryId: s.queryId,
15814
15830
  runCreatedAt: runCreatedAt.get(s.runId) ?? s.createdAt
15815
15831
  }));
15816
- const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq19(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
15832
+ const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq19(competitors.projectId, project.id)).all().map((c) => hostOf(c.domain) ?? "").filter((d) => d.length > 0);
15817
15833
  const response = computeCitationVisibility({
15818
15834
  queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
15819
15835
  snapshots,
@@ -15887,18 +15903,19 @@ function computeCitationVisibility(input) {
15887
15903
  totalProviders: providers.length
15888
15904
  });
15889
15905
  }
15890
- const competitorSet = new Set(competitorDomains);
15891
15906
  const competitorGaps = [];
15892
15907
  const queryById = new Map(qs.map((q) => [q.id, q.query]));
15893
15908
  for (const snap of latestByPair.values()) {
15894
15909
  if (citationStateToCited(snap.citationState)) continue;
15895
- if (competitorSet.size === 0) continue;
15910
+ if (competitorDomains.length === 0) continue;
15896
15911
  const cited = snap.citedDomains;
15897
15912
  const overlap = snap.competitorOverlap;
15898
15913
  const candidates = new Set(
15899
- [...cited, ...overlap].map((d) => normalizeDomain2(d)).filter((d) => d.length > 0)
15914
+ [...cited, ...overlap].map((d) => hostOf(d) ?? "").filter((d) => d.length > 0)
15915
+ );
15916
+ const citingCompetitors = competitorDomains.filter(
15917
+ (competitor) => [...candidates].some((candidate) => hostMatchesDomain(candidate, competitor))
15900
15918
  );
15901
- const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
15902
15919
  if (citingCompetitors.length === 0) continue;
15903
15920
  competitorGaps.push({
15904
15921
  queryId: snap.queryId,
@@ -15940,9 +15957,6 @@ function computeCitationVisibility(input) {
15940
15957
  status: "ready"
15941
15958
  };
15942
15959
  }
15943
- function normalizeDomain2(domain) {
15944
- return domain.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/$/, "");
15945
- }
15946
15960
 
15947
15961
  // ../api-routes/src/visibility-stats.ts
15948
15962
  import { and as and16, desc as desc9, eq as eq20, inArray as inArray9 } from "drizzle-orm";
@@ -15952,9 +15966,6 @@ var VISIBILITY_COMPARE_MIN_RUNS = 5;
15952
15966
  function basketPairKey(queryId, provider) {
15953
15967
  return JSON.stringify([queryId, provider]);
15954
15968
  }
15955
- function citedHostMatches(citedHost, competitorHost) {
15956
- return citedHost === competitorHost || citedHost.endsWith(`.${competitorHost}`);
15957
- }
15958
15969
  function restrict(snapshots, attribution, pairs) {
15959
15970
  const out = [];
15960
15971
  for (const snap of snapshots) {
@@ -16035,7 +16046,7 @@ function countPeriod(snaps, competitors2) {
16035
16046
  if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
16036
16047
  const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
16037
16048
  for (const compHost of competitorHosts) {
16038
- if (citedHosts.some((ch) => citedHostMatches(ch, compHost))) competitorCited += 1;
16049
+ if (citedHosts.some((ch) => hostMatchesDomain(ch, compHost))) competitorCited += 1;
16039
16050
  }
16040
16051
  }
16041
16052
  }
@@ -17501,6 +17512,7 @@ var SCHEMA_TABLE = {
17501
17512
  LatestProjectRunDto: latestProjectRunDtoSchema,
17502
17513
  LocationContext: locationContextSchema,
17503
17514
  NotificationDto: notificationDtoSchema,
17515
+ OnboardingTelemetryEvent: onboardingTelemetryEventSchema,
17504
17516
  OrganicEvidenceDto: organicEvidenceDtoSchema,
17505
17517
  ProjectDto: projectDtoSchema,
17506
17518
  ProjectUpsertRequest: projectUpsertRequestSchema,
@@ -17522,6 +17534,7 @@ var SCHEMA_TABLE = {
17522
17534
  SnapshotListResponse: snapshotListResponseSchema,
17523
17535
  SnapshotReportDto: snapshotReportSchema,
17524
17536
  SourceBreakdownDto: sourceBreakdownDtoSchema,
17537
+ TelemetryEventAcceptedDto: telemetryEventAcceptedDtoSchema,
17525
17538
  TrafficBackfillResponse: trafficBackfillResponseSchema,
17526
17539
  TrafficEventsResponse: trafficEventsResponseSchema,
17527
17540
  TrafficSourceDetailDto: trafficSourceDetailDtoSchema,
@@ -18547,7 +18560,9 @@ var routeCatalog = [
18547
18560
  },
18548
18561
  responses: {
18549
18562
  201: jsonResponse("Run queued.", "RunDto"),
18550
- 409: errorResponse("Run already in progress.")
18563
+ 422: errorResponse("Project has no tracked queries."),
18564
+ 409: errorResponse("Run already in progress."),
18565
+ 503: errorResponse("No runnable answer provider is configured.")
18551
18566
  }
18552
18567
  },
18553
18568
  {
@@ -19172,6 +19187,24 @@ var routeCatalog = [
19172
19187
  501: errorResponse("Telemetry configuration is not available.")
19173
19188
  }
19174
19189
  },
19190
+ {
19191
+ method: "post",
19192
+ path: "/api/v1/telemetry/onboarding",
19193
+ summary: "Record a privacy-safe onboarding milestone",
19194
+ tags: ["telemetry"],
19195
+ requestBody: {
19196
+ required: true,
19197
+ content: {
19198
+ "application/json": {
19199
+ schema: { $ref: "#/components/schemas/OnboardingTelemetryEvent" }
19200
+ }
19201
+ }
19202
+ },
19203
+ responses: {
19204
+ 202: jsonResponse("Onboarding milestone accepted.", "TelemetryEventAcceptedDto"),
19205
+ 400: errorResponse("Invalid onboarding telemetry event.")
19206
+ }
19207
+ },
19175
19208
  {
19176
19209
  method: "get",
19177
19210
  path: "/api/v1/screenshots/{snapshotId}",
@@ -22822,6 +22855,19 @@ async function telemetryRoutes(app, opts) {
22822
22855
  anonymousId: status?.anonymousId ? status.anonymousId.slice(0, 8) + "..." : void 0
22823
22856
  };
22824
22857
  });
22858
+ app.post("/telemetry/onboarding", async (request, reply) => {
22859
+ const parsed = onboardingTelemetryEventSchema.safeParse(request.body);
22860
+ if (!parsed.success) {
22861
+ throw validationError("Invalid onboarding telemetry event", {
22862
+ issues: parsed.error.issues.map((issue) => ({
22863
+ code: issue.code,
22864
+ path: issue.path.join(".")
22865
+ }))
22866
+ });
22867
+ }
22868
+ opts.recordOnboardingEvent?.(parsed.data);
22869
+ return reply.status(202).send({ accepted: Boolean(opts.recordOnboardingEvent) });
22870
+ });
22825
22871
  }
22826
22872
 
22827
22873
  // ../api-routes/src/schedules.ts
@@ -24843,12 +24889,28 @@ function stableStringify3(value) {
24843
24889
  }
24844
24890
 
24845
24891
  // ../api-routes/src/google.ts
24892
+ var GOOGLE_OAUTH_COMPLETE_MESSAGE = "canonry:google-oauth-complete";
24893
+ function googleOAuthSuccessHtml(type) {
24894
+ const message = JSON.stringify({
24895
+ type: GOOGLE_OAUTH_COMPLETE_MESSAGE,
24896
+ connectionType: type
24897
+ });
24898
+ return `<html><body style="font-family:system-ui;text-align:center;padding:60px">
24899
+ <h2>Connected successfully!</h2>
24900
+ <p>Google ${type.toUpperCase()} has been linked to your domain.</p>
24901
+ <p style="color:#888">This window closes automatically.</p>
24902
+ <script>
24903
+ if (window.opener && !window.opener.closed) {
24904
+ window.opener.postMessage(${message}, '*')
24905
+ window.close()
24906
+ }
24907
+ </script>
24908
+ </body></html>`;
24909
+ }
24846
24910
  function isOnProjectDomain(url, projectDomain) {
24847
- try {
24848
- return new URL(url).hostname.toLowerCase().replace(/^www\./, "") === projectDomain;
24849
- } catch {
24850
- return false;
24851
- }
24911
+ const urlHost = hostOf(url);
24912
+ const projectHost = hostOf(projectDomain);
24913
+ return urlHost !== null && projectHost !== null && urlHost === projectHost;
24852
24914
  }
24853
24915
  function scopesForConnectionType(type) {
24854
24916
  switch (type) {
@@ -24870,12 +24932,10 @@ function isSitemapOwnedByProperty(sitemapUrl, propertyId, canonicalDomain) {
24870
24932
  if (sitemap.protocol !== "http:" && sitemap.protocol !== "https:") return false;
24871
24933
  if (/^sc-domain:/i.test(propertyId)) {
24872
24934
  const domain = propertyId.slice("sc-domain:".length).toLowerCase();
24873
- const host = sitemap.hostname.toLowerCase();
24874
- return host === domain || host.endsWith(`.${domain}`);
24935
+ return hostMatchesDomain(sitemap.hostname, domain);
24875
24936
  }
24876
24937
  if (/^\d+$/.test(propertyId)) {
24877
- const domain = normalizeProjectDomain(canonicalDomain);
24878
- return sitemap.hostname.toLowerCase().replace(/^www\./, "") === domain;
24938
+ return hostOf(sitemap.hostname) === hostOf(canonicalDomain);
24879
24939
  }
24880
24940
  try {
24881
24941
  const property = new URL(propertyId);
@@ -25172,13 +25232,7 @@ async function googleRoutes(app, opts) {
25172
25232
  entityId: type,
25173
25233
  diff: { domain, type, propertyId }
25174
25234
  });
25175
- return reply.type("text/html").send(
25176
- `<html><body style="font-family:system-ui;text-align:center;padding:60px">
25177
- <h2>Connected successfully!</h2>
25178
- <p>Google ${type.toUpperCase()} has been linked to your domain.</p>
25179
- <p style="color:#888">You can close this tab.</p>
25180
- </body></html>`
25181
- );
25235
+ return reply.type("text/html").send(googleOAuthSuccessHtml(type));
25182
25236
  }
25183
25237
  app.get("/google/callback", async (request, reply) => {
25184
25238
  return handleOAuthCallback(request, reply);
@@ -38436,9 +38490,7 @@ function normalizeHost2(host) {
38436
38490
  return host.trim().toLowerCase().replace(/^www\./, "");
38437
38491
  }
38438
38492
  function hostMatches(host, domain) {
38439
- const normalizedHost2 = normalizeHost2(host);
38440
- const normalizedDomain = normalizeHost2(domain);
38441
- return normalizedHost2 === normalizedDomain || normalizedHost2.endsWith(`.${normalizedDomain}`);
38493
+ return hostMatchesDomain(host, domain);
38442
38494
  }
38443
38495
  function utmTokenMatchesDomain(utmSource, domain) {
38444
38496
  if (hostMatches(utmSource, domain)) return true;
@@ -38447,12 +38499,7 @@ function utmTokenMatchesDomain(utmSource, domain) {
38447
38499
  return Boolean(firstLabel) && normalizedUtm === firstLabel;
38448
38500
  }
38449
38501
  function hostFromUrl(value) {
38450
- if (!value) return null;
38451
- try {
38452
- return normalizeHost2(new URL(value).hostname);
38453
- } catch {
38454
- return null;
38455
- }
38502
+ return hostOf(value);
38456
38503
  }
38457
38504
  function utmSourceFromQuery(queryString) {
38458
38505
  if (!queryString) return null;
@@ -38622,13 +38669,9 @@ function sessionWindowBucket(value, windowMs) {
38622
38669
  if (Number.isNaN(date.getTime())) return value;
38623
38670
  return new Date(Math.floor(date.getTime() / windowMs) * windowMs).toISOString();
38624
38671
  }
38625
- function normalizeHost3(host) {
38626
- if (!host) return null;
38627
- return host.trim().toLowerCase().replace(/^www\./, "") || null;
38628
- }
38629
38672
  function sameHost(a, b) {
38630
- const normalizedA = normalizeHost3(a);
38631
- const normalizedB = normalizeHost3(b);
38673
+ const normalizedA = hostOf(a);
38674
+ const normalizedB = hostOf(b);
38632
38675
  return !!normalizedA && !!normalizedB && normalizedA === normalizedB;
38633
38676
  }
38634
38677
  function pathFromSameOriginReferer(event) {
@@ -43888,7 +43931,7 @@ function upsertDomainClassifications(db, projectId, sessionId, competitorMap) {
43888
43931
  if (competitorMap.length === 0) return;
43889
43932
  const now = (/* @__PURE__ */ new Date()).toISOString();
43890
43933
  for (const entry of competitorMap) {
43891
- const domain = normalizeDomain(entry.domain);
43934
+ const domain = hostOf(entry.domain) ?? "";
43892
43935
  if (!domain) continue;
43893
43936
  db.insert(domainClassifications).values({
43894
43937
  id: crypto31.randomUUID(),
@@ -44219,7 +44262,8 @@ async function apiRoutes(app, opts) {
44219
44262
  await api.register(competitorRoutes);
44220
44263
  await api.register(runRoutes, {
44221
44264
  onRunCreated: opts.onRunCreated,
44222
- validProviderNames: opts.providerAdapters?.map((a) => a.name)
44265
+ validProviderNames: opts.providerAdapters?.map((a) => a.name),
44266
+ getRunnableProviderNames: opts.getRunnableProviderNames
44223
44267
  });
44224
44268
  await api.register(applyRoutes, {
44225
44269
  onScheduleUpdated: opts.onScheduleUpdated,
@@ -44270,7 +44314,8 @@ async function apiRoutes(app, opts) {
44270
44314
  });
44271
44315
  await api.register(telemetryRoutes, {
44272
44316
  getTelemetryStatus: opts.getTelemetryStatus,
44273
- setTelemetryEnabled: opts.setTelemetryEnabled
44317
+ setTelemetryEnabled: opts.setTelemetryEnabled,
44318
+ recordOnboardingEvent: opts.recordOnboardingEvent
44274
44319
  });
44275
44320
  await api.register(adsRoutes, {
44276
44321
  adsCredentialStore: opts.adsCredentialStore,
@@ -44555,38 +44600,20 @@ function createLogger(module) {
44555
44600
  }
44556
44601
 
44557
44602
  // src/citation-utils.ts
44558
- function domainMatches(domain, canonicalDomain) {
44559
- const normalized = normalizeProjectDomain(canonicalDomain);
44560
- const d = normalizeProjectDomain(domain);
44561
- return d === normalized || d.endsWith(`.${normalized}`);
44562
- }
44563
44603
  function pickProjectCitedDomain(citedDomains, projectDomains) {
44564
44604
  for (const cited of citedDomains) {
44565
- if (projectDomains.some((pd) => domainMatches(cited, pd))) return cited;
44605
+ if (projectDomains.some((pd) => hostMatchesDomain(cited, pd))) return cited;
44566
44606
  }
44567
44607
  return void 0;
44568
44608
  }
44569
44609
  function determineCitationState(normalized, domains) {
44570
44610
  for (const canonicalDomain of domains) {
44571
- const bareDomain = normalizeProjectDomain(canonicalDomain);
44572
- if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
44611
+ if (normalized.citedDomains.some((d) => hostMatchesDomain(d, canonicalDomain))) {
44573
44612
  return "cited";
44574
44613
  }
44575
- const lowerDomain = bareDomain.toLowerCase();
44576
44614
  for (const source of normalized.groundingSources) {
44577
- try {
44578
- const uri = source.uri.toLowerCase();
44579
- if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
44580
- return "cited";
44581
- }
44582
- } catch {
44583
- }
44584
- if (source.title) {
44585
- const titleLower = source.title.toLowerCase().replace(/^www\./, "");
44586
- if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
44587
- return "cited";
44588
- }
44589
- }
44615
+ if (hostMatchesDomain(source.uri, canonicalDomain)) return "cited";
44616
+ if (source.title && hostMatchesDomain(source.title, canonicalDomain)) return "cited";
44590
44617
  }
44591
44618
  }
44592
44619
  return "not-cited";
@@ -44595,27 +44622,25 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
44595
44622
  const overlapSet = /* @__PURE__ */ new Set();
44596
44623
  for (const d of normalized.citedDomains) {
44597
44624
  for (const cd of competitorDomains) {
44598
- if (domainMatches(d, cd)) {
44625
+ if (hostMatchesDomain(d, cd)) {
44599
44626
  overlapSet.add(cd);
44600
44627
  }
44601
44628
  }
44602
44629
  }
44603
44630
  for (const source of normalized.groundingSources) {
44604
- const uri = source.uri.toLowerCase();
44605
44631
  for (const cd of competitorDomains) {
44606
- if (uri.includes(cd.toLowerCase())) {
44632
+ if (hostMatchesDomain(source.uri, cd)) {
44607
44633
  overlapSet.add(cd);
44608
44634
  }
44609
44635
  }
44610
44636
  }
44611
44637
  if (normalized.answerText) {
44612
- const lowerAnswer = normalized.answerText.toLowerCase();
44613
44638
  for (const cd of competitorDomains) {
44614
- if (lowerAnswer.includes(cd.toLowerCase())) {
44639
+ if (textContainsDomain(normalized.answerText, cd)) {
44615
44640
  overlapSet.add(cd);
44616
44641
  }
44617
44642
  const brand = brandLabelFromDomain(cd);
44618
- if (brand.length >= 4 && new RegExp(`\\b${escapeRegExp5(brand)}\\b`, "i").test(lowerAnswer)) {
44643
+ if (brandKeyFromText(brand).length >= 4 && textContainsBrandAlias(normalized.answerText, brand)) {
44619
44644
  overlapSet.add(cd);
44620
44645
  }
44621
44646
  }
@@ -44626,27 +44651,24 @@ function computeCitedCompetitorDomains(citedDomains, competitorDomains) {
44626
44651
  const citedCompetitors = /* @__PURE__ */ new Set();
44627
44652
  for (const citedDomain of citedDomains) {
44628
44653
  for (const competitorDomain of competitorDomains) {
44629
- if (domainMatches(citedDomain, competitorDomain)) citedCompetitors.add(competitorDomain);
44654
+ if (hostMatchesDomain(citedDomain, competitorDomain)) citedCompetitors.add(competitorDomain);
44630
44655
  }
44631
44656
  }
44632
44657
  return [...citedCompetitors];
44633
44658
  }
44634
- function escapeRegExp5(value) {
44635
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
44636
- }
44637
44659
  function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
44638
44660
  if (!answerText || answerText.length < 20) return [];
44639
- const ownBrandKeys = new Set(
44640
- ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
44661
+ const ownBrandAliases = new Set(
44662
+ ownDomains.flatMap((domain) => collectBrandAliasesFromDomain(domain))
44641
44663
  );
44642
44664
  for (const name of ownBrandNames) {
44643
- const key = brandKeyFromText(name);
44644
- if (key.length >= 4) ownBrandKeys.add(key);
44665
+ if (brandKeyFromText(name).length >= 4) ownBrandAliases.add(name);
44645
44666
  }
44646
- const knownCompetitorKeys = new Set(
44647
- [...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
44667
+ const ownKeys = new Set([...ownBrandAliases].map(brandKeyFromText));
44668
+ const knownCompetitorAliases = new Set(
44669
+ [...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandAliasesFromDomain(domain)).filter((alias) => !ownKeys.has(brandKeyFromText(alias)))
44648
44670
  );
44649
- if (knownCompetitorKeys.size === 0) return [];
44671
+ if (knownCompetitorAliases.size === 0) return [];
44650
44672
  const candidatePatterns = [
44651
44673
  /^\s*(?:[-*]|\d+\.)\s+(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?\s*[:\u2014\u2013-]/gm,
44652
44674
  /\*\*([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\*\*/g,
@@ -44696,8 +44718,8 @@ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, com
44696
44718
  if (!candidateKey) continue;
44697
44719
  if (genericKeys.has(candidateKey)) continue;
44698
44720
  if (candidate.split(/\s+/).length > 6) continue;
44699
- if (matchesBrandKey(candidateKey, ownBrandKeys)) continue;
44700
- if (!matchesBrandKey(candidateKey, knownCompetitorKeys)) continue;
44721
+ if (matchesBrandAlias(candidate, ownBrandAliases)) continue;
44722
+ if (!matchesBrandAlias(candidate, knownCompetitorAliases)) continue;
44701
44723
  if (!seen.has(candidateKey)) seen.set(candidateKey, candidate);
44702
44724
  }
44703
44725
  }
@@ -44706,27 +44728,17 @@ function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, com
44706
44728
  function cleanCandidateName(candidate) {
44707
44729
  return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
44708
44730
  }
44709
- function collectBrandKeysFromDomain(domain) {
44731
+ function collectBrandAliasesFromDomain(domain) {
44710
44732
  const reg = registrableDomain(domain);
44711
- if (!reg) {
44712
- const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
44713
- const fallback = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
44714
- return fallback.length >= 4 ? [fallback] : [];
44715
- }
44716
- const keys = /* @__PURE__ */ new Set();
44717
- const fullKey = reg.replace(/[^a-z0-9]/gi, "").toLowerCase();
44718
- if (fullKey.length >= 4) keys.add(fullKey);
44719
- const brand = brandLabelFromDomain(reg).replace(/[^a-z0-9]/gi, "").toLowerCase();
44720
- if (brand.length >= 4) keys.add(brand);
44721
- return [...keys];
44722
- }
44723
- function matchesBrandKey(candidateKey, brandKeys) {
44724
- for (const brandKey of brandKeys) {
44725
- if (candidateKey === brandKey) return true;
44726
- if (candidateKey.startsWith(brandKey) || candidateKey.endsWith(brandKey)) return true;
44727
- if (brandKey.startsWith(candidateKey) || brandKey.endsWith(candidateKey)) return true;
44728
- }
44729
- return false;
44733
+ if (!reg) return [];
44734
+ const aliases = /* @__PURE__ */ new Set();
44735
+ if (brandKeyFromText(reg).length >= 4) aliases.add(reg);
44736
+ const brand = brandLabelFromDomain(reg);
44737
+ if (brandKeyFromText(brand).length >= 4) aliases.add(brand);
44738
+ return [...aliases];
44739
+ }
44740
+ function matchesBrandAlias(candidate, aliases) {
44741
+ return [...aliases].some((alias) => textContainsBrandAlias(candidate, alias));
44730
44742
  }
44731
44743
 
44732
44744
  // src/intelligence-service.ts