@witchpot/gmc 0.6.0 → 0.8.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.
@@ -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.6.0
11
+ version: 0.15.0
12
12
  ---
13
13
 
14
14
  # GMC Analysis
@@ -51,8 +51,32 @@ applies identically. Mechanics map as follows:
51
51
  - Cohort evidence (`reviews patterns`, `cohort ...`) -> `cohort_evidence`,
52
52
  `cohort_review_categories`, `search_review_claims`.
53
53
  - Single-title drill-down (`games detail` / `games analysis`) ->
54
- `game_profile`; name resolution -> `resolve`; showcases ->
55
- `showcase_fit`.
54
+ `game_profile`; name resolution -> `resolve`; showcase submission
55
+ candidates -> `showcase_fit`; per-title participation history ->
56
+ `showcase_history`; Steam store-page neighbours (`games more-like-this`)
57
+ -> `more_like_this`; review topics by review language (`games
58
+ language-topics`) -> `game_language_topics`; review-count history with
59
+ its lifecycle/announcement/discount context (`games review-history`) ->
60
+ `game_review_history`.
61
+ - Per-family collection status ("has GMC actually collected X for this
62
+ game?") has no CLI command yet and is MCP-only: `game_collection_status`
63
+ (see the dedicated section below).
64
+ - Publisher/developer questions ("what else has this studio shipped", how
65
+ often they release, how big their audience is) have no CLI command yet
66
+ and are MCP-only: `entity_resolve` turns a raw Steam publisher/developer
67
+ string into a stable `entityId`, then `entity_profile` returns that
68
+ entity's portfolio, composition, release cadence and audience footprint.
69
+ Always resolve first — a game record credits names, never ids — and pass
70
+ the id, since one entity commonly has several raw spellings and a
71
+ name-keyed lookup silently omits the titles credited to the siblings.
72
+ `resolved: null` means that exact string (case-sensitive, whitespace
73
+ included) is not in the master tables; it never means the entity has no
74
+ titles. Read `aggregatesAvailability` before anything else:
75
+ `not_collected` means the aggregates were not computed yet, so the block
76
+ is absent rather than zero, and it never means the studio has published
77
+ nothing — `pagination.total` still carries the live title count. A null
78
+ `totalFollowers` is not collected, never zero, and follower counts can
79
+ never be summed across platforms.
56
80
  - The response envelope differs from the CLI: MCP tool responses carry
57
81
  `meta.quota` = `{ used, limit, remaining, resets_at }` (credits) plus
58
82
  `meta.credits_charged`, `meta.basis`, `meta.denominator`,
@@ -103,10 +127,13 @@ A Game List is a workspace-scoped, reusable cohort input — persist a filter
103
127
  on every call.
104
128
 
105
129
  - 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.
130
+ `remove`, `delete`, `resolve`, `materialize`) or the MCP tools
131
+ `list_game_lists` / `get_game_list` / `create_game_list` /
132
+ `update_game_list` / `add_game_list_items` / `remove_game_list_item` /
133
+ `delete_game_list` / `resolve_game_list` / `materialize_game_list`. All are
134
+ scoped to the caller's own workspace. The first seven are 0-credit;
135
+ `resolve_game_list` and `materialize_game_list` cost 2 each (see
136
+ "Query-backed lists" below).
110
137
  - Two kinds: `manual` (explicit appid membership) and `filter` (a stored
111
138
  `GameFilter`, re-evaluated live against current data on every read). The
112
139
  MCP `game_list_id` input accepts `filter`-kind lists only; the CLI
@@ -124,8 +151,56 @@ on every call.
124
151
  calls — diff hashes across calls instead of re-diffing the filter
125
152
  yourself.
126
153
 
