@witchpot/gmc 0.6.0 → 0.8.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 +58 -0
- package/README.md +3 -0
- package/package.json +2 -2
- package/skill/gmc-analysis/CHANGELOG.md +172 -0
- package/skill/gmc-analysis/SKILL.md +372 -7
- package/skill/gmc-analysis.manifest.json +2 -2
- package/src/args.js +42 -4
- package/src/constants.js +20 -3
- package/src/help.js +208 -1
- package/src/index.js +1 -0
- package/src/requests.js +399 -2
- package/src/requests.test.js +870 -0
- package/src/workflows.js +15 -1
- package/src/workflows.test.js +156 -0
package/src/workflows.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
composeResolvePayload,
|
|
12
12
|
composeReviewPatternsPayload,
|
|
13
13
|
composeShowcaseFitPayload,
|
|
14
|
+
composeShowcaseHistoryPayload,
|
|
14
15
|
getPreferredShowcaseTags,
|
|
15
16
|
mergeSearchPages
|
|
16
17
|
} from "@witchpot/gmc-core";
|
|
@@ -22,7 +23,8 @@ export {
|
|
|
22
23
|
composeComparePayload,
|
|
23
24
|
composeResolvePayload,
|
|
24
25
|
composeReviewPatternsPayload,
|
|
25
|
-
composeShowcaseFitPayload
|
|
26
|
+
composeShowcaseFitPayload,
|
|
27
|
+
composeShowcaseHistoryPayload
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
export async function executeRequestPlan(baseUrl, request, { apiKey }) {
|
|
@@ -47,6 +49,9 @@ export async function executeRequestPlan(baseUrl, request, { apiKey }) {
|
|
|
47
49
|
if (request.kind === "showcaseFit") {
|
|
48
50
|
return executeShowcaseFitRequest(baseUrl, request, { apiKey });
|
|
49
51
|
}
|
|
52
|
+
if (request.kind === "showcaseHistory") {
|
|
53
|
+
return executeShowcaseHistoryRequest(baseUrl, request, { apiKey });
|
|
54
|
+
}
|
|
50
55
|
|
|
51
56
|
const response = await fetchJson(createRequestUrl(baseUrl, request), {
|
|
52
57
|
apiKey,
|
|
@@ -240,6 +245,15 @@ async function executeShowcaseFitRequest(baseUrl, request, { apiKey }) {
|
|
|
240
245
|
});
|
|
241
246
|
}
|
|
242
247
|
|
|
248
|
+
async function executeShowcaseHistoryRequest(baseUrl, request, { apiKey }) {
|
|
249
|
+
const detailRequest = {
|
|
250
|
+
path: `/api/v1/steam/games/${encodeURIComponent(request.externalId)}/analysis`,
|
|
251
|
+
query: request.detailQuery
|
|
252
|
+
};
|
|
253
|
+
const detail = await fetchJson(createRequestUrl(baseUrl, detailRequest), { apiKey });
|
|
254
|
+
return composeShowcaseHistoryPayload(request, { detail });
|
|
255
|
+
}
|
|
256
|
+
|
|
243
257
|
async function executeSearchPages(baseUrl, searchRequest, sampling, { apiKey }) {
|
|
244
258
|
if (!sampling) {
|
|
245
259
|
return {
|
package/src/workflows.test.js
CHANGED
|
@@ -152,6 +152,162 @@ test("executeRequestPlan normalizes list game resolution for cohort workflows",
|
|
|
152
152
|
}
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
test("games language-topics passes the insufficient-evidence envelope through untouched", async () => {
|
|
156
|
+
// NAK-455: the CLI is a transport for this endpoint. Evidence state,
|
|
157
|
+
// coverage, denominators, and the raw/weighted split must arrive exactly as
|
|
158
|
+
// the API produced them — an insufficient_evidence answer must never be
|
|
159
|
+
// flattened into empty rows or a zero.
|
|
160
|
+
const envelope = {
|
|
161
|
+
data: {
|
|
162
|
+
appid: 730,
|
|
163
|
+
evidenceState: "insufficient_evidence",
|
|
164
|
+
snapshot: {
|
|
165
|
+
periodStart: "2026-02-06",
|
|
166
|
+
periodEnd: "2026-08-06",
|
|
167
|
+
analysisVersion: "v1",
|
|
168
|
+
generatedAt: "2026-08-06T04:00:00.000Z"
|
|
169
|
+
},
|
|
170
|
+
estimateCaveat: "Weighted values are estimates.",
|
|
171
|
+
coverage: {
|
|
172
|
+
totalRows: 226,
|
|
173
|
+
rowsAtThreshold: 0,
|
|
174
|
+
languagesAtThreshold: 0,
|
|
175
|
+
topicsAtThreshold: 0,
|
|
176
|
+
appliedMinReviews: 30,
|
|
177
|
+
insufficientRowsReturned: 1,
|
|
178
|
+
insufficientRowsTruncated: false
|
|
179
|
+
},
|
|
180
|
+
rows: [
|
|
181
|
+
{
|
|
182
|
+
appid: 730,
|
|
183
|
+
clusterId: 11,
|
|
184
|
+
topic: "performance",
|
|
185
|
+
language: "german",
|
|
186
|
+
counts: {
|
|
187
|
+
sourceReviews: 400,
|
|
188
|
+
sampledReviews: 90,
|
|
189
|
+
analyzedReviews: 21,
|
|
190
|
+
distinctReviews: 12
|
|
191
|
+
},
|
|
192
|
+
estimates: { samplingWeightMethod: "source_over_analyzed_by_language_voted_up_v1" },
|
|
193
|
+
evidence: { minimumN: 30, status: "insufficient_evidence", meetsRequestedThreshold: false }
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
197
|
+
meta: { requestId: "req_test" },
|
|
198
|
+
warnings: []
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const originalFetch = globalThis.fetch;
|
|
202
|
+
const seenUrls = [];
|
|
203
|
+
globalThis.fetch = async (url) => {
|
|
204
|
+
seenUrls.push(String(url));
|
|
205
|
+
return jsonResponse(envelope);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
|
|
210
|
+
path: "/api/v1/steam/games/730/review-topics/languages",
|
|
211
|
+
query: { include_insufficient: "true" }
|
|
212
|
+
}, { apiKey: "gmck_test" });
|
|
213
|
+
|
|
214
|
+
assert.deepEqual(payload, envelope);
|
|
215
|
+
assert.equal(payload.data.evidenceState, "insufficient_evidence");
|
|
216
|
+
assert.equal(payload.data.coverage.totalRows, 226);
|
|
217
|
+
assert.equal(payload.data.rows[0].counts.distinctReviews, 12);
|
|
218
|
+
assert.deepEqual(seenUrls, [
|
|
219
|
+
"https://gamemarketcopilot.com/api/v1/steam/games/730/review-topics/languages?include_insufficient=true"
|
|
220
|
+
]);
|
|
221
|
+
} finally {
|
|
222
|
+
globalThis.fetch = originalFetch;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("games review-history passes the three arrays through untouched", async () => {
|
|
227
|
+
// NAK-428: the CLI is a transport for this endpoint. An empty events or
|
|
228
|
+
// discountWindows array is a COLLECTION state the agent has to see as-is —
|
|
229
|
+
// the CLI must never drop, merge, or window the arrays, and must never turn
|
|
230
|
+
// an unmatched (open) discount window into a closed one.
|
|
231
|
+
const envelope = {
|
|
232
|
+
data: {
|
|
233
|
+
series: [
|
|
234
|
+
{ date: "2026-08-01", totalReviews: 1000 },
|
|
235
|
+
{ date: "2026-08-03", totalReviews: 1040 }
|
|
236
|
+
],
|
|
237
|
+
events: [
|
|
238
|
+
{ type: "release", date: "2026-07-01" },
|
|
239
|
+
{
|
|
240
|
+
type: "announcement",
|
|
241
|
+
date: "2026-08-02",
|
|
242
|
+
count: 3,
|
|
243
|
+
label: "Patch 1.2 is live",
|
|
244
|
+
url: "https://store.steampowered.com/news/app/730/view/1"
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
discountWindows: [
|
|
248
|
+
{ startDate: "2026-08-01", endDate: "2026-08-05", discountPercent: 33 },
|
|
249
|
+
{ startDate: "2026-08-20", endDate: null, discountPercent: null }
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
meta: { requestId: "req_test" },
|
|
253
|
+
warnings: []
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const originalFetch = globalThis.fetch;
|
|
257
|
+
const seenUrls = [];
|
|
258
|
+
globalThis.fetch = async (url) => {
|
|
259
|
+
seenUrls.push(String(url));
|
|
260
|
+
return jsonResponse(envelope);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
|
|
265
|
+
path: "/api/v1/steam/games/730/review-history",
|
|
266
|
+
query: {}
|
|
267
|
+
}, { apiKey: "gmck_test" });
|
|
268
|
+
|
|
269
|
+
assert.deepEqual(payload, envelope);
|
|
270
|
+
// The gap between 08-01 and 08-03 survives: collection gaps are real and
|
|
271
|
+
// consumers must scale by date, not by array index.
|
|
272
|
+
assert.deepEqual(payload.data.series.map((point) => point.date), ["2026-08-01", "2026-08-03"]);
|
|
273
|
+
assert.equal(payload.data.discountWindows[1].endDate, null);
|
|
274
|
+
// No query string appended for an endpoint that takes no parameters.
|
|
275
|
+
assert.deepEqual(seenUrls, [
|
|
276
|
+
"https://gamemarketcopilot.com/api/v1/steam/games/730/review-history"
|
|
277
|
+
]);
|
|
278
|
+
} finally {
|
|
279
|
+
globalThis.fetch = originalFetch;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("games review-history passes an all-empty payload through as-is", async () => {
|
|
284
|
+
// Empty arrays mean "nothing collected", never "nothing happened". The CLI
|
|
285
|
+
// must hand the agent the same empty arrays the API produced rather than
|
|
286
|
+
// omitting the keys, which would read as absent fields.
|
|
287
|
+
const envelope = {
|
|
288
|
+
data: { series: [], events: [], discountWindows: [] },
|
|
289
|
+
meta: { requestId: "req_test" },
|
|
290
|
+
warnings: ["static_fixture_data"]
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const originalFetch = globalThis.fetch;
|
|
294
|
+
globalThis.fetch = async () => jsonResponse(envelope);
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const payload = await executeRequestPlan("https://gamemarketcopilot.com", {
|
|
298
|
+
path: "/api/v1/steam/games/730/review-history",
|
|
299
|
+
query: {}
|
|
300
|
+
}, { apiKey: "gmck_test" });
|
|
301
|
+
|
|
302
|
+
assert.deepEqual(payload, envelope);
|
|
303
|
+
assert.deepEqual(payload.data.events, []);
|
|
304
|
+
assert.deepEqual(payload.data.discountWindows, []);
|
|
305
|
+
assert.deepEqual(payload.warnings, ["static_fixture_data"]);
|
|
306
|
+
} finally {
|
|
307
|
+
globalThis.fetch = originalFetch;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
155
311
|
function jsonResponse(payload, status = 200) {
|
|
156
312
|
return {
|
|
157
313
|
ok: status >= 200 && status < 300,
|