@gscdump/analysis 0.25.13 → 0.26.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.
@@ -3,6 +3,7 @@ import { comparisonOf, defaultEndDate, padTimeseries, periodOf } from "@gscdump/
3
3
  import { enumeratePartitions } from "@gscdump/engine/planner";
4
4
  import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
5
5
  import { num } from "@gscdump/engine/analysis-types";
6
+ import { err, ok, unwrapResult } from "gscdump/result";
6
7
  import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
7
8
  import { MS_PER_DAY, daysAgo, toIsoDate } from "gscdump";
8
9
  import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
@@ -445,12 +446,72 @@ function pagesQueryState(period, limit = DEFAULT_LIMIT$1) {
445
446
  function datesQueryState(period, limit = DEFAULT_LIMIT$1) {
446
447
  return gsc.select(date).where(between(date, period.startDate, period.endDate)).limit(limit).getState();
447
448
  }
449
+ const analysisErrors = {
450
+ missingReportParam(report, param, message) {
451
+ return {
452
+ kind: "missing-report-param",
453
+ report,
454
+ param,
455
+ message
456
+ };
457
+ },
458
+ missingComparisonWindow(report, message) {
459
+ return {
460
+ kind: "missing-comparison-window",
461
+ report,
462
+ message
463
+ };
464
+ },
465
+ missingBrandTerms() {
466
+ return {
467
+ kind: "missing-brand-terms",
468
+ message: "Brand analysis requires brandTerms"
469
+ };
470
+ },
471
+ unknownReport(report, available) {
472
+ return {
473
+ kind: "unknown-report",
474
+ report,
475
+ available,
476
+ message: `unknown report "${report}"; available: ${available.join(", ")}`
477
+ };
478
+ },
479
+ unknownAnalyzer(analyzer) {
480
+ return {
481
+ kind: "unknown-analyzer",
482
+ analyzer,
483
+ message: `unknown analyzer "${analyzer}"`
484
+ };
485
+ },
486
+ requiredStepFailed(report, stepKey, stepError, cause) {
487
+ return {
488
+ kind: "required-step-failed",
489
+ report,
490
+ stepKey,
491
+ stepError,
492
+ cause,
493
+ message: `runReport(${report}): required step "${stepKey}" failed: ${stepError}`
494
+ };
495
+ }
496
+ };
497
+ function analysisErrorToException(error) {
498
+ const exception = new Error(error.message);
499
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
500
+ exception.analysisError = error;
501
+ return exception;
502
+ }
448
503
  function escapeRegexAlt(s) {
449
504
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
450
505
  }
451
506
  function str$21(v) {
452
507
  return v == null ? "" : String(v);
453
508
  }
509
+ function requireBrandTermsResult(brandTerms) {
510
+ return brandTerms?.length ? ok(brandTerms) : err(analysisErrors.missingBrandTerms());
511
+ }
512
+ function requireBrandTerms(brandTerms) {
513
+ return unwrapResult(requireBrandTermsResult(brandTerms), analysisErrorToException);
514
+ }
454
515
  function analyzeBrandSegmentation(keywords, options) {
455
516
  const { brandTerms, minImpressions = 10 } = options;
456
517
  const lowerBrandTerms = brandTerms.map((t) => t.toLowerCase());
@@ -479,11 +540,11 @@ function analyzeBrandSegmentation(keywords, options) {
479
540
  const brandAnalyzer = defineAnalyzer({
480
541
  id: "brand",
481
542
  buildSql(params) {
482
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
543
+ const brandTerms = requireBrandTerms(params.brandTerms);
483
544
  const { startDate, endDate } = periodOf(params);
484
545
  const minImpressions = params.minImpressions ?? 10;
485
546
  const limit = params.limit ?? 1e4;
486
- const regex = `(${params.brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
547
+ const regex = `(${brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
487
548
  return {
488
549
  sql: `
489
550
  WITH agg AS (
@@ -558,9 +619,9 @@ const brandAnalyzer = defineAnalyzer({
558
619
  return { queries: queriesQueryState(periodOf(params), params.limit) };
559
620
  },
560
621
  reduceRows(rows, params) {
561
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
622
+ const brandTerms = requireBrandTerms(params.brandTerms);
562
623
  const result = analyzeBrandSegmentation(Array.isArray(rows) ? rows : [], {
563
- brandTerms: params.brandTerms,
624
+ brandTerms,
564
625
  minImpressions: params.minImpressions
565
626
  });
566
627
  return {
@@ -3,6 +3,7 @@ import { comparisonOf, defaultEndDate, padTimeseries, periodOf } from "@gscdump/
3
3
  import { enumeratePartitions } from "@gscdump/engine/planner";
4
4
  import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
5
5
  import { num } from "@gscdump/engine/analysis-types";
6
+ import { err, ok, unwrapResult } from "gscdump/result";
6
7
  import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
7
8
  import { MS_PER_DAY, daysAgo, toIsoDate } from "gscdump";
8
9
  import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
@@ -445,12 +446,72 @@ function pagesQueryState(period, limit = DEFAULT_LIMIT$1) {
445
446
  function datesQueryState(period, limit = DEFAULT_LIMIT$1) {
446
447
  return gsc.select(date).where(between(date, period.startDate, period.endDate)).limit(limit).getState();
447
448
  }
449
+ const analysisErrors = {
450
+ missingReportParam(report, param, message) {
451
+ return {
452
+ kind: "missing-report-param",
453
+ report,
454
+ param,
455
+ message
456
+ };
457
+ },
458
+ missingComparisonWindow(report, message) {
459
+ return {
460
+ kind: "missing-comparison-window",
461
+ report,
462
+ message
463
+ };
464
+ },
465
+ missingBrandTerms() {
466
+ return {
467
+ kind: "missing-brand-terms",
468
+ message: "Brand analysis requires brandTerms"
469
+ };
470
+ },
471
+ unknownReport(report, available) {
472
+ return {
473
+ kind: "unknown-report",
474
+ report,
475
+ available,
476
+ message: `unknown report "${report}"; available: ${available.join(", ")}`
477
+ };
478
+ },
479
+ unknownAnalyzer(analyzer) {
480
+ return {
481
+ kind: "unknown-analyzer",
482
+ analyzer,
483
+ message: `unknown analyzer "${analyzer}"`
484
+ };
485
+ },
486
+ requiredStepFailed(report, stepKey, stepError, cause) {
487
+ return {
488
+ kind: "required-step-failed",
489
+ report,
490
+ stepKey,
491
+ stepError,
492
+ cause,
493
+ message: `runReport(${report}): required step "${stepKey}" failed: ${stepError}`
494
+ };
495
+ }
496
+ };
497
+ function analysisErrorToException(error) {
498
+ const exception = new Error(error.message);
499
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
500
+ exception.analysisError = error;
501
+ return exception;
502
+ }
448
503
  function escapeRegexAlt(s) {
449
504
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
450
505
  }
451
506
  function str$21(v) {
452
507
  return v == null ? "" : String(v);
453
508
  }
509
+ function requireBrandTermsResult(brandTerms) {
510
+ return brandTerms?.length ? ok(brandTerms) : err(analysisErrors.missingBrandTerms());
511
+ }
512
+ function requireBrandTerms(brandTerms) {
513
+ return unwrapResult(requireBrandTermsResult(brandTerms), analysisErrorToException);
514
+ }
454
515
  function analyzeBrandSegmentation(keywords, options) {
455
516
  const { brandTerms, minImpressions = 10 } = options;
456
517
  const lowerBrandTerms = brandTerms.map((t) => t.toLowerCase());
@@ -479,11 +540,11 @@ function analyzeBrandSegmentation(keywords, options) {
479
540
  const brandAnalyzer = defineAnalyzer({
480
541
  id: "brand",
481
542
  buildSql(params) {
482
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
543
+ const brandTerms = requireBrandTerms(params.brandTerms);
483
544
  const { startDate, endDate } = periodOf(params);
484
545
  const minImpressions = params.minImpressions ?? 10;
485
546
  const limit = params.limit ?? 1e4;
486
- const regex = `(${params.brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
547
+ const regex = `(${brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
487
548
  return {
488
549
  sql: `
489
550
  WITH agg AS (
@@ -558,9 +619,9 @@ const brandAnalyzer = defineAnalyzer({
558
619
  return { queries: queriesQueryState(periodOf(params), params.limit) };
559
620
  },
560
621
  reduceRows(rows, params) {
561
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
622
+ const brandTerms = requireBrandTerms(params.brandTerms);
562
623
  const result = analyzeBrandSegmentation(Array.isArray(rows) ? rows : [], {
563
- brandTerms: params.brandTerms,
624
+ brandTerms,
564
625
  minImpressions: params.minImpressions
565
626
  });
566
627
  return {
@@ -0,0 +1,52 @@
1
+ type AnalysisErrorKind = 'missing-report-param' | 'missing-comparison-window' | 'missing-brand-terms' | 'unknown-report' | 'unknown-analyzer' | 'required-step-failed';
2
+ type AnalysisError = {
3
+ kind: 'missing-report-param';
4
+ report: string;
5
+ param: string;
6
+ message: string;
7
+ } | {
8
+ kind: 'missing-comparison-window';
9
+ report: string;
10
+ message: string;
11
+ } | {
12
+ kind: 'missing-brand-terms';
13
+ message: string;
14
+ } | {
15
+ kind: 'unknown-report';
16
+ report: string;
17
+ available: readonly string[];
18
+ message: string;
19
+ } | {
20
+ kind: 'unknown-analyzer';
21
+ analyzer: string;
22
+ message: string;
23
+ } | {
24
+ kind: 'required-step-failed';
25
+ report: string;
26
+ stepKey: string;
27
+ stepError: string;
28
+ message: string;
29
+ cause?: unknown;
30
+ };
31
+ declare const analysisErrors: {
32
+ readonly missingReportParam: (report: string, param: string, message: string) => AnalysisError;
33
+ readonly missingComparisonWindow: (report: string, message: string) => AnalysisError;
34
+ readonly missingBrandTerms: () => AnalysisError;
35
+ readonly unknownReport: (report: string, available: readonly string[]) => AnalysisError;
36
+ readonly unknownAnalyzer: (analyzer: string) => AnalysisError;
37
+ readonly requiredStepFailed: (report: string, stepKey: string, stepError: string, cause?: unknown) => AnalysisError;
38
+ };
39
+ declare function isAnalysisError(value: unknown): value is AnalysisError;
40
+ /** The human-readable rendering of an `AnalysisError`, for logs and string sinks. */
41
+ declare function formatAnalysisError(error: AnalysisError): string;
42
+ /**
43
+ * Re-raises an `AnalysisError` value as a generic `Error`, stashing the union
44
+ * under `.analysisError` for stack-walking and preserving the original `cause`
45
+ * (so a `required-step-failed` keeps the underlying thrown value reachable).
46
+ * Used by the throwing wrappers over the `Result`-returning cores. The thrown
47
+ * `.message` is kept verbatim from the union, so existing message-regex
48
+ * assertions (`/--target/`, `/comparison window/`, `/required step "k"/`)
49
+ * continue to match.
50
+ */
51
+ declare function analysisErrorToException(error: AnalysisError): Error;
52
+ export { AnalysisError, AnalysisErrorKind, analysisErrorToException, analysisErrors, formatAnalysisError, isAnalysisError };
@@ -0,0 +1,69 @@
1
+ const analysisErrors = {
2
+ missingReportParam(report, param, message) {
3
+ return {
4
+ kind: "missing-report-param",
5
+ report,
6
+ param,
7
+ message
8
+ };
9
+ },
10
+ missingComparisonWindow(report, message) {
11
+ return {
12
+ kind: "missing-comparison-window",
13
+ report,
14
+ message
15
+ };
16
+ },
17
+ missingBrandTerms() {
18
+ return {
19
+ kind: "missing-brand-terms",
20
+ message: "Brand analysis requires brandTerms"
21
+ };
22
+ },
23
+ unknownReport(report, available) {
24
+ return {
25
+ kind: "unknown-report",
26
+ report,
27
+ available,
28
+ message: `unknown report "${report}"; available: ${available.join(", ")}`
29
+ };
30
+ },
31
+ unknownAnalyzer(analyzer) {
32
+ return {
33
+ kind: "unknown-analyzer",
34
+ analyzer,
35
+ message: `unknown analyzer "${analyzer}"`
36
+ };
37
+ },
38
+ requiredStepFailed(report, stepKey, stepError, cause) {
39
+ return {
40
+ kind: "required-step-failed",
41
+ report,
42
+ stepKey,
43
+ stepError,
44
+ cause,
45
+ message: `runReport(${report}): required step "${stepKey}" failed: ${stepError}`
46
+ };
47
+ }
48
+ };
49
+ const ANALYSIS_ERROR_KINDS = new Set([
50
+ "missing-report-param",
51
+ "missing-comparison-window",
52
+ "missing-brand-terms",
53
+ "unknown-report",
54
+ "unknown-analyzer",
55
+ "required-step-failed"
56
+ ]);
57
+ function isAnalysisError(value) {
58
+ return typeof value === "object" && value !== null && ANALYSIS_ERROR_KINDS.has(value.kind) && typeof value.message === "string";
59
+ }
60
+ function formatAnalysisError(error) {
61
+ return error.message;
62
+ }
63
+ function analysisErrorToException(error) {
64
+ const exception = new Error(error.message);
65
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
66
+ exception.analysisError = error;
67
+ return exception;
68
+ }
69
+ export { analysisErrorToException, analysisErrors, formatAnalysisError, isAnalysisError };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Analyzer, Analyzer as Analyzer$1, AnalyzerCapabilityError, AnalyzerRegistry, AnalyzerRegistry as AnalyzerRegistry$1, AnalyzerRegistryInit, AnalyzerVariants, DefineAnalyzerOptions, DefinedAnalyzer, Plan, ReduceContext, ReduceCtx, Reducer, RequiredCapability, RowQueriesPlan, SqlExtraQuery, SqlPlan, SqlPlanSpec, TypedRowQuery, createAnalyzerRegistry, defineAnalyzer, runAnalyzerFromSource } from "@gscdump/engine/analyzer";
2
2
  import { AnalysisPeriod, ComparisonMode, ComparisonPeriod, PadTimeseriesOptions, ResolveWindowOptions, ResolvedWindow, WindowPreset, comparisonOf, padTimeseries, periodOf, resolveWindow, windowToComparisonPeriod, windowToPeriod } from "@gscdump/engine/period";
3
3
  import { AnalysisParams, AnalysisParams as AnalysisParams$1, AnalysisResult, AnalysisResult as AnalysisResult$1, AnalysisTool, num } from "@gscdump/engine/analysis-types";
4
+ import { Result } from "gscdump/result";
4
5
  import { BuilderState } from "gscdump/query";
5
6
  import { AnalysisQuerySource, AnalysisQuerySource as AnalysisQuerySource$1, AnalysisSourceKind, AttachedTableRunner, AttachedTableRunner as AnalyzerRunner, AttachedTableSourceOptions, AttachedTableSourceOptions as BrowserAnalyzeOptions, ENGINE_QUERY_CAPABILITIES, EngineQuerySourceOptions, ExecuteSqlOptions, FileSet, QueryRow, QueryRow as QueryRow$1, SourceCapabilities, TypedQuery, createEngineQuerySource, queryComparisonRows, queryRows, rewriteForTableSource, runAnalyzerWithEngine, typedQuery } from "@gscdump/engine/source";
6
7
  import { DefineReportOptions, DefinedReport, DefinedReport as DefinedReport$1, ReportAction, ReportContext, ReportContext as ReportContext$1, ReportFinding, ReportParams, ReportPlanStep, ReportResult, ReportResult as ReportResult$1, ReportSection } from "@gscdump/engine/report";
@@ -388,6 +389,57 @@ declare function analyzeInBrowser(runner: AttachedTableRunner, opts: AttachedTab
388
389
  * with the flat `ROW_ANALYZERS` / `SQL_ANALYZERS` arrays.
389
390
  */
390
391
  declare const defaultAnalyzerRegistry: import("@gscdump/engine/analyzer").AnalyzerRegistry;
392
+ type AnalysisErrorKind = 'missing-report-param' | 'missing-comparison-window' | 'missing-brand-terms' | 'unknown-report' | 'unknown-analyzer' | 'required-step-failed';
393
+ type AnalysisError = {
394
+ kind: 'missing-report-param';
395
+ report: string;
396
+ param: string;
397
+ message: string;
398
+ } | {
399
+ kind: 'missing-comparison-window';
400
+ report: string;
401
+ message: string;
402
+ } | {
403
+ kind: 'missing-brand-terms';
404
+ message: string;
405
+ } | {
406
+ kind: 'unknown-report';
407
+ report: string;
408
+ available: readonly string[];
409
+ message: string;
410
+ } | {
411
+ kind: 'unknown-analyzer';
412
+ analyzer: string;
413
+ message: string;
414
+ } | {
415
+ kind: 'required-step-failed';
416
+ report: string;
417
+ stepKey: string;
418
+ stepError: string;
419
+ message: string;
420
+ cause?: unknown;
421
+ };
422
+ declare const analysisErrors: {
423
+ readonly missingReportParam: (report: string, param: string, message: string) => AnalysisError;
424
+ readonly missingComparisonWindow: (report: string, message: string) => AnalysisError;
425
+ readonly missingBrandTerms: () => AnalysisError;
426
+ readonly unknownReport: (report: string, available: readonly string[]) => AnalysisError;
427
+ readonly unknownAnalyzer: (analyzer: string) => AnalysisError;
428
+ readonly requiredStepFailed: (report: string, stepKey: string, stepError: string, cause?: unknown) => AnalysisError;
429
+ };
430
+ declare function isAnalysisError(value: unknown): value is AnalysisError;
431
+ /** The human-readable rendering of an `AnalysisError`, for logs and string sinks. */
432
+ declare function formatAnalysisError(error: AnalysisError): string;
433
+ /**
434
+ * Re-raises an `AnalysisError` value as a generic `Error`, stashing the union
435
+ * under `.analysisError` for stack-walking and preserving the original `cause`
436
+ * (so a `required-step-failed` keeps the underlying thrown value reachable).
437
+ * Used by the throwing wrappers over the `Result`-returning cores. The thrown
438
+ * `.message` is kept verbatim from the union, so existing message-regex
439
+ * assertions (`/--target/`, `/comparison window/`, `/required step "k"/`)
440
+ * continue to match.
441
+ */
442
+ declare function analysisErrorToException(error: AnalysisError): Error;
391
443
  /**
392
444
  * Produce a canonical form of a search query for grouping near-duplicates.
393
445
  * Idempotent: `normalizeQuery(normalizeQuery(q)) === normalizeQuery(q)`.
@@ -406,11 +458,28 @@ interface RunReportOptions<P extends ReportParams = ReportParams> {
406
458
  ctx: ReportContext$1<P>;
407
459
  }
408
460
  /**
409
- * Run a defined report against a source. Steps execute in parallel via
410
- * `Promise.all`. The report's `reduce` is invoked with a results bag that
411
- * only contains successful steps sections that depended on a failed step
412
- * should set their own `coverage: 'partial'` (the runtime additionally
413
- * marks `meta.degraded` when any step errored).
461
+ * `Result`-returning core for {@link runReport}. Models the one
462
+ * caller-actionable failure of a structurally-valid report run: a required
463
+ * step's analyzer threw (`required-step-failed`, with the underlying error as
464
+ * `cause`). Hosts can map that to a 4xx/partial response instead of catching an
465
+ * untyped `Error`.
466
+ *
467
+ * Steps execute in parallel via `Promise.all`. The report's `reduce` is invoked
468
+ * with a results bag that only contains successful steps — sections that
469
+ * depended on a failed step should set their own `coverage: 'partial'` (the
470
+ * runtime additionally marks `meta.degraded` when any step errored).
471
+ *
472
+ * The report's own `plan()` param-validation throws (`--target` etc.) are
473
+ * defects from this core's perspective and still propagate; those are modelled
474
+ * at the report-definition boundary, not here.
475
+ */
476
+ declare function runReportResult<P extends ReportParams = ReportParams>(report: DefinedReport$1<P>, opts: RunReportOptions<P>): Promise<Result<ReportResult$1, AnalysisError>>;
477
+ /**
478
+ * Throwing wrapper over {@link runReportResult}, preserving the historical
479
+ * call-site ergonomics (a required-step failure rejects). The thrown message is
480
+ * kept verbatim (`runReport(id): required step "k" failed: ...`) so existing
481
+ * assertions hold; the typed `AnalysisError` is reachable via `.analysisError`
482
+ * and the original failure via `.cause`.
414
483
  */
415
484
  declare function runReport<P extends ReportParams = ReportParams>(report: DefinedReport$1<P>, opts: RunReportOptions<P>): Promise<ReportResult$1>;
416
485
  interface DryRunReportResult {
@@ -498,4 +567,4 @@ interface InMemoryQuerySourceOptions {
498
567
  }
499
568
  declare function createInMemoryQuerySource(options: InMemoryQuerySourceOptions): AnalysisQuerySource$1;
500
569
  declare const SQL_ANALYZERS: readonly Analyzer$1[];
501
- export { type ActionPriorityResult, type ActionPrioritySourceState, type ActionPrioritySourceStatus, type ActionSource, 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 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, 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, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, type OpportunityResult, 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, SQL_ANALYZERS, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceResult, type TypedQuery, type TypedRowQuery, type WindowPreset, type ZeroClickResult, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, formatReport, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryComparisonRows, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, scorePriorityActions, typedQuery, windowToComparisonPeriod, windowToPeriod };
570
+ 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 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, 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, IN_MEMORY_DEFAULT_CAPABILITIES, type InMemoryQuerySourceOptions, type KeywordCluster, type MonthlyData, type MoverData, type MoversInput, type MoversOptions, type MoversResult, type MoversSortMetric, type OpportunityResult, 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, SQL_ANALYZERS, type SeasonalityMetric, type SeasonalityOptions, type SeasonalityResult, type SitemapDelta, type SitemapHealthDiff, type SitemapHealthInput, type SitemapHealthRow, type SitemapHealthTotals, type SortOrder, type SourceCapabilities, type SqlExtraQuery, type SqlPlan, type SqlPlanSpec, type StrikingDistanceInputRow, type StrikingDistanceResult, type TypedQuery, type TypedRowQuery, type WindowPreset, type ZeroClickResult, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryComparisonRows, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions, typedQuery, windowToComparisonPeriod, windowToPeriod };
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { comparisonOf, comparisonOf as comparisonOf$1, defaultEndDate, padTimese
3
3
  import { enumeratePartitions } from "@gscdump/engine/planner";
4
4
  import { METRIC_EXPR } from "@gscdump/engine/sql-fragments";
5
5
  import { num, num as num$1 } from "@gscdump/engine/analysis-types";
6
+ import { err, ok, unwrapResult } from "gscdump/result";
6
7
  import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
7
8
  import { MS_PER_DAY, daysAgo, toIsoDate } from "gscdump";
8
9
  import { buildExtrasQueries, buildTotalsSql, mergeExtras, pgResolverAdapter, resolveComparisonSQL, resolveToSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
@@ -662,12 +663,86 @@ function pagesQueryState(period, limit = DEFAULT_LIMIT$2) {
662
663
  function datesQueryState(period, limit = DEFAULT_LIMIT$2) {
663
664
  return gsc.select(date).where(between(date, period.startDate, period.endDate)).limit(limit).getState();
664
665
  }
666
+ const analysisErrors = {
667
+ missingReportParam(report, param, message) {
668
+ return {
669
+ kind: "missing-report-param",
670
+ report,
671
+ param,
672
+ message
673
+ };
674
+ },
675
+ missingComparisonWindow(report, message) {
676
+ return {
677
+ kind: "missing-comparison-window",
678
+ report,
679
+ message
680
+ };
681
+ },
682
+ missingBrandTerms() {
683
+ return {
684
+ kind: "missing-brand-terms",
685
+ message: "Brand analysis requires brandTerms"
686
+ };
687
+ },
688
+ unknownReport(report, available) {
689
+ return {
690
+ kind: "unknown-report",
691
+ report,
692
+ available,
693
+ message: `unknown report "${report}"; available: ${available.join(", ")}`
694
+ };
695
+ },
696
+ unknownAnalyzer(analyzer) {
697
+ return {
698
+ kind: "unknown-analyzer",
699
+ analyzer,
700
+ message: `unknown analyzer "${analyzer}"`
701
+ };
702
+ },
703
+ requiredStepFailed(report, stepKey, stepError, cause) {
704
+ return {
705
+ kind: "required-step-failed",
706
+ report,
707
+ stepKey,
708
+ stepError,
709
+ cause,
710
+ message: `runReport(${report}): required step "${stepKey}" failed: ${stepError}`
711
+ };
712
+ }
713
+ };
714
+ const ANALYSIS_ERROR_KINDS = new Set([
715
+ "missing-report-param",
716
+ "missing-comparison-window",
717
+ "missing-brand-terms",
718
+ "unknown-report",
719
+ "unknown-analyzer",
720
+ "required-step-failed"
721
+ ]);
722
+ function isAnalysisError(value) {
723
+ return typeof value === "object" && value !== null && ANALYSIS_ERROR_KINDS.has(value.kind) && typeof value.message === "string";
724
+ }
725
+ function formatAnalysisError(error) {
726
+ return error.message;
727
+ }
728
+ function analysisErrorToException(error) {
729
+ const exception = new Error(error.message);
730
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
731
+ exception.analysisError = error;
732
+ return exception;
733
+ }
665
734
  function escapeRegexAlt(s) {
666
735
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
667
736
  }
668
737
  function str$21(v) {
669
738
  return v == null ? "" : String(v);
670
739
  }
740
+ function requireBrandTermsResult(brandTerms) {
741
+ return brandTerms?.length ? ok(brandTerms) : err(analysisErrors.missingBrandTerms());
742
+ }
743
+ function requireBrandTerms(brandTerms) {
744
+ return unwrapResult(requireBrandTermsResult(brandTerms), analysisErrorToException);
745
+ }
671
746
  function analyzeBrandSegmentation(keywords, options) {
672
747
  const { brandTerms, minImpressions = 10 } = options;
673
748
  const lowerBrandTerms = brandTerms.map((t) => t.toLowerCase());
@@ -696,11 +771,11 @@ function analyzeBrandSegmentation(keywords, options) {
696
771
  const brandAnalyzer = defineAnalyzer$1({
697
772
  id: "brand",
698
773
  buildSql(params) {
699
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
774
+ const brandTerms = requireBrandTerms(params.brandTerms);
700
775
  const { startDate, endDate } = periodOf$1(params);
701
776
  const minImpressions = params.minImpressions ?? 10;
702
777
  const limit = params.limit ?? 1e4;
703
- const regex = `(${params.brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
778
+ const regex = `(${brandTerms.map((t) => escapeRegexAlt(t.toLowerCase())).join("|")})`;
704
779
  return {
705
780
  sql: `
706
781
  WITH agg AS (
@@ -775,9 +850,9 @@ const brandAnalyzer = defineAnalyzer$1({
775
850
  return { queries: queriesQueryState(periodOf$1(params), params.limit) };
776
851
  },
777
852
  reduceRows(rows, params) {
778
- if (!params.brandTerms?.length) throw new Error("Brand analysis requires brandTerms");
853
+ const brandTerms = requireBrandTerms(params.brandTerms);
779
854
  const result = analyzeBrandSegmentation(Array.isArray(rows) ? rows : [], {
780
- brandTerms: params.brandTerms,
855
+ brandTerms,
781
856
  minImpressions: params.minImpressions
782
857
  });
783
858
  return {
@@ -5347,6 +5422,18 @@ function formatNumber(n) {
5347
5422
  if (Number.isInteger(n)) return String(n);
5348
5423
  return n.toFixed(2);
5349
5424
  }
5425
+ function requireReportParamResult(report, param, value, message) {
5426
+ return value && value.trim() ? ok(value) : err(analysisErrors.missingReportParam(report, param, message));
5427
+ }
5428
+ function requireReportParam(report, param, value, message) {
5429
+ return unwrapResult(requireReportParamResult(report, param, value, message), analysisErrorToException);
5430
+ }
5431
+ function requireComparisonWindowResult(report, window, message) {
5432
+ return window.comparison ? ok(window.comparison) : err(analysisErrors.missingComparisonWindow(report, message));
5433
+ }
5434
+ function requireComparisonWindow(report, window, message) {
5435
+ return unwrapResult(requireComparisonWindowResult(report, window, message), analysisErrorToException);
5436
+ }
5350
5437
  const DEFAULT_MAX$7 = 5;
5351
5438
  const brandReport = defineReport({
5352
5439
  id: "brand",
@@ -5366,8 +5453,7 @@ const brandReport = defineReport({
5366
5453
  }
5367
5454
  },
5368
5455
  plan: (params, window) => {
5369
- if (!params.brandTerms || !params.brandTerms.trim()) throw new Error("brand report requires --brand-terms <comma,separated,list>");
5370
- const brandTerms = params.brandTerms.split(",").map((t) => t.trim()).filter(Boolean);
5456
+ const brandTerms = requireReportParam("brand", "brand-terms", params.brandTerms, "brand report requires --brand-terms <comma,separated,list>").split(",").map((t) => t.trim()).filter(Boolean);
5371
5457
  const dates = {
5372
5458
  startDate: window.start,
5373
5459
  endDate: window.end
@@ -5789,14 +5875,14 @@ const moversReport = defineReport({
5789
5875
  }
5790
5876
  },
5791
5877
  plan: (_params, window) => {
5792
- if (!window.comparison) throw new Error("movers report requires a comparison window — pass --vs prev-period");
5878
+ const comparison = requireComparisonWindow("movers", window, "movers report requires a comparison window — pass --vs prev-period");
5793
5879
  const cur = {
5794
5880
  startDate: window.start,
5795
5881
  endDate: window.end
5796
5882
  };
5797
5883
  const prev = {
5798
- prevStartDate: window.comparison.start,
5799
- prevEndDate: window.comparison.end
5884
+ prevStartDate: comparison.start,
5885
+ prevEndDate: comparison.end
5800
5886
  };
5801
5887
  return [
5802
5888
  {
@@ -6204,7 +6290,7 @@ const prePublishReport = defineReport({
6204
6290
  }
6205
6291
  },
6206
6292
  plan: (params, window) => {
6207
- if (!params.topic) throw new Error("pre-publish report requires --topic <topic-or-url>");
6293
+ requireReportParam("pre-publish", "topic", params.topic, "pre-publish report requires --topic <topic-or-url>");
6208
6294
  const dates = {
6209
6295
  startDate: window.start,
6210
6296
  endDate: window.end
@@ -6410,14 +6496,14 @@ const risksReport = defineReport({
6410
6496
  default: DEFAULT_MAX$1
6411
6497
  } },
6412
6498
  plan: (_params, window) => {
6413
- if (!window.comparison) throw new Error("risks report requires a comparison window — pass --vs prev-period");
6499
+ const comparison = requireComparisonWindow("risks", window, "risks report requires a comparison window — pass --vs prev-period");
6414
6500
  const dates = {
6415
6501
  startDate: window.start,
6416
6502
  endDate: window.end
6417
6503
  };
6418
6504
  const prev = {
6419
- prevStartDate: window.comparison.start,
6420
- prevEndDate: window.comparison.end
6505
+ prevStartDate: comparison.start,
6506
+ prevEndDate: comparison.end
6421
6507
  };
6422
6508
  return [
6423
6509
  {
@@ -6674,7 +6760,7 @@ const triageReport = defineReport({
6674
6760
  }
6675
6761
  },
6676
6762
  plan: (params, window) => {
6677
- if (!params.target) throw new Error("triage report requires --target <page-or-query>");
6763
+ requireReportParam("triage", "target", params.target, "triage report requires --target <page-or-query>");
6678
6764
  const dates = {
6679
6765
  startDate: window.start,
6680
6766
  endDate: window.end
@@ -6844,17 +6930,20 @@ async function executeStep(source, analyzers, step) {
6844
6930
  status: "done"
6845
6931
  },
6846
6932
  result
6847
- })).catch((err) => {
6848
- const message = err?.message ?? String(err);
6849
- return { state: {
6850
- key: step.key,
6851
- type: step.type,
6852
- status: "error",
6853
- error: message
6854
- } };
6933
+ })).catch((thrown) => {
6934
+ const message = thrown?.message ?? String(thrown);
6935
+ return {
6936
+ state: {
6937
+ key: step.key,
6938
+ type: step.type,
6939
+ status: "error",
6940
+ error: message
6941
+ },
6942
+ cause: thrown
6943
+ };
6855
6944
  });
6856
6945
  }
6857
- async function runReport(report, opts) {
6946
+ async function runReportResult(report, opts) {
6858
6947
  const startedAt = Date.now();
6859
6948
  const generatedAt = new Date(startedAt).toISOString();
6860
6949
  const inputHash = await computeInputHash({
@@ -6868,13 +6957,13 @@ async function runReport(report, opts) {
6868
6957
  const outcomes = await Promise.all(steps.map((s) => executeStep(opts.source, opts.analyzers, s)));
6869
6958
  const required = new Map(steps.filter((s) => s.required).map((s) => [s.key, s]));
6870
6959
  const errored = outcomes.filter((o) => o.state.status === "error");
6871
- for (const o of errored) if (required.has(o.state.key)) throw new Error(`runReport(${report.id}): required step "${o.state.key}" failed: ${o.state.error}`);
6960
+ 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));
6872
6961
  const resultsByKey = {};
6873
6962
  for (const o of outcomes) if (o.result) resultsByKey[o.state.key] = o.result;
6874
6963
  const sections = report.reduce(resultsByKey, opts.ctx).sections;
6875
6964
  const degraded = errored.length > 0;
6876
6965
  const stepStates = outcomes.map((o) => o.state);
6877
- return {
6966
+ return ok({
6878
6967
  id: report.id,
6879
6968
  site: opts.ctx.site,
6880
6969
  inputHash,
@@ -6887,7 +6976,10 @@ async function runReport(report, opts) {
6887
6976
  degraded,
6888
6977
  steps: stepStates
6889
6978
  }
6890
- };
6979
+ });
6980
+ }
6981
+ async function runReport(report, opts) {
6982
+ return unwrapResult(await runReportResult(report, opts), analysisErrorToException);
6891
6983
  }
6892
6984
  async function dryRunReport(report, ctx) {
6893
6985
  return {
@@ -6995,4 +7087,4 @@ function createInMemoryQuerySource(options) {
6995
7087
  };
6996
7088
  }
6997
7089
  const SQL_ANALYZERS = ALL_ANALYZERS.flatMap((d) => d.sql ? [d.sql] : []);
6998
- export { AnalyzerCapabilityError, DEFAULT_PRIORITY_SOURCES, ENGINE_QUERY_CAPABILITIES, IN_MEMORY_DEFAULT_CAPABILITIES, REPORTS, ROW_ANALYZERS, SQL_ANALYZERS, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, formatReport, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryComparisonRows, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, scorePriorityActions, typedQuery, windowToComparisonPeriod, windowToPeriod };
7090
+ export { AnalyzerCapabilityError, DEFAULT_PRIORITY_SOURCES, ENGINE_QUERY_CAPABILITIES, IN_MEMORY_DEFAULT_CAPABILITIES, REPORTS, ROW_ANALYZERS, SQL_ANALYZERS, analysisErrorToException, analysisErrors, analyzeBrandSegmentation, analyzeCannibalization, analyzeClustering, analyzeConcentration, analyzeDecay, analyzeInBrowser, analyzeKeywordConcentration, analyzeMovers, analyzePageConcentration, analyzeSeasonality, comparisonOf, createAnalyzerRegistry, createCompositeSource, createEngineQuerySource, createInMemoryQuerySource, createSorter, defaultAnalyzerRegistry, defaultReportRegistry, defineAnalyzer, diffSitemapHealth, dryRunReport, formatAnalysisError, formatReport, isAnalysisError, mergePriorityActions, normalizePriorityActions, normalizeQuery, num, padTimeseries, periodOf, queryComparisonRows, queryRows, resolveWindow, rewriteForTableSource, runAnalyzerFromSource, runAnalyzerWithEngine, runReport, runReportResult, scorePriorityActions, typedQuery, windowToComparisonPeriod, windowToPeriod };
@@ -1,4 +1,5 @@
1
1
  import { DefinedReport, ReportContext, ReportParams, ReportResult } from "@gscdump/engine/report";
2
+ import { Result } from "gscdump/result";
2
3
  import { AnalyzerRegistry } from "@gscdump/engine/analyzer";
3
4
  import { AnalysisQuerySource } from "@gscdump/engine/source";
4
5
  interface FormatReportOptions {
@@ -36,17 +37,63 @@ interface ResolveTargetResult {
36
37
  unresolved: boolean;
37
38
  }
38
39
  declare function resolveTarget(opts: ResolveTargetInput): ResolveTargetResult;
40
+ type AnalysisError = {
41
+ kind: 'missing-report-param';
42
+ report: string;
43
+ param: string;
44
+ message: string;
45
+ } | {
46
+ kind: 'missing-comparison-window';
47
+ report: string;
48
+ message: string;
49
+ } | {
50
+ kind: 'missing-brand-terms';
51
+ message: string;
52
+ } | {
53
+ kind: 'unknown-report';
54
+ report: string;
55
+ available: readonly string[];
56
+ message: string;
57
+ } | {
58
+ kind: 'unknown-analyzer';
59
+ analyzer: string;
60
+ message: string;
61
+ } | {
62
+ kind: 'required-step-failed';
63
+ report: string;
64
+ stepKey: string;
65
+ stepError: string;
66
+ message: string;
67
+ cause?: unknown;
68
+ };
39
69
  interface RunReportOptions<P extends ReportParams = ReportParams> {
40
70
  source: AnalysisQuerySource;
41
71
  analyzers: AnalyzerRegistry;
42
72
  ctx: ReportContext<P>;
43
73
  }
44
74
  /**
45
- * Run a defined report against a source. Steps execute in parallel via
46
- * `Promise.all`. The report's `reduce` is invoked with a results bag that
47
- * only contains successful steps sections that depended on a failed step
48
- * should set their own `coverage: 'partial'` (the runtime additionally
49
- * marks `meta.degraded` when any step errored).
75
+ * `Result`-returning core for {@link runReport}. Models the one
76
+ * caller-actionable failure of a structurally-valid report run: a required
77
+ * step's analyzer threw (`required-step-failed`, with the underlying error as
78
+ * `cause`). Hosts can map that to a 4xx/partial response instead of catching an
79
+ * untyped `Error`.
80
+ *
81
+ * Steps execute in parallel via `Promise.all`. The report's `reduce` is invoked
82
+ * with a results bag that only contains successful steps — sections that
83
+ * depended on a failed step should set their own `coverage: 'partial'` (the
84
+ * runtime additionally marks `meta.degraded` when any step errored).
85
+ *
86
+ * The report's own `plan()` param-validation throws (`--target` etc.) are
87
+ * defects from this core's perspective and still propagate; those are modelled
88
+ * at the report-definition boundary, not here.
89
+ */
90
+ declare function runReportResult<P extends ReportParams = ReportParams>(report: DefinedReport<P>, opts: RunReportOptions<P>): Promise<Result<ReportResult, AnalysisError>>;
91
+ /**
92
+ * Throwing wrapper over {@link runReportResult}, preserving the historical
93
+ * call-site ergonomics (a required-step failure rejects). The thrown message is
94
+ * kept verbatim (`runReport(id): required step "k" failed: ...`) so existing
95
+ * assertions hold; the typed `AnalysisError` is reachable via `.analysisError`
96
+ * and the original failure via `.cause`.
50
97
  */
51
98
  declare function runReport<P extends ReportParams = ReportParams>(report: DefinedReport<P>, opts: RunReportOptions<P>): Promise<ReportResult>;
52
99
  interface DryRunReportResult {
@@ -67,4 +114,4 @@ interface DryRunReportResult {
67
114
  * Useful right now only as an "is this report wired up correctly?" check.
68
115
  */
69
116
  declare function dryRunReport<P extends ReportParams = ReportParams>(report: DefinedReport<P>, ctx: ReportContext<P>): Promise<DryRunReportResult>;
70
- export { type DryRunReportResult, type FormatReportOptions, REPORTS, type ResolveTargetInput, type ResolveTargetKind, type ResolveTargetResult, type RunReportOptions, defaultReportRegistry, dryRunReport, formatReport, resolveTarget, runReport };
117
+ export { type DryRunReportResult, type FormatReportOptions, REPORTS, type ResolveTargetInput, type ResolveTargetKind, type ResolveTargetResult, type RunReportOptions, defaultReportRegistry, dryRunReport, formatReport, resolveTarget, runReport, runReportResult };
@@ -1,4 +1,5 @@
1
1
  import { computeInputHash, createReportRegistry, defineReport } from "@gscdump/engine/report";
2
+ import { err, ok, unwrapResult } from "gscdump/result";
2
3
  import { runAnalyzerFromSource } from "@gscdump/engine/analyzer";
3
4
  const SEVERITY_GLYPH = {
4
5
  info: "i",
@@ -44,6 +45,72 @@ function formatNumber(n) {
44
45
  if (Number.isInteger(n)) return String(n);
45
46
  return n.toFixed(2);
46
47
  }
48
+ const analysisErrors = {
49
+ missingReportParam(report, param, message) {
50
+ return {
51
+ kind: "missing-report-param",
52
+ report,
53
+ param,
54
+ message
55
+ };
56
+ },
57
+ missingComparisonWindow(report, message) {
58
+ return {
59
+ kind: "missing-comparison-window",
60
+ report,
61
+ message
62
+ };
63
+ },
64
+ missingBrandTerms() {
65
+ return {
66
+ kind: "missing-brand-terms",
67
+ message: "Brand analysis requires brandTerms"
68
+ };
69
+ },
70
+ unknownReport(report, available) {
71
+ return {
72
+ kind: "unknown-report",
73
+ report,
74
+ available,
75
+ message: `unknown report "${report}"; available: ${available.join(", ")}`
76
+ };
77
+ },
78
+ unknownAnalyzer(analyzer) {
79
+ return {
80
+ kind: "unknown-analyzer",
81
+ analyzer,
82
+ message: `unknown analyzer "${analyzer}"`
83
+ };
84
+ },
85
+ requiredStepFailed(report, stepKey, stepError, cause) {
86
+ return {
87
+ kind: "required-step-failed",
88
+ report,
89
+ stepKey,
90
+ stepError,
91
+ cause,
92
+ message: `runReport(${report}): required step "${stepKey}" failed: ${stepError}`
93
+ };
94
+ }
95
+ };
96
+ function analysisErrorToException(error) {
97
+ const exception = new Error(error.message);
98
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
99
+ exception.analysisError = error;
100
+ return exception;
101
+ }
102
+ function requireReportParamResult(report, param, value, message) {
103
+ return value && value.trim() ? ok(value) : err(analysisErrors.missingReportParam(report, param, message));
104
+ }
105
+ function requireReportParam(report, param, value, message) {
106
+ return unwrapResult(requireReportParamResult(report, param, value, message), analysisErrorToException);
107
+ }
108
+ function requireComparisonWindowResult(report, window, message) {
109
+ return window.comparison ? ok(window.comparison) : err(analysisErrors.missingComparisonWindow(report, message));
110
+ }
111
+ function requireComparisonWindow(report, window, message) {
112
+ return unwrapResult(requireComparisonWindowResult(report, window, message), analysisErrorToException);
113
+ }
47
114
  const DEFAULT_MAX$7 = 5;
48
115
  const brandReport = defineReport({
49
116
  id: "brand",
@@ -63,8 +130,7 @@ const brandReport = defineReport({
63
130
  }
64
131
  },
65
132
  plan: (params, window) => {
66
- if (!params.brandTerms || !params.brandTerms.trim()) throw new Error("brand report requires --brand-terms <comma,separated,list>");
67
- const brandTerms = params.brandTerms.split(",").map((t) => t.trim()).filter(Boolean);
133
+ const brandTerms = requireReportParam("brand", "brand-terms", params.brandTerms, "brand report requires --brand-terms <comma,separated,list>").split(",").map((t) => t.trim()).filter(Boolean);
68
134
  const dates = {
69
135
  startDate: window.start,
70
136
  endDate: window.end
@@ -486,14 +552,14 @@ const moversReport = defineReport({
486
552
  }
487
553
  },
488
554
  plan: (_params, window) => {
489
- if (!window.comparison) throw new Error("movers report requires a comparison window — pass --vs prev-period");
555
+ const comparison = requireComparisonWindow("movers", window, "movers report requires a comparison window — pass --vs prev-period");
490
556
  const cur = {
491
557
  startDate: window.start,
492
558
  endDate: window.end
493
559
  };
494
560
  const prev = {
495
- prevStartDate: window.comparison.start,
496
- prevEndDate: window.comparison.end
561
+ prevStartDate: comparison.start,
562
+ prevEndDate: comparison.end
497
563
  };
498
564
  return [
499
565
  {
@@ -901,7 +967,7 @@ const prePublishReport = defineReport({
901
967
  }
902
968
  },
903
969
  plan: (params, window) => {
904
- if (!params.topic) throw new Error("pre-publish report requires --topic <topic-or-url>");
970
+ requireReportParam("pre-publish", "topic", params.topic, "pre-publish report requires --topic <topic-or-url>");
905
971
  const dates = {
906
972
  startDate: window.start,
907
973
  endDate: window.end
@@ -1316,14 +1382,14 @@ const risksReport = defineReport({
1316
1382
  default: DEFAULT_MAX$1
1317
1383
  } },
1318
1384
  plan: (_params, window) => {
1319
- if (!window.comparison) throw new Error("risks report requires a comparison window — pass --vs prev-period");
1385
+ const comparison = requireComparisonWindow("risks", window, "risks report requires a comparison window — pass --vs prev-period");
1320
1386
  const dates = {
1321
1387
  startDate: window.start,
1322
1388
  endDate: window.end
1323
1389
  };
1324
1390
  const prev = {
1325
- prevStartDate: window.comparison.start,
1326
- prevEndDate: window.comparison.end
1391
+ prevStartDate: comparison.start,
1392
+ prevEndDate: comparison.end
1327
1393
  };
1328
1394
  return [
1329
1395
  {
@@ -1580,7 +1646,7 @@ const triageReport = defineReport({
1580
1646
  }
1581
1647
  },
1582
1648
  plan: (params, window) => {
1583
- if (!params.target) throw new Error("triage report requires --target <page-or-query>");
1649
+ requireReportParam("triage", "target", params.target, "triage report requires --target <page-or-query>");
1584
1650
  const dates = {
1585
1651
  startDate: window.start,
1586
1652
  endDate: window.end
@@ -1750,17 +1816,20 @@ async function executeStep(source, analyzers, step) {
1750
1816
  status: "done"
1751
1817
  },
1752
1818
  result
1753
- })).catch((err) => {
1754
- const message = err?.message ?? String(err);
1755
- return { state: {
1756
- key: step.key,
1757
- type: step.type,
1758
- status: "error",
1759
- error: message
1760
- } };
1819
+ })).catch((thrown) => {
1820
+ const message = thrown?.message ?? String(thrown);
1821
+ return {
1822
+ state: {
1823
+ key: step.key,
1824
+ type: step.type,
1825
+ status: "error",
1826
+ error: message
1827
+ },
1828
+ cause: thrown
1829
+ };
1761
1830
  });
1762
1831
  }
1763
- async function runReport(report, opts) {
1832
+ async function runReportResult(report, opts) {
1764
1833
  const startedAt = Date.now();
1765
1834
  const generatedAt = new Date(startedAt).toISOString();
1766
1835
  const inputHash = await computeInputHash({
@@ -1774,13 +1843,13 @@ async function runReport(report, opts) {
1774
1843
  const outcomes = await Promise.all(steps.map((s) => executeStep(opts.source, opts.analyzers, s)));
1775
1844
  const required = new Map(steps.filter((s) => s.required).map((s) => [s.key, s]));
1776
1845
  const errored = outcomes.filter((o) => o.state.status === "error");
1777
- for (const o of errored) if (required.has(o.state.key)) throw new Error(`runReport(${report.id}): required step "${o.state.key}" failed: ${o.state.error}`);
1846
+ 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));
1778
1847
  const resultsByKey = {};
1779
1848
  for (const o of outcomes) if (o.result) resultsByKey[o.state.key] = o.result;
1780
1849
  const sections = report.reduce(resultsByKey, opts.ctx).sections;
1781
1850
  const degraded = errored.length > 0;
1782
1851
  const stepStates = outcomes.map((o) => o.state);
1783
- return {
1852
+ return ok({
1784
1853
  id: report.id,
1785
1854
  site: opts.ctx.site,
1786
1855
  inputHash,
@@ -1793,7 +1862,10 @@ async function runReport(report, opts) {
1793
1862
  degraded,
1794
1863
  steps: stepStates
1795
1864
  }
1796
- };
1865
+ });
1866
+ }
1867
+ async function runReport(report, opts) {
1868
+ return unwrapResult(await runReportResult(report, opts), analysisErrorToException);
1797
1869
  }
1798
1870
  async function dryRunReport(report, ctx) {
1799
1871
  return {
@@ -1808,4 +1880,4 @@ async function dryRunReport(report, ctx) {
1808
1880
  }
1809
1881
  };
1810
1882
  }
1811
- export { REPORTS, defaultReportRegistry, dryRunReport, formatReport, resolveTarget, runReport };
1883
+ export { REPORTS, defaultReportRegistry, dryRunReport, formatReport, resolveTarget, runReport, runReportResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/analysis",
3
3
  "type": "module",
4
- "version": "0.25.13",
4
+ "version": "0.26.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",
@@ -41,6 +41,11 @@
41
41
  "import": "./dist/source/index.mjs",
42
42
  "default": "./dist/source/index.mjs"
43
43
  },
44
+ "./errors": {
45
+ "types": "./dist/errors.d.mts",
46
+ "import": "./dist/errors.mjs",
47
+ "default": "./dist/errors.mjs"
48
+ },
44
49
  "./semantic": {
45
50
  "types": "./dist/semantic/index.d.mts",
46
51
  "import": "./dist/semantic/index.mjs",
@@ -70,12 +75,12 @@
70
75
  },
71
76
  "dependencies": {
72
77
  "drizzle-orm": "1.0.0-rc.3",
73
- "@gscdump/engine-gsc-api": "0.25.13",
74
- "@gscdump/engine": "0.25.13",
75
- "gscdump": "0.25.13"
78
+ "@gscdump/engine-gsc-api": "0.26.0",
79
+ "@gscdump/engine": "0.26.0",
80
+ "gscdump": "0.26.0"
76
81
  },
77
82
  "devDependencies": {
78
- "vitest": "^4.1.7"
83
+ "vitest": "^4.1.8"
79
84
  },
80
85
  "scripts": {
81
86
  "build": "obuild",