@gscdump/analysis 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@ const movers = analyzeMovers(currentRows, previousRows)
41
41
  const decay = analyzeDecay(currentRows, previousRows)
42
42
  ```
43
43
 
44
- Meta-analyses live in the report layer. Run a composed, ranked priority list via `runReport`:
44
+ Meta-analyses live in the report layer. Run a composed evidence report via `runReport`:
45
45
 
46
46
  ```ts
47
47
  import {
@@ -51,14 +51,14 @@ import {
51
51
  } from '@gscdump/analysis'
52
52
  import { resolveWindow } from '@gscdump/engine/period'
53
53
 
54
- const report = defaultReportRegistry.getReport('priority')!
55
- const window = resolveWindow({ preset: 'last-28d', comparison: 'prev-period' })
54
+ const report = defaultReportRegistry.getReport('health')!
55
+ const window = resolveWindow({ preset: 'last-28d', comparison: 'none' })
56
56
  const result = await runReport(report, {
57
57
  source,
58
58
  analyzers: defaultAnalyzerRegistry,
59
59
  ctx: { site: siteUrl, window, params: {}, registryVersion: defaultReportRegistry.version },
60
60
  })
61
- // result.sections[0].findings — ranked priority actions, page+query keyed.
61
+ // result.sections[0].findings — bounded evidence, page+query keyed.
62
62
  ```
63
63
 
64
64
  Source adapters compose a GSC client + analyzer in one call:
@@ -1,7 +1,7 @@
1
- import { percentDifference } from "../scoring.mjs";
2
1
  import { parseJsonRows, rowString } from "../analyzer/row-values.mjs";
3
2
  import { queriesQueryState } from "../analyzer/adapt-rows.mjs";
4
3
  import { buildPeriodMap } from "../types.mjs";
4
+ import { percentDifference } from "../scoring.mjs";
5
5
  import { defineAnalyzer } from "@gscdump/engine/analyzer";
6
6
  import { comparisonOf } from "@gscdump/engine/period";
7
7
  import { enumeratePartitions } from "@gscdump/engine/planner";
package/dist/index.d.mts CHANGED
@@ -2,7 +2,6 @@ import { ROW_ANALYZERS } from "./analyzer/row-analyzers.mjs";
2
2
  import { SQL_ANALYZERS } from "./sql-analyzers.mjs";
3
3
  import { defaultAnalyzerRegistry } from "./default-registry.mjs";
4
4
  import { AnalysisError, AnalysisErrorKind, analysisErrorToException, analysisErrors, formatAnalysisError, isAnalysisError } from "./errors.mjs";
5
- import { ActionPriorityResult, ActionPrioritySourceState, ActionPrioritySourceStatus, ActionSource, DEFAULT_PRIORITY_SOURCES, Effort, PriorityAction, mergePriorityActions, normalizePriorityActions, scorePriorityActions } from "./action-priority.mjs";
6
5
  import { BaseMetrics, DateRow, PageRow, QueriesRow, QueryPageRow, SortOrder, createSorter } from "./types.mjs";
7
6
  import { BrandSegmentationOptions, BrandSegmentationResult, BrandSegmentationRow, BrandSummary, analyzeBrandSegmentation } from "./analyzers/brand.mjs";
8
7
  import { CannibalizationCompetitor, CannibalizationEvent, CannibalizationOptions, CannibalizationPage, CannibalizationResult, CannibalizationSortMetric, analyzeCannibalization } from "./analyzers/cannibalization.mjs";
@@ -29,5 +28,5 @@ import { Analyzer, AnalyzerCapabilityError, AnalyzerRegistry, AnalyzerRegistryIn
29
28
  import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
30
29
  import { AnalysisParams, AnalysisResult, AnalysisTool, num } from "@gscdump/engine/analysis-types";
31
30
  import { AnalysisQuerySource, AnalysisSourceKind, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, SourceCapabilities, createEngineQuerySource, queryRows, runAnalyzerWithEngine } from "@gscdump/engine/source";
32
- import { DefineReportOptions, DefinedReport, ReportAction, ReportContext, ReportFinding, ReportPlanStep, ReportResult, ReportSection } from "@gscdump/engine/report";
33
- export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, type CurrentSitemapScope, DEFAULT_PRIORITY_SOURCES, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type Effort, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type PriorityAction, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportAction, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapCollapsePolicy, type SitemapCollapseState, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions, sitemapHistoryHasCollapse };
31
+ import { DefineReportOptions, DefinedReport, ReportContext, ReportFinding, ReportPlanStep, ReportResult, ReportSection } from "@gscdump/engine/report";
32
+ export { type AnalysisError, type AnalysisErrorKind, type AnalysisParams, type AnalysisPeriod, type AnalysisQuerySource, type AnalysisResult, type AnalysisSourceKind, type AnalysisTool, type Analyzer, AnalyzerCapabilityError, type AnalyzerRegistry, type AnalyzerRegistryInit, type AnalyzerRunner, type AnalyzerVariants, type BaseMetrics, type BrandSegmentationOptions, type BrandSegmentationResult, type BrandSegmentationRow, type BrandSummary, type BrowserAnalyzeOptions, type CannibalizationCompetitor, type CannibalizationEvent, type CannibalizationOptions, type CannibalizationPage, type CannibalizationResult, type CannibalizationSortMetric, type ClusterType, type ClusteringOptions, type ClusteringResult, type ComparisonMode, type ComparisonPeriod, type CompositeSourceOptions, type ConcentrationInput, type ConcentrationItem, type ConcentrationOptions, type ConcentrationResult, type ConcentrationRiskLevel, type CurrentSitemapScope, type DateRow, type DecayInput, type DecayOptions, type DecayResult, type DecaySeriesPoint, type DecaySortMetric, type DefineAnalyzerOptions, type DefineReportOptions, type DefinedAnalyzer, type DefinedReport, type DryRunReportResult, ENGINE_QUERY_CAPABILITIES, type EngineQuerySourceOptions, type ExecuteSqlOptions, type FileSet, type FormatReportOptions, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type IntentClassification, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, NORMALIZER_VERSION, type OpportunityFactors, type OpportunityOptions, type OpportunityResult, type OpportunitySortMetric, type OpportunityWeights, type PadTimeseriesOptions, type PageRow, type Plan, type QueriesRow, type QueryPageRow, type QueryRow, REPORTS, ROW_ANALYZERS, type ReduceContext, type ReduceCtx, type Reducer, type ReportContext, type ReportFinding, type ReportPlanStep, type ReportResult, type ReportSection, type RequiredCapability, type ResolveWindowOptions, type ResolvedWindow, type RowQueriesPlan, type RunReportOptions, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, SQL_ANALYZERS, type SearchIntent, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapCollapsePolicy, type SitemapCollapseState, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceOptions, type StrikingDistanceResult, type TypedRowQuery, type WindowPreset, type ZeroClickOptions, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, sitemapHistoryHasCollapse };
package/dist/index.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { DEFAULT_PRIORITY_SOURCES, mergePriorityActions, normalizePriorityActions, scorePriorityActions } from "./action-priority.mjs";
2
1
  import { analysisErrorToException, analysisErrors, formatAnalysisError, isAnalysisError } from "./errors.mjs";
3
2
  import { analyzeBrandSegmentation } from "./analyzers/brand.mjs";
4
3
  import { createSorter } from "./types.mjs";
@@ -29,4 +28,4 @@ import { AnalyzerCapabilityError, createAnalyzerRegistry, defineAnalyzer, runAna
29
28
  import { comparisonOf, padTimeseries, periodOf, resolveWindow } from "@gscdump/engine/period";
30
29
  import { num } from "@gscdump/engine/analysis-types";
31
30
  import { ENGINE_QUERY_CAPABILITIES, createEngineQuerySource, queryRows, runAnalyzerWithEngine } from "@gscdump/engine/source";
32
- export { AnalyzerCapabilityError, DEFAULT_PRIORITY_SOURCES, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, NORMALIZER_VERSION, REPORTS, ROW_ANALYZERS, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, SQL_ANALYZERS, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions, sitemapHistoryHasCollapse };
31
+ export { AnalyzerCapabilityError, ENGINE_QUERY_CAPABILITIES, INTENT_CLASSIFIER_VERSION, IN_MEMORY_DEFAULT_CAPABILITIES, NORMALIZER_VERSION, REPORTS, ROW_ANALYZERS, SEARCH_INTENT_CODE, SITEMAP_SYNC_COLLAPSE_POLICY, SITEMAP_TRUST_COLLAPSE_POLICY, SQL_ANALYZERS, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzeOpportunity, analyzePageConcentration, analyzeSeasonality, analyzeStrikingDistance, analyzeZeroClick, classifyQueryIntent, classifySitemapCollapse, compareCurrentSitemapScope, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, encodeIntent, formatAnalysisError, formatReport, isAnalysisError, normalizeQuery, num, padTimeseries, periodOf, queryRows, resolveWindow, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, sitemapHistoryHasCollapse };
@@ -30,11 +30,6 @@ function renderSection(section, cap) {
30
30
  lines.push(` - [${f.entity.kind}] ${f.entity.value} ${metricsStr}${f.why ? ` — ${f.why}` : ""}`);
31
31
  }
32
32
  if (section.truncated && section.truncated.kept < section.truncated.total) lines.push(` … +${section.truncated.total - section.truncated.kept} more`);
33
- for (const a of section.actions) {
34
- const target = a.target ? ` ${a.target.kind}=${a.target.value}` : "";
35
- lines.push(` → ${a.kind}${target}: ${a.rationale}`);
36
- if (a.cliHint) lines.push(` $ ${a.cliHint}`);
37
- }
38
33
  lines.push("");
39
34
  return lines;
40
35
  }
@@ -4,7 +4,6 @@ import { healthReport } from "./reports/health.mjs";
4
4
  import { moversReport } from "./reports/movers.mjs";
5
5
  import { opportunitiesReport } from "./reports/opportunities.mjs";
6
6
  import { prePublishReport } from "./reports/pre-publish.mjs";
7
- import { priorityReport } from "./reports/priority.mjs";
8
7
  import { risksReport } from "./reports/risks.mjs";
9
8
  import { triageReport } from "./reports/triage.mjs";
10
9
  import { createReportRegistry } from "@gscdump/engine/report";
@@ -15,7 +14,6 @@ const REPORTS = [
15
14
  moversReport,
16
15
  opportunitiesReport,
17
16
  prePublishReport,
18
- priorityReport,
19
17
  risksReport,
20
18
  triageReport
21
19
  ];
@@ -74,7 +74,6 @@ function buildBrandSplitSection(res, max) {
74
74
  summary: { magnitudeLabel: summary ? `brand share ${(brandShare * 100).toFixed(1)}% (${summary.brandClicks} brand vs ${summary.nonBrandClicks} non-brand clicks)` : "no summary available" },
75
75
  findings,
76
76
  coverage: sectionCoverage(res),
77
- actions: [],
78
77
  artifact: sectionArtifact(res, "brand")
79
78
  };
80
79
  }
@@ -98,7 +97,6 @@ function buildConcentrationSection(res, max) {
98
97
  summary: head ? { magnitudeLabel: `HHI ${head.hhi.toFixed(0)} (${head.riskLevel}); top-N share ${(head.topNConcentration * 100).toFixed(1)}%` } : {},
99
98
  findings,
100
99
  coverage: sectionCoverage(res),
101
- actions: [],
102
100
  artifact: sectionArtifact(res, "concentration", { dimension: "keywords" })
103
101
  };
104
102
  }
@@ -73,7 +73,6 @@ function buildContentVelocity(res) {
73
73
  summary: { magnitudeLabel: `${totalNew} new keywords across ${rows.length} weeks (avg ${avgPerWeek.toFixed(1)}/wk)` },
74
74
  findings: [],
75
75
  coverage: sectionCoverage(res),
76
- actions: [],
77
76
  artifact: sectionArtifact(res, "content-velocity")
78
77
  };
79
78
  }
@@ -88,7 +87,6 @@ function buildKeywordBreadth(res) {
88
87
  summary: { magnitudeLabel: top ? `${totalPages} pages; modal bucket "${top.bucket}" (${top.pageCount} pages)` : "no data" },
89
88
  findings: [],
90
89
  coverage: sectionCoverage(res),
91
- actions: [],
92
90
  artifact: sectionArtifact(res, "keyword-breadth")
93
91
  };
94
92
  }
@@ -101,7 +99,6 @@ function buildIntentAtlas(res) {
101
99
  summary: { magnitudeLabel: `${rows.length} clusters covering ${rows.reduce((s, r) => s + r.keywordCount, 0)} keywords` },
102
100
  findings: [],
103
101
  coverage: sectionCoverage(res),
104
- actions: [],
105
102
  artifact: sectionArtifact(res, "intent-atlas")
106
103
  };
107
104
  }
@@ -137,7 +134,6 @@ function buildLongTail(res, max) {
137
134
  findings,
138
135
  truncated: truncation(rows.length, kept.length),
139
136
  coverage: sectionCoverage(res),
140
- actions: [],
141
137
  artifact: sectionArtifact(res, "long-tail")
142
138
  };
143
139
  }
@@ -82,7 +82,6 @@ function buildCtrAnomalySection(res, max) {
82
82
  findings,
83
83
  truncated: truncation(rows.length, kept.length),
84
84
  coverage: sectionCoverage(res),
85
- actions: [],
86
85
  artifact: sectionArtifact(res, "ctr-anomaly")
87
86
  };
88
87
  }
@@ -108,7 +107,6 @@ function buildChangePointSection(res, max) {
108
107
  findings,
109
108
  truncated: truncation(rows.length, kept.length),
110
109
  coverage: sectionCoverage(res),
111
- actions: [],
112
110
  artifact: sectionArtifact(res, "change-point")
113
111
  };
114
112
  }
@@ -134,7 +132,6 @@ function buildPositionVolatilitySection(res, max) {
134
132
  findings,
135
133
  truncated: truncation(rows.length, kept.length),
136
134
  coverage: sectionCoverage(res),
137
- actions: [],
138
135
  artifact: sectionArtifact(res, "position-volatility")
139
136
  };
140
137
  }
@@ -111,7 +111,6 @@ function buildMoversSection(res, direction, max, minChange) {
111
111
  findings,
112
112
  truncated: truncation(total, kept.length),
113
113
  coverage: sectionCoverage(res),
114
- actions: [],
115
114
  artifact: sectionArtifact(res, "movers")
116
115
  };
117
116
  }
@@ -162,17 +161,7 @@ function buildDeclinersSection(moversRes, decayRes, max, minChange) {
162
161
  magnitudeLabel: `${Math.round(totalLost)} clicks lost`
163
162
  },
164
163
  findings,
165
- coverage: decayRes && moversRes ? "full" : "partial",
166
- actions: lostPages.slice(0, 1).map((r) => ({
167
- kind: "analyzer",
168
- target: {
169
- kind: "page",
170
- value: r.page
171
- },
172
- params: { type: "change-point" },
173
- rationale: "Investigate the change-point on the worst-affected page",
174
- cliHint: `gscdump analyze change-point --start <date> --end <date>`
175
- }))
164
+ coverage: decayRes && moversRes ? "full" : "partial"
176
165
  };
177
166
  }
178
167
  function buildStrikingSection(res, max) {
@@ -199,7 +188,6 @@ function buildStrikingSection(res, max) {
199
188
  findings,
200
189
  truncated: truncation(rows.length, kept.length),
201
190
  coverage: sectionCoverage(res),
202
- actions: [],
203
191
  artifact: sectionArtifact(res, "striking-distance")
204
192
  };
205
193
  }
@@ -89,7 +89,6 @@ function buildStrikingSection(res, max) {
89
89
  findings,
90
90
  truncated: truncation(rows.length, kept.length),
91
91
  coverage: sectionCoverage(res),
92
- actions: [],
93
92
  artifact: sectionArtifact(res, "striking-distance")
94
93
  };
95
94
  }
@@ -117,17 +116,6 @@ function buildOpportunitySection(res, max) {
117
116
  })),
118
117
  truncated: truncation(rows.length, kept.length),
119
118
  coverage: sectionCoverage(res),
120
- actions: kept.slice(0, 1).map((r) => ({
121
- kind: "fix",
122
- target: r.page ? {
123
- kind: "page",
124
- value: r.page
125
- } : {
126
- kind: "query",
127
- value: r.keyword
128
- },
129
- rationale: "Rewrite title/description to lift CTR at this position"
130
- })),
131
119
  artifact: sectionArtifact(res, "opportunity")
132
120
  };
133
121
  }
@@ -154,7 +142,6 @@ function buildZeroClickSection(res, max) {
154
142
  findings,
155
143
  truncated: truncation(rows.length, kept.length),
156
144
  coverage: sectionCoverage(res),
157
- actions: [],
158
145
  artifact: sectionArtifact(res, "zero-click")
159
146
  };
160
147
  }
@@ -179,7 +166,6 @@ function buildMigrationSection(res, max) {
179
166
  })),
180
167
  truncated: truncation(rows.length, kept.length),
181
168
  coverage: sectionCoverage(res),
182
- actions: [],
183
169
  artifact: sectionArtifact(res, "query-migration")
184
170
  };
185
171
  }
@@ -76,10 +76,6 @@ function buildCannibalizationSection(res, matches, max) {
76
76
  findings,
77
77
  truncated: truncation(rows.length, kept.length),
78
78
  coverage: sectionCoverage(res),
79
- actions: kept.slice(0, 1).map(() => ({
80
- kind: "fix",
81
- rationale: "Decide before publishing: redirect existing page, target a different angle, or accept overlap."
82
- })),
83
79
  artifact: sectionArtifact(res, "cannibalization")
84
80
  };
85
81
  }
@@ -106,7 +102,6 @@ function buildStrikingPeersSection(res, matches, max, topic) {
106
102
  findings,
107
103
  truncated: truncation(rows.length, kept.length),
108
104
  coverage: sectionCoverage(res),
109
- actions: [],
110
105
  artifact: sectionArtifact(res, "striking-distance")
111
106
  };
112
107
  }
@@ -102,15 +102,6 @@ function buildDecaySection(res, max) {
102
102
  findings,
103
103
  truncated: truncation(rows.length, kept.length),
104
104
  coverage: sectionCoverage(res),
105
- actions: kept.slice(0, 1).map((r) => ({
106
- kind: "analyzer",
107
- target: {
108
- kind: "page",
109
- value: r.page
110
- },
111
- params: { type: "change-point" },
112
- rationale: "Investigate change-point on the worst-affected page"
113
- })),
114
105
  artifact: sectionArtifact(res, "decay")
115
106
  };
116
107
  }
@@ -140,7 +131,6 @@ function buildCannibalizationSection(res, max) {
140
131
  findings,
141
132
  truncated: truncation(rows.length, kept.length),
142
133
  coverage: sectionCoverage(res),
143
- actions: [],
144
134
  artifact: sectionArtifact(res, "cannibalization")
145
135
  };
146
136
  }
@@ -167,7 +157,6 @@ function buildDarkTrafficSection(res, max) {
167
157
  findings,
168
158
  truncated: truncation(rows.length, kept.length),
169
159
  coverage: sectionCoverage(res),
170
- actions: [],
171
160
  artifact: sectionArtifact(res, "dark-traffic")
172
161
  };
173
162
  }
@@ -194,7 +183,6 @@ function buildDeviceGapSection(res, max) {
194
183
  })),
195
184
  truncated: truncation(rows.length, kept.length),
196
185
  coverage: sectionCoverage(res),
197
- actions: [],
198
186
  artifact: sectionArtifact(res, "device-gap")
199
187
  };
200
188
  }
@@ -96,7 +96,6 @@ function buildChangePointSection(res, kind, matches, max) {
96
96
  findings,
97
97
  truncated: truncation(rows.length, kept.length),
98
98
  coverage: sectionCoverage(res),
99
- actions: [],
100
99
  artifact: sectionArtifact(res, "change-point")
101
100
  };
102
101
  }
@@ -122,7 +121,6 @@ function buildMigrationSection(res, kind, matches, max, _rawTarget) {
122
121
  findings,
123
122
  truncated: truncation(rows.length, kept.length),
124
123
  coverage: sectionCoverage(res),
125
- actions: [],
126
124
  artifact: sectionArtifact(res, "query-migration")
127
125
  };
128
126
  }
@@ -148,7 +146,6 @@ function buildVolatilitySection(res, kind, matches, max) {
148
146
  findings,
149
147
  truncated: truncation(rows.length, kept.length),
150
148
  coverage: sectionCoverage(res),
151
- actions: [],
152
149
  artifact: sectionArtifact(res, "position-volatility")
153
150
  };
154
151
  }
@@ -34,8 +34,7 @@ function unavailableSection(section, failedKeys) {
34
34
  severity: "unknown",
35
35
  summary: { magnitudeLabel: `unavailable — ${failedKeys.join(", ")} failed` },
36
36
  findings: [],
37
- coverage: "partial",
38
- actions: []
37
+ coverage: "partial"
39
38
  };
40
39
  }
41
40
  function markUnavailableSections(sections, steps, outcomes) {
package/dist/scoring.mjs CHANGED
@@ -1,15 +1,5 @@
1
- function clamp01(value) {
2
- if (value < 0) return 0;
3
- if (value > 1) return 1;
4
- return value;
5
- }
6
- function clamp(value, min, max) {
7
- if (value < min) return min;
8
- if (value > max) return max;
9
- return value;
10
- }
11
1
  function percentDifference(current, previous) {
12
2
  if (previous === 0) return current > 0 ? 100 : 0;
13
3
  return (current - previous) / previous * 100;
14
4
  }
15
- export { clamp, clamp01, percentDifference };
5
+ export { percentDifference };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "2.2.0",
4
+ "version": "3.0.0",
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": "^2.2.0",
61
- "@gscdump/engine-gsc-api": "^2.2.0",
62
- "gscdump": "^2.2.0"
60
+ "@gscdump/engine": "^3.0.0",
61
+ "@gscdump/engine-gsc-api": "^3.0.0",
62
+ "gscdump": "^3.0.0"
63
63
  },
64
64
  "devDependencies": {
65
65
  "vitest": "^4.1.10"
@@ -1,34 +0,0 @@
1
- import { AnalysisResult } from "@gscdump/engine/analysis-types";
2
- type ActionSource = 'cannibalization' | 'striking-distance' | 'ctr-anomaly' | 'change-point' | 'opportunity';
3
- type Effort = 'low' | 'medium' | 'high';
4
- interface PriorityAction {
5
- id: string;
6
- title: string;
7
- keyword: string;
8
- page: string;
9
- sources: ActionSource[];
10
- severity: number;
11
- impressions: number;
12
- impact: number;
13
- why: string;
14
- effort: Effort;
15
- priorityScore: number;
16
- data: Partial<Record<ActionSource, Record<string, unknown>>>;
17
- }
18
- type ActionPrioritySourceStatus = 'pending' | 'running' | 'done' | 'skipped' | 'error';
19
- interface ActionPrioritySourceState {
20
- source: ActionSource;
21
- status: ActionPrioritySourceStatus;
22
- count: number;
23
- error?: string;
24
- }
25
- interface ActionPriorityResult {
26
- actions: PriorityAction[];
27
- totalSignals: number;
28
- sources: ActionPrioritySourceState[];
29
- }
30
- declare const DEFAULT_PRIORITY_SOURCES: ActionSource[];
31
- declare function normalizePriorityActions(source: ActionSource, result: AnalysisResult): PriorityAction[];
32
- declare function mergePriorityActions(all: PriorityAction[]): PriorityAction[];
33
- declare function scorePriorityActions(actions: PriorityAction[]): PriorityAction[];
34
- export { ActionPriorityResult, ActionPrioritySourceState, ActionPrioritySourceStatus, ActionSource, DEFAULT_PRIORITY_SOURCES, Effort, PriorityAction, mergePriorityActions, normalizePriorityActions, scorePriorityActions };
@@ -1,201 +0,0 @@
1
- import { clamp, clamp01 } from "./scoring.mjs";
2
- const DEFAULT_PRIORITY_SOURCES = [
3
- "striking-distance",
4
- "opportunity",
5
- "cannibalization",
6
- "ctr-anomaly",
7
- "change-point"
8
- ];
9
- const EFFORT_BY_SOURCE = {
10
- "striking-distance": "low",
11
- "opportunity": "low",
12
- "cannibalization": "medium",
13
- "ctr-anomaly": "high",
14
- "change-point": "high"
15
- };
16
- const EFFORT_MULTIPLIER = {
17
- low: 1.3,
18
- medium: 1,
19
- high: .7
20
- };
21
- const EFFORT_RANK = {
22
- low: 0,
23
- medium: 1,
24
- high: 2
25
- };
26
- function idKey(keyword, page) {
27
- return `${keyword.toLowerCase()}|${page.toLowerCase()}`;
28
- }
29
- function truncate(s, n) {
30
- return s.length <= n ? s : `${s.slice(0, n - 1)}...`;
31
- }
32
- function buildAction(spec) {
33
- return {
34
- id: idKey(spec.keyword, spec.page),
35
- title: spec.title,
36
- keyword: spec.keyword,
37
- page: spec.page,
38
- sources: [spec.source],
39
- severity: spec.severity,
40
- impressions: spec.impressions,
41
- impact: spec.impact,
42
- effort: EFFORT_BY_SOURCE[spec.source],
43
- why: spec.why,
44
- priorityScore: 0,
45
- data: { [spec.source]: spec.data }
46
- };
47
- }
48
- function fromStrikingDistance(rows) {
49
- const out = [];
50
- for (const r of rows) {
51
- if (r.page == null) continue;
52
- const impact = Math.max(0, r.potentialClicks);
53
- if (impact <= 0) continue;
54
- const posScore = clamp01((20 - r.position) / 16);
55
- const imprScore = Math.min(1, r.impressions / 5e3);
56
- out.push(buildAction({
57
- source: "striking-distance",
58
- keyword: r.keyword,
59
- page: r.page,
60
- title: `Push "${truncate(r.keyword, 40)}" onto page 1`,
61
- why: `Ranks #${r.position.toFixed(1)} with ${Math.round(r.impressions)} impressions; small gains unlock page-1 clicks.`,
62
- severity: Math.round(100 * Math.sqrt(posScore * imprScore)),
63
- impressions: r.impressions,
64
- impact,
65
- data: r
66
- }));
67
- }
68
- return out;
69
- }
70
- function fromOpportunity(rows) {
71
- const out = [];
72
- for (const r of rows) {
73
- if (r.page == null) continue;
74
- const impact = Math.max(0, r.potentialClicks);
75
- if (impact <= 0) continue;
76
- out.push(buildAction({
77
- source: "opportunity",
78
- keyword: r.keyword,
79
- page: r.page,
80
- title: `Improve on-page for "${truncate(r.keyword, 40)}"`,
81
- why: `Opportunity score ${Math.round(r.opportunityScore)}; CTR ${(r.ctr * 100).toFixed(1)}% vs expected at pos ${r.position.toFixed(1)}.`,
82
- severity: Math.round(r.opportunityScore),
83
- impressions: r.impressions,
84
- impact,
85
- data: r
86
- }));
87
- }
88
- return out;
89
- }
90
- function fromCannibalization(events) {
91
- const out = [];
92
- for (const ev of events) {
93
- if (ev.severity < 30) continue;
94
- out.push(buildAction({
95
- source: "cannibalization",
96
- keyword: ev.keyword,
97
- page: ev.leaderUrl,
98
- title: `Consolidate cannibalization on "${truncate(ev.keyword, 36)}"`,
99
- why: `${ev.competitorCount} URLs split ${Math.round(ev.totalImpressions)} impressions; leader loses ~${Math.round(ev.stolenClicks)} clicks to siblings.`,
100
- severity: Math.round(ev.severity),
101
- impressions: ev.totalImpressions,
102
- impact: Math.max(0, ev.stolenClicks),
103
- data: ev
104
- }));
105
- }
106
- return out;
107
- }
108
- function fromCtrAnomaly(rows) {
109
- const out = [];
110
- let maxRaw = 0;
111
- for (const r of rows) if (r.severity > maxRaw) maxRaw = r.severity;
112
- for (const r of rows) {
113
- const impact = Math.max(0, r.clicksLost);
114
- if (impact <= 0) continue;
115
- out.push(buildAction({
116
- source: "ctr-anomaly",
117
- keyword: r.keyword,
118
- page: r.page,
119
- title: `Lift CTR on "${truncate(r.keyword, 36)}"`,
120
- why: `CTR collapsed ${r.breachDaysDown} days at flat position; ~${Math.round(r.clicksLost)} clicks lost vs baseline ${(r.baselineCtr * 100).toFixed(1)}%.`,
121
- severity: maxRaw > 0 ? Math.round(r.severity / maxRaw * 100) : 0,
122
- impressions: r.totalImpressions,
123
- impact,
124
- data: r
125
- }));
126
- }
127
- return out;
128
- }
129
- function fromChangePoint(rows) {
130
- const out = [];
131
- for (const r of rows) {
132
- if (r.direction !== "worsened") continue;
133
- const days = Math.max(1, r.totalDays / 2);
134
- const impact = Math.abs(r.leftMean - r.rightMean) * days;
135
- if (impact <= 0) continue;
136
- out.push(buildAction({
137
- source: "change-point",
138
- keyword: r.keyword,
139
- page: r.page,
140
- title: `Diagnose drop on "${truncate(r.keyword, 34)}"`,
141
- why: `Significant regression around ${r.changeDate} (${r.leftMean.toFixed(1)} -> ${r.rightMean.toFixed(1)}, LLR ${r.llr.toFixed(0)}).`,
142
- severity: clamp(Math.round(Math.log10(Math.max(10, r.llr)) / 3 * 100), 0, 100),
143
- impressions: r.totalImpressions,
144
- impact,
145
- data: r
146
- }));
147
- }
148
- return out;
149
- }
150
- function normalizePriorityActions(source, result) {
151
- const rows = result.results;
152
- if (source === "striking-distance") return fromStrikingDistance(rows);
153
- if (source === "opportunity") return fromOpportunity(rows);
154
- if (source === "cannibalization") return fromCannibalization(rows);
155
- if (source === "ctr-anomaly") return fromCtrAnomaly(rows);
156
- return fromChangePoint(rows);
157
- }
158
- function mergePriorityActions(all) {
159
- const byId = /* @__PURE__ */ new Map();
160
- for (const a of all) {
161
- const existing = byId.get(a.id);
162
- if (existing == null) {
163
- byId.set(a.id, {
164
- ...a,
165
- sources: [...a.sources],
166
- data: { ...a.data }
167
- });
168
- continue;
169
- }
170
- const mergedSources = [.../* @__PURE__ */ new Set([...existing.sources, ...a.sources])];
171
- const preferNew = a.severity > existing.severity;
172
- const mergedEffort = EFFORT_RANK[a.effort] < EFFORT_RANK[existing.effort] ? a.effort : existing.effort;
173
- byId.set(a.id, {
174
- id: existing.id,
175
- title: preferNew ? a.title : existing.title,
176
- keyword: existing.keyword,
177
- page: existing.page,
178
- sources: mergedSources,
179
- severity: Math.max(existing.severity, a.severity),
180
- impressions: Math.max(existing.impressions, a.impressions),
181
- impact: existing.impact + a.impact,
182
- why: preferNew ? a.why : existing.why,
183
- effort: mergedEffort,
184
- priorityScore: 0,
185
- data: {
186
- ...existing.data,
187
- ...a.data
188
- }
189
- });
190
- }
191
- return [...byId.values()];
192
- }
193
- function scorePriorityActions(actions) {
194
- for (const a of actions) {
195
- const mult = EFFORT_MULTIPLIER[a.effort];
196
- a.priorityScore = a.impact * (1 + a.severity / 100) * mult;
197
- }
198
- actions.sort((a, b) => b.priorityScore - a.priorityScore);
199
- return actions;
200
- }
201
- export { DEFAULT_PRIORITY_SOURCES, mergePriorityActions, normalizePriorityActions, scorePriorityActions };
@@ -1,98 +0,0 @@
1
- import { DEFAULT_PRIORITY_SOURCES, mergePriorityActions, normalizePriorityActions, scorePriorityActions } from "../../action-priority.mjs";
2
- import { defineReport } from "@gscdump/engine/report";
3
- const DEFAULT_LIMIT = 40;
4
- const DEFAULT_ACTIONS = 10;
5
- const priorityReport = defineReport({
6
- id: "priority",
7
- description: "Ranked priority actions composed from striking-distance, opportunity, cannibalization, ctr-anomaly, and change-point signals.",
8
- defaultPeriod: "last-28d",
9
- defaultComparison: "prev-period",
10
- argsSpec: {
11
- "limit": {
12
- type: "number",
13
- description: "Max actions in the section",
14
- default: DEFAULT_LIMIT
15
- },
16
- "max-actions": {
17
- type: "number",
18
- description: "Top-N rendered as ReportAction",
19
- default: DEFAULT_ACTIONS
20
- }
21
- },
22
- plan: (_params, window) => {
23
- const dates = {
24
- startDate: window.start,
25
- endDate: window.end
26
- };
27
- const cmp = window.comparison ? {
28
- prevStartDate: window.comparison.start,
29
- prevEndDate: window.comparison.end
30
- } : {};
31
- return DEFAULT_PRIORITY_SOURCES.map((source) => ({
32
- key: source,
33
- type: source,
34
- params: {
35
- ...dates,
36
- ...cmp,
37
- limit: 100
38
- },
39
- required: false,
40
- feeds: ["priority"]
41
- }));
42
- },
43
- reduce: (results, ctx) => {
44
- const limit = ctx.params.limit ?? DEFAULT_LIMIT;
45
- const maxActions = ctx.params.maxActions ?? DEFAULT_ACTIONS;
46
- const all = [];
47
- let anyMissing = false;
48
- for (const source of DEFAULT_PRIORITY_SOURCES) {
49
- const r = results[source];
50
- if (!r) {
51
- anyMissing = true;
52
- continue;
53
- }
54
- all.push(...normalizePriorityActions(source, r));
55
- }
56
- const ranked = scorePriorityActions(mergePriorityActions(all)).slice(0, limit);
57
- const findings = ranked.map((a) => ({
58
- entity: {
59
- kind: "query",
60
- value: a.keyword
61
- },
62
- metrics: {
63
- priorityScore: a.priorityScore,
64
- severity: a.severity,
65
- impact: a.impact,
66
- impressions: a.impressions
67
- },
68
- why: `${a.title} — ${a.why} (page: ${a.page})`
69
- }));
70
- const actions = ranked.slice(0, maxActions).map((a) => {
71
- const primarySource = a.sources[0];
72
- return {
73
- kind: primarySource === "cannibalization" ? "fix" : "analyzer",
74
- target: {
75
- kind: "page",
76
- value: a.page
77
- },
78
- params: { type: primarySource },
79
- rationale: a.title
80
- };
81
- });
82
- const topSeverity = ranked[0]?.severity ?? 0;
83
- return { sections: [{
84
- id: "priority",
85
- title: "Priority actions",
86
- severity: topSeverity >= 70 ? "high" : topSeverity >= 40 ? "medium" : ranked.length ? "low" : "info",
87
- summary: { magnitudeLabel: `${ranked.length} actions ranked` },
88
- findings,
89
- truncated: all.length > ranked.length ? {
90
- kept: ranked.length,
91
- total: all.length
92
- } : void 0,
93
- coverage: anyMissing ? "partial" : "full",
94
- actions
95
- }] };
96
- }
97
- });
98
- export { priorityReport };