@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.
- package/dist/action-priority.d.mts +34 -0
- package/dist/action-priority.mjs +201 -0
- package/dist/analyzer/adapt-rows.mjs +12 -0
- package/dist/analyzer/all.mjs +61 -0
- package/dist/analyzer/paginate.mjs +85 -0
- package/dist/analyzer/row-analyzers.d.mts +3 -0
- package/dist/analyzer/row-analyzers.mjs +3 -0
- package/dist/analyzer/row-values.mjs +22 -0
- package/dist/analyzers/bayesian-ctr.mjs +177 -0
- package/dist/analyzers/bipartite-pagerank.mjs +243 -0
- package/dist/analyzers/brand.d.mts +33 -0
- package/dist/analyzers/brand.mjs +154 -0
- package/dist/analyzers/cannibalization.d.mts +58 -0
- package/dist/analyzers/cannibalization.mjs +267 -0
- package/dist/analyzers/change-point.mjs +197 -0
- package/dist/analyzers/clustering.d.mts +29 -0
- package/dist/analyzers/clustering.mjs +217 -0
- package/dist/analyzers/concentration.d.mts +42 -0
- package/dist/analyzers/concentration.mjs +180 -0
- package/dist/analyzers/content-velocity.mjs +87 -0
- package/dist/analyzers/ctr-anomaly.mjs +189 -0
- package/dist/analyzers/ctr-curve.mjs +128 -0
- package/dist/analyzers/dark-traffic.mjs +130 -0
- package/dist/analyzers/data-detail.mjs +44 -0
- package/dist/analyzers/data-query.mjs +44 -0
- package/dist/analyzers/decay.d.mts +38 -0
- package/dist/analyzers/decay.mjs +187 -0
- package/dist/analyzers/device-gap.mjs +135 -0
- package/dist/analyzers/intent-atlas.mjs +175 -0
- package/dist/analyzers/keyword-breadth.mjs +115 -0
- package/dist/analyzers/long-tail.mjs +150 -0
- package/dist/analyzers/movers.d.mts +43 -0
- package/dist/analyzers/movers.mjs +283 -0
- package/dist/analyzers/opportunity.d.mts +32 -0
- package/dist/analyzers/opportunity.mjs +212 -0
- package/dist/analyzers/position-distribution.mjs +57 -0
- package/dist/analyzers/position-volatility.mjs +143 -0
- package/dist/analyzers/query-migration.mjs +178 -0
- package/dist/analyzers/seasonality.d.mts +28 -0
- package/dist/analyzers/seasonality.mjs +136 -0
- package/dist/analyzers/stl-decompose.mjs +187 -0
- package/dist/analyzers/striking-distance.d.mts +54 -0
- package/dist/analyzers/striking-distance.mjs +82 -0
- package/dist/analyzers/survival.mjs +200 -0
- package/dist/analyzers/trends.mjs +156 -0
- package/dist/analyzers/zero-click.d.mts +17 -0
- package/dist/analyzers/zero-click.mjs +122 -0
- package/dist/browser.d.mts +5 -0
- package/dist/browser.mjs +11 -0
- package/dist/default-registry.d.mts +3 -1
- package/dist/default-registry.mjs +5 -1
- package/dist/errors.d.mts +51 -1
- package/dist/index.d.mts +30 -494
- package/dist/index.mjs +28 -49
- package/dist/query/analyzers.mjs +301 -0
- package/dist/query/index.mjs +4 -0
- package/dist/query/intent.d.mts +20 -0
- package/dist/query/intent.mjs +139 -0
- package/dist/query/normalize.d.mts +15 -0
- package/dist/query/normalize.mjs +204 -0
- package/dist/report/format.d.mts +7 -0
- package/dist/report/format.mjs +45 -0
- package/dist/report/index.d.mts +4 -1
- package/dist/report/index.mjs +4 -1
- package/dist/report/registry.d.mts +4 -0
- package/dist/report/registry.mjs +26 -0
- package/dist/report/reports/brand.mjs +104 -0
- package/dist/report/reports/growth.mjs +144 -0
- package/dist/report/reports/health.mjs +141 -0
- package/dist/report/reports/movers.mjs +203 -0
- package/dist/report/reports/opportunities.mjs +184 -0
- package/dist/report/reports/pre-publish.mjs +111 -0
- package/dist/report/reports/priority.mjs +97 -0
- package/dist/report/reports/risks.mjs +201 -0
- package/dist/report/reports/triage.mjs +155 -0
- package/dist/report/require.mjs +15 -0
- package/dist/report/resolve-target.d.mts +29 -0
- package/dist/report/resolve-target.mjs +32 -0
- package/dist/{_chunks/index.d.mts → report/runtime.d.mts} +2 -37
- package/dist/report/runtime.mjs +80 -0
- package/dist/report/sections.mjs +22 -0
- package/dist/sitemap-health.d.mts +34 -0
- package/dist/sitemap-health.mjs +37 -0
- package/dist/{_chunks/index2.d.mts → source/composite.d.mts} +2 -13
- package/dist/{_chunks/source.mjs → source/composite.mjs} +1 -17
- package/dist/source/in-memory.d.mts +14 -0
- package/dist/source/in-memory.mjs +17 -0
- package/dist/source/index.d.mts +2 -1
- package/dist/source/index.mjs +2 -1
- package/dist/sql-analyzers.d.mts +3 -0
- package/dist/sql-analyzers.mjs +3 -0
- package/dist/types.d.mts +34 -0
- package/dist/types.mjs +21 -0
- package/package.json +4 -4
- package/dist/_chunks/default-registry.d.mts +0 -5
- package/dist/_chunks/default-registry.mjs +0 -5183
- package/dist/_chunks/errors.d.mts +0 -52
- package/dist/_chunks/report.mjs +0 -1710
- /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 "
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 };
|
package/dist/source/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import { CompositeSourceOptions,
|
|
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 };
|
package/dist/source/index.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import {
|
|
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 };
|
package/dist/types.d.mts
ADDED
|
@@ -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.
|
|
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
|
|
61
|
-
"@gscdump/engine": "^1.4.
|
|
62
|
-
"gscdump": "^1.4.
|
|
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 };
|