154
+ ### Query-backed lists (structured plus natural language)
155
+
156
+ A `filter` list's definition may also be the version 2 query envelope:
157
+
158
+ ```json
159
+ {"version":2,"mode":"query",
160
+ "structured":{"tags":["cozy"],"reviewsMin":500},
161
+ "semanticQuery":"games about running a shop",
162
+ "semanticMode":"rank"}
163
+ ```
164
+
165
+ Read it exactly this way, and say it this way to the user:
166
+
167
+ - **`structured` decides membership.** Under the default `semanticMode:"rank"`
168
+ the natural-language part only ORDERS results inside that universe; it never
169
+ adds a game the structured conditions exclude. Under `"narrow"` it also
170
+ narrows membership.
171
+ - **The total may not be a catalog count.** `meta.resolution.total_kind` says
172
+ which: `exact`, `approximate`, `at_least` (a floor), `lexical_only` (the total
173
+ counts the lexical population while the page is wider), or `clipped`. Anything
174
+ other than `exact` must be reported as approximate. Never restate it as an
175
+ exact number.
176
+ - **A `semanticQuery` list needs a resolution before its games can be read.**
177
+ `list_games` / `get_game_list` / `gmc lists games <id>` answer
178
+ `RESOLUTION_REQUIRED` until `resolve_game_list` (CLI: `gmc lists resolve <id>`)
179
+ has run. Resolve costs 2 credits every call, including when an existing
180
+ resolution is reused, and it never changes membership.
181
+ - **Do not pass `sort` or `as_of` when reading a `semanticQuery` list or a
182
+ frozen one.** Both are refused with `INVALID_INPUT`: such a list is served in
183
+ its resolved ranking order or its stored order, so a sort you supplied could
184
+ only be ignored. Omit them.
185
+ - **If a resolve comes back `CONFLICT`, someone edited the list while it ran.**
186
+ Re-read the list and resolve again; `details.reason` is
187
+ `definition_revision_mismatch`. `NOT_FOUND` or `SNAPSHOT_IS_FROZEN` from a
188
+ resolve means the list was deleted or frozen mid-flight.
189
+ - **`materialize_game_list` is not the same operation.** It FREEZES the current
190
+ members, so the list stops following its definition, and the only way back is
191
+ to discard the frozen rows. It costs 2 credits, requires `confirm: true` (CLI:
192
+ `--confirm`), and must never be called speculatively — if the user only wants
193
+ the list evaluated now, `resolve_game_list` is the non-destructive option.
194
+
127
195
  Sharp edges:
128
196
 
197
+ - **A `semanticQuery` list cannot be aggregated.** `market_aggregate` and
198
+ `cohort_review_categories` (and `cohort_evidence`) reject it with
199
+ `SEMANTIC_LIST_UNSUPPORTED_FOR_TOOL`, in `rank` mode as well as `narrow`.
200
+ They could only aggregate the structured half, which is a different population
201
+ from the one the list contains — a wrong number, not a degraded one. Use
202
+ `list_games` with the `game_list_id` to read its games. A **structured-only**
203
+ v2 envelope has no such limit and behaves exactly like a flat filter.
129
204
  - **Composite (union) lists** work only with `list_games`/`cohort_evidence`;
130
205
  `market_aggregate`/`cohort_review_categories` reject them with
