@gscdump/analysis 0.9.2 → 0.11.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.
@@ -11,16 +11,28 @@ interface QueryAnalyzerPlan<TK extends string = string> {
11
11
  sql: string;
12
12
  params: unknown[];
13
13
  extraQueries?: QueryAnalyzerExtraQuery[];
14
- shape: (rows: Row[], params: AnalysisParams, extras?: Record<string, Row[]>) => {
15
- results: Row[];
16
- meta: Record<string, unknown>;
17
- };
18
14
  }
19
15
  declare function buildDataQueryPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
16
+ /**
17
+ * Pure post-processing for `data-query` rows. Adapter-free so reducers can
18
+ * call it without re-running the SQL plan.
19
+ */
20
+ declare function shapeDataQueryRows(rows: Row[], params: AnalysisParams, extras?: Record<string, Row[]>): {
21
+ results: Row[];
22
+ meta: Record<string, unknown>;
23
+ };
20
24
  declare function buildDataDetailPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
25
+ /**
26
+ * Pure post-processing for `data-detail` rows. Reads the date range off
27
+ * `params.q` so it stays adapter-free; reducers call it directly.
28
+ */
29
+ declare function shapeDataDetailRows(rows: Row[], params: AnalysisParams, extras?: Record<string, Row[]>): {
30
+ results: Row[];
31
+ meta: Record<string, unknown>;
32
+ };
21
33
  /**
22
34
  * Produce a canonical form of a search query for grouping near-duplicates.
23
35
  * Idempotent: `normalizeQuery(normalizeQuery(q)) === normalizeQuery(q)`.
24
36
  */
25
37
  declare function normalizeQuery(query: string): string;
26
- export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataQueryPlan, normalizeQuery };
38
+ export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
@@ -88,8 +88,7 @@ function buildDataQueryPlan(params, options) {
88
88
  tableKey,
89
89
  sql: comparison.sql,
90
90
  params: comparison.params,
91
- extraQueries,
92
- shape: (rows, _params, resolvedExtras) => shapeDataQuery(rows, resolvedExtras, { hasPrev: true })
91
+ extraQueries
93
92
  };
94
93
  }
95
94
  const optimized = resolveToSQLOptimized(state, options);
@@ -97,10 +96,12 @@ function buildDataQueryPlan(params, options) {
97
96
  tableKey,
98
97
  sql: optimized.sql,
99
98
  params: optimized.params,
100
- extraQueries,
101
- shape: (rows, _params, resolvedExtras) => shapeDataQuery(rows, resolvedExtras, { hasPrev: false })
99
+ extraQueries
102
100
  };
103
101
  }
102
+ function shapeDataQueryRows(rows, params, extras) {
103
+ return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
104
+ }
104
105
  function buildDataDetailPlan(params, options) {
105
106
  const state = requireBuilderState(params.q, "data-detail");
106
107
  if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
@@ -120,40 +121,39 @@ function buildDataDetailPlan(params, options) {
120
121
  params: previousTotals.params
121
122
  });
122
123
  }
