@witchpot/gmc 0.1.3 → 0.1.5

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to the Game Market Copilot CLI will be recorded here.
4
4
 
5
+ ## 0.1.5 - 2026-06-12
6
+
7
+ - Added `gmc games success-report` for the per-game Success Factor Analysis
8
+ report (executive summary plus markdown body; plan-gated like the API).
9
+ - Added `--coverage <basic|partial|full[,...]>` to games search, count,
10
+ aggregate, benchmark, resolve, review patterns, and cohort workflows so
11
+ agents can scope cohorts to researched titles before spending quota.
12
+ - Added cohort `skippedReasons` tallies to review-pattern and cohort evidence
13
+ summaries so "no data collected" is distinguishable from "no findings".
14
+ - Added a `node --test` unit harness for CLI request planning (`npm test`).
15
+
16
+ ## 0.1.4 - 2026-06-01
17
+
18
+ - Reissued the beta CLI package after the count and aggregate API server
19
+ implementation was committed for deployment alignment.
20
+
5
21
  ## 0.1.3 - 2026-06-01
6
22
 
7
23
  - Added `gmc games count` for fast Steam catalog counts with bounded filters.
package/README.md CHANGED
@@ -76,9 +76,12 @@ when using output in agent answers.
76
76
  - `gmc auth status`
77
77
  - `gmc schema`
78
78
  - `gmc games search`
79
+ - `gmc games count`
80
+ - `gmc games aggregate`
79
81
  - `gmc games resolve`
80
82
  - `gmc games detail`
81
83
  - `gmc games analysis`
84
+ - `gmc games success-report`
82
85
  - `gmc campaigns signals`
83
86
  - `gmc games topics`
84
87
  - `gmc games compare`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@witchpot/gmc",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Command line tools for Game Market Copilot market research.",
6
6
  "license": "UNLICENSED",
@@ -36,6 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "scripts": {
39
+ "test": "node --test src/*.test.js",
39
40
  "release:check": "node scripts/check-release-readiness.mjs && node scripts/smoke-pack.mjs",
40
41
  "prepublishOnly": "npm run release:check",
41
42
  "smoke:beta": "node scripts/smoke-beta.mjs",
package/src/args.js CHANGED
@@ -71,7 +71,7 @@ function isCommandToken(command, token) {
71
71
  return ["get", "set"].includes(token);
72
72
  }
73
73
  if (command[0] === "games") {
74
- return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics"].includes(token);
74
+ return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "success-report"].includes(token);
75
75
  }
76
76
  if (command[0] === "reviews") {
77
77
  return token === "patterns";
package/src/constants.js CHANGED
@@ -8,7 +8,7 @@ export const EXIT_API = 4;
8
8
 
9
9
  export const CLI_PACKAGE_NAME = "@witchpot/gmc";
10
10
  export const CLI_BETA_PACKAGE_NAME = "@witchpot/gmc";
11
- export const CLI_VERSION = "0.1.3";
11
+ export const CLI_VERSION = "0.1.5";
12
12
 
13
13
  export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
14
14
  export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
@@ -25,6 +25,7 @@ export const SUPPORTED_BETA_COMMANDS = [
25
25
  "gmc games resolve --source steam --query <title> --json",
26
26
  "gmc games detail --source steam <externalId> --json",
27
27
  "gmc games analysis --source steam <externalId> --sections reviews,marketing --json",
28
+ "gmc games success-report --source steam <externalId> --json",
28
29
  "gmc campaigns signals --source steam <externalId> --json",
29
30
  "gmc games topics --source steam <externalId> --json",
30
31
  "gmc games compare --source steam <externalId> <externalId...> --focus market --json",
package/src/help.js CHANGED
@@ -31,6 +31,7 @@ Usage:
31
31
  gmc games detail --source steam <externalId> --json
32
32
  gmc games detail --source steam <externalId> --fields title,snapshot.rating,snapshot.reviews --json
33
33
  gmc games analysis --source steam <externalId> --sections reviews,marketing --json
34
+ gmc games success-report --source steam <externalId> --json
34
35
  gmc campaigns signals --source steam <externalId> --json
35
36
  gmc games compare --source steam <externalId> <externalId...> --focus market --json
36
37
  gmc games topics --source steam <externalId> --json
@@ -81,6 +82,9 @@ Filters:
81
82
  --rating-min <number> Inclusive rating lower bound, 0-100.
82
83
  --price-min <number> Inclusive price lower bound.
83
84
  --price-max <number> Inclusive price upper bound.
85
+ --coverage <list> Research coverage levels: basic, partial, full.
86
+ Use --coverage full,partial to keep only games
87
+ with collected analysis data.
84
88
 
85
89
  Sorting:
86
90
  --sort <key> reviews_desc, reviews_asc, rating_desc,
package/src/requests.js CHANGED
@@ -129,6 +129,18 @@ export function buildRequest(command, options, positional) {
129
129
  };
130
130
  }
131
131
 
132
+ if (matches(command, ["games", "success-report"])) {
133
+ assertSource(options.source);
134
+ const externalId = positional[0];
135
+ assertExternalId(externalId, "gmc games success-report --source steam <externalId> --json");
136
+ return {
137
+ path: `/api/v1/steam/games/${encodeURIComponent(externalId)}/success-report`,
138
+ query: pickQuery(options, {
139
+ locale: "locale"
140
+ })
141
+ };
142
+ }
143
+
132
144
  if (matches(command, ["campaigns", "signals"])) {
133
145
  assertSource(options.source);
134
146
  const externalId = positional[0];
@@ -426,12 +438,14 @@ function buildGameCountQuery(options) {
426
438
  "rating-min": "ratingMin",
427
439
  "price-min": "priceMin",
428
440
  "price-max": "priceMax",
441
+ coverage: "coverage",
429
442
  locale: "locale"
430
443
  });
431
444
 
432
445
  applyReleaseYearAlias(query, options);
433
446
  assertReviewBoundOptions(options);
434
447
  if (query.tags) query.tags = normalizeTagSlugCsv(query.tags);
448
+ if (query.coverage) query.coverage = assertCoverageCsv(query.coverage);
435
449
  return query;
436
450
  }
