@gscdump/sdk 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -781,17 +781,39 @@ interface TriageEvidence {
781
781
  label: string;
782
782
  value: string;
783
783
  }
784
+ /**
785
+ * Distance-to-next-stage for one axis, so the UI never re-derives thresholds.
786
+ *
787
+ * `direction`:
788
+ * - `advance` — up-path rung (waiting → emerging → growing). `pct` = value/target.
789
+ * - `escape` — off-ramp recovery (declining/faded/decayed) or a held health gate
790
+ * (crawl_faults/quality_rejection). `pct` = inverse distance: closer to passing ⇒ higher.
791
+ * - `sustain` — already good (growing/healthy). `nextStage` null, `pct` 1, framed as momentum.
792
+ *
793
+ * `gapLabel` is always a concrete count (pages/clicks/points), never a bare %.
794
+ */
795
+ interface StageProgression {
796
+ nextStage: ReachStage | HealthStage | null;
797
+ metric: string;
798
+ value: number;
799
+ target: number;
800
+ pct: number;
801
+ gapLabel: string;
802
+ direction: 'advance' | 'escape' | 'sustain';
803
+ }
784
804
  interface ReachVerdict {
785
805
  stage: ReachStage;
786
806
  summary: string;
787
807
  primaryAction: string;
788
808
  evidence: TriageEvidence[];
809
+ progression: StageProgression;
789
810
  }
790
811
  interface HealthVerdict {
791
812
  stage: HealthStage;
792
813
  summary: string;
793
814
  primaryAction: string;
794
815
  evidence: TriageEvidence[];
816
+ progression: StageProgression;
795
817
  }
