@witchpot/gmc 0.2.1 → 0.4.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.
@@ -0,0 +1,78 @@
1
+ # Statistical validity and claim safety
2
+
3
+ Every number you publish from gmc data — in an internal doc, a chat answer,
4
+ or a chart card — must pass this checklist. These rules exist because QA on
5
+ real analysis output repeatedly caught bad denominators, small-N
6
+ generalizations, confounded comparisons, and causal overclaims before they
7
+ reached readers. Run the checklist before handing off, not after.
8
+
9
+ ## Validity checklist (apply to every reported number)
10
+
11
+ 1. **Denominator and N on every share.** Never report a bare percentage.
12
+ State what the share is out of and how many that is: "34% (412 of 1,204
13
+ titles matching the filter)". If the denominator is a subset (analyzed
14
+ games, usable games), say so — `cohort.usableGames` vs the plain cohort
15
+ count are different denominators (see the pitfalls reference).
16
+ 2. **Counting basis named.** A tag/genre share must say whether it is
17
+ single-assignment (`primary_market_tag`) or multi-assignment (`tag`
18
+ grouping / `tags` filter). The two bases are not comparable — pitfall P9.
19
+ 3. **Observation window stated.** Every metric is a snapshot or a windowed
20
+ observation. State the window or snapshot date next to the number
21
+ ("releases 2024-01..2024-12, snapshot as of the response's
22
+ `meta.snapshot`/`generatedAt`"). Numbers without a window cannot be
23
+ reproduced or compared later.
24
+ 4. **Small-N warning.** If a group has N < 30, label the finding as
25
+ directional/anecdotal and do not generalize from it ("only 12 titles in
26
+ this bucket — treat as anecdote, not a segment truth"). Do not silently
27
+ drop small groups either; show them with the warning.
28
+ 5. **Effect size over bare significance.** Report the magnitude of a
29
+ difference, not just that one exists. On large N, tiny differences look
30
+ "real" but mean nothing for decisions; on small N, large-looking
31
+ differences may be noise. Ask: would this gap change anyone's decision?
32
+ 6. **Stratify obvious confounds before comparing groups.** Before claiming
33
+ "group A outperforms group B", check whether the gap survives splitting
34
+ by the usual suspects: publisher axis (`self_published`), price band,
35
+ and release year. If you cannot stratify (quota, missing dimension),
36
+ state the unchecked confound explicitly.
37
+ 7. **Disclose multiple comparisons.** If you scanned many tags, buckets, or
38
+ metrics and are reporting the extremes ("the top tag by rating"),
39
+ say that the winner came from a scan of K groups. Top-of-scan findings
40
+ are selection-biased; confirm them on an independent slice or label them
41
+ as hypotheses.
42
+
43
+ ## Claim-safety rules
44
+
45
+ - **No causal verbs without causal evidence.** gmc aggregates are
46
+ observational. "Drives", "boosts", "causes", "leads to", "because of" are
47
+ off-limits for correlations; write "is associated with", "co-occurs
48
+ with", "titles with X tend to Y". This matches the chart-card rule that
49
+ titles must not make causal claims (charts reference) — apply it to prose
50
+ too.
51
+ - **Label external estimates.** Any figure not computed from gmc responses
52
+ — units-per-review multipliers, wishlist conversion folklore, revenue
53
+ guesses, industry benchmarks — must be labeled as an external estimate
54
+ with its source, and must never be blended into gmc-derived numbers as if
55
+ measured.
56
+ - **`not_collected` means not collected.** Missing research coverage,
57
+ skipped games, and `not_collected` markers must never be reported as zero
58
+ events or absent sentiment ("no complaints"). Report them as "not
59
+ collected" with the skipped count — see the hard rules in SKILL.md and
60
+ pitfall P2.
61
+ - **Locked/preview data stays partial.** Never extrapolate what a locked or
62
+ preview payload "probably" contains (pitfall P3).
63
+
64
+ ## Compliant output example
65
+
66
+ > Of the 1,204 titles released 2024-01-01..2024-12-31 with >= 10 reviews
67
+ > (snapshot 2026-07-08), 412 (34%) carry the roguelike-deckbuilder tag
68
+ > (multi-assignment basis: the same title also counts toward its other
69
+ > tags' shares, so per-tag shares do not sum to 100%).
70
+ > Median review count is 86 for these vs 54 for the rest; the gap holds
71
+ > within the self-published stratum (78 vs 51, N=301) but not for
72
+ > publisher-backed titles (N=23 — too few to call, directional only).
73
+ > This is an observed association; nothing here shows the tag causes
74
+ > higher review counts.
75
+
76
+ Every share above carries a denominator, an N, a window/snapshot, and a
77
+ named counting basis; comparisons are stratified or flagged; language stays
78
+ associational. That is the bar for skill-produced output.
package/src/args.js CHANGED
@@ -49,6 +49,7 @@ function isCommandToken(command, token) {
49
49
  return [
50
50
  "status",
51
51
  "schema",
52
+ "tags",
52
53
  "games",
53
54
  "reviews",
54
55
  "campaigns",
package/src/constants.js CHANGED
@@ -7,7 +7,7 @@ export const EXIT_CONFIG = 3;
7
7
  export const EXIT_API = 4;
8
8
 
9
9
  export const CLI_PACKAGE_NAME = "@witchpot/gmc";
10
- export const CLI_VERSION = "0.2.1";
10
+ export const CLI_VERSION = "0.4.0";
11
11
 
12
12
  export const DEFAULT_API_BASE_URL = "https://gamemarketcopilot.com";
13
13
  export const DEFAULT_CONFIG_PATH = path.join(homedir(), ".config", "gmc", "config.json");
@@ -16,6 +16,8 @@ export const SUPPORTED_BETA_COMMANDS = [
16
16
  "gmc status --json",
17
17
  "gmc auth status --json",
18
18
  "gmc schema --json",
19
+ "gmc tags --json",
20
+ "gmc tags --q <term> --json",
19
21
  "gmc --version",
20
22
  "gmc update --json",
21
23
  "gmc skill install --json",
@@ -53,14 +55,7 @@ export const PROVISIONAL_BETA_COMMANDS = [
53
55
  ];
54
56
 
55
57
  export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
56
- export const COMPARE_FOCUS_SECTIONS = {
57
- overview: [],
58
- market: [],
59
- reviews: ["reviews"],
60
- marketing: ["marketing", "creators", "media"],
61
- audience: ["reviews", "coverage", "creators"],
62
- showcases: ["showcases"]
63
- };
58
+ export { COMPARE_FOCUS_SECTIONS } from "@witchpot/gmc-core";
64
59
 
65
60
  export const DEFAULT_BENCHMARK_LIMIT = "20";
66
61
  export const DEFAULT_CAMPAIGN_SIGNALS_LIMIT = "30";
package/src/help.js CHANGED
@@ -25,6 +25,8 @@ Usage:
25
25
  gmc skill print --format agents-md
26
26
  gmc skill status --json
27
27
  gmc schema --json
28
+ gmc tags --json
29
+ gmc tags --q <term> --limit <number> --json
28
30
  gmc lists --json
29
31
  gmc lists create --name <name> --kind manual --json
30
32
  gmc lists games <id> --json
@@ -56,6 +58,19 @@ Usage:
56
58
  gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json
57
59
  gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json
58
60
 
61
+ Tag taxonomy:
62
+ gmc tags --json List the full tag dictionary (slug, name).
63
+ gmc tags --q <term> --json Search tags by free-text term.
64
+ gmc tags --q <term> --limit <number> --json
65
+ Cap the number of matches, 1-500 (default 50).
66
+
67
+ Commands that filter by --tags (games search/count/aggregate/benchmark,
68
+ reviews patterns, marketing/creators/cohort/showcases workflows) reject
69
+ unknown tag slugs with a 400 INVALID_TAG error and a
70
+ details.unknown_tags / details.suggestions payload. Run gmc tags --q
71
+ <term> first to resolve a genre or keyword to valid slugs before
72
+ building a --tags filter.
73
+
59
74
  Binary aliases:
60
75
  gmcp and game-market-copilot run the same CLI. Use them when a shell
61
76
  alias shadows gmc (Oh My Zsh's git plugin defines gmc as
package/src/requests.js CHANGED
@@ -14,6 +14,9 @@ import {
14
14
  } from "./constants.js";
15
15
  import { matches } from "./args.js";
16
16
  import { writeJsonError } from "./output.js";
17
+ import { filterShowcasesByDeadline } from "@witchpot/gmc-core";
18
+
19
+ export { filterShowcasesByDeadline };
17
20
 
18
21
  export function buildRequest(command, options, positional) {
19
22
  if (matches(command, ["status"])) {
@@ -37,6 +40,17 @@ export function buildRequest(command, options, positional) {
37
40
  };
38
41
  }
39
42
 
43
+ if (matches(command, ["tags"])) {
44
+ const q = options.q ?? options.query;
45
+ return {
46
+ path: "/api/v1/tags",
47
+ query: {
48
+ ...(q != null ? { q } : {}),
49
+ ...pickQuery(options, { limit: "limit" })
50
+ }
51
+ };
52
+ }
53
+
40
54
  if (matches(command, ["lists"])) {
41
55
  return {
42
56
  path: "/api/v1/lists",
@@ -886,18 +900,3 @@ function normalizeTagSlugCsv(value) {
886
900
  .filter(Boolean)
887
901
  .join(",");
888
902
  }
889
-
890
- export function filterShowcasesByDeadline(payload, deadlineBefore) {
891
- if (!payload || !Array.isArray(payload.data)) return payload;
892
- return {
893
- ...payload,
894
- data: payload.data.filter((showcase) => {
895
- if (!showcase || typeof showcase !== "object" || !showcase.deadline) return false;
896
- return String(showcase.deadline) <= deadlineBefore;
897
- }),
898
- warnings: [
899
- ...(payload.warnings ?? []),
900
- "deadline-before was applied client-side by the gmc CLI"
901
- ]
902
- };
903
- }
@@ -39,6 +39,41 @@ test("games success-report forwards locale", () => {
39
39
  assert.deepEqual(request.query, { locale: "ja" });
40
40
  });
41
41
 
42
+ test("tags builds the full-dictionary request plan", () => {
43
+ const request = buildRequestFromArgv(["tags", "--json"]);
44
+
45
+ assert.equal(request.path, "/api/v1/tags");
46
+ assert.deepEqual(request.query, {});
47
+ });
48
+
49
+ test("tags forwards --q as the q query param", () => {
50
+ const request = buildRequestFromArgv(["tags", "--q", "roguelike", "--json"]);
51
+
52
+ assert.equal(request.path, "/api/v1/tags");
53
+ assert.deepEqual(request.query, { q: "roguelike" });
54
+ });
55
+
56
+ test("tags accepts --query as an alias for --q", () => {
57
+ const request = buildRequestFromArgv(["tags", "--query", "roguelike", "--json"]);
58
+
59
+ assert.equal(request.path, "/api/v1/tags");
60
+ assert.deepEqual(request.query, { q: "roguelike" });
61
+ });
62
+
63
+ test("tags forwards --limit alongside --q", () => {
64
+ const request = buildRequestFromArgv([
65
+ "tags",
66
+ "--q",
67
+ "deck",
68
+ "--limit",
69
+ "10",
70
+ "--json"
71
+ ]);
72
+
73
+ assert.equal(request.path, "/api/v1/tags");
74
+ assert.deepEqual(request.query, { q: "deck", limit: "10" });
75
+ });
76
+
42
77
  test("games detail request plan stays unchanged", () => {
43
78
  const request = buildRequestFromArgv([
44
79
  "games",
package/src/skill.js CHANGED
@@ -7,7 +7,7 @@ import { EXIT_USAGE } from "./constants.js";
7
7
  import { writeJsonError } from "./output.js";
8
8
 
9
9
  export const SKILL_NAME = "gmc-analysis";
10
- const SKILL_REFERENCE_FILES = ["recipes.md", "pitfalls.md", "quota.md"];
10
+ const SKILL_REFERENCE_FILES = ["recipes.md", "pitfalls.md", "validity.md", "quota.md", "charts.md"];
11
11
  const PACKAGED_SKILL_DIR = fileURLToPath(new URL(`../skill/${SKILL_NAME}/`, import.meta.url));
12
12
 
13
13
  export function getPackagedSkillDir() {
package/src/skill.test.js CHANGED
@@ -25,10 +25,19 @@ test("agents-md edition is self-contained and demotes headings", () => {
25
25
  // References must be inlined for AGENTS.md consumers without the skill dir.
26
26
  assert.match(section, /^### Recipes/m);
27
27
  assert.match(section, /^### Pitfalls and pivots/m);
28
+ assert.match(section, /^### Statistical validity and claim safety/m);
28
29
  assert.match(section, /^### Quota etiquette/m);
30
+ assert.match(section, /^### Chart cards/m);
29
31
  // Core field-test learnings must survive into the distributed edition.
30
32
  assert.match(section, /skippedReasons/);
31
33
  assert.match(section, /appearsInGames/);
34
+ // Validity guardrails must survive into the distributed edition.
35
+ assert.match(section, /Denominator and N on every share/);
36
+ assert.match(section, /No causal verbs without causal evidence/);
37
+ assert.match(section, /never mix them/);
38
+ // Chart attribution guardrails must survive into the distributed edition.
39
+ assert.match(section, /GMC database/);
40
+ assert.match(section, /gmc-lockup\.svg/);
32
41
  });
33
42
 
34
43
  test("skill install copies the packaged skill to a custom directory", () => {