131
206
  `COMPOSITE_LIST_UNSUPPORTED_FOR_TOOL` (those two are single aggregate
@@ -139,6 +214,296 @@ Sharp edges:
139
214
  `CONFIRMATION_REQUIRED`, naming the list; only pass `confirm` set to that
140
215
  exact name after the user has explicitly asked to delete it — never
141
216
  guess or pre-fill it speculatively.
217
+ - `list_game_lists` reports `membership` per row and `get_game_list` adds
218
+ `definition_revision` and `last_resolved_at`. A `membership:"snapshot"` list
219
+ is frozen: it serves stored rows, ignores its definition, and answers
220
+ `SNAPSHOT_IS_FROZEN` if you try to resolve it.
221
+
222
+ ## Showcase workflows (fit + history)
223
+
224
+ Two complementary surfaces; use them as a pair for "where should we
225
+ submit?" questions:
226
+
227
+ - **Forward-looking fit**: `gmc showcases fit --source steam <externalId>
228
+ --json` (MCP: `showcase_fit`) — tag-overlap candidates from the public
229
+ showcase directory, filtered to currently recommendable entries.
230
+ - **Participation history**: `gmc showcases history --source steam
231
+ <externalId> --json` (MCP: `showcase_history`) — which showcases/events a
232
+ title actually appeared in, with a `directory` link (series, next
233
+ edition, submission window) attached when the event name resolves to the
234
+ directory. The chain "similar games -> which showcases did they attend ->
235
+ next edition deadline" is: resolve the cohort, run history per title,
236
+ then read the next-edition field on the linked rows.
237
+ - Field casing differs by transport: the CLI keeps the API's camelCase
238
+ (`directory.nextEdition.submissionCloses`), while the MCP tool emits
239
+ snake_case (`directory.next_edition.submission_closes`). Look for the
240
+ casing that matches the surface you called before concluding a field is
241
+ absent.
242
+
243
+ Interpretation guardrails (these are honesty rules, not suggestions):
244
+
245
+ - Read the response's warnings/caveats BEFORE interpreting missing
246
+ `directory` fields. On plan-limited responses the appearance rows remain
247
+ but every directory link is redacted (the CLI summary then reports
248
+ `linkedToDirectoryCount: null`, and a plan-limited warning is attached) —
249
+ that state is **indeterminate**, not "unlinked". The same applies when a
250
+ directory-lookup warning is present.
251
+ - Absent such warnings, a row without `directory` means the event name is
252
+ **not linked** to the directory (unresolved alias, or a non-showcase
253
+ event such as an award show) — it NEVER means the appearance didn't
254
+ happen. Do not drop unlinked rows from the story; label them as
255
+ unlinked.
256
+ - Over MCP, `showcase_history` merges a second source (`steam_event`,
257
+ Steam-operated events like Next Fest). A `meta.warnings` entry saying the
258
+ steam_event source is unavailable means that source was NOT collected —
259
+ report "research-source only", never "zero Steam event participations".
260
+ The CLI command covers the research source only and says so in its
261
+ warnings.
262
+ - The next-edition data depends on the directory's edition coverage,
263
+ which is still sparse for submission windows — absence of the
264
+ next-edition field (or of its submission-close date) means "window
265
+ unknown", not "no upcoming edition". When present, check the
266
+ submission-close date against today before recommending a submission.
267
+
268
+ ## Steam "More Like This" neighbours
269
+
270
+ `gmc games more-like-this --source steam <externalId> --json` (MCP:
271
+ `more_like_this`) returns the games Steam itself showed in a title's
272
+ store-page "More Like This" carousel, for one storefront country.
273
+ `--country` accepts `de`, `fr`, `gb`, `jp`, `kr`, `us` and defaults to
274
+ `us`; omit the flag unless the question is about a specific storefront.
275
+
276
+ Use it as a cohort seed — "who does Steam put next to this game?" — then
277
+ feed the candidate appids into `coverage_check` and the cohort workflows
278
+ above. It is an observation of Steam's storefront, not a computed
279
+ similarity model, so it answers "what does Steam associate?" and not "what
280
+ is objectively similar?".
281
+
282
+ Interpretation guardrails (honesty rules, not suggestions):
283
+
284
+ - `position` is the order Steam displayed the candidate. It is **not** a
285
+ similarity score and not a relevance ranking. Never sort by it as if it
286
+ measured closeness, and never say "the most similar game is X" because X
287
+ sat at position 1.
288
+ - An empty candidate list is ambiguous until you read `availability`.
289
+ `observed_empty` means the store page was collected and the carousel was
290
+ empty; `not_observed` means that game and country pair has not been
291
+ collected yet. Report the second as missing collection — presenting it as
292
+ "this game has no similar games" states a collection gap as a fact about
293
+ the market.
294
+ - `stale: true` means the observation is older than `staleAfterDays`. The
295
+ candidates are still returned in full; date the claim ("as observed in
296
+ <month>") instead of dropping it or presenting it as current.
297
+ - Candidates with `resolved: false` are part of the observed set but have
298
+ no catalog details (delisted, or not a game-type app). Keep them in the
299
+ count and label them unresolved; do not silently drop them.
300
+
301
+ ## Review topics by review language
302
+
303
+ `gmc games language-topics --source steam <externalId> --json` (MCP:
304
+ `game_language_topics`) breaks one game's review topics down by the
305
+ language each review was written in — one row per topic cluster x
306
+ language. `--min-reviews <10-200>` moves the evidence floor (server
307
+ default 30); `--include-insufficient` also returns the rows below it,
308
+ capped by the server. Those two flags are the whole parameter surface,
309
+ matching the API exactly: there is no language filter, topic filter, or row
310
+ limit, so narrow by reading the rows rather than by inventing a flag.
311
+
312
+ Use it for "which topics land differently depending on the language players
313
+ review in?" — a localization and community-priority question, not a
314
+ market-sizing one.
315
+
316
+ Field paths differ by surface: the CLI and REST wrap the payload, so read
317
+ `data.evidenceState` / `data.snapshot` / `data.coverage`. The MCP tool
318
+ returns the same fields at the top level, so read `evidenceState` /
319
+ `snapshot` / `coverage`. The guardrails below name the CLI path; drop the
320
+ `data.` prefix over MCP.
321
+
322
+ Interpretation guardrails (honesty rules, not suggestions):
323
+
324
+ - **Read `data.evidenceState` first.** `ready` = at least one topic x
325
+ language row cleared the floor. `insufficient_evidence` = rows exist but
326
+ none cleared it — thin evidence, not absent sentiment. `not_analyzed` =
327
+ no rows exist — no data, not zero mentions. Neither of the last two is
328
+ ever a zero, and neither may be reported as "no complaints in that
329
+ language".
330
+ - **The population is the analyzed subset**, about 2,500 titles today,
331
+ selected for review analysis rather than sampled from the catalog. Say so
332
+ every time, and never extrapolate a share of Steam from it.
333
+ - **Review language is not geography.** It is the language the text was
334
+ written in — never a country, a region, a market, or a culture. "Reviews
335
+ written in German" is a claim you can make; "German players" is not.
336
+ - **Observed and estimated are separate objects.** `counts`/`rates` are
337
+ observed; `estimates` are sample-expanded and carry
338
+ `samplingWeightMethod`. Never combine them in one figure, and always say
339
+ which one a number came from.
340
+ - **Name the denominator.** `counts.sourceReviews`, `sampledReviews`,
341
+ `analyzedReviews`, and `distinctReviews` are four different bases;
342
+ `min_reviews` is applied to `distinctReviews`.
343
+ - **Date the claim.** `data.snapshot` carries `periodStart`/`periodEnd`,
344
+ `analysisVersion`, and `generatedAt`. The aggregate is refreshed by the
345
+ reviews pipeline, so report it as a snapshot rather than as current.
346
+ - **`data.coverage` survives the plan lock.** When
347
+ `reviews.topicLanguages.full` is locked you receive at most 5 rows with
348
+ `estimates` reduced to `samplingWeightMethod`, but `coverage` still
349
+ states how much evidence exists — present the rows as a preview and name
350
+ the lock key.
351
+
352
+ ## Which review count you are looking at
353
+
354
+ Steam's `appreviews` endpoint returns two different totals depending on
355
+ `purchase_type`, and the gap is not a rounding difference. Measured
356
+ 2026-07-28: appid 570 came back with 14,375 under the default and 2,746,987
357
+ with `purchase_type=all`.
358
+
359
+ - `steam` is the endpoint DEFAULT and counts only reviewers who activated the
360
+ game on Steam. **Every plain `reviews` field in every tool and CLI payload
361
+ is this family**, including list rows, the `reviews_desc` sort, the
362
+ `reviews_min`/`reviews_max` filters, `market_aggregate`'s `*_reviews`
363
+ metrics, and the entity portfolio aggregates.
364
+ - `all` counts every reviewer and is a strict superset. It is GMC's headline
365
+ current total and is served by `game_profile` as `detail.reviewCounts`.
366
+
367
+ Rules:
368
+ - **Name the family and the observation date** with any count you report.
369
+ "2.7M reviews across all purchase types, observed 2026-08-11" is a claim;
370
+ "2.7M reviews" is not.
371
+ - **`allAvailable: false` means not collected.** The wider count does not
372
+ exist for that title yet. It is never zero, and the steam number beside it
373
+ must not be presented as if it were the wide one.
374
+ - **Compare the families only through `steamAtAllObservation`.** The
375
+ top-level `steam` observation can be newer than `all`, so a share taken
376
+ across the two would mix dates. `steamShare` is already computed from the
377
+ same-row pair.
378
+ - **Derive a positive rate inside one family.** `snapshot.rating` is the
379
+ steam-derived percentage; pairing it with an all-family total is a
380
+ mixed-family figure.
381
+ - **Never splice the families into one series.** The all family only starts
382
+ at its capture epoch, so a joined line shows a jump that never happened.
383
+ `game_review_history` and `game_profile.series` both carry
384
+ `family: "steam"` for their whole length, which means their last point is
385
+ normally SMALLER than the all-family headline. They are not
386
+ interchangeable, and a difference between them is not a decline.
387
+ - **Neither family is sales.** A review count is not units, owners, copies,
388
+ or revenue.
389
+
390
+ ## Review-count history and the events around it
391
+
392
+ `gmc games review-history --source steam <externalId> --json` (MCP:
393
+ `game_review_history`) returns one game's full daily total-review series
394
+ plus the collected market context beside it: `events` (release,
395
+ early-access, demo lifecycle points and clustered official announcements)
396
+ and `discountWindows` (observed major-discount states).
397
+
398
+ There are no flags. The endpoint takes no query parameters, so the whole
399
+ series comes back every time — window it yourself when the question has a
400
+ range, and say which window you chose. Do not look for a `--range` or
401
+ `--from`/`--to`; narrowing belongs in your reading, not in the request. The
402
+ CLI rejects those flags with a usage error rather than ignoring them, so
403
+ that failure is not transient — drop the flag and window the result.
404
+
405
+ Use it for "how did this title's reviews accumulate, and what was going on
406
+ around the jumps?" — a timeline question. It answers what was OBSERVED near
407
+ a movement, never what caused it.
408
+
409
+ Field paths differ by surface: the CLI and REST wrap the payload, so read
410
+ `data.series` / `data.events` / `data.discountWindows`. The MCP tool returns
411
+ the same fields at the top level. The guardrails below name the CLI path;
412
+ drop the `data.` prefix over MCP.
413
+
414
+ Interpretation guardrails (honesty rules, not suggestions):
415
+
416
+ - **Scale by date, never by array index.** Collection has real gaps, so two
417
+ adjacent entries are not two adjacent days. A chart or a rate computed on
418
+ index positions is wrong.
419
+ - **The series is cleaned, and it can lag.** It is published-run-only and
420
+ contamination-filtered: an observation more than 2% below the highest
421
+ earlier one for the same game is withheld as invalid. Differences taken
422
+ from it are safe to report, but its last point can be OLDER than the
423
+ game's headline review count until that day's run is published. Date the
424
+ claim. An empty series means no published daily observation has covered
425
+ the title yet — not a review count of zero.
426
+ - **Empty event arrays are collection states.** An empty `data.events` or
427
+ `data.discountWindows` means nothing was COLLECTED for that source. Never
428
+ report it as "no announcements", "no updates", or "never discounted".
429
+ - **The announcement list is capped.** Official Steam feeds only, most
430
+ recent 1000 items before clustering, one cluster per UTC day (`count` is
431
+ the cluster size). For a long-lived game the oldest announcements are
432
+ absent, so this is not its complete announcement history.
433
+ - **A discount window is a threshold observation, not a sale.** It is an
434
+ observed state at the >=20% collection threshold. `endDate` is the first
435
+ observation back below the threshold and is NOT part of the window, and
436
+ because observations have gaps you cannot infer the actual last discounted
437
+ day from it. `endDate: null` means no end has been observed yet;
438
+ `discountPercent` is the opening observation, not the deepest or final
439
+ depth.
440
+ - **Temporal comparison only.** "Reviews rose in the week after the update"
441
+ is a claim you can make; "the update caused the rise" is not.
442
+
443
+ ## Per-family collection status
444
+
445
+ `game_collection_status` (MCP-only, no CLI command) returns per-signal-family
446
+ collection status for one appid: `families[]` always carries all six —
447
+ `metadata`, `reviews`, `research`, `followers`, `social`, `ccu` — each with a
448
+ `state` of `not_collected`, `enrolled`, `current`, `stale`, `unsupported`, or
449
+ `unknown`. Use it for "has GMC actually collected X for this game yet?"
450
+ questions before treating a thin or empty result from another tool as a real
451
+ finding about the title.
452
+
453
+ Interpretation guardrails (honesty rules, not suggestions):
454
+
455
+ - **`unknown` is not `not_collected`.** They differ in KIND, not degree.
456
+ `not_collected` is a positive finding — nothing has been collected for that
457
+ family. `unknown` means the status itself could not be determined (the read
458
+ model was unreachable, or a preceding catalogue read failed) and is
459
+ evidence of neither collection nor non-collection. Never restate an
460
+ `unknown` family as uncollected.
461
+ - **`available: false` and the `collection_status_unavailable` warning are
462
+ coupled.** When the whole status could not be determined, every family
463
+ reports `unknown` and the bare token `collection_status_unavailable`
464
+ appears in `warnings` if and only if `available` is `false` — branch on
465
+ either alone.
466
+ - **`requestable` is narrow.** It says only whether the product's
467
+ detailed-analysis request routes demand to that family TODAY — only
468
+ `research` is `true`. It is never a statement about any other family's own
469
+ collection schedule, and requesting analysis never starts collecting a
470
+ family whose `requestable` is `false`.
471
+ - **Read-only.** Calling this tool never enqueues, enrolls, or prioritizes
472
+ collection work, no matter how many times it is called — it is a status
473
+ read, not a collection request.
474
+ - An unknown appid returns `NOT_FOUND`, with the fixed weight-1 credit
475
+ already charged — resolve the appid with `resolve`/`list_games` first
476
+ rather than probing.
477
+
478
+ ## AI disclosure state
479
+
480
+ Every title carries a tri-state AI-disclosure read from its Steam store page:
481
+ `disclosed`, `absent`, or `unconfirmed`. Filter a cohort with
482
+ `--ai-disclosure <state>` (CLI) or `filter.ai_disclosure` (MCP) on games
483
+ search/count and `market_aggregate`; group with
484
+ `--group-by ai_disclosure` (it cross-tabs with `release_month`); read one
485
+ title's state from `detail.aiDisclosure` (`state`, `observedAt`,
486
+ `rawCategoryValue`).
487
+
488
+ Interpretation guardrails (honesty rules, not suggestions):
489
+
490
+ - **`absent` is an observation, not a verdict.** It means a read of the store
491
+ page found no disclosure block. Steam requires disclosure only in defined
492
+ cases and disclosure text can be edited away, so `absent` must never be
493
+ reported as "this title uses no generative AI".
494
+ - **`unconfirmed` is not a finding.** It means not yet successfully observed,
495
+ and it includes titles nobody has checked. Report it as its own bucket with
496
+ its count; never fold it into `absent` and never drop it from a share, or
497
+ the remaining two states become a claim about the whole cohort.
498
+ - **The date matters.** `observedAt` is when the read happened, not the
499
+ release date, and the state is the latest observed one. A title can disclose
500
+ today and not have when it shipped.
501
+ - **`aiDisclosure: null` on a detail read** means only that the read model
502
+ could not be reached; the response also carries `ai_disclosure_unavailable`.
503
+ Never report it as "not disclosed".
504
+ - `cohort_review_categories` and `compare_as_of` refuse this filter outright
505
+ (the underlying rollups have no disclosure predicate). Size disclosure
506
+ cohorts with `market_aggregate` instead.
142
507
 
143
508
  ## Hard rules
144
509
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0",
3
- "contentHash": "bef9bbc3cff1cd235dd2eab321088ce29002951e120eca300df64fefd8d956a6",
2
+ "version": "0.15.0",
3
+ "contentHash": "74cc6c0184ced1c74a624bd193f531bfad70dae3507f52b1a8090b02d81427b9",
4
4
  "files": 7
5
5
  }