796
818
  interface SiteTriage {
797
819
  reach: ReachVerdict;
package/dist/index.mjs CHANGED
@@ -1911,6 +1911,57 @@ function issueCount(issues, ...types) {
1911
1911
  function fmt(n) {
1912
1912
  return new Intl.NumberFormat("en").format(Math.max(0, Math.round(n)));
1913
1913
  }
1914
+ function clamp01(n) {
1915
+ if (!Number.isFinite(n)) return 0;
1916
+ return Math.min(1, Math.max(0, n));
1917
+ }
1918
+ function pctStr(n) {
1919
+ return `${n >= 0 ? "+" : ""}${Math.round(n)}%`;
1920
+ }
1921
+ function advance(nextStage, metric, value, target, gapLabel) {
1922
+ return {
1923
+ nextStage,
1924
+ metric,
1925
+ value,
1926
+ target,
1927
+ pct: clamp01(value / target),
1928
+ gapLabel,
1929
+ direction: "advance"
1930
+ };
1931
+ }
1932
+ function escapeToward(nextStage, metric, value, target, gapLabel) {
1933
+ return {
1934
+ nextStage,
1935
+ metric,
1936
+ value,
1937
+ target,
1938
+ pct: clamp01(value / target),
1939
+ gapLabel,
1940
+ direction: "escape"
1941
+ };
1942
+ }
1943
+ function escapeReduce(nextStage, metric, value, target, gapLabel) {
1944
+ return {
1945
+ nextStage,
1946
+ metric,
1947
+ value,
1948
+ target,
1949
+ pct: target <= 0 ? value <= 0 ? 1 : 0 : clamp01(2 - value / target),
1950
+ gapLabel,
1951
+ direction: "escape"
1952
+ };
1953
+ }
1954
+ function sustain(metric, value, gapLabel) {
1955
+ return {
1956
+ nextStage: null,
1957
+ metric,
1958
+ value,
1959
+ target: value,
1960
+ pct: 1,
1961
+ gapLabel,
1962
+ direction: "sustain"
1963
+ };
1964
+ }
1914
1965
  const HEALTH_COPY = {
1915
1966
  healthy: {
1916
1967
  summary: "Google can crawl and index the pages that should be indexed.",
@@ -1942,30 +1993,41 @@ function classifyHealthStage(input) {
1942
1993
  const intentionalDead = isIntentionalRetirementSite(input, notFound, serverError) ? notFound : 0;
1943
1994
  const indexableUrls = Math.max(1, totalUrls - noindex - intentionalDead);
1944
1995
  const hardBlocks = serverError + (input.crawlAuditBlockerCount ?? 0);
1945
- if (hardBlocks > Math.max(HARD_BLOCK_FLOOR, indexableUrls * HARD_BLOCK_SHARE)) return {
1946
- stage: "crawl_faults",
1947
- ...HEALTH_COPY.crawl_faults,
1948
- evidence: [{
1949
- label: "Access faults (5xx / broken)",
1950
- value: fmt(hardBlocks)
1951
- }]
1952
- };
1996
+ const faultTarget = Math.max(HARD_BLOCK_FLOOR, indexableUrls * HARD_BLOCK_SHARE);
1997
+ if (hardBlocks > faultTarget) {
1998
+ const toFix = Math.ceil(hardBlocks - faultTarget);
1999
+ return {
2000
+ stage: "crawl_faults",
2001
+ ...HEALTH_COPY.crawl_faults,
2002
+ evidence: [{
2003
+ label: "Access faults (5xx / broken)",
2004
+ value: fmt(hardBlocks)
2005
+ }],
2006
+ progression: escapeReduce("healthy", "access faults", hardBlocks, faultTarget, `${fmt(hardBlocks)} faults — fix ~${fmt(toFix)} to clear the gate`)
2007
+ };
2008
+ }
1953
2009
  const rejectPool = crawledNotIndexed + softFound;
1954
- if (totalUrls > 0 && rejectPool > REJECT_MIN && rejectPool / totalUrls >= REJECT_SHARE) return {
1955
- stage: "quality_rejection",
1956
- ...HEALTH_COPY.quality_rejection,
1957
- evidence: [{
1958
- label: "Crawled, then refused",
1959
- value: fmt(rejectPool)
1960
- }, {
1961
- label: "Share of known URLs",
1962
- value: `${(rejectPool / totalUrls * 100).toFixed(0)}%`
1963
- }]
1964
- };
2010
+ if (totalUrls > 0 && rejectPool > REJECT_MIN && rejectPool / totalUrls >= REJECT_SHARE) {
2011
+ const share = rejectPool / totalUrls;
2012
+ const toClear = Math.max(0, Math.ceil(rejectPool - REJECT_SHARE * totalUrls));
2013
+ return {
2014
+ stage: "quality_rejection",
2015
+ ...HEALTH_COPY.quality_rejection,
2016
+ evidence: [{
2017
+ label: "Crawled, then refused",
2018
+ value: fmt(rejectPool)
2019
+ }, {
2020
+ label: "Share of known URLs",
2021
+ value: `${(share * 100).toFixed(0)}%`
2022
+ }],
2023
+ progression: escapeReduce("healthy", "crawled-rejected share", share, REJECT_SHARE, `${(share * 100).toFixed(0)}% rejected — improve/consolidate ~${fmt(toClear)} pages to clear`)
2024
+ };
2025
+ }
1965
2026
  return {
1966
2027
  stage: "healthy",
1967
2028
  ...HEALTH_COPY.healthy,
1968
- evidence: []
2029
+ evidence: [],
2030
+ progression: sustain("indexing health", 1, "Indexable pages are getting indexed — no gate blocking reach.")
1969
2031
  };
1970
2032
  }
1971
2033
  const REACH_COPY = {
@@ -1998,11 +2060,12 @@ const REACH_COPY = {
1998
2060
  primaryAction: "Refresh the decayed top pages, or mark the site low-priority if it is no longer maintained."
1999
2061
  }
2000
2062
  };
2001
- function reach(stage, evidence) {
2063
+ function reach(stage, evidence, progression) {
2002
2064
  return {
2003
2065
  stage,
2004
2066
  ...REACH_COPY[stage],
2005
- evidence
2067
+ evidence,
2068
+ progression
2006
2069
  };
2007
2070
  }
2008
2071
  function classifyReachStage(input) {
@@ -2018,27 +2081,39 @@ function classifyReachStage(input) {
2018
2081
  if (imp12m != null && imp12m < LIFETIME_FLOOR) return reach("waiting_for_data", [{
2019
2082
  label: "Impressions (12m)",
2020
2083
  value: fmt(imp12m)
2021
- }]);
2084
+ }], advance("emerging", "lifetime impressions", imp12m, LIFETIME_FLOOR, `${fmt(imp12m)} of ${fmt(LIFETIME_FLOOR)} lifetime impressions — collecting data, keep indexing`));
2022
2085
  const hadRealReach = (imp12m ?? imp28d) > LIFETIME_FLOOR;
2023
2086
  const isGrowing = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT || imp90dPct != null && imp90dPct > GROWTH_IMPRESSIONS_PCT && posDelta != null && posDelta < 0;
2024
2087
  if (hadRealReach && liveness != null && liveness < FADED_LIVENESS && !isGrowing) return reach("faded", [{
2025
2088
  label: "Recent week vs peak",
2026
2089
  value: `${(liveness * 100).toFixed(0)}%`
2027
- }]);
2090
+ }], escapeToward("growing", "recent week vs peak (liveness)", liveness, .5, `recent week is ${(liveness * 100).toFixed(0)}% of peak — revive toward ~50%`));
2028
2091
  if (clicks90dPct != null && clicks90dPct < DECLINE_CLICKS_PCT && clicks28dPct != null && clicks28dPct < DECLINE_CLICKS_PCT && priorClicks != null && priorClicks >= MIN_DECLINE_PRIOR_CLICKS) return reach("declining", [{
2029
2092
  label: "Clicks 90d",
2030
- value: `${clicks90dPct.toFixed(0)}%`
2093
+ value: pctStr(clicks90dPct)
2031
2094
  }, {
2032
2095
  label: "Clicks 28d",
2033
- value: `${clicks28dPct.toFixed(0)}%`
2034
- }]);
2035
- if (isGrowing) return reach("growing", [...clicks90dPct != null ? [{
2036
- label: "Clicks 90d",
2037
- value: `+${clicks90dPct.toFixed(0)}%`
2038
- }] : [], ...imp90dPct != null ? [{
2039
- label: "Impressions 90d",
2040
- value: `+${imp90dPct.toFixed(0)}%`
2041
- }] : []]);
2096
+ value: pctStr(clicks28dPct)
2097
+ }], {
2098
+ nextStage: "plateaued",
2099
+ metric: "90d clicks change",
2100
+ value: clicks90dPct,
2101
+ target: DECLINE_CLICKS_PCT,
2102
+ pct: clamp01(2 - Math.abs(clicks90dPct) / Math.abs(DECLINE_CLICKS_PCT)),
2103
+ gapLabel: `down ${pctStr(clicks90dPct)} (90d) — recover clicks above ${pctStr(DECLINE_CLICKS_PCT)} to exit`,
2104
+ direction: "escape"
2105
+ });
2106
+ if (isGrowing) {
2107
+ const growthPct = clicks90dPct ?? imp90dPct ?? 0;
2108
+ const growthLabel = clicks90dPct != null ? `${pctStr(clicks90dPct)} 90d clicks — comfortably growing; defend & expand` : `${pctStr(growthPct)} 90d impressions — comfortably growing; defend & expand`;
2109
+ return reach("growing", [...clicks90dPct != null ? [{
2110
+ label: "Clicks 90d",
2111
+ value: `+${clicks90dPct.toFixed(0)}%`
2112
+ }] : [], ...imp90dPct != null ? [{
2113
+ label: "Impressions 90d",
2114
+ value: `+${imp90dPct.toFixed(0)}%`
2115
+ }] : []], sustain("90d clicks growth", growthPct, growthLabel));
2116
+ }
2042
2117
  const ctr = clicks28d != null && imp28d > 0 ? clicks28d / imp28d : null;