123
- const tableKey = options.adapter.inferTable(state.dimensions);
124
- const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(state.filter);
125
124
  return {
126
- tableKey,
125
+ tableKey: options.adapter.inferTable(state.dimensions),
127
126
  sql: main.sql,
128
127
  params: main.params,
129
- extraQueries,
130
- shape: (rows, _params, extras) => {
131
- const coerced = rows.map(coerceNumericCols);
132
- const daily = rangeStart && rangeEnd ? padTimeseries(coerced, {
133
- startDate: rangeStart,
134
- endDate: rangeEnd
135
- }) : coerced;
136
- const totalsRow = extras?.totals?.[0] ?? {};
137
- const meta = { totals: {
138
- clicks: Number(totalsRow.clicks ?? 0),
139
- impressions: Number(totalsRow.impressions ?? 0),
140
- ctr: Number(totalsRow.ctr ?? 0),
141
- position: Number(totalsRow.position ?? 0)
142
- } };
143
- if (extras?.prevTotals) {
144
- const previousTotalsRow = extras.prevTotals[0] ?? {};
145
- meta.previousTotals = {
146
- clicks: Number(previousTotalsRow.clicks ?? 0),
147
- impressions: Number(previousTotalsRow.impressions ?? 0),
148
- ctr: Number(previousTotalsRow.ctr ?? 0),
149
- position: Number(previousTotalsRow.position ?? 0)
150
- };
151
- }
152
- return {
153
- results: daily,
154
- meta
155
- };
156
- }
128
+ extraQueries
129
+ };
130
+ }
131
+ function shapeDataDetailRows(rows, params, extras) {
132
+ const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
133
+ const coerced = rows.map(coerceNumericCols);
134
+ const daily = rangeStart && rangeEnd ? padTimeseries(coerced, {
135
+ startDate: rangeStart,
136
+ endDate: rangeEnd
137
+ }) : coerced;
138
+ const totalsRow = extras?.totals?.[0] ?? {};
139
+ const meta = { totals: {
140
+ clicks: Number(totalsRow.clicks ?? 0),
141
+ impressions: Number(totalsRow.impressions ?? 0),
142
+ ctr: Number(totalsRow.ctr ?? 0),
143
+ position: Number(totalsRow.position ?? 0)
144
+ } };
145
+ if (extras?.prevTotals) {
146
+ const previousTotalsRow = extras.prevTotals[0] ?? {};
147
+ meta.previousTotals = {
148
+ clicks: Number(previousTotalsRow.clicks ?? 0),
149
+ impressions: Number(previousTotalsRow.impressions ?? 0),
150
+ ctr: Number(previousTotalsRow.ctr ?? 0),
151
+ position: Number(previousTotalsRow.position ?? 0)
152
+ };
153
+ }
154
+ return {
155
+ results: daily,
156
+ meta
157
157
  };
158
158
  }
