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