@mapletip/data 0.2.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 (43) hide show
  1. package/README.md +204 -2
  2. package/client/contract-compatibility.mjs +10 -0
  3. package/client/core.d.ts +5 -0
  4. package/client/core.js +10 -0
  5. package/client/items.d.ts +28 -5
  6. package/client/items.js +60 -8
  7. package/client/map-geography.d.ts +4 -0
  8. package/client/map-geography.js +2 -0
  9. package/client/maps.d.ts +22 -0
  10. package/client/maps.js +25 -0
  11. package/client/monster-client.d.ts +14 -1
  12. package/client/monster-client.js +55 -3
  13. package/client/monsters.d.ts +37 -8
  14. package/client/monsters.js +51 -0
  15. package/client/static-client.d.ts +7 -5
  16. package/client/static-client.js +3 -2
  17. package/client/static-domains.d.ts +8 -1
  18. package/client/static-domains.js +27 -3
  19. package/generated/item-browse-pack-2.d.ts +83 -1
  20. package/generated/item-browse-pack-2.validator.js +2 -2
  21. package/generated/item-related-entity-browse-1.d.ts +48 -0
  22. package/generated/item-related-entity-browse-1.validator.js +1 -0
  23. package/generated/map-catalog-2-1.d.ts +103 -0
  24. package/generated/map-catalog-2-1.validator.d.ts +18 -0
  25. package/generated/map-catalog-2-1.validator.js +1 -0
  26. package/generated/map-geography.d.ts +79 -0
  27. package/generated/map-geography.validator.d.ts +18 -0
  28. package/generated/map-geography.validator.js +1 -0
  29. package/generated/monster-catalog-2-1.d.ts +253 -0
  30. package/generated/monster-catalog-2-1.validator.js +17 -0
  31. package/generated/monster-catalog-2-2.d.ts +332 -0
  32. package/generated/monster-catalog-2-2.validator.js +17 -0
  33. package/generated/monster-catalog-2-3.d.ts +339 -0
  34. package/generated/monster-catalog-2-3.validator.js +17 -0
  35. package/generated/monster-drop-browse.d.ts +123 -0
  36. package/generated/monster-drop-browse.validator.js +17 -0
  37. package/generated/monster-location-browse-1-1.d.ts +79 -0
  38. package/generated/monster-location-browse-1-1.validator.js +17 -0
  39. package/generated/monster-location-browse.d.ts +69 -0
  40. package/generated/monster-location-browse.validator.js +17 -0
  41. package/generated/product-manifest.d.ts +23 -0
  42. package/generated/product-manifest.validator.js +1 -1
  43. package/package.json +38 -2
package/README.md CHANGED
@@ -55,6 +55,124 @@ existing typed failure (`UNKNOWN_BUILD`, `UNSUPPORTED_PRODUCT`, `SCOPE_ERROR`,
55
55
  Supported builds are `cw-launch` (`classic-world`) and `gms-v83` (`gms`). When several
56
56
  reads share one release, keep using `openRelease` and reuse the opened release client.
57
57
 
58
+ ### Monster sprites
59
+
60
+ Monster catalog contract `2.1` gives every row a `sprite` fact: when `known`, its value
61
+ is the content ID (PNG SHA-256) of the `monster.sprite` image that the same build's Media
62
+ product selects. `monsterSpriteUrl` turns it into the blob URL under the release root, so
63
+ a browse page needs no Media catalog, Media detail pack or second product:
64
+
65
+ ```ts
66
+ import { getMonsterBrowse, hasMonsterSprites, monsterSpriteUrl } from '@mapletip/data/monsters';
67
+
68
+ const catalog = await getMonsterBrowse({ baseUrl: releaseRootUrl, pin: releasePin, buildId: 'gms-v83' });
69
+ if (!hasMonsterSprites(catalog)) throw new Error('this pin predates Monster sprites');
70
+ const rows = catalog.records.map((row) => ({
71
+ key: row.key,
72
+ spriteUrl: row.sprite.state === 'known' ? monsterSpriteUrl(releaseRootUrl, row.sprite.value) : undefined,
73
+ }));
74
+ ```
75
+
76
+ The `sprite` state mirrors Media exactly: `unknown` (for example, a monster with only
77
+ stub frames), `absent` with its coverage key, or `unsupported`. Never substitute a
78
+ placeholder or another build's image for a non-known state. Release assembly fails unless
79
+ every row agrees with Media and every known content ID names a blob in the release.
80
+ `monsterSpriteUrl` throws `SCHEMA_VALIDATION_ERROR` for anything but a lowercase 64-hex
81
+ digest. `getMonsterBrowse` still requests only the release manifest named by the pin, the
82
+ build index, the product manifest and the Monster catalog.
83
+
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 revisions `2.1` or `2.2`. `@mapletip/data`
86
+ `0.2.0` and older reject a `2.1` catalog with a typed `SCHEMA_VALIDATION_ERROR` instead of
87
+ returning rows without sprites, so upgrade to `0.3.0` before adopting a pin that carries
88
+ it.
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
+
58
176
  `pin` is the parsed `pin.json` published next to the release root by whoever assembled
59
177
  the release: it names the manifest path, SHA-256 and byte length and is the caller's
60
178
  trust anchor. The SDK never resolves a "current" release on its own and never fetches a
@@ -113,6 +231,66 @@ It rejects any content ID that is not a 64-character lowercase SHA-256 digest wi
113
231
  `SCHEMA_VALIDATION_ERROR`, so a row's `icon.value` can be used without building a
114
232
  make-believe descriptor.
115
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
+
116
294
  ## What each entry loads
117
295
 
118
296
  Monster catalog validation and the release/manifest/index validators are imported
@@ -121,8 +299,8 @@ operation that needs it, as a separate on-demand chunk:
121
299
 
122
300
  | Entry | Eager generated validators | Loaded on demand |
123
301
  | --- | --- | --- |
124
- | `/monsters` | release pin, release manifest, build index, product manifest, Monster catalog 1.x and 2.0 | Monster detail, location/relationship, core-relationship and evidence packs |
125
- | `/items` | product manifest only (every Item validator is on demand) | browse, catalog, detail, relationship and evidence packs, plus the release client behind `getItemBrowse` |
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 |
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` |
126
304
  | `/equipment` | that domain's catalog, detail, relationship and evidence packs | — |
127
305
  | `/quests` | none (Quest validators are always on demand) | catalog, browse, detail, relationship, auxiliary and evidence packs |
128
306
  | `/media` | that domain's catalog, detail packs | — |
@@ -256,3 +434,27 @@ const evPage = await client.getEvidencePage('quest-ev-1000');
256
434
 
257
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.
258
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/core.d.ts CHANGED
@@ -120,6 +120,11 @@ export declare function normalizedBaseUrl(value: string | URL): URL;
120
120
  export declare function verifyBlob(bytes: Uint8Array, descriptor: IntegrityDescriptor): Promise<void>;
121
121
  export declare function resolveReleasePath(baseUrl: URL | string, path: string): string;
122
122
  export declare function resolveMediaUrl(baseUrl: URL | string, descriptor: IntegrityDescriptor): string;
123
+ /**
124
+ * Resolve a Media content ID (the PNG's lowercase SHA-256 digest) to its blob URL under
125
+ * the release root. `subject` names the caller's field in the validation error.
126
+ */
127
+ export declare function resolveMediaContentUrl(baseUrl: URL | string, contentId: string, subject: string): string;
123
128
  export declare function assertCompatibleManifest(manifest: ConnectedProductManifest | ReleaseManifest): number;
124
129
  export declare function createJsonFetcher(options: {
125
130
  readonly baseUrl: URL;
package/client/core.js CHANGED
@@ -127,6 +127,16 @@ export function resolveReleasePath(baseUrl, path) {
127
127
  export function resolveMediaUrl(baseUrl, descriptor) {
128
128
  return resolveReleasePath(baseUrl, descriptor.path);
129
129
  }
130
+ /**
131
+ * Resolve a Media content ID (the PNG's lowercase SHA-256 digest) to its blob URL under
132
+ * the release root. `subject` names the caller's field in the validation error.
133
+ */
134
+ export function resolveMediaContentUrl(baseUrl, contentId, subject) {
135
+ if (!/^[0-9a-f]{64}$/.test(contentId)) {
136
+ throw new SchemaValidationError(contentId, `${subject} content ID must be a SHA-256 hex digest`);
137
+ }
138
+ return resolveReleasePath(baseUrl, `media/images/${contentId.slice(0, 2)}/${contentId}.png`);
139
+ }
130
140
  function parseJson(bytes, path) {
131
141
  try {
132
142
  return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
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, resolveReleasePath, } 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,10 +180,14 @@ 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
- if (!/^[0-9a-f]{64}$/.test(contentId)) {
138
- throw new SchemaValidationError(contentId, 'Item icon content ID must be a SHA-256 hex digest');
139
- }
140
- return resolveReleasePath(baseUrl, `media/images/${contentId.slice(0, 2)}/${contentId}.png`);
192
+ return resolveMediaContentUrl(baseUrl, contentId, 'Item icon');
141
193
  }
@@ -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
+ }
@@ -8,6 +8,12 @@
8
8
  */
9
9
  import type { MonsterCatalog } from '../generated/monster-catalog.d.ts';
10
10
  import type { MonsterCatalogV2 } from '../generated/monster-catalog-2.d.ts';
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';
11
17
  import type { MonsterDetailRecord, PageDescriptor } from '../generated/monster-detail.d.ts';
12
18
  import type { MonsterDetailRecordV2 } from '../generated/monster-detail-2.d.ts';
13
19
  import type { MonsterEvidencePage } from '../generated/monster-evidence-pack.d.ts';
@@ -15,12 +21,19 @@ import type { MonsterEvidencePageV2 } from '../generated/monster-evidence-pack-2
15
21
  import type { MonsterLocationOccurrence } from '../generated/monster-relationship-pack.d.ts';
16
22
  import type { MonsterCoreRelationshipPackV2 } from '../generated/monster-core-relationship-pack-2.d.ts';
17
23
  import { type OpenProductOptions, type OperationOptions, type StaticProductManifest, type VerifiedProductContext } from './core.js';
18
- export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2;
24
+ /**
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.
27
+ */
28
+ export type StaticMonsterCatalog = MonsterCatalog | MonsterCatalogV2 | MonsterCatalogV2_1 | MonsterCatalogV2_2 | MonsterCatalogV2_3;
29
+ export type StaticMonsterLocationBrowse = MonsterLocationBrowseV1 | MonsterLocationBrowseV1_1;
19
30
  export type StaticMonsterDetail = MonsterDetailRecord | MonsterDetailRecordV2;
20
31
  export type StaticMonsterEvidencePage = MonsterEvidencePage | MonsterEvidencePageV2;
21
32
  export interface StaticProductClient {
22
33
  readonly manifest: StaticProductManifest;
23
34
  loadCatalog(options?: OperationOptions): Promise<StaticMonsterCatalog>;
35
+ loadDropBrowse(options?: OperationOptions): Promise<MonsterDropBrowseV1>;
36
+ loadLocationBrowse(options?: OperationOptions): Promise<StaticMonsterLocationBrowse>;
24
37
  getDetail(key: string, options?: OperationOptions): Promise<StaticMonsterDetail>;
25
38
  getRelationshipPage(page: PageDescriptor, options?: OperationOptions): Promise<MonsterLocationOccurrence[]>;
26
39
  getEvidencePage(page: PageDescriptor, options?: OperationOptions): Promise<StaticMonsterEvidencePage>;