@gscdump/analysis 1.4.0 → 1.4.2

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.
Files changed (99) hide show
  1. package/dist/action-priority.d.mts +34 -0
  2. package/dist/action-priority.mjs +201 -0
  3. package/dist/analyzer/adapt-rows.mjs +12 -0
  4. package/dist/analyzer/all.mjs +61 -0
  5. package/dist/analyzer/paginate.mjs +85 -0
  6. package/dist/analyzer/row-analyzers.d.mts +3 -0
  7. package/dist/analyzer/row-analyzers.mjs +3 -0
  8. package/dist/analyzer/row-values.mjs +22 -0
  9. package/dist/analyzers/bayesian-ctr.mjs +177 -0
  10. package/dist/analyzers/bipartite-pagerank.mjs +243 -0
  11. package/dist/analyzers/brand.d.mts +33 -0
  12. package/dist/analyzers/brand.mjs +154 -0
  13. package/dist/analyzers/cannibalization.d.mts +58 -0
  14. package/dist/analyzers/cannibalization.mjs +267 -0
  15. package/dist/analyzers/change-point.mjs +197 -0
  16. package/dist/analyzers/clustering.d.mts +29 -0
  17. package/dist/analyzers/clustering.mjs +217 -0
  18. package/dist/analyzers/concentration.d.mts +42 -0
  19. package/dist/analyzers/concentration.mjs +180 -0
  20. package/dist/analyzers/content-velocity.mjs +87 -0
  21. package/dist/analyzers/ctr-anomaly.mjs +189 -0
  22. package/dist/analyzers/ctr-curve.mjs +128 -0
  23. package/dist/analyzers/dark-traffic.mjs +130 -0
  24. package/dist/analyzers/data-detail.mjs +44 -0
  25. package/dist/analyzers/data-query.mjs +44 -0
  26. package/dist/analyzers/decay.d.mts +38 -0
  27. package/dist/analyzers/decay.mjs +187 -0
  28. package/dist/analyzers/device-gap.mjs +135 -0
  29. package/dist/analyzers/intent-atlas.mjs +175 -0
  30. package/dist/analyzers/keyword-breadth.mjs +115 -0
  31. package/dist/analyzers/long-tail.mjs +150 -0
  32. package/dist/analyzers/movers.d.mts +43 -0
  33. package/dist/analyzers/movers.mjs +283 -0
  34. package/dist/analyzers/opportunity.d.mts +32 -0
  35. package/dist/analyzers/opportunity.mjs +212 -0
  36. package/dist/analyzers/position-distribution.mjs +57 -0
  37. package/dist/analyzers/position-volatility.mjs +143 -0
  38. package/dist/analyzers/query-migration.mjs +178 -0
  39. package/dist/analyzers/seasonality.d.mts +28 -0
  40. package/dist/analyzers/seasonality.mjs +136 -0
  41. package/dist/analyzers/stl-decompose.mjs +187 -0
  42. package/dist/analyzers/striking-distance.d.mts +54 -0
  43. package/dist/analyzers/striking-distance.mjs +82 -0
  44. package/dist/analyzers/survival.mjs +200 -0
  45. package/dist/analyzers/trends.mjs +156 -0
  46. package/dist/analyzers/zero-click.d.mts +17 -0
  47. package/dist/analyzers/zero-click.mjs +122 -0
  48. package/dist/browser.d.mts +5 -0
  49. package/dist/browser.mjs +11 -0
  50. package/dist/default-registry.d.mts +3 -1
  51. package/dist/default-registry.mjs +5 -1
  52. package/dist/errors.d.mts +51 -1
  53. package/dist/index.d.mts +30 -494
  54. package/dist/index.mjs +28 -49
  55. package/dist/query/analyzers.mjs +301 -0
  56. package/dist/query/index.mjs +4 -0
  57. package/dist/query/intent.d.mts +20 -0
  58. package/dist/query/intent.mjs +139 -0
  59. package/dist/query/normalize.d.mts +15 -0
  60. package/dist/query/normalize.mjs +204 -0
  61. package/dist/report/format.d.mts +7 -0
  62. package/dist/report/format.mjs +45 -0
  63. package/dist/report/index.d.mts +4 -1
  64. package/dist/report/index.mjs +4 -1
  65. package/dist/report/registry.d.mts +4 -0
  66. package/dist/report/registry.mjs +26 -0
  67. package/dist/report/reports/brand.mjs +104 -0
  68. package/dist/report/reports/growth.mjs +144 -0
  69. package/dist/report/reports/health.mjs +141 -0
  70. package/dist/report/reports/movers.mjs +203 -0
  71. package/dist/report/reports/opportunities.mjs +184 -0
  72. package/dist/report/reports/pre-publish.mjs +111 -0
  73. package/dist/report/reports/priority.mjs +97 -0
  74. package/dist/report/reports/risks.mjs +201 -0
  75. package/dist/report/reports/triage.mjs +155 -0
  76. package/dist/report/require.mjs +15 -0
  77. package/dist/report/resolve-target.d.mts +29 -0
  78. package/dist/report/resolve-target.mjs +32 -0
  79. package/dist/{_chunks/index.d.mts → report/runtime.d.mts} +2 -37
  80. package/dist/report/runtime.mjs +80 -0
  81. package/dist/report/sections.mjs +22 -0
  82. package/dist/sitemap-health.d.mts +34 -0
  83. package/dist/sitemap-health.mjs +37 -0
  84. package/dist/{_chunks/index2.d.mts → source/composite.d.mts} +2 -13
  85. package/dist/{_chunks/source.mjs → source/composite.mjs} +1 -17
  86. package/dist/source/in-memory.d.mts +14 -0
  87. package/dist/source/in-memory.mjs +17 -0
  88. package/dist/source/index.d.mts +2 -1
  89. package/dist/source/index.mjs +2 -1
  90. package/dist/sql-analyzers.d.mts +3 -0
  91. package/dist/sql-analyzers.mjs +3 -0
  92. package/dist/types.d.mts +34 -0
  93. package/dist/types.mjs +21 -0
  94. package/package.json +4 -4
  95. package/dist/_chunks/default-registry.d.mts +0 -5
  96. package/dist/_chunks/default-registry.mjs +0 -5183
  97. package/dist/_chunks/errors.d.mts +0 -52
  98. package/dist/_chunks/report.mjs +0 -1710
  99. /package/dist/{_chunks/scoring.mjs → scoring.mjs} +0 -0
