@witchpot/gmc 0.1.8 → 0.2.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/src/workflows.js CHANGED
@@ -25,12 +25,19 @@ export async function executeRequestPlan(baseUrl, request, { apiKey }) {
25
25
  return executeShowcaseFitRequest(baseUrl, request, { apiKey });
26
26
  }
27
27
 
28
- const response = await fetchJson(createRequestUrl(baseUrl, request), { apiKey });
28
+ const response = await fetchJson(createRequestUrl(baseUrl, request), {
29
+ apiKey,
30
+ method: request.method,
31
+ body: request.body
32
+ });
29
33
  return request.transform ? request.transform(response) : response;
30
34
  }
31
35
 
32
36
  async function executeCohortEvidenceRequest(baseUrl, request, { apiKey }) {
33
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
37
+ const [sampled, count] = await Promise.all([
38
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
39
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
40
+ ]);
34
41
  const search = mergeSearchPages(sampled.searches, request.sampling);
35
42
  const games = Array.isArray(search?.data) ? search.data : [];
36
43
  const analysisResults = request.analysisQuery
@@ -69,6 +76,7 @@ async function executeCohortEvidenceRequest(baseUrl, request, { apiKey }) {
69
76
 
70
77
  return composeCohortEvidencePayload(request, {
71
78
  search,
79
+ count,
72
80
  searches: sampled.searches,
73
81
  searchPageErrors: sampled.pageErrors,
74
82
  analysisResults
@@ -81,8 +89,11 @@ async function executeResolveRequest(baseUrl, request, { apiKey }) {
81
89
  }
82
90
 
83
91
  async function executeBenchmarkRequest(baseUrl, request, { apiKey }) {
84
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
85
- return composeBenchmarkPayload(request, sampled);
92
+ const [sampled, count] = await Promise.all([
93
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
94
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
95
+ ]);
96
+ return composeBenchmarkPayload(request, { ...sampled, count });
86
97
  }
87
98
 
88
99
  async function executeCompareRequest(baseUrl, request, { apiKey }) {
@@ -116,7 +127,10 @@ async function executeCompareRequest(baseUrl, request, { apiKey }) {
116
127
  }
117
128
 
118
129
  async function executeReviewPatternsRequest(baseUrl, request, { apiKey }) {
119
- const sampled = await executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey });
130
+ const [sampled, count] = await Promise.all([
131
+ executeSearchPages(baseUrl, request.searchRequest, request.sampling, { apiKey }),
132
+ fetchOptionalCount(baseUrl, request.countRequest, { apiKey })
133
+ ]);
120
134
  const search = mergeSearchPages(sampled.searches, request.sampling);
121
135
  const games = Array.isArray(search?.data) ? search.data : [];
122
136
  const topicResults = await Promise.all(
@@ -153,6 +167,7 @@ async function executeReviewPatternsRequest(baseUrl, request, { apiKey }) {
153
167
 
154
168
  return composeReviewPatternsPayload(request, {
155
169
  search,
170
+ count,
156
171
  searches: sampled.searches,
157
172
  searchPageErrors: sampled.pageErrors,
158
173
  topicResults
@@ -205,12 +220,12 @@ async function executeShowcaseFitRequest(baseUrl, request, { apiKey }) {
205
220
  async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey }) {
206
221
  if (!sampling) {
207
222
  return {
208
- searches: [await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey })],
223
+ searches: [normalizeSearchEnvelope(await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey }))],
209
224
  pageErrors: []
210
225
  };
211
226
  }
212
227
 
213
- const firstSearch = await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey });
228
+ const firstSearch = normalizeSearchEnvelope(await fetchJson(createRequestUrl(baseUrl, searchRequest), { apiKey }));
214
229
  const searches = [firstSearch];
215
230
  const pageErrors = [];
216
231
  let nextOffset = firstSearch?.pagination?.nextOffset;
@@ -233,8 +248,9 @@ async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey })
233
248
  break;
234
249
  }
235
250
 
236
- searches.push(pageResult.payload);
237
- nextOffset = pageResult.payload?.pagination?.nextOffset;
251
+ const normalizedPage = normalizeSearchEnvelope(pageResult.payload);
252
+ searches.push(normalizedPage);
253
+ nextOffset = normalizedPage?.pagination?.nextOffset;
238
254
  }
239
255
 
