@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.
- package/CHANGELOG.md +36 -1
- package/README.md +9 -3
- package/package.json +4 -1
- package/scripts/check-release-readiness.mjs +31 -0
- package/scripts/smoke-pack.mjs +3 -0
- package/skill/gmc-analysis/CHANGELOG.md +42 -0
- package/skill/gmc-analysis/SKILL.md +66 -6
- package/skill/gmc-analysis/references/charts.md +236 -0
- package/skill/gmc-analysis/references/pitfalls.md +50 -0
- package/skill/gmc-analysis/references/quota.md +14 -7
- package/skill/gmc-analysis/references/validity.md +78 -0
- package/src/args.js +1 -0
- package/src/constants.js +4 -9
- package/src/help.js +15 -0
- package/src/requests.js +14 -15
- package/src/requests.test.js +35 -0
- package/src/skill.js +1 -1
- package/src/skill.test.js +9 -0
- package/src/workflows.js +23 -1589
|
@@ -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
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.
|
|
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
|
|
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
|
-
}
|
package/src/requests.test.js
CHANGED
|
@@ -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", () => {
|