@gscdump/analysis 2.3.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 +4 -4
- package/dist/analyzers/movers.mjs +1 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.mjs +1 -2
- package/dist/report/format.mjs +0 -5
- package/dist/report/registry.mjs +0 -2
- package/dist/report/reports/brand.mjs +0 -2
- package/dist/report/reports/growth.mjs +0 -4
- package/dist/report/reports/health.mjs +0 -3
- package/dist/report/reports/movers.mjs +1 -13
- package/dist/report/reports/opportunities.mjs +0 -14
- package/dist/report/reports/pre-publish.mjs +0 -5
- package/dist/report/reports/risks.mjs +0 -12
- package/dist/report/reports/triage.mjs +0 -3
- package/dist/report/runtime.mjs +1 -2
- package/dist/scoring.mjs +1 -11
- package/package.json +4 -4
- package/dist/action-priority.d.mts +0 -34
- package/dist/action-priority.mjs +0 -201
- package/dist/report/reports/priority.mjs +0 -98
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
|
|
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('
|
|
55
|
-
const window = resolveWindow({ preset: 'last-28d', comparison: '
|
|
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 —
|
|
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,
|
|
33
|
-
export { type
|
|
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,
|
|
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 };
|
package/dist/report/format.mjs
CHANGED
|
@@ -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
|
}
|
package/dist/report/registry.mjs
CHANGED
|
@@ -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
|
}
|
package/dist/report/runtime.mjs
CHANGED
|
@@ -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 {
|
|
5
|
+
export { percentDifference };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
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": "^
|
|
61
|
-
"gscdump": "^
|
|
62
|
-
"
|
|
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 };
|
package/dist/action-priority.mjs
DELETED
|
@@ -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 };
|