@ainyc/canonry 3.6.3 → 4.1.1

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.
@@ -4,7 +4,7 @@ import {
4
4
  configExists,
5
5
  loadConfig,
6
6
  saveConfigPatch
7
- } from "./chunk-GLPZ5NVP.js";
7
+ } from "./chunk-KCETXLDF.js";
8
8
  import {
9
9
  IntelligenceService,
10
10
  MIN_TREND_POINTS,
@@ -40,16 +40,16 @@ import {
40
40
  insights,
41
41
  isBlogShapedQuery,
42
42
  isTrendBaseline,
43
- keywords,
44
43
  mapOpportunitiesToNextSteps,
45
44
  notifications,
46
45
  parseJsonColumn,
47
46
  projects,
47
+ queries,
48
48
  querySnapshots,
49
49
  runs,
50
50
  schedules,
51
51
  usageCounters
52
- } from "./chunk-W463NVVC.js";
52
+ } from "./chunk-NCWCPBOT.js";
53
53
  import {
54
54
  AGENT_MEMORY_VALUE_MAX_BYTES,
55
55
  AGENT_PROVIDER_IDS,
@@ -99,7 +99,10 @@ import {
99
99
  projectConfigSchema,
100
100
  projectUpsertRequestSchema,
101
101
  providerError,
102
+ queryGenerateRequestSchema,
102
103
  registrableDomain,
104
+ resolveConfigSpecQueries,
105
+ resolveSnapshotRequestQueries,
103
106
  runInProgress,
104
107
  runNotCancellable,
105
108
  runTriggerRequestSchema,
@@ -112,7 +115,7 @@ import {
112
115
  visibilityStateFromAnswerMentioned,
113
116
  windowCutoff,
114
117
  wordpressEnvSchema
115
- } from "./chunk-RDX6GBWM.js";
118
+ } from "./chunk-O5JZQUPX.js";
116
119
 
117
120
  // src/telemetry.ts
118
121
  import crypto from "crypto";
@@ -526,7 +529,7 @@ async function projectRoutes(app, opts) {
526
529
  });