package/src/args.js CHANGED
@@ -1,7 +1,21 @@
1
1
  import { EXIT_USAGE } from "./constants.js";
2
2
  import { writeJsonError } from "./output.js";
3
3
 
4
- export const BOOLEAN_OPTIONS = new Set(["json", "help", "version", "no-open", "global", "include-total"]);
4
+ export const BOOLEAN_OPTIONS = new Set([
5
+ "json",
6
+ "help",
7
+ "version",
8
+ "no-open",
9
+ "global",
10
+ "include-total",
11
+ // gmc games language-topics (NAK-455): mirrors the REST
12
+ // `include_insufficient` flag, which takes no value.
13
+ "include-insufficient",
14
+ // gmc lists materialize (NAK-525): the freeze acknowledgement. A value-taking
15
+ // flag here would swallow the next token, so `--confirm --json` would parse as
16
+ // `confirm: "--json"` and the freeze would run unacknowledged.
17
+ "confirm"
18
+ ]);
5
19
 
6
20
  export function parseArgs(args) {
7
21
  const command = [];
@@ -16,6 +30,19 @@ export function parseArgs(args) {
16
30
  const key = separatorIndex >= 0 ? raw.slice(0, separatorIndex) : raw;
17
31
  const inlineValue = separatorIndex >= 0 ? raw.slice(separatorIndex + 1) : undefined;
18
32
  if (BOOLEAN_OPTIONS.has(key)) {
33
+ // Every option in BOOLEAN_OPTIONS is presence-only, so the `--flag=value`
34
+ // spelling has no meaning here. It used to be accepted and the value
35
+ // dropped on the floor, which made `--include-insufficient=false` (and
36
+ // `--json=false`) read as TRUE — the caller's stated intent silently
37
+ // inverted. Reject it instead of teaching these flags a second, parallel
38
+ // value grammar: the fix for `--flag=false` is to omit the flag.
39
+ if (inlineValue != null) {
40
+ writeJsonError({
41
+ code: "CLI_USAGE_ERROR",
42
+ message: `--${key} is a flag and takes no value. Pass --${key} on its own to enable it, or omit it to leave it off.`
43
+ });
44
+ process.exit(EXIT_USAGE);
45
+ }
19
46
  options[key] = true;
20
47
  continue;
21
48
  }
@@ -58,6 +85,11 @@ function isCommandToken(command, token) {
58
85
  "cohort",
59
86
  "showcases",
60
87
  "lists",
88
+ // NAK-706. A token missing from this list is NOT an error: it falls
89
+ // through to positional, so `gmc steam tag-trends` would parse as the
90
+ // unknown command `steam` with a stray positional rather than reaching
91
+ // the tag-trends branch.
92
+ "steam",
61
93
  "auth",
62
94
  "login",
63
95
  "logout",
@@ -77,7 +109,7 @@ function isCommandToken(command, token) {
77
109
  return ["install", "print", "status"].includes(token);
78
110
  }
79
111
  if (command[0] === "games") {
80
- return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "success-report"].includes(token);
112
+ return ["search", "count", "aggregate", "resolve", "benchmark", "detail", "analysis", "compare", "topics", "language-topics", "review-history", "success-report", "more-like-this"].includes(token);
81
113
  }
82
114
  if (command[0] === "reviews") {
83
115
  return token === "patterns";
@@ -95,10 +127,16 @@ function isCommandToken(command, token) {
95
127
  return ["trends", "pricing", "brief", "compare"].includes(token);
96
128
  }
97
129
  if (command[0] === "showcases") {
98
- return ["search", "fit", "patterns"].includes(token);
130
+ return ["search", "fit", "history", "patterns"].includes(token);
99
131
  }
100
132
  if (command[0] === "lists") {
101
- return ["create", "show", "games", "add", "remove", "delete"].includes(token);
133
+ // NAK-525 adds materialize/resolve. A token missing from this list is NOT
134
+ // an error: it falls through to positional, so `gmc lists materialize <id>`
135
+ // would silently run the bare `gmc lists` index and exit 0.
136
+ return ["create", "show", "games", "add", "remove", "delete", "materialize", "resolve"].includes(token);
137
+ }
138
+ if (command[0] === "steam") {
139
+ return ["tag-trends", "trend-stories"].includes(token);
102
140
  }
103
141
  return false;
104
142
  }
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.6.0";
10
+ export const CLI_VERSION = "0.8.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");
@@ -32,13 +32,17 @@ export const SUPPORTED_BETA_COMMANDS = [
32
32
  "gmc games success-report --source steam <externalId> --json",
33
33
  "gmc campaigns signals --source steam <externalId> --json",
34
34
  "gmc games topics --source steam <externalId> --json",
35
+ "gmc games language-topics --source steam <externalId> --json",
36
+ "gmc games review-history --source steam <externalId> --json",
37
+ "gmc games more-like-this --source steam <externalId> --json",
35
38
  "gmc games compare --source steam <externalId> <externalId...> --focus market --json",
36
39
  "gmc games benchmark --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
37
40
  "gmc lists --json",
38
41
  "gmc lists games <id> --json",
39
42
  "gmc reviews patterns --source steam --tags <tag_slug[,tag_slug...]> --reviews-min <number> --json",
40
43
  "gmc showcases search --query <text> --json",
41
- "gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json"
44
+ "gmc showcases fit --source steam <externalId> --deadline-before <YYYY-MM-DD> --json",
45
+ "gmc showcases history --source steam <externalId> --json"
42
46
  ];
43
47
 
44
48
  export const PROVISIONAL_BETA_COMMANDS = [
@@ -51,7 +55,12 @@ export const PROVISIONAL_BETA_COMMANDS = [
51
55
  "gmc cohort compare --source steam --tags <tag_slug[,tag_slug...]> --json",
52
56
  "gmc cohort brief --source steam --tags <tag_slug[,tag_slug...]> --json",
53
57
  "gmc cohort brief --source steam --list <id> --json",
54
- "gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json"
58
+ "gmc showcases patterns --source steam --tags <tag_slug[,tag_slug...]> --json",
59
+ // NAK-706. Provisional rather than supported: the endpoint is pre-launch and
60
+ // default-off, so claiming supported-beta status would promise a stability
61
+ // the feature flag has not yet been asked to keep.
62
+ "gmc steam trend-stories --locale en --json",
63
+ "gmc steam tag-trends --view featured|board|group|table|tag --json"
55
64
  ];
56
65
 
57
66
  export const COMPARE_FOCUSES = ["overview", "market", "reviews", "marketing", "audience", "showcases"];
@@ -65,5 +74,13 @@ export const DEFAULT_REVIEW_PATTERNS_LIMIT = "20";
65
74
  export const DEFAULT_REVIEW_PATTERNS_MAX_TOPICS_PER_GAME = "10";
66
75
  export const DEFAULT_REVIEW_PATTERNS_MIN_GAMES = "3";
67
76
  export const DEFAULT_SHOWCASE_FIT_LIMIT = "25";
77
+ // Mirrors LANGUAGE_TOPIC_MIN_REVIEWS_FLOOR/CEILING on the server (NAK-452).
78
+ // Kept as local literals because the CLI package cannot import server code;
79
+ // the server remains the enforcing authority and returns 400 (param
80
+ // min_reviews) for anything it rejects. The DEFAULT (30) is deliberately NOT
81
+ // mirrored: the CLI omits the parameter entirely when the caller does, so the
82
+ // server's default can never drift from a copy kept here.
83
+ export const LANGUAGE_TOPIC_MIN_REVIEWS_FLOOR = 10;
84
+ export const LANGUAGE_TOPIC_MIN_REVIEWS_CEILING = 200;
68
85
  export const MAX_SAMPLE_PAGES = 10;
69
86
  export const MAX_SAMPLE_SIZE = 500;