2043
2118
  if (hadRealReach && imp28d >= NASCENT_IMPRESSIONS_28D && ctr != null && ctr < DECAY_CTR) return reach("decayed", [{
2044
2119
  label: "Impressions (28d)",
@@ -2046,19 +2121,22 @@ function classifyReachStage(input) {
2046
2121
  }, {
2047
2122
  label: "Clicks (28d)",
2048
2123
  value: fmt(clicks28d ?? 0)
2049
- }]);
2124
+ }], escapeToward("growing", "CTR (clicks/impressions)", ctr, DECAY_CTR, `${(ctr * 100).toFixed(2)}% CTR on ${fmt(imp28d)} impressions — refresh content toward ~${(DECAY_CTR * 100).toFixed(1)}%`));
2125
+ const growthVal = clicks90dPct ?? 0;
2126
+ const growthGap = Math.max(0, GROWTH_CLICKS_PCT - growthVal);
2127
+ const growthProgression = (stage) => advance("growing", "90d clicks growth", growthVal, GROWTH_CLICKS_PCT, clicks90dPct != null ? `${pctStr(growthVal)} 90d clicks — need ${pctStr(growthGap)} more to clear the +${GROWTH_CLICKS_PCT}% growth bar` : `${stage === "plateaued" ? "flat" : "rising"} — reach +${GROWTH_CLICKS_PCT}% 90d clicks growth to break into growing`);
2050
2128
  if (imp28d >= ESTABLISHED_IMPRESSIONS_28D) return reach("plateaued", [{
2051
2129
  label: "Impressions (28d)",
2052
2130
  value: fmt(imp28d)
2053
- }]);
2131
+ }], growthProgression("plateaued"));
2054
2132
  if (imp28d < NASCENT_IMPRESSIONS_28D) return reach("emerging", [{
2055
2133
  label: "Impressions (28d)",
2056
2134
  value: fmt(imp28d)
2057
- }]);
2135
+ }], growthProgression("emerging"));
2058
2136
  return reach("plateaued", [{
2059
2137
  label: "Impressions (28d)",
2060
2138
  value: fmt(imp28d)
2061
- }]);
2139
+ }], growthProgression("plateaued"));
2062
2140
  }
2063
2141
  function classifySiteTriage(input) {
2064
2142
  const health = classifyHealthStage(input);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/sdk",
3
3
  "type": "module",
4
- "version": "0.24.0",
4
+ "version": "0.25.0",
5
5
  "description": "Consumer SDK for hosted gscdump.com integrations.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -41,7 +41,7 @@
41
41
  "node": ">=18"
42
42
  },
43
43
  "peerDependencies": {
44
- "gscdump": "0.24.0"
44
+ "gscdump": "0.25.0"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "gscdump": {
@@ -52,9 +52,9 @@
52
52
  "date-fns": "^4.3.0",
53
53
  "ofetch": "^1.5.1",
54
54
  "zod": "^4.4.3",
55
- "@gscdump/analysis": "0.24.0",
56
- "@gscdump/engine": "0.24.0",
57
- "@gscdump/contracts": "0.24.0"
55
+ "@gscdump/contracts": "0.25.0",
56
+ "@gscdump/analysis": "0.25.0",
57
+ "@gscdump/engine": "0.25.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "typescript": "^6.0.3",