527
530
  app.get("/projects/:name/export", async (request, reply) => {
528
531
  const project = resolveProject(app.db, request.params.name);
529
- const kws = app.db.select().from(keywords).where(eq3(keywords.projectId, project.id)).all();
532
+ const qs = app.db.select().from(queries).where(eq3(queries.projectId, project.id)).all();
530
533
  const comps = app.db.select().from(competitors).where(eq3(competitors.projectId, project.id)).all();
531
534
  const schedule = app.db.select().from(schedules).where(eq3(schedules.projectId, project.id)).get();
532
535
  const notificationRows = app.db.select().from(notifications).where(eq3(notifications.projectId, project.id)).all();
@@ -543,7 +546,7 @@ async function projectRoutes(app, opts) {
543
546
  ownedDomains: parseJsonColumn(project.ownedDomains, []),
544
547
  country: project.country,
545
548
  language: project.language,
546
- keywords: kws.map((k) => k.keyword),
549
+ queries: qs.map((q) => q.query),
547
550
  competitors: comps.map((c) => c.domain),
548
551
  providers: parseJsonColumn(project.providers, []),
549
552
  locations: parseJsonColumn(project.locations, []),
@@ -591,14 +594,147 @@ function formatProject(row) {
591
594
  };
592
595
  }
593
596
 
594
- // ../api-routes/src/keywords.ts
597
+ // ../api-routes/src/queries.ts
595
598
  import crypto5 from "crypto";
596
599
  import { eq as eq4 } from "drizzle-orm";
597
- async function keywordRoutes(app, opts) {
600
+ async function queryRoutes(app, opts) {
601
+ app.get("/projects/:name/queries", async (request, reply) => {
602
+ const project = resolveProject(app.db, request.params.name);
603
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
604
+ return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
605
+ });
606
+ app.put("/projects/:name/queries", async (request, reply) => {
607
+ const project = resolveProject(app.db, request.params.name);
608
+ const body = request.body;
609
+ if (!body || !Array.isArray(body.queries)) {
610
+ throw validationError('Body must contain a "queries" array');
611
+ }
612
+ const now = (/* @__PURE__ */ new Date()).toISOString();
613
+ app.db.transaction((tx) => {
614
+ tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
615
+ for (const q of body.queries) {
616
+ tx.insert(queries).values({
617
+ id: crypto5.randomUUID(),
618
+ projectId: project.id,
619
+ query: q,
620
+ createdAt: now
621
+ }).run();
622
+ }
623
+ writeAuditLog(tx, {
624
+ projectId: project.id,
625
+ actor: "api",
626
+ action: "queries.replaced",
627
+ entityType: "query",
628
+ diff: { queries: body.queries }
629
+ });
630
+ });
631
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
632
+ return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
633
+ });
634
+ app.delete("/projects/:name/queries", async (request, reply) => {
635
+ const project = resolveProject(app.db, request.params.name);
636
+ const body = request.body;
637
+ if (!body || !Array.isArray(body.queries) || body.queries.length === 0) {
638
+ throw validationError('Body must contain a non-empty "queries" array');
639
+ }
640
+ const existing = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
641
+ const toDelete = new Set(body.queries);
642
+ const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
643
+ if (idsToDelete.length > 0) {
644
+ app.db.transaction((tx) => {
645
+ for (const id of idsToDelete) {
646
+ tx.delete(queries).where(eq4(queries.id, id)).run();
647
+ }
648
+ writeAuditLog(tx, {
649
+ projectId: project.id,
650
+ actor: "api",
651
+ action: "queries.deleted",
652
+ entityType: "query",
653
+ diff: { deleted: body.queries.filter((q) => existing.some((e) => e.query === q)) }
654
+ });
655
+ });
656
+ }
657
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
658
+ return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
659
+ });
660
+ app.post("/projects/:name/queries", async (request, reply) => {
661
+ const project = resolveProject(app.db, request.params.name);
662
+ const body = request.body;
663
+ if (!body || !Array.isArray(body.queries)) {
664
+ throw validationError('Body must contain a "queries" array');
665
+ }
666
+ const now = (/* @__PURE__ */ new Date()).toISOString();
667
+ const existing = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
668
+ const existingSet = new Set(existing.map((q) => q.query));
669
+ const added = [];
670
+ for (const q of body.queries) {
671
+ if (!existingSet.has(q)) {
672
+ app.db.insert(queries).values({
673
+ id: crypto5.randomUUID(),
674
+ projectId: project.id,
675
+ query: q,
676
+ createdAt: now
677
+ }).run();
678
+ added.push(q);
679
+ existingSet.add(q);
680
+ }
681
+ }
682
+ if (added.length > 0) {
683
+ writeAuditLog(app.db, {
684
+ projectId: project.id,
685
+ actor: "api",
686
+ action: "queries.appended",
687
+ entityType: "query",
688
+ diff: { added }
689
+ });
690
+ }
691
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
692
+ return reply.send(rows.map((r) => ({ id: r.id, query: r.query, createdAt: r.createdAt })));
693
+ });
694
+ app.post("/projects/:name/queries/generate", async (request, reply) => {
695
+ const project = resolveProject(app.db, request.params.name);
696
+ const parsed = queryGenerateRequestSchema.safeParse(request.body);
697
+ if (!parsed.success) {
698
+ throw validationError("Invalid query generation request", {
699
+ issues: parsed.error.issues.map((issue) => ({
700
+ path: issue.path.join("."),
701
+ message: issue.message
702
+ }))
703
+ });
704
+ }
705
+ const body = parsed.data;
706
+ const provider = body.provider.trim().toLowerCase();
707
+ const validNames = opts.validProviderNames ?? [];
708
+ if (validNames.length && !validNames.includes(provider)) {
709
+ throw validationError(`Unknown provider "${body.provider}". Valid providers: ${validNames.join(", ")}`, {
710
+ provider: body.provider,
711
+ validProviders: validNames
712
+ });
713
+ }
714
+ const count = body.count ?? 5;
715
+ if (!opts.onGenerateQueries) {
716
+ throw notImplemented("Query generation is not supported in this deployment");
717
+ }
718
+ const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
719
+ const existingQueries = existingRows.map((r) => r.query);
720
+ try {
721
+ const generated = await opts.onGenerateQueries(provider, count, {
722
+ domain: project.canonicalDomain,
723
+ displayName: project.displayName,
724
+ country: project.country,
725
+ language: project.language,
726
+ existingQueries
727
+ });
728
+ return reply.send({ queries: generated, provider });
729
+ } catch (err) {
730
+ request.log.error({ err }, "Query generation failed");
731
+ throw internalError(err instanceof Error ? err.message : "Failed to generate queries");
732
+ }
733
+ });
598
734
  app.get("/projects/:name/keywords", async (request, reply) => {
599
735
  const project = resolveProject(app.db, request.params.name);
600
- const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
601
- return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
736
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
737
+ return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
602
738
  });
603
739
  app.put("/projects/:name/keywords", async (request, reply) => {
604
740
  const project = resolveProject(app.db, request.params.name);
@@ -608,25 +744,25 @@ async function keywordRoutes(app, opts) {
608
744
  }
609
745
  const now = (/* @__PURE__ */ new Date()).toISOString();
610
746
  app.db.transaction((tx) => {
611
- tx.delete(keywords).where(eq4(keywords.projectId, project.id)).run();
612
- for (const kw of body.keywords) {
613
- tx.insert(keywords).values({
747
+ tx.delete(queries).where(eq4(queries.projectId, project.id)).run();
748
+ for (const keyword of body.keywords) {
749
+ tx.insert(queries).values({
614
750
  id: crypto5.randomUUID(),
615
751
  projectId: project.id,
616
- keyword: kw,
752
+ query: keyword,
617
753
  createdAt: now
618
754
  }).run();
619
755
  }
620
756
  writeAuditLog(tx, {
621
757
  projectId: project.id,
622
758
  actor: "api",
623
- action: "keywords.replaced",
624
- entityType: "keyword",
625
- diff: { keywords: body.keywords }
759
+ action: "queries.replaced",
760
+ entityType: "query",
761
+ diff: { queries: body.keywords }
626
762
  });
627
763
  });
628
- const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
629
- return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
764
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
765
+ return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
630
766
  });
631
767
  app.delete("/projects/:name/keywords", async (request, reply) => {
632
768
  const project = resolveProject(app.db, request.params.name);
@@ -634,25 +770,25 @@ async function keywordRoutes(app, opts) {
634
770
  if (!body || !Array.isArray(body.keywords) || body.keywords.length === 0) {
635
771
  throw validationError('Body must contain a non-empty "keywords" array');
636
772
  }
637
- const existing = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
773
+ const existing = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
638
774
  const toDelete = new Set(body.keywords);
639
- const idsToDelete = existing.filter((k) => toDelete.has(k.keyword)).map((k) => k.id);
775
+ const idsToDelete = existing.filter((q) => toDelete.has(q.query)).map((q) => q.id);
640
776
  if (idsToDelete.length > 0) {
641
777
  app.db.transaction((tx) => {
642
778
  for (const id of idsToDelete) {
643
- tx.delete(keywords).where(eq4(keywords.id, id)).run();
779
+ tx.delete(queries).where(eq4(queries.id, id)).run();
644
780
  }
645
781
  writeAuditLog(tx, {
646
782
  projectId: project.id,
647
783
  actor: "api",
648
- action: "keywords.deleted",
649
- entityType: "keyword",
650
- diff: { deleted: body.keywords.filter((kw) => existing.some((e) => e.keyword === kw)) }
784
+ action: "queries.deleted",
785
+ entityType: "query",
786
+ diff: { deleted: body.keywords.filter((keyword) => existing.some((e) => e.query === keyword)) }
651
787
  });
652
788
  });
653
789
  }
654
- const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
655
- return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
790
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
791
+ return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
656
792
  });
657
793
  app.post("/projects/:name/keywords", async (request, reply) => {
658
794
  const project = resolveProject(app.db, request.params.name);
@@ -661,32 +797,32 @@ async function keywordRoutes(app, opts) {
661
797
  throw validationError('Body must contain a "keywords" array');
662
798
  }
663
799
  const now = (/* @__PURE__ */ new Date()).toISOString();
664
- const existing = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
665
- const existingSet = new Set(existing.map((k) => k.keyword));
800
+ const existing = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
801
+ const existingSet = new Set(existing.map((q) => q.query));
666
802
  const added = [];
667
- for (const kw of body.keywords) {
668
- if (!existingSet.has(kw)) {
669
- app.db.insert(keywords).values({
803
+ for (const keyword of body.keywords) {
804
+ if (!existingSet.has(keyword)) {
805
+ app.db.insert(queries).values({
670
806
  id: crypto5.randomUUID(),
671
807
  projectId: project.id,
672
- keyword: kw,
808
+ query: keyword,
673
809
  createdAt: now
674
810
  }).run();
675
- added.push(kw);
676
- existingSet.add(kw);
811
+ added.push(keyword);
812
+ existingSet.add(keyword);
677
813
  }
678
814
  }
679
815
  if (added.length > 0) {
680
816
  writeAuditLog(app.db, {
681
817
  projectId: project.id,
682
818
  actor: "api",
683
- action: "keywords.appended",
684
- entityType: "keyword",
819
+ action: "queries.appended",
820
+ entityType: "query",
685
821
  diff: { added }
686
822
  });
687
823
  }
688
- const rows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
689
- return reply.send(rows.map((r) => ({ id: r.id, keyword: r.keyword, createdAt: r.createdAt })));
824
+ const rows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
825
+ return reply.send(rows.map((r) => ({ id: r.id, keyword: r.query, createdAt: r.createdAt })));
690
826
  });
691
827
  app.post("/projects/:name/keywords/generate", async (request, reply) => {
692
828
  const project = resolveProject(app.db, request.params.name);
@@ -709,23 +845,23 @@ async function keywordRoutes(app, opts) {
709
845
  });
710
846
  }
711
847
  const count = body.count ?? 5;
712
- if (!opts.onGenerateKeywords) {
713
- throw notImplemented("Key phrase generation is not supported in this deployment");
848
+ if (!opts.onGenerateQueries) {
849
+ throw notImplemented("Keyword generation is not supported in this deployment");
714
850
  }
715
- const existingRows = app.db.select().from(keywords).where(eq4(keywords.projectId, project.id)).all();
716
- const existingKeywords = existingRows.map((r) => r.keyword);
851
+ const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
852
+ const existingQueries = existingRows.map((r) => r.query);
717
853
  try {
718
- const generated = await opts.onGenerateKeywords(provider, count, {
854
+ const generated = await opts.onGenerateQueries(provider, count, {
719
855
  domain: project.canonicalDomain,
720
856
  displayName: project.displayName,
721
857
  country: project.country,
722
858
  language: project.language,
723
- existingKeywords
859
+ existingQueries
724
860
  });
725
861
  return reply.send({ keywords: generated, provider });
726
862
  } catch (err) {
727
- request.log.error({ err }, "Key phrase generation failed");
728
- throw internalError(err instanceof Error ? err.message : "Failed to generate key phrases");
863
+ request.log.error({ err }, "Keyword generation failed");
864
+ throw internalError(err instanceof Error ? err.message : "Failed to generate keywords");
729
865
  }
730
866
  });
731
867
  }
@@ -1138,8 +1274,8 @@ function loadRunDetail(app, run) {
1138
1274
  const snapshots = app.db.select({
1139
1275
  id: querySnapshots.id,
1140
1276
  runId: querySnapshots.runId,
1141
- keywordId: querySnapshots.keywordId,
1142
- keyword: keywords.keyword,
1277
+ queryId: querySnapshots.queryId,
1278
+ query: queries.query,
1143
1279
  provider: querySnapshots.provider,
1144
1280
  model: querySnapshots.model,
1145
1281
  citationState: querySnapshots.citationState,
@@ -1151,7 +1287,7 @@ function loadRunDetail(app, run) {
1151
1287
  location: querySnapshots.location,
1152
1288
  rawResponse: querySnapshots.rawResponse,
1153
1289
  createdAt: querySnapshots.createdAt
1154
- }).from(querySnapshots).leftJoin(keywords, eq7(querySnapshots.keywordId, keywords.id)).where(eq7(querySnapshots.runId, run.id)).all();
1290
+ }).from(querySnapshots).leftJoin(queries, eq7(querySnapshots.queryId, queries.id)).where(eq7(querySnapshots.runId, run.id)).all();
1155
1291
  return {
1156
1292
  ...formatRun(run),
1157
1293
  snapshots: snapshots.map((s) => {
@@ -1160,8 +1296,8 @@ function loadRunDetail(app, run) {
1160
1296
  return {
1161
1297
  id: s.id,
1162
1298
  runId: s.runId,
1163
- keywordId: s.keywordId,
1164
- keyword: s.keyword,
1299
+ queryId: s.queryId,
1300
+ query: s.query,
1165
1301
  provider: s.provider,
1166
1302
  citationState: s.citationState,
1167
1303
  answerMentioned,
@@ -1497,6 +1633,7 @@ async function applyRoutes(app, opts) {
1497
1633
  }
1498
1634
  const now = (/* @__PURE__ */ new Date()).toISOString();
1499
1635
  const name = config.metadata.name;
1636
+ const configQueries = resolveConfigSpecQueries(config.spec);
1500
1637
  let projectId;
1501
1638
  let scheduleAction = null;
1502
1639
  app.db.transaction((tx) => {
@@ -1554,21 +1691,21 @@ async function applyRoutes(app, opts) {
1554
1691
  entityId: projectId
1555
1692
  });
1556
1693
  }
1557
- tx.delete(keywords).where(eq8(keywords.projectId, projectId)).run();
1558
- for (const kw of config.spec.keywords) {
1559
- tx.insert(keywords).values({
1694
+ tx.delete(queries).where(eq8(queries.projectId, projectId)).run();
1695
+ for (const q of configQueries) {
1696
+ tx.insert(queries).values({
1560
1697
  id: crypto10.randomUUID(),
1561
1698
  projectId,
1562
- keyword: kw,
1699
+ query: q,
1563
1700
  createdAt: now
1564
1701
  }).run();
1565
1702
  }
1566
1703
  writeAuditLog(tx, {
1567
1704
  projectId,
1568
1705
  actor: "api",
1569
- action: "keywords.replaced",
1570
- entityType: "keyword",
1571
- diff: { keywords: config.spec.keywords }
1706
+ action: "queries.replaced",
1707
+ entityType: "query",
1708
+ diff: { queries: configQueries }
1572
1709
  });
1573
1710
  tx.delete(competitors).where(eq8(competitors.projectId, projectId)).run();
1574
1711
  const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
@@ -1752,8 +1889,8 @@ async function historyRoutes(app) {
1752
1889
  const allSnapshots = app.db.select({
1753
1890
  id: querySnapshots.id,
1754
1891
  runId: querySnapshots.runId,
1755
- keywordId: querySnapshots.keywordId,
1756
- keyword: keywords.keyword,
1892
+ queryId: querySnapshots.queryId,
1893
+ query: queries.query,
1757
1894
  provider: querySnapshots.provider,
1758
1895
  model: querySnapshots.model,
1759
1896
  citationState: querySnapshots.citationState,
@@ -1764,7 +1901,7 @@ async function historyRoutes(app) {
1764
1901
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
1765
1902
  location: querySnapshots.location,
1766
1903
  createdAt: querySnapshots.createdAt
1767
- }).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
1904
+ }).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(inArray(querySnapshots.runId, projectRuns.map((r) => r.id))).orderBy(desc2(querySnapshots.createdAt)).all();
1768
1905
  const locationFilter = request.query.location;
1769
1906
  const filtered = locationFilter !== void 0 ? allSnapshots.filter((s) => s.location === (locationFilter || null)) : allSnapshots;
1770
1907
  const total = filtered.length;
@@ -1773,8 +1910,8 @@ async function historyRoutes(app) {
1773
1910
  snapshots: paged.map((s) => ({
1774
1911
  id: s.id,
1775
1912
  runId: s.runId,
1776
- keywordId: s.keywordId,
1777
- keyword: s.keyword,
1913
+ queryId: s.queryId,
1914
+ query: s.query,
1778
1915
  provider: s.provider,
1779
1916
  model: s.model,
1780
1917
  citationState: s.citationState,
@@ -1792,9 +1929,9 @@ async function historyRoutes(app) {
1792
1929
  });
1793
1930
  app.get("/projects/:name/timeline", async (request, reply) => {
1794
1931
  const project = resolveProject(app.db, request.params.name);
1795
- const projectKeywords = app.db.select().from(keywords).where(eq9(keywords.projectId, project.id)).all();
1932
+ const projectQueries = app.db.select().from(queries).where(eq9(queries.projectId, project.id)).all();
1796
1933
  const projectRuns = app.db.select().from(runs).where(eq9(runs.projectId, project.id)).orderBy(runs.createdAt).all();
1797
- if (projectRuns.length === 0 || projectKeywords.length === 0) {
1934
+ if (projectRuns.length === 0 || projectQueries.length === 0) {
1798
1935
  return reply.send([]);
1799
1936
  }
1800
1937
  const runIds = new Set(projectRuns.map((r) => r.id));
@@ -1807,26 +1944,26 @@ async function historyRoutes(app) {
1807
1944
  }));
1808
1945
  const deduped = /* @__PURE__ */ new Map();
1809
1946
  for (const snap of allSnapshots) {
1810
- const key = `${snap.runId}:${snap.keywordId}`;
1947
+ const key = `${snap.runId}:${snap.queryId}`;
1811
1948
  const existing = deduped.get(key);
1812
1949
  if (!existing || !existing.answerMentioned && snap.answerMentioned || existing.answerMentioned === snap.answerMentioned && snap.citationState === "cited") {
1813
1950
  deduped.set(key, snap);
1814
1951
  }
1815
1952
  }
1816
1953
  const dedupedSnapshots = [...deduped.values()];
1817
- const rawByKwProvider = /* @__PURE__ */ new Map();
1954
+ const rawByQueryProvider = /* @__PURE__ */ new Map();
1818
1955
  for (const snap of allSnapshots) {
1819
- const key = `${snap.keywordId}::${snap.provider}`;
1820
- const arr = rawByKwProvider.get(key);
1956
+ const key = `${snap.queryId}::${snap.provider}`;
1957
+ const arr = rawByQueryProvider.get(key);
1821
1958
  if (arr) arr.push(snap);
1822
- else rawByKwProvider.set(key, [snap]);
1959
+ else rawByQueryProvider.set(key, [snap]);
1823
1960
  }
1824
- const rawByKwModel = /* @__PURE__ */ new Map();
1961
+ const rawByQueryModel = /* @__PURE__ */ new Map();
1825
1962
  for (const snap of allSnapshots) {
1826
- const key = `${snap.keywordId}::${snap.provider}:${snap.model ?? "unknown"}`;
1827
- const arr = rawByKwModel.get(key);
1963
+ const key = `${snap.queryId}::${snap.provider}:${snap.model ?? "unknown"}`;
1964
+ const arr = rawByQueryModel.get(key);
1828
1965
  if (arr) arr.push(snap);
1829
- else rawByKwModel.set(key, [snap]);
1966
+ else rawByQueryModel.set(key, [snap]);
1830
1967
  }
1831
1968
  function computeTransitions(snaps) {
1832
1969
  return snaps.map((snap, idx) => {
@@ -1860,25 +1997,25 @@ async function historyRoutes(app) {
1860
1997
  };
1861
1998
  });
1862
1999
  }
1863
- const timeline = projectKeywords.map((kw) => {
1864
- const kwSnapshots = dedupedSnapshots.filter((s) => s.keywordId === kw.id).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1865
- const runEntries = computeTransitions(kwSnapshots);
2000
+ const timeline = projectQueries.map((q) => {
2001
+ const qSnapshots = dedupedSnapshots.filter((s) => s.queryId === q.id).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2002
+ const runEntries = computeTransitions(qSnapshots);
1866
2003
  const providerRuns = {};
1867
- const providerKeys = [...rawByKwProvider.keys()].filter((k) => k.startsWith(`${kw.id}::`));
2004
+ const providerKeys = [...rawByQueryProvider.keys()].filter((k) => k.startsWith(`${q.id}::`));
1868
2005
  for (const pk of providerKeys) {
1869
2006
  const provider = pk.split("::")[1];
1870
- const provSnaps = rawByKwProvider.get(pk).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2007
+ const provSnaps = rawByQueryProvider.get(pk).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1871
2008
  providerRuns[provider] = computeTransitions(provSnaps);
1872
2009
  }
1873
2010
  const modelRuns = {};
1874
- const modelKeys = [...rawByKwModel.keys()].filter((k) => k.startsWith(`${kw.id}::`));
2011
+ const modelKeys = [...rawByQueryModel.keys()].filter((k) => k.startsWith(`${q.id}::`));
1875
2012
  for (const mk of modelKeys) {
1876
2013
  const modelKey = mk.split("::")[1];
1877
- const modelSnaps = rawByKwModel.get(mk).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2014
+ const modelSnaps = rawByQueryModel.get(mk).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
1878
2015
  modelRuns[modelKey] = computeTransitions(modelSnaps);
1879
2016
  }
1880
2017
  return {
1881
- keyword: kw.keyword,
2018
+ query: q.query,
1882
2019
  runs: runEntries,
1883
2020
  providerRuns,
1884
2021
  modelRuns
@@ -1893,19 +2030,19 @@ async function historyRoutes(app) {
1893
2030
  throw validationError("Both run1 and run2 query params are required");
1894
2031
  }
1895
2032
  const snaps1 = app.db.select({
1896
- keywordId: querySnapshots.keywordId,
1897
- keyword: keywords.keyword,
2033
+ queryId: querySnapshots.queryId,
2034
+ query: queries.query,
1898
2035
  citationState: querySnapshots.citationState,
1899
2036
  answerMentioned: querySnapshots.answerMentioned,
1900
2037
  answerText: querySnapshots.answerText
1901
- }).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(eq9(querySnapshots.runId, run1)).all();
2038
+ }).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(eq9(querySnapshots.runId, run1)).all();
1902
2039
  const snaps2 = app.db.select({
1903
- keywordId: querySnapshots.keywordId,
1904
- keyword: keywords.keyword,
2040
+ queryId: querySnapshots.queryId,
2041
+ query: queries.query,
1905
2042
  citationState: querySnapshots.citationState,
1906
2043
  answerMentioned: querySnapshots.answerMentioned,
1907
2044
  answerText: querySnapshots.answerText
1908
- }).from(querySnapshots).leftJoin(keywords, eq9(querySnapshots.keywordId, keywords.id)).where(eq9(querySnapshots.runId, run2)).all();
2045
+ }).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(eq9(querySnapshots.runId, run2)).all();
1909
2046
  const map1 = /* @__PURE__ */ new Map();
1910
2047
  for (const s of snaps1) {
1911
2048
  const resolved = {
@@ -1913,9 +2050,9 @@ async function historyRoutes(app) {
1913
2050
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
1914
2051
  resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
1915
2052
  };
1916
- const existing = map1.get(s.keywordId);
2053
+ const existing = map1.get(s.queryId);
1917
2054
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === "cited") {
1918
- map1.set(s.keywordId, resolved);
2055
+ map1.set(s.queryId, resolved);
1919
2056
  }
1920
2057
  }
1921
2058
  const map2 = /* @__PURE__ */ new Map();
@@ -1925,18 +2062,18 @@ async function historyRoutes(app) {
1925
2062
  resolvedAnswerMentioned: resolveSnapshotAnswerMentioned(s, project),
1926
2063
  resolvedVisibilityState: resolveSnapshotVisibilityState(s, project)
1927
2064
  };
1928
- const existing = map2.get(s.keywordId);
2065
+ const existing = map2.get(s.queryId);
1929
2066
  if (!existing || !existing.resolvedAnswerMentioned && resolved.resolvedAnswerMentioned || existing.resolvedAnswerMentioned === resolved.resolvedAnswerMentioned && resolved.citationState === "cited") {
1930
- map2.set(s.keywordId, resolved);
2067
+ map2.set(s.queryId, resolved);
1931
2068
  }
1932
2069
  }
1933
- const allKeywordIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
1934
- const diff = [...allKeywordIds].map((kwId) => {
1935
- const s1 = map1.get(kwId);
1936
- const s2 = map2.get(kwId);
2070
+ const allQueryIds = /* @__PURE__ */ new Set([...map1.keys(), ...map2.keys()]);
2071
+ const diff = [...allQueryIds].map((qId) => {
2072
+ const s1 = map1.get(qId);
2073
+ const s2 = map2.get(qId);
1937
2074
  return {
1938
- keywordId: kwId,
1939
- keyword: s2?.keyword ?? s1?.keyword ?? null,
2075
+ queryId: qId,
2076
+ query: s2?.query ?? s1?.query ?? null,
1940
2077
  run1State: s1?.citationState ?? null,
1941
2078
  run2State: s2?.citationState ?? null,
1942
2079
  run1AnswerMentioned: s1?.resolvedAnswerMentioned ?? null,
@@ -1979,13 +2116,13 @@ async function analyticsRoutes(app) {
1979
2116
  byProvider: {},
1980
2117
  trend: "stable",
1981
2118
  mentionTrend: "stable",
1982
- keywordChanges: []
2119
+ queryChanges: []
1983
2120
  });
1984
2121
  }
1985
2122
  const runIds = projectRuns.map((r) => r.id);
1986
2123
  const rawSnapshots = app.db.select({
1987
2124
  runId: querySnapshots.runId,
1988
- keywordId: querySnapshots.keywordId,
2125
+ queryId: querySnapshots.queryId,
1989
2126
  provider: querySnapshots.provider,
1990
2127
  citationState: querySnapshots.citationState,
1991
2128
  answerMentioned: querySnapshots.answerMentioned,
@@ -1996,8 +2133,8 @@ async function analyticsRoutes(app) {
1996
2133
  ...s,
1997
2134
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
1998
2135
  }));
1999
- const projectKeywords = app.db.select({ id: keywords.id, createdAt: keywords.createdAt }).from(keywords).where(eq10(keywords.projectId, project.id)).all();
2000
- const keywordCreatedAt = new Map(projectKeywords.map((k) => [k.id, k.createdAt]));
2136
+ const projectQueries = app.db.select({ id: queries.id, createdAt: queries.createdAt }).from(queries).where(eq10(queries.projectId, project.id)).all();
2137
+ const queryCreatedAt = new Map(projectQueries.map((q) => [q.id, q.createdAt]));
2001
2138
  const overall = computeProviderMetric(allSnapshots);
2002
2139
  const byProvider = {};
2003
2140
  const providers = new Set(allSnapshots.map((s) => s.provider));
@@ -2008,11 +2145,11 @@ async function analyticsRoutes(app) {
2008
2145
  const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
2009
2146
  const spanDays = Math.max(1, Math.ceil((latest.getTime() - earliest.getTime()) / 864e5));
2010
2147
  const bucketSize = bucketSizeForSpan(spanDays);
2011
- const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, keywordCreatedAt);
2148
+ const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, queryCreatedAt);
2012
2149
  const trend = computeTrend(buckets, "citationRate");
2013
2150
  const mentionTrend = computeTrend(buckets, "mentionRate");
2014
- const keywordChanges = computeKeywordChanges(projectKeywords, cutoff);
2015
- return reply.send({ window, buckets, overall, byProvider, trend, mentionTrend, keywordChanges });
2151
+ const queryChanges = computeQueryChanges(projectQueries, cutoff);
2152
+ return reply.send({ window, buckets, overall, byProvider, trend, mentionTrend, queryChanges });
2016
2153
  });
2017
2154
  app.get("/projects/:name/analytics/gaps", async (request, reply) => {
2018
2155
  const project = resolveProject(app.db, request.params.name);
@@ -2020,24 +2157,24 @@ async function analyticsRoutes(app) {
2020
2157
  const cutoff = windowCutoff(window);
2021
2158
  const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
2022
2159
  if (!latestRun) {
2023
- return reply.send({ cited: [], gap: [], uncited: [], mentionedKeywords: [], mentionGap: [], notMentioned: [], runId: "", window });
2160
+ return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
2024
2161
  }
2025
2162
  const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2026
2163
  const windowRunIds = windowRuns.map((r) => r.id);
2027
2164
  const consistencyMap = /* @__PURE__ */ new Map();
2028
2165
  if (windowRunIds.length > 0) {
2029
2166
  const allWindowSnaps = app.db.select({
2030
- keywordId: querySnapshots.keywordId,
2167
+ queryId: querySnapshots.queryId,
2031
2168
  runId: querySnapshots.runId,
2032
2169
  citationState: querySnapshots.citationState,
2033
2170
  answerMentioned: querySnapshots.answerMentioned,
2034
2171
  answerText: querySnapshots.answerText
2035
2172
  }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2036
2173
  for (const s of allWindowSnaps) {
2037
- let entry = consistencyMap.get(s.keywordId);
2174
+ let entry = consistencyMap.get(s.queryId);
2038
2175
  if (!entry) {
2039
2176
  entry = { citedRuns: /* @__PURE__ */ new Set(), totalRuns: /* @__PURE__ */ new Set(), mentionedRuns: /* @__PURE__ */ new Set() };
2040
- consistencyMap.set(s.keywordId, entry);
2177
+ consistencyMap.set(s.queryId, entry);
2041
2178
  }
2042
2179
  entry.totalRuns.add(s.runId);
2043
2180
  if (s.citationState === "cited") entry.citedRuns.add(s.runId);
@@ -2045,41 +2182,41 @@ async function analyticsRoutes(app) {
2045
2182
  }
2046
2183
  }
2047
2184
  const rawSnapshots = app.db.select({
2048
- keywordId: querySnapshots.keywordId,
2049
- keyword: keywords.keyword,
2185
+ queryId: querySnapshots.queryId,
2186
+ query: queries.query,
2050
2187
  provider: querySnapshots.provider,
2051
2188
  citationState: querySnapshots.citationState,
2052
2189
  answerMentioned: querySnapshots.answerMentioned,
2053
2190
  answerText: querySnapshots.answerText,
2054
2191
  competitorOverlap: querySnapshots.competitorOverlap
2055
- }).from(querySnapshots).leftJoin(keywords, eq10(querySnapshots.keywordId, keywords.id)).where(eq10(querySnapshots.runId, latestRun.id)).all();
2192
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(eq10(querySnapshots.runId, latestRun.id)).all();
2056
2193
  const snapshots = rawSnapshots.map((s) => ({
2057
2194
  ...s,
2058
2195
  resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
2059
2196
  }));
2060
- const byKeyword = /* @__PURE__ */ new Map();
2197
+ const byQuery = /* @__PURE__ */ new Map();
2061
2198
  for (const s of snapshots) {
2062
- const key = s.keywordId;
2063
- const arr = byKeyword.get(key);
2199
+ const key = s.queryId;
2200
+ const arr = byQuery.get(key);
2064
2201
  if (arr) arr.push(s);
2065
- else byKeyword.set(key, [s]);
2202
+ else byQuery.set(key, [s]);
2066
2203
  }
2067
2204
  const cited = [];
2068
2205
  const gap = [];
2069
2206
  const uncited = [];
2070
- const mentionedKeywords = [];
2207
+ const mentionedQueries = [];
2071
2208
  const mentionGap = [];
2072
2209
  const notMentioned = [];
2073
- for (const [keywordId, kwSnapshots] of byKeyword) {
2074
- const keyword = kwSnapshots[0]?.keyword ?? "";
2075
- const citedProviders = kwSnapshots.filter((s) => s.citationState === "cited").map((s) => s.provider);
2076
- const mentionedProviders = kwSnapshots.filter((s) => s.resolvedMentioned).map((s) => s.provider);
2210
+ for (const [queryId, qSnapshots] of byQuery) {
2211
+ const query = qSnapshots[0]?.query ?? "";
2212
+ const citedProviders = qSnapshots.filter((s) => s.citationState === "cited").map((s) => s.provider);
2213
+ const mentionedProviders = qSnapshots.filter((s) => s.resolvedMentioned).map((s) => s.provider);
2077
2214
  const competitorsCiting = /* @__PURE__ */ new Set();
2078
- for (const s of kwSnapshots) {
2215
+ for (const s of qSnapshots) {
2079
2216
  const overlap = parseJsonColumn(s.competitorOverlap, []);
2080
2217
  for (const c of overlap) competitorsCiting.add(c);
2081
2218
  }
2082
- const cons = consistencyMap.get(keywordId);
2219
+ const cons = consistencyMap.get(queryId);
2083
2220
  const consistency = {
2084
2221
  citedRuns: cons?.citedRuns.size ?? 0,
2085
2222
  totalRuns: cons?.totalRuns.size ?? 0,
@@ -2094,8 +2231,8 @@ async function analyticsRoutes(app) {
2094
2231
  category = "uncited";
2095
2232
  }
2096
2233
  const citationEntry = {
2097
- keyword,
2098
- keywordId,
2234
+ query,
2235
+ queryId,
2099
2236
  category,
2100
2237
  providers: citedProviders,
2101
2238
  competitorsCiting: [...competitorsCiting],
@@ -2113,24 +2250,24 @@ async function analyticsRoutes(app) {
2113
2250
  mentionCategory = "uncited";
2114
2251
  }
2115
2252
  const mentionEntry = {
2116
- keyword,
2117
- keywordId,
2253
+ query,
2254
+ queryId,
2118
2255
  category: mentionCategory,
2119
2256
  providers: mentionedProviders,
2120
2257
  competitorsCiting: [...competitorsCiting],
2121
2258
  consistency
2122
2259
  };
2123
- if (mentionCategory === "cited") mentionedKeywords.push(mentionEntry);
2260
+ if (mentionCategory === "cited") mentionedQueries.push(mentionEntry);
2124
2261
  else if (mentionCategory === "gap") mentionGap.push(mentionEntry);
2125
2262
  else notMentioned.push(mentionEntry);
2126
2263
  }
2127
2264
  gap.sort((a, b) => b.competitorsCiting.length - a.competitorsCiting.length);
2128
- cited.sort((a, b) => a.keyword.localeCompare(b.keyword));
2129
- uncited.sort((a, b) => a.keyword.localeCompare(b.keyword));
2265
+ cited.sort((a, b) => a.query.localeCompare(b.query));
2266
+ uncited.sort((a, b) => a.query.localeCompare(b.query));
2130
2267
  mentionGap.sort((a, b) => b.competitorsCiting.length - a.competitorsCiting.length);
2131
- mentionedKeywords.sort((a, b) => a.keyword.localeCompare(b.keyword));
2132
- notMentioned.sort((a, b) => a.keyword.localeCompare(b.keyword));
2133
- return reply.send({ cited, gap, uncited, mentionedKeywords, mentionGap, notMentioned, runId: latestRun.id, window });
2268
+ mentionedQueries.sort((a, b) => a.query.localeCompare(b.query));
2269
+ notMentioned.sort((a, b) => a.query.localeCompare(b.query));
2270
+ return reply.send({ cited, gap, uncited, mentionedQueries, mentionGap, notMentioned, runId: latestRun.id, window });
2134
2271
  });
2135
2272
  app.get("/projects/:name/analytics/sources", async (request, reply) => {
2136
2273
  const project = resolveProject(app.db, request.params.name);
@@ -2138,35 +2275,35 @@ async function analyticsRoutes(app) {
2138
2275
  const cutoff = windowCutoff(window);
2139
2276
  const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2140
2277
  if (windowRuns.length === 0) {
2141
- return reply.send({ overall: [], byKeyword: {}, runId: "", window });
2278
+ return reply.send({ overall: [], byQuery: {}, runId: "", window });
2142
2279
  }
2143
2280
  const latestRunId = windowRuns[0].id;
2144
2281
  const windowRunIds = windowRuns.map((r) => r.id);
2145
2282
  const snapshots = app.db.select({
2146
- keywordId: querySnapshots.keywordId,
2147
- keyword: keywords.keyword,
2283
+ queryId: querySnapshots.queryId,
2284
+ query: queries.query,
2148
2285
  rawResponse: querySnapshots.rawResponse
2149
- }).from(querySnapshots).leftJoin(keywords, eq10(querySnapshots.keywordId, keywords.id)).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2286
+ }).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2150
2287
  const overallCounts = /* @__PURE__ */ new Map();
2151
- const byKeyword = {};
2288
+ const byQuery = {};
2152
2289
  for (const snap of snapshots) {
2153
2290
  const sources = parseGroundingSources(snap.rawResponse);
2154
- const kwCounts = /* @__PURE__ */ new Map();
2291
+ const qCounts = /* @__PURE__ */ new Map();
2155
2292
  for (const source of sources) {
2156
2293
  const { category, domain } = categorizeSource(source.uri);
2157
2294
  if (!overallCounts.has(category)) overallCounts.set(category, /* @__PURE__ */ new Map());
2158
2295
  const oDomains = overallCounts.get(category);
2159
2296
  oDomains.set(domain, (oDomains.get(domain) ?? 0) + 1);
2160
- if (!kwCounts.has(category)) kwCounts.set(category, /* @__PURE__ */ new Map());
2161
- const kDomains = kwCounts.get(category);
2162
- kDomains.set(domain, (kDomains.get(domain) ?? 0) + 1);
2297
+ if (!qCounts.has(category)) qCounts.set(category, /* @__PURE__ */ new Map());
2298
+ const qDomains = qCounts.get(category);
2299
+ qDomains.set(domain, (qDomains.get(domain) ?? 0) + 1);
2163
2300
  }
2164
- if (sources.length > 0 && snap.keyword) {
2165
- byKeyword[snap.keyword] = buildCategoryCounts(kwCounts);
2301
+ if (sources.length > 0 && snap.query) {
2302
+ byQuery[snap.query] = buildCategoryCounts(qCounts);
2166
2303
  }
2167
2304
  }
2168
2305
  const overall = buildCategoryCounts(overallCounts);
2169
- return reply.send({ overall, byKeyword, runId: latestRunId, window });
2306
+ return reply.send({ overall, byQuery, runId: latestRunId, window });
2170
2307
  });
2171
2308
  }
2172
2309
  var PROVIDER_INFRA_DOMAINS = /* @__PURE__ */ new Set([
@@ -2211,7 +2348,7 @@ function computeProviderMetric(snapshots) {
2211
2348
  mentionedCount
2212
2349
  };
2213
2350
  }
2214
- function computeBuckets(snapshots, projectRuns, bucketDays, keywordCreatedAt) {
2351
+ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
2215
2352
  if (projectRuns.length === 0) return [];
2216
2353
  const earliest = new Date(projectRuns[0].createdAt);
2217
2354
  const latest = new Date(projectRuns[projectRuns.length - 1].createdAt);
@@ -2226,22 +2363,22 @@ function computeBuckets(snapshots, projectRuns, bucketDays, keywordCreatedAt) {
2226
2363
  const inBucket = snapshots.filter((s) => s.createdAt >= startISO && s.createdAt < endISO);
2227
2364
  if (inBucket.length > 0) {
2228
2365
  let usable = inBucket;
2229
- if (keywordCreatedAt) {
2366
+ if (queryCreatedAt) {
2230
2367
  const eligible = inBucket.filter((s) => {
2231
- const kwCreated = keywordCreatedAt.get(s.keywordId);
2232
- return kwCreated !== void 0 && kwCreated < startISO;
2368
+ const qCreated = queryCreatedAt.get(s.queryId);
2369
+ return qCreated !== void 0 && qCreated < startISO;
2233
2370
  });
2234
2371
  if (eligible.length > 0) usable = eligible;
2235
2372
  }
2236
2373
  const metric = computeProviderMetric(usable);
2237
- const keywordCount = new Set(usable.map((s) => s.keywordId)).size;
2374
+ const queryCount = new Set(usable.map((s) => s.queryId)).size;
2238
2375
  buckets.push({
2239
2376
  startDate: startISO,
2240
2377
  endDate: endISO,
2241
2378
  citationRate: metric.citationRate,
2242
2379
  cited: metric.cited,
2243
2380
  total: metric.total,
2244
- keywordCount,
2381
+ queryCount,
2245
2382
  mentionRate: metric.mentionRate,
2246
2383
  mentionedCount: metric.mentionedCount
2247
2384
  });
@@ -2250,11 +2387,11 @@ function computeBuckets(snapshots, projectRuns, bucketDays, keywordCreatedAt) {
2250
2387
  }
2251
2388
  return buckets;
2252
2389
  }
2253
- function computeKeywordChanges(projectKeywords, cutoff) {
2390
+ function computeQueryChanges(projectQueries, cutoff) {
2254
2391
  const byDay = /* @__PURE__ */ new Map();
2255
- for (const kw of projectKeywords) {
2256
- if (cutoff && kw.createdAt < cutoff) continue;
2257
- const day = kw.createdAt.slice(0, 10);
2392
+ for (const q of projectQueries) {
2393
+ if (cutoff && q.createdAt < cutoff) continue;
2394
+ const day = q.createdAt.slice(0, 10);
2258
2395
  byDay.set(day, (byDay.get(day) ?? 0) + 1);
2259
2396
  }
2260
2397
  const days = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
@@ -2328,7 +2465,7 @@ function mapInsightRow(r) {
2328
2465
  type: r.type,
2329
2466
  severity: r.severity,
2330
2467
  title: r.title,
2331
- keyword: r.keyword,
2468
+ query: r.query,
2332
2469
  provider: r.provider,
2333
2470
  recommendation: parseJsonColumn(r.recommendation, void 0),
2334
2471
  cause: parseJsonColumn(r.cause, void 0),
@@ -2752,8 +2889,8 @@ function renderExecutiveSummary(report) {
2752
2889
  delta: `<span class="tone-${trendTone}">${trendLabel}</span> \xB7 ${s.providerCount} provider${s.providerCount === 1 ? "" : "s"}`
2753
2890
  },
2754
2891
  {
2755
- label: "Keywords tracked",
2756
- value: formatNumber(s.keywordCount),
2892
+ label: "Queries tracked",
2893
+ value: formatNumber(s.queryCount),
2757
2894
  delta: `${s.competitorCount} competitor${s.competitorCount === 1 ? "" : "s"} tracked`
2758
2895
  }
2759
2896
  ];
@@ -2820,13 +2957,13 @@ function renderProviderBars(rates) {
2820
2957
  </div>`;
2821
2958
  }
2822
2959
  function renderCitationMatrix(scorecard) {
2823
- if (scorecard.keywords.length === 0 || scorecard.providers.length === 0) {
2960
+ if (scorecard.queries.length === 0 || scorecard.providers.length === 0) {
2824
2961
  return renderEmpty("Run a visibility sweep to populate the citation matrix.");
2825
2962
  }
2826
2963
  const headers = scorecard.providers.map((p) => `<th>${escapeHtml(p)}</th>`).join("");
2827
- const rows = scorecard.keywords.map((kw, ki) => {
2964
+ const rows = scorecard.queries.map((q, qi) => {
2828
2965
  const cells = scorecard.providers.map((_, pi) => {
2829
- const cell = scorecard.matrix[ki]?.[pi];
2966
+ const cell = scorecard.matrix[qi]?.[pi];
2830
2967
  if (!cell) {
2831
2968
  return '<td><span class="cell-pending">\u2014</span></td>';
2832
2969
  }
@@ -2835,10 +2972,10 @@ function renderCitationMatrix(scorecard) {
2835
2972
  }
2836
2973
  return '<td><span class="cell-not-cited">Not cited</span></td>';
2837
2974
  }).join("");
2838
- return `<tr><td>${escapeHtml(kw)}</td>${cells}</tr>`;
2975
+ return `<tr><td>${escapeHtml(q)}</td>${cells}</tr>`;
2839
2976
  }).join("");
2840
2977
  return `<table class="report-table">
2841
- <thead><tr><th>Keyword</th>${headers}</tr></thead>
2978
+ <thead><tr><th>Query</th>${headers}</tr></thead>
2842
2979
  <tbody>${rows}</tbody>
2843
2980
  </table>`;
2844
2981
  }
@@ -2848,7 +2985,7 @@ function renderCitationScorecard(report) {
2848
2985
  ${renderCitationMatrix(report.citationScorecard)}
2849
2986
  `;
2850
2987
  return section(
2851
- { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Whether your domain appeared in each AI engine\u2019s source list for every tracked keyword in the latest sweep \u2014 a cell turns green when your domain was cited, red when it was not, and gray when no snapshot exists for that pair." },
2988
+ { id: "citation-scorecard", eyebrow: "Section 2", title: "Citation Scorecard", intro: "Whether your domain appeared in each AI engine\u2019s source list for every tracked query in the latest sweep \u2014 a cell turns green when your domain was cited, red when it was not, and gray when no snapshot exists for that pair." },
2852
2989
  body
2853
2990
  );
2854
2991
  }
@@ -2915,11 +3052,11 @@ function renderCompetitorLandscape(report) {
2915
3052
  <td class="numeric">${c.citationCount} / ${c.totalCount}</td>
2916
3053
  <td class="numeric">${mentionCount} / ${mentionTotal}</td>
2917
3054
  <td class="numeric">${c.sharePct}%</td>
2918
- <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
3055
+ <td>${escapeHtml(c.citedQueries.slice(0, 5).join(", "))}${c.citedQueries.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
2919
3056
  </tr>`;
2920
3057
  }).join("");
2921
3058
  const table = competitors2.length > 0 ? `<table class="report-table">
2922
- <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">Mentions</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
3059
+ <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">Mentions</th><th class="numeric">SOV</th><th>Cited queries</th></tr></thead>
2923
3060
  <tbody>${rows}</tbody>
2924
3061
  </table>` : renderEmpty("No competitors configured.");
2925
3062
  const citationBars = renderCompetitorBars(report.competitorLandscape, report.meta.project.canonicalDomain);
@@ -3066,14 +3203,14 @@ function renderGsc(report) {
3066
3203
  );
3067
3204
  const crossoverBlocks = [];
3068
3205
  if (gsc.trackedButNoGsc.length > 0) {
3069
- crossoverBlocks.push(`<div class="chart-card"><h3>AEO keywords without search demand</h3>
3070
- <p class="section-intro">Tracked AEO keywords with no GSC impressions in this window \u2014 review whether they represent real search demand.</p>
3071
- <ul>${gsc.trackedButNoGsc.map((k) => `<li>${escapeHtml(k)}</li>`).join("")}</ul>
3206
+ crossoverBlocks.push(`<div class="chart-card"><h3>AEO queries without search demand</h3>
3207
+ <p class="section-intro">Tracked AEO queries with no GSC impressions in this window \u2014 review whether they represent real search demand.</p>
3208
+ <ul>${gsc.trackedButNoGsc.map((q) => `<li>${escapeHtml(q)}</li>`).join("")}</ul>
3072
3209
  </div>`);
3073
3210
  }
3074
3211
  if (gsc.gscButNotTracked.length > 0) {
3075
3212
  crossoverBlocks.push(`<div class="chart-card"><h3>Search queries you should track</h3>
3076
- <p class="section-intro">GSC top queries (by impressions) that aren't tracked in your AEO project \u2014 candidates to add as keywords.</p>
3213
+ <p class="section-intro">GSC top queries (by impressions) that aren't tracked in your AEO project \u2014 candidates to add as queries.</p>
3077
3214
  <ul>${gsc.gscButNotTracked.map((q) => `<li>${escapeHtml(q)}</li>`).join("")}</ul>
3078
3215
  </div>`);
3079
3216
  }
@@ -3322,7 +3459,7 @@ function renderInsights(report) {
3322
3459
  return `<tr>
3323
3460
  <td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
3324
3461
  <td>${escapeHtml(i.title)}${countChip}</td>
3325
- <td>${escapeHtml(i.keyword)}</td>
3462
+ <td>${escapeHtml(i.query)}</td>
3326
3463
  <td>${escapeHtml(i.provider)}</td>
3327
3464
  <td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
3328
3465
  </tr>`;
@@ -3330,7 +3467,7 @@ function renderInsights(report) {
3330
3467
  return section(
3331
3468
  { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Regressions (citations lost), gains (citations won), and opportunities surfaced by the intelligence engine across the most recent sweeps \u2014 ordered by severity and recurrence." },
3332
3469
  `<table class="report-table">
3333
- <thead><tr><th>Severity</th><th>Title</th><th>Keyword</th><th>Provider</th><th>Recommendation</th></tr></thead>
3470
+ <thead><tr><th>Severity</th><th>Title</th><th>Query</th><th>Provider</th><th>Recommendation</th></tr></thead>
3334
3471
  <tbody>${rows}</tbody>
3335
3472
  </table>`
3336
3473
  );
@@ -3436,8 +3573,8 @@ function loadOrchestratorInput(db, project) {
3436
3573
  const ownDomain = normalizeDomain(project.canonicalDomain);
3437
3574
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
3438
3575
  const ourDomains = /* @__PURE__ */ new Set([ownDomain, ...ownedDomains.map(normalizeDomain)]);
3439
- const trackedKeywords = listKeywords(db, projectId);
3440
- const candidateQueryStrings = trackedKeywords.filter(isBlogShapedQuery);
3576
+ const trackedQueries = listQueries(db, projectId);
3577
+ const candidateQueryStrings = trackedQueries.filter(isBlogShapedQuery);
3441
3578
  const trackedCompetitors = listCompetitorDomains(db, projectId).map(normalizeDomain);
3442
3579
  const competitorSet = new Set(trackedCompetitors);
3443
3580
  const recentRunIds = listRecentAnswerVisibilityRunIds(db, projectId, RECENT_RUNS_WINDOW);
@@ -3474,8 +3611,8 @@ function loadOrchestratorInput(db, project) {
3474
3611
  inProgressActions: /* @__PURE__ */ new Map()
3475
3612
  };
3476
3613
  }
3477
- function listKeywords(db, projectId) {
3478
- const rows = db.select({ text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, projectId)).all();
3614
+ function listQueries(db, projectId) {
3615
+ const rows = db.select({ text: queries.query }).from(queries).where(eq12(queries.projectId, projectId)).all();
3479
3616
  return rows.map((r) => r.text);
3480
3617
  }
3481
3618
  function listCompetitorDomains(db, projectId) {
@@ -3528,21 +3665,21 @@ function buildCandidateQueries(opts) {
3528
3665
  if (opts.candidateQueryStrings.length === 0 || opts.recentRunIds.length === 0) {
3529
3666
  return opts.candidateQueryStrings.map((query) => emptyCandidate(query));
3530
3667
  }
3531
- const keywordRows = opts.db.select({ id: keywords.id, text: keywords.keyword }).from(keywords).where(eq12(keywords.projectId, opts.projectId)).all();
3532
- const keywordIdByText = new Map(keywordRows.map((r) => [r.text, r.id]));
3533
- const candidateKeywordIds = opts.candidateQueryStrings.map((q) => keywordIdByText.get(q)).filter((id) => Boolean(id));
3534
- const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateKeywordIds.includes(r.keywordId));
3535
- const snapshotsByKeyword = /* @__PURE__ */ new Map();
3668
+ const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
3669
+ const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
3670
+ const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
3671
+ const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
3672
+ const snapshotsByQuery = /* @__PURE__ */ new Map();
3536
3673
  for (const row of snapshotRows) {
3537
- const list = snapshotsByKeyword.get(row.keywordId) ?? [];
3674
+ const list = snapshotsByQuery.get(row.queryId) ?? [];
3538
3675
  list.push(row);
3539
- snapshotsByKeyword.set(row.keywordId, list);
3676
+ snapshotsByQuery.set(row.queryId, list);
3540
3677
  }
3541
3678
  const gscRows = opts.db.select().from(gscSearchData).where(eq12(gscSearchData.projectId, opts.projectId)).all();
3542
3679
  const gscByQuery = aggregateGscByQuery(gscRows);
3543
3680
  return opts.candidateQueryStrings.map((query) => {
3544
- const keywordId = keywordIdByText.get(query);
3545
- const snaps = keywordId ? snapshotsByKeyword.get(keywordId) ?? [] : [];
3681
+ const queryId = queryIdByText.get(query);
3682
+ const snaps = queryId ? snapshotsByQuery.get(queryId) ?? [] : [];
3546
3683
  const gsc = gscByQuery.get(query) ?? null;
3547
3684
  return aggregateCandidate({
3548
3685
  query,
@@ -3736,7 +3873,7 @@ function loadSnapshotsForRun(db, runId) {
3736
3873
  return rows.map((r) => ({
3737
3874
  id: r.id,
3738
3875
  runId: r.runId,
3739
- keywordId: r.keywordId,
3876
+ queryId: r.queryId,
3740
3877
  provider: r.provider,
3741
3878
  model: r.model,
3742
3879
  citationState: r.citationState,
@@ -3748,37 +3885,37 @@ function loadSnapshotsForRun(db, runId) {
3748
3885
  createdAt: r.createdAt
3749
3886
  }));
3750
3887
  }
3751
- function loadKeywordLookup(db, projectId) {
3752
- const rows = db.select().from(keywords).where(eq13(keywords.projectId, projectId)).all();
3888
+ function loadQueryLookup(db, projectId) {
3889
+ const rows = db.select().from(queries).where(eq13(queries.projectId, projectId)).all();
3753
3890
  const byId = /* @__PURE__ */ new Map();
3754
- for (const row of rows) byId.set(row.id, row.keyword);
3891
+ for (const row of rows) byId.set(row.id, row.query);
3755
3892
  return { byId };
3756
3893
  }
3757
- function buildCitationScorecard(snapshots, keywordLookup) {
3894
+ function buildCitationScorecard(snapshots, queryLookup) {
3758
3895
  if (snapshots.length === 0) {
3759
- return { keywords: [], providers: [], matrix: [], providerRates: [] };
3896
+ return { queries: [], providers: [], matrix: [], providerRates: [] };
3760
3897
  }
3761
- const keywordSet = /* @__PURE__ */ new Set();
3898
+ const querySet = /* @__PURE__ */ new Set();
3762
3899
  const providerSet = /* @__PURE__ */ new Set();
3763
3900
  for (const snap of snapshots) {
3764
- const kw = keywordLookup.byId.get(snap.keywordId);
3765
- if (!kw) continue;
3766
- keywordSet.add(kw);
3901
+ const q = queryLookup.byId.get(snap.queryId);
3902
+ if (!q) continue;
3903
+ querySet.add(q);
3767
3904
  providerSet.add(snap.provider);
3768
3905
  }
3769
- const keywordList = [...keywordSet].sort();
3906
+ const queryList = [...querySet].sort();
3770
3907
  const providerList = [...providerSet].sort();
3771
- const matrix = keywordList.map(
3908
+ const matrix = queryList.map(
3772
3909
  () => providerList.map(() => null)
3773
3910
  );
3774
3911
  const providerCounts = /* @__PURE__ */ new Map();
3775
3912
  for (const snap of snapshots) {
3776
- const kw = keywordLookup.byId.get(snap.keywordId);
3777
- if (!kw) continue;
3778
- const ki = keywordList.indexOf(kw);
3913
+ const q = queryLookup.byId.get(snap.queryId);
3914
+ if (!q) continue;
3915
+ const qi = queryList.indexOf(q);
3779
3916
  const pi = providerList.indexOf(snap.provider);
3780
- if (ki < 0 || pi < 0) continue;
3781
- matrix[ki][pi] = {
3917
+ if (qi < 0 || pi < 0) continue;
3918
+ matrix[qi][pi] = {
3782
3919
  citationState: snap.citationState === "cited" ? "cited" : "not-cited",
3783
3920
  answerMentioned: snap.answerMentioned ?? null,
3784
3921
  model: snap.model
@@ -3798,16 +3935,16 @@ function buildCitationScorecard(snapshots, keywordLookup) {
3798
3935
  citationRate
3799
3936
  };
3800
3937
  });
3801
- return { keywords: keywordList, providers: providerList, matrix, providerRates };
3938
+ return { queries: queryList, providers: providerList, matrix, providerRates };
3802
3939
  }
3803
- function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains, keywordLookup) {
3940
+ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains, queryLookup) {
3804
3941
  let projectCitationCount = 0;
3805
3942
  const competitorMap = /* @__PURE__ */ new Map();
3806
3943
  for (const c of competitorDomains) {
3807
- competitorMap.set(c, { count: 0, keywords: /* @__PURE__ */ new Set(), pages: /* @__PURE__ */ new Map() });
3944
+ competitorMap.set(c, { count: 0, queries: /* @__PURE__ */ new Set(), pages: /* @__PURE__ */ new Map() });
3808
3945
  }
3809
3946
  for (const snap of snapshots) {
3810
- const kw = keywordLookup.byId.get(snap.keywordId);
3947
+ const q = queryLookup.byId.get(snap.queryId);
3811
3948
  const allDomains = [...snap.citedDomains, ...snap.competitorOverlap];
3812
3949
  if (allDomains.some((d) => citedDomainBelongsToProject(d, projectDomains))) {
3813
3950
  projectCitationCount++;
@@ -3816,7 +3953,7 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
3816
3953
  if (allDomains.some((d) => citedDomainBelongsToProject(d, [competitor]))) {
3817
3954
  const entry = competitorMap.get(competitor);
3818
3955
  entry.count++;
3819
- if (kw) entry.keywords.add(kw);
3956
+ if (q) entry.queries.add(q);
3820
3957
  }
3821
3958
  const competitorNorm = normalizeDomain(competitor);
3822
3959
  for (const gs of snap.groundingSources) {
@@ -3824,9 +3961,9 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
3824
3961
  if (!host) continue;
3825
3962
  if (host === competitorNorm || host.endsWith(`.${competitorNorm}`)) {
3826
3963
  const entry = competitorMap.get(competitor);
3827
- const pageKeywords = entry.pages.get(gs.uri) ?? /* @__PURE__ */ new Set();
3828
- if (kw) pageKeywords.add(kw);
3829
- entry.pages.set(gs.uri, pageKeywords);
3964
+ const pageQueries = entry.pages.get(gs.uri) ?? /* @__PURE__ */ new Set();
3965
+ if (q) pageQueries.add(q);
3966
+ entry.pages.set(gs.uri, pageQueries);
3830
3967
  }
3831
3968
  }
3832
3969
  }
@@ -3842,13 +3979,13 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
3842
3979
  else pressureLabel = "Low";
3843
3980
  }
3844
3981
  const sharePct = totalCitedSlots > 0 ? Math.round(data.count / totalCitedSlots * 100) : 0;
3845
- const theirCitedPages = [...data.pages.entries()].map(([url, kws]) => ({ url, citedFor: [...kws].sort() })).sort((a, b) => b.citedFor.length - a.citedFor.length);
3982
+ const theirCitedPages = [...data.pages.entries()].map(([url, qs]) => ({ url, citedFor: [...qs].sort() })).sort((a, b) => b.citedFor.length - a.citedFor.length);
3846
3983
  return {
3847
3984
  domain,
3848
3985
  citationCount: data.count,
3849
3986
  totalCount: total,
3850
3987
  pressureLabel,
3851
- citedKeywords: [...data.keywords].sort(),
3988
+ citedQueries: [...data.queries].sort(),
3852
3989
  sharePct,
3853
3990
  theirCitedPages
3854
3991
  };
@@ -3856,18 +3993,18 @@ function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
3856
3993
  competitorRows.sort((a, b) => b.citationCount - a.citationCount);
3857
3994
  return { projectCitationCount, competitors: competitorRows };
3858
3995
  }
3859
- function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName, projectDomains, keywordLookup) {
3996
+ function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName, projectDomains, queryLookup) {
3860
3997
  let projectMentionCount = 0;
3861
3998
  let totalAnswerSnapshots = 0;
3862
3999
  const competitorMap = /* @__PURE__ */ new Map();
3863
4000
  for (const c of competitorDomains) {
3864
- competitorMap.set(c, { count: 0, keywords: /* @__PURE__ */ new Set() });
4001
+ competitorMap.set(c, { count: 0, queries: /* @__PURE__ */ new Set() });
3865
4002
  }
3866
4003
  for (const snap of snapshots) {
3867
4004
  const text = snap.answerText;
3868
4005
  if (!text) continue;
3869
4006
  totalAnswerSnapshots++;
3870
- const kw = keywordLookup.byId.get(snap.keywordId);
4007
+ const q = queryLookup.byId.get(snap.queryId);
3871
4008
  const projectMentioned = snap.answerMentioned ?? determineAnswerMentioned(
3872
4009
  text,
3873
4010
  projectDisplayName,
@@ -3880,7 +4017,7 @@ function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName,
3880
4017
  if (mentioned) {
3881
4018
  const entry = competitorMap.get(competitor);
3882
4019
  entry.count++;
3883
- if (kw) entry.keywords.add(kw);
4020
+ if (q) entry.queries.add(q);
3884
4021
  }
3885
4022
  }
3886
4023
  }
@@ -3899,7 +4036,7 @@ function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName,
3899
4036
  mentionCount: data.count,
3900
4037
  totalCount: totalAnswerSnapshots,
3901
4038
  pressureLabel,
3902
- mentionedKeywords: [...data.keywords].sort(),
4039
+ mentionedQueries: [...data.queries].sort(),
3903
4040
  sharePct
3904
4041
  };
3905
4042
  });
@@ -3934,7 +4071,7 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains) {
3934
4071
  })).sort((a, b) => b.count - a.count).slice(0, TOP_SOURCE_DOMAINS_LIMIT);
3935
4072
  return { categories, topDomains };
3936
4073
  }
3937
- function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedKeywords) {
4074
+ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedQueries) {
3938
4075
  const rows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
3939
4076
  if (rows.length === 0) return null;
3940
4077
  let totalClicks = 0;
@@ -3981,9 +4118,9 @@ function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, tra
3981
4118
  sharePct: totalClicks > 0 ? Math.round(agg.clicks / totalClicks * 100) : 0
3982
4119
  })).sort((a, b) => b.clicks - a.clicks);
3983
4120
  const trend = [...trendAgg.entries()].map(([date, agg]) => ({ date, clicks: agg.clicks, impressions: agg.impressions })).sort((a, b) => a.date.localeCompare(b.date));
3984
- const trackedSet = new Set(trackedKeywords.map((k) => k.toLowerCase()));
4121
+ const trackedSet = new Set(trackedQueries.map((q) => q.toLowerCase()));
3985
4122
  const gscQuerySet = new Set([...queryAgg.keys()].map((q) => q.toLowerCase()));
3986
- const trackedButNoGsc = trackedKeywords.filter((k) => !gscQuerySet.has(k.toLowerCase())).sort();
4123
+ const trackedButNoGsc = trackedQueries.filter((q) => !gscQuerySet.has(q.toLowerCase())).sort();
3987
4124
  const gscButNotTracked = [...queryAgg.entries()].filter(([q]) => !trackedSet.has(q.toLowerCase())).filter(([q]) => categorizeQuery(q, projectDisplayName, canonicalDomain) !== "brand").sort((a, b) => b[1].impressions - a[1].impressions).map(([q]) => q).slice(0, TOP_QUERIES_LIMIT);
3988
4125
  return {
3989
4126
  totalClicks,
@@ -4169,7 +4306,7 @@ function buildIndexingHealth(db, projectId) {
4169
4306
  }
4170
4307
  return null;
4171
4308
  }
4172
- function buildCitationsTrend(db, projectId, keywordLookup) {
4309
+ function buildCitationsTrend(db, projectId, queryLookup) {
4173
4310
  const visibilityRuns = db.select().from(runs).where(and4(eq13(runs.projectId, projectId), eq13(runs.kind, RunKinds["answer-visibility"]))).all();
4174
4311
  const points = [];
4175
4312
  for (const run of visibilityRuns) {
@@ -4180,7 +4317,7 @@ function buildCitationsTrend(db, projectId, keywordLookup) {
4180
4317
  let considered = 0;
4181
4318
  const providerCounts = /* @__PURE__ */ new Map();
4182
4319
  for (const snap of snaps) {
4183
- if (!keywordLookup.byId.has(snap.keywordId)) continue;
4320
+ if (!queryLookup.byId.has(snap.queryId)) continue;
4184
4321
  considered++;
4185
4322
  if (snap.citationState === "cited") cited++;
4186
4323
  const counts = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
@@ -4230,7 +4367,7 @@ function buildInsightList(db, projectId) {
4230
4367
  type: r.type,
4231
4368
  severity: r.severity,
4232
4369
  title: r.title,
4233
- keyword: r.keyword,
4370
+ query: r.query,
4234
4371
  provider: r.provider,
4235
4372
  recommendation: recText,
4236
4373
  createdAt: r.createdAt,
@@ -4246,7 +4383,7 @@ function buildInsightList(db, projectId) {
4246
4383
  type: rep.type,
4247
4384
  severity: rep.severity,
4248
4385
  title: rep.title,
4249
- keyword: rep.keyword,
4386
+ query: rep.query,
4250
4387
  provider: rep.provider,
4251
4388
  recommendation: rep.recommendation,
4252
4389
  createdAt: rep.createdAt,
@@ -4332,7 +4469,7 @@ function buildExecutiveFindings(citationRate, trend, trendsPoints, trendBaseline
4332
4469
  }
4333
4470
  function buildProjectReport(db, projectName) {
4334
4471
  const project = resolveProject(db, projectName);
4335
- const keywordLookup = loadKeywordLookup(db, project.id);
4472
+ const queryLookup = loadQueryLookup(db, project.id);
4336
4473
  const allRuns = db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc6(runs.createdAt)).all();
4337
4474
  const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
4338
4475
  const latestRun = visibilityRuns.find(
@@ -4343,34 +4480,34 @@ function buildProjectReport(db, projectName) {
4343
4480
  const competitorDomains = competitorRows.map((c) => c.domain);
4344
4481
  const ownedDomains = parseJsonColumn(project.ownedDomains, []);
4345
4482
  const projectDomains = [project.canonicalDomain, ...ownedDomains];
4346
- const citationScorecard = buildCitationScorecard(latestSnapshots, keywordLookup);
4483
+ const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
4347
4484
  const competitorLandscape = buildCompetitorLandscape(
4348
4485
  latestSnapshots,
4349
4486
  competitorDomains,
4350
4487
  projectDomains,
4351
- keywordLookup
4488
+ queryLookup
4352
4489
  );
4353
4490
  const mentionLandscape = buildMentionLandscape(
4354
4491
  latestSnapshots,
4355
4492
  competitorDomains,
4356
4493
  project.displayName,
4357
4494
  projectDomains,
4358
- keywordLookup
4495
+ queryLookup
4359
4496
  );
4360
4497
  const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
4361
- const trackedKeywords = [...keywordLookup.byId.values()];
4498
+ const trackedQueries = [...queryLookup.byId.values()];
4362
4499
  const gscSection = buildGscSection(
4363
4500
  db,
4364
4501
  project.id,
4365
4502
  project.displayName,
4366
4503
  project.canonicalDomain,
4367
- trackedKeywords
4504
+ trackedQueries
4368
4505
  );
4369
4506
  const gaSection = buildGaSection(db, project.id);
4370
4507
  const socialSection = buildSocialReferrals(db, project.id);
4371
4508
  const aiReferralsSection = buildAiReferrals(db, project.id);
4372
4509
  const indexingHealthSection = buildIndexingHealth(db, project.id);
4373
- const citationsTrend = buildCitationsTrend(db, project.id, keywordLookup);
4510
+ const citationsTrend = buildCitationsTrend(db, project.id, queryLookup);
4374
4511
  const insightList = buildInsightList(db, project.id);
4375
4512
  const orchestratorInput = loadOrchestratorInput(db, project);
4376
4513
  const contentOpportunities = buildContentTargetRows(orchestratorInput);
@@ -4384,7 +4521,7 @@ function buildProjectReport(db, projectName) {
4384
4521
  let latestCited = 0;
4385
4522
  let latestConsidered = 0;
4386
4523
  for (const snap of latestSnapshots) {
4387
- if (!keywordLookup.byId.has(snap.keywordId)) continue;
4524
+ if (!queryLookup.byId.has(snap.queryId)) continue;
4388
4525
  latestConsidered++;
4389
4526
  if (snap.citationState === "cited") latestCited++;
4390
4527
  }
@@ -4430,7 +4567,7 @@ function buildProjectReport(db, projectName) {
4430
4567
  executiveSummary: {
4431
4568
  citationRate,
4432
4569
  trend,
4433
- keywordCount: keywordLookup.byId.size,
4570
+ queryCount: queryLookup.byId.size,
4434
4571
  competitorCount: competitorDomains.length,
4435
4572
  providerCount: citationScorecard.providers.length,
4436
4573
  gsc: gscSection ? {
@@ -4489,9 +4626,9 @@ async function citationRoutes(app) {
4489
4626
  app.get("/projects/:name/citations/visibility", async (request, reply) => {
4490
4627
  const project = resolveProject(app.db, request.params.name);
4491
4628
  const configuredProviders = parseJsonColumn(project.providers, []);
4492
- const projectKeywords = app.db.select().from(keywords).where(eq14(keywords.projectId, project.id)).all();
4493
- if (projectKeywords.length === 0) {
4494
- return reply.send(emptyCitationVisibility("no-keywords"));
4629
+ const projectQueries = app.db.select().from(queries).where(eq14(queries.projectId, project.id)).all();
4630
+ if (projectQueries.length === 0) {
4631
+ return reply.send(emptyCitationVisibility("no-queries"));
4495
4632
  }
4496
4633
  const projectRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(eq14(runs.projectId, project.id)).all();
4497
4634
  if (projectRuns.length === 0) {
@@ -4501,7 +4638,7 @@ async function citationRoutes(app) {
4501
4638
  const rawSnapshots = app.db.select({
4502
4639
  id: querySnapshots.id,
4503
4640
  runId: querySnapshots.runId,
4504
- keywordId: querySnapshots.keywordId,
4641
+ queryId: querySnapshots.queryId,
4505
4642
  provider: querySnapshots.provider,
4506
4643
  citationState: querySnapshots.citationState,
4507
4644
  citedDomains: querySnapshots.citedDomains,
@@ -4518,7 +4655,7 @@ async function citationRoutes(app) {
4518
4655
  }));
4519
4656
  const projectCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq14(competitors.projectId, project.id)).all().map((c) => normalizeDomain2(c.domain)).filter((d) => d.length > 0);
4520
4657
  const response = computeCitationVisibility({
4521
- keywords: projectKeywords.map((k) => ({ id: k.id, keyword: k.keyword })),
4658
+ queries: projectQueries.map((q) => ({ id: q.id, query: q.query })),
4522
4659
  snapshots,
4523
4660
  configuredProviders,
4524
4661
  competitorDomains: projectCompetitors
@@ -4527,10 +4664,10 @@ async function citationRoutes(app) {
4527
4664
  });
4528
4665
  }
4529
4666
  function computeCitationVisibility(input) {
4530
- const { keywords: kws, snapshots, configuredProviders, competitorDomains } = input;
4667
+ const { queries: qs, snapshots, configuredProviders, competitorDomains } = input;
4531
4668
  const latestByPair = /* @__PURE__ */ new Map();
4532
4669
  for (const snap of snapshots) {
4533
- const key = `${snap.keywordId}::${snap.provider}`;
4670
+ const key = `${snap.queryId}::${snap.provider}`;
4534
4671
  const existing = latestByPair.get(key);
4535
4672
  if (!existing || snap.createdAt > existing.createdAt) {
4536
4673
  latestByPair.set(key, snap);
@@ -4541,17 +4678,17 @@ function computeCitationVisibility(input) {
4541
4678
  const providerUniverse = configuredProviders.length > 0 ? Array.from(new Set(configuredProviders)) : Array.from(observedProviders).sort();
4542
4679
  const providersCitingTracker = /* @__PURE__ */ new Set();
4543
4680
  const providersMentioningTracker = /* @__PURE__ */ new Set();
4544
- let keywordsCitedAndMentioned = 0;
4545
- let keywordsCitedOnly = 0;
4546
- let keywordsMentionedOnly = 0;
4547
- let keywordsInvisible = 0;
4548
- const byKeyword = [];
4549
- for (const kw of kws) {
4681
+ let queriesCitedAndMentioned = 0;
4682
+ let queriesCitedOnly = 0;
4683
+ let queriesMentionedOnly = 0;
4684
+ let queriesInvisible = 0;
4685
+ const byQuery = [];
4686
+ for (const q of qs) {
4550
4687
  const providers = [];
4551
4688
  let citedCount = 0;
4552
4689
  let mentionedCount = 0;
4553
4690
  for (const provider of providerUniverse) {
4554
- const snap = latestByPair.get(`${kw.id}::${provider}`);
4691
+ const snap = latestByPair.get(`${q.id}::${provider}`);
4555
4692
  if (!snap) continue;
4556
4693
  const state = snap.citationState;
4557
4694
  const cited = citationStateToCited(state);
@@ -4576,14 +4713,14 @@ function computeCitationVisibility(input) {
4576
4713
  if (providers.length > 0) {
4577
4714
  const anyCited = citedCount > 0;
4578
4715
  const anyMentioned = mentionedCount > 0;
4579
- if (anyCited && anyMentioned) keywordsCitedAndMentioned++;
4580
- else if (anyCited) keywordsCitedOnly++;
4581
- else if (anyMentioned) keywordsMentionedOnly++;
4582
- else keywordsInvisible++;
4583
- }
4584
- byKeyword.push({
4585
- keywordId: kw.id,
4586
- keyword: kw.keyword,
4716
+ if (anyCited && anyMentioned) queriesCitedAndMentioned++;
4717
+ else if (anyCited) queriesCitedOnly++;
4718
+ else if (anyMentioned) queriesMentionedOnly++;
4719
+ else queriesInvisible++;
4720
+ }
4721
+ byQuery.push({
4722
+ queryId: q.id,
4723
+ query: q.query,
4587
4724
  providers,
4588
4725
  citedCount,
4589
4726
  mentionedCount,
@@ -4592,7 +4729,7 @@ function computeCitationVisibility(input) {
4592
4729
  }
4593
4730
  const competitorSet = new Set(competitorDomains);
4594
4731
  const competitorGaps = [];
4595
- const keywordById = new Map(kws.map((k) => [k.id, k.keyword]));
4732
+ const queryById = new Map(qs.map((q) => [q.id, q.query]));
4596
4733
  for (const snap of latestByPair.values()) {
4597
4734
  if (citationStateToCited(snap.citationState)) continue;
4598
4735
  if (competitorSet.size === 0) continue;
@@ -4604,8 +4741,8 @@ function computeCitationVisibility(input) {
4604
4741
  const citingCompetitors = Array.from(candidates).filter((d) => competitorSet.has(d));
4605
4742
  if (citingCompetitors.length === 0) continue;
4606
4743
  competitorGaps.push({
4607
- keywordId: snap.keywordId,
4608
- keyword: keywordById.get(snap.keywordId) ?? "",
4744
+ queryId: snap.queryId,
4745
+ query: queryById.get(snap.queryId) ?? "",
4609
4746
  provider: snap.provider,
4610
4747
  citingCompetitors: citingCompetitors.sort(),
4611
4748
  runId: snap.runId,
@@ -4613,7 +4750,7 @@ function computeCitationVisibility(input) {
4613
4750
  });
4614
4751
  }
4615
4752
  competitorGaps.sort((a, b) => {
4616
- if (a.keyword !== b.keyword) return a.keyword.localeCompare(b.keyword);
4753
+ if (a.query !== b.query) return a.query.localeCompare(b.query);
4617
4754
  return a.provider.localeCompare(b.provider);
4618
4755
  });
4619
4756
  let latestRunId = null;
@@ -4628,17 +4765,17 @@ function computeCitationVisibility(input) {
4628
4765
  providersConfigured: providerUniverse.length,
4629
4766
  providersCiting: providersCitingTracker.size,
4630
4767
  providersMentioning: providersMentioningTracker.size,
4631
- totalKeywords: kws.length,
4632
- keywordsCitedAndMentioned,
4633
- keywordsCitedOnly,
4634
- keywordsMentionedOnly,
4635
- keywordsInvisible,
4768
+ totalQueries: qs.length,
4769
+ queriesCitedAndMentioned,
4770
+ queriesCitedOnly,
4771
+ queriesMentionedOnly,
4772
+ queriesInvisible,
4636
4773
  latestRunId,
4637
4774
  latestRunAt
4638
4775
  };
4639
4776
  return {
4640
4777
  summary,
4641
- byKeyword,
4778
+ byQuery,
4642
4779
  competitorGaps,
4643
4780
  status: "ready"
4644
4781
  };
@@ -4664,14 +4801,14 @@ async function compositeRoutes(app) {
4664
4801
  const health = healthRow ? mapHealthRow2(healthRow) : null;
4665
4802
  const insightRows = app.db.select().from(insights).where(eq15(insights.projectId, project.id)).orderBy(desc7(insights.createdAt)).all();
4666
4803
  const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
4667
- const { keywordCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
4804
+ const { queryCounts, providers } = summarizeLatestRun(app, latestRunRow ?? null);
4668
4805
  const transitions = summarizeTransitions(app, latestRunRow ?? null, previousRunRow ?? null);
4669
4806
  const result = {
4670
4807
  project: formatProject2(project),
4671
4808
  latestRun,
4672
4809
  health,
4673
4810
  topInsights,
4674
- keywordCounts,
4811
+ queryCounts,
4675
4812
  providers,
4676
4813
  transitions
4677
4814
  };
@@ -4689,8 +4826,8 @@ async function compositeRoutes(app) {
4689
4826
  const snapshotMatches = app.db.select({
4690
4827
  id: querySnapshots.id,
4691
4828
  runId: querySnapshots.runId,
4692
- keywordId: querySnapshots.keywordId,
4693
- keywordText: keywords.keyword,
4829
+ queryId: querySnapshots.queryId,
4830
+ queryText: queries.query,
4694
4831
  provider: querySnapshots.provider,
4695
4832
  model: querySnapshots.model,
4696
4833
  citationState: querySnapshots.citationState,
@@ -4698,14 +4835,14 @@ async function compositeRoutes(app) {
4698
4835
  citedDomains: querySnapshots.citedDomains,
4699
4836
  rawResponse: querySnapshots.rawResponse,
4700
4837
  createdAt: querySnapshots.createdAt
4701
- }).from(querySnapshots).innerJoin(keywords, eq15(querySnapshots.keywordId, keywords.id)).where(
4838
+ }).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
4702
4839
  and5(
4703
- eq15(keywords.projectId, project.id),
4840
+ eq15(queries.projectId, project.id),
4704
4841
  or3(
4705
4842
  sql3`${querySnapshots.answerText} LIKE ${pattern} ESCAPE '\\'`,
4706
4843
  sql3`${querySnapshots.citedDomains} LIKE ${pattern} ESCAPE '\\'`,
4707
4844
  sql3`${querySnapshots.rawResponse} LIKE ${pattern} ESCAPE '\\'`,
4708
- like(keywords.keyword, pattern)
4845
+ like(queries.query, pattern)
4709
4846
  )
4710
4847
  )
4711
4848
  ).orderBy(desc7(querySnapshots.createdAt)).limit(limit + 1).all();
@@ -4714,7 +4851,7 @@ async function compositeRoutes(app) {
4714
4851
  eq15(insights.projectId, project.id),
4715
4852
  or3(
4716
4853
  like(insights.title, pattern),
4717
- like(insights.keyword, pattern),
4854
+ like(insights.query, pattern),
4718
4855
  sql3`${insights.recommendation} LIKE ${pattern} ESCAPE '\\'`,
4719
4856
  sql3`${insights.cause} LIKE ${pattern} ESCAPE '\\'`
4720
4857
  )
@@ -4766,35 +4903,35 @@ function summarizeRun(run) {
4766
4903
  }
4767
4904
  function summarizeLatestRun(app, run) {
4768
4905
  const empty = {
4769
- keywordCounts: { totalKeywords: 0, citedKeywords: 0, notCitedKeywords: 0, citedRate: 0 },
4906
+ queryCounts: { totalQueries: 0, citedQueries: 0, notCitedQueries: 0, citedRate: 0 },
4770
4907
  providers: []
4771
4908
  };
4772
4909
  if (!run) return empty;
4773
4910
  const rows = app.db.select({
4774
- keywordId: querySnapshots.keywordId,
4911
+ queryId: querySnapshots.queryId,
4775
4912
  provider: querySnapshots.provider,
4776
4913
  citationState: querySnapshots.citationState
4777
4914
  }).from(querySnapshots).where(eq15(querySnapshots.runId, run.id)).all();
4778
4915
  if (rows.length === 0) return empty;
4779
- const perKeyword = /* @__PURE__ */ new Map();
4916
+ const perQuery = /* @__PURE__ */ new Map();
4780
4917
  const perProvider = /* @__PURE__ */ new Map();
4781
4918
  for (const row of rows) {
4782
4919
  const cited = row.citationState === "cited";
4783
- if (!perKeyword.has(row.keywordId) || cited) {
4784
- perKeyword.set(row.keywordId, cited);
4920
+ if (!perQuery.has(row.queryId) || cited) {
4921
+ perQuery.set(row.queryId, cited);
4785
4922
  }
4786
4923
  const bucket = perProvider.get(row.provider) ?? { cited: 0, total: 0 };
4787
4924
  bucket.total += 1;
4788
4925
  if (cited) bucket.cited += 1;
4789
4926
  perProvider.set(row.provider, bucket);
4790
4927
  }
4791
- const totalKeywords = perKeyword.size;
4792
- let citedKeywords = 0;
4793
- for (const wasCited of perKeyword.values()) {
4794
- if (wasCited) citedKeywords += 1;
4928
+ const totalQueries = perQuery.size;
4929
+ let citedQueries = 0;
4930
+ for (const wasCited of perQuery.values()) {
4931
+ if (wasCited) citedQueries += 1;
4795
4932
  }
4796
- const notCitedKeywords = totalKeywords - citedKeywords;
4797
- const citedRate = totalKeywords === 0 ? 0 : Number((citedKeywords / totalKeywords).toFixed(4));
4933
+ const notCitedQueries = totalQueries - citedQueries;
4934
+ const citedRate = totalQueries === 0 ? 0 : Number((citedQueries / totalQueries).toFixed(4));
4798
4935
  const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
4799
4936
  provider,
4800
4937
  cited,
@@ -4802,7 +4939,7 @@ function summarizeLatestRun(app, run) {
4802
4939
  citedRate: total === 0 ? 0 : Number((cited / total).toFixed(4))
4803
4940
  })).sort((a, b) => a.provider.localeCompare(b.provider));
4804
4941
  return {
4805
- keywordCounts: { totalKeywords, citedKeywords, notCitedKeywords, citedRate },
4942
+ queryCounts: { totalQueries, citedQueries, notCitedQueries, citedRate },
4806
4943
  providers
4807
4944
  };
4808
4945
  }
@@ -4811,13 +4948,13 @@ function summarizeTransitions(app, latest, previous) {
4811
4948
  if (!latest || !previous) return empty;
4812
4949
  const fetchCited = (runId) => {
4813
4950
  const rows = app.db.select({
4814
- keywordId: querySnapshots.keywordId,
4951
+ queryId: querySnapshots.queryId,
4815
4952
  citationState: querySnapshots.citationState
4816
4953
  }).from(querySnapshots).where(eq15(querySnapshots.runId, runId)).all();
4817
4954
  const map = /* @__PURE__ */ new Map();
4818
4955
  for (const row of rows) {
4819
4956
  const cited = row.citationState === "cited";
4820
- if (!map.has(row.keywordId) || cited) map.set(row.keywordId, cited);
4957
+ if (!map.has(row.queryId) || cited) map.set(row.queryId, cited);
4821
4958
  }
4822
4959
  return map;
4823
4960
  };
@@ -4826,8 +4963,8 @@ function summarizeTransitions(app, latest, previous) {
4826
4963
  let gained = 0;
4827
4964
  let lost = 0;
4828
4965
  let emerging = 0;
4829
- for (const [keywordId, latestCited] of latestMap) {
4830
- const previousCited = previousMap.get(keywordId);
4966
+ for (const [queryId, latestCited] of latestMap) {
4967
+ const previousCited = previousMap.get(queryId);
4831
4968
  if (previousCited === void 0) {
4832
4969
  if (latestCited) emerging += 1;
4833
4970
  continue;
@@ -4845,7 +4982,7 @@ function mapInsightRow2(r) {
4845
4982
  type: r.type,
4846
4983
  severity: r.severity,
4847
4984
  title: r.title,
4848
- keyword: r.keyword,
4985
+ query: r.query,
4849
4986
  provider: r.provider,
4850
4987
  recommendation: parseJsonColumn(r.recommendation, void 0),
4851
4988
  cause: parseJsonColumn(r.cause, void 0),
@@ -4886,9 +5023,9 @@ function formatProject2(row) {
4886
5023
  updatedAt: row.updatedAt
4887
5024
  };
4888
5025
  }
4889
- function buildSnapshotHit(row, query) {
4890
- const lower = query.toLowerCase();
4891
- const keyword = row.keywordText ?? "";
5026
+ function buildSnapshotHit(row, searchTerm) {
5027
+ const lower = searchTerm.toLowerCase();
5028
+ const query = row.queryText ?? "";
4892
5029
  const answer = row.answerText ?? "";
4893
5030
  const cited = row.citedDomains;
4894
5031
  const raw = row.rawResponse ?? "";
@@ -4896,22 +5033,22 @@ function buildSnapshotHit(row, query) {
4896
5033
  let snippet;
4897
5034
  if (answer.toLowerCase().includes(lower)) {
4898
5035
  matchedField = "answerText";
4899
- snippet = makeSnippet(answer, query);
5036
+ snippet = makeSnippet(answer, searchTerm);
4900
5037
  } else if (cited.toLowerCase().includes(lower)) {
4901
5038
  matchedField = "citedDomains";
4902
- snippet = makeSnippet(cited, query);
5039
+ snippet = makeSnippet(cited, searchTerm);
4903
5040
  } else if (raw.toLowerCase().includes(lower)) {
4904
5041
  matchedField = "searchQueries";
4905
- snippet = makeSnippet(raw, query);
5042
+ snippet = makeSnippet(raw, searchTerm);
4906
5043
  } else {
4907
- matchedField = "keyword";
4908
- snippet = keyword;
5044
+ matchedField = "query";
5045
+ snippet = query;
4909
5046
  }
4910
5047
  return {
4911
5048
  kind: "snapshot",
4912
5049
  id: row.id,
4913
5050
  runId: row.runId,
4914
- keyword,
5051
+ query,
4915
5052
  provider: row.provider,
4916
5053
  model: row.model,
4917
5054
  citationState: row.citationState,
@@ -4920,24 +5057,24 @@ function buildSnapshotHit(row, query) {
4920
5057
  createdAt: row.createdAt
4921
5058
  };
4922
5059
  }
4923
- function buildInsightHit(row, query) {
4924
- const lower = query.toLowerCase();
5060
+ function buildInsightHit(row, searchTerm) {
5061
+ const lower = searchTerm.toLowerCase();
4925
5062
  const recommendation = row.recommendation ?? "";
4926
5063
  const cause = row.cause ?? "";
4927
5064
  let matchedField;
4928
5065
  let snippet;
4929
5066
  if (row.title.toLowerCase().includes(lower)) {
4930
5067
  matchedField = "title";
4931
- snippet = makeSnippet(row.title, query);
4932
- } else if (row.keyword.toLowerCase().includes(lower)) {
4933
- matchedField = "keyword";
4934
- snippet = row.keyword;
5068
+ snippet = makeSnippet(row.title, searchTerm);
5069
+ } else if (row.query.toLowerCase().includes(lower)) {
5070
+ matchedField = "query";
5071
+ snippet = row.query;
4935
5072
  } else if (recommendation.toLowerCase().includes(lower)) {
4936
5073
  matchedField = "recommendation";
4937
- snippet = makeSnippet(recommendation, query);
5074
+ snippet = makeSnippet(recommendation, searchTerm);
4938
5075
  } else {
4939
5076
  matchedField = "cause";
4940
- snippet = makeSnippet(cause, query);
5077
+ snippet = makeSnippet(cause, searchTerm);
4941
5078
  }
4942
5079
  return {
4943
5080
  kind: "insight",
@@ -4946,7 +5083,7 @@ function buildInsightHit(row, query) {
4946
5083
  type: row.type,
4947
5084
  severity: row.severity,
4948
5085
  title: row.title,
4949
- keyword: row.keyword,
5086
+ query: row.query,
4950
5087
  provider: row.provider,
4951
5088
  matchedField,
4952
5089
  snippet,
@@ -5292,21 +5429,130 @@ var routeCatalog = [
5292
5429
  404: { description: "Project not found." }
5293
5430
  }
5294
5431
  },
5432
+ {
5433
+ method: "get",
5434
+ path: "/api/v1/projects/{name}/queries",
5435
+ summary: "List queries",
5436
+ tags: ["queries"],
5437
+ parameters: [nameParameter],
5438
+ responses: {
5439
+ 200: { description: "Queries returned." }
5440
+ }
5441
+ },
5442
+ {
5443
+ method: "put",
5444
+ path: "/api/v1/projects/{name}/queries",
5445
+ summary: "Replace queries",
5446
+ tags: ["queries"],
5447
+ parameters: [nameParameter],
5448
+ requestBody: {
5449
+ required: true,
5450
+ content: {
5451
+ "application/json": {
5452
+ schema: {
5453
+ type: "object",
5454
+ required: ["queries"],
5455
+ properties: {
5456
+ queries: stringArraySchema
5457
+ }
5458
+ }
5459
+ }
5460
+ }
5461
+ },
5462
+ responses: {
5463
+ 200: { description: "Queries replaced." }
5464
+ }
5465
+ },
5466
+ {
5467
+ method: "delete",
5468
+ path: "/api/v1/projects/{name}/queries",
5469
+ summary: "Delete specific queries",
5470
+ tags: ["queries"],
5471
+ parameters: [nameParameter],
5472
+ requestBody: {
5473
+ required: true,
5474
+ content: {
5475
+ "application/json": {
5476
+ schema: {
5477
+ type: "object",
5478
+ required: ["queries"],
5479
+ properties: {
5480
+ queries: stringArraySchema
5481
+ }
5482
+ }
5483
+ }
5484
+ }
5485
+ },
5486
+ responses: {
5487
+ 200: { description: "Remaining queries returned." },
5488
+ 400: { description: "Invalid query delete request." }
5489
+ }
5490
+ },
5491
+ {
5492
+ method: "post",
5493
+ path: "/api/v1/projects/{name}/queries",
5494
+ summary: "Append queries",
5495
+ tags: ["queries"],
5496
+ parameters: [nameParameter],
5497
+ requestBody: {
5498
+ required: true,
5499
+ content: {
5500
+ "application/json": {
5501
+ schema: {
5502
+ type: "object",
5503
+ required: ["queries"],
5504
+ properties: {
5505
+ queries: stringArraySchema
5506
+ }
5507
+ }
5508
+ }
5509
+ }
5510
+ },
5511
+ responses: {
5512
+ 200: { description: "Queries appended." }
5513
+ }
5514
+ },
5515
+ {
5516
+ method: "post",
5517
+ path: "/api/v1/projects/{name}/queries/generate",
5518
+ summary: "Generate query suggestions",
5519
+ tags: ["queries"],
5520
+ parameters: [nameParameter],
5521
+ requestBody: {
5522
+ required: true,
5523
+ content: {
5524
+ "application/json": {
5525
+ schema: {
5526
+ type: "object",
5527
+ required: ["provider"],
5528
+ properties: {
5529
+ provider: { type: "string", enum: ["gemini", "openai", "claude", "perplexity", "local"] },
5530
+ count: integerSchema
5531
+ }
5532
+ }
5533
+ }
5534
+ }
5535
+ },
5536
+ responses: {
5537
+ 200: { description: "Query suggestions returned." },
5538
+ 501: { description: "Query generation is not available." }
5539
+ }
5540
+ },
5295
5541
  {
5296
5542
  method: "get",
5297
5543
  path: "/api/v1/projects/{name}/keywords",
5298
- summary: "List keywords",
5299
- tags: ["keywords"],
5544
+ summary: "List keywords (legacy alias for queries)",
5545
+ tags: ["queries"],
5300
5546
  parameters: [nameParameter],
5301
5547
  responses: {
5302
- 200: { description: "Keywords returned." }
5548
+ 200: { description: "Legacy keyword-shaped queries returned." }
5303
5549
  }
5304
5550
  },
5305
5551
  {
5306
5552
  method: "put",
5307
5553
  path: "/api/v1/projects/{name}/keywords",
5308
- summary: "Replace keywords",
5309
- tags: ["keywords"],
5554
+ summary: "Replace keywords (legacy alias for queries)",
5555
+ tags: ["queries"],
5310
5556
  parameters: [nameParameter],
5311
5557
  requestBody: {
5312
5558
  required: true,
@@ -5323,14 +5569,14 @@ var routeCatalog = [
5323
5569
  }
5324
5570
  },
5325
5571
  responses: {
5326
- 200: { description: "Keywords replaced." }
5572
+ 200: { description: "Legacy keyword-shaped queries replaced." }
5327
5573
  }
5328
5574
  },
5329
5575
  {
5330
5576
  method: "delete",
5331
5577
  path: "/api/v1/projects/{name}/keywords",
5332
- summary: "Delete specific keywords",
5333
- tags: ["keywords"],
5578
+ summary: "Delete keywords (legacy alias for queries)",
5579
+ tags: ["queries"],
5334
5580
  parameters: [nameParameter],
5335
5581
  requestBody: {
5336
5582
  required: true,
@@ -5347,15 +5593,15 @@ var routeCatalog = [
5347
5593
  }
5348
5594
  },
5349
5595
  responses: {
5350
- 200: { description: "Remaining keywords returned." },
5351
- 400: { description: "Invalid keyword delete request." }
5596
+ 200: { description: "Remaining legacy keyword-shaped queries returned." },
5597
+ 400: { description: "Invalid legacy keyword delete request." }
5352
5598
  }
5353
5599
  },
5354
5600
  {
5355
5601
  method: "post",
5356
5602
  path: "/api/v1/projects/{name}/keywords",
5357
- summary: "Append keywords",
5358
- tags: ["keywords"],
5603
+ summary: "Append keywords (legacy alias for queries)",
5604
+ tags: ["queries"],
5359
5605
  parameters: [nameParameter],
5360
5606
  requestBody: {
5361
5607
  required: true,
@@ -5372,14 +5618,14 @@ var routeCatalog = [
5372
5618
  }
5373
5619
  },
5374
5620
  responses: {
5375
- 200: { description: "Keywords appended." }
5621
+ 200: { description: "Legacy keyword-shaped queries appended." }
5376
5622
  }
5377
5623
  },
5378
5624
  {
5379
5625
  method: "post",
5380
5626
  path: "/api/v1/projects/{name}/keywords/generate",
5381
- summary: "Generate keyword suggestions",
5382
- tags: ["keywords"],
5627
+ summary: "Generate keyword suggestions (legacy alias for queries)",
5628
+ tags: ["queries"],
5383
5629
  parameters: [nameParameter],
5384
5630
  requestBody: {
5385
5631
  required: true,
@@ -5397,8 +5643,8 @@ var routeCatalog = [
5397
5643
  }
5398
5644
  },
5399
5645
  responses: {
5400
- 200: { description: "Keyword suggestions returned." },
5401
- 501: { description: "Keyword generation is not available." }
5646
+ 200: { description: "Legacy keyword suggestions returned." },
5647
+ 501: { description: "Legacy keyword generation is not available." }
5402
5648
  }
5403
5649
  },
5404
5650
  {
@@ -5643,7 +5889,7 @@ var routeCatalog = [
5643
5889
  {
5644
5890
  method: "get",
5645
5891
  path: "/api/v1/projects/{name}/timeline",
5646
- summary: "Get keyword timeline",
5892
+ summary: "Get query timeline",
5647
5893
  tags: ["history"],
5648
5894
  parameters: [nameParameter, locationQueryParameter],
5649
5895
  responses: {
@@ -5788,7 +6034,7 @@ var routeCatalog = [
5788
6034
  properties: {
5789
6035
  companyName: stringSchema,
5790
6036
  domain: stringSchema,
5791
- phrases: stringArraySchema,
6037
+ queries: stringArraySchema,
5792
6038
  competitors: stringArraySchema
5793
6039
  }
5794
6040
  }
@@ -7276,8 +7522,8 @@ var routeCatalog = [
7276
7522
  {
7277
7523
  method: "get",
7278
7524
  path: "/api/v1/projects/{name}/citations/visibility",
7279
- summary: "Citation visibility headline (citation + answer-mention, by engine + keyword)",
7280
- description: 'Single-call read for the AI citation surface. Returns two parallel headline metrics (`providersCiting` = engines that cite the project in their grounding/source list, `providersMentioning` = engines that name the project in answer prose), per-keyword cross-tab buckets (`keywordsCitedAndMentioned` / `keywordsCitedOnly` / `keywordsMentionedOnly` / `keywordsInvisible` \u2014 mutually exclusive over keywords that have at least one snapshot), per-keyword engine coverage rows from the latest snapshot per (keyword \xD7 provider) with both `cited` and `mentioned` flags, and a competitor-gap list (keywords where the project is not cited but a configured competitor is). Status `no-data` with `reason: "no-runs-yet"` or `"no-keywords"` when the project lacks the inputs.',
7525
+ summary: "Citation visibility headline (citation + answer-mention, by engine + query)",
7526
+ description: 'Single-call read for the AI citation surface. Returns two parallel headline metrics (`providersCiting` = engines that cite the project in their grounding/source list, `providersMentioning` = engines that name the project in answer prose), per-query cross-tab buckets (`queriesCitedAndMentioned` / `queriesCitedOnly` / `queriesMentionedOnly` / `queriesInvisible` \u2014 mutually exclusive over queries that have at least one snapshot), per-query engine coverage rows from the latest snapshot per (query \xD7 provider) with both `cited` and `mentioned` flags, and a competitor-gap list (queries where the project is not cited but a configured competitor is). Status `no-data` with `reason: "no-runs-yet"` or `"no-queries"` when the project lacks the inputs.',
7281
7527
  tags: ["intelligence"],
7282
7528
  parameters: [nameParameter],
7283
7529
  responses: {
@@ -7331,7 +7577,7 @@ var routeCatalog = [
7331
7577
  method: "get",
7332
7578
  path: "/api/v1/projects/{name}/overview",
7333
7579
  summary: "Get a composite overview of project health",
7334
- description: 'Bundles project info, latest run, top undismissed insights, the latest health snapshot, keyword cited rate, per-provider breakdown, and transitions vs. the previous run. Designed for the "how is project X doing?" question so agents can answer in one call.',
7580
+ description: 'Bundles project info, latest run, top undismissed insights, the latest health snapshot, query cited rate, per-provider breakdown, and transitions vs. the previous run. Designed for the "how is project X doing?" question so agents can answer in one call.',
7335
7581
  tags: ["intelligence"],
7336
7582
  parameters: [nameParameter],
7337
7583
  responses: {
@@ -7343,7 +7589,7 @@ var routeCatalog = [
7343
7589
  method: "get",
7344
7590
  path: "/api/v1/projects/{name}/search",
7345
7591
  summary: "Search query snapshots and insights for text",
7346
- description: "Returns the most recent snapshots and insights whose answer text, cited domains, raw response, or insight title/keyword/recommendation/cause matches the query. Use to find anything mentioning a competitor, term, or URL without paginating snapshots.",
7592
+ description: "Returns the most recent snapshots and insights whose answer text, cited domains, raw response, or insight title/query/recommendation/cause matches the query. Use to find anything mentioning a competitor, term, or URL without paginating snapshots.",
7347
7593
  tags: ["intelligence"],
7348
7594
  parameters: [
7349
7595
  nameParameter,
@@ -7889,8 +8135,13 @@ async function snapshotRoutes(app, opts) {
7889
8135
  if (!opts.onSnapshotRequested) {
7890
8136
  throw notImplemented("Snapshot reporting is not supported in this deployment");
7891
8137
  }
8138
+ const input = {
8139
+ ...parsed.data,
8140
+ queries: resolveSnapshotRequestQueries(parsed.data)
8141
+ };
8142
+ delete input.phrases;
7892
8143
  try {
7893
- return await opts.onSnapshotRequested(parsed.data);
8144
+ return await opts.onSnapshotRequested(input);
7894
8145
  } catch (err) {
7895
8146
  request.log.error({ err }, "Snapshot report generation failed");
7896
8147
  throw internalError(err instanceof Error ? err.message : "Failed to generate snapshot report");
@@ -8129,7 +8380,7 @@ async function notificationRoutes(app) {
8129
8380
  project: { name: project.name, canonicalDomain: project.canonicalDomain },
8130
8381
  run: { id: "test-run-id", status: "completed", finishedAt: (/* @__PURE__ */ new Date()).toISOString() },
8131
8382
  transitions: [
8132
- { keyword: "test keyword", from: "not-cited", to: "cited", provider: "gemini" }
8383
+ { query: "test query", from: "not-cited", to: "cited", provider: "gemini" }
8133
8384
  ],
8134
8385
  dashboardUrl: `/projects/${project.name}`
8135
8386
  };
@@ -10534,22 +10785,22 @@ async function cdpRoutes(app, opts) {
10534
10785
  }
10535
10786
  const snapshots = app.db.select({
10536
10787
  id: querySnapshots.id,
10537
- keywordId: querySnapshots.keywordId,
10788
+ queryId: querySnapshots.queryId,
10538
10789
  provider: querySnapshots.provider,
10539
10790
  citationState: querySnapshots.citationState,
10540
10791
  citedDomains: querySnapshots.citedDomains,
10541
10792
  screenshotPath: querySnapshots.screenshotPath,
10542
10793
  rawResponse: querySnapshots.rawResponse
10543
10794
  }).from(querySnapshots).where(eq20(querySnapshots.runId, runId)).all();
10544
- const keywordRows = app.db.select({ id: keywords.id, keyword: keywords.keyword }).from(keywords).where(eq20(keywords.projectId, project.id)).all();
10545
- const keywordMap = new Map(keywordRows.map((k) => [k.id, k.keyword]));
10546
- const byKeyword = /* @__PURE__ */ new Map();
10795
+ const queryRows = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq20(queries.projectId, project.id)).all();
10796
+ const queryMap = new Map(queryRows.map((q) => [q.id, q.query]));
10797
+ const byQuery = /* @__PURE__ */ new Map();
10547
10798
  for (const snap of snapshots) {
10548
- const kwName = keywordMap.get(snap.keywordId) ?? snap.keywordId;
10549
- if (!byKeyword.has(snap.keywordId)) {
10550
- byKeyword.set(snap.keywordId, { keyword: kwName, api: null, browser: null });
10799
+ const qName = queryMap.get(snap.queryId) ?? snap.queryId;
10800
+ if (!byQuery.has(snap.queryId)) {
10801
+ byQuery.set(snap.queryId, { query: qName, api: null, browser: null });
10551
10802
  }
10552
- const entry = byKeyword.get(snap.keywordId);
10803
+ const entry = byQuery.get(snap.queryId);
10553
10804
  if (snap.provider === "cdp:chatgpt") {
10554
10805
  entry.browser = snap;
10555
10806
  } else if (snap.provider === "openai") {
@@ -10561,7 +10812,7 @@ async function cdpRoutes(app, opts) {
10561
10812
  let browserOnlyCited = 0;
10562
10813
  let disagreed = 0;
10563
10814
  let total = 0;
10564
- const keywordResults = [...byKeyword.values()].map(({ keyword, api, browser }) => {
10815
+ const queryResults = [...byQuery.values()].map(({ query, api, browser }) => {
10565
10816
  total++;
10566
10817
  const apiCited = api?.citationState === "cited";
10567
10818
  const browserCited = browser?.citationState === "cited";
@@ -10597,17 +10848,17 @@ async function cdpRoutes(app, opts) {
10597
10848
  }
10598
10849
  };
10599
10850
  return {
10600
- keyword,
10851
+ query,
10601
10852
  api: api ? {
10602
10853
  provider: api.provider,
10603
10854
  citationState: api.citationState,
10604
- citedDomains: JSON.parse(api.citedDomains || "[]"),
10855
+ citedDomains: parseJsonColumn(api.citedDomains, []),
10605
10856
  groundingSources: parseGroundingSources2(api)
10606
10857
  } : null,
10607
10858
  browser: browser ? {
10608
10859
  provider: browser.provider,
10609
10860
  citationState: browser.citationState,
10610
- citedDomains: JSON.parse(browser.citedDomains || "[]"),
10861
+ citedDomains: parseJsonColumn(browser.citedDomains, []),
10611
10862
  groundingSources: parseGroundingSources2(browser),
10612
10863
  screenshotUrl: browser.screenshotPath ? `${opts.routePrefix ?? "/api/v1"}/screenshots/${browser.id}` : void 0
10613
10864
  } : null,
@@ -10616,7 +10867,7 @@ async function cdpRoutes(app, opts) {
10616
10867
  });
10617
10868
  return reply.send({
10618
10869
  summary: { total, agreed, apiOnly: apiOnlyCited, browserOnly: browserOnlyCited, disagreed },
10619
- keywords: keywordResults
10870
+ queries: queryResults
10620
10871
  });
10621
10872
  }
10622
10873
  );
@@ -14491,8 +14742,8 @@ async function apiRoutes(app, opts) {
14491
14742
  onProjectUpserted: opts.onProjectUpserted,
14492
14743
  validProviderNames: opts.providerAdapters?.map((a) => a.name)
14493
14744
  });
14494
- await api.register(keywordRoutes, {
14495
- onGenerateKeywords: opts.onGenerateKeywords,
14745
+ await api.register(queryRoutes, {
14746
+ onGenerateQueries: opts.onGenerateQueries,
14496
14747
  validProviderNames: opts.providerAdapters?.filter((a) => a.mode === "api").map((a) => a.name)
14497
14748
  });
14498
14749
  await api.register(competitorRoutes);
@@ -14706,7 +14957,7 @@ async function healthcheck(config) {
14706
14957
  }
14707
14958
  async function executeTrackedQuery(input) {
14708
14959
  const model = resolveModel(input.config);
14709
- const prompt = buildPrompt(input.keyword, input.location);
14960
+ const prompt = buildPrompt(input.query, input.location);
14710
14961
  const client = createClient(input.config);
14711
14962
  try {
14712
14963
  const result = await withRetry(
@@ -14760,11 +15011,11 @@ function reparseStoredResult(rawResponse) {
14760
15011
  searchQueries
14761
15012
  };
14762
15013
  }
14763
- function buildPrompt(keyword, location) {
15014
+ function buildPrompt(query, location) {
14764
15015
  if (location) {
14765
- return `${keyword} (searching from ${location.city}, ${location.region}, ${location.country})`;
15016
+ return `${query} (searching from ${location.city}, ${location.region}, ${location.country})`;
14766
15017
  }
14767
- return keyword;
15018
+ return query;
14768
15019
  }
14769
15020
  function extractAnswerText(rawResponse) {
14770
15021
  try {
@@ -14953,7 +15204,7 @@ var geminiAdapter = {
14953
15204
  },
14954
15205
  async executeTrackedQuery(input, config) {
14955
15206
  const raw = await executeTrackedQuery({
14956
- keyword: input.keyword,
15207
+ query: input.query,
14957
15208
  canonicalDomains: input.canonicalDomains,
14958
15209
  competitorDomains: input.competitorDomains,
14959
15210
  config: toGeminiConfig(config),
@@ -15087,7 +15338,7 @@ async function executeTrackedQuery2(input) {
15087
15338
  model,
15088
15339
  tools: [webSearchTool],
15089
15340
  tool_choice: "required",
15090
- input: buildPrompt2(input.keyword)
15341
+ input: buildPrompt2(input.query)
15091
15342
  })
15092
15343
  );
15093
15344
  const rawResponse = responseToRecord2(response);
@@ -15132,8 +15383,8 @@ function reparseStoredResult2(rawResponse) {
15132
15383
  searchQueries
15133
15384
  };
15134
15385
  }
15135
- function buildPrompt2(keyword) {
15136
- return keyword;
15386
+ function buildPrompt2(query) {
15387
+ return query;
15137
15388
  }
15138
15389
  function extractResponseText(response) {
15139
15390
  try {
@@ -15180,7 +15431,7 @@ function extractGroundingSourcesFromRaw(rawResponse) {
15180
15431
  return sources;
15181
15432
  }
15182
15433
  function extractSearchQueriesFromRaw2(rawResponse) {
15183
- const queries = /* @__PURE__ */ new Set();
15434
+ const queries2 = /* @__PURE__ */ new Set();
15184
15435
  try {
15185
15436
  const output = rawResponse.output;
15186
15437
  if (!output) return [];
@@ -15188,19 +15439,19 @@ function extractSearchQueriesFromRaw2(rawResponse) {
15188
15439
  if (item.type !== "web_search_call" || !item.action) continue;
15189
15440
  const action = item.action;
15190
15441
  if (typeof action.query === "string" && action.query.length > 0) {
15191
- queries.add(action.query);
15442
+ queries2.add(action.query);
15192
15443
  }
15193
15444
  if (Array.isArray(action.queries)) {
15194
15445
  for (const query of action.queries) {
15195
15446
  if (typeof query === "string" && query.length > 0) {
15196
- queries.add(query);
15447
+ queries2.add(query);
15197
15448
  }
15198
15449
  }
15199
15450
  }
15200
15451
  }
15201
15452
  } catch {
15202
15453
  }
15203
- return [...queries];
15454
+ return [...queries2];
15204
15455
  }
15205
15456
  function extractAnswerTextFromRaw(rawResponse) {
15206
15457
  try {
@@ -15307,7 +15558,7 @@ var openaiAdapter = {
15307
15558
  },
15308
15559
  async executeTrackedQuery(input, config) {
15309
15560
  const raw = await executeTrackedQuery2({
15310
- keyword: input.keyword,
15561
+ query: input.query,
15311
15562
  canonicalDomains: input.canonicalDomains,
15312
15563
  competitorDomains: input.competitorDomains,
15313
15564
  config: toOpenAIConfig(config),
@@ -15459,7 +15710,7 @@ async function executeTrackedQuery3(input) {
15459
15710
  model,
15460
15711
  max_tokens: 4096,
15461
15712
  tools: [webSearchTool],
15462
- messages: [{ role: "user", content: input.keyword }]
15713
+ messages: [{ role: "user", content: input.query }]
15463
15714
  })
15464
15715
  );
15465
15716
  const rawResponse = responseToRecord3(response);
@@ -15546,19 +15797,19 @@ function extractGroundingSourcesFromRaw2(rawResponse) {
15546
15797
  return sources;
15547
15798
  }
15548
15799
  function extractSearchQueriesFromRaw3(rawResponse) {
15549
- const queries = /* @__PURE__ */ new Set();
15800
+ const queries2 = /* @__PURE__ */ new Set();
15550
15801
  try {
15551
15802
  const content = rawResponse.content;
15552
15803
  if (!content) return [];
15553
15804
  for (const block of content) {
15554
15805
  if (block.type === "server_tool_use" && block.name === "web_search") {
15555
15806
  if (typeof block.input?.query === "string" && block.input.query.length > 0) {
15556
- queries.add(block.input.query);
15807
+ queries2.add(block.input.query);
15557
15808
  }
15558
15809
  if (Array.isArray(block.input?.queries)) {
15559
15810
  for (const query of block.input.queries) {
15560
15811
  if (typeof query === "string" && query.length > 0) {
15561
- queries.add(query);
15812
+ queries2.add(query);
15562
15813
  }
15563
15814
  }
15564
15815
  }
@@ -15566,7 +15817,7 @@ function extractSearchQueriesFromRaw3(rawResponse) {
15566
15817
  }
15567
15818
  } catch {
15568
15819
  }
15569
- return [...queries];
15820
+ return [...queries2];
15570
15821
  }
15571
15822
  function extractAnswerTextFromRaw2(rawResponse) {
15572
15823
  try {
@@ -15684,7 +15935,7 @@ var claudeAdapter = {
15684
15935
  },
15685
15936
  async executeTrackedQuery(input, config) {
15686
15937
  const raw = await executeTrackedQuery3({
15687
- keyword: input.keyword,
15938
+ query: input.query,
15688
15939
  canonicalDomains: input.canonicalDomains,
15689
15940
  competitorDomains: input.competitorDomains,
15690
15941
  config: toClaudeConfig(config),
@@ -15821,7 +16072,7 @@ async function executeTrackedQuery4(input) {
15821
16072
  },
15822
16073
  {
15823
16074
  role: "user",
15824
- content: buildPrompt3(input.keyword, input.location)
16075
+ content: buildPrompt3(input.query, input.location)
15825
16076
  }
15826
16077
  ]
15827
16078
  })
@@ -15853,9 +16104,9 @@ function normalizeResult4(raw) {
15853
16104
  searchQueries: raw.searchQueries
15854
16105
  };
15855
16106
  }
15856
- function buildPrompt3(keyword, location) {
16107
+ function buildPrompt3(query, location) {
15857
16108
  const locationContext = location ? ` The user is searching from ${location.city}, ${location.region}, ${location.country}.` : "";
15858
- return `Based on your training knowledge, what websites, services, or organizations are commonly associated with "${keyword}"?${locationContext} List the most relevant ones and include their domain names (e.g. example.com) where you know them.`;
16109
+ return `Based on your training knowledge, what websites, services, or organizations are commonly associated with "${query}"?${locationContext} List the most relevant ones and include their domain names (e.g. example.com) where you know them.`;
15859
16110
  }
15860
16111
  function extractAnswerText2(rawResponse) {
15861
16112
  try {
@@ -15942,7 +16193,7 @@ var localAdapter = {
15942
16193
  },
15943
16194
  async executeTrackedQuery(input, config) {
15944
16195
  const raw = await executeTrackedQuery4({
15945
- keyword: input.keyword,
16196
+ query: input.query,
15946
16197
  canonicalDomains: input.canonicalDomains,
15947
16198
  competitorDomains: input.competitorDomains,
15948
16199
  config: toLocalConfig(config),
@@ -16160,7 +16411,7 @@ var chatgptTarget = {
16160
16411
  baseUrl: "https://chatgpt.com",
16161
16412
  newConversationUrl: "https://chatgpt.com/?model=auto",
16162
16413
  responseSelector: '[data-testid="conversation-turn-3"], article:last-of-type, .agent-turn:last-of-type',
16163
- async submitQuery(client, keyword) {
16414
+ async submitQuery(client, query) {
16164
16415
  const inputReady = await waitForElement(
16165
16416
  client,
16166
16417
  '#prompt-textarea, [contenteditable="true"][data-placeholder]',
@@ -16177,7 +16428,7 @@ var chatgptTarget = {
16177
16428
  expression: `(document.querySelector('#prompt-textarea') || document.querySelector('[contenteditable="true"][data-placeholder]')).focus()`
16178
16429
  });
16179
16430
  await sleep2(200);
16180
- for (const char of keyword) {
16431
+ for (const char of query) {
16181
16432
  await client.Input.dispatchKeyEvent({
16182
16433
  type: "keyDown",
16183
16434
  key: char,
@@ -16503,7 +16754,7 @@ var cdpChatgptAdapter = {
16503
16754
  const target = chatgptTarget;
16504
16755
  try {
16505
16756
  const client = await conn.prepareForQuery(target);
16506
- await target.submitQuery(client, input.keyword);
16757
+ await target.submitQuery(client, input.query);
16507
16758
  await target.waitForResponse(client);
16508
16759
  const answerText = await target.extractAnswer(client);
16509
16760
  const groundingSources = await target.extractCitations(client);
@@ -16528,7 +16779,7 @@ var cdpChatgptAdapter = {
16528
16779
  },
16529
16780
  model: "chatgpt-web",
16530
16781
  groundingSources,
16531
- searchQueries: [input.keyword],
16782
+ searchQueries: [input.query],
16532
16783
  screenshotPath: capturedScreenshotPath
16533
16784
  };
16534
16785
  } catch (err) {
@@ -16627,7 +16878,7 @@ async function healthcheck5(config) {
16627
16878
  async function executeTrackedQuery5(input) {
16628
16879
  const model = input.config.model ?? DEFAULT_MODEL5;
16629
16880
  const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
16630
- const prompt = buildPrompt4(input.keyword, input.location);
16881
+ const prompt = buildPrompt4(input.query, input.location);
16631
16882
  try {
16632
16883
  const response = await withRetry5(
16633
16884
  () => client.chat.completions.create({
@@ -16684,11 +16935,11 @@ function reparseStoredResult4(rawResponse) {
16684
16935
  searchQueries: []
16685
16936
  };
16686
16937
  }
16687
- function buildPrompt4(keyword, location) {
16938
+ function buildPrompt4(query, location) {
16688
16939
  if (location) {
16689
- return `${keyword} (searching from ${location.city}, ${location.region}, ${location.country})`;
16940
+ return `${query} (searching from ${location.city}, ${location.region}, ${location.country})`;
16690
16941
  }
16691
- return keyword;
16942
+ return query;
16692
16943
  }
16693
16944
  function extractCitations(rawResponse) {
16694
16945
  if (Array.isArray(rawResponse.citations)) {
@@ -16851,7 +17102,7 @@ var perplexityAdapter = {
16851
17102
  },
16852
17103
  async executeTrackedQuery(input, config) {
16853
17104
  const raw = await executeTrackedQuery5({
16854
- keyword: input.keyword,
17105
+ query: input.query,
16855
17106
  canonicalDomains: input.canonicalDomains,
16856
17107
  competitorDomains: input.competitorDomains,
16857
17108
  config: toPerplexityConfig(config),
@@ -17318,7 +17569,7 @@ var JobRunner = class {
17318
17569
  const startTime = Date.now();
17319
17570
  let runLocation;
17320
17571
  let activeProviders = [];
17321
- let projectKeywords = [];
17572
+ let projectQueries = [];
17322
17573
  const providerDispatchCounts = /* @__PURE__ */ new Map();
17323
17574
  try {
17324
17575
  const existingRun = this.getRunState(runId);
@@ -17329,7 +17580,7 @@ var JobRunner = class {
17329
17580
  this.handleCancelledRun(runId, projectId, startTime, {
17330
17581
  providerCount: 0,
17331
17582
  providers: [],
17332
- keywordCount: 0
17583
+ queryCount: 0
17333
17584
  });
17334
17585
  return;
17335
17586
  }
@@ -17360,7 +17611,7 @@ var JobRunner = class {
17360
17611
  throw new Error("No providers configured. Add at least one provider API key.");
17361
17612
  }
17362
17613
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
17363
- projectKeywords = this.db.select().from(keywords).where(eq23(keywords.projectId, projectId)).all();
17614
+ projectQueries = this.db.select().from(queries).where(eq23(queries.projectId, projectId)).all();
17364
17615
  const projectCompetitors = this.db.select().from(competitors).where(eq23(competitors.projectId, projectId)).all();
17365
17616
  const competitorDomains = projectCompetitors.map((c) => c.domain);
17366
17617
  const allDomains = effectiveDomains({
@@ -17370,10 +17621,10 @@ var JobRunner = class {
17370
17621
  const executionContext = {
17371
17622
  providerCount: activeProviders.length,
17372
17623
  providers: activeProviders.map((provider) => provider.adapter.name),
17373
- keywordCount: projectKeywords.length,
17624
+ queryCount: projectQueries.length,
17374
17625
  ...runLocation ? { location: runLocation.label } : {}
17375
17626
  };
17376
- const queriesPerProvider = projectKeywords.length;
17627
+ const queriesPerProvider = projectQueries.length;
17377
17628
  const todayPeriod = getCurrentUsageDay();
17378
17629
  for (const p of activeProviders) {
17379
17630
  const providerScope = `${projectId}:${p.adapter.name}`;
@@ -17399,7 +17650,7 @@ var JobRunner = class {
17399
17650
  let totalSnapshotsInserted = 0;
17400
17651
  const apiProviders = activeProviders.filter((p) => !isBrowserProvider(p.adapter.name));
17401
17652
  const browserProviders = activeProviders.filter((p) => isBrowserProvider(p.adapter.name));
17402
- const processKeywordForProvider = async (registeredProvider, kw) => {
17653
+ const processQueryForProvider = async (registeredProvider, q) => {
17403
17654
  const { adapter, config } = registeredProvider;
17404
17655
  const providerName = adapter.name;
17405
17656
  const gate = executionGates.get(providerName);
@@ -17412,7 +17663,7 @@ var JobRunner = class {
17412
17663
  providerDispatchCounts.set(providerName, (providerDispatchCounts.get(providerName) ?? 0) + 1);
17413
17664
  const raw = await adapter.executeTrackedQuery(
17414
17665
  {
17415
- keyword: kw.keyword,
17666
+ query: q.query,
17416
17667
  canonicalDomains: allDomains,
17417
17668
  competitorDomains,
17418
17669
  location: runLocation
@@ -17421,7 +17672,7 @@ var JobRunner = class {
17421
17672
  );
17422
17673
  this.throwIfRunCancelled(runId);
17423
17674
  const normalized = adapter.normalizeResult(raw);
17424
- log.info("query.result", { runId, provider: providerName, keyword: kw.keyword, citedDomains: normalized.citedDomains, groundingSources: normalized.groundingSources.map((s) => s.uri), matchDomains: allDomains });
17675
+ log.info("query.result", { runId, provider: providerName, query: q.query, citedDomains: normalized.citedDomains, groundingSources: normalized.groundingSources.map((s) => s.uri), matchDomains: allDomains });
17425
17676
  const citationState = determineCitationState(normalized, allDomains);
17426
17677
  const answerMentioned = determineAnswerMentioned(
17427
17678
  normalized.answerText,
@@ -17446,7 +17697,7 @@ var JobRunner = class {
17446
17697
  this.db.insert(querySnapshots).values({
17447
17698
  id: snapshotId,
17448
17699
  runId,
17449
- keywordId: kw.id,
17700
+ queryId: q.id,
17450
17701
  provider: providerName,
17451
17702
  model: raw.model,
17452
17703
  citationState,
@@ -17469,7 +17720,7 @@ var JobRunner = class {
17469
17720
  this.db.insert(querySnapshots).values({
17470
17721
  id: crypto19.randomUUID(),
17471
17722
  runId,
17472
- keywordId: kw.id,
17723
+ queryId: q.id,
17473
17724
  provider: providerName,
17474
17725
  model: raw.model,
17475
17726
  citationState,
@@ -17489,7 +17740,7 @@ var JobRunner = class {
17489
17740
  }).run();
17490
17741
  }
17491
17742
  totalSnapshotsInserted++;
17492
- log.info("query.citation", { runId, provider: providerName, keyword: kw.keyword, citationState, answerMentioned });
17743
+ log.info("query.citation", { runId, provider: providerName, query: q.query, citationState, answerMentioned });
17493
17744
  });
17494
17745
  } catch (err) {
17495
17746
  if (err instanceof RunCancelledError) {
@@ -17497,20 +17748,20 @@ var JobRunner = class {
17497
17748
  }
17498
17749
  const msg = err instanceof Error ? err.message : String(err);
17499
17750
  const stack = err instanceof Error ? err.stack : void 0;
17500
- log.error("query.failed", { runId, provider: providerName, keyword: kw.keyword, error: msg, stack });
17751
+ log.error("query.failed", { runId, provider: providerName, query: q.query, error: msg, stack });
17501
17752
  if (!providerErrors.has(providerName)) {
17502
17753
  providerErrors.set(providerName, msg);
17503
17754
  }
17504
17755
  }
17505
17756
  };
17506
17757
  await runWithConcurrency(apiProviders, resolveProviderFanout(), async (registeredProvider) => {
17507
- await Promise.all(projectKeywords.map(async (kw) => {
17508
- await processKeywordForProvider(registeredProvider, kw);
17758
+ await Promise.all(projectQueries.map(async (q) => {
17759
+ await processQueryForProvider(registeredProvider, q);
17509
17760
  }));
17510
17761
  });
17511
17762
  for (const registeredProvider of browserProviders) {
17512
- for (const kw of projectKeywords) {
17513
- await processKeywordForProvider(registeredProvider, kw);
17763
+ for (const q of projectQueries) {
17764
+ await processQueryForProvider(registeredProvider, q);
17514
17765
  }
17515
17766
  }
17516
17767
  this.throwIfRunCancelled(runId);
@@ -17531,7 +17782,7 @@ var JobRunner = class {
17531
17782
  status: finalStatus,
17532
17783
  providerCount: executionContext.providerCount,
17533
17784
  providers: executionContext.providers,
17534
- keywordCount: executionContext.keywordCount,
17785
+ queryCount: executionContext.queryCount,
17535
17786
  durationMs: Date.now() - startTime,
17536
17787
  ...executionContext.location ? { location: executionContext.location } : {}
17537
17788
  });
@@ -17545,7 +17796,7 @@ var JobRunner = class {
17545
17796
  const executionContext = {
17546
17797
  providerCount: activeProviders.length,
17547
17798
  providers: activeProviders.map((provider) => provider.adapter.name),
17548
- keywordCount: projectKeywords.length,
17799
+ queryCount: projectQueries.length,
17549
17800
  ...runLocation ? { location: runLocation.label } : {}
17550
17801
  };
17551
17802
  if (err instanceof RunCancelledError || this.isRunCancelled(runId)) {
@@ -17564,7 +17815,7 @@ var JobRunner = class {
17564
17815
  status: "failed",
17565
17816
  providerCount: executionContext.providerCount,
17566
17817
  providers: executionContext.providers,
17567
- keywordCount: executionContext.keywordCount,
17818
+ queryCount: executionContext.queryCount,
17568
17819
  durationMs: Date.now() - startTime,
17569
17820
  ...executionContext.location ? { location: executionContext.location } : {}
17570
17821
  });
@@ -17623,7 +17874,7 @@ var JobRunner = class {
17623
17874
  status: "cancelled",
17624
17875
  providerCount: context.providerCount,
17625
17876
  providers: context.providers,
17626
- keywordCount: context.keywordCount,
17877
+ queryCount: context.queryCount,
17627
17878
  durationMs: Date.now() - startTime,
17628
17879
  ...context.location ? { location: context.location } : {}
17629
17880
  });
@@ -18819,7 +19070,7 @@ var Notifier = class {
18819
19070
  type: i.type,
18820
19071
  severity: i.severity,
18821
19072
  title: i.title,
18822
- keyword: i.keyword,
19073
+ query: i.query,
18823
19074
  provider: i.provider
18824
19075
  })),
18825
19076
  dashboardUrl: `${this.serverUrl}/projects/${project.name}`
@@ -18840,27 +19091,27 @@ var Notifier = class {
18840
19091
  const previousRunId = recentRuns[1].id;
18841
19092
  if (currentRunId !== runId) return [];
18842
19093
  const currentSnapshots = this.db.select({
18843
- keywordId: querySnapshots.keywordId,
18844
- keyword: keywords.keyword,
19094
+ queryId: querySnapshots.queryId,
19095
+ query: queries.query,
18845
19096
  provider: querySnapshots.provider,
18846
19097
  citationState: querySnapshots.citationState
18847
- }).from(querySnapshots).leftJoin(keywords, eq30(querySnapshots.keywordId, keywords.id)).where(eq30(querySnapshots.runId, currentRunId)).all();
19098
+ }).from(querySnapshots).leftJoin(queries, eq30(querySnapshots.queryId, queries.id)).where(eq30(querySnapshots.runId, currentRunId)).all();
18848
19099
  const previousSnapshots = this.db.select({
18849
- keywordId: querySnapshots.keywordId,
19100
+ queryId: querySnapshots.queryId,
18850
19101
  provider: querySnapshots.provider,
18851
19102
  citationState: querySnapshots.citationState
18852
19103
  }).from(querySnapshots).where(eq30(querySnapshots.runId, previousRunId)).all();
18853
19104
  const prevMap = /* @__PURE__ */ new Map();
18854
19105
  for (const s of previousSnapshots) {
18855
- prevMap.set(`${s.keywordId}:${s.provider}`, s.citationState);
19106
+ prevMap.set(`${s.queryId}:${s.provider}`, s.citationState);
18856
19107
  }
18857
19108
  const transitions = [];
18858
19109
  for (const s of currentSnapshots) {
18859
- const key = `${s.keywordId}:${s.provider}`;
19110
+ const key = `${s.queryId}:${s.provider}`;
18860
19111
  const prevState = prevMap.get(key);
18861
19112
  if (prevState && prevState !== s.citationState) {
18862
19113
  transitions.push({
18863
- keyword: s.keyword ?? s.keywordId,
19114
+ query: s.query ?? s.queryId,
18864
19115
  from: prevState,
18865
19116
  to: s.citationState,
18866
19117
  provider: s.provider
@@ -20330,7 +20581,7 @@ var SnapshotService = class {
20330
20581
  async createReport(input) {
20331
20582
  const companyName = input.companyName.trim();
20332
20583
  const domain = normalizeDomain3(input.domain);
20333
- const manualPhrases = normalizeStringList(input.phrases ?? []);
20584
+ const manualQueries = normalizeStringList(resolveSnapshotRequestQueries(input));
20334
20585
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
20335
20586
  const providers = this.registry.getAll();
20336
20587
  if (providers.length === 0) {
@@ -20342,9 +20593,9 @@ var SnapshotService = class {
20342
20593
  fetchSiteText(domain),
20343
20594
  this.runAudit(homepageUrl)
20344
20595
  ]);
20345
- if (manualPhrases.length === 0 && !siteText) {
20596
+ if (manualQueries.length === 0 && !siteText) {
20346
20597
  throw new Error(
20347
- `Could not analyze https://${extractHostname2(domain)}. Try again with a reachable homepage or pass manual category queries via --phrases.`
20598
+ `Could not analyze https://${extractHostname2(domain)}. Try again with a reachable homepage or pass manual category queries via --queries.`
20348
20599
  );
20349
20600
  }
20350
20601
  const profile = await this.buildProfile({
@@ -20352,13 +20603,13 @@ var SnapshotService = class {
20352
20603
  domain,
20353
20604
  siteText,
20354
20605
  audit,
20355
- manualPhrases,
20606
+ manualQueries,
20356
20607
  analysisProvider
20357
20608
  });
20358
20609
  const queryResults = await this.runSnapshotQueries({
20359
20610
  companyName,
20360
20611
  domain,
20361
- phrases: profile.phrases,
20612
+ queries: profile.queries,
20362
20613
  providers,
20363
20614
  manualCompetitors
20364
20615
  });
@@ -20372,7 +20623,7 @@ var SnapshotService = class {
20372
20623
  analysisProvider
20373
20624
  });
20374
20625
  const enrichedResults = applyBatchAssessment(queryResults, batchAssessment);
20375
- const summary = buildSnapshotSummary(companyName, profile.phrases, providers, enrichedResults, audit, batchAssessment);
20626
+ const summary = buildSnapshotSummary(companyName, profile.queries, providers, enrichedResults, audit, batchAssessment);
20376
20627
  const reportCompetitors = uniqueStrings([
20377
20628
  ...manualCompetitors,
20378
20629
  ...summary.topCompetitors.map((entry) => entry.name)
@@ -20382,7 +20633,7 @@ var SnapshotService = class {
20382
20633
  domain,
20383
20634
  homepageUrl,
20384
20635
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20385
- phrases: profile.phrases,
20636
+ queries: profile.queries,
20386
20637
  competitors: reportCompetitors,
20387
20638
  profile: {
20388
20639
  industry: profile.industry,
@@ -20419,16 +20670,16 @@ var SnapshotService = class {
20419
20670
  try {
20420
20671
  const raw = await ctx.analysisProvider.adapter.generateText(prompt, ctx.analysisProvider.config);
20421
20672
  const parsed = parseJsonObject(raw);
20422
- const parsedPhrases = ctx.manualPhrases.length > 0 ? ctx.manualPhrases : normalizeStringList(parsed.phrases ?? []).slice(0, SNAPSHOT_QUERY_COUNT);
20423
- if (ctx.manualPhrases.length === 0 && parsedPhrases.length === 0) {
20424
- throw new Error("no phrases returned");
20673
+ const parsedQueries = ctx.manualQueries.length > 0 ? ctx.manualQueries : normalizeStringList(parsed.queries ?? []).slice(0, SNAPSHOT_QUERY_COUNT);
20674
+ if (ctx.manualQueries.length === 0 && parsedQueries.length === 0) {
20675
+ throw new Error("no queries returned");
20425
20676
  }
20426
20677
  return {
20427
20678
  industry: parsed.industry?.trim() || "Unknown",
20428
20679
  summary: parsed.summary?.trim() || ctx.audit.summary,
20429
20680
  services: uniqueStrings(parsed.services ?? []).slice(0, 6),
20430
20681
  categoryTerms: uniqueStrings(parsed.categoryTerms ?? []).slice(0, 8),
20431
- phrases: parsedPhrases
20682
+ queries: parsedQueries
20432
20683
  };
20433
20684
  } catch (err) {
20434
20685
  log12.warn("profile.generation-failed", {
@@ -20438,9 +20689,9 @@ var SnapshotService = class {
20438
20689
  });
20439
20690
  }
20440
20691
  }
20441
- if (ctx.manualPhrases.length === 0) {
20692
+ if (ctx.manualQueries.length === 0) {
20442
20693
  throw new Error(
20443
- "Automatic category-query generation requires a configured API provider. Add OpenAI, Claude, Gemini, Perplexity, or Local, or pass --phrases manually."
20694
+ "Automatic category-query generation requires a configured API provider. Add OpenAI, Claude, Gemini, Perplexity, or Local, or pass --queries manually."
20444
20695
  );
20445
20696
  }
20446
20697
  return {
@@ -20448,7 +20699,7 @@ var SnapshotService = class {
20448
20699
  summary: ctx.audit.summary,
20449
20700
  services: [],
20450
20701
  categoryTerms: [],
20451
- phrases: ctx.manualPhrases
20702
+ queries: ctx.manualQueries
20452
20703
  };
20453
20704
  }
20454
20705
  async runSnapshotQueries(ctx) {
@@ -20463,15 +20714,15 @@ var SnapshotService = class {
20463
20714
  );
20464
20715
  }
20465
20716
  const competitorDomains = ctx.manualCompetitors.filter(isDomainLike);
20466
- return Promise.all(ctx.phrases.map(async (phrase) => ({
20467
- phrase,
20717
+ return Promise.all(ctx.queries.map(async (query) => ({
20718
+ query,
20468
20719
  providerResults: await Promise.all(ctx.providers.map(async (provider) => {
20469
20720
  const gate = gates.get(provider.adapter.name);
20470
20721
  return gate.run(async () => {
20471
20722
  try {
20472
20723
  const raw = await provider.adapter.executeTrackedQuery(
20473
20724
  {
20474
- keyword: phrase,
20725
+ query,
20475
20726
  canonicalDomains: [ctx.domain],
20476
20727
  competitorDomains
20477
20728
  },
@@ -20528,8 +20779,8 @@ var SnapshotService = class {
20528
20779
  return buildFallbackBatchAssessment(ctx.companyName, ctx.audit);
20529
20780
  }
20530
20781
  const responses = ctx.queryResults.flatMap(
20531
- (query) => query.providerResults.filter((result) => !result.error).map((result) => ({
20532
- phrase: query.phrase,
20782
+ (queryResult) => queryResult.providerResults.filter((result) => !result.error).map((result) => ({
20783
+ query: queryResult.query,
20533
20784
  provider: result.provider,
20534
20785
  displayName: result.displayName,
20535
20786
  heuristicMentioned: result.mentioned,
@@ -20555,10 +20806,10 @@ var SnapshotService = class {
20555
20806
  const raw = await ctx.analysisProvider.adapter.generateText(prompt, ctx.analysisProvider.config);
20556
20807
  const parsed = parseJsonObject(raw);
20557
20808
  return {
20558
- assessments: (parsed.assessments ?? []).filter((assessment) => assessment.phrase && assessment.provider).map((assessment) => {
20809
+ assessments: (parsed.assessments ?? []).filter((assessment) => assessment.query && assessment.provider).map((assessment) => {
20559
20810
  const hasReviewedCompetitors = assessment.recommendedCompetitors !== void 0;
20560
20811
  return {
20561
- phrase: assessment.phrase,
20812
+ query: assessment.query,
20562
20813
  provider: assessment.provider,
20563
20814
  mentioned: assessment.mentioned,
20564
20815
  describedAccurately: assessment.describedAccurately,
@@ -20594,11 +20845,11 @@ function buildProfilePrompt(ctx) {
20594
20845
  "Use ONLY the homepage text below. Do not browse or invent facts.",
20595
20846
  "Infer the company category, summarize what it sells, and generate non-branded category queries buyers would ask an AI assistant.",
20596
20847
  'Never produce brand queries like "what does Acme do?"',
20597
- `Return strict JSON with keys: industry, summary, services, categoryTerms, phrases.`,
20598
- `phrases must contain exactly ${SNAPSHOT_QUERY_COUNT} buyer-style category/recommendation queries unless manual phrases are provided.`
20848
+ `Return strict JSON with keys: industry, summary, services, categoryTerms, queries.`,
20849
+ `queries must contain exactly ${SNAPSHOT_QUERY_COUNT} buyer-style category/recommendation queries unless manual queries are provided.`
20599
20850
  ];
20600
- if (ctx.manualPhrases.length > 0) {
20601
- instructions.push('Manual phrases were already supplied. Echo them back unchanged in the "phrases" array.');
20851
+ if (ctx.manualQueries.length > 0) {
20852
+ instructions.push('Manual queries were already supplied. Echo them back unchanged in the "queries" array.');
20602
20853
  }
20603
20854
  return [
20604
20855
  ...instructions,
@@ -20616,7 +20867,7 @@ function buildBatchAnalysisPrompt(ctx) {
20616
20867
  "You are reviewing AI answer-engine responses for a sales-facing AEO snapshot report.",
20617
20868
  "Use ONLY the provided facts and responses. Do not invent companies or claims.",
20618
20869
  "Return strict JSON with keys: assessments, whatThisMeans, recommendedActions.",
20619
- "Each assessment must include: phrase, provider, mentioned, describedAccurately, accuracyNotes, incorrectClaims, recommendedCompetitors.",
20870
+ "Each assessment must include: query, provider, mentioned, describedAccurately, accuracyNotes, incorrectClaims, recommendedCompetitors.",
20620
20871
  "describedAccurately must be one of: yes, no, unknown, not-mentioned.",
20621
20872
  "",
20622
20873
  "CRITICAL \u2014 recommendedCompetitors extraction:",
@@ -20652,12 +20903,12 @@ function buildFallbackBatchAssessment(companyName, audit) {
20652
20903
  function applyBatchAssessment(queryResults, batchAssessment) {
20653
20904
  const assessmentMap = /* @__PURE__ */ new Map();
20654
20905
  for (const assessment of batchAssessment.assessments) {
20655
- assessmentMap.set(`${assessment.phrase}::${assessment.provider}`, assessment);
20906
+ assessmentMap.set(`${assessment.query}::${assessment.provider}`, assessment);
20656
20907
  }
20657
- return queryResults.map((query) => ({
20658
- phrase: query.phrase,
20659
- providerResults: query.providerResults.map((result) => {
20660
- const assessment = assessmentMap.get(`${query.phrase}::${result.provider}`);
20908
+ return queryResults.map((queryResult) => ({
20909
+ query: queryResult.query,
20910
+ providerResults: queryResult.providerResults.map((result) => {
20911
+ const assessment = assessmentMap.get(`${queryResult.query}::${result.provider}`);
20661
20912
  if (!assessment) {
20662
20913
  return {
20663
20914
  ...result,
@@ -20680,8 +20931,8 @@ function applyBatchAssessment(queryResults, batchAssessment) {
20680
20931
  })
20681
20932
  }));
20682
20933
  }
20683
- function buildSnapshotSummary(companyName, phrases, providers, queryResults, audit, batchAssessment) {
20684
- const allResults = queryResults.flatMap((query) => query.providerResults);
20934
+ function buildSnapshotSummary(companyName, queries2, providers, queryResults, audit, batchAssessment) {
20935
+ const allResults = queryResults.flatMap((queryResult) => queryResult.providerResults);
20685
20936
  const successfulResults = allResults.filter((result) => !result.error);
20686
20937
  const failedComparisons = allResults.length - successfulResults.length;
20687
20938
  const mentionCount = successfulResults.filter((result) => result.mentioned).length;
@@ -20694,7 +20945,7 @@ function buildSnapshotSummary(companyName, phrases, providers, queryResults, aud
20694
20945
  }
20695
20946
  }
20696
20947
  const topCompetitors = [...competitorCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 10).map(([name, count]) => ({ name, count }));
20697
- const defaultMeaning = totalComparisons > 0 ? `${companyName} was mentioned in ${mentionCount}/${totalComparisons} successful provider-query response${totalComparisons === 1 ? "" : "s"} across ${phrases.length} category queries.` : `No successful provider responses were returned across ${phrases.length} category queries.`;
20948
+ const defaultMeaning = totalComparisons > 0 ? `${companyName} was mentioned in ${mentionCount}/${totalComparisons} successful provider-query response${totalComparisons === 1 ? "" : "s"} across ${queries2.length} category queries.` : `No successful provider responses were returned across ${queries2.length} category queries.`;
20698
20949
  const failureNote = failedComparisons > 0 ? [
20699
20950
  `${failedComparisons} provider response${failedComparisons === 1 ? "" : "s"} failed and ${failedComparisons === 1 ? "was" : "were"} excluded from visibility totals.`
20700
20951
  ] : [];
@@ -20704,7 +20955,7 @@ function buildSnapshotSummary(companyName, phrases, providers, queryResults, aud
20704
20955
  const combinedWhatThisMeans = uniqueStrings([...whatThisMeans, ...failureNote]).slice(0, 5);
20705
20956
  const recommendedActions = batchAssessment.recommendedActions.length > 0 ? batchAssessment.recommendedActions : buildFallbackRecommendedActions(audit);
20706
20957
  return {
20707
- totalQueries: phrases.length,
20958
+ totalQueries: queries2.length,
20708
20959
  totalProviders: providers.length,
20709
20960
  totalComparisons,
20710
20961
  mentionCount,
@@ -20712,7 +20963,7 @@ function buildSnapshotSummary(companyName, phrases, providers, queryResults, aud
20712
20963
  topCompetitors,
20713
20964
  visibilityGap: buildVisibilityGap(
20714
20965
  companyName,
20715
- phrases.length,
20966
+ queries2.length,
20716
20967
  providers.length,
20717
20968
  totalComparisons,
20718
20969
  mentionCount,
@@ -21646,7 +21897,7 @@ async function createServer(opts) {
21646
21897
  const conn = registry.get("cdp:chatgpt");
21647
21898
  if (!conn) throw new Error("CDP provider not configured");
21648
21899
  const result = await conn.adapter.executeTrackedQuery(
21649
- { keyword: query, canonicalDomains: [], competitorDomains: [] },
21900
+ { query, canonicalDomains: [], competitorDomains: [] },
21650
21901
  conn.config
21651
21902
  );
21652
21903
  const raw = result.rawResponse;
@@ -21657,21 +21908,21 @@ async function createServer(opts) {
21657
21908
  citations: raw.groundingSources ?? []
21658
21909
  }];
21659
21910
  },
21660
- onGenerateKeywords: async (providerName, count, project) => {
21911
+ onGenerateQueries: async (providerName, count, project) => {
21661
21912
  const provider = registry.get(providerName);
21662
21913
  if (!provider) throw new Error(`Provider "${providerName}" is not configured`);
21663
21914
  const siteText = await fetchSiteText(project.domain);
21664
- const prompt = buildKeywordGenerationPrompt({
21915
+ const prompt = buildQueryGenerationPrompt({
21665
21916
  domain: project.domain,
21666
21917
  displayName: project.displayName,
21667
21918
  country: project.country,
21668
21919
  language: project.language,
21669
- existingKeywords: project.existingKeywords,
21920
+ existingQueries: project.existingQueries,
21670
21921
  siteText,
21671
21922
  count
21672
21923
  });
21673
21924
  const raw = await provider.adapter.generateText(prompt, provider.config);
21674
- return parseKeywordResponse(raw, count);
21925
+ return parseQueryResponse(raw, count);
21675
21926
  },
21676
21927
  onSnapshotRequested: async (input) => {
21677
21928
  return snapshotService.createReport(input);
@@ -21742,7 +21993,7 @@ async function createServer(opts) {
21742
21993
  });
21743
21994
  return app;
21744
21995
  }
21745
- function buildKeywordGenerationPrompt(ctx) {
21996
+ function buildQueryGenerationPrompt(ctx) {
21746
21997
  const lines = [
21747
21998
  "You are an SEO and AEO (Answer Engine Optimization) expert. Given a website's content, generate search queries that potential users would type into AI answer engines (ChatGPT, Gemini, Claude) to find services, products, or information like what this site offers.",
21748
21999
  "",
@@ -21754,23 +22005,23 @@ function buildKeywordGenerationPrompt(ctx) {
21754
22005
  if (ctx.siteText) {
21755
22006
  lines.push("", "--- Site Content ---", ctx.siteText, "--- End Site Content ---");
21756
22007
  }
21757
- if (ctx.existingKeywords.length > 0) {
21758
- lines.push("", `Already tracking (do NOT duplicate): ${ctx.existingKeywords.join(", ")}`);
22008
+ if (ctx.existingQueries.length > 0) {
22009
+ lines.push("", `Already tracking (do NOT duplicate): ${ctx.existingQueries.join(", ")}`);
21759
22010
  }
21760
22011
  lines.push(
21761
22012
  "",
21762
- `Generate exactly ${ctx.count} key phrases that:`,
22013
+ `Generate exactly ${ctx.count} queries that:`,
21763
22014
  '- Are short and concise (2-5 words each, like "best dentist brooklyn" not "what is the best dentist office in the brooklyn area for families")',
21764
22015
  "- Are natural phrases people would type into AI answer engines",
21765
22016
  "- Cover different intents (informational, transactional, navigational)",
21766
22017
  `- Are relevant to the ${ctx.country} market in ${ctx.language}`,
21767
22018
  "- Reflect the actual services/products/content found on the site",
21768
22019
  "",
21769
- "Return ONLY the key phrases, one per line, no numbering or bullets."
22020
+ "Return ONLY the queries, one per line, no numbering or bullets."
21770
22021
  );
21771
22022
  return lines.join("\n");
21772
22023
  }
21773
- function parseKeywordResponse(raw, count) {
22024
+ function parseQueryResponse(raw, count) {
21774
22025
  const seen = /* @__PURE__ */ new Set();
21775
22026
  const results = [];
21776
22027
  for (const line of raw.split("\n")) {