@witchpot/gmc 0.1.7 → 0.1.9

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/src/workflows.js CHANGED
@@ -30,7 +30,10 @@ export async function executeRequestPlan(baseUrl, request, { apiKey }) {
30
30
  }
31
31
 
32
32
  async function executeCohortEvidenceRequest(baseUrl, request, { apiKey }) {
33
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
33
+ const [sampled, count] = await Promise.all([
34
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
35
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
36
+ ]);
34
37
  const search = mergeSearchPages(sampled.searches, request.sampling);
35
38
  const games = Array.isArray(search?.data) ? search.data : [];
36
39
  const analysisResults = request.analysisQuery
@@ -69,6 +72,7 @@ async function executeCohortEvidenceRequest(baseUrl, request, { apiKey }) {
69
72
 
70
73
  return composeCohortEvidencePayload(request, {
71
74
  search,
75
+ count,
72
76
  searches: sampled.searches,
73
77
  searchPageErrors: sampled.pageErrors,
74
78
  analysisResults
@@ -81,8 +85,11 @@ async function executeResolveRequest(baseUrl, request, { apiKey }) {
81
85
  }
82
86
 
83
87
  async function executeBenchmarkRequest(baseUrl, request, { apiKey }) {
84
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
85
- return composeBenchmarkPayload(request, sampled);
88
+ const [sampled, count] = await Promise.all([
89
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
90
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
91
+ ]);
92
+ return composeBenchmarkPayload(request, { ...sampled, count });
86
93
  }
87
94
 
88
95
  async function executeCompareRequest(baseUrl, request, { apiKey }) {
@@ -116,7 +123,10 @@ async function executeCompareRequest(baseUrl, request, { apiKey }) {
116
123
  }
117
124
 
118
125
  async function executeReviewPatternsRequest(baseUrl, request, { apiKey }) {
119
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
126
+ const [sampled, count] = await Promise.all([
127
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
128
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
129
+ ]);
120
130
  const search = mergeSearchPages(sampled.searches, request.sampling);
121
131
  const games = Array.isArray(search?.data) ? search.data : [];
122
132
  const topicResults = await Promise.all(
@@ -153,6 +163,7 @@ async function executeReviewPatternsRequest(baseUrl, request, { apiKey }) {
153
163
 
154
164
  return composeReviewPatternsPayload(request, {
155
165
  search,
166
+ count,
156
167
  searches: sampled.searches,
157
168
  searchPageErrors: sampled.pageErrors,
158
169
  topicResults
@@ -243,6 +254,11 @@ async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey })
243
254
  };
244
255
  }
245
256
 
257
+ async function fetchOptionalCount(baseUrl, countRequest, { apiKey }) {
258
+ if (!countRequest) return undefined;
259
+ return fetchJson(createRequestUrl(baseUrl, countRequest), { apiKey });
260
+ }
261
+
246
262
  function shouldFetchNextSamplePage(sampling, searches) {
247
263
  if (sampling.mode === "sample-pages") return searches.length < sampling.requestedPages;
248
264
  const fetchedRows = searches.reduce((sum, search) => sum + asArray(search?.data).length, 0);
@@ -383,6 +399,8 @@ export function composeBenchmarkPayload(request, search, options = {}) {
383
399
  : search;
384
400
  const games = Array.isArray(mergedSearch?.data) ? mergedSearch.data : [];
385
401
  const sample = buildSampleMetadata(request, searches, games, pageErrors);
402
+ const totalMatches = getTotalMatches(search.count, mergedSearch?.pagination);
403
+ const pagination = withPaginationTotal(mergedSearch?.pagination, totalMatches);
386
404
 
387
405
  return {
388
406
  data: {
@@ -391,19 +409,19 @@ export function composeBenchmarkPayload(request, search, options = {}) {
391
409
  workflow: "gamesBenchmark",
392
410
  inputs: {
393
411
  filters: request.query,
394
- ...(sample ? { sample: buildEvidencePackSample(sample, games.length) } : {
395
- sample: buildReturnedPageSample(mergedSearch?.pagination, games.length)
412
+ ...(sample ? { sample: buildEvidencePackSample(sample, games.length, totalMatches) } : {
413
+ sample: buildReturnedPageSample(pagination, games.length, totalMatches)
396
414
  })
397
415
  },
398
416
  query: request.query,
399
417
  summary: withSummaryBasis(
400
- buildBenchmarkSummary(games, mergedSearch?.pagination, request.source, { sampled: Boolean(sample) }),
418
+ buildBenchmarkSummary(games, pagination, request.source, { sampled: Boolean(sample) }),
401
419
  sample ? "bounded_sample" : "returned_page"
402
420
  ),
403
421
  ...(sample
404
422
  ? {
405
423
  sampledSummary: withSummaryBasis(
406
- buildBenchmarkSummary(games, mergedSearch?.pagination, request.source, { sampled: true }),
424
+ buildBenchmarkSummary(games, pagination, request.source, { sampled: true }),
407
425
  "bounded_sample"
408
426
  )
409
427
  }
@@ -424,11 +442,15 @@ export function composeBenchmarkPayload(request, search, options = {}) {
424
442
  requestId,
425
443
  composedBy: "gmc-cli",
426
444
  ...(sample ? { sample } : {}),
427
- upstreamRequests: buildSearchUpstreamRequests(searches, pageErrors)
445
+ upstreamRequests: [
446
+ ...buildSearchUpstreamRequests(searches, pageErrors),
447
+ ...buildCountUpstreamRequests(search.count)
448
+ ]
428
449
  },
429
- ...(mergedSearch?.pagination ? { pagination: mergedSearch.pagination } : {}),
450
+ ...(pagination ? { pagination } : {}),
430
451
  warnings: uniqueStrings([
431
452
  ...searches.flatMap((entry) => safeWarnings(entry)),
453
+ ...safeWarnings(search.count),
432
454
  ...pageErrors.map((entry) => `benchmark sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
433
455
  sample
434
456
  ? "sampledSummary is computed from bounded sampled pages; it is not a full market-size estimate"
@@ -457,6 +479,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
457
479
  const includedGames = normalizedGames.filter((game) => !game.skippedReason);
458
480
  const usableGames = includedGames.filter((game) => game.topics.length > 0);
459
481
  const sample = buildSampleMetadata(request, searches, games, searchPageErrors);
482
+ const totalMatches = getTotalMatches(result.count, result.search?.pagination);
460
483
  const skipped = normalizedGames
461
484
  .filter((game) => game.skippedReason)
462
485
  .map((game) => ({
@@ -469,6 +492,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
469
492
  }));
470
493
  const warnings = uniqueStrings([
471
494
  ...safeWarnings(result.search),
495
+ ...safeWarnings(result.count),
472
496
  ...searches.flatMap((entry) => safeWarnings(entry)),
473
497
  ...searchPageErrors.map((entry) => `review pattern sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
474
498
  ...topicResults.flatMap((entry) => safeWarnings(entry.topics)),
@@ -491,8 +515,8 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
491
515
  ...(request.topic ? { topic: request.topic } : {}),
492
516
  ...(request.compareBy ? { compareBy: request.compareBy } : {}),
493
517
  ...(sample
494
- ? { sample: buildEvidencePackSample(sample, usableGames.length) }
495
- : { sample: buildReturnedPageSample(result.search?.pagination, usableGames.length) })
518
+ ? { sample: buildEvidencePackSample(sample, usableGames.length, totalMatches) }
519
+ : { sample: buildReturnedPageSample(result.search?.pagination, usableGames.length, totalMatches) })
496
520
  },
497
521
  cohort: {
498
522
  query: request.query,
@@ -505,7 +529,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
505
529
  usableGames: usableGames.length,
506
530
  skippedGames: skipped.length,
507
531
  skippedReasons: tallySkippedReasons(skipped),
508
- totalMatches: result.search?.pagination?.total,
532
+ ...(typeof totalMatches === "number" ? { totalMatches } : {}),
509
533
  included: usableGames.map((game) => ({
510
534
  source: request.source,
511
535
  externalId: game.externalId,
@@ -521,7 +545,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
521
545
  skipped
522
546
  },
523
547
  summary: withSummaryBasis(
524
- buildReviewPatternsSummary(request, usableGames, skipped, result.search?.pagination),
548
+ buildReviewPatternsSummary(request, usableGames, skipped, withPaginationTotal(result.search?.pagination, totalMatches)),
525
549
  sample ? "bounded_sample" : "returned_page"
526
550
  ),
527
551
  patterns: buildReviewPatterns(usableGames),
@@ -547,6 +571,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
547
571
  ...(sample ? { sample } : {}),
548
572
  upstreamRequests: [
549
573
  ...buildSearchUpstreamRequests(searches, searchPageErrors),
574
+ ...buildCountUpstreamRequests(result.count),
550
575
  ...topicResults
551
576
  .filter((entry) => entry.topics || entry.error)
552
577
  .map((entry) => ({
@@ -559,7 +584,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
559
584
  }))
560
585
  ]
561
586
  },
562
- ...(result.search?.pagination ? { pagination: result.search.pagination } : {}),
587
+ ...(result.search?.pagination ? { pagination: withPaginationTotal(result.search.pagination, totalMatches) } : {}),
563
588
  ...(warnings.length ? { warnings } : {})
564
589
  };
565
590
  }
@@ -670,8 +695,10 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
670
695
  ...(entry.error ? { error: entry.error } : {})
671
696
  }));
672
697
  const sample = buildSampleMetadata(request, searches, games, searchPageErrors);
698
+ const totalMatches = getTotalMatches(result.count, result.search?.pagination);
673
699
  const warnings = uniqueStrings([
674
700
  ...safeWarnings(result.search),
701
+ ...safeWarnings(result.count),
675
702
  ...searches.flatMap((entry) => safeWarnings(entry)),
676
703
  ...searchPageErrors.map((entry) => `${request.workflow} sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
677
704
  ...analysisResults.flatMap((entry) => safeWarnings(entry.analysis)),
@@ -688,8 +715,8 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
688
715
  inputs: {
689
716
  filters: request.query,
690
717
  sections: request.sections,
691
- ...(sample ? { sample: buildEvidencePackSample(sample, included.length) } : {
692
- sample: buildReturnedPageSample(result.search?.pagination, included.length)
718
+ ...(sample ? { sample: buildEvidencePackSample(sample, included.length, totalMatches) } : {
719
+ sample: buildReturnedPageSample(result.search?.pagination, included.length, totalMatches)
693
720
  })
694
721
  },
695
722
  cohort: {
@@ -698,7 +725,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
698
725
  usableGames: included.length,
699
726
  skippedGames: skipped.length,
700
727
  skippedReasons: tallySkippedReasons(skipped),
701
- totalMatches: result.search?.pagination?.total,
728
+ ...(typeof totalMatches === "number" ? { totalMatches } : {}),
702
729
  included: included.map((entry) => ({
703
730
  source: request.source,
704
731
  externalId: entry.externalId,
@@ -716,12 +743,12 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
716
743
  skipped
717
744
  },
718
745
  summary: withSummaryBasis(
719
- buildCohortEvidenceSummary(request, included, result.search?.pagination),
746
+ buildCohortEvidenceSummary(request, included, withPaginationTotal(result.search?.pagination, totalMatches)),
720
747
  sample ? "bounded_sample" : "returned_page"
721
748
  ),
722
749
  patterns: buildCohortEvidencePatterns(request, included),
723
750
  ...(request.evidenceType === "pricing" ? { segments: buildPricingSegments(included) } : {}),
724
- ...(request.evidenceType === "trends" || request.evidenceType === "brief" ? { trends: buildTrendSummary(included, result.search?.pagination, request.source) } : {}),
751
+ ...(request.evidenceType === "trends" || request.evidenceType === "brief" ? { trends: buildTrendSummary(included, withPaginationTotal(result.search?.pagination, totalMatches), request.source) } : {}),
725
752
  games: included.map((entry) => ({
726
753
  source: request.source,
727
754
  externalId: entry.externalId,
@@ -744,6 +771,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
744
771
  ...(sample ? { sample } : {}),
745
772
  upstreamRequests: [
746
773
  ...buildSearchUpstreamRequests(searches, searchPageErrors),
774
+ ...buildCountUpstreamRequests(result.count),
747
775
  ...analysisResults
748
776
  .filter((entry) => entry.analysis || entry.error)
749
777
  .map((entry) => ({
@@ -756,7 +784,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
756
784
  }))
757
785
  ]
758
786
  },
759
- ...(result.search?.pagination ? { pagination: result.search.pagination } : {}),
787
+ ...(result.search?.pagination ? { pagination: withPaginationTotal(result.search.pagination, totalMatches) } : {}),
760
788
  ...(warnings.length ? { warnings } : {})
761
789
  };
762
790
  }
@@ -1021,7 +1049,7 @@ function mergeSearchPages(searches, sampling) {
1021
1049
  ...(first.pagination ?? {}),
1022
1050
  limit: first.pagination?.limit,
1023
1051
  offset: first.pagination?.offset,
1024
- total: first.pagination?.total,
1052
+ ...(typeof first.pagination?.total === "number" ? { total: first.pagination.total } : {}),
1025
1053
  nextOffset: last.pagination?.nextOffset
1026
1054
  },
1027
1055
  warnings: uniqueStrings(searches.flatMap((search) => safeWarnings(search)))
@@ -1038,12 +1066,12 @@ function buildSampleMetadata(request, searches, games, pageErrors) {
1038
1066
  fetchedPages: searches.length,
1039
1067
  fetchedGames: games.length,
1040
1068
  skippedPages: pageErrors.length,
1041
- totalMatches: searches[0]?.pagination?.total,
1069
+ ...(typeof searches[0]?.pagination?.total === "number" ? { totalMatches: searches[0].pagination.total } : {}),
1042
1070
  quotaCaveat: "Sampled workflows issue one search request per fetched page, plus downstream per-game requests when the command needs detail evidence."
1043
1071
  };
1044
1072
  }
1045
1073
 
1046
- function buildEvidencePackSample(sample, returnedRows) {
1074
+ function buildEvidencePackSample(sample, returnedRows, totalMatches) {
1047
1075
  return {
1048
1076
  mode: normalizeEvidencePackSampleMode(sample.mode),
1049
1077
  requestedRows: sample.requestedSize,
@@ -1051,16 +1079,31 @@ function buildEvidencePackSample(sample, returnedRows) {
1051
1079
  returnedRows,
1052
1080
  fetchedPages: sample.fetchedPages,
1053
1081
  skippedPages: sample.skippedPages,
1054
- totalMatches: sample.totalMatches
1082
+ totalMatches: typeof totalMatches === "number" ? totalMatches : sample.totalMatches
1055
1083
  };
1056
1084
  }
1057
1085
 
1058
- function buildReturnedPageSample(pagination, returnedRows) {
1086
+ function buildReturnedPageSample(pagination, returnedRows, totalMatches) {
1059
1087
  return {
1060
1088
  mode: "returned_page",
1061
1089
  requestedRows: pagination?.limit,
1062
1090
  returnedRows,
1063
- totalMatches: pagination?.total
1091
+ totalMatches: typeof totalMatches === "number" ? totalMatches : pagination?.total
1092
+ };
1093
+ }
1094
+
1095
+ function getTotalMatches(count, pagination) {
1096
+ const countValue = numericValue(count?.data?.count);
1097
+ if (Number.isFinite(countValue)) return countValue;
1098
+ const paginationTotal = numericValue(pagination?.total);
1099
+ return Number.isFinite(paginationTotal) ? paginationTotal : undefined;
1100
+ }
1101
+
1102
+ function withPaginationTotal(pagination, totalMatches) {
1103
+ if (typeof totalMatches !== "number") return pagination;
1104
+ return {
1105
+ ...(pagination ?? {}),
1106
+ total: totalMatches
1064
1107
  };
1065
1108
  }
1066
1109
 
@@ -1096,6 +1139,17 @@ function buildSearchUpstreamRequests(searches, pageErrors = []) {
1096
1139
  ];
1097
1140
  }
1098
1141
 
1142
+ function buildCountUpstreamRequests(count) {
1143
+ if (!count) return [];
1144
+ return [{
1145
+ kind: "count",
1146
+ requestId: count?.meta?.requestId,
1147
+ source: count?.meta?.source,
1148
+ entitlements: count?.meta?.entitlements,
1149
+ count: count?.data?.count
1150
+ }];
1151
+ }
1152
+
1099
1153
  function buildReviewPatternGame(request, entry) {
1100
1154
  const game = entry.game ?? {};
1101
1155
  const externalId = asString(entry.externalId ?? game?.externalId ?? game?.appid);
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import { composeCohortEvidencePayload } from "./workflows.js";
4
+ import { composeBenchmarkPayload, composeCohortEvidencePayload } from "./workflows.js";
5
5
 
6
6
  test("cohort evidence summarizes skipped games by reason", () => {
7
7
  const request = {
@@ -78,3 +78,30 @@ test("cohort evidence reports an empty reason tally when nothing is skipped", ()
78
78
 
79
79
  assert.deepEqual(payload.data.cohort.skippedReasons, {});
80
80
  });
81
+
82
+ test("benchmark attaches exact total only when a count response is supplied", () => {
83
+ const request = {
84
+ source: "steam",
85
+ query: { sort: "reviews_desc", limit: 20, includeTotal: false }
86
+ };
87
+ const payload = composeBenchmarkPayload(request, {
88
+ searches: [{
89
+ data: [{ appid: 1, title: "Usable", reviews: 100 }],
90
+ pagination: { limit: 20, offset: 0, nextOffset: 20 },
91
+ meta: { requestId: "search_req", source: "steam-analysis" }
92
+ }],
93
+ pageErrors: [],
94
+ count: {
95
+ data: { count: 123 },
96
+ meta: { requestId: "count_req", source: "steam-analysis" }
97
+ }
98
+ }, {
99
+ generatedAt: "2026-06-12T00:00:00.000Z",
100
+ requestId: "req_test"
101
+ });
102
+
103
+ assert.equal(payload.data.summary.totalMatches, 123);
104
+ assert.equal(payload.data.inputs.sample.totalMatches, 123);
105
+ assert.equal(payload.pagination.total, 123);
106
+ assert.deepEqual(payload.meta.upstreamRequests.map((entry) => entry.kind), ["search", "count"]);
107
+ });