@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.
@@ -111,6 +111,27 @@ function requireComparisonWindowResult(report, window, message) {
111
111
  function requireComparisonWindow(report, window, message) {
112
112
  return unwrapResult(requireComparisonWindowResult(report, window, message), analysisErrorToException);
113
113
  }
114
+ function reportRows(result) {
115
+ return result?.results ?? [];
116
+ }
117
+ function sectionCoverage(result) {
118
+ return result ? "full" : "partial";
119
+ }
120
+ function sectionArtifact(result, analyzer, params = {}) {
121
+ return result ? {
122
+ analyzer,
123
+ params: {
124
+ type: analyzer,
125
+ ...params
126
+ }
127
+ } : void 0;
128
+ }
129
+ function truncation(total, kept) {
130
+ return total > kept ? {
131
+ kept,
132
+ total
133
+ } : void 0;
134
+ }
114
135
  const DEFAULT_MAX$7 = 5;
115
136
  const brandReport = defineReport({
116
137
  id: "brand",
@@ -160,7 +181,7 @@ const brandReport = defineReport({
160
181
  }
161
182
  });
162
183
  function buildBrandSplitSection(res, max) {
163
- const rows = res?.results ?? [];
184
+ const rows = reportRows(res);
164
185
  const summary = res?.meta?.summary;
165
186
  const findings = rows.filter((r) => r.segment === "brand").sort((a, b) => b.clicks - a.clicks).slice(0, max).map((r) => ({
166
187
  entity: {
@@ -182,16 +203,13 @@ function buildBrandSplitSection(res, max) {
182
203
  severity: "info",
183
204
  summary: { magnitudeLabel: summary ? `brand share ${(brandShare * 100).toFixed(1)}% (${summary.brandClicks} brand vs ${summary.nonBrandClicks} non-brand clicks)` : "no summary available" },
184
205
  findings,
185
- coverage: res ? "full" : "partial",
206
+ coverage: sectionCoverage(res),
186
207
  actions: [],
187
- artifact: res ? {
188
- analyzer: "brand",
189
- params: { type: "brand" }
190
- } : void 0
208
+ artifact: sectionArtifact(res, "brand")
191
209
  };
192
210
  }
193
211
  function buildConcentrationSection(res, max) {
194
- const head = (res?.results ?? [])[0];
212
+ const head = reportRows(res)[0];
195
213
  const findings = (head?.topNItems ?? []).slice(0, max).map((it) => ({
196
214
  entity: {
197
215
  kind: "query",
@@ -209,15 +227,9 @@ function buildConcentrationSection(res, max) {
209
227
  severity: head ? severity : "info",
210
228
  summary: head ? { magnitudeLabel: `HHI ${head.hhi.toFixed(0)} (${head.riskLevel}); top-N share ${(head.topNConcentration * 100).toFixed(1)}%` } : {},
211
229
  findings,
212
- coverage: res ? "full" : "partial",
230
+ coverage: sectionCoverage(res),
213
231
  actions: [],
214
- artifact: res ? {
215
- analyzer: "concentration",
216
- params: {
217
- type: "concentration",
218
- dimension: "keywords"
219
- }
220
- } : void 0
232
+ artifact: sectionArtifact(res, "concentration", { dimension: "keywords" })
221
233
  };
222
234
  }
223
235
  const DEFAULT_MAX$6 = 5;
@@ -283,7 +295,7 @@ const growthReport = defineReport({
283
295
  }
284
296
  });
285
297
  function buildContentVelocity(res) {
286
- const rows = res?.results ?? [];
298
+ const rows = reportRows(res);
287
299
  const totalNew = rows.reduce((s, r) => s + r.newKeywords, 0);
288
300
  const avgPerWeek = rows.length > 0 ? totalNew / rows.length : 0;
289
301
  return {
@@ -292,16 +304,13 @@ function buildContentVelocity(res) {
292
304
  severity: "info",
293
305
  summary: { magnitudeLabel: `${totalNew} new keywords across ${rows.length} weeks (avg ${avgPerWeek.toFixed(1)}/wk)` },
294
306
  findings: [],
295
- coverage: res ? "full" : "partial",
307
+ coverage: sectionCoverage(res),
296
308
  actions: [],
297
- artifact: res ? {
298
- analyzer: "content-velocity",
299
- params: { type: "content-velocity" }
300
- } : void 0
309
+ artifact: sectionArtifact(res, "content-velocity")
301
310
  };
302
311
  }
303
312
  function buildKeywordBreadth(res) {
304
- const rows = res?.results ?? [];
313
+ const rows = reportRows(res);
305
314
  const totalPages = rows.reduce((s, r) => s + r.pageCount, 0);
306
315
  const top = rows[0];
307
316
  return {
@@ -310,32 +319,26 @@ function buildKeywordBreadth(res) {
310
319
  severity: "info",
311
320
  summary: { magnitudeLabel: top ? `${totalPages} pages; modal bucket "${top.bucket}" (${top.pageCount} pages)` : "no data" },
312
321
  findings: [],
313
- coverage: res ? "full" : "partial",
322
+ coverage: sectionCoverage(res),
314
323
  actions: [],
315
- artifact: res ? {
316
- analyzer: "keyword-breadth",
317
- params: { type: "keyword-breadth" }
318
- } : void 0
324
+ artifact: sectionArtifact(res, "keyword-breadth")
319
325
  };
320
326
  }
321
327
  function buildIntentAtlas(res) {
322
- const rows = res?.results ?? [];
328
+ const rows = reportRows(res);
323
329
  return {
324
330
  id: "intent-atlas",
325
331
  title: "Intent atlas",
326
332
  severity: "info",
327
333
  summary: { magnitudeLabel: `${rows.length} clusters covering ${rows.reduce((s, r) => s + r.keywordCount, 0)} keywords` },
328
334
  findings: [],
329
- coverage: res ? "full" : "partial",
335
+ coverage: sectionCoverage(res),
330
336
  actions: [],
331
- artifact: res ? {
332
- analyzer: "intent-atlas",
333
- params: { type: "intent-atlas" }
334
- } : void 0
337
+ artifact: sectionArtifact(res, "intent-atlas")
335
338
  };
336
339
  }
337
340
  function buildLongTail(res, max) {
338
- const rows = (res?.results ?? []).sort((a, b) => {
341
+ const rows = reportRows(res).sort((a, b) => {
339
342
  const rank = {
340
343
  "head-heavy": 0,
341
344
  "balanced": 1,
@@ -364,16 +367,10 @@ function buildLongTail(res, max) {
364
367
  severity: headHeavy > rows.length / 3 ? "medium" : "info",
365
368
  summary: { magnitudeLabel: `${rows.length} pages analysed; ${headHeavy} head-heavy` },
366
369
  findings,
367
- truncated: rows.length > max ? {
368
- kept: kept.length,
369
- total: rows.length
370
- } : void 0,
371
- coverage: res ? "full" : "partial",
370
+ truncated: truncation(rows.length, kept.length),
371
+ coverage: sectionCoverage(res),
372
372
  actions: [],
373
- artifact: res ? {
374
- analyzer: "long-tail",
375
- params: { type: "long-tail" }
376
- } : void 0
373
+ artifact: sectionArtifact(res, "long-tail")
377
374
  };
378
375
  }
379
376
  const DEFAULT_MAX$5 = 5;
@@ -430,7 +427,7 @@ const healthReport = defineReport({
430
427
  }
431
428
  });
432
429
  function buildCtrAnomalySection(res, max) {
433
- const rows = (res?.results ?? []).filter((r) => r.breachDaysDown > 0).sort((a, b) => b.clicksLost - a.clicksLost);
430
+ const rows = reportRows(res).filter((r) => r.breachDaysDown > 0).sort((a, b) => b.clicksLost - a.clicksLost);
434
431
  const kept = rows.slice(0, max);
435
432
  const totalLost = kept.reduce((s, r) => s + r.clicksLost, 0);
436
433
  const findings = kept.map((r) => ({
@@ -456,20 +453,14 @@ function buildCtrAnomalySection(res, max) {
456
453
  magnitudeLabel: `${Math.round(totalLost)} clicks lost vs baseline`
457
454
  },
458
455
  findings,
459
- truncated: rows.length > max ? {
460
- kept: kept.length,
461
- total: rows.length
462
- } : void 0,
463
- coverage: res ? "full" : "partial",
456
+ truncated: truncation(rows.length, kept.length),
457
+ coverage: sectionCoverage(res),
464
458
  actions: [],
465
- artifact: res ? {
466
- analyzer: "ctr-anomaly",
467
- params: { type: "ctr-anomaly" }
468
- } : void 0
459
+ artifact: sectionArtifact(res, "ctr-anomaly")
469
460
  };
470
461
  }
471
462
  function buildChangePointSection$1(res, max) {
472
- const rows = (res?.results ?? []).filter((r) => r.direction === "worsened").sort((a, b) => b.llr - a.llr);
463
+ const rows = reportRows(res).filter((r) => r.direction === "worsened").sort((a, b) => b.llr - a.llr);
473
464
  const kept = rows.slice(0, max);
474
465
  const findings = kept.map((r) => ({
475
466
  entity: {
@@ -488,20 +479,14 @@ function buildChangePointSection$1(res, max) {
488
479
  severity: kept.length ? "medium" : "info",
489
480
  summary: { magnitudeLabel: `${kept.length} worsening segments` },
490
481
  findings,
491
- truncated: rows.length > max ? {
492
- kept: kept.length,
493
- total: rows.length
494
- } : void 0,
495
- coverage: res ? "full" : "partial",
482
+ truncated: truncation(rows.length, kept.length),
483
+ coverage: sectionCoverage(res),
496
484
  actions: [],
497
- artifact: res ? {
498
- analyzer: "change-point",
499
- params: { type: "change-point" }
500
- } : void 0
485
+ artifact: sectionArtifact(res, "change-point")
501
486
  };
502
487
  }
503
488
  function buildPositionVolatilitySection(res, max) {
504
- const rows = (res?.results ?? []).sort((a, b) => b.peakVolatility - a.peakVolatility);
489
+ const rows = reportRows(res).sort((a, b) => b.peakVolatility - a.peakVolatility);
505
490
  const kept = rows.slice(0, max);
506
491
  const findings = kept.map((r) => ({
507
492
  entity: {
@@ -520,16 +505,10 @@ function buildPositionVolatilitySection(res, max) {
520
505
  severity: kept.length ? "low" : "info",
521
506
  summary: {},
522
507
  findings,
523
- truncated: rows.length > max ? {
524
- kept: kept.length,
525
- total: rows.length
526
- } : void 0,
527
- coverage: res ? "full" : "partial",
508
+ truncated: truncation(rows.length, kept.length),
509
+ coverage: sectionCoverage(res),
528
510
  actions: [],
529
- artifact: res ? {
530
- analyzer: "position-volatility",
531
- params: { type: "position-volatility" }
532
- } : void 0
511
+ artifact: sectionArtifact(res, "position-volatility")
533
512
  };
534
513
  }
535
514
  const DEFAULT_MAX$4 = 5;
@@ -605,7 +584,7 @@ const moversReport = defineReport({
605
584
  }
606
585
  });
607
586
  function buildMoversSection(res, direction, max, minChange) {
608
- 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));
587
+ 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));
609
588
  const total = rows.length;
610
589
  const kept = rows.slice(0, max);
611
590
  const findings = kept.map((r) => ({
@@ -637,21 +616,15 @@ function buildMoversSection(res, direction, max, minChange) {
637
616
  direction: totalDelta > 0 ? "up" : totalDelta < 0 ? "down" : "flat"
638
617
  },
639
618
  findings,
640
- truncated: total > max ? {
641
- kept: kept.length,
642
- total
643
- } : void 0,
644
- coverage: res ? "full" : "partial",
619
+ truncated: truncation(total, kept.length),
620
+ coverage: sectionCoverage(res),
645
621
  actions: [],
646
- artifact: res ? {
647
- analyzer: "movers",
648
- params: { type: "movers" }
649
- } : void 0
622
+ artifact: sectionArtifact(res, "movers")
650
623
  };
651
624
  }
652
625
  function buildDeclinersSection(moversRes, decayRes, max, minChange) {
653
- const decliningQueries = (moversRes?.results ?? []).filter((r) => r.direction === "declining" && Math.abs(r.clicksChange) >= minChange).slice(0, max);
654
- const lostPages = (decayRes?.results ?? []).sort((a, b) => b.lostClicks - a.lostClicks).slice(0, max);
626
+ const decliningQueries = reportRows(moversRes).filter((r) => r.direction === "declining" && Math.abs(r.clicksChange) >= minChange).slice(0, max);
627
+ const lostPages = reportRows(decayRes).sort((a, b) => b.lostClicks - a.lostClicks).slice(0, max);
655
628
  const findings = [...decliningQueries.map((r) => ({
656
629
  entity: {
657
630
  kind: "query",
@@ -710,7 +683,7 @@ function buildDeclinersSection(moversRes, decayRes, max, minChange) {
710
683
  };
711
684
  }
712
685
  function buildStrikingSection$1(res, max) {
713
- const rows = (res?.results ?? []).sort((a, b) => b.potentialClicks - a.potentialClicks);
686
+ const rows = reportRows(res).sort((a, b) => b.potentialClicks - a.potentialClicks);
714
687
  const kept = rows.slice(0, max);
715
688
  const findings = kept.map((r) => ({
716
689
  entity: {
@@ -731,16 +704,10 @@ function buildStrikingSection$1(res, max) {
731
704
  severity: "low",
732
705
  summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.potentialClicks, 0)} potential clicks` },
733
706
  findings,
734
- truncated: rows.length > max ? {
735
- kept: kept.length,
736
- total: rows.length
737
- } : void 0,
738
- coverage: res ? "full" : "partial",
707
+ truncated: truncation(rows.length, kept.length),
708
+ coverage: sectionCoverage(res),
739
709
  actions: [],
740
- artifact: res ? {
741
- analyzer: "striking-distance",
742
- params: { type: "striking-distance" }
743
- } : void 0
710
+ artifact: sectionArtifact(res, "striking-distance")
744
711
  };
745
712
  }
746
713
  const DEFAULT_MAX$3 = 5;
@@ -806,7 +773,7 @@ const opportunitiesReport = defineReport({
806
773
  }
807
774
  });
808
775
  function buildStrikingSection(res, max) {
809
- const rows = (res?.results ?? []).sort((a, b) => b.potentialClicks - a.potentialClicks);
776
+ const rows = reportRows(res).sort((a, b) => b.potentialClicks - a.potentialClicks);
810
777
  const kept = rows.slice(0, max);
811
778
  const findings = kept.map((r) => ({
812
779
  entity: {
@@ -828,20 +795,14 @@ function buildStrikingSection(res, max) {
828
795
  severity: "low",
829
796
  summary: { magnitudeLabel: `${Math.round(totalPotential)} potential clicks` },
830
797
  findings,
831
- truncated: rows.length > max ? {
832
- kept: kept.length,
833
- total: rows.length
834
- } : void 0,
835
- coverage: res ? "full" : "partial",
798
+ truncated: truncation(rows.length, kept.length),
799
+ coverage: sectionCoverage(res),
836
800
  actions: [],
837
- artifact: res ? {
838
- analyzer: "striking-distance",
839
- params: { type: "striking-distance" }
840
- } : void 0
801
+ artifact: sectionArtifact(res, "striking-distance")
841
802
  };
842
803
  }
843
804
  function buildOpportunitySection(res, max) {
844
- const rows = (res?.results ?? []).sort((a, b) => b.opportunityScore - a.opportunityScore);
805
+ const rows = reportRows(res).sort((a, b) => b.opportunityScore - a.opportunityScore);
845
806
  const kept = rows.slice(0, max);
846
807
  return {
847
808
  id: "low-ctr",
@@ -862,11 +823,8 @@ function buildOpportunitySection(res, max) {
862
823
  },
863
824
  why: r.page ? `low CTR on ${r.page}` : "low CTR vs position"
864
825
  })),
865
- truncated: rows.length > max ? {
866
- kept: kept.length,
867
- total: rows.length
868
- } : void 0,
869
- coverage: res ? "full" : "partial",
826
+ truncated: truncation(rows.length, kept.length),
827
+ coverage: sectionCoverage(res),
870
828
  actions: kept.slice(0, 1).map((r) => ({
871
829
  kind: "fix",
872
830
  target: r.page ? {
@@ -878,14 +836,11 @@ function buildOpportunitySection(res, max) {
878
836
  },
879
837
  rationale: "Rewrite title/description to lift CTR at this position"
880
838
  })),
881
- artifact: res ? {
882
- analyzer: "opportunity",
883
- params: { type: "opportunity" }
884
- } : void 0
839
+ artifact: sectionArtifact(res, "opportunity")
885
840
  };
886
841
  }
887
842
  function buildZeroClickSection(res, max) {
888
- const rows = (res?.results ?? []).sort((a, b) => b.impressions - a.impressions);
843
+ const rows = reportRows(res).sort((a, b) => b.impressions - a.impressions);
889
844
  const kept = rows.slice(0, max);
890
845
  const findings = kept.map((r) => ({
891
846
  entity: {
@@ -905,20 +860,14 @@ function buildZeroClickSection(res, max) {
905
860
  severity: "info",
906
861
  summary: { magnitudeLabel: `${kept.reduce((s, r) => s + r.impressions, 0)} impressions wasted` },
907
862
  findings,
908
- truncated: rows.length > max ? {
909
- kept: kept.length,
910
- total: rows.length
911
- } : void 0,
912
- coverage: res ? "full" : "partial",
863
+ truncated: truncation(rows.length, kept.length),
864
+ coverage: sectionCoverage(res),
913
865
  actions: [],
914
- artifact: res ? {
915
- analyzer: "zero-click",
916
- params: { type: "zero-click" }
917
- } : void 0
866
+ artifact: sectionArtifact(res, "zero-click")
918
867
  };
919
868
  }
920
869
  function buildMigrationSection$1(res, max) {
921
- const rows = (res?.results ?? []).sort((a, b) => b.weight - a.weight);
870
+ const rows = reportRows(res).sort((a, b) => b.weight - a.weight);
922
871
  const kept = rows.slice(0, max);
923
872
  return {
924
873
  id: "query-migration",
@@ -936,16 +885,10 @@ function buildMigrationSection$1(res, max) {
936
885
  },
937
886
  why: `migrating from ${r.sourcePage}`
938
887
  })),
939
- truncated: rows.length > max ? {
940
- kept: kept.length,
941
- total: rows.length
942
- } : void 0,
943
- coverage: res ? "full" : "partial",
888
+ truncated: truncation(rows.length, kept.length),
889
+ coverage: sectionCoverage(res),
944
890
  actions: [],
945
- artifact: res ? {
946
- analyzer: "query-migration",
947
- params: { type: "query-migration" }
948
- } : void 0
891
+ artifact: sectionArtifact(res, "query-migration")
949
892
  };
950
893
  }
951
894
  const DEFAULT_MAX$2 = 10;
@@ -996,7 +939,7 @@ const prePublishReport = defineReport({
996
939
  }
997
940
  });
998
941
  function buildCannibalizationSection$1(res, matches, max) {
999
- const rows = (res?.results ?? []).filter((r) => matches(r.keyword) || (r.competitors ?? []).some((p) => matches(p.url))).sort((a, b) => b.totalClicks - a.totalClicks);
942
+ const rows = reportRows(res).filter((r) => matches(r.keyword) || (r.competitors ?? []).some((p) => matches(p.url))).sort((a, b) => b.totalClicks - a.totalClicks);
1000
943
  const kept = rows.slice(0, max);
1001
944
  const findings = kept.map((r) => {
1002
945
  const pageCount = r.competitorCount ?? r.competitors?.length ?? 0;
@@ -1019,23 +962,17 @@ function buildCannibalizationSection$1(res, matches, max) {
1019
962
  severity: kept.length ? "high" : "info",
1020
963
  summary: { magnitudeLabel: kept.length ? `${kept.length} existing competition` : "no existing competition" },
1021
964
  findings,
1022
- truncated: rows.length > max ? {
1023
- kept: kept.length,
1024
- total: rows.length
1025
- } : void 0,
1026
- coverage: res ? "full" : "partial",
965
+ truncated: truncation(rows.length, kept.length),
966
+ coverage: sectionCoverage(res),
1027
967
  actions: kept.slice(0, 1).map(() => ({
1028
968
  kind: "fix",
1029
969
  rationale: "Decide before publishing: redirect existing page, target a different angle, or accept overlap."
1030
970
  })),
1031
- artifact: res ? {
1032
- analyzer: "cannibalization",
1033
- params: { type: "cannibalization" }
1034
- } : void 0
971
+ artifact: sectionArtifact(res, "cannibalization")
1035
972
  };
1036
973
  }
1037
974
  function buildStrikingPeersSection(res, matches, max, topic) {
1038
- const rows = (res?.results ?? []).filter((r) => matches(r.keyword) || matches(r.page)).sort((a, b) => b.potentialClicks - a.potentialClicks);
975
+ const rows = reportRows(res).filter((r) => matches(r.keyword) || matches(r.page)).sort((a, b) => b.potentialClicks - a.potentialClicks);
1039
976
  const kept = rows.slice(0, max);
1040
977
  const findings = kept.map((r) => ({
1041
978
  entity: {
@@ -1055,16 +992,10 @@ function buildStrikingPeersSection(res, matches, max, topic) {
1055
992
  severity: kept.length ? "low" : "info",
1056
993
  summary: { magnitudeLabel: kept.length ? `${kept.length} adjacent rankings for "${topic}"` : "no adjacent rankings" },
1057
994
  findings,
1058
- truncated: rows.length > max ? {
1059
- kept: kept.length,
1060
- total: rows.length
1061
- } : void 0,
1062
- coverage: res ? "full" : "partial",
995
+ truncated: truncation(rows.length, kept.length),
996
+ coverage: sectionCoverage(res),
1063
997
  actions: [],
1064
- artifact: res ? {
1065
- analyzer: "striking-distance",
1066
- params: { type: "striking-distance" }
1067
- } : void 0
998
+ artifact: sectionArtifact(res, "striking-distance")
1068
999
  };
1069
1000
  }
1070
1001
  function clamp01(value) {
@@ -1439,7 +1370,7 @@ const risksReport = defineReport({
1439
1370
  }
1440
1371
  });
1441
1372
  function buildDecaySection(res, max) {
1442
- const rows = (res?.results ?? []).sort((a, b) => b.lostClicks - a.lostClicks);
1373
+ const rows = reportRows(res).sort((a, b) => b.lostClicks - a.lostClicks);
1443
1374
  const kept = rows.slice(0, max);
1444
1375
  const totalLost = kept.reduce((s, r) => s + r.lostClicks, 0);
1445
1376
  const findings = kept.map((r) => ({
@@ -1469,11 +1400,8 @@ function buildDecaySection(res, max) {
1469
1400
  magnitudeLabel: `${Math.round(totalLost)} clicks lost`
1470
1401
  },
1471
1402
  findings,
1472
- truncated: rows.length > max ? {
1473
- kept: kept.length,
1474
- total: rows.length
1475
- } : void 0,
1476
- coverage: res ? "full" : "partial",
1403
+ truncated: truncation(rows.length, kept.length),
1404
+ coverage: sectionCoverage(res),
1477
1405
  actions: kept.slice(0, 1).map((r) => ({
1478
1406
  kind: "analyzer",
1479
1407
  target: {
@@ -1483,14 +1411,11 @@ function buildDecaySection(res, max) {
1483
1411
  params: { type: "change-point" },
1484
1412
  rationale: "Investigate change-point on the worst-affected page"
1485
1413
  })),
1486
- artifact: res ? {
1487
- analyzer: "decay",
1488
- params: { type: "decay" }
1489
- } : void 0
1414
+ artifact: sectionArtifact(res, "decay")
1490
1415
  };
1491
1416
  }
1492
1417
  function buildCannibalizationSection(res, max) {
1493
- const rows = (res?.results ?? []).sort((a, b) => b.totalClicks - a.totalClicks);
1418
+ const rows = reportRows(res).sort((a, b) => b.totalClicks - a.totalClicks);
1494
1419
  const kept = rows.slice(0, max);
1495
1420
  const findings = kept.map((r) => {
1496
1421
  const pageCount = r.competitorCount ?? r.competitors?.length ?? 0;
@@ -1513,20 +1438,14 @@ function buildCannibalizationSection(res, max) {
1513
1438
  severity: kept.length ? "medium" : "info",
1514
1439
  summary: {},
1515
1440
  findings,
1516
- truncated: rows.length > max ? {
1517
- kept: kept.length,
1518
- total: rows.length
1519
- } : void 0,
1520
- coverage: res ? "full" : "partial",
1441
+ truncated: truncation(rows.length, kept.length),
1442
+ coverage: sectionCoverage(res),
1521
1443
  actions: [],
1522
- artifact: res ? {
1523
- analyzer: "cannibalization",
1524
- params: { type: "cannibalization" }
1525
- } : void 0
1444
+ artifact: sectionArtifact(res, "cannibalization")
1526
1445
  };
1527
1446
  }
1528
1447
  function buildDarkTrafficSection(res, max) {
1529
- const rows = (res?.results ?? []).sort((a, b) => b.darkClicks - a.darkClicks);
1448
+ const rows = reportRows(res).sort((a, b) => b.darkClicks - a.darkClicks);
1530
1449
  const kept = rows.slice(0, max);
1531
1450
  const totalDark = kept.reduce((s, r) => s + r.darkClicks, 0);
1532
1451
  const findings = kept.map((r) => ({
@@ -1546,20 +1465,14 @@ function buildDarkTrafficSection(res, max) {
1546
1465
  severity: kept.length ? "low" : "info",
1547
1466
  summary: { magnitudeLabel: `${Math.round(totalDark)} unattributed clicks` },
1548
1467
  findings,
1549
- truncated: rows.length > max ? {
1550
- kept: kept.length,
1551
- total: rows.length
1552
- } : void 0,
1553
- coverage: res ? "full" : "partial",
1468
+ truncated: truncation(rows.length, kept.length),
1469
+ coverage: sectionCoverage(res),
1554
1470
  actions: [],
1555
- artifact: res ? {
1556
- analyzer: "dark-traffic",
1557
- params: { type: "dark-traffic" }
1558
- } : void 0
1471
+ artifact: sectionArtifact(res, "dark-traffic")
1559
1472
  };
1560
1473
  }
1561
1474
  function buildDeviceGapSection(res, max) {
1562
- const rows = (res?.results ?? []).sort((a, b) => Math.abs(b.gaps.positionGap) - Math.abs(a.gaps.positionGap));
1475
+ const rows = reportRows(res).sort((a, b) => Math.abs(b.gaps.positionGap) - Math.abs(a.gaps.positionGap));
1563
1476
  const kept = rows.slice(0, max);
1564
1477
  return {
1565
1478
  id: "device-gap",
@@ -1579,16 +1492,10 @@ function buildDeviceGapSection(res, max) {
1579
1492
  },
1580
1493
  why: `desktop vs mobile delta`
1581
1494
  })),
1582
- truncated: rows.length > max ? {
1583
- kept: kept.length,
1584
- total: rows.length
1585
- } : void 0,
1586
- coverage: res ? "full" : "partial",
1495
+ truncated: truncation(rows.length, kept.length),
1496
+ coverage: sectionCoverage(res),
1587
1497
  actions: [],
1588
- artifact: res ? {
1589
- analyzer: "device-gap",
1590
- params: { type: "device-gap" }
1591
- } : void 0
1498
+ artifact: sectionArtifact(res, "device-gap")
1592
1499
  };
1593
1500
  }
1594
1501
  function resolveTarget(opts) {
@@ -1695,7 +1602,7 @@ const triageReport = defineReport({
1695
1602
  }
1696
1603
  });
1697
1604
  function buildChangePointSection(res, kind, matches, max) {
1698
- const rows = (res?.results ?? []).filter((r) => matches(kind === "page" ? r.page : r.keyword)).sort((a, b) => b.llr - a.llr);
1605
+ const rows = reportRows(res).filter((r) => matches(kind === "page" ? r.page : r.keyword)).sort((a, b) => b.llr - a.llr);
1699
1606
  const kept = rows.slice(0, max);
1700
1607
  const findings = kept.map((r) => ({
1701
1608
  entity: {
@@ -1714,20 +1621,14 @@ function buildChangePointSection(res, kind, matches, max) {
1714
1621
  severity: kept.length ? "medium" : "info",
1715
1622
  summary: { magnitudeLabel: `${kept.length} change-point${kept.length === 1 ? "" : "s"}` },
1716
1623
  findings,
1717
- truncated: rows.length > max ? {
1718
- kept: kept.length,
1719
- total: rows.length
1720
- } : void 0,
1721
- coverage: res ? "full" : "partial",
1624
+ truncated: truncation(rows.length, kept.length),
1625
+ coverage: sectionCoverage(res),
1722
1626
  actions: [],
1723
- artifact: res ? {
1724
- analyzer: "change-point",
1725
- params: { type: "change-point" }
1726
- } : void 0
1627
+ artifact: sectionArtifact(res, "change-point")
1727
1628
  };
1728
1629
  }
1729
1630
  function buildMigrationSection(res, kind, matches, max, _rawTarget) {
1730
- const rows = (res?.results ?? []).filter((r) => kind === "page" ? matches(r.sourcePage) || matches(r.targetPage) : false).sort((a, b) => b.weight - a.weight);
1631
+ const rows = reportRows(res).filter((r) => kind === "page" ? matches(r.sourcePage) || matches(r.targetPage) : false).sort((a, b) => b.weight - a.weight);
1731
1632
  const kept = rows.slice(0, max);
1732
1633
  const findings = kept.map((r) => ({
1733
1634
  entity: {
@@ -1746,20 +1647,14 @@ function buildMigrationSection(res, kind, matches, max, _rawTarget) {
1746
1647
  severity: "info",
1747
1648
  summary: { magnitudeLabel: kind === "page" ? `${kept.length} migration edges touching target` : "N/A for query target" },
1748
1649
  findings,
1749
- truncated: rows.length > max ? {
1750
- kept: kept.length,
1751
- total: rows.length
1752
- } : void 0,
1753
- coverage: res ? "full" : "partial",
1650
+ truncated: truncation(rows.length, kept.length),
1651
+ coverage: sectionCoverage(res),
1754
1652
  actions: [],
1755
- artifact: res ? {
1756
- analyzer: "query-migration",
1757
- params: { type: "query-migration" }
1758
- } : void 0
1653
+ artifact: sectionArtifact(res, "query-migration")
1759
1654
  };
1760
1655
  }
1761
1656
  function buildVolatilitySection(res, kind, matches, max) {
1762
- const rows = kind === "page" ? (res?.results ?? []).filter((r) => matches(r.page)) : [];
1657
+ const rows = kind === "page" ? reportRows(res).filter((r) => matches(r.page)) : [];
1763
1658
  const kept = rows.sort((a, b) => b.peakVolatility - a.peakVolatility).slice(0, max);
1764
1659
  const findings = kept.map((r) => ({
1765
1660
  entity: {
@@ -1778,16 +1673,10 @@ function buildVolatilitySection(res, kind, matches, max) {
1778
1673
  severity: kept.length ? "low" : "info",
1779
1674
  summary: { magnitudeLabel: kind === "page" ? `${kept.length} volatile day${kept.length === 1 ? "" : "s"}` : "N/A for query target" },
1780
1675
  findings,
1781
- truncated: rows.length > max ? {
1782
- kept: kept.length,
1783
- total: rows.length
1784
- } : void 0,
1785
- coverage: res ? "full" : "partial",
1676
+ truncated: truncation(rows.length, kept.length),
1677
+ coverage: sectionCoverage(res),
1786
1678
  actions: [],
1787
- artifact: res ? {
1788
- analyzer: "position-volatility",
1789
- params: { type: "position-volatility" }
1790
- } : void 0
1679
+ artifact: sectionArtifact(res, "position-volatility")
1791
1680
  };
1792
1681
  }
1793
1682
  const REPORTS = [
@@ -1,4 +1,11 @@
1
1
  import { AnalysisQuerySource } from "@gscdump/engine/source";
2
+ interface ContentGapQueryCandidate {
3
+ query: string;
4
+ impressions: number;
5
+ clicks: number;
6
+ avgPosition: number;
7
+ currentUrl: string;
8
+ }
2
9
  interface ContentGapResult {
3
10
  query: string;
4
11
  impressions: number;
@@ -55,16 +62,9 @@ interface ContentGapAnalysis {
55
62
  modelId: string;
56
63
  };
57
64
  }
58
- interface QueryCandidate {
59
- query: string;
60
- impressions: number;
61
- clicks: number;
62
- avgPosition: number;
63
- currentUrl: string;
64
- }
65
65
  declare function normalizeUrl(u: string): string;
66
66
  declare function deriveUrlText(url: string): string;
67
67
  declare function cosineNormalized(a: Float32Array, b: Float32Array): number;
68
- declare function rankContentGaps(queries: QueryCandidate[], urls: string[], queryEmbeddings: Float32Array[], urlEmbeddings: Float32Array[], minDivergence: number): ContentGapResult[];
68
+ declare function rankContentGaps(queries: ContentGapQueryCandidate[], urls: string[], queryEmbeddings: Float32Array[], urlEmbeddings: Float32Array[], minDivergence: number): ContentGapResult[];
69
69
  declare function analyzeContentGap(source: AnalysisQuerySource, opts?: ContentGapOptions): Promise<ContentGapAnalysis>;
70
70
  export { type ContentGapAnalysis, type ContentGapOptions, type ContentGapProgress, type ContentGapResult, ContentGapSourceUnsupportedError, analyzeContentGap, cosineNormalized, deriveUrlText, normalizeUrl, rankContentGaps };