@witchpot/gmc 0.4.0 → 0.6.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 +27 -0
- package/package.json +2 -1
- package/scripts/check-release-readiness.mjs +19 -0
- package/scripts/gen-skill-manifest.mjs +151 -0
- package/skill/gmc-analysis/CHANGELOG.md +56 -0
- package/skill/gmc-analysis/SKILL.md +74 -1
- package/skill/gmc-analysis/references/charts.md +51 -7
- package/skill/gmc-analysis/references/quota.md +4 -2
- package/skill/gmc-analysis/references/recipes.md +27 -0
- package/skill/gmc-analysis.manifest.json +5 -0
- package/src/constants.js +1 -1
- package/src/skill-manifest.test.js +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.6.0 - 2026-07-17
|
|
6
|
+
|
|
7
|
+
- gmc-analysis skill v0.6.0: new "Product documentation (gmc-docs)"
|
|
8
|
+
section in SKILL.md pointing agents at docs.gamemarketcopilot.com for
|
|
9
|
+
product features, setup, plans/credits, API/CLI/MCP usage, and
|
|
10
|
+
methodology definitions, via the read-only Docs MCP or plain-HTTP
|
|
11
|
+
llms.txt / per-page .md fetch (no second MCP connection required);
|
|
12
|
+
answers drawing on docs must cite the canonical URL. The skill remains
|
|
13
|
+
the source of truth for statistical validity, claim safety, and chart
|
|
14
|
+
guidance. references/quota.md now points current plan limits at the
|
|
15
|
+
live pricing page.
|
|
16
|
+
|
|
17
|
+
## 0.5.0 - 2026-07-13
|
|
18
|
+
|
|
19
|
+
- gmc-analysis skill v0.5.0 (NAK-190): new "Saved Game Lists" guidance in
|
|
20
|
+
SKILL.md and recipe R5 (reusable cohorts via `gmc lists ...` / the MCP
|
|
21
|
+
Game List tools / `game_list_id` and `--list <id>` inputs), plus a skill
|
|
22
|
+
changelog entry retroactively documenting the chart-card guidance that
|
|
23
|
+
shipped unversioned in CLI 0.4.0-era updates (new chart types, capsule
|
|
24
|
+
rows, export geometry, label placement, embedded brand typography).
|
|
25
|
+
- New skill content-hash manifest guard: `npm run skill:manifest`
|
|
26
|
+
regenerates `skill/gmc-analysis.manifest.json`;
|
|
27
|
+
`src/skill-manifest.test.js` (runs in `npm test` / repo `verify`) and
|
|
28
|
+
`release:check` both fail when skill content changes without a version
|
|
29
|
+
bump and manifest regeneration, so the bundled skill and the public
|
|
30
|
+
gmc-agent-skills distribution repo can no longer drift silently.
|
|
31
|
+
|
|
5
32
|
## 0.4.0 - 2026-07-09
|
|
6
33
|
|
|
7
34
|
- gmc-analysis skill v0.4.0: new `references/validity.md` with the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"test": "node --test src/*.test.js",
|
|
46
|
+
"skill:manifest": "node scripts/gen-skill-manifest.mjs",
|
|
46
47
|
"release:check": "node scripts/check-release-readiness.mjs && node scripts/smoke-pack.mjs",
|
|
47
48
|
"prepublishOnly": "npm run release:check",
|
|
48
49
|
"smoke:beta": "node scripts/smoke-beta.mjs",
|
|
@@ -122,6 +122,25 @@ for (const file of sourceFiles) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// The skill content-hash manifest (NAK-190) is also enforced by
|
|
126
|
+
// src/skill-manifest.test.js, but release:check/prepublishOnly run WITHOUT
|
|
127
|
+
// the test suite — recheck here so a stale manifest can never reach npm.
|
|
128
|
+
const { buildSkillManifest, MANIFEST_PATH } = await import("./gen-skill-manifest.mjs");
|
|
129
|
+
if (!existsSync(MANIFEST_PATH)) {
|
|
130
|
+
failures.push("skill/gmc-analysis.manifest.json is missing — run npm run skill:manifest.");
|
|
131
|
+
} else {
|
|
132
|
+
const committedManifest = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
133
|
+
const currentManifest = buildSkillManifest();
|
|
134
|
+
if (
|
|
135
|
+
committedManifest.contentHash !== currentManifest.contentHash ||
|
|
136
|
+
committedManifest.version !== currentManifest.version
|
|
137
|
+
) {
|
|
138
|
+
failures.push(
|
|
139
|
+
"skill/gmc-analysis.manifest.json is stale: bump metadata.version in SKILL.md, run npm run skill:manifest, and re-sync the gmc-agent-skills distribution repo."
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
// The public distribution repo (github.com/witchpot-studio/gmc-agent-skills)
|
|
126
145
|
// mirrors skill/gmc-analysis. When it is checked out as a workspace sibling,
|
|
127
146
|
// a drift between the two is a release blocker — MCP-only users install the
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Regenerates skill/gmc-analysis.manifest.json — a content-hash fingerprint
|
|
4
|
+
// of the packaged gmc-analysis skill (SKILL.md + references/ + CHANGELOG.md).
|
|
5
|
+
//
|
|
6
|
+
// Maintainer rule: any time skill/gmc-analysis/** content changes, bump
|
|
7
|
+
// metadata.version in SKILL.md, then run `npm run skill:manifest` (in
|
|
8
|
+
// packages/gmc-cli) to regenerate this manifest, and re-sync the
|
|
9
|
+
// gmc-agent-skills distribution repo (node scripts/sync-from-cli.mjs there).
|
|
10
|
+
// src/skill-manifest.test.js recomputes the same hash on every
|
|
11
|
+
// `npm run test:cli` / `npm run verify` and fails loudly if it no longer
|
|
12
|
+
// matches the committed manifest — this is the drift guard that would have
|
|
13
|
+
// caught the unversioned charts.md changes in PR #18 / PR #43 (NAK-190).
|
|
14
|
+
//
|
|
15
|
+
// The manifest is written OUTSIDE skill/gmc-analysis/ on purpose: that
|
|
16
|
+
// directory is mirrored byte-for-byte into the public gmc-agent-skills
|
|
17
|
+
// distribution repo, and the manifest must never be part of (or affect the
|
|
18
|
+
// hash of) the content it describes.
|
|
19
|
+
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const packageDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
27
|
+
export const SKILL_DIR = path.join(packageDir, "skill", "gmc-analysis");
|
|
28
|
+
export const MANIFEST_PATH = path.join(packageDir, "skill", "gmc-analysis.manifest.json");
|
|
29
|
+
|
|
30
|
+
/** List every file under `dir`, as sorted relative POSIX paths. */
|
|
31
|
+
export function listSkillFiles(dir = SKILL_DIR) {
|
|
32
|
+
const results = [];
|
|
33
|
+
walk(dir, dir, results);
|
|
34
|
+
return results.sort();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function walk(root, current, results) {
|
|
38
|
+
const entries = readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const abs = path.join(current, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
walk(root, abs, results);
|
|
43
|
+
} else if (entry.isFile()) {
|
|
44
|
+
results.push(path.relative(root, abs).split(path.sep).join("/"));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sha256Hex(buffer) {
|
|
50
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hash every file under `dir` and fold the per-file hashes into one
|
|
55
|
+
* top-level sha256 over `relpath\nfilehash\n` lines (sorted, deterministic).
|
|
56
|
+
*/
|
|
57
|
+
export function computeSkillContentHash(dir = SKILL_DIR) {
|
|
58
|
+
const files = listSkillFiles(dir);
|
|
59
|
+
const digest = createHash("sha256");
|
|
60
|
+
for (const relPath of files) {
|
|
61
|
+
const fileHash = sha256Hex(readFileSync(path.join(dir, relPath)));
|
|
62
|
+
digest.update(`${relPath}\n${fileHash}\n`);
|
|
63
|
+
}
|
|
64
|
+
return { contentHash: digest.digest("hex"), files: files.length };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Parse metadata.version out of SKILL.md source text; undefined when absent. */
|
|
68
|
+
export function parseSkillVersion(source) {
|
|
69
|
+
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/);
|
|
70
|
+
return frontmatter?.[1].match(/^\s*version:\s*"?([0-9][0-9A-Za-z.\-]*)"?\s*$/m)?.[1];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Read metadata.version from SKILL.md's YAML frontmatter. */
|
|
74
|
+
export function readSkillManifestVersion(dir = SKILL_DIR) {
|
|
75
|
+
const version = parseSkillVersion(readFileSync(path.join(dir, "SKILL.md"), "utf8"));
|
|
76
|
+
if (!version) {
|
|
77
|
+
throw new Error(`${path.join(dir, "SKILL.md")} must declare metadata.version in its frontmatter.`);
|
|
78
|
+
}
|
|
79
|
+
return version;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Build the manifest object (version + contentHash + files) for `dir`. */
|
|
83
|
+
export function buildSkillManifest(dir = SKILL_DIR) {
|
|
84
|
+
const { contentHash, files } = computeSkillContentHash(dir);
|
|
85
|
+
return { version: readSkillManifestVersion(dir), contentHash, files };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Baseline manifest for the version-bump rule. The authoritative baseline is
|
|
90
|
+
* the manifest COMMITTED ON origin/main (read via `git show`, so deleting or
|
|
91
|
+
* hand-editing the local file cannot dodge the rule); the local file is only
|
|
92
|
+
* a fallback for environments where the git baseline is unreadable.
|
|
93
|
+
*/
|
|
94
|
+
function baselineSkillManifest() {
|
|
95
|
+
const result = spawnSync("git", ["show", "origin/main:packages/gmc-cli/skill/gmc-analysis.manifest.json"], {
|
|
96
|
+
cwd: packageDir,
|
|
97
|
+
encoding: "utf8"
|
|
98
|
+
});
|
|
99
|
+
if (result.status === 0 && typeof result.stdout === "string") {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(result.stdout);
|
|
102
|
+
} catch {
|
|
103
|
+
// fall through to the local file
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (existsSync(MANIFEST_PATH)) {
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Pure regen-permission rule (unit-tested in src/skill-manifest.test.js).
|
|
118
|
+
* Content that differs from the baseline while keeping the baseline's
|
|
119
|
+
* version is refused: once a version's content is on origin/main, changing
|
|
120
|
+
* content under that version always requires a bump. A differing version
|
|
121
|
+
* (the in-flight bump case, iterated freely within one branch) and a
|
|
122
|
+
* missing baseline (true first generation) are allowed. There is
|
|
123
|
+
* deliberately NO force escape hatch: an override flag would let
|
|
124
|
+
* unversioned content changes reach npm and the public skills repo.
|
|
125
|
+
*/
|
|
126
|
+
export function assertRegenAllowed({ baseline, next }) {
|
|
127
|
+
if (!baseline) return;
|
|
128
|
+
if (baseline.contentHash === next.contentHash) return;
|
|
129
|
+
if (baseline.version !== next.version) return;
|
|
130
|
+
throw new Error(
|
|
131
|
+
`skill content changed but metadata.version is still ${next.version}, whose content is already fixed by the baseline manifest (origin/main) — bump the version in SKILL.md, then rerun npm run skill:manifest.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function main() {
|
|
136
|
+
const manifest = buildSkillManifest();
|
|
137
|
+
try {
|
|
138
|
+
assertRegenAllowed({ baseline: baselineSkillManifest(), next: manifest });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(error.message);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
writeFileSync(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
144
|
+
process.stdout.write(
|
|
145
|
+
`Wrote ${path.relative(packageDir, MANIFEST_PATH)}: version ${manifest.version}, ${manifest.files} files, contentHash ${manifest.contentHash}\n`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
150
|
+
main();
|
|
151
|
+
}
|
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# gmc-analysis skill changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 - 2026-07-17
|
|
4
|
+
|
|
5
|
+
- New "Product documentation (gmc-docs)" section in SKILL.md:
|
|
6
|
+
points at the public docs site (`https://docs.gamemarketcopilot.com`) for
|
|
7
|
+
product features, setup, plans/credits, API/CLI/MCP usage,
|
|
8
|
+
troubleshooting, and methodology definitions; documents the read-only
|
|
9
|
+
Docs MCP (`search_docs`/`get_page`/`list_pages`/`get_navigation`, no
|
|
10
|
+
auth) for clients that support a second MCP connection, and `llms.txt` /
|
|
11
|
+
per-page `.md` fetch over plain HTTP as the no-second-MCP-required
|
|
12
|
+
fallback; requires citing the canonical docs URL when an answer draws on
|
|
13
|
+
it. Keeps the distinction crisp: `gmc-docs` is product documentation, not
|
|
14
|
+
market evidence; this skill remains the source of truth for statistical
|
|
15
|
+
validity, claim safety, and chart guidance, and the docs never override
|
|
16
|
+
it.
|
|
17
|
+
- `references/quota.md`: one-line pointer to the published plan/credit page
|
|
18
|
+
(`https://docs.gamemarketcopilot.com/plans.md`) alongside the existing
|
|
19
|
+
"treat as indicative" caveat.
|
|
20
|
+
|
|
21
|
+
## 0.5.0 - 2026-07-13
|
|
22
|
+
|
|
23
|
+
- New "Saved Game Lists" section in SKILL.md, plus recipe R5 in
|
|
24
|
+
`references/recipes.md` (NAK-155/NAK-178/NAK-189): documents Game Lists as
|
|
25
|
+
reusable, workspace-scoped cohort inputs — `gmc lists ...` (CLI) or the 7
|
|
26
|
+
MCP tools (`list_game_lists`, `get_game_list`, `create_game_list`,
|
|
27
|
+
`update_game_list`, `add_game_list_items`, `remove_game_list_item`,
|
|
28
|
+
`delete_game_list`), all 0-credit; passing a filter-kind list's id as
|
|
29
|
+
`game_list_id`/`--list <id>` to `list_games`, `market_aggregate`,
|
|
30
|
+
`cohort_review_categories`, or `cohort_evidence` instead of re-sending an
|
|
31
|
+
inline filter; reading `game_list.definition_hash` to detect a changed
|
|
32
|
+
stored definition between calls; the composite-list tool restriction
|
|
33
|
+
(`COMPOSITE_LIST_UNSUPPORTED_FOR_TOOL` on `market_aggregate`/
|
|
34
|
+
`cohort_review_categories`), the manual-list restriction
|
|
35
|
+
(`GAME_LIST_KIND_UNSUPPORTED`), and `delete_game_list`'s MCP confirm-by-
|
|
36
|
+
exact-name two-call contract.
|
|
37
|
+
- `references/charts.md` retroactive documentation (NAK-190): this
|
|
38
|
+
changelog entry catches up two content changes that shipped without a
|
|
39
|
+
skill version bump because nothing enforced one at the time (see the new
|
|
40
|
+
content-hash manifest guard below, added in this same release, to prevent
|
|
41
|
+
a repeat):
|
|
42
|
+
- PR #18 (NAK-139..143): new chart types beyond horizontal bar (lollipop,
|
|
43
|
+
grouped column, stacked column, dumbbell, scatter, quadrant with
|
|
44
|
+
computed threshold lines, donut, hero-number card); Steam capsule rows
|
|
45
|
+
for per-game bar charts; wide (16:9, 1200x675) and square (1:1,
|
|
46
|
+
1080x1080) export geometry replacing the earlier vertical (4:5) option.
|
|
47
|
+
- PR #43 (NAK-166/NAK-142): direct-label placement rules (values at bar/
|
|
48
|
+
line ends instead of legends, corner labels on quadrant charts) and the
|
|
49
|
+
embedded brand typography stack (Inter / JetBrains Mono / Quicksand)
|
|
50
|
+
used across card title, footer, and source-label text.
|
|
51
|
+
- Added `packages/gmc-cli/scripts/gen-skill-manifest.mjs` and
|
|
52
|
+
`packages/gmc-cli/src/skill-manifest.test.js` (NAK-190): a content-hash
|
|
53
|
+
manifest (`skill/gmc-analysis.manifest.json`, outside the synced skill
|
|
54
|
+
directory) that fails `npm run test:cli` whenever `skill/gmc-analysis/**`
|
|
55
|
+
content changes without a matching `metadata.version` bump and manifest
|
|
56
|
+
regeneration (`npm run skill:manifest`) — the gap that let the two chart
|
|
57
|
+
entries above ship unversioned.
|
|
58
|
+
|
|
3
59
|
## 0.4.0 - 2026-07-09
|
|
4
60
|
|
|
5
61
|
- Added `references/validity.md` (NAK-82): statistical-validity checklist
|
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
or campaigns that GMC can answer — including turning results into
|
|
9
9
|
publish-ready branded chart cards.
|
|
10
10
|
metadata:
|
|
11
|
-
version: 0.
|
|
11
|
+
version: 0.6.0
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# GMC Analysis
|
|
@@ -96,6 +96,50 @@ Search commands are optimized for row retrieval. Do not expect
|
|
|
96
96
|
unless you explicitly pass `--include-total`; for count-only questions use
|
|
97
97
|
`gmc games count` instead.
|
|
98
98
|
|
|
99
|
+
## Saved Game Lists (reusable cohort definitions)
|
|
100
|
+
|
|
101
|
+
A Game List is a workspace-scoped, reusable cohort input — persist a filter
|
|
102
|
+
(or an explicit appid set) once instead of re-sending a large inline filter
|
|
103
|
+
on every call.
|
|
104
|
+
|
|
105
|
+
- Manage lists via `gmc lists ...` (`list`, `create`, `show`, `games`, `add`,
|
|
106
|
+
`remove`, `delete`) or the MCP tools `list_game_lists` / `get_game_list` /
|
|
107
|
+
`create_game_list` / `update_game_list` / `add_game_list_items` /
|
|
108
|
+
`remove_game_list_item` / `delete_game_list`. All are 0-credit and scoped
|
|
109
|
+
to the caller's own workspace.
|
|
110
|
+
- Two kinds: `manual` (explicit appid membership) and `filter` (a stored
|
|
111
|
+
`GameFilter`, re-evaluated live against current data on every read). The
|
|
112
|
+
MCP `game_list_id` input accepts `filter`-kind lists only; the CLI
|
|
113
|
+
`--list <id>` flag resolves membership server-side and accepts BOTH kinds.
|
|
114
|
+
- Pass a filter-kind list's `id` as `game_list_id` to `list_games`,
|
|
115
|
+
`market_aggregate`, `cohort_review_categories`, or `cohort_evidence`
|
|
116
|
+
instead of re-sending the inline filter — mutually exclusive with
|
|
117
|
+
`filter` (`INVALID_INPUT` if both or neither are given). CLI equivalent:
|
|
118
|
+
`--list <id>` on `gmc reviews patterns`, `gmc marketing patterns`, and
|
|
119
|
+
`gmc cohort ...` (cohort-evidence-shaped commands only — `gmc games
|
|
120
|
+
count`/`gmc games aggregate` have no `--list` flag; use `market_aggregate`
|
|
121
|
+
with `game_list_id` over MCP for that).
|
|
122
|
+
- The response's `game_list.definition_hash` (a SHA-256 over `{kind,
|
|
123
|
+
filter, sort}`) tells you whether the stored definition changed between
|
|
124
|
+
calls — diff hashes across calls instead of re-diffing the filter
|
|
125
|
+
yourself.
|
|
126
|
+
|
|
127
|
+
Sharp edges:
|
|
128
|
+
|
|
129
|
+
- **Composite (union) lists** work only with `list_games`/`cohort_evidence`;
|
|
130
|
+
`market_aggregate`/`cohort_review_categories` reject them with
|
|
131
|
+
`COMPOSITE_LIST_UNSUPPORTED_FOR_TOOL` (those two are single aggregate
|
|
132
|
+
calls that cannot be bound to an explicit appid population).
|
|
133
|
+
- **Manual lists cannot be used as `game_list_id`** (MCP only) — a
|
|
134
|
+
`kind:"manual"` list fails `GAME_LIST_KIND_UNSUPPORTED` there. Use the
|
|
135
|
+
CLI `--list <id>` flag (which accepts manual lists), or read membership
|
|
136
|
+
directly (`gmc lists games <id>` / `get_game_list`).
|
|
137
|
+
- `delete_game_list` is destructive and, over MCP, uses a confirm-by-
|
|
138
|
+
exact-name two-call contract: a call without `confirm` fails
|
|
139
|
+
`CONFIRMATION_REQUIRED`, naming the list; only pass `confirm` set to that
|
|
140
|
+
exact name after the user has explicitly asked to delete it — never
|
|
141
|
+
guess or pre-fill it speculatively.
|
|
142
|
+
|
|
99
143
|
## Hard rules
|
|
100
144
|
|
|
101
145
|
- **Missing data is not absent sentiment.** Report `cohort.usableGames`,
|
|
@@ -124,6 +168,35 @@ unless you explicitly pass `--include-total`; for count-only questions use
|
|
|
124
168
|
Details in the quota reference.
|
|
125
169
|
- Preserve `meta.requestId` values for anything you may need to report.
|
|
126
170
|
|
|
171
|
+
## Product documentation (gmc-docs)
|
|
172
|
+
|
|
173
|
+
Public product documentation lives at `https://docs.gamemarketcopilot.com`
|
|
174
|
+
(read-only, no auth). It covers product features, setup, plans/credits,
|
|
175
|
+
API/CLI/MCP usage, troubleshooting, and methodology definitions (what a
|
|
176
|
+
field like `coverage` or `primary_market_tag` means); it carries no market
|
|
177
|
+
data.
|
|
178
|
+
|
|
179
|
+
Consult it BEFORE answering when you need current product behavior, setup
|
|
180
|
+
steps, plan/credit limits, or a definition you are not confident is still
|
|
181
|
+
accurate:
|
|
182
|
+
|
|
183
|
+
- Docs MCP (when the client supports a second MCP connection): connect to
|
|
184
|
+
`https://docs.gamemarketcopilot.com/mcp` (no auth) and call `search_docs`,
|
|
185
|
+
`get_page`, `list_pages`, or `get_navigation`.
|
|
186
|
+
- Otherwise, plain HTTP (no second MCP connection is required): fetch
|
|
187
|
+
`https://docs.gamemarketcopilot.com/llms.txt` for the page index, or
|
|
188
|
+
append `.md` to any docs page URL to fetch it as Markdown (e.g.
|
|
189
|
+
`https://docs.gamemarketcopilot.com/plans.md`).
|
|
190
|
+
|
|
191
|
+
Cite the canonical docs URL (the page without `.md`) whenever an answer
|
|
192
|
+
draws on it.
|
|
193
|
+
|
|
194
|
+
Keep the distinction crisp: `gmc-docs` is product documentation; the GMC
|
|
195
|
+
MCP/CLI is market data. Product docs are never market evidence, and they
|
|
196
|
+
never override this skill; it remains the source of truth for statistical
|
|
197
|
+
validity, claim safety, and chart guidance (hard rules and references
|
|
198
|
+
above).
|
|
199
|
+
|
|
127
200
|
## Chart cards
|
|
128
201
|
|
|
129
202
|
When the user wants a shareable or publish-ready visual of gmc results,
|
|
@@ -34,25 +34,68 @@ violates the non-negotiables below must not carry GMC attribution.
|
|
|
34
34
|
entirely. If one must appear, add a second, visually separate footnote
|
|
35
35
|
line: `External estimate: <source> — not GMC data`.
|
|
36
36
|
7. **Opaque background** (`#fafaf7` page, `#ffffff` card). Never export
|
|
37
|
-
transparent PNGs.
|
|
38
|
-
|
|
37
|
+
transparent PNGs. Export geometry: 16:9 1200x675 (X default) or 1:1
|
|
38
|
+
1080x1080 (X-safe; donut/kpi/quadrant read best). Vertical (4:5) cards
|
|
39
|
+
are not produced. Default to 16:9 when unsure.
|
|
39
40
|
|
|
40
41
|
## Picking the chart type from gmc output shapes
|
|
41
42
|
|
|
42
43
|
| Output | Shape | Chart |
|
|
43
44
|
|---|---|---|
|
|
44
45
|
| `gmc games aggregate --group-by <dim>` / MCP `market_aggregate` | 6+ groups, rank matters | Horizontal bar, sorted desc |
|
|
46
|
+
| same | 6+ groups, rank matters, compact point emphasis | Lollipop (bar_h variant) |
|
|
45
47
|
| same | 2-5 groups | Vertical column |
|
|
48
|
+
| same | 2-4 series × categories, e.g. release count by year × price band | Grouped column |
|
|
49
|
+
| same | 2-4 part-of-whole series across categories | Stacked column |
|
|
50
|
+
| same | before/after pairs per category, exactly 2 series | Dumbbell |
|
|
46
51
|
| same, time buckets (release windows, months) | series over time | Line (single/few series) or column |
|
|
47
52
|
| MCP `market_aggregate` with `segment_filter` | segment vs base | Two big numbers + ratio, or grouped bar |
|
|
48
53
|
| `gmc reviews patterns` / MCP `cohort_evidence` / clustered `negativeThemes` | theme -> game counts | Horizontal bar; caption carries `x of N usable games` |
|
|
49
54
|
| MCP `coverage_check` / `gmc games count --group-by coverage` | composition, 2-4 segments | Donut with absolute counts printed, or one 100% stacked bar |
|
|
55
|
+
| any tool output with x/y metric pairs per point | two metrics correlation | Scatter |
|
|
56
|
+
| any tool output with x/y metric pairs for positioning | scatter + median threshold lines + 4 descriptive corner labels; thresholds are computed from plotted values, never chosen | Quadrant |
|
|
50
57
|
| single KPI (one count, one median, one share) | one number | Hero number card (no chart canvas) |
|
|
51
58
|
|
|
52
59
|
Rules of thumb: when unsure, use a horizontal bar. One message per card.
|
|
53
60
|
Never: 3D, dual axes, cropped bar baselines, pie with 5+ slices, line
|
|
54
61
|
charts for non-time data.
|
|
55
62
|
|
|
63
|
+
## Steam capsules for game rows
|
|
64
|
+
|
|
65
|
+
When bar rows are individual game titles, show the Steam capsule
|
|
66
|
+
(`https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/{appid}/capsule_231x87.jpg`,
|
|
67
|
+
231x87) left of the row label. Preload images before render; skip rows whose
|
|
68
|
+
image failed so the text-only fallback keeps the layout. Scatter/quadrant
|
|
69
|
+
points get the same treatment as a thumbnail overlay per the visual guide's
|
|
70
|
+
thumbnail-overlay pattern: a white, 1px-framed capsule anchored near the
|
|
71
|
+
point with a dashed leader line back to it, instead of a row-left capsule.
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const rows = [{ label: '{{GAME TITLE}}', appid: 0 }];
|
|
75
|
+
const capsuleImages = await Promise.all(rows.map(({ appid }) => new Promise((resolve) => {
|
|
76
|
+
if (!appid) return resolve(null);
|
|
77
|
+
const img = new Image();
|
|
78
|
+
img.onload = () => resolve(img);
|
|
79
|
+
img.onerror = () => resolve(null);
|
|
80
|
+
img.src = `https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/${appid}/capsule_231x87.jpg`;
|
|
81
|
+
})));
|
|
82
|
+
|
|
83
|
+
const steamCapsules = {
|
|
84
|
+
id: 'steamCapsules',
|
|
85
|
+
afterDraw(chart) {
|
|
86
|
+
const yScale = chart.scales.y;
|
|
87
|
+
if (!yScale) return;
|
|
88
|
+
const { ctx } = chart;
|
|
89
|
+
yScale.ticks.forEach((_, i) => {
|
|
90
|
+
const img = capsuleImages[i];
|
|
91
|
+
if (!img) return;
|
|
92
|
+
const y = yScale.getPixelForTick(i) - 14;
|
|
93
|
+
ctx.drawImage(img, yScale.left - 66, y, 54, 20);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
56
99
|
## Color
|
|
57
100
|
|
|
58
101
|
Neutrals (fixed): page `#fafaf7`, card `#ffffff`, ink `#1a1d20`, muted
|
|
@@ -79,7 +122,7 @@ network access for fonts, Chart.js CDN, and the brand lockup.
|
|
|
79
122
|
<head>
|
|
80
123
|
<meta charset="utf-8">
|
|
81
124
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
82
|
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=
|
|
125
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&family=Quicksand:wght@700&display=swap" rel="stylesheet">
|
|
83
126
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
84
127
|
<style>
|
|
85
128
|
:root {
|
|
@@ -88,7 +131,7 @@ network access for fonts, Chart.js CDN, and the brand lockup.
|
|
|
88
131
|
--rule: #e8e5de; --accent: #ec7336;
|
|
89
132
|
--sans: 'Inter', system-ui, sans-serif;
|
|
90
133
|
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
|
91
|
-
--
|
|
134
|
+
--title: 'Quicksand', 'Inter', system-ui, sans-serif;
|
|
92
135
|
}
|
|
93
136
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
94
137
|
body { background: var(--bg); font-family: var(--sans); color: var(--ink);
|
|
@@ -101,8 +144,8 @@ network access for fonts, Chart.js CDN, and the brand lockup.
|
|
|
101
144
|
color: var(--accent); max-width: 76%; }
|
|
102
145
|
.card-cat { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
|
|
103
146
|
text-transform: uppercase; color: var(--ink-faint); white-space: nowrap; }
|
|
104
|
-
.card-title { padding: 26px 20px 8px; font-family: var(--
|
|
105
|
-
font-size: 34px; line-height: 1.12; font-weight:
|
|
147
|
+
.card-title { padding: 26px 20px 8px; font-family: var(--title);
|
|
148
|
+
font-size: 34px; line-height: 1.12; font-weight: 700; }
|
|
106
149
|
.chart-wrap { position: relative; margin: 18px 20px 8px;
|
|
107
150
|
min-height: 470px; height: 470px; }
|
|
108
151
|
.card-foot { display: flex; justify-content: space-between; align-items: center;
|
|
@@ -208,7 +251,8 @@ Variants:
|
|
|
208
251
|
|
|
209
252
|
Japanese edition: add `&family=Noto+Sans+JP:wght@400;500;600` to the fonts
|
|
210
253
|
URL, append `'Noto Sans JP'` to `--sans`, set `<html lang="ja">`, and use
|
|
211
|
-
`GMCデータベース` as the source label.
|
|
254
|
+
`GMCデータベース` as the source label. Add `&family=Noto+Serif+JP:wght@400&`
|
|
255
|
+
to the fonts URL so JA titles render serif. Steam tags in JA charts:
|
|
212
256
|
`English Tag(日本語タグ名)` on first appearance.
|
|
213
257
|
|
|
214
258
|
## Capture
|
|
@@ -6,8 +6,10 @@ Starter 2,500 credits/month, Pro 7,000; the counter resets at 00:00 UTC on
|
|
|
6
6
|
the first of each month. Anonymous public preview is far smaller. Credits
|
|
7
7
|
are charged when a request is accepted at the gate, so requests that later
|
|
8
8
|
fail validation or 404 still consume — validate appids before batch
|
|
9
|
-
fan-outs. Treat these numbers as indicative
|
|
10
|
-
|
|
9
|
+
fan-outs. Treat these numbers as indicative; the response metadata is the source
|
|
10
|
+
of truth. Current published plan limits live on the pricing page at
|
|
11
|
+
https://gamemarketcopilot.com/plans, and
|
|
12
|
+
https://docs.gamemarketcopilot.com/plans explains how credits work.
|
|
11
13
|
|
|
12
14
|
## Rules
|
|
13
15
|
|
|
@@ -95,6 +95,33 @@ gives the well-rated share. Divide by the unconditional bucket counts.
|
|
|
95
95
|
Never read unconditional rating aggregates in low-traction buckets
|
|
96
96
|
(pitfall P5).
|
|
97
97
|
|
|
98
|
+
## R5. Repeatable cohort via a saved Game List
|
|
99
|
+
|
|
100
|
+
When a cohort filter will be reused across a session or a recurring
|
|
101
|
+
digest, persist it once instead of re-sending the inline filter on every
|
|
102
|
+
call.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 1. Create the filter-kind list once
|
|
106
|
+
gmc lists create --name "<label>" --kind filter \
|
|
107
|
+
--filter '{"tags":["<tag>"],"reviewsMin":<n>}' --json # -> data.id
|
|
108
|
+
|
|
109
|
+
# 2. Reference it on every follow-up call instead of the inline filter
|
|
110
|
+
gmc reviews patterns --source steam --list <id> --json
|
|
111
|
+
gmc cohort brief --source steam --list <id> --json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Over MCP, `create_game_list` once, then pass its `id` as `game_list_id` to
|
|
115
|
+
`list_games` / `market_aggregate` / `cohort_review_categories` /
|
|
116
|
+
`cohort_evidence` on every follow-up call. Check `game_list.definition_hash`
|
|
117
|
+
on each response — a changed hash means the stored DEFINITION was edited;
|
|
118
|
+
re-state the cohort definition before comparing numbers. A stable hash does
|
|
119
|
+
not freeze the population: filter lists re-evaluate live, so denominators
|
|
120
|
+
can still move between calls. Composite (union) lists only work with `list_games`/
|
|
121
|
+
`cohort_evidence` (this recipe's map-reduce sibling, R2); manual lists are
|
|
122
|
+
not a `game_list_id` input over MCP (the CLI `--list <id>` flag does accept
|
|
123
|
+
them) — read their membership with `gmc lists games <id>` / `get_game_list`.
|
|
124
|
+
|
|
98
125
|
## Output contract for every analysis
|
|
99
126
|
|
|
100
127
|
Always include in your final answer: the cohort definition and size, how many
|
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.6.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");
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
MANIFEST_PATH,
|
|
7
|
+
assertRegenAllowed,
|
|
8
|
+
computeSkillContentHash,
|
|
9
|
+
readSkillManifestVersion
|
|
10
|
+
} from "../scripts/gen-skill-manifest.mjs";
|
|
11
|
+
|
|
12
|
+
const FAILURE_MESSAGE =
|
|
13
|
+
"skill content changed: bump metadata.version in SKILL.md, run npm run skill:manifest (in packages/gmc-cli), and re-sync the gmc-agent-skills distribution repo (node scripts/sync-from-cli.mjs there)";
|
|
14
|
+
|
|
15
|
+
function readCommittedManifest() {
|
|
16
|
+
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("committed skill manifest content hash matches the packaged skill content", () => {
|
|
20
|
+
const manifest = readCommittedManifest();
|
|
21
|
+
const { contentHash } = computeSkillContentHash();
|
|
22
|
+
|
|
23
|
+
assert.equal(contentHash, manifest.contentHash, FAILURE_MESSAGE);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("committed skill manifest version matches SKILL.md metadata.version", () => {
|
|
27
|
+
const manifest = readCommittedManifest();
|
|
28
|
+
const skillVersion = readSkillManifestVersion();
|
|
29
|
+
|
|
30
|
+
assert.equal(manifest.version, skillVersion, FAILURE_MESSAGE);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("regen rule: content change under the baseline's version is refused", () => {
|
|
34
|
+
assert.throws(
|
|
35
|
+
() =>
|
|
36
|
+
assertRegenAllowed({
|
|
37
|
+
baseline: { version: "0.5.0", contentHash: "aaa" },
|
|
38
|
+
next: { version: "0.5.0", contentHash: "bbb" }
|
|
39
|
+
}),
|
|
40
|
+
/bump the version in SKILL\.md/
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("regen rule: allowed on a bump, on unchanged content, and on first generation", () => {
|
|
45
|
+
// A bump (in-flight iteration keeps hitting this branch: baseline stays
|
|
46
|
+
// at origin/main's version until the bump merges).
|
|
47
|
+
assert.doesNotThrow(() =>
|
|
48
|
+
assertRegenAllowed({
|
|
49
|
+
baseline: { version: "0.4.0", contentHash: "aaa" },
|
|
50
|
+
next: { version: "0.5.0", contentHash: "bbb" }
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
// Idempotent rerun.
|
|
54
|
+
assert.doesNotThrow(() =>
|
|
55
|
+
assertRegenAllowed({
|
|
56
|
+
baseline: { version: "0.5.0", contentHash: "aaa" },
|
|
57
|
+
next: { version: "0.5.0", contentHash: "aaa" }
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
// True first generation (no committed baseline anywhere).
|
|
61
|
+
assert.doesNotThrow(() => assertRegenAllowed({ baseline: undefined, next: { version: "0.5.0", contentHash: "aaa" } }));
|
|
62
|
+
});
|