@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.
Files changed (32) hide show
  1. package/assets/agent-workspace/skills/aero/references/portfolio-analysis.md +1 -1
  2. package/assets/agent-workspace/skills/aero/references/reporting.md +9 -4
  3. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +4 -1
  4. package/assets/assets/{AuditHistoryPanel-BSh3Oezj.js → AuditHistoryPanel-teTDC-4-.js} +1 -1
  5. package/assets/assets/{BacklinksPage-DQ59dhCx.js → BacklinksPage-Bqm7bido.js} +1 -1
  6. package/assets/assets/{HistoryPage-BBvgid3o.js → HistoryPage-IcqwPBmt.js} +1 -1
  7. package/assets/assets/{MeasurementPropertyPage-Dmb18JbT.js → MeasurementPropertyPage-BLpptT3_.js} +1 -1
  8. package/assets/assets/{ProjectPage-CPg0q2Dy.js → ProjectPage-nn2Y9H-m.js} +1 -1
  9. package/assets/assets/{RunRow-BK8sZCID.js → RunRow-S_SCHr0o.js} +1 -1
  10. package/assets/assets/{RunsPage-CIcVBTdP.js → RunsPage-DFiif626.js} +1 -1
  11. package/assets/assets/{SettingsPage-Bc3aR-gz.js → SettingsPage-CitZ6FAS.js} +1 -1
  12. package/assets/assets/{SiteHealthSection-FF14aIXT.js → SiteHealthSection-C2dsyNGY.js} +3 -3
  13. package/assets/assets/{TrafficPage-C_qoeGDl.js → TrafficPage-lY8na5Pg.js} +1 -1
  14. package/assets/assets/{TrafficSourceDetailPage-B3i_B2MI.js → TrafficSourceDetailPage-BTykoVGT.js} +1 -1
  15. package/assets/assets/{extract-error-message-BisUn6nN.js → extract-error-message-r0Gpc7mv.js} +1 -1
  16. package/assets/assets/{index-Dht7JNX5.js → index-DwlO8KfE.js} +21 -21
  17. package/assets/assets/{react-sigma_core.esm.min-tWhS09SS.js → react-sigma_core.esm.min-DDrDxbO1.js} +1 -1
  18. package/assets/assets/{v2-overview-adapter-CAoeWKQJ.js → v2-overview-adapter-OsQiglXU.js} +1 -1
  19. package/assets/index.html +1 -1
  20. package/dist/{chunk-TWLJRAWA.js → chunk-3LCR6WU6.js} +2 -2
  21. package/dist/{chunk-JJPZ4OQ2.js → chunk-DDL2XSNM.js} +8 -4
  22. package/dist/{chunk-PXVJ3P5G.js → chunk-GLLY2GEV.js} +8 -5
  23. package/dist/{chunk-NSKFW3NF.js → chunk-P5VXVFQW.js} +1095 -457
  24. package/dist/{chunk-TOSPZ7I5.js → chunk-S5OSY3PI.js} +78 -5
  25. package/dist/{chunk-P73YBGZP.js → chunk-XFHXKLNT.js} +23 -12
  26. package/dist/{chunk-CVUR3O5G.js → chunk-YTSXQUJ3.js} +3 -3
  27. package/dist/cli.js +76 -16
  28. package/dist/{demo-server-FH2K6TCD.js → demo-server-HULNEORN.js} +4 -4
  29. package/dist/index.js +6 -6
  30. package/dist/{intelligence-service-QHSR2H7F.js → intelligence-service-CXNBKRHV.js} +2 -2
  31. package/dist/mcp.js +4 -4
  32. 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-TOSPZ7I5.js";
802
+ } from "./chunk-S5OSY3PI.js";
796
803
 
797
804
  // src/intelligence-service.ts