159
159
  const SYNONYMS = {
@@ -305,4 +305,4 @@ const WHITESPACE_RE = /\s+/g;
305
305
  function normalizeQuery(query) {
306
306
  return query.toLowerCase().replace(SEPARATOR_RE, " ").replace(WHITESPACE_RE, " ").trim().split(" ").filter(Boolean).map((token) => SYNONYMS[token] ?? token).filter(Boolean).map(depluralize).sort().join(" ");
307
307
  }
308
- export { buildDataDetailPlan, buildDataQueryPlan, normalizeQuery };
308
+ export { buildDataDetailPlan, buildDataQueryPlan, normalizeQuery, shapeDataDetailRows, shapeDataQueryRows };
@@ -1,7 +1,7 @@
1
1
  import * as _$_gscdump_engine_report0 from "@gscdump/engine/report";
2
2
  import { DefinedReport, ReportContext, ReportParams, ReportResult } from "@gscdump/engine/report";
3
3
  import { AnalyzerRegistry } from "@gscdump/engine/analyzer";
4
- import { AnalysisQuerySource } from "@gscdump/engine/resolver";
4
+ import { AnalysisQuerySource } from "@gscdump/engine/source";
5
5
  interface FormatReportOptions {
6
6
  /** Cap findings rendered per section. Defaults to all (already bounded by report). */
7
7
  maxFindingsPerSection?: number;
@@ -1739,11 +1739,8 @@ const defaultReportRegistry = createReportRegistry({
1739
1739
  reports: REPORTS,
1740
1740
  version: "0"
1741
1741
  });
1742
- async function analyzeFromSource(source, params, registry) {
1743
- return runAnalyzerFromSource(source, params, registry);
1744
- }
1745
1742
  async function executeStep(source, analyzers, step) {
1746
- return analyzeFromSource(source, {
1743
+ return runAnalyzerFromSource(source, {
1747
1744
  ...step.params,
1748
1745
  type: step.type
1749
1746
  }, analyzers).then((result) => ({
@@ -1,4 +1,4 @@
1
- import { AnalysisQuerySource } from "@gscdump/engine/resolver";
1
+ import { AnalysisQuerySource } from "@gscdump/engine/source";
2
2
  interface ContentGapResult {
3
3
  query: string;
4
4
  impressions: number;
@@ -1,19 +1,16 @@
1
- import { AnalyzerCapabilityError, AnalyzerRegistry } from "@gscdump/engine/analyzer";
2
1
  import { BuilderState } from "gscdump/query";
3
- import { AnalysisParams, AnalysisResult } from "@gscdump/engine/analysis-types";
4
- import { AnalysisPeriod, ComparisonPeriod } from "@gscdump/engine/period";
5
- import { AnalysisQuerySource, QueryRow, RowQuerySource, SqlQuerySource } from "@gscdump/engine/resolver";
2
+ import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
6
3
  import { PlannerCapabilities } from "gscdump/query/plan";
7
- declare function analyzeFromSource(source: AnalysisQuerySource, params: AnalysisParams, registry: AnalyzerRegistry): Promise<AnalysisResult>;
4
+ interface SyncedRange {
5
+ oldestDateSynced: string | null;
6
+ newestDateSynced: string | null;
7
+ }
8
8
  interface CompositeSourceOptions {
9
- engine: SqlQuerySource;
9
+ engine: AnalysisQuerySource;
10
10
  live: AnalysisQuerySource;
11
- site: {
12
- oldestDateSynced: string | null;
13
- newestDateSynced: string | null;
14
- };
11
+ site: SyncedRange;
15
12
  }
16
- declare function createCompositeSource(opts: CompositeSourceOptions): SqlQuerySource;
13
+ declare function createCompositeSource(opts: CompositeSourceOptions): AnalysisQuerySource;
17
14
  /**
18
15
  * Permissive defaults: in-memory sources are usually test doubles, so they
19
16
  * advertise every capability unless the test explicitly narrows them.
@@ -23,237 +20,5 @@ interface InMemoryQuerySourceOptions {
23
20
  queryRows: (state: BuilderState) => Promise<QueryRow[]> | QueryRow[];
24
21
  capabilities?: PlannerCapabilities;
25
22
  }
26
- declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): RowQuerySource;
27
- /**
28
- * Domain row shapes + analysis utilities. Analyzer-call contracts
29
- * (`AnalysisParams`, `AnalysisResult`, `AnalysisTool`, `num`) live in
30
- * `@gscdump/engine/analysis-types`.
31
- */
32
- type SortOrder = 'asc' | 'desc';
33
- /** Base search metrics */
34
- interface BaseMetrics {
35
- clicks: number;
36
- impressions: number;
37
- ctr: number;
38
- position: number;
39
- }
40
- /** Keyword row from query */
41
- interface KeywordRow extends BaseMetrics {
42
- query: string;
43
- page?: string;
44
- }
45
- /** Page row from query */
46
- interface PageRow extends BaseMetrics {
47
- page: string;
48
- }
49
- /** Date row from query */
50
- interface DateRow extends BaseMetrics {
51
- date: string;
52
- }
53
- interface BrandSegmentationOptions {
54
- /** Brand terms to match against keywords (case-insensitive) */
55
- brandTerms: string[];
56
- /** Minimum impressions for a keyword to be included. Default: 10 */
57
- minImpressions?: number;
58
- }
59
- interface BrandSummary {
60
- brandClicks: number;
61
- nonBrandClicks: number;
62
- brandShare: number;
63
- brandImpressions: number;
64
- nonBrandImpressions: number;
65
- }
66
- interface BrandSegmentationResult {
67
- brand: KeywordRow[];
68
- nonBrand: KeywordRow[];
69
- summary: BrandSummary;
70
- }
71
- type ClusterType = 'prefix' | 'intent' | 'both';
72
- interface ClusteringOptions {
73
- /** Minimum keywords for a cluster to be reported. Default: 2 */
74
- minClusterSize?: number;
75
- /** Minimum impressions for a keyword to be included. Default: 10 */
76
- minImpressions?: number;
77
- /** Clustering method. Default: 'both' */
78
- clusterBy?: ClusterType;
79
- }
80
- interface KeywordCluster {
81
- clusterName: string;
82
- clusterType: 'prefix' | 'intent';
83
- keywords: KeywordRow[];
84
- totalClicks: number;
85
- totalImpressions: number;
86
- avgPosition: number;
87
- keywordCount: number;
88
- }
89
- interface ClusteringResult {
90
- clusters: KeywordCluster[];
91
- unclustered: KeywordRow[];
92
- }
93
- type ConcentrationRiskLevel = 'low' | 'medium' | 'high';
94
- interface ConcentrationOptions {
95
- /** Number of top items to report. Default: 10 */
96
- topN?: number;
97
- }
98
- interface ConcentrationItem {
99
- key: string;
100
- clicks: number;
101
- share: number;
102
- }
103
- interface ConcentrationResult {
104
- /** Gini coefficient: 0 = equal distribution, 1 = fully concentrated */
105
- giniCoefficient: number;
106
- /** Herfindahl-Hirschman Index: 0-10000, >2500 = highly concentrated */
107
- hhi: number;
108
- /** Percentage of total clicks from top N items */
109
- topNConcentration: number;
110
- topNItems: ConcentrationItem[];
111
- totalItems: number;
112
- totalClicks: number;
113
- /** Risk level derived from HHI: <1500 low, 1500-2500 medium, >2500 high */
114
- riskLevel: ConcentrationRiskLevel;
115
- }
116
- type DecaySortMetric = 'lostClicks' | 'declinePercent' | 'currentClicks';
117
- interface DecayOptions {
118
- /** Minimum clicks in previous period to consider. Default: 50 */
119
- minPreviousClicks?: number;
120
- /** Minimum decline percentage (0-1). Default: 0.2 (20%) */
121
- threshold?: number;
122
- /** Metric to sort results by. Default: lostClicks */
123
- sortBy?: DecaySortMetric;
124
- }
125
- interface DecaySeriesPoint {
126
- week: string;
127
- clicks: number;
128
- impressions: number;
129
- }
130
- interface DecayResult {
131
- page: string;
132
- currentClicks: number;
133
- previousClicks: number;
134
- lostClicks: number;
135
- declinePercent: number;
136
- currentPosition: number;
137
- previousPosition: number;
138
- positionDrop: number;
139
- series?: DecaySeriesPoint[];
140
- }
141
- type MoversSortMetric = 'clicks' | 'impressions' | 'clicksChange' | 'impressionsChange' | 'positionChange';
142
- interface MoversOptions {
143
- /** Minimum change threshold to flag. Default: 0.2 (20%) */
144
- changeThreshold?: number;
145
- /** Minimum impressions in recent period. Default: 50 */
146
- minImpressions?: number;
147
- /** Metric to sort results by. Default: clicksChange */
148
- sortBy?: MoversSortMetric;
149
- }
150
- interface MoverData {
151
- keyword: string;
152
- page: string | null;
153
- recentClicks: number;
154
- recentImpressions: number;
155
- recentPosition: number;
156
- baselineClicks: number;
157
- baselineImpressions: number;
158
- baselinePosition: number;
159
- clicksChange: number;
160
- clicksChangePercent: number;
161
- impressionsChangePercent: number;
162
- positionChange: number;
163
- }
164
- interface MoversResult {
165
- rising: MoverData[];
166
- declining: MoverData[];
167
- stable: MoverData[];
168
- }
169
- interface OpportunityFactors {
170
- positionScore: number;
171
- impressionScore: number;
172
- ctrGapScore: number;
173
- }
174
- interface OpportunityResult {
175
- keyword: string;
176
- page: string | null;
177
- clicks: number;
178
- impressions: number;
179
- ctr: number;
180
- position: number;
181
- opportunityScore: number;
182
- potentialClicks: number;
183
- factors: OpportunityFactors;
184
- }
185
- type SeasonalityMetric = 'clicks' | 'impressions';
186
- interface SeasonalityOptions {
187
- /** Metric to analyze for seasonality. Default: clicks */
188
- metric?: SeasonalityMetric;
189
- }
190
- interface MonthlyData {
191
- month: string;
192
- value: number;
193
- vsAverage: number;
194
- isPeak: boolean;
195
- isTrough: boolean;
196
- }
197
- interface SeasonalityResult {
198
- hasSeasonality: boolean;
199
- /** Coefficient of variation: std dev / mean. Higher = more seasonal. */
200
- strength: number;
201
- peakMonths: string[];
202
- troughMonths: string[];
203
- monthlyBreakdown: MonthlyData[];
204
- insufficientData: boolean;
205
- }
206
- interface StrikingDistanceResult {
207
- keyword: string;
208
- page: string | null;
209
- clicks: number;
210
- impressions: number;
211
- ctr: number;
212
- position: number;
213
- /** Estimated clicks at ~15% CTR (the average for positions 1–3). */
214
- potentialClicks: number;
215
- }
216
- type StrikingDistanceSortMetric = 'clicks' | 'impressions' | 'ctr' | 'position' | 'potentialClicks';
217
- interface StrikingDistanceOptions {
218
- /** Minimum position (inclusive). Default: 4 */
219
- minPosition?: number;
220
- /** Maximum position (inclusive). Default: 20 */
221
- maxPosition?: number;
222
- /** Minimum impressions. Default: 100 */
223
- minImpressions?: number;
224
- /** Maximum CTR (queries with low CTR have more potential). Default: 0.05 (5%) */
225
- maxCtr?: number;
226
- /** Sort metric. Default: potentialClicks */
227
- sortBy?: StrikingDistanceSortMetric;
228
- /** Sort order. Default: desc */
229
- sortOrder?: SortOrder;
230
- }
231
- type QueryDimension = 'keywords' | 'pages' | 'dates';
232
- interface QueryOptions {
233
- dimension?: QueryDimension;
234
- limit?: number;
235
- }
236
- interface QueryResult {
237
- keywords: KeywordRow[];
238
- pages: PageRow[];
239
- dates: DateRow[];
240
- }
241
- interface ComparisonQueryResult {
242
- current: QueryResult;
243
- previous: QueryResult;
244
- }
245
- interface OpportunityOptions {
246
- minImpressions?: number;
247
- }
248
- declare function queryAnalyticsFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: QueryOptions): Promise<QueryResult>;
249
- declare function queryComparisonFromSource(source: AnalysisQuerySource, periods: ComparisonPeriod, options?: QueryOptions): Promise<ComparisonQueryResult>;
250
- declare function analyzeStrikingDistanceFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: StrikingDistanceOptions): Promise<StrikingDistanceResult[]>;
251
- declare function analyzeOpportunityFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: OpportunityOptions): Promise<OpportunityResult[]>;
252
- declare function analyzeBrandSegmentationFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options: BrandSegmentationOptions): Promise<BrandSegmentationResult>;
253
- declare function analyzePageConcentrationFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: ConcentrationOptions): Promise<ConcentrationResult>;
254
- declare function analyzeKeywordConcentrationFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: ConcentrationOptions): Promise<ConcentrationResult>;
255
- declare function analyzeClusteringFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: ClusteringOptions): Promise<ClusteringResult>;
256
- declare function analyzeSeasonalityFromSource(source: AnalysisQuerySource, period: AnalysisPeriod, options?: SeasonalityOptions): Promise<SeasonalityResult>;
257
- declare function analyzeDecayFromSource(source: AnalysisQuerySource, periods: ComparisonPeriod, options?: DecayOptions): Promise<DecayResult[]>;
258
- declare function analyzeMoversFromSource(source: AnalysisQuerySource, periods: ComparisonPeriod, options?: MoversOptions): Promise<MoversResult>;
259
- export { AnalyzerCapabilityError, type ComparisonQueryResult, type CompositeSourceOptions, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type QueryDimension, type QueryOptions, type QueryResult, analyzeBrandSegmentationFromSource, analyzeClusteringFromSource, analyzeDecayFromSource, analyzeFromSource, analyzeKeywordConcentrationFromSource, analyzeMoversFromSource, analyzeOpportunityFromSource, analyzePageConcentrationFromSource, analyzeSeasonalityFromSource, analyzeStrikingDistanceFromSource, createCompositeSource, createInMemoryQuerySource, queryAnalyticsFromSource, queryComparisonFromSource };
23
+ declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource;
24
+ export { type CompositeSourceOptions, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type SyncedRange, createCompositeSource, createInMemoryQuerySource };