240
256
  return {
@@ -243,6 +259,23 @@ async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey })
243
259
  };
244
260
  }
245
261
 
262
+ function normalizeSearchEnvelope(payload) {
263
+ if (Array.isArray(payload?.data?.games)) {
264
+ return {
265
+ ...payload,
266
+ data: payload.data.games,
267
+ list: payload.data.list,
268
+ notes: payload.data.notes
269
+ };
270
+ }
271
+ return payload;
272
+ }
273
+
274
+ async function fetchOptionalCount(baseUrl, countRequest, { apiKey }) {
275
+ if (!countRequest) return undefined;
276
+ return fetchJson(createRequestUrl(baseUrl, countRequest), { apiKey });
277
+ }
278
+
246
279
  function shouldFetchNextSamplePage(sampling, searches) {
247
280
  if (sampling.mode === "sample-pages") return searches.length < sampling.requestedPages;
248
281
  const fetchedRows = searches.reduce((sum, search) => sum + asArray(search?.data).length, 0);
@@ -383,6 +416,8 @@ export function composeBenchmarkPayload(request, search, options = {}) {
383
416
  : search;
384
417
  const games = Array.isArray(mergedSearch?.data) ? mergedSearch.data : [];
385
418
  const sample = buildSampleMetadata(request, searches, games, pageErrors);
419
+ const totalMatches = getTotalMatches(search.count, mergedSearch?.pagination);
420
+ const pagination = withPaginationTotal(mergedSearch?.pagination, totalMatches);
386
421
 
387
422
  return {
388
423
  data: {
@@ -391,19 +426,19 @@ export function composeBenchmarkPayload(request, search, options = {}) {
391
426
  workflow: "gamesBenchmark",
392
427
  inputs: {
393
428
  filters: request.query,
394
- ...(sample ? { sample: buildEvidencePackSample(sample, games.length) } : {
395
- sample: buildReturnedPageSample(mergedSearch?.pagination, games.length)
429
+ ...(sample ? { sample: buildEvidencePackSample(sample, games.length, totalMatches) } : {
430
+ sample: buildReturnedPageSample(pagination, games.length, totalMatches)
396
431
  })
397
432
  },
398
433
  query: request.query,
399
434
  summary: withSummaryBasis(
400
- buildBenchmarkSummary(games, mergedSearch?.pagination, request.source, { sampled: Boolean(sample) }),
435
+ buildBenchmarkSummary(games, pagination, request.source, { sampled: Boolean(sample) }),
401
436
  sample ? "bounded_sample" : "returned_page"
402
437
  ),
403
438
  ...(sample
404
439
  ? {
405
440
  sampledSummary: withSummaryBasis(
406
- buildBenchmarkSummary(games, mergedSearch?.pagination, request.source, { sampled: true }),
441
+ buildBenchmarkSummary(games, pagination, request.source, { sampled: true }),
407
442
  "bounded_sample"
408
443
  )
409
444
  }
@@ -424,11 +459,15 @@ export function composeBenchmarkPayload(request, search, options = {}) {
424
459
  requestId,
425
460
  composedBy: "gmc-cli",
426
461
  ...(sample ? { sample } : {}),
427
- upstreamRequests: buildSearchUpstreamRequests(searches, pageErrors)
462
+ upstreamRequests: [
463
+ ...buildSearchUpstreamRequests(searches, pageErrors),
464
+ ...buildCountUpstreamRequests(search.count)
465
+ ]
428
466
  },
429
- ...(mergedSearch?.pagination ? { pagination: mergedSearch.pagination } : {}),
467
+ ...(pagination ? { pagination } : {}),
430
468
  warnings: uniqueStrings([
431
469
  ...searches.flatMap((entry) => safeWarnings(entry)),
470
+ ...safeWarnings(search.count),
432
471
  ...pageErrors.map((entry) => `benchmark sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
433
472
  sample
434
473
  ? "sampledSummary is computed from bounded sampled pages; it is not a full market-size estimate"
@@ -457,6 +496,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
457
496
  const includedGames = normalizedGames.filter((game) => !game.skippedReason);
458
497
  const usableGames = includedGames.filter((game) => game.topics.length > 0);
459
498
  const sample = buildSampleMetadata(request, searches, games, searchPageErrors);
499
+ const totalMatches = getTotalMatches(result.count, result.search?.pagination);
460
500
  const skipped = normalizedGames
461
501
  .filter((game) => game.skippedReason)
462
502
  .map((game) => ({
@@ -469,6 +509,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
469
509
  }));
470
510
  const warnings = uniqueStrings([
471
511
  ...safeWarnings(result.search),
512
+ ...safeWarnings(result.count),
472
513
  ...searches.flatMap((entry) => safeWarnings(entry)),
473
514
  ...searchPageErrors.map((entry) => `review pattern sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
474
515
  ...topicResults.flatMap((entry) => safeWarnings(entry.topics)),
@@ -491,8 +532,8 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
491
532
  ...(request.topic ? { topic: request.topic } : {}),
492
533
  ...(request.compareBy ? { compareBy: request.compareBy } : {}),
493
534
  ...(sample
494
- ? { sample: buildEvidencePackSample(sample, usableGames.length) }
495
- : { sample: buildReturnedPageSample(result.search?.pagination, usableGames.length) })
535
+ ? { sample: buildEvidencePackSample(sample, usableGames.length, totalMatches) }
536
+ : { sample: buildReturnedPageSample(result.search?.pagination, usableGames.length, totalMatches) })
496
537
  },
497
538
  cohort: {
498
539
  query: request.query,
@@ -505,7 +546,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
505
546
  usableGames: usableGames.length,
506
547
  skippedGames: skipped.length,
507
548
  skippedReasons: tallySkippedReasons(skipped),
508
- totalMatches: result.search?.pagination?.total,
549
+ ...(typeof totalMatches === "number" ? { totalMatches } : {}),
509
550
  included: usableGames.map((game) => ({
510
551
  source: request.source,
511
552
  externalId: game.externalId,
@@ -521,7 +562,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
521
562
  skipped
522
563
  },
523
564
  summary: withSummaryBasis(
524
- buildReviewPatternsSummary(request, usableGames, skipped, result.search?.pagination),
565
+ buildReviewPatternsSummary(request, usableGames, skipped, withPaginationTotal(result.search?.pagination, totalMatches)),
525
566
  sample ? "bounded_sample" : "returned_page"
526
567
  ),
527
568
  patterns: buildReviewPatterns(usableGames),
@@ -547,6 +588,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
547
588
  ...(sample ? { sample } : {}),
548
589
  upstreamRequests: [
549
590
  ...buildSearchUpstreamRequests(searches, searchPageErrors),
591
+ ...buildCountUpstreamRequests(result.count),
550
592
  ...topicResults
551
593
  .filter((entry) => entry.topics || entry.error)
552
594
  .map((entry) => ({
@@ -559,7 +601,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
559
601
  }))
560
602
  ]
561
603
  },
562
- ...(result.search?.pagination ? { pagination: result.search.pagination } : {}),
604
+ ...(result.search?.pagination ? { pagination: withPaginationTotal(result.search.pagination, totalMatches) } : {}),
563
605
  ...(warnings.length ? { warnings } : {})
564
606
  };
565
607
  }
@@ -657,7 +699,8 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
657
699
  const searches = Array.isArray(result.searches) ? result.searches : [result.search];
658
700
  const searchPageErrors = Array.isArray(result.searchPageErrors) ? result.searchPageErrors : [];
659
701
  const analysisResults = Array.isArray(result.analysisResults) ? result.analysisResults : [];
660
- const entries = analysisResults.map((entry) => buildCohortEvidenceGame(request, entry));
702
+ const listNotes = collectListNotes(searches);
703
+ const entries = analysisResults.map((entry) => buildCohortEvidenceGame(request, entry, listNotes));
661
704
  const included = entries.filter((entry) => !entry.skippedReason);
662
705
  const skipped = entries
663
706
  .filter((entry) => entry.skippedReason)
@@ -670,8 +713,10 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
670
713
  ...(entry.error ? { error: entry.error } : {})
671
714
  }));
672
715
  const sample = buildSampleMetadata(request, searches, games, searchPageErrors);
716
+ const totalMatches = getTotalMatches(result.count, result.search?.pagination);
673
717
  const warnings = uniqueStrings([
674
718
  ...safeWarnings(result.search),
719
+ ...safeWarnings(result.count),
675
720
  ...searches.flatMap((entry) => safeWarnings(entry)),
676
721
  ...searchPageErrors.map((entry) => `${request.workflow} sample page failed at offset ${entry.offset}: ${entry.error?.code ?? "CLI_API_ERROR"}`),
677
722
  ...analysisResults.flatMap((entry) => safeWarnings(entry.analysis)),
@@ -688,8 +733,8 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
688
733
  inputs: {
689
734
  filters: request.query,
690
735
  sections: request.sections,
691
- ...(sample ? { sample: buildEvidencePackSample(sample, included.length) } : {
692
- sample: buildReturnedPageSample(result.search?.pagination, included.length)
736
+ ...(sample ? { sample: buildEvidencePackSample(sample, included.length, totalMatches) } : {
737
+ sample: buildReturnedPageSample(result.search?.pagination, included.length, totalMatches)
693
738
  })
694
739
  },
695
740
  cohort: {
@@ -698,7 +743,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
698
743
  usableGames: included.length,
699
744
  skippedGames: skipped.length,
700
745
  skippedReasons: tallySkippedReasons(skipped),
701
- totalMatches: result.search?.pagination?.total,
746
+ ...(typeof totalMatches === "number" ? { totalMatches } : {}),
702
747
  included: included.map((entry) => ({
703
748
  source: request.source,
704
749
  externalId: entry.externalId,
@@ -710,18 +755,19 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
710
755
  releaseDate: entry.releaseDate,
711
756
  coverage: entry.coverage,
712
757
  tags: entry.tags,
758
+ ...(entry.listNote ? { listNote: entry.listNote } : {}),
713
759
  patternEvidence: entry.patternEvidence
714
760
  }
715
761
  })),
716
762
  skipped
717
763
  },
718
764
  summary: withSummaryBasis(
719
- buildCohortEvidenceSummary(request, included, result.search?.pagination),
765
+ buildCohortEvidenceSummary(request, included, withPaginationTotal(result.search?.pagination, totalMatches)),
720
766
  sample ? "bounded_sample" : "returned_page"
721
767
  ),
722
768
  patterns: buildCohortEvidencePatterns(request, included),
723
769
  ...(request.evidenceType === "pricing" ? { segments: buildPricingSegments(included) } : {}),
724
- ...(request.evidenceType === "trends" || request.evidenceType === "brief" ? { trends: buildTrendSummary(included, result.search?.pagination, request.source) } : {}),
770
+ ...(request.evidenceType === "trends" || request.evidenceType === "brief" ? { trends: buildTrendSummary(included, withPaginationTotal(result.search?.pagination, totalMatches), request.source) } : {}),
725
771
  games: included.map((entry) => ({
726
772
  source: request.source,
727
773
  externalId: entry.externalId,
@@ -733,6 +779,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
733
779
  releaseDate: entry.releaseDate,
734
780
  coverage: entry.coverage,
735
781
  tags: entry.tags,
782
+ ...(entry.listNote ? { listNote: entry.listNote } : {}),
736
783
  patternEvidence: entry.patternEvidence
737
784
  })),
738
785
  skipped
@@ -744,6 +791,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
744
791
  ...(sample ? { sample } : {}),
745
792
  upstreamRequests: [
746
793
  ...buildSearchUpstreamRequests(searches, searchPageErrors),
794
+ ...buildCountUpstreamRequests(result.count),
747
795
  ...analysisResults
748
796
  .filter((entry) => entry.analysis || entry.error)
749
797
  .map((entry) => ({
@@ -756,7 +804,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
756
804
  }))
757
805
  ]
758
806
  },
759
- ...(result.search?.pagination ? { pagination: result.search.pagination } : {}),
807
+ ...(result.search?.pagination ? { pagination: withPaginationTotal(result.search.pagination, totalMatches) } : {}),
760
808
  ...(warnings.length ? { warnings } : {})
761
809
  };
762
810
  }
@@ -826,11 +874,16 @@ export function composeShowcaseFitPayload(request, result, options = {}) {
826
874
  };
827
875
  }
828
876
 
829
- async function fetchJson(url, { apiKey }) {
877
+ async function fetchJson(url, { apiKey, method = "GET", body } = {}) {
830
878
  let response;
831
879
  try {
832
880
  response = await fetch(url, {
833
- headers: buildRequestHeaders(apiKey)
881
+ method,
882
+ headers: {
883
+ ...buildRequestHeaders(apiKey),
884
+ ...(body == null ? {} : { "content-type": "application/json" })
885
+ },
886
+ ...(body == null ? {} : { body: JSON.stringify(body) })
834
887
  });
835
888
  } catch (error) {
836
889
  writeJsonError({
@@ -1021,7 +1074,7 @@ function mergeSearchPages(searches, sampling) {
1021
1074
  ...(first.pagination ?? {}),
1022
1075
  limit: first.pagination?.limit,
1023
1076
  offset: first.pagination?.offset,
1024
- total: first.pagination?.total,
1077
+ ...(typeof first.pagination?.total === "number" ? { total: first.pagination.total } : {}),
1025
1078
  nextOffset: last.pagination?.nextOffset
1026
1079
  },
1027
1080
  warnings: uniqueStrings(searches.flatMap((search) => safeWarnings(search)))
@@ -1038,12 +1091,12 @@ function buildSampleMetadata(request, searches, games, pageErrors) {
1038
1091
  fetchedPages: searches.length,
1039
1092
  fetchedGames: games.length,
1040
1093
  skippedPages: pageErrors.length,
1041
- totalMatches: searches[0]?.pagination?.total,
1094
+ ...(typeof searches[0]?.pagination?.total === "number" ? { totalMatches: searches[0].pagination.total } : {}),
1042
1095
  quotaCaveat: "Sampled workflows issue one search request per fetched page, plus downstream per-game requests when the command needs detail evidence."
1043
1096
  };
1044
1097
  }
1045
1098
 
1046
- function buildEvidencePackSample(sample, returnedRows) {
1099
+ function buildEvidencePackSample(sample, returnedRows, totalMatches) {
1047
1100
  return {
1048
1101
  mode: normalizeEvidencePackSampleMode(sample.mode),
1049
1102
  requestedRows: sample.requestedSize,
@@ -1051,16 +1104,31 @@ function buildEvidencePackSample(sample, returnedRows) {
1051
1104
  returnedRows,
1052
1105
  fetchedPages: sample.fetchedPages,
1053
1106
  skippedPages: sample.skippedPages,
1054
- totalMatches: sample.totalMatches
1107
+ totalMatches: typeof totalMatches === "number" ? totalMatches : sample.totalMatches
1055
1108
  };
1056
1109
  }
1057
1110
 
1058
- function buildReturnedPageSample(pagination, returnedRows) {
1111
+ function buildReturnedPageSample(pagination, returnedRows, totalMatches) {
1059
1112
  return {
1060
1113
  mode: "returned_page",
1061
1114
  requestedRows: pagination?.limit,
1062
1115
  returnedRows,
1063
- totalMatches: pagination?.total
1116
+ totalMatches: typeof totalMatches === "number" ? totalMatches : pagination?.total
1117
+ };
1118
+ }
1119
+
1120
+ function getTotalMatches(count, pagination) {
1121
+ const countValue = numericValue(count?.data?.count);
1122
+ if (Number.isFinite(countValue)) return countValue;
1123
+ const paginationTotal = numericValue(pagination?.total);
1124
+ return Number.isFinite(paginationTotal) ? paginationTotal : undefined;
1125
+ }
1126
+
1127
+ function withPaginationTotal(pagination, totalMatches) {
1128
+ if (typeof totalMatches !== "number") return pagination;
1129
+ return {
1130
+ ...(pagination ?? {}),
1131
+ total: totalMatches
1064
1132
  };
1065
1133
  }
1066
1134
 
@@ -1096,6 +1164,17 @@ function buildSearchUpstreamRequests(searches, pageErrors = []) {
1096
1164
  ];
1097
1165
  }
1098
1166
 
1167
+ function buildCountUpstreamRequests(count) {
1168
+ if (!count) return [];
1169
+ return [{
1170
+ kind: "count",
1171
+ requestId: count?.meta?.requestId,
1172
+ source: count?.meta?.source,
1173
+ entitlements: count?.meta?.entitlements,
1174
+ count: count?.data?.count
1175
+ }];
1176
+ }
1177
+
1099
1178
  function buildReviewPatternGame(request, entry) {
1100
1179
  const game = entry.game ?? {};
1101
1180
  const externalId = asString(entry.externalId ?? game?.externalId ?? game?.appid);
@@ -1303,7 +1382,7 @@ function buildReviewPatternSegments(games, segment) {
1303
1382
  }));
1304
1383
  }
1305
1384
 
1306
- function buildCohortEvidenceGame(request, entry) {
1385
+ function buildCohortEvidenceGame(request, entry, listNotes = new Map()) {
1307
1386
  const game = entry.game ?? {};
1308
1387
  const externalId = asString(entry.externalId ?? game?.externalId ?? game?.appid);
1309
1388
  const sections = getAnalysisSections(entry.analysis);
@@ -1324,6 +1403,7 @@ function buildCohortEvidenceGame(request, entry) {
1324
1403
  coverage: game?.coverage,
1325
1404
  tags: getRankedTagNames(game).slice(0, 8),
1326
1405
  genres: game?.genres,
1406
+ listNote: lookupListNote(listNotes, game?.appid, externalId),
1327
1407
  analysis: entry.analysis,
1328
1408
  sections,
1329
1409
  patternEvidence,
@@ -1332,6 +1412,23 @@ function buildCohortEvidenceGame(request, entry) {
1332
1412
  };
1333
1413
  }
1334
1414
 
1415
+ function collectListNotes(searches) {
1416
+ const notes = new Map();
1417
+ for (const search of searches) {
1418
+ if (!search?.notes || typeof search.notes !== "object") continue;
1419
+ for (const [appid, note] of Object.entries(search.notes)) {
1420
+ if (typeof note === "string" && note.trim()) notes.set(String(appid), note);
1421
+ }
1422
+ }
1423
+ return notes;
1424
+ }
1425
+
1426
+ function lookupListNote(notes, appid, externalId) {
1427
+ if (!notes?.size) return undefined;
1428
+ const appidKey = appid == null ? undefined : String(appid);
1429
+ return (appidKey ? notes.get(appidKey) : undefined) ?? (externalId ? notes.get(String(externalId)) : undefined);
1430
+ }
1431
+
1335
1432
  function buildCohortEvidenceSummary(request, entries, pagination) {
1336
1433
  return {
1337
1434
  sampleSize: entries.length,
@@ -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, executeRequestPlan } from "./workflows.js";
5
5
 
6
6
  test("cohort evidence summarizes skipped games by reason", () => {
7
7
  const request = {
@@ -78,3 +78,85 @@ 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
+ });
108
+
109
+ test("executeRequestPlan normalizes list game resolution for cohort workflows", async () => {
110
+ const originalFetch = globalThis.fetch;
111
+ const seenUrls = [];
112
+ globalThis.fetch = async (url) => {
113
+ seenUrls.push(String(url));
114
+ if (String(url).includes("/analysis")) {
115
+ return jsonResponse({
116
+ data: { appid: 730, sections: {} },
117
+ meta: { requestId: "analysis_req" }
118
+ });
119
+ }
120
+ return jsonResponse({
121
+ data: {
122
+ list: { id: "list-1", name: "Competitors", kind: "manual" },
123
+ games: [{ appid: 730, externalId: "730", title: "Example", reviews: 100 }],
124
+ notes: { 730: "Positioning reference" }
125
+ },
126
+ pagination: { limit: 20, offset: 0, total: 1, nextOffset: null },
127
+ meta: { requestId: "list_req", source: "static-fixture" }
128
+ });
129
+ };
130
+
131
+ try {
132
+ const payload = await executeRequestPlan("https://gmc.test", {
133
+ kind: "cohortEvidence",
134
+ source: "steam",
135
+ workflow: "cohortTrends",
136
+ evidenceType: "trends",
137
+ sections: [],
138
+ query: { list: "list-1", limit: "20" },
139
+ searchRequest: {
140
+ path: "/api/v1/lists/list-1/games",
141
+ query: { list: "list-1", limit: "20" }
142
+ }
143
+ }, { apiKey: "gmck_test" });
144
+
145
+ assert.equal(payload.data.cohort.returnedGames, 1);
146
+ assert.equal(payload.data.cohort.included[0].externalId, "730");
147
+ assert.equal(payload.data.cohort.included[0].evidence.listNote, "Positioning reference");
148
+ assert.equal(payload.data.games[0].listNote, "Positioning reference");
149
+ assert.ok(seenUrls.some((url) => url.includes("/api/v1/lists/list-1/games")));
150
+ } finally {
151
+ globalThis.fetch = originalFetch;
152
+ }
153
+ });
154
+
155
+ function jsonResponse(payload, status = 200) {
156
+ return {
157
+ ok: status >= 200 && status < 300,
158
+ status,
159
+ headers: new Headers({ "content-type": "application/json" }),
160
+ json: async () => payload
161
+ };
162
+ }