@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/CHANGELOG.md +17 -0
- package/README.md +17 -12
- package/package.json +3 -1
- package/scripts/smoke-beta.mjs +35 -9
- package/skill/gmc-analysis/SKILL.md +5 -0
- package/skill/gmc-analysis/references/pitfalls.md +3 -1
- package/skill/gmc-analysis/references/quota.md +3 -0
- package/skill/gmc-analysis/references/recipes.md +3 -1
- package/src/args.js +5 -1
- package/src/config.js +5 -5
- package/src/constants.js +4 -1
- package/src/help.js +9 -2
- package/src/main.js +12 -0
- package/src/requests.js +204 -10
- package/src/requests.test.js +101 -6
- package/src/skill.js +2 -1
- package/src/telemetry.js +341 -0
- package/src/telemetry.test.js +121 -0
- package/src/workflows.js +133 -36
- package/src/workflows.test.js +83 -1
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), {
|
|
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
|
|
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
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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:
|
|
462
|
+
upstreamRequests: [
|
|
463
|
+
...buildSearchUpstreamRequests(searches, pageErrors),
|
|
464
|
+
...buildCountUpstreamRequests(search.count)
|
|
465
|
+
]
|
|
428
466
|
},
|
|
429
|
-
...(
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
package/src/workflows.test.js
CHANGED
|
@@ -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
|
+
}
|