798
- import { eq as eq71, desc as desc32, asc as asc12, and as and60, ne as ne11, or as or18, inArray as inArray27, gte as gte18, lte as lte15, isNull as isNull13, sql as sql28, exists } from "drizzle-orm";
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 isCited = snap.citationState === CitationStates.cited;
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 (competitorHosts.length > 0 && snap.citedDomains.length > 0) {
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
- const mentionShare = buildMentionShare(
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: snaps.length,
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
- const fromEvidence = classifyModelEvidence(fromCandidateModels.get(provider) ?? []);
34977
- const toEvidence = classifyModelEvidence(toCandidateModels.get(provider) ?? []);
34978
- const fromModels = modelIds(fromEvidence);
34979
- const toModels = modelIds(toEvidence);
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(fromCounts);
35000
- const toShare = shareCounts(toCounts);
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: fromCounts.competitors, to: toCounts.competitors }
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 project = resolveProject(app.db, request.params.name);
35310
- const { from: fromRaw, to: toRaw } = request.query;
35311
- if (fromRaw === void 0 || fromRaw === "") throw validationError('"from" (YYYY-MM) is required');
35312
- if (toRaw === void 0 || toRaw === "") throw validationError('"to" (YYYY-MM) is required');
35313
- let fromBounds;
35314
- let toBounds;
35315
- try {
35316
- fromBounds = calendarMonthBounds(fromRaw);
35317
- } catch (err) {
35318
- throw validationError(err instanceof RangeError ? `"from": ${err.message}` : '"from" must be in YYYY-MM format');
35319
- }
35320
- try {
35321
- toBounds = calendarMonthBounds(toRaw);
35322
- } catch (err) {
35323
- throw validationError(err instanceof RangeError ? `"to": ${err.message}` : '"to" must be in YYYY-MM format');
35324
- }
35325
- if (Date.parse(fromBounds.since) >= Date.parse(toBounds.since)) {
35326
- throw validationError('"from" must be a month strictly before "to"');
35327
- }
35328
- const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq39(queries.projectId, project.id)).all();
35329
- const competitorRows2 = app.db.select({ domain: competitors.domain }).from(competitors).where(eq39(competitors.projectId, project.id)).all();
35330
- const competitorInputs = mentionShareCompetitorsFromDomains(competitorRows2.map((c) => c.domain));
35331
- const loadMonth = (bounds) => {
35332
- const sinceMs = Date.parse(bounds.since);
35333
- const untilMs = Date.parse(bounds.until);
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 fromMonth = loadMonth(fromBounds);
35351
- const toMonth = loadMonth(toBounds);
35352
- const dto = computeVisibilityCompare({
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: projectQueries,
35355
- competitors: competitorInputs,
35356
- brandNames: effectiveBrandNames(project),
35357
- from: { month: fromRaw, since: fromBounds.since, until: fromBounds.until, runCount: fromMonth.runCount, snapshots: fromMonth.snapshots },
35358
- to: { month: toRaw, since: toBounds.since, until: toBounds.until, runCount: toMonth.runCount, snapshots: toMonth.snapshots }
35359
- });
35360
- return reply.send(dto);
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: [nameParameter, compareFromQueryParameter, compareToQueryParameter],
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 and52, eq as eq58 } from "drizzle-orm";
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(eq58(projects.id, ctx.project.id)).get();
80139
- const readySync = ctx.db.select({ id: ccReleaseSyncs.id }).from(ccReleaseSyncs).where(eq58(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).limit(1).get();
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(and52(
80153
- eq58(backlinkSummaries.projectId, ctx.project.id),
80154
- eq58(backlinkSummaries.source, BacklinkSources.commoncrawl)
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 eq59 } from "drizzle-orm";
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(eq59(projects.id, ctx.project.id)).get() ?? null;
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 eq60 } from "drizzle-orm";
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(eq60(adsConnections.projectId, ctx.project.id)).get();
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(eq60(adsConnections.projectId, ctx.project.id)).get();
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 and53, desc as desc28, eq as eq61, inArray as inArray23 } from "drizzle-orm";
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(and53(eq61(runs.projectId, ctx.project.id), inArray23(runs.kind, kinds), eq61(runs.status, "completed"))).orderBy(desc28(runs.createdAt)).limit(1).get();
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(eq61(gaDailyTotals.projectId, ctx.project.id)).orderBy(desc28(gaDailyTotals.date)).limit(1).get();
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(eq61(gscDataWatermarks.projectId, ctx.project.id)).get();
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(eq61(gscDailyTotals.projectId, ctx.project.id)).orderBy(desc28(gscDailyTotals.date)).limit(1).get();
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 and54, eq as eq62 } from "drizzle-orm";
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(and54(eq62(gbpLocations.projectId, ctx.project.id), eq62(gbpLocations.selected, true))).all();
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 eq63 } from "drizzle-orm";
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(eq63(gbpLocations.projectId, ctx.project.id)).all();
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 and55, eq as eq64, gte as gte15, inArray as inArray24, ne as ne10, sql as sql25 } from "drizzle-orm";
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
- and55(
82006
- eq64(trafficSources.projectId, ctx.project.id),
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
- and55(
82113
- eq64(crawlerEventsHourly.projectId, ctx.project.id),
82114
- inArray24(crawlerEventsHourly.sourceId, activeSourceIds),
82115
- gte15(crawlerEventsHourly.tsHour, warnCutoff)
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
- and55(
82122
- eq64(aiReferralEventsHourly.projectId, ctx.project.id),
82123
- inArray24(aiReferralEventsHourly.sourceId, activeSourceIds),
82124
- gte15(aiReferralEventsHourly.tsHour, warnCutoff)
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
- and55(
82131
- eq64(aiUserFetchEventsHourly.projectId, ctx.project.id),
82132
- inArray24(aiUserFetchEventsHourly.sourceId, activeSourceIds),
82133
- gte15(aiUserFetchEventsHourly.tsHour, warnCutoff)
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
- and55(
82157
- eq64(crawlerEventsHourly.projectId, ctx.project.id),
82158
- inArray24(crawlerEventsHourly.sourceId, activeSourceIds),
82159
- gte15(crawlerEventsHourly.tsHour, failCutoff)
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
- and55(
82166
- eq64(aiReferralEventsHourly.projectId, ctx.project.id),
82167
- inArray24(aiReferralEventsHourly.sourceId, activeSourceIds),
82168
- gte15(aiReferralEventsHourly.tsHour, failCutoff)
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
- and55(
82175
- eq64(aiUserFetchEventsHourly.projectId, ctx.project.id),
82176
- inArray24(aiUserFetchEventsHourly.sourceId, activeSourceIds),
82177
- gte15(aiUserFetchEventsHourly.tsHour, failCutoff)
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
- status: CheckStatuses.fail,
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 and56, desc as desc29, eq as eq65, gte as gte16, inArray as inArray25, isNull as isNull11, or as or15 } from "drizzle-orm";
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(and56(
83198
- eq65(discoverySessions.projectId, project.id),
83199
- eq65(discoverySessions.icpDescription, icpDescription),
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) : eq65(discoverySessions.buyerDescription, parsed.data.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), eq65(discoverySessions.locations, locations)) : eq65(discoverySessions.locations, 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) : eq65(discoverySessions.seedProviders, seedProviders),
83218
- inArray25(discoverySessions.status, [
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
- gte16(discoverySessions.createdAt, ageFloorIso)
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(eq65(discoverySessions.projectId, project.id)).orderBy(desc29(discoverySessions.createdAt)).limit(limit).all();
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(eq65(discoverySessions.id, request.params.id)).get();
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(eq65(discoveryProbes.sessionId, session.id)).all();
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(eq65(discoverySessions.id, request.params.id)).get();
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(eq65(discoveryProbes.sessionId, session.id)).all();
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(eq65(queries.projectId, project.id)).all().map((r) => r.query);
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(eq65(discoverySessions.id, request.params.id)).get();
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(eq65(discoveryProbes.sessionId, session.id)).all();
83388
- const existingCompetitors = app.db.select({ domain: competitors.domain }).from(competitors).where(eq65(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase());
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(eq65(discoverySessions.id, request.params.id)).get();
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(eq65(discoveryProbes.sessionId, session.id)).all();
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(eq65(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
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(eq65(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
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 eq66 } from "drizzle-orm";
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(eq66(discoverySessions.id, opts.sessionId)).run();
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(eq66(discoverySessions.id, opts.sessionId)).run();
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(eq66(discoverySessions.id, opts.sessionId)).run();
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(eq66(discoverySessions.id, sessionId)).run();
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 and57, asc as asc11, count as count2, desc as desc30, eq as eq67, inArray as inArray26, isNotNull as isNotNull5, isNull as isNull12, lt as lt11, or as or16, sql as sql26 } from "drizzle-orm";
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 eq67(siteCrawlEdges.isTemplate, false);
84057
- if (linkKind === SiteHealthLinkKinds.template) return eq67(siteCrawlEdges.isTemplate, true);
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
- eq67(siteCrawlSnapshots.projectId, projectId),
84163
- eq67(runs.projectId, projectId),
84164
- eq67(runs.kind, RunKinds["site-audit"]),
84165
- inArray26(runs.status, SURFACEABLE_STATUSES),
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(eq67(siteCrawlSnapshots.runId, runId));
84806
+ filters.push(eq70(siteCrawlSnapshots.runId, runId));
84170
84807
  } else {
84171
- filters.push(eq67(siteCrawlSnapshots.complete, true), eq67(runs.status, RunStatuses.completed));
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, eq67(siteCrawlSnapshots.runId, runs.id)).where(and57(...filters)).orderBy(desc30(siteCrawlSnapshots.createdAt), desc30(siteCrawlSnapshots.runId)).limit(1).get();
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(and57(
84179
- eq67(siteCrawlPages.projectId, scope.projectId),
84180
- eq67(siteCrawlPages.runId, scope.runId),
84181
- eq67(siteCrawlPages.attemptId, scope.attemptId),
84182
- selector.nodeKey ? eq67(siteCrawlPages.nodeKey, selector.nodeKey) : eq67(siteCrawlPages.url, selector.url)
84183
- )).orderBy(asc11(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
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(and57(
84186
- eq67(siteCrawlPages.projectId, scope.projectId),
84187
- eq67(siteCrawlPages.runId, scope.runId),
84188
- eq67(siteCrawlPages.attemptId, scope.attemptId),
84189
- eq67(siteCrawlPages.depth, 0)
84190
- )).orderBy(asc11(siteCrawlPages.nodeKey)).limit(1).get() ?? null;
84191
- const isSurfaceableAuditRun = (projectId, runId) => Boolean(app.db.select({ id: runs.id }).from(runs).where(and57(
84192
- eq67(runs.id, runId),
84193
- eq67(runs.projectId, projectId),
84194
- eq67(runs.kind, RunKinds["site-audit"]),
84195
- inArray26(runs.status, SURFACEABLE_STATUSES),
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, eq67(siteAuditSnapshots.runId, runs.id)).where(and57(
84202
- eq67(siteAuditSnapshots.projectId, projectId),
84203
- eq67(runs.projectId, projectId),
84204
- eq67(runs.kind, RunKinds["site-audit"]),
84205
- inArray26(runs.status, SURFACEABLE_STATUSES),
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
- eq67(siteAuditSnapshots.projectId, project.id),
84227
- eq67(runs.projectId, project.id),
84228
- eq67(runs.kind, RunKinds["site-audit"]),
84229
- inArray26(runs.status, SURFACEABLE_STATUSES),
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, eq67(siteAuditSnapshots.runId, request.query.runId)] : baseFilters;
84233
- const latest = app.db.select({ snap: siteAuditSnapshots, runStatus: runs.status }).from(siteAuditSnapshots).innerJoin(runs, eq67(siteAuditSnapshots.runId, runs.id)).where(and57(...targetFilters)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get();
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, eq67(siteAuditSnapshots.runId, runs.id)).where(and57(...baseFilters, lt11(siteAuditSnapshots.createdAt, snap.createdAt))).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get()?.snap ?? null;
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
- eq67(siteAuditSnapshots.projectId, project.id),
84267
- eq67(runs.projectId, project.id),
84268
- eq67(runs.kind, RunKinds["site-audit"]),
84269
- inArray26(runs.status, SURFACEABLE_STATUSES),
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(eq67(siteAuditSnapshots.runId, request.query.runId));
84273
- const latest = app.db.select({ runId: siteAuditSnapshots.runId, auditedAt: siteAuditSnapshots.auditedAt }).from(siteAuditSnapshots).innerJoin(runs, eq67(siteAuditSnapshots.runId, runs.id)).where(and57(...targetFilters)).orderBy(desc30(siteAuditSnapshots.createdAt)).limit(1).get();
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 = [eq67(siteAuditPages.projectId, project.id), eq67(siteAuditPages.runId, latest.runId)];
84282
- if (statusFilter) conds.push(eq67(siteAuditPages.status, statusFilter));
84283
- const where = and57(...conds);
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" ? asc11(siteAuditPages.url) : asc11(siteAuditPages.overallScore);
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, eq67(siteAuditSnapshots.runId, runs.id)).where(and57(
84308
- eq67(siteAuditSnapshots.projectId, project.id),
84309
- eq67(runs.projectId, project.id),
84310
- eq67(runs.kind, RunKinds["site-audit"]),
84311
- inArray26(runs.status, SURFACEABLE_STATUSES),
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(and57(
84406
- eq67(siteCrawlGraphLayouts.projectId, project.id),
84407
- eq67(siteCrawlGraphLayouts.runId, snapshot.runId),
84408
- eq67(siteCrawlGraphLayouts.attemptId, snapshot.attemptId)
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
- eq67(siteCrawlGraphNodes.projectId, project.id),
84460
- eq67(siteCrawlGraphNodes.runId, snapshot.runId),
84461
- eq67(siteCrawlGraphNodes.attemptId, snapshot.attemptId)
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, and57(
84481
- eq67(siteCrawlPages.projectId, siteCrawlGraphNodes.projectId),
84482
- eq67(siteCrawlPages.runId, siteCrawlGraphNodes.runId),
84483
- eq67(siteCrawlPages.attemptId, siteCrawlGraphNodes.attemptId),
84484
- eq67(siteCrawlPages.nodeKey, siteCrawlGraphNodes.nodeKey)
84485
- )).where(and57(...graphScope, lt11(siteCrawlGraphNodes.sampleRank, maxNodes))).orderBy(asc11(siteCrawlGraphNodes.sampleRank)).all();
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 : eq67(siteCrawlGraphEdges.isTemplate, linkKind === SiteHealthLinkKinds.template);
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, and57(
84499
- eq67(graphSourceNode.projectId, siteCrawlGraphEdges.projectId),
84500
- eq67(graphSourceNode.runId, siteCrawlGraphEdges.runId),
84501
- eq67(graphSourceNode.attemptId, siteCrawlGraphEdges.attemptId),
84502
- eq67(graphSourceNode.nodeKey, siteCrawlGraphEdges.sourceNodeKey),
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, and57(
84505
- eq67(graphTargetNode.projectId, siteCrawlGraphEdges.projectId),
84506
- eq67(graphTargetNode.runId, siteCrawlGraphEdges.runId),
84507
- eq67(graphTargetNode.attemptId, siteCrawlGraphEdges.attemptId),
84508
- eq67(graphTargetNode.nodeKey, siteCrawlGraphEdges.targetNodeKey),
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(and57(
84511
- eq67(siteCrawlGraphEdges.projectId, project.id),
84512
- eq67(siteCrawlGraphEdges.runId, snapshot.runId),
84513
- eq67(siteCrawlGraphEdges.attemptId, snapshot.attemptId),
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(asc11(siteCrawlGraphEdges.sampleRank)).all();
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(and57(
84608
- eq67(siteCrawlEdges.projectId, scope.projectId),
84609
- eq67(siteCrawlEdges.runId, scope.runId),
84610
- eq67(siteCrawlEdges.attemptId, scope.attemptId),
84611
- eq67(siteCrawlEdges.internal, true),
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
- inArray26(siteCrawlEdges.sourceNodeKey, frontier),
84615
- inArray26(siteCrawlEdges.targetNodeKey, frontier)
85251
+ inArray27(siteCrawlEdges.sourceNodeKey, frontier),
85252
+ inArray27(siteCrawlEdges.targetNodeKey, frontier)
84616
85253
  )
84617
- )).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(maxEdges + maxNodes + 1).all();
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(and57(
84649
- eq67(siteCrawlPages.projectId, scope.projectId),
84650
- eq67(siteCrawlPages.runId, scope.runId),
84651
- eq67(siteCrawlPages.attemptId, scope.attemptId),
84652
- inArray26(siteCrawlPages.nodeKey, nodeKeys)
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(and57(
84657
- eq67(siteCrawlEdges.projectId, scope.projectId),
84658
- eq67(siteCrawlEdges.runId, scope.runId),
84659
- eq67(siteCrawlEdges.attemptId, scope.attemptId),
84660
- eq67(siteCrawlEdges.internal, true),
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
- inArray26(siteCrawlEdges.sourceNodeKey, outermostNodeKeys),
84663
- inArray26(siteCrawlEdges.targetNodeKey, outermostNodeKeys)
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(and57(
84759
- eq67(siteCrawlEdges.projectId, scope.projectId),
84760
- eq67(siteCrawlEdges.runId, scope.runId),
84761
- eq67(siteCrawlEdges.attemptId, scope.attemptId),
84762
- eq67(siteCrawlEdges.internal, true),
84763
- eq67(siteCrawlEdges.followable, true),
84764
- eq67(siteCrawlEdges.relation, "anchor"),
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
- inArray26(siteCrawlEdges.sourceNodeKey, frontier)
84767
- )).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(SITE_HEALTH_PATH_MAX_VISITED_NODES + 1).all();
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(and57(
84771
- eq67(siteCrawlPages.projectId, scope.projectId),
84772
- eq67(siteCrawlPages.runId, scope.runId),
84773
- eq67(siteCrawlPages.attemptId, scope.attemptId),
84774
- inArray26(siteCrawlPages.nodeKey, candidateTargetKeys)
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(and57(
84817
- eq67(siteCrawlPages.projectId, scope.projectId),
84818
- eq67(siteCrawlPages.runId, scope.runId),
84819
- eq67(siteCrawlPages.attemptId, scope.attemptId),
84820
- inArray26(siteCrawlPages.nodeKey, pathKeys)
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
- eq67(siteCrawlSnapshots.projectId, project.id),
84850
- eq67(runs.projectId, project.id),
84851
- eq67(runs.kind, RunKinds["site-audit"]),
84852
- eq67(runs.status, RunStatuses.completed),
84853
- eq67(siteCrawlSnapshots.complete, true),
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, eq67(siteCrawlSnapshots.runId, runs.id)).where(and57(...completeSnapshotFilters)).orderBy(desc30(siteCrawlSnapshots.createdAt), desc30(siteCrawlSnapshots.runId)).limit(1).get();
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, eq67(siteCrawlSnapshots.runId, runs.id)).where(and57(
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
- and57(
84863
- eq67(siteCrawlSnapshots.createdAt, afterTarget.snapshot.createdAt),
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(and57(
85099
- eq67(siteCrawlPages.projectId, scope.projectId),
85100
- eq67(siteCrawlPages.runId, scope.runId),
85101
- eq67(siteCrawlPages.attemptId, scope.attemptId),
85102
- inArray26(siteCrawlPages.nodeKey, pageKeys)
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(and57(
85105
- eq67(siteCrawlEdges.projectId, scope.projectId),
85106
- eq67(siteCrawlEdges.runId, scope.runId),
85107
- eq67(siteCrawlEdges.attemptId, scope.attemptId),
85108
- eq67(siteCrawlEdges.internal, true),
85109
- inArray26(siteCrawlEdges.edgeKey, linkKeys)
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
- eq67(siteCrawlPages.projectId, project.id),
85235
- eq67(siteCrawlPages.runId, snapshot.runId),
85236
- eq67(siteCrawlPages.attemptId, snapshot.attemptId)
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(eq67(siteCrawlPages.nodeKey, request.query.nodeKey));
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(eq67(siteCrawlPages.inventoryEligible, inventoryEligible));
85241
- if (request.query.fetchState) filters.push(eq67(siteCrawlPages.fetchState, request.query.fetchState));
85242
- if (request.query.indexabilityState) filters.push(eq67(siteCrawlPages.indexabilityState, request.query.indexabilityState));
85243
- if (request.query.auditState) filters.push(eq67(siteCrawlPages.auditState, request.query.auditState));
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), asc11(siteCrawlPages.nodeKey)] : request.query.sort === "score-asc" ? [asc11(siteCrawlPages.auditScore), asc11(siteCrawlPages.nodeKey)] : request.query.sort === "path" ? [asc11(siteCrawlPages.path), asc11(siteCrawlPages.nodeKey)] : [asc11(siteCrawlPages.url), asc11(siteCrawlPages.nodeKey)];
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(and57(
85251
- eq67(siteCrawlPages.projectId, project.id),
85252
- eq67(siteCrawlPages.runId, snapshot.runId),
85253
- eq67(siteCrawlPages.attemptId, snapshot.attemptId),
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(eq67(siteCrawlPages.healthState, healthState));
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 = and57(...filters);
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(and57(
85302
- eq67(siteCrawlPages.projectId, project.id),
85303
- eq67(siteCrawlPages.runId, snapshot.runId),
85304
- eq67(siteCrawlPages.attemptId, snapshot.attemptId)
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
- eq67(siteCrawlEdges.projectId, project.id),
85384
- eq67(siteCrawlEdges.runId, snapshot.runId),
85385
- eq67(siteCrawlEdges.attemptId, snapshot.attemptId),
85386
- eq67(siteCrawlEdges.internal, true),
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(eq67(siteCrawlEdges.sourceUrl, request.query.sourceUrl));
85390
- if (request.query.targetUrl) filters.push(eq67(siteCrawlEdges.targetUrl, request.query.targetUrl));
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(eq67(siteCrawlEdges.followable, followable));
85393
- const where = and57(...filters);
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(asc11(siteCrawlEdges.edgeKey)).limit(limit).offset(offset).all();
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
- eq67(siteCrawlEdges.projectId, project.id),
85452
- eq67(siteCrawlEdges.runId, snapshot.runId),
85453
- eq67(siteCrawlEdges.attemptId, snapshot.attemptId),
85454
- eq67(siteCrawlEdges.internal, true),
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(eq67(siteCrawlEdges.targetNodeKey, request.query.nodeKey), eq67(siteCrawlEdges.targetUrl, request.query.url)) : request.query.nodeKey ? eq67(siteCrawlEdges.targetNodeKey, request.query.nodeKey) : eq67(siteCrawlEdges.targetUrl, request.query.url);
85458
- const outboundMatch = request.query.nodeKey && request.query.url ? or16(eq67(siteCrawlEdges.sourceNodeKey, request.query.nodeKey), eq67(siteCrawlEdges.sourceUrl, request.query.url)) : request.query.nodeKey ? eq67(siteCrawlEdges.sourceNodeKey, request.query.nodeKey) : eq67(siteCrawlEdges.sourceUrl, request.query.url);
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(and57(...scope, inboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
85461
- const outboundRows = app.db.select().from(siteCrawlEdges).where(and57(...scope, outboundMatch)).orderBy(asc11(siteCrawlEdges.edgeKey)).limit(limit + 1).all();
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 = and57(
85495
- eq67(siteCrawlFindings.projectId, project.id),
85496
- eq67(siteCrawlFindings.runId, snapshot.runId),
85497
- eq67(siteCrawlFindings.attemptId, snapshot.attemptId),
85498
- eq67(siteCrawlFindings.findingType, "dead-link"),
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(asc11(siteCrawlFindings.findingKey)).limit(limit).offset(offset).all();
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(and57(
85548
- eq67(runs.projectId, project.id),
85549
- eq67(runs.kind, RunKinds["site-audit"]),
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, eq67(siteCrawlSnapshots.runId, runs.id)).where(and57(
85554
- eq67(siteCrawlSnapshots.projectId, project.id),
85555
- eq67(runs.projectId, project.id),
85556
- eq67(runs.kind, RunKinds["site-audit"]),
85557
- inArray26(runs.status, SURFACEABLE_STATUSES),
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
- inArray26(siteCrawlSnapshots.runId, runIds)
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(and57(
85573
- eq67(runs.id, request.params.runId),
85574
- eq67(runs.projectId, project.id),
85575
- eq67(runs.kind, RunKinds["site-audit"]),
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(and57(
85580
- eq67(siteCrawlAttempts.projectId, project.id),
85581
- eq67(siteCrawlAttempts.runId, run.id)
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(and57(
85584
- eq67(siteCrawlGraphLayouts.projectId, project.id),
85585
- eq67(siteCrawlGraphLayouts.runId, run.id),
85586
- eq67(siteCrawlGraphLayouts.attemptId, attempt.id)
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(eq67(projects.name, request.params.name)).get();
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(and57(
85620
- eq67(runs.id, request.params.runId),
85621
- eq67(runs.projectId, project.id),
85622
- eq67(runs.kind, RunKinds["site-audit"]),
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(and57(
85627
- eq67(siteCrawlAttempts.projectId, project.id),
85628
- eq67(siteCrawlAttempts.runId, run.id)
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 = and57(
85643
- eq67(siteCrawlPages.projectId, project.id),
85644
- eq67(siteCrawlPages.runId, run.id),
85645
- eq67(siteCrawlPages.attemptId, attempt.id),
85646
- eq67(siteCrawlPages.auditState, "success")
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(and57(
86294
+ }).from(siteCrawlPages).where(and59(
85658
86295
  auditedWhere,
85659
86296
  isNotNull5(siteCrawlPages.auditScore),
85660
86297
  lt11(siteCrawlPages.auditScore, 70)
85661
- )).orderBy(asc11(siteCrawlPages.auditScore), asc11(siteCrawlPages.nodeKey)).limit(LIVE_PAGE_HEALTH_CANDIDATE_LIMIT).all();
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, and57(
85694
- eq67(siteCrawlRunRequests.projectId, runs.projectId),
85695
- eq67(siteCrawlRunRequests.runId, runs.id)
85696
- )).where(and57(
85697
- eq67(runs.projectId, project.id),
85698
- eq67(runs.kind, RunKinds["site-audit"]),
85699
- inArray26(runs.status, [RunStatuses.queued, RunStatuses.running])
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 and58, count as count3, desc as desc31, eq as eq68, gte as gte17, lt as lt12, or as or17, sql as sql27 } from "drizzle-orm";
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(and58(eq68(researchRuns.projectId, project.id), eq68(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
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(and58(eq68(researchRuns.projectId, project.id), eq68(researchRuns.idempotencyKey, input.idempotencyKey))).get();
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(and58(
85812
- eq68(researchRuns.projectId, project.id),
85813
- gte17(researchRuns.createdAt, start),
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(and58(
85844
- eq68(researchRuns.projectId, project.id),
85845
- eq68(researchRuns.idempotencyKey, receiptKeys[0])
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(and58(
85850
- eq68(researchRuns.projectId, project.id),
85851
- eq68(researchRuns.idempotencyKey, key)
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(and58(
85867
- eq68(researchRuns.projectId, project.id),
85868
- gte17(researchRuns.createdAt, start),
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(and58(
85922
- eq68(researchRuns.projectId, project.id),
85923
- cursor ? or17(lt12(researchRuns.createdAt, cursor.createdAt), and58(eq68(researchRuns.createdAt, cursor.createdAt), lt12(researchRuns.id, cursor.id))) : void 0
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(and58(
86051
- eq68(measurementQueryTemplates.projectId, projectId),
86052
- eq68(measurementQueryTemplates.id, selection.templateId)
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(eq68(projects.id, projectId)).get();
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(and58(eq68(researchRuns.id, id), eq68(researchRuns.projectId, projectId))).get();
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(eq68(researchRunQueries.researchRunId, id)).orderBy(researchRunQueries.position).all().map(serializeQuery);
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 eq69 } from "drizzle-orm";
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(eq69(measurementPlanVersions.id, versionId)).get();
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 and59, eq as eq70 } from "drizzle-orm";
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(and59(eq70(runs.projectId, input.projectId), eq70(runs.id, input.runId))).get();
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(and59(eq70(simpleMeasurementDefinitions.projectId, input.projectId), eq70(simpleMeasurementDefinitions.runId, input.runId))).get();
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(eq70(querySnapshots.runId, input.runId)).limit(1).get()) {
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(eq70(queries.projectId, input.projectId)).all().map((query) => query.id));
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(eq70(competitors.projectId, input.projectId)).all().map((competitor) => competitor.domain));
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
- eq71(runs.projectId, projectId),
87820
+ eq74(runs.projectId, projectId),
87184
87821
  // Only this kind writes query_snapshots.
87185
- eq71(runs.kind, RunKinds["answer-visibility"]),
87186
- or18(eq71(runs.status, RunStatuses.completed), eq71(runs.status, RunStatuses.partial)),
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(and60(eq71(runs.id, runId), ...analyzableRunPredicates(projectId))).get();
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
- and60(
87875
+ and62(
87239
87876
  ...analyzableRunPredicates(projectId),
87240
- currentLocation === null ? isNull13(runs.location) : eq71(runs.location, currentLocation),
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
- lte15(runChronologyKey, currentRunRecord.finishedAt ?? currentRunRecord.createdAt),
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(eq71(querySnapshots.runId, runs.id))
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(eq71(runs.id, runId)).get();
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(and60(
87332
- eq71(gbpLocations.projectId, projectId),
87333
- eq71(gbpLocations.selected, true),
87334
- gte18(gbpLocations.syncedAt, windowStart),
87335
- lte15(gbpLocations.syncedAt, windowEnd)
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(and60(eq71(gbpDailyMetrics.projectId, projectId), eq71(gbpDailyMetrics.locationName, locationName))).all();
87369
- const placeActionRows = this.db.select({ placeActionType: gbpPlaceActions.placeActionType, providerType: gbpPlaceActions.providerType }).from(gbpPlaceActions).where(and60(eq71(gbpPlaceActions.projectId, projectId), eq71(gbpPlaceActions.locationName, locationName))).all();
87370
- const lodgingRow = this.db.select({ populatedGroupCount: gbpLodgingSnapshots.populatedGroupCount }).from(gbpLodgingSnapshots).where(and60(eq71(gbpLodgingSnapshots.projectId, projectId), eq71(gbpLodgingSnapshots.locationName, locationName))).orderBy(desc32(gbpLodgingSnapshots.syncedAt)).limit(1).get();
87371
- const ownerRow = this.db.select({ description: gbpLocations.description }).from(gbpLocations).where(and60(eq71(gbpLocations.projectId, projectId), eq71(gbpLocations.locationName, locationName))).get();
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(and60(eq71(gbpPlaceDetails.projectId, projectId), eq71(gbpPlaceDetails.locationName, locationName))).orderBy(desc32(gbpPlaceDetails.syncedAt)).limit(1).get();
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(and60(eq71(gbpKeywordMonthly.projectId, projectId), eq71(gbpKeywordMonthly.locationName, locationName))).all();
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(and60(eq71(insights.projectId, projectId), eq71(insights.provider, GBP_INSIGHT_PROVIDER))).all();
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(eq71(insights.id, id)).run();
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(eq71(projects.name, projectName)).get();
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(and60(...analyzableRunPredicates(project.id))).orderBy(asc12(runs.finishedAt)).all();
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(inArray27(insights.runId, targetRuns.map((r) => r.id))).all();
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(eq71(competitors.projectId, projectId)).all().map((r) => r.domain);
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(inArray27(querySnapshots.runId, [...runIds])).groupBy(querySnapshots.runId).all();
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(eq71(insights.runId, runId)).all();
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(eq71(insights.runId, runId)).run();
87665
- tx.delete(healthSnapshots).where(eq71(healthSnapshots.runId, runId)).run();
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(and60(
87713
- eq71(gscSearchData.projectId, projectId),
87714
- gte18(gscSearchData.date, window.startDate),
87715
- lte15(gscSearchData.date, window.endDate)
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(eq71(projects.id, projectId)).get();
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
- and60(
87774
- eq71(runs.projectId, projectId),
87775
- eq71(runs.kind, RunKinds["answer-visibility"]),
87776
- or18(eq71(runs.status, "completed"), eq71(runs.status, "partial")),
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(and60(eq71(insights.type, "regression"), inArray27(insights.runId, recentRunIds))).all();
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(eq71(projects.id, projectId)).get();
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, eq71(querySnapshots.queryId, queries.id)).where(eq71(querySnapshots.runId, runId)).all();
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,