@gscdump/sdk 0.23.4 → 0.24.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
@@ -636,6 +636,27 @@ interface SearchConsoleStageSummary {
636
636
  change7d?: number | null;
637
637
  change28d?: number | null;
638
638
  }
639
+ /**
640
+ * Trajectory + maturity signals (v2). These are first-class axes: a site that
641
+ * is growing over the robust 90-day window is told to keep expanding, never to
642
+ * "fix indexing", regardless of coverage%. Percent fields are whole numbers
643
+ * (e.g. 42.6 for +42.6%); `positionDelta90d` is current − prior (negative =
644
+ * rank improved). Window contract: callers MUST drop the trailing ~3 GSC lag
645
+ * days before computing these, and the 7-day window is intentionally absent —
646
+ * it is too lag-contaminated to classify on.
647
+ */
648
+ interface SearchConsoleStageTrajectory {
649
+ clicksPct90d?: number | null;
650
+ impressionsPct90d?: number | null;
651
+ positionDelta90d?: number | null;
652
+ clicksPct28d?: number | null;
653
+ /**
654
+ * Absolute clicks in the PRIOR 28-day window — the baseline a decline would
655
+ * be measured against. Gates decline detection so a percentage crash on
656
+ * trivial traffic (8 → 2 clicks) is not mistaken for a real loss.
657
+ */
658
+ clicksPrior28d?: number | null;
659
+ }
639
660
  interface SearchConsoleStageSitemap {
640
661
  errors?: number | null;
641
662
  warnings?: number | null;
@@ -657,8 +678,165 @@ interface ClassifySearchConsoleStageInput {
657
678
  pageInventory?: SearchConsoleStagePage[] | null;
658
679
  ctrOutlierCount?: number | null;
659
680
  pageMoverDropCount?: number | null;
681
+ /** v2 trajectory axis — when present, drives the growth override + decline detection. */
682
+ trajectory?: SearchConsoleStageTrajectory | null;
683
+ /** v2 maturity axis — impressions over the trailing 28 days. Gates whether coverage% is even meaningful. */
684
+ impressions28d?: number | null;
685
+ /**
686
+ * v2 on-page technical faults from the crawl audit (broken links/images,
687
+ * server errors, access failures) — counted as hard blockers alongside GSC
688
+ * crawl reasons. Excludes intentional noindex.
689
+ */
690
+ crawlAuditBlockerCount?: number | null;
691
+ /** v2 authority signal — open recoverable broken backlinks (expansion lever, not a defect). */
692
+ recoverableBacklinkCount?: number | null;
693
+ /** v2 authority signal — cross-competitor content-gap topics (expansion readiness). */
694
+ competitorGapCount?: number | null;
695
+ /**
696
+ * v2 site purpose (AI profile `type`). Benchmarks the verdict against intent:
697
+ * informational types (docs/blog/portfolio) earn structurally low CTR, so the
698
+ * visible-not-clicked bar is raised for them. Unknown/null → `other`.
699
+ */
700
+ siteType?: string | null;
660
701
  }
702
+ /**
703
+ * v2 classifier. Trajectory and maturity are first-class axes that run BEFORE
704
+ * the coverage/discovery rungs, so a growing site is told to keep expanding —
705
+ * never to "fix indexing". Reuses the existing stage-key enum (growth →
706
+ * `healthy_growth_ready`, nascent → `waiting_for_data`, mass crawled-not-indexed
707
+ * → `index_rejection`, on-page/crawl faults → `crawl_blocked`).
708
+ */
661
709
  declare function classifySearchConsoleStage(input: ClassifySearchConsoleStageInput): SearchConsoleStage;
710
+ type SiteType = 'saas' | 'ecommerce' | 'docs' | 'blog' | 'agency' | 'portfolio' | 'other';
711
+ /**
712
+ * Normalise the AI profile `type` to the closed enum. The categoriser mostly
713
+ * emits the 7 values but occasionally leaks free-text (e.g. "event") or null
714
+ * (~40% of live sites are unprofiled) — everything unknown collapses to `other`
715
+ * so downstream logic always has a defined bucket.
716
+ */
717
+ declare function normalizeSiteType(raw: string | null | undefined): SiteType;
718
+ interface SiteTypeBaseline {
719
+ label: string;
720
+ /**
721
+ * Whether indexed-coverage% is a meaningful health signal for this type.
722
+ * docs/blog/portfolio accumulate intentional low-value pages (tags, versions,
723
+ * pagination, archives) so coverage% is noise; ecommerce/saas/agency care.
724
+ */
725
+ coverageMatters: boolean;
726
+ /**
727
+ * Typical click-through at good positions. Informational types (docs/blog)
728
+ * earn structurally lower CTR (answer shown in SERP, multi-page research), so
729
+ * a low CTR is NOT a defect — the classifier raises the visible-not-clicked
730
+ * bar for these.
731
+ */
732
+ ctrExpectation: 'low' | 'medium' | 'high';
733
+ /** The default growth lever when the site is healthy and leading. */
734
+ primaryGoal: 'content' | 'authority' | 'conversion' | 'coverage';
735
+ }
736
+ declare const SITE_TYPE_BASELINE: Record<SiteType, SiteTypeBaseline>;
737
+ declare function siteTypeBaseline(raw: string | null | undefined): SiteTypeBaseline;
738
+ type PeerStanding = 'leader' | 'on_par' | 'behind' | 'unknown';
739
+ /** How much to trust the standing: a median over 1–2 peers is noisy. */
740
+ type PeerConfidence = 'high' | 'low' | 'none';
741
+ /**
742
+ * Peer metrics from tracked competitors (`siteCompetitors`). Domain rank is the
743
+ * DataForSEO-derived 0–100 score (`min(100, log10(keywordCount)*20)`), NOT a
744
+ * true authority metric — good enough for relative standing within a peer set.
745
+ * The site's OWN metrics are not stored alongside competitors, so callers must
746
+ * supply them (one cheap cached `estimateDomainTraffic` call).
747
+ */
748
+ interface PeerBaselineInput {
749
+ siteDomainRank?: number | null;
750
+ siteOrganicTraffic?: number | null;
751
+ competitorDomainRanks?: number[] | null;
752
+ competitorOrganicTraffic?: number[] | null;
753
+ }
754
+ interface SiteBaseline {
755
+ siteType: SiteType;
756
+ baseline: SiteTypeBaseline;
757
+ peerStanding: PeerStanding;
758
+ peerConfidence: PeerConfidence;
759
+ peerMedianDomainRank: number | null;
760
+ peerMedianOrganicTraffic: number | null;
761
+ /** Human-facing goal headline combining type + standing. */
762
+ recommendedGoal: string;
763
+ goalKind: SiteTypeBaseline['primaryGoal'];
764
+ }
765
+ /** Standing is decided on domain rank first; organic traffic breaks the tie. */
766
+ declare function derivePeerStanding(input: PeerBaselineInput): {
767
+ standing: PeerStanding;
768
+ confidence: PeerConfidence;
769
+ peerMedianDomainRank: number | null;
770
+ peerMedianOrganicTraffic: number | null;
771
+ };
772
+ /**
773
+ * Combine the site type with its peer standing into a relative goal. A leader
774
+ * defends and expands on its type's lever; a site that's behind closes the gap
775
+ * on the same lever; unknown standing falls back to the type default.
776
+ */
777
+ declare function deriveSiteBaseline(rawType: string | null | undefined, peer?: PeerBaselineInput): SiteBaseline;
778
+ type ReachStage = 'waiting_for_data' | 'emerging' | 'growing' | 'plateaued' | 'declining' | 'faded' | 'decayed';
779
+ type HealthStage = 'healthy' | 'crawl_faults' | 'quality_rejection';
780
+ interface TriageEvidence {
781
+ label: string;
782
+ value: string;
783
+ }
784
+ interface ReachVerdict {
785
+ stage: ReachStage;
786
+ summary: string;
787
+ primaryAction: string;
788
+ evidence: TriageEvidence[];
789
+ }
790
+ interface HealthVerdict {
791
+ stage: HealthStage;
792
+ summary: string;
793
+ primaryAction: string;
794
+ evidence: TriageEvidence[];
795
+ }
796
+ interface SiteTriage {
797
+ reach: ReachVerdict;
798
+ health: HealthVerdict;
799
+ /** Which axis leads the dashboard headline. */
800
+ headline: 'reach' | 'health';
801
+ }
802
+ interface SiteTriageInput {
803
+ connected: boolean;
804
+ /** Impressions over the trailing 28 days — the maturity tier. */
805
+ impressions28d?: number | null;
806
+ /** Lifetime-ish impressions (trailing 12 months) — separates new from decayed/faded. */
807
+ impressions12m?: number | null;
808
+ /** Absolute clicks over the trailing 28 days — for the clicks≪impressions decay tell. */
809
+ clicks28d?: number | null;
810
+ clicksPct90d?: number | null;
811
+ clicksPct28d?: number | null;
812
+ /** Absolute prior-28d clicks — gates decline so a % crash on trivial traffic isn't a false decline. */
813
+ clicksPrior28d?: number | null;
814
+ impressionsPct90d?: number | null;
815
+ positionDelta90d?: number | null;
816
+ /** Latest complete week ÷ 90d peak week (lag-trimmed). <0.2 = faded (spike→died). */
817
+ livenessRatio?: number | null;
818
+ totalUrls?: number | null;
819
+ indexed?: number | null;
820
+ issues?: SearchConsoleStageIssue[] | null;
821
+ /** Real on-page faults from the crawl audit: 5xx, broken internal links/images. Excludes intentional noindex/404. */
822
+ crawlAuditBlockerCount?: number | null;
823
+ /** AI profile type — drives purpose-expected subtraction. */
824
+ siteType?: string | null;
825
+ }
826
+ /**
827
+ * Reach liveness: latest complete week ÷ peak rolling-7d week over a daily
828
+ * impressions series (typically the trailing 90 days). `<0.2` means recent
829
+ * impressions have collapsed versus the site's own peak (spike→died) — the
830
+ * signal a period-vs-prior delta cannot see. Trailing zero-impression days
831
+ * (GSC reporting lag) are trimmed before the latest-week sum. Returns null when
832
+ * the series is too short to judge.
833
+ */
834
+ declare function reachLivenessRatio(daily: Array<{
835
+ impressions: number;
836
+ }> | null | undefined): number | null;
837
+ declare function classifyHealthStage(input: SiteTriageInput): HealthVerdict;
838
+ declare function classifyReachStage(input: SiteTriageInput): ReachVerdict;
839
+ declare function classifySiteTriage(input: SiteTriageInput): SiteTriage;
662
840
  declare function serializeWebhookPayload(payload: string | object): string;
663
841
  declare function verifyWebhookSignature(payload: string | object, signature: string | null | undefined, secret: string): Promise<boolean>;
664
842
  declare function parseWebhookPayload<TData extends Record<string, unknown> = Record<string, unknown>>(payload: string | object, options?: {
@@ -668,4 +846,4 @@ declare function parseWebhookPayload<TData extends Record<string, unknown> = Rec
668
846
  validateSignature?: boolean;
669
847
  }): Promise<WebhookEnvelope$1<TData>>;
670
848
  declare function readWebhookHeaders(headers: Headers | PartnerWebhookHeaders$1 | null | undefined): Required<PartnerWebhookHeaders$1>;
671
- export { ARCHETYPE_EXECUTION_CLASS, type AddPartnerTeamMemberParams, type AnalyticsClient, type AnalyticsClientOptions, type AnalyticsFetch, type AnalyticsFetchOptions, type AnalyticsHeaders, type ArbitrarySqlQuery, type ArchetypeExecutionClass, type ArchetypeQuery, type ArchetypeQueryBase, type ArchetypeResult, type ArchetypeResultRow, type ArchetypeResultSource, type AuxCloudOnlyQuery, type BackfillRange, type BackfillResponse, type BindPartnerSiteTeamParams, type BuilderState, type BulkRegisterPartnerSiteResult, type BulkRegisterPartnerSitesParams, type BulkRegisterPartnerSitesResponse, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, type CalendarPeriod, type CanonicalDailyRow, type CanonicalWebhookEventType, type ClassifySearchConsoleStageInput, type CompareMode, type CreatePartnerTeamParams, type CreateWebhookEnvelopeOptions, type CustomPeriod, type CwvBucket, type DailyAnonInput, type DataDetailOptions, type DataQueryOptions, type DateRange, type DateRangeResult, type DeletePartnerUserResponse, type EntityDailySparklineQuery, type EntityDailyTimeseriesQuery, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, type GscAnalyzerAccent, type GscAnalyzerCapabilities, type GscAnalyzerCapability, type GscAnalyzerDefinition, type GscAnalyzerDefinitionWithCapability, type GscAnalyzerInsightCard, type GscAnalyzerKind, type GscAnalyzerPanelResult, type GscAnalyzerPanelSpec, type GscAnalyzerStatTile, type GscClassifiedError, type GscColumn, type GscColumnOption, type GscComparisonFilter, type GscConsoleUrlOpts, type GscDailySummary, type GscErrorStatus, type GscRowTotals, type GscdumpAnalysisParams, type GscdumpAnalysisPreset, type GscdumpAnalysisResponse, type GscdumpAnalysisSourcesResponse, type GscdumpAvailableSite, type GscdumpCanonicalMismatchesResponse, type GscdumpDataDetailResponse, type GscdumpDataResponse, type GscdumpDataRow, type GscdumpDateRangeParams, type GscdumpIndexPercentResponse, type GscdumpIndexingDiagnosticsResponse, type GscdumpIndexingResponse, type GscdumpIndexingUrlStatus, type GscdumpIndexingUrlsResponse, type GscdumpKeywordSparklinesParams, type GscdumpKeywordSparklinesResponse, type GscdumpMeta, type GscdumpPermissionRecovery, type GscdumpQueryTrendParams, type GscdumpQueryTrendResponse, type GscdumpSiteRegistration, type GscdumpSitemap, type GscdumpSitemapChangesResponse, type GscdumpSitemapHistory, type GscdumpSitemapsResponse, type GscdumpSyncStatusResponse, type GscdumpTeamMemberRow, type GscdumpTeamRow, type GscdumpTopAssociationParams, type GscdumpTopAssociationResponse, type GscdumpTotals, type GscdumpUserRegistration, type GscdumpUserSettings, type GscdumpUserSite, type GscdumpUserStatus, type GscdumpUserTokenUpdate, type IndexingIssue, type IndexingIssueDetail, type IndexingUrlsParams, type InspectionHistoryResponse, type InspectionIndex, type IssueGroup, type IssueSeverity, type MultiSeriesStackedDailyQuery, PERIOD_PRESETS, PartnerApiError, type PartnerClient, type PartnerClientOptions, type PartnerErrorInfo, type PartnerErrorKind, type PartnerFetch, type PartnerFetchOptions, type PartnerHeaders, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, type PartnerRealtimeClient, type PartnerRealtimeEvent, type PartnerRealtimeEventType, type PartnerRealtimeHandler, type PartnerRealtimeMessage, type PartnerRealtimeOptions, type PartnerRealtimeScope, type PartnerRealtimeStatus, type PartnerWebSocketConstructor, type PartnerWebSocketLike, type PartnerWebhookData, type PartnerWebhookHeaders, type Period, type PeriodPreset, type PresetAnalyzerQuery, type QueryArchetype, type RawDailyRow, type RealtimeAuthFailedEvent, type RealtimeAuthRequiredMessage, type RealtimeConnectedMessage, type RealtimeEnrichmentCompleteEvent, type RealtimeErrorMessage, type RealtimeJobFailedEvent, type RealtimeNeedsReauthEvent, type RealtimePongMessage, type RealtimeSiteAddedEvent, type RealtimeSiteRemovedEvent, type RealtimeSubscribedMessage, type RealtimeSyncCompleteEvent, type RealtimeSyncFailedEvent, type RealtimeSyncJobCompleteEvent, type RealtimeSyncProgressEvent, type RealtimeSyncSiteCompleteEvent, type RegisterPartnerSiteParams, type RegisterPartnerUserParams, type ResolvedArchetypeQuery, type RollingPeriod, type RollupEnvelope, type SearchConsoleStage, type SearchConsoleStageEvidence, type SearchConsoleStageIssue, type SearchConsoleStageKey, type SearchConsoleStagePage, type SearchConsoleStageSeverity, type SearchConsoleStageSitemap, type SearchConsoleStageSummary, type SingleRowLookupQuery, type SiteDailyTimeseriesQuery, type TopNBreakdownQuery, type TwoDimensionDetailQuery, type UpdatePartnerUserTokensParams, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, type WebhookEnvelope, type WebhookEventType, type WhoamiResponse, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifySearchConsoleStage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
849
+ export { ARCHETYPE_EXECUTION_CLASS, type AddPartnerTeamMemberParams, type AnalyticsClient, type AnalyticsClientOptions, type AnalyticsFetch, type AnalyticsFetchOptions, type AnalyticsHeaders, type ArbitrarySqlQuery, type ArchetypeExecutionClass, type ArchetypeQuery, type ArchetypeQueryBase, type ArchetypeResult, type ArchetypeResultRow, type ArchetypeResultSource, type AuxCloudOnlyQuery, type BackfillRange, type BackfillResponse, type BindPartnerSiteTeamParams, type BuilderState, type BulkRegisterPartnerSiteResult, type BulkRegisterPartnerSitesParams, type BulkRegisterPartnerSitesResponse, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, type CalendarPeriod, type CanonicalDailyRow, type CanonicalWebhookEventType, type ClassifySearchConsoleStageInput, type CompareMode, type CreatePartnerTeamParams, type CreateWebhookEnvelopeOptions, type CustomPeriod, type CwvBucket, type DailyAnonInput, type DataDetailOptions, type DataQueryOptions, type DateRange, type DateRangeResult, type DeletePartnerUserResponse, type EntityDailySparklineQuery, type EntityDailyTimeseriesQuery, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, type GscAnalyzerAccent, type GscAnalyzerCapabilities, type GscAnalyzerCapability, type GscAnalyzerDefinition, type GscAnalyzerDefinitionWithCapability, type GscAnalyzerInsightCard, type GscAnalyzerKind, type GscAnalyzerPanelResult, type GscAnalyzerPanelSpec, type GscAnalyzerStatTile, type GscClassifiedError, type GscColumn, type GscColumnOption, type GscComparisonFilter, type GscConsoleUrlOpts, type GscDailySummary, type GscErrorStatus, type GscRowTotals, type GscdumpAnalysisParams, type GscdumpAnalysisPreset, type GscdumpAnalysisResponse, type GscdumpAnalysisSourcesResponse, type GscdumpAvailableSite, type GscdumpCanonicalMismatchesResponse, type GscdumpDataDetailResponse, type GscdumpDataResponse, type GscdumpDataRow, type GscdumpDateRangeParams, type GscdumpIndexPercentResponse, type GscdumpIndexingDiagnosticsResponse, type GscdumpIndexingResponse, type GscdumpIndexingUrlStatus, type GscdumpIndexingUrlsResponse, type GscdumpKeywordSparklinesParams, type GscdumpKeywordSparklinesResponse, type GscdumpMeta, type GscdumpPermissionRecovery, type GscdumpQueryTrendParams, type GscdumpQueryTrendResponse, type GscdumpSiteRegistration, type GscdumpSitemap, type GscdumpSitemapChangesResponse, type GscdumpSitemapHistory, type GscdumpSitemapsResponse, type GscdumpSyncStatusResponse, type GscdumpTeamMemberRow, type GscdumpTeamRow, type GscdumpTopAssociationParams, type GscdumpTopAssociationResponse, type GscdumpTotals, type GscdumpUserRegistration, type GscdumpUserSettings, type GscdumpUserSite, type GscdumpUserStatus, type GscdumpUserTokenUpdate, type HealthStage, type HealthVerdict, type IndexingIssue, type IndexingIssueDetail, type IndexingUrlsParams, type InspectionHistoryResponse, type InspectionIndex, type IssueGroup, type IssueSeverity, type MultiSeriesStackedDailyQuery, PERIOD_PRESETS, PartnerApiError, type PartnerClient, type PartnerClientOptions, type PartnerErrorInfo, type PartnerErrorKind, type PartnerFetch, type PartnerFetchOptions, type PartnerHeaders, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, type PartnerRealtimeClient, type PartnerRealtimeEvent, type PartnerRealtimeEventType, type PartnerRealtimeHandler, type PartnerRealtimeMessage, type PartnerRealtimeOptions, type PartnerRealtimeScope, type PartnerRealtimeStatus, type PartnerWebSocketConstructor, type PartnerWebSocketLike, type PartnerWebhookData, type PartnerWebhookHeaders, type PeerBaselineInput, type PeerConfidence, type PeerStanding, type Period, type PeriodPreset, type PresetAnalyzerQuery, type QueryArchetype, type RawDailyRow, type ReachStage, type ReachVerdict, type RealtimeAuthFailedEvent, type RealtimeAuthRequiredMessage, type RealtimeConnectedMessage, type RealtimeEnrichmentCompleteEvent, type RealtimeErrorMessage, type RealtimeJobFailedEvent, type RealtimeNeedsReauthEvent, type RealtimePongMessage, type RealtimeSiteAddedEvent, type RealtimeSiteRemovedEvent, type RealtimeSubscribedMessage, type RealtimeSyncCompleteEvent, type RealtimeSyncFailedEvent, type RealtimeSyncJobCompleteEvent, type RealtimeSyncProgressEvent, type RealtimeSyncSiteCompleteEvent, type RegisterPartnerSiteParams, type RegisterPartnerUserParams, type ResolvedArchetypeQuery, type RollingPeriod, type RollupEnvelope, SITE_TYPE_BASELINE, type SearchConsoleStage, type SearchConsoleStageEvidence, type SearchConsoleStageIssue, type SearchConsoleStageKey, type SearchConsoleStagePage, type SearchConsoleStageSeverity, type SearchConsoleStageSitemap, type SearchConsoleStageSummary, type SearchConsoleStageTrajectory, type SingleRowLookupQuery, type SiteBaseline, type SiteDailyTimeseriesQuery, type SiteTriage, type SiteTriageInput, type SiteType, type SiteTypeBaseline, type TopNBreakdownQuery, type TriageEvidence, type TwoDimensionDetailQuery, type UpdatePartnerUserTokensParams, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, type WebhookEnvelope, type WebhookEventType, type WhoamiResponse, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifyHealthStage, classifyReachStage, classifySearchConsoleStage, classifySiteTriage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, derivePeerStanding, deriveSiteBaseline, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, normalizeSiteType, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, reachLivenessRatio, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteTypeBaseline, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
package/dist/index.mjs CHANGED
@@ -414,7 +414,8 @@ function dateRangeQuery(params) {
414
414
  function queryTrendQuery(params) {
415
415
  const query = {
416
416
  startDate: params.startDate,
417
- endDate: params.endDate
417
+ endDate: params.endDate,
418
+ searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
418
419
  };
419
420
  if (params.prevStartDate) query.prevStartDate = params.prevStartDate;
420
421
  if (params.prevEndDate) query.prevEndDate = params.prevEndDate;
@@ -625,7 +626,11 @@ function createPartnerClient(options = {}) {
625
626
  return request(partnerRoutes.sites.topAssociation(siteId), { query }, partnerEndpointSchemas.getTopAssociation.response);
626
627
  },
627
628
  getKeywordSparklines(siteId, params) {
628
- const body = shouldValidate(options, "request") ? partnerEndpointSchemas.getKeywordSparklines.body.parse(params) : params;
629
+ const withSearchType = {
630
+ ...params,
631
+ searchType: params.searchType ?? DEFAULT_SEARCH_TYPE
632
+ };
633
+ const body = shouldValidate(options, "request") ? partnerEndpointSchemas.getKeywordSparklines.body.parse(withSearchType) : withSearchType;
629
634
  return request(partnerRoutes.sites.keywordSparklines(siteId), {
630
635
  method: "POST",
631
636
  body
@@ -1261,7 +1266,7 @@ const CALENDAR_TO_UPSTREAM = {
1261
1266
  "this-month": "mtd",
1262
1267
  "this-year": "ytd"
1263
1268
  };
1264
- function fmt(d) {
1269
+ function fmt$1(d) {
1265
1270
  return format(d, "yyyy-MM-dd");
1266
1271
  }
1267
1272
  function buildResultFromIso(start, end) {
@@ -1305,7 +1310,7 @@ function periodToDateRange(period, stableData = true) {
1305
1310
  }
1306
1311
  const today = todayInPST();
1307
1312
  const end = stableData ? subDays(today, 3) : subDays(today, 1);
1308
- const endIso = fmt(end);
1313
+ const endIso = fmt$1(end);
1309
1314
  const upstreamPreset = ROLLING_TO_UPSTREAM[period] ?? CALENDAR_TO_UPSTREAM[period];
1310
1315
  if (upstreamPreset) {
1311
1316
  const win = resolveWindow({
@@ -1321,14 +1326,14 @@ function periodToDateRange(period, stableData = true) {
1321
1326
  break;
1322
1327
  case "last-month": {
1323
1328
  const prevMonth = subMonths(end, 1);
1324
- return buildResultFromIso(fmt(startOfMonth(prevMonth)), fmt(endOfMonth(prevMonth)));
1329
+ return buildResultFromIso(fmt$1(startOfMonth(prevMonth)), fmt$1(endOfMonth(prevMonth)));
1325
1330
  }
1326
1331
  case "this-quarter":
1327
1332
  start = startOfQuarter(end);
1328
1333
  break;
1329
1334
  default: start = subDays(end, 27);
1330
1335
  }
1331
- return buildResultFromIso(fmt(start), endIso);
1336
+ return buildResultFromIso(fmt$1(start), endIso);
1332
1337
  }
1333
1338
  function periodToDays(period) {
1334
1339
  return periodToDateRange(period).days;
@@ -1478,10 +1483,121 @@ function createPartnerRealtimeClient(options) {
1478
1483
  };
1479
1484
  }
1480
1485
  const createGscdumpRealtimeClient = createPartnerRealtimeClient;
1486
+ const KNOWN_SITE_TYPES = new Set([
1487
+ "saas",
1488
+ "ecommerce",
1489
+ "docs",
1490
+ "blog",
1491
+ "agency",
1492
+ "portfolio",
1493
+ "other"
1494
+ ]);
1495
+ function normalizeSiteType(raw) {
1496
+ if (!raw) return "other";
1497
+ const t = raw.toLowerCase().trim();
1498
+ return KNOWN_SITE_TYPES.has(t) ? t : "other";
1499
+ }
1500
+ const SITE_TYPE_BASELINE = {
1501
+ docs: {
1502
+ label: "Documentation",
1503
+ coverageMatters: false,
1504
+ ctrExpectation: "low",
1505
+ primaryGoal: "content"
1506
+ },
1507
+ blog: {
1508
+ label: "Blog / content",
1509
+ coverageMatters: false,
1510
+ ctrExpectation: "low",
1511
+ primaryGoal: "content"
1512
+ },
1513
+ portfolio: {
1514
+ label: "Portfolio",
1515
+ coverageMatters: false,
1516
+ ctrExpectation: "low",
1517
+ primaryGoal: "content"
1518
+ },
1519
+ saas: {
1520
+ label: "SaaS",
1521
+ coverageMatters: true,
1522
+ ctrExpectation: "medium",
1523
+ primaryGoal: "authority"
1524
+ },
1525
+ agency: {
1526
+ label: "Agency",
1527
+ coverageMatters: true,
1528
+ ctrExpectation: "medium",
1529
+ primaryGoal: "authority"
1530
+ },
1531
+ ecommerce: {
1532
+ label: "Ecommerce",
1533
+ coverageMatters: true,
1534
+ ctrExpectation: "high",
1535
+ primaryGoal: "coverage"
1536
+ },
1537
+ other: {
1538
+ label: "General",
1539
+ coverageMatters: true,
1540
+ ctrExpectation: "medium",
1541
+ primaryGoal: "content"
1542
+ }
1543
+ };
1544
+ function siteTypeBaseline(raw) {
1545
+ return SITE_TYPE_BASELINE[normalizeSiteType(raw)];
1546
+ }
1547
+ const MIN_CONFIDENT_PEERS = 3;
1548
+ const LEADER_RATIO = 1.15;
1549
+ const BEHIND_RATIO = .85;
1550
+ function median(values) {
1551
+ if (!values || values.length === 0) return null;
1552
+ const xs = [...values].sort((a, b) => a - b);
1553
+ const mid = Math.floor(xs.length / 2);
1554
+ return xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2;
1555
+ }
1556
+ function derivePeerStanding(input) {
1557
+ const peerMedianDomainRank = median(input.competitorDomainRanks);
1558
+ const peerMedianOrganicTraffic = median(input.competitorOrganicTraffic);
1559
+ const peerCount = Math.max(input.competitorDomainRanks?.length ?? 0, input.competitorOrganicTraffic?.length ?? 0);
1560
+ const confidence = peerCount === 0 ? "none" : peerCount >= MIN_CONFIDENT_PEERS ? "high" : "low";
1561
+ const compare = (site, peer) => {
1562
+ if (site == null || peer == null || peer === 0) return null;
1563
+ const ratio = site / peer;
1564
+ if (ratio >= LEADER_RATIO) return "leader";
1565
+ if (ratio <= BEHIND_RATIO) return "behind";
1566
+ return "on_par";
1567
+ };
1568
+ return {
1569
+ standing: compare(input.siteDomainRank, peerMedianDomainRank) ?? compare(input.siteOrganicTraffic, peerMedianOrganicTraffic) ?? "unknown",
1570
+ confidence,
1571
+ peerMedianDomainRank,
1572
+ peerMedianOrganicTraffic
1573
+ };
1574
+ }
1575
+ const GOAL_HEADLINE = {
1576
+ content: "publish & expand content",
1577
+ authority: "build authority & backlinks",
1578
+ conversion: "lift conversion & CTR",
1579
+ coverage: "expand indexable catalogue"
1580
+ };
1581
+ function deriveSiteBaseline(rawType, peer = {}) {
1582
+ const siteType = normalizeSiteType(rawType);
1583
+ const baseline = SITE_TYPE_BASELINE[siteType];
1584
+ const { standing, confidence, peerMedianDomainRank, peerMedianOrganicTraffic } = derivePeerStanding(peer);
1585
+ const lever = GOAL_HEADLINE[baseline.primaryGoal];
1586
+ return {
1587
+ siteType,
1588
+ baseline,
1589
+ peerStanding: standing,
1590
+ peerConfidence: confidence,
1591
+ peerMedianDomainRank,
1592
+ peerMedianOrganicTraffic,
1593
+ recommendedGoal: standing === "behind" ? `Close the gap on peers — ${lever}` : standing === "leader" ? `Defend the lead — ${lever}` : standing === "on_par" ? `Pull ahead of peers — ${lever}` : `${baseline.label}: ${lever}`,
1594
+ goalKind: baseline.primaryGoal
1595
+ };
1596
+ }
1481
1597
  function formatCount(value) {
1482
1598
  return new Intl.NumberFormat("en").format(Math.max(0, Math.round(value)));
1483
1599
  }
1484
- function issueCount(issues, ...types) {
1600
+ function issueCount$1(issues, ...types) {
1485
1601
  if (!issues?.length) return 0;
1486
1602
  const wanted = new Set(types);
1487
1603
  return issues.reduce((sum, issue) => sum + (wanted.has(issue.type) ? issue.count : 0), 0);
@@ -1607,6 +1723,12 @@ function stage(key, evidence) {
1607
1723
  }[key]
1608
1724
  };
1609
1725
  }
1726
+ const ESTABLISHED_IMPRESSIONS_28D$1 = 2e4;
1727
+ const NASCENT_IMPRESSIONS_28D$1 = 1e3;
1728
+ const GROWTH_CLICKS_PCT_90D = 10;
1729
+ const GROWTH_IMPRESSIONS_PCT_90D = 20;
1730
+ const DECLINE_CLICKS_PCT$1 = -10;
1731
+ const MIN_DECLINE_PRIOR_CLICKS$1 = 50;
1610
1732
  function classifySearchConsoleStage(input) {
1611
1733
  const issues = input.issues ?? [];
1612
1734
  const summary = input.summary ?? null;
@@ -1626,33 +1748,84 @@ function classifySearchConsoleStage(input) {
1626
1748
  source: "indexing"
1627
1749
  }]);
1628
1750
  const sitemapErrors = totalSitemapErrors(sitemaps);
1629
- const sitemapUrlCount = sitemaps.reduce((sum, sitemap) => sum + (sitemap.urlCount ?? 0), 0);
1630
- const unknown = issueCount(issues, "unknown_to_google");
1631
- const discovered = issueCount(issues, "discovered_not_indexed");
1632
- const crawled = issueCount(issues, "crawled_not_indexed");
1633
- const crawlBlocks = issueCount(issues, "blocked_robots", "server_error", "not_found", "soft_404", "access_denied", "forbidden");
1634
- const indexBlocks = issueCount(issues, "noindex", "canonical_mismatch");
1635
- const canonicalMismatches = input.canonicalMismatchCount ?? issueCount(issues, "canonical_mismatch");
1636
- const zeroImpressionPages = (input.pageInventory ?? []).filter((page) => page.impressions === 0).length;
1751
+ const unknown = issueCount$1(issues, "unknown_to_google");
1752
+ const discovered = issueCount$1(issues, "discovered_not_indexed");
1753
+ const crawled = issueCount$1(issues, "crawled_not_indexed");
1754
+ const hardBlocks = issueCount$1(issues, "blocked_robots", "server_error", "access_denied", "forbidden") + (input.crawlAuditBlockerCount ?? 0);
1755
+ const canonicalMismatches = input.canonicalMismatchCount ?? issueCount$1(issues, "canonical_mismatch");
1637
1756
  const visibleNoClickPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && page.clicks === 0).length;
1638
1757
  const poorPositionPages = (input.pageInventory ?? []).filter((page) => page.impressions >= 50 && (page.position ?? 0) > 20).length;
1639
- const pageMoverDropCount = input.pageMoverDropCount ?? 0;
1640
1758
  const ctrOutlierCount = input.ctrOutlierCount ?? 0;
1641
- if (summary.change7d != null && summary.change7d <= -5) return stage("declining_visibility", [{
1642
- label: "Index rate change",
1643
- value: `${summary.change7d.toFixed(1)}% in 7 days`,
1759
+ const traj = input.trajectory ?? null;
1760
+ const impressions28d = input.impressions28d ?? null;
1761
+ const clicks90d = traj?.clicksPct90d ?? null;
1762
+ const imp90d = traj?.impressionsPct90d ?? null;
1763
+ const posDelta90d = traj?.positionDelta90d ?? null;
1764
+ const clicks28d = traj?.clicksPct28d ?? null;
1765
+ const clicksPrior28d = traj?.clicksPrior28d ?? null;
1766
+ const isGrowing = clicks90d != null && clicks90d > GROWTH_CLICKS_PCT_90D || imp90d != null && imp90d > GROWTH_IMPRESSIONS_PCT_90D && posDelta90d != null && posDelta90d < 0;
1767
+ const isDeclining = clicks90d != null && clicks90d < DECLINE_CLICKS_PCT$1 && clicks28d != null && clicks28d < DECLINE_CLICKS_PCT$1 && clicksPrior28d != null && clicksPrior28d >= MIN_DECLINE_PRIOR_CLICKS$1;
1768
+ const isNascent = impressions28d != null && impressions28d < NASCENT_IMPRESSIONS_28D$1;
1769
+ const isEstablished = impressions28d != null && impressions28d >= ESTABLISHED_IMPRESSIONS_28D$1;
1770
+ if (hardBlocks > Math.max(10, totalUrls * .05)) return stage("crawl_blocked", [{
1771
+ label: "Crawl / on-page faults",
1772
+ value: formatCount(hardBlocks),
1644
1773
  source: "indexing"
1645
1774
  }, {
1646
1775
  label: "Indexed pages",
1647
1776
  value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
1648
1777
  source: "indexing"
1649
1778
  }]);
1650
- if (pageMoverDropCount > 0) return stage("declining_visibility", [{
1651
- label: "Falling pages",
1652
- value: formatCount(pageMoverDropCount),
1779
+ if (isDeclining) return stage("declining_visibility", [{
1780
+ label: "Clicks 90d",
1781
+ value: `${clicks90d.toFixed(1)}%`,
1782
+ source: "performance"
1783
+ }, {
1784
+ label: "Clicks 28d",
1785
+ value: `${clicks28d.toFixed(1)}%`,
1786
+ source: "performance"
1787
+ }]);
1788
+ if (isGrowing) return stage("healthy_growth_ready", [
1789
+ ...clicks90d != null ? [{
1790
+ label: "Clicks 90d",
1791
+ value: `+${clicks90d.toFixed(1)}%`,
1792
+ source: "performance"
1793
+ }] : [],
1794
+ ...imp90d != null ? [{
1795
+ label: "Impressions 90d",
1796
+ value: `+${imp90d.toFixed(1)}%`,
1797
+ source: "performance"
1798
+ }] : [],
1799
+ ...(input.recoverableBacklinkCount ?? 0) > 0 ? [{
1800
+ label: "Recoverable backlinks",
1801
+ value: formatCount(input.recoverableBacklinkCount),
1802
+ source: "performance"
1803
+ }] : [],
1804
+ ...(input.competitorGapCount ?? 0) > 0 ? [{
1805
+ label: "Competitor content gaps",
1806
+ value: formatCount(input.competitorGapCount),
1807
+ source: "performance"
1808
+ }] : []
1809
+ ]);
1810
+ if (crawled > Math.max(10, totalUrls * .3) && totalUrls > 500) return stage("index_rejection", [{
1811
+ label: "Crawled, not indexed",
1812
+ value: formatCount(crawled),
1813
+ source: "indexing"
1814
+ }, {
1815
+ label: "Not indexed",
1816
+ value: formatCount(notIndexed),
1817
+ source: "indexing"
1818
+ }]);
1819
+ if (isNascent) return stage("waiting_for_data", [{
1820
+ label: "Impressions (28d)",
1821
+ value: formatCount(impressions28d ?? 0),
1653
1822
  source: "performance"
1823
+ }, {
1824
+ label: "Indexed pages",
1825
+ value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
1826
+ source: "indexing"
1654
1827
  }]);
1655
- if (sitemaps.length === 0 || sitemapUrlCount === 0 || sitemapErrors > 0 || unknown > Math.max(5, totalUrls * .1)) return stage("weak_discovery", [{
1828
+ if (!isEstablished && (sitemaps.length === 0 || sitemapErrors > 0 || unknown > Math.max(5, totalUrls * .1))) return stage("weak_discovery", [{
1656
1829
  label: "Sitemaps",
1657
1830
  value: sitemaps.length === 0 ? "None registered" : `${formatCount(sitemapErrors)} errors`,
1658
1831
  source: "sitemap"
@@ -1670,48 +1843,15 @@ function classifySearchConsoleStage(input) {
1670
1843
  value: `${indexedPercent.toFixed(1)}%`,
1671
1844
  source: "indexing"
1672
1845
  }]);
1673
- if (crawlBlocks > Math.max(5, totalUrls * .05)) return stage("crawl_blocked", [{
1674
- label: "Crawl blockers",
1675
- value: formatCount(crawlBlocks),
1676
- source: "indexing"
1677
- }, {
1678
- label: "Indexed pages",
1679
- value: `${formatCount(indexed)} of ${formatCount(totalUrls)}`,
1680
- source: "indexing"
1681
- }]);
1682
- if (indexBlocks > Math.max(5, totalUrls * .05) || canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [...indexBlocks > 0 ? [{
1683
- label: "Index signal blockers",
1684
- value: formatCount(indexBlocks),
1685
- source: "indexing"
1686
- }] : [], ...canonicalMismatches > 0 ? [{
1846
+ if (canonicalMismatches > Math.max(5, totalUrls * .05)) return stage("indexability_blocked", [{
1687
1847
  label: "Canonical mismatches",
1688
1848
  value: formatCount(canonicalMismatches),
1689
1849
  source: "canonical"
1690
- }] : []]);
1691
- if (crawled > Math.max(10, totalUrls * .15)) return stage("index_rejection", [{
1692
- label: "Crawled, not indexed",
1693
- value: formatCount(crawled),
1694
- source: "indexing"
1695
- }, {
1696
- label: "Not indexed",
1697
- value: formatCount(notIndexed),
1698
- source: "indexing"
1699
1850
  }]);
1700
- if (indexedPercent < 80) return stage("partially_indexed", [{
1701
- label: "Indexed",
1702
- value: `${indexedPercent.toFixed(1)}%`,
1703
- source: "indexing"
1704
- }, {
1705
- label: "Not indexed",
1706
- value: formatCount(notIndexed),
1707
- source: "indexing"
1708
- }]);
1709
- if (zeroImpressionPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("indexed_invisible", [{
1710
- label: "No-impression pages",
1711
- value: formatCount(zeroImpressionPages),
1712
- source: "performance"
1713
- }]);
1714
- if (ctrOutlierCount > 0 || visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * .25)) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
1851
+ const lowCtrType = siteTypeBaseline(input.siteType).ctrExpectation === "low";
1852
+ const noClickShare = lowCtrType ? .5 : .25;
1853
+ const noClickTrigger = visibleNoClickPages >= Math.max(1, (input.pageInventory?.length ?? 0) * noClickShare);
1854
+ if (ctrOutlierCount > 0 && !lowCtrType || noClickTrigger) return stage("visible_not_clicked", [...ctrOutlierCount > 0 ? [{
1715
1855
  label: "CTR outliers",
1716
1856
  value: formatCount(ctrOutlierCount),
1717
1857
  source: "performance"
@@ -1735,6 +1875,199 @@ function classifySearchConsoleStage(input) {
1735
1875
  source: "indexing"
1736
1876
  }]);
1737
1877
  }
1878
+ const NASCENT_IMPRESSIONS_28D = 1e3;
1879
+ const ESTABLISHED_IMPRESSIONS_28D = 2e4;
1880
+ const LIFETIME_FLOOR = 500;
1881
+ const GROWTH_CLICKS_PCT = 10;
1882
+ const GROWTH_IMPRESSIONS_PCT = 20;
1883
+ const DECLINE_CLICKS_PCT = -10;
1884
+ const MIN_DECLINE_PRIOR_CLICKS = 50;
1885
+ const FADED_LIVENESS = .2;
1886
+ const DECAY_CTR = .005;
1887
+ const HARD_BLOCK_FLOOR = 10;
1888
+ const HARD_BLOCK_SHARE = .03;
1889
+ const REJECT_SHARE = .3;
1890
+ const REJECT_MIN = 50;
1891
+ function reachLivenessRatio(daily) {
1892
+ if (!daily || daily.length < 14) return null;
1893
+ const series = daily.map((d) => d.impressions);
1894
+ while (series.length && series[series.length - 1] === 0) series.pop();
1895
+ if (series.length < 14) return null;
1896
+ const rolling7 = (end) => {
1897
+ let sum = 0;
1898
+ for (let i = Math.max(0, end - 6); i <= end; i++) sum += series[i];
1899
+ return sum;
1900
+ };
1901
+ const latestWeek = rolling7(series.length - 1);
1902
+ let peakWeek = 0;
1903
+ for (let i = 6; i < series.length; i++) peakWeek = Math.max(peakWeek, rolling7(i));
1904
+ return peakWeek > 0 ? latestWeek / peakWeek : null;
1905
+ }
1906
+ function issueCount(issues, ...types) {
1907
+ if (!issues?.length) return 0;
1908
+ const wanted = new Set(types);
1909
+ return issues.reduce((sum, i) => sum + (wanted.has(i.type) ? i.count : 0), 0);
1910
+ }
1911
+ function fmt(n) {
1912
+ return new Intl.NumberFormat("en").format(Math.max(0, Math.round(n)));
1913
+ }
1914
+ const HEALTH_COPY = {
1915
+ healthy: {
1916
+ summary: "Google can crawl and index the pages that should be indexed.",
1917
+ primaryAction: "No indexing cleanup needed — focus on reach."
1918
+ },
1919
+ crawl_faults: {
1920
+ summary: "Real access faults (server errors / broken links) are capping otherwise-indexable pages.",
1921
+ primaryAction: "Fix the 5xx and broken URLs; return 410/404 for pages you retire on purpose."
1922
+ },
1923
+ quality_rejection: {
1924
+ summary: "Google is crawling pages and refusing to index them — a soft quality signal it never reports explicitly.",
1925
+ primaryAction: "Consolidate or improve the rejected pages, or noindex the thin/low-value set."
1926
+ }
1927
+ };
1928
+ function isIntentionalRetirementSite(input, notFound, serverError) {
1929
+ const type = normalizeSiteType(input.siteType);
1930
+ const typeMatches = type === "agency" || type === "portfolio" || type === "other";
1931
+ const fingerprint = notFound > 50 && serverError <= Math.max(5, notFound * .1);
1932
+ return typeMatches && fingerprint;
1933
+ }
1934
+ function classifyHealthStage(input) {
1935
+ const issues = input.issues ?? [];
1936
+ const totalUrls = input.totalUrls ?? 0;
1937
+ const noindex = issueCount(issues, "noindex");
1938
+ const notFound = issueCount(issues, "not_found");
1939
+ const softFound = issueCount(issues, "soft_404");
1940
+ const serverError = issueCount(issues, "server_error", "blocked_robots", "access_denied", "forbidden");
1941
+ const crawledNotIndexed = issueCount(issues, "crawled_not_indexed");
1942
+ const intentionalDead = isIntentionalRetirementSite(input, notFound, serverError) ? notFound : 0;
1943
+ const indexableUrls = Math.max(1, totalUrls - noindex - intentionalDead);
1944
+ 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
+ };
1953
+ 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
+ };
1965
+ return {
1966
+ stage: "healthy",
1967
+ ...HEALTH_COPY.healthy,
1968
+ evidence: []
1969
+ };
1970
+ }
1971
+ const REACH_COPY = {
1972
+ waiting_for_data: {
1973
+ summary: "Too new to diagnose — not enough search history yet.",
1974
+ primaryAction: "Submit a clean sitemap, add internal links, and give it time."
1975
+ },
1976
+ emerging: {
1977
+ summary: "Early but climbing — real impressions are starting to land.",
1978
+ primaryAction: "Keep publishing on the themes already gaining impressions."
1979
+ },
1980
+ growing: {
1981
+ summary: "Discoverable, indexed-enough, and trending up. The next work is expansion, not cleanup.",
1982
+ primaryAction: "Expand: striking-distance pages, content gaps, authority."
1983
+ },
1984
+ plateaued: {
1985
+ summary: "Established but flat — visibility is steady, not compounding.",
1986
+ primaryAction: "Refresh top pages and open a new content cluster to restart growth."
1987
+ },
1988
+ declining: {
1989
+ summary: "Established visibility is genuinely falling versus the previous period.",
1990
+ primaryAction: "Investigate the losing pages, recent releases, and competitor moves before expanding."
1991
+ },
1992
+ faded: {
1993
+ summary: "The site had real reach that has collapsed — it spiked and is now near-invisible in search.",
1994
+ primaryAction: "Diagnose the drop (quality, deindexing, lost rankings) — the 90-day total hides it; look at the recent week."
1995
+ },
1996
+ decayed: {
1997
+ summary: "Still shows for old terms but earns almost no clicks — the content has aged out of relevance.",
1998
+ primaryAction: "Refresh the decayed top pages, or mark the site low-priority if it is no longer maintained."
1999
+ }
2000
+ };
2001
+ function reach(stage, evidence) {
2002
+ return {
2003
+ stage,
2004
+ ...REACH_COPY[stage],
2005
+ evidence
2006
+ };
2007
+ }
2008
+ function classifyReachStage(input) {
2009
+ const imp28d = input.impressions28d ?? 0;
2010
+ const imp12m = input.impressions12m ?? null;
2011
+ const clicks28d = input.clicks28d ?? null;
2012
+ const clicks90dPct = input.clicksPct90d ?? null;
2013
+ const clicks28dPct = input.clicksPct28d ?? null;
2014
+ const priorClicks = input.clicksPrior28d ?? null;
2015
+ const imp90dPct = input.impressionsPct90d ?? null;
2016
+ const posDelta = input.positionDelta90d ?? null;
2017
+ const liveness = input.livenessRatio ?? null;
2018
+ if (imp12m != null && imp12m < LIFETIME_FLOOR) return reach("waiting_for_data", [{
2019
+ label: "Impressions (12m)",
2020
+ value: fmt(imp12m)
2021
+ }]);
2022
+ const hadRealReach = (imp12m ?? imp28d) > LIFETIME_FLOOR;
2023
+ const isGrowing = clicks90dPct != null && clicks90dPct > GROWTH_CLICKS_PCT || imp90dPct != null && imp90dPct > GROWTH_IMPRESSIONS_PCT && posDelta != null && posDelta < 0;
2024
+ if (hadRealReach && liveness != null && liveness < FADED_LIVENESS && !isGrowing) return reach("faded", [{
2025
+ label: "Recent week vs peak",
2026
+ value: `${(liveness * 100).toFixed(0)}%`
2027
+ }]);
2028
+ if (clicks90dPct != null && clicks90dPct < DECLINE_CLICKS_PCT && clicks28dPct != null && clicks28dPct < DECLINE_CLICKS_PCT && priorClicks != null && priorClicks >= MIN_DECLINE_PRIOR_CLICKS) return reach("declining", [{
2029
+ label: "Clicks 90d",
2030
+ value: `${clicks90dPct.toFixed(0)}%`
2031
+ }, {
2032
+ 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
+ }] : []]);
2042
+ const ctr = clicks28d != null && imp28d > 0 ? clicks28d / imp28d : null;
2043
+ if (hadRealReach && imp28d >= NASCENT_IMPRESSIONS_28D && ctr != null && ctr < DECAY_CTR) return reach("decayed", [{
2044
+ label: "Impressions (28d)",
2045
+ value: fmt(imp28d)
2046
+ }, {
2047
+ label: "Clicks (28d)",
2048
+ value: fmt(clicks28d ?? 0)
2049
+ }]);
2050
+ if (imp28d >= ESTABLISHED_IMPRESSIONS_28D) return reach("plateaued", [{
2051
+ label: "Impressions (28d)",
2052
+ value: fmt(imp28d)
2053
+ }]);
2054
+ if (imp28d < NASCENT_IMPRESSIONS_28D) return reach("emerging", [{
2055
+ label: "Impressions (28d)",
2056
+ value: fmt(imp28d)
2057
+ }]);
2058
+ return reach("plateaued", [{
2059
+ label: "Impressions (28d)",
2060
+ value: fmt(imp28d)
2061
+ }]);
2062
+ }
2063
+ function classifySiteTriage(input) {
2064
+ const health = classifyHealthStage(input);
2065
+ return {
2066
+ reach: classifyReachStage(input),
2067
+ health,
2068
+ headline: health.stage === "healthy" ? "reach" : "health"
2069
+ };
2070
+ }
1738
2071
  const encoder = new TextEncoder();
1739
2072
  function toPayloadString(payload) {
1740
2073
  return typeof payload === "string" ? payload : JSON.stringify(payload);
@@ -1809,4 +2142,4 @@ function readWebhookHeaders(headers) {
1809
2142
  signature: headers.signature ?? null
1810
2143
  };
1811
2144
  }
1812
- export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, PERIOD_PRESETS, PartnerApiError, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifySearchConsoleStage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
2145
+ export { ARCHETYPE_EXECUTION_CLASS, CANONICAL_WEBHOOK_EVENTS, COMPARE_OPTIONS, COUNTRY_NAMES, CWV_GOOD_CLS, CWV_GOOD_INP, CWV_GOOD_LCP, CWV_POOR_CLS, CWV_POOR_INP, CWV_POOR_LCP, GSC_COLUMN_OPTIONS, GSC_PERIOD_OPTIONS, GSC_PERIOD_OPTIONS_LONG, GSC_STABLE_LATENCY_DAYS, PERIOD_PRESETS, PartnerApiError, SITE_TYPE_BASELINE, VALID_WEBHOOK_EVENTS, WEBHOOK_CONTRACT_VERSION, WEBHOOK_CONTRACT_VERSION_HEADER, WEBHOOK_DELIVERY_HEADER, WEBHOOK_EVENT_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, analyticsStatusToSyncStatus, andFilter, classifyGscError, classifyHealthStage, classifyReachStage, classifySearchConsoleStage, classifySiteTriage, coerceRowMetrics, compareRange, countryName, coverageLabel, coverageLabels, createAnalyticsClient, createGscdumpClient, createGscdumpRealtimeClient, createPartnerClient, createPartnerRealtimeClient, cwvBucket, dateFilter, defineGscAnalyzer, derivePeerStanding, deriveSiteBaseline, enrichIssueDetails, findLifecycleSite, getGscUnstableCutoffDate, gscConsoleUrl, investigationStatusConfig, isCustomPeriod, issueDetails, issueGroups, issueTypeToGroup, lifecycleSiteToSyncStatus, lifecycleSiteToUserSite, normalizeSiteType, nuxtSeoTips, parseCustomPeriod, parseWebhookPayload, periodToDateRange, periodToDays, positionFor, reachLivenessRatio, readWebhookHeaders, serializeWebhookPayload, severityOrder, siteTypeBaseline, siteUrlToHostname, splitOpportunityTitle, summarizeDailyRows, toPartnerError, truncateQuery, verifyWebhookSignature, weightedAnonPct };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/sdk",
3
3
  "type": "module",
4
- "version": "0.23.4",
4
+ "version": "0.24.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.23.4"
44
+ "gscdump": "0.24.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.23.4",
56
- "@gscdump/engine": "0.23.4",
57
- "@gscdump/contracts": "0.23.4"
55
+ "@gscdump/analysis": "0.24.0",
56
+ "@gscdump/engine": "0.24.0",
57
+ "@gscdump/contracts": "0.24.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "typescript": "^6.0.3",