@@ -0,0 +1,155 @@
1
+ import { requireReportParam } from "../require.mjs";
2
+ import { reportRows, sectionArtifact, sectionCoverage, truncation } from "../sections.mjs";
3
+ import { resolveTarget } from "../resolve-target.mjs";
4
+ import { defineReport } from "@gscdump/engine/report";
5
+ const DEFAULT_MAX = 10;
6
+ const triageReport = defineReport({
7
+ id: "triage",
8
+ description: "Focused investigation: change-points, query migration, and position volatility scoped to one page or query.",
9
+ defaultPeriod: "last-90d",
10
+ defaultComparison: "none",
11
+ argsSpec: {
12
+ "target": {
13
+ type: "string",
14
+ description: "Target page URL or query string",
15
+ required: true
16
+ },
17
+ "target-kind": {
18
+ type: "string",
19
+ description: "page | query",
20
+ default: "page"
21
+ },
22
+ "max-findings": {
23
+ type: "number",
24
+ description: "Cap findings per section",
25
+ default: DEFAULT_MAX
26
+ }
27
+ },
28
+ plan: (params, window) => {
29
+ requireReportParam("triage", "target", params.target, "triage report requires --target <page-or-query>");
30
+ const dates = {
31
+ startDate: window.start,
32
+ endDate: window.end
33
+ };
34
+ return [
35
+ {
36
+ key: "change-point",
37
+ type: "change-point",
38
+ params: {
39
+ ...dates,
40
+ limit: 200
41
+ }
42
+ },
43
+ {
44
+ key: "query-migration",
45
+ type: "query-migration",
46
+ params: {
47
+ ...dates,
48
+ limit: 200
49
+ }
50
+ },
51
+ {
52
+ key: "position-volatility",
53
+ type: "position-volatility",
54
+ params: {
55
+ ...dates,
56
+ limit: 200
57
+ }
58
+ }
59
+ ];
60
+ },
61
+ reduce: (results, ctx) => {
62
+ const target = ctx.params.target;
63
+ const kind = ctx.params.targetKind ?? "page";
64
+ const max = ctx.params.maxFindings ?? DEFAULT_MAX;
65
+ const needle = (resolveTarget({
66
+ kind,
67
+ input: target
68
+ }).exact ?? target).toLowerCase();
69
+ const matches = (val) => val.toLowerCase().includes(needle);
70
+ return { sections: [
71
+ buildChangePointSection(results["change-point"], kind, matches, max),
72
+ buildMigrationSection(results["query-migration"], kind, matches, max, target),
73
+ buildVolatilitySection(results["position-volatility"], kind, matches, max)
74
+ ] };
75
+ }
76
+ });
77
+ function buildChangePointSection(res, kind, matches, max) {
78
+ const rows = reportRows(res).filter((r) => matches(kind === "page" ? r.page : r.keyword)).sort((a, b) => b.llr - a.llr);
79
+ const kept = rows.slice(0, max);
80
+ const findings = kept.map((r) => ({
81
+ entity: {
82
+ kind: kind === "page" ? "page" : "query",
83
+ value: kind === "page" ? r.page : r.keyword
84
+ },
85
+ metrics: {
86
+ llr: r.llr,
87
+ delta: r.delta
88
+ },
89
+ why: `${r.direction} on ${r.changeDate}`
90
+ }));
91
+ return {
92
+ id: "change-point",
93
+ title: "Change-points",
94
+ severity: kept.length ? "medium" : "info",
95
+ summary: { magnitudeLabel: `${kept.length} change-point${kept.length === 1 ? "" : "s"}` },
96
+ findings,
97
+ truncated: truncation(rows.length, kept.length),
98
+ coverage: sectionCoverage(res),
99
+ actions: [],
100
+ artifact: sectionArtifact(res, "change-point")
101
+ };
102
+ }
103
+ function buildMigrationSection(res, kind, matches, max, _rawTarget) {
104
+ const rows = reportRows(res).filter((r) => kind === "page" ? matches(r.sourcePage) || matches(r.targetPage) : false).sort((a, b) => b.weight - a.weight);
105
+ const kept = rows.slice(0, max);
106
+ const findings = kept.map((r) => ({
107
+ entity: {
108
+ kind: "page",
109
+ value: r.targetPage
110
+ },
111
+ metrics: {
112
+ weight: r.weight,
113
+ queryCount: r.queryCount
114
+ },
115
+ why: `from ${r.sourcePage}`
116
+ }));
117
+ return {
118
+ id: "query-migration",
119
+ title: "Query migration",
120
+ severity: "info",
121
+ summary: { magnitudeLabel: kind === "page" ? `${kept.length} migration edges touching target` : "N/A for query target" },
122
+ findings,
123
+ truncated: truncation(rows.length, kept.length),
124
+ coverage: sectionCoverage(res),
125
+ actions: [],
126
+ artifact: sectionArtifact(res, "query-migration")
127
+ };
128
+ }
129
+ function buildVolatilitySection(res, kind, matches, max) {
130
+ const rows = kind === "page" ? reportRows(res).filter((r) => matches(r.page)) : [];
131
+ const kept = rows.sort((a, b) => b.peakVolatility - a.peakVolatility).slice(0, max);
132
+ const findings = kept.map((r) => ({
133
+ entity: {
134
+ kind: "page",
135
+ value: r.page
136
+ },
137
+ metrics: {
138
+ avgVolatility: r.avgVolatility,
139
+ peakVolatility: r.peakVolatility,
140
+ impressions: r.totalImpressions
141
+ }
142
+ }));
143
+ return {
144
+ id: "position-volatility",
145
+ title: "Position volatility",
146
+ severity: kept.length ? "low" : "info",
147
+ summary: { magnitudeLabel: kind === "page" ? `${kept.length} volatile day${kept.length === 1 ? "" : "s"}` : "N/A for query target" },
148
+ findings,
149
+ truncated: truncation(rows.length, kept.length),
150
+ coverage: sectionCoverage(res),
151
+ actions: [],
152
+ artifact: sectionArtifact(res, "position-volatility")
153
+ };
154
+ }
155
+ export { triageReport };
@@ -0,0 +1,15 @@
1
+ import { analysisErrorToException, analysisErrors } from "../errors.mjs";
2
+ import { err, ok, unwrapResult } from "gscdump/result";
3
+ function requireReportParamResult(report, param, value, message) {
4
+ return value && value.trim() ? ok(value) : err(analysisErrors.missingReportParam(report, param, message));
5
+ }
6
+ function requireReportParam(report, param, value, message) {
7
+ return unwrapResult(requireReportParamResult(report, param, value, message), analysisErrorToException);
8
+ }
9
+ function requireComparisonWindowResult(report, window, message) {
10
+ return window.comparison ? ok(window.comparison) : err(analysisErrors.missingComparisonWindow(report, message));
11
+ }
12
+ function requireComparisonWindow(report, window, message) {
13
+ return unwrapResult(requireComparisonWindowResult(report, window, message), analysisErrorToException);
14
+ }
15
+ export { requireComparisonWindow, requireComparisonWindowResult, requireReportParam, requireReportParamResult };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `resolveTarget()` — stub day-one resolver. Future versions can wire in
3
+ * fuzzy matching / embedding similarity / a manifest cache without
4
+ * changing the contract.
5
+ *
6
+ * Today: case-insensitive exact match, falling back to substring (LIKE %x%)
7
+ * over a caller-supplied candidate list.
8
+ */
9
+ type ResolveTargetKind = 'page' | 'query';
10
+ interface ResolveTargetInput {
11
+ kind: ResolveTargetKind;
12
+ input: string;
13
+ /**
14
+ * Pool to resolve against. Empty pool ⇒ caller trusts the input verbatim
15
+ * (resolver returns it as `exact`). Useful when the caller doesn't have
16
+ * a candidate list yet but knows the value is correct.
17
+ */
18
+ candidates?: readonly string[];
19
+ }
20
+ interface ResolveTargetResult {
21
+ /** Best exact match from `candidates` (case-insensitive), or trusted input when no candidates were supplied. */
22
+ exact: string | null;
23
+ /** All candidates that include the input as a substring (case-insensitive). Includes `exact` if matched. */
24
+ matches: string[];
25
+ /** True when no candidates matched at all. */
26
+ unresolved: boolean;
27
+ }
28
+ declare function resolveTarget(opts: ResolveTargetInput): ResolveTargetResult;
29
+ export { ResolveTargetInput, ResolveTargetKind, ResolveTargetResult, resolveTarget };
@@ -0,0 +1,32 @@
1
+ function resolveTarget(opts) {
2
+ const needle = opts.input.trim();
3
+ if (!needle) return {
4
+ exact: null,
5
+ matches: [],
6
+ unresolved: true
7
+ };
8
+ const candidates = opts.candidates;
9
+ if (!candidates || candidates.length === 0) return {
10
+ exact: needle,
11
+ matches: [needle],
12
+ unresolved: false
13
+ };
14
+ const lower = needle.toLowerCase();
15
+ let exact = null;
16
+ const matches = [];
17
+ for (const c of candidates) {
18
+ const cl = c.toLowerCase();
19
+ if (cl === lower) {
20
+ exact = c;
21
+ if (!matches.includes(c)) matches.unshift(c);
22
+ continue;
23
+ }
24
+ if (cl.includes(lower)) matches.push(c);
25
+ }
26
+ return {
27
+ exact,
28
+ matches,
29
+ unresolved: matches.length === 0
30
+ };
31
+ }
32
+ export { resolveTarget };
@@ -1,43 +1,8 @@
1
- import { AnalysisError } from "./errors.mjs";
1
+ import { AnalysisError } from "../errors.mjs";
2
2
  import { AnalyzerRegistry } from "@gscdump/engine/analyzer";
