@surrealdb/memory 1.0.0-alpha.10 → 1.0.0-alpha.11

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/README.md CHANGED
@@ -64,6 +64,9 @@ await client.context("Summarise preferences", { k: 5 });
64
64
  await client.reflect("What changed this week?", { persist: true });
65
65
  await client.forget("Remove old project notes", { purge: true });
66
66
 
67
+ // What the context knows about a subject, in one round trip.
68
+ await client.lookup("Who is Tobie?");
69
+
67
70
  // Snapshots and maintenance.
68
71
  await client.state({ limit: 500 }); // bounded per table; check `truncated`
69
72
  await client.profile();
@@ -90,12 +93,118 @@ for await (const chunk of stream) {
90
93
  }
91
94
  ```
92
95
 
96
+ ## Known-about lookups
97
+
98
+ `client.lookup(query)` answers what the context knows about a subject in one
99
+ round trip. Everything it returns is a stored row: nothing is generated, nothing
100
+ is summarised by a model, and an identical query returns an identical answer.
101
+
102
+ Branch on `resolution.kind` rather than inferring the case from an array length
103
+ — "one candidate" and "confidently one entity" are different answers:
104
+
105
+ ```ts
106
+ const answer = await client.lookup("Atlas");
107
+
108
+ switch (answer.resolution.kind) {
109
+ case "entity":
110
+ render(answer.resolution.subject, answer.facts.items);
111
+ break;
112
+ case "ambiguous":
113
+ // Each candidate carries a `distinguisher` so they can be told apart.
114
+ offerChoice(answer.resolution.candidates);
115
+ break;
116
+ case "topic":
117
+ // No single subject: the answer is the cluster in `entities` / `passages`.
118
+ renderCluster(answer.entities.items, answer.passages.items);
119
+ break;
120
+ case "empty":
121
+ // `nearest` separates "not stored" from "stored under another name".
122
+ suggest(answer.resolution.nearest);
123
+ break;
124
+ }
125
+ ```
126
+
127
+ Every section is bounded and reports `truncated`; none of them page. A section
128
+ that was cut points at its own collection endpoint, which does walk:
129
+
130
+ | Section | Walk it with |
131
+ | --- | --- |
132
+ | `facts` | `client.facts.attributes({ entity })` |
133
+ | `relations` | `client.facts.allEdgesOf(entity)` — the section carries edges in either direction, so one of `src` / `dst` alone reproduces half of it |
134
+ | `events` | `client.facts.actions({ actor: entity })` |
135
+ | `passages` | `client.recall(...)` |
136
+ | `uncertainty` | `client.uncertainty.list({ entity })` |
137
+
138
+ `facts` is ranked by importance while its collection pages in write order, so
139
+ the ranked head is a different question from the walk, not its first page.
140
+
141
+ Following a trail is cheap: pass `subject` to skip resolution when a hop already
142
+ knows which entity it landed on.
143
+
144
+ ```ts
145
+ await client.lookup("Atlas", { subject: "product/atlas", include: ["facts", "relations"] });
146
+ ```
147
+
148
+ ### Finding a subject
149
+
150
+ ```ts
151
+ // Name search: lexical, deterministic, best match first. A ranked head, not a walk.
152
+ await client.entities.search("tobie", { type: "person", limit: 5 });
153
+
154
+ // Where to start when there is no query yet.
155
+ await client.entities.top({ by: "coverage" }); // also `importance`, `recency`
156
+
157
+ // One hop out, each neighbour carrying its own fact count.
158
+ await client.entities.neighbours("person", "tobie", { minFacts: 2 });
159
+
160
+ // What changed about a subject: every key's supersession chain, newest first.
161
+ await client.entities.changes("person", "tobie", { limit: 50 });
162
+
163
+ // The collections a bounded head points at, all cursor-paged in the same order.
164
+ await client.facts.attributes({ entity: "person/tobie" });
165
+ await client.facts.allEdgesOf("person/tobie"); // both directions
166
+ await client.facts.actions({ actor: "person/tobie", since: "2026-01-01T00:00:00Z" });
167
+ ```
168
+
169
+ `entities.get` returns a **bounded head** of the entity's attributes and
170
+ relations, newest first, and reports `truncated` per section:
171
+
172
+ ```ts
173
+ const { entity, attributes, relations, truncated } = await client.entities.get("person", "tobie");
174
+ if (truncated.relations) {
175
+ // The head is a genuine prefix of the walk, so follow the collection for the rest.
176
+ const every = await client.facts.allEdgesOf("person/tobie");
177
+ }
178
+ ```
179
+
180
+ ### Settling a contradiction
181
+
182
+ ```ts
183
+ const open = await client.uncertainty.list({ resolved: false });
184
+
185
+ for (const flag of open.unknowns) {
186
+ // `resolvable` says whether settling would succeed for *this* key: it is false
187
+ // for a subject-less flag, an already-settled one, and one scoped beyond the
188
+ // caller's write region.
189
+ if (flag.resolvable) {
190
+ await client.uncertainty.resolve(flag.id, "SurrealDB", { note: "confirmed in the offer letter" });
191
+ }
192
+ }
193
+ ```
194
+
195
+ One call claims the flag, writes the accepted value through the reconciler, and
196
+ retires the values it beats. Settlement converges on retry rather than being
197
+ transactional: repeating a failed call dedups the value and finishes the
198
+ retirement.
199
+
93
200
  ## Namespaces
94
201
 
95
202
  | Namespace | Highlights |
96
203
  | --- | --- |
97
204
  | `client.documents` | `upload`, `reprocess`, `get`, `raw`, `chunks`, `allChunks`, `list`, `listAll`, `count`, `delete`, `query`, `recomputeLinks`, `keywords.*` |
98
- | `client.entities` | `list`, `listAll`, `count`, `get`, `history`, `delete` |
205
+ | `client.entities` | `list`, `listAll`, `count`, `search`, `top`, `get`, `neighbours`, `allNeighbours`, `changes`, `allChanges`, `history`, `delete` |
206
+ | `client.facts` | `attributes`, `allAttributes`, `relations`, `allRelations`, `allEdgesOf`, `actions`, `allActions` |
207
+ | `client.uncertainty` | `list`, `listAll`, `count`, `resolve` |
99
208
  | `client.sessions` | `create` → `Session` (`turns`, `allTurns`, `context`, `close`) |
100
209
  | `client.lifecycle` | `expire`, `decay` |
101
210
  | `client.traces` | `list`, `listAll`, `get`, `stats` |
@@ -137,14 +246,27 @@ await client.documents.count(); // number, without fetching the documents
137
246
  ```
138
247
 
139
248
  `listAll` is an unbounded read by construction — reach for it when the
140
- collection is a tree or a filter source, not a screenful. `documents.allChunks`
141
- takes a `max` for the bounded case.
249
+ collection is a tree or a filter source, not a screenful. `documents.allChunks`,
250
+ `entities.allNeighbours` and `entities.allChanges` take a `max` for the bounded
251
+ case.
252
+
253
+ Not every bounded read is a page. `entities.search`, `entities.top`, the
254
+ sections of `client.lookup`, and the fact sections of `entities.get` are ranked
255
+ or aggregate heads: they carry no cursor, and a cut one is signalled by
256
+ `truncated` rather than continued. Follow the collection endpoint each one names
257
+ instead of re-requesting it with a bigger limit.
142
258
 
143
259
  `totalSize` is opt-in (`count: true`) because it costs a full count of the
144
260
  filtered set. Two endpoints do not offer it at all — `scopes.list` and
145
261
  `client.audit` take `limit` and `cursor` only, typed as `CursorOptions`, so
146
262
  asking them for a count is a type error rather than a rejected request.
147
263
 
264
+ `entities.neighbours` offers `count`, but not beside `minFacts`: the filter runs
265
+ after each page is hydrated, so honouring it in a total would cost the
266
+ per-neighbour counts the walk exists to avoid. The server rejects that pairing
267
+ with a `400`, and `NeighbourhoodOptions` is an exclusive union, so it fails to
268
+ compile instead.
269
+
148
270
  `/documents`, `/documents/{id}/chunks`, and `/documents/keywords` also still
149
271
  accept the pre-cursor `page`/`pageSize` parameters, for callers with numbered
150
272
  page controls that need a page index and a total. The server rejects `cursor`
@@ -157,7 +279,9 @@ and `CursorOptions` types.
157
279
 
158
280
  ## Delegation
159
281
 
160
- `client.onBehalfOf(principalId)` returns a new client whose every request carries the `X-Spectron-On-Behalf-Of` header, so calls run with that principal's authorisation. This requires the `manage` grant. The original client is left unchanged.
282
+ `client.onBehalfOf(principalId)` returns a new client whose every request carries the delegation header, so calls run with that principal's authorisation. This requires the `manage` grant. The original client is left unchanged.
283
+
284
+ The header is spelled `X-Spectron-On-Behalf-Of`. That is a wire constant rather than branding: the service matches the name exactly and its CORS allowlist carries only that spelling, so it stays until the service renames it.
161
285
 
162
286
  ```ts
163
287
  const asAlex = client.onBehalfOf("principal:alex");