@ainyc/canonry 4.72.4 → 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/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-SRBO33HB.js → chunk-MRC4JMIH.js} +369 -176
- package/dist/{chunk-ZUBBADMR.js → chunk-W6GBIRFA.js} +162 -1
- package/dist/{chunk-HOKVBMOD.js → chunk-ZRZHIS22.js} +383 -72
- package/dist/cli.js +236 -30
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-CSW4R4I7.js → intelligence-service-GPO2VMEC.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +9 -9
- 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;
|
|
@@ -31675,6 +31836,151 @@ function dedupeStrings(input) {
|
|
|
31675
31836
|
return out;
|
|
31676
31837
|
}
|
|
31677
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
|
+
|
|
31678
31984
|
// ../api-routes/src/index.ts
|
|
31679
31985
|
async function apiRoutes(app, opts) {
|
|
31680
31986
|
app.decorate("db", opts.db);
|
|
@@ -31839,6 +32145,9 @@ async function apiRoutes(app, opts) {
|
|
|
31839
32145
|
await api.register(discoveryRoutes, {
|
|
31840
32146
|
onDiscoveryRunRequested: opts.onDiscoveryRunRequested
|
|
31841
32147
|
});
|
|
32148
|
+
await api.register(technicalAeoRoutes, {
|
|
32149
|
+
onSiteAuditRequested: opts.onSiteAuditRequested
|
|
32150
|
+
});
|
|
31842
32151
|
await api.register(doctorRoutes, {
|
|
31843
32152
|
googleConnectionStore: opts.googleConnectionStore,
|
|
31844
32153
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -31994,7 +32303,7 @@ function buildTrafficSourceValidators(opts) {
|
|
|
31994
32303
|
}
|
|
31995
32304
|
|
|
31996
32305
|
// src/intelligence-service.ts
|
|
31997
|
-
import
|
|
32306
|
+
import crypto28 from "crypto";
|
|
31998
32307
|
|
|
31999
32308
|
// src/logger.ts
|
|
32000
32309
|
var IS_TTY = process.stdout.isTTY === true;
|
|
@@ -32225,16 +32534,16 @@ var IntelligenceService = class {
|
|
|
32225
32534
|
*/
|
|
32226
32535
|
analyzeAndPersist(runId, projectId) {
|
|
32227
32536
|
const recentRuns = this.db.select().from(runs).where(
|
|
32228
|
-
|
|
32229
|
-
|
|
32230
|
-
or5(
|
|
32537
|
+
and24(
|
|
32538
|
+
eq33(runs.projectId, projectId),
|
|
32539
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32231
32540
|
// Defensive: RunCoordinator already skips probes before this is
|
|
32232
32541
|
// called, but if a future call site invokes analyzeAndPersist
|
|
32233
32542
|
// directly for a probe, probes still must not pollute the
|
|
32234
32543
|
// intelligence window.
|
|
32235
32544
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32236
32545
|
)
|
|
32237
|
-
).orderBy(
|
|
32546
|
+
).orderBy(desc17(runs.finishedAt), desc17(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
32238
32547
|
if (recentRuns.length === 0) {
|
|
32239
32548
|
log.info("intelligence.skip", { runId, reason: "no completed runs" });
|
|
32240
32549
|
return null;
|
|
@@ -32309,7 +32618,7 @@ var IntelligenceService = class {
|
|
|
32309
32618
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
32310
32619
|
*/
|
|
32311
32620
|
analyzeAndPersistGbp(runId, projectId) {
|
|
32312
|
-
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();
|
|
32313
32622
|
if (!runRow) {
|
|
32314
32623
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
32315
32624
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -32317,9 +32626,9 @@ var IntelligenceService = class {
|
|
|
32317
32626
|
}
|
|
32318
32627
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
32319
32628
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
32320
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
32321
|
-
|
|
32322
|
-
|
|
32629
|
+
const selected = this.db.select().from(gbpLocations).where(and24(
|
|
32630
|
+
eq33(gbpLocations.projectId, projectId),
|
|
32631
|
+
eq33(gbpLocations.selected, true),
|
|
32323
32632
|
gte6(gbpLocations.syncedAt, windowStart),
|
|
32324
32633
|
lte3(gbpLocations.syncedAt, windowEnd)
|
|
32325
32634
|
)).all();
|
|
@@ -32354,10 +32663,10 @@ var IntelligenceService = class {
|
|
|
32354
32663
|
}
|
|
32355
32664
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
32356
32665
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
32357
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
32358
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
32359
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
32360
|
-
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();
|
|
32361
32670
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
32362
32671
|
const summary = buildGbpSummary({
|
|
32363
32672
|
locationName,
|
|
@@ -32389,7 +32698,7 @@ var IntelligenceService = class {
|
|
|
32389
32698
|
/** Build the month-over-month keyword series for a location from the
|
|
32390
32699
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
32391
32700
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
32392
|
-
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();
|
|
32393
32702
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
32394
32703
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
32395
32704
|
const recentMonth = months[0] ?? null;
|
|
@@ -32420,7 +32729,7 @@ var IntelligenceService = class {
|
|
|
32420
32729
|
*/
|
|
32421
32730
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
32422
32731
|
const covered = new Set(coveredLocationNames);
|
|
32423
|
-
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();
|
|
32424
32733
|
const staleIds = [];
|
|
32425
32734
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
32426
32735
|
for (const row of existing) {
|
|
@@ -32431,7 +32740,7 @@ var IntelligenceService = class {
|
|
|
32431
32740
|
}
|
|
32432
32741
|
this.db.transaction((tx) => {
|
|
32433
32742
|
for (const id of staleIds) {
|
|
32434
|
-
tx.delete(insights).where(
|
|
32743
|
+
tx.delete(insights).where(eq33(insights.id, id)).run();
|
|
32435
32744
|
}
|
|
32436
32745
|
for (const insight of gbpInsights) {
|
|
32437
32746
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -32509,7 +32818,7 @@ var IntelligenceService = class {
|
|
|
32509
32818
|
* create per run + aggregate). DB is left untouched.
|
|
32510
32819
|
*/
|
|
32511
32820
|
backfill(projectName, opts, onProgress) {
|
|
32512
|
-
const project = this.db.select().from(projects).where(
|
|
32821
|
+
const project = this.db.select().from(projects).where(eq33(projects.name, projectName)).get();
|
|
32513
32822
|
if (!project) {
|
|
32514
32823
|
throw new Error(`Project "${projectName}" not found`);
|
|
32515
32824
|
}
|
|
@@ -32522,13 +32831,13 @@ var IntelligenceService = class {
|
|
|
32522
32831
|
sinceTimestamp = parsed;
|
|
32523
32832
|
}
|
|
32524
32833
|
const allRuns = this.db.select().from(runs).where(
|
|
32525
|
-
|
|
32526
|
-
|
|
32527
|
-
or5(
|
|
32834
|
+
and24(
|
|
32835
|
+
eq33(runs.projectId, project.id),
|
|
32836
|
+
or5(eq33(runs.status, "completed"), eq33(runs.status, "partial")),
|
|
32528
32837
|
// Backfill must not replay probe runs as if they were real sweeps.
|
|
32529
32838
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32530
32839
|
)
|
|
32531
|
-
).orderBy(
|
|
32840
|
+
).orderBy(asc4(runs.finishedAt)).all();
|
|
32532
32841
|
let startIdx = 0;
|
|
32533
32842
|
let endIdx = allRuns.length;
|
|
32534
32843
|
if (opts?.fromRunId) {
|
|
@@ -32557,7 +32866,7 @@ var IntelligenceService = class {
|
|
|
32557
32866
|
let wouldDeleteTotal = 0;
|
|
32558
32867
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
32559
32868
|
if (isDryRun && targetRuns.length > 0) {
|
|
32560
|
-
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();
|
|
32561
32870
|
for (const r of rows) {
|
|
32562
32871
|
if (r.runId == null) continue;
|
|
32563
32872
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -32603,7 +32912,7 @@ var IntelligenceService = class {
|
|
|
32603
32912
|
return { processed, skipped, totalInsights };
|
|
32604
32913
|
}
|
|
32605
32914
|
loadTrackedCompetitors(projectId) {
|
|
32606
|
-
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);
|
|
32607
32916
|
}
|
|
32608
32917
|
/**
|
|
32609
32918
|
* Wipe transition signals from an analysis result while keeping health.
|
|
@@ -32624,15 +32933,15 @@ var IntelligenceService = class {
|
|
|
32624
32933
|
}
|
|
32625
32934
|
persistResult(result, runId, projectId) {
|
|
32626
32935
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
32627
|
-
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();
|
|
32628
32937
|
for (const row of existingInsights) {
|
|
32629
32938
|
if (row.dismissed) {
|
|
32630
32939
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
32631
32940
|
}
|
|
32632
32941
|
}
|
|
32633
32942
|
this.db.transaction((tx) => {
|
|
32634
|
-
tx.delete(insights).where(
|
|
32635
|
-
tx.delete(healthSnapshots).where(
|
|
32943
|
+
tx.delete(insights).where(eq33(insights.runId, runId)).run();
|
|
32944
|
+
tx.delete(healthSnapshots).where(eq33(healthSnapshots.runId, runId)).run();
|
|
32636
32945
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32637
32946
|
for (const insight of result.insights) {
|
|
32638
32947
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -32652,7 +32961,7 @@ var IntelligenceService = class {
|
|
|
32652
32961
|
}).run();
|
|
32653
32962
|
}
|
|
32654
32963
|
tx.insert(healthSnapshots).values({
|
|
32655
|
-
id:
|
|
32964
|
+
id: crypto28.randomUUID(),
|
|
32656
32965
|
projectId,
|
|
32657
32966
|
runId,
|
|
32658
32967
|
overallCitedRate: String(result.health.overallCitedRate),
|
|
@@ -32683,28 +32992,28 @@ var IntelligenceService = class {
|
|
|
32683
32992
|
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
32684
32993
|
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
32685
32994
|
if (regressions.length === 0) return rawInsights;
|
|
32686
|
-
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();
|
|
32687
32996
|
const gscConnected = gscRows.length > 0;
|
|
32688
32997
|
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
32689
32998
|
for (const row of gscRows) {
|
|
32690
32999
|
const key = row.query.toLowerCase();
|
|
32691
33000
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
32692
33001
|
}
|
|
32693
|
-
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();
|
|
32694
33003
|
const locationCount = Math.max(
|
|
32695
33004
|
1,
|
|
32696
33005
|
(projectRow?.locations ?? []).length
|
|
32697
33006
|
);
|
|
32698
33007
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
32699
33008
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
32700
|
-
|
|
32701
|
-
|
|
32702
|
-
|
|
32703
|
-
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")),
|
|
32704
33013
|
// Defensive — see top of file.
|
|
32705
33014
|
ne5(runs.trigger, RunTriggers.probe)
|
|
32706
33015
|
)
|
|
32707
|
-
).orderBy(
|
|
33016
|
+
).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
32708
33017
|
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
32709
33018
|
const recentRunIds = [];
|
|
32710
33019
|
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
@@ -32720,7 +33029,7 @@ var IntelligenceService = class {
|
|
|
32720
33029
|
const haveHistory = recentRunIds.length > 0;
|
|
32721
33030
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
32722
33031
|
if (haveHistory) {
|
|
32723
|
-
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();
|
|
32724
33033
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
32725
33034
|
for (const row of priorRows) {
|
|
32726
33035
|
if (!row.runId) continue;
|
|
@@ -32749,7 +33058,7 @@ var IntelligenceService = class {
|
|
|
32749
33058
|
});
|
|
32750
33059
|
}
|
|
32751
33060
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
32752
|
-
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();
|
|
32753
33062
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
32754
33063
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
32755
33064
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -32765,7 +33074,7 @@ var IntelligenceService = class {
|
|
|
32765
33074
|
citedDomains: querySnapshots.citedDomains,
|
|
32766
33075
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
32767
33076
|
snapshotLocation: querySnapshots.location
|
|
32768
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
33077
|
+
}).from(querySnapshots).leftJoin(queries, eq33(querySnapshots.queryId, queries.id)).where(eq33(querySnapshots.runId, runId)).all();
|
|
32769
33078
|
const snapshots = [];
|
|
32770
33079
|
let orphanCount = 0;
|
|
32771
33080
|
for (const r of rows) {
|
|
@@ -32816,6 +33125,8 @@ export {
|
|
|
32816
33125
|
gscSearchData,
|
|
32817
33126
|
gscUrlInspections,
|
|
32818
33127
|
gscCoverageSnapshots,
|
|
33128
|
+
siteAuditSnapshots,
|
|
33129
|
+
siteAuditPages,
|
|
32819
33130
|
bingCoverageSnapshots,
|
|
32820
33131
|
bingUrlInspections,
|
|
32821
33132
|
gaTrafficSnapshots,
|