@canonry/canonry 5.21.0 → 5.22.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 +1 -1
- package/assets/assets/{AuditHistoryPanel-CjxWA3lx.js → AuditHistoryPanel-teTDC-4-.js} +1 -1
- package/assets/assets/{BacklinksPage-Bdd164Up.js → BacklinksPage-Bqm7bido.js} +1 -1
- package/assets/assets/{HistoryPage-BvJmE-kP.js → HistoryPage-IcqwPBmt.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-HuP6RMZ-.js → MeasurementPropertyPage-BLpptT3_.js} +1 -1
- package/assets/assets/{ProjectPage-DOvXVbyy.js → ProjectPage-nn2Y9H-m.js} +1 -1
- package/assets/assets/{RunRow-Cnb3JOBw.js → RunRow-S_SCHr0o.js} +1 -1
- package/assets/assets/{RunsPage-Dvz1JYIm.js → RunsPage-DFiif626.js} +1 -1
- package/assets/assets/{SettingsPage-DYGvphkg.js → SettingsPage-CitZ6FAS.js} +1 -1
- package/assets/assets/{SiteHealthSection-CUNiKaod.js → SiteHealthSection-C2dsyNGY.js} +3 -3
- package/assets/assets/{TrafficPage-Bf4Tq95q.js → TrafficPage-lY8na5Pg.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-CIuLWZgT.js → TrafficSourceDetailPage-BTykoVGT.js} +1 -1
- package/assets/assets/{extract-error-message-DZex1e4H.js → extract-error-message-r0Gpc7mv.js} +1 -1
- package/assets/assets/{index-CeXW-lzj.js → index-DwlO8KfE.js} +20 -20
- package/assets/assets/{react-sigma_core.esm.min-CxvtwEkv.js → react-sigma_core.esm.min-DDrDxbO1.js} +1 -1
- package/assets/assets/{v2-overview-adapter-Bh4qIzLp.js → v2-overview-adapter-OsQiglXU.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-HDJHQPW3.js → chunk-3LCR6WU6.js} +2 -2
- package/dist/{chunk-WUT3G2GM.js → chunk-DDL2XSNM.js} +4 -2
- package/dist/{chunk-FRWZWJB3.js → chunk-GLLY2GEV.js} +6 -3
- package/dist/{chunk-FAVYLZQV.js → chunk-P5VXVFQW.js} +845 -445
- package/dist/{chunk-2LO7FLVD.js → chunk-S5OSY3PI.js} +30 -0
- package/dist/{chunk-NFN5GSEJ.js → chunk-XFHXKLNT.js} +23 -12
- package/dist/{chunk-MEAKE5W7.js → chunk-YTSXQUJ3.js} +3 -3
- package/dist/cli.js +15 -12
- package/dist/{demo-server-7BVVVXPM.js → demo-server-HULNEORN.js} +4 -4
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-3UUKRAZ5.js → intelligence-service-CXNBKRHV.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +11 -11
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
COMPETITOR_LANDSCAPE_OBSERVED_NAME_LIMIT,
|
|
36
36
|
CcReleaseSyncStatuses,
|
|
37
37
|
CheckCategories,
|
|
38
|
+
CheckNotificationPolicies,
|
|
38
39
|
CheckScopes,
|
|
39
40
|
CheckStatuses,
|
|
40
41
|
CitationStates,
|
|
@@ -377,6 +378,8 @@ import {
|
|
|
377
378
|
googleMarketingOAuthConnectRequestSchema,
|
|
378
379
|
googleMarketingOAuthConnectResponseSchema,
|
|
379
380
|
googleMarketingProviderSchema,
|
|
381
|
+
gradedReportMonths,
|
|
382
|
+
groupIsoDateRanges,
|
|
380
383
|
gscCoverageSnapshotDtoSchema,
|
|
381
384
|
gscCoverageSummaryDtoSchema,
|
|
382
385
|
gscDeindexedRowSchema,
|
|
@@ -629,6 +632,8 @@ import {
|
|
|
629
632
|
reportLineChartLabel,
|
|
630
633
|
reportMarketScope,
|
|
631
634
|
reportMissRateLabel,
|
|
635
|
+
reportMonthSchema,
|
|
636
|
+
reportMonthsForDoctor,
|
|
632
637
|
reportMoreChipLabel,
|
|
633
638
|
reportMovementChangeCopy,
|
|
634
639
|
reportOpportunityActionLine,
|
|
@@ -794,10 +799,10 @@ import {
|
|
|
794
799
|
wordpressSchemaDeployResultDtoSchema,
|
|
795
800
|
wordpressSchemaStatusResultDtoSchema,
|
|
796
801
|
wordpressStatusDtoSchema
|
|
797
|
-
} from "./chunk-
|
|
802
|
+
} from "./chunk-S5OSY3PI.js";
|
|
798
803
|
|
|
799
804
|
// src/intelligence-service.ts
|
|
800
|
-
import { eq as
|
|
805
|
+
import { eq as eq74, desc as desc32, asc as asc13, and as and62, ne as ne11, or as or18, inArray as inArray28, gte as gte20, lte as lte17, isNull as isNull13, sql as sql28, exists } from "drizzle-orm";
|
|
801
806
|
|
|
802
807
|
// ../db/src/client.ts
|
|
803
808
|
import { mkdirSync } from "fs";
|
|
@@ -25146,7 +25151,7 @@ async function visibilityReportRoutes(app) {
|
|
|
25146
25151
|
async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
|
|
25147
25152
|
);
|
|
25148
25153
|
}
|
|
25149
|
-
function readVisibilityComparisonRuns(db, projectId, since, until) {
|
|
25154
|
+
function readVisibilityComparisonRuns(db, projectId, since, until, includeEvidence = true) {
|
|
25150
25155
|
const sourceRuns = db.select().from(runs).where(and21(
|
|
25151
25156
|
eq26(runs.projectId, projectId),
|
|
25152
25157
|
eq26(runs.kind, RunKinds["answer-visibility"]),
|
|
@@ -25172,7 +25177,7 @@ function readVisibilityComparisonRuns(db, projectId, since, until) {
|
|
|
25172
25177
|
const presentation = versions.get(heads.get(source.row.id) ?? source.row.id) ?? source;
|
|
25173
25178
|
const snapshots = byRun.get(run.id) ?? [];
|
|
25174
25179
|
const models = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot.model]));
|
|
25175
|
-
const materialized = advancedRun(run, presentation.row, presentation.plan, snapshots, comparableVersionIds(rowsById, presentation.row.id));
|
|
25180
|
+
const materialized = advancedRun(run, presentation.row, presentation.plan, snapshots, comparableVersionIds(rowsById, presentation.row.id), includeEvidence);
|
|
25176
25181
|
return [{ ...materialized, observations: materialized.observations.map((observation) => ({ ...observation, model: models.get(observation.answerId) ?? null })) }];
|
|
25177
25182
|
});
|
|
25178
25183
|
return { runs: selected, unavailable: unavailable5 };
|
|
@@ -35457,70 +35462,23 @@ function readVisibilityCompare(db, projectName, query) {
|
|
|
35457
35462
|
if (scope !== "project" && filters.scopeKey === void 0) throw validationError("A non-project comparison scope requires scopeKey.");
|
|
35458
35463
|
if (scope === "project" && filters.scopeKey !== void 0) throw validationError("scopeKey is not valid for project scope.");
|
|
35459
35464
|
if (scope === "market" && filters.marketKey !== void 0) throw validationError("marketKey is not valid for market scope.");
|
|
35460
|
-
const {
|
|
35461
|
-
if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
|
|
35462
|
-
if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
|
|
35463
|
-
let fromBounds;
|
|
35464
|
-
let toBounds;
|
|
35465
|
-
try {
|
|
35466
|
-
fromBounds = calendarMonthBounds(fromRaw);
|
|
35467
|
-
} catch (err) {
|
|
35468
|
-
throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
|
|
35469
|
-
}
|
|
35470
|
-
try {
|
|
35471
|
-
toBounds = calendarMonthBounds(toRaw);
|
|
35472
|
-
} catch (err) {
|
|
35473
|
-
throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
|
|
35474
|
-
}
|
|
35475
|
-
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
35476
|
-
throw validationError('"from" must be a month strictly before "to"');
|
|
35477
|
-
}
|
|
35465
|
+
const { fromRaw, toRaw, fromBounds, toBounds } = compareMonths(query.from, query.to);
|
|
35478
35466
|
const providerKey2 = filters.provider === void 0 ? void 0 : normalizeText3(filters.provider);
|
|
35479
35467
|
const locationKey2 = filters.location === void 0 ? void 0 : normalizeText3(filters.location);
|
|
35480
35468
|
const scoped = scope !== "project" || filters.marketKey !== void 0;
|
|
35481
35469
|
let advancedComparison;
|
|
35482
35470
|
if (activeMeasurementPlan(db, project.id) !== null) {
|
|
35483
|
-
|
|
35484
|
-
|
|
35485
|
-
|
|
35486
|
-
|
|
35487
|
-
|
|
35488
|
-
|
|
35489
|
-
let runCount = 0;
|
|
35490
|
-
for (const run of selected.runs) {
|
|
35491
|
-
let population;
|
|
35492
|
-
try {
|
|
35493
|
-
population = visibilityComparisonPopulation(run, {
|
|
35494
|
-
...filters,
|
|
35495
|
-
scope,
|
|
35496
|
-
queryClass: "all",
|
|
35497
|
-
location: locationKey2 === void 0 ? { kind: "all" } : locationKey2 === "none" ? { kind: "none" } : { kind: "exact", value: filters.location },
|
|
35498
|
-
limit: 100
|
|
35499
|
-
});
|
|
35500
|
-
} catch (error) {
|
|
35501
|
-
if (error instanceof VisibilityReportScopeError && error.details !== void 0) {
|
|
35502
|
-
missingScope ??= error;
|
|
35503
|
-
continue;
|
|
35504
|
-
}
|
|
35505
|
-
if (error instanceof VisibilityReportScopeError) throw validationError(error.message, error.details);
|
|
35506
|
-
throw error;
|
|
35507
|
-
}
|
|
35508
|
-
runCount += 1;
|
|
35509
|
-
const adapt = (snapshot) => ({
|
|
35510
|
-
...snapshot,
|
|
35511
|
-
citationState: snapshot.citation === true ? CitationStates.cited : CitationStates["not-cited"],
|
|
35512
|
-
citationChecked: snapshot.citation !== null,
|
|
35513
|
-
citedDomains: []
|
|
35514
|
-
});
|
|
35515
|
-
snapshots.push(...population.snapshots.map(adapt));
|
|
35516
|
-
classSnapshots.push(...population.classSnapshots.map(adapt));
|
|
35517
|
-
}
|
|
35518
|
-
measuredScope += runCount;
|
|
35519
|
-
return { runCount, snapshots, classSnapshots, classificationAvailable: !selected.unavailable };
|
|
35471
|
+
const readerSelection = {
|
|
35472
|
+
...filters,
|
|
35473
|
+
scope,
|
|
35474
|
+
queryClass: "all",
|
|
35475
|
+
location: locationKey2 === void 0 ? { kind: "all" } : locationKey2 === "none" ? { kind: "none" } : { kind: "exact", value: filters.location },
|
|
35476
|
+
limit: 100
|
|
35520
35477
|
};
|
|
35521
|
-
const fromMonth2 =
|
|
35522
|
-
const toMonth2 =
|
|
35523
|
-
|
|
35478
|
+
const { missingScope: fromMissingScope, ...fromMonth2 } = loadAdvancedCompareMonth(db, project.id, fromBounds, readerSelection);
|
|
35479
|
+
const { missingScope: toMissingScope, ...toMonth2 } = loadAdvancedCompareMonth(db, project.id, toBounds, readerSelection);
|
|
35480
|
+
const missingScope = fromMissingScope ?? toMissingScope;
|
|
35481
|
+
if (missingScope !== void 0 && fromMonth2.runCount + toMonth2.runCount === 0) throw validationError(missingScope.message, missingScope.details);
|
|
35524
35482
|
const queries2 = [...new Map([...fromMonth2.snapshots, ...toMonth2.snapshots].map((snapshot) => [snapshot.queryId, { id: snapshot.queryId, query: snapshot.queryText }])).values()];
|
|
35525
35483
|
advancedComparison = { ...computeVisibilityCompare({
|
|
35526
35484
|
project: project.name,
|
|
@@ -35543,12 +35501,7 @@ function readVisibilityCompare(db, projectName, query) {
|
|
|
35543
35501
|
const competitorRows2 = db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all();
|
|
35544
35502
|
const competitorInputs = mentionShareCompetitorsFromDomains(competitorRows2.map((c) => c.domain));
|
|
35545
35503
|
const loadMonth = (bounds) => {
|
|
35546
|
-
const
|
|
35547
|
-
const untilMs = Date.parse(bounds.until);
|
|
35548
|
-
const monthRuns = db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and35(eq39(runs.projectId, project.id), eq39(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
35549
|
-
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
35550
|
-
);
|
|
35551
|
-
const runIds = monthRuns.map((r) => r.id);
|
|
35504
|
+
const runIds = legacyCompareRunIds(db, project.id, bounds);
|
|
35552
35505
|
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? db.select({
|
|
35553
35506
|
queryId: querySnapshots.queryId,
|
|
35554
35507
|
queryText: querySnapshots.queryText,
|
|
@@ -35561,7 +35514,7 @@ function readVisibilityCompare(db, projectName, query) {
|
|
|
35561
35514
|
location: querySnapshots.location
|
|
35562
35515
|
}).from(querySnapshots).where(inArray18(querySnapshots.runId, runIds)).all() : [];
|
|
35563
35516
|
return {
|
|
35564
|
-
runCount:
|
|
35517
|
+
runCount: runIds.length,
|
|
35565
35518
|
snapshots: snapshots.filter((snapshot) => (providerKey2 === void 0 || normalizeText3(snapshot.provider) === providerKey2) && (locationKey2 === void 0 || (locationKey2 === "none" ? snapshot.location === null : snapshot.location !== null && normalizeText3(snapshot.location) === locationKey2)))
|
|
35566
35519
|
};
|
|
35567
35520
|
};
|
|
@@ -35589,6 +35542,104 @@ function readVisibilityCompare(db, projectName, query) {
|
|
|
35589
35542
|
}
|
|
35590
35543
|
return { ...dto, selection: filters };
|
|
35591
35544
|
}
|
|
35545
|
+
function compareMonths(fromRaw, toRaw) {
|
|
35546
|
+
if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
|
|
35547
|
+
if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
|
|
35548
|
+
let fromBounds;
|
|
35549
|
+
let toBounds;
|
|
35550
|
+
try {
|
|
35551
|
+
fromBounds = calendarMonthBounds(fromRaw);
|
|
35552
|
+
} catch (err) {
|
|
35553
|
+
throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
|
|
35554
|
+
}
|
|
35555
|
+
try {
|
|
35556
|
+
toBounds = calendarMonthBounds(toRaw);
|
|
35557
|
+
} catch (err) {
|
|
35558
|
+
throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
|
|
35559
|
+
}
|
|
35560
|
+
if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
|
|
35561
|
+
throw validationError('"from" must be a month strictly before "to"');
|
|
35562
|
+
}
|
|
35563
|
+
return { fromRaw, toRaw, fromBounds, toBounds };
|
|
35564
|
+
}
|
|
35565
|
+
function legacyCompareRunIds(db, projectId, bounds) {
|
|
35566
|
+
const sinceMs = Date.parse(bounds.since);
|
|
35567
|
+
const untilMs = Date.parse(bounds.until);
|
|
35568
|
+
return db.select({ id: runs.id, createdAt: runs.createdAt, status: runs.status }).from(runs).where(and35(eq39(runs.projectId, projectId), eq39(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter(
|
|
35569
|
+
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
35570
|
+
).map((r) => r.id);
|
|
35571
|
+
}
|
|
35572
|
+
function loadAdvancedCompareMonth(db, projectId, bounds, selection, includeEvidence = true) {
|
|
35573
|
+
const selected = readVisibilityComparisonRuns(db, projectId, bounds.since, bounds.until, includeEvidence);
|
|
35574
|
+
const snapshots = [];
|
|
35575
|
+
const classSnapshots = [];
|
|
35576
|
+
let runCount = 0;
|
|
35577
|
+
let missingScope;
|
|
35578
|
+
for (const run of selected.runs) {
|
|
35579
|
+
let population;
|
|
35580
|
+
try {
|
|
35581
|
+
population = visibilityComparisonPopulation(run, selection);
|
|
35582
|
+
} catch (error) {
|
|
35583
|
+
if (error instanceof VisibilityReportScopeError && error.details !== void 0) {
|
|
35584
|
+
missingScope ??= error;
|
|
35585
|
+
continue;
|
|
35586
|
+
}
|
|
35587
|
+
if (error instanceof VisibilityReportScopeError) throw validationError(error.message, error.details);
|
|
35588
|
+
throw error;
|
|
35589
|
+
}
|
|
35590
|
+
runCount += 1;
|
|
35591
|
+
const adapt = (snapshot) => ({
|
|
35592
|
+
...snapshot,
|
|
35593
|
+
citationState: snapshot.citation === true ? CitationStates.cited : CitationStates["not-cited"],
|
|
35594
|
+
citationChecked: snapshot.citation !== null,
|
|
35595
|
+
citedDomains: []
|
|
35596
|
+
});
|
|
35597
|
+
snapshots.push(...population.snapshots.map(adapt));
|
|
35598
|
+
classSnapshots.push(...population.classSnapshots.map(adapt));
|
|
35599
|
+
}
|
|
35600
|
+
return { runCount, snapshots, classSnapshots, classificationAvailable: !selected.unavailable, missingScope };
|
|
35601
|
+
}
|
|
35602
|
+
function readVisibilityContinuity(db, projectName, months) {
|
|
35603
|
+
const project = resolveProject(db, projectName);
|
|
35604
|
+
const { fromRaw, toRaw, fromBounds, toBounds } = compareMonths(months.from, months.to);
|
|
35605
|
+
const bare = (snapshot) => ({
|
|
35606
|
+
queryId: snapshot.queryId,
|
|
35607
|
+
queryText: snapshot.queryText,
|
|
35608
|
+
provider: snapshot.provider,
|
|
35609
|
+
model: snapshot.model,
|
|
35610
|
+
...snapshot.cohortKey === void 0 ? {} : { cohortKey: snapshot.cohortKey },
|
|
35611
|
+
citationState: CitationStates["not-cited"],
|
|
35612
|
+
answerMentioned: null,
|
|
35613
|
+
answerText: null,
|
|
35614
|
+
citedDomains: []
|
|
35615
|
+
});
|
|
35616
|
+
const gate = (trackedQueries2, fromMonth, toMonth) => computeVisibilityCompare({
|
|
35617
|
+
project: project.name,
|
|
35618
|
+
queries: trackedQueries2,
|
|
35619
|
+
competitors: [],
|
|
35620
|
+
from: { month: fromRaw, ...fromBounds, ...fromMonth },
|
|
35621
|
+
to: { month: toRaw, ...toBounds, ...toMonth }
|
|
35622
|
+
}).continuity;
|
|
35623
|
+
const trackedQueries = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all();
|
|
35624
|
+
const loadProjectMonth = (bounds) => {
|
|
35625
|
+
const runIds = legacyCompareRunIds(db, project.id, bounds);
|
|
35626
|
+
const rows = runIds.length > 0 && trackedQueries.length > 0 ? db.select({ queryId: querySnapshots.queryId, queryText: querySnapshots.queryText, provider: querySnapshots.provider, model: querySnapshots.model }).from(querySnapshots).where(inArray18(querySnapshots.runId, runIds)).all() : [];
|
|
35627
|
+
return { runCount: runIds.length, snapshots: rows.map(bare) };
|
|
35628
|
+
};
|
|
35629
|
+
const frames = [{ frame: "project", continuity: gate(trackedQueries, loadProjectMonth(fromBounds), loadProjectMonth(toBounds)) }];
|
|
35630
|
+
if (activeMeasurementPlan(db, project.id) !== null) {
|
|
35631
|
+
const selection = { scope: "project", queryClass: "all", location: { kind: "all" }, limit: 100 };
|
|
35632
|
+
const fromMonth = loadAdvancedCompareMonth(db, project.id, fromBounds, selection, false);
|
|
35633
|
+
const toMonth = loadAdvancedCompareMonth(db, project.id, toBounds, selection, false);
|
|
35634
|
+
if (fromMonth.classificationAvailable && toMonth.classificationAvailable) {
|
|
35635
|
+
const bareFrom = { runCount: fromMonth.runCount, snapshots: fromMonth.snapshots.map(bare) };
|
|
35636
|
+
const bareTo = { runCount: toMonth.runCount, snapshots: toMonth.snapshots.map(bare) };
|
|
35637
|
+
const frozenQueries = [...new Map([...bareFrom.snapshots, ...bareTo.snapshots].map((snapshot) => [snapshot.queryId, { id: snapshot.queryId, query: snapshot.queryText }])).values()];
|
|
35638
|
+
frames.push({ frame: "class", continuity: gate(frozenQueries, bareFrom, bareTo) });
|
|
35639
|
+
}
|
|
35640
|
+
}
|
|
35641
|
+
return frames;
|
|
35642
|
+
}
|
|
35592
35643
|
|
|
35593
35644
|
// ../api-routes/src/results-export.ts
|
|
35594
35645
|
import { and as and36, asc as asc5, eq as eq40, gte as gte11, lte as lte9 } from "drizzle-orm";
|
|
@@ -42925,6 +42976,12 @@ var routeCatalog = [
|
|
|
42925
42976
|
description: "Runs all global-scope checks (provider keys, etc.). Use ?check=<id> or ?check=<prefix>* (comma-separated) to filter. Returns a structured DoctorReport with per-check status, code, summary, remediation, and details.",
|
|
42926
42977
|
tags: ["doctor"],
|
|
42927
42978
|
parameters: [
|
|
42979
|
+
{
|
|
42980
|
+
name: "reportMonth",
|
|
42981
|
+
in: "query",
|
|
42982
|
+
description: "Report month YYYY-MM (not future). Omit for this month plus the previous month through UTC day 3. Read selection only; no writes or provider calls by report checks.",
|
|
42983
|
+
schema: { type: "string", pattern: "^[1-9][0-9]{3}-(0[1-9]|1[0-2])$" }
|
|
42984
|
+
},
|
|
42928
42985
|
{
|
|
42929
42986
|
name: "check",
|
|
42930
42987
|
in: "query",
|
|
@@ -42933,7 +42990,8 @@ var routeCatalog = [
|
|
|
42933
42990
|
}
|
|
42934
42991
|
],
|
|
42935
42992
|
responses: {
|
|
42936
|
-
200: jsonResponse("Doctor report returned.", "DoctorReportDto")
|
|
42993
|
+
200: jsonResponse("Doctor report returned.", "DoctorReportDto"),
|
|
42994
|
+
400: errorResponse("Invalid report month.")
|
|
42937
42995
|
}
|
|
42938
42996
|
},
|
|
42939
42997
|
{
|
|
@@ -42944,6 +43002,12 @@ var routeCatalog = [
|
|
|
42944
43002
|
tags: ["doctor"],
|
|
42945
43003
|
parameters: [
|
|
42946
43004
|
nameParameter,
|
|
43005
|
+
{
|
|
43006
|
+
name: "reportMonth",
|
|
43007
|
+
in: "query",
|
|
43008
|
+
description: "Report month YYYY-MM (not future). Omit for this month plus the previous month through UTC day 3. Read selection only; no writes or provider calls by report checks.",
|
|
43009
|
+
schema: { type: "string", pattern: "^[1-9][0-9]{3}-(0[1-9]|1[0-2])$" }
|
|
43010
|
+
},
|
|
42947
43011
|
{
|
|
42948
43012
|
name: "check",
|
|
42949
43013
|
in: "query",
|
|
@@ -42953,6 +43017,7 @@ var routeCatalog = [
|
|
|
42953
43017
|
],
|
|
42954
43018
|
responses: {
|
|
42955
43019
|
200: jsonResponse("Doctor report returned.", "DoctorReportDto"),
|
|
43020
|
+
400: errorResponse("Invalid report month."),
|
|
42956
43021
|
404: errorResponse("Project not found.")
|
|
42957
43022
|
}
|
|
42958
43023
|
},
|
|
@@ -80029,6 +80094,322 @@ async function trafficRoutes(app, opts) {
|
|
|
80029
80094
|
});
|
|
80030
80095
|
}
|
|
80031
80096
|
|
|
80097
|
+
// ../api-routes/src/doctor/checks/report-sweeps.ts
|
|
80098
|
+
import { and as and52, asc as asc11, eq as eq58, gte as gte15, lte as lte15 } from "drizzle-orm";
|
|
80099
|
+
|
|
80100
|
+
// ../api-routes/src/visibility-attribution.ts
|
|
80101
|
+
function buildQueryAttribution2(projectQueries) {
|
|
80102
|
+
const byId = /* @__PURE__ */ new Map();
|
|
80103
|
+
const byText = /* @__PURE__ */ new Map();
|
|
80104
|
+
for (const q of projectQueries) {
|
|
80105
|
+
byId.set(q.id, q);
|
|
80106
|
+
byText.set(q.query, q);
|
|
80107
|
+
}
|
|
80108
|
+
return { byId, byText };
|
|
80109
|
+
}
|
|
80110
|
+
function resolveCurrentQuery2(attribution, snap) {
|
|
80111
|
+
if (snap.queryId && attribution.byId.has(snap.queryId)) return attribution.byId.get(snap.queryId);
|
|
80112
|
+
if (snap.queryText && attribution.byText.has(snap.queryText)) return attribution.byText.get(snap.queryText);
|
|
80113
|
+
return void 0;
|
|
80114
|
+
}
|
|
80115
|
+
|
|
80116
|
+
// ../api-routes/src/doctor/checks/report-sweeps.ts
|
|
80117
|
+
function slotCoverage(expected, answered2) {
|
|
80118
|
+
return { signature: JSON.stringify([...expected].sort()), expected, answered: answered2 };
|
|
80119
|
+
}
|
|
80120
|
+
function inspectSweep(ctx, run) {
|
|
80121
|
+
const base = { runId: run.id, planVersionId: run.measurementPlanVersionId };
|
|
80122
|
+
if (run.measurementScope !== null) return { detail: { ...base, complete: false, reason: "spot-check" } };
|
|
80123
|
+
if (run.queries !== null) return { detail: { ...base, complete: false, reason: "query-subset" } };
|
|
80124
|
+
if (run.status !== RunStatuses.completed && run.status !== RunStatuses.partial) return { detail: { ...base, complete: false, reason: "not-terminal" } };
|
|
80125
|
+
const snapshots = ctx.db.select({
|
|
80126
|
+
queryId: querySnapshots.queryId,
|
|
80127
|
+
queryText: querySnapshots.queryText,
|
|
80128
|
+
provider: querySnapshots.provider,
|
|
80129
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
80130
|
+
answerText: querySnapshots.answerText,
|
|
80131
|
+
measurementExecutionId: querySnapshots.measurementExecutionId
|
|
80132
|
+
}).from(querySnapshots).where(eq58(querySnapshots.runId, run.id)).all();
|
|
80133
|
+
if (snapshots.length === 0) return { detail: { ...base, complete: false, reason: "no-answers" } };
|
|
80134
|
+
try {
|
|
80135
|
+
if (run.measurementPlanVersionId) {
|
|
80136
|
+
const version = ctx.db.select().from(measurementPlanVersions).where(and52(eq58(measurementPlanVersions.projectId, ctx.project.id), eq58(measurementPlanVersions.id, run.measurementPlanVersionId))).get();
|
|
80137
|
+
if (!version) return { detail: { ...base, complete: false, reason: "missing-plan" } };
|
|
80138
|
+
const plan = parseStoredMeasurementPlanAnyVersion(version.canonicalJson);
|
|
80139
|
+
const manifest = measurementRunExpectedSlots(run, plan);
|
|
80140
|
+
const expected2 = new Set(manifest.expectedSlots.map((slot) => measurementSlotKey(slot.executionId, slot.provider)));
|
|
80141
|
+
const answered3 = /* @__PURE__ */ new Set();
|
|
80142
|
+
let unbound = false;
|
|
80143
|
+
for (const snapshot of snapshots) {
|
|
80144
|
+
const executionId = snapshot.measurementExecutionId?.trim();
|
|
80145
|
+
if (!executionId) {
|
|
80146
|
+
unbound = true;
|
|
80147
|
+
continue;
|
|
80148
|
+
}
|
|
80149
|
+
const key = measurementSlotKey(executionId, snapshot.provider);
|
|
80150
|
+
if (expected2.has(key) && snapshot.answerText !== null) answered3.add(key);
|
|
80151
|
+
}
|
|
80152
|
+
const detail = {
|
|
80153
|
+
...base,
|
|
80154
|
+
schemaVersion: plan.schemaVersion,
|
|
80155
|
+
complete: expected2.size > 0 && !unbound && answered3.size === expected2.size,
|
|
80156
|
+
reason: unbound ? "unbound-answers" : "frozen-plan",
|
|
80157
|
+
expected: expected2.size,
|
|
80158
|
+
answered: answered3.size,
|
|
80159
|
+
providers: [...new Set(manifest.expectedSlots.map((slot) => slot.provider))],
|
|
80160
|
+
...plan.schemaVersion === MEASUREMENT_PLAN_V2_SCHEMA_VERSION ? {
|
|
80161
|
+
targetKeys: plan.targets.map((target) => target.stableKey),
|
|
80162
|
+
marketKeys: (plan.reportingScopes ?? []).map((market) => market.stableKey),
|
|
80163
|
+
queryClasses: [...new Set(plan.assignments.map((assignment) => assignment.queryClass))]
|
|
80164
|
+
} : {}
|
|
80165
|
+
};
|
|
80166
|
+
return unbound ? { detail } : { detail, coverage: slotCoverage(expected2, answered3) };
|
|
80167
|
+
}
|
|
80168
|
+
const stored = ctx.db.select().from(simpleMeasurementDefinitions).where(eq58(simpleMeasurementDefinitions.runId, run.id)).get();
|
|
80169
|
+
const frozen = stored ? simpleMeasurementDefinitionSchema.parse(stored.definition) : null;
|
|
80170
|
+
const project = ctx.db.select().from(projects).where(eq58(projects.id, ctx.project.id)).get();
|
|
80171
|
+
const basket = frozen ? frozen.queries.map((query) => ({ id: query.queryId, query: query.queryText })) : ctx.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq58(queries.projectId, project.id)).all();
|
|
80172
|
+
const providers = frozen ? frozen.engines.map((engine) => engine.provider) : project.providers.length > 0 ? project.providers : [...new Set(snapshots.map((snapshot) => snapshot.provider))];
|
|
80173
|
+
const attribution = buildQueryAttribution2(basket);
|
|
80174
|
+
const expected = new Set(basket.flatMap((query) => providers.map((provider) => JSON.stringify([query.id, provider.trim().toLowerCase()]))));
|
|
80175
|
+
const answered2 = /* @__PURE__ */ new Set();
|
|
80176
|
+
for (const snapshot of snapshots) {
|
|
80177
|
+
const query = resolveCurrentQuery2(attribution, snapshot);
|
|
80178
|
+
if (!query || snapshot.answerMentioned === null && !snapshot.answerText) continue;
|
|
80179
|
+
const key = JSON.stringify([query.id, snapshot.provider.trim().toLowerCase()]);
|
|
80180
|
+
if (expected.has(key)) answered2.add(key);
|
|
80181
|
+
}
|
|
80182
|
+
return {
|
|
80183
|
+
detail: { ...base, complete: expected.size > 0 && answered2.size === expected.size, reason: frozen ? "frozen-simple" : "current-basket", expected: expected.size, answered: answered2.size, providers },
|
|
80184
|
+
coverage: slotCoverage(expected, answered2)
|
|
80185
|
+
};
|
|
80186
|
+
} catch {
|
|
80187
|
+
return { detail: { ...base, complete: false, reason: "unreadable-evidence" } };
|
|
80188
|
+
}
|
|
80189
|
+
}
|
|
80190
|
+
function monthCoverage(evidence) {
|
|
80191
|
+
const groups = /* @__PURE__ */ new Map();
|
|
80192
|
+
for (const { detail, coverage } of evidence) {
|
|
80193
|
+
if (!coverage) continue;
|
|
80194
|
+
const group = groups.get(coverage.signature) ?? { expected: coverage.expected, answered: /* @__PURE__ */ new Set(), runIds: [] };
|
|
80195
|
+
for (const key of coverage.answered) group.answered.add(key);
|
|
80196
|
+
group.runIds.push(String(detail.runId));
|
|
80197
|
+
groups.set(coverage.signature, group);
|
|
80198
|
+
}
|
|
80199
|
+
const all = [...groups.values()];
|
|
80200
|
+
const ranked = all.filter((group) => !all.some((wider) => wider.expected.size > group.expected.size && [...group.expected].every((key) => wider.expected.has(key)))).map((group) => ({ ...group, covered: group.expected.size > 0 && [...group.expected].every((key) => group.answered.has(key)) })).sort((left, right) => Number(right.covered) - Number(left.covered) || right.answered.size / Math.max(1, right.expected.size) - left.answered.size / Math.max(1, left.expected.size));
|
|
80201
|
+
const best = ranked[0];
|
|
80202
|
+
return {
|
|
80203
|
+
eligibleRunIds: ranked.filter((group) => group.covered).flatMap((group) => group.runIds),
|
|
80204
|
+
coverage: best ? { expected: best.expected.size, answered: best.answered.size, sweeps: best.runIds.length } : null
|
|
80205
|
+
};
|
|
80206
|
+
}
|
|
80207
|
+
var reportSweepsCheck = {
|
|
80208
|
+
id: "report.sweeps",
|
|
80209
|
+
category: CheckCategories.schedules,
|
|
80210
|
+
scope: CheckScopes.project,
|
|
80211
|
+
notificationPolicy: CheckNotificationPolicies.silent,
|
|
80212
|
+
title: "Monthly report sweep readiness",
|
|
80213
|
+
run(ctx) {
|
|
80214
|
+
if (!ctx.project) return { status: CheckStatuses.skipped, code: "report.sweeps.no-project", summary: "Project context required." };
|
|
80215
|
+
const now = /* @__PURE__ */ new Date();
|
|
80216
|
+
const reportMonths = reportMonthsForDoctor(ctx.reportMonth, now);
|
|
80217
|
+
const graded = gradedReportMonths(reportMonths, now);
|
|
80218
|
+
const months = reportMonths.map((month) => {
|
|
80219
|
+
const bounds = calendarMonthBounds(month);
|
|
80220
|
+
const monthRuns = ctx.db.select().from(runs).where(and52(eq58(runs.projectId, ctx.project.id), eq58(runs.kind, RunKinds["answer-visibility"]), notProbeRun(), gte15(runs.createdAt, bounds.since), lte15(runs.createdAt, bounds.until))).orderBy(asc11(runs.createdAt), asc11(runs.id)).all();
|
|
80221
|
+
const evidence = monthRuns.map((run) => inspectSweep(ctx, run));
|
|
80222
|
+
return {
|
|
80223
|
+
month,
|
|
80224
|
+
// Through day 3 the new month is shown but only the closed month is graded.
|
|
80225
|
+
graded: graded.has(month),
|
|
80226
|
+
daysRemaining: Math.max(0, Math.ceil((Date.parse(bounds.until) - now.getTime()) / 864e5) - 1),
|
|
80227
|
+
...monthCoverage(evidence),
|
|
80228
|
+
runs: evidence.map((run) => run.detail)
|
|
80229
|
+
};
|
|
80230
|
+
});
|
|
80231
|
+
const missing = months.filter((month) => month.graded && month.eligibleRunIds.length === 0);
|
|
80232
|
+
return {
|
|
80233
|
+
status: missing.length ? CheckStatuses.warn : CheckStatuses.ok,
|
|
80234
|
+
code: missing.length ? "report.sweeps.missing" : "report.sweeps.ready",
|
|
80235
|
+
summary: missing.length ? `Whole-project sweeps do not yet answer every expected slot for ${missing.map((month) => `${month.month} (${month.daysRemaining} days left)`).join(", ")}.` : `Whole-project sweeps answer every expected slot for ${months.filter((month) => month.graded).map((month) => month.month).join(", ")}.`,
|
|
80236
|
+
remediation: missing.length ? `Review the monthly schedule and sweep scope for ${ctx.project.name}; running a sweep spends provider quota.` : null,
|
|
80237
|
+
details: { months }
|
|
80238
|
+
};
|
|
80239
|
+
}
|
|
80240
|
+
};
|
|
80241
|
+
|
|
80242
|
+
// ../api-routes/src/doctor/checks/report-models.ts
|
|
80243
|
+
import { and as and53, eq as eq59, gte as gte16, inArray as inArray23, lte as lte16 } from "drizzle-orm";
|
|
80244
|
+
var reportModelsCheck = {
|
|
80245
|
+
id: "report.models",
|
|
80246
|
+
category: CheckCategories.providers,
|
|
80247
|
+
scope: CheckScopes.project,
|
|
80248
|
+
notificationPolicy: CheckNotificationPolicies.silent,
|
|
80249
|
+
title: "Monthly report model continuity",
|
|
80250
|
+
run(ctx) {
|
|
80251
|
+
if (!ctx.project) return { status: CheckStatuses.skipped, code: "report.models.no-project", summary: "Project context required." };
|
|
80252
|
+
const now = /* @__PURE__ */ new Date();
|
|
80253
|
+
const reportMonths = reportMonthsForDoctor(ctx.reportMonth, now);
|
|
80254
|
+
const graded = gradedReportMonths(reportMonths, now);
|
|
80255
|
+
const months = reportMonths.flatMap((month) => {
|
|
80256
|
+
const bounds = calendarMonthBounds(month);
|
|
80257
|
+
const previous = new Date(Date.parse(bounds.since) - 1).toISOString().slice(0, 7);
|
|
80258
|
+
const observations = ctx.db.select({ provider: querySnapshots.provider, model: querySnapshots.model, at: runs.createdAt }).from(querySnapshots).innerJoin(runs, eq59(runs.id, querySnapshots.runId)).where(and53(eq59(runs.projectId, ctx.project.id), eq59(runs.kind, RunKinds["answer-visibility"]), inArray23(runs.status, [RunStatuses.completed, RunStatuses.partial]), notProbeRun(), gte16(runs.createdAt, bounds.since), lte16(runs.createdAt, bounds.until))).all();
|
|
80259
|
+
return readVisibilityContinuity(ctx.db, ctx.project.name, { from: previous, to: month }).map(({ frame, continuity: continuity2 }) => ({
|
|
80260
|
+
month,
|
|
80261
|
+
previousMonth: previous,
|
|
80262
|
+
// Through day 3 the new month is shown but only the closed month is graded.
|
|
80263
|
+
graded: graded.has(month),
|
|
80264
|
+
selection: {},
|
|
80265
|
+
frame,
|
|
80266
|
+
continuity: continuity2.status,
|
|
80267
|
+
providers: continuity2.providers.map((provider) => {
|
|
80268
|
+
const modelDates = provider.toModels.map((model) => ({
|
|
80269
|
+
model,
|
|
80270
|
+
firstObservedAt: observations.filter((observation) => observation.provider === provider.provider && observation.model?.trim() === model).map((observation) => observation.at).sort()[0] ?? null
|
|
80271
|
+
}));
|
|
80272
|
+
const addedModelDates = modelDates.filter((model) => !provider.fromModels.includes(model.model));
|
|
80273
|
+
return {
|
|
80274
|
+
...provider,
|
|
80275
|
+
// Dates are project observations in this report month, not provider deployment dates.
|
|
80276
|
+
firstObservedAtBasis: "new-project-provider-model-in-report-month",
|
|
80277
|
+
firstObservedAt: addedModelDates.map((model) => model.firstObservedAt).filter((at) => at !== null).sort()[0] ?? null,
|
|
80278
|
+
modelDates
|
|
80279
|
+
};
|
|
80280
|
+
})
|
|
80281
|
+
}));
|
|
80282
|
+
});
|
|
80283
|
+
const frameLabel = (month) => month.frame === "class" ? `${month.month}, class basket` : month.month;
|
|
80284
|
+
const gradedMonths = months.filter((month) => month.graded);
|
|
80285
|
+
const excluded = gradedMonths.flatMap((month) => month.providers.filter((provider) => provider.status !== "included").map((provider) => `${provider.provider}: ${provider.fromModels.join(", ") || "unknown"} \u2192 ${provider.toModels.join(", ") || "unknown"} (${frameLabel(month)}; ${provider.firstObservedAt ? `new model first observed ${provider.firstObservedAt}` : "change date unknown"})`));
|
|
80286
|
+
const insufficient = gradedMonths.filter((month) => month.providers.length === 0);
|
|
80287
|
+
const flagged = gradedMonths.find((month) => month.providers.length === 0 || month.providers.some((provider) => provider.status !== "included")) ?? gradedMonths[0];
|
|
80288
|
+
return {
|
|
80289
|
+
status: excluded.length || insufficient.length ? CheckStatuses.warn : CheckStatuses.ok,
|
|
80290
|
+
code: excluded.length ? "report.models.excluded" : insufficient.length ? "report.models.insufficient" : "report.models.continuous",
|
|
80291
|
+
summary: excluded.length ? `Monthly comparison excludes ${excluded.join("; ")}.` : insufficient.length ? `No common query/provider evidence establishes model continuity for ${insufficient.map(frameLabel).join(", ")}.` : "All compared providers have continuous snapshot model evidence.",
|
|
80292
|
+
remediation: excluded.length || insufficient.length ? `Inspect canonry visibility-compare ${ctx.project.name} --from ${flagged.previousMonth} --to ${flagged.month} --format json; do not pool excluded providers into a trend.` : null,
|
|
80293
|
+
details: { months }
|
|
80294
|
+
};
|
|
80295
|
+
}
|
|
80296
|
+
};
|
|
80297
|
+
|
|
80298
|
+
// ../api-routes/src/doctor/checks/report-daily-data.ts
|
|
80299
|
+
import { eq as eq60 } from "drizzle-orm";
|
|
80300
|
+
var REPORTING_LAG_DAYS = 3;
|
|
80301
|
+
function dateSpan(dates) {
|
|
80302
|
+
const sorted = [...dates].sort();
|
|
80303
|
+
return sorted.length === 0 ? null : { start: sorted[0], end: sorted.at(-1) };
|
|
80304
|
+
}
|
|
80305
|
+
function latestSyncRange(ctx, source) {
|
|
80306
|
+
const projectId = ctx.project.id;
|
|
80307
|
+
if (source === "ga") {
|
|
80308
|
+
const rows2 = ctx.db.select({ date: gaDailyTotals.date, syncedAt: gaDailyTotals.syncedAt }).from(gaDailyTotals).where(eq60(gaDailyTotals.projectId, projectId)).all();
|
|
80309
|
+
const latest2 = rows2.reduce((max2, row) => max2 === null || row.syncedAt > max2 ? row.syncedAt : max2, null);
|
|
80310
|
+
const summary3 = ctx.db.select({ start: gaTrafficSummaries.periodStart, end: gaTrafficSummaries.periodEnd, syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq60(gaTrafficSummaries.projectId, projectId)).get();
|
|
80311
|
+
if (summary3 && (latest2 === null || summary3.syncedAt >= latest2)) return { start: summary3.start, end: summary3.end, syncedAt: summary3.syncedAt };
|
|
80312
|
+
const span2 = dateSpan(rows2.filter((row) => row.syncedAt === latest2).map((row) => row.date));
|
|
80313
|
+
return span2 && latest2 !== null ? { ...span2, syncedAt: latest2 } : null;
|
|
80314
|
+
}
|
|
80315
|
+
const rows = ctx.db.select({ date: gscDailyTotals.date, createdAt: gscDailyTotals.createdAt }).from(gscDailyTotals).where(eq60(gscDailyTotals.projectId, projectId)).all();
|
|
80316
|
+
const latest = rows.reduce((max2, row) => max2 === null || row.createdAt > max2 ? row.createdAt : max2, null);
|
|
80317
|
+
const span = dateSpan(rows.filter((row) => row.createdAt === latest).map((row) => row.date));
|
|
80318
|
+
if (!span || latest === null) return null;
|
|
80319
|
+
const watermark = ctx.db.select({ through: gscDataWatermarks.syncedThroughDate, updatedAt: gscDataWatermarks.updatedAt }).from(gscDataWatermarks).where(eq60(gscDataWatermarks.projectId, projectId)).get();
|
|
80320
|
+
return watermark?.through && watermark.updatedAt === latest && watermark.through > span.end ? { start: span.start, end: watermark.through, syncedAt: latest } : { ...span, syncedAt: latest };
|
|
80321
|
+
}
|
|
80322
|
+
function dailyCoverage(ctx, month, source) {
|
|
80323
|
+
const project = ctx.project;
|
|
80324
|
+
const connection = source === "ga" ? ctx.ga4CredentialStore?.getConnection(project.name) ?? ctx.googleConnectionStore?.getConnection(project.canonicalDomain, "ga4") : ctx.googleConnectionStore?.getConnection(project.canonicalDomain, "gsc");
|
|
80325
|
+
const stored = source === "ga" ? ctx.db.select({ date: gaDailyTotals.date, count: gaDailyTotals.sessions }).from(gaDailyTotals).where(eq60(gaDailyTotals.projectId, project.id)).all() : ctx.db.select({ date: gscDailyTotals.date, count: gscDailyTotals.impressions }).from(gscDailyTotals).where(eq60(gscDailyTotals.projectId, project.id)).all();
|
|
80326
|
+
if (!connection && stored.length === 0) return null;
|
|
80327
|
+
const bounds = calendarMonthBounds(month);
|
|
80328
|
+
const start = bounds.since.slice(0, 10);
|
|
80329
|
+
const end = bounds.until.slice(0, 10);
|
|
80330
|
+
const timeZone = source === "gsc" ? "America/Los_Angeles" : "UTC";
|
|
80331
|
+
const matureThrough = isoDateDaysBeforeInTimeZone((/* @__PURE__ */ new Date()).toISOString(), REPORTING_LAG_DAYS, timeZone);
|
|
80332
|
+
const created = ctx.db.select({ date: projects.createdAt }).from(projects).where(eq60(projects.id, project.id)).get().date.slice(0, 10);
|
|
80333
|
+
const connectedAt = connection?.createdAt?.slice(0, 10) ?? created;
|
|
80334
|
+
const onboardingDate = [connectedAt, ...stored.map((row) => row.date)].sort()[0];
|
|
80335
|
+
const byDate = new Map(stored.map((row) => [row.date, row.count]));
|
|
80336
|
+
const coverage = latestSyncRange(ctx, source);
|
|
80337
|
+
const lagFloor = coverage ? isoDateDaysBeforeInTimeZone(coverage.syncedAt, REPORTING_LAG_DAYS, timeZone) : null;
|
|
80338
|
+
const zeroProvenThrough = coverage && lagFloor ? coverage.end < lagFloor ? coverage.end : lagFloor : null;
|
|
80339
|
+
const absent = [];
|
|
80340
|
+
let observedDays = 0;
|
|
80341
|
+
let observedZeroDays = 0;
|
|
80342
|
+
let queriedEmptyDays = 0;
|
|
80343
|
+
let pendingDays = 0;
|
|
80344
|
+
let beforeConnectionDays = 0;
|
|
80345
|
+
for (const date of calendarDateRange(start, end)) {
|
|
80346
|
+
if (date > matureThrough) {
|
|
80347
|
+
pendingDays++;
|
|
80348
|
+
continue;
|
|
80349
|
+
}
|
|
80350
|
+
if (date < onboardingDate) {
|
|
80351
|
+
beforeConnectionDays++;
|
|
80352
|
+
continue;
|
|
80353
|
+
}
|
|
80354
|
+
const count4 = byDate.get(date);
|
|
80355
|
+
if (count4 !== void 0) {
|
|
80356
|
+
observedDays++;
|
|
80357
|
+
if (count4 === 0) observedZeroDays++;
|
|
80358
|
+
} else if (coverage && zeroProvenThrough && date >= coverage.start && date <= zeroProvenThrough) queriedEmptyDays++;
|
|
80359
|
+
else absent.push(date);
|
|
80360
|
+
}
|
|
80361
|
+
return {
|
|
80362
|
+
source,
|
|
80363
|
+
start,
|
|
80364
|
+
end,
|
|
80365
|
+
matureThrough,
|
|
80366
|
+
timeZone,
|
|
80367
|
+
timeZoneBasis: source === "ga" ? "fallback" : "provider",
|
|
80368
|
+
onboardingDate,
|
|
80369
|
+
observedDays,
|
|
80370
|
+
observedZeroDays,
|
|
80371
|
+
// Queried by the latest sync and returned no row: the source reports no activity for them.
|
|
80372
|
+
latestSyncRange: coverage,
|
|
80373
|
+
zeroProvenThrough,
|
|
80374
|
+
queriedEmptyDays,
|
|
80375
|
+
unknownDays: absent.length,
|
|
80376
|
+
unknownRanges: groupIsoDateRanges(absent),
|
|
80377
|
+
// Only the latest sync's range is on record, so an absent date outside it may never have been queried.
|
|
80378
|
+
confirmedMissingDays: 0,
|
|
80379
|
+
pendingDays,
|
|
80380
|
+
beforeConnectionDays
|
|
80381
|
+
};
|
|
80382
|
+
}
|
|
80383
|
+
var reportDailyDataCheck = {
|
|
80384
|
+
id: "report.daily-data",
|
|
80385
|
+
category: CheckCategories.integrations,
|
|
80386
|
+
scope: CheckScopes.project,
|
|
80387
|
+
notificationPolicy: CheckNotificationPolicies.silent,
|
|
80388
|
+
title: "Monthly report daily data coverage",
|
|
80389
|
+
run(ctx) {
|
|
80390
|
+
if (!ctx.project) return { status: CheckStatuses.skipped, code: "report.daily-data.no-project", summary: "Project context required." };
|
|
80391
|
+
const months = reportMonthsForDoctor(ctx.reportMonth).map((month) => ({ month, sources: ["ga", "gsc"].flatMap((source) => {
|
|
80392
|
+
const coverage = dailyCoverage(ctx, month, source);
|
|
80393
|
+
return coverage ? [coverage] : [];
|
|
80394
|
+
}) }));
|
|
80395
|
+
const sources = months.flatMap((month) => month.sources);
|
|
80396
|
+
if (!sources.length) return { status: CheckStatuses.skipped, code: "report.daily-data.not-connected", summary: "No connected or stored GA4/Search Console evidence.", details: { months } };
|
|
80397
|
+
const unknown = sources.reduce((sum, source) => sum + source.unknownDays, 0);
|
|
80398
|
+
const mature = sources.some((source) => source.observedDays + source.queriedEmptyDays + source.unknownDays > 0);
|
|
80399
|
+
const gaUnknown = sources.some((source) => source.source === "ga" && source.unknownDays > 0);
|
|
80400
|
+
const oldest = months[0].month;
|
|
80401
|
+
const days = inclusiveDayCount(calendarMonthBounds(oldest).since.slice(0, 10), (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) ?? 1;
|
|
80402
|
+
const commands = [...new Set(sources.filter((source) => source.unknownDays > 0).map((source) => source.source === "ga" ? `canonry ga sync ${ctx.project.name} --days ${Math.min(90, days)}` : `canonry google sync ${ctx.project.name} --days ${Math.min(480, days)}`))];
|
|
80403
|
+
return {
|
|
80404
|
+
status: unknown ? CheckStatuses.warn : mature ? CheckStatuses.ok : CheckStatuses.skipped,
|
|
80405
|
+
code: unknown ? "report.daily-data.unknown" : mature ? "report.daily-data.observed" : "report.daily-data.pending",
|
|
80406
|
+
summary: unknown ? `${unknown} source-days have unknown coverage: no row, and not proven quiet by the latest sync (outside its range, or too close to when it ran for the source to have published them). Absent rows can mean zero activity or unsynced data.` : mature ? "Every mature date has an observed daily total or was queried by the latest sync and returned no activity." : "No mature dates since connection; reporting latency is excluded.",
|
|
80407
|
+
remediation: unknown ? `Review coverage and backfill: ${commands.map((command) => `\`${command}\``).join("; ")}. A sync whose window covers these dates records them as queried, so dates it returns no row for then count as zero activity.${gaUnknown && days > 90 ? " GA4 sync supports at most 90 days; older unknown dates need an external export." : ""}` : null,
|
|
80408
|
+
details: { months }
|
|
80409
|
+
};
|
|
80410
|
+
}
|
|
80411
|
+
};
|
|
80412
|
+
|
|
80032
80413
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
80033
80414
|
import crypto46 from "crypto";
|
|
80034
80415
|
import fs6 from "fs";
|
|
@@ -80357,7 +80738,7 @@ var skillsTriggerSurfaceCheck = {
|
|
|
80357
80738
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck, skillsTriggerSurfaceCheck];
|
|
80358
80739
|
|
|
80359
80740
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
80360
|
-
import { and as
|
|
80741
|
+
import { and as and54, eq as eq61 } from "drizzle-orm";
|
|
80361
80742
|
function skippedNoProject() {
|
|
80362
80743
|
return {
|
|
80363
80744
|
status: CheckStatuses.skipped,
|
|
@@ -80373,8 +80754,8 @@ var BACKLINKS_CHECKS = [
|
|
|
80373
80754
|
title: "Backlinks source connected",
|
|
80374
80755
|
run: (ctx) => {
|
|
80375
80756
|
if (!ctx.project) return skippedNoProject();
|
|
80376
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
80377
|
-
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(
|
|
80757
|
+
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(eq61(projects.id, ctx.project.id)).get();
|
|
80758
|
+
const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq61(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
|
|
80378
80759
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
80379
80760
|
const connected = [];
|
|
80380
80761
|
if (ccConnected) connected.push(BacklinkSources.commoncrawl);
|
|
@@ -80387,9 +80768,9 @@ var BACKLINKS_CHECKS = [
|
|
|
80387
80768
|
details: { commoncrawl: ccConnected }
|
|
80388
80769
|
};
|
|
80389
80770
|
}
|
|
80390
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
80391
|
-
|
|
80392
|
-
|
|
80771
|
+
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(and54(
|
|
80772
|
+
eq61(backlinkSummaries.projectId, ctx.project.id),
|
|
80773
|
+
eq61(backlinkSummaries.source, BacklinkSources.commoncrawl)
|
|
80393
80774
|
)).limit(1).get() : false;
|
|
80394
80775
|
return {
|
|
80395
80776
|
status: CheckStatuses.ok,
|
|
@@ -80549,7 +80930,7 @@ var BING_AUTH_CHECKS = [
|
|
|
80549
80930
|
];
|
|
80550
80931
|
|
|
80551
80932
|
// ../api-routes/src/doctor/checks/content.ts
|
|
80552
|
-
import { eq as
|
|
80933
|
+
import { eq as eq62 } from "drizzle-orm";
|
|
80553
80934
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
80554
80935
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
80555
80936
|
function skippedNoProject2() {
|
|
@@ -80562,7 +80943,7 @@ function skippedNoProject2() {
|
|
|
80562
80943
|
}
|
|
80563
80944
|
function loadProject(ctx) {
|
|
80564
80945
|
if (!ctx.project) return null;
|
|
80565
|
-
return ctx.db.select().from(projects).where(
|
|
80946
|
+
return ctx.db.select().from(projects).where(eq62(projects.id, ctx.project.id)).get() ?? null;
|
|
80566
80947
|
}
|
|
80567
80948
|
function percent(value) {
|
|
80568
80949
|
return Math.round(value * 100);
|
|
@@ -80654,7 +81035,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
80654
81035
|
);
|
|
80655
81036
|
|
|
80656
81037
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
80657
|
-
import { eq as
|
|
81038
|
+
import { eq as eq63 } from "drizzle-orm";
|
|
80658
81039
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
80659
81040
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
80660
81041
|
var adsConnectionCheck = {
|
|
@@ -80671,7 +81052,7 @@ var adsConnectionCheck = {
|
|
|
80671
81052
|
remediation: null
|
|
80672
81053
|
};
|
|
80673
81054
|
}
|
|
80674
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
81055
|
+
const row = ctx.db.select().from(adsConnections).where(eq63(adsConnections.projectId, ctx.project.id)).get();
|
|
80675
81056
|
if (!row) {
|
|
80676
81057
|
return {
|
|
80677
81058
|
status: CheckStatuses.skipped,
|
|
@@ -80721,7 +81102,7 @@ var adsRecentSyncCheck = {
|
|
|
80721
81102
|
remediation: null
|
|
80722
81103
|
};
|
|
80723
81104
|
}
|
|
80724
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
81105
|
+
const row = ctx.db.select().from(adsConnections).where(eq63(adsConnections.projectId, ctx.project.id)).get();
|
|
80725
81106
|
if (!row) {
|
|
80726
81107
|
return {
|
|
80727
81108
|
status: CheckStatuses.skipped,
|
|
@@ -80912,7 +81293,7 @@ var ga4ConnectionCheck = {
|
|
|
80912
81293
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
80913
81294
|
|
|
80914
81295
|
// ../api-routes/src/doctor/checks/data-freshness.ts
|
|
80915
|
-
import { and as
|
|
81296
|
+
import { and as and55, desc as desc28, eq as eq64, inArray as inArray24 } from "drizzle-orm";
|
|
80916
81297
|
var GA_DATA_AGING_DAYS = 3;
|
|
80917
81298
|
var GA_DATA_STALE_DAYS = 5;
|
|
80918
81299
|
var GSC_DATA_AGING_DAYS = 5;
|
|
@@ -80940,7 +81321,7 @@ function skipped(code, summary3) {
|
|
|
80940
81321
|
return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
|
|
80941
81322
|
}
|
|
80942
81323
|
function lastSyncAgeDays(ctx, kinds) {
|
|
80943
|
-
const row = ctx.db.select({ createdAt: runs.createdAt }).from(runs).where(
|
|
81324
|
+
const row = ctx.db.select({ createdAt: runs.createdAt }).from(runs).where(and55(eq64(runs.projectId, ctx.project.id), inArray24(runs.kind, kinds), eq64(runs.status, "completed"))).orderBy(desc28(runs.createdAt)).limit(1).get();
|
|
80944
81325
|
if (!row?.createdAt) return null;
|
|
80945
81326
|
const age = Math.floor((Date.now() - Date.parse(row.createdAt)) / DAY_MS2);
|
|
80946
81327
|
return Number.isFinite(age) ? age : null;
|
|
@@ -81036,7 +81417,7 @@ var gaRecentDataCheck = {
|
|
|
81036
81417
|
const connection = gaConnected(ctx);
|
|
81037
81418
|
if (connection === "store-unavailable") return skipped("ga.data.store-unavailable", "No GA4 credential store configured for this deployment.");
|
|
81038
81419
|
if (connection === "not-connected") return skipped("ga.data.not-connected", "GA4 is not connected for this project.");
|
|
81039
|
-
const newest = ctx.db.select({ date: gaDailyTotals.date }).from(gaDailyTotals).where(
|
|
81420
|
+
const newest = ctx.db.select({ date: gaDailyTotals.date }).from(gaDailyTotals).where(eq64(gaDailyTotals.projectId, ctx.project.id)).orderBy(desc28(gaDailyTotals.date)).limit(1).get();
|
|
81040
81421
|
return grade(ctx, GA_SPEC, newest?.date);
|
|
81041
81422
|
}
|
|
81042
81423
|
};
|
|
@@ -81051,9 +81432,9 @@ var gscRecentDataCheck = {
|
|
|
81051
81432
|
if (!ctx.googleConnectionStore.getConnection(ctx.project.canonicalDomain, "gsc")) {
|
|
81052
81433
|
return skipped("gsc.data.not-connected", "Search Console is not connected for this project.");
|
|
81053
81434
|
}
|
|
81054
|
-
const watermark = ctx.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(
|
|
81435
|
+
const watermark = ctx.db.select({ dataThroughDate: gscDataWatermarks.dataThroughDate }).from(gscDataWatermarks).where(eq64(gscDataWatermarks.projectId, ctx.project.id)).get();
|
|
81055
81436
|
if (watermark?.dataThroughDate) return grade(ctx, GSC_SPEC, watermark.dataThroughDate);
|
|
81056
|
-
const newest = ctx.db.select({ date: gscDailyTotals.date }).from(gscDailyTotals).where(
|
|
81437
|
+
const newest = ctx.db.select({ date: gscDailyTotals.date }).from(gscDailyTotals).where(eq64(gscDailyTotals.projectId, ctx.project.id)).orderBy(desc28(gscDailyTotals.date)).limit(1).get();
|
|
81057
81438
|
return grade(ctx, GSC_SPEC, newest?.date);
|
|
81058
81439
|
}
|
|
81059
81440
|
};
|
|
@@ -81321,7 +81702,7 @@ var siteReachabilityCheck = {
|
|
|
81321
81702
|
var SITE_REACHABILITY_CHECKS = [siteReachabilityCheck];
|
|
81322
81703
|
|
|
81323
81704
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
81324
|
-
import { and as
|
|
81705
|
+
import { and as and56, eq as eq65 } from "drizzle-orm";
|
|
81325
81706
|
var RECENT_SYNC_WARN_DAYS2 = 4;
|
|
81326
81707
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
81327
81708
|
function skippedNoProject3() {
|
|
@@ -81554,7 +81935,7 @@ var recentSyncCheck = {
|
|
|
81554
81935
|
title: "GBP recent sync",
|
|
81555
81936
|
run: (ctx) => {
|
|
81556
81937
|
if (!ctx.project) return skippedNoProject3();
|
|
81557
|
-
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(
|
|
81938
|
+
const selected = ctx.db.select({ locationName: gbpLocations.locationName, syncedAt: gbpLocations.syncedAt }).from(gbpLocations).where(and56(eq65(gbpLocations.projectId, ctx.project.id), eq65(gbpLocations.selected, true))).all();
|
|
81558
81939
|
if (selected.length === 0) {
|
|
81559
81940
|
return {
|
|
81560
81941
|
status: CheckStatuses.skipped,
|
|
@@ -81614,7 +81995,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
81614
81995
|
);
|
|
81615
81996
|
|
|
81616
81997
|
// ../api-routes/src/doctor/checks/places.ts
|
|
81617
|
-
import { eq as
|
|
81998
|
+
import { eq as eq66 } from "drizzle-orm";
|
|
81618
81999
|
var apiKeyCheck = {
|
|
81619
82000
|
id: "gbp.places.api-key",
|
|
81620
82001
|
category: CheckCategories.auth,
|
|
@@ -81659,7 +82040,7 @@ var apiKeyCheck = {
|
|
|
81659
82040
|
details: { tier: cfg.tier }
|
|
81660
82041
|
};
|
|
81661
82042
|
}
|
|
81662
|
-
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(
|
|
82043
|
+
const rows = ctx.db.select({ placeId: gbpLocations.placeId, selected: gbpLocations.selected }).from(gbpLocations).where(eq66(gbpLocations.projectId, ctx.project.id)).all();
|
|
81663
82044
|
const selected = rows.filter((r) => r.selected);
|
|
81664
82045
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
81665
82046
|
const details = {
|
|
@@ -82201,7 +82582,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
82201
82582
|
];
|
|
82202
82583
|
|
|
82203
82584
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
82204
|
-
import { and as
|
|
82585
|
+
import { and as and57, eq as eq67, gte as gte17, inArray as inArray25, ne as ne10, sql as sql25 } from "drizzle-orm";
|
|
82205
82586
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
82206
82587
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
82207
82588
|
function isCloudflareDirectPush(source) {
|
|
@@ -82240,8 +82621,8 @@ function skippedNoProject5() {
|
|
|
82240
82621
|
function loadProbes(ctx) {
|
|
82241
82622
|
if (!ctx.project) return [];
|
|
82242
82623
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
82243
|
-
|
|
82244
|
-
|
|
82624
|
+
and57(
|
|
82625
|
+
eq67(trafficSources.projectId, ctx.project.id),
|
|
82245
82626
|
ne10(trafficSources.status, TrafficSourceStatuses.archived)
|
|
82246
82627
|
)
|
|
82247
82628
|
).all();
|
|
@@ -82347,28 +82728,28 @@ var recentDataCheck = {
|
|
|
82347
82728
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
82348
82729
|
const recentCrawlers = Number(
|
|
82349
82730
|
ctx.db.select({ total: sql25`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
82350
|
-
|
|
82351
|
-
|
|
82352
|
-
|
|
82353
|
-
|
|
82731
|
+
and57(
|
|
82732
|
+
eq67(crawlerEventsHourly.projectId, ctx.project.id),
|
|
82733
|
+
inArray25(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
82734
|
+
gte17(crawlerEventsHourly.tsHour, warnCutoff)
|
|
82354
82735
|
)
|
|
82355
82736
|
).get()?.total ?? 0
|
|
82356
82737
|
);
|
|
82357
82738
|
const recentReferrals = Number(
|
|
82358
82739
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
82359
|
-
|
|
82360
|
-
|
|
82361
|
-
|
|
82362
|
-
|
|
82740
|
+
and57(
|
|
82741
|
+
eq67(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
82742
|
+
inArray25(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
82743
|
+
gte17(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
82363
82744
|
)
|
|
82364
82745
|
).get()?.total ?? 0
|
|
82365
82746
|
);
|
|
82366
82747
|
const recentUserFetches = Number(
|
|
82367
82748
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
82368
|
-
|
|
82369
|
-
|
|
82370
|
-
|
|
82371
|
-
|
|
82749
|
+
and57(
|
|
82750
|
+
eq67(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
82751
|
+
inArray25(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
82752
|
+
gte17(aiUserFetchEventsHourly.tsHour, warnCutoff)
|
|
82372
82753
|
)
|
|
82373
82754
|
).get()?.total ?? 0
|
|
82374
82755
|
);
|
|
@@ -82391,28 +82772,28 @@ var recentDataCheck = {
|
|
|
82391
82772
|
}
|
|
82392
82773
|
const olderCrawlers = Number(
|
|
82393
82774
|
ctx.db.select({ total: sql25`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
82394
|
-
|
|
82395
|
-
|
|
82396
|
-
|
|
82397
|
-
|
|
82775
|
+
and57(
|
|
82776
|
+
eq67(crawlerEventsHourly.projectId, ctx.project.id),
|
|
82777
|
+
inArray25(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
82778
|
+
gte17(crawlerEventsHourly.tsHour, failCutoff)
|
|
82398
82779
|
)
|
|
82399
82780
|
).get()?.total ?? 0
|
|
82400
82781
|
);
|
|
82401
82782
|
const olderReferrals = Number(
|
|
82402
82783
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
82403
|
-
|
|
82404
|
-
|
|
82405
|
-
|
|
82406
|
-
|
|
82784
|
+
and57(
|
|
82785
|
+
eq67(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
82786
|
+
inArray25(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
82787
|
+
gte17(aiReferralEventsHourly.tsHour, failCutoff)
|
|
82407
82788
|
)
|
|
82408
82789
|
).get()?.total ?? 0
|
|
82409
82790
|
);
|
|
82410
82791
|
const olderUserFetches = Number(
|
|
82411
82792
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
82412
|
-
|
|
82413
|
-
|
|
82414
|
-
|
|
82415
|
-
|
|
82793
|
+
and57(
|
|
82794
|
+
eq67(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
82795
|
+
inArray25(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
82796
|
+
gte17(aiUserFetchEventsHourly.tsHour, failCutoff)
|
|
82416
82797
|
)
|
|
82417
82798
|
).get()?.total ?? 0
|
|
82418
82799
|
);
|
|
@@ -83253,6 +83634,9 @@ var ALL_CHECKS = [
|
|
|
83253
83634
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
83254
83635
|
...GA_AUTH_CHECKS,
|
|
83255
83636
|
...DATA_FRESHNESS_CHECKS,
|
|
83637
|
+
reportSweepsCheck,
|
|
83638
|
+
reportModelsCheck,
|
|
83639
|
+
reportDailyDataCheck,
|
|
83256
83640
|
...ADS_CHECKS,
|
|
83257
83641
|
...GOOGLE_MARKETING_DOCTOR_CHECKS,
|
|
83258
83642
|
...PROVIDERS_CHECKS,
|
|
@@ -83263,6 +83647,9 @@ var ALL_CHECKS = [
|
|
|
83263
83647
|
// Network probe last, so a slow or unreachable site never delays the local checks.
|
|
83264
83648
|
...SITE_REACHABILITY_CHECKS
|
|
83265
83649
|
];
|
|
83650
|
+
function scheduledHealthCheckIds() {
|
|
83651
|
+
return ALL_CHECKS.filter((check2) => check2.scope === CheckScopes.project && !check2.optIn && check2.notificationPolicy !== CheckNotificationPolicies.silent).map((check2) => check2.id);
|
|
83652
|
+
}
|
|
83266
83653
|
|
|
83267
83654
|
// ../api-routes/src/doctor/runner.ts
|
|
83268
83655
|
function matchesCheckId(checkId, filters) {
|
|
@@ -83295,7 +83682,9 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83295
83682
|
} catch (err) {
|
|
83296
83683
|
const message = describeError(err);
|
|
83297
83684
|
output = {
|
|
83298
|
-
|
|
83685
|
+
// An advisory that cannot run is still only an advisory: it must not
|
|
83686
|
+
// fail the doctor (and its exit code) the way a broken health check does.
|
|
83687
|
+
status: definition.notificationPolicy === CheckNotificationPolicies.silent ? CheckStatuses.warn : CheckStatuses.fail,
|
|
83299
83688
|
code: `${definition.id}.runtime-error`,
|
|
83300
83689
|
summary: `Check threw an unexpected error: ${message}`,
|
|
83301
83690
|
remediation: null,
|
|
@@ -83308,6 +83697,7 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83308
83697
|
scope: definition.scope,
|
|
83309
83698
|
title: definition.title,
|
|
83310
83699
|
status: output.status,
|
|
83700
|
+
...definition.notificationPolicy ? { notificationPolicy: definition.notificationPolicy } : {},
|
|
83311
83701
|
code: output.code,
|
|
83312
83702
|
summary: output.summary,
|
|
83313
83703
|
remediation: output.remediation ?? null,
|
|
@@ -83318,6 +83708,7 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83318
83708
|
return {
|
|
83319
83709
|
scope: targetScope,
|
|
83320
83710
|
project: projectName,
|
|
83711
|
+
...ctx.project ? { reportMonths: reportMonthsForDoctor(ctx.reportMonth, startedAt) } : {},
|
|
83321
83712
|
generatedAt: startedAt.toISOString(),
|
|
83322
83713
|
durationMs: Date.now() - startedAt.getTime(),
|
|
83323
83714
|
summary: summarizeCheckResults(results),
|
|
@@ -83326,6 +83717,12 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83326
83717
|
}
|
|
83327
83718
|
|
|
83328
83719
|
// ../api-routes/src/doctor.ts
|
|
83720
|
+
function parseReportMonth(raw) {
|
|
83721
|
+
if (raw === void 0) return void 0;
|
|
83722
|
+
const parsed = reportMonthSchema.safeParse(raw);
|
|
83723
|
+
if (!parsed.success || raw > (/* @__PURE__ */ new Date()).toISOString().slice(0, 7)) throw validationError('"reportMonth" must be YYYY-MM and cannot be in the future');
|
|
83724
|
+
return parsed.data;
|
|
83725
|
+
}
|
|
83329
83726
|
function parseCheckIds(raw) {
|
|
83330
83727
|
if (!raw) return [];
|
|
83331
83728
|
return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
@@ -83340,6 +83737,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83340
83737
|
const checkIds = parseCheckIds(request.query.check);
|
|
83341
83738
|
const ctx = {
|
|
83342
83739
|
db: app.db,
|
|
83740
|
+
reportMonth: parseReportMonth(request.query.reportMonth),
|
|
83343
83741
|
project: null,
|
|
83344
83742
|
googleConnectionStore: opts.googleConnectionStore,
|
|
83345
83743
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -83367,6 +83765,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83367
83765
|
const checkIds = parseCheckIds(request.query.check);
|
|
83368
83766
|
const ctx = {
|
|
83369
83767
|
db: app.db,
|
|
83768
|
+
reportMonth: parseReportMonth(request.query.reportMonth),
|
|
83370
83769
|
project: {
|
|
83371
83770
|
id: project.id,
|
|
83372
83771
|
name: project.name,
|
|
@@ -83398,7 +83797,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83398
83797
|
|
|
83399
83798
|
// ../api-routes/src/discovery/routes.ts
|
|
83400
83799
|
import crypto47 from "crypto";
|
|
83401
|
-
import { and as
|
|
83800
|
+
import { and as and58, desc as desc29, eq as eq68, gte as gte18, inArray as inArray26, isNull as isNull11, or as or15 } from "drizzle-orm";
|
|
83402
83801
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
83403
83802
|
async function discoveryRoutes(app, opts) {
|
|
83404
83803
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -83432,13 +83831,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
83432
83831
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
83433
83832
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
83434
83833
|
const decision = app.db.transaction((tx) => {
|
|
83435
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
83436
|
-
|
|
83437
|
-
|
|
83834
|
+
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and58(
|
|
83835
|
+
eq68(discoverySessions.projectId, project.id),
|
|
83836
|
+
eq68(discoverySessions.icpDescription, icpDescription),
|
|
83438
83837
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
83439
83838
|
// semantics, so a request with a different (or no) buyer must start
|
|
83440
83839
|
// its own session, never adopt another buyer's probes.
|
|
83441
|
-
parsed.data.buyerDescription == null ? isNull11(discoverySessions.buyerDescription) :
|
|
83840
|
+
parsed.data.buyerDescription == null ? isNull11(discoverySessions.buyerDescription) : eq68(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
83442
83841
|
// Locations are identity too: a different service-area subset seeds
|
|
83443
83842
|
// and probes a different geo, so it must never reuse another geo's
|
|
83444
83843
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -83448,17 +83847,17 @@ async function discoveryRoutes(app, opts) {
|
|
|
83448
83847
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
83449
83848
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
83450
83849
|
// upgrade, never a wrong reuse.
|
|
83451
|
-
locations.length === 0 ? or15(isNull11(discoverySessions.locations),
|
|
83850
|
+
locations.length === 0 ? or15(isNull11(discoverySessions.locations), eq68(discoverySessions.locations, locations)) : eq68(discoverySessions.locations, locations),
|
|
83452
83851
|
// Seed provider set is identity: a different phrasing distribution
|
|
83453
83852
|
// must never reuse another set's session. Null (= Gemini-only
|
|
83454
83853
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
83455
|
-
seedProviders == null ? isNull11(discoverySessions.seedProviders) :
|
|
83456
|
-
|
|
83854
|
+
seedProviders == null ? isNull11(discoverySessions.seedProviders) : eq68(discoverySessions.seedProviders, seedProviders),
|
|
83855
|
+
inArray26(discoverySessions.status, [
|
|
83457
83856
|
DiscoverySessionStatuses.queued,
|
|
83458
83857
|
DiscoverySessionStatuses.seeding,
|
|
83459
83858
|
DiscoverySessionStatuses.probing
|
|
83460
83859
|
]),
|
|
83461
|
-
|
|
83860
|
+
gte18(discoverySessions.createdAt, ageFloorIso)
|
|
83462
83861
|
)).orderBy(desc29(discoverySessions.createdAt)).get();
|
|
83463
83862
|
if (existing && existing.runId) {
|
|
83464
83863
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -83528,7 +83927,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83528
83927
|
const project = resolveProject(app.db, request.params.name);
|
|
83529
83928
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
83530
83929
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
83531
|
-
const rows = app.db.select().from(discoverySessions).where(
|
|
83930
|
+
const rows = app.db.select().from(discoverySessions).where(eq68(discoverySessions.projectId, project.id)).orderBy(desc29(discoverySessions.createdAt)).limit(limit).all();
|
|
83532
83931
|
return reply.send(rows.map(serializeSession));
|
|
83533
83932
|
}
|
|
83534
83933
|
);
|
|
@@ -83536,11 +83935,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
83536
83935
|
"/projects/:name/discover/sessions/:id",
|
|
83537
83936
|
async (request, reply) => {
|
|
83538
83937
|
const project = resolveProject(app.db, request.params.name);
|
|
83539
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
83938
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83540
83939
|
if (!session || session.projectId !== project.id) {
|
|
83541
83940
|
throw notFound("Discovery session", request.params.id);
|
|
83542
83941
|
}
|
|
83543
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83942
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83544
83943
|
const detail = {
|
|
83545
83944
|
...serializeSession(session),
|
|
83546
83945
|
probes: probeRows.map(serializeProbe)
|
|
@@ -83552,7 +83951,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83552
83951
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
83553
83952
|
async (request, reply) => {
|
|
83554
83953
|
const project = resolveProject(app.db, request.params.name);
|
|
83555
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
83954
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83556
83955
|
if (!session || session.projectId !== project.id) {
|
|
83557
83956
|
throw notFound("Discovery session", request.params.id);
|
|
83558
83957
|
}
|
|
@@ -83560,7 +83959,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83560
83959
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
83561
83960
|
const applyAnchor = request.query.anchor !== "false";
|
|
83562
83961
|
const provider = session.seedProvider ?? "gemini";
|
|
83563
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83962
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83564
83963
|
const extract = opts.harvestSearchQueries;
|
|
83565
83964
|
const probesWithQueries = probeRows.map((row) => {
|
|
83566
83965
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -83571,7 +83970,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83571
83970
|
return { searchQueries: [] };
|
|
83572
83971
|
}
|
|
83573
83972
|
});
|
|
83574
|
-
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(
|
|
83973
|
+
const trackedQueries = app.db.select({ query: queries.query }).from(queries).where(eq68(queries.projectId, project.id)).all().map((r) => r.query);
|
|
83575
83974
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
83576
83975
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
83577
83976
|
effectiveDomains(project)
|
|
@@ -83618,12 +84017,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
83618
84017
|
"/projects/:name/discover/sessions/:id/promote",
|
|
83619
84018
|
async (request, reply) => {
|
|
83620
84019
|
const project = resolveProject(app.db, request.params.name);
|
|
83621
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
84020
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83622
84021
|
if (!session || session.projectId !== project.id) {
|
|
83623
84022
|
throw notFound("Discovery session", request.params.id);
|
|
83624
84023
|
}
|
|
83625
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83626
|
-
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
84024
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
84025
|
+
const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq68(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
|
|
83627
84026
|
const seenCompetitors = new Set(existingCompetitors);
|
|
83628
84027
|
const cited = /* @__PURE__ */ new Set();
|
|
83629
84028
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -83652,7 +84051,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83652
84051
|
);
|
|
83653
84052
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
83654
84053
|
const project = resolveProject(app.db, request.params.name);
|
|
83655
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
84054
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83656
84055
|
if (!session || session.projectId !== project.id) {
|
|
83657
84056
|
throw notFound("Discovery session", request.params.id);
|
|
83658
84057
|
}
|
|
@@ -83675,7 +84074,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83675
84074
|
const bucketSet = new Set(buckets);
|
|
83676
84075
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
83677
84076
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
83678
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
84077
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83679
84078
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
83680
84079
|
for (const probe of probeRows) {
|
|
83681
84080
|
if (!probe.bucket) continue;
|
|
@@ -83683,7 +84082,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83683
84082
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
83684
84083
|
}
|
|
83685
84084
|
const existingQueries = new Set(
|
|
83686
|
-
app.db.select({ query: queries.query }).from(queries).where(
|
|
84085
|
+
app.db.select({ query: queries.query }).from(queries).where(eq68(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
|
|
83687
84086
|
);
|
|
83688
84087
|
const promotedQueries = [];
|
|
83689
84088
|
const skippedQueries = [];
|
|
@@ -83699,7 +84098,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83699
84098
|
const skippedCompetitors = [];
|
|
83700
84099
|
if (includeCompetitors) {
|
|
83701
84100
|
const existingCompetitors = new Set(
|
|
83702
|
-
app.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
84101
|
+
app.db.select({ domain: competitors.domain }).from(competitors).where(eq68(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
|
|
83703
84102
|
);
|
|
83704
84103
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
83705
84104
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -83818,7 +84217,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
83818
84217
|
|
|
83819
84218
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
83820
84219
|
import crypto48 from "crypto";
|
|
83821
|
-
import { eq as
|
|
84220
|
+
import { eq as eq69 } from "drizzle-orm";
|
|
83822
84221
|
var DEFAULT_MAX_PROBES = 100;
|
|
83823
84222
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
83824
84223
|
function classifyProbeBucket(input) {
|
|
@@ -83905,7 +84304,7 @@ async function executeDiscovery(opts) {
|
|
|
83905
84304
|
status: DiscoverySessionStatuses.seeding,
|
|
83906
84305
|
dedupThreshold,
|
|
83907
84306
|
startedAt
|
|
83908
|
-
}).where(
|
|
84307
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
83909
84308
|
const seedResult = await opts.deps.seed({
|
|
83910
84309
|
project: opts.project,
|
|
83911
84310
|
icpDescription: opts.icpDescription,
|
|
@@ -83966,7 +84365,7 @@ async function executeDiscovery(opts) {
|
|
|
83966
84365
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
83967
84366
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
83968
84367
|
warning
|
|
83969
|
-
}).where(
|
|
84368
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
83970
84369
|
const probeLocation = opts.locations?.[0];
|
|
83971
84370
|
const probeResults = await mapWithConcurrency(
|
|
83972
84371
|
probedCanonicals,
|
|
@@ -84020,7 +84419,7 @@ async function executeDiscovery(opts) {
|
|
|
84020
84419
|
wastedCount: buckets["wasted-surface"],
|
|
84021
84420
|
competitorMap,
|
|
84022
84421
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
84023
|
-
}).where(
|
|
84422
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
84024
84423
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
84025
84424
|
return {
|
|
84026
84425
|
buckets,
|
|
@@ -84060,7 +84459,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
84060
84459
|
status: DiscoverySessionStatuses.failed,
|
|
84061
84460
|
error,
|
|
84062
84461
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
84063
|
-
}).where(
|
|
84462
|
+
}).where(eq69(discoverySessions.id, sessionId)).run();
|
|
84064
84463
|
}
|
|
84065
84464
|
function dedupeStrings(input) {
|
|
84066
84465
|
const seen2 = /* @__PURE__ */ new Set();
|
|
@@ -84078,7 +84477,7 @@ function dedupeStrings(input) {
|
|
|
84078
84477
|
|
|
84079
84478
|
// ../api-routes/src/technical-aeo.ts
|
|
84080
84479
|
import crypto49 from "crypto";
|
|
84081
|
-
import { and as
|
|
84480
|
+
import { and as and59, asc as asc12, count as count2, desc as desc30, eq as eq70, inArray as inArray27, isNotNull as isNotNull5, isNull as isNull12, lt as lt11, or as or16, sql as sql26 } from "drizzle-orm";
|
|
84082
84481
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
84083
84482
|
var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
|
|
84084
84483
|
...SiteCrawlFetchedStates,
|
|
@@ -84291,8 +84690,8 @@ function parseLinkKind(value) {
|
|
|
84291
84690
|
return allowed.data;
|
|
84292
84691
|
}
|
|
84293
84692
|
function linkKindFilter(linkKind) {
|
|
84294
|
-
if (linkKind === SiteHealthLinkKinds.content) return
|
|
84295
|
-
if (linkKind === SiteHealthLinkKinds.template) return
|
|
84693
|
+
if (linkKind === SiteHealthLinkKinds.content) return eq70(siteCrawlEdges.isTemplate, false);
|
|
84694
|
+
if (linkKind === SiteHealthLinkKinds.template) return eq70(siteCrawlEdges.isTemplate, true);
|
|
84296
84695
|
return void 0;
|
|
84297
84696
|
}
|
|
84298
84697
|
var graphSourceNode = alias(siteCrawlGraphNodes, "site_crawl_graph_source_node");
|
|
@@ -84397,50 +84796,50 @@ function changedFields(before, after, fields) {
|
|
|
84397
84796
|
async function technicalAeoRoutes(app, opts) {
|
|
84398
84797
|
const resolveCrawl = (projectId, runId) => {
|
|
84399
84798
|
const filters = [
|
|
84400
|
-
|
|
84401
|
-
|
|
84402
|
-
|
|
84403
|
-
|
|
84799
|
+
eq70(siteCrawlSnapshots.projectId, projectId),
|
|
84800
|
+
eq70(runs.projectId, projectId),
|
|
84801
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84802
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84404
84803
|
notProbeRun()
|
|
84405
84804
|
];
|
|
84406
84805
|
if (runId) {
|
|
84407
|
-
filters.push(
|
|
84806
|
+
filters.push(eq70(siteCrawlSnapshots.runId, runId));
|
|
84408
84807
|
} else {
|
|
84409
|
-
filters.push(
|
|
84808
|
+
filters.push(eq70(siteCrawlSnapshots.complete, true), eq70(runs.status, RunStatuses.completed));
|
|
84410
84809
|
}
|
|
84411
|
-
return app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
84810
|
+
return app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq70(siteCrawlSnapshots.runId, runs.id)).where(and59(...filters)).orderBy(desc30(siteCrawlSnapshots.createdAt), desc30(siteCrawlSnapshots.runId)).limit(1).get();
|
|
84412
84811
|
};
|
|
84413
84812
|
const detailScopeFor = (projectId, snapshot) => snapshot.detailsAvailable && snapshot.attemptId ? { projectId, runId: snapshot.runId, attemptId: snapshot.attemptId } : null;
|
|
84414
84813
|
const pageInScope = (scope, selector) => {
|
|
84415
84814
|
if (!selector.nodeKey && !selector.url) return null;
|
|
84416
|
-
return app.db.select().from(siteCrawlPages).where(
|
|
84417
|
-
|
|
84418
|
-
|
|
84419
|
-
|
|
84420
|
-
selector.nodeKey ?
|
|
84421
|
-
)).orderBy(
|
|
84815
|
+
return app.db.select().from(siteCrawlPages).where(and59(
|
|
84816
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
84817
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
84818
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
84819
|
+
selector.nodeKey ? eq70(siteCrawlPages.nodeKey, selector.nodeKey) : eq70(siteCrawlPages.url, selector.url)
|
|
84820
|
+
)).orderBy(asc12(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
|
|
84422
84821
|
};
|
|
84423
|
-
const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(
|
|
84424
|
-
|
|
84425
|
-
|
|
84426
|
-
|
|
84427
|
-
|
|
84428
|
-
)).orderBy(
|
|
84429
|
-
const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(
|
|
84430
|
-
|
|
84431
|
-
|
|
84432
|
-
|
|
84433
|
-
|
|
84822
|
+
const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(and59(
|
|
84823
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
84824
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
84825
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
84826
|
+
eq70(siteCrawlPages.depth, 0)
|
|
84827
|
+
)).orderBy(asc12(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
|
|
84828
|
+
const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(and59(
|
|
84829
|
+
eq70(runs.id, runId),
|
|
84830
|
+
eq70(runs.projectId, projectId),
|
|
84831
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84832
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84434
84833
|
notProbeRun()
|
|
84435
84834
|
)).limit(1).get());
|
|
84436
84835
|
const assertKnownAuditRun = (projectId, runId) => {
|
|
84437
84836
|
if (runId && !isSurfaceableAuditRun(projectId, runId)) throw notFound("Site crawl run", runId);
|
|
84438
84837
|
};
|
|
84439
|
-
const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs,
|
|
84440
|
-
|
|
84441
|
-
|
|
84442
|
-
|
|
84443
|
-
|
|
84838
|
+
const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs, eq70(siteAuditSnapshots.runId, runs.id)).where(and59(
|
|
84839
|
+
eq70(siteAuditSnapshots.projectId, projectId),
|
|
84840
|
+
eq70(runs.projectId, projectId),
|
|
84841
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84842
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84444
84843
|
notProbeRun()
|
|
84445
84844
|
)).limit(1).get());
|
|
84446
84845
|
const emptyCrawlSummary = (projectName, legacyAuditAvailable) => ({
|
|
@@ -84461,20 +84860,20 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84461
84860
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
84462
84861
|
const project = resolveProject(app.db, request.params.name);
|
|
84463
84862
|
const baseFilters = [
|
|
84464
|
-
|
|
84465
|
-
|
|
84466
|
-
|
|
84467
|
-
|
|
84863
|
+
eq70(siteAuditSnapshots.projectId, project.id),
|
|
84864
|
+
eq70(runs.projectId, project.id),
|
|
84865
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84866
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84468
84867
|
notProbeRun()
|
|
84469
84868
|
];
|
|
84470
|
-
const targetFilters = request.query.runId ? [...baseFilters,
|
|
84471
|
-
const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs,
|
|
84869
|
+
const targetFilters = request.query.runId ? [...baseFilters, eq70(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
|
|
84870
|
+
const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq70(siteAuditSnapshots.runId, runs.id)).where(and59(...targetFilters)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
84472
84871
|
if (!latest) {
|
|
84473
84872
|
if (request.query.runId) throw notFound("Site audit run", request.query.runId);
|
|
84474
84873
|
return emptyScore(project.name);
|
|
84475
84874
|
}
|
|
84476
84875
|
const snap = latest.snap;
|
|
84477
|
-
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs,
|
|
84876
|
+
const previous = app.db.select({ snap: siteAuditSnapshots }).from(siteAuditSnapshots).innerJoin(runs, eq70(siteAuditSnapshots.runId, runs.id)).where(and59(...baseFilters, lt11(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
|
|
84478
84877
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
84479
84878
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
84480
84879
|
return {
|
|
@@ -84501,14 +84900,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84501
84900
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
84502
84901
|
const project = resolveProject(app.db, request.params.name);
|
|
84503
84902
|
const targetFilters = [
|
|
84504
|
-
|
|
84505
|
-
|
|
84506
|
-
|
|
84507
|
-
|
|
84903
|
+
eq70(siteAuditSnapshots.projectId, project.id),
|
|
84904
|
+
eq70(runs.projectId, project.id),
|
|
84905
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84906
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84508
84907
|
notProbeRun()
|
|
84509
84908
|
];
|
|
84510
|
-
if (request.query.runId) targetFilters.push(
|
|
84511
|
-
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs,
|
|
84909
|
+
if (request.query.runId) targetFilters.push(eq70(siteAuditSnapshots.runId, request.query.runId));
|
|
84910
|
+
const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq70(siteAuditSnapshots.runId, runs.id)).where(and59(...targetFilters)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get();
|
|
84512
84911
|
if (!latest && request.query.runId) {
|
|
84513
84912
|
throw notFound("Site audit run", request.query.runId);
|
|
84514
84913
|
}
|
|
@@ -84516,14 +84915,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84516
84915
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
84517
84916
|
}
|
|
84518
84917
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
84519
|
-
const conds = [
|
|
84520
|
-
if (statusFilter) conds.push(
|
|
84521
|
-
const where =
|
|
84918
|
+
const conds = [eq70(siteAuditPages.projectId, project.id), eq70(siteAuditPages.runId, latest.runId)];
|
|
84919
|
+
if (statusFilter) conds.push(eq70(siteAuditPages.status, statusFilter));
|
|
84920
|
+
const where = and59(...conds);
|
|
84522
84921
|
const totalRow = app.db.select({ value: count2() }).from(siteAuditPages).where(where).get();
|
|
84523
84922
|
const total = totalRow?.value ?? 0;
|
|
84524
84923
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
84525
84924
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
84526
|
-
const orderBy = request.query.sort === "score-desc" ? desc30(siteAuditPages.overallScore) : request.query.sort === "url" ?
|
|
84925
|
+
const orderBy = request.query.sort === "score-desc" ? desc30(siteAuditPages.overallScore) : request.query.sort === "url" ? asc12(siteAuditPages.url) : asc12(siteAuditPages.overallScore);
|
|
84527
84926
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
84528
84927
|
const pages = rows.map((row) => ({
|
|
84529
84928
|
url: row.url,
|
|
@@ -84542,11 +84941,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84542
84941
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
84543
84942
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
84544
84943
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
84545
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
84546
|
-
|
|
84547
|
-
|
|
84548
|
-
|
|
84549
|
-
|
|
84944
|
+
}).from(siteAuditSnapshots).innerJoin(runs, eq70(siteAuditSnapshots.runId, runs.id)).where(and59(
|
|
84945
|
+
eq70(siteAuditSnapshots.projectId, project.id),
|
|
84946
|
+
eq70(runs.projectId, project.id),
|
|
84947
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84948
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84550
84949
|
notProbeRun()
|
|
84551
84950
|
)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
84552
84951
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -84640,10 +85039,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84640
85039
|
{ projectId: project.id, runId: snapshot.runId, attemptId: snapshot.attemptId },
|
|
84641
85040
|
snapshot.rootUrl
|
|
84642
85041
|
)?.nodeKey ?? null;
|
|
84643
|
-
const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(
|
|
84644
|
-
|
|
84645
|
-
|
|
84646
|
-
|
|
85042
|
+
const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(and59(
|
|
85043
|
+
eq70(siteCrawlGraphLayouts.projectId, project.id),
|
|
85044
|
+
eq70(siteCrawlGraphLayouts.runId, snapshot.runId),
|
|
85045
|
+
eq70(siteCrawlGraphLayouts.attemptId, snapshot.attemptId)
|
|
84647
85046
|
)).limit(1).get();
|
|
84648
85047
|
if (!persistedLayout) {
|
|
84649
85048
|
return {
|
|
@@ -84694,9 +85093,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84694
85093
|
const maxNodes = parseBoundedLimit(request.query.maxNodes, SITE_CRAWL_GRAPH_DEFAULT_MAX_NODES, SITE_CRAWL_GRAPH_MAX_NODES);
|
|
84695
85094
|
const maxEdges = parseBoundedLimit(request.query.maxEdges, SITE_CRAWL_GRAPH_DEFAULT_MAX_EDGES, SITE_CRAWL_GRAPH_MAX_EDGES);
|
|
84696
85095
|
const graphScope = [
|
|
84697
|
-
|
|
84698
|
-
|
|
84699
|
-
|
|
85096
|
+
eq70(siteCrawlGraphNodes.projectId, project.id),
|
|
85097
|
+
eq70(siteCrawlGraphNodes.runId, snapshot.runId),
|
|
85098
|
+
eq70(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
|
|
84700
85099
|
];
|
|
84701
85100
|
const nodeRows = app.db.select({
|
|
84702
85101
|
nodeKey: siteCrawlPages.nodeKey,
|
|
@@ -84715,17 +85114,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84715
85114
|
linkScoreNormalized: siteCrawlPages.linkScoreNormalized,
|
|
84716
85115
|
x: siteCrawlGraphNodes.x,
|
|
84717
85116
|
y: siteCrawlGraphNodes.y
|
|
84718
|
-
}).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages,
|
|
84719
|
-
|
|
84720
|
-
|
|
84721
|
-
|
|
84722
|
-
|
|
84723
|
-
)).where(
|
|
85117
|
+
}).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages, and59(
|
|
85118
|
+
eq70(siteCrawlPages.projectId, siteCrawlGraphNodes.projectId),
|
|
85119
|
+
eq70(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
|
|
85120
|
+
eq70(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
|
|
85121
|
+
eq70(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
|
|
85122
|
+
)).where(and59(...graphScope, lt11(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc12(siteCrawlGraphNodes.sampleRank)).all();
|
|
84724
85123
|
const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
|
|
84725
85124
|
...row,
|
|
84726
85125
|
healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey })
|
|
84727
85126
|
}));
|
|
84728
|
-
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 :
|
|
85127
|
+
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 : eq70(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
|
|
84729
85128
|
const edges = app.db.select({
|
|
84730
85129
|
edgeKey: siteCrawlGraphEdges.edgeKey,
|
|
84731
85130
|
sourceNodeKey: siteCrawlGraphEdges.sourceNodeKey,
|
|
@@ -84733,25 +85132,25 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84733
85132
|
followable: siteCrawlGraphEdges.followable,
|
|
84734
85133
|
occurrences: siteCrawlGraphEdges.occurrences,
|
|
84735
85134
|
isTemplate: siteCrawlGraphEdges.isTemplate
|
|
84736
|
-
}).from(siteCrawlGraphEdges).innerJoin(graphSourceNode,
|
|
84737
|
-
|
|
84738
|
-
|
|
84739
|
-
|
|
84740
|
-
|
|
85135
|
+
}).from(siteCrawlGraphEdges).innerJoin(graphSourceNode, and59(
|
|
85136
|
+
eq70(graphSourceNode.projectId, siteCrawlGraphEdges.projectId),
|
|
85137
|
+
eq70(graphSourceNode.runId, siteCrawlGraphEdges.runId),
|
|
85138
|
+
eq70(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
85139
|
+
eq70(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
|
|
84741
85140
|
lt11(graphSourceNode.sampleRank, maxNodes)
|
|
84742
|
-
)).innerJoin(graphTargetNode,
|
|
84743
|
-
|
|
84744
|
-
|
|
84745
|
-
|
|
84746
|
-
|
|
85141
|
+
)).innerJoin(graphTargetNode, and59(
|
|
85142
|
+
eq70(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
|
|
85143
|
+
eq70(graphTargetNode.runId, siteCrawlGraphEdges.runId),
|
|
85144
|
+
eq70(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
|
|
85145
|
+
eq70(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
|
|
84747
85146
|
lt11(graphTargetNode.sampleRank, maxNodes)
|
|
84748
|
-
)).where(
|
|
84749
|
-
|
|
84750
|
-
|
|
84751
|
-
|
|
85147
|
+
)).where(and59(
|
|
85148
|
+
eq70(siteCrawlGraphEdges.projectId, project.id),
|
|
85149
|
+
eq70(siteCrawlGraphEdges.runId, snapshot.runId),
|
|
85150
|
+
eq70(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
|
|
84752
85151
|
lt11(siteCrawlGraphEdges.sampleRank, maxEdges),
|
|
84753
85152
|
graphLinkKindFilter
|
|
84754
|
-
)).orderBy(
|
|
85153
|
+
)).orderBy(asc12(siteCrawlGraphEdges.sampleRank)).all();
|
|
84755
85154
|
const totalNodes = persistedLayout.totalNodes;
|
|
84756
85155
|
const totalEdges = persistedLayout.totalEdges;
|
|
84757
85156
|
const totalTemplateEdges = persistedLayout.totalTemplateEdges;
|
|
@@ -84842,17 +85241,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84842
85241
|
let frontier = [focus.nodeKey];
|
|
84843
85242
|
let queryTruncated = false;
|
|
84844
85243
|
for (let distance = 0; distance < hops && frontier.length > 0; distance += 1) {
|
|
84845
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
84846
|
-
|
|
84847
|
-
|
|
84848
|
-
|
|
84849
|
-
|
|
85244
|
+
const candidates = app.db.select().from(siteCrawlEdges).where(and59(
|
|
85245
|
+
eq70(siteCrawlEdges.projectId, scope.projectId),
|
|
85246
|
+
eq70(siteCrawlEdges.runId, scope.runId),
|
|
85247
|
+
eq70(siteCrawlEdges.attemptId, scope.attemptId),
|
|
85248
|
+
eq70(siteCrawlEdges.internal, true),
|
|
84850
85249
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
84851
85250
|
or16(
|
|
84852
|
-
|
|
84853
|
-
|
|
85251
|
+
inArray27(siteCrawlEdges.sourceNodeKey, frontier),
|
|
85252
|
+
inArray27(siteCrawlEdges.targetNodeKey, frontier)
|
|
84854
85253
|
)
|
|
84855
|
-
)).orderBy(
|
|
85254
|
+
)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
|
|
84856
85255
|
if (candidates.length > maxEdges + maxNodes) queryTruncated = true;
|
|
84857
85256
|
const next = /* @__PURE__ */ new Set();
|
|
84858
85257
|
for (const edge of candidates.slice(0, maxEdges + maxNodes)) {
|
|
@@ -84883,22 +85282,22 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84883
85282
|
frontier = [...next].sort();
|
|
84884
85283
|
}
|
|
84885
85284
|
const nodeKeys = [...distances.keys()];
|
|
84886
|
-
const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
84887
|
-
|
|
84888
|
-
|
|
84889
|
-
|
|
84890
|
-
|
|
85285
|
+
const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and59(
|
|
85286
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
85287
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
85288
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
85289
|
+
inArray27(siteCrawlPages.nodeKey, nodeKeys)
|
|
84891
85290
|
)).all();
|
|
84892
85291
|
const persistedNodeKeys = new Set(pageRows.map((row) => row.nodeKey));
|
|
84893
85292
|
const outermostNodeKeys = pageRows.filter((row) => distances.get(row.nodeKey) === hops).map((row) => row.nodeKey);
|
|
84894
|
-
const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(
|
|
84895
|
-
|
|
84896
|
-
|
|
84897
|
-
|
|
84898
|
-
|
|
85293
|
+
const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(and59(
|
|
85294
|
+
eq70(siteCrawlEdges.projectId, scope.projectId),
|
|
85295
|
+
eq70(siteCrawlEdges.runId, scope.runId),
|
|
85296
|
+
eq70(siteCrawlEdges.attemptId, scope.attemptId),
|
|
85297
|
+
eq70(siteCrawlEdges.internal, true),
|
|
84899
85298
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
84900
|
-
|
|
84901
|
-
|
|
85299
|
+
inArray27(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
|
|
85300
|
+
inArray27(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
|
|
84902
85301
|
)).limit(1).get();
|
|
84903
85302
|
if (omittedOutermostEdge && !seenEdgeKeys.has(omittedOutermostEdge.edgeKey)) {
|
|
84904
85303
|
omittedEdgeKeys.add(omittedOutermostEdge.edgeKey);
|
|
@@ -84993,23 +85392,23 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84993
85392
|
let found = fromPage.nodeKey === toPage.nodeKey;
|
|
84994
85393
|
let truncated = false;
|
|
84995
85394
|
for (let depth = 0; depth < maxDepth && frontier.length > 0 && !found; depth += 1) {
|
|
84996
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
84997
|
-
|
|
84998
|
-
|
|
84999
|
-
|
|
85000
|
-
|
|
85001
|
-
|
|
85002
|
-
|
|
85395
|
+
const candidates = app.db.select().from(siteCrawlEdges).where(and59(
|
|
85396
|
+
eq70(siteCrawlEdges.projectId, scope.projectId),
|
|
85397
|
+
eq70(siteCrawlEdges.runId, scope.runId),
|
|
85398
|
+
eq70(siteCrawlEdges.attemptId, scope.attemptId),
|
|
85399
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85400
|
+
eq70(siteCrawlEdges.followable, true),
|
|
85401
|
+
eq70(siteCrawlEdges.relation, "anchor"),
|
|
85003
85402
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
85004
|
-
|
|
85005
|
-
)).orderBy(
|
|
85403
|
+
inArray27(siteCrawlEdges.sourceNodeKey, frontier)
|
|
85404
|
+
)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
|
|
85006
85405
|
if (candidates.length > SITE_HEALTH_PATH_MAX_VISITED_NODES) truncated = true;
|
|
85007
85406
|
const candidateTargetKeys = [...new Set(candidates.map((edge) => edge.targetNodeKey).filter((nodeKey) => nodeKey != null))];
|
|
85008
|
-
const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(
|
|
85009
|
-
|
|
85010
|
-
|
|
85011
|
-
|
|
85012
|
-
|
|
85407
|
+
const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(and59(
|
|
85408
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
85409
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
85410
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
85411
|
+
inArray27(siteCrawlPages.nodeKey, candidateTargetKeys)
|
|
85013
85412
|
)).all().map((row) => row.nodeKey));
|
|
85014
85413
|
const next = /* @__PURE__ */ new Set();
|
|
85015
85414
|
for (const edge of candidates.slice(0, SITE_HEALTH_PATH_MAX_VISITED_NODES)) {
|
|
@@ -85051,11 +85450,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85051
85450
|
pathEdges.unshift(step.edge);
|
|
85052
85451
|
pathKeys.unshift(step.nodeKey);
|
|
85053
85452
|
}
|
|
85054
|
-
const pathRows = app.db.select().from(siteCrawlPages).where(
|
|
85055
|
-
|
|
85056
|
-
|
|
85057
|
-
|
|
85058
|
-
|
|
85453
|
+
const pathRows = app.db.select().from(siteCrawlPages).where(and59(
|
|
85454
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
85455
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
85456
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
85457
|
+
inArray27(siteCrawlPages.nodeKey, pathKeys)
|
|
85059
85458
|
)).all();
|
|
85060
85459
|
const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
|
|
85061
85460
|
const pathDetection = templateDetectionOf(snapshot.templateDetection);
|
|
@@ -85084,21 +85483,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85084
85483
|
throw validationError("change must be all, added, removed, or changed");
|
|
85085
85484
|
}
|
|
85086
85485
|
const completeSnapshotFilters = [
|
|
85087
|
-
|
|
85088
|
-
|
|
85089
|
-
|
|
85090
|
-
|
|
85091
|
-
|
|
85486
|
+
eq70(siteCrawlSnapshots.projectId, project.id),
|
|
85487
|
+
eq70(runs.projectId, project.id),
|
|
85488
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
85489
|
+
eq70(runs.status, RunStatuses.completed),
|
|
85490
|
+
eq70(siteCrawlSnapshots.complete, true),
|
|
85092
85491
|
notProbeRun()
|
|
85093
85492
|
];
|
|
85094
|
-
const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
85493
|
+
const afterTarget = request.query.toRunId ? resolveCrawl(project.id, request.query.toRunId) : app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq70(siteCrawlSnapshots.runId, runs.id)).where(and59(...completeSnapshotFilters)).orderBy(desc30(siteCrawlSnapshots.createdAt), desc30(siteCrawlSnapshots.runId)).limit(1).get();
|
|
85095
85494
|
if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
|
|
85096
|
-
const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
85495
|
+
const beforeTarget = request.query.fromRunId ? resolveCrawl(project.id, request.query.fromRunId) : afterTarget ? app.db.select({ snapshot: siteCrawlSnapshots, runStatus: runs.status }).from(siteCrawlSnapshots).innerJoin(runs, eq70(siteCrawlSnapshots.runId, runs.id)).where(and59(
|
|
85097
85496
|
...completeSnapshotFilters,
|
|
85098
85497
|
or16(
|
|
85099
85498
|
lt11(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
85100
|
-
|
|
85101
|
-
|
|
85499
|
+
and59(
|
|
85500
|
+
eq70(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
85102
85501
|
lt11(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
|
|
85103
85502
|
)
|
|
85104
85503
|
)
|
|
@@ -85333,18 +85732,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85333
85732
|
const visibleKeys = keyRows.slice(0, limit);
|
|
85334
85733
|
const pageKeys = visibleKeys.filter((row) => row.entity === "page").map((row) => row.key);
|
|
85335
85734
|
const linkKeys = visibleKeys.filter((row) => row.entity === "link").map((row) => row.key);
|
|
85336
|
-
const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
85337
|
-
|
|
85338
|
-
|
|
85339
|
-
|
|
85340
|
-
|
|
85735
|
+
const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(and59(
|
|
85736
|
+
eq70(siteCrawlPages.projectId, scope.projectId),
|
|
85737
|
+
eq70(siteCrawlPages.runId, scope.runId),
|
|
85738
|
+
eq70(siteCrawlPages.attemptId, scope.attemptId),
|
|
85739
|
+
inArray27(siteCrawlPages.nodeKey, pageKeys)
|
|
85341
85740
|
)).all();
|
|
85342
|
-
const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(
|
|
85343
|
-
|
|
85344
|
-
|
|
85345
|
-
|
|
85346
|
-
|
|
85347
|
-
|
|
85741
|
+
const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(and59(
|
|
85742
|
+
eq70(siteCrawlEdges.projectId, scope.projectId),
|
|
85743
|
+
eq70(siteCrawlEdges.runId, scope.runId),
|
|
85744
|
+
eq70(siteCrawlEdges.attemptId, scope.attemptId),
|
|
85745
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85746
|
+
inArray27(siteCrawlEdges.edgeKey, linkKeys)
|
|
85348
85747
|
)).all();
|
|
85349
85748
|
const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
85350
85749
|
const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
@@ -85469,30 +85868,30 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85469
85868
|
return { project: project.name, hasCrawlData: true, runId: snapshot.runId, total: 0, nextCursor: null, healthStateFilter: null, pages: [] };
|
|
85470
85869
|
}
|
|
85471
85870
|
const filters = [
|
|
85472
|
-
|
|
85473
|
-
|
|
85474
|
-
|
|
85871
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
85872
|
+
eq70(siteCrawlPages.runId, snapshot.runId),
|
|
85873
|
+
eq70(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
85475
85874
|
];
|
|
85476
|
-
if (request.query.nodeKey) filters.push(
|
|
85875
|
+
if (request.query.nodeKey) filters.push(eq70(siteCrawlPages.nodeKey, request.query.nodeKey));
|
|
85477
85876
|
const inventoryEligible = parseBoolean2(request.query.inventoryEligible);
|
|
85478
|
-
if (inventoryEligible != null) filters.push(
|
|
85479
|
-
if (request.query.fetchState) filters.push(
|
|
85480
|
-
if (request.query.indexabilityState) filters.push(
|
|
85481
|
-
if (request.query.auditState) filters.push(
|
|
85877
|
+
if (inventoryEligible != null) filters.push(eq70(siteCrawlPages.inventoryEligible, inventoryEligible));
|
|
85878
|
+
if (request.query.fetchState) filters.push(eq70(siteCrawlPages.fetchState, request.query.fetchState));
|
|
85879
|
+
if (request.query.indexabilityState) filters.push(eq70(siteCrawlPages.indexabilityState, request.query.indexabilityState));
|
|
85880
|
+
if (request.query.auditState) filters.push(eq70(siteCrawlPages.auditState, request.query.auditState));
|
|
85482
85881
|
const healthState = parseSiteHealthState(request.query.healthState);
|
|
85483
85882
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85484
85883
|
const offset = decodeCursor(request.query.cursor);
|
|
85485
|
-
const orderBy = request.query.sort === "score-desc" ? [desc30(siteCrawlPages.auditScore),
|
|
85884
|
+
const orderBy = request.query.sort === "score-desc" ? [desc30(siteCrawlPages.auditScore), asc12(siteCrawlPages.nodeKey)] : request.query.sort === "score-asc" ? [asc12(siteCrawlPages.auditScore), asc12(siteCrawlPages.nodeKey)] : request.query.sort === "path" ? [asc12(siteCrawlPages.path), asc12(siteCrawlPages.nodeKey)] : [asc12(siteCrawlPages.url), asc12(siteCrawlPages.nodeKey)];
|
|
85486
85885
|
let healthStateFilter = null;
|
|
85487
85886
|
if (healthState) {
|
|
85488
|
-
const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(
|
|
85489
|
-
|
|
85490
|
-
|
|
85491
|
-
|
|
85887
|
+
const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(and59(
|
|
85888
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
85889
|
+
eq70(siteCrawlPages.runId, snapshot.runId),
|
|
85890
|
+
eq70(siteCrawlPages.attemptId, snapshot.attemptId),
|
|
85492
85891
|
isNull12(siteCrawlPages.healthState)
|
|
85493
85892
|
)).limit(1).get();
|
|
85494
85893
|
healthStateFilter = legacyRow ? "unavailable-legacy-scan" : "applied";
|
|
85495
|
-
if (healthStateFilter === "applied") filters.push(
|
|
85894
|
+
if (healthStateFilter === "applied") filters.push(eq70(siteCrawlPages.healthState, healthState));
|
|
85496
85895
|
}
|
|
85497
85896
|
if (healthStateFilter === "unavailable-legacy-scan") {
|
|
85498
85897
|
return {
|
|
@@ -85505,7 +85904,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85505
85904
|
pages: []
|
|
85506
85905
|
};
|
|
85507
85906
|
}
|
|
85508
|
-
const where =
|
|
85907
|
+
const where = and59(...filters);
|
|
85509
85908
|
const total = app.db.select({ value: count2() }).from(siteCrawlPages).where(where).get()?.value ?? 0;
|
|
85510
85909
|
const rows = app.db.select().from(siteCrawlPages).where(where).orderBy(...orderBy).limit(limit).offset(offset).all();
|
|
85511
85910
|
const nextOffset = offset + rows.length;
|
|
@@ -85536,10 +85935,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85536
85935
|
url: siteCrawlPages.url,
|
|
85537
85936
|
inventoryEligible: siteCrawlPages.inventoryEligible,
|
|
85538
85937
|
fetchState: siteCrawlPages.fetchState
|
|
85539
|
-
}).from(siteCrawlPages).where(
|
|
85540
|
-
|
|
85541
|
-
|
|
85542
|
-
|
|
85938
|
+
}).from(siteCrawlPages).where(and59(
|
|
85939
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
85940
|
+
eq70(siteCrawlPages.runId, snapshot.runId),
|
|
85941
|
+
eq70(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
85543
85942
|
)).limit(MAX_STRUCTURE_SOURCE_ROWS + 1).all();
|
|
85544
85943
|
if (sourceRows.length > MAX_STRUCTURE_SOURCE_ROWS) {
|
|
85545
85944
|
throw validationError(`Persisted crawl exceeds the ${MAX_STRUCTURE_SOURCE_ROWS}-page structure limit`);
|
|
@@ -85618,21 +86017,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85618
86017
|
};
|
|
85619
86018
|
}
|
|
85620
86019
|
const filters = [
|
|
85621
|
-
|
|
85622
|
-
|
|
85623
|
-
|
|
85624
|
-
|
|
86020
|
+
eq70(siteCrawlEdges.projectId, project.id),
|
|
86021
|
+
eq70(siteCrawlEdges.runId, snapshot.runId),
|
|
86022
|
+
eq70(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
86023
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85625
86024
|
linkKindFilter(linkKind)
|
|
85626
86025
|
];
|
|
85627
|
-
if (request.query.sourceUrl) filters.push(
|
|
85628
|
-
if (request.query.targetUrl) filters.push(
|
|
86026
|
+
if (request.query.sourceUrl) filters.push(eq70(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
|
|
86027
|
+
if (request.query.targetUrl) filters.push(eq70(siteCrawlEdges.targetUrl, request.query.targetUrl));
|
|
85629
86028
|
const followable = parseBoolean2(request.query.followable);
|
|
85630
|
-
if (followable != null) filters.push(
|
|
85631
|
-
const where =
|
|
86029
|
+
if (followable != null) filters.push(eq70(siteCrawlEdges.followable, followable));
|
|
86030
|
+
const where = and59(...filters);
|
|
85632
86031
|
const total = app.db.select({ value: count2() }).from(siteCrawlEdges).where(where).get()?.value ?? 0;
|
|
85633
86032
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85634
86033
|
const offset = decodeCursor(request.query.cursor);
|
|
85635
|
-
const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(
|
|
86034
|
+
const rows = app.db.select().from(siteCrawlEdges).where(where).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
|
|
85636
86035
|
const nextOffset = offset + rows.length;
|
|
85637
86036
|
return {
|
|
85638
86037
|
project: project.name,
|
|
@@ -85686,17 +86085,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85686
86085
|
};
|
|
85687
86086
|
}
|
|
85688
86087
|
const scope = [
|
|
85689
|
-
|
|
85690
|
-
|
|
85691
|
-
|
|
85692
|
-
|
|
86088
|
+
eq70(siteCrawlEdges.projectId, project.id),
|
|
86089
|
+
eq70(siteCrawlEdges.runId, snapshot.runId),
|
|
86090
|
+
eq70(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
86091
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85693
86092
|
linkKindFilter(linkKind)
|
|
85694
86093
|
];
|
|
85695
|
-
const inboundMatch = request.query.nodeKey && request.query.url ? or16(
|
|
85696
|
-
const outboundMatch = request.query.nodeKey && request.query.url ? or16(
|
|
86094
|
+
const inboundMatch = request.query.nodeKey && request.query.url ? or16(eq70(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq70(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq70(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq70(siteCrawlEdges.targetUrl, request.query.url);
|
|
86095
|
+
const outboundMatch = request.query.nodeKey && request.query.url ? or16(eq70(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq70(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq70(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq70(siteCrawlEdges.sourceUrl, request.query.url);
|
|
85697
86096
|
const limit = parseBoundedLimit(request.query.limit, 50, 100);
|
|
85698
|
-
const inboundRows = app.db.select().from(siteCrawlEdges).where(
|
|
85699
|
-
const outboundRows = app.db.select().from(siteCrawlEdges).where(
|
|
86097
|
+
const inboundRows = app.db.select().from(siteCrawlEdges).where(and59(...scope, inboundMatch)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
86098
|
+
const outboundRows = app.db.select().from(siteCrawlEdges).where(and59(...scope, outboundMatch)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
|
|
85700
86099
|
return {
|
|
85701
86100
|
project: project.name,
|
|
85702
86101
|
hasCrawlData: true,
|
|
@@ -85729,11 +86128,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85729
86128
|
if (!snapshot.attemptId) {
|
|
85730
86129
|
return { project: project.name, runId: snapshot.runId, state: "partial", checkDeadLinks: true, checked: snapshot.deadLinksChecked, found: snapshot.deadLinksFound, unverified: snapshot.deadLinksUnverified, total: 0, nextCursor: null, deadLinks: [] };
|
|
85731
86130
|
}
|
|
85732
|
-
const where =
|
|
85733
|
-
|
|
85734
|
-
|
|
85735
|
-
|
|
85736
|
-
|
|
86131
|
+
const where = and59(
|
|
86132
|
+
eq70(siteCrawlFindings.projectId, project.id),
|
|
86133
|
+
eq70(siteCrawlFindings.runId, snapshot.runId),
|
|
86134
|
+
eq70(siteCrawlFindings.attemptId, snapshot.attemptId),
|
|
86135
|
+
eq70(siteCrawlFindings.findingType, "dead-link"),
|
|
85737
86136
|
// Belt and braces on the client-facing surface: a row without a status
|
|
85738
86137
|
// code is not evidence of a broken link, and this response is where such
|
|
85739
86138
|
// a row would be read as one. Migration 140 removed every stored row that
|
|
@@ -85748,7 +86147,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85748
86147
|
const total = app.db.select({ value: count2() }).from(siteCrawlFindings).where(where).get()?.value ?? 0;
|
|
85749
86148
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85750
86149
|
const offset = decodeCursor(request.query.cursor);
|
|
85751
|
-
const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(
|
|
86150
|
+
const rows = app.db.select().from(siteCrawlFindings).where(where).orderBy(asc12(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
|
|
85752
86151
|
const nextOffset = offset + rows.length;
|
|
85753
86152
|
const deadLinks = rows.map((row) => ({
|
|
85754
86153
|
findingKey: row.findingKey,
|
|
@@ -85782,19 +86181,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85782
86181
|
createdAt: runs.createdAt,
|
|
85783
86182
|
startedAt: runs.startedAt,
|
|
85784
86183
|
finishedAt: runs.finishedAt
|
|
85785
|
-
}).from(runs).where(
|
|
85786
|
-
|
|
85787
|
-
|
|
86184
|
+
}).from(runs).where(and59(
|
|
86185
|
+
eq70(runs.projectId, project.id),
|
|
86186
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
85788
86187
|
notProbeRun()
|
|
85789
86188
|
)).orderBy(desc30(runs.createdAt), desc30(runs.id)).limit(limit).all();
|
|
85790
86189
|
const runIds = rows.map((row) => row.runId);
|
|
85791
|
-
const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
85792
|
-
|
|
85793
|
-
|
|
85794
|
-
|
|
85795
|
-
|
|
86190
|
+
const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs, eq70(siteCrawlSnapshots.runId, runs.id)).where(and59(
|
|
86191
|
+
eq70(siteCrawlSnapshots.projectId, project.id),
|
|
86192
|
+
eq70(runs.projectId, project.id),
|
|
86193
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
86194
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
85796
86195
|
notProbeRun(),
|
|
85797
|
-
|
|
86196
|
+
inArray27(siteCrawlSnapshots.runId, runIds)
|
|
85798
86197
|
)).all().map((row) => row.runId));
|
|
85799
86198
|
return {
|
|
85800
86199
|
project: project.name,
|
|
@@ -85807,21 +86206,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85807
86206
|
});
|
|
85808
86207
|
app.get("/projects/:name/technical-aeo/runs/:runId/progress", async (request) => {
|
|
85809
86208
|
const project = resolveProject(app.db, request.params.name);
|
|
85810
|
-
const run = app.db.select().from(runs).where(
|
|
85811
|
-
|
|
85812
|
-
|
|
85813
|
-
|
|
86209
|
+
const run = app.db.select().from(runs).where(and59(
|
|
86210
|
+
eq70(runs.id, request.params.runId),
|
|
86211
|
+
eq70(runs.projectId, project.id),
|
|
86212
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
85814
86213
|
notProbeRun()
|
|
85815
86214
|
)).get();
|
|
85816
86215
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
85817
|
-
const attempt = app.db.select().from(siteCrawlAttempts).where(
|
|
85818
|
-
|
|
85819
|
-
|
|
86216
|
+
const attempt = app.db.select().from(siteCrawlAttempts).where(and59(
|
|
86217
|
+
eq70(siteCrawlAttempts.projectId, project.id),
|
|
86218
|
+
eq70(siteCrawlAttempts.runId, run.id)
|
|
85820
86219
|
)).orderBy(desc30(siteCrawlAttempts.attemptNumber), desc30(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
85821
|
-
const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(
|
|
85822
|
-
|
|
85823
|
-
|
|
85824
|
-
|
|
86220
|
+
const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(and59(
|
|
86221
|
+
eq70(siteCrawlGraphLayouts.projectId, project.id),
|
|
86222
|
+
eq70(siteCrawlGraphLayouts.runId, run.id),
|
|
86223
|
+
eq70(siteCrawlGraphLayouts.attemptId, attempt.id)
|
|
85825
86224
|
)).get() : void 0;
|
|
85826
86225
|
const layoutState = persistedLayout?.state === "ready" || persistedLayout?.state === "unavailable" ? persistedLayout.state : run.status === RunStatuses.completed || run.status === RunStatuses.partial ? "unavailable" : "pending";
|
|
85827
86226
|
return {
|
|
@@ -85852,18 +86251,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85852
86251
|
};
|
|
85853
86252
|
});
|
|
85854
86253
|
app.get("/projects/:name/technical-aeo/runs/:runId/page-health-preview", async (request) => app.db.transaction((tx) => {
|
|
85855
|
-
const project = tx.select().from(projects).where(
|
|
86254
|
+
const project = tx.select().from(projects).where(eq70(projects.name, request.params.name)).get();
|
|
85856
86255
|
if (!project) throw notFound("Project", request.params.name);
|
|
85857
|
-
const run = tx.select().from(runs).where(
|
|
85858
|
-
|
|
85859
|
-
|
|
85860
|
-
|
|
86256
|
+
const run = tx.select().from(runs).where(and59(
|
|
86257
|
+
eq70(runs.id, request.params.runId),
|
|
86258
|
+
eq70(runs.projectId, project.id),
|
|
86259
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
85861
86260
|
notProbeRun()
|
|
85862
86261
|
)).get();
|
|
85863
86262
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
85864
|
-
const attempt = tx.select().from(siteCrawlAttempts).where(
|
|
85865
|
-
|
|
85866
|
-
|
|
86263
|
+
const attempt = tx.select().from(siteCrawlAttempts).where(and59(
|
|
86264
|
+
eq70(siteCrawlAttempts.projectId, project.id),
|
|
86265
|
+
eq70(siteCrawlAttempts.runId, run.id)
|
|
85867
86266
|
)).orderBy(desc30(siteCrawlAttempts.attemptNumber), desc30(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
85868
86267
|
const state = run.status === RunStatuses.queued ? "waiting" : run.status === RunStatuses.running ? "collecting" : "terminal";
|
|
85869
86268
|
const base = {
|
|
@@ -85877,11 +86276,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85877
86276
|
if (!attempt) {
|
|
85878
86277
|
return { ...base, pagesAudited: 0, examples: [] };
|
|
85879
86278
|
}
|
|
85880
|
-
const auditedWhere =
|
|
85881
|
-
|
|
85882
|
-
|
|
85883
|
-
|
|
85884
|
-
|
|
86279
|
+
const auditedWhere = and59(
|
|
86280
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
86281
|
+
eq70(siteCrawlPages.runId, run.id),
|
|
86282
|
+
eq70(siteCrawlPages.attemptId, attempt.id),
|
|
86283
|
+
eq70(siteCrawlPages.auditState, "success")
|
|
85885
86284
|
);
|
|
85886
86285
|
const pagesAudited = tx.select({ value: count2() }).from(siteCrawlPages).where(auditedWhere).get()?.value ?? 0;
|
|
85887
86286
|
if (state !== "collecting") {
|
|
@@ -85892,11 +86291,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85892
86291
|
url: siteCrawlPages.url,
|
|
85893
86292
|
auditScore: siteCrawlPages.auditScore,
|
|
85894
86293
|
auditFields: siteCrawlPages.auditFields
|
|
85895
|
-
}).from(siteCrawlPages).where(
|
|
86294
|
+
}).from(siteCrawlPages).where(and59(
|
|
85896
86295
|
auditedWhere,
|
|
85897
86296
|
isNotNull5(siteCrawlPages.auditScore),
|
|
85898
86297
|
lt11(siteCrawlPages.auditScore, 70)
|
|
85899
|
-
)).orderBy(
|
|
86298
|
+
)).orderBy(asc12(siteCrawlPages.auditScore), asc12(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
|
|
85900
86299
|
const examples = candidates.flatMap((candidate) => {
|
|
85901
86300
|
if (candidate.auditScore == null) return [];
|
|
85902
86301
|
const checksNeedingAttention = countLivePageHealthChecks(candidate.auditFields);
|
|
@@ -85928,13 +86327,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85928
86327
|
status: runs.status,
|
|
85929
86328
|
identityKey: siteCrawlRunRequests.identityKey,
|
|
85930
86329
|
effectiveOptions: siteCrawlRunRequests.effectiveOptions
|
|
85931
|
-
}).from(runs).leftJoin(siteCrawlRunRequests,
|
|
85932
|
-
|
|
85933
|
-
|
|
85934
|
-
)).where(
|
|
85935
|
-
|
|
85936
|
-
|
|
85937
|
-
|
|
86330
|
+
}).from(runs).leftJoin(siteCrawlRunRequests, and59(
|
|
86331
|
+
eq70(siteCrawlRunRequests.projectId, runs.projectId),
|
|
86332
|
+
eq70(siteCrawlRunRequests.runId, runs.id)
|
|
86333
|
+
)).where(and59(
|
|
86334
|
+
eq70(runs.projectId, project.id),
|
|
86335
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
86336
|
+
inArray27(runs.status, [RunStatuses.queued, RunStatuses.running])
|
|
85938
86337
|
)).get();
|
|
85939
86338
|
if (existing) {
|
|
85940
86339
|
if (existing.identityKey === identityKey) {
|
|
@@ -85988,7 +86387,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85988
86387
|
// ../api-routes/src/research.ts
|
|
85989
86388
|
import crypto50 from "crypto";
|
|
85990
86389
|
import { z as z5 } from "zod";
|
|
85991
|
-
import { and as
|
|
86390
|
+
import { and as and60, count as count3, desc as desc31, eq as eq71, gte as gte19, lt as lt12, or as or17, sql as sql27 } from "drizzle-orm";
|
|
85992
86391
|
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
85993
86392
|
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
85994
86393
|
async function researchRoutes(app, opts) {
|
|
@@ -86006,7 +86405,7 @@ async function researchRoutes(app, opts) {
|
|
|
86006
86405
|
const input = parsed.data;
|
|
86007
86406
|
if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
86008
86407
|
const requestHash2 = directResearchRequestHash(input);
|
|
86009
|
-
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(
|
|
86408
|
+
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and60(eq71(researchRuns.projectId, project.id), eq71(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
|
|
86010
86409
|
if (existingReceipt) {
|
|
86011
86410
|
assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
|
|
86012
86411
|
const result2 = getDetail(app, project.id, existingReceipt.id);
|
|
@@ -86038,7 +86437,7 @@ async function researchRoutes(app, opts) {
|
|
|
86038
86437
|
const initiatedBy = researchPrincipal(request);
|
|
86039
86438
|
const decision = app.db.transaction((tx) => {
|
|
86040
86439
|
if (input.idempotencyKey) {
|
|
86041
|
-
const existing = tx.select().from(researchRuns).where(
|
|
86440
|
+
const existing = tx.select().from(researchRuns).where(and60(eq71(researchRuns.projectId, project.id), eq71(researchRuns.idempotencyKey, input.idempotencyKey))).get();
|
|
86042
86441
|
if (existing) {
|
|
86043
86442
|
assertSameDirectResearchRequest(existing, input, requestHash2);
|
|
86044
86443
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
@@ -86046,9 +86445,9 @@ async function researchRoutes(app, opts) {
|
|
|
86046
86445
|
}
|
|
86047
86446
|
if (initiatedBy?.limited) {
|
|
86048
86447
|
const { start, end, date } = utcDayBounds(now);
|
|
86049
|
-
const used = tx.select({ value: count3() }).from(researchRuns).where(
|
|
86050
|
-
|
|
86051
|
-
|
|
86448
|
+
const used = tx.select({ value: count3() }).from(researchRuns).where(and60(
|
|
86449
|
+
eq71(researchRuns.projectId, project.id),
|
|
86450
|
+
gte19(researchRuns.createdAt, start),
|
|
86052
86451
|
lt12(researchRuns.createdAt, end),
|
|
86053
86452
|
sql27`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
86054
86453
|
)).get()?.value ?? 0;
|
|
@@ -86078,15 +86477,15 @@ async function researchRoutes(app, opts) {
|
|
|
86078
86477
|
if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
86079
86478
|
const requestHash2 = researchBatchRequestHash(input);
|
|
86080
86479
|
const receiptKeys = input.runs.map((_run, index2) => batchChildReceiptKey(input.idempotencyKey, index2));
|
|
86081
|
-
const firstReceipt = app.db.select().from(researchRuns).where(
|
|
86082
|
-
|
|
86083
|
-
|
|
86480
|
+
const firstReceipt = app.db.select().from(researchRuns).where(and60(
|
|
86481
|
+
eq71(researchRuns.projectId, project.id),
|
|
86482
|
+
eq71(researchRuns.idempotencyKey, receiptKeys[0])
|
|
86084
86483
|
)).get();
|
|
86085
86484
|
if (firstReceipt) {
|
|
86086
86485
|
if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
86087
|
-
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(
|
|
86088
|
-
|
|
86089
|
-
|
|
86486
|
+
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and60(
|
|
86487
|
+
eq71(researchRuns.projectId, project.id),
|
|
86488
|
+
eq71(researchRuns.idempotencyKey, key)
|
|
86090
86489
|
)).get());
|
|
86091
86490
|
if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
86092
86491
|
const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
|
|
@@ -86101,9 +86500,9 @@ async function researchRoutes(app, opts) {
|
|
|
86101
86500
|
const decision = app.db.transaction((tx) => {
|
|
86102
86501
|
if (initiatedBy?.limited) {
|
|
86103
86502
|
const { start, end, date } = utcDayBounds(now);
|
|
86104
|
-
const used = tx.select({ value: count3() }).from(researchRuns).where(
|
|
86105
|
-
|
|
86106
|
-
|
|
86503
|
+
const used = tx.select({ value: count3() }).from(researchRuns).where(and60(
|
|
86504
|
+
eq71(researchRuns.projectId, project.id),
|
|
86505
|
+
gte19(researchRuns.createdAt, start),
|
|
86107
86506
|
lt12(researchRuns.createdAt, end),
|
|
86108
86507
|
sql27`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
86109
86508
|
)).get()?.value ?? 0;
|
|
@@ -86156,9 +86555,9 @@ async function researchRoutes(app, opts) {
|
|
|
86156
86555
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
86157
86556
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
86158
86557
|
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
86159
|
-
const rows = app.db.select().from(researchRuns).where(
|
|
86160
|
-
|
|
86161
|
-
cursor ? or17(lt12(researchRuns.createdAt, cursor.createdAt),
|
|
86558
|
+
const rows = app.db.select().from(researchRuns).where(and60(
|
|
86559
|
+
eq71(researchRuns.projectId, project.id),
|
|
86560
|
+
cursor ? or17(lt12(researchRuns.createdAt, cursor.createdAt), and60(eq71(researchRuns.createdAt, cursor.createdAt), lt12(researchRuns.id, cursor.id))) : void 0
|
|
86162
86561
|
)).orderBy(desc31(researchRuns.createdAt), desc31(researchRuns.id)).limit(limit + 1).all();
|
|
86163
86562
|
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
86164
86563
|
const last = runs2.at(-1);
|
|
@@ -86285,9 +86684,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
|
|
|
86285
86684
|
return stored;
|
|
86286
86685
|
}
|
|
86287
86686
|
function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
86288
|
-
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(
|
|
86289
|
-
|
|
86290
|
-
|
|
86687
|
+
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and60(
|
|
86688
|
+
eq71(measurementQueryTemplates.projectId, projectId),
|
|
86689
|
+
eq71(measurementQueryTemplates.id, selection.templateId)
|
|
86291
86690
|
)).get();
|
|
86292
86691
|
if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
|
|
86293
86692
|
const templateVersion = savedTemplate.updatedAt;
|
|
@@ -86296,7 +86695,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
|
86296
86695
|
}
|
|
86297
86696
|
const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
|
|
86298
86697
|
if (bindingLocation) {
|
|
86299
|
-
const project = app.db.select({ locations: projects.locations }).from(projects).where(
|
|
86698
|
+
const project = app.db.select({ locations: projects.locations }).from(projects).where(eq71(projects.id, projectId)).get();
|
|
86300
86699
|
if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
|
|
86301
86700
|
}
|
|
86302
86701
|
const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
|
|
@@ -86309,9 +86708,9 @@ function classifyResearchQueries(project, plan, queries2) {
|
|
|
86309
86708
|
return queries2.map((query) => classifier?.classify(query) ?? null);
|
|
86310
86709
|
}
|
|
86311
86710
|
function getDetail(app, projectId, id) {
|
|
86312
|
-
const row = app.db.select().from(researchRuns).where(
|
|
86711
|
+
const row = app.db.select().from(researchRuns).where(and60(eq71(researchRuns.id, id), eq71(researchRuns.projectId, projectId))).get();
|
|
86313
86712
|
if (!row) throw notFound("Research run", id);
|
|
86314
|
-
const queries2 = app.db.select().from(researchRunQueries).where(
|
|
86713
|
+
const queries2 = app.db.select().from(researchRunQueries).where(eq71(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
|
|
86315
86714
|
return { ...serializeRun(row), queries: queries2 };
|
|
86316
86715
|
}
|
|
86317
86716
|
function serializeRun(row) {
|
|
@@ -86340,7 +86739,7 @@ function utcDayBounds(now) {
|
|
|
86340
86739
|
}
|
|
86341
86740
|
|
|
86342
86741
|
// ../api-routes/src/plan-competitors.ts
|
|
86343
|
-
import { eq as
|
|
86742
|
+
import { eq as eq72 } from "drizzle-orm";
|
|
86344
86743
|
function competitorKey(value) {
|
|
86345
86744
|
const host = (hostOf(value) ?? value).trim().toLowerCase().replace(/\.$/, "");
|
|
86346
86745
|
return host.startsWith("www.") ? host.slice(4) : host;
|
|
@@ -86356,7 +86755,7 @@ function toList(merged) {
|
|
|
86356
86755
|
return [...merged].sort(([left], [right]) => left.localeCompare(right)).map(([domain, names]) => ({ domain, aliases: [...names] }));
|
|
86357
86756
|
}
|
|
86358
86757
|
function readScope(db, versionId) {
|
|
86359
|
-
const row = db.select({ canonicalJson: measurementPlanVersions.canonicalJson }).from(measurementPlanVersions).where(
|
|
86758
|
+
const row = db.select({ canonicalJson: measurementPlanVersions.canonicalJson }).from(measurementPlanVersions).where(eq72(measurementPlanVersions.id, versionId)).get();
|
|
86360
86759
|
if (!row) return null;
|
|
86361
86760
|
let groups;
|
|
86362
86761
|
let edges;
|
|
@@ -86420,7 +86819,7 @@ function createRunCompetitorResolver(db, projectCompetitorDomains) {
|
|
|
86420
86819
|
|
|
86421
86820
|
// ../api-routes/src/simple-measurement-definitions.ts
|
|
86422
86821
|
import crypto51 from "crypto";
|
|
86423
|
-
import { and as
|
|
86822
|
+
import { and as and61, eq as eq73 } from "drizzle-orm";
|
|
86424
86823
|
function captureSimpleMeasurementDefinition(db, input) {
|
|
86425
86824
|
return db.transaction((tx) => {
|
|
86426
86825
|
const run = tx.select({
|
|
@@ -86428,7 +86827,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86428
86827
|
trigger: runs.trigger,
|
|
86429
86828
|
status: runs.status,
|
|
86430
86829
|
measurementPlanVersionId: runs.measurementPlanVersionId
|
|
86431
|
-
}).from(runs).where(
|
|
86830
|
+
}).from(runs).where(and61(eq73(runs.projectId, input.projectId), eq73(runs.id, input.runId))).get();
|
|
86432
86831
|
if (!run) throw notFound("Run", input.runId);
|
|
86433
86832
|
if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
|
|
86434
86833
|
return null;
|
|
@@ -86438,7 +86837,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86438
86837
|
if (definition.queries.some((query) => query.queryClass !== (classifier?.classify(query.queryText) ?? null))) {
|
|
86439
86838
|
throw validationError("Query classes must match the captured identity and text.");
|
|
86440
86839
|
}
|
|
86441
|
-
const existing = tx.select().from(simpleMeasurementDefinitions).where(
|
|
86840
|
+
const existing = tx.select().from(simpleMeasurementDefinitions).where(and61(eq73(simpleMeasurementDefinitions.projectId, input.projectId), eq73(simpleMeasurementDefinitions.runId, input.runId))).get();
|
|
86442
86841
|
if (existing) {
|
|
86443
86842
|
const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
|
|
86444
86843
|
const replayDefinition = {
|
|
@@ -86459,15 +86858,15 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86459
86858
|
if (run.status !== RunStatuses.running) {
|
|
86460
86859
|
throw validationError("Only a running run can capture a new measurement definition.");
|
|
86461
86860
|
}
|
|
86462
|
-
if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(
|
|
86861
|
+
if (tx.select({ id: querySnapshots.id }).from(querySnapshots).where(eq73(querySnapshots.runId, input.runId)).limit(1).get()) {
|
|
86463
86862
|
throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
|
|
86464
86863
|
}
|
|
86465
|
-
const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(
|
|
86864
|
+
const projectQueryIds = new Set(tx.select({ id: queries.id }).from(queries).where(eq73(queries.projectId, input.projectId)).all().map((query) => query.id));
|
|
86466
86865
|
if (definition.queries.some((query) => !projectQueryIds.has(query.queryId))) {
|
|
86467
86866
|
throw validationError("Every captured query must belong to the run project.");
|
|
86468
86867
|
}
|
|
86469
86868
|
if (definition.competitors !== void 0) {
|
|
86470
|
-
const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(
|
|
86869
|
+
const liveCompetitorDomains = new Set(tx.select({ domain: competitors.domain }).from(competitors).where(eq73(competitors.projectId, input.projectId)).all().map((competitor) => competitor.domain));
|
|
86471
86870
|
const capturedDomains = new Set(definition.competitors.map((competitor) => competitor.domain));
|
|
86472
86871
|
if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
|
|
86473
86872
|
throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
|
|
@@ -87418,10 +87817,10 @@ var log = createLogger("IntelligenceService");
|
|
|
87418
87817
|
var runChronologyKey = sql28`coalesce(${runs.finishedAt}, ${runs.createdAt})`;
|
|
87419
87818
|
function analyzableRunPredicates(projectId) {
|
|
87420
87819
|
return [
|
|
87421
|
-
|
|
87820
|
+
eq74(runs.projectId, projectId),
|
|
87422
87821
|
// Only this kind writes query_snapshots.
|
|
87423
|
-
|
|
87424
|
-
or18(
|
|
87822
|
+
eq74(runs.kind, RunKinds["answer-visibility"]),
|
|
87823
|
+
or18(eq74(runs.status, RunStatuses.completed), eq74(runs.status, RunStatuses.partial)),
|
|
87425
87824
|
// Defensive: RunCoordinator already skips probes before analyzeAndPersist
|
|
87426
87825
|
// is called, but a future call site invoking it directly for a probe must
|
|
87427
87826
|
// still not pollute the intelligence window.
|
|
@@ -87466,24 +87865,24 @@ var IntelligenceService = class {
|
|
|
87466
87865
|
* Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
|
|
87467
87866
|
*/
|
|
87468
87867
|
analyzeAndPersist(runId, projectId) {
|
|
87469
|
-
const currentRunRecord = this.db.select().from(runs).where(
|
|
87868
|
+
const currentRunRecord = this.db.select().from(runs).where(and62(eq74(runs.id, runId), ...analyzableRunPredicates(projectId))).get();
|
|
87470
87869
|
if (!currentRunRecord) {
|
|
87471
87870
|
log.info("intelligence.skip", { runId, reason: "run not eligible for analysis" });
|
|
87472
87871
|
return null;
|
|
87473
87872
|
}
|
|
87474
87873
|
const currentLocation = currentRunRecord.location ?? null;
|
|
87475
87874
|
const recentRuns = this.db.select().from(runs).where(
|
|
87476
|
-
|
|
87875
|
+
and62(
|
|
87477
87876
|
...analyzableRunPredicates(projectId),
|
|
87478
|
-
currentLocation === null ? isNull13(runs.location) :
|
|
87877
|
+
currentLocation === null ? isNull13(runs.location) : eq74(runs.location, currentLocation),
|
|
87479
87878
|
// Never look forward. The window is this run's own past, so
|
|
87480
87879
|
// re-analyzing a historical run compares it against its true
|
|
87481
87880
|
// predecessor rather than against sweeps that came after it.
|
|
87482
|
-
|
|
87881
|
+
lte17(runChronologyKey, currentRunRecord.finishedAt ?? currentRunRecord.createdAt),
|
|
87483
87882
|
// MEASUREMENT: only runs that actually wrote snapshots — see above.
|
|
87484
87883
|
// Index seek on idx_snapshots_run, short-circuited on the first row.
|
|
87485
87884
|
exists(
|
|
87486
|
-
this.db.select({ one: sql28`1` }).from(querySnapshots).where(
|
|
87885
|
+
this.db.select({ one: sql28`1` }).from(querySnapshots).where(eq74(querySnapshots.runId, runs.id))
|
|
87487
87886
|
)
|
|
87488
87887
|
)
|
|
87489
87888
|
).orderBy(desc32(runs.finishedAt), desc32(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
@@ -87558,7 +87957,7 @@ var IntelligenceService = class {
|
|
|
87558
87957
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
87559
87958
|
*/
|
|
87560
87959
|
analyzeAndPersistGbp(runId, projectId) {
|
|
87561
|
-
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(
|
|
87960
|
+
const runRow = this.db.select({ createdAt: runs.createdAt, startedAt: runs.startedAt, finishedAt: runs.finishedAt }).from(runs).where(eq74(runs.id, runId)).get();
|
|
87562
87961
|
if (!runRow) {
|
|
87563
87962
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
87564
87963
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -87566,11 +87965,11 @@ var IntelligenceService = class {
|
|
|
87566
87965
|
}
|
|
87567
87966
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
87568
87967
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
87569
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
87570
|
-
|
|
87571
|
-
|
|
87572
|
-
|
|
87573
|
-
|
|
87968
|
+
const selected = this.db.select().from(gbpLocations).where(and62(
|
|
87969
|
+
eq74(gbpLocations.projectId, projectId),
|
|
87970
|
+
eq74(gbpLocations.selected, true),
|
|
87971
|
+
gte20(gbpLocations.syncedAt, windowStart),
|
|
87972
|
+
lte17(gbpLocations.syncedAt, windowEnd)
|
|
87574
87973
|
)).all();
|
|
87575
87974
|
if (selected.length === 0) {
|
|
87576
87975
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -87603,12 +88002,12 @@ var IntelligenceService = class {
|
|
|
87603
88002
|
}
|
|
87604
88003
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
87605
88004
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
87606
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
87607
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
87608
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
87609
|
-
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(
|
|
88005
|
+
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(and62(eq74(gbpDailyMetrics.projectId, projectId), eq74(gbpDailyMetrics.locationName, locationName))).all();
|
|
88006
|
+
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and62(eq74(gbpPlaceActions.projectId, projectId), eq74(gbpPlaceActions.locationName, locationName))).all();
|
|
88007
|
+
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and62(eq74(gbpLodgingSnapshots.projectId, projectId), eq74(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc32(gbpLodgingSnapshots.syncedAt)).limit(1).get();
|
|
88008
|
+
const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and62(eq74(gbpLocations.projectId, projectId), eq74(gbpLocations.locationName, locationName))).get();
|
|
87610
88009
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
87611
|
-
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(
|
|
88010
|
+
const placeRow = this.db.select({ attributes: gbpPlaceDetails.attributes }).from(gbpPlaceDetails).where(and62(eq74(gbpPlaceDetails.projectId, projectId), eq74(gbpPlaceDetails.locationName, locationName))).orderBy(desc32(gbpPlaceDetails.syncedAt)).limit(1).get();
|
|
87612
88011
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
87613
88012
|
const summary3 = buildGbpSummary({
|
|
87614
88013
|
locationName,
|
|
@@ -87644,7 +88043,7 @@ var IntelligenceService = class {
|
|
|
87644
88043
|
/** Build the month-over-month keyword series for a location from the
|
|
87645
88044
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
87646
88045
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
87647
|
-
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(
|
|
88046
|
+
const rows = this.db.select({ month: gbpKeywordMonthly.month, keyword: gbpKeywordMonthly.keyword, valueCount: gbpKeywordMonthly.valueCount }).from(gbpKeywordMonthly).where(and62(eq74(gbpKeywordMonthly.projectId, projectId), eq74(gbpKeywordMonthly.locationName, locationName))).all();
|
|
87648
88047
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
87649
88048
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
87650
88049
|
const recentMonth = months[0] ?? null;
|
|
@@ -87675,7 +88074,7 @@ var IntelligenceService = class {
|
|
|
87675
88074
|
*/
|
|
87676
88075
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
87677
88076
|
const covered = new Set(coveredLocationNames);
|
|
87678
|
-
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(
|
|
88077
|
+
const existing = this.db.select({ id: insights.id, dismissed: insights.dismissed }).from(insights).where(and62(eq74(insights.projectId, projectId), eq74(insights.provider, GBP_INSIGHT_PROVIDER))).all();
|
|
87679
88078
|
const staleIds = [];
|
|
87680
88079
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
87681
88080
|
for (const row of existing) {
|
|
@@ -87686,7 +88085,7 @@ var IntelligenceService = class {
|
|
|
87686
88085
|
}
|
|
87687
88086
|
this.db.transaction((tx) => {
|
|
87688
88087
|
for (const id of staleIds) {
|
|
87689
|
-
tx.delete(insights).where(
|
|
88088
|
+
tx.delete(insights).where(eq74(insights.id, id)).run();
|
|
87690
88089
|
}
|
|
87691
88090
|
for (const insight of gbpInsights) {
|
|
87692
88091
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -87772,7 +88171,7 @@ var IntelligenceService = class {
|
|
|
87772
88171
|
* create per run + aggregate). DB is left untouched.
|
|
87773
88172
|
*/
|
|
87774
88173
|
backfill(projectName, opts, onProgress) {
|
|
87775
|
-
const project = this.db.select().from(projects).where(
|
|
88174
|
+
const project = this.db.select().from(projects).where(eq74(projects.name, projectName)).get();
|
|
87776
88175
|
if (!project) {
|
|
87777
88176
|
throw new Error(`Project "${projectName}" not found`);
|
|
87778
88177
|
}
|
|
@@ -87784,7 +88183,7 @@ var IntelligenceService = class {
|
|
|
87784
88183
|
}
|
|
87785
88184
|
sinceTimestamp = parsed;
|
|
87786
88185
|
}
|
|
87787
|
-
const allRuns = this.db.select().from(runs).where(
|
|
88186
|
+
const allRuns = this.db.select().from(runs).where(and62(...analyzableRunPredicates(project.id))).orderBy(asc13(runs.finishedAt)).all();
|
|
87788
88187
|
const measuredRunIds = this.runIdsWithSnapshots(allRuns.map((r) => r.id));
|
|
87789
88188
|
const measuredRuns = allRuns.filter((r) => measuredRunIds.has(r.id));
|
|
87790
88189
|
let startIdx = 0;
|
|
@@ -87815,7 +88214,7 @@ var IntelligenceService = class {
|
|
|
87815
88214
|
let wouldDeleteTotal = 0;
|
|
87816
88215
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
87817
88216
|
if (isDryRun && targetRuns.length > 0) {
|
|
87818
|
-
const rows = this.db.select({ runId: insights.runId }).from(insights).where(
|
|
88217
|
+
const rows = this.db.select({ runId: insights.runId }).from(insights).where(inArray28(insights.runId, targetRuns.map((r) => r.id))).all();
|
|
87819
88218
|
for (const r of rows) {
|
|
87820
88219
|
if (r.runId == null) continue;
|
|
87821
88220
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -87861,7 +88260,7 @@ var IntelligenceService = class {
|
|
|
87861
88260
|
return { processed, skipped: skipped3, totalInsights };
|
|
87862
88261
|
}
|
|
87863
88262
|
loadTrackedCompetitors(projectId) {
|
|
87864
|
-
return this.db.select({ domain: competitors.domain }).from(competitors).where(
|
|
88263
|
+
return this.db.select({ domain: competitors.domain }).from(competitors).where(eq74(competitors.projectId, projectId)).all().map((r) => r.domain);
|
|
87865
88264
|
}
|
|
87866
88265
|
/**
|
|
87867
88266
|
* Which of `runIds` actually wrote query snapshots. One grouped read, so
|
|
@@ -87870,7 +88269,7 @@ var IntelligenceService = class {
|
|
|
87870
88269
|
*/
|
|
87871
88270
|
runIdsWithSnapshots(runIds) {
|
|
87872
88271
|
if (runIds.length === 0) return /* @__PURE__ */ new Set();
|
|
87873
|
-
const rows = this.db.select({ runId: querySnapshots.runId }).from(querySnapshots).where(
|
|
88272
|
+
const rows = this.db.select({ runId: querySnapshots.runId }).from(querySnapshots).where(inArray28(querySnapshots.runId, [...runIds])).groupBy(querySnapshots.runId).all();
|
|
87874
88273
|
return new Set(rows.map((r) => r.runId));
|
|
87875
88274
|
}
|
|
87876
88275
|
/**
|
|
@@ -87892,15 +88291,15 @@ var IntelligenceService = class {
|
|
|
87892
88291
|
}
|
|
87893
88292
|
persistResult(result, runId, projectId) {
|
|
87894
88293
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
87895
|
-
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(
|
|
88294
|
+
const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq74(insights.runId, runId)).all();
|
|
87896
88295
|
for (const row of existingInsights) {
|
|
87897
88296
|
if (row.dismissed) {
|
|
87898
88297
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
87899
88298
|
}
|
|
87900
88299
|
}
|
|
87901
88300
|
this.db.transaction((tx) => {
|
|
87902
|
-
tx.delete(insights).where(
|
|
87903
|
-
tx.delete(healthSnapshots).where(
|
|
88301
|
+
tx.delete(insights).where(eq74(insights.runId, runId)).run();
|
|
88302
|
+
tx.delete(healthSnapshots).where(eq74(healthSnapshots.runId, runId)).run();
|
|
87904
88303
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
87905
88304
|
for (const insight of result.insights) {
|
|
87906
88305
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -87947,10 +88346,10 @@ var IntelligenceService = class {
|
|
|
87947
88346
|
clicks: gscSearchData.clicks,
|
|
87948
88347
|
impressions: gscSearchData.impressions,
|
|
87949
88348
|
position: gscSearchData.position
|
|
87950
|
-
}).from(gscSearchData).where(
|
|
87951
|
-
|
|
87952
|
-
|
|
87953
|
-
|
|
88349
|
+
}).from(gscSearchData).where(and62(
|
|
88350
|
+
eq74(gscSearchData.projectId, projectId),
|
|
88351
|
+
gte20(gscSearchData.date, window.startDate),
|
|
88352
|
+
lte17(gscSearchData.date, window.endDate)
|
|
87954
88353
|
)).all();
|
|
87955
88354
|
const byDay = /* @__PURE__ */ new Map();
|
|
87956
88355
|
for (const r of rows) {
|
|
@@ -88001,17 +88400,17 @@ var IntelligenceService = class {
|
|
|
88001
88400
|
const key = row.query.toLowerCase();
|
|
88002
88401
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
88003
88402
|
}
|
|
88004
|
-
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(
|
|
88403
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq74(projects.id, projectId)).get();
|
|
88005
88404
|
const locationCount = Math.max(
|
|
88006
88405
|
1,
|
|
88007
88406
|
(projectRow?.locations ?? []).length
|
|
88008
88407
|
);
|
|
88009
88408
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
88010
88409
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
88011
|
-
|
|
88012
|
-
|
|
88013
|
-
|
|
88014
|
-
or18(
|
|
88410
|
+
and62(
|
|
88411
|
+
eq74(runs.projectId, projectId),
|
|
88412
|
+
eq74(runs.kind, RunKinds["answer-visibility"]),
|
|
88413
|
+
or18(eq74(runs.status, "completed"), eq74(runs.status, "partial")),
|
|
88015
88414
|
// Defensive — see top of file.
|
|
88016
88415
|
ne11(runs.trigger, RunTriggers.probe)
|
|
88017
88416
|
)
|
|
@@ -88031,7 +88430,7 @@ var IntelligenceService = class {
|
|
|
88031
88430
|
const haveHistory = recentRunIds.length > 0;
|
|
88032
88431
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
88033
88432
|
if (haveHistory) {
|
|
88034
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(
|
|
88433
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and62(eq74(insights.type, "regression"), inArray28(insights.runId, recentRunIds))).all();
|
|
88035
88434
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
88036
88435
|
for (const row of priorRows) {
|
|
88037
88436
|
if (!row.runId) continue;
|
|
@@ -88060,7 +88459,7 @@ var IntelligenceService = class {
|
|
|
88060
88459
|
});
|
|
88061
88460
|
}
|
|
88062
88461
|
buildRunData(runId, projectId, completedAt, location = null, trackedCompetitors = []) {
|
|
88063
|
-
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(
|
|
88462
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq74(projects.id, projectId)).get();
|
|
88064
88463
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
88065
88464
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
88066
88465
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -88078,7 +88477,7 @@ var IntelligenceService = class {
|
|
|
88078
88477
|
citedDomains: querySnapshots.citedDomains,
|
|
88079
88478
|
rawResponse: querySnapshots.rawResponse,
|
|
88080
88479
|
snapshotLocation: querySnapshots.location
|
|
88081
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
88480
|
+
}).from(querySnapshots).leftJoin(queries, eq74(querySnapshots.queryId, queries.id)).where(eq74(querySnapshots.runId, runId)).all();
|
|
88082
88481
|
const snapshots = [];
|
|
88083
88482
|
let orphanCount = 0;
|
|
88084
88483
|
for (const r of rows) {
|
|
@@ -88300,6 +88699,7 @@ export {
|
|
|
88300
88699
|
generateWranglerToml,
|
|
88301
88700
|
SITE_REACHABILITY_CHECK_ID,
|
|
88302
88701
|
SITE_REACHABILITY_CHECKS,
|
|
88702
|
+
scheduledHealthCheckIds,
|
|
88303
88703
|
runChecks,
|
|
88304
88704
|
executeDiscovery,
|
|
88305
88705
|
markSessionFailed,
|