@witchpot/gmc 0.8.0 → 0.9.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 +30 -0
- package/package.json +1 -2
- package/skill/gmc-analysis/CHANGELOG.md +25 -0
- package/skill/gmc-analysis/SKILL.md +42 -3
- package/skill/gmc-analysis.manifest.json +2 -2
- package/src/args.js +1 -1
- package/src/constants.js +5 -2
- package/src/help.js +30 -0
- package/src/main.js +7 -1
- package/src/main.test.js +130 -0
- package/src/projection.js +21 -0
- package/src/requests.js +75 -0
- package/src/requests.test.js +198 -3
- package/scripts/check-release-readiness.mjs +0 -177
- package/scripts/gen-skill-manifest.mjs +0 -151
- package/scripts/smoke-beta.mjs +0 -246
- package/scripts/smoke-pack.mjs +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Game Market Copilot CLI will be recorded here.
|
|
4
4
|
|
|
5
|
+
## 0.9.0 - 2026-09-16
|
|
6
|
+
|
|
7
|
+
- New `--release-status <released|upcoming|tba>` filter on the games
|
|
8
|
+
search/count/aggregate commands (NAK-803), forwarded verbatim and
|
|
9
|
+
case-sensitively as the `releaseStatus` query param. The parameter was
|
|
10
|
+
already in the REST contract; the CLI simply had no mapping for it, which
|
|
11
|
+
forced CLI recipes for "released games only" to lean on the release window
|
|
12
|
+
alone. The filter is an exact match layered on top of the guard that
|
|
13
|
+
already drops any title still marked `upcoming` past its provisional
|
|
14
|
+
release date.
|
|
15
|
+
|
|
16
|
+
- New command `gmc games ai-disclosure <appid> --json` (NAK-804): one title's
|
|
17
|
+
developer-authored AI-use disclosure text, the store page it was read from,
|
|
18
|
+
and its observed history of change. It takes the appid alone — no `--source`
|
|
19
|
+
selector and no query parameters, matching the route — and rejects any other
|
|
20
|
+
flag as a usage error rather than ignoring it. `absent` means no disclosure
|
|
21
|
+
block was observed on the store page, never that the title uses no
|
|
22
|
+
generative AI; `unconfirmed` includes titles never checked, and is also what
|
|
23
|
+
an appid never observed returns, since the route has no existence check.
|
|
24
|
+
Listed as Provisional in the beta command set.
|
|
25
|
+
- Bundled `gmc-analysis` skill bumped to 0.16.0 with a "Disclosure text and
|
|
26
|
+
change history" subsection covering that command and how to report its text
|
|
27
|
+
and events honestly.
|
|
28
|
+
- An inapplicable `--preset` is now rejected as a usage error BEFORE the
|
|
29
|
+
metered request, for every command. Previously the request executed (and
|
|
30
|
+
charged a credit) and the preset was only rejected while shaping the output.
|
|
31
|
+
- Packaging: `scripts/` (release tooling) no longer ships in the npm tarball,
|
|
32
|
+
and the release-readiness gate now scans every file the tarball actually
|
|
33
|
+
contains instead of a hardcoded source list.
|
|
34
|
+
|
|
5
35
|
## 0.8.0 - 2026-09-14
|
|
6
36
|
|
|
7
37
|
For npm users this release lands together with 0.7.0's content, because
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@witchpot/gmc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Command line tools for Game Market Copilot market research.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"files": [
|
|
34
34
|
"bin",
|
|
35
35
|
"src",
|
|
36
|
-
"scripts",
|
|
37
36
|
"skill",
|
|
38
37
|
"README.md",
|
|
39
38
|
"CHANGELOG.md"
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# gmc-analysis skill changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0 - 2026-09-16
|
|
4
|
+
|
|
5
|
+
- New "Disclosure text and change history" subsection under "AI disclosure
|
|
6
|
+
state" (NAK-804): the surface behind the tri-state flag — the developer's
|
|
7
|
+
disclosure text, the store page it was read from, the first and latest
|
|
8
|
+
observation dates, and the observed change history. Reached with
|
|
9
|
+
`gmc games ai-disclosure <appid>` (appid only, no `--source` and no query
|
|
10
|
+
parameters) or `game_profile` with `ai_disclosure` in `sections` (opt-in, so
|
|
11
|
+
a call that does not name it performs no extra read, and raw-read weight
|
|
12
|
+
class, so asking for it beside `detail` leaves the per-appid cost at 1).
|
|
13
|
+
- Adds the interpretation guardrails as honesty rules: `disclosureText` is the
|
|
14
|
+
developer's own wording, normalized only for whitespace and never classified
|
|
15
|
+
or scored by Game Market Copilot, so it is quoted and attributed to them
|
|
16
|
+
rather than paraphrased into a verdict; each event is an OBSERVED change
|
|
17
|
+
dated as the window between `previousObservedAt` and `observedAt`, never as
|
|
18
|
+
a moment; an empty `events` array means no change was observed, never that
|
|
19
|
+
nothing changed, because a title's first observation is a baseline that
|
|
20
|
+
never appears as an event; `events` can be non-empty while `state` is
|
|
21
|
+
`unconfirmed`; `unconfirmed` still covers titles never checked, since this
|
|
22
|
+
surface has no existence check; a `null` payload carries
|
|
23
|
+
`ai_disclosure_detail_unavailable` and means the read could not be reached,
|
|
24
|
+
never "not disclosed".
|
|
25
|
+
- MCP-only mapping updated: per-title AI-disclosure text and change history ->
|
|
26
|
+
`game_profile` with `ai_disclosure` in `sections`.
|
|
27
|
+
|
|
3
28
|
## 0.15.0 - 2026-09-14
|
|
4
29
|
|
|
5
30
|
- New "AI disclosure state" section in SKILL.md (NAK-773): the tri-state
|
|
@@ -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.16.0
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# GMC Analysis
|
|
@@ -57,7 +57,9 @@ applies identically. Mechanics map as follows:
|
|
|
57
57
|
-> `more_like_this`; review topics by review language (`games
|
|
58
58
|
language-topics`) -> `game_language_topics`; review-count history with
|
|
59
59
|
its lifecycle/announcement/discount context (`games review-history`) ->
|
|
60
|
-
`game_review_history
|
|
60
|
+
`game_review_history`; per-title AI-disclosure text and change history
|
|
61
|
+
(`games ai-disclosure`) -> `game_profile` with `ai_disclosure` in
|
|
62
|
+
`sections`.
|
|
61
63
|
- Per-family collection status ("has GMC actually collected X for this
|
|
62
64
|
game?") has no CLI command yet and is MCP-only: `game_collection_status`
|
|
63
65
|
(see the dedicated section below).
|
|
@@ -483,7 +485,8 @@ Every title carries a tri-state AI-disclosure read from its Steam store page:
|
|
|
483
485
|
search/count and `market_aggregate`; group with
|
|
484
486
|
`--group-by ai_disclosure` (it cross-tabs with `release_month`); read one
|
|
485
487
|
title's state from `detail.aiDisclosure` (`state`, `observedAt`,
|
|
486
|
-
`rawCategoryValue`).
|
|
488
|
+
`rawCategoryValue`). The text and history behind that flag are a separate
|
|
489
|
+
surface — see the subsection below.
|
|
487
490
|
|
|
488
491
|
Interpretation guardrails (honesty rules, not suggestions):
|
|
489
492
|
|
|
@@ -505,6 +508,42 @@ Interpretation guardrails (honesty rules, not suggestions):
|
|
|
505
508
|
(the underlying rollups have no disclosure predicate). Size disclosure
|
|
506
509
|
cohorts with `market_aggregate` instead.
|
|
507
510
|
|
|
511
|
+
### Disclosure text and change history
|
|
512
|
+
|
|
513
|
+
`gmc games ai-disclosure <appid> --json` (MCP: `game_profile` with
|
|
514
|
+
`ai_disclosure` in `sections`) returns what sits behind that flag: the
|
|
515
|
+
developer's disclosure text, the store page it was read from, the first and
|
|
516
|
+
latest observation dates, and the observed change history. The CLI command
|
|
517
|
+
takes the appid and nothing else — no `--source`, no query parameters. Over
|
|
518
|
+
MCP the section is opt-in, so a call that does not name it performs no extra
|
|
519
|
+
read, and it stays in the raw-read weight class: asking for it beside `detail`
|
|
520
|
+
leaves the per-appid cost at 1.
|
|
521
|
+
|
|
522
|
+
Interpretation guardrails (honesty rules, not suggestions):
|
|
523
|
+
|
|
524
|
+
- **The text is the developer's, never ours.** `disclosureText` is their own
|
|
525
|
+
wording as read from the store page, normalized only for whitespace. Game
|
|
526
|
+
Market Copilot never classifies it, scores it, or infers from it how much AI
|
|
527
|
+
was used. Quote it and attribute it to the developer; never paraphrase it
|
|
528
|
+
into a verdict of your own. It is `null` whenever `state` is not
|
|
529
|
+
`disclosed`.
|
|
530
|
+
- **Each event is an observed change, so date it as a window.** The store page
|
|
531
|
+
is read periodically, not continuously, so a change happened SOMEWHERE
|
|
532
|
+
between `previousObservedAt` and `observedAt`. Say "between those two
|
|
533
|
+
reads"; never say it happened on either date.
|
|
534
|
+
- **An empty `events` array is not "nothing ever changed".** A title's FIRST
|
|
535
|
+
observation is a baseline and never appears as an event, so an empty array
|
|
536
|
+
means only that no change has been observed between two reads. `events` can
|
|
537
|
+
also be non-empty while `state` is `unconfirmed` — history outlives a later
|
|
538
|
+
read that failed.
|
|
539
|
+
- **`unconfirmed` still includes never checked.** This surface has no
|
|
540
|
+
existence check, so an appid never observed comes back `unconfirmed` with no
|
|
541
|
+
events rather than an error. That is a statement about what has been
|
|
542
|
+
observed, never about the title.
|
|
543
|
+
- **A `null` payload is unavailability.** It carries
|
|
544
|
+
`ai_disclosure_detail_unavailable` and means the read could not be reached.
|
|
545
|
+
Never report it as "not disclosed", and never as not collected.
|
|
546
|
+
|
|
508
547
|
## Hard rules
|
|
509
548
|
|
|
510
549
|
- **Missing data is not absent sentiment.** Report `cohort.usableGames`,
|
package/src/args.js
CHANGED
|
@@ -109,7 +109,7 @@ function isCommandToken(command, token) {
|
|
|
109
109
|
return ["install", "print", "status"].includes(token);
|
|
110
110
|
}
|
|
111
111
|
if (command[0] === "games") {
|
|
112
|
-
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this"].includes(token);
|
|
112
|
+
return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this", "ai-disclosure"].includes(token);
|
|
113
113
|
}
|
|
114
114
|
if (command[0] === "reviews") {
|
|
115
115
|
return token === "patterns";
|
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.9.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");
|
|
@@ -60,7 +60,10 @@ export const PROVISIONAL_BETA_COMMANDS = [
|
|
|
60
60
|
// default-off, so claiming supported-beta status would promise a stability
|
|
61
61
|
// the feature flag has not yet been asked to keep.
|
|
62
62
|
"gmc steam trend-stories --locale en --json",
|
|
63
|
-
"gmc steam tag-trends --view featured|board|group|table|tag --json"
|
|
63
|
+
"gmc steam tag-trends --view featured|board|group|table|tag --json",
|
|
64
|
+
// NAK-804. New command; starts Provisional per the closed-beta roadmap's
|
|
65
|
+
// default for anything not yet through broader real-data smoke testing.
|
|
66
|
+
"gmc games ai-disclosure <appid> --json"
|
|
64
67
|
];
|
|
65
68
|
|
|
66
69
|
export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
|
package/src/help.js
CHANGED
|
@@ -51,6 +51,7 @@ Usage:
|
|
|
51
51
|
gmc games language-topics --source steam <externalId> --min-reviews <number> --include-insufficient --json
|
|
52
52
|
gmc games review-history --source steam <externalId> --json
|
|
53
53
|
gmc games more-like-this --source steam <externalId> --country <country> --json
|
|
54
|
+
gmc games ai-disclosure <appid> --json
|
|
54
55
|
gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
|
|
55
56
|
gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --sample-size <number> --preset summary --json
|
|
56
57
|
gmc marketing patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json
|
|
@@ -151,6 +152,31 @@ Review-count history:
|
|
|
151
152
|
Examples:
|
|
152
153
|
gmc games review-history --source steam 730 --json
|
|
153
154
|
|
|
155
|
+
AI-disclosure text and history:
|
|
156
|
+
gmc games ai-disclosure <appid> --json
|
|
157
|
+
One game's developer-authored AI-use
|
|
158
|
+
disclosure text, the store page it was
|
|
159
|
+
read from, and its observed history of
|
|
160
|
+
change. There are no parameters beyond
|
|
161
|
+
the appid.
|
|
162
|
+
|
|
163
|
+
data.state is disclosed, absent, or unconfirmed. absent means no
|
|
164
|
+
disclosure block was observed on the store page -- never that the title
|
|
165
|
+
uses no generative AI. unconfirmed means Game Market Copilot has not yet
|
|
166
|
+
successfully observed the store page either way, including a title never
|
|
167
|
+
attempted at all.
|
|
168
|
+
|
|
169
|
+
data.disclosureText is the developer's own wording, normalized only for
|
|
170
|
+
whitespace and never classified or summarized by Game Market Copilot. It
|
|
171
|
+
is null unless state is disclosed. data.sourceUrl is the store page the
|
|
172
|
+
observation was read from.
|
|
173
|
+
|
|
174
|
+
data.events lists only OBSERVED changes to the state, oldest first; a
|
|
175
|
+
title's first observation is a baseline and never appears as an event.
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
gmc games ai-disclosure 730 --json
|
|
179
|
+
|
|
154
180
|
Steam tag trends:
|
|
155
181
|
gmc steam tag-trends --view <view> --json
|
|
156
182
|
Tag supply trends over one frozen monthly
|
|
@@ -306,6 +332,10 @@ Filters:
|
|
|
306
332
|
result carries catalog_as_of_filter_unavailable
|
|
307
333
|
instead: there is no availability history behind
|
|
308
334
|
fixtures, so the filter was not applied at all.
|
|
335
|
+
--release-status <value> released, upcoming, or tba. An exact match on
|
|
336
|
+
top of the guard that already drops any title
|
|
337
|
+
still marked upcoming after its provisional
|
|
338
|
+
release date has passed, on every query.
|
|
309
339
|
--ai-disclosure <value> disclosed, absent, or unconfirmed. absent means
|
|
310
340
|
no AI-use disclosure was observed in collected
|
|
311
341
|
data -- not confirmed non-use. unconfirmed means
|
package/src/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { matches, parseArgs } from "./args.js";
|
|
|
10
10
|
import { handleLocalCommand, loadConfig, parseBaseUrl } from "./config.js";
|
|
11
11
|
import { printGamesSearchHelp, printHelp } from "./help.js";
|
|
12
12
|
import { writeJsonError } from "./output.js";
|
|
13
|
-
import { applyOutputProjection } from "./projection.js";
|
|
13
|
+
import { applyOutputProjection, assertPresetApplicable } from "./projection.js";
|
|
14
14
|
import { buildRequest } from "./requests.js";
|
|
15
15
|
import { trackCliCommandExecuted } from "./telemetry.js";
|
|
16
16
|
import { executeRequestPlan } from "./workflows.js";
|
|
@@ -71,6 +71,12 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
71
71
|
process.exit(EXIT_USAGE);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// NAK-804 cross-model review: validated BEFORE the metered request below,
|
|
75
|
+
// not only afterward inside applyOutputProjection — an inapplicable
|
|
76
|
+
// --preset must never pay for the HTTP round trip (or its credit charge)
|
|
77
|
+
// only to be rejected once the response is already back.
|
|
78
|
+
assertPresetApplicable({ fields: options.fields, preset: options.preset, request });
|
|
79
|
+
|
|
74
80
|
if (request.requiresApiKey && !apiKey) {
|
|
75
81
|
writeJsonError({
|
|
76
82
|
code: "CLI_MISSING_CONFIG",
|
package/src/main.test.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { main } from "./main.js";
|
|
5
|
+
import { assertPresetApplicable } from "./projection.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Async counterpart to the `captureUsageFailure` helper in requests.test.js:
|
|
9
|
+
* `main()` is async, so `process.exit` must be mocked to throw INTO the
|
|
10
|
+
* awaited call rather than a synchronous one.
|
|
11
|
+
*/
|
|
12
|
+
async function captureCliExit(run) {
|
|
13
|
+
const originalWrite = process.stdout.write;
|
|
14
|
+
const originalExit = process.exit;
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let exitCode;
|
|
17
|
+
process.stdout.write = (chunk) => {
|
|
18
|
+
chunks.push(String(chunk));
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
process.exit = (code) => {
|
|
22
|
+
exitCode = code;
|
|
23
|
+
throw new Error("__cli_exit__");
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
await run();
|
|
27
|
+
throw new Error("expected the CLI to exit");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.message !== "__cli_exit__") throw error;
|
|
30
|
+
} finally {
|
|
31
|
+
process.stdout.write = originalWrite;
|
|
32
|
+
process.exit = originalExit;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { output: JSON.parse(chunks.join("")), exitCode };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* NAK-804 cross-model review (Codex Sol): `gmc games ai-disclosure 730
|
|
40
|
+
* --preset summary --json` used to build the request, send it, and only
|
|
41
|
+
* THEN discover in applyOutputProjection that ai-disclosure supports no
|
|
42
|
+
* preset — charging a credit for a call whose answer was always going to be
|
|
43
|
+
* a usage error. The counting fetch stub below is the direct evidence that
|
|
44
|
+
* the metered request never fires: if the ordering regresses, this test
|
|
45
|
+
* fails on `fetchCalls === 0` even though the exit code/message stay right.
|
|
46
|
+
*/
|
|
47
|
+
test("(NAK-804) an unsupported --preset is rejected before the metered request executes", async () => {
|
|
48
|
+
const originalFetch = globalThis.fetch;
|
|
49
|
+
let fetchCalls = 0;
|
|
50
|
+
globalThis.fetch = async () => {
|
|
51
|
+
fetchCalls += 1;
|
|
52
|
+
throw new Error("fetch must not be called once --preset is rejected");
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const { output, exitCode } = await captureCliExit(() =>
|
|
57
|
+
main(["games", "ai-disclosure", "730", "--preset", "summary", "--json"])
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
assert.equal(fetchCalls, 0);
|
|
61
|
+
assert.equal(exitCode, 2);
|
|
62
|
+
assert.equal(output.error.code, "CLI_USAGE_ERROR");
|
|
63
|
+
assert.match(
|
|
64
|
+
output.error.message,
|
|
65
|
+
/--preset summary is supported for gmc reviews patterns, gmc games benchmark, and gmc cohort\/pattern workflows/
|
|
66
|
+
);
|
|
67
|
+
} finally {
|
|
68
|
+
globalThis.fetch = originalFetch;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The same early check is generic (it reuses resolvePresetFields exactly),
|
|
74
|
+
* not an ai-disclosure special case: an unknown preset NAME is rejected
|
|
75
|
+
* before the request too, for any command.
|
|
76
|
+
*/
|
|
77
|
+
test("(NAK-804) an unknown --preset value is rejected before the metered request executes", async () => {
|
|
78
|
+
const originalFetch = globalThis.fetch;
|
|
79
|
+
let fetchCalls = 0;
|
|
80
|
+
globalThis.fetch = async () => {
|
|
81
|
+
fetchCalls += 1;
|
|
82
|
+
throw new Error("fetch must not be called once --preset is rejected");
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const { output, exitCode } = await captureCliExit(() =>
|
|
87
|
+
main(["games", "ai-disclosure", "730", "--preset", "not-a-real-preset", "--json"])
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.equal(fetchCalls, 0);
|
|
91
|
+
assert.equal(exitCode, 2);
|
|
92
|
+
assert.equal(output.error.code, "CLI_USAGE_ERROR");
|
|
93
|
+
assert.match(output.error.message, /--preset must be one of: summary/);
|
|
94
|
+
} finally {
|
|
95
|
+
globalThis.fetch = originalFetch;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Negative control on the pure check itself (no CLI/network plumbing
|
|
101
|
+
* needed): a request `kind` that IS in the preset allowlist must not be
|
|
102
|
+
* rejected — the early check only intercepts INAPPLICABLE presets, not
|
|
103
|
+
* every --preset call.
|
|
104
|
+
*/
|
|
105
|
+
test("(NAK-804) assertPresetApplicable does not reject a preset the request kind supports", () => {
|
|
106
|
+
assert.doesNotThrow(() =>
|
|
107
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "reviewPatterns" } })
|
|
108
|
+
);
|
|
109
|
+
assert.doesNotThrow(() =>
|
|
110
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "benchmark" } })
|
|
111
|
+
);
|
|
112
|
+
assert.doesNotThrow(() =>
|
|
113
|
+
assertPresetApplicable({ preset: "summary", request: { kind: "cohortEvidence" } })
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Same precedence rule applyOutputProjection enforces: --fields silences an
|
|
119
|
+
* otherwise-inapplicable --preset entirely, so passing both together must
|
|
120
|
+
* not trip the early check either.
|
|
121
|
+
*/
|
|
122
|
+
test("(NAK-804) assertPresetApplicable defers to --fields precedence, like applyOutputProjection", () => {
|
|
123
|
+
assert.doesNotThrow(() =>
|
|
124
|
+
assertPresetApplicable({
|
|
125
|
+
fields: "state",
|
|
126
|
+
preset: "summary",
|
|
127
|
+
request: { kind: undefined }
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
});
|
package/src/projection.js
CHANGED
|
@@ -47,6 +47,27 @@ export function applyOutputProjection(payload, { fields, preset, request } = {})
|
|
|
47
47
|
return applyFieldsProjection(payload, presetFields, `preset ${preset}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* NAK-804 cross-model review (Codex Sol): every metered command built a
|
|
52
|
+
* usage-error-only exit on an inapplicable --preset, but the check ran
|
|
53
|
+
* INSIDE applyOutputProjection, which main() calls only AFTER the HTTP
|
|
54
|
+
* request already executed (and, for a metered endpoint, already charged a
|
|
55
|
+
* credit). This is the same validation, called right after buildRequest()
|
|
56
|
+
* and before that request is ever sent, so the usage error lands before any
|
|
57
|
+
* network call or charge. General fix, not ai-disclosure-specific: it reuses
|
|
58
|
+
* resolvePresetFields exactly, so any command missing from its
|
|
59
|
+
* kind-allowlist gets the early rejection for free.
|
|
60
|
+
*
|
|
61
|
+
* Mirrors applyOutputProjection's own precedence: --fields silences --preset
|
|
62
|
+
* entirely (never even inspected) both here and there, so the two agree on
|
|
63
|
+
* every input rather than only on the paths that reach a real request.
|
|
64
|
+
*/
|
|
65
|
+
export function assertPresetApplicable({ fields, preset, request } = {}) {
|
|
66
|
+
if (fields != null && fields !== "") return;
|
|
67
|
+
if (preset == null || preset === "") return;
|
|
68
|
+
resolvePresetFields(preset, request);
|
|
69
|
+
}
|
|
70
|
+
|
|
50
71
|
function resolvePresetFields(preset, request) {
|
|
51
72
|
if (preset !== "summary") {
|
|
52
73
|
writeJsonError({
|
package/src/requests.js
CHANGED
|
@@ -387,6 +387,29 @@ export function buildRequest(command, options, positional) {
|
|
|
387
387
|
};
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
// NAK-804. A different route shape from every other `games` sub-path above:
|
|
391
|
+
// there is exactly one game namespace behind it, keyed on the plain numeric
|
|
392
|
+
// appid, so there is no --source flag to select and no `assertSource` call.
|
|
393
|
+
// The endpoint takes no query parameters at all (the review-history
|
|
394
|
+
// sibling's shape), so `assertSupportedOptions` closes the same gap that
|
|
395
|
+
// one documents: an unsupported flag would otherwise be silently dropped
|
|
396
|
+
// and answer with exit 0 as if the narrowing had been applied.
|
|
397
|
+
if (matches(command, ["games", "ai-disclosure"])) {
|
|
398
|
+
assertSupportedOptions(options, {
|
|
399
|
+
command: "gmc games ai-disclosure",
|
|
400
|
+
allowed: []
|
|
401
|
+
});
|
|
402
|
+
const appid = assertAppid(positional[0], "gmc games ai-disclosure <appid> --json");
|
|
403
|
+
assertNoExtraPositionals(positional, {
|
|
404
|
+
expected: 1,
|
|
405
|
+
usage: "gmc games ai-disclosure <appid> --json"
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
path: `/api/v1/games/${encodeURIComponent(String(appid))}/ai-disclosure`,
|
|
409
|
+
query: {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
390
413
|
if (matches(command, ["reviews", "patterns"])) {
|
|
391
414
|
assertSource(options.source);
|
|
392
415
|
const isCompare = positional[0] === "compare";
|
|
@@ -749,6 +772,13 @@ function buildOptionalGameCountRequest(options) {
|
|
|
749
772
|
// server would have rejected.
|
|
750
773
|
const AI_DISCLOSURE_VALUES = ["disclosed", "absent", "unconfirmed"];
|
|
751
774
|
|
|
775
|
+
// Mirrors RELEASE_STATUS_FILTER_VALUES in the app's games repository module
|
|
776
|
+
// (NAK-803), which the market RPCs re-check against the same literal triple.
|
|
777
|
+
// Kept as a local literal for the same reason as AI_DISCLOSURE_VALUES above:
|
|
778
|
+
// the CLI package cannot import server code, and the server remains the
|
|
779
|
+
// enforcing authority.
|
|
780
|
+
const RELEASE_STATUS_VALUES = ["released", "upcoming", "tba"];
|
|
781
|
+
|
|
752
782
|
function buildGameCountQuery(options) {
|
|
753
783
|
const query = pickQuery(options, {
|
|
754
784
|
query: "q",
|
|
@@ -834,6 +864,26 @@ function buildGameCountQuery(options) {
|
|
|
834
864
|
AI_DISCLOSURE_VALUES
|
|
835
865
|
);
|
|
836
866
|
}
|
|
867
|
+
// NAK-803: --release-status is a FILTER, so it lives in the shared
|
|
868
|
+
// count-query builder and reaches games search, count and aggregate alike.
|
|
869
|
+
// `releaseStatus` was already in the REST contract but had no CLI mapping,
|
|
870
|
+
// which forced the CLI form of a "released games only" question to lean on
|
|
871
|
+
// the release window alone (see docs/cli-use-cases.md section 67). Like
|
|
872
|
+
// --ai-disclosure, the three values are a closed set the server enforces
|
|
873
|
+
// case-sensitively, so a mistyped value is a usage error here rather than a
|
|
874
|
+
// silently-unfiltered, still-charged request.
|
|
875
|
+
//
|
|
876
|
+
// Note this filter is an exact match layered ON TOP of the unconditional
|
|
877
|
+
// stale-upcoming guard (NAK-78): a title still marked `upcoming` past its
|
|
878
|
+
// provisional release date is dropped from every query regardless of this
|
|
879
|
+
// flag, so `--release-status upcoming` never resurrects one.
|
|
880
|
+
if (options["release-status"] != null) {
|
|
881
|
+
query.releaseStatus = assertEnumQueryOption(
|
|
882
|
+
options["release-status"],
|
|
883
|
+
"--release-status",
|
|
884
|
+
RELEASE_STATUS_VALUES
|
|
885
|
+
);
|
|
886
|
+
}
|
|
837
887
|
return query;
|
|
838
888
|
}
|
|
839
889
|
|
|
@@ -1168,6 +1218,31 @@ function assertExternalId(value, usage) {
|
|
|
1168
1218
|
}
|
|
1169
1219
|
}
|
|
1170
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* NAK-804. Validates the appid positional for `gmc games ai-disclosure`,
|
|
1223
|
+
* mirroring the strict path-parameter check the app applies to every
|
|
1224
|
+
* `/api/v1/.../games/{appid}` route: a plain positive decimal integer, and
|
|
1225
|
+
* nothing else. The lexical test is deliberately `/^\d+$/` rather than
|
|
1226
|
+
* `Number(value)`, the same reasoning `assertLanguageTopicMinReviews` above
|
|
1227
|
+
* documents for `--min-reviews` -- `Number` also accepts the exponent, hex
|
|
1228
|
+
* and decimal-point spellings ("7e2", "0x2da", "730.0") that the app's
|
|
1229
|
+
* validator rejects outright, so letting those through here would mean a
|
|
1230
|
+
* value the API answers 400 for silently reaches the wire as if it were
|
|
1231
|
+
* fine. This check is advisory; the server remains the enforcing authority.
|
|
1232
|
+
*/
|
|
1233
|
+
function assertAppid(value, usage) {
|
|
1234
|
+
const raw = value == null ? "" : String(value);
|
|
1235
|
+
const appid = DECIMAL_INTEGER_RE.test(raw) ? Number.parseInt(raw, 10) : Number.NaN;
|
|
1236
|
+
if (!Number.isSafeInteger(appid) || appid <= 0) {
|
|
1237
|
+
writeJsonError({
|
|
1238
|
+
code: "CLI_USAGE_ERROR",
|
|
1239
|
+
message: `Missing or invalid appid (must be a positive integer). Usage: ${usage}`
|
|
1240
|
+
});
|
|
1241
|
+
process.exit(EXIT_USAGE);
|
|
1242
|
+
}
|
|
1243
|
+
return appid;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1171
1246
|
function assertResolveQuery(value) {
|
|
1172
1247
|
if (value == null || String(value).trim() === "") {
|
|
1173
1248
|
writeJsonError({
|