@ainyc/canonry 3.6.4 → 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.
- package/README.md +10 -10
- package/assets/agent-workspace/AGENTS.md +4 -4
- package/assets/agent-workspace/USER.md +1 -1
- package/assets/agent-workspace/skills/aero/SKILL.md +4 -4
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +3 -3
- package/assets/agent-workspace/skills/aero/references/orchestration.md +6 -6
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +7 -7
- package/assets/agent-workspace/skills/aero/references/reporting.md +8 -8
- package/assets/agent-workspace/skills/aero/soul.md +1 -1
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +5 -5
- package/assets/agent-workspace/skills/canonry-setup/references/aeo-analysis.md +15 -15
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +8 -8
- package/assets/assets/index-D7T5wSBj.css +1 -0
- package/assets/assets/index-Dtgn4FDp.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-JMVBV3AT.js → chunk-BQN6BBHI.js} +707 -456
- package/dist/{chunk-RQMOJEJT.js → chunk-KCETXLDF.js} +106 -16
- package/dist/{chunk-GP2P2WPS.js → chunk-NCWCPBOT.js} +111 -49
- package/dist/{chunk-O7EVT3AF.js → chunk-O5JZQUPX.js} +71 -33
- package/dist/cli.js +472 -193
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-NL4BG3SM.js → intelligence-service-EITZP4KG.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +7 -7
- package/assets/assets/index-C9XiA1Ol.js +0 -302
- package/assets/assets/index-D3wFrrZA.css +0 -1
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
loadConfig,
|
|
6
6
|
saveConfigPatch
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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/
|
|
597
|
+
// ../api-routes/src/queries.ts
|
|
595
598
|
import crypto5 from "crypto";
|
|
596
599
|
import { eq as eq4 } from "drizzle-orm";
|
|
597
|
-
async function
|
|
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(
|
|
601
|
-
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.
|
|
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(
|
|
612
|
-
for (const
|
|
613
|
-
tx.insert(
|
|
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
|
-
|
|
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: "
|
|
624
|
-
entityType: "
|
|
625
|
-
diff: {
|
|
759
|
+
action: "queries.replaced",
|
|
760
|
+
entityType: "query",
|
|
761
|
+
diff: { queries: body.keywords }
|
|
626
762
|
});
|
|
627
763
|
});
|
|
628
|
-
const rows = app.db.select().from(
|
|
629
|
-
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.
|
|
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(
|
|
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((
|
|
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(
|
|
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: "
|
|
649
|
-
entityType: "
|
|
650
|
-
diff: { deleted: body.keywords.filter((
|
|
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(
|
|
655
|
-
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.
|
|
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(
|
|
665
|
-
const existingSet = new Set(existing.map((
|
|
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
|
|
668
|
-
if (!existingSet.has(
|
|
669
|
-
app.db.insert(
|
|
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
|
-
|
|
808
|
+
query: keyword,
|
|
673
809
|
createdAt: now
|
|
674
810
|
}).run();
|
|
675
|
-
added.push(
|
|
676
|
-
existingSet.add(
|
|
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: "
|
|
684
|
-
entityType: "
|
|
819
|
+
action: "queries.appended",
|
|
820
|
+
entityType: "query",
|
|
685
821
|
diff: { added }
|
|
686
822
|
});
|
|
687
823
|
}
|
|
688
|
-
const rows = app.db.select().from(
|
|
689
|
-
return reply.send(rows.map((r) => ({ id: r.id, keyword: r.
|
|
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.
|
|
713
|
-
throw notImplemented("
|
|
848
|
+
if (!opts.onGenerateQueries) {
|
|
849
|
+
throw notImplemented("Keyword generation is not supported in this deployment");
|
|
714
850
|
}
|
|
715
|
-
const existingRows = app.db.select().from(
|
|
716
|
-
const
|
|
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.
|
|
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
|
-
|
|
859
|
+
existingQueries
|
|
724
860
|
});
|
|
725
861
|
return reply.send({ keywords: generated, provider });
|
|
726
862
|
} catch (err) {
|
|
727
|
-
request.log.error({ err }, "
|
|
728
|
-
throw internalError(err instanceof Error ? err.message : "Failed to generate
|
|
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
|
-
|
|
1142
|
-
|
|
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(
|
|
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
|
-
|
|
1164
|
-
|
|
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(
|
|
1558
|
-
for (const
|
|
1559
|
-
tx.insert(
|
|
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
|
-
|
|
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: "
|
|
1570
|
-
entityType: "
|
|
1571
|
-
diff: {
|
|
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
|
-
|
|
1756
|
-
|
|
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(
|
|
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
|
-
|
|
1777
|
-
|
|
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
|
|
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 ||
|
|
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.
|
|
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
|
|
1954
|
+
const rawByQueryProvider = /* @__PURE__ */ new Map();
|
|
1818
1955
|
for (const snap of allSnapshots) {
|
|
1819
|
-
const key = `${snap.
|
|
1820
|
-
const arr =
|
|
1956
|
+
const key = `${snap.queryId}::${snap.provider}`;
|
|
1957
|
+
const arr = rawByQueryProvider.get(key);
|
|
1821
1958
|
if (arr) arr.push(snap);
|
|
1822
|
-
else
|
|
1959
|
+
else rawByQueryProvider.set(key, [snap]);
|
|
1823
1960
|
}
|
|
1824
|
-
const
|
|
1961
|
+
const rawByQueryModel = /* @__PURE__ */ new Map();
|
|
1825
1962
|
for (const snap of allSnapshots) {
|
|
1826
|
-
const key = `${snap.
|
|
1827
|
-
const arr =
|
|
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
|
|
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 =
|
|
1864
|
-
const
|
|
1865
|
-
const runEntries = computeTransitions(
|
|
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 = [...
|
|
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 =
|
|
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 = [...
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
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(
|
|
2038
|
+
}).from(querySnapshots).leftJoin(queries, eq9(querySnapshots.queryId, queries.id)).where(eq9(querySnapshots.runId, run1)).all();
|
|
1902
2039
|
const snaps2 = app.db.select({
|
|
1903
|
-
|
|
1904
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2067
|
+
map2.set(s.queryId, resolved);
|
|
1931
2068
|
}
|
|
1932
2069
|
}
|
|
1933
|
-
const
|
|
1934
|
-
const diff = [...
|
|
1935
|
-
const s1 = map1.get(
|
|
1936
|
-
const s2 = map2.get(
|
|
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
|
-
|
|
1939
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2000
|
-
const
|
|
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,
|
|
2148
|
+
const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, queryCreatedAt);
|
|
2012
2149
|
const trend = computeTrend(buckets, "citationRate");
|
|
2013
2150
|
const mentionTrend = computeTrend(buckets, "mentionRate");
|
|
2014
|
-
const
|
|
2015
|
-
return reply.send({ window, buckets, overall, byProvider, trend, mentionTrend,
|
|
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: [],
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2049
|
-
|
|
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(
|
|
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
|
|
2197
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
2061
2198
|
for (const s of snapshots) {
|
|
2062
|
-
const key = s.
|
|
2063
|
-
const arr =
|
|
2199
|
+
const key = s.queryId;
|
|
2200
|
+
const arr = byQuery.get(key);
|
|
2064
2201
|
if (arr) arr.push(s);
|
|
2065
|
-
else
|
|
2202
|
+
else byQuery.set(key, [s]);
|
|
2066
2203
|
}
|
|
2067
2204
|
const cited = [];
|
|
2068
2205
|
const gap = [];
|
|
2069
2206
|
const uncited = [];
|
|
2070
|
-
const
|
|
2207
|
+
const mentionedQueries = [];
|
|
2071
2208
|
const mentionGap = [];
|
|
2072
2209
|
const notMentioned = [];
|
|
2073
|
-
for (const [
|
|
2074
|
-
const
|
|
2075
|
-
const citedProviders =
|
|
2076
|
-
const mentionedProviders =
|
|
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
|
|
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(
|
|
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
|
-
|
|
2098
|
-
|
|
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
|
-
|
|
2117
|
-
|
|
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")
|
|
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.
|
|
2129
|
-
uncited.sort((a, b) => a.
|
|
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
|
-
|
|
2132
|
-
notMentioned.sort((a, b) => a.
|
|
2133
|
-
return reply.send({ cited, gap, uncited,
|
|
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: [],
|
|
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
|
-
|
|
2147
|
-
|
|
2283
|
+
queryId: querySnapshots.queryId,
|
|
2284
|
+
query: queries.query,
|
|
2148
2285
|
rawResponse: querySnapshots.rawResponse
|
|
2149
|
-
}).from(querySnapshots).leftJoin(
|
|
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
|
|
2288
|
+
const byQuery = {};
|
|
2152
2289
|
for (const snap of snapshots) {
|
|
2153
2290
|
const sources = parseGroundingSources(snap.rawResponse);
|
|
2154
|
-
const
|
|
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 (!
|
|
2161
|
-
const
|
|
2162
|
-
|
|
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.
|
|
2165
|
-
|
|
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,
|
|
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,
|
|
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 (
|
|
2366
|
+
if (queryCreatedAt) {
|
|
2230
2367
|
const eligible = inBucket.filter((s) => {
|
|
2231
|
-
const
|
|
2232
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
2390
|
+
function computeQueryChanges(projectQueries, cutoff) {
|
|
2254
2391
|
const byDay = /* @__PURE__ */ new Map();
|
|
2255
|
-
for (const
|
|
2256
|
-
if (cutoff &&
|
|
2257
|
-
const day =
|
|
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
|
-
|
|
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: "
|
|
2756
|
-
value: formatNumber(s.
|
|
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.
|
|
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.
|
|
2964
|
+
const rows = scorecard.queries.map((q, qi) => {
|
|
2828
2965
|
const cells = scorecard.providers.map((_, pi) => {
|
|
2829
|
-
const cell = scorecard.matrix[
|
|
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(
|
|
2975
|
+
return `<tr><td>${escapeHtml(q)}</td>${cells}</tr>`;
|
|
2839
2976
|
}).join("");
|
|
2840
2977
|
return `<table class="report-table">
|
|
2841
|
-
<thead><tr><th>
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
3070
|
-
<p class="section-intro">Tracked AEO
|
|
3071
|
-
<ul>${gsc.trackedButNoGsc.map((
|
|
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
|
|
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.
|
|
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>
|
|
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
|
|
3440
|
-
const candidateQueryStrings =
|
|
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
|
|
3478
|
-
const rows = db.select({ text:
|
|
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
|
|
3532
|
-
const
|
|
3533
|
-
const
|
|
3534
|
-
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray3(querySnapshots.runId, opts.recentRunIds)).all().filter((r) =>
|
|
3535
|
-
const
|
|
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 =
|
|
3674
|
+
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
3538
3675
|
list.push(row);
|
|
3539
|
-
|
|
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
|
|
3545
|
-
const snaps =
|
|
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
|
-
|
|
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
|
|
3752
|
-
const rows = db.select().from(
|
|
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.
|
|
3891
|
+
for (const row of rows) byId.set(row.id, row.query);
|
|
3755
3892
|
return { byId };
|
|
3756
3893
|
}
|
|
3757
|
-
function buildCitationScorecard(snapshots,
|
|
3894
|
+
function buildCitationScorecard(snapshots, queryLookup) {
|
|
3758
3895
|
if (snapshots.length === 0) {
|
|
3759
|
-
return {
|
|
3896
|
+
return { queries: [], providers: [], matrix: [], providerRates: [] };
|
|
3760
3897
|
}
|
|
3761
|
-
const
|
|
3898
|
+
const querySet = /* @__PURE__ */ new Set();
|
|
3762
3899
|
const providerSet = /* @__PURE__ */ new Set();
|
|
3763
3900
|
for (const snap of snapshots) {
|
|
3764
|
-
const
|
|
3765
|
-
if (!
|
|
3766
|
-
|
|
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
|
|
3906
|
+
const queryList = [...querySet].sort();
|
|
3770
3907
|
const providerList = [...providerSet].sort();
|
|
3771
|
-
const matrix =
|
|
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
|
|
3777
|
-
if (!
|
|
3778
|
-
const
|
|
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 (
|
|
3781
|
-
matrix[
|
|
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 {
|
|
3938
|
+
return { queries: queryList, providers: providerList, matrix, providerRates };
|
|
3802
3939
|
}
|
|
3803
|
-
function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains,
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
|
3828
|
-
if (
|
|
3829
|
-
entry.pages.set(gs.uri,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 =
|
|
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,
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
4483
|
+
const citationScorecard = buildCitationScorecard(latestSnapshots, queryLookup);
|
|
4347
4484
|
const competitorLandscape = buildCompetitorLandscape(
|
|
4348
4485
|
latestSnapshots,
|
|
4349
4486
|
competitorDomains,
|
|
4350
4487
|
projectDomains,
|
|
4351
|
-
|
|
4488
|
+
queryLookup
|
|
4352
4489
|
);
|
|
4353
4490
|
const mentionLandscape = buildMentionLandscape(
|
|
4354
4491
|
latestSnapshots,
|
|
4355
4492
|
competitorDomains,
|
|
4356
4493
|
project.displayName,
|
|
4357
4494
|
projectDomains,
|
|
4358
|
-
|
|
4495
|
+
queryLookup
|
|
4359
4496
|
);
|
|
4360
4497
|
const aiSourceOrigin = buildAiSourceOrigin(latestSnapshots, projectDomains, competitorDomains);
|
|
4361
|
-
const
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
4493
|
-
if (
|
|
4494
|
-
return reply.send(emptyCitationVisibility("no-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
4545
|
-
let
|
|
4546
|
-
let
|
|
4547
|
-
let
|
|
4548
|
-
const
|
|
4549
|
-
for (const
|
|
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(`${
|
|
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)
|
|
4580
|
-
else if (anyCited)
|
|
4581
|
-
else if (anyMentioned)
|
|
4582
|
-
else
|
|
4583
|
-
}
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
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
|
|
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
|
-
|
|
4608
|
-
|
|
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.
|
|
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
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
4693
|
-
|
|
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(
|
|
4838
|
+
}).from(querySnapshots).innerJoin(queries, eq15(querySnapshots.queryId, queries.id)).where(
|
|
4702
4839
|
and5(
|
|
4703
|
-
eq15(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
4784
|
-
|
|
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
|
|
4792
|
-
let
|
|
4793
|
-
for (const wasCited of
|
|
4794
|
-
if (wasCited)
|
|
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
|
|
4797
|
-
const citedRate =
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 [
|
|
4830
|
-
const previousCited = previousMap.get(
|
|
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
|
-
|
|
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,
|
|
4890
|
-
const lower =
|
|
4891
|
-
const
|
|
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,
|
|
5036
|
+
snippet = makeSnippet(answer, searchTerm);
|
|
4900
5037
|
} else if (cited.toLowerCase().includes(lower)) {
|
|
4901
5038
|
matchedField = "citedDomains";
|
|
4902
|
-
snippet = makeSnippet(cited,
|
|
5039
|
+
snippet = makeSnippet(cited, searchTerm);
|
|
4903
5040
|
} else if (raw.toLowerCase().includes(lower)) {
|
|
4904
5041
|
matchedField = "searchQueries";
|
|
4905
|
-
snippet = makeSnippet(raw,
|
|
5042
|
+
snippet = makeSnippet(raw, searchTerm);
|
|
4906
5043
|
} else {
|
|
4907
|
-
matchedField = "
|
|
4908
|
-
snippet =
|
|
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
|
-
|
|
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,
|
|
4924
|
-
const lower =
|
|
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,
|
|
4932
|
-
} else if (row.
|
|
4933
|
-
matchedField = "
|
|
4934
|
-
snippet = row.
|
|
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,
|
|
5074
|
+
snippet = makeSnippet(recommendation, searchTerm);
|
|
4938
5075
|
} else {
|
|
4939
5076
|
matchedField = "cause";
|
|
4940
|
-
snippet = makeSnippet(cause,
|
|
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
|
-
|
|
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: ["
|
|
5544
|
+
summary: "List keywords (legacy alias for queries)",
|
|
5545
|
+
tags: ["queries"],
|
|
5300
5546
|
parameters: [nameParameter],
|
|
5301
5547
|
responses: {
|
|
5302
|
-
200: { description: "
|
|
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: ["
|
|
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: "
|
|
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
|
|
5333
|
-
tags: ["
|
|
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
|
|
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: ["
|
|
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: "
|
|
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: ["
|
|
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: "
|
|
5401
|
-
501: { description: "
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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-
|
|
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,
|
|
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/
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
10545
|
-
const
|
|
10546
|
-
const
|
|
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
|
|
10549
|
-
if (!
|
|
10550
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
10851
|
+
query,
|
|
10601
10852
|
api: api ? {
|
|
10602
10853
|
provider: api.provider,
|
|
10603
10854
|
citationState: api.citationState,
|
|
10604
|
-
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:
|
|
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
|
-
|
|
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(
|
|
14495
|
-
|
|
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.
|
|
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(
|
|
15014
|
+
function buildPrompt(query, location) {
|
|
14764
15015
|
if (location) {
|
|
14765
|
-
return `${
|
|
15016
|
+
return `${query} (searching from ${location.city}, ${location.region}, ${location.country})`;
|
|
14766
15017
|
}
|
|
14767
|
-
return
|
|
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
|
-
|
|
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.
|
|
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(
|
|
15136
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15447
|
+
queries2.add(query);
|
|
15197
15448
|
}
|
|
15198
15449
|
}
|
|
15199
15450
|
}
|
|
15200
15451
|
}
|
|
15201
15452
|
} catch {
|
|
15202
15453
|
}
|
|
15203
|
-
return [...
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 [...
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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 "${
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
16938
|
+
function buildPrompt4(query, location) {
|
|
16688
16939
|
if (location) {
|
|
16689
|
-
return `${
|
|
16940
|
+
return `${query} (searching from ${location.city}, ${location.region}, ${location.country})`;
|
|
16690
16941
|
}
|
|
16691
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17624
|
+
queryCount: projectQueries.length,
|
|
17374
17625
|
...runLocation ? { location: runLocation.label } : {}
|
|
17375
17626
|
};
|
|
17376
|
-
const queriesPerProvider =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
17508
|
-
await
|
|
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
|
|
17513
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18844
|
-
|
|
19094
|
+
queryId: querySnapshots.queryId,
|
|
19095
|
+
query: queries.query,
|
|
18845
19096
|
provider: querySnapshots.provider,
|
|
18846
19097
|
citationState: querySnapshots.citationState
|
|
18847
|
-
}).from(querySnapshots).leftJoin(
|
|
19098
|
+
}).from(querySnapshots).leftJoin(queries, eq30(querySnapshots.queryId, queries.id)).where(eq30(querySnapshots.runId, currentRunId)).all();
|
|
18848
19099
|
const previousSnapshots = this.db.select({
|
|
18849
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 --
|
|
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
|
-
|
|
20606
|
+
manualQueries,
|
|
20356
20607
|
analysisProvider
|
|
20357
20608
|
});
|
|
20358
20609
|
const queryResults = await this.runSnapshotQueries({
|
|
20359
20610
|
companyName,
|
|
20360
20611
|
domain,
|
|
20361
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
20423
|
-
if (ctx.
|
|
20424
|
-
throw new Error("no
|
|
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
|
-
|
|
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.
|
|
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 --
|
|
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
|
-
|
|
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.
|
|
20467
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
20532
|
-
|
|
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.
|
|
20809
|
+
assessments: (parsed.assessments ?? []).filter((assessment) => assessment.query && assessment.provider).map((assessment) => {
|
|
20559
20810
|
const hasReviewedCompetitors = assessment.recommendedCompetitors !== void 0;
|
|
20560
20811
|
return {
|
|
20561
|
-
|
|
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,
|
|
20598
|
-
`
|
|
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.
|
|
20601
|
-
instructions.push('Manual
|
|
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:
|
|
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.
|
|
20906
|
+
assessmentMap.set(`${assessment.query}::${assessment.provider}`, assessment);
|
|
20656
20907
|
}
|
|
20657
|
-
return queryResults.map((
|
|
20658
|
-
|
|
20659
|
-
providerResults:
|
|
20660
|
-
const assessment = assessmentMap.get(`${query
|
|
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,
|
|
20684
|
-
const allResults = queryResults.flatMap((
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
21758
|
-
lines.push("", `Already tracking (do NOT duplicate): ${ctx.
|
|
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}
|
|
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
|
|
22020
|
+
"Return ONLY the queries, one per line, no numbering or bullets."
|
|
21770
22021
|
);
|
|
21771
22022
|
return lines.join("\n");
|
|
21772
22023
|
}
|
|
21773
|
-
function
|
|
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")) {
|