@mapletip/data 0.3.0 → 0.5.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.
Files changed (39) hide show
  1. package/README.md +172 -2
  2. package/client/contract-compatibility.mjs +10 -0
  3. package/client/items.d.ts +28 -5
  4. package/client/items.js +59 -4
  5. package/client/map-geography.d.ts +4 -0
  6. package/client/map-geography.js +2 -0
  7. package/client/maps.d.ts +22 -0
  8. package/client/maps.js +25 -0
  9. package/client/monster-client.d.ts +11 -3
  10. package/client/monster-client.js +44 -5
  11. package/client/monsters.d.ts +27 -6
  12. package/client/monsters.js +30 -4
  13. package/client/static-client.d.ts +7 -5
  14. package/client/static-client.js +3 -2
  15. package/client/static-domains.d.ts +8 -1
  16. package/client/static-domains.js +27 -3
  17. package/generated/item-browse-pack-2.d.ts +83 -1
  18. package/generated/item-browse-pack-2.validator.js +2 -2
  19. package/generated/item-related-entity-browse-1.d.ts +48 -0
  20. package/generated/item-related-entity-browse-1.validator.js +1 -0
  21. package/generated/map-catalog-2-1.d.ts +103 -0
  22. package/generated/map-catalog-2-1.validator.d.ts +18 -0
  23. package/generated/map-catalog-2-1.validator.js +1 -0
  24. package/generated/map-geography.d.ts +79 -0
  25. package/generated/map-geography.validator.d.ts +18 -0
  26. package/generated/map-geography.validator.js +1 -0
  27. package/generated/monster-catalog-2-2.d.ts +332 -0
  28. package/generated/monster-catalog-2-2.validator.js +17 -0
  29. package/generated/monster-catalog-2-3.d.ts +339 -0
  30. package/generated/monster-catalog-2-3.validator.js +17 -0
  31. package/generated/monster-drop-browse.d.ts +123 -0
  32. package/generated/monster-drop-browse.validator.js +17 -0
  33. package/generated/monster-location-browse-1-1.d.ts +79 -0
  34. package/generated/monster-location-browse-1-1.validator.js +17 -0
  35. package/generated/monster-location-browse.d.ts +69 -0
  36. package/generated/monster-location-browse.validator.js +17 -0
  37. package/generated/product-manifest.d.ts +23 -0
  38. package/generated/product-manifest.validator.js +1 -1
  39. package/package.json +36 -2
package/README.md CHANGED
@@ -82,11 +82,97 @@ digest. `getMonsterBrowse` still requests only the release manifest named by the
82
82
  build index, the product manifest and the Monster catalog.
83
83
 
84
84
  Releases built before revision `2.1` carry contract-`2.0` catalogs without `sprite`; this
85
- SDK reads both, and `hasMonsterSprites` narrows a catalog to `2.1`. `@mapletip/data`
85
+ SDK reads both, and `hasMonsterSprites` narrows a catalog to revisions `2.1` or `2.2`. `@mapletip/data`
86
86
  `0.2.0` and older reject a `2.1` catalog with a typed `SCHEMA_VALIDATION_ERROR` instead of
87
87
  returning rows without sprites, so upgrade to `0.3.0` before adopting a pin that carries
88
88
  it.
89
89
 