3
3
  import { Result } from "gscdump/result";
4
4
  import { AnalysisQuerySource } from "@gscdump/engine/source";
5
5
  import { DefinedReport, ReportContext, ReportParams, ReportResult } from "@gscdump/engine/report";
6
- interface FormatReportOptions {
7
- /** Cap findings rendered per section. Defaults to all (already bounded by report). */
8
- maxFindingsPerSection?: number;
9
- }
10
- declare function formatReport(report: ReportResult, opts?: FormatReportOptions): string;
11
- declare const REPORTS: readonly DefinedReport<ReportParams>[];
12
- declare const defaultReportRegistry: import("@gscdump/engine/report").ReportRegistry;
13
- /**
14
- * `resolveTarget()` — stub day-one resolver. Future versions can wire in
15
- * fuzzy matching / embedding similarity / a manifest cache without
16
- * changing the contract.
17
- *
18
- * Today: case-insensitive exact match, falling back to substring (LIKE %x%)
19
- * over a caller-supplied candidate list.
20
- */
21
- type ResolveTargetKind = 'page' | 'query';
22
- interface ResolveTargetInput {
23
- kind: ResolveTargetKind;
24
- input: string;
25
- /**
26
- * Pool to resolve against. Empty pool ⇒ caller trusts the input verbatim
27
- * (resolver returns it as `exact`). Useful when the caller doesn't have
28
- * a candidate list yet but knows the value is correct.
29
- */
30
- candidates?: readonly string[];
31
- }
32
- interface ResolveTargetResult {
33
- /** Best exact match from `candidates` (case-insensitive), or trusted input when no candidates were supplied. */
34
- exact: string | null;
35
- /** All candidates that include the input as a substring (case-insensitive). Includes `exact` if matched. */
36
- matches: string[];
37
- /** True when no candidates matched at all. */
38
- unresolved: boolean;
39
- }
40
- declare function resolveTarget(opts: ResolveTargetInput): ResolveTargetResult;
41
6
  interface RunReportOptions<P extends ReportParams = ReportParams> {
42
7
  source: AnalysisQuerySource;
43
8
  analyzers: AnalyzerRegistry;
@@ -86,4 +51,4 @@ interface DryRunReportResult {
86
51
  * Useful right now only as an "is this report wired up correctly?" check.
87
52
  */
88
53
  declare function dryRunReport<P extends ReportParams = ReportParams>(report: DefinedReport<P>, ctx: ReportContext<P>): Promise<DryRunReportResult>;
89
- export { DryRunReportResult, FormatReportOptions, REPORTS, ResolveTargetInput, ResolveTargetKind, ResolveTargetResult, RunReportOptions, defaultReportRegistry, dryRunReport, formatReport, resolveTarget, runReport, runReportResult };
54
+ export { DryRunReportResult, RunReportOptions, dryRunReport, runReport, runReportResult };
@@ -0,0 +1,80 @@
1
+ import { analysisErrorToException, analysisErrors } from "../errors.mjs";
2
+ import { runAnalyzerFromSource } from "@gscdump/engine/analyzer";
3
+ import { err, ok, unwrapResult } from "gscdump/result";
4
+ import { computeInputHash } from "@gscdump/engine/report";
5
+ async function executeStep(source, analyzers, step) {
6
+ return runAnalyzerFromSource(source, {
7
+ ...step.params,
8
+ type: step.type
9
+ }, analyzers).then((result) => ({
10
+ state: {
11
+ key: step.key,
12
+ type: step.type,
13
+ status: "done"
14
+ },
15
+ result
16
+ })).catch((thrown) => {
17
+ const message = thrown?.message ?? String(thrown);
18
+ return {
19
+ state: {
20
+ key: step.key,
21
+ type: step.type,
22
+ status: "error",
23
+ error: message
24
+ },
25
+ cause: thrown
26
+ };
27
+ });
28
+ }
29
+ async function runReportResult(report, opts) {
30
+ const startedAt = Date.now();
31
+ const generatedAt = new Date(startedAt).toISOString();
32
+ const inputHash = await computeInputHash({
33
+ id: report.id,
34
+ site: opts.ctx.site,
35
+ window: opts.ctx.window,
36
+ params: opts.ctx.params,
37
+ registryVersion: opts.ctx.registryVersion
38
+ });
39
+ const steps = report.plan(opts.ctx.params, opts.ctx.window);
40
+ const outcomes = await Promise.all(steps.map((s) => executeStep(opts.source, opts.analyzers, s)));
41
+ const required = new Map(steps.filter((s) => s.required).map((s) => [s.key, s]));
42
+ const errored = outcomes.filter((o) => o.state.status === "error");
43
+ for (const o of errored) if (required.has(o.state.key)) return err(analysisErrors.requiredStepFailed(report.id, o.state.key, o.state.error ?? "unknown error", o.cause));
44
+ const resultsByKey = {};
45
+ for (const o of outcomes) if (o.result) resultsByKey[o.state.key] = o.result;
46
+ const sections = report.reduce(resultsByKey, opts.ctx).sections;
47
+ const degraded = errored.length > 0;
48
+ const stepStates = outcomes.map((o) => o.state);
49
+ return ok({
50
+ id: report.id,
51
+ site: opts.ctx.site,
52
+ inputHash,
53
+ generatedAt,
54
+ window: opts.ctx.window,
55
+ sections,
56
+ meta: {
57
+ durationMs: Date.now() - startedAt,
58
+ rowsScanned: 0,
59
+ degraded,
60
+ steps: stepStates
61
+ }
62
+ });
63
+ }
64
+ async function runReport(report, opts) {
65
+ return unwrapResult(await runReportResult(report, opts), analysisErrorToException);
66
+ }
67
+ async function dryRunReport(report, ctx) {
68
+ return {
69
+ steps: report.plan(ctx.params, ctx.window).map((s) => ({
70
+ key: s.key,
71
+ type: s.type
72
+ })),
73
+ windowResolved: {
74
+ start: ctx.window.start,
75
+ end: ctx.window.end,
76
+ days: ctx.window.days
77
+ }
78
+ };
79
+ }
80
+ export { dryRunReport, runReport, runReportResult };
@@ -0,0 +1,22 @@
1
+ function reportRows(result) {
2
+ return result?.results ?? [];
3
+ }
4
+ function sectionCoverage(result) {
5
+ return result ? "full" : "partial";
6
+ }
7
+ function sectionArtifact(result, analyzer, params = {}) {
8
+ return result ? {
9
+ analyzer,
10
+ params: {
11
+ type: analyzer,
12
+ ...params
13
+ }
14
+ } : void 0;
15
+ }
16
+ function truncation(total, kept) {
17
+ return total > kept ? {
18
+ kept,
19
+ total
20
+ } : void 0;
21
+ }
22
+ export { reportRows, sectionArtifact, sectionCoverage, truncation };
@@ -0,0 +1,34 @@
1
+ interface SitemapHealthRow {
2
+ path: string;
3
+ errors: number;
4
+ warnings: number;
5
+ }
6
+ interface SitemapHealthInput extends SitemapHealthRow {
7
+ isPending?: boolean;
8
+ type?: string;
9
+ contents?: Array<{
10
+ submitted: number | string;
11
+ }>;
12
+ }
13
+ interface SitemapDelta {
14
+ path: string;
15
+ errorDelta: number;
16
+ warningDelta: number;
17
+ newErrors: number;
18
+ newWarnings: number;
19
+ }
20
+ interface SitemapHealthTotals {
21
+ totalSitemaps: number;
22
+ totalUrls: number;
23
+ totalErrors: number;
24
+ totalWarnings: number;
25
+ pendingCount: number;
26
+ indexSitemaps: number;
27
+ }
28
+ interface SitemapHealthDiff {
29
+ changed: boolean;
30
+ deltas: SitemapDelta[];
31
+ totals: SitemapHealthTotals;
32
+ }
33
+ declare function diffSitemapHealth(prev: readonly SitemapHealthRow[], curr: readonly SitemapHealthInput[]): SitemapHealthDiff;
34
+ export { SitemapDelta, SitemapHealthDiff, SitemapHealthInput, SitemapHealthRow, SitemapHealthTotals, diffSitemapHealth };
@@ -0,0 +1,37 @@
1
+ function urlCountOf(sitemap) {
2
+ return sitemap.contents?.reduce((sum, c) => sum + (Number(c.submitted) || 0), 0) || 0;
3
+ }
4
+ function diffSitemapHealth(prev, curr) {
5
+ const previousByPath = new Map(prev.map((r) => [r.path, r]));
6
+ const deltas = [];
7
+ let changed = false;
8
+ for (const sitemap of curr) {
9
+ const prior = previousByPath.get(sitemap.path);
10
+ const errorDelta = prior ? sitemap.errors - (prior.errors || 0) : 0;
11
+ const warningDelta = prior ? sitemap.warnings - (prior.warnings || 0) : 0;
12
+ if (errorDelta !== 0 || warningDelta !== 0) {
13
+ changed = true;
14
+ deltas.push({
15
+ path: sitemap.path,
16
+ errorDelta,
17
+ warningDelta,
18
+ newErrors: sitemap.errors,
19
+ newWarnings: sitemap.warnings
20
+ });
21
+ }
22
+ }
23
+ const totals = {
24
+ totalSitemaps: curr.length,
25
+ totalUrls: curr.reduce((sum, s) => sum + urlCountOf(s), 0),
26
+ totalErrors: curr.reduce((sum, s) => sum + s.errors, 0),
27
+ totalWarnings: curr.reduce((sum, s) => sum + s.warnings, 0),
28
+ pendingCount: curr.filter((s) => s.isPending).length,
29
+ indexSitemaps: curr.filter((s) => s.type === "sitemapIndex").length
30
+ };
31
+ return {
32
+ changed,
33
+ deltas,
34
+ totals
35
+ };
36
+ }
37
+ export { diffSitemapHealth };
@@ -1,6 +1,5 @@
1
1
  import { BuilderState } from "gscdump/query";
2
- import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
3
- import { PlannerCapabilities } from "gscdump/query/plan";
2
+ import { AnalysisQuerySource } from "@gscdump/engine/source";
4
3
  interface SyncedRange {
5
4
  oldestDateSynced: string | null;
6
5
  newestDateSynced: string | null;
@@ -24,14 +23,4 @@ interface CompositeSourceOptions {
24
23
  site: SyncedRange;
25
24
  }
26
25
  declare function createCompositeSource(opts: CompositeSourceOptions): AnalysisQuerySource;
27
- /**
28
- * Permissive defaults: in-memory sources are usually test doubles, so they
29
- * advertise every capability unless the test explicitly narrows them.
30
- */
31
- declare const IN_MEMORY_DEFAULT_CAPABILITIES: PlannerCapabilities;
32
- interface InMemoryQuerySourceOptions {
33
- queryRows: (state: BuilderState) => Promise<QueryRow[]> | QueryRow[];
34
- capabilities?: PlannerCapabilities;
35
- }
36
- declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource;
37
- export { CompositeSourceOptions, IN_MEMORY_DEFAULT_CAPABILITIES, InMemoryQuerySourceOptions, SyncedRange, createCompositeSource, createInMemoryQuerySource };
26
+ export { CompositeSourceOptions, SyncedRange, createCompositeSource };
@@ -41,20 +41,4 @@ function createCompositeSource(opts) {
41
41
  executeSql: engineExecuteSql ? (sql, params, execOpts) => engineExecuteSql(sql, params, execOpts) : void 0
42
42
  };
43
43
  }
44
- const IN_MEMORY_DEFAULT_CAPABILITIES = {
45
- regex: true,
46
- multiDataset: true,
47
- comparisonJoin: true,
48
- windowTotals: true
49
- };
50
- function createInMemoryQuerySource(options) {
51
- return {
52
- name: "memory",
53
- kind: "in-memory",
54
- capabilities: options.capabilities ?? IN_MEMORY_DEFAULT_CAPABILITIES,
55
- async queryRows(state) {
56
- return await options.queryRows(state);
57
- }
58
- };
59
- }
60
- export { IN_MEMORY_DEFAULT_CAPABILITIES, createCompositeSource, createInMemoryQuerySource };
44
+ export { createCompositeSource, hasGapInCoveredSpans, shouldRouteToLive };
@@ -0,0 +1,14 @@
1
+ import { BuilderState } from "gscdump/query";
2
+ import { AnalysisQuerySource, QueryRow } from "@gscdump/engine/source";
3
+ import { PlannerCapabilities } from "gscdump/query/plan";
4
+ /**
5
+ * Permissive defaults: in-memory sources are usually test doubles, so they
6
+ * advertise every capability unless the test explicitly narrows them.
7
+ */
8
+ declare const IN_MEMORY_DEFAULT_CAPABILITIES: PlannerCapabilities;
9
+ interface InMemoryQuerySourceOptions {
10
+ queryRows: (state: BuilderState) => Promise<QueryRow[]> | QueryRow[];
11
+ capabilities?: PlannerCapabilities;
12
+ }
13
+ declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource;
14
+ export { IN_MEMORY_DEFAULT_CAPABILITIES, InMemoryQuerySourceOptions, createInMemoryQuerySource };
@@ -0,0 +1,17 @@
1
+ const IN_MEMORY_DEFAULT_CAPABILITIES = {
2
+ regex: true,
3
+ multiDataset: true,
4
+ comparisonJoin: true,
5
+ windowTotals: true
6
+ };
7
+ function createInMemoryQuerySource(options) {
8
+ return {
9
+ name: "memory",
10
+ kind: "in-memory",
11
+ capabilities: options.capabilities ?? IN_MEMORY_DEFAULT_CAPABILITIES,
12
+ async queryRows(state) {
13
+ return await options.queryRows(state);
14
+ }
15
+ };
16
+ }
17
+ export { IN_MEMORY_DEFAULT_CAPABILITIES, createInMemoryQuerySource };
@@ -1,2 +1,3 @@
1
- import { CompositeSourceOptions, IN_MEMORY_DEFAULT_CAPABILITIES, InMemoryQuerySourceOptions, SyncedRange, createCompositeSource, createInMemoryQuerySource } from "../_chunks/index2.mjs";
1
+ import { CompositeSourceOptions, SyncedRange, createCompositeSource } from "./composite.mjs";
2
+ import { IN_MEMORY_DEFAULT_CAPABILITIES, InMemoryQuerySourceOptions, createInMemoryQuerySource } from "./in-memory.mjs";
2
3
  export { type CompositeSourceOptions, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type SyncedRange, createCompositeSource, createInMemoryQuerySource };
@@ -1,2 +1,3 @@
1
- import { IN_MEMORY_DEFAULT_CAPABILITIES, createCompositeSource, createInMemoryQuerySource } from "../_chunks/source.mjs";
1
+ import { createCompositeSource } from "./composite.mjs";
2
+ import { IN_MEMORY_DEFAULT_CAPABILITIES, createInMemoryQuerySource } from "./in-memory.mjs";
2
3
  export { IN_MEMORY_DEFAULT_CAPABILITIES, createCompositeSource, createInMemoryQuerySource };
@@ -0,0 +1,3 @@
1
+ import { Analyzer } from "@gscdump/engine/analyzer";
2
+ declare const SQL_ANALYZERS: readonly Analyzer[];
3
+ export { SQL_ANALYZERS };
@@ -0,0 +1,3 @@
1
+ import { ALL_ANALYZERS } from "./analyzer/all.mjs";
2
+ const SQL_ANALYZERS = ALL_ANALYZERS.flatMap((d) => d.sql ? [d.sql] : []);
3
+ export { SQL_ANALYZERS };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Domain row shapes + analysis utilities. Analyzer-call contracts
3
+ * (`AnalysisParams`, `AnalysisResult`, `AnalysisTool`, `num`) live in
4
+ * `@gscdump/engine/analysis-types`.
5
+ */
6
+ type SortOrder = 'asc' | 'desc';
7
+ /** Base search metrics */
8
+ interface BaseMetrics {
9
+ clicks: number;
10
+ impressions: number;
11
+ ctr: number;
12
+ position: number;
13
+ }
14
+ /** Keyword row from query */
15
+ interface QueriesRow extends BaseMetrics {
16
+ query: string;
17
+ page?: string;
18
+ }
19
+ /** Page row from query */
20
+ interface PageRow extends BaseMetrics {
21
+ page: string;
22
+ }
23
+ /** Row with both query and page dimensions, both required */
24
+ interface QueryPageRow extends BaseMetrics {
25
+ query: string;
26
+ page: string;
27
+ }
28
+ /** Date row from query */
29
+ interface DateRow extends BaseMetrics {
30
+ date: string;
31
+ }
32
+ /** Create a generic sorter for any metric type */
33
+ declare function createSorter<T, M extends string>(getValue: (item: T, metric: M) => number, defaultMetric: M, defaultOrder?: SortOrder): (items: T[], sortBy?: M, sortOrder?: SortOrder) => T[];
34
+ export { BaseMetrics, DateRow, PageRow, QueriesRow, QueryPageRow, SortOrder, createSorter };
package/dist/types.mjs ADDED
@@ -0,0 +1,21 @@
1
+ function buildPeriodMap(rows, key, value, filter) {
2
+ const out = /* @__PURE__ */ new Map();
3
+ for (const row of rows) {
4
+ if (filter && !filter(row)) continue;
5
+ out.set(key(row), value(row));
6
+ }
7
+ return out;
8
+ }
9
+ function createSorter(getValue, defaultMetric, defaultOrder = "desc") {
10
+ return (items, sortBy = defaultMetric, sortOrder = defaultOrder) => {
11
+ const mult = sortOrder === "desc" ? -1 : 1;
12
+ return [...items].sort((a, b) => (getValue(a, sortBy) - getValue(b, sortBy)) * mult);
13
+ };
14
+ }
15
+ function createMetricSorter(defaultMetric, orderByMetric) {
16
+ return (items, sortBy = defaultMetric) => {
17
+ const mult = orderByMetric[sortBy] === "desc" ? -1 : 1;
18
+ return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
19
+ };
20
+ }
21
+ export { buildPeriodMap, createMetricSorter, createSorter };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "1.4.0",
4
+ "version": "1.4.2",
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",
@@ -57,9 +57,9 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "pluralize": "^8.0.0",
60
- "@gscdump/engine-gsc-api": "^1.4.0",
61
- "@gscdump/engine": "^1.4.0",
62
- "gscdump": "^1.4.0"
60
+ "@gscdump/engine": "^1.4.2",
61
+ "@gscdump/engine-gsc-api": "^1.4.2",
62
+ "gscdump": "^1.4.2"
63
63
  },
64
64
  "devDependencies": {
65
65
  "vitest": "^4.1.10"
@@ -1,5 +0,0 @@
1
- import { Analyzer } from "@gscdump/engine/analyzer";
2
- declare const ROW_ANALYZERS: readonly Analyzer[];
3
- declare const SQL_ANALYZERS: readonly Analyzer[];
4
- declare const defaultAnalyzerRegistry: import("@gscdump/engine/analyzer").AnalyzerRegistry;
5
- export { ROW_ANALYZERS, SQL_ANALYZERS, defaultAnalyzerRegistry };