@canonry/canonry 5.20.4 → 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/aero/references/portfolio-analysis.md +1 -1
- package/assets/agent-workspace/skills/aero/references/reporting.md +9 -4
- package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -1
- package/assets/assets/{AuditHistoryPanel-BSh3Oezj.js → AuditHistoryPanel-teTDC-4-.js} +1 -1
- package/assets/assets/{BacklinksPage-DQ59dhCx.js → BacklinksPage-Bqm7bido.js} +1 -1
- package/assets/assets/{HistoryPage-BBvgid3o.js → HistoryPage-IcqwPBmt.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-Dmb18JbT.js → MeasurementPropertyPage-BLpptT3_.js} +1 -1
- package/assets/assets/{ProjectPage-CPg0q2Dy.js → ProjectPage-nn2Y9H-m.js} +1 -1
- package/assets/assets/{RunRow-BK8sZCID.js → RunRow-S_SCHr0o.js} +1 -1
- package/assets/assets/{RunsPage-CIcVBTdP.js → RunsPage-DFiif626.js} +1 -1
- package/assets/assets/{SettingsPage-Bc3aR-gz.js → SettingsPage-CitZ6FAS.js} +1 -1
- package/assets/assets/{SiteHealthSection-FF14aIXT.js → SiteHealthSection-C2dsyNGY.js} +3 -3
- package/assets/assets/{TrafficPage-C_qoeGDl.js → TrafficPage-lY8na5Pg.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-B3i_B2MI.js → TrafficSourceDetailPage-BTykoVGT.js} +1 -1
- package/assets/assets/{extract-error-message-BisUn6nN.js → extract-error-message-r0Gpc7mv.js} +1 -1
- package/assets/assets/{index-Dht7JNX5.js → index-DwlO8KfE.js} +21 -21
- package/assets/assets/{react-sigma_core.esm.min-tWhS09SS.js → react-sigma_core.esm.min-DDrDxbO1.js} +1 -1
- package/assets/assets/{v2-overview-adapter-CAoeWKQJ.js → v2-overview-adapter-OsQiglXU.js} +1 -1
- package/assets/index.html +1 -1
- package/dist/{chunk-TWLJRAWA.js → chunk-3LCR6WU6.js} +2 -2
- package/dist/{chunk-JJPZ4OQ2.js → chunk-DDL2XSNM.js} +8 -4
- package/dist/{chunk-PXVJ3P5G.js → chunk-GLLY2GEV.js} +8 -5
- package/dist/{chunk-NSKFW3NF.js → chunk-P5VXVFQW.js} +1095 -457
- package/dist/{chunk-TOSPZ7I5.js → chunk-S5OSY3PI.js} +78 -5
- package/dist/{chunk-P73YBGZP.js → chunk-XFHXKLNT.js} +23 -12
- package/dist/{chunk-CVUR3O5G.js → chunk-YTSXQUJ3.js} +3 -3
- package/dist/cli.js +76 -16
- package/dist/{demo-server-FH2K6TCD.js → demo-server-HULNEORN.js} +4 -4
- package/dist/index.js +6 -6
- package/dist/{intelligence-service-QHSR2H7F.js → intelligence-service-CXNBKRHV.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +12 -12
|
@@ -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,
|
|
@@ -417,6 +420,7 @@ import {
|
|
|
417
420
|
isTemplateLinkRatio,
|
|
418
421
|
isTrendBaseline,
|
|
419
422
|
isVertexGroundingRedirect,
|
|
423
|
+
isVisibilityCompareClassMetric,
|
|
420
424
|
isoDateDaysBeforeInTimeZone,
|
|
421
425
|
keywordDtoSchema,
|
|
422
426
|
keywordGenerateRequestSchema,
|
|
@@ -628,6 +632,8 @@ import {
|
|
|
628
632
|
reportLineChartLabel,
|
|
629
633
|
reportMarketScope,
|
|
630
634
|
reportMissRateLabel,
|
|
635
|
+
reportMonthSchema,
|
|
636
|
+
reportMonthsForDoctor,
|
|
631
637
|
reportMoreChipLabel,
|
|
632
638
|
reportMovementChangeCopy,
|
|
633
639
|
reportOpportunityActionLine,
|
|
@@ -771,6 +777,7 @@ import {
|
|
|
771
777
|
userListDtoSchema,
|
|
772
778
|
validationError,
|
|
773
779
|
visibilityCompareDtoSchema,
|
|
780
|
+
visibilityCompareSelectionSchema,
|
|
774
781
|
visibilityReportQuerySchema,
|
|
775
782
|
visibilityReportResponseSchema,
|
|
776
783
|
visibilityStateFromAnswerMentioned,
|
|
@@ -792,10 +799,10 @@ import {
|
|
|
792
799
|
wordpressSchemaDeployResultDtoSchema,
|
|
793
800
|
wordpressSchemaStatusResultDtoSchema,
|
|
794
801
|
wordpressStatusDtoSchema
|
|
795
|
-
} from "./chunk-
|
|
802
|
+
} from "./chunk-S5OSY3PI.js";
|
|
796
803
|
|
|
797
804
|
// src/intelligence-service.ts
|
|
798
|
-
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";
|
|
799
806
|
|
|
800
807
|
// ../db/src/client.ts
|
|
801
808
|
import { mkdirSync } from "fs";
|
|
@@ -16046,6 +16053,17 @@ function modelEvidenceStatesEqual(a, b) {
|
|
|
16046
16053
|
if (a.status !== "mixed" || b.status !== "mixed") return false;
|
|
16047
16054
|
return a.includesUnknown === b.includesUnknown && a.models.length === b.models.length && a.models.every((model, index2) => model === b.models[index2]);
|
|
16048
16055
|
}
|
|
16056
|
+
function compareModelContinuity(from, to) {
|
|
16057
|
+
const fromEvidence = classifyModelEvidence(from);
|
|
16058
|
+
const toEvidence = classifyModelEvidence(to);
|
|
16059
|
+
const ids = (evidence) => evidence.status === "known" ? [evidence.model] : evidence.status === "mixed" ? evidence.models : [];
|
|
16060
|
+
const unknown = (evidence) => evidence.status === "unknown" || evidence.status === "mixed" && evidence.includesUnknown;
|
|
16061
|
+
return {
|
|
16062
|
+
status: unknown(fromEvidence) || unknown(toEvidence) ? "model-unknown" : fromEvidence.status === "known" && toEvidence.status === "known" && fromEvidence.model === toEvidence.model ? "included" : "model-discontinuous",
|
|
16063
|
+
fromModels: ids(fromEvidence),
|
|
16064
|
+
toModels: ids(toEvidence)
|
|
16065
|
+
};
|
|
16066
|
+
}
|
|
16049
16067
|
|
|
16050
16068
|
// ../api-routes/src/competitor-landscape.ts
|
|
16051
16069
|
var COMPETITOR_LANDSCAPE_RANKED_ROW_LIMIT = 100;
|
|
@@ -24412,6 +24430,50 @@ function buildVisibilityReport(input) {
|
|
|
24412
24430
|
populations
|
|
24413
24431
|
});
|
|
24414
24432
|
}
|
|
24433
|
+
function visibilityComparisonPopulation(run, selection) {
|
|
24434
|
+
const targets = targetMap(run.definition);
|
|
24435
|
+
const byClass = ALL_CLASSES.map((queryClass) => ({ queryClass, candidates: candidatesFor(run, selection, queryClass).candidates }));
|
|
24436
|
+
const pooled = /* @__PURE__ */ new Map();
|
|
24437
|
+
for (const { candidates } of byClass) {
|
|
24438
|
+
for (const candidate of candidates) {
|
|
24439
|
+
const existing = pooled.get(candidate.slot.id);
|
|
24440
|
+
if (existing) existing.edges.push(...candidate.edges);
|
|
24441
|
+
else pooled.set(candidate.slot.id, { ...candidate, edges: [...candidate.edges] });
|
|
24442
|
+
}
|
|
24443
|
+
}
|
|
24444
|
+
const cohortKeys = new Map([...pooled.values()].map((candidate) => [candidate.slot.id, JSON.stringify({
|
|
24445
|
+
definition: run.comparableDefinitionIds.at(-1) ?? run.definitionId,
|
|
24446
|
+
query: candidate.slot.query,
|
|
24447
|
+
execution: candidate.slot.executionId,
|
|
24448
|
+
location: candidate.slot.location,
|
|
24449
|
+
edges: candidate.edges.map((edge) => [edge.targetKey, edge.queryClass, [...edge.marketKeys].sort()]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
|
|
24450
|
+
})]));
|
|
24451
|
+
const row = (candidate, queryClass) => {
|
|
24452
|
+
const signals = targetValues(candidate, targets);
|
|
24453
|
+
return {
|
|
24454
|
+
queryId: candidate.slot.queryId ?? candidate.slot.queryKey,
|
|
24455
|
+
queryText: candidate.slot.query,
|
|
24456
|
+
provider: candidate.slot.provider,
|
|
24457
|
+
model: candidate.observation.model,
|
|
24458
|
+
answerMentioned: signals.mention,
|
|
24459
|
+
mentionApplicable: candidate.edges.some((edge) => targets.get(edge.targetKey)?.mentionEligible === true),
|
|
24460
|
+
citation: citationForCoverage(candidate, targets),
|
|
24461
|
+
answerText: candidate.observation.answerText,
|
|
24462
|
+
competitorDomains: [...new Set(candidate.edges.flatMap((edge) => edge.competitorDomains))],
|
|
24463
|
+
competitorMentions: candidate.observation.competitorMentionDomains.filter((domain) => candidate.edges.some((edge) => edge.competitorDomains.includes(domain))),
|
|
24464
|
+
competitorCitations: candidate.observation.competitorCitationDomains.filter((domain) => candidate.edges.some((edge) => edge.competitorDomains.includes(domain))),
|
|
24465
|
+
cohortKey: cohortKeys.get(candidate.slot.id),
|
|
24466
|
+
queryClass: queryClass === "unknown" ? null : queryClass
|
|
24467
|
+
};
|
|
24468
|
+
};
|
|
24469
|
+
return {
|
|
24470
|
+
snapshots: [...pooled.values()].filter((candidate) => candidate.observation !== null).map((candidate) => {
|
|
24471
|
+
const classes = new Set(candidate.edges.map((edge) => edge.queryClass));
|
|
24472
|
+
return row(candidate, classes.size === 1 ? candidate.edges[0].queryClass : null);
|
|
24473
|
+
}),
|
|
24474
|
+
classSnapshots: byClass.flatMap(({ queryClass, candidates }) => candidates.filter((candidate) => candidate.observation !== null).map((candidate) => row(candidate, queryClass)))
|
|
24475
|
+
};
|
|
24476
|
+
}
|
|
24415
24477
|
|
|
24416
24478
|
// ../api-routes/src/visibility-report.ts
|
|
24417
24479
|
function normalizedText3(value) {
|
|
@@ -24928,6 +24990,13 @@ function comparableVersionIds(rows, versionId) {
|
|
|
24928
24990
|
}
|
|
24929
24991
|
return ids;
|
|
24930
24992
|
}
|
|
24993
|
+
function comparableChainHeads(rows, rowsById) {
|
|
24994
|
+
const heads = /* @__PURE__ */ new Map();
|
|
24995
|
+
for (const row of [...rows].sort((left, right) => left.revision - right.revision)) {
|
|
24996
|
+
for (const id of comparableVersionIds(rowsById, row.id)) heads.set(id, row.id);
|
|
24997
|
+
}
|
|
24998
|
+
return heads;
|
|
24999
|
+
}
|
|
24931
25000
|
function assignmentSignature(plan, assignment) {
|
|
24932
25001
|
const node = plan.executionNodes.find((candidate) => candidate.stableKey === assignment.executionNodeKey);
|
|
24933
25002
|
if (!node) throw new Error(`Frozen assignment references missing execution node ${assignment.executionNodeKey}`);
|
|
@@ -25082,6 +25151,37 @@ async function visibilityReportRoutes(app) {
|
|
|
25082
25151
|
async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
|
|
25083
25152
|
);
|
|
25084
25153
|
}
|
|
25154
|
+
function readVisibilityComparisonRuns(db, projectId, since, until, includeEvidence = true) {
|
|
25155
|
+
const sourceRuns = db.select().from(runs).where(and21(
|
|
25156
|
+
eq26(runs.projectId, projectId),
|
|
25157
|
+
eq26(runs.kind, RunKinds["answer-visibility"]),
|
|
25158
|
+
inArray10(runs.status, [RunStatuses.completed, RunStatuses.partial]),
|
|
25159
|
+
notProbeRun(),
|
|
25160
|
+
isNotNull(runs.measurementPlanVersionId),
|
|
25161
|
+
isNull5(runs.measurementScope),
|
|
25162
|
+
gte4(runs.createdAt, since),
|
|
25163
|
+
lte4(runs.createdAt, until)
|
|
25164
|
+
)).all();
|
|
25165
|
+
const versionRows = db.select().from(measurementPlanVersions).where(eq26(measurementPlanVersions.projectId, projectId)).all();
|
|
25166
|
+
const rowsById = new Map(versionRows.map((version) => [version.id, version]));
|
|
25167
|
+
const versions = parseV2Versions(versionRows);
|
|
25168
|
+
const heads = comparableChainHeads(versionRows, rowsById);
|
|
25169
|
+
const byRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
|
|
25170
|
+
let unavailable5 = false;
|
|
25171
|
+
const selected = sourceRuns.flatMap((run) => {
|
|
25172
|
+
const source = versions.get(run.measurementPlanVersionId);
|
|
25173
|
+
if (!source) {
|
|
25174
|
+
unavailable5 = true;
|
|
25175
|
+
return [];
|
|
25176
|
+
}
|
|
25177
|
+
const presentation = versions.get(heads.get(source.row.id) ?? source.row.id) ?? source;
|
|
25178
|
+
const snapshots = byRun.get(run.id) ?? [];
|
|
25179
|
+
const models = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot.model]));
|
|
25180
|
+
const materialized = advancedRun(run, presentation.row, presentation.plan, snapshots, comparableVersionIds(rowsById, presentation.row.id), includeEvidence);
|
|
25181
|
+
return [{ ...materialized, observations: materialized.observations.map((observation) => ({ ...observation, model: models.get(observation.answerId) ?? null })) }];
|
|
25182
|
+
});
|
|
25183
|
+
return { runs: selected, unavailable: unavailable5 };
|
|
25184
|
+
}
|
|
25085
25185
|
|
|
25086
25186
|
// ../api-routes/src/query-tracking.ts
|
|
25087
25187
|
import crypto21 from "crypto";
|
|
@@ -34822,15 +34922,15 @@ import { and as and35, desc as desc18, eq as eq39, inArray as inArray18 } from "
|
|
|
34822
34922
|
|
|
34823
34923
|
// ../api-routes/src/visibility-compare.ts
|
|
34824
34924
|
var VISIBILITY_COMPARE_MIN_RUNS = 5;
|
|
34825
|
-
function basketPairKey(queryId, provider) {
|
|
34826
|
-
return JSON.stringify([queryId, provider]);
|
|
34925
|
+
function basketPairKey(queryId, provider, cohortKey) {
|
|
34926
|
+
return JSON.stringify([queryId, provider, cohortKey ?? null]);
|
|
34827
34927
|
}
|
|
34828
34928
|
function restrict(snapshots, attribution, pairs) {
|
|
34829
34929
|
const out = [];
|
|
34830
34930
|
for (const snap of snapshots) {
|
|
34831
34931
|
const resolved = resolveCurrentQuery(attribution, snap);
|
|
34832
34932
|
if (!resolved) continue;
|
|
34833
|
-
if (!pairs.has(basketPairKey(resolved.id, snap.provider))) continue;
|
|
34933
|
+
if (!pairs.has(basketPairKey(resolved.id, snap.provider, snap.cohortKey))) continue;
|
|
34834
34934
|
out.push({ ...snap, queryId: resolved.id });
|
|
34835
34935
|
}
|
|
34836
34936
|
return out;
|
|
@@ -34851,8 +34951,8 @@ function observedPairs(snapshots, attribution, queryIds) {
|
|
|
34851
34951
|
for (const snap of snapshots) {
|
|
34852
34952
|
const resolved = resolveCurrentQuery(attribution, snap);
|
|
34853
34953
|
if (!resolved || !queryIds.has(resolved.id)) continue;
|
|
34854
|
-
const pair = { queryId: resolved.id, provider: snap.provider };
|
|
34855
|
-
pairs.set(basketPairKey(pair.queryId, pair.provider), pair);
|
|
34954
|
+
const pair = { queryId: resolved.id, provider: snap.provider, cohortKey: snap.cohortKey };
|
|
34955
|
+
pairs.set(basketPairKey(pair.queryId, pair.provider, pair.cohortKey), pair);
|
|
34856
34956
|
}
|
|
34857
34957
|
return pairs;
|
|
34858
34958
|
}
|
|
@@ -34899,7 +34999,9 @@ function modelEvidenceOf(snaps) {
|
|
|
34899
34999
|
function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable, projectMatcher) {
|
|
34900
35000
|
let checked = 0;
|
|
34901
35001
|
let mentioned = 0;
|
|
35002
|
+
let total = 0;
|
|
34902
35003
|
let cited = 0;
|
|
35004
|
+
let citationUnknown = 0;
|
|
34903
35005
|
let competitorCited = 0;
|
|
34904
35006
|
const perProvider = /* @__PURE__ */ new Map();
|
|
34905
35007
|
const mentionedQueries = /* @__PURE__ */ new Set();
|
|
@@ -34911,21 +35013,26 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
|
|
|
34911
35013
|
mentioned += 1;
|
|
34912
35014
|
mentionedQueries.add(snap.queryId);
|
|
34913
35015
|
}
|
|
34914
|
-
const
|
|
35016
|
+
const citationKnown = snap.citationChecked !== false;
|
|
35017
|
+
if (citationKnown) total += 1;
|
|
35018
|
+
else citationUnknown += 1;
|
|
35019
|
+
const isCited = citationKnown && snap.citationState === CitationStates.cited;
|
|
34915
35020
|
if (isCited) cited += 1;
|
|
34916
35021
|
const pp = perProvider.get(snap.provider) ?? { checked: 0, mentioned: 0, cited: 0 };
|
|
34917
35022
|
if (snap.answerMentioned === true || snap.answerMentioned === false) pp.checked += 1;
|
|
34918
35023
|
if (isMentioned) pp.mentioned += 1;
|
|
34919
35024
|
if (isCited) pp.cited += 1;
|
|
34920
35025
|
perProvider.set(snap.provider, pp);
|
|
34921
|
-
if (
|
|
35026
|
+
if (!citationKnown) continue;
|
|
35027
|
+
if (snap.competitorCitations !== void 0) competitorCited += snap.competitorCitations.length;
|
|
35028
|
+
else if (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
|
|
34922
35029
|
const citedHosts = snap.citedDomains.map((d) => hostOf(d)).filter((h) => h !== null && h.length > 0);
|
|
34923
35030
|
for (const compHost of competitorHosts) {
|
|
34924
35031
|
if (citedHosts.some((ch) => hostMatchesDomain(ch, compHost))) competitorCited += 1;
|
|
34925
35032
|
}
|
|
34926
35033
|
}
|
|
34927
35034
|
}
|
|
34928
|
-
|
|
35035
|
+
let mentionShare = buildMentionShare(
|
|
34929
35036
|
snaps.map((s) => ({
|
|
34930
35037
|
// Share uses current identity; named-rate counts above deliberately keep
|
|
34931
35038
|
// their historical persisted-boolean semantics.
|
|
@@ -34935,11 +35042,26 @@ function countPeriod(snaps, competitors2, queryClassOf, classificationAvailable,
|
|
|
34935
35042
|
})),
|
|
34936
35043
|
{ competitors: competitors2, classificationAvailable }
|
|
34937
35044
|
);
|
|
35045
|
+
if (snaps.some((snapshot) => snapshot.competitorMentions !== void 0)) {
|
|
35046
|
+
const selected = snaps.filter((snapshot) => (!classificationAvailable || queryClassOf(snapshot) === "non-brand") && snapshot.answerMentioned !== null);
|
|
35047
|
+
const perCompetitor = competitors2.map((competitor) => ({
|
|
35048
|
+
domain: competitor.domain,
|
|
35049
|
+
mentionSnapshots: selected.filter((snapshot) => snapshot.competitorMentions?.includes(competitor.domain)).length
|
|
35050
|
+
})).filter((row) => row.mentionSnapshots > 0).sort((a, b) => b.mentionSnapshots - a.mentionSnapshots || (a.domain < b.domain ? -1 : 1));
|
|
35051
|
+
mentionShare = { ...mentionShare, breakdown: {
|
|
35052
|
+
...mentionShare.breakdown,
|
|
35053
|
+
projectMentionSnapshots: selected.filter((snapshot) => snapshot.answerMentioned === true).length,
|
|
35054
|
+
competitorMentionSnapshots: perCompetitor.reduce((total2, competitor) => total2 + competitor.mentionSnapshots, 0),
|
|
35055
|
+
perCompetitor
|
|
35056
|
+
} };
|
|
35057
|
+
}
|
|
34938
35058
|
return {
|
|
34939
35059
|
checked,
|
|
34940
35060
|
mentioned,
|
|
34941
|
-
total
|
|
35061
|
+
total,
|
|
34942
35062
|
cited,
|
|
35063
|
+
citationUnknown,
|
|
35064
|
+
citationEvidence: snaps.some((snapshot) => snapshot.citationChecked !== void 0),
|
|
34943
35065
|
projectCited: cited,
|
|
34944
35066
|
competitorCited,
|
|
34945
35067
|
queriesMentioned: mentionedQueries.size,
|
|
@@ -34953,15 +35075,12 @@ function modelIds(evidence) {
|
|
|
34953
35075
|
if (evidence.status === "mixed") return evidence.models;
|
|
34954
35076
|
return [];
|
|
34955
35077
|
}
|
|
34956
|
-
function isUnknownModelEvidence(evidence) {
|
|
34957
|
-
return evidence.status === "unknown" || evidence.status === "mixed" && evidence.includesUnknown;
|
|
34958
|
-
}
|
|
34959
35078
|
function computeVisibilityCompare(input) {
|
|
34960
35079
|
const attribution = buildQueryAttribution(input.queries);
|
|
34961
35080
|
const classifier = compileQueryClassifier(input.brandNames ?? []);
|
|
34962
|
-
const classificationAvailable = classifier !== null;
|
|
35081
|
+
const classificationAvailable = input.frozenClassification === true || classifier !== null;
|
|
34963
35082
|
const queryTextById = new Map(input.queries.map((q) => [q.id, q.query]));
|
|
34964
|
-
const queryClassOf = (snap) => classifier ? classifier.classify(queryTextById.get(snap.queryId) ?? snap.queryText) : null;
|
|
35083
|
+
const queryClassOf = (snap) => input.frozenClassification ? snap.queryClass ?? null : classifier ? classifier.classify(queryTextById.get(snap.queryId) ?? snap.queryText) : null;
|
|
34965
35084
|
const projectMatcher = compileBrandAliases(input.brandNames ?? []);
|
|
34966
35085
|
const fromObs = observed(input.from.snapshots, attribution);
|
|
34967
35086
|
const toObs = observed(input.to.snapshots, attribution);
|
|
@@ -34973,12 +35092,10 @@ function computeVisibilityCompare(input) {
|
|
|
34973
35092
|
const fromCandidateModels = modelEvidenceOf(restrict(input.from.snapshots, attribution, pairsBoth));
|
|
34974
35093
|
const toCandidateModels = modelEvidenceOf(restrict(input.to.snapshots, attribution, pairsBoth));
|
|
34975
35094
|
const continuityProviders = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
34976
|
-
|
|
34977
|
-
|
|
34978
|
-
|
|
34979
|
-
|
|
34980
|
-
const status = isUnknownModelEvidence(fromEvidence) || isUnknownModelEvidence(toEvidence) ? "model-unknown" : fromEvidence.status === "known" && toEvidence.status === "known" && fromEvidence.model === toEvidence.model ? "included" : "model-discontinuous";
|
|
34981
|
-
return { provider, status, fromModels, toModels };
|
|
35095
|
+
return { provider, ...compareModelContinuity(
|
|
35096
|
+
fromCandidateModels.get(provider) ?? [],
|
|
35097
|
+
toCandidateModels.get(provider) ?? []
|
|
35098
|
+
) };
|
|
34982
35099
|
});
|
|
34983
35100
|
const providersBoth = new Set(
|
|
34984
35101
|
continuityProviders.filter((provider) => provider.status === "included").map((provider) => provider.provider)
|
|
@@ -34992,12 +35109,18 @@ function computeVisibilityCompare(input) {
|
|
|
34992
35109
|
const toSnaps = restrict(input.to.snapshots, attribution, comparedPairs);
|
|
34993
35110
|
const fromCounts = countPeriod(fromSnaps, input.competitors, queryClassOf, classificationAvailable, projectMatcher);
|
|
34994
35111
|
const toCounts = countPeriod(toSnaps, input.competitors, queryClassOf, classificationAvailable, projectMatcher);
|
|
35112
|
+
const fromClassSnaps = input.from.classSnapshots === void 0 ? fromSnaps : restrict(input.from.classSnapshots, attribution, comparedPairs);
|
|
35113
|
+
const toClassSnaps = input.to.classSnapshots === void 0 ? toSnaps : restrict(input.to.classSnapshots, attribution, comparedPairs);
|
|
35114
|
+
const noProjectMatcher = compileBrandAliases([]);
|
|
35115
|
+
const fromShareCounts = input.frozenClassification ? countPeriod(fromClassSnaps, input.competitors, queryClassOf, true, noProjectMatcher) : fromCounts;
|
|
35116
|
+
const toShareCounts = input.frozenClassification ? countPeriod(toClassSnaps, input.competitors, queryClassOf, true, noProjectMatcher) : toCounts;
|
|
34995
35117
|
const shareCounts = (c) => ({
|
|
34996
35118
|
proj: c.mentionShare.breakdown.projectMentionSnapshots,
|
|
34997
35119
|
comp: c.mentionShare.breakdown.competitorMentionSnapshots
|
|
34998
35120
|
});
|
|
34999
|
-
const fromShare = shareCounts(
|
|
35000
|
-
const toShare = shareCounts(
|
|
35121
|
+
const fromShare = shareCounts(fromShareCounts);
|
|
35122
|
+
const toShare = shareCounts(toShareCounts);
|
|
35123
|
+
const citationPeriod = (c, value) => c.citationEvidence ? { ...value, excludedUnknown: c.citationUnknown } : value;
|
|
35001
35124
|
const metrics = [
|
|
35002
35125
|
metric(
|
|
35003
35126
|
"mention-share-of-voice",
|
|
@@ -35018,8 +35141,8 @@ function computeVisibilityCompare(input) {
|
|
|
35018
35141
|
"Cited share of voice",
|
|
35019
35142
|
"all",
|
|
35020
35143
|
true,
|
|
35021
|
-
input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : unavailableCompetitivePeriod(fromCounts.projectCited),
|
|
35022
|
-
input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : unavailableCompetitivePeriod(toCounts.projectCited),
|
|
35144
|
+
citationPeriod(fromCounts, input.competitors.length > 0 ? period(fromCounts.projectCited, fromCounts.projectCited + fromCounts.competitorCited) : unavailableCompetitivePeriod(fromCounts.projectCited)),
|
|
35145
|
+
citationPeriod(toCounts, input.competitors.length > 0 ? period(toCounts.projectCited, toCounts.projectCited + toCounts.competitorCited) : unavailableCompetitivePeriod(toCounts.projectCited)),
|
|
35023
35146
|
continuityBlock
|
|
35024
35147
|
),
|
|
35025
35148
|
metric(
|
|
@@ -35036,11 +35159,31 @@ function computeVisibilityCompare(input) {
|
|
|
35036
35159
|
"Cited rate",
|
|
35037
35160
|
"all",
|
|
35038
35161
|
false,
|
|
35039
|
-
period(fromCounts.cited, fromCounts.total),
|
|
35040
|
-
period(toCounts.cited, toCounts.total),
|
|
35162
|
+
citationPeriod(fromCounts, period(fromCounts.cited, fromCounts.total)),
|
|
35163
|
+
citationPeriod(toCounts, period(toCounts.cited, toCounts.total)),
|
|
35041
35164
|
continuityBlock
|
|
35042
35165
|
)
|
|
35043
35166
|
];
|
|
35167
|
+
const classPeriod = (snapshots, queryClass, signal) => {
|
|
35168
|
+
if (!classificationAvailable) return { ...period(0, 0), availability: "classification-unavailable" };
|
|
35169
|
+
const selected = snapshots.filter((snapshot) => queryClassOf(snapshot) === queryClass && (signal === "cited" || snapshot.mentionApplicable !== false));
|
|
35170
|
+
const checked = selected.filter((snapshot) => signal === "mention" ? typeof snapshot.answerMentioned === "boolean" : snapshot.citationChecked !== false);
|
|
35171
|
+
const numerator = checked.filter((snapshot) => signal === "mention" ? snapshot.answerMentioned === true : snapshot.citationState === CitationStates.cited).length;
|
|
35172
|
+
return { ...period(numerator, checked.length), excludedUnknown: selected.length - checked.length };
|
|
35173
|
+
};
|
|
35174
|
+
for (const queryClass of ["branded", "non-brand"]) {
|
|
35175
|
+
for (const signal of ["mention", "cited"]) {
|
|
35176
|
+
metrics.push(metric(
|
|
35177
|
+
`${signal}-rate-${queryClass}`,
|
|
35178
|
+
signal === "mention" ? "Mention rate" : "Cited rate",
|
|
35179
|
+
queryClass,
|
|
35180
|
+
false,
|
|
35181
|
+
classPeriod(fromClassSnaps, queryClass, signal),
|
|
35182
|
+
classPeriod(toClassSnaps, queryClass, signal),
|
|
35183
|
+
continuityBlock
|
|
35184
|
+
));
|
|
35185
|
+
}
|
|
35186
|
+
}
|
|
35044
35187
|
const modelChanges = [...candidateProviders].sort((a, b) => a.localeCompare(b)).map((provider) => {
|
|
35045
35188
|
const fromModels = modelIds(classifyModelEvidence(fromCandidateModels.get(provider) ?? []));
|
|
35046
35189
|
const toModels = modelIds(classifyModelEvidence(toCandidateModels.get(provider) ?? []));
|
|
@@ -35088,7 +35231,7 @@ function computeVisibilityCompare(input) {
|
|
|
35088
35231
|
comparedProviders: [...providersBoth].sort((a, b) => a.localeCompare(b)),
|
|
35089
35232
|
providers: continuityProviders
|
|
35090
35233
|
},
|
|
35091
|
-
competitors: { from:
|
|
35234
|
+
competitors: { from: fromShareCounts.competitors, to: toShareCounts.competitors }
|
|
35092
35235
|
};
|
|
35093
35236
|
}
|
|
35094
35237
|
|
|
@@ -35306,59 +35449,196 @@ async function visibilityStatsRoutes(app) {
|
|
|
35306
35449
|
return reply.send(response);
|
|
35307
35450
|
});
|
|
35308
35451
|
app.get("/projects/:name/visibility-compare", async (request, reply) => {
|
|
35309
|
-
const
|
|
35310
|
-
|
|
35311
|
-
|
|
35312
|
-
|
|
35313
|
-
|
|
35314
|
-
|
|
35315
|
-
|
|
35316
|
-
|
|
35317
|
-
|
|
35318
|
-
|
|
35319
|
-
|
|
35320
|
-
|
|
35321
|
-
|
|
35322
|
-
|
|
35323
|
-
|
|
35324
|
-
|
|
35325
|
-
|
|
35326
|
-
|
|
35327
|
-
|
|
35328
|
-
const
|
|
35329
|
-
|
|
35330
|
-
|
|
35331
|
-
|
|
35332
|
-
|
|
35333
|
-
|
|
35334
|
-
const monthRuns = app.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(
|
|
35335
|
-
(r) => (r.status === RunStatuses.completed || r.status === RunStatuses.partial) && Date.parse(r.createdAt) >= sinceMs && Date.parse(r.createdAt) <= untilMs
|
|
35336
|
-
);
|
|
35337
|
-
const runIds = monthRuns.map((r) => r.id);
|
|
35338
|
-
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? app.db.select({
|
|
35339
|
-
queryId: querySnapshots.queryId,
|
|
35340
|
-
queryText: querySnapshots.queryText,
|
|
35341
|
-
provider: querySnapshots.provider,
|
|
35342
|
-
model: querySnapshots.model,
|
|
35343
|
-
citationState: querySnapshots.citationState,
|
|
35344
|
-
answerMentioned: querySnapshots.answerMentioned,
|
|
35345
|
-
answerText: querySnapshots.answerText,
|
|
35346
|
-
citedDomains: querySnapshots.citedDomains
|
|
35347
|
-
}).from(querySnapshots).where(inArray18(querySnapshots.runId, runIds)).all() : [];
|
|
35348
|
-
return { runCount: monthRuns.length, snapshots };
|
|
35452
|
+
const dto = readVisibilityCompare(app.db, request.params.name, request.query);
|
|
35453
|
+
return reply.send(dto);
|
|
35454
|
+
});
|
|
35455
|
+
}
|
|
35456
|
+
function readVisibilityCompare(db, projectName, query) {
|
|
35457
|
+
const project = resolveProject(db, projectName);
|
|
35458
|
+
const selection = visibilityCompareSelectionSchema.safeParse(query);
|
|
35459
|
+
if (!selection.success) throw validationError("Invalid comparison selection", { issues: selection.error.issues });
|
|
35460
|
+
const filters = selection.data;
|
|
35461
|
+
const scope = filters.scope ?? "project";
|
|
35462
|
+
if (scope !== "project" && filters.scopeKey === void 0) throw validationError("A non-project comparison scope requires scopeKey.");
|
|
35463
|
+
if (scope === "project" && filters.scopeKey !== void 0) throw validationError("scopeKey is not valid for project scope.");
|
|
35464
|
+
if (scope === "market" && filters.marketKey !== void 0) throw validationError("marketKey is not valid for market scope.");
|
|
35465
|
+
const { fromRaw, toRaw, fromBounds, toBounds } = compareMonths(query.from, query.to);
|
|
35466
|
+
const providerKey2 = filters.provider === void 0 ? void 0 : normalizeText3(filters.provider);
|
|
35467
|
+
const locationKey2 = filters.location === void 0 ? void 0 : normalizeText3(filters.location);
|
|
35468
|
+
const scoped = scope !== "project" || filters.marketKey !== void 0;
|
|
35469
|
+
let advancedComparison;
|
|
35470
|
+
if (activeMeasurementPlan(db, project.id) !== null) {
|
|
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
|
|
35349
35477
|
};
|
|
35350
|
-
const
|
|
35351
|
-
const
|
|
35352
|
-
const
|
|
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);
|
|
35482
|
+
const queries2 = [...new Map([...fromMonth2.snapshots, ...toMonth2.snapshots].map((snapshot) => [snapshot.queryId, { id: snapshot.queryId, query: snapshot.queryText }])).values()];
|
|
35483
|
+
advancedComparison = { ...computeVisibilityCompare({
|
|
35353
35484
|
project: project.name,
|
|
35354
|
-
queries:
|
|
35355
|
-
competitors:
|
|
35356
|
-
|
|
35357
|
-
from: { month: fromRaw,
|
|
35358
|
-
to: { month: toRaw,
|
|
35359
|
-
});
|
|
35360
|
-
|
|
35485
|
+
queries: queries2,
|
|
35486
|
+
competitors: [...new Set([...fromMonth2.snapshots, ...toMonth2.snapshots].flatMap((snapshot) => snapshot.competitorDomains ?? []))].map((domain) => ({ domain, brandTokens: [] })),
|
|
35487
|
+
frozenClassification: fromMonth2.classificationAvailable && toMonth2.classificationAvailable,
|
|
35488
|
+
from: { ...fromMonth2, month: fromRaw, ...fromBounds },
|
|
35489
|
+
to: { ...toMonth2, month: toRaw, ...toBounds }
|
|
35490
|
+
}), selection: filters };
|
|
35491
|
+
if (scoped) {
|
|
35492
|
+
const unreadable = [fromMonth2.classificationAvailable ? null : fromRaw, toMonth2.classificationAvailable ? null : toRaw].filter((month) => month !== null);
|
|
35493
|
+
if (unreadable.length > 0) {
|
|
35494
|
+
throw validationError(`Property, group and market comparisons need frozen schema-v2 plan history; ${unreadable.join(" and ")} include runs whose plan cannot be reconstructed.`, { months: unreadable });
|
|
35495
|
+
}
|
|
35496
|
+
return advancedComparison;
|
|
35497
|
+
}
|
|
35498
|
+
}
|
|
35499
|
+
if (advancedComparison === void 0 && scoped) throw validationError("Property, group and market comparison scopes require an Advanced portfolio.");
|
|
35500
|
+
const projectQueries = db.select({ id: queries.id, query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all();
|
|
35501
|
+
const competitorRows2 = db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all();
|
|
35502
|
+
const competitorInputs = mentionShareCompetitorsFromDomains(competitorRows2.map((c) => c.domain));
|
|
35503
|
+
const loadMonth = (bounds) => {
|
|
35504
|
+
const runIds = legacyCompareRunIds(db, project.id, bounds);
|
|
35505
|
+
const snapshots = runIds.length > 0 && projectQueries.length > 0 ? db.select({
|
|
35506
|
+
queryId: querySnapshots.queryId,
|
|
35507
|
+
queryText: querySnapshots.queryText,
|
|
35508
|
+
provider: querySnapshots.provider,
|
|
35509
|
+
model: querySnapshots.model,
|
|
35510
|
+
citationState: querySnapshots.citationState,
|
|
35511
|
+
answerMentioned: querySnapshots.answerMentioned,
|
|
35512
|
+
answerText: querySnapshots.answerText,
|
|
35513
|
+
citedDomains: querySnapshots.citedDomains,
|
|
35514
|
+
location: querySnapshots.location
|
|
35515
|
+
}).from(querySnapshots).where(inArray18(querySnapshots.runId, runIds)).all() : [];
|
|
35516
|
+
return {
|
|
35517
|
+
runCount: runIds.length,
|
|
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)))
|
|
35519
|
+
};
|
|
35520
|
+
};
|
|
35521
|
+
const fromMonth = loadMonth(fromBounds);
|
|
35522
|
+
const toMonth = loadMonth(toBounds);
|
|
35523
|
+
const dto = computeVisibilityCompare({
|
|
35524
|
+
project: project.name,
|
|
35525
|
+
queries: projectQueries,
|
|
35526
|
+
competitors: competitorInputs,
|
|
35527
|
+
brandNames: effectiveBrandNames(project),
|
|
35528
|
+
from: { month: fromRaw, since: fromBounds.since, until: fromBounds.until, runCount: fromMonth.runCount, snapshots: fromMonth.snapshots },
|
|
35529
|
+
to: { month: toRaw, since: toBounds.since, until: toBounds.until, runCount: toMonth.runCount, snapshots: toMonth.snapshots }
|
|
35361
35530
|
});
|
|
35531
|
+
if (advancedComparison !== void 0) {
|
|
35532
|
+
const { from, to, basket, continuity: continuity2, modelChanges } = advancedComparison;
|
|
35533
|
+
return {
|
|
35534
|
+
...dto,
|
|
35535
|
+
selection: filters,
|
|
35536
|
+
metrics: [
|
|
35537
|
+
...dto.metrics.filter((metric2) => !isVisibilityCompareClassMetric(metric2.key)),
|
|
35538
|
+
...advancedComparison.metrics.filter((metric2) => isVisibilityCompareClassMetric(metric2.key))
|
|
35539
|
+
],
|
|
35540
|
+
classComparison: { from, to, basket, continuity: continuity2, modelChanges }
|
|
35541
|
+
};
|
|
35542
|
+
}
|
|
35543
|
+
return { ...dto, selection: filters };
|
|
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;
|
|
35362
35642
|
}
|
|
35363
35643
|
|
|
35364
35644
|
// ../api-routes/src/results-export.ts
|
|
@@ -39055,9 +39335,18 @@ var routeCatalog = [
|
|
|
39055
39335
|
method: "get",
|
|
39056
39336
|
path: "/api/v1/projects/{name}/visibility-compare",
|
|
39057
39337
|
summary: "Compare AEO visibility month over month",
|
|
39058
|
-
description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. A silent upstream version bump under an unchanged configured id remains undetectable.",
|
|
39338
|
+
description: "Statistically honest month-over-month AEO comparison in one call. PRIMARY metric is share of voice (brand vs competitor mentions in the same answers), which is less exposed to broad model-wide naming propensity than an absolute rate but never bypasses model continuity. Rates are pooled per-snapshot over each month (invariant to sweep count), restricted to query/provider pairs present in BOTH months, and then restricted again to providers with exactly one known, identical configured model id in both months. `continuity` reports every provider and its model evidence; changed, mixed mid-month, or legacy-unknown models are excluded. When no provider remains, metrics return a continuity-blocked verdict rather than a directional call. `from` must be a month strictly before `to`. Adds separate branded/non-brand mention and cited rates with classification-unavailable rather than pooled fallback. Advanced class metrics read frozen Property/market/execution assignments and reuse each answer once per class. Without a scope or marketKey the four original metrics keep the project frame (provider/location narrow it the same way for Simple and Advanced) and Advanced class metrics carry their own `classComparison` cohort. A scope or marketKey answers entirely from the frozen frame and is refused when either month holds runs whose plan cannot be reconstructed (schema v1). On frozen frames, answers with incomplete source capture leave every citation figure and are counted in `excludedUnknown`. A silent upstream version bump under an unchanged configured id remains undetectable.",
|
|
39059
39339
|
tags: ["analytics"],
|
|
39060
|
-
parameters: [
|
|
39340
|
+
parameters: [
|
|
39341
|
+
nameParameter,
|
|
39342
|
+
compareFromQueryParameter,
|
|
39343
|
+
compareToQueryParameter,
|
|
39344
|
+
{ name: "scope", in: "query", description: "Advanced frozen population scope; default project.", schema: { type: "string", enum: ["project", "group", "market", "property"] } },
|
|
39345
|
+
{ name: "scopeKey", in: "query", description: "Stable Property Target, group, or market key; required for non-project scope.", schema: stringSchema },
|
|
39346
|
+
{ name: "marketKey", in: "query", description: "Intersect project/group/property selection with exact frozen market edges.", schema: stringSchema },
|
|
39347
|
+
{ name: "provider", in: "query", description: "Restrict both frames to one provider (case-insensitive).", schema: stringSchema },
|
|
39348
|
+
{ name: "location", in: "query", description: "Execution location label (case-insensitive), or none for no location.", schema: stringSchema }
|
|
39349
|
+
],
|
|
39061
39350
|
responses: {
|
|
39062
39351
|
200: jsonResponse("Month-over-month visibility comparison returned.", "VisibilityCompareDto"),
|
|
39063
39352
|
400: errorResponse("Invalid or missing from/to months."),
|
|
@@ -42687,6 +42976,12 @@ var routeCatalog = [
|
|
|
42687
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.",
|
|
42688
42977
|
tags: ["doctor"],
|
|
42689
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
|
+
},
|
|
42690
42985
|
{
|
|
42691
42986
|
name: "check",
|
|
42692
42987
|
in: "query",
|
|
@@ -42695,7 +42990,8 @@ var routeCatalog = [
|
|
|
42695
42990
|
}
|
|
42696
42991
|
],
|
|
42697
42992
|
responses: {
|
|
42698
|
-
200: jsonResponse("Doctor report returned.", "DoctorReportDto")
|
|
42993
|
+
200: jsonResponse("Doctor report returned.", "DoctorReportDto"),
|
|
42994
|
+
400: errorResponse("Invalid report month.")
|
|
42699
42995
|
}
|
|
42700
42996
|
},
|
|
42701
42997
|
{
|
|
@@ -42706,6 +43002,12 @@ var routeCatalog = [
|
|
|
42706
43002
|
tags: ["doctor"],
|
|
42707
43003
|
parameters: [
|
|
42708
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
|
+
},
|
|
42709
43011
|
{
|
|
42710
43012
|
name: "check",
|
|
42711
43013
|
in: "query",
|
|
@@ -42715,6 +43017,7 @@ var routeCatalog = [
|
|
|
42715
43017
|
],
|
|
42716
43018
|
responses: {
|
|
42717
43019
|
200: jsonResponse("Doctor report returned.", "DoctorReportDto"),
|
|
43020
|
+
400: errorResponse("Invalid report month."),
|
|
42718
43021
|
404: errorResponse("Project not found.")
|
|
42719
43022
|
}
|
|
42720
43023
|
},
|
|
@@ -79791,6 +80094,322 @@ async function trafficRoutes(app, opts) {
|
|
|
79791
80094
|
});
|
|
79792
80095
|
}
|
|
79793
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
|
+
|
|
79794
80413
|
// ../api-routes/src/doctor/checks/agent.ts
|
|
79795
80414
|
import crypto46 from "crypto";
|
|
79796
80415
|
import fs6 from "fs";
|
|
@@ -80119,7 +80738,7 @@ var skillsTriggerSurfaceCheck = {
|
|
|
80119
80738
|
var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck, skillsTriggerSurfaceCheck];
|
|
80120
80739
|
|
|
80121
80740
|
// ../api-routes/src/doctor/checks/backlinks.ts
|
|
80122
|
-
import { and as
|
|
80741
|
+
import { and as and54, eq as eq61 } from "drizzle-orm";
|
|
80123
80742
|
function skippedNoProject() {
|
|
80124
80743
|
return {
|
|
80125
80744
|
status: CheckStatuses.skipped,
|
|
@@ -80135,8 +80754,8 @@ var BACKLINKS_CHECKS = [
|
|
|
80135
80754
|
title: "Backlinks source connected",
|
|
80136
80755
|
run: (ctx) => {
|
|
80137
80756
|
if (!ctx.project) return skippedNoProject();
|
|
80138
|
-
const projectRow = ctx.db.select({ autoExtract: projects.autoExtractBacklinks }).from(projects).where(
|
|
80139
|
-
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();
|
|
80140
80759
|
const ccConnected = projectRow?.autoExtract === true && !!readySync;
|
|
80141
80760
|
const connected = [];
|
|
80142
80761
|
if (ccConnected) connected.push(BacklinkSources.commoncrawl);
|
|
@@ -80149,9 +80768,9 @@ var BACKLINKS_CHECKS = [
|
|
|
80149
80768
|
details: { commoncrawl: ccConnected }
|
|
80150
80769
|
};
|
|
80151
80770
|
}
|
|
80152
|
-
const ccHasData = ccConnected ? !!ctx.db.select({ id: backlinkSummaries.id }).from(backlinkSummaries).where(
|
|
80153
|
-
|
|
80154
|
-
|
|
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)
|
|
80155
80774
|
)).limit(1).get() : false;
|
|
80156
80775
|
return {
|
|
80157
80776
|
status: CheckStatuses.ok,
|
|
@@ -80311,7 +80930,7 @@ var BING_AUTH_CHECKS = [
|
|
|
80311
80930
|
];
|
|
80312
80931
|
|
|
80313
80932
|
// ../api-routes/src/doctor/checks/content.ts
|
|
80314
|
-
import { eq as
|
|
80933
|
+
import { eq as eq62 } from "drizzle-orm";
|
|
80315
80934
|
var WINNABILITY_COVERAGE_WARN_THRESHOLD = 0.8;
|
|
80316
80935
|
var UNCLASSIFIED_DOMAIN_SAMPLE_LIMIT = 10;
|
|
80317
80936
|
function skippedNoProject2() {
|
|
@@ -80324,7 +80943,7 @@ function skippedNoProject2() {
|
|
|
80324
80943
|
}
|
|
80325
80944
|
function loadProject(ctx) {
|
|
80326
80945
|
if (!ctx.project) return null;
|
|
80327
|
-
return ctx.db.select().from(projects).where(
|
|
80946
|
+
return ctx.db.select().from(projects).where(eq62(projects.id, ctx.project.id)).get() ?? null;
|
|
80328
80947
|
}
|
|
80329
80948
|
function percent(value) {
|
|
80330
80949
|
return Math.round(value * 100);
|
|
@@ -80416,7 +81035,7 @@ var CONTENT_CHECK_BY_ID = Object.fromEntries(
|
|
|
80416
81035
|
);
|
|
80417
81036
|
|
|
80418
81037
|
// ../api-routes/src/doctor/checks/ads.ts
|
|
80419
|
-
import { eq as
|
|
81038
|
+
import { eq as eq63 } from "drizzle-orm";
|
|
80420
81039
|
var RECENT_SYNC_WARN_DAYS = 7;
|
|
80421
81040
|
var RECENT_SYNC_FAIL_DAYS = 30;
|
|
80422
81041
|
var adsConnectionCheck = {
|
|
@@ -80433,7 +81052,7 @@ var adsConnectionCheck = {
|
|
|
80433
81052
|
remediation: null
|
|
80434
81053
|
};
|
|
80435
81054
|
}
|
|
80436
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
81055
|
+
const row = ctx.db.select().from(adsConnections).where(eq63(adsConnections.projectId, ctx.project.id)).get();
|
|
80437
81056
|
if (!row) {
|
|
80438
81057
|
return {
|
|
80439
81058
|
status: CheckStatuses.skipped,
|
|
@@ -80483,7 +81102,7 @@ var adsRecentSyncCheck = {
|
|
|
80483
81102
|
remediation: null
|
|
80484
81103
|
};
|
|
80485
81104
|
}
|
|
80486
|
-
const row = ctx.db.select().from(adsConnections).where(
|
|
81105
|
+
const row = ctx.db.select().from(adsConnections).where(eq63(adsConnections.projectId, ctx.project.id)).get();
|
|
80487
81106
|
if (!row) {
|
|
80488
81107
|
return {
|
|
80489
81108
|
status: CheckStatuses.skipped,
|
|
@@ -80674,7 +81293,7 @@ var ga4ConnectionCheck = {
|
|
|
80674
81293
|
var GA_AUTH_CHECKS = [ga4ConnectionCheck];
|
|
80675
81294
|
|
|
80676
81295
|
// ../api-routes/src/doctor/checks/data-freshness.ts
|
|
80677
|
-
import { and as
|
|
81296
|
+
import { and as and55, desc as desc28, eq as eq64, inArray as inArray24 } from "drizzle-orm";
|
|
80678
81297
|
var GA_DATA_AGING_DAYS = 3;
|
|
80679
81298
|
var GA_DATA_STALE_DAYS = 5;
|
|
80680
81299
|
var GSC_DATA_AGING_DAYS = 5;
|
|
@@ -80702,7 +81321,7 @@ function skipped(code, summary3) {
|
|
|
80702
81321
|
return { status: CheckStatuses.skipped, code, summary: summary3, remediation: null };
|
|
80703
81322
|
}
|
|
80704
81323
|
function lastSyncAgeDays(ctx, kinds) {
|
|
80705
|
-
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();
|
|
80706
81325
|
if (!row?.createdAt) return null;
|
|
80707
81326
|
const age = Math.floor((Date.now() - Date.parse(row.createdAt)) / DAY_MS2);
|
|
80708
81327
|
return Number.isFinite(age) ? age : null;
|
|
@@ -80798,7 +81417,7 @@ var gaRecentDataCheck = {
|
|
|
80798
81417
|
const connection = gaConnected(ctx);
|
|
80799
81418
|
if (connection === "store-unavailable") return skipped("ga.data.store-unavailable", "No GA4 credential store configured for this deployment.");
|
|
80800
81419
|
if (connection === "not-connected") return skipped("ga.data.not-connected", "GA4 is not connected for this project.");
|
|
80801
|
-
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();
|
|
80802
81421
|
return grade(ctx, GA_SPEC, newest?.date);
|
|
80803
81422
|
}
|
|
80804
81423
|
};
|
|
@@ -80813,9 +81432,9 @@ var gscRecentDataCheck = {
|
|
|
80813
81432
|
if (!ctx.googleConnectionStore.getConnection(ctx.project.canonicalDomain, "gsc")) {
|
|
80814
81433
|
return skipped("gsc.data.not-connected", "Search Console is not connected for this project.");
|
|
80815
81434
|
}
|
|
80816
|
-
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();
|
|
80817
81436
|
if (watermark?.dataThroughDate) return grade(ctx, GSC_SPEC, watermark.dataThroughDate);
|
|
80818
|
-
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();
|
|
80819
81438
|
return grade(ctx, GSC_SPEC, newest?.date);
|
|
80820
81439
|
}
|
|
80821
81440
|
};
|
|
@@ -81083,7 +81702,7 @@ var siteReachabilityCheck = {
|
|
|
81083
81702
|
var SITE_REACHABILITY_CHECKS = [siteReachabilityCheck];
|
|
81084
81703
|
|
|
81085
81704
|
// ../api-routes/src/doctor/checks/gbp-auth.ts
|
|
81086
|
-
import { and as
|
|
81705
|
+
import { and as and56, eq as eq65 } from "drizzle-orm";
|
|
81087
81706
|
var RECENT_SYNC_WARN_DAYS2 = 4;
|
|
81088
81707
|
var RECENT_SYNC_FAIL_DAYS2 = 30;
|
|
81089
81708
|
function skippedNoProject3() {
|
|
@@ -81316,7 +81935,7 @@ var recentSyncCheck = {
|
|
|
81316
81935
|
title: "GBP recent sync",
|
|
81317
81936
|
run: (ctx) => {
|
|
81318
81937
|
if (!ctx.project) return skippedNoProject3();
|
|
81319
|
-
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();
|
|
81320
81939
|
if (selected.length === 0) {
|
|
81321
81940
|
return {
|
|
81322
81941
|
status: CheckStatuses.skipped,
|
|
@@ -81376,7 +81995,7 @@ var GBP_AUTH_CHECK_BY_ID = Object.fromEntries(
|
|
|
81376
81995
|
);
|
|
81377
81996
|
|
|
81378
81997
|
// ../api-routes/src/doctor/checks/places.ts
|
|
81379
|
-
import { eq as
|
|
81998
|
+
import { eq as eq66 } from "drizzle-orm";
|
|
81380
81999
|
var apiKeyCheck = {
|
|
81381
82000
|
id: "gbp.places.api-key",
|
|
81382
82001
|
category: CheckCategories.auth,
|
|
@@ -81421,7 +82040,7 @@ var apiKeyCheck = {
|
|
|
81421
82040
|
details: { tier: cfg.tier }
|
|
81422
82041
|
};
|
|
81423
82042
|
}
|
|
81424
|
-
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();
|
|
81425
82044
|
const selected = rows.filter((r) => r.selected);
|
|
81426
82045
|
const locationsWithPlaceId = selected.filter((r) => Boolean(r.placeId)).length;
|
|
81427
82046
|
const details = {
|
|
@@ -81963,7 +82582,7 @@ var RUNTIME_STATE_CHECKS = [
|
|
|
81963
82582
|
];
|
|
81964
82583
|
|
|
81965
82584
|
// ../api-routes/src/doctor/checks/traffic-source.ts
|
|
81966
|
-
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";
|
|
81967
82586
|
var RECENT_DATA_WARN_DAYS = 7;
|
|
81968
82587
|
var RECENT_DATA_FAIL_DAYS = 30;
|
|
81969
82588
|
function isCloudflareDirectPush(source) {
|
|
@@ -82002,8 +82621,8 @@ function skippedNoProject5() {
|
|
|
82002
82621
|
function loadProbes(ctx) {
|
|
82003
82622
|
if (!ctx.project) return [];
|
|
82004
82623
|
const rows = ctx.db.select().from(trafficSources).where(
|
|
82005
|
-
|
|
82006
|
-
|
|
82624
|
+
and57(
|
|
82625
|
+
eq67(trafficSources.projectId, ctx.project.id),
|
|
82007
82626
|
ne10(trafficSources.status, TrafficSourceStatuses.archived)
|
|
82008
82627
|
)
|
|
82009
82628
|
).all();
|
|
@@ -82109,28 +82728,28 @@ var recentDataCheck = {
|
|
|
82109
82728
|
const failCutoff = new Date(now.getTime() - RECENT_DATA_FAIL_DAYS * 24 * 60 * 6e4).toISOString();
|
|
82110
82729
|
const recentCrawlers = Number(
|
|
82111
82730
|
ctx.db.select({ total: sql25`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
82112
|
-
|
|
82113
|
-
|
|
82114
|
-
|
|
82115
|
-
|
|
82731
|
+
and57(
|
|
82732
|
+
eq67(crawlerEventsHourly.projectId, ctx.project.id),
|
|
82733
|
+
inArray25(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
82734
|
+
gte17(crawlerEventsHourly.tsHour, warnCutoff)
|
|
82116
82735
|
)
|
|
82117
82736
|
).get()?.total ?? 0
|
|
82118
82737
|
);
|
|
82119
82738
|
const recentReferrals = Number(
|
|
82120
82739
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
82121
|
-
|
|
82122
|
-
|
|
82123
|
-
|
|
82124
|
-
|
|
82740
|
+
and57(
|
|
82741
|
+
eq67(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
82742
|
+
inArray25(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
82743
|
+
gte17(aiReferralEventsHourly.tsHour, warnCutoff)
|
|
82125
82744
|
)
|
|
82126
82745
|
).get()?.total ?? 0
|
|
82127
82746
|
);
|
|
82128
82747
|
const recentUserFetches = Number(
|
|
82129
82748
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
82130
|
-
|
|
82131
|
-
|
|
82132
|
-
|
|
82133
|
-
|
|
82749
|
+
and57(
|
|
82750
|
+
eq67(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
82751
|
+
inArray25(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
82752
|
+
gte17(aiUserFetchEventsHourly.tsHour, warnCutoff)
|
|
82134
82753
|
)
|
|
82135
82754
|
).get()?.total ?? 0
|
|
82136
82755
|
);
|
|
@@ -82153,28 +82772,28 @@ var recentDataCheck = {
|
|
|
82153
82772
|
}
|
|
82154
82773
|
const olderCrawlers = Number(
|
|
82155
82774
|
ctx.db.select({ total: sql25`COALESCE(SUM(${crawlerEventsHourly.hits}), 0)` }).from(crawlerEventsHourly).where(
|
|
82156
|
-
|
|
82157
|
-
|
|
82158
|
-
|
|
82159
|
-
|
|
82775
|
+
and57(
|
|
82776
|
+
eq67(crawlerEventsHourly.projectId, ctx.project.id),
|
|
82777
|
+
inArray25(crawlerEventsHourly.sourceId, activeSourceIds),
|
|
82778
|
+
gte17(crawlerEventsHourly.tsHour, failCutoff)
|
|
82160
82779
|
)
|
|
82161
82780
|
).get()?.total ?? 0
|
|
82162
82781
|
);
|
|
82163
82782
|
const olderReferrals = Number(
|
|
82164
82783
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiReferralEventsHourly.sessionsOrHits}), 0)` }).from(aiReferralEventsHourly).where(
|
|
82165
|
-
|
|
82166
|
-
|
|
82167
|
-
|
|
82168
|
-
|
|
82784
|
+
and57(
|
|
82785
|
+
eq67(aiReferralEventsHourly.projectId, ctx.project.id),
|
|
82786
|
+
inArray25(aiReferralEventsHourly.sourceId, activeSourceIds),
|
|
82787
|
+
gte17(aiReferralEventsHourly.tsHour, failCutoff)
|
|
82169
82788
|
)
|
|
82170
82789
|
).get()?.total ?? 0
|
|
82171
82790
|
);
|
|
82172
82791
|
const olderUserFetches = Number(
|
|
82173
82792
|
ctx.db.select({ total: sql25`COALESCE(SUM(${aiUserFetchEventsHourly.hits}), 0)` }).from(aiUserFetchEventsHourly).where(
|
|
82174
|
-
|
|
82175
|
-
|
|
82176
|
-
|
|
82177
|
-
|
|
82793
|
+
and57(
|
|
82794
|
+
eq67(aiUserFetchEventsHourly.projectId, ctx.project.id),
|
|
82795
|
+
inArray25(aiUserFetchEventsHourly.sourceId, activeSourceIds),
|
|
82796
|
+
gte17(aiUserFetchEventsHourly.tsHour, failCutoff)
|
|
82178
82797
|
)
|
|
82179
82798
|
).get()?.total ?? 0
|
|
82180
82799
|
);
|
|
@@ -83015,6 +83634,9 @@ var ALL_CHECKS = [
|
|
|
83015
83634
|
...WORDPRESS_PUBLISH_CHECKS,
|
|
83016
83635
|
...GA_AUTH_CHECKS,
|
|
83017
83636
|
...DATA_FRESHNESS_CHECKS,
|
|
83637
|
+
reportSweepsCheck,
|
|
83638
|
+
reportModelsCheck,
|
|
83639
|
+
reportDailyDataCheck,
|
|
83018
83640
|
...ADS_CHECKS,
|
|
83019
83641
|
...GOOGLE_MARKETING_DOCTOR_CHECKS,
|
|
83020
83642
|
...PROVIDERS_CHECKS,
|
|
@@ -83025,6 +83647,9 @@ var ALL_CHECKS = [
|
|
|
83025
83647
|
// Network probe last, so a slow or unreachable site never delays the local checks.
|
|
83026
83648
|
...SITE_REACHABILITY_CHECKS
|
|
83027
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
|
+
}
|
|
83028
83653
|
|
|
83029
83654
|
// ../api-routes/src/doctor/runner.ts
|
|
83030
83655
|
function matchesCheckId(checkId, filters) {
|
|
@@ -83057,7 +83682,9 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83057
83682
|
} catch (err) {
|
|
83058
83683
|
const message = describeError(err);
|
|
83059
83684
|
output = {
|
|
83060
|
-
|
|
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,
|
|
83061
83688
|
code: `${definition.id}.runtime-error`,
|
|
83062
83689
|
summary: `Check threw an unexpected error: ${message}`,
|
|
83063
83690
|
remediation: null,
|
|
@@ -83070,6 +83697,7 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83070
83697
|
scope: definition.scope,
|
|
83071
83698
|
title: definition.title,
|
|
83072
83699
|
status: output.status,
|
|
83700
|
+
...definition.notificationPolicy ? { notificationPolicy: definition.notificationPolicy } : {},
|
|
83073
83701
|
code: output.code,
|
|
83074
83702
|
summary: output.summary,
|
|
83075
83703
|
remediation: output.remediation ?? null,
|
|
@@ -83080,6 +83708,7 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83080
83708
|
return {
|
|
83081
83709
|
scope: targetScope,
|
|
83082
83710
|
project: projectName,
|
|
83711
|
+
...ctx.project ? { reportMonths: reportMonthsForDoctor(ctx.reportMonth, startedAt) } : {},
|
|
83083
83712
|
generatedAt: startedAt.toISOString(),
|
|
83084
83713
|
durationMs: Date.now() - startedAt.getTime(),
|
|
83085
83714
|
summary: summarizeCheckResults(results),
|
|
@@ -83088,6 +83717,12 @@ async function runChecks(ctx, checks, options = {}) {
|
|
|
83088
83717
|
}
|
|
83089
83718
|
|
|
83090
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
|
+
}
|
|
83091
83726
|
function parseCheckIds(raw) {
|
|
83092
83727
|
if (!raw) return [];
|
|
83093
83728
|
return raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
|
|
@@ -83102,6 +83737,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83102
83737
|
const checkIds = parseCheckIds(request.query.check);
|
|
83103
83738
|
const ctx = {
|
|
83104
83739
|
db: app.db,
|
|
83740
|
+
reportMonth: parseReportMonth(request.query.reportMonth),
|
|
83105
83741
|
project: null,
|
|
83106
83742
|
googleConnectionStore: opts.googleConnectionStore,
|
|
83107
83743
|
bingConnectionStore: opts.bingConnectionStore,
|
|
@@ -83129,6 +83765,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83129
83765
|
const checkIds = parseCheckIds(request.query.check);
|
|
83130
83766
|
const ctx = {
|
|
83131
83767
|
db: app.db,
|
|
83768
|
+
reportMonth: parseReportMonth(request.query.reportMonth),
|
|
83132
83769
|
project: {
|
|
83133
83770
|
id: project.id,
|
|
83134
83771
|
name: project.name,
|
|
@@ -83160,7 +83797,7 @@ async function doctorRoutes(app, opts) {
|
|
|
83160
83797
|
|
|
83161
83798
|
// ../api-routes/src/discovery/routes.ts
|
|
83162
83799
|
import crypto47 from "crypto";
|
|
83163
|
-
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";
|
|
83164
83801
|
var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
|
|
83165
83802
|
async function discoveryRoutes(app, opts) {
|
|
83166
83803
|
app.post("/projects/:name/discover/run", async (request, reply) => {
|
|
@@ -83194,13 +83831,13 @@ async function discoveryRoutes(app, opts) {
|
|
|
83194
83831
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
83195
83832
|
const ageFloorIso = new Date(Date.now() - MAX_INFLIGHT_DISCOVERY_AGE_MS).toISOString();
|
|
83196
83833
|
const decision = app.db.transaction((tx) => {
|
|
83197
|
-
const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(
|
|
83198
|
-
|
|
83199
|
-
|
|
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),
|
|
83200
83837
|
// Buyer is part of session identity: it changes the seed prompt's
|
|
83201
83838
|
// semantics, so a request with a different (or no) buyer must start
|
|
83202
83839
|
// its own session, never adopt another buyer's probes.
|
|
83203
|
-
parsed.data.buyerDescription == null ? isNull11(discoverySessions.buyerDescription) :
|
|
83840
|
+
parsed.data.buyerDescription == null ? isNull11(discoverySessions.buyerDescription) : eq68(discoverySessions.buyerDescription, parsed.data.buyerDescription),
|
|
83204
83841
|
// Locations are identity too: a different service-area subset seeds
|
|
83205
83842
|
// and probes a different geo, so it must never reuse another geo's
|
|
83206
83843
|
// session. resolveLocations is deterministic (project-config order),
|
|
@@ -83210,17 +83847,17 @@ async function discoveryRoutes(app, opts) {
|
|
|
83210
83847
|
// locations a NULL row's subset is unknowable, so it conservatively
|
|
83211
83848
|
// never reuses — a one-time, bounded (2h window) non-reuse after
|
|
83212
83849
|
// upgrade, never a wrong reuse.
|
|
83213
|
-
locations.length === 0 ? or15(isNull11(discoverySessions.locations),
|
|
83850
|
+
locations.length === 0 ? or15(isNull11(discoverySessions.locations), eq68(discoverySessions.locations, locations)) : eq68(discoverySessions.locations, locations),
|
|
83214
83851
|
// Seed provider set is identity: a different phrasing distribution
|
|
83215
83852
|
// must never reuse another set's session. Null (= Gemini-only
|
|
83216
83853
|
// default, including explicit ['gemini']) matches legacy rows.
|
|
83217
|
-
seedProviders == null ? isNull11(discoverySessions.seedProviders) :
|
|
83218
|
-
|
|
83854
|
+
seedProviders == null ? isNull11(discoverySessions.seedProviders) : eq68(discoverySessions.seedProviders, seedProviders),
|
|
83855
|
+
inArray26(discoverySessions.status, [
|
|
83219
83856
|
DiscoverySessionStatuses.queued,
|
|
83220
83857
|
DiscoverySessionStatuses.seeding,
|
|
83221
83858
|
DiscoverySessionStatuses.probing
|
|
83222
83859
|
]),
|
|
83223
|
-
|
|
83860
|
+
gte18(discoverySessions.createdAt, ageFloorIso)
|
|
83224
83861
|
)).orderBy(desc29(discoverySessions.createdAt)).get();
|
|
83225
83862
|
if (existing && existing.runId) {
|
|
83226
83863
|
return { reused: true, sessionId: existing.id, runId: existing.runId };
|
|
@@ -83290,7 +83927,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83290
83927
|
const project = resolveProject(app.db, request.params.name);
|
|
83291
83928
|
const parsedLimit = parseInt(request.query.limit ?? "", 10);
|
|
83292
83929
|
const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? 50 : parsedLimit;
|
|
83293
|
-
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();
|
|
83294
83931
|
return reply.send(rows.map(serializeSession));
|
|
83295
83932
|
}
|
|
83296
83933
|
);
|
|
@@ -83298,11 +83935,11 @@ async function discoveryRoutes(app, opts) {
|
|
|
83298
83935
|
"/projects/:name/discover/sessions/:id",
|
|
83299
83936
|
async (request, reply) => {
|
|
83300
83937
|
const project = resolveProject(app.db, request.params.name);
|
|
83301
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
83938
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83302
83939
|
if (!session || session.projectId !== project.id) {
|
|
83303
83940
|
throw notFound("Discovery session", request.params.id);
|
|
83304
83941
|
}
|
|
83305
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83942
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83306
83943
|
const detail = {
|
|
83307
83944
|
...serializeSession(session),
|
|
83308
83945
|
probes: probeRows.map(serializeProbe)
|
|
@@ -83314,7 +83951,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83314
83951
|
"/projects/:name/discover/sessions/:id/harvest",
|
|
83315
83952
|
async (request, reply) => {
|
|
83316
83953
|
const project = resolveProject(app.db, request.params.name);
|
|
83317
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
83954
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83318
83955
|
if (!session || session.projectId !== project.id) {
|
|
83319
83956
|
throw notFound("Discovery session", request.params.id);
|
|
83320
83957
|
}
|
|
@@ -83322,7 +83959,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83322
83959
|
const minProbeHits = Number.isNaN(parsedFloor) || parsedFloor < 1 ? 1 : parsedFloor;
|
|
83323
83960
|
const applyAnchor = request.query.anchor !== "false";
|
|
83324
83961
|
const provider = session.seedProvider ?? "gemini";
|
|
83325
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83962
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83326
83963
|
const extract = opts.harvestSearchQueries;
|
|
83327
83964
|
const probesWithQueries = probeRows.map((row) => {
|
|
83328
83965
|
if (!extract || !row.rawResponse) return { searchQueries: [] };
|
|
@@ -83333,7 +83970,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83333
83970
|
return { searchQueries: [] };
|
|
83334
83971
|
}
|
|
83335
83972
|
});
|
|
83336
|
-
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);
|
|
83337
83974
|
const anchorTerms = buildHarvestAnchorTerms(
|
|
83338
83975
|
[session.icpDescription ?? "", ...trackedQueries],
|
|
83339
83976
|
effectiveDomains(project)
|
|
@@ -83380,12 +84017,12 @@ async function discoveryRoutes(app, opts) {
|
|
|
83380
84017
|
"/projects/:name/discover/sessions/:id/promote",
|
|
83381
84018
|
async (request, reply) => {
|
|
83382
84019
|
const project = resolveProject(app.db, request.params.name);
|
|
83383
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
84020
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83384
84021
|
if (!session || session.projectId !== project.id) {
|
|
83385
84022
|
throw notFound("Discovery session", request.params.id);
|
|
83386
84023
|
}
|
|
83387
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
83388
|
-
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());
|
|
83389
84026
|
const seenCompetitors = new Set(existingCompetitors);
|
|
83390
84027
|
const cited = /* @__PURE__ */ new Set();
|
|
83391
84028
|
const aspirational = /* @__PURE__ */ new Set();
|
|
@@ -83414,7 +84051,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83414
84051
|
);
|
|
83415
84052
|
app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
|
|
83416
84053
|
const project = resolveProject(app.db, request.params.name);
|
|
83417
|
-
const session = app.db.select().from(discoverySessions).where(
|
|
84054
|
+
const session = app.db.select().from(discoverySessions).where(eq68(discoverySessions.id, request.params.id)).get();
|
|
83418
84055
|
if (!session || session.projectId !== project.id) {
|
|
83419
84056
|
throw notFound("Discovery session", request.params.id);
|
|
83420
84057
|
}
|
|
@@ -83437,7 +84074,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83437
84074
|
const bucketSet = new Set(buckets);
|
|
83438
84075
|
const includeCompetitors = parsed.data.includeCompetitors ?? true;
|
|
83439
84076
|
const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
|
|
83440
|
-
const probeRows = app.db.select().from(discoveryProbes).where(
|
|
84077
|
+
const probeRows = app.db.select().from(discoveryProbes).where(eq68(discoveryProbes.sessionId, session.id)).all();
|
|
83441
84078
|
const candidateQueries = /* @__PURE__ */ new Set();
|
|
83442
84079
|
for (const probe of probeRows) {
|
|
83443
84080
|
if (!probe.bucket) continue;
|
|
@@ -83445,7 +84082,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83445
84082
|
if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
|
|
83446
84083
|
}
|
|
83447
84084
|
const existingQueries = new Set(
|
|
83448
|
-
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())
|
|
83449
84086
|
);
|
|
83450
84087
|
const promotedQueries = [];
|
|
83451
84088
|
const skippedQueries = [];
|
|
@@ -83461,7 +84098,7 @@ async function discoveryRoutes(app, opts) {
|
|
|
83461
84098
|
const skippedCompetitors = [];
|
|
83462
84099
|
if (includeCompetitors) {
|
|
83463
84100
|
const existingCompetitors = new Set(
|
|
83464
|
-
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())
|
|
83465
84102
|
);
|
|
83466
84103
|
const competitorMap = parseCompetitorMap(session.competitorMap);
|
|
83467
84104
|
for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
|
|
@@ -83580,7 +84217,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
|
|
|
83580
84217
|
|
|
83581
84218
|
// ../api-routes/src/discovery/orchestrate.ts
|
|
83582
84219
|
import crypto48 from "crypto";
|
|
83583
|
-
import { eq as
|
|
84220
|
+
import { eq as eq69 } from "drizzle-orm";
|
|
83584
84221
|
var DEFAULT_MAX_PROBES = 100;
|
|
83585
84222
|
var ABSOLUTE_MAX_PROBES = 500;
|
|
83586
84223
|
function classifyProbeBucket(input) {
|
|
@@ -83667,7 +84304,7 @@ async function executeDiscovery(opts) {
|
|
|
83667
84304
|
status: DiscoverySessionStatuses.seeding,
|
|
83668
84305
|
dedupThreshold,
|
|
83669
84306
|
startedAt
|
|
83670
|
-
}).where(
|
|
84307
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
83671
84308
|
const seedResult = await opts.deps.seed({
|
|
83672
84309
|
project: opts.project,
|
|
83673
84310
|
icpDescription: opts.icpDescription,
|
|
@@ -83728,7 +84365,7 @@ async function executeDiscovery(opts) {
|
|
|
83728
84365
|
dedupBandPairFraction: dedupStats.bandPairFraction,
|
|
83729
84366
|
dedupPairsTotal: dedupStats.pairsTotal,
|
|
83730
84367
|
warning
|
|
83731
|
-
}).where(
|
|
84368
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
83732
84369
|
const probeLocation = opts.locations?.[0];
|
|
83733
84370
|
const probeResults = await mapWithConcurrency(
|
|
83734
84371
|
probedCanonicals,
|
|
@@ -83782,7 +84419,7 @@ async function executeDiscovery(opts) {
|
|
|
83782
84419
|
wastedCount: buckets["wasted-surface"],
|
|
83783
84420
|
competitorMap,
|
|
83784
84421
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
83785
|
-
}).where(
|
|
84422
|
+
}).where(eq69(discoverySessions.id, opts.sessionId)).run();
|
|
83786
84423
|
upsertDomainClassifications(opts.db, opts.project.id, opts.sessionId, competitorMap);
|
|
83787
84424
|
return {
|
|
83788
84425
|
buckets,
|
|
@@ -83822,7 +84459,7 @@ function markSessionFailed(db, sessionId, error) {
|
|
|
83822
84459
|
status: DiscoverySessionStatuses.failed,
|
|
83823
84460
|
error,
|
|
83824
84461
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
83825
|
-
}).where(
|
|
84462
|
+
}).where(eq69(discoverySessions.id, sessionId)).run();
|
|
83826
84463
|
}
|
|
83827
84464
|
function dedupeStrings(input) {
|
|
83828
84465
|
const seen2 = /* @__PURE__ */ new Set();
|
|
@@ -83840,7 +84477,7 @@ function dedupeStrings(input) {
|
|
|
83840
84477
|
|
|
83841
84478
|
// ../api-routes/src/technical-aeo.ts
|
|
83842
84479
|
import crypto49 from "crypto";
|
|
83843
|
-
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";
|
|
83844
84481
|
import { alias } from "drizzle-orm/sqlite-core";
|
|
83845
84482
|
var FETCHED_SITE_CRAWL_STATES = /* @__PURE__ */ new Set([
|
|
83846
84483
|
...SiteCrawlFetchedStates,
|
|
@@ -84053,8 +84690,8 @@ function parseLinkKind(value) {
|
|
|
84053
84690
|
return allowed.data;
|
|
84054
84691
|
}
|
|
84055
84692
|
function linkKindFilter(linkKind) {
|
|
84056
|
-
if (linkKind === SiteHealthLinkKinds.content) return
|
|
84057
|
-
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);
|
|
84058
84695
|
return void 0;
|
|
84059
84696
|
}
|
|
84060
84697
|
var graphSourceNode = alias(siteCrawlGraphNodes, "site_crawl_graph_source_node");
|
|
@@ -84159,50 +84796,50 @@ function changedFields(before, after, fields) {
|
|
|
84159
84796
|
async function technicalAeoRoutes(app, opts) {
|
|
84160
84797
|
const resolveCrawl = (projectId, runId) => {
|
|
84161
84798
|
const filters = [
|
|
84162
|
-
|
|
84163
|
-
|
|
84164
|
-
|
|
84165
|
-
|
|
84799
|
+
eq70(siteCrawlSnapshots.projectId, projectId),
|
|
84800
|
+
eq70(runs.projectId, projectId),
|
|
84801
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
84802
|
+
inArray27(runs.status, SURFACEABLE_STATUSES),
|
|
84166
84803
|
notProbeRun()
|
|
84167
84804
|
];
|
|
84168
84805
|
if (runId) {
|
|
84169
|
-
filters.push(
|
|
84806
|
+
filters.push(eq70(siteCrawlSnapshots.runId, runId));
|
|
84170
84807
|
} else {
|
|
84171
|
-
filters.push(
|
|
84808
|
+
filters.push(eq70(siteCrawlSnapshots.complete, true), eq70(runs.status, RunStatuses.completed));
|
|
84172
84809
|
}
|
|
84173
|
-
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();
|
|
84174
84811
|
};
|
|
84175
84812
|
const detailScopeFor = (projectId, snapshot) => snapshot.detailsAvailable && snapshot.attemptId ? { projectId, runId: snapshot.runId, attemptId: snapshot.attemptId } : null;
|
|
84176
84813
|
const pageInScope = (scope, selector) => {
|
|
84177
84814
|
if (!selector.nodeKey && !selector.url) return null;
|
|
84178
|
-
return app.db.select().from(siteCrawlPages).where(
|
|
84179
|
-
|
|
84180
|
-
|
|
84181
|
-
|
|
84182
|
-
selector.nodeKey ?
|
|
84183
|
-
)).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;
|
|
84184
84821
|
};
|
|
84185
|
-
const rootPageInScope = (scope, rootUrl) => pageInScope(scope, { url: rootUrl }) ?? app.db.select().from(siteCrawlPages).where(
|
|
84186
|
-
|
|
84187
|
-
|
|
84188
|
-
|
|
84189
|
-
|
|
84190
|
-
)).orderBy(
|
|
84191
|
-
const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(
|
|
84192
|
-
|
|
84193
|
-
|
|
84194
|
-
|
|
84195
|
-
|
|
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),
|
|
84196
84833
|
notProbeRun()
|
|
84197
84834
|
)).limit(1).get());
|
|
84198
84835
|
const assertKnownAuditRun = (projectId, runId) => {
|
|
84199
84836
|
if (runId && !isSurfaceableAuditRun(projectId, runId)) throw notFound("Site crawl run", runId);
|
|
84200
84837
|
};
|
|
84201
|
-
const hasLegacyAudit = (projectId) => Boolean(app.db.select({ runId: siteAuditSnapshots.runId }).from(siteAuditSnapshots).innerJoin(runs,
|
|
84202
|
-
|
|
84203
|
-
|
|
84204
|
-
|
|
84205
|
-
|
|
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),
|
|
84206
84843
|
notProbeRun()
|
|
84207
84844
|
)).limit(1).get());
|
|
84208
84845
|
const emptyCrawlSummary = (projectName, legacyAuditAvailable) => ({
|
|
@@ -84223,20 +84860,20 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84223
84860
|
app.get("/projects/:name/technical-aeo", async (request) => {
|
|
84224
84861
|
const project = resolveProject(app.db, request.params.name);
|
|
84225
84862
|
const baseFilters = [
|
|
84226
|
-
|
|
84227
|
-
|
|
84228
|
-
|
|
84229
|
-
|
|
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),
|
|
84230
84867
|
notProbeRun()
|
|
84231
84868
|
];
|
|
84232
|
-
const targetFilters = request.query.runId ? [...baseFilters,
|
|
84233
|
-
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();
|
|
84234
84871
|
if (!latest) {
|
|
84235
84872
|
if (request.query.runId) throw notFound("Site audit run", request.query.runId);
|
|
84236
84873
|
return emptyScore(project.name);
|
|
84237
84874
|
}
|
|
84238
84875
|
const snap = latest.snap;
|
|
84239
|
-
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;
|
|
84240
84877
|
const deltaScore = previous ? snap.aggregateScore - previous.aggregateScore : null;
|
|
84241
84878
|
const trend = deltaScore == null ? null : deltaScore > 0 ? SiteAuditTrendDirections.up : deltaScore < 0 ? SiteAuditTrendDirections.down : SiteAuditTrendDirections.flat;
|
|
84242
84879
|
return {
|
|
@@ -84263,14 +84900,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84263
84900
|
app.get("/projects/:name/technical-aeo/pages", async (request) => {
|
|
84264
84901
|
const project = resolveProject(app.db, request.params.name);
|
|
84265
84902
|
const targetFilters = [
|
|
84266
|
-
|
|
84267
|
-
|
|
84268
|
-
|
|
84269
|
-
|
|
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),
|
|
84270
84907
|
notProbeRun()
|
|
84271
84908
|
];
|
|
84272
|
-
if (request.query.runId) targetFilters.push(
|
|
84273
|
-
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();
|
|
84274
84911
|
if (!latest && request.query.runId) {
|
|
84275
84912
|
throw notFound("Site audit run", request.query.runId);
|
|
84276
84913
|
}
|
|
@@ -84278,14 +84915,14 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84278
84915
|
return { project: project.name, runId: null, auditedAt: null, total: 0, pages: [] };
|
|
84279
84916
|
}
|
|
84280
84917
|
const statusFilter = request.query.status === "success" || request.query.status === "error" ? request.query.status : null;
|
|
84281
|
-
const conds = [
|
|
84282
|
-
if (statusFilter) conds.push(
|
|
84283
|
-
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);
|
|
84284
84921
|
const totalRow = app.db.select({ value: count2() }).from(siteAuditPages).where(where).get();
|
|
84285
84922
|
const total = totalRow?.value ?? 0;
|
|
84286
84923
|
const limit = parsePositiveInt(request.query.limit, 100, 500);
|
|
84287
84924
|
const offset = parsePositiveInt(request.query.offset, 0, Number.MAX_SAFE_INTEGER);
|
|
84288
|
-
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);
|
|
84289
84926
|
const rows = app.db.select().from(siteAuditPages).where(where).orderBy(orderBy).limit(limit).offset(offset).all();
|
|
84290
84927
|
const pages = rows.map((row) => ({
|
|
84291
84928
|
url: row.url,
|
|
@@ -84304,11 +84941,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84304
84941
|
auditedAt: siteAuditSnapshots.auditedAt,
|
|
84305
84942
|
aggregateScore: siteAuditSnapshots.aggregateScore,
|
|
84306
84943
|
pagesAudited: siteAuditSnapshots.pagesAudited
|
|
84307
|
-
}).from(siteAuditSnapshots).innerJoin(runs,
|
|
84308
|
-
|
|
84309
|
-
|
|
84310
|
-
|
|
84311
|
-
|
|
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),
|
|
84312
84949
|
notProbeRun()
|
|
84313
84950
|
)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(limit).all();
|
|
84314
84951
|
return { project: project.name, points: rows.reverse() };
|
|
@@ -84402,10 +85039,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84402
85039
|
{ projectId: project.id, runId: snapshot.runId, attemptId: snapshot.attemptId },
|
|
84403
85040
|
snapshot.rootUrl
|
|
84404
85041
|
)?.nodeKey ?? null;
|
|
84405
|
-
const persistedLayout = app.db.select().from(siteCrawlGraphLayouts).where(
|
|
84406
|
-
|
|
84407
|
-
|
|
84408
|
-
|
|
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)
|
|
84409
85046
|
)).limit(1).get();
|
|
84410
85047
|
if (!persistedLayout) {
|
|
84411
85048
|
return {
|
|
@@ -84456,9 +85093,9 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84456
85093
|
const maxNodes = parseBoundedLimit(request.query.maxNodes, SITE_CRAWL_GRAPH_DEFAULT_MAX_NODES, SITE_CRAWL_GRAPH_MAX_NODES);
|
|
84457
85094
|
const maxEdges = parseBoundedLimit(request.query.maxEdges, SITE_CRAWL_GRAPH_DEFAULT_MAX_EDGES, SITE_CRAWL_GRAPH_MAX_EDGES);
|
|
84458
85095
|
const graphScope = [
|
|
84459
|
-
|
|
84460
|
-
|
|
84461
|
-
|
|
85096
|
+
eq70(siteCrawlGraphNodes.projectId, project.id),
|
|
85097
|
+
eq70(siteCrawlGraphNodes.runId, snapshot.runId),
|
|
85098
|
+
eq70(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
|
|
84462
85099
|
];
|
|
84463
85100
|
const nodeRows = app.db.select({
|
|
84464
85101
|
nodeKey: siteCrawlPages.nodeKey,
|
|
@@ -84477,17 +85114,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84477
85114
|
linkScoreNormalized: siteCrawlPages.linkScoreNormalized,
|
|
84478
85115
|
x: siteCrawlGraphNodes.x,
|
|
84479
85116
|
y: siteCrawlGraphNodes.y
|
|
84480
|
-
}).from(siteCrawlGraphNodes).innerJoin(siteCrawlPages,
|
|
84481
|
-
|
|
84482
|
-
|
|
84483
|
-
|
|
84484
|
-
|
|
84485
|
-
)).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();
|
|
84486
85123
|
const nodes = nodeRows.map(({ indexabilityReasons, canonicalNodeKey, ...row }) => ({
|
|
84487
85124
|
...row,
|
|
84488
85125
|
healthState: deriveSiteHealthState({ ...row, indexabilityReasons, canonicalNodeKey })
|
|
84489
85126
|
}));
|
|
84490
|
-
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 :
|
|
85127
|
+
const graphLinkKindFilter = linkKind === SiteHealthLinkKinds.all ? void 0 : eq70(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
|
|
84491
85128
|
const edges = app.db.select({
|
|
84492
85129
|
edgeKey: siteCrawlGraphEdges.edgeKey,
|
|
84493
85130
|
sourceNodeKey: siteCrawlGraphEdges.sourceNodeKey,
|
|
@@ -84495,25 +85132,25 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84495
85132
|
followable: siteCrawlGraphEdges.followable,
|
|
84496
85133
|
occurrences: siteCrawlGraphEdges.occurrences,
|
|
84497
85134
|
isTemplate: siteCrawlGraphEdges.isTemplate
|
|
84498
|
-
}).from(siteCrawlGraphEdges).innerJoin(graphSourceNode,
|
|
84499
|
-
|
|
84500
|
-
|
|
84501
|
-
|
|
84502
|
-
|
|
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),
|
|
84503
85140
|
lt11(graphSourceNode.sampleRank, maxNodes)
|
|
84504
|
-
)).innerJoin(graphTargetNode,
|
|
84505
|
-
|
|
84506
|
-
|
|
84507
|
-
|
|
84508
|
-
|
|
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),
|
|
84509
85146
|
lt11(graphTargetNode.sampleRank, maxNodes)
|
|
84510
|
-
)).where(
|
|
84511
|
-
|
|
84512
|
-
|
|
84513
|
-
|
|
85147
|
+
)).where(and59(
|
|
85148
|
+
eq70(siteCrawlGraphEdges.projectId, project.id),
|
|
85149
|
+
eq70(siteCrawlGraphEdges.runId, snapshot.runId),
|
|
85150
|
+
eq70(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
|
|
84514
85151
|
lt11(siteCrawlGraphEdges.sampleRank, maxEdges),
|
|
84515
85152
|
graphLinkKindFilter
|
|
84516
|
-
)).orderBy(
|
|
85153
|
+
)).orderBy(asc12(siteCrawlGraphEdges.sampleRank)).all();
|
|
84517
85154
|
const totalNodes = persistedLayout.totalNodes;
|
|
84518
85155
|
const totalEdges = persistedLayout.totalEdges;
|
|
84519
85156
|
const totalTemplateEdges = persistedLayout.totalTemplateEdges;
|
|
@@ -84604,17 +85241,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84604
85241
|
let frontier = [focus.nodeKey];
|
|
84605
85242
|
let queryTruncated = false;
|
|
84606
85243
|
for (let distance = 0; distance < hops && frontier.length > 0; distance += 1) {
|
|
84607
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
84608
|
-
|
|
84609
|
-
|
|
84610
|
-
|
|
84611
|
-
|
|
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),
|
|
84612
85249
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
84613
85250
|
or16(
|
|
84614
|
-
|
|
84615
|
-
|
|
85251
|
+
inArray27(siteCrawlEdges.sourceNodeKey, frontier),
|
|
85252
|
+
inArray27(siteCrawlEdges.targetNodeKey, frontier)
|
|
84616
85253
|
)
|
|
84617
|
-
)).orderBy(
|
|
85254
|
+
)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
|
|
84618
85255
|
if (candidates.length > maxEdges + maxNodes) queryTruncated = true;
|
|
84619
85256
|
const next = /* @__PURE__ */ new Set();
|
|
84620
85257
|
for (const edge of candidates.slice(0, maxEdges + maxNodes)) {
|
|
@@ -84645,22 +85282,22 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84645
85282
|
frontier = [...next].sort();
|
|
84646
85283
|
}
|
|
84647
85284
|
const nodeKeys = [...distances.keys()];
|
|
84648
|
-
const pageRows = nodeKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
84649
|
-
|
|
84650
|
-
|
|
84651
|
-
|
|
84652
|
-
|
|
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)
|
|
84653
85290
|
)).all();
|
|
84654
85291
|
const persistedNodeKeys = new Set(pageRows.map((row) => row.nodeKey));
|
|
84655
85292
|
const outermostNodeKeys = pageRows.filter((row) => distances.get(row.nodeKey) === hops).map((row) => row.nodeKey);
|
|
84656
|
-
const omittedOutermostEdge = outermostNodeKeys.length === 0 ? void 0 : app.db.select({ edgeKey: siteCrawlEdges.edgeKey }).from(siteCrawlEdges).where(
|
|
84657
|
-
|
|
84658
|
-
|
|
84659
|
-
|
|
84660
|
-
|
|
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),
|
|
84661
85298
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
84662
|
-
|
|
84663
|
-
|
|
85299
|
+
inArray27(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
|
|
85300
|
+
inArray27(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
|
|
84664
85301
|
)).limit(1).get();
|
|
84665
85302
|
if (omittedOutermostEdge && !seenEdgeKeys.has(omittedOutermostEdge.edgeKey)) {
|
|
84666
85303
|
omittedEdgeKeys.add(omittedOutermostEdge.edgeKey);
|
|
@@ -84755,23 +85392,23 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84755
85392
|
let found = fromPage.nodeKey === toPage.nodeKey;
|
|
84756
85393
|
let truncated = false;
|
|
84757
85394
|
for (let depth = 0; depth < maxDepth && frontier.length > 0 && !found; depth += 1) {
|
|
84758
|
-
const candidates = app.db.select().from(siteCrawlEdges).where(
|
|
84759
|
-
|
|
84760
|
-
|
|
84761
|
-
|
|
84762
|
-
|
|
84763
|
-
|
|
84764
|
-
|
|
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"),
|
|
84765
85402
|
isNotNull5(siteCrawlEdges.targetNodeKey),
|
|
84766
|
-
|
|
84767
|
-
)).orderBy(
|
|
85403
|
+
inArray27(siteCrawlEdges.sourceNodeKey, frontier)
|
|
85404
|
+
)).orderBy(asc12(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
|
|
84768
85405
|
if (candidates.length > SITE_HEALTH_PATH_MAX_VISITED_NODES) truncated = true;
|
|
84769
85406
|
const candidateTargetKeys = [...new Set(candidates.map((edge) => edge.targetNodeKey).filter((nodeKey) => nodeKey != null))];
|
|
84770
|
-
const persistedTargetKeys = new Set(candidateTargetKeys.length === 0 ? [] : app.db.select({ nodeKey: siteCrawlPages.nodeKey }).from(siteCrawlPages).where(
|
|
84771
|
-
|
|
84772
|
-
|
|
84773
|
-
|
|
84774
|
-
|
|
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)
|
|
84775
85412
|
)).all().map((row) => row.nodeKey));
|
|
84776
85413
|
const next = /* @__PURE__ */ new Set();
|
|
84777
85414
|
for (const edge of candidates.slice(0, SITE_HEALTH_PATH_MAX_VISITED_NODES)) {
|
|
@@ -84813,11 +85450,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84813
85450
|
pathEdges.unshift(step.edge);
|
|
84814
85451
|
pathKeys.unshift(step.nodeKey);
|
|
84815
85452
|
}
|
|
84816
|
-
const pathRows = app.db.select().from(siteCrawlPages).where(
|
|
84817
|
-
|
|
84818
|
-
|
|
84819
|
-
|
|
84820
|
-
|
|
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)
|
|
84821
85458
|
)).all();
|
|
84822
85459
|
const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
|
|
84823
85460
|
const pathDetection = templateDetectionOf(snapshot.templateDetection);
|
|
@@ -84846,21 +85483,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
84846
85483
|
throw validationError("change must be all, added, removed, or changed");
|
|
84847
85484
|
}
|
|
84848
85485
|
const completeSnapshotFilters = [
|
|
84849
|
-
|
|
84850
|
-
|
|
84851
|
-
|
|
84852
|
-
|
|
84853
|
-
|
|
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),
|
|
84854
85491
|
notProbeRun()
|
|
84855
85492
|
];
|
|
84856
|
-
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();
|
|
84857
85494
|
if (request.query.toRunId && !afterTarget) throw notFound("Site crawl run", request.query.toRunId);
|
|
84858
|
-
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(
|
|
84859
85496
|
...completeSnapshotFilters,
|
|
84860
85497
|
or16(
|
|
84861
85498
|
lt11(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
84862
|
-
|
|
84863
|
-
|
|
85499
|
+
and59(
|
|
85500
|
+
eq70(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
|
|
84864
85501
|
lt11(siteCrawlSnapshots.runId, afterTarget.snapshot.runId)
|
|
84865
85502
|
)
|
|
84866
85503
|
)
|
|
@@ -85095,18 +85732,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85095
85732
|
const visibleKeys = keyRows.slice(0, limit);
|
|
85096
85733
|
const pageKeys = visibleKeys.filter((row) => row.entity === "page").map((row) => row.key);
|
|
85097
85734
|
const linkKeys = visibleKeys.filter((row) => row.entity === "link").map((row) => row.key);
|
|
85098
|
-
const pageRowsFor = (scope) => pageKeys.length === 0 ? [] : app.db.select().from(siteCrawlPages).where(
|
|
85099
|
-
|
|
85100
|
-
|
|
85101
|
-
|
|
85102
|
-
|
|
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)
|
|
85103
85740
|
)).all();
|
|
85104
|
-
const edgeRowsFor = (scope) => linkKeys.length === 0 ? [] : app.db.select().from(siteCrawlEdges).where(
|
|
85105
|
-
|
|
85106
|
-
|
|
85107
|
-
|
|
85108
|
-
|
|
85109
|
-
|
|
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)
|
|
85110
85747
|
)).all();
|
|
85111
85748
|
const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
85112
85749
|
const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
@@ -85231,30 +85868,30 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85231
85868
|
return { project: project.name, hasCrawlData: true, runId: snapshot.runId, total: 0, nextCursor: null, healthStateFilter: null, pages: [] };
|
|
85232
85869
|
}
|
|
85233
85870
|
const filters = [
|
|
85234
|
-
|
|
85235
|
-
|
|
85236
|
-
|
|
85871
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
85872
|
+
eq70(siteCrawlPages.runId, snapshot.runId),
|
|
85873
|
+
eq70(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
85237
85874
|
];
|
|
85238
|
-
if (request.query.nodeKey) filters.push(
|
|
85875
|
+
if (request.query.nodeKey) filters.push(eq70(siteCrawlPages.nodeKey, request.query.nodeKey));
|
|
85239
85876
|
const inventoryEligible = parseBoolean2(request.query.inventoryEligible);
|
|
85240
|
-
if (inventoryEligible != null) filters.push(
|
|
85241
|
-
if (request.query.fetchState) filters.push(
|
|
85242
|
-
if (request.query.indexabilityState) filters.push(
|
|
85243
|
-
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));
|
|
85244
85881
|
const healthState = parseSiteHealthState(request.query.healthState);
|
|
85245
85882
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85246
85883
|
const offset = decodeCursor(request.query.cursor);
|
|
85247
|
-
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)];
|
|
85248
85885
|
let healthStateFilter = null;
|
|
85249
85886
|
if (healthState) {
|
|
85250
|
-
const legacyRow = app.db.select({ id: siteCrawlPages.id }).from(siteCrawlPages).where(
|
|
85251
|
-
|
|
85252
|
-
|
|
85253
|
-
|
|
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),
|
|
85254
85891
|
isNull12(siteCrawlPages.healthState)
|
|
85255
85892
|
)).limit(1).get();
|
|
85256
85893
|
healthStateFilter = legacyRow ? "unavailable-legacy-scan" : "applied";
|
|
85257
|
-
if (healthStateFilter === "applied") filters.push(
|
|
85894
|
+
if (healthStateFilter === "applied") filters.push(eq70(siteCrawlPages.healthState, healthState));
|
|
85258
85895
|
}
|
|
85259
85896
|
if (healthStateFilter === "unavailable-legacy-scan") {
|
|
85260
85897
|
return {
|
|
@@ -85267,7 +85904,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85267
85904
|
pages: []
|
|
85268
85905
|
};
|
|
85269
85906
|
}
|
|
85270
|
-
const where =
|
|
85907
|
+
const where = and59(...filters);
|
|
85271
85908
|
const total = app.db.select({ value: count2() }).from(siteCrawlPages).where(where).get()?.value ?? 0;
|
|
85272
85909
|
const rows = app.db.select().from(siteCrawlPages).where(where).orderBy(...orderBy).limit(limit).offset(offset).all();
|
|
85273
85910
|
const nextOffset = offset + rows.length;
|
|
@@ -85298,10 +85935,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85298
85935
|
url: siteCrawlPages.url,
|
|
85299
85936
|
inventoryEligible: siteCrawlPages.inventoryEligible,
|
|
85300
85937
|
fetchState: siteCrawlPages.fetchState
|
|
85301
|
-
}).from(siteCrawlPages).where(
|
|
85302
|
-
|
|
85303
|
-
|
|
85304
|
-
|
|
85938
|
+
}).from(siteCrawlPages).where(and59(
|
|
85939
|
+
eq70(siteCrawlPages.projectId, project.id),
|
|
85940
|
+
eq70(siteCrawlPages.runId, snapshot.runId),
|
|
85941
|
+
eq70(siteCrawlPages.attemptId, snapshot.attemptId)
|
|
85305
85942
|
)).limit(MAX_STRUCTURE_SOURCE_ROWS + 1).all();
|
|
85306
85943
|
if (sourceRows.length > MAX_STRUCTURE_SOURCE_ROWS) {
|
|
85307
85944
|
throw validationError(`Persisted crawl exceeds the ${MAX_STRUCTURE_SOURCE_ROWS}-page structure limit`);
|
|
@@ -85380,21 +86017,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85380
86017
|
};
|
|
85381
86018
|
}
|
|
85382
86019
|
const filters = [
|
|
85383
|
-
|
|
85384
|
-
|
|
85385
|
-
|
|
85386
|
-
|
|
86020
|
+
eq70(siteCrawlEdges.projectId, project.id),
|
|
86021
|
+
eq70(siteCrawlEdges.runId, snapshot.runId),
|
|
86022
|
+
eq70(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
86023
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85387
86024
|
linkKindFilter(linkKind)
|
|
85388
86025
|
];
|
|
85389
|
-
if (request.query.sourceUrl) filters.push(
|
|
85390
|
-
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));
|
|
85391
86028
|
const followable = parseBoolean2(request.query.followable);
|
|
85392
|
-
if (followable != null) filters.push(
|
|
85393
|
-
const where =
|
|
86029
|
+
if (followable != null) filters.push(eq70(siteCrawlEdges.followable, followable));
|
|
86030
|
+
const where = and59(...filters);
|
|
85394
86031
|
const total = app.db.select({ value: count2() }).from(siteCrawlEdges).where(where).get()?.value ?? 0;
|
|
85395
86032
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85396
86033
|
const offset = decodeCursor(request.query.cursor);
|
|
85397
|
-
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();
|
|
85398
86035
|
const nextOffset = offset + rows.length;
|
|
85399
86036
|
return {
|
|
85400
86037
|
project: project.name,
|
|
@@ -85448,17 +86085,17 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85448
86085
|
};
|
|
85449
86086
|
}
|
|
85450
86087
|
const scope = [
|
|
85451
|
-
|
|
85452
|
-
|
|
85453
|
-
|
|
85454
|
-
|
|
86088
|
+
eq70(siteCrawlEdges.projectId, project.id),
|
|
86089
|
+
eq70(siteCrawlEdges.runId, snapshot.runId),
|
|
86090
|
+
eq70(siteCrawlEdges.attemptId, snapshot.attemptId),
|
|
86091
|
+
eq70(siteCrawlEdges.internal, true),
|
|
85455
86092
|
linkKindFilter(linkKind)
|
|
85456
86093
|
];
|
|
85457
|
-
const inboundMatch = request.query.nodeKey && request.query.url ? or16(
|
|
85458
|
-
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);
|
|
85459
86096
|
const limit = parseBoundedLimit(request.query.limit, 50, 100);
|
|
85460
|
-
const inboundRows = app.db.select().from(siteCrawlEdges).where(
|
|
85461
|
-
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();
|
|
85462
86099
|
return {
|
|
85463
86100
|
project: project.name,
|
|
85464
86101
|
hasCrawlData: true,
|
|
@@ -85491,11 +86128,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85491
86128
|
if (!snapshot.attemptId) {
|
|
85492
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: [] };
|
|
85493
86130
|
}
|
|
85494
|
-
const where =
|
|
85495
|
-
|
|
85496
|
-
|
|
85497
|
-
|
|
85498
|
-
|
|
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"),
|
|
85499
86136
|
// Belt and braces on the client-facing surface: a row without a status
|
|
85500
86137
|
// code is not evidence of a broken link, and this response is where such
|
|
85501
86138
|
// a row would be read as one. Migration 140 removed every stored row that
|
|
@@ -85510,7 +86147,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85510
86147
|
const total = app.db.select({ value: count2() }).from(siteCrawlFindings).where(where).get()?.value ?? 0;
|
|
85511
86148
|
const limit = parseBoundedLimit(request.query.limit, 100, 200);
|
|
85512
86149
|
const offset = decodeCursor(request.query.cursor);
|
|
85513
|
-
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();
|
|
85514
86151
|
const nextOffset = offset + rows.length;
|
|
85515
86152
|
const deadLinks = rows.map((row) => ({
|
|
85516
86153
|
findingKey: row.findingKey,
|
|
@@ -85544,19 +86181,19 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85544
86181
|
createdAt: runs.createdAt,
|
|
85545
86182
|
startedAt: runs.startedAt,
|
|
85546
86183
|
finishedAt: runs.finishedAt
|
|
85547
|
-
}).from(runs).where(
|
|
85548
|
-
|
|
85549
|
-
|
|
86184
|
+
}).from(runs).where(and59(
|
|
86185
|
+
eq70(runs.projectId, project.id),
|
|
86186
|
+
eq70(runs.kind, RunKinds["site-audit"]),
|
|
85550
86187
|
notProbeRun()
|
|
85551
86188
|
)).orderBy(desc30(runs.createdAt), desc30(runs.id)).limit(limit).all();
|
|
85552
86189
|
const runIds = rows.map((row) => row.runId);
|
|
85553
|
-
const crawlRunIds = new Set(runIds.length === 0 ? [] : app.db.select({ runId: siteCrawlSnapshots.runId }).from(siteCrawlSnapshots).innerJoin(runs,
|
|
85554
|
-
|
|
85555
|
-
|
|
85556
|
-
|
|
85557
|
-
|
|
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),
|
|
85558
86195
|
notProbeRun(),
|
|
85559
|
-
|
|
86196
|
+
inArray27(siteCrawlSnapshots.runId, runIds)
|
|
85560
86197
|
)).all().map((row) => row.runId));
|
|
85561
86198
|
return {
|
|
85562
86199
|
project: project.name,
|
|
@@ -85569,21 +86206,21 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85569
86206
|
});
|
|
85570
86207
|
app.get("/projects/:name/technical-aeo/runs/:runId/progress", async (request) => {
|
|
85571
86208
|
const project = resolveProject(app.db, request.params.name);
|
|
85572
|
-
const run = app.db.select().from(runs).where(
|
|
85573
|
-
|
|
85574
|
-
|
|
85575
|
-
|
|
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"]),
|
|
85576
86213
|
notProbeRun()
|
|
85577
86214
|
)).get();
|
|
85578
86215
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
85579
|
-
const attempt = app.db.select().from(siteCrawlAttempts).where(
|
|
85580
|
-
|
|
85581
|
-
|
|
86216
|
+
const attempt = app.db.select().from(siteCrawlAttempts).where(and59(
|
|
86217
|
+
eq70(siteCrawlAttempts.projectId, project.id),
|
|
86218
|
+
eq70(siteCrawlAttempts.runId, run.id)
|
|
85582
86219
|
)).orderBy(desc30(siteCrawlAttempts.attemptNumber), desc30(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
85583
|
-
const persistedLayout = attempt ? app.db.select().from(siteCrawlGraphLayouts).where(
|
|
85584
|
-
|
|
85585
|
-
|
|
85586
|
-
|
|
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)
|
|
85587
86224
|
)).get() : void 0;
|
|
85588
86225
|
const layoutState = persistedLayout?.state === "ready" || persistedLayout?.state === "unavailable" ? persistedLayout.state : run.status === RunStatuses.completed || run.status === RunStatuses.partial ? "unavailable" : "pending";
|
|
85589
86226
|
return {
|
|
@@ -85614,18 +86251,18 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85614
86251
|
};
|
|
85615
86252
|
});
|
|
85616
86253
|
app.get("/projects/:name/technical-aeo/runs/:runId/page-health-preview", async (request) => app.db.transaction((tx) => {
|
|
85617
|
-
const project = tx.select().from(projects).where(
|
|
86254
|
+
const project = tx.select().from(projects).where(eq70(projects.name, request.params.name)).get();
|
|
85618
86255
|
if (!project) throw notFound("Project", request.params.name);
|
|
85619
|
-
const run = tx.select().from(runs).where(
|
|
85620
|
-
|
|
85621
|
-
|
|
85622
|
-
|
|
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"]),
|
|
85623
86260
|
notProbeRun()
|
|
85624
86261
|
)).get();
|
|
85625
86262
|
if (!run) throw notFound("Site audit run", request.params.runId);
|
|
85626
|
-
const attempt = tx.select().from(siteCrawlAttempts).where(
|
|
85627
|
-
|
|
85628
|
-
|
|
86263
|
+
const attempt = tx.select().from(siteCrawlAttempts).where(and59(
|
|
86264
|
+
eq70(siteCrawlAttempts.projectId, project.id),
|
|
86265
|
+
eq70(siteCrawlAttempts.runId, run.id)
|
|
85629
86266
|
)).orderBy(desc30(siteCrawlAttempts.attemptNumber), desc30(siteCrawlAttempts.updatedAt)).limit(1).get();
|
|
85630
86267
|
const state = run.status === RunStatuses.queued ? "waiting" : run.status === RunStatuses.running ? "collecting" : "terminal";
|
|
85631
86268
|
const base = {
|
|
@@ -85639,11 +86276,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85639
86276
|
if (!attempt) {
|
|
85640
86277
|
return { ...base, pagesAudited: 0, examples: [] };
|
|
85641
86278
|
}
|
|
85642
|
-
const auditedWhere =
|
|
85643
|
-
|
|
85644
|
-
|
|
85645
|
-
|
|
85646
|
-
|
|
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")
|
|
85647
86284
|
);
|
|
85648
86285
|
const pagesAudited = tx.select({ value: count2() }).from(siteCrawlPages).where(auditedWhere).get()?.value ?? 0;
|
|
85649
86286
|
if (state !== "collecting") {
|
|
@@ -85654,11 +86291,11 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85654
86291
|
url: siteCrawlPages.url,
|
|
85655
86292
|
auditScore: siteCrawlPages.auditScore,
|
|
85656
86293
|
auditFields: siteCrawlPages.auditFields
|
|
85657
|
-
}).from(siteCrawlPages).where(
|
|
86294
|
+
}).from(siteCrawlPages).where(and59(
|
|
85658
86295
|
auditedWhere,
|
|
85659
86296
|
isNotNull5(siteCrawlPages.auditScore),
|
|
85660
86297
|
lt11(siteCrawlPages.auditScore, 70)
|
|
85661
|
-
)).orderBy(
|
|
86298
|
+
)).orderBy(asc12(siteCrawlPages.auditScore), asc12(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
|
|
85662
86299
|
const examples = candidates.flatMap((candidate) => {
|
|
85663
86300
|
if (candidate.auditScore == null) return [];
|
|
85664
86301
|
const checksNeedingAttention = countLivePageHealthChecks(candidate.auditFields);
|
|
@@ -85690,13 +86327,13 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85690
86327
|
status: runs.status,
|
|
85691
86328
|
identityKey: siteCrawlRunRequests.identityKey,
|
|
85692
86329
|
effectiveOptions: siteCrawlRunRequests.effectiveOptions
|
|
85693
|
-
}).from(runs).leftJoin(siteCrawlRunRequests,
|
|
85694
|
-
|
|
85695
|
-
|
|
85696
|
-
)).where(
|
|
85697
|
-
|
|
85698
|
-
|
|
85699
|
-
|
|
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])
|
|
85700
86337
|
)).get();
|
|
85701
86338
|
if (existing) {
|
|
85702
86339
|
if (existing.identityKey === identityKey) {
|
|
@@ -85750,7 +86387,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
85750
86387
|
// ../api-routes/src/research.ts
|
|
85751
86388
|
import crypto50 from "crypto";
|
|
85752
86389
|
import { z as z5 } from "zod";
|
|
85753
|
-
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";
|
|
85754
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;
|
|
85755
86392
|
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
85756
86393
|
async function researchRoutes(app, opts) {
|
|
@@ -85768,7 +86405,7 @@ async function researchRoutes(app, opts) {
|
|
|
85768
86405
|
const input = parsed.data;
|
|
85769
86406
|
if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
85770
86407
|
const requestHash2 = directResearchRequestHash(input);
|
|
85771
|
-
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;
|
|
85772
86409
|
if (existingReceipt) {
|
|
85773
86410
|
assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
|
|
85774
86411
|
const result2 = getDetail(app, project.id, existingReceipt.id);
|
|
@@ -85800,7 +86437,7 @@ async function researchRoutes(app, opts) {
|
|
|
85800
86437
|
const initiatedBy = researchPrincipal(request);
|
|
85801
86438
|
const decision = app.db.transaction((tx) => {
|
|
85802
86439
|
if (input.idempotencyKey) {
|
|
85803
|
-
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();
|
|
85804
86441
|
if (existing) {
|
|
85805
86442
|
assertSameDirectResearchRequest(existing, input, requestHash2);
|
|
85806
86443
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
@@ -85808,9 +86445,9 @@ async function researchRoutes(app, opts) {
|
|
|
85808
86445
|
}
|
|
85809
86446
|
if (initiatedBy?.limited) {
|
|
85810
86447
|
const { start, end, date } = utcDayBounds(now);
|
|
85811
|
-
const used = tx.select({ value: count3() }).from(researchRuns).where(
|
|
85812
|
-
|
|
85813
|
-
|
|
86448
|
+
const used = tx.select({ value: count3() }).from(researchRuns).where(and60(
|
|
86449
|
+
eq71(researchRuns.projectId, project.id),
|
|
86450
|
+
gte19(researchRuns.createdAt, start),
|
|
85814
86451
|
lt12(researchRuns.createdAt, end),
|
|
85815
86452
|
sql27`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
85816
86453
|
)).get()?.value ?? 0;
|
|
@@ -85840,15 +86477,15 @@ async function researchRoutes(app, opts) {
|
|
|
85840
86477
|
if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
85841
86478
|
const requestHash2 = researchBatchRequestHash(input);
|
|
85842
86479
|
const receiptKeys = input.runs.map((_run, index2) => batchChildReceiptKey(input.idempotencyKey, index2));
|
|
85843
|
-
const firstReceipt = app.db.select().from(researchRuns).where(
|
|
85844
|
-
|
|
85845
|
-
|
|
86480
|
+
const firstReceipt = app.db.select().from(researchRuns).where(and60(
|
|
86481
|
+
eq71(researchRuns.projectId, project.id),
|
|
86482
|
+
eq71(researchRuns.idempotencyKey, receiptKeys[0])
|
|
85846
86483
|
)).get();
|
|
85847
86484
|
if (firstReceipt) {
|
|
85848
86485
|
if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
85849
|
-
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(
|
|
85850
|
-
|
|
85851
|
-
|
|
86486
|
+
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and60(
|
|
86487
|
+
eq71(researchRuns.projectId, project.id),
|
|
86488
|
+
eq71(researchRuns.idempotencyKey, key)
|
|
85852
86489
|
)).get());
|
|
85853
86490
|
if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
85854
86491
|
const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
|
|
@@ -85863,9 +86500,9 @@ async function researchRoutes(app, opts) {
|
|
|
85863
86500
|
const decision = app.db.transaction((tx) => {
|
|
85864
86501
|
if (initiatedBy?.limited) {
|
|
85865
86502
|
const { start, end, date } = utcDayBounds(now);
|
|
85866
|
-
const used = tx.select({ value: count3() }).from(researchRuns).where(
|
|
85867
|
-
|
|
85868
|
-
|
|
86503
|
+
const used = tx.select({ value: count3() }).from(researchRuns).where(and60(
|
|
86504
|
+
eq71(researchRuns.projectId, project.id),
|
|
86505
|
+
gte19(researchRuns.createdAt, start),
|
|
85869
86506
|
lt12(researchRuns.createdAt, end),
|
|
85870
86507
|
sql27`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
85871
86508
|
)).get()?.value ?? 0;
|
|
@@ -85918,9 +86555,9 @@ async function researchRoutes(app, opts) {
|
|
|
85918
86555
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
85919
86556
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
85920
86557
|
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
85921
|
-
const rows = app.db.select().from(researchRuns).where(
|
|
85922
|
-
|
|
85923
|
-
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
|
|
85924
86561
|
)).orderBy(desc31(researchRuns.createdAt), desc31(researchRuns.id)).limit(limit + 1).all();
|
|
85925
86562
|
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
85926
86563
|
const last = runs2.at(-1);
|
|
@@ -86047,9 +86684,9 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
|
|
|
86047
86684
|
return stored;
|
|
86048
86685
|
}
|
|
86049
86686
|
function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
86050
|
-
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(
|
|
86051
|
-
|
|
86052
|
-
|
|
86687
|
+
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and60(
|
|
86688
|
+
eq71(measurementQueryTemplates.projectId, projectId),
|
|
86689
|
+
eq71(measurementQueryTemplates.id, selection.templateId)
|
|
86053
86690
|
)).get();
|
|
86054
86691
|
if (!savedTemplate) throw validationError("Unknown or stale research template. Reload the selected template and try again.");
|
|
86055
86692
|
const templateVersion = savedTemplate.updatedAt;
|
|
@@ -86058,7 +86695,7 @@ function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
|
86058
86695
|
}
|
|
86059
86696
|
const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
|
|
86060
86697
|
if (bindingLocation) {
|
|
86061
|
-
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();
|
|
86062
86699
|
if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
|
|
86063
86700
|
}
|
|
86064
86701
|
const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
|
|
@@ -86071,9 +86708,9 @@ function classifyResearchQueries(project, plan, queries2) {
|
|
|
86071
86708
|
return queries2.map((query) => classifier?.classify(query) ?? null);
|
|
86072
86709
|
}
|
|
86073
86710
|
function getDetail(app, projectId, id) {
|
|
86074
|
-
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();
|
|
86075
86712
|
if (!row) throw notFound("Research run", id);
|
|
86076
|
-
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);
|
|
86077
86714
|
return { ...serializeRun(row), queries: queries2 };
|
|
86078
86715
|
}
|
|
86079
86716
|
function serializeRun(row) {
|
|
@@ -86102,7 +86739,7 @@ function utcDayBounds(now) {
|
|
|
86102
86739
|
}
|
|
86103
86740
|
|
|
86104
86741
|
// ../api-routes/src/plan-competitors.ts
|
|
86105
|
-
import { eq as
|
|
86742
|
+
import { eq as eq72 } from "drizzle-orm";
|
|
86106
86743
|
function competitorKey(value) {
|
|
86107
86744
|
const host = (hostOf(value) ?? value).trim().toLowerCase().replace(/\.$/, "");
|
|
86108
86745
|
return host.startsWith("www.") ? host.slice(4) : host;
|
|
@@ -86118,7 +86755,7 @@ function toList(merged) {
|
|
|
86118
86755
|
return [...merged].sort(([left], [right]) => left.localeCompare(right)).map(([domain, names]) => ({ domain, aliases: [...names] }));
|
|
86119
86756
|
}
|
|
86120
86757
|
function readScope(db, versionId) {
|
|
86121
|
-
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();
|
|
86122
86759
|
if (!row) return null;
|
|
86123
86760
|
let groups;
|
|
86124
86761
|
let edges;
|
|
@@ -86182,7 +86819,7 @@ function createRunCompetitorResolver(db, projectCompetitorDomains) {
|
|
|
86182
86819
|
|
|
86183
86820
|
// ../api-routes/src/simple-measurement-definitions.ts
|
|
86184
86821
|
import crypto51 from "crypto";
|
|
86185
|
-
import { and as
|
|
86822
|
+
import { and as and61, eq as eq73 } from "drizzle-orm";
|
|
86186
86823
|
function captureSimpleMeasurementDefinition(db, input) {
|
|
86187
86824
|
return db.transaction((tx) => {
|
|
86188
86825
|
const run = tx.select({
|
|
@@ -86190,7 +86827,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86190
86827
|
trigger: runs.trigger,
|
|
86191
86828
|
status: runs.status,
|
|
86192
86829
|
measurementPlanVersionId: runs.measurementPlanVersionId
|
|
86193
|
-
}).from(runs).where(
|
|
86830
|
+
}).from(runs).where(and61(eq73(runs.projectId, input.projectId), eq73(runs.id, input.runId))).get();
|
|
86194
86831
|
if (!run) throw notFound("Run", input.runId);
|
|
86195
86832
|
if (run.kind !== RunKinds["answer-visibility"] || run.trigger === RunTriggers.probe || run.measurementPlanVersionId !== null) {
|
|
86196
86833
|
return null;
|
|
@@ -86200,7 +86837,7 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86200
86837
|
if (definition.queries.some((query) => query.queryClass !== (classifier?.classify(query.queryText) ?? null))) {
|
|
86201
86838
|
throw validationError("Query classes must match the captured identity and text.");
|
|
86202
86839
|
}
|
|
86203
|
-
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();
|
|
86204
86841
|
if (existing) {
|
|
86205
86842
|
const frozen = simpleMeasurementDefinitionSchema.parse(existing.definition);
|
|
86206
86843
|
const replayDefinition = {
|
|
@@ -86221,15 +86858,15 @@ function captureSimpleMeasurementDefinition(db, input) {
|
|
|
86221
86858
|
if (run.status !== RunStatuses.running) {
|
|
86222
86859
|
throw validationError("Only a running run can capture a new measurement definition.");
|
|
86223
86860
|
}
|
|
86224
|
-
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()) {
|
|
86225
86862
|
throw validationError("This run already has stored answers. Its measurement definition cannot be inferred afterward.");
|
|
86226
86863
|
}
|
|
86227
|
-
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));
|
|
86228
86865
|
if (definition.queries.some((query) => !projectQueryIds.has(query.queryId))) {
|
|
86229
86866
|
throw validationError("Every captured query must belong to the run project.");
|
|
86230
86867
|
}
|
|
86231
86868
|
if (definition.competitors !== void 0) {
|
|
86232
|
-
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));
|
|
86233
86870
|
const capturedDomains = new Set(definition.competitors.map((competitor) => competitor.domain));
|
|
86234
86871
|
if (liveCompetitorDomains.size !== capturedDomains.size || [...liveCompetitorDomains].some((domain) => !capturedDomains.has(domain))) {
|
|
86235
86872
|
throw validationError("Captured competitors must exactly match the project competitors dispatched for this run.");
|
|
@@ -87180,10 +87817,10 @@ var log = createLogger("IntelligenceService");
|
|
|
87180
87817
|
var runChronologyKey = sql28`coalesce(${runs.finishedAt}, ${runs.createdAt})`;
|
|
87181
87818
|
function analyzableRunPredicates(projectId) {
|
|
87182
87819
|
return [
|
|
87183
|
-
|
|
87820
|
+
eq74(runs.projectId, projectId),
|
|
87184
87821
|
// Only this kind writes query_snapshots.
|
|
87185
|
-
|
|
87186
|
-
or18(
|
|
87822
|
+
eq74(runs.kind, RunKinds["answer-visibility"]),
|
|
87823
|
+
or18(eq74(runs.status, RunStatuses.completed), eq74(runs.status, RunStatuses.partial)),
|
|
87187
87824
|
// Defensive: RunCoordinator already skips probes before analyzeAndPersist
|
|
87188
87825
|
// is called, but a future call site invoking it directly for a probe must
|
|
87189
87826
|
// still not pollute the intelligence window.
|
|
@@ -87228,24 +87865,24 @@ var IntelligenceService = class {
|
|
|
87228
87865
|
* Returns the analysis result for the coordinator to inspect (e.g. for webhook dispatch).
|
|
87229
87866
|
*/
|
|
87230
87867
|
analyzeAndPersist(runId, projectId) {
|
|
87231
|
-
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();
|
|
87232
87869
|
if (!currentRunRecord) {
|
|
87233
87870
|
log.info("intelligence.skip", { runId, reason: "run not eligible for analysis" });
|
|
87234
87871
|
return null;
|
|
87235
87872
|
}
|
|
87236
87873
|
const currentLocation = currentRunRecord.location ?? null;
|
|
87237
87874
|
const recentRuns = this.db.select().from(runs).where(
|
|
87238
|
-
|
|
87875
|
+
and62(
|
|
87239
87876
|
...analyzableRunPredicates(projectId),
|
|
87240
|
-
currentLocation === null ? isNull13(runs.location) :
|
|
87877
|
+
currentLocation === null ? isNull13(runs.location) : eq74(runs.location, currentLocation),
|
|
87241
87878
|
// Never look forward. The window is this run's own past, so
|
|
87242
87879
|
// re-analyzing a historical run compares it against its true
|
|
87243
87880
|
// predecessor rather than against sweeps that came after it.
|
|
87244
|
-
|
|
87881
|
+
lte17(runChronologyKey, currentRunRecord.finishedAt ?? currentRunRecord.createdAt),
|
|
87245
87882
|
// MEASUREMENT: only runs that actually wrote snapshots — see above.
|
|
87246
87883
|
// Index seek on idx_snapshots_run, short-circuited on the first row.
|
|
87247
87884
|
exists(
|
|
87248
|
-
this.db.select({ one: sql28`1` }).from(querySnapshots).where(
|
|
87885
|
+
this.db.select({ one: sql28`1` }).from(querySnapshots).where(eq74(querySnapshots.runId, runs.id))
|
|
87249
87886
|
)
|
|
87250
87887
|
)
|
|
87251
87888
|
).orderBy(desc32(runs.finishedAt), desc32(runs.createdAt)).limit(HISTORY_WINDOW_RUNS).all();
|
|
@@ -87320,7 +87957,7 @@ var IntelligenceService = class {
|
|
|
87320
87957
|
* Returns the persisted insights so the coordinator can count critical/high.
|
|
87321
87958
|
*/
|
|
87322
87959
|
analyzeAndPersistGbp(runId, projectId) {
|
|
87323
|
-
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();
|
|
87324
87961
|
if (!runRow) {
|
|
87325
87962
|
log.info("gbp-intelligence.skip", { runId, reason: "run not found" });
|
|
87326
87963
|
this.persistGbpInsights(runId, projectId, [], []);
|
|
@@ -87328,11 +87965,11 @@ var IntelligenceService = class {
|
|
|
87328
87965
|
}
|
|
87329
87966
|
const windowStart = runRow.startedAt ?? runRow.createdAt;
|
|
87330
87967
|
const windowEnd = runRow.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
87331
|
-
const selected = this.db.select().from(gbpLocations).where(
|
|
87332
|
-
|
|
87333
|
-
|
|
87334
|
-
|
|
87335
|
-
|
|
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)
|
|
87336
87973
|
)).all();
|
|
87337
87974
|
if (selected.length === 0) {
|
|
87338
87975
|
log.info("gbp-intelligence.skip", { runId, reason: "no locations synced during run" });
|
|
@@ -87365,12 +88002,12 @@ var IntelligenceService = class {
|
|
|
87365
88002
|
}
|
|
87366
88003
|
/** Build the per-location signal bundle the GBP analyzer consumes. */
|
|
87367
88004
|
buildGbpLocationSignals(projectId, locationName, displayName, fallbackDate) {
|
|
87368
|
-
const metricRows = this.db.select({ metric: gbpDailyMetrics.metric, date: gbpDailyMetrics.date, value: gbpDailyMetrics.value }).from(gbpDailyMetrics).where(
|
|
87369
|
-
const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(
|
|
87370
|
-
const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(
|
|
87371
|
-
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();
|
|
87372
88009
|
const descriptionMissing = !(ownerRow?.description ?? "").trim();
|
|
87373
|
-
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();
|
|
87374
88011
|
const placesAmenities = placeRow ? extractPlaceAmenities(placeRow.attributes) : [];
|
|
87375
88012
|
const summary3 = buildGbpSummary({
|
|
87376
88013
|
locationName,
|
|
@@ -87406,7 +88043,7 @@ var IntelligenceService = class {
|
|
|
87406
88043
|
/** Build the month-over-month keyword series for a location from the
|
|
87407
88044
|
* accumulating gbp_keyword_monthly table (latest complete month vs prior). */
|
|
87408
88045
|
buildGbpKeywordTrend(projectId, locationName) {
|
|
87409
|
-
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();
|
|
87410
88047
|
if (rows.length === 0) return { recentMonth: null, priorMonth: null, points: [] };
|
|
87411
88048
|
const months = [...new Set(rows.map((r) => r.month))].sort().reverse();
|
|
87412
88049
|
const recentMonth = months[0] ?? null;
|
|
@@ -87437,7 +88074,7 @@ var IntelligenceService = class {
|
|
|
87437
88074
|
*/
|
|
87438
88075
|
persistGbpInsights(runId, projectId, gbpInsights, coveredLocationNames) {
|
|
87439
88076
|
const covered = new Set(coveredLocationNames);
|
|
87440
|
-
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();
|
|
87441
88078
|
const staleIds = [];
|
|
87442
88079
|
const dismissedSlots = /* @__PURE__ */ new Set();
|
|
87443
88080
|
for (const row of existing) {
|
|
@@ -87448,7 +88085,7 @@ var IntelligenceService = class {
|
|
|
87448
88085
|
}
|
|
87449
88086
|
this.db.transaction((tx) => {
|
|
87450
88087
|
for (const id of staleIds) {
|
|
87451
|
-
tx.delete(insights).where(
|
|
88088
|
+
tx.delete(insights).where(eq74(insights.id, id)).run();
|
|
87452
88089
|
}
|
|
87453
88090
|
for (const insight of gbpInsights) {
|
|
87454
88091
|
const parsed = parseGbpInsightId(insight.id);
|
|
@@ -87534,7 +88171,7 @@ var IntelligenceService = class {
|
|
|
87534
88171
|
* create per run + aggregate). DB is left untouched.
|
|
87535
88172
|
*/
|
|
87536
88173
|
backfill(projectName, opts, onProgress) {
|
|
87537
|
-
const project = this.db.select().from(projects).where(
|
|
88174
|
+
const project = this.db.select().from(projects).where(eq74(projects.name, projectName)).get();
|
|
87538
88175
|
if (!project) {
|
|
87539
88176
|
throw new Error(`Project "${projectName}" not found`);
|
|
87540
88177
|
}
|
|
@@ -87546,7 +88183,7 @@ var IntelligenceService = class {
|
|
|
87546
88183
|
}
|
|
87547
88184
|
sinceTimestamp = parsed;
|
|
87548
88185
|
}
|
|
87549
|
-
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();
|
|
87550
88187
|
const measuredRunIds = this.runIdsWithSnapshots(allRuns.map((r) => r.id));
|
|
87551
88188
|
const measuredRuns = allRuns.filter((r) => measuredRunIds.has(r.id));
|
|
87552
88189
|
let startIdx = 0;
|
|
@@ -87577,7 +88214,7 @@ var IntelligenceService = class {
|
|
|
87577
88214
|
let wouldDeleteTotal = 0;
|
|
87578
88215
|
const existingByRunId = /* @__PURE__ */ new Map();
|
|
87579
88216
|
if (isDryRun && targetRuns.length > 0) {
|
|
87580
|
-
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();
|
|
87581
88218
|
for (const r of rows) {
|
|
87582
88219
|
if (r.runId == null) continue;
|
|
87583
88220
|
existingByRunId.set(r.runId, (existingByRunId.get(r.runId) ?? 0) + 1);
|
|
@@ -87623,7 +88260,7 @@ var IntelligenceService = class {
|
|
|
87623
88260
|
return { processed, skipped: skipped3, totalInsights };
|
|
87624
88261
|
}
|
|
87625
88262
|
loadTrackedCompetitors(projectId) {
|
|
87626
|
-
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);
|
|
87627
88264
|
}
|
|
87628
88265
|
/**
|
|
87629
88266
|
* Which of `runIds` actually wrote query snapshots. One grouped read, so
|
|
@@ -87632,7 +88269,7 @@ var IntelligenceService = class {
|
|
|
87632
88269
|
*/
|
|
87633
88270
|
runIdsWithSnapshots(runIds) {
|
|
87634
88271
|
if (runIds.length === 0) return /* @__PURE__ */ new Set();
|
|
87635
|
-
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();
|
|
87636
88273
|
return new Set(rows.map((r) => r.runId));
|
|
87637
88274
|
}
|
|
87638
88275
|
/**
|
|
@@ -87654,15 +88291,15 @@ var IntelligenceService = class {
|
|
|
87654
88291
|
}
|
|
87655
88292
|
persistResult(result, runId, projectId) {
|
|
87656
88293
|
const previouslyDismissed = /* @__PURE__ */ new Set();
|
|
87657
|
-
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();
|
|
87658
88295
|
for (const row of existingInsights) {
|
|
87659
88296
|
if (row.dismissed) {
|
|
87660
88297
|
previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
|
|
87661
88298
|
}
|
|
87662
88299
|
}
|
|
87663
88300
|
this.db.transaction((tx) => {
|
|
87664
|
-
tx.delete(insights).where(
|
|
87665
|
-
tx.delete(healthSnapshots).where(
|
|
88301
|
+
tx.delete(insights).where(eq74(insights.runId, runId)).run();
|
|
88302
|
+
tx.delete(healthSnapshots).where(eq74(healthSnapshots.runId, runId)).run();
|
|
87666
88303
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
87667
88304
|
for (const insight of result.insights) {
|
|
87668
88305
|
const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
|
|
@@ -87709,10 +88346,10 @@ var IntelligenceService = class {
|
|
|
87709
88346
|
clicks: gscSearchData.clicks,
|
|
87710
88347
|
impressions: gscSearchData.impressions,
|
|
87711
88348
|
position: gscSearchData.position
|
|
87712
|
-
}).from(gscSearchData).where(
|
|
87713
|
-
|
|
87714
|
-
|
|
87715
|
-
|
|
88349
|
+
}).from(gscSearchData).where(and62(
|
|
88350
|
+
eq74(gscSearchData.projectId, projectId),
|
|
88351
|
+
gte20(gscSearchData.date, window.startDate),
|
|
88352
|
+
lte17(gscSearchData.date, window.endDate)
|
|
87716
88353
|
)).all();
|
|
87717
88354
|
const byDay = /* @__PURE__ */ new Map();
|
|
87718
88355
|
for (const r of rows) {
|
|
@@ -87763,17 +88400,17 @@ var IntelligenceService = class {
|
|
|
87763
88400
|
const key = row.query.toLowerCase();
|
|
87764
88401
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
87765
88402
|
}
|
|
87766
|
-
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();
|
|
87767
88404
|
const locationCount = Math.max(
|
|
87768
88405
|
1,
|
|
87769
88406
|
(projectRow?.locations ?? []).length
|
|
87770
88407
|
);
|
|
87771
88408
|
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
87772
88409
|
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
87773
|
-
|
|
87774
|
-
|
|
87775
|
-
|
|
87776
|
-
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")),
|
|
87777
88414
|
// Defensive — see top of file.
|
|
87778
88415
|
ne11(runs.trigger, RunTriggers.probe)
|
|
87779
88416
|
)
|
|
@@ -87793,7 +88430,7 @@ var IntelligenceService = class {
|
|
|
87793
88430
|
const haveHistory = recentRunIds.length > 0;
|
|
87794
88431
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
87795
88432
|
if (haveHistory) {
|
|
87796
|
-
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();
|
|
87797
88434
|
const regressionGroups = /* @__PURE__ */ new Map();
|
|
87798
88435
|
for (const row of priorRows) {
|
|
87799
88436
|
if (!row.runId) continue;
|
|
@@ -87822,7 +88459,7 @@ var IntelligenceService = class {
|
|
|
87822
88459
|
});
|
|
87823
88460
|
}
|
|
87824
88461
|
buildRunData(runId, projectId, completedAt, location = null, trackedCompetitors = []) {
|
|
87825
|
-
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();
|
|
87826
88463
|
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
87827
88464
|
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
87828
88465
|
ownedDomains: projectDomainRow.ownedDomains
|
|
@@ -87840,7 +88477,7 @@ var IntelligenceService = class {
|
|
|
87840
88477
|
citedDomains: querySnapshots.citedDomains,
|
|
87841
88478
|
rawResponse: querySnapshots.rawResponse,
|
|
87842
88479
|
snapshotLocation: querySnapshots.location
|
|
87843
|
-
}).from(querySnapshots).leftJoin(queries,
|
|
88480
|
+
}).from(querySnapshots).leftJoin(queries, eq74(querySnapshots.queryId, queries.id)).where(eq74(querySnapshots.runId, runId)).all();
|
|
87844
88481
|
const snapshots = [];
|
|
87845
88482
|
let orphanCount = 0;
|
|
87846
88483
|
for (const r of rows) {
|
|
@@ -88062,6 +88699,7 @@ export {
|
|
|
88062
88699
|
generateWranglerToml,
|
|
88063
88700
|
SITE_REACHABILITY_CHECK_ID,
|
|
88064
88701
|
SITE_REACHABILITY_CHECKS,
|
|
88702
|
+
scheduledHealthCheckIds,
|
|
88065
88703
|
runChecks,
|
|
88066
88704
|
executeDiscovery,
|
|
88067
88705
|
markSessionFailed,
|