@onlyworlds/sdk 4.10.0 → 4.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -0
- package/CHANGELOG.md +40 -0
- package/README.md +28 -0
- package/dist/index.d.ts +254 -1
- package/dist/index.js +474 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -40,6 +40,14 @@ against live data). The v1 surface (`OnlyWorldsClient`, the `ElementType` enum)
|
|
|
40
40
|
- Images: `uploadImage(bytes)` returns `{ url, ... }`; set `url` as `image_url`. The bytes go
|
|
41
41
|
to the edge with a single-use ticket from keel, never with the API key. `removeImage(urlOrKey)`
|
|
42
42
|
takes one back (its uploader or the world's owner); it leaves `image_url` on elements as is.
|
|
43
|
+
- Invite links (since 4.12.0): `previewJoin(code)` (no credential sent) shows who invites, to
|
|
44
|
+
which world, in what role; `acceptJoin(code)` with an `ow_a_` account token joins. Bad code:
|
|
45
|
+
`err.isNotFound`; `err.isAlreadyMember`, `err.isPinRequired` for the two 409s.
|
|
46
|
+
- Lookup without a model (since 4.11.0): `buildWorldIndex(elements)` then
|
|
47
|
+
`lookupWorld(index, question)` ranks the elements a question is about (names, then kind, one
|
|
48
|
+
link step, then text); `renderElement(el, index)` is a compact card with links by name.
|
|
49
|
+
- Photos (browser only, since 4.11.0): `preparePhoto(blob)` shrinks to 2048 px and converts to
|
|
50
|
+
webp (JPEG where webp can't be encoded) before `uploadImage`.
|
|
43
51
|
- World files (no account needed; since 4.10.0): `readWorldFile(bytes)` opens a `.zip` holding one
|
|
44
52
|
world folder and returns `{ worldId, name, world, files: [{ path, data }] }`;
|
|
45
53
|
`writeWorldFile(files, topDir)` makes one. Refusals throw `OwWorldFileError` with a `reason`.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ earlier history lives in git log only.
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [4.12.0] — 2026-10-12
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Joining a world by link (keel #212).** `previewJoin(code)` reads what a join code holds
|
|
12
|
+
without using it (`OwJoinPreview`: `reader`, `world`, `invited_by`, `owner_character`,
|
|
13
|
+
`role`, `with_character`, `expires_at`) and sends no credential, whatever key the client
|
|
14
|
+
holds. `acceptJoin(code)` accepts an invite link as the account behind the client's `ow_a_`
|
|
15
|
+
token and returns the new membership (`OwJoinedMembership`, `roster_character` included);
|
|
16
|
+
a world key is a `TypeError` before anything is sent. New getters on `OwApiError`:
|
|
17
|
+
`isNotFound` (every bad code is one 404), `isAlreadyMember` and `isPinRequired` (the two
|
|
18
|
+
409s, told apart by code). Shapes read from keel's code (`accounts/join_api.py`,
|
|
19
|
+
`accounts/api.py`), since the OpenAPI document gives no response bodies for these routes.
|
|
20
|
+
Checked live: a bad code on production answers `isNotFound`. A real accept has not been
|
|
21
|
+
run from here (no account token); Lantern's door is its first live use.
|
|
22
|
+
|
|
23
|
+
## [4.11.0] — 2026-10-12
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **World lookup.** `buildWorldIndex(elements)`, `lookupWorld(index, question, { limit })` and
|
|
27
|
+
`renderElement(element, index)` find the elements a plain-language question is about, in a
|
|
28
|
+
world held in memory: no model, no network, a few milliseconds. Ranking: names and short
|
|
29
|
+
forms first, then name words, supertype and subtype, one link step in both directions, then
|
|
30
|
+
other text and numbers (always below any name hit). Each hit says why it matched.
|
|
31
|
+
`renderElement` gives a compact text card with links by name, which is what a model reads
|
|
32
|
+
when one phrases the answer. Measured on two real worlds (Assembly's Lantern Ask trial):
|
|
33
|
+
the answer was in the first three results for 60 of 60 questions that name their subject
|
|
34
|
+
and 17 of 24 held-out questions that only describe it; this code returns the same top 8
|
|
35
|
+
as the trial's on all 100 questions. Names in any script are matched (Chinese and Japanese
|
|
36
|
+
only where spaces or punctuation separate the words: there is no word splitting). The question words
|
|
37
|
+
that steer the link step ("where", "rules", "allies") are English.
|
|
38
|
+
- **Photo preparation.** `preparePhoto(blob, { maxSide, quality, maxBytes })` shrinks and
|
|
39
|
+
converts a picture in the browser before `uploadImage`: longest side 2048 by default, webp at
|
|
40
|
+
quality 0.82, from the upload page's converter. A browser that cannot encode webp returns a
|
|
41
|
+
PNG when asked; the result's real type is checked, and a photo then falls back to JPEG while a
|
|
42
|
+
PNG stays PNG. When re-encoding would only make a file bigger, the original is kept.
|
|
43
|
+
`planPhoto` is the pure part (sizes and type order). Walked in Chromium, WebKit and Firefox
|
|
44
|
+
(a 4032×3024 photo became a 2048-wide webp of about 0.5 MB) and with webp encoding forced off.
|
|
45
|
+
Needs a browser; in Node it throws a `TypeError` that says so. `PHOTO_TYPES`,
|
|
46
|
+
`PHOTO_MAX_BYTES` are keel's accepted types and its 15 MB limit.
|
|
47
|
+
|
|
8
48
|
## [4.10.0] — 2026-10-11
|
|
9
49
|
|
|
10
50
|
### Added
|
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ const image = await writer.uploadImage(file); // a Blob, File, ArrayBuffer or Ui
|
|
|
99
99
|
await writer.patch('character', id, { image_url: image.url });
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
In a browser, shrink a phone photo first: `const photo = await preparePhoto(file)` gives a webp (or JPEG where the browser can't encode webp) of at most 2048 px on its longest side; upload `photo.blob`.
|
|
103
|
+
|
|
102
104
|
The SDK asks the API for a single-use ticket, then sends the bytes straight to the upload host, which never sees your key. webp, png, jpeg or avif (no SVG or gif), up to 15 MB. Each upload counts toward the world's daily limit and the account's storage. To upload yourself (for a progress bar), call `createMediaTicket()` and POST the bytes to its `upload_url` with `Authorization: Bearer <ticket>`.
|
|
103
105
|
|
|
104
106
|
```typescript
|
|
@@ -108,6 +110,17 @@ await writer.patch('character', id, { image_url: null });
|
|
|
108
110
|
|
|
109
111
|
The image's uploader or the world's owner can remove an uploaded image; the space goes back to the uploader's storage. Removal leaves elements as they are, so clear their `image_url` yourself (`createRemovalTicket(key).referenced` says how many still show it). A copy the network already holds can keep answering for a while.
|
|
110
112
|
|
|
113
|
+
## Invite links
|
|
114
|
+
|
|
115
|
+
A world's owner can invite a person with a link (`https://www.onlyworlds.com/join#ow_j_…`). The code is the part after `#`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
const invite = await client.previewJoin(code); // who invites, which world, what role; sends no key
|
|
119
|
+
const membership = await account.acceptJoin(code); // account = a client with an ow_a_ token
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A bad code (unknown, used, revoked, expired) is one 404 (`err.isNotFound`). Accepting can answer `err.isAlreadyMember` or `err.isPinRequired` (set a PIN first); a refusal never uses up the code.
|
|
123
|
+
|
|
111
124
|
## Sync
|
|
112
125
|
|
|
113
126
|
```typescript
|
|
@@ -135,6 +148,21 @@ try {
|
|
|
135
148
|
|
|
136
149
|
The check is per element and the server never merges. Compare the copy you edited from with `err.current`: when the fields you changed are not the ones that moved, send again with `ifMatch: err.current.change_seq`; when the same field moved on both sides, ask the user which to keep. Without `ifMatch` a write is unconditional, as before.
|
|
137
150
|
|
|
151
|
+
## Finding things in a world
|
|
152
|
+
|
|
153
|
+
When you hold a world's elements in memory (from a sync, or a world file), `lookupWorld` finds the ones a plain question is about, with no model and no network:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { buildWorldIndex, lookupWorld, renderElement } from '@onlyworlds/sdk';
|
|
157
|
+
|
|
158
|
+
const index = buildWorldIndex(elements); // once per world; each element needs id and type
|
|
159
|
+
const hits = lookupWorld(index, 'who runs the mill in Ostvik?');
|
|
160
|
+
hits[0].element; hits[0].reasons; // best first, with why each matched
|
|
161
|
+
renderElement(hits[0].element, index); // a short text card, links by name
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Names come first, then supertype and subtype, then one link step in either direction, then the elements' other text. To have a model phrase an answer, give it the cards of the top hits and ask it to answer only from them.
|
|
165
|
+
|
|
138
166
|
## World files
|
|
139
167
|
|
|
140
168
|
A world file is one `.zip` holding a whole world folder (`world.json` and `elements/<type>/*.json`, as Atlas keeps a world on disk), the way Atlas downloads and opens one. It needs no account and works in the browser and in Node.
|
package/dist/index.d.ts
CHANGED
|
@@ -1988,6 +1988,37 @@ interface OwRemovalTicket {
|
|
|
1988
1988
|
referenced: number;
|
|
1989
1989
|
}
|
|
1990
1990
|
/** The edge's 200 for one removal. */
|
|
1991
|
+
/** POST /join/preview: what a join code holds, read without using it (keel #212). */
|
|
1992
|
+
interface OwJoinPreview {
|
|
1993
|
+
/** `person`: an invite link, accepted with acceptJoin. `agent`: redeemed by an AI agent at POST /join. */
|
|
1994
|
+
reader: 'person' | 'agent' | (string & {});
|
|
1995
|
+
world: {
|
|
1996
|
+
name: string;
|
|
1997
|
+
description: string | null;
|
|
1998
|
+
};
|
|
1999
|
+
/** The name the inviter signed the link with, or null. */
|
|
2000
|
+
invited_by: string | null;
|
|
2001
|
+
/** The world owner's chosen Character's name, never a username; null when none is set. */
|
|
2002
|
+
owner_character: string | null;
|
|
2003
|
+
role: 'co-builder' | 'contributor' | 'guest' | (string & {});
|
|
2004
|
+
/** Whether joining gives the joiner a Character of their own (agent links). */
|
|
2005
|
+
with_character: boolean | null;
|
|
2006
|
+
/** ISO 8601, UTC. */
|
|
2007
|
+
expires_at: string;
|
|
2008
|
+
[k: string]: unknown;
|
|
2009
|
+
}
|
|
2010
|
+
/** POST /account/join: the new membership (the same row as accepting an email invite). */
|
|
2011
|
+
interface OwJoinedMembership {
|
|
2012
|
+
/** The membership's id. */
|
|
2013
|
+
id: string;
|
|
2014
|
+
world_id: string;
|
|
2015
|
+
world_name: string;
|
|
2016
|
+
role: 'co-builder' | 'contributor' | 'guest' | (string & {});
|
|
2017
|
+
joined_at: string | null;
|
|
2018
|
+
/** The id of the joiner's roster Character, or null. */
|
|
2019
|
+
roster_character: string | null;
|
|
2020
|
+
[k: string]: unknown;
|
|
2021
|
+
}
|
|
1991
2022
|
interface OwRemovedImage {
|
|
1992
2023
|
/** The object key that was removed. */
|
|
1993
2024
|
removed: string;
|
|
@@ -2139,6 +2170,12 @@ declare class OwApiError extends Error {
|
|
|
2139
2170
|
* is now, so no second read is needed.
|
|
2140
2171
|
*/
|
|
2141
2172
|
get isPreconditionFailed(): boolean;
|
|
2173
|
+
/** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
|
|
2174
|
+
get isNotFound(): boolean;
|
|
2175
|
+
/** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
|
|
2176
|
+
get isAlreadyMember(): boolean;
|
|
2177
|
+
/** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
|
|
2178
|
+
get isPinRequired(): boolean;
|
|
2142
2179
|
}
|
|
2143
2180
|
/**
|
|
2144
2181
|
* 412 `precondition_failed` (keel #210): the element changed since the `change_seq` sent as
|
|
@@ -2368,6 +2405,23 @@ declare class OwV2Client {
|
|
|
2368
2405
|
removeImage(keyOrUrl: string, opts?: {
|
|
2369
2406
|
ticket?: OwRemovalTicket;
|
|
2370
2407
|
}): Promise<OwRemovedImage>;
|
|
2408
|
+
/**
|
|
2409
|
+
* POST /join/preview -- what a join code holds, without using it: who invites, to
|
|
2410
|
+
* which world, in what role, and `reader` (`person`: accept it with acceptJoin;
|
|
2411
|
+
* `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
|
|
2412
|
+
* this client holds. Every bad code (unknown, used, revoked, expired) is the same
|
|
2413
|
+
* 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
|
|
2414
|
+
*/
|
|
2415
|
+
previewJoin(code: string): Promise<OwJoinPreview>;
|
|
2416
|
+
/**
|
|
2417
|
+
* POST /account/join -- accept an invite link's code as the account this client's
|
|
2418
|
+
* `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
|
|
2419
|
+
* (every bad code, and a code made for an AI agent), 409 `already_member`
|
|
2420
|
+
* (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
|
|
2421
|
+
* has no PIN yet). A refusal never spends the code. A world key here is a TypeError
|
|
2422
|
+
* before anything is sent.
|
|
2423
|
+
*/
|
|
2424
|
+
acceptJoin(code: string): Promise<OwJoinedMembership>;
|
|
2371
2425
|
/**
|
|
2372
2426
|
* Raw authenticated request against this client's baseUrl. Public since 4.0
|
|
2373
2427
|
* so auxiliary resources can ride the same transport — it structurally
|
|
@@ -2526,6 +2580,205 @@ declare function readWorldFile(bytes: Uint8Array, opts?: OwReadWorldFileOptions)
|
|
|
2526
2580
|
*/
|
|
2527
2581
|
declare function writeWorldFile(files: OwWorldFileEntry[], topDir: string): Uint8Array;
|
|
2528
2582
|
|
|
2583
|
+
/**
|
|
2584
|
+
* World lookup: find the elements a plain-language question is about, in a
|
|
2585
|
+
* world held in memory, with no model and no network. Ported from Assembly's
|
|
2586
|
+
* Lantern Ask trial (`dossiers/lantern-ask/lookup.mjs`, round 2), with the
|
|
2587
|
+
* same ranking, so the trial's measurements on two real worlds describe this
|
|
2588
|
+
* code. Pure: runs in a browser and in Node alike.
|
|
2589
|
+
*
|
|
2590
|
+
* const index = buildWorldIndex(elements); // API elements or a world folder's files
|
|
2591
|
+
* const hits = lookupWorld(index, question); // ranked, best first, each with its reasons
|
|
2592
|
+
* const text = renderElement(hits[0].element, index); // a compact text card, links by name
|
|
2593
|
+
*
|
|
2594
|
+
* Each element needs `id` and `type` (a world folder's files may lack `type`:
|
|
2595
|
+
* set it from the folder name), and usually `name`.
|
|
2596
|
+
*
|
|
2597
|
+
* LINKS: a field is a link when `FIELD_SCHEMA` says so for the element's type,
|
|
2598
|
+
* or when its value is an id-shaped string or an array of them. The second
|
|
2599
|
+
* rule is the trial's own, and it is kept because real data links outside the
|
|
2600
|
+
* schema's names: a world folder's pins and markers carry `map_id` and
|
|
2601
|
+
* `zone_id`, and extension fields (`x_*`) hold references. A link to an id
|
|
2602
|
+
* outside the set is kept as unresolved, so a card can say a link exists.
|
|
2603
|
+
*
|
|
2604
|
+
* RANKING (higher score first; ties: shorter name, then id):
|
|
2605
|
+
* 0. Question and names are normalised: lower case, diacritics stripped,
|
|
2606
|
+
* possessive 's dropped, "7,875" read as 7875; letters and digits of any
|
|
2607
|
+
* script are kept, anything else is a space. A content word has 3+
|
|
2608
|
+
* characters and is no English stopword; a word holding a non-Latin letter
|
|
2609
|
+
* always counts (a CJK name is often one or two characters). Words are
|
|
2610
|
+
* split on spaces and punctuation only, so a CJK name is found when the
|
|
2611
|
+
* question sets it apart that way.
|
|
2612
|
+
* 1. Name phrase: the whole name appears in the question -> 100 + 10 per word.
|
|
2613
|
+
* 2. Alias phrase: a short form appears -> 80 + 10 per word. Short forms: the
|
|
2614
|
+
* name without a leading "the"; the part before ":" or "("; for characters
|
|
2615
|
+
* also the part before " of " or "," and the first word.
|
|
2616
|
+
* 3. Name words: question words (3+ letters, not stopwords) equal to a name
|
|
2617
|
+
* word, or sharing a 5+ letter prefix with it -> 20 + 40 x the
|
|
2618
|
+
* rarity-weighted share of the name's words matched. Kept only when better
|
|
2619
|
+
* than 1-2.
|
|
2620
|
+
* 4. Kind: +15 when a question word matches the supertype or subtype (a word
|
|
2621
|
+
* the name already used does not count again).
|
|
2622
|
+
* 5. One link step from the 12 best direct hits, both directions: a linked-to
|
|
2623
|
+
* element scores 0.6 x the hit, a linking element 0.45 x; x1.5 when the
|
|
2624
|
+
* field fits a question word ("where" -> location fields, "rules" ->
|
|
2625
|
+
* primary_power, ...). Every reason is kept; the best score counts.
|
|
2626
|
+
* 6. Text: question words (and numbers) found in the element's other text and
|
|
2627
|
+
* number fields add 3 x their summed rarity. An element found only this
|
|
2628
|
+
* way scores under 19, so below every name hit.
|
|
2629
|
+
* 7. Map furniture (pin, marker, zone, map) x0.85, so a pin never outranks
|
|
2630
|
+
* its location; it carries no text.
|
|
2631
|
+
*/
|
|
2632
|
+
/** What the lookup reads from an element: `id`, `type`, `name`, and any other fields. `OwElement` fits. */
|
|
2633
|
+
interface OwLookupElement {
|
|
2634
|
+
id: string;
|
|
2635
|
+
type: string;
|
|
2636
|
+
name?: string;
|
|
2637
|
+
[field: string]: unknown;
|
|
2638
|
+
}
|
|
2639
|
+
/** One link between two elements of the index, by field. */
|
|
2640
|
+
interface OwLookupLink {
|
|
2641
|
+
field: string;
|
|
2642
|
+
id: string;
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* The index `lookupWorld` and `renderElement` read, built once per world by
|
|
2646
|
+
* `buildWorldIndex`. `byId` is the elements by id; the other tables are the
|
|
2647
|
+
* lookup's working state: read them if useful, but their shape may change.
|
|
2648
|
+
*/
|
|
2649
|
+
interface OwWorldIndex<T extends OwLookupElement = OwLookupElement> {
|
|
2650
|
+
byId: ReadonlyMap<string, T>;
|
|
2651
|
+
/** Links each element holds to elements in the set. */
|
|
2652
|
+
out: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2653
|
+
/** Links pointing at each element, with the holder's id. */
|
|
2654
|
+
inn: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2655
|
+
/** Links each element holds to ids not in the set. */
|
|
2656
|
+
unresolved: ReadonlyMap<string, readonly OwLookupLink[]>;
|
|
2657
|
+
phrase: ReadonlyMap<string, readonly {
|
|
2658
|
+
id: string;
|
|
2659
|
+
kind: 'name' | 'alias';
|
|
2660
|
+
}[]>;
|
|
2661
|
+
tokenIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
2662
|
+
nameWords: ReadonlyMap<string, readonly string[]>;
|
|
2663
|
+
idf: ReadonlyMap<string, number>;
|
|
2664
|
+
textIds: ReadonlyMap<string, ReadonlySet<string>>;
|
|
2665
|
+
textIdf: ReadonlyMap<string, number>;
|
|
2666
|
+
}
|
|
2667
|
+
/** Why an element matched: by its name or a short form of it, by name words, by other text, or by one link. */
|
|
2668
|
+
type OwLookupReason = {
|
|
2669
|
+
kind: 'name' | 'alias' | 'word' | 'text';
|
|
2670
|
+
text: string;
|
|
2671
|
+
} | {
|
|
2672
|
+
kind: 'link';
|
|
2673
|
+
dir: 'out' | 'in';
|
|
2674
|
+
field: string;
|
|
2675
|
+
from: string | undefined;
|
|
2676
|
+
fromId: string;
|
|
2677
|
+
};
|
|
2678
|
+
/** One ranked result. `dir: 'out'` on a link reason: the hit `from` links to this element; `'in'`: this element links to it. */
|
|
2679
|
+
interface OwLookupHit<T extends OwLookupElement = OwLookupElement> {
|
|
2680
|
+
element: T;
|
|
2681
|
+
/** Rounded to one decimal; comparable within one lookup only. */
|
|
2682
|
+
score: number;
|
|
2683
|
+
reasons: OwLookupReason[];
|
|
2684
|
+
}
|
|
2685
|
+
interface OwLookupOptions {
|
|
2686
|
+
/** How many hits to return (default 8). */
|
|
2687
|
+
limit?: number;
|
|
2688
|
+
}
|
|
2689
|
+
interface OwRenderOptions {
|
|
2690
|
+
/** Description characters kept (default 500). */
|
|
2691
|
+
descMax?: number;
|
|
2692
|
+
/** Linked names listed per field (default 12). */
|
|
2693
|
+
linkMax?: number;
|
|
2694
|
+
/** Incoming names listed per type and field (default 10). */
|
|
2695
|
+
incomingMax?: number;
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Index a world's elements for `lookupWorld` and `renderElement`. Elements
|
|
2699
|
+
* without an `id` are left out; a repeated id keeps the last. Build it once
|
|
2700
|
+
* per world and again after the world changes.
|
|
2701
|
+
*/
|
|
2702
|
+
declare function buildWorldIndex<T extends OwLookupElement>(elements: Iterable<T>): OwWorldIndex<T>;
|
|
2703
|
+
/**
|
|
2704
|
+
* The elements a question is most likely about, best first (`limit`, default
|
|
2705
|
+
* 8). Names rank first, then elements one link away from a named one, then
|
|
2706
|
+
* elements whose other text holds the question's words. An empty question
|
|
2707
|
+
* returns no hits.
|
|
2708
|
+
*/
|
|
2709
|
+
declare function lookupWorld<T extends OwLookupElement>(index: OwWorldIndex<T>, question: string, { limit }?: OwLookupOptions): OwLookupHit<T>[];
|
|
2710
|
+
/**
|
|
2711
|
+
* A compact text card for one element, for a person or a model: name, type,
|
|
2712
|
+
* kind, description, its fields, its links by name (or how many point outside
|
|
2713
|
+
* the data), and what links to it.
|
|
2714
|
+
*/
|
|
2715
|
+
declare function renderElement<T extends OwLookupElement>(element: T, index: OwWorldIndex<T>, { descMax, linkMax, incomingMax }?: OwRenderOptions): string;
|
|
2716
|
+
|
|
2717
|
+
/**
|
|
2718
|
+
* Photo preparation: make a picture from a phone's camera or files ready for
|
|
2719
|
+
* `uploadImage` (webp, png, jpeg or avif, at most 15 MB), in the browser,
|
|
2720
|
+
* before it leaves the device. Lifted from the upload page's converter
|
|
2721
|
+
* (upload.onlyworlds.com, Boss's): canvas, webp at quality 0.82, alpha kept.
|
|
2722
|
+
*
|
|
2723
|
+
* Two traps it handles:
|
|
2724
|
+
* - A browser that cannot encode webp hands back a PNG instead (Safari has
|
|
2725
|
+
* long done this), and a PNG of a photo is huge. The result's real type is
|
|
2726
|
+
* checked; when webp did not come back, a photo falls back to JPEG at the
|
|
2727
|
+
* same quality and a PNG source stays PNG (it may have transparency).
|
|
2728
|
+
* - Re-encoding an already compressed file at the same size can make it
|
|
2729
|
+
* bigger: then the original is kept, if its type is one keel accepts.
|
|
2730
|
+
*
|
|
2731
|
+
* `planPhoto` is the pure part (sizes and types), so it is tested anywhere;
|
|
2732
|
+
* `preparePhoto` needs a browser (`createImageBitmap` and a canvas).
|
|
2733
|
+
*/
|
|
2734
|
+
/** The picture types keel's image upload accepts. */
|
|
2735
|
+
declare const PHOTO_TYPES: readonly ["image/webp", "image/png", "image/jpeg", "image/avif"];
|
|
2736
|
+
/** keel's limit for one picture. */
|
|
2737
|
+
declare const PHOTO_MAX_BYTES: number;
|
|
2738
|
+
interface OwPhotoOptions {
|
|
2739
|
+
/** The longest side in pixels, after shrinking. Default 2048: a notebook page stays readable and a phone upload stays small. */
|
|
2740
|
+
maxSide?: number;
|
|
2741
|
+
/** Encoder quality for webp and jpeg, 0 to 1. Default 0.82 (the media pipeline's q82). */
|
|
2742
|
+
quality?: number;
|
|
2743
|
+
/** Refuse a result over this many bytes. Default 15 MB, keel's limit. */
|
|
2744
|
+
maxBytes?: number;
|
|
2745
|
+
}
|
|
2746
|
+
interface OwPhotoPlan {
|
|
2747
|
+
width: number;
|
|
2748
|
+
height: number;
|
|
2749
|
+
/** 1 when no shrinking is needed. */
|
|
2750
|
+
scale: number;
|
|
2751
|
+
/** The types to try, in order; the first one the browser really produces wins. */
|
|
2752
|
+
targets: string[];
|
|
2753
|
+
}
|
|
2754
|
+
interface OwPreparedPhoto {
|
|
2755
|
+
blob: Blob;
|
|
2756
|
+
type: string;
|
|
2757
|
+
width: number;
|
|
2758
|
+
height: number;
|
|
2759
|
+
/** The input, for showing "4032×3024 · 3.1 MB → 2048×1536 webp · 410 KB". */
|
|
2760
|
+
original: {
|
|
2761
|
+
width: number;
|
|
2762
|
+
height: number;
|
|
2763
|
+
type: string;
|
|
2764
|
+
size: number;
|
|
2765
|
+
};
|
|
2766
|
+
/** True when the original bytes were kept unchanged. */
|
|
2767
|
+
kept: boolean;
|
|
2768
|
+
}
|
|
2769
|
+
/** Sizes and types for one picture: pure, no browser needed. */
|
|
2770
|
+
declare function planPhoto(source: {
|
|
2771
|
+
width: number;
|
|
2772
|
+
height: number;
|
|
2773
|
+
type: string;
|
|
2774
|
+
}, opts?: OwPhotoOptions): OwPhotoPlan;
|
|
2775
|
+
/**
|
|
2776
|
+
* Shrink and convert a picture for upload. Throws `TypeError` outside a
|
|
2777
|
+
* browser, for a file the browser cannot decode, or when the result is still
|
|
2778
|
+
* over `maxBytes`.
|
|
2779
|
+
*/
|
|
2780
|
+
declare function preparePhoto(input: Blob, opts?: OwPhotoOptions): Promise<OwPreparedPhoto>;
|
|
2781
|
+
|
|
2529
2782
|
/**
|
|
2530
2783
|
* OnlyWorlds Token Management API Types
|
|
2531
2784
|
*
|
|
@@ -2810,4 +3063,4 @@ declare class TokenResource {
|
|
|
2810
3063
|
getEncryptionInfo(): Promise<EncryptionInfo>;
|
|
2811
3064
|
}
|
|
2812
3065
|
|
|
2813
|
-
export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwKeyKind, type OwLinkEdit, type OwMediaTicket, OwNetworkError, type OwPage, OwPreconditionFailedError, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldMeta, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, parseErrorEnvelope, pinExpectation, readWorldFile, writeWorldFile };
|
|
3066
|
+
export { type AccessKeyResponse, ELEMENT_FAMILIES, ELEMENT_ICONS, ELEMENT_LABELS, ELEMENT_SECTIONS, ELEMENT_TYPES, type ElementFamily, type ElementType, type EncryptionInfo, FAMILY_COLORS, FAMILY_ORDER, FIELD_SCHEMA, type FieldInfo, type FieldType, GameTier, type ListParams, ONLYWORLDS_VERSION, OwApiError, type OwAuthErrorCode, type OwBulkItem, type OwBulkItemResult, type OwBulkResponse, type OwChange, type OwChangesPage, type OwClientConfig, type OwElement, type OwElementBase, type OwErrorBody, type OwJoinPreview, type OwJoinedMembership, type OwKeyKind, type OwLinkEdit, type OwLookupElement, type OwLookupHit, type OwLookupLink, type OwLookupOptions, type OwLookupReason, type OwMediaTicket, OwNetworkError, type OwPage, type OwPhotoOptions, type OwPhotoPlan, OwPreconditionFailedError, type OwPreparedPhoto, type OwReadWorldFile, type OwReadWorldFileOptions, type OwRemovalTicket, type OwRemovedImage, type OwRenderOptions, type OwUploadedImage, OwV2Client, type OwWorldFileEntry, OwWorldFileError, type OwWorldFileReason, type OwWorldIndex, type OwWorldMeta, PHOTO_MAX_BYTES, PHOTO_TYPES, type RevokeAllSessionsResponse, type RevokeSessionResponse, SPATIAL_TYPES, type SectionInfo, type TokenConsumeParams, type TokenConsumeResponse, TokenResource, type TokenStatus, type TokenTransport, WORLD_FILE_MAX_BYTES, WORLD_FILE_MAX_ENTRIES, buildWorldIndex, detectKeyKind, elementColor, errorFromResponse, familyOf, getElementIcon, getElementLabel, isDemoKey, kindCanWrite, lookupWorld, parseErrorEnvelope, pinExpectation, planPhoto, preparePhoto, readWorldFile, renderElement, writeWorldFile };
|
package/dist/index.js
CHANGED
|
@@ -75,6 +75,18 @@ var OwApiError = class extends Error {
|
|
|
75
75
|
get isPreconditionFailed() {
|
|
76
76
|
return this.status === 412 && this.code === "precondition_failed";
|
|
77
77
|
}
|
|
78
|
+
/** 404 `not_found`: no such element or route, or a join code that is unknown, used, revoked or expired. */
|
|
79
|
+
get isNotFound() {
|
|
80
|
+
return this.status === 404 && this.code === "not_found";
|
|
81
|
+
}
|
|
82
|
+
/** acceptJoin: the account is already a member of that world (409 `already_member`). The code is not spent. */
|
|
83
|
+
get isAlreadyMember() {
|
|
84
|
+
return this.status === 409 && this.code === "already_member";
|
|
85
|
+
}
|
|
86
|
+
/** acceptJoin: the account has no PIN yet, and a member writes with their own (409 `pin_required`). The code is not spent. */
|
|
87
|
+
get isPinRequired() {
|
|
88
|
+
return this.status === 409 && this.code === "pin_required";
|
|
89
|
+
}
|
|
78
90
|
};
|
|
79
91
|
var OwPreconditionFailedError = class extends OwApiError {
|
|
80
92
|
constructor(status, code, message, docUrl, detail, type = null, param = null, retryAfter = null) {
|
|
@@ -410,6 +422,31 @@ var OwV2Client = class {
|
|
|
410
422
|
if (!res.ok) throw await errorFromResponse(res);
|
|
411
423
|
return await res.json();
|
|
412
424
|
}
|
|
425
|
+
// -- Joining a world by link (keel #212) ----------------------------------
|
|
426
|
+
/**
|
|
427
|
+
* POST /join/preview -- what a join code holds, without using it: who invites, to
|
|
428
|
+
* which world, in what role, and `reader` (`person`: accept it with acceptJoin;
|
|
429
|
+
* `agent`: an AI agent redeems it at POST /join). Sends no credential, whatever key
|
|
430
|
+
* this client holds. Every bad code (unknown, used, revoked, expired) is the same
|
|
431
|
+
* 404 `not_found` (`err.isNotFound`). `code` is the part of the link after `#`.
|
|
432
|
+
*/
|
|
433
|
+
async previewJoin(code) {
|
|
434
|
+
return this.request("POST", "/join/preview", { auth: false, body: { code } });
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* POST /account/join -- accept an invite link's code as the account this client's
|
|
438
|
+
* `ow_a_` token belongs to: 201 with the new membership. Refusals: 404 `not_found`
|
|
439
|
+
* (every bad code, and a code made for an AI agent), 409 `already_member`
|
|
440
|
+
* (`err.isAlreadyMember`) and 409 `pin_required` (`err.isPinRequired`: the account
|
|
441
|
+
* has no PIN yet). A refusal never spends the code. A world key here is a TypeError
|
|
442
|
+
* before anything is sent.
|
|
443
|
+
*/
|
|
444
|
+
async acceptJoin(code) {
|
|
445
|
+
if (this.keyKind !== "account") {
|
|
446
|
+
throw new TypeError("acceptJoin needs an account token (ow_a_): a world key cannot join a world");
|
|
447
|
+
}
|
|
448
|
+
return this.request("POST", "/account/join", { body: { code } });
|
|
449
|
+
}
|
|
413
450
|
// -- Core request machinery ----------------------------------------------
|
|
414
451
|
/**
|
|
415
452
|
* Raw authenticated request against this client's baseUrl. Public since 4.0
|
|
@@ -1467,6 +1504,436 @@ function writeWorldFile(files, topDir) {
|
|
|
1467
1504
|
return zipSync(input);
|
|
1468
1505
|
}
|
|
1469
1506
|
|
|
1507
|
+
// src/lookup.ts
|
|
1508
|
+
var STOP = new Set("a an the of in on at to for from by with and or is are was were be been who whom whose what which where when why how does do did doesn has have had it its this that these those there their they them he she his her him i me my we our you your about tell know any some all can could would should will not no yes than then so if into out up down over under one two own same other more most much many very just also only s t don isn".split(" "));
|
|
1509
|
+
var SPATIAL = /* @__PURE__ */ new Set(["pin", "marker", "zone", "map"]);
|
|
1510
|
+
var FIELD_HINTS = {
|
|
1511
|
+
where: ["location", "parent_location", "birthplace", "locations", "estates", "element_id"],
|
|
1512
|
+
live: ["location", "locations", "populations"],
|
|
1513
|
+
lives: ["location", "locations", "populations"],
|
|
1514
|
+
located: ["location", "parent_location"],
|
|
1515
|
+
location: ["location", "parent_location"],
|
|
1516
|
+
born: ["birthplace"],
|
|
1517
|
+
birthplace: ["birthplace"],
|
|
1518
|
+
species: ["species", "parent_species"],
|
|
1519
|
+
race: ["species"],
|
|
1520
|
+
people: ["species", "populations"],
|
|
1521
|
+
rules: ["primary_power", "governs", "operator"],
|
|
1522
|
+
rule: ["primary_power", "governs", "operator"],
|
|
1523
|
+
ruled: ["primary_power", "governs"],
|
|
1524
|
+
holds: ["primary_power", "governs"],
|
|
1525
|
+
realm: ["primary_power"],
|
|
1526
|
+
govern: ["primary_power", "governs"],
|
|
1527
|
+
governs: ["primary_power", "governs"],
|
|
1528
|
+
controls: ["primary_power", "operator"],
|
|
1529
|
+
culture: ["populations"],
|
|
1530
|
+
cultures: ["populations"],
|
|
1531
|
+
folk: ["populations"],
|
|
1532
|
+
ally: ["allies", "friends"],
|
|
1533
|
+
allies: ["allies", "friends"],
|
|
1534
|
+
allied: ["allies"],
|
|
1535
|
+
friends: ["friends"],
|
|
1536
|
+
friend: ["friends"],
|
|
1537
|
+
enemy: ["adversaries", "rivals"],
|
|
1538
|
+
enemies: ["adversaries", "rivals"],
|
|
1539
|
+
rival: ["rivals"],
|
|
1540
|
+
rivals: ["rivals"],
|
|
1541
|
+
fought: ["institutions"],
|
|
1542
|
+
war: ["institutions"],
|
|
1543
|
+
fight: ["institutions"],
|
|
1544
|
+
family: ["family"],
|
|
1545
|
+
house: ["family"],
|
|
1546
|
+
clan: ["family"],
|
|
1547
|
+
member: ["characters", "institutions", "family"],
|
|
1548
|
+
members: ["characters", "institutions", "family"],
|
|
1549
|
+
belong: ["characters", "institutions", "family"],
|
|
1550
|
+
belongs: ["characters", "institutions", "family"],
|
|
1551
|
+
ancestor: ["ancestors"],
|
|
1552
|
+
ancestors: ["ancestors"],
|
|
1553
|
+
region: ["parent_location"],
|
|
1554
|
+
part: ["parent_location", "parent_species"],
|
|
1555
|
+
parent: ["parent_location", "parent_species"]
|
|
1556
|
+
};
|
|
1557
|
+
var TEXT_SKIP = /* @__PURE__ */ new Set([
|
|
1558
|
+
"id",
|
|
1559
|
+
"name",
|
|
1560
|
+
"type",
|
|
1561
|
+
"image_url",
|
|
1562
|
+
"created_at",
|
|
1563
|
+
"updated_at",
|
|
1564
|
+
"local_updated_at",
|
|
1565
|
+
"server_updated_at",
|
|
1566
|
+
"world",
|
|
1567
|
+
"image_media_id",
|
|
1568
|
+
"element_type"
|
|
1569
|
+
]);
|
|
1570
|
+
var RENDER_SKIP = /* @__PURE__ */ new Set([
|
|
1571
|
+
"id",
|
|
1572
|
+
"name",
|
|
1573
|
+
"description",
|
|
1574
|
+
"type",
|
|
1575
|
+
"supertype",
|
|
1576
|
+
"subtype",
|
|
1577
|
+
"image_url",
|
|
1578
|
+
"created_at",
|
|
1579
|
+
"updated_at",
|
|
1580
|
+
"local_updated_at",
|
|
1581
|
+
"server_updated_at",
|
|
1582
|
+
"world",
|
|
1583
|
+
"image_media_id"
|
|
1584
|
+
]);
|
|
1585
|
+
var ID_SHAPE = /^[0-9a-f-]{32,36}$/i;
|
|
1586
|
+
var MARKS = new RegExp(`[${String.fromCharCode(768)}-${String.fromCharCode(879)}]`, "g");
|
|
1587
|
+
var POSSESSIVE = new RegExp(`['${String.fromCharCode(8217)}]s\\b`, "g");
|
|
1588
|
+
function normalize(s) {
|
|
1589
|
+
return String(s ?? "").normalize("NFD").replace(MARKS, "").toLowerCase().replace(POSSESSIVE, "").replace(/(\d),(\d{3})\b/g, "$1$2").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
1590
|
+
}
|
|
1591
|
+
var words = (s) => s ? s.split(" ") : [];
|
|
1592
|
+
var NON_LATIN = /[^\p{Script=Latin}\p{N}]/u;
|
|
1593
|
+
var isContent = (w) => w.length >= 3 && !STOP.has(w) || NON_LATIN.test(w);
|
|
1594
|
+
var schemaLinks;
|
|
1595
|
+
function linkFieldsOf(type) {
|
|
1596
|
+
if (!schemaLinks) {
|
|
1597
|
+
schemaLinks = /* @__PURE__ */ new Map();
|
|
1598
|
+
for (const [t, fields] of Object.entries(FIELD_SCHEMA)) {
|
|
1599
|
+
const s = /* @__PURE__ */ new Set();
|
|
1600
|
+
for (const [f, info] of Object.entries(fields)) if (info.type === "single_link" || info.type === "multi_link") s.add(f);
|
|
1601
|
+
schemaLinks.set(t, s);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return typeof type === "string" ? schemaLinks.get(type) : void 0;
|
|
1605
|
+
}
|
|
1606
|
+
function aliasesOf(el) {
|
|
1607
|
+
const raw = String(el.name ?? "");
|
|
1608
|
+
const full = normalize(raw);
|
|
1609
|
+
const out = /* @__PURE__ */ new Set();
|
|
1610
|
+
const add = (s) => {
|
|
1611
|
+
const n = normalize(s);
|
|
1612
|
+
if (n && n !== full && words(n).some(isContent)) out.add(n);
|
|
1613
|
+
};
|
|
1614
|
+
if (/^the\s/i.test(raw)) add(raw.replace(/^the\s+/i, ""));
|
|
1615
|
+
if (/[:(]/.test(raw)) add(raw.split(/[:(]/)[0]);
|
|
1616
|
+
if (el.type === "character") {
|
|
1617
|
+
if (/\sof\s/i.test(raw)) add(raw.split(/\sof\s/i)[0]);
|
|
1618
|
+
if (raw.includes(",")) add(raw.split(",")[0]);
|
|
1619
|
+
const first = words(full)[0];
|
|
1620
|
+
if (first && isContent(first)) add(first);
|
|
1621
|
+
}
|
|
1622
|
+
return [...out];
|
|
1623
|
+
}
|
|
1624
|
+
function buildWorldIndex(elements) {
|
|
1625
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1626
|
+
for (const el of elements) if (el && el.id) byId.set(el.id, el);
|
|
1627
|
+
const out = /* @__PURE__ */ new Map(), inn = /* @__PURE__ */ new Map();
|
|
1628
|
+
const unresolved = /* @__PURE__ */ new Map();
|
|
1629
|
+
const phrase = /* @__PURE__ */ new Map();
|
|
1630
|
+
const tokenIds = /* @__PURE__ */ new Map();
|
|
1631
|
+
const nameWords = /* @__PURE__ */ new Map();
|
|
1632
|
+
const textIds = /* @__PURE__ */ new Map();
|
|
1633
|
+
const pushTo = (m, k, v) => {
|
|
1634
|
+
if (!m.has(k)) m.set(k, []);
|
|
1635
|
+
m.get(k).push(v);
|
|
1636
|
+
};
|
|
1637
|
+
for (const el of byId.values()) {
|
|
1638
|
+
const schema = linkFieldsOf(el.type);
|
|
1639
|
+
const linkFields = /* @__PURE__ */ new Set();
|
|
1640
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1641
|
+
if (field === "id") continue;
|
|
1642
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
1643
|
+
if (!vals.length) continue;
|
|
1644
|
+
if (schema?.has(field)) {
|
|
1645
|
+
const ids = vals.filter((x) => typeof x === "string" && x !== "");
|
|
1646
|
+
if (!ids.length) continue;
|
|
1647
|
+
linkFields.add(field);
|
|
1648
|
+
for (const id of ids) {
|
|
1649
|
+
if (byId.has(id)) {
|
|
1650
|
+
pushTo(out, el.id, { field, id });
|
|
1651
|
+
pushTo(inn, id, { field, id: el.id });
|
|
1652
|
+
} else pushTo(unresolved, el.id, { field, id });
|
|
1653
|
+
}
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
if (!vals.every((x) => typeof x === "string" && ID_SHAPE.test(x))) continue;
|
|
1657
|
+
for (const id of vals) {
|
|
1658
|
+
if (byId.has(id)) {
|
|
1659
|
+
pushTo(out, el.id, { field, id });
|
|
1660
|
+
pushTo(inn, id, { field, id: el.id });
|
|
1661
|
+
} else pushTo(unresolved, el.id, { field, id });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (!SPATIAL.has(el.type)) {
|
|
1665
|
+
const tw = /* @__PURE__ */ new Set();
|
|
1666
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1667
|
+
if (TEXT_SKIP.has(field) || linkFields.has(field) || field.startsWith("x_") || field.startsWith("atlas_")) continue;
|
|
1668
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
1669
|
+
for (const x of vals) {
|
|
1670
|
+
if (typeof x === "number" && Number.isFinite(x)) tw.add(String(x));
|
|
1671
|
+
else if (typeof x === "string" && x && !ID_SHAPE.test(x)) {
|
|
1672
|
+
for (const w of words(normalize(x))) if (isContent(w) || /^\d+$/.test(w)) tw.add(w);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
for (const w of tw) {
|
|
1677
|
+
if (!textIds.has(w)) textIds.set(w, /* @__PURE__ */ new Set());
|
|
1678
|
+
textIds.get(w).add(el.id);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const full = normalize(el.name);
|
|
1682
|
+
if (!full) continue;
|
|
1683
|
+
pushTo(phrase, full, { id: el.id, kind: "name" });
|
|
1684
|
+
for (const a of aliasesOf(el)) pushTo(phrase, a, { id: el.id, kind: "alias" });
|
|
1685
|
+
const cw = [...new Set(words(full).filter(isContent))];
|
|
1686
|
+
nameWords.set(el.id, cw);
|
|
1687
|
+
for (const w of cw) {
|
|
1688
|
+
if (!tokenIds.has(w)) tokenIds.set(w, /* @__PURE__ */ new Set());
|
|
1689
|
+
tokenIds.get(w).add(el.id);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const N = byId.size || 1;
|
|
1693
|
+
const idf = /* @__PURE__ */ new Map();
|
|
1694
|
+
for (const [w, ids] of tokenIds) idf.set(w, Math.log(1 + N / ids.size));
|
|
1695
|
+
const textIdf = /* @__PURE__ */ new Map();
|
|
1696
|
+
for (const [w, ids] of textIds) textIdf.set(w, Math.log(1 + N / ids.size));
|
|
1697
|
+
return { byId, out, inn, unresolved, phrase, tokenIds, nameWords, idf, textIds, textIdf };
|
|
1698
|
+
}
|
|
1699
|
+
function similarWords(w, vocab) {
|
|
1700
|
+
const res = [];
|
|
1701
|
+
if (vocab.has(w)) res.push(w);
|
|
1702
|
+
if (w.length >= 5 && !/^\d+$/.test(w)) {
|
|
1703
|
+
for (const t of vocab.keys()) {
|
|
1704
|
+
if (t === w || t.length < 5) continue;
|
|
1705
|
+
const p = Math.min(w.length, t.length);
|
|
1706
|
+
if (p >= 5 && Math.abs(w.length - t.length) <= 3 && (t.startsWith(w) || w.startsWith(t))) res.push(t);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return res;
|
|
1710
|
+
}
|
|
1711
|
+
function lookupWorld(index, question, { limit = 8 } = {}) {
|
|
1712
|
+
const seeds = 12;
|
|
1713
|
+
const q = words(normalize(question));
|
|
1714
|
+
const qSet = new Set(q);
|
|
1715
|
+
const cand = /* @__PURE__ */ new Map();
|
|
1716
|
+
const bump = (id, score, reason, direct) => {
|
|
1717
|
+
const c = cand.get(id) || { score: 0, direct: 0, reasons: [] };
|
|
1718
|
+
c.reasons.push(reason);
|
|
1719
|
+
if (score > c.score) c.score = score;
|
|
1720
|
+
if (direct && score > c.direct) c.direct = score;
|
|
1721
|
+
cand.set(id, c);
|
|
1722
|
+
};
|
|
1723
|
+
const qUsed = /* @__PURE__ */ new Map();
|
|
1724
|
+
const useWords = (id, ws) => {
|
|
1725
|
+
if (!qUsed.has(id)) qUsed.set(id, /* @__PURE__ */ new Set());
|
|
1726
|
+
for (const w of ws) qUsed.get(id).add(w);
|
|
1727
|
+
};
|
|
1728
|
+
for (let n = Math.min(8, q.length); n >= 1; n--) {
|
|
1729
|
+
for (let i = 0; i + n <= q.length; i++) {
|
|
1730
|
+
const g = q.slice(i, i + n);
|
|
1731
|
+
if (n === 1 && !isContent(g[0])) continue;
|
|
1732
|
+
const hits = index.phrase.get(g.join(" "));
|
|
1733
|
+
if (!hits) continue;
|
|
1734
|
+
for (const h of hits) {
|
|
1735
|
+
const base = h.kind === "name" ? 100 : 80;
|
|
1736
|
+
bump(h.id, base + 10 * n, { kind: h.kind, text: g.join(" ") }, true);
|
|
1737
|
+
useWords(h.id, g);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const matched = /* @__PURE__ */ new Map();
|
|
1742
|
+
for (const w of qSet) {
|
|
1743
|
+
if (!isContent(w)) continue;
|
|
1744
|
+
for (const t of similarWords(w, index.tokenIds)) {
|
|
1745
|
+
for (const id of index.tokenIds.get(t)) {
|
|
1746
|
+
if (!matched.has(id)) matched.set(id, /* @__PURE__ */ new Set());
|
|
1747
|
+
matched.get(id).add(t);
|
|
1748
|
+
useWords(id, [w]);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
for (const [id, ws] of matched) {
|
|
1753
|
+
const nw = index.nameWords.get(id) || [];
|
|
1754
|
+
const tot = nw.reduce((s, w) => s + index.idf.get(w), 0) || 1;
|
|
1755
|
+
const got = [...ws].reduce((s, w) => s + index.idf.get(w), 0);
|
|
1756
|
+
const score = 20 + 40 * (got / tot);
|
|
1757
|
+
const c = cand.get(id);
|
|
1758
|
+
if (!c || c.direct < score) bump(id, score, { kind: "word", text: [...ws].join(" ") }, true);
|
|
1759
|
+
}
|
|
1760
|
+
const kindBonus = (id) => {
|
|
1761
|
+
const el = index.byId.get(id);
|
|
1762
|
+
const kw = new Set(words(normalize(`${el.supertype ?? ""} ${el.subtype ?? ""}`)).filter(isContent));
|
|
1763
|
+
const used = qUsed.get(id);
|
|
1764
|
+
for (const w of qSet) {
|
|
1765
|
+
if (used?.has(w)) continue;
|
|
1766
|
+
if (kw.has(w) || [...kw].some((k) => k.length >= 5 && w.length >= 5 && (k.startsWith(w) || w.startsWith(k)))) {
|
|
1767
|
+
useWords(id, [w]);
|
|
1768
|
+
return 15;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return 0;
|
|
1772
|
+
};
|
|
1773
|
+
for (const [id, c] of cand) {
|
|
1774
|
+
const b = kindBonus(id);
|
|
1775
|
+
if (b) {
|
|
1776
|
+
c.score += b;
|
|
1777
|
+
c.direct += b;
|
|
1778
|
+
c.kinded = true;
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
const hintFields = /* @__PURE__ */ new Set();
|
|
1782
|
+
for (const w of qSet) for (const f of FIELD_HINTS[w] || []) hintFields.add(f);
|
|
1783
|
+
const seedList = [...cand.entries()].filter(([, c]) => c.direct > 0).sort((a, b) => b[1].direct - a[1].direct).slice(0, seeds);
|
|
1784
|
+
for (const [sid, sc] of seedList) {
|
|
1785
|
+
const sname = index.byId.get(sid).name;
|
|
1786
|
+
for (const { field, id } of index.out.get(sid) || []) {
|
|
1787
|
+
const m = hintFields.has(field) ? 1.5 : 1;
|
|
1788
|
+
bump(id, sc.direct * 0.6 * m, { kind: "link", dir: "out", field, from: sname, fromId: sid }, false);
|
|
1789
|
+
}
|
|
1790
|
+
for (const { field, id } of index.inn.get(sid) || []) {
|
|
1791
|
+
const m = hintFields.has(field) ? 1.5 : 1;
|
|
1792
|
+
bump(id, sc.direct * 0.45 * m, { kind: "link", dir: "in", field, from: sname, fromId: sid }, false);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
for (const [id, c] of cand) {
|
|
1796
|
+
if (!c.kinded) {
|
|
1797
|
+
const b = kindBonus(id);
|
|
1798
|
+
if (b && c.direct === 0) c.score += b;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
const textHits = /* @__PURE__ */ new Map();
|
|
1802
|
+
for (const w of qSet) {
|
|
1803
|
+
if (!isContent(w) && !/^\d+$/.test(w)) continue;
|
|
1804
|
+
for (const t of similarWords(w, index.textIds)) {
|
|
1805
|
+
const f = index.textIdf.get(t);
|
|
1806
|
+
for (const id of index.textIds.get(t)) {
|
|
1807
|
+
if (!textHits.has(id)) textHits.set(id, /* @__PURE__ */ new Map());
|
|
1808
|
+
const m = textHits.get(id);
|
|
1809
|
+
if (!((m.get(w) ?? NaN) >= f)) m.set(w, f);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
for (const [id, m] of textHits) {
|
|
1814
|
+
const used = qUsed.get(id);
|
|
1815
|
+
const ws = [...m.keys()].filter((w) => !used?.has(w));
|
|
1816
|
+
if (!ws.length) continue;
|
|
1817
|
+
const raw = 3 * ws.reduce((s, w) => s + m.get(w), 0);
|
|
1818
|
+
const reason = { kind: "text", text: ws.join(" ") };
|
|
1819
|
+
const c = cand.get(id);
|
|
1820
|
+
if (c) {
|
|
1821
|
+
c.score += raw;
|
|
1822
|
+
c.reasons.push(reason);
|
|
1823
|
+
} else cand.set(id, { score: 19 * (1 - Math.exp(-raw / 30)), direct: 0, reasons: [reason] });
|
|
1824
|
+
}
|
|
1825
|
+
for (const [id, c] of cand) if (SPATIAL.has(index.byId.get(id).type)) c.score *= 0.85;
|
|
1826
|
+
return [...cand.entries()].map(([id, c]) => ({ element: index.byId.get(id), score: Math.round(c.score * 10) / 10, reasons: c.reasons })).sort((a, b) => b.score - a.score || String(a.element.name).length - String(b.element.name).length || (a.element.id < b.element.id ? -1 : 1)).slice(0, limit);
|
|
1827
|
+
}
|
|
1828
|
+
function renderElement(element, index, { descMax = 500, linkMax = 12, incomingMax = 10 } = {}) {
|
|
1829
|
+
const el = element;
|
|
1830
|
+
const name = (id) => index.byId.get(id)?.name ?? "?";
|
|
1831
|
+
const kind = [el.supertype, el.subtype].filter((x) => x && x !== "undefined").join(" / ");
|
|
1832
|
+
const lines = [`${el.name} (${el.type}${kind ? `; ${kind}` : ""})`];
|
|
1833
|
+
if (el.description) {
|
|
1834
|
+
const d = String(el.description);
|
|
1835
|
+
lines.push(`description: ${d.length > descMax ? d.slice(0, descMax) + "..." : d}`);
|
|
1836
|
+
}
|
|
1837
|
+
const outBy = /* @__PURE__ */ new Map();
|
|
1838
|
+
for (const { field, id } of index.out.get(el.id) || []) {
|
|
1839
|
+
if (!outBy.has(field)) outBy.set(field, []);
|
|
1840
|
+
outBy.get(field).push(name(id));
|
|
1841
|
+
}
|
|
1842
|
+
const unres = /* @__PURE__ */ new Map();
|
|
1843
|
+
for (const { field } of index.unresolved.get(el.id) || []) unres.set(field, (unres.get(field) || 0) + 1);
|
|
1844
|
+
for (const [field, v] of Object.entries(el)) {
|
|
1845
|
+
if (RENDER_SKIP.has(field) || field.startsWith("x_") || field.startsWith("atlas_")) continue;
|
|
1846
|
+
if (outBy.has(field) || unres.has(field)) {
|
|
1847
|
+
const names = outBy.get(field) || [];
|
|
1848
|
+
let s = names.slice(0, linkMax).join(", ");
|
|
1849
|
+
if (names.length > linkMax) s += ` (+${names.length - linkMax} more)`;
|
|
1850
|
+
const u = unres.get(field);
|
|
1851
|
+
if (u) s += `${s ? "; " : ""}${u} linked element(s) not present in the world data`;
|
|
1852
|
+
lines.push(`${field}: ${s}`);
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
1855
|
+
if (v === null || v === void 0 || v === "" || Array.isArray(v) && !v.length || typeof v === "object" && !Array.isArray(v)) continue;
|
|
1856
|
+
lines.push(`${field}: ${Array.isArray(v) ? v.join(", ") : v}`);
|
|
1857
|
+
}
|
|
1858
|
+
const inBy = /* @__PURE__ */ new Map();
|
|
1859
|
+
for (const { field, id } of index.inn.get(el.id) || []) {
|
|
1860
|
+
const src = index.byId.get(id);
|
|
1861
|
+
if (SPATIAL.has(src.type)) continue;
|
|
1862
|
+
const k = `${src.type}.${field}`;
|
|
1863
|
+
if (!inBy.has(k)) inBy.set(k, []);
|
|
1864
|
+
inBy.get(k).push(src.name);
|
|
1865
|
+
}
|
|
1866
|
+
for (const [k, names] of inBy) {
|
|
1867
|
+
let s = names.slice(0, incomingMax).join(", ");
|
|
1868
|
+
if (names.length > incomingMax) s += ` (+${names.length - incomingMax} more)`;
|
|
1869
|
+
lines.push(`linked from (${k}): ${s}`);
|
|
1870
|
+
}
|
|
1871
|
+
return lines.join("\n");
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/photo.ts
|
|
1875
|
+
var PHOTO_TYPES = ["image/webp", "image/png", "image/jpeg", "image/avif"];
|
|
1876
|
+
var PHOTO_MAX_BYTES = 15 * 1024 * 1024;
|
|
1877
|
+
function planPhoto(source, opts = {}) {
|
|
1878
|
+
const maxSide = opts.maxSide ?? 2048;
|
|
1879
|
+
if (!(maxSide > 0)) throw new TypeError("preparePhoto: maxSide must be a positive number");
|
|
1880
|
+
if (!(source.width > 0 && source.height > 0)) throw new TypeError("preparePhoto: the picture has no size");
|
|
1881
|
+
const scale = Math.min(1, maxSide / Math.max(source.width, source.height));
|
|
1882
|
+
const width = Math.max(1, Math.round(source.width * scale));
|
|
1883
|
+
const height = Math.max(1, Math.round(source.height * scale));
|
|
1884
|
+
const fallback = source.type === "image/png" ? "image/png" : "image/jpeg";
|
|
1885
|
+
return { width, height, scale, targets: ["image/webp", fallback] };
|
|
1886
|
+
}
|
|
1887
|
+
function makeCanvas(width, height) {
|
|
1888
|
+
const g = globalThis;
|
|
1889
|
+
if (g.OffscreenCanvas) {
|
|
1890
|
+
const canvas = new g.OffscreenCanvas(width, height);
|
|
1891
|
+
return { canvas, encode: (type, quality) => canvas.convertToBlob({ type, quality }).catch(() => null) };
|
|
1892
|
+
}
|
|
1893
|
+
if (g.document) {
|
|
1894
|
+
const canvas = g.document.createElement("canvas");
|
|
1895
|
+
canvas.width = width;
|
|
1896
|
+
canvas.height = height;
|
|
1897
|
+
return { canvas, encode: (type, quality) => new Promise((res) => canvas.toBlob(res, type, quality)) };
|
|
1898
|
+
}
|
|
1899
|
+
throw new TypeError("preparePhoto needs a browser: no canvas here");
|
|
1900
|
+
}
|
|
1901
|
+
async function preparePhoto(input, opts = {}) {
|
|
1902
|
+
const quality = opts.quality ?? 0.82;
|
|
1903
|
+
const maxBytes = opts.maxBytes ?? PHOTO_MAX_BYTES;
|
|
1904
|
+
const decode = globalThis.createImageBitmap;
|
|
1905
|
+
if (!decode) throw new TypeError("preparePhoto needs a browser: no createImageBitmap here");
|
|
1906
|
+
let bitmap;
|
|
1907
|
+
try {
|
|
1908
|
+
bitmap = await decode(input, { imageOrientation: "from-image" });
|
|
1909
|
+
} catch {
|
|
1910
|
+
throw new TypeError("preparePhoto: this browser cannot read that picture");
|
|
1911
|
+
}
|
|
1912
|
+
const original = { width: bitmap.width, height: bitmap.height, type: input.type, size: input.size };
|
|
1913
|
+
try {
|
|
1914
|
+
const plan = planPhoto(original, opts);
|
|
1915
|
+
const { canvas, encode } = makeCanvas(plan.width, plan.height);
|
|
1916
|
+
canvas.getContext("2d").drawImage(bitmap, 0, 0, plan.width, plan.height);
|
|
1917
|
+
let made = null;
|
|
1918
|
+
for (const type of plan.targets) {
|
|
1919
|
+
const b = await encode(type, quality);
|
|
1920
|
+
if (b && b.type === type) {
|
|
1921
|
+
made = b;
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (!made) throw new TypeError("preparePhoto: this browser could not encode the picture");
|
|
1926
|
+
const keepOriginal = plan.scale === 1 && made.size >= input.size && PHOTO_TYPES.includes(input.type);
|
|
1927
|
+
const out = keepOriginal ? { blob: input, type: input.type, width: original.width, height: original.height, kept: true } : { blob: made, type: made.type, width: plan.width, height: plan.height, kept: false };
|
|
1928
|
+
if (out.blob.size > maxBytes) {
|
|
1929
|
+
throw new TypeError(`preparePhoto: still ${Math.ceil(out.blob.size / 1048576)} MB after shrinking; try a smaller maxSide`);
|
|
1930
|
+
}
|
|
1931
|
+
return { ...out, original };
|
|
1932
|
+
} finally {
|
|
1933
|
+
bitmap.close?.();
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1470
1937
|
// src/token-resource.ts
|
|
1471
1938
|
var TokenResource = class {
|
|
1472
1939
|
constructor(client) {
|
|
@@ -1653,10 +2120,13 @@ export {
|
|
|
1653
2120
|
OwPreconditionFailedError,
|
|
1654
2121
|
OwV2Client,
|
|
1655
2122
|
OwWorldFileError,
|
|
2123
|
+
PHOTO_MAX_BYTES,
|
|
2124
|
+
PHOTO_TYPES,
|
|
1656
2125
|
SPATIAL_TYPES,
|
|
1657
2126
|
TokenResource,
|
|
1658
2127
|
WORLD_FILE_MAX_BYTES,
|
|
1659
2128
|
WORLD_FILE_MAX_ENTRIES,
|
|
2129
|
+
buildWorldIndex,
|
|
1660
2130
|
detectKeyKind,
|
|
1661
2131
|
elementColor,
|
|
1662
2132
|
errorFromResponse,
|
|
@@ -1665,8 +2135,12 @@ export {
|
|
|
1665
2135
|
getElementLabel,
|
|
1666
2136
|
isDemoKey,
|
|
1667
2137
|
kindCanWrite,
|
|
2138
|
+
lookupWorld,
|
|
1668
2139
|
parseErrorEnvelope,
|
|
1669
2140
|
pinExpectation,
|
|
2141
|
+
planPhoto,
|
|
2142
|
+
preparePhoto,
|
|
1670
2143
|
readWorldFile,
|
|
2144
|
+
renderElement,
|
|
1671
2145
|
writeWorldFile
|
|
1672
2146
|
};
|