@gscdump/analysis 0.24.1 → 0.25.1

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "0.24.1",
4
+ "version": "0.25.1",
5
5
  "description": "GSC analyzers — striking-distance, opportunity, movers, decay, brand, clustering, concentration, seasonality. Pure row-based + DuckDB-native.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -36,11 +36,6 @@
36
36
  "import": "./dist/default-registry.mjs",
37
37
  "default": "./dist/default-registry.mjs"
38
38
  },
39
- "./query": {
40
- "types": "./dist/query/index.d.mts",
41
- "import": "./dist/query/index.mjs",
42
- "default": "./dist/query/index.mjs"
43
- },
44
39
  "./source": {
45
40
  "types": "./dist/source/index.d.mts",
46
41
  "import": "./dist/source/index.mjs",
@@ -75,9 +70,9 @@
75
70
  },
76
71
  "dependencies": {
77
72
  "drizzle-orm": "1.0.0-rc.3",
78
- "@gscdump/engine": "0.24.1",
79
- "@gscdump/engine-gsc-api": "0.24.1",
80
- "gscdump": "0.24.1"
73
+ "@gscdump/engine-gsc-api": "0.25.1",
74
+ "gscdump": "0.25.1",
75
+ "@gscdump/engine": "0.25.1"
81
76
  },
82
77
  "devDependencies": {
83
78
  "vitest": "^4.1.7"
@@ -1,53 +0,0 @@
1
- import { ResolverOptions } from "@gscdump/engine/resolver";
2
- import { BuilderState } from "gscdump/query";
3
- import { AnalysisParams } from "@gscdump/engine/analysis-types";
4
- import { Row } from "@gscdump/engine/contracts";
5
- interface QueryAnalyzerExtraQuery {
6
- name: string;
7
- sql: string;
8
- params: unknown[];
9
- }
10
- interface QueryAnalyzerPlan<TK extends string = string> {
11
- tableKey: TK;
12
- sql: string;
13
- params: unknown[];
14
- extraQueries?: QueryAnalyzerExtraQuery[];
15
- }
16
- declare function buildDataQueryPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
17
- /** Row-query plan for `data-query`. Comparison (`qc`) joins two periods. */
18
- declare function buildDataQueryRows(params: AnalysisParams): Record<string, BuilderState>;
19
- /** Post-processing for `data-query` row results. Mirrors `shapeDataQuery`. */
20
- declare function shapeDataQueryRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
21
- results: Row[];
22
- meta: Record<string, unknown>;
23
- };
24
- /** Row-query plan for `data-detail`. `qc` adds a previous-period totals query. */
25
- declare function buildDataDetailRows(params: AnalysisParams): Record<string, BuilderState>;
26
- /** Post-processing for `data-detail` row results. Mirrors `shapeDataDetailRows`. */
27
- declare function shapeDataDetailRowResults(rowMap: Record<string, Row[]>, params: AnalysisParams): {
28
- results: Row[];
29
- meta: Record<string, unknown>;
30
- };
31
- /**
32
- * Pure post-processing for `data-query` rows. Adapter-free so reducers can
33
- * call it without re-running the SQL plan.
34
- */
35
- declare function shapeDataQueryRows(rows: Row[], params: AnalysisParams, extras?: Record<string, Row[]>): {
36
- results: Row[];
37
- meta: Record<string, unknown>;
38
- };
39
- declare function buildDataDetailPlan<TK extends string>(params: AnalysisParams, options: ResolverOptions<TK>): QueryAnalyzerPlan<TK>;
40
- /**
41
- * Pure post-processing for `data-detail` rows. Reads the date range off
42
- * `params.q` so it stays adapter-free; reducers call it directly.
43
- */
44
- declare function shapeDataDetailRows(rows: Row[], params: AnalysisParams, extras?: Record<string, Row[]>): {
45
- results: Row[];
46
- meta: Record<string, unknown>;
47
- };
48
- /**
49
- * Produce a canonical form of a search query for grouping near-duplicates.
50
- * Idempotent: `normalizeQuery(normalizeQuery(q)) === normalizeQuery(q)`.
51
- */
52
- declare function normalizeQuery(query: string): string;
53
- export { type QueryAnalyzerExtraQuery, type QueryAnalyzerPlan, buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };
@@ -1,444 +0,0 @@
1
- import { padTimeseries } from "@gscdump/engine/period";
2
- import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
3
- import { extractDateRange } from "gscdump/query";
4
- function requireBuilderState(input, tool) {
5
- if (!input || typeof input !== "object" || !("dimensions" in input) || !Array.isArray(input.dimensions)) throw new Error(`${tool}: params.q is required (BuilderState)`);
6
- return input;
7
- }
8
- function optionalBuilderState(input, tool, key) {
9
- if (input == null) return null;
10
- if (typeof input !== "object" || !("dimensions" in input) || !Array.isArray(input.dimensions)) throw new Error(`${tool}: params.${key} must be a BuilderState`);
11
- return input;
12
- }
13
- const NUMERIC_METRIC_COLS = [
14
- "clicks",
15
- "impressions",
16
- "ctr",
17
- "position",
18
- "prevClicks",
19
- "prevImpressions",
20
- "prevCtr",
21
- "prevPosition",
22
- "variantCount",
23
- "totalCount"
24
- ];
25
- function coerceNumericCols(row) {
26
- const out = { ...row };
27
- for (const col of NUMERIC_METRIC_COLS) if (col in out && out[col] != null) out[col] = Number(out[col]);
28
- return out;
29
- }
30
- function shapeDataQuery(rows, extras, opts) {
31
- let totalCount;
32
- let cleaned;
33
- if (opts.hasPrev) {
34
- cleaned = rows.map(coerceNumericCols);
35
- totalCount = Number((extras?.count?.[0])?.total ?? cleaned.length);
36
- } else {
37
- const first = rows[0];
38
- totalCount = Number(first?.totalCount ?? 0);
39
- cleaned = rows.map((raw) => {
40
- const { totalCount: _tc, totalClicks: _tclk, totalImpressions: _timp, totalCtr: _tctr, totalPosition: _tpos, sum_position: _sp, ...rest } = raw;
41
- return coerceNumericCols(rest);
42
- });
43
- }
44
- const totalsRow = extras?.totals?.[0] ?? {};
45
- const totals = {
46
- clicks: Number(totalsRow.clicks ?? 0),
47
- impressions: Number(totalsRow.impressions ?? 0),
48
- ctr: Number(totalsRow.ctr ?? 0),
49
- position: Number(totalsRow.position ?? 0)
50
- };
51
- const extrasResults = [];
52
- if (extras?.canonicalExtras) extrasResults.push({
53
- key: "canonicalExtras",
54
- results: extras.canonicalExtras
55
- });
56
- return {
57
- results: mergeExtras(cleaned, extrasResults),
58
- meta: {
59
- totalCount,
60
- totals
61
- }
62
- };
63
- }
64
- function buildDataQueryPlan(params, options) {
65
- const state = requireBuilderState(params.q, "data-query");
66
- if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
67
- const prev = optionalBuilderState(params.qc, "data-query", "qc");
68
- const totals = buildTotalsSql(state, options);
69
- const extras = buildExtrasQueries(state, options);
70
- const extraQueries = [{
71
- name: "totals",
72
- sql: totals.sql,
73
- params: totals.params
74
- }, ...extras.map((extra) => ({
75
- name: extra.key,
76
- sql: extra.sql,
77
- params: extra.params
78
- }))];
79
- const tableKey = options.adapter.inferTable(state.dimensions);
80
- if (prev) {
81
- const comparison = resolveComparisonSQL(state, prev, options, params.comparisonFilter);
82
- extraQueries.push({
83
- name: "count",
84
- sql: comparison.countSql,
85
- params: comparison.countParams
86
- });
87
- return {
88
- tableKey,
89
- sql: comparison.sql,
90
- params: comparison.params,
91
- extraQueries
92
- };
93
- }
94
- const optimized = resolveToSQLOptimized(state, options);
95
- return {
96
- tableKey,
97
- sql: optimized.sql,
98
- params: optimized.params,
99
- extraQueries
100
- };
101
- }
102
- function totalsState(state) {
103
- return {
104
- ...state,
105
- dimensions: [],
106
- orderBy: void 0,
107
- rowLimit: 1
108
- };
109
- }
110
- function readTotals(row) {
111
- return {
112
- clicks: Number(row?.clicks ?? 0),
113
- impressions: Number(row?.impressions ?? 0),
114
- ctr: Number(row?.ctr ?? 0),
115
- position: Number(row?.position ?? 0)
116
- };
117
- }
118
- function buildDataQueryRows(params) {
119
- const state = requireBuilderState(params.q, "data-query");
120
- if (state.dimensions.includes("date")) throw new Error("data-query: date dimension not supported; use data-detail");
121
- const queries = {
122
- main: state,
123
- totals: totalsState(state)
124
- };
125
- const prev = optionalBuilderState(params.qc, "data-query", "qc");
126
- if (prev) {
127
- queries.prevMain = prev;
128
- queries.prevTotals = totalsState(prev);
129
- }
130
- return queries;
131
- }
132
- function shapeDataQueryRowResults(rowMap, params) {
133
- const main = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
134
- const totals = readTotals(rowMap.totals?.[0]);
135
- if (params.qc == null) return {
136
- results: main,
137
- meta: {
138
- totalCount: main.length,
139
- totals
140
- }
141
- };
142
- const state = requireBuilderState(params.q, "data-query");
143
- const dims = state.dimensions;
144
- const keyOf = (row) => dims.map((d) => String(row[d] ?? "")).join("\0");
145
- const prevByKey = /* @__PURE__ */ new Map();
146
- for (const r of rowMap.prevMain ?? []) prevByKey.set(keyOf(r), r);
147
- const filter = params.comparisonFilter;
148
- const merged = [];
149
- const seen = /* @__PURE__ */ new Set();
150
- for (const cur of main) {
151
- const key = keyOf(cur);
152
- seen.add(key);
153
- const prev = prevByKey.get(key);
154
- const row = {
155
- ...cur,
156
- prevClicks: Number(prev?.clicks ?? 0),
157
- prevImpressions: Number(prev?.impressions ?? 0),
158
- prevCtr: Number(prev?.ctr ?? 0),
159
- prevPosition: Number(prev?.position ?? 0)
160
- };
161
- if (passesComparisonFilter(filter, {
162
- isNew: !prev,
163
- isLost: false,
164
- clicksChange: Number(cur.clicks ?? 0) - Number(prev?.clicks ?? 0)
165
- })) merged.push(row);
166
- }
167
- for (const prev of rowMap.prevMain ?? []) {
168
- const p = prev;
169
- const key = keyOf(p);
170
- if (seen.has(key)) continue;
171
- const row = {
172
- ...p,
173
- clicks: 0,
174
- impressions: 0,
175
- ctr: 0,
176
- position: 0,
177
- prevClicks: Number(p.clicks ?? 0),
178
- prevImpressions: Number(p.impressions ?? 0),
179
- prevCtr: Number(p.ctr ?? 0),
180
- prevPosition: Number(p.position ?? 0)
181
- };
182
- if (passesComparisonFilter(filter, {
183
- isNew: false,
184
- isLost: true,
185
- clicksChange: -Number(p.clicks ?? 0)
186
- })) merged.push(row);
187
- }
188
- if (state.orderBy) {
189
- const { column, dir } = state.orderBy;
190
- merged.sort((a, b) => {
191
- const av = Number(a[column]) || 0;
192
- const bv = Number(b[column]) || 0;
193
- return dir === "asc" ? av - bv : bv - av;
194
- });
195
- }
196
- return {
197
- results: merged.map((r) => coerceNumericCols(r)),
198
- meta: {
199
- totalCount: merged.length,
200
- totals
201
- }
202
- };
203
- }
204
- function passesComparisonFilter(filter, ctx) {
205
- switch (filter) {
206
- case "new": return ctx.isNew;
207
- case "lost": return ctx.isLost;
208
- case "improving": return ctx.clicksChange > 0;
209
- case "declining": return ctx.clicksChange < 0;
210
- default: return true;
211
- }
212
- }
213
- function buildDataDetailRows(params) {
214
- const state = requireBuilderState(params.q, "data-detail");
215
- if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
216
- const queries = {
217
- main: state,
218
- totals: totalsState(state)
219
- };
220
- const prev = optionalBuilderState(params.qc, "data-detail", "qc");
221
- if (prev) queries.prevTotals = totalsState(prev);
222
- return queries;
223
- }
224
- function shapeDataDetailRowResults(rowMap, params) {
225
- const { startDate, endDate } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
226
- const coerced = (rowMap.main ?? []).map((r) => coerceNumericCols(r));
227
- const daily = startDate && endDate ? padTimeseries(coerced, {
228
- startDate,
229
- endDate
230
- }) : coerced;
231
- const meta = { totals: readTotals(rowMap.totals?.[0]) };
232
- if (rowMap.prevTotals) meta.previousTotals = readTotals(rowMap.prevTotals[0]);
233
- return {
234
- results: daily,
235
- meta
236
- };
237
- }
238
- function shapeDataQueryRows(rows, params, extras) {
239
- return shapeDataQuery(rows, extras, { hasPrev: params.qc != null });
240
- }
241
- function buildDataDetailPlan(params, options) {
242
- const state = requireBuilderState(params.q, "data-detail");
243
- if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
244
- const main = resolveToSQL(state, options);
245
- const totals = buildTotalsSql(state, options);
246
- const prev = optionalBuilderState(params.qc, "data-detail", "qc");
247
- const extraQueries = [{
248
- name: "totals",
249
- sql: totals.sql,
250
- params: totals.params
251
- }];
252
- if (prev) {
253
- const previousTotals = buildTotalsSql(prev, options);
254
- extraQueries.push({
255
- name: "prevTotals",
256
- sql: previousTotals.sql,
257
- params: previousTotals.params
258
- });
259
- }
260
- return {
261
- tableKey: options.adapter.inferTable(state.dimensions),
262
- sql: main.sql,
263
- params: main.params,
264
- extraQueries
265
- };
266
- }
267
- function shapeDataDetailRows(rows, params, extras) {
268
- const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
269
- const coerced = rows.map(coerceNumericCols);
270
- const daily = rangeStart && rangeEnd ? padTimeseries(coerced, {
271
- startDate: rangeStart,
272
- endDate: rangeEnd
273
- }) : coerced;
274
- const totalsRow = extras?.totals?.[0] ?? {};
275
- const meta = { totals: {
276
- clicks: Number(totalsRow.clicks ?? 0),
277
- impressions: Number(totalsRow.impressions ?? 0),
278
- ctr: Number(totalsRow.ctr ?? 0),
279
- position: Number(totalsRow.position ?? 0)
280
- } };
281
- if (extras?.prevTotals) {
282
- const previousTotalsRow = extras.prevTotals[0] ?? {};
283
- meta.previousTotals = {
284
- clicks: Number(previousTotalsRow.clicks ?? 0),
285
- impressions: Number(previousTotalsRow.impressions ?? 0),
286
- ctr: Number(previousTotalsRow.ctr ?? 0),
287
- position: Number(previousTotalsRow.position ?? 0)
288
- };
289
- }
290
- return {
291
- results: daily,
292
- meta
293
- };
294
- }
295
- const SYNONYMS = {
296
- checker: "validator",
297
- tester: "validator",
298
- verifier: "validator",
299
- verify: "validate",
300
- check: "validate",
301
- test: "validate",
302
- checking: "validate",
303
- testing: "validate",
304
- creator: "generator",
305
- builder: "generator",
306
- maker: "generator",
307
- create: "generate",
308
- build: "generate",
309
- make: "generate",
310
- lookup: "search",
311
- finder: "search",
312
- find: "search",
313
- online: "",
314
- free: ""
315
- };
316
- const NO_STRIP_S = new Set([
317
- "css",
318
- "js",
319
- "ts",
320
- "os",
321
- "as",
322
- "is",
323
- "us",
324
- "has",
325
- "was",
326
- "its",
327
- "this",
328
- "yes",
329
- "no",
330
- "bus",
331
- "gas",
332
- "dns",
333
- "rss",
334
- "sms",
335
- "gps",
336
- "aws",
337
- "sas",
338
- "cms",
339
- "ios",
340
- "less",
341
- "loss",
342
- "miss",
343
- "pass",
344
- "class",
345
- "access",
346
- "process",
347
- "express",
348
- "address",
349
- "cross",
350
- "press",
351
- "stress",
352
- "progress",
353
- "success",
354
- "business",
355
- "wordpress",
356
- "status",
357
- "radius",
358
- "nexus",
359
- "focus",
360
- "bonus",
361
- "campus",
362
- "census",
363
- "corpus",
364
- "nucleus",
365
- "stimulus",
366
- "terminus",
367
- "versus",
368
- "virus",
369
- "surplus",
370
- "cactus",
371
- "analysis",
372
- "basis",
373
- "thesis",
374
- "crisis",
375
- "axis",
376
- "genesis",
377
- "synopsis",
378
- "diagnosis",
379
- "emphasis",
380
- "hypothesis",
381
- "synthesis",
382
- "parenthesis",
383
- "redis",
384
- "apis",
385
- "chaos",
386
- "demos",
387
- "logos",
388
- "photos",
389
- "videos",
390
- "nuxtjs",
391
- "nextjs",
392
- "nodejs",
393
- "reactjs",
394
- "vuejs",
395
- "angularjs",
396
- "expressjs",
397
- "nestjs",
398
- "threejs",
399
- "alpinejs",
400
- "solidjs",
401
- "sveltejs",
402
- "dejs",
403
- "bunjs",
404
- "denojs",
405
- "canvas",
406
- "atlas",
407
- "alias",
408
- "bias",
409
- "perhaps",
410
- "whereas",
411
- "kubernetes",
412
- "sass",
413
- "postgres",
414
- "always",
415
- "across",
416
- "previous",
417
- "various",
418
- "serious",
419
- "famous",
420
- "anonymous",
421
- "continuous",
422
- "dangerous",
423
- "generous",
424
- "obvious",
425
- "numerous",
426
- "curious",
427
- "nervous",
428
- "conscious"
429
- ]);
430
- function depluralize(token) {
431
- if (token.length <= 3) return token;
432
- if (NO_STRIP_S.has(token)) return token;
433
- if (token.endsWith("ies") && token.length > 4) return `${token.slice(0, -3)}y`;
434
- if (token.endsWith("ses") && token.length > 4) return token.slice(0, -1);
435
- if (token.endsWith("shes") || token.endsWith("ches") || token.endsWith("xes") || token.endsWith("zes")) return token.slice(0, -2);
436
- if (token.endsWith("s") && !token.endsWith("ss")) return token.slice(0, -1);
437
- return token;
438
- }
439
- const SEPARATOR_RE = /[-_/.@#:+]+/g;
440
- const WHITESPACE_RE = /\s+/g;
441
- function normalizeQuery(query) {
442
- return query.toLowerCase().replace(SEPARATOR_RE, " ").replace(WHITESPACE_RE, " ").trim().split(" ").filter(Boolean).map((token) => SYNONYMS[token] ?? token).filter(Boolean).map(depluralize).sort().join(" ");
443
- }
444
- export { buildDataDetailPlan, buildDataDetailRows, buildDataQueryPlan, buildDataQueryRows, normalizeQuery, shapeDataDetailRowResults, shapeDataDetailRows, shapeDataQueryRowResults, shapeDataQueryRows };