437
451
 
@@ -588,6 +602,18 @@ function assertBoundedPositiveIntegerOption(value, optionName, maxValue) {
588
602
  return number;
589
603
  }
590
604
 
605
+ function assertCoverageCsv(value) {
606
+ const levels = parseCsvString(value).map((level) => level.toLowerCase());
607
+ if (levels.length === 0 || levels.some((level) => !["basic", "partial", "full"].includes(level))) {
608
+ writeJsonError({
609
+ code: "CLI_USAGE_ERROR",
610
+ message: "--coverage accepts a comma-separated subset of: basic, partial, full"
611
+ });
612
+ process.exit(EXIT_USAGE);
613
+ }
614
+ return levels.join(",");
615
+ }
616
+
591
617
  function assertReviewPatternSentiment(value) {
592
618
  const sentiment = String(value);
593
619
  if (!["positive", "negative", "mixed"].includes(sentiment)) {
@@ -0,0 +1,104 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { parseArgs } from "./args.js";
5
+ import { buildRequest } from "./requests.js";
6
+
7
+ function buildRequestFromArgv(argv) {
8
+ const { command, options, positional } = parseArgs(argv);
9
+ return buildRequest(command, options, positional);
10
+ }
11
+
12
+ test("games success-report builds the success-report request plan", () => {
13
+ const request = buildRequestFromArgv([
14
+ "games",
15
+ "success-report",
16
+ "--source",
17
+ "steam",
18
+ "1062090",
19
+ "--json"
20
+ ]);
21
+
22
+ assert.equal(request.path, "/api/v1/steam/games/1062090/success-report");
23
+ assert.deepEqual(request.query, {});
24
+ });
25
+
26
+ test("games success-report forwards locale", () => {
27
+ const request = buildRequestFromArgv([
28
+ "games",
29
+ "success-report",
30
+ "--source",
31
+ "steam",
32
+ "1062090",
33
+ "--locale",
34
+ "ja",
35
+ "--json"
36
+ ]);
37
+
38
+ assert.equal(request.path, "/api/v1/steam/games/1062090/success-report");
39
+ assert.deepEqual(request.query, { locale: "ja" });
40
+ });
41
+
42
+ test("games detail request plan stays unchanged", () => {
43
+ const request = buildRequestFromArgv([
44
+ "games",
45
+ "detail",
46
+ "--source",
47
+ "steam",
48
+ "1062090",
49
+ "--json"
50
+ ]);
51
+
52
+ assert.equal(request.path, "/api/v1/steam/games/1062090");
53
+ assert.deepEqual(request.query, {});
54
+ });
55
+
56
+ test("games search forwards coverage as a filter", () => {
57
+ const request = buildRequestFromArgv([
58
+ "games",
59
+ "search",
60
+ "--source",
61
+ "steam",
62
+ "--coverage",
63
+ "full,partial",
64
+ "--reviews-min",
65
+ "1000",
66
+ "--json"
67
+ ]);
68
+
69
+ assert.equal(request.path, "/api/v1/steam/games");
70
+ assert.equal(request.query.coverage, "full,partial");
71
+ assert.equal(request.query.reviewsMin, "1000");
72
+ });
73
+
74
+ test("games count forwards coverage for cohort pre-estimation", () => {
75
+ const request = buildRequestFromArgv([
76
+ "games",
77
+ "count",
78
+ "--source",
79
+ "steam",
80
+ "--coverage",
81
+ "full",
82
+ "--json"
83
+ ]);
84
+
85
+ assert.equal(request.path, "/api/v1/steam/games/count");
86
+ assert.equal(request.query.coverage, "full");
87
+ });
88
+
89
+ test("reviews patterns forwards coverage into the cohort search request", () => {
90
+ const request = buildRequestFromArgv([
91
+ "reviews",
92
+ "patterns",
93
+ "--source",
94
+ "steam",
95
+ "--tags",
96
+ "roguelike",
97
+ "--coverage",
98
+ "full,partial",
99
+ "--json"
100
+ ]);
101
+
102
+ assert.equal(request.kind, "reviewPatterns");
103
+ assert.equal(request.searchRequest.query.coverage, "full,partial");
104
+ });
package/src/workflows.js CHANGED
@@ -437,6 +437,15 @@ export function composeBenchmarkPayload(request, search, options = {}) {
437
437
  };
438
438
  }
439
439
 
440
+ function tallySkippedReasons(skipped) {
441
+ const reasons = {};
442
+ for (const entry of skipped) {
443
+ const reason = entry?.reason ?? "unknown";
444
+ reasons[reason] = (reasons[reason] ?? 0) + 1;
445
+ }
446
+ return reasons;
447
+ }
448
+
440
449
  export function composeReviewPatternsPayload(request, result, options = {}) {
441
450
  const generatedAt = options.generatedAt ?? new Date().toISOString();
442
451
  const requestId = options.requestId ?? createCliRequestId();
@@ -495,6 +504,7 @@ export function composeReviewPatternsPayload(request, result, options = {}) {
495
504
  returnedGames: games.length,
496
505
  usableGames: usableGames.length,
497
506
  skippedGames: skipped.length,
507
+ skippedReasons: tallySkippedReasons(skipped),
498
508
  totalMatches: result.search?.pagination?.total,
499
509
  included: usableGames.map((game) => ({
500
510
  source: request.source,
@@ -687,6 +697,7 @@ export function composeCohortEvidencePayload(request, result, options = {}) {
687
697
  returnedGames: games.length,
688
698
  usableGames: included.length,
689
699
  skippedGames: skipped.length,
700
+ skippedReasons: tallySkippedReasons(skipped),
690
701
  totalMatches: result.search?.pagination?.total,
691
702
  included: included.map((entry) => ({
692
703
  source: request.source,
@@ -0,0 +1,80 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { composeCohortEvidencePayload } from "./workflows.js";
5
+
6
+ test("cohort evidence summarizes skipped games by reason", () => {
7
+ const request = {
8
+ source: "steam",
9
+ workflow: "cohortTrends",
10
+ evidenceType: "trends",
11
+ sections: [],
12
+ query: { sort: "reviews_desc", limit: 20 }
13
+ };
14
+ const result = {
15
+ search: {
16
+ data: [
17
+ { appid: 1, title: "Usable", reviews: 100 },
18
+ { appid: 2, title: "Failed A" },
19
+ { appid: 3, title: "Failed B" },
20
+ { appid: 4, title: "No Id" }
21
+ ],
22
+ pagination: { limit: 20, offset: 0, total: 4, nextOffset: null }
23
+ },
24
+ analysisResults: [
25
+ { game: { appid: 1, title: "Usable", reviews: 100 }, externalId: "1" },
26
+ {
27
+ game: { appid: 2, title: "Failed A" },
28
+ externalId: "2",
29
+ skippedReason: "analysis_request_failed",
30
+ error: { code: "UPSTREAM_UNAVAILABLE" }
31
+ },
32
+ {
33
+ game: { appid: 3, title: "Failed B" },
34
+ externalId: "3",
35
+ skippedReason: "analysis_request_failed",
36
+ error: { code: "UPSTREAM_UNAVAILABLE" }
37
+ },
38
+ { game: { appid: 4, title: "No Id" }, skippedReason: "missing_external_id" }
39
+ ]
40
+ };
41
+
42
+ const payload = composeCohortEvidencePayload(request, result, {
43
+ generatedAt: "2026-06-12T00:00:00.000Z",
44
+ requestId: "req_test"
45
+ });
46
+
47
+ assert.equal(payload.data.cohort.returnedGames, 4);
48
+ assert.equal(payload.data.cohort.usableGames, 1);
49
+ assert.equal(payload.data.cohort.skippedGames, 3);
50
+ assert.deepEqual(payload.data.cohort.skippedReasons, {
51
+ analysis_request_failed: 2,
52
+ missing_external_id: 1
53
+ });
54
+ });
55
+
56
+ test("cohort evidence reports an empty reason tally when nothing is skipped", () => {
57
+ const request = {
58
+ source: "steam",
59
+ workflow: "cohortTrends",
60
+ evidenceType: "trends",
61
+ sections: [],
62
+ query: {}
63
+ };
64
+ const result = {
65
+ search: {
66
+ data: [{ appid: 1, title: "Usable", reviews: 100 }],
67
+ pagination: { limit: 20, offset: 0, total: 1, nextOffset: null }
68
+ },
69
+ analysisResults: [
70
+ { game: { appid: 1, title: "Usable", reviews: 100 }, externalId: "1" }
71
+ ]
72
+ };
73
+
74
+ const payload = composeCohortEvidencePayload(request, result, {
75
+ generatedAt: "2026-06-12T00:00:00.000Z",
76
+ requestId: "req_test"
77
+ });
78
+
79
+ assert.deepEqual(payload.data.cohort.skippedReasons, {});
80
+ });