@gscdump/analysis 0.32.12 → 0.33.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -225,15 +225,26 @@ function scorePriorityActions(actions) {
225
225
  actions.sort((a, b) => b.priorityScore - a.priorityScore);
226
226
  return actions;
227
227
  }
228
- function num$6(v) {
229
- if (typeof v === "number") return v;
230
- if (typeof v === "bigint") return Number(v);
231
- if (v == null) return 0;
232
- const n = Number(v);
228
+ function rowNumber(value) {
229
+ if (typeof value === "number") return Number.isFinite(value) ? value : 0;
230
+ if (typeof value === "bigint") return Number(value);
231
+ if (value == null) return 0;
232
+ const n = Number(value);
233
233
  return Number.isFinite(n) ? n : 0;
234
234
  }
235
- function str$23(v) {
236
- return v == null ? "" : String(v);
235
+ function rowString(value) {
236
+ return value == null ? "" : String(value);
237
+ }
238
+ function rowBoolean(value) {
239
+ return value === true || value === 1 || value === "true";
240
+ }
241
+ function parseJsonRows(value) {
242
+ if (Array.isArray(value)) return value;
243
+ if (typeof value === "string" && value.length > 0) {
244
+ const parsed = JSON.parse(value);
245
+ return Array.isArray(parsed) ? parsed : [];
246
+ }
247
+ return [];
237
248
  }
238
249
  const bayesianCtrAnalyzer = defineAnalyzer$1({
239
250
  id: "bayesian-ctr",
@@ -373,24 +384,24 @@ const bayesianCtrAnalyzer = defineAnalyzer$1({
373
384
  const arr = Array.isArray(rows) ? rows : [];
374
385
  const minImpressions = params.minImpressions ?? 50;
375
386
  const results = arr.map((r) => ({
376
- keyword: str$23(r.keyword),
377
- page: str$23(r.page),
378
- clicks: num$6(r.clicks),
379
- impressions: num$6(r.impressions),
380
- observedCtr: num$6(r.observedCtr),
381
- position: num$6(r.position),
382
- bucket: num$6(r.bucket),
383
- priorAlpha: num$6(r.priorAlpha),
384
- priorBeta: num$6(r.priorBeta),
385
- bucketPriorMean: num$6(r.bucketPriorMean),
386
- posteriorMean: num$6(r.posteriorMean),
387
- posteriorSd: num$6(r.posteriorSd),
388
- ciLow: num$6(r.ciLow),
389
- ciHigh: num$6(r.ciHigh),
390
- shrinkageDelta: num$6(r.shrinkageDelta),
391
- expectedClicksDelta: num$6(r.expectedClicksDelta),
392
- significance: num$6(r.significance),
393
- classification: str$23(r.classification)
387
+ keyword: rowString(r.keyword),
388
+ page: rowString(r.page),
389
+ clicks: rowNumber(r.clicks),
390
+ impressions: rowNumber(r.impressions),
391
+ observedCtr: rowNumber(r.observedCtr),
392
+ position: rowNumber(r.position),
393
+ bucket: rowNumber(r.bucket),
394
+ priorAlpha: rowNumber(r.priorAlpha),
395
+ priorBeta: rowNumber(r.priorBeta),
396
+ bucketPriorMean: rowNumber(r.bucketPriorMean),
397
+ posteriorMean: rowNumber(r.posteriorMean),
398
+ posteriorSd: rowNumber(r.posteriorSd),
399
+ ciLow: rowNumber(r.ciLow),
400
+ ciHigh: rowNumber(r.ciHigh),
401
+ shrinkageDelta: rowNumber(r.shrinkageDelta),
402
+ expectedClicksDelta: rowNumber(r.expectedClicksDelta),
403
+ significance: rowNumber(r.significance),
404
+ classification: rowString(r.classification)
394
405
  }));
395
406
  const under = results.filter((r) => r.classification === "underperforming").length;
396
407
  const over = results.filter((r) => r.classification === "overperforming").length;
@@ -408,17 +419,6 @@ const bayesianCtrAnalyzer = defineAnalyzer$1({
408
419
  });
409
420
  const BIPARTITE_PAGERANK_ITERATIONS = 25;
410
421
  const BIPARTITE_PAGERANK_DAMPING = .85;
411
- function str$22(v) {
412
- return v == null ? "" : String(v);
413
- }
414
- function parseJsonList$16(v) {
415
- if (Array.isArray(v)) return v;
416
- if (typeof v === "string" && v.length > 0) {
417
- const parsed = JSON.parse(v);
418
- return Array.isArray(parsed) ? parsed : [];
419
- }
420
- return [];
421
- }
422
422
  const bipartitePagerankAnalyzer = defineAnalyzer$1({
423
423
  id: "bipartite-pagerank",
424
424
  buildSql(params) {
@@ -624,8 +624,8 @@ const bipartitePagerankAnalyzer = defineAnalyzer$1({
624
624
  const iterations = BIPARTITE_PAGERANK_ITERATIONS;
625
625
  const d = BIPARTITE_PAGERANK_DAMPING;
626
626
  const results = arr.map((r) => ({
627
- kind: str$22(r.kind),
628
- id: str$22(r.id),
627
+ kind: rowString(r.kind),
628
+ id: rowString(r.id),
629
629
  rank: num$1(r.rank),
630
630
  bridging: num$1(r.bridging),
631
631
  anchoring: num$1(r.anchoring),
@@ -635,7 +635,7 @@ const bipartitePagerankAnalyzer = defineAnalyzer$1({
635
635
  const first = arr[0] ?? {};
636
636
  const queryCount = num$1(first.queryCount);
637
637
  const urlCount = num$1(first.urlCount);
638
- const deltas = parseJsonList$16(first.deltasJson).map((e) => ({
638
+ const deltas = parseJsonRows(first.deltasJson).map((e) => ({
639
639
  step: num$1(e.step),
640
640
  l1: num$1(e.l1)
641
641
  }));
@@ -735,9 +735,6 @@ function analysisErrorToException(error) {
735
735
  function escapeRegexAlt(s) {
736
736
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
737
737
  }
738
- function str$21(v) {
739
- return v == null ? "" : String(v);
740
- }
741
738
  function requireBrandTermsResult(brandTerms) {
742
739
  return brandTerms?.length ? ok(brandTerms) : err(analysisErrors.missingBrandTerms());
743
740
  }
@@ -813,13 +810,13 @@ const brandAnalyzer = defineAnalyzer$1({
813
810
  },
814
811
  reduceSql(rows) {
815
812
  const normalized = (Array.isArray(rows) ? rows : []).map((r) => ({
816
- query: str$21(r.query),
817
- page: r.page == null ? void 0 : str$21(r.page),
813
+ query: rowString(r.query),
814
+ page: r.page == null ? void 0 : rowString(r.page),
818
815
  clicks: num$1(r.clicks),
819
816
  impressions: num$1(r.impressions),
820
817
  ctr: num$1(r.ctr),
821
818
  position: num$1(r.position),
822
- segment: str$21(r.segment)
819
+ segment: rowString(r.segment)
823
820
  }));
824
821
  let brandClicks = 0;
825
822
  let nonBrandClicks = 0;
@@ -896,17 +893,6 @@ const sortRowResults$1 = createSorter((item, metric) => {
896
893
  case "pageCount": return item.pages.length;
897
894
  }
898
895
  }, "clicks");
899
- function str$20(v) {
900
- return v == null ? "" : String(v);
901
- }
902
- function parseJsonList$15(v) {
903
- if (Array.isArray(v)) return v;
904
- if (typeof v === "string" && v.length > 0) {
905
- const parsed = JSON.parse(v);
906
- return Array.isArray(parsed) ? parsed : [];
907
- }
908
- return [];
909
- }
910
896
  function analyzeCannibalization(rows, options = {}) {
911
897
  const { minImpressions = 10, maxPositionSpread = 10, minPages = 2, sortBy = "clicks", sortOrder = "desc" } = options;
912
898
  const queryMap = /* @__PURE__ */ new Map();
@@ -1059,19 +1045,19 @@ const cannibalizationAnalyzer = defineAnalyzer$1({
1059
1045
  },
1060
1046
  reduceSql(rows) {
1061
1047
  const events = (Array.isArray(rows) ? rows : []).map((r) => ({
1062
- keyword: str$20(r.keyword),
1048
+ keyword: rowString(r.keyword),
1063
1049
  totalImpressions: num$1(r.totalImpressions),
1064
1050
  totalClicks: num$1(r.totalClicks),
1065
1051
  competitorCount: num$1(r.competitorCount),
1066
- leaderUrl: str$20(r.leaderUrl),
1052
+ leaderUrl: rowString(r.leaderUrl),
1067
1053
  leaderCtr: num$1(r.leaderCtr),
1068
1054
  leaderPosition: num$1(r.leaderPosition),
1069
1055
  hhi: num$1(r.hhi),
1070
1056
  fragmentation: num$1(r.fragmentation),
1071
1057
  stolenClicks: num$1(r.stolenClicks),
1072
1058
  severity: num$1(r.severity),
1073
- competitors: parseJsonList$15(r.competitors).map((c) => ({
1074
- url: str$20(c.url),
1059
+ competitors: parseJsonRows(r.competitors).map((c) => ({
1060
+ url: rowString(c.url),
1075
1061
  clicks: num$1(c.clicks),
1076
1062
  impressions: num$1(c.impressions),
1077
1063
  ctr: num$1(c.ctr),
@@ -1148,24 +1134,6 @@ const cannibalizationAnalyzer = defineAnalyzer$1({
1148
1134
  };
1149
1135
  }
1150
1136
  });
1151
- function num$5(v) {
1152
- if (typeof v === "number") return v;
1153
- if (typeof v === "bigint") return Number(v);
1154
- if (v == null) return 0;
1155
- const n = Number(v);
1156
- return Number.isFinite(n) ? n : 0;
1157
- }
1158
- function str$19(v) {
1159
- return v == null ? "" : String(v);
1160
- }
1161
- function parseJsonList$14(v) {
1162
- if (Array.isArray(v)) return v;
1163
- if (typeof v === "string" && v.length > 0) {
1164
- const parsed = JSON.parse(v);
1165
- return Array.isArray(parsed) ? parsed : [];
1166
- }
1167
- return [];
1168
- }
1169
1137
  const changePointAnalyzer = defineAnalyzer$1({
1170
1138
  id: "change-point",
1171
1139
  buildSql(params) {
@@ -1318,24 +1286,24 @@ const changePointAnalyzer = defineAnalyzer$1({
1318
1286
  const metric = params.metric === "clicks" || params.metric === "impressions" ? params.metric : "position";
1319
1287
  const lowerIsBetter = metric === "position";
1320
1288
  const results = arr.map((r) => {
1321
- const delta = num$5(r.delta);
1289
+ const delta = rowNumber(r.delta);
1322
1290
  const improved = lowerIsBetter ? delta < 0 : delta > 0;
1323
1291
  return {
1324
- keyword: str$19(r.keyword),
1325
- page: str$19(r.page),
1326
- totalDays: num$5(r.totalDays),
1327
- totalImpressions: num$5(r.totalImpressions),
1328
- changeDate: str$19(r.changeDate),
1329
- llr: num$5(r.llr),
1330
- leftMean: num$5(r.leftMean),
1331
- rightMean: num$5(r.rightMean),
1292
+ keyword: rowString(r.keyword),
1293
+ page: rowString(r.page),
1294
+ totalDays: rowNumber(r.totalDays),
1295
+ totalImpressions: rowNumber(r.totalImpressions),
1296
+ changeDate: rowString(r.changeDate),
1297
+ llr: rowNumber(r.llr),
1298
+ leftMean: rowNumber(r.leftMean),
1299
+ rightMean: rowNumber(r.rightMean),
1332
1300
  delta,
1333
- leftStddev: num$5(r.leftStddev),
1334
- rightStddev: num$5(r.rightStddev),
1301
+ leftStddev: rowNumber(r.leftStddev),
1302
+ rightStddev: rowNumber(r.rightStddev),
1335
1303
  direction: improved ? "improved" : "worsened",
1336
- series: parseJsonList$14(r.seriesJson).map((s) => ({
1337
- date: str$19(s.date),
1338
- value: num$5(s.value)
1304
+ series: parseJsonRows(r.seriesJson).map((s) => ({
1305
+ date: rowString(s.date),
1306
+ value: rowNumber(s.value)
1339
1307
  }))
1340
1308
  };
1341
1309
  });
@@ -1372,17 +1340,6 @@ const INTENT_PREFIXES = [
1372
1340
  "near me"
1373
1341
  ];
1374
1342
  const WHITESPACE_RE$2 = /\s+/;
1375
- function str$18(v) {
1376
- return v == null ? "" : String(v);
1377
- }
1378
- function parseJsonList$13(v) {
1379
- if (Array.isArray(v)) return v;
1380
- if (typeof v === "string" && v.length > 0) {
1381
- const parsed = JSON.parse(v);
1382
- return Array.isArray(parsed) ? parsed : [];
1383
- }
1384
- return [];
1385
- }
1386
1343
  function extractIntentPrefix(keyword) {
1387
1344
  const lower = keyword.toLowerCase();
1388
1345
  for (const prefix of INTENT_PREFIXES) if (lower.startsWith(`${prefix} `) || lower.startsWith(prefix)) return prefix;
@@ -1520,14 +1477,14 @@ const clusteringAnalyzer = defineAnalyzer$1({
1520
1477
  },
1521
1478
  reduceSql(rows) {
1522
1479
  const clusters = (Array.isArray(rows) ? rows : []).map((r) => ({
1523
- clusterName: str$18(r.clusterName),
1524
- clusterType: str$18(r.clusterType),
1480
+ clusterName: rowString(r.clusterName),
1481
+ clusterType: rowString(r.clusterType),
1525
1482
  keywordCount: num$1(r.keywordCount),
1526
1483
  totalClicks: num$1(r.totalClicks),
1527
1484
  totalImpressions: num$1(r.totalImpressions),
1528
1485
  avgPosition: num$1(r.avgPosition),
1529
- keywords: parseJsonList$13(r.keywords).map((k) => ({
1530
- query: str$18(k.query),
1486
+ keywords: parseJsonRows(r.keywords).map((k) => ({
1487
+ query: rowString(k.query),
1531
1488
  clicks: num$1(k.clicks),
1532
1489
  impressions: num$1(k.impressions),
1533
1490
  ctr: num$1(k.ctr),
@@ -1557,17 +1514,6 @@ const clusteringAnalyzer = defineAnalyzer$1({
1557
1514
  };
1558
1515
  }
1559
1516
  });
1560
- function str$17(v) {
1561
- return v == null ? "" : String(v);
1562
- }
1563
- function parseJsonList$12(v) {
1564
- if (Array.isArray(v)) return v;
1565
- if (typeof v === "string" && v.length > 0) {
1566
- const parsed = JSON.parse(v);
1567
- return Array.isArray(parsed) ? parsed : [];
1568
- }
1569
- return [];
1570
- }
1571
1517
  function calculateGini(values) {
1572
1518
  if (values.length === 0) return 0;
1573
1519
  const sorted = [...values].sort((a, b) => a - b);
@@ -1708,20 +1654,20 @@ const concentrationAnalyzer = defineAnalyzer$1({
1708
1654
  },
1709
1655
  reduceSql(rows, params) {
1710
1656
  const r = (Array.isArray(rows) ? rows : [])[0] ?? {};
1711
- const topRaw = parseJsonList$12(r.topNItems);
1657
+ const topRaw = parseJsonRows(r.topNItems);
1712
1658
  return {
1713
1659
  results: [{
1714
1660
  giniCoefficient: num$1(r.giniCoefficient),
1715
1661
  hhi: num$1(r.hhi),
1716
1662
  topNConcentration: num$1(r.topNConcentration),
1717
1663
  topNItems: topRaw.map((t) => ({
1718
- key: str$17(t.key),
1664
+ key: rowString(t.key),
1719
1665
  clicks: num$1(t.clicks),
1720
1666
  share: num$1(t.share)
1721
1667
  })),
1722
1668
  totalItems: num$1(r.totalItems),
1723
1669
  totalClicks: num$1(r.totalClicks),
1724
- riskLevel: str$17(r.riskLevel)
1670
+ riskLevel: rowString(r.riskLevel)
1725
1671
  }],
1726
1672
  meta: {
1727
1673
  total: 1,
@@ -1746,16 +1692,6 @@ const concentrationAnalyzer = defineAnalyzer$1({
1746
1692
  };
1747
1693
  }
1748
1694
  });
1749
- function num$4(v) {
1750
- if (typeof v === "number") return v;
1751
- if (typeof v === "bigint") return Number(v);
1752
- if (v == null) return 0;
1753
- const n = Number(v);
1754
- return Number.isFinite(n) ? n : 0;
1755
- }
1756
- function str$16(v) {
1757
- return v == null ? "" : String(v);
1758
- }
1759
1695
  const contentVelocityAnalyzer = defineAnalyzer$1({
1760
1696
  id: "content-velocity",
1761
1697
  buildSql(params) {
@@ -1812,9 +1748,9 @@ const contentVelocityAnalyzer = defineAnalyzer$1({
1812
1748
  startDateD.setUTCDate(startDateD.getUTCDate() - days);
1813
1749
  const startDate = toIsoDate(startDateD);
1814
1750
  const weekly = arr.map((r) => ({
1815
- week: str$16(r.week),
1816
- newKeywords: num$4(r.newKeywords),
1817
- totalKeywords: num$4(r.totalKeywords)
1751
+ week: rowString(r.week),
1752
+ newKeywords: rowNumber(r.newKeywords),
1753
+ totalKeywords: rowNumber(r.totalKeywords)
1818
1754
  }));
1819
1755
  const total = weekly.reduce((s, w) => s + w.newKeywords, 0);
1820
1756
  const avg = weekly.length > 0 ? total / weekly.length : 0;
@@ -1837,27 +1773,6 @@ const contentVelocityAnalyzer = defineAnalyzer$1({
1837
1773
  };
1838
1774
  }
1839
1775
  });
1840
- function num$3(v) {
1841
- if (typeof v === "number") return v;
1842
- if (typeof v === "bigint") return Number(v);
1843
- if (v == null) return 0;
1844
- const n = Number(v);
1845
- return Number.isFinite(n) ? n : 0;
1846
- }
1847
- function str$15(v) {
1848
- return v == null ? "" : String(v);
1849
- }
1850
- function bool$2(v) {
1851
- return v === true || v === 1 || v === "true";
1852
- }
1853
- function parseJsonList$11(v) {
1854
- if (Array.isArray(v)) return v;
1855
- if (typeof v === "string" && v.length > 0) {
1856
- const parsed = JSON.parse(v);
1857
- return Array.isArray(parsed) ? parsed : [];
1858
- }
1859
- return [];
1860
- }
1861
1776
  const ctrAnomalyAnalyzer = defineAnalyzer$1({
1862
1777
  id: "ctr-anomaly",
1863
1778
  buildSql(params) {
@@ -1999,26 +1914,26 @@ const ctrAnomalyAnalyzer = defineAnalyzer$1({
1999
1914
  const minRollingN = 14;
2000
1915
  const zThreshold = params.threshold ?? 2;
2001
1916
  const anomalies = arr.map((r) => ({
2002
- keyword: str$15(r.keyword),
2003
- page: str$15(r.page),
2004
- breachDaysDown: num$3(r.breachDaysDown),
2005
- breachDaysUp: num$3(r.breachDaysUp),
2006
- clicksLost: num$3(r.clicksLost),
2007
- severity: num$3(r.severityRaw),
2008
- maxZ: num$3(r.maxZ),
2009
- baselineCtr: num$3(r.baselineCtr),
2010
- baselinePosition: num$3(r.baselinePosition),
2011
- totalImpressions: num$3(r.totalImpressions),
2012
- totalClicks: num$3(r.totalClicks),
2013
- series: parseJsonList$11(r.seriesJson).map((s) => ({
2014
- date: str$15(s.date),
2015
- ctr: num$3(s.ctr),
2016
- position: num$3(s.position),
2017
- impressions: num$3(s.impressions),
2018
- rollingCtr: s.rollingCtr == null ? null : num$3(s.rollingCtr),
2019
- rollingStddev: s.rollingStddev == null ? null : num$3(s.rollingStddev),
2020
- z: num$3(s.z),
2021
- breach: bool$2(s.breach)
1917
+ keyword: rowString(r.keyword),
1918
+ page: rowString(r.page),
1919
+ breachDaysDown: rowNumber(r.breachDaysDown),
1920
+ breachDaysUp: rowNumber(r.breachDaysUp),
1921
+ clicksLost: rowNumber(r.clicksLost),
1922
+ severity: rowNumber(r.severityRaw),
1923
+ maxZ: rowNumber(r.maxZ),
1924
+ baselineCtr: rowNumber(r.baselineCtr),
1925
+ baselinePosition: rowNumber(r.baselinePosition),
1926
+ totalImpressions: rowNumber(r.totalImpressions),
1927
+ totalClicks: rowNumber(r.totalClicks),
1928
+ series: parseJsonRows(r.seriesJson).map((s) => ({
1929
+ date: rowString(s.date),
1930
+ ctr: rowNumber(s.ctr),
1931
+ position: rowNumber(s.position),
1932
+ impressions: rowNumber(s.impressions),
1933
+ rollingCtr: s.rollingCtr == null ? null : rowNumber(s.rollingCtr),
1934
+ rollingStddev: s.rollingStddev == null ? null : rowNumber(s.rollingStddev),
1935
+ z: rowNumber(s.z),
1936
+ breach: rowBoolean(s.breach)
2022
1937
  }))
2023
1938
  }));
2024
1939
  const totalClicksLost = anomalies.reduce((s, a) => s + a.clicksLost, 0);
@@ -2035,24 +1950,6 @@ const ctrAnomalyAnalyzer = defineAnalyzer$1({
2035
1950
  };
2036
1951
  }
2037
1952
  });
2038
- function num$2(v) {
2039
- if (typeof v === "number") return v;
2040
- if (typeof v === "bigint") return Number(v);
2041
- if (v == null) return 0;
2042
- const n = Number(v);
2043
- return Number.isFinite(n) ? n : 0;
2044
- }
2045
- function str$14(v) {
2046
- return v == null ? "" : String(v);
2047
- }
2048
- function parseJsonList$10(v) {
2049
- if (Array.isArray(v)) return v;
2050
- if (typeof v === "string" && v.length > 0) {
2051
- const parsed = JSON.parse(v);
2052
- return Array.isArray(parsed) ? parsed : [];
2053
- }
2054
- return [];
2055
- }
2056
1953
  const ctrCurveAnalyzer = defineAnalyzer$1({
2057
1954
  id: "ctr-curve",
2058
1955
  buildSql(params) {
@@ -2147,22 +2044,22 @@ const ctrCurveAnalyzer = defineAnalyzer$1({
2147
2044
  const arr = Array.isArray(rows) ? rows : [];
2148
2045
  const { startDate, endDate } = periodOf$1(params);
2149
2046
  const row = arr[0] ?? {};
2150
- const curve = parseJsonList$10(row.curve_json).map((r) => ({
2151
- bucket: str$14(r.bucket),
2152
- avgCtr: num$2(r.avgCtr),
2153
- medianPosition: num$2(r.medianPosition),
2154
- keywordCount: num$2(r.keywordCount),
2155
- totalClicks: num$2(r.totalClicks),
2156
- totalImpressions: num$2(r.totalImpressions)
2047
+ const curve = parseJsonRows(row.curve_json).map((r) => ({
2048
+ bucket: rowString(r.bucket),
2049
+ avgCtr: rowNumber(r.avgCtr),
2050
+ medianPosition: rowNumber(r.medianPosition),
2051
+ keywordCount: rowNumber(r.keywordCount),
2052
+ totalClicks: rowNumber(r.totalClicks),
2053
+ totalImpressions: rowNumber(r.totalImpressions)
2157
2054
  }));
2158
- const outliers = parseJsonList$10(row.outliers_json).map((r) => ({
2159
- query: str$14(r.query),
2160
- clicks: num$2(r.clicks),
2161
- impressions: num$2(r.impressions),
2162
- ctr: num$2(r.ctr),
2163
- position: num$2(r.position),
2164
- expectedCtr: num$2(r.expectedCtr),
2165
- ctrDiff: num$2(r.ctrDiff)
2055
+ const outliers = parseJsonRows(row.outliers_json).map((r) => ({
2056
+ query: rowString(r.query),
2057
+ clicks: rowNumber(r.clicks),
2058
+ impressions: rowNumber(r.impressions),
2059
+ ctr: rowNumber(r.ctr),
2060
+ position: rowNumber(r.position),
2061
+ expectedCtr: rowNumber(r.expectedCtr),
2062
+ ctrDiff: rowNumber(r.ctrDiff)
2166
2063
  }));
2167
2064
  return {
2168
2065
  results: curve,
@@ -2175,17 +2072,6 @@ const ctrCurveAnalyzer = defineAnalyzer$1({
2175
2072
  };
2176
2073
  }
2177
2074
  });
2178
- function str$13(v) {
2179
- return v == null ? "" : String(v);
2180
- }
2181
- function parseJsonList$9(v) {
2182
- if (Array.isArray(v)) return v;
2183
- if (typeof v === "string" && v.length > 0) {
2184
- const parsed = JSON.parse(v);
2185
- return Array.isArray(parsed) ? parsed : [];
2186
- }
2187
- return [];
2188
- }
2189
2075
  const darkTrafficAnalyzer = defineAnalyzer$1({
2190
2076
  id: "dark-traffic",
2191
2077
  buildSql(params) {
@@ -2287,8 +2173,8 @@ const darkTrafficAnalyzer = defineAnalyzer$1({
2287
2173
  const darkClicks = Math.max(0, totalClicks - attributedClicks);
2288
2174
  const darkPercent = totalClicks > 0 ? darkClicks / totalClicks : 0;
2289
2175
  return {
2290
- results: parseJsonList$9(row.pages_json).map((r) => ({
2291
- url: str$13(r.url),
2176
+ results: parseJsonRows(row.pages_json).map((r) => ({
2177
+ url: rowString(r.url),
2292
2178
  totalClicks: num$1(r.totalClicks),
2293
2179
  attributedClicks: num$1(r.attributedClicks),
2294
2180
  darkClicks: num$1(r.darkClicks),
@@ -3040,17 +2926,6 @@ const sortResults$1 = createMetricSorter("lostClicks", {
3040
2926
  declinePercent: "desc",
3041
2927
  currentClicks: "asc"
3042
2928
  });
3043
- function str$12(v) {
3044
- return v == null ? "" : String(v);
3045
- }
3046
- function parseJsonList$8(v) {
3047
- if (Array.isArray(v)) return v;
3048
- if (typeof v === "string" && v.length > 0) {
3049
- const parsed = JSON.parse(v);
3050
- return Array.isArray(parsed) ? parsed : [];
3051
- }
3052
- return [];
3053
- }
3054
2929
  function analyzeDecay(input, options = {}) {
3055
2930
  const { minPreviousClicks = 50, threshold = .2, sortBy = "lostClicks" } = options;
3056
2931
  const currentMap = buildPeriodMap(input.current, (r) => r.page, (r) => ({
@@ -3177,7 +3052,7 @@ const decayAnalyzer = defineAnalyzer$1({
3177
3052
  },
3178
3053
  reduceSql(rows, params) {
3179
3054
  const mapped = (Array.isArray(rows) ? rows : []).map((r) => ({
3180
- page: str$12(r.page),
3055
+ page: rowString(r.page),
3181
3056
  currentClicks: num$1(r.currentClicks),
3182
3057
  previousClicks: num$1(r.previousClicks),
3183
3058
  lostClicks: num$1(r.lostClicks),
@@ -3185,8 +3060,8 @@ const decayAnalyzer = defineAnalyzer$1({
3185
3060
  currentPosition: num$1(r.currentPosition),
3186
3061
  previousPosition: num$1(r.previousPosition),
3187
3062
  positionDrop: num$1(r.positionDrop),
3188
- series: parseJsonList$8(r.seriesJson).map((s) => ({
3189
- week: str$12(s.week),
3063
+ series: parseJsonRows(r.seriesJson).map((s) => ({
3064
+ week: rowString(s.week),
3190
3065
  clicks: num$1(s.clicks),
3191
3066
  impressions: num$1(s.impressions)
3192
3067
  }))
@@ -3224,9 +3099,6 @@ const decayAnalyzer = defineAnalyzer$1({
3224
3099
  };
3225
3100
  }
3226
3101
  });
3227
- function str$11(v) {
3228
- return v == null ? "" : String(v);
3229
- }
3230
3102
  const deviceGapAnalyzer = defineAnalyzer$1({
3231
3103
  id: "device-gap",
3232
3104
  buildSql(params) {
@@ -3277,8 +3149,8 @@ const deviceGapAnalyzer = defineAnalyzer$1({
3277
3149
  const arr = Array.isArray(rows) ? rows : [];
3278
3150
  const { startDate, endDate } = periodOf$1(params);
3279
3151
  const typed = arr.map((r) => ({
3280
- date: str$11(r.date),
3281
- device: str$11(r.device).toUpperCase(),
3152
+ date: rowString(r.date),
3153
+ device: rowString(r.device).toUpperCase(),
3282
3154
  clicks: num$1(r.clicks),
3283
3155
  impressions: num$1(r.impressions),
3284
3156
  ctr: num$1(r.ctr),
@@ -3356,17 +3228,6 @@ const deviceGapAnalyzer = defineAnalyzer$1({
3356
3228
  };
3357
3229
  }
3358
3230
  });
3359
- function str$10(v) {
3360
- return v == null ? "" : String(v);
3361
- }
3362
- function parseJsonList$7(v) {
3363
- if (Array.isArray(v)) return v;
3364
- if (typeof v === "string" && v.length > 0) {
3365
- const parsed = JSON.parse(v);
3366
- return Array.isArray(parsed) ? parsed : [];
3367
- }
3368
- return [];
3369
- }
3370
3231
  const INTENT_ATLAS_STOP_WORDS = [
3371
3232
  "the",
3372
3233
  "a",
@@ -3509,14 +3370,14 @@ const intentAtlasAnalyzer = defineAnalyzer$1({
3509
3370
  },
3510
3371
  reduceSql(rows) {
3511
3372
  const clusters = (Array.isArray(rows) ? rows : []).map((r) => ({
3512
- clusterKey: str$10(r.clusterKey),
3373
+ clusterKey: rowString(r.clusterKey),
3513
3374
  keywordCount: num$1(r.keywordCount),
3514
3375
  totalImpressions: num$1(r.totalImpressions),
3515
3376
  totalClicks: num$1(r.totalClicks),
3516
3377
  ctr: num$1(r.ctr),
3517
3378
  avgPosition: num$1(r.avgPosition),
3518
- keywords: parseJsonList$7(r.keywords).slice(0, 25).map((k) => ({
3519
- query: str$10(k.query),
3379
+ keywords: parseJsonRows(r.keywords).slice(0, 25).map((k) => ({
3380
+ query: rowString(k.query),
3520
3381
  impressions: num$1(k.impressions),
3521
3382
  clicks: num$1(k.clicks),
3522
3383
  position: num$1(k.position)
@@ -3534,17 +3395,6 @@ const intentAtlasAnalyzer = defineAnalyzer$1({
3534
3395
  };
3535
3396
  }
3536
3397
  });
3537
- function str$9(v) {
3538
- return v == null ? "" : String(v);
3539
- }
3540
- function parseJsonList$6(v) {
3541
- if (Array.isArray(v)) return v;
3542
- if (typeof v === "string" && v.length > 0) {
3543
- const parsed = JSON.parse(v);
3544
- return Array.isArray(parsed) ? parsed : [];
3545
- }
3546
- return [];
3547
- }
3548
3398
  const keywordBreadthAnalyzer = defineAnalyzer$1({
3549
3399
  id: "keyword-breadth",
3550
3400
  buildSql(params) {
@@ -3619,18 +3469,18 @@ const keywordBreadthAnalyzer = defineAnalyzer$1({
3619
3469
  const arr = Array.isArray(rows) ? rows : [];
3620
3470
  const { startDate, endDate } = periodOf$1(params);
3621
3471
  const row = arr[0] ?? {};
3622
- const distribution = parseJsonList$6(row.distribution_json).sort((a, b) => num$1(a.sortKey) - num$1(b.sortKey)).map((r) => ({
3623
- bucket: str$9(r.bucket),
3472
+ const distribution = parseJsonRows(row.distribution_json).sort((a, b) => num$1(a.sortKey) - num$1(b.sortKey)).map((r) => ({
3473
+ bucket: rowString(r.bucket),
3624
3474
  pageCount: num$1(r.pageCount)
3625
3475
  }));
3626
- const fragile = parseJsonList$6(row.fragile_json).map((r) => ({
3627
- url: str$9(r.url),
3476
+ const fragile = parseJsonRows(row.fragile_json).map((r) => ({
3477
+ url: rowString(r.url),
3628
3478
  keywordCount: num$1(r.keywordCount),
3629
3479
  clicks: num$1(r.clicks),
3630
3480
  impressions: num$1(r.impressions)
3631
3481
  }));
3632
- const authority = parseJsonList$6(row.authority_json).map((r) => ({
3633
- url: str$9(r.url),
3482
+ const authority = parseJsonRows(row.authority_json).map((r) => ({
3483
+ url: rowString(r.url),
3634
3484
  keywordCount: num$1(r.keywordCount),
3635
3485
  clicks: num$1(r.clicks),
3636
3486
  impressions: num$1(r.impressions)
@@ -3653,23 +3503,12 @@ const keywordBreadthAnalyzer = defineAnalyzer$1({
3653
3503
  };
3654
3504
  }
3655
3505
  });
3656
- function str$8(v) {
3657
- return v == null ? "" : String(v);
3658
- }
3659
- function parseJsonList$5(v) {
3660
- if (Array.isArray(v)) return v;
3661
- if (typeof v === "string" && v.length > 0) {
3662
- const parsed = JSON.parse(v);
3663
- return Array.isArray(parsed) ? parsed : [];
3664
- }
3665
- return [];
3666
- }
3667
3506
  function downsampleLogRank(points) {
3668
3507
  const all = points.map((p) => ({
3669
3508
  rank: num$1(p.rank),
3670
3509
  impressions: num$1(p.impressions),
3671
3510
  clicks: num$1(p.clicks),
3672
- query: str$8(p.query)
3511
+ query: rowString(p.query)
3673
3512
  }));
3674
3513
  if (all.length <= 80) return all;
3675
3514
  const top = all.slice(0, 10);
@@ -3778,7 +3617,7 @@ const longTailAnalyzer = defineAnalyzer$1({
3778
3617
  },
3779
3618
  reduceSql(rows) {
3780
3619
  const results = (Array.isArray(rows) ? rows : []).map((r) => ({
3781
- page: str$8(r.page),
3620
+ page: rowString(r.page),
3782
3621
  queryCount: num$1(r.queryCount),
3783
3622
  totalImpressions: num$1(r.totalImpressions),
3784
3623
  totalClicks: num$1(r.totalClicks),
@@ -3787,8 +3626,8 @@ const longTailAnalyzer = defineAnalyzer$1({
3787
3626
  r2: num$1(r.r2),
3788
3627
  headImpressions: num$1(r.headImpressions),
3789
3628
  headShare: num$1(r.headShare),
3790
- fingerprint: str$8(r.fingerprint),
3791
- points: downsampleLogRank(parseJsonList$5(r.pointsJson))
3629
+ fingerprint: rowString(r.fingerprint),
3630
+ points: downsampleLogRank(parseJsonRows(r.pointsJson))
3792
3631
  }));
3793
3632
  const counts = {
3794
3633
  "flat-tail": 0,
@@ -3806,17 +3645,6 @@ const longTailAnalyzer = defineAnalyzer$1({
3806
3645
  };
3807
3646
  }
3808
3647
  });
3809
- function str$7(v) {
3810
- return v == null ? "" : String(v);
3811
- }
3812
- function parseJsonList$4(v) {
3813
- if (Array.isArray(v)) return v;
3814
- if (typeof v === "string" && v.length > 0) {
3815
- const parsed = JSON.parse(v);
3816
- return Array.isArray(parsed) ? parsed : [];
3817
- }
3818
- return [];
3819
- }
3820
3648
  function withCanonicalFields(r) {
3821
3649
  return {
3822
3650
  ...r,
@@ -4042,8 +3870,8 @@ const moversAnalyzer = defineAnalyzer$1({
4042
3870
  const baselineImpressions = Math.round(num$1(r.baselineImpressions));
4043
3871
  const baselinePosition = num$1(r.baselinePosition);
4044
3872
  return {
4045
- keyword: str$7(r.keyword),
4046
- page: r.page == null ? null : str$7(r.page),
3873
+ keyword: rowString(r.keyword),
3874
+ page: r.page == null ? null : rowString(r.page),
4047
3875
  recentClicks,
4048
3876
  recentImpressions,
4049
3877
  recentPosition,
@@ -4054,9 +3882,9 @@ const moversAnalyzer = defineAnalyzer$1({
4054
3882
  clicksChangePercent: num$1(r.clicksChangePercent),
4055
3883
  impressionsChangePercent: num$1(r.impressionsChangePercent),
4056
3884
  positionChange: num$1(r.positionChange),
4057
- direction: str$7(r.direction),
4058
- series: parseJsonList$4(r.seriesJson).map((s) => ({
4059
- week: str$7(s.week),
3885
+ direction: rowString(r.direction),
3886
+ series: parseJsonRows(r.seriesJson).map((s) => ({
3887
+ week: rowString(s.week),
4060
3888
  clicks: num$1(s.clicks),
4061
3889
  impressions: num$1(s.impressions)
4062
3890
  }))
@@ -4292,9 +4120,6 @@ const opportunityAnalyzer = defineAnalyzer$1({
4292
4120
  };
4293
4121
  }
4294
4122
  });
4295
- function str$6(v) {
4296
- return v == null ? "" : String(v);
4297
- }
4298
4123
  const positionDistributionAnalyzer = defineAnalyzer$1({
4299
4124
  id: "position-distribution",
4300
4125
  buildSql(params) {
@@ -4331,7 +4156,7 @@ const positionDistributionAnalyzer = defineAnalyzer$1({
4331
4156
  const { startDate, endDate } = periodOf$1(params);
4332
4157
  return {
4333
4158
  results: arr.map((r) => ({
4334
- date: str$6(r.date),
4159
+ date: rowString(r.date),
4335
4160
  pos_1_3: num$1(r.pos_1_3),
4336
4161
  pos_4_10: num$1(r.pos_4_10),
4337
4162
  pos_11_20: num$1(r.pos_11_20),
@@ -4346,9 +4171,6 @@ const positionDistributionAnalyzer = defineAnalyzer$1({
4346
4171
  };
4347
4172
  }
4348
4173
  });
4349
- function str$5(v) {
4350
- return v == null ? "" : String(v);
4351
- }
4352
4174
  const positionVolatilityAnalyzer = defineAnalyzer$1({
4353
4175
  id: "position-volatility",
4354
4176
  buildSql(params) {
@@ -4449,8 +4271,8 @@ const positionVolatilityAnalyzer = defineAnalyzer$1({
4449
4271
  const byPage = /* @__PURE__ */ new Map();
4450
4272
  const allDates = /* @__PURE__ */ new Set();
4451
4273
  for (const r of arr) {
4452
- const page = str$5(r.page);
4453
- const date = str$5(r.date);
4274
+ const page = rowString(r.page);
4275
+ const date = rowString(r.date);
4454
4276
  allDates.add(date);
4455
4277
  const entry = byPage.get(page) ?? {
4456
4278
  page,
@@ -4485,17 +4307,6 @@ const positionVolatilityAnalyzer = defineAnalyzer$1({
4485
4307
  };
4486
4308
  }
4487
4309
  });
4488
- function str$4(v) {
4489
- return v == null ? "" : String(v);
4490
- }
4491
- function parseJsonList$3(v) {
4492
- if (Array.isArray(v)) return v;
4493
- if (typeof v === "string" && v.length > 0) {
4494
- const parsed = JSON.parse(v);
4495
- return Array.isArray(parsed) ? parsed : [];
4496
- }
4497
- return [];
4498
- }
4499
4310
  const queryMigrationAnalyzer = defineAnalyzer$1({
4500
4311
  id: "query-migration",
4501
4312
  buildSql(params) {
@@ -4617,17 +4428,17 @@ const queryMigrationAnalyzer = defineAnalyzer$1({
4617
4428
  prevStart = toIsoDate(new Date(curStartMs - MS_PER_DAY - span));
4618
4429
  }
4619
4430
  const edges = arr.map((r) => ({
4620
- sourcePage: str$4(r.source_page),
4621
- targetPage: str$4(r.target_page),
4431
+ sourcePage: rowString(r.source_page),
4432
+ targetPage: rowString(r.target_page),
4622
4433
  weight: num$1(r.weight),
4623
4434
  queryCount: num$1(r.query_count),
4624
4435
  exactCount: num$1(r.exact_count),
4625
4436
  fuzzyCount: num$1(r.query_count) - num$1(r.exact_count),
4626
- examples: parseJsonList$3(r.examplesJson).slice(0, 8).map((e) => ({
4627
- sourceQuery: str$4(e.sourceQuery),
4628
- targetQuery: str$4(e.targetQuery),
4437
+ examples: parseJsonRows(r.examplesJson).slice(0, 8).map((e) => ({
4438
+ sourceQuery: rowString(e.sourceQuery),
4439
+ targetQuery: rowString(e.targetQuery),
4629
4440
  absorbed: num$1(e.absorbed),
4630
- matchType: str$4(e.matchType)
4441
+ matchType: rowString(e.matchType)
4631
4442
  }))
4632
4443
  }));
4633
4444
  const nodeAgg = /* @__PURE__ */ new Map();
@@ -4666,12 +4477,6 @@ const queryMigrationAnalyzer = defineAnalyzer$1({
4666
4477
  };
4667
4478
  }
4668
4479
  });
4669
- function str$3(v) {
4670
- return v == null ? "" : String(v);
4671
- }
4672
- function bool$1(v) {
4673
- return v === true || v === 1 || v === "true";
4674
- }
4675
4480
  function calculateCV(values) {
4676
4481
  if (values.length === 0) return 0;
4677
4482
  const mean = values.reduce((a, b) => a + b, 0) / values.length;
@@ -4765,11 +4570,11 @@ const seasonalityAnalyzer = defineAnalyzer$1({
4765
4570
  reduceSql(rows) {
4766
4571
  const arr = Array.isArray(rows) ? rows : [];
4767
4572
  const breakdown = arr.map((r) => ({
4768
- month: str$3(r.month),
4573
+ month: rowString(r.month),
4769
4574
  value: num$1(r.value),
4770
4575
  vsAverage: num$1(r.vsAverage),
4771
- isPeak: bool$1(r.isPeak),
4772
- isTrough: bool$1(r.isTrough)
4576
+ isPeak: rowBoolean(r.isPeak),
4577
+ isTrough: rowBoolean(r.isTrough)
4773
4578
  }));
4774
4579
  const first = arr[0];
4775
4580
  const strength = first ? num$1(first.strength) : 0;
@@ -4801,20 +4606,6 @@ const seasonalityAnalyzer = defineAnalyzer$1({
4801
4606
  };
4802
4607
  }
4803
4608
  });
4804
- function str$2(v) {
4805
- return v == null ? "" : String(v);
4806
- }
4807
- function bool(v) {
4808
- return v === true || v === 1 || v === "true";
4809
- }
4810
- function parseJsonList$2(v) {
4811
- if (Array.isArray(v)) return v;
4812
- if (typeof v === "string" && v.length > 0) {
4813
- const parsed = JSON.parse(v);
4814
- return Array.isArray(parsed) ? parsed : [];
4815
- }
4816
- return [];
4817
- }
4818
4609
  const stlDecomposeAnalyzer = defineAnalyzer$1({
4819
4610
  id: "stl-decompose",
4820
4611
  buildSql(params) {
@@ -4967,21 +4758,21 @@ const stlDecomposeAnalyzer = defineAnalyzer$1({
4967
4758
  const arr = Array.isArray(rows) ? rows : [];
4968
4759
  const metric = params.metric === "clicks" ? "clicks" : "impressions";
4969
4760
  const results = arr.map((r) => ({
4970
- keyword: str$2(r.keyword),
4971
- page: str$2(r.page),
4761
+ keyword: rowString(r.keyword),
4762
+ page: rowString(r.page),
4972
4763
  totalImpressions: num$1(r.totalImpressions),
4973
4764
  days: num$1(r.days),
4974
4765
  seasonalStrength: num$1(r.seasonalStrength),
4975
4766
  trendStrength: num$1(r.trendStrength),
4976
4767
  residualAnomalies: num$1(r.residualAnomalies),
4977
4768
  trendSlope: num$1(r.trendSlope),
4978
- series: parseJsonList$2(r.seriesJson).map((s) => ({
4979
- date: str$2(s.date),
4769
+ series: parseJsonRows(r.seriesJson).map((s) => ({
4770
+ date: rowString(s.date),
4980
4771
  observed: num$1(s.observed),
4981
4772
  trend: s.trend == null ? null : num$1(s.trend),
4982
4773
  seasonal: s.seasonal == null ? null : num$1(s.seasonal),
4983
4774
  residual: s.residual == null ? null : num$1(s.residual),
4984
- anomaly: bool(s.anomaly)
4775
+ anomaly: rowBoolean(s.anomaly)
4985
4776
  }))
4986
4777
  }));
4987
4778
  return {
@@ -5064,17 +4855,6 @@ const strikingDistanceAnalyzer = defineAnalyzer$1({
5064
4855
  return { queries: queriesQueryState(periodOf$1(params), params.limit ?? DEFAULT_ROW_LIMIT$1) };
5065
4856
  }
5066
4857
  });
5067
- function str$1(v) {
5068
- return v == null ? "" : String(v);
5069
- }
5070
- function parseJsonList$1(v) {
5071
- if (Array.isArray(v)) return v;
5072
- if (typeof v === "string" && v.length > 0) {
5073
- const parsed = JSON.parse(v);
5074
- return Array.isArray(parsed) ? parsed : [];
5075
- }
5076
- return [];
5077
- }
5078
4858
  const survivalAnalyzer = defineAnalyzer$1({
5079
4859
  id: "survival",
5080
4860
  buildSql(params) {
@@ -5227,7 +5007,7 @@ const survivalAnalyzer = defineAnalyzer$1({
5227
5007
  const startDate = params.startDate ?? daysAgo(183);
5228
5008
  const windowDays = Math.round((new Date(endDate).getTime() - new Date(startDate).getTime()) / MS_PER_DAY) + 1;
5229
5009
  const results = arr.map((r) => {
5230
- const curve = parseJsonList$1(r.curveJson).map((p) => ({
5010
+ const curve = parseJsonRows(r.curveJson).map((p) => ({
5231
5011
  tenure: num$1(p.tenure),
5232
5012
  survival: num$1(p.survival),
5233
5013
  atRisk: num$1(p.atRisk),
@@ -5250,7 +5030,7 @@ const survivalAnalyzer = defineAnalyzer$1({
5250
5030
  const last = curve[curve.length - 1];
5251
5031
  if (medianTenure === 0 && last && last.survival > .5) medianTenure = last.tenure;
5252
5032
  return {
5253
- cohort: str$1(r.cohort),
5033
+ cohort: rowString(r.cohort),
5254
5034
  episodeCount: num$1(r.episodeCount),
5255
5035
  censoringRate: num$1(r.censoringRate),
5256
5036
  medianTenure,
@@ -5267,17 +5047,6 @@ const survivalAnalyzer = defineAnalyzer$1({
5267
5047
  };
5268
5048
  }
5269
5049
  });
5270
- function str(v) {
5271
- return v == null ? "" : String(v);
5272
- }
5273
- function parseJsonList(v) {
5274
- if (Array.isArray(v)) return v;
5275
- if (typeof v === "string" && v.length > 0) {
5276
- const parsed = JSON.parse(v);
5277
- return Array.isArray(parsed) ? parsed : [];
5278
- }
5279
- return [];
5280
- }
5281
5050
  const trendsAnalyzer = defineAnalyzer$1({
5282
5051
  id: "trends",
5283
5052
  buildSql(params) {
@@ -5388,20 +5157,20 @@ const trendsAnalyzer = defineAnalyzer$1({
5388
5157
  const startDate = params.startDate || toIsoDate(/* @__PURE__ */ new Date(Date.parse(endDate) - (weeks * 7 - 1) * MS_PER_DAY));
5389
5158
  const dim = params.dimension === "keywords" ? "keywords" : "pages";
5390
5159
  const results = arr.map((r) => {
5391
- const series = parseJsonList(r.seriesJson).map((s) => ({
5392
- week: str(s.week),
5160
+ const series = parseJsonRows(r.seriesJson).map((s) => ({
5161
+ week: rowString(s.week),
5393
5162
  clicks: num$1(s.clicks),
5394
5163
  impressions: num$1(s.impressions)
5395
5164
  }));
5396
5165
  return {
5397
- [dim === "keywords" ? "query" : "page"]: str(r.entity),
5166
+ [dim === "keywords" ? "query" : "page"]: rowString(r.entity),
5398
5167
  totalClicks: num$1(r.totalClicks),
5399
5168
  totalImpressions: num$1(r.totalImpressions),
5400
5169
  weeksWithData: num$1(r.weeksWithData),
5401
5170
  slope: num$1(r.slope),
5402
5171
  growthRatio: num$1(r.growthRatio),
5403
5172
  avgPosition: num$1(r.avgPosition),
5404
- trend: str(r.trend),
5173
+ trend: rowString(r.trend),
5405
5174
  series
5406
5175
  };
5407
5176
  });
@@ -5639,6 +5408,27 @@ function requireComparisonWindowResult(report, window, message) {
5639
5408
  function requireComparisonWindow(report, window, message) {
5640
5409
  return unwrapResult(requireComparisonWindowResult(report, window, message), analysisErrorToException);
5641
5410
  }
5411
+ function reportRows(result) {
5412
+ return result?.results ?? [];
5413
+ }
5414
+ function sectionCoverage(result) {
5415
+ return result ? "full" : "partial";
5416
+ }
5417
+ function sectionArtifact(result, analyzer, params = {}) {
5418
+ return result ? {
5419
+ analyzer,
5420
+ params: {
5421
+ type: analyzer,
5422
+ ...params
5423
+ }
5424
+ } : void 0;
5425
+ }
5426
+ function truncation(total, kept) {
5427
+ return total > kept ? {
5428
+ kept,
5429
+ total
5430
+ } : void 0;
5431
+ }
5642
5432
  const DEFAULT_MAX$7 = 5;
5643
5433
  const brandReport = defineReport({
5644
5434
  id: "brand",
@@ -5688,7 +5478,7 @@ const brandReport = defineReport({
5688
5478
  }
5689
5479
  });
5690
5480
  function buildBrandSplitSection(res, max) {
5691
- const rows = res?.results ?? [];
5481
+ const rows = reportRows(res);
5692
5482
  const summary = res?.meta?.summary;
5693
5483
  const findings = rows.filter((r) => r.segment === "brand").sort((a, b) => b.clicks - a.clicks).slice(0, max).map((r) => ({
5694
5484
  entity: {
@@ -5710,16 +5500,13 @@ function buildBrandSplitSection(res, max) {
5710
5500
  severity: "info",
5711
5501
  summary: { magnitudeLabel: summary ? `brand share ${(brandShare * 100).toFixed(1)}% (${summary.brandClicks} brand vs ${summary.nonBrandClicks} non-brand clicks)` : "no summary available" },
5712
5502
  findings,
5713
- coverage: res ? "full" : "partial",
5503
+ coverage: sectionCoverage(res),
5714
5504
  actions: [],
5715
- artifact: res ? {
5716
- analyzer: "brand",
5717
- params: { type: "brand" }
5718
- } : void 0
5505
+ artifact: sectionArtifact(res, "brand")
5719
5506
  };
5720
5507
  }
5721
5508
  function buildConcentrationSection(res, max) {
5722
- const head = (res?.results ?? [])[0];
5509
+ const head = reportRows(res)[0];
5723
5510
  const findings = (head?.topNItems ?? []).slice(0, max).map((it) => ({
5724
5511
  entity: {
5725
5512
  kind: "query",
@@ -5737,15 +5524,9 @@ function buildConcentrationSection(res, max) {
5737
5524
  severity: head ? severity : "info",
5738
5525
  summary: head ? { magnitudeLabel: `HHI ${head.hhi.toFixed(0)} (${head.riskLevel}); top-N share ${(head.topNConcentration * 100).toFixed(1)}%` } : {},
5739
5526
  findings,
5740
- coverage: res ? "full" : "partial",
5527
+ coverage: sectionCoverage(res),
5741
5528
  actions: [],
5742
- artifact: res ? {
5743
- analyzer: "concentration",
5744
- params: {
5745
- type: "concentration",
5746
- dimension: "keywords"
5747
- }
5748
- } : void 0
5529
+ artifact: sectionArtifact(res, "concentration", { dimension: "keywords" })
5749
5530
  };
5750
5531
  }
5751
5532
  const DEFAULT_MAX$6 = 5;
@@ -5811,7 +5592,7 @@ const growthReport = defineReport({
5811
5592
  }
5812
5593
  });
5813
5594
  function buildContentVelocity(res) {
5814
- const rows = res?.results ?? [];
5595
+ const rows = reportRows(res);
5815
5596
  const totalNew = rows.reduce((s, r) => s + r.newKeywords, 0);
5816
5597
  const avgPerWeek = rows.length > 0 ? totalNew / rows.length : 0;
5817
5598
  return {
@@ -5820,16 +5601,13 @@ function buildContentVelocity(res) {
5820
5601
  severity: "info",
5821
5602
  summary: { magnitudeLabel: `${totalNew} new keywords across ${rows.length} weeks (avg ${avgPerWeek.toFixed(1)}/wk)` },
5822
5603
  findings: [],
5823
- coverage: res ? "full" : "partial",
5604
+ coverage: sectionCoverage(res),
5824
5605
  actions: [],
5825
- artifact: res ? {
5826
- analyzer: "content-velocity",
5827
- params: { type: "content-velocity" }
5828
- } : void 0
5606
+ artifact: sectionArtifact(res, "content-velocity")
5829
5607
  };
5830
5608
  }
5831
5609
  function buildKeywordBreadth(res) {
5832
- const rows = res?.results ?? [];
5610
+ const rows = reportRows(res);
5833
5611
  const totalPages = rows.reduce((s, r) => s + r.pageCount, 0);
5834
5612
  const top = rows[0];
5835
5613
  return {
@@ -5838,32 +5616,26 @@ function buildKeywordBreadth(res) {
5838
5616
  severity: "info",
5839
5617
  summary: { magnitudeLabel: top ? `${totalPages} pages; modal bucket "${top.bucket}" (${top.pageCount} pages)` : "no data" },
5840
5618
  findings: [],
5841
- coverage: res ? "full" : "partial",
5619
+ coverage: sectionCoverage(res),
5842
5620
  actions: [],
5843
- artifact: res ? {
5844
- analyzer: "keyword-breadth",
5845
- params: { type: "keyword-breadth" }
5846
- } : void 0
5621
+ artifact: sectionArtifact(res, "keyword-breadth")
5847
5622
  };
5848
5623
  }
5849
5624
  function buildIntentAtlas(res) {
5850
- const rows = res?.results ?? [];
5625
+ const rows = reportRows(res);
5851
5626
  return {
5852
5627
  id: "intent-atlas",
5853
5628
  title: "Intent atlas",
5854
5629
  severity: "info",
5855
5630
  summary: { magnitudeLabel: `${rows.length} clusters covering ${rows.reduce((s, r) => s + r.keywordCount, 0)} keywords` },
5856
5631
  findings: [],
5857
- coverage: res ? "full" : "partial",
5632
+ coverage: sectionCoverage(res),
5858
5633
  actions: [],
5859
- artifact: res ? {
5860
- analyzer: "intent-atlas",
5861
- params: { type: "intent-atlas" }
5862
- } : void 0
5634
+ artifact: sectionArtifact(res, "intent-atlas")
5863
5635
  };
5864
5636
  }
5865
5637
  function buildLongTail(res, max) {
5866
- const rows = (res?.results ?? []).sort((a, b) => {
5638
+ const rows = reportRows(res).sort((a, b) => {
5867
5639
  const rank = {
5868
5640
  "head-heavy": 0,
5869
5641
  "balanced": 1,
@@ -5892,16 +5664,10 @@ function buildLongTail(res, max) {
5892
5664
  severity: headHeavy > rows.length / 3 ? "medium" : "info",
5893
5665
  summary: { magnitudeLabel: `${rows.length} pages analysed; ${headHeavy} head-heavy` },
5894
5666
  findings,
5895
- truncated: rows.length > max ? {
5896
- kept: kept.length,
5897
- total: rows.length
5898
- } : void 0,
5899
- coverage: res ? "full" : "partial",
5667
+ truncated: truncation(rows.length, kept.length),
5668
+ coverage: sectionCoverage(res),
5900
5669
  actions: [],
5901
- artifact: res ? {
5902
- analyzer: "long-tail",
5903
- params: { type: "long-tail" }
5904
- } : void 0
5670
+ artifact: sectionArtifact(res, "long-tail")
5905
5671
  };
5906
5672
  }
5907
5673
  const DEFAULT_MAX$5 = 5;
@@ -5958,7 +5724,7 @@ const healthReport = defineReport({
5958
5724
  }
5959
5725
  });
5960
5726
  function buildCtrAnomalySection(res, max) {
5961
- const rows = (res?.results ?? []).filter((r) => r.breachDaysDown > 0).sort((a, b) => b.clicksLost - a.clicksLost);
5727
+ const rows = reportRows(res).filter((r) => r.breachDaysDown > 0).sort((a, b) => b.clicksLost - a.clicksLost);
5962
5728
  const kept = rows.slice(0, max);
5963
5729
  const totalLost = kept.reduce((s, r) => s + r.clicksLost, 0);
5964
5730
  const findings = kept.map((r) => ({
@@ -5984,20 +5750,14 @@ function buildCtrAnomalySection(res, max) {
5984
5750
  magnitudeLabel: `${Math.round(totalLost)} clicks lost vs baseline`
5985
5751
  },
5986
5752
  findings,
5987
- truncated: rows.length > max ? {
5988
- kept: kept.length,
5989
- total: rows.length
5990
- } : void 0,
5991
- coverage: res ? "full" : "partial",
5753
+ truncated: truncation(rows.length, kept.length),
5754
+ coverage: sectionCoverage(res),
5992
5755
  actions: [],
5993
- artifact: res ? {
5994
- analyzer: "ctr-anomaly",
5995
- params: { type: "ctr-anomaly" }
5996
- } : void 0
5756
+ artifact: sectionArtifact(res, "ctr-anomaly")
5997
5757
  };
5998
5758
  }
5999
5759
  function buildChangePointSection$1(res, max) {
6000
- const rows = (res?.results ?? []).filter((r) => r.direction === "worsened").sort((a, b) => b.llr - a.llr);
5760
+ const rows = reportRows(res).filter((r) => r.direction === "worsened").sort((a, b) => b.llr - a.llr);
6001
5761
  const kept = rows.slice(0, max);
6002
5762
  const findings = kept.map((r) => ({
6003
5763
  entity: {
@@ -6016,20 +5776,14 @@ function buildChangePointSection$1(res, max) {
6016
5776
  severity: kept.length ? "medium" : "info",
6017
5777
  summary: { magnitudeLabel: `${kept.length} worsening segments` },
6018
5778
  findings,
6019
- truncated: rows.length > max ? {
6020
- kept: kept.length,
6021
- total: rows.length
6022
- } : void 0,
6023
- coverage: res ? "full" : "partial",
5779
+ truncated: truncation(rows.length, kept.length),
5780
+ coverage: sectionCoverage(res),
6024
5781
  actions: [],
6025
- artifact: res ? {
6026
- analyzer: "change-point",
6027
- params: { type: "change-point" }
6028
- } : void 0
5782
+ artifact: sectionArtifact(res, "change-point")
6029
5783
  };
6030
5784
  }
6031
5785
  function buildPositionVolatilitySection(res, max) {
6032
- const rows = (res?.results ?? []).sort((a, b) => b.peakVolatility - a.peakVolatility);
5786
+ const rows = reportRows(res).sort((a, b) => b.peakVolatility - a.peakVolatility);
6033
5787
  const kept = rows.slice(0, max);
6034
5788
  const findings = kept.map((r) => ({
6035
5789
  entity: {
@@ -6048,16 +5802,10 @@ function buildPositionVolatilitySection(res, max) {
6048
5802
  severity: kept.length ? "low" : "info",
6049
5803
  summary: {},
6050
5804
  findings,
6051
- truncated: rows.length > max ? {
6052
- kept: kept.length,
6053
- total: rows.length
6054
- } : void 0,
6055
- coverage: res ? "full" : "partial",
5805
+ truncated: truncation(rows.length, kept.length),
5806
+ coverage: sectionCoverage(res),
6056
5807
  actions: [],
6057
- artifact: res ? {
6058
- analyzer: "position-volatility",
6059
- params: { type: "position-volatility" }
6060
- } : void 0
5808
+ artifact: sectionArtifact(res, "position-volatility")
6061
5809
  };
6062
5810
  }
6063
5811
  const DEFAULT_MAX$4 = 5;
@@ -6133,7 +5881,7 @@ const moversReport = defineReport({
6133
5881
  }
6134
5882
  });
6135
5883
  function buildMoversSection(res, direction, max, minChange) {
6136
- const rows = (res?.results ?? []).filter((r) => r.direction === direction && Math.abs(r.clicksChange) >= minChange).sort((a, b) => Math.abs(b.clicksChange) - Math.abs(a.clicksChange));
5884
+ const rows = reportRows(res).filter((r) => r.direction === direction && Math.abs(r.clicksChange) >= minChange).sort((a, b) => Math.abs(b.clicksChange) - Math.abs(a.clicksChange));
6137
5885
  const total = rows.length;
6138
5886
  const kept = rows.slice(0, max);
6139
5887
  const findings = kept.map((r) => ({
@@ -6165,21 +5913,15 @@ function buildMoversSection(res, direction, max, minChange) {
6165
5913
  direction: totalDelta > 0 ? "up" : totalDelta < 0 ? "down" : "flat"
6166
5914
  },
6167
5915
  findings,
6168
- truncated: total > max ? {
6169
- kept: kept.length,
6170
- total
6171
- } : void 0,
6172
- coverage: res ? "full" : "partial",
5916
+ truncated: truncation(total, kept.length),
5917
+ coverage: sectionCoverage(res),
6173
5918
  actions: [],
6174
- artifact: res ? {
6175
- analyzer: "movers",
6176
- params: { type: "movers" }
6177
- } : void 0
5919
+ artifact: sectionArtifact(res, "movers")
6178
5920
  };
6179
5921
  }
6180
5922
  function buildDeclinersSection(moversRes, decayRes, max, minChange) {
6181
- const decliningQueries = (moversRes?.results ?? []).filter((r) => r.direction === "declining" && Math.abs(r.clicksChange) >= minChange).slice(0, max);
6182
- const lostPages = (decayRes?.results ?? []).sort((a, b) => b.lostClicks - a.lostClicks).slice(0, max);
5923
+ const decliningQueries = reportRows(moversRes).filter((r) => r.direction === "declining" && Math.abs(r.clicksChange) >= minChange).slice(0, max);
5924
+ const lostPages = reportRows(decayRes).sort((a, b) => b.lostClicks - a.lostClicks).slice(0, max);
6183
5925
  const findings = [...decliningQueries.map((r) => ({
6184
5926
  entity: {
6185
5927
  kind: "query",
@@ -6238,7 +5980,7 @@ function buildDeclinersSection(moversRes, decayRes, max, minChange) {
6238
5980
  };
6239
5981
  }
6240
5982
  function buildStrikingSection$1(res, max) {
6241
- const rows = (res?.results ?? []).sort((a, b) => b.potentialClicks - a.potentialClicks);
5983
+ const rows = reportRows(res).sort((a, b) => b.potentialClicks - a.potentialClicks);
6242
5984
  const kept = rows.slice(0, max);
6243
5985
  const findings = kept.map((r) => ({
6244
5986
  entity: {
@@ -6259,16 +6001,10 @@ function buildStrikingSection$1(res, max) {
6259
6001
  severity: "low",
6260
6002
  summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.potentialClicks, 0)} potential clicks` },
6261
6003
  findings,
6262
- truncated: rows.length > max ? {
6263
- kept: kept.length,
6264
- total: rows.length
6265
- } : void 0,
6266
- coverage: res ? "full" : "partial",
6004
+ truncated: truncation(rows.length, kept.length),
6005
+ coverage: sectionCoverage(res),
6267
6006
  actions: [],
6268
- artifact: res ? {
6269
- analyzer: "striking-distance",
6270
- params: { type: "striking-distance" }
6271
- } : void 0
6007
+ artifact: sectionArtifact(res, "striking-distance")
6272
6008
  };
6273
6009
  }
6274
6010
  const DEFAULT_MAX$3 = 5;
@@ -6334,7 +6070,7 @@ const opportunitiesReport = defineReport({
6334
6070
  }
6335
6071
  });
6336
6072
  function buildStrikingSection(res, max) {
6337
- const rows = (res?.results ?? []).sort((a, b) => b.potentialClicks - a.potentialClicks);
6073
+ const rows = reportRows(res).sort((a, b) => b.potentialClicks - a.potentialClicks);
6338
6074
  const kept = rows.slice(0, max);
6339
6075
  const findings = kept.map((r) => ({
6340
6076
  entity: {
@@ -6356,20 +6092,14 @@ function buildStrikingSection(res, max) {
6356
6092
  severity: "low",
6357
6093
  summary: { magnitudeLabel: `${Math.round(totalPotential)} potential clicks` },
6358
6094
  findings,
6359
- truncated: rows.length > max ? {
6360
- kept: kept.length,
6361
- total: rows.length
6362
- } : void 0,
6363
- coverage: res ? "full" : "partial",
6095
+ truncated: truncation(rows.length, kept.length),
6096
+ coverage: sectionCoverage(res),
6364
6097
  actions: [],
6365
- artifact: res ? {
6366
- analyzer: "striking-distance",
6367
- params: { type: "striking-distance" }
6368
- } : void 0
6098
+ artifact: sectionArtifact(res, "striking-distance")
6369
6099
  };
6370
6100
  }
6371
6101
  function buildOpportunitySection(res, max) {
6372
- const rows = (res?.results ?? []).sort((a, b) => b.opportunityScore - a.opportunityScore);
6102
+ const rows = reportRows(res).sort((a, b) => b.opportunityScore - a.opportunityScore);
6373
6103
  const kept = rows.slice(0, max);
6374
6104
  return {
6375
6105
  id: "low-ctr",
@@ -6390,11 +6120,8 @@ function buildOpportunitySection(res, max) {
6390
6120
  },
6391
6121
  why: r.page ? `low CTR on ${r.page}` : "low CTR vs position"
6392
6122
  })),
6393
- truncated: rows.length > max ? {
6394
- kept: kept.length,
6395
- total: rows.length
6396
- } : void 0,
6397
- coverage: res ? "full" : "partial",
6123
+ truncated: truncation(rows.length, kept.length),
6124
+ coverage: sectionCoverage(res),
6398
6125
  actions: kept.slice(0, 1).map((r) => ({
6399
6126
  kind: "fix",
6400
6127
  target: r.page ? {
@@ -6406,14 +6133,11 @@ function buildOpportunitySection(res, max) {
6406
6133
  },
6407
6134
  rationale: "Rewrite title/description to lift CTR at this position"
6408
6135
  })),
6409
- artifact: res ? {
6410
- analyzer: "opportunity",
6411
- params: { type: "opportunity" }
6412
- } : void 0
6136
+ artifact: sectionArtifact(res, "opportunity")
6413
6137
  };
6414
6138
  }
6415
6139
  function buildZeroClickSection(res, max) {
6416
- const rows = (res?.results ?? []).sort((a, b) => b.impressions - a.impressions);
6140
+ const rows = reportRows(res).sort((a, b) => b.impressions - a.impressions);
6417
6141
  const kept = rows.slice(0, max);
6418
6142
  const findings = kept.map((r) => ({
6419
6143
  entity: {
@@ -6433,20 +6157,14 @@ function buildZeroClickSection(res, max) {
6433
6157
  severity: "info",
6434
6158
  summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.impressions, 0)} impressions wasted` },
6435
6159
  findings,
6436
- truncated: rows.length > max ? {
6437
- kept: kept.length,
6438
- total: rows.length
6439
- } : void 0,
6440
- coverage: res ? "full" : "partial",
6160
+ truncated: truncation(rows.length, kept.length),
6161
+ coverage: sectionCoverage(res),
6441
6162
  actions: [],
6442
- artifact: res ? {
6443
- analyzer: "zero-click",
6444
- params: { type: "zero-click" }
6445
- } : void 0
6163
+ artifact: sectionArtifact(res, "zero-click")
6446
6164
  };
6447
6165
  }
6448
6166
  function buildMigrationSection$1(res, max) {
6449
- const rows = (res?.results ?? []).sort((a, b) => b.weight - a.weight);
6167
+ const rows = reportRows(res).sort((a, b) => b.weight - a.weight);
6450
6168
  const kept = rows.slice(0, max);
6451
6169
  return {
6452
6170
  id: "query-migration",
@@ -6464,16 +6182,10 @@ function buildMigrationSection$1(res, max) {
6464
6182
  },
6465
6183
  why: `migrating from ${r.sourcePage}`
6466
6184
  })),
6467
- truncated: rows.length > max ? {
6468
- kept: kept.length,
6469
- total: rows.length
6470
- } : void 0,
6471
- coverage: res ? "full" : "partial",
6185
+ truncated: truncation(rows.length, kept.length),
6186
+ coverage: sectionCoverage(res),
6472
6187
  actions: [],
6473
- artifact: res ? {
6474
- analyzer: "query-migration",
6475
- params: { type: "query-migration" }
6476
- } : void 0
6188
+ artifact: sectionArtifact(res, "query-migration")
6477
6189
  };
6478
6190
  }
6479
6191
  const DEFAULT_MAX$2 = 10;
@@ -6524,7 +6236,7 @@ const prePublishReport = defineReport({
6524
6236
  }
6525
6237
  });
6526
6238
  function buildCannibalizationSection$1(res, matches, max) {
6527
- const rows = (res?.results ?? []).filter((r) => matches(r.keyword) || (r.competitors ?? []).some((p) => matches(p.url))).sort((a, b) => b.totalClicks - a.totalClicks);
6239
+ const rows = reportRows(res).filter((r) => matches(r.keyword) || (r.competitors ?? []).some((p) => matches(p.url))).sort((a, b) => b.totalClicks - a.totalClicks);
6528
6240
  const kept = rows.slice(0, max);
6529
6241
  const findings = kept.map((r) => {
6530
6242
  const pageCount = r.competitorCount ?? r.competitors?.length ?? 0;
@@ -6547,23 +6259,17 @@ function buildCannibalizationSection$1(res, matches, max) {
6547
6259
  severity: kept.length ? "high" : "info",
6548
6260
  summary: { magnitudeLabel: kept.length ? `${kept.length} existing competition` : "no existing competition" },
6549
6261
  findings,
6550
- truncated: rows.length > max ? {
6551
- kept: kept.length,
6552
- total: rows.length
6553
- } : void 0,
6554
- coverage: res ? "full" : "partial",
6262
+ truncated: truncation(rows.length, kept.length),
6263
+ coverage: sectionCoverage(res),
6555
6264
  actions: kept.slice(0, 1).map(() => ({
6556
6265
  kind: "fix",
6557
6266
  rationale: "Decide before publishing: redirect existing page, target a different angle, or accept overlap."
6558
6267
  })),
6559
- artifact: res ? {
6560
- analyzer: "cannibalization",
6561
- params: { type: "cannibalization" }
6562
- } : void 0
6268
+ artifact: sectionArtifact(res, "cannibalization")
6563
6269
  };
6564
6270
  }
6565
6271
  function buildStrikingPeersSection(res, matches, max, topic) {
6566
- const rows = (res?.results ?? []).filter((r) => matches(r.keyword) || matches(r.page)).sort((a, b) => b.potentialClicks - a.potentialClicks);
6272
+ const rows = reportRows(res).filter((r) => matches(r.keyword) || matches(r.page)).sort((a, b) => b.potentialClicks - a.potentialClicks);
6567
6273
  const kept = rows.slice(0, max);
6568
6274
  const findings = kept.map((r) => ({
6569
6275
  entity: {
@@ -6583,16 +6289,10 @@ function buildStrikingPeersSection(res, matches, max, topic) {
6583
6289
  severity: kept.length ? "low" : "info",
6584
6290
  summary: { magnitudeLabel: kept.length ? `${kept.length} adjacent rankings for "${topic}"` : "no adjacent rankings" },
6585
6291
  findings,
6586
- truncated: rows.length > max ? {
6587
- kept: kept.length,
6588
- total: rows.length
6589
- } : void 0,
6590
- coverage: res ? "full" : "partial",
6292
+ truncated: truncation(rows.length, kept.length),
6293
+ coverage: sectionCoverage(res),
6591
6294
  actions: [],
6592
- artifact: res ? {
6593
- analyzer: "striking-distance",
6594
- params: { type: "striking-distance" }
6595
- } : void 0
6295
+ artifact: sectionArtifact(res, "striking-distance")
6596
6296
  };
6597
6297
  }
6598
6298
  const DEFAULT_LIMIT = 40;
@@ -6758,7 +6458,7 @@ const risksReport = defineReport({
6758
6458
  }
6759
6459
  });
6760
6460
  function buildDecaySection(res, max) {
6761
- const rows = (res?.results ?? []).sort((a, b) => b.lostClicks - a.lostClicks);
6461
+ const rows = reportRows(res).sort((a, b) => b.lostClicks - a.lostClicks);
6762
6462
  const kept = rows.slice(0, max);
6763
6463
  const totalLost = kept.reduce((s, r) => s + r.lostClicks, 0);
6764
6464
  const findings = kept.map((r) => ({
@@ -6788,11 +6488,8 @@ function buildDecaySection(res, max) {
6788
6488
  magnitudeLabel: `${Math.round(totalLost)} clicks lost`
6789
6489
  },
6790
6490
  findings,
6791
- truncated: rows.length > max ? {
6792
- kept: kept.length,
6793
- total: rows.length
6794
- } : void 0,
6795
- coverage: res ? "full" : "partial",
6491
+ truncated: truncation(rows.length, kept.length),
6492
+ coverage: sectionCoverage(res),
6796
6493
  actions: kept.slice(0, 1).map((r) => ({
6797
6494
  kind: "analyzer",
6798
6495
  target: {
@@ -6802,14 +6499,11 @@ function buildDecaySection(res, max) {
6802
6499
  params: { type: "change-point" },
6803
6500
  rationale: "Investigate change-point on the worst-affected page"
6804
6501
  })),
6805
- artifact: res ? {
6806
- analyzer: "decay",
6807
- params: { type: "decay" }
6808
- } : void 0
6502
+ artifact: sectionArtifact(res, "decay")
6809
6503
  };
6810
6504
  }
6811
6505
  function buildCannibalizationSection(res, max) {
6812
- const rows = (res?.results ?? []).sort((a, b) => b.totalClicks - a.totalClicks);
6506
+ const rows = reportRows(res).sort((a, b) => b.totalClicks - a.totalClicks);
6813
6507
  const kept = rows.slice(0, max);
6814
6508
  const findings = kept.map((r) => {
6815
6509
  const pageCount = r.competitorCount ?? r.competitors?.length ?? 0;
@@ -6832,20 +6526,14 @@ function buildCannibalizationSection(res, max) {
6832
6526
  severity: kept.length ? "medium" : "info",
6833
6527
  summary: {},
6834
6528
  findings,
6835
- truncated: rows.length > max ? {
6836
- kept: kept.length,
6837
- total: rows.length
6838
- } : void 0,
6839
- coverage: res ? "full" : "partial",
6529
+ truncated: truncation(rows.length, kept.length),
6530
+ coverage: sectionCoverage(res),
6840
6531
  actions: [],
6841
- artifact: res ? {
6842
- analyzer: "cannibalization",
6843
- params: { type: "cannibalization" }
6844
- } : void 0
6532
+ artifact: sectionArtifact(res, "cannibalization")
6845
6533
  };
6846
6534
  }
6847
6535
  function buildDarkTrafficSection(res, max) {
6848
- const rows = (res?.results ?? []).sort((a, b) => b.darkClicks - a.darkClicks);
6536
+ const rows = reportRows(res).sort((a, b) => b.darkClicks - a.darkClicks);
6849
6537
  const kept = rows.slice(0, max);
6850
6538
  const totalDark = kept.reduce((s, r) => s + r.darkClicks, 0);
6851
6539
  const findings = kept.map((r) => ({
@@ -6865,20 +6553,14 @@ function buildDarkTrafficSection(res, max) {
6865
6553
  severity: kept.length ? "low" : "info",
6866
6554
  summary: { magnitudeLabel: `${Math.round(totalDark)} unattributed clicks` },
6867
6555
  findings,
6868
- truncated: rows.length > max ? {
6869
- kept: kept.length,
6870
- total: rows.length
6871
- } : void 0,
6872
- coverage: res ? "full" : "partial",
6556
+ truncated: truncation(rows.length, kept.length),
6557
+ coverage: sectionCoverage(res),
6873
6558
  actions: [],
6874
- artifact: res ? {
6875
- analyzer: "dark-traffic",
6876
- params: { type: "dark-traffic" }
6877
- } : void 0
6559
+ artifact: sectionArtifact(res, "dark-traffic")
6878
6560
  };
6879
6561
  }
6880
6562
  function buildDeviceGapSection(res, max) {
6881
- const rows = (res?.results ?? []).sort((a, b) => Math.abs(b.gaps.positionGap) - Math.abs(a.gaps.positionGap));
6563
+ const rows = reportRows(res).sort((a, b) => Math.abs(b.gaps.positionGap) - Math.abs(a.gaps.positionGap));
6882
6564
  const kept = rows.slice(0, max);
6883
6565
  return {
6884
6566
  id: "device-gap",
@@ -6898,16 +6580,10 @@ function buildDeviceGapSection(res, max) {
6898
6580
  },
6899
6581
  why: `desktop vs mobile delta`
6900
6582
  })),
6901
- truncated: rows.length > max ? {
6902
- kept: kept.length,
6903
- total: rows.length
6904
- } : void 0,
6905
- coverage: res ? "full" : "partial",
6583
+ truncated: truncation(rows.length, kept.length),
6584
+ coverage: sectionCoverage(res),
6906
6585
  actions: [],
6907
- artifact: res ? {
6908
- analyzer: "device-gap",
6909
- params: { type: "device-gap" }
6910
- } : void 0
6586
+ artifact: sectionArtifact(res, "device-gap")
6911
6587
  };
6912
6588
  }
6913
6589
  function resolveTarget(opts) {
@@ -7014,7 +6690,7 @@ const triageReport = defineReport({
7014
6690
  }
7015
6691
  });
7016
6692
  function buildChangePointSection(res, kind, matches, max) {
7017
- const rows = (res?.results ?? []).filter((r) => matches(kind === "page" ? r.page : r.keyword)).sort((a, b) => b.llr - a.llr);
6693
+ const rows = reportRows(res).filter((r) => matches(kind === "page" ? r.page : r.keyword)).sort((a, b) => b.llr - a.llr);
7018
6694
  const kept = rows.slice(0, max);
7019
6695
  const findings = kept.map((r) => ({
7020
6696
  entity: {
@@ -7033,20 +6709,14 @@ function buildChangePointSection(res, kind, matches, max) {
7033
6709
  severity: kept.length ? "medium" : "info",
7034
6710
  summary: { magnitudeLabel: `${kept.length} change-point${kept.length === 1 ? "" : "s"}` },
7035
6711
  findings,
7036
- truncated: rows.length > max ? {
7037
- kept: kept.length,
7038
- total: rows.length
7039
- } : void 0,
7040
- coverage: res ? "full" : "partial",
6712
+ truncated: truncation(rows.length, kept.length),
6713
+ coverage: sectionCoverage(res),
7041
6714
  actions: [],
7042
- artifact: res ? {
7043
- analyzer: "change-point",
7044
- params: { type: "change-point" }
7045
- } : void 0
6715
+ artifact: sectionArtifact(res, "change-point")
7046
6716
  };
7047
6717
  }
7048
6718
  function buildMigrationSection(res, kind, matches, max, _rawTarget) {
7049
- const rows = (res?.results ?? []).filter((r) => kind === "page" ? matches(r.sourcePage) || matches(r.targetPage) : false).sort((a, b) => b.weight - a.weight);
6719
+ const rows = reportRows(res).filter((r) => kind === "page" ? matches(r.sourcePage) || matches(r.targetPage) : false).sort((a, b) => b.weight - a.weight);
7050
6720
  const kept = rows.slice(0, max);
7051
6721
  const findings = kept.map((r) => ({
7052
6722
  entity: {
@@ -7065,20 +6735,14 @@ function buildMigrationSection(res, kind, matches, max, _rawTarget) {
7065
6735
  severity: "info",
7066
6736
  summary: { magnitudeLabel: kind === "page" ? `${kept.length} migration edges touching target` : "N/A for query target" },
7067
6737
  findings,
7068
- truncated: rows.length > max ? {
7069
- kept: kept.length,
7070
- total: rows.length
7071
- } : void 0,
7072
- coverage: res ? "full" : "partial",
6738
+ truncated: truncation(rows.length, kept.length),
6739
+ coverage: sectionCoverage(res),
7073
6740
  actions: [],
7074
- artifact: res ? {
7075
- analyzer: "query-migration",
7076
- params: { type: "query-migration" }
7077
- } : void 0
6741
+ artifact: sectionArtifact(res, "query-migration")
7078
6742
  };
7079
6743
  }
7080
6744
  function buildVolatilitySection(res, kind, matches, max) {
7081
- const rows = kind === "page" ? (res?.results ?? []).filter((r) => matches(r.page)) : [];
6745
+ const rows = kind === "page" ? reportRows(res).filter((r) => matches(r.page)) : [];
7082
6746
  const kept = rows.sort((a, b) => b.peakVolatility - a.peakVolatility).slice(0, max);
7083
6747
  const findings = kept.map((r) => ({
7084
6748
  entity: {
@@ -7097,16 +6761,10 @@ function buildVolatilitySection(res, kind, matches, max) {
7097
6761
  severity: kept.length ? "low" : "info",
7098
6762
  summary: { magnitudeLabel: kind === "page" ? `${kept.length} volatile day${kept.length === 1 ? "" : "s"}` : "N/A for query target" },
7099
6763
  findings,
7100
- truncated: rows.length > max ? {
7101
- kept: kept.length,
7102
- total: rows.length
7103
- } : void 0,
7104
- coverage: res ? "full" : "partial",
6764
+ truncated: truncation(rows.length, kept.length),
6765
+ coverage: sectionCoverage(res),
7105
6766
  actions: [],
7106
- artifact: res ? {
7107
- analyzer: "position-volatility",
7108
- params: { type: "position-volatility" }
7109
- } : void 0
6767
+ artifact: sectionArtifact(res, "position-volatility")
7110
6768
  };
7111
6769
  }
7112
6770
  const REPORTS = [