@ainyc/canonry 4.72.3 → 4.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +17 -0
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +56 -3
- package/assets/assets/{BacklinksPage-CjfpwZEH.js → BacklinksPage-ClgP7CUd.js} +1 -1
- package/assets/assets/{ChartPrimitives-Ckf2FrUy.js → ChartPrimitives-PGDrQBXP.js} +1 -1
- package/assets/assets/ProjectPage-xfLeh2vB.js +6 -0
- package/assets/assets/{RunRow-BuFyG0V_.js → RunRow-DL-lUm35.js} +1 -1
- package/assets/assets/{RunsPage-D-pr000K.js → RunsPage-BCL_lU-R.js} +1 -1
- package/assets/assets/{SettingsPage-CiaapCYn.js → SettingsPage-D67UQYJa.js} +1 -1
- package/assets/assets/{TrafficPage-B40xytJD.js → TrafficPage-DVRcPxCk.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-7hHem-gM.js → TrafficSourceDetailPage-JzX1fhGQ.js} +1 -1
- package/assets/assets/{extract-error-message-3GkDsu1h.js → extract-error-message-Cia_CilL.js} +1 -1
- package/assets/assets/index-CFVX11lK.css +1 -0
- package/assets/assets/{index-BVdH2O9w.js → index-DHg9_-PB.js} +118 -118
- package/assets/assets/{server-traffic-CsgPsudZ.js → server-traffic-GBmLS3L7.js} +1 -1
- package/assets/assets/{trash-2-B8Ipf9rI.js → trash-2-Bk7PYGBN.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-JXFNERK4.js → chunk-A7JX3FZB.js} +1094 -995
- package/dist/{chunk-SIB4NMEH.js → chunk-MRC4JMIH.js} +369 -176
- package/dist/{chunk-ZUBBADMR.js → chunk-W6GBIRFA.js} +162 -1
- package/dist/{chunk-HSX32G47.js → chunk-ZRZHIS22.js} +414 -73
- package/dist/cli.js +236 -30
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-ZW3ARLJT.js → intelligence-service-GPO2VMEC.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/assets/assets/ProjectPage-DZeplYeC.js +0 -6
- package/assets/assets/index-B3nENtU0.css +0 -1
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
RunTriggers,
|
|
22
22
|
SKILL_MANIFEST_FILENAME,
|
|
23
23
|
SchedulableRunKinds,
|
|
24
|
+
SiteAuditTrendDirections,
|
|
24
25
|
TrafficEventConfidences,
|
|
25
26
|
TrafficEventKinds,
|
|
26
27
|
TrafficEvidenceKinds,
|
|
@@ -186,6 +187,11 @@ import {
|
|
|
186
187
|
scheduleUpsertRequestSchema,
|
|
187
188
|
serializeRunError,
|
|
188
189
|
settingsDtoSchema,
|
|
190
|
+
siteAuditPagesResponseSchema,
|
|
191
|
+
siteAuditRunRequestSchema,
|
|
192
|
+
siteAuditRunResponseSchema,
|
|
193
|
+
siteAuditScoreSchema,
|
|
194
|
+
siteAuditTrendResponseSchema,
|
|
189
195
|
snapshotDiffResponseSchema,
|
|
190
196
|
snapshotListResponseSchema,
|
|
191
197
|
snapshotReportSchema,
|
|
@@ -223,10 +229,10 @@ import {
|
|
|
223
229
|
wordpressSchemaDeployResultDtoSchema,
|
|
224
230
|
wordpressSchemaStatusResultDtoSchema,
|
|
225
231
|
wordpressStatusDtoSchema
|
|
226
|
-
} from "./chunk-
|
|
232
|
+
} from "./chunk-A7JX3FZB.js";
|
|
227
233
|
|
|
228
234
|
// src/intelligence-service.ts
|
|
229
|
-
import { eq as
|
|
235
|
+
import { eq as eq33, desc as desc17, asc as asc4, and as and24, ne as ne5, or as or5, inArray as inArray12, gte as gte6, lte as lte3 } from "drizzle-orm";
|
|
230
236
|
|
|
231
237
|
// ../db/src/client.ts
|
|
232
238
|
import { mkdirSync } from "fs";
|
|
@@ -285,6 +291,8 @@ __export(schema_exports, {
|
|
|
285
291
|
recommendationExplanations: () => recommendationExplanations,
|
|
286
292
|
runs: () => runs,
|
|
287
293
|
schedules: () => schedules,
|
|
294
|
+
siteAuditPages: () => siteAuditPages,
|
|
295
|
+
siteAuditSnapshots: () => siteAuditSnapshots,
|
|
288
296
|
trafficSources: () => trafficSources,
|
|
289
297
|
usageCounters: () => usageCounters
|
|
290
298
|
});
|
|
@@ -533,6 +541,39 @@ var gscCoverageSnapshots = sqliteTable("gsc_coverage_snapshots", {
|
|
|
533
541
|
index("idx_gsc_coverage_snap_project_date").on(table.projectId, table.date),
|
|
534
542
|
index("idx_gsc_coverage_snap_run").on(table.syncRunId)
|
|
535
543
|
]);
|
|
544
|
+
var siteAuditSnapshots = sqliteTable("site_audit_snapshots", {
|
|
545
|
+
id: text("id").primaryKey(),
|
|
546
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
547
|
+
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
548
|
+
sitemapUrl: text("sitemap_url").notNull(),
|
|
549
|
+
auditedAt: text("audited_at").notNull(),
|
|
550
|
+
aggregateScore: integer("aggregate_score").notNull().default(0),
|
|
551
|
+
pagesDiscovered: integer("pages_discovered").notNull().default(0),
|
|
552
|
+
pagesAudited: integer("pages_audited").notNull().default(0),
|
|
553
|
+
pagesSkipped: integer("pages_skipped").notNull().default(0),
|
|
554
|
+
pagesErrored: integer("pages_errored").notNull().default(0),
|
|
555
|
+
factorAverages: text("factor_averages", { mode: "json" }).$type().notNull().default([]),
|
|
556
|
+
crossCuttingIssues: text("cross_cutting_issues", { mode: "json" }).$type().notNull().default([]),
|
|
557
|
+
prioritizedFixes: text("prioritized_fixes", { mode: "json" }).$type().notNull().default([]),
|
|
558
|
+
createdAt: text("created_at").notNull()
|
|
559
|
+
}, (table) => [
|
|
560
|
+
index("idx_site_audit_snap_project_created").on(table.projectId, table.createdAt),
|
|
561
|
+
index("idx_site_audit_snap_run").on(table.runId)
|
|
562
|
+
]);
|
|
563
|
+
var siteAuditPages = sqliteTable("site_audit_pages", {
|
|
564
|
+
id: text("id").primaryKey(),
|
|
565
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
566
|
+
runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
|
|
567
|
+
url: text("url").notNull(),
|
|
568
|
+
overallScore: integer("overall_score").notNull().default(0),
|
|
569
|
+
status: text("status").notNull(),
|
|
570
|
+
error: text("error"),
|
|
571
|
+
factors: text("factors", { mode: "json" }).$type().notNull().default([]),
|
|
572
|
+
createdAt: text("created_at").notNull()
|
|
573
|
+
}, (table) => [
|
|
574
|
+
index("idx_site_audit_pages_run").on(table.runId),
|
|
575
|
+
index("idx_site_audit_pages_project_score").on(table.projectId, table.overallScore)
|
|
576
|
+
]);
|
|
536
577
|
var bingCoverageSnapshots = sqliteTable("bing_coverage_snapshots", {
|
|
537
578
|
id: text("id").primaryKey(),
|
|
538
579
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
@@ -2780,6 +2821,47 @@ var MIGRATION_VERSIONS = [
|
|
|
2780
2821
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_recommendation_briefs_unique ON recommendation_briefs(project_id, target_ref, prompt_version)`,
|
|
2781
2822
|
`CREATE INDEX IF NOT EXISTS idx_recommendation_briefs_project ON recommendation_briefs(project_id)`
|
|
2782
2823
|
]
|
|
2824
|
+
},
|
|
2825
|
+
{
|
|
2826
|
+
// Technical AEO — site-wide audit persistence. `site_audit_snapshots` is the
|
|
2827
|
+
// per-run summary (drives the score + trend); `site_audit_pages` is the
|
|
2828
|
+
// per-page breakdown (drives the drill-down table). Both cascade off runs so
|
|
2829
|
+
// a run delete cleans up its audit data.
|
|
2830
|
+
version: 75,
|
|
2831
|
+
name: "site-audit-tables",
|
|
2832
|
+
statements: [
|
|
2833
|
+
`CREATE TABLE IF NOT EXISTS site_audit_snapshots (
|
|
2834
|
+
id TEXT PRIMARY KEY,
|
|
2835
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2836
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
2837
|
+
sitemap_url TEXT NOT NULL,
|
|
2838
|
+
audited_at TEXT NOT NULL,
|
|
2839
|
+
aggregate_score INTEGER NOT NULL DEFAULT 0,
|
|
2840
|
+
pages_discovered INTEGER NOT NULL DEFAULT 0,
|
|
2841
|
+
pages_audited INTEGER NOT NULL DEFAULT 0,
|
|
2842
|
+
pages_skipped INTEGER NOT NULL DEFAULT 0,
|
|
2843
|
+
pages_errored INTEGER NOT NULL DEFAULT 0,
|
|
2844
|
+
factor_averages TEXT NOT NULL DEFAULT '[]',
|
|
2845
|
+
cross_cutting_issues TEXT NOT NULL DEFAULT '[]',
|
|
2846
|
+
prioritized_fixes TEXT NOT NULL DEFAULT '[]',
|
|
2847
|
+
created_at TEXT NOT NULL
|
|
2848
|
+
)`,
|
|
2849
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_snap_project_created ON site_audit_snapshots(project_id, created_at)`,
|
|
2850
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_snap_run ON site_audit_snapshots(run_id)`,
|
|
2851
|
+
`CREATE TABLE IF NOT EXISTS site_audit_pages (
|
|
2852
|
+
id TEXT PRIMARY KEY,
|
|
2853
|
+
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
2854
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
2855
|
+
url TEXT NOT NULL,
|
|
2856
|
+
overall_score INTEGER NOT NULL DEFAULT 0,
|
|
2857
|
+
status TEXT NOT NULL,
|
|
2858
|
+
error TEXT,
|
|
2859
|
+
factors TEXT NOT NULL DEFAULT '[]',
|
|
2860
|
+
created_at TEXT NOT NULL
|
|
2861
|
+
)`,
|
|
2862
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_pages_run ON site_audit_pages(run_id)`,
|
|
2863
|
+
`CREATE INDEX IF NOT EXISTS idx_site_audit_pages_project_score ON site_audit_pages(project_id, overall_score)`
|
|
2864
|
+
]
|
|
2783
2865
|
}
|
|
2784
2866
|
];
|
|
2785
2867
|
function isDuplicateColumnError(err) {
|
|
@@ -4098,15 +4180,15 @@ function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains, topDo
|
|
|
4098
4180
|
totalCitations++;
|
|
4099
4181
|
}
|
|
4100
4182
|
}
|
|
4101
|
-
const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
|
|
4183
|
+
const categories = [...categoryCounts.entries()].map(([category, { label, count: count2 }]) => ({
|
|
4102
4184
|
category,
|
|
4103
4185
|
label,
|
|
4104
|
-
count,
|
|
4105
|
-
sharePct: totalCitations > 0 ? Math.round(
|
|
4186
|
+
count: count2,
|
|
4187
|
+
sharePct: totalCitations > 0 ? Math.round(count2 / totalCitations * 100) : 0
|
|
4106
4188
|
})).sort((a, b) => b.count - a.count);
|
|
4107
|
-
const topDomains = [...domainCounts.entries()].map(([domain,
|
|
4189
|
+
const topDomains = [...domainCounts.entries()].map(([domain, count2]) => ({
|
|
4108
4190
|
domain,
|
|
4109
|
-
count,
|
|
4191
|
+
count: count2,
|
|
4110
4192
|
isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
|
|
4111
4193
|
})).sort((a, b) => b.count - a.count).slice(0, topDomainsLimit);
|
|
4112
4194
|
return { categories, topDomains };
|
|
@@ -5203,7 +5285,7 @@ async function projectRoutes(app, opts) {
|
|
|
5203
5285
|
app.get("/projects/:name/delete-preview", async (request, reply) => {
|
|
5204
5286
|
const project = resolveProject(app.db, request.params.name);
|
|
5205
5287
|
const pid = project.id;
|
|
5206
|
-
const
|
|
5288
|
+
const count2 = (n) => n ?? 0;
|
|
5207
5289
|
const queryCount = app.db.select({ n: sql3`count(*)` }).from(queries).where(eq3(queries.projectId, pid)).get();
|
|
5208
5290
|
const competitorCount = app.db.select({ n: sql3`count(*)` }).from(competitors).where(eq3(competitors.projectId, pid)).get();
|
|
5209
5291
|
const runCount = app.db.select({ n: sql3`count(*)` }).from(runs).where(eq3(runs.projectId, pid)).get();
|
|
@@ -5213,14 +5295,14 @@ async function projectRoutes(app, opts) {
|
|
|
5213
5295
|
return reply.send({
|
|
5214
5296
|
project: { id: project.id, name: project.name },
|
|
5215
5297
|
cascadeRows: {
|
|
5216
|
-
queries:
|
|
5217
|
-
competitors:
|
|
5218
|
-
runs:
|
|
5219
|
-
snapshots:
|
|
5220
|
-
insights:
|
|
5298
|
+
queries: count2(queryCount?.n),
|
|
5299
|
+
competitors: count2(competitorCount?.n),
|
|
5300
|
+
runs: count2(runCount?.n),
|
|
5301
|
+
snapshots: count2(snapshotCount?.n),
|
|
5302
|
+
insights: count2(insightCount?.n)
|
|
5221
5303
|
},
|
|
5222
5304
|
detachedRows: {
|
|
5223
|
-
auditLog:
|
|
5305
|
+
auditLog: count2(auditLogCount?.n)
|
|
5224
5306
|
}
|
|
5225
5307
|
});
|
|
5226
5308
|
});
|
|
@@ -5552,14 +5634,14 @@ async function queryRoutes(app, opts) {
|
|
|
5552
5634
|
validProviders: validNames
|
|
5553
5635
|
});
|
|
5554
5636
|
}
|
|
5555
|
-
const
|
|
5637
|
+
const count2 = body.count ?? 5;
|
|
5556
5638
|
if (!opts.onGenerateQueries) {
|
|
5557
5639
|
throw notImplemented("Query generation is not supported in this deployment");
|
|
5558
5640
|
}
|
|
5559
5641
|
const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
|
|
5560
5642
|
const existingQueries = existingRows.map((r) => r.query);
|
|
5561
5643
|
try {
|
|
5562
|
-
const generated = await opts.onGenerateQueries(provider,
|
|
5644
|
+
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
5563
5645
|
domain: project.canonicalDomain,
|
|
5564
5646
|
displayName: project.displayName,
|
|
5565
5647
|
country: project.country,
|
|
@@ -5689,14 +5771,14 @@ async function queryRoutes(app, opts) {
|
|
|
5689
5771
|
validProviders: validNames
|
|
5690
5772
|
});
|
|
5691
5773
|
}
|
|
5692
|
-
const
|
|
5774
|
+
const count2 = body.count ?? 5;
|
|
5693
5775
|
if (!opts.onGenerateQueries) {
|
|
5694
5776
|
throw notImplemented("Keyword generation is not supported in this deployment");
|
|
5695
5777
|
}
|
|
5696
5778
|
const existingRows = app.db.select().from(queries).where(eq4(queries.projectId, project.id)).all();
|
|
5697
5779
|
const existingQueries = existingRows.map((r) => r.query);
|
|
5698
5780
|
try {
|
|
5699
|
-
const generated = await opts.onGenerateQueries(provider,
|
|
5781
|
+
const generated = await opts.onGenerateQueries(provider, count2, {
|
|
5700
5782
|
domain: project.canonicalDomain,
|
|
5701
5783
|
displayName: project.displayName,
|
|
5702
5784
|
country: project.country,
|
|
@@ -7438,10 +7520,10 @@ function computeQueryChanges(projectQueries, cutoff) {
|
|
|
7438
7520
|
}
|
|
7439
7521
|
const days = [...byDay.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
7440
7522
|
if (days.length <= 1) return [];
|
|
7441
|
-
return days.slice(1).map(([date,
|
|
7523
|
+
return days.slice(1).map(([date, count2]) => ({
|
|
7442
7524
|
date: (/* @__PURE__ */ new Date(date + "T00:00:00.000Z")).toISOString(),
|
|
7443
|
-
delta:
|
|
7444
|
-
label: `+${
|
|
7525
|
+
delta: count2,
|
|
7526
|
+
label: `+${count2} kp`
|
|
7445
7527
|
}));
|
|
7446
7528
|
}
|
|
7447
7529
|
function computeTrend(buckets, rateKey) {
|
|
@@ -7506,15 +7588,15 @@ function buildRankedList(domains, limit) {
|
|
|
7506
7588
|
function buildCategoryCounts(counts) {
|
|
7507
7589
|
let grandTotal = 0;
|
|
7508
7590
|
for (const domains of counts.values()) {
|
|
7509
|
-
for (const
|
|
7591
|
+
for (const count2 of domains.values()) grandTotal += count2;
|
|
7510
7592
|
}
|
|
7511
7593
|
const result = [];
|
|
7512
7594
|
for (const [category, domains] of counts) {
|
|
7513
7595
|
let categoryTotal = 0;
|
|
7514
7596
|
const domainEntries = [];
|
|
7515
|
-
for (const [domain,
|
|
7516
|
-
categoryTotal +=
|
|
7517
|
-
domainEntries.push({ domain, count });
|
|
7597
|
+
for (const [domain, count2] of domains) {
|
|
7598
|
+
categoryTotal += count2;
|
|
7599
|
+
domainEntries.push({ domain, count: count2 });
|
|
7518
7600
|
}
|
|
7519
7601
|
domainEntries.sort((a, b) => b.count - a.count);
|
|
7520
7602
|
result.push({
|
|
@@ -8488,8 +8570,8 @@ function gscDateRange(report) {
|
|
|
8488
8570
|
const end = summary?.periodEnd || gsc?.periodEnd || gsc?.trend.at(-1)?.date || "";
|
|
8489
8571
|
return formatDateRange(start, end);
|
|
8490
8572
|
}
|
|
8491
|
-
function pluralize(
|
|
8492
|
-
return
|
|
8573
|
+
function pluralize(count2, singular, plural = `${singular}s`) {
|
|
8574
|
+
return count2 === 1 ? singular : plural;
|
|
8493
8575
|
}
|
|
8494
8576
|
var PROVIDER_DISPLAY_NAMES = {
|
|
8495
8577
|
gemini: "Gemini",
|
|
@@ -10292,9 +10374,9 @@ function renderInsights(report) {
|
|
|
10292
10374
|
);
|
|
10293
10375
|
}
|
|
10294
10376
|
const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
|
|
10295
|
-
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count }) => {
|
|
10377
|
+
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count: count2 }) => {
|
|
10296
10378
|
const tone = severityTone(i.severity);
|
|
10297
|
-
const countChip =
|
|
10379
|
+
const countChip = count2 > 1 ? ` <span class="badge tone-neutral">\xD7 ${count2}</span>` : "";
|
|
10298
10380
|
return `<tr>
|
|
10299
10381
|
<td class="col-severity"><span class="badge tone-${tone}">${escapeHtml(reportSeverityLabel(i.severity))}</span></td>
|
|
10300
10382
|
<td class="col-title">${escapeHtml(i.title)}${countChip}</td>
|
|
@@ -11494,9 +11576,9 @@ function contentActionVerb(action) {
|
|
|
11494
11576
|
return "Add schema to";
|
|
11495
11577
|
}
|
|
11496
11578
|
}
|
|
11497
|
-
function confidenceFromEvidence(
|
|
11498
|
-
if (
|
|
11499
|
-
if (
|
|
11579
|
+
function confidenceFromEvidence(count2) {
|
|
11580
|
+
if (count2 >= 3) return "high";
|
|
11581
|
+
if (count2 >= 1) return "medium";
|
|
11500
11582
|
return "low";
|
|
11501
11583
|
}
|
|
11502
11584
|
function actionAudienceMatches2(action, audience) {
|
|
@@ -13003,6 +13085,10 @@ var SCHEMA_TABLE = {
|
|
|
13003
13085
|
SchedulableRunKind: schedulableRunKindSchema,
|
|
13004
13086
|
ScheduleDto: scheduleDtoSchema,
|
|
13005
13087
|
SettingsDto: settingsDtoSchema,
|
|
13088
|
+
SiteAuditPagesResponseDto: siteAuditPagesResponseSchema,
|
|
13089
|
+
SiteAuditRunResponseDto: siteAuditRunResponseSchema,
|
|
13090
|
+
SiteAuditScoreDto: siteAuditScoreSchema,
|
|
13091
|
+
SiteAuditTrendResponseDto: siteAuditTrendResponseSchema,
|
|
13006
13092
|
SnapshotDiffResponse: snapshotDiffResponseSchema,
|
|
13007
13093
|
SnapshotListResponse: snapshotListResponseSchema,
|
|
13008
13094
|
SnapshotReportDto: snapshotReportSchema,
|
|
@@ -16688,6 +16774,78 @@ var routeCatalog = [
|
|
|
16688
16774
|
400: errorResponse("Session is not completed, or invalid request body."),
|
|
16689
16775
|
404: errorResponse("Project or session not found.")
|
|
16690
16776
|
}
|
|
16777
|
+
},
|
|
16778
|
+
{
|
|
16779
|
+
method: "get",
|
|
16780
|
+
path: "/api/v1/projects/{name}/technical-aeo",
|
|
16781
|
+
summary: "Get the Technical AEO scorecard for a project",
|
|
16782
|
+
description: "Returns the latest completed/partial site-audit: aggregate 0\u2013100 score, page counts, the full per-factor scorecard (site-level averages with pass/partial/fail distribution), cross-cutting issues, prioritized fixes, and the delta vs the previous audit. When the project has never been audited, `hasData` is false and the numeric fields are zeroed \u2014 render an onboarding state.",
|
|
16783
|
+
tags: ["technical-aeo"],
|
|
16784
|
+
parameters: [nameParameter],
|
|
16785
|
+
responses: {
|
|
16786
|
+
200: jsonResponse("Technical AEO scorecard returned.", "SiteAuditScoreDto"),
|
|
16787
|
+
404: errorResponse("Project not found.")
|
|
16788
|
+
}
|
|
16789
|
+
},
|
|
16790
|
+
{
|
|
16791
|
+
method: "get",
|
|
16792
|
+
path: "/api/v1/projects/{name}/technical-aeo/pages",
|
|
16793
|
+
summary: "List audited pages from the latest site-audit run",
|
|
16794
|
+
description: "Returns the per-page breakdown of the latest completed/partial site-audit run (paginated). Filter to `status=error` to surface unreachable pages; sort `score-asc` (default) to surface the worst-scoring pages first.",
|
|
16795
|
+
tags: ["technical-aeo"],
|
|
16796
|
+
parameters: [
|
|
16797
|
+
nameParameter,
|
|
16798
|
+
{ name: "status", in: "query", description: "Filter by page audit status: `success` or `error`.", schema: { type: "string", enum: ["success", "error"] } },
|
|
16799
|
+
{ name: "sort", in: "query", description: "Sort order: `score-asc` (default), `score-desc`, or `url`.", schema: { type: "string", enum: ["score-asc", "score-desc", "url"] } },
|
|
16800
|
+
limitQueryParameter,
|
|
16801
|
+
offsetQueryParameter
|
|
16802
|
+
],
|
|
16803
|
+
responses: {
|
|
16804
|
+
200: jsonResponse("Audited pages returned.", "SiteAuditPagesResponseDto"),
|
|
16805
|
+
404: errorResponse("Project not found.")
|
|
16806
|
+
}
|
|
16807
|
+
},
|
|
16808
|
+
{
|
|
16809
|
+
method: "get",
|
|
16810
|
+
path: "/api/v1/projects/{name}/technical-aeo/trend",
|
|
16811
|
+
summary: "Get the Technical AEO aggregate-score trend",
|
|
16812
|
+
description: "Returns historical aggregate scores across completed/partial site-audit runs, oldest-first, for the trend chart.",
|
|
16813
|
+
tags: ["technical-aeo"],
|
|
16814
|
+
parameters: [
|
|
16815
|
+
nameParameter,
|
|
16816
|
+
{ name: "limit", in: "query", description: "Max data points returned (most recent runs). Default 30.", schema: integerSchema }
|
|
16817
|
+
],
|
|
16818
|
+
responses: {
|
|
16819
|
+
200: jsonResponse("Technical AEO trend returned.", "SiteAuditTrendResponseDto"),
|
|
16820
|
+
404: errorResponse("Project not found.")
|
|
16821
|
+
}
|
|
16822
|
+
},
|
|
16823
|
+
{
|
|
16824
|
+
method: "post",
|
|
16825
|
+
path: "/api/v1/projects/{name}/technical-aeo/runs",
|
|
16826
|
+
summary: "Trigger a Technical AEO site-audit run",
|
|
16827
|
+
description: "Queues a `site-audit` run that crawls the project sitemap and audits every reachable page. Idempotent: if a site-audit run is already queued/running for the project, returns that run instead of starting a second.",
|
|
16828
|
+
tags: ["technical-aeo"],
|
|
16829
|
+
parameters: [nameParameter],
|
|
16830
|
+
requestBody: {
|
|
16831
|
+
required: false,
|
|
16832
|
+
content: {
|
|
16833
|
+
"application/json": {
|
|
16834
|
+
schema: {
|
|
16835
|
+
type: "object",
|
|
16836
|
+
properties: {
|
|
16837
|
+
sitemapUrl: { ...stringSchema, description: "Override the sitemap URL. Defaults to https://<canonicalDomain>/sitemap.xml." },
|
|
16838
|
+
limit: { ...integerSchema, description: "Cap pages audited (highest sitemap <priority> first). Max 2000." }
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
}
|
|
16842
|
+
}
|
|
16843
|
+
},
|
|
16844
|
+
responses: {
|
|
16845
|
+
200: jsonResponse("Site-audit run queued (or the in-flight run returned).", "SiteAuditRunResponseDto"),
|
|
16846
|
+
400: errorResponse("Invalid site-audit request."),
|
|
16847
|
+
404: errorResponse("Project not found.")
|
|
16848
|
+
}
|
|
16691
16849
|
}
|
|
16692
16850
|
];
|
|
16693
16851
|
var canonryLocalRouteCatalog = [
|
|
@@ -17201,6 +17359,9 @@ async function scheduleRoutes(app, opts) {
|
|
|
17201
17359
|
if (kind === SchedulableRunKinds["backlinks-sync"] && providers && providers.length > 0) {
|
|
17202
17360
|
throw validationError('"providers" is not valid for kind "backlinks-sync"');
|
|
17203
17361
|
}
|
|
17362
|
+
if (kind === SchedulableRunKinds["site-audit"] && providers && providers.length > 0) {
|
|
17363
|
+
throw validationError('"providers" is not valid for kind "site-audit"');
|
|
17364
|
+
}
|
|
17204
17365
|
const validNames = opts.validProviderNames ?? [];
|
|
17205
17366
|
if (validNames.length && providers?.length) {
|
|
17206
17367
|
const invalid = providers.filter((p) => !validNames.includes(p));
|
|
@@ -18898,12 +19059,12 @@ async function getLodging(accessToken, locationName, opts = {}) {
|
|
|
18898
19059
|
}
|
|
18899
19060
|
}
|
|
18900
19061
|
function countPopulatedGroups(lodging) {
|
|
18901
|
-
let
|
|
19062
|
+
let count2 = 0;
|
|
18902
19063
|
for (const [key, value] of Object.entries(lodging)) {
|
|
18903
19064
|
if (key === "name" || key === "metadata") continue;
|
|
18904
|
-
if (isPopulated(value))
|
|
19065
|
+
if (isPopulated(value)) count2++;
|
|
18905
19066
|
}
|
|
18906
|
-
return
|
|
19067
|
+
return count2;
|
|
18907
19068
|
}
|
|
18908
19069
|
function isPopulated(value) {
|
|
18909
19070
|
if (value === null || value === void 0) return false;
|
|
@@ -30949,11 +31110,41 @@ var scopesCheck3 = {
|
|
|
30949
31110
|
return summarizePerSourceResults("scopes", "scopes", results);
|
|
30950
31111
|
}
|
|
30951
31112
|
};
|
|
31113
|
+
var cacheBlindSpotCheck = {
|
|
31114
|
+
id: "traffic.source.cache-blindspot",
|
|
31115
|
+
category: CheckCategories.integrations,
|
|
31116
|
+
scope: CheckScopes.project,
|
|
31117
|
+
title: "WordPress traffic cache blind spot",
|
|
31118
|
+
run: (ctx) => {
|
|
31119
|
+
if (!ctx.project) return skippedNoProject4();
|
|
31120
|
+
const wpSources = loadProbes(ctx).filter(
|
|
31121
|
+
(s) => s.sourceType === TrafficSourceTypes.wordpress
|
|
31122
|
+
);
|
|
31123
|
+
if (wpSources.length === 0) {
|
|
31124
|
+
return {
|
|
31125
|
+
status: CheckStatuses.skipped,
|
|
31126
|
+
code: "traffic.cache-blindspot.no-wordpress-source",
|
|
31127
|
+
summary: "No WordPress traffic source connected, so the plugin cache blind spot does not apply (log and edge adapters see cache-served requests)."
|
|
31128
|
+
};
|
|
31129
|
+
}
|
|
31130
|
+
return {
|
|
31131
|
+
status: CheckStatuses.warn,
|
|
31132
|
+
code: "traffic.cache-blindspot.wordpress-plugin",
|
|
31133
|
+
summary: `${wpSources.length} WordPress traffic source(s) capture via the Canonry Traffic Logger plugin, which only logs requests that execute PHP. A full-page cache (LiteSpeed, WP Rocket, W3 Total Cache, WP Super Cache) or CDN serves cached pages before PHP runs, so cache-served page views, including live AI user-fetches such as Claude-User and ChatGPT-User, are not captured. Bot crawls of uncached endpoints (sitemap, feeds, assets, cache misses) still appear, which can make capture look healthy while real page views go uncounted.`,
|
|
31134
|
+
remediation: 'Exclude AI user-agents from the page cache so their requests reach PHP: LiteSpeed Cache has "Do Not Cache User Agents" under Cache > Excludes; WP Rocket uses the `rocket_cache_reject_ua` filter; W3 Total Cache and WP Super Cache have a "Rejected User Agents" box. Mirror the rule at any CDN in front. For cache-independent capture, ingest from server or edge access logs (a `cloud-run` or `vercel` source, or an edge worker) instead of the WordPress plugin.',
|
|
31135
|
+
details: {
|
|
31136
|
+
wordpressSourceCount: wpSources.length,
|
|
31137
|
+
wordpressSourceIds: wpSources.map((s) => s.id)
|
|
31138
|
+
}
|
|
31139
|
+
};
|
|
31140
|
+
}
|
|
31141
|
+
};
|
|
30952
31142
|
var TRAFFIC_SOURCE_CHECKS = [
|
|
30953
31143
|
sourceConnectedCheck,
|
|
30954
31144
|
recentDataCheck,
|
|
30955
31145
|
credentialsCheck,
|
|
30956
|
-
scopesCheck3
|
|
31146
|
+
scopesCheck3,
|
|
31147
|
+
cacheBlindSpotCheck
|
|
30957
31148
|
];
|
|
30958
31149
|
|
|
30959
31150
|
// ../api-routes/src/doctor/checks/wordpress-publish.ts
|
|
@@ -31645,6 +31836,151 @@ function dedupeStrings(input) {
|
|
|
31645
31836
|
return out;
|
|
31646
31837
|
}
|
|
31647
31838
|
|
|
31839
|
+
// ../api-routes/src/technical-aeo.ts
|
|
31840
|
+
import crypto27 from "crypto";
|
|
31841
|
+
import { and as and23, asc as asc3, count, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
|
|
31842
|
+
var SURFACEABLE_STATUSES = [RunStatuses.completed, RunStatuses.partial];
|
|
31843
|
+
function emptyScore(projectName) {
|
|
31844
|
+
return {
|
|
31845
|
+
project: projectName,
|
|
31846
|
+
hasData: false,
|
|
31847
|
+
runId: null,
|
|
31848
|
+
runStatus: null,
|
|
31849
|
+
sitemapUrl: null,
|
|
31850
|
+
auditedAt: null,
|
|
31851
|
+
aggregateScore: 0,
|
|
31852
|
+
pagesDiscovered: 0,
|
|
31853
|
+
pagesAudited: 0,
|
|
31854
|
+
pagesSkipped: 0,
|
|
31855
|
+
pagesErrored: 0,
|
|
31856
|
+
deltaScore: null,
|
|
31857
|
+
trend: null,
|
|
31858
|
+
previousScore: null,
|
|
31859
|
+
previousAuditedAt: null,
|
|
31860
|
+
factors: [],
|
|
31861
|
+
crossCuttingIssues: [],
|
|
31862
|
+
prioritizedFixes: []
|
|
31863
|
+
};
|
|
31864
|
+
}
|
|
31865
|
+
function parsePositiveInt(value, fallback, max) {
|
|
31866
|
+
const n = typeof value === "string" ? Number.parseInt(value, 10) : typeof value === "number" ? value : NaN;
|
|
31867
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
31868
|
+
return Math.min(max, Math.floor(n));
|
|
31869
|
+
}
|
|
31870
|
+
async function technicalAeoRoutes(app, opts) {
|
|
31871
|
+
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
31872
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31873
|
+
const rows = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31874
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31875
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31876
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31877
|
+
notProbeRun()
|
|
31878
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(2).all();
|
|
31879
|
+
const latest = rows[0];
|
|
31880
|
+
if (!latest) return emptyScore(project.name);
|
|
31881
|
+
const snap = latest.snap;
|
|
31882
|
+
const previous = rows[1]?.snap ?? null;
|
|
31883
|
+
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
31884
|
+
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
31885
|
+
return {
|
|
31886
|
+
project: project.name,
|
|
31887
|
+
hasData: true,
|
|
31888
|
+
runId: snap.runId,
|
|
31889
|
+
runStatus: latest.runStatus,
|
|
31890
|
+
sitemapUrl: snap.sitemapUrl,
|
|
31891
|
+
auditedAt: snap.auditedAt,
|
|
31892
|
+
aggregateScore: snap.aggregateScore,
|
|
31893
|
+
pagesDiscovered: snap.pagesDiscovered,
|
|
31894
|
+
pagesAudited: snap.pagesAudited,
|
|
31895
|
+
pagesSkipped: snap.pagesSkipped,
|
|
31896
|
+
pagesErrored: snap.pagesErrored,
|
|
31897
|
+
deltaScore,
|
|
31898
|
+
trend,
|
|
31899
|
+
previousScore: previous?.aggregateScore ?? null,
|
|
31900
|
+
previousAuditedAt: previous?.auditedAt ?? null,
|
|
31901
|
+
factors: snap.factorAverages,
|
|
31902
|
+
crossCuttingIssues: snap.crossCuttingIssues,
|
|
31903
|
+
prioritizedFixes: snap.prioritizedFixes
|
|
31904
|
+
};
|
|
31905
|
+
});
|
|
31906
|
+
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
31907
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31908
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31909
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31910
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31911
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31912
|
+
notProbeRun()
|
|
31913
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
31914
|
+
if (!latest) {
|
|
31915
|
+
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
31916
|
+
}
|
|
31917
|
+
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
31918
|
+
const conds = [eq32(siteAuditPages.runId, latest.runId)];
|
|
31919
|
+
if (statusFilter) conds.push(eq32(siteAuditPages.status, statusFilter));
|
|
31920
|
+
const where = and23(...conds);
|
|
31921
|
+
const totalRow = app.db.select({ value: count() }).from(siteAuditPages).where(where).get();
|
|
31922
|
+
const total = totalRow?.value ?? 0;
|
|
31923
|
+
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
31924
|
+
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
31925
|
+
const orderBy = request.query.sort === "score-desc" ? desc16(siteAuditPages.overallScore) : request.query.sort === "url" ? asc3(siteAuditPages.url) : asc3(siteAuditPages.overallScore);
|
|
31926
|
+
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
31927
|
+
const pages = rows.map((row) => ({
|
|
31928
|
+
url: row.url,
|
|
31929
|
+
overallScore: row.overallScore,
|
|
31930
|
+
status: row.status === "error" ? "error" : "success",
|
|
31931
|
+
error: row.error,
|
|
31932
|
+
factors: row.factors
|
|
31933
|
+
}));
|
|
31934
|
+
return { project: project.name, runId: latest.runId, auditedAt: latest.auditedAt, total, pages };
|
|
31935
|
+
});
|
|
31936
|
+
app.get("/projects/:name/technical-aeo/trend", async (request) => {
|
|
31937
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31938
|
+
const limit = parsePositiveInt(request.query.limit, 30, 365);
|
|
31939
|
+
const rows = app.db.select({
|
|
31940
|
+
runId: siteAuditSnapshots.runId,
|
|
31941
|
+
auditedAt: siteAuditSnapshots.auditedAt,
|
|
31942
|
+
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
31943
|
+
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
31944
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq32(siteAuditSnapshots.runId, runs.id)).where(and23(
|
|
31945
|
+
eq32(siteAuditSnapshots.projectId, project.id),
|
|
31946
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31947
|
+
inArray11(runs.status, SURFACEABLE_STATUSES),
|
|
31948
|
+
notProbeRun()
|
|
31949
|
+
)).orderBy(desc16(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
31950
|
+
return { project: project.name, points: rows.reverse() };
|
|
31951
|
+
});
|
|
31952
|
+
app.post("/projects/:name/technical-aeo/runs", async (request) => {
|
|
31953
|
+
const project = resolveProject(app.db, request.params.name);
|
|
31954
|
+
const parsed = siteAuditRunRequestSchema.safeParse(request.body ?? {});
|
|
31955
|
+
if (!parsed.success) {
|
|
31956
|
+
throw validationError(parsed.error.issues[0]?.message ?? "Invalid site-audit request");
|
|
31957
|
+
}
|
|
31958
|
+
const existing = app.db.select({ id: runs.id, status: runs.status }).from(runs).where(and23(
|
|
31959
|
+
eq32(runs.projectId, project.id),
|
|
31960
|
+
eq32(runs.kind, RunKinds["site-audit"]),
|
|
31961
|
+
inArray11(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
31962
|
+
)).get();
|
|
31963
|
+
if (existing) {
|
|
31964
|
+
return { runId: existing.id, status: existing.status };
|
|
31965
|
+
}
|
|
31966
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
31967
|
+
const runId = crypto27.randomUUID();
|
|
31968
|
+
app.db.insert(runs).values({
|
|
31969
|
+
id: runId,
|
|
31970
|
+
projectId: project.id,
|
|
31971
|
+
kind: RunKinds["site-audit"],
|
|
31972
|
+
status: RunStatuses.queued,
|
|
31973
|
+
trigger: RunTriggers.manual,
|
|
31974
|
+
createdAt: now
|
|
31975
|
+
}).run();
|
|
31976
|
+
opts.onSiteAuditRequested?.(runId, project.id, {
|
|
31977
|
+
sitemapUrl: parsed.data.sitemapUrl,
|
|
31978
|
+
limit: parsed.data.limit
|
|
31979
|
+
});
|
|
31980
|
+
return { runId, status: RunStatuses.queued };
|
|
31981
|
+
});
|
|
31982
|
+
}
|
|
31983
|
+
|
|
31648
31984
|
// ../api-routes/src/index.ts
|
|
31649
31985
|
async function apiRoutes(app, opts) {
|
|
31650
31986
|
app.decorate("db", opts.db);
|
|
@@ -31809,6 +32145,9 @@ async function apiRoutes(app, opts) {
|
|
|
31809
32145
|
await api.register(discoveryRoutes, {
|
|
31810
32146
|
onDiscoveryRunRequested: opts.onDiscoveryRunRequested
|
|
31811
32147
|
});
|
|
32148
|
+
await api.register(technicalAeoRoutes, {
|
|
32149
|
+
onSiteAuditRequested: opts.onSiteAuditRequested
|
|
32150
|
+
});
|
|
31812
32151
|
await api.register(doctorRoutes, {
|
|
31813
32152
|
googleConnectionStore: opts.googleConnectionStore,
|
|
31814
32153
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -31964,7 +32303,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
31964
32303
|
}
|
|
31965
32304
|
|
|
31966
32305
|
// src/intelligence-service.ts
|
|
31967
|
-
import
|
|
32306
|
+
import crypto28 from "crypto";
|
|
31968
32307
|
|
|
31969
32308
|
// src/logger.ts
|
|
31970
32309
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32195,16 +32534,16 @@ var IntelligenceService = class {
|
|
|
32195
32534
|
*/
|
|
32196
32535
|
analyzeAndPersist(runId, projectId) {
|
|
32197
32536
|
const recentRuns = this.db.select().from(runs).where(
|
|
32198
|
-
|
|
32199
|
-
|
|
32200
|
-
or5(
|
|
32537
|
+
and24(
|
|
32538
|
+
eq33(runs.projectId, projectId),
|
|
32539
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32201
32540
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32202
32541
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32203
32542
|
// directly for a probe, probes still must not pollute the
|
|
32204
32543
|
// intelligence window.
|
|
32205
32544
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32206
32545
|
)
|
|
32207
|
-
).orderBy(
|
|
32546
|
+
).orderBy(desc17(runs.finishedAt), desc17(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
32208
32547
|
if (recentRuns.length === 0) {
|
|
32209
32548
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
32210
32549
|
return null;
|
|
@@ -32279,7 +32618,7 @@ var IntelligenceService = class {
|
|
|
32279
32618
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32280
32619
|
*/
|
|
32281
32620
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32282
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
32621
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq33(runs.id, runId)).get();
|
|
32283
32622
|
if (!runRow) {
|
|
32284
32623
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32285
32624
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32287,9 +32626,9 @@ var IntelligenceService = class {
|
|
|
32287
32626
|
}
|
|
32288
32627
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32289
32628
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32290
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32291
|
-
|
|
32292
|
-
|
|
32629
|
+
const selected = this.db.select().from(gbpLocations).where(and24(
|
|
32630
|
+
eq33(gbpLocations.projectId, projectId),
|
|
32631
|
+
eq33(gbpLocations.selected, true),
|
|
32293
32632
|
gte6(gbpLocations.syncedAt, windowStart),
|
|
32294
32633
|
lte3(gbpLocations.syncedAt, windowEnd)
|
|
32295
32634
|
)).all();
|
|
@@ -32324,10 +32663,10 @@ var IntelligenceService = class {
|
|
|
32324
32663
|
}
|
|
32325
32664
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32326
32665
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
32327
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
32328
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
32329
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
32330
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
32666
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and24(eq33(gbpDailyMetrics.projectId, projectId), eq33(gbpDailyMetrics.locationName, locationName))).all();
|
|
32667
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and24(eq33(gbpPlaceActions.projectId, projectId), eq33(gbpPlaceActions.locationName, locationName))).all();
|
|
32668
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and24(eq33(gbpLodgingSnapshots.projectId, projectId), eq33(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc17(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
32669
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and24(eq33(gbpPlaceDetails.projectId, projectId), eq33(gbpPlaceDetails.locationName, locationName))).orderBy(desc17(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
32331
32670
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32332
32671
|
const summary = buildGbpSummary({
|
|
32333
32672
|
locationName,
|
|
@@ -32359,7 +32698,7 @@ var IntelligenceService = class {
|
|
|
32359
32698
|
/** Build the month-over-month keyword series for a location from the
|
|
32360
32699
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32361
32700
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32362
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
32701
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and24(eq33(gbpKeywordMonthly.projectId, projectId), eq33(gbpKeywordMonthly.locationName, locationName))).all();
|
|
32363
32702
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32364
32703
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32365
32704
|
const recentMonth = months[0] ?? null;
|
|
@@ -32390,7 +32729,7 @@ var IntelligenceService = class {
|
|
|
32390
32729
|
*/
|
|
32391
32730
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32392
32731
|
const covered = new Set(coveredLocationNames);
|
|
32393
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
32732
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and24(eq33(insights.projectId, projectId), eq33(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
32394
32733
|
const staleIds = [];
|
|
32395
32734
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32396
32735
|
for (const row of existing) {
|
|
@@ -32401,7 +32740,7 @@ var IntelligenceService = class {
|
|
|
32401
32740
|
}
|
|
32402
32741
|
this.db.transaction((tx) => {
|
|
32403
32742
|
for (const id of staleIds) {
|
|
32404
|
-
tx.delete(insights).where(
|
|
32743
|
+
tx.delete(insights).where(eq33(insights.id, id)).run();
|
|
32405
32744
|
}
|
|
32406
32745
|
for (const insight of gbpInsights) {
|
|
32407
32746
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32479,7 +32818,7 @@ var IntelligenceService = class {
|
|
|
32479
32818
|
* create per run + aggregate). DB is left untouched.
|
|
32480
32819
|
*/
|
|
32481
32820
|
backfill(projectName, opts, onProgress) {
|
|
32482
|
-
const project = this.db.select().from(projects).where(
|
|
32821
|
+
const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
|
|
32483
32822
|
if (!project) {
|
|
32484
32823
|
throw new Error(`Project "${projectName}" not found`);
|
|
32485
32824
|
}
|
|
@@ -32492,13 +32831,13 @@ var IntelligenceService = class {
|
|
|
32492
32831
|
sinceTimestamp = parsed;
|
|
32493
32832
|
}
|
|
32494
32833
|
const allRuns = this.db.select().from(runs).where(
|
|
32495
|
-
|
|
32496
|
-
|
|
32497
|
-
or5(
|
|
32834
|
+
and24(
|
|
32835
|
+
eq33(runs.projectId, project.id),
|
|
32836
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32498
32837
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32499
32838
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32500
32839
|
)
|
|
32501
|
-
).orderBy(
|
|
32840
|
+
).orderBy(asc4(runs.finishedAt)).all();
|
|
32502
32841
|
let startIdx = 0;
|
|
32503
32842
|
let endIdx = allRuns.length;
|
|
32504
32843
|
if (opts?.fromRunId) {
|
|
@@ -32527,7 +32866,7 @@ var IntelligenceService = class {
|
|
|
32527
32866
|
let wouldDeleteTotal = 0;
|
|
32528
32867
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
32529
32868
|
if (isDryRun && targetRuns.length > 0) {
|
|
32530
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
32869
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray12(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
32531
32870
|
for (const r of rows) {
|
|
32532
32871
|
if (r.runId == null) continue;
|
|
32533
32872
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -32573,7 +32912,7 @@ var IntelligenceService = class {
|
|
|
32573
32912
|
return { processed, skipped, totalInsights };
|
|
32574
32913
|
}
|
|
32575
32914
|
loadTrackedCompetitors(projectId) {
|
|
32576
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
32915
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq33(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
32577
32916
|
}
|
|
32578
32917
|
/**
|
|
32579
32918
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -32594,15 +32933,15 @@ var IntelligenceService = class {
|
|
|
32594
32933
|
}
|
|
32595
32934
|
persistResult(result, runId, projectId) {
|
|
32596
32935
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
32597
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
32936
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq33(insights.runId, runId)).all();
|
|
32598
32937
|
for (const row of existingInsights) {
|
|
32599
32938
|
if (row.dismissed) {
|
|
32600
32939
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
32601
32940
|
}
|
|
32602
32941
|
}
|
|
32603
32942
|
this.db.transaction((tx) => {
|
|
32604
|
-
tx.delete(insights).where(
|
|
32605
|
-
tx.delete(healthSnapshots).where(
|
|
32943
|
+
tx.delete(insights).where(eq33(insights.runId, runId)).run();
|
|
32944
|
+
tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
|
|
32606
32945
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32607
32946
|
for (const insight of result.insights) {
|
|
32608
32947
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -32622,7 +32961,7 @@ var IntelligenceService = class {
|
|
|
32622
32961
|
}).run();
|
|
32623
32962
|
}
|
|
32624
32963
|
tx.insert(healthSnapshots).values({
|
|
32625
|
-
id:
|
|
32964
|
+
id: crypto28.randomUUID(),
|
|
32626
32965
|
projectId,
|
|
32627
32966
|
runId,
|
|
32628
32967
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -32653,28 +32992,28 @@ var IntelligenceService = class {
|
|
|
32653
32992
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
32654
32993
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
32655
32994
|
if (regressions.length === 0) return rawInsights;
|
|
32656
|
-
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(
|
|
32995
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq33(gscSearchData.projectId, projectId)).all();
|
|
32657
32996
|
const gscConnected = gscRows.length > 0;
|
|
32658
32997
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
32659
32998
|
for (const row of gscRows) {
|
|
32660
32999
|
const key = row.query.toLowerCase();
|
|
32661
33000
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
32662
33001
|
}
|
|
32663
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
33002
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
32664
33003
|
const locationCount = Math.max(
|
|
32665
33004
|
1,
|
|
32666
33005
|
(projectRow?.locations ?? []).length
|
|
32667
33006
|
);
|
|
32668
33007
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
32669
33008
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
32670
|
-
|
|
32671
|
-
|
|
32672
|
-
|
|
32673
|
-
or5(
|
|
33009
|
+
and24(
|
|
33010
|
+
eq33(runs.projectId, projectId),
|
|
33011
|
+
eq33(runs.kind, RunKinds["answer-visibility"]),
|
|
33012
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32674
33013
|
// Defensive — see top of file.
|
|
32675
33014
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32676
33015
|
)
|
|
32677
|
-
).orderBy(
|
|
33016
|
+
).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
32678
33017
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
32679
33018
|
const recentRunIds = [];
|
|
32680
33019
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -32690,7 +33029,7 @@ var IntelligenceService = class {
|
|
|
32690
33029
|
const haveHistory = recentRunIds.length > 0;
|
|
32691
33030
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
32692
33031
|
if (haveHistory) {
|
|
32693
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
33032
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and24(eq33(insights.type, "regression"), inArray12(insights.runId, recentRunIds))).all();
|
|
32694
33033
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
32695
33034
|
for (const row of priorRows) {
|
|
32696
33035
|
if (!row.runId) continue;
|
|
@@ -32719,7 +33058,7 @@ var IntelligenceService = class {
|
|
|
32719
33058
|
});
|
|
32720
33059
|
}
|
|
32721
33060
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
32722
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
33061
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq33(projects.id, projectId)).get();
|
|
32723
33062
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
32724
33063
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
32725
33064
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -32735,7 +33074,7 @@ var IntelligenceService = class {
|
|
|
32735
33074
|
citedDomains: querySnapshots.citedDomains,
|
|
32736
33075
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
32737
33076
|
snapshotLocation: querySnapshots.location
|
|
32738
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
33077
|
+
}).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
|
|
32739
33078
|
const snapshots = [];
|
|
32740
33079
|
let orphanCount = 0;
|
|
32741
33080
|
for (const r of rows) {
|
|
@@ -32786,6 +33125,8 @@ export {
|
|
|
32786
33125
|
gscSearchData,
|
|
32787
33126
|
gscUrlInspections,
|
|
32788
33127
|
gscCoverageSnapshots,
|
|
33128
|
+
siteAuditSnapshots,
|
|
33129
|
+
siteAuditPages,
|
|
32789
33130
|
bingCoverageSnapshots,
|
|
32790
33131
|
bingUrlInspections,
|
|
32791
33132
|
gaTrafficSnapshots,
|