90
+ ### Monster drops and locations
91
+
92
+ Monster catalog `2.2` keeps these summaries on every Monster row. The catalog-level
93
+ `browseCoverage` carries the shared witnesses and conventions: `nativeDropsAbsentCoverageKey`
94
+ is present exactly when at least one native drop row is `absent`; `historicalDrops` says whether
95
+ the build has a historical overlay; `locationsAbsentCoverageKey` is present exactly when at
96
+ least one location row is `absent`; and `locationScope` is `client-placements`. An absent row
97
+ does not repeat the catalog coverage key.
98
+
99
+ `drops.native` describes evidence from the selected build. Its `lowerBound: true` marker
100
+ says a `known` count is a lower bound while native source coverage is partial.
101
+ `unknown` means the count could not be established; it does not mean zero.
102
+ `drops.historical` is present only when the build
103
+ advertises historical overlay support. CW Launch reports native data separately from
104
+ its historical entries, so a Monster can have no native data and still have historical
105
+ drop assertions. GMS v83 reports historical drops as unsupported.
106
+
107
+ The drop browse artifact stores each distinct item once in its sorted `items[]` table.
108
+ Each record entry's `item` is an index into that table. Native entries describe this
109
+ build; historical entries also carry `sourceBuild` and `confidence`. A resolved
110
+ Equipment category is `{ kind: "equip-slot", code }`; display labels for that code come
111
+ from the same build's Equipment product slot labels. Other resolved item categories use
112
+ `{ kind: "item-class", value }`, where the value is `ItemBrowseClass`. Unresolved item
113
+ references use `{ kind: "unknown" }`.
114
+
115
+ `detail.dropEvidence.state` keeps its existing native-only meaning. It describes the
116
+ native evidence collection for the Monster and is not changed by historical overlay
117
+ assertions. Read the compact historical assertions from the drop browse artifact:
118
+
119
+ ```ts
120
+ import { getMonsterDropBrowse } from '@mapletip/data/monsters';
121
+
122
+ const dropBrowse = await getMonsterDropBrowse({
123
+ baseUrl: releaseRootUrl,
124
+ pin: releasePin,
125
+ buildId: 'cw-launch',
126
+ });
127
+ const itemKey = 'item-0123456789abcdef';
128
+ const monstersWithItem = Object.entries(dropBrowse.records)
129
+ .filter(([, record]) => record?.entries.some((entry) => {
130
+ const item = dropBrowse.items[entry.item];
131
+ return item?.itemRef.state === 'resolved' && item.itemRef.key === itemKey;
132
+ }))
133
+ .map(([monsterKey]) => monsterKey);
134
+ ```
135
+
136
+ Location summaries count client map placements, not live spawn rates. `known` carries
137
+ distinct map and placement counts; `absent` carries neither count and uses the
138
+ catalog-level coverage key; `unresolved` carries counts for the resolved portion when
139
+ some placements could not be attributed. Location browse entries carry the public
140
+ `mapKey`, map name and placement count, with no coordinates or media references.
141
+ `regionCode` is the raw source classification code, not a geographic region.
142
+
143
+ Filter by map key, then resolve that map's minimap through the same build's Media product.
144
+ The Media record key is the map key; its `map.minimap` descriptor resolves to a
145
+ content-addressed image URL:
146
+
147
+ ```ts
148
+ import { getMonsterLocationBrowse } from '@mapletip/data/monsters';
149
+ import { openRelease } from '@mapletip/data/release';
150
+ import { resolveMediaUrl } from '@mapletip/data/media';
151
+
152
+ const buildId = 'gms-v83';
153
+ const locationBrowse = await getMonsterLocationBrowse({
154
+ baseUrl: releaseRootUrl,
155
+ pin: releasePin,
156
+ buildId,
157
+ });
158
+ const mapKey = 'map-0123456789abcdef';
159
+ const monstersOnMap = Object.entries(locationBrowse.records)
160
+ .filter(([, record]) => record?.entries.some((entry) => entry.mapKey === mapKey))
161
+ .map(([monsterKey]) => monsterKey);
162
+
163
+ const release = await openRelease({ baseUrl: releaseRootUrl, pin: releasePin });
164
+ const media = await release.openMediaBuild(buildId);
165
+ const mapMedia = (await media.loadCatalog()).records.find((record) => record.key === mapKey);
166
+ const minimap = mapMedia?.media['map.minimap'];
167
+ const minimapUrl = minimap?.state === 'known'
168
+ ? resolveMediaUrl(releaseRootUrl, minimap.value)
169
+ : undefined;
170
+ ```
171
+
172
+ `0.3.0` reads catalog revisions `2.0` and `2.1` but rejects `2.2` with a typed
173
+ `SCHEMA_VALIDATION_ERROR`. Use `0.4.0` before adopting a release with drop and location
174
+ browse summaries.
175
+
90
176
  `pin` is the parsed `pin.json` published next to the release root by whoever assembled
91
177
  the release: it names the manifest path, SHA-256 and byte length and is the caller's
92
178
  trust anchor. The SDK never resolves a "current" release on its own and never fetches a
@@ -145,6 +231,66 @@ It rejects any content ID that is not a 64-character lowercase SHA-256 digest wi
145
231
  `SCHEMA_VALIDATION_ERROR`, so a row's `icon.value` can be used without building a
146
232
  make-believe descriptor.
147
233
 
234
+ ### Player facts and related identities
235
+
236
+ Item Browse 2.1 adds sparse typed player facts. An omitted sparse fact means its value is
237
+ unknown, unless that field's per-build coverage says `unsupported`; it does not mean
238
+ zero. A recorded `sellPrice: 0` is a real zero. Use
239
+ `hasItemPlayerFacts` to narrow a browse result before relying on the 2.1 coverage block;
240
+ older pinned 2.0 products remain readable. `coverage.playerFacts.requirementJobs` is
241
+ `unsupported` in both current builds because neither has a normalized non-equipment job
242
+ requirement source. NPC shop purchase prices are deferred: sell price is not a shop price.
243
+
244
+ `requirement.level` is the item's direct use-level requirement. The separate
245
+ `levelRange` fact carries GMS Monster Crystal `monsterLevelMin`/`monsterLevelMax` and
246
+ Secret Crystal `requirementLevelDelta`; these values are never folded into
247
+ `requirement.level`. Recovery keeps flat `recovery.hp`/`mp` distinct from percentage
248
+ `recovery.hpPercent`/`mpPercent`. Buff effects use the closed `ItemBrowseBuffStat`
249
+ vocabulary, and unknown source `spec` keys fail the publication gate unless they have
250
+ an explicit exclusion.
251
+
252
+ Related identities are stored in one lazy roster artifact per relationship kind. The
253
+ Items manifest pins each artifact by digest and byte length. In a roster,
254
+ `targets` is the target index table and `records[itemKey]` contains indices into that
255
+ table. Load only the kind needed for the current view. For example, this resolves the
256
+ `Dropped by` names for one item without loading all seven rosters:
257
+
258
+ ```ts
259
+ import {
260
+ getItemBrowse,
261
+ getItemRelatedEntities,
262
+ hasItemPlayerFacts,
263
+ } from '@mapletip/data/items';
264
+ import type { GetItemBrowseOptions } from '@mapletip/data/items';
265
+
266
+ declare const releaseRootUrl: GetItemBrowseOptions['baseUrl'];
267
+ declare const releasePin: GetItemBrowseOptions['pin'];
268
+
269
+ const browse = await getItemBrowse({
270
+ baseUrl: releaseRootUrl,
271
+ pin: releasePin,
272
+ buildId: 'gms-v83',
273
+ });
274
+ if (!hasItemPlayerFacts(browse)) throw new Error('this pin predates Item player facts');
275
+
276
+ const item = browse.records.find((row) => row.sourceId === '02022001');
277
+ if (!item) throw new Error('item is not present in this build');
278
+ const roster = await getItemRelatedEntities({
279
+ baseUrl: releaseRootUrl,
280
+ pin: releasePin,
281
+ buildId: 'gms-v83',
282
+ kind: 'monster-drop',
283
+ });
284
+ const droppedBy = (roster.records[item.key] ?? []).flatMap((targetIndex) => {
285
+ const target = roster.targets[targetIndex];
286
+ return target ? [{ key: target.key, name: target.displayName ?? target.key }] : [];
287
+ });
288
+ ```
289
+
290
+ The `droppedBy` result can be rendered as the row's `Dropped by` names. Target names may
291
+ be unknown, in which case the stable target key remains available. The roster read is
292
+ digest-verified, build-scoped, uncached and has no fallback to another build.
293
+
148
294
  ## What each entry loads
149
295
 
150
296
  Monster catalog validation and the release/manifest/index validators are imported
@@ -154,7 +300,7 @@ operation that needs it, as a separate on-demand chunk:
154
300
  | Entry | Eager generated validators | Loaded on demand |
155
301
  | --- | --- | --- |
156
302
  | `/monsters` | release pin, release manifest, build index, product manifest, Monster catalog 1.x, 2.0 and 2.1 | Monster detail, location/relationship, core-relationship and evidence packs |
157
- | `/items` | product manifest only (every Item validator is on demand) | browse, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` |
303
+ | `/items` | product manifest only (every Item validator is on demand) | browse, per-kind related-entity roster, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` and `getItemRelatedEntities` |
158
304
  | `/equipment` | that domain's catalog, detail, relationship and evidence packs | — |
159
305
  | `/quests` | none (Quest validators are always on demand) | catalog, browse, detail, relationship, auxiliary and evidence packs |
160
306
  | `/media` | that domain's catalog, detail packs | — |
@@ -288,3 +434,27 @@ const evPage = await client.getEvidencePage('quest-ev-1000');
288
434
 
289
435
  The Quest client supports lazy per-operation validator loading via dynamic `import()` to ensure that only the validator required for a specific operation is fetched. Browse records and detail records are lazily addressed through bounded key-range descriptors without per-record eager loads or mandatory catalog fetching for direct detail lookups.
290
436
 
437
+ ## Map geography
438
+
439
+ Map geography uses a build-scoped, digest-verified read model. `getMapGeography` reads that model from the `maps` product; `openMapProduct` returns a product with `loadGeography()`, and `release.openDomainBuild(buildId, 'maps')` provides the same operation when an opened release is reused. Map catalog rows carry compact geography keys and provenance, while the geography model stores the labels once.
440
+
441
+ The derivation layers run in order: reviewed placements, the build-family region allowlist, area names from Map marks (Hidden Street marks are excluded), then collection classification. Maps with no applicable layer have provenance `none`. Provenance values are `reviewed-overlay`, `source-region-allowlist`, `source-map-mark`, `source-classification`, and `none`.
442
+
443
+ Reviewed placement status `supported` means local witnesses support the entrance and placement. Citation register entries that could not be fetched remain inactive; they are not treated as verified external evidence. The carried-over placements in these builds use `supported` status.
444
+
445
+ Geography keys are stable filters: `region:<slug>`, `area:<region-slug>/<area-slug>`, and `collection:<slug>`. Monster catalog 2.3 rows contain the union of their location entries' region and location keys. A Region filter can use those keys directly:
446
+
447
+ ```ts
448
+ import { getMonsterBrowse, hasMonsterGeography } from '@mapletip/data/monsters';
449
+
450
+ const catalog = await getMonsterBrowse({
451
+ baseUrl: releaseRootUrl,
452
+ pin: releasePin,
453
+ buildId: 'gms-v83',
454
+ });
455
+ if (!hasMonsterGeography(catalog)) throw new Error('This release predates Monster geography');
456
+
457
+ const regionKey = 'region:henesys';
458
+ const monstersInRegion = catalog.records.filter((row) => row.geography.regionKeys.includes(regionKey));
459
+ ```
460
+
@@ -8,11 +8,16 @@ const MINIMUM_VERSION_BY_CAPABILITY = new Map([
8
8
  ['product-input-identity-v1', { major: 1, minor: 2 }],
9
9
  ['release-manifest-v1', { major: 1, minor: 2 }],
10
10
  ['monster-core-v1', { major: 2, minor: 0 }],
11
+ ['monster-drop-browse-v1', { major: 2, minor: 0 }],
12
+ ['monster-location-browse-v1', { major: 2, minor: 0 }],
13
+ ['item-player-facts-v1', { major: 2, minor: 0 }],
14
+ ['item-related-entities-v1', { major: 2, minor: 0 }],
11
15
  ['quests-v1', { major: 2, minor: 0 }],
12
16
  ['items-v1', { major: 2, minor: 0 }],
13
17
  ['equipment-v1', { major: 2, minor: 0 }],
14
18
  ['media-v1', { major: 2, minor: 0 }],
15
19
  ['maps-v1', { major: 2, minor: 0 }],
20
+ ['map-geography-v1', { major: 2, minor: 0 }],
16
21
  ['npcs-v1', { major: 2, minor: 0 }],
17
22
  ['reactors-v1', { major: 2, minor: 0 }],
18
23
  ['skills-v1', { major: 2, minor: 0 }],
@@ -33,11 +38,16 @@ const supportedClientCapabilities = new Set([
33
38
  'product-input-identity-v1',
34
39
  'release-manifest-v1',
35
40
  'monster-core-v1',
41
+ 'monster-drop-browse-v1',
42
+ 'monster-location-browse-v1',
43
+ 'item-player-facts-v1',
44
+ 'item-related-entities-v1',
36
45
  'quests-v1',
37
46
  'items-v1',
38
47
  'equipment-v1',
39
48
  'media-v1',
40
49
  'maps-v1',
50
+ 'map-geography-v1',
41
51
  'npcs-v1',
42
52
  'reactors-v1',
43
53
  'skills-v1',
package/client/items.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * client is imported on demand, so a browse-only consumer never downloads catalog,
6
6
  * detail, relationship, evidence validators or the Monster client.
7
7
  */
8
- import type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord } from '../generated/item-browse-pack-2.d.ts';
8
+ import type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowsePlayerFactCoverage, ItemBrowseBuff, ItemBrowseBuffEffect, ItemBrowseBuffStat, ItemBrowseBuffUnit, ItemBrowseCoverageState, ItemBrowseJobFamily, ItemBrowseLevelRange, ItemBrowseRecovery, ItemBrowseRequirement, ItemBrowseRecord } from '../generated/item-browse-pack-2.d.ts';
9
+ import type { ItemBrowseRelationshipKind, ItemRelatedEntityBrowsePack, ItemRelatedEntityTarget } from '../generated/item-related-entity-browse-1.d.ts';
9
10
  import type { ItemCatalog } from '../generated/item-catalog-2.d.ts';
10
11
  import type { ItemDetailRecord } from '../generated/item-detail-2.d.ts';
11
12
  import type { ItemRelationshipPage } from '../generated/item-relationship-pack-2.d.ts';
@@ -15,22 +16,37 @@ import { type GetRelationshipOptions, type OpenProductOptions, type OperationOpt
15
16
  export type StaticItemCatalog = ItemCatalog;
16
17
  export type StaticItemDetail = ItemDetailRecord;
17
18
  export type { GetRelationshipOptions };
18
- export type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowseRecord };
19
+ export type { ItemBrowsePack, ItemBrowseCoverage, ItemBrowsePlayerFactCoverage, ItemBrowseBuff, ItemBrowseBuffEffect, ItemBrowseBuffStat, ItemBrowseBuffUnit, ItemBrowseCoverageState, ItemBrowseJobFamily, ItemBrowseLevelRange, ItemBrowseRecovery, ItemBrowseRequirement, ItemBrowseRecord, ItemBrowseRelationshipKind, ItemRelatedEntityBrowsePack, ItemRelatedEntityTarget, };
19
20
  export type { OpenReleaseOptions };
20
21
  /** One browse row: the compact record plus its stable public key. */
21
22
  export type ItemBrowseRow = ItemBrowseRecord & {
22
23
  readonly key: string;
23
24
  };
24
- /** The whole verified browse roster of one Item build. */
25
- export interface StaticItemBrowse {
25
+ /** Verified Item Browse 2.0, retained for older pinned products. */
26
+ export interface StaticItemBrowseV2_0 {
27
+ readonly contractVersion: '2.0';
28
+ readonly scope: ProductScope;
29
+ readonly coverage: Omit<ItemBrowseCoverage, 'playerFacts'>;
30
+ readonly records: readonly ItemBrowseRow[];
31
+ }
32
+ /** Verified Item Browse 2.1 with its per-build player-fact coverage. */
33
+ export interface StaticItemBrowseV2_1 {
34
+ readonly contractVersion: '2.1';
26
35
  readonly scope: ProductScope;
27
- readonly coverage: ItemBrowseCoverage;
36
+ readonly coverage: ItemBrowseCoverage & {
37
+ readonly playerFacts: ItemBrowsePlayerFactCoverage;
38
+ };
28
39
  readonly records: readonly ItemBrowseRow[];
29
40
  }
41
+ /** The whole verified browse roster of one Item build. */
42
+ export type StaticItemBrowse = StaticItemBrowseV2_0 | StaticItemBrowseV2_1;
43
+ /** Narrow an Item browse result to contract 2.1, which carries player-fact coverage. */
44
+ export declare function hasItemPlayerFacts(browse: StaticItemBrowse): browse is StaticItemBrowseV2_1;
30
45
  export interface StaticItemProductClient {
31
46
  readonly manifest: StaticProductManifest;
32
47
  loadCatalog(options?: OperationOptions): Promise<StaticItemCatalog>;
33
48
  loadBrowse(options?: OperationOptions): Promise<StaticItemBrowse>;
49
+ loadRelatedEntities(kind: ItemBrowseRelationshipKind, options?: OperationOptions): Promise<ItemRelatedEntityBrowsePack>;
34
50
  getDetail(key: string, options?: OperationOptions): Promise<StaticItemDetail>;
35
51
  getRelationshipPage(pageKey: string, options?: GetRelationshipOptions): Promise<ItemRelationshipPage>;
36
52
  getEvidencePage(pageKey: string, options?: OperationOptions): Promise<ItemEvidencePage>;
@@ -54,5 +70,12 @@ export type GetItemBrowseOptions = OpenReleaseOptions & {
54
70
  * client is imported on demand so the Item browse entry stays narrow.
55
71
  */
56
72
  export declare function getItemBrowse(options: GetItemBrowseOptions): Promise<StaticItemBrowse>;
73
+ /** Options for one verified, one-shot related-entity roster read. */
74
+ export type GetItemRelatedEntitiesOptions = OpenReleaseOptions & {
75
+ readonly buildId: string;
76
+ readonly kind: ItemBrowseRelationshipKind;
77
+ };
78
+ /** Read one build's manifest-pinned roster for exactly one Item relationship kind. */
79
+ export declare function getItemRelatedEntities(options: GetItemRelatedEntitiesOptions): Promise<ItemRelatedEntityBrowsePack>;
57
80
  /** Resolve one Item icon blob URL from its content-addressed media digest. */
58
81
  export declare function itemIconUrl(baseUrl: URL | string, contentId: string): string;
package/client/items.js CHANGED
@@ -1,5 +1,10 @@
1
- import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedCapabilityError, UnsupportedProductError, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, resolveMediaContentUrl, } from './core.js';
1
+ import { MissingPageError, MissingRecordError, SchemaValidationError, ScopeError, UnsupportedCapabilityError, UnsupportedProductError, assertSameScope, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, resolveMediaContentUrl, } from './core.js';
2
+ /** Narrow an Item browse result to contract 2.1, which carries player-fact coverage. */
3
+ export function hasItemPlayerFacts(browse) {
4
+ return browse.contractVersion === '2.1' && browse.coverage.playerFacts !== undefined;
5
+ }
2
6
  const loadBrowsePackValidator = lazyValidator(() => import('../generated/item-browse-pack-2.validator.js'));
7
+ const loadRelatedEntityBrowseValidator = lazyValidator(() => import('../generated/item-related-entity-browse-1.validator.js'));
3
8
  const loadCatalogValidator = lazyValidator(() => import('../generated/item-catalog-2.validator.js'));
4
9
  const loadDetailValidator = lazyValidator(() => import('../generated/item-detail-2.validator.js'));
5
10
  const loadDetailPackValidator = lazyValidator(() => import('../generated/item-pack-2.validator.js'));
@@ -7,7 +12,7 @@ const loadRelationshipPackValidator = lazyValidator(() => import('../generated/i
7
12
  const loadEvidencePackValidator = lazyValidator(() => import('../generated/item-evidence-pack-2.validator.js'));
8
13
  /** Build an Item client over an already verified product manifest. */
9
14
  export function itemProductClient(context) {
10
- const { manifest, fetchEnvelope } = context;
15
+ const { manifest, fetchJson, fetchEnvelope } = context;
11
16
  if (manifest.productId !== 'items')
12
17
  throw new UnsupportedProductError(manifest.productId);
13
18
  let catalogPromise;
@@ -37,9 +42,13 @@ export function itemProductClient(context) {
37
42
  const rows = [];
38
43
  let previousKey;
39
44
  const coverage = packs[0].coverage;
45
+ const contractVersion = packs[0].contractVersion;
40
46
  const coverageJson = JSON.stringify(coverage);
41
47
  for (const [index, pack] of packs.entries()) {
42
48
  const descriptor = descriptors[index];
49
+ if (pack.contractVersion !== contractVersion) {
50
+ throw new SchemaValidationError(descriptor.path, 'browse pack contract version does not match the first browse pack');
51
+ }
43
52
  const keys = Object.keys(pack.records);
44
53
  if (keys.length !== descriptor.count) {
45
54
  throw new SchemaValidationError(descriptor.path, `browse pack has ${keys.length} records but its descriptor declares ${descriptor.count}`);
@@ -68,12 +77,51 @@ export function itemProductClient(context) {
68
77
  if (rows.length !== manifest.totalEntityCount) {
69
78
  throw new SchemaValidationError(manifest.catalog.path, `browse roster has ${rows.length} rows but the manifest declares ${manifest.totalEntityCount}`);
70
79
  }
71
- return { scope: manifest.scope, coverage, records: rows };
80
+ if (contractVersion === '2.1') {
81
+ const playerFacts = coverage.playerFacts;
82
+ if (!playerFacts) {
83
+ throw new SchemaValidationError(descriptors[0].path, 'Item Browse 2.1 is missing player-fact coverage');
84
+ }
85
+ return {
86
+ contractVersion,
87
+ scope: manifest.scope,
88
+ coverage: { ...coverage, playerFacts },
89
+ records: rows,
90
+ };
91
+ }
92
+ if (contractVersion === '2.0') {
93
+ if ((manifest.requiredClientCapabilities ?? []).includes('item-player-facts-v1')) {
94
+ throw new SchemaValidationError(descriptors[0].path, 'Item product requires player facts but the browse pack is contract 2.0');
95
+ }
96
+ const { playerFacts: _playerFacts, ...legacyCoverage } = coverage;
97
+ return { contractVersion, scope: manifest.scope, coverage: legacyCoverage, records: rows };
98
+ }
99
+ throw new SchemaValidationError(descriptors[0].path, `unsupported Item Browse contract ${contractVersion}`);
72
100
  })();
73
101
  void browsePromise.catch(() => { browsePromise = undefined; });
74
102
  }
75
103
  return browsePromise;
76
104
  }
105
+ async function loadRelatedEntities(kind, operation) {
106
+ if (!(manifest.requiredClientCapabilities ?? []).includes('item-related-entities-v1')) {
107
+ throw new UnsupportedCapabilityError('Item product manifest does not require item-related-entities-v1');
108
+ }
109
+ const descriptor = manifest.relatedEntityBrowsePacks?.[kind];
110
+ if (!descriptor) {
111
+ throw new UnsupportedCapabilityError(`Item product manifest does not provide ${kind} related entities`);
112
+ }
113
+ const validator = await loadRelatedEntityBrowseValidator();
114
+ const raw = await fetchJson(descriptor, 'browse', operation);
115
+ if (!validator(raw)) {
116
+ throw new SchemaValidationError(descriptor.path, JSON.stringify(validator.errors ?? []));
117
+ }
118
+ const pack = raw;
119
+ assertSameScope(pack.scope, manifest.scope, descriptor.path);
120
+ if (pack.relationshipKind !== kind) {
121
+ throw new SchemaValidationError(descriptor.path, `related-entity roster kind ${pack.relationshipKind} does not match requested ${kind}`);
122
+ }
123
+ return pack;
124
+ }
77
125
  async function getDetail(key, operation) {
78
126
  const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Item');
79
127
  const packValidator = await loadDetailPackValidator();
@@ -110,7 +158,7 @@ export function itemProductClient(context) {
110
158
  throw new MissingPageError(pageKey, 'evidence');
111
159
  return page;
112
160
  }
113
- return { manifest, loadCatalog, loadBrowse, getDetail, getRelationshipPage, getEvidencePage };
161
+ return { manifest, loadCatalog, loadBrowse, loadRelatedEntities, getDetail, getRelationshipPage, getEvidencePage };
114
162
  }
115
163
  /** Open one pinned Item product. */
116
164
  export async function openItemProduct(options) {
@@ -132,6 +180,13 @@ export async function getItemBrowse(options) {
132
180
  const items = await release.openItemBuild(options.buildId);
133
181
  return items.loadBrowse({ signal: options.signal });
134
182
  }
183
+ /** Read one build's manifest-pinned roster for exactly one Item relationship kind. */
184
+ export async function getItemRelatedEntities(options) {
185
+ const { openRelease } = await import('./release.js');
186
+ const release = await openRelease(options);
187
+ const items = await release.openItemBuild(options.buildId);
188
+ return items.loadRelatedEntities(options.kind, { signal: options.signal });
189
+ }
135
190
  /** Resolve one Item icon blob URL from its content-addressed media digest. */
136
191
  export function itemIconUrl(baseUrl, contentId) {
137
192
  return resolveMediaContentUrl(baseUrl, contentId, 'Item icon');
@@ -0,0 +1,4 @@
1
+ export type { MapGeographyFact, MapGeographyProvenance, MapGeographyReviewStatus, } from '../generated/map-catalog-2-1.d.ts';
2
+ export type { MapGeographyArea, MapGeographyCitation, MapGeographyCollection, MapGeographyEntryPoint, MapGeographyModel, MapGeographyRegion, MapGeographyReviewedPlacement, MapGeographyWitnessCount, } from '../generated/map-geography.d.ts';
3
+ export { default as validateMapCatalogV2_1 } from '../generated/map-catalog-2-1.validator.js';
4
+ export { default as validateMapGeography } from '../generated/map-geography.validator.js';
@@ -0,0 +1,2 @@
1
+ export { default as validateMapCatalogV2_1 } from '../generated/map-catalog-2-1.validator.js';
2
+ export { default as validateMapGeography } from '../generated/map-geography.validator.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `@mapletip/data/maps` — verified Map geography and Map catalog reads.
3
+ */
4
+ import { isMapGeographyModel, type MapGeographyModel, type StaticDomainProductClient } from './static-domains.js';
5
+ import { type OpenProductOptions, type StaticProductManifest } from './core.js';
6
+ import { type OpenReleaseOptions } from './release.js';
7
+ export { isMapGeographyModel };
8
+ export type { MapGeographyModel, StaticDomainProductClient };
9
+ export type StaticMapProductClient = Omit<StaticDomainProductClient, 'manifest'> & {
10
+ readonly manifest: StaticProductManifest & {
11
+ readonly productId: 'maps';
12
+ };
13
+ };
14
+ /** Open a pinned Maps domain product directly. */
15
+ export declare function openMapProduct(options: OpenProductOptions): Promise<StaticMapProductClient>;
16
+ export declare function isMapProductClient(product: StaticDomainProductClient): product is StaticMapProductClient;
17
+ /** Options for the one-shot, manifest-pinned geography model read. */
18
+ export type GetMapGeographyOptions = OpenReleaseOptions & {
19
+ readonly buildId: string;
20
+ };
21
+ /** Read the selected build's digest-verified Map geography model. */
22
+ export declare function getMapGeography(options: GetMapGeographyOptions): Promise<MapGeographyModel>;
package/client/maps.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `@mapletip/data/maps` — verified Map geography and Map catalog reads.
3
+ */
4
+ import { isMapGeographyModel, } from './static-domains.js';
5
+ import { UnsupportedProductError, } from './core.js';
6
+ import { openStaticDomainProduct } from './static-domains.js';
7
+ import { openRelease } from './release.js';
8
+ export { isMapGeographyModel };
9
+ /** Open a pinned Maps domain product directly. */
10
+ export async function openMapProduct(options) {
11
+ const product = await openStaticDomainProduct(options);
12
+ if (product.manifest.productId !== 'maps') {
13
+ throw new UnsupportedProductError(product.manifest.productId);
14
+ }
15
+ return product;
16
+ }
17
+ export function isMapProductClient(product) {
18
+ return product.manifest.productId === 'maps';
19
+ }
20
+ /** Read the selected build's digest-verified Map geography model. */
21
+ export async function getMapGeography(options) {
22
+ const release = await openRelease(options);
23
+ const maps = await release.openDomainBuild(options.buildId, 'maps');
24
+ return maps.loadGeography({ signal: options.signal });
25
+ }
@@ -9,6 +9,11 @@
9
9
  import type { MonsterCatalog } from '../generated/monster-catalog.d.ts';
10
10
  import type { MonsterCatalogV2 } from '../generated/monster-catalog-2.d.ts';
11
11
  import type { MonsterCatalogV2_1 } from '../generated/monster-catalog-2-1.d.ts';
12
+ import type { MonsterCatalogV2_2 } from '../generated/monster-catalog-2-2.d.ts';
13
+ import type { MonsterCatalogV2_3 } from '../generated/monster-catalog-2-3.d.ts';
14
+ import type { MonsterDropBrowseV1 } from '../generated/monster-drop-browse.d.ts';
15
+ import type { MonsterLocationBrowseV1 } from '../generated/monster-location-browse.d.ts';
16
+ import type { MonsterLocationBrowseV1_1 } from '../generated/monster-location-browse-1-1.d.ts';
12
17
  import type { MonsterDetailRecord, PageDescriptor } from '../generated/monster-detail.d.ts';
13
18
  import type { MonsterDetailRecordV2 } from '../generated/monster-detail-2.d.ts';
14
19
  import type { MonsterEvidencePage } from '../generated/monster-evidence-pack.d.ts';
@@ -17,15 +22,18 @@ import type { MonsterLocationOccurrence } from '../generated/monster-relationshi
17
22
  import type { MonsterCoreRelationshipPackV2 } from '../generated/monster-core-relationship-pack-2.d.ts';
18
23
  import { type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
19
24
  /**
20
- * A verified Monster catalog. Contract 2.1 records carry `sprite`, a Media content ID
21
- * fact for the monster's representative image; 2.0 and 1.x records have none.
25
+ * A verified Monster catalog. Contracts 2.1 and 2.2 carry the Media content ID fact
26
+ * for a monster's representative image; 2.0 and 1.x records have none.
22
27
  */
23
- export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1;
28
+ export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1 | MonsterCatalogV2_2 | MonsterCatalogV2_3;
29
+ export type StaticMonsterLocationBrowse = MonsterLocationBrowseV1 | MonsterLocationBrowseV1_1;
24
30
  export type StaticMonsterDetail = MonsterDetailRecord | MonsterDetailRecordV2;
25
31
  export type StaticMonsterEvidencePage = MonsterEvidencePage | MonsterEvidencePageV2;
26
32
  export interface StaticProductClient {
27
33
  readonly manifest: StaticProductManifest;
28
34
  loadCatalog(options?: OperationOptions): Promise<StaticMonsterCatalog>;
35
+ loadDropBrowse(options?: OperationOptions): Promise<MonsterDropBrowseV1>;
36
+ loadLocationBrowse(options?: OperationOptions): Promise<StaticMonsterLocationBrowse>;
29
37
  getDetail(key: string, options?: OperationOptions): Promise<StaticMonsterDetail>;
30
38
  getRelationshipPage(page: PageDescriptor, options?: OperationOptions): Promise<MonsterLocationOccurrence[]>;
31
39
  getEvidencePage(page: PageDescriptor, options?: OperationOptions): Promise<StaticMonsterEvidencePage>;
@@ -1,15 +1,25 @@
1
1
  import validateCatalog from '../generated/monster-catalog.validator.js';
2
2
  import validateCatalogV2 from '../generated/monster-catalog-2.validator.js';
3
3
  import validateCatalogV2_1 from '../generated/monster-catalog-2-1.validator.js';
4
- import { MissingPageError, MissingRecordError, SchemaValidationError, UnsupportedProductError, assertEnvelopeVersion, assertSameScope, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, } from './core.js';
4
+ import validateCatalogV2_2 from '../generated/monster-catalog-2-2.validator.js';
5
+ import validateCatalogV2_3 from '../generated/monster-catalog-2-3.validator.js';
6
+ import validateDropBrowse from '../generated/monster-drop-browse.validator.js';
7
+ import validateLocationBrowse from '../generated/monster-location-browse.validator.js';
8
+ import validateLocationBrowseV1_1 from '../generated/monster-location-browse-1-1.validator.js';
9
+ import { MissingPageError, MissingRecordError, SchemaValidationError, UnsupportedCapabilityError, UnsupportedProductError, assertEnvelopeVersion, assertSameScope, lazyValidator, memoized, openProductContext, rangeDescriptor, recordRangeDescriptor, } from './core.js';
5
10
  /**
6
11
  * Contract-2 catalog validator that picks the revision's strict schema from the document's
7
12
  * own `contractVersion`. Unsupported revisions fail the 2.0 schema with a typed error.
8
13
  */
9
14
  const validateCatalogRevision = Object.assign((data) => {
10
- const validator = (data?.contractVersion === '2.1'
11
- ? validateCatalogV2_1
12
- : validateCatalogV2);
15
+ const version = data?.contractVersion;
16
+ const validator = version === '2.3'
17
+ ? validateCatalogV2_3
18
+ : version === '2.2'
19
+ ? validateCatalogV2_2
20
+ : version === '2.1'
21
+ ? validateCatalogV2_1
22
+ : validateCatalogV2;
13
23
  const valid = validator(data);
14
24
  validateCatalogRevision.errors = validator.errors;
15
25
  return valid;
@@ -23,9 +33,16 @@ const loadLocationsV2 = lazyValidator(() => import('../generated/monster-locatio
23
33
  const loadCoreRelationshipsV2 = lazyValidator(() => import('../generated/monster-core-relationship-pack-2.validator.js'));
24
34
  const loadEvidence = lazyValidator(() => import('../generated/monster-evidence-pack.validator.js'));
25
35
  const loadEvidenceV2 = lazyValidator(() => import('../generated/monster-evidence-pack-2.validator.js'));
36
+ const validateLocationBrowseRevision = Object.assign((data) => {
37
+ const version = data?.contractVersion;
38
+ const validator = version === '1.1' ? validateLocationBrowseV1_1 : validateLocationBrowse;
39
+ const valid = validator(data);
40
+ validateLocationBrowseRevision.errors = validator.errors;
41
+ return valid;
42
+ }, { errors: undefined });
26
43
  /** Build a Monster client over an already verified product manifest. */
27
44
  export function monsterProductClient(context) {
28
- const { manifest, contract2, fetchEnvelope } = context;
45
+ const { manifest, contract2, fetchJson, fetchEnvelope } = context;
29
46
  if (manifest.productId !== 'monsters')
30
47
  throw new UnsupportedProductError(manifest.productId);
31
48
  let catalogPromise;
@@ -49,6 +66,26 @@ export function monsterProductClient(context) {
49
66
  }
50
67
  return catalog;
51
68
  }
69
+ async function loadBrowseArtifact(descriptor, capability, validator, label, operation) {
70
+ if (!(manifest.requiredClientCapabilities ?? []).includes(capability)) {
71
+ throw new UnsupportedCapabilityError('Monster product manifest does not require ' + capability);
72
+ }
73
+ if (!descriptor) {
74
+ throw new UnsupportedCapabilityError('Monster product manifest does not provide ' + label);
75
+ }
76
+ const raw = await fetchJson(descriptor, 'browse', operation);
77
+ if (!validator(raw)) {
78
+ throw new SchemaValidationError(descriptor.path, JSON.stringify(validator.errors ?? []));
79
+ }
80
+ assertSameScope(raw.scope, manifest.scope, descriptor.path);
81
+ return raw;
82
+ }
83
+ async function loadDropBrowse(operation) {
84
+ return loadBrowseArtifact(manifest.dropBrowse, 'monster-drop-browse-v1', validateDropBrowse, 'drop browse artifact', operation);
85
+ }
86
+ async function loadLocationBrowse(operation) {
87
+ return loadBrowseArtifact(manifest.locationBrowse, 'monster-location-browse-v1', validateLocationBrowseRevision, 'location browse artifact', operation);
88
+ }
52
89
  async function getDetail(key, operation) {
53
90
  const descriptor = recordRangeDescriptor(manifest.detailPacks, key, 'detail', 'Monster');
54
91
  if (contract2) {
@@ -153,6 +190,8 @@ export function monsterProductClient(context) {
153
190
  return {
154
191
  manifest,
155
192
  loadCatalog,
193
+ loadDropBrowse,
194
+ loadLocationBrowse,
156
195
  getDetail,
157
196
  getRelationshipPage,
158
197
  getEvidencePage,