@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 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.8.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.15.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`,
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.15.0",
3
- "contentHash": "74cc6c0184ced1c74a624bd193f531bfad70dae3507f52b1a8090b02d81427b9",
2
+ "version": "0.16.0",
3
+ "contentHash": "a751df9637a24adb16604c91f77618ba1af16ce191843d85d3d8f2e6ae4df363",
4
4
  "files": 7
5
5
  }
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.8.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",
@@ -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({