@typicalday/firegraph 0.12.0 → 0.13.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 (70) hide show
  1. package/README.md +317 -73
  2. package/dist/backend-DuvHGgK1.d.cts +1897 -0
  3. package/dist/backend-DuvHGgK1.d.ts +1897 -0
  4. package/dist/backend.cjs +222 -3
  5. package/dist/backend.cjs.map +1 -1
  6. package/dist/backend.d.cts +25 -5
  7. package/dist/backend.d.ts +25 -5
  8. package/dist/backend.js +197 -4
  9. package/dist/backend.js.map +1 -1
  10. package/dist/chunk-2DHMNTV6.js +16 -0
  11. package/dist/chunk-2DHMNTV6.js.map +1 -0
  12. package/dist/chunk-4MMQ5W74.js +288 -0
  13. package/dist/chunk-4MMQ5W74.js.map +1 -0
  14. package/dist/chunk-D4J7Z4FE.js +67 -0
  15. package/dist/chunk-D4J7Z4FE.js.map +1 -0
  16. package/dist/chunk-N5HFDWQX.js +23 -0
  17. package/dist/chunk-N5HFDWQX.js.map +1 -0
  18. package/dist/chunk-PAD7WFFU.js +573 -0
  19. package/dist/chunk-PAD7WFFU.js.map +1 -0
  20. package/dist/{chunk-AWW4MUJ5.js → chunk-TK64DNVK.js} +12 -1
  21. package/dist/chunk-TK64DNVK.js.map +1 -0
  22. package/dist/{chunk-HONQY4HF.js → chunk-WRTFC5NG.js} +362 -17
  23. package/dist/chunk-WRTFC5NG.js.map +1 -0
  24. package/dist/client-BKi3vk0Q.d.ts +34 -0
  25. package/dist/client-BrsaXtDV.d.cts +34 -0
  26. package/dist/cloudflare/index.cjs +930 -3
  27. package/dist/cloudflare/index.cjs.map +1 -1
  28. package/dist/cloudflare/index.d.cts +213 -12
  29. package/dist/cloudflare/index.d.ts +213 -12
  30. package/dist/cloudflare/index.js +562 -281
  31. package/dist/cloudflare/index.js.map +1 -1
  32. package/dist/codegen/index.d.cts +1 -1
  33. package/dist/codegen/index.d.ts +1 -1
  34. package/dist/errors-BRc3I_eH.d.cts +73 -0
  35. package/dist/errors-BRc3I_eH.d.ts +73 -0
  36. package/dist/firestore-enterprise/index.cjs +3877 -0
  37. package/dist/firestore-enterprise/index.cjs.map +1 -0
  38. package/dist/firestore-enterprise/index.d.cts +141 -0
  39. package/dist/firestore-enterprise/index.d.ts +141 -0
  40. package/dist/firestore-enterprise/index.js +985 -0
  41. package/dist/firestore-enterprise/index.js.map +1 -0
  42. package/dist/firestore-standard/index.cjs +3117 -0
  43. package/dist/firestore-standard/index.cjs.map +1 -0
  44. package/dist/firestore-standard/index.d.cts +49 -0
  45. package/dist/firestore-standard/index.d.ts +49 -0
  46. package/dist/firestore-standard/index.js +283 -0
  47. package/dist/firestore-standard/index.js.map +1 -0
  48. package/dist/index.cjs +590 -550
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +9 -37
  51. package/dist/index.d.ts +9 -37
  52. package/dist/index.js +178 -555
  53. package/dist/index.js.map +1 -1
  54. package/dist/{registry-Fi074zVa.d.ts → registry-Bc7h6WTM.d.cts} +1 -1
  55. package/dist/{registry-B1qsVL0E.d.cts → registry-C2KUPVZj.d.ts} +1 -1
  56. package/dist/{scope-path-B1G3YiA7.d.cts → scope-path-CROFZGr9.d.cts} +1 -56
  57. package/dist/{scope-path-B1G3YiA7.d.ts → scope-path-CROFZGr9.d.ts} +1 -56
  58. package/dist/sqlite/index.cjs +3631 -0
  59. package/dist/sqlite/index.cjs.map +1 -0
  60. package/dist/sqlite/index.d.cts +111 -0
  61. package/dist/sqlite/index.d.ts +111 -0
  62. package/dist/sqlite/index.js +1164 -0
  63. package/dist/sqlite/index.js.map +1 -0
  64. package/package.json +33 -3
  65. package/dist/backend-BsR0lnFL.d.ts +0 -200
  66. package/dist/backend-Ct-fLlkG.d.cts +0 -200
  67. package/dist/chunk-AWW4MUJ5.js.map +0 -1
  68. package/dist/chunk-HONQY4HF.js.map +0 -1
  69. package/dist/types-DxYLy8Ol.d.cts +0 -770
  70. package/dist/types-DxYLy8Ol.d.ts +0 -770
package/README.md CHANGED
@@ -35,9 +35,11 @@ npm install -D tsup typescript
35
35
  ```typescript
36
36
  import { Firestore } from '@google-cloud/firestore';
37
37
  import { createGraphClient, generateId } from 'firegraph';
38
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
38
39
 
39
40
  const db = new Firestore();
40
- const g = createGraphClient(db, 'graph');
41
+ const backend = createFirestoreStandardBackend(db, 'graph');
42
+ const g = createGraphClient(backend);
41
43
 
42
44
  // Create nodes
43
45
  const tourId = generateId();
@@ -83,18 +85,149 @@ UIDs **must** be generated via `generateId()` (21-char nanoid). Short sequential
83
85
 
84
86
  ```typescript
85
87
  import { createGraphClient } from 'firegraph';
88
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
89
+ // or for Enterprise Firestore (Pipelines, DML, server-side traversal, FTS, geo):
90
+ import { createFirestoreEnterpriseBackend } from 'firegraph/firestore-enterprise';
86
91
 
87
- const g = createGraphClient(db, 'graph');
92
+ const backend = createFirestoreStandardBackend(db, 'graph');
93
+ const g = createGraphClient(backend);
88
94
  // or with options:
89
- const g = createGraphClient(db, 'graph', { registry });
95
+ const g = createGraphClient(backend, { registry });
90
96
  ```
91
97
 
98
+ For non-Firestore backends (SQLite, Cloudflare DO, routing backend) use `createGraphClientFromBackend`, which accepts any raw `StorageBackend<C>` without requiring a named factory:
99
+
100
+ ```typescript
101
+ import { createGraphClientFromBackend } from 'firegraph';
102
+ const g = createGraphClientFromBackend(backend, opts, metaBackend);
103
+ ```
104
+
105
+ `createGraphClientFromBackend` is a deprecated alias for `createGraphClient` — prefer `createGraphClient` directly. Both accept the same `opts` and `metaBackend` arguments.
106
+
92
107
  **Parameters:**
93
108
 
94
- - `db` — A `Firestore` instance from `@google-cloud/firestore`
95
- - `collectionPath` — Firestore collection path for all graph data
96
- - `options.registry` — Optional `GraphRegistry` for schema validation
97
- - `options.queryMode` — Query backend: `'pipeline'` (default) or `'standard'`
109
+ - `backend` — A `StorageBackend<C>` from `createFirestoreStandardBackend`, `createFirestoreEnterpriseBackend`, or another backend factory
110
+ - `opts.registry` — Optional `GraphRegistry` for schema validation
111
+ - `opts.registryMode` — Optional dynamic registry config (`{ mode: 'dynamic', collection? }`). Pass alongside `opts.registry` for merged mode (static + dynamic).
112
+ - `opts.migrationWriteBack` — Optional global write-back mode (`'off'` | `'eager'` | `'background'`)
113
+ - `opts.migrationSandbox` — Optional custom migration evaluator (overrides the default SES executor)
114
+ - `opts.queryMode` — Optional Firestore query backend (`'pipeline'` | `'standard'`; default `'pipeline'`). Ignored by non-Firestore backends.
115
+ - `opts.scanProtection` — Optional full-collection-scan gate (`'off'` | `'warn'` | `'error'`; default `'error'`)
116
+ - `metaBackend` — Optional separate backend for meta-type storage (dynamic registry)
117
+
118
+ ### Capability System
119
+
120
+ Every client exposes a `capabilities` property (a `BackendCapabilities` set) that reflects what the underlying backend supports. Use it for portable feature checks at runtime:
121
+
122
+ ```typescript
123
+ if (client.capabilities.has('query.join')) {
124
+ const result = await (client as JoinExtension).expand({ ... });
125
+ }
126
+ ```
127
+
128
+ `GraphClient<C>` is a generic type — the type parameter `C` is a union of the backend's declared `Capability` strings and controls which extension methods are present on the type. `CoreGraphClient` is the unconditional base (read + write + transactions + batch + subgraph + `capabilities`). Helper functions that should accept any client should be typed to `CoreGraphClient` or `GraphReader`/`GraphWriter`.
129
+
130
+ **Capability values:**
131
+
132
+ | Capability | Methods unlocked | Backends |
133
+ | ----------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
134
+ | `core.read` / `core.write` / `core.batch` / `core.subgraph` | `getNode`, `putNode`, `findEdges`, `batch()`, `subgraph()`, etc. | All |
135
+ | `core.transactions` | `runTransaction(fn)` | Firestore (both), SQLite (`better-sqlite3` only; absent on D1); **absent on Cloudflare DO** |
136
+ | `query.aggregate` | `aggregate(spec)` | All; `min`/`max` only on SQLite + DO (both Firestore editions reject `min`/`max` — classic `Query.aggregate` exposes only count/sum/avg) |
137
+ | `query.select` | `findEdgesProjected(params)` | All |
138
+ | `query.join` | `expand(params)` | All |
139
+ | `query.dml` | `bulkDelete(params)`, `bulkUpdate(params)` | Enterprise (requires `previewDml: true`), SQLite, DO |
140
+ | `traversal.serverSide` | `runEngineTraversal(params)` | Enterprise |
141
+ | `search.vector` | `findNearest(params)` | Firestore (both) |
142
+ | `search.fullText` | `fullTextSearch(params)` | Enterprise. **Note:** the `fields` option is not yet supported — passing a non-empty `fields` array throws `INVALID_QUERY`. |
143
+ | `search.geo` | `geoSearch(params)` | Enterprise |
144
+ | `raw.firestore` | _(reserved — no methods yet)_ | Firestore (both) |
145
+ | `raw.sql` | _(reserved — no methods yet)_ | SQLite |
146
+ | `realtime.listen` | _(reserved — no methods yet)_ | _(none currently)_ |
147
+
148
+ ### Extension Methods
149
+
150
+ Methods unlocked by optional capabilities. Cast the client to the extension interface or check `client.capabilities` at runtime.
151
+
152
+ ```typescript
153
+ // query.aggregate — count / sum / avg (all backends); min / max (SQLite + DO only)
154
+ const stats = await (client as AggregateExtension).aggregate({
155
+ aType: 'tour',
156
+ axbType: 'is',
157
+ bType: 'tour',
158
+ ops: [
159
+ { op: 'count', alias: 'total' },
160
+ { op: 'avg', field: 'data.price', alias: 'avgPrice' },
161
+ ],
162
+ });
163
+
164
+ // query.select — projected edge scan
165
+ const names = await (client as SelectExtension).findEdgesProjected({
166
+ aType: 'tour',
167
+ axbType: 'is',
168
+ bType: 'tour',
169
+ fields: ['data.name', 'aUid'],
170
+ });
171
+
172
+ // query.join — server-side expand (fan-out from a set of sources)
173
+ const legs = await (client as JoinExtension).expand({
174
+ aType: 'tour',
175
+ axbType: 'hasDeparture',
176
+ bType: 'departure',
177
+ sources: [{ aUid: tourId }],
178
+ });
179
+
180
+ // query.dml — bulk delete / update (Enterprise opt-in; SQLite + DO always on)
181
+ await (client as DmlExtension).bulkDelete({
182
+ aType: 'tour',
183
+ axbType: 'hasDeparture',
184
+ bType: 'departure',
185
+ aUid: tourId,
186
+ });
187
+ await (client as DmlExtension).bulkUpdate(
188
+ {
189
+ aType: 'tour',
190
+ axbType: 'is',
191
+ bType: 'tour',
192
+ filters: [{ field: 'data.status', op: '==', value: 'draft' }],
193
+ },
194
+ { 'data.status': 'archived' },
195
+ );
196
+
197
+ // traversal.serverSide — multi-hop traversal in one Pipeline round-trip (Enterprise)
198
+ const tree = await (client as TraversalExtension).runEngineTraversal({
199
+ sources: [{ aType: 'tour', aUid: tourId }],
200
+ hops: [{ axbType: 'hasDeparture', bType: 'departure', limitPerSource: 10 }],
201
+ });
202
+
203
+ // search.vector — approximate nearest-neighbour (Firestore both editions)
204
+ const similar = await (client as VectorExtension).findNearest({
205
+ aType: 'tour',
206
+ axbType: 'is',
207
+ bType: 'tour',
208
+ queryVector: [0.1, 0.2, 0.3],
209
+ vectorField: 'data.embedding',
210
+ limit: 5,
211
+ });
212
+
213
+ // search.fullText — full-text search (Enterprise; `fields` throws INVALID_QUERY if non-empty)
214
+ const results = await (client as FullTextExtension).fullTextSearch({
215
+ aType: 'tour',
216
+ axbType: 'is',
217
+ bType: 'tour',
218
+ query: 'dolomites',
219
+ });
220
+
221
+ // search.geo — geospatial radius search (Enterprise)
222
+ const nearby = await (client as GeoExtension).geoSearch({
223
+ aType: 'tour',
224
+ axbType: 'is',
225
+ bType: 'tour',
226
+ geoField: 'data.location',
227
+ center: { latitude: 46.4, longitude: 11.9 },
228
+ radiusMeters: 50000,
229
+ });
230
+ ```
98
231
 
99
232
  ### Nodes
100
233
 
@@ -151,6 +284,10 @@ await g.replaceEdge('tour', tourId, 'hasDeparture', 'departure', depId, { order:
151
284
 
152
285
  // Delete an edge
153
286
  await g.removeEdge(tourId, 'hasDeparture', depId);
287
+
288
+ // Bulk delete all edges matching a filter (available on all backends)
289
+ const result = await g.bulkRemoveEdges({ aUid: tourId, axbType: 'hasDeparture' });
290
+ // → BulkResult { deleted: number, errors: BulkBatchError[] }
154
291
  ```
155
292
 
156
293
  ### Field Deletion
@@ -286,11 +423,12 @@ await g.runTransaction(async (tx) => {
286
423
 
287
424
  #### Run Options
288
425
 
289
- | Option | Type | Default | Description |
290
- | --------------------- | --------- | ------- | ---------------------------- |
291
- | `maxReads` | `number` | `100` | Total Firestore read budget |
292
- | `concurrency` | `number` | `5` | Max parallel queries per hop |
293
- | `returnIntermediates` | `boolean` | `false` | Include edges from all hops |
426
+ | Option | Type | Default | Description |
427
+ | --------------------- | ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
428
+ | `maxReads` | `number` | `100` | Total read budget |
429
+ | `concurrency` | `number` | `5` | Max parallel queries per hop |
430
+ | `returnIntermediates` | `boolean` | `false` | Include edges from all hops |
431
+ | `engineTraversal` | `'auto' \| 'force' \| 'off'` | `'auto'` | Engine-level traversal on Enterprise backends. `'auto'` silently falls back if ineligible; `'force'` throws if unavailable; `'off'` disables |
294
432
 
295
433
  When `filter` is set, the `limit` is applied after filtering (in-memory), so Firestore returns all matching edges and the filter + slice happens client-side.
296
434
 
@@ -300,6 +438,7 @@ Optional type validation using Zod (or any object with a `.parse()` method):
300
438
 
301
439
  ```typescript
302
440
  import { createRegistry, createGraphClient } from 'firegraph';
441
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
303
442
  import { z } from 'zod';
304
443
 
305
444
  const registry = createRegistry([
@@ -320,7 +459,8 @@ const registry = createRegistry([
320
459
  },
321
460
  ]);
322
461
 
323
- const g = createGraphClient(db, 'graph', { registry });
462
+ const backend = createFirestoreStandardBackend(db, 'graph');
463
+ const g = createGraphClient(backend, { registry });
324
464
 
325
465
  // This validates against the registry before writing:
326
466
  const id = generateId();
@@ -337,8 +477,10 @@ For agent-driven or runtime-extensible schemas, firegraph supports a **dynamic r
337
477
 
338
478
  ```typescript
339
479
  import { createGraphClient } from 'firegraph';
480
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
340
481
 
341
- const g = createGraphClient(db, 'graph', {
482
+ const backend = createFirestoreStandardBackend(db, 'graph');
483
+ const g = createGraphClient(backend, {
342
484
  registryMode: { mode: 'dynamic' },
343
485
  });
344
486
 
@@ -371,7 +513,7 @@ Key behaviors:
371
513
  - **After `reloadRegistry()`**: Domain writes are validated against the compiled registry. Unknown types are always rejected.
372
514
  - **Upsert semantics**: Calling `defineNodeType('tour', ...)` twice overwrites the previous definition. After reloading, the latest schema is used.
373
515
  - **Separate collection**: Meta-nodes can be stored in a different collection via `registryMode: { mode: 'dynamic', collection: 'meta' }`.
374
- - **Merged mode**: Provide both `registry` (static) and `registryMode` (dynamic) to get a merged registry where static entries take priority and dynamic definitions can only add new types not override existing ones.
516
+ - **Merged mode**: Pass both `registry` (the static side, typically built via `createRegistry` or `createMergedRegistry`) and `registryMode: { mode: 'dynamic' }`. Firegraph then merges them static entries take priority and dynamic definitions can only add new types, never override existing ones. There is no separate `mode: 'merged'` value; merged behavior is implied by supplying both options together.
375
517
 
376
518
  Dynamic registry returns a `DynamicGraphClient` which extends `GraphClient` with `defineNodeType()`, `defineEdgeType()`, and `reloadRegistry()`. Transactions and batches also validate against the compiled dynamic registry.
377
519
 
@@ -381,6 +523,7 @@ Firegraph supports schema versioning with automatic migration of records on read
381
523
 
382
524
  ```typescript
383
525
  import { createRegistry, createGraphClient } from 'firegraph';
526
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
384
527
  import type { MigrationStep } from 'firegraph';
385
528
 
386
529
  const migrations: MigrationStep[] = [
@@ -399,7 +542,8 @@ const registry = createRegistry([
399
542
  },
400
543
  ]);
401
544
 
402
- const g = createGraphClient(db, 'graph', { registry });
545
+ const backend = createFirestoreStandardBackend(db, 'graph');
546
+ const g = createGraphClient(backend, { registry });
403
547
 
404
548
  // Reading a v0 record automatically migrates it to v2 in memory
405
549
  const tour = await g.getNode(tourId);
@@ -427,7 +571,8 @@ Resolution order: `entry.migrationWriteBack > client.migrationWriteBack > 'off'`
427
571
 
428
572
  ```typescript
429
573
  // Global default
430
- const g = createGraphClient(db, 'graph', {
574
+ const backend = createFirestoreStandardBackend(db, 'graph');
575
+ const g = createGraphClient(backend, {
431
576
  registry,
432
577
  migrationWriteBack: 'background',
433
578
  });
@@ -461,7 +606,8 @@ Stored migration strings must be self-contained — no `import`, `require`, or e
461
606
  For custom sandboxing, pass `migrationSandbox` to `createGraphClient()`:
462
607
 
463
608
  ```typescript
464
- const g = createGraphClient(db, 'graph', {
609
+ const backend = createFirestoreStandardBackend(db, 'graph');
610
+ const g = createGraphClient(backend, {
465
611
  registryMode: { mode: 'dynamic' },
466
612
  migrationSandbox: (source) => {
467
613
  const compartment = new Compartment({
@@ -529,7 +675,8 @@ const registry = createRegistry([
529
675
  { aType: 'task', axbType: 'is', bType: 'task', allowedIn: ['workspace', '**/workspace'] },
530
676
  ]);
531
677
 
532
- const g = createGraphClient(db, 'graph', { registry });
678
+ const backend = createFirestoreStandardBackend(db, 'graph');
679
+ const g = createGraphClient(backend, { registry });
533
680
 
534
681
  // Agent only at root
535
682
  await g.putNode('agent', agentId, {}); // OK
@@ -603,6 +750,7 @@ Edges that connect nodes across different subgraphs. The key rule: **edges live
603
750
 
604
751
  ```typescript
605
752
  import { createGraphClient, createRegistry, createTraversal, generateId } from 'firegraph';
753
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
606
754
 
607
755
  // Registry declares that 'assignedTo' edges live in the 'workflow' subgraph
608
756
  const registry = createRegistry([
@@ -611,7 +759,8 @@ const registry = createRegistry([
611
759
  { aType: 'task', axbType: 'assignedTo', bType: 'agent', targetGraph: 'workflow' },
612
760
  ]);
613
761
 
614
- const g = createGraphClient(db, 'graph', { registry });
762
+ const backend = createFirestoreStandardBackend(db, 'graph');
763
+ const g = createGraphClient(backend, { registry });
615
764
 
616
765
  // Create a task in the root graph
617
766
  const taskId = generateId();
@@ -686,7 +835,7 @@ This uses Firestore collection group queries and requires collection group index
686
835
 
687
836
  #### Multi-Hop Limitation
688
837
 
689
- Each hop resolves its reader from the root client. If hop 1 crosses into a subgraph, hop 2 does **not** stay in that subgraph it reverts to the root. To chain hops within a subgraph, create a separate traversal from the subgraph client:
838
+ Each hop carries its reader context forward if hop 1 crosses into a subgraph, hop 2 stays in that subgraph. To return to the root or traverse a different subgraph, create a separate traversal from the desired client:
690
839
 
691
840
  ```typescript
692
841
  // This traversal finds agents in the workflow subgraph
@@ -722,19 +871,22 @@ const id = generateId(); // 21-char URL-safe nanoid
722
871
 
723
872
  All errors extend `FiregraphError` with a `code` property:
724
873
 
725
- | Error Class | Code | When |
726
- | ------------------------ | ------------------------ | --------------------------------------------------------------- |
727
- | `FiregraphError` | varies | Base class |
728
- | `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
729
- | `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails |
730
- | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry JSON Schema validation) |
731
- | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
732
- | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
733
- | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
734
- | `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
735
- | `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
736
- | `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
737
- | `TraversalError` | `TRAVERSAL_ERROR` | `run()` called with zero hops |
874
+ | Error Class | Code | When |
875
+ | ------------------------------ | --------------------------- | ----------------------------------------------------------------------- |
876
+ | `FiregraphError` | varies | Base class |
877
+ | `NodeNotFoundError` | `NODE_NOT_FOUND` | Node lookup fails (not thrown by `getNode` — it returns `null`) |
878
+ | `EdgeNotFoundError` | `EDGE_NOT_FOUND` | Edge lookup fails (not thrown by `getEdge` — it returns `null`) |
879
+ | `ValidationError` | `VALIDATION_ERROR` | Schema validation fails (registry JSON Schema validation) |
880
+ | `RegistryViolationError` | `REGISTRY_VIOLATION` | Triple not registered |
881
+ | `RegistryScopeError` | `REGISTRY_SCOPE` | Type not allowed at this subgraph scope |
882
+ | `MigrationError` | `MIGRATION_ERROR` | Migration function fails or chain is incomplete |
883
+ | `DynamicRegistryError` | `DYNAMIC_REGISTRY_ERROR` | Dynamic registry misconfiguration or misuse |
884
+ | `InvalidQueryError` | `INVALID_QUERY` | `findEdges` called with no filters |
885
+ | `QuerySafetyError` | `QUERY_SAFETY` | Query would cause a full collection scan |
886
+ | `TraversalError` | `TRAVERSAL_ERROR` | `run()` called with zero hops |
887
+ | `CapabilityNotSupportedError` | `CAPABILITY_NOT_SUPPORTED` | Capability-gated method called on a backend that doesn't declare it |
888
+ | `CrossBackendTransactionError` | `CROSS_BACKEND_TRANSACTION` | `runTransaction()` attempted across backends with different storage |
889
+ | `DiscoveryError` | `DISCOVERY_ERROR` | Entity discovery fails (missing required files, malformed schema, etc.) |
738
890
 
739
891
  ```typescript
740
892
  import { FiregraphError, ValidationError } from 'firegraph';
@@ -765,15 +917,49 @@ import type {
765
917
  QueryPlan,
766
918
  QueryFilter,
767
919
  QueryOptions,
768
-
769
- // Client interfaces
920
+ QueryMode,
921
+ ScanProtection,
922
+ WhereClause,
923
+ IndexFieldSpec,
924
+ IndexSpec,
925
+
926
+ // Client interfaces — CoreGraphClient is the unconditional base
927
+ Capability,
928
+ CoreGraphClient,
770
929
  GraphReader,
771
930
  GraphWriter,
772
- GraphClient,
931
+ GraphClient, // generic GraphClient<C extends Capability>
773
932
  GraphTransaction,
774
933
  GraphBatch,
775
934
  GraphClientOptions,
776
935
 
936
+ // Capability-gated extensions
937
+ AggregateExtension,
938
+ AggregateField,
939
+ AggregateOp,
940
+ AggregateResult,
941
+ AggregateSpec,
942
+ SelectExtension,
943
+ FindEdgesProjectedParams,
944
+ ProjectedRow,
945
+ JoinExtension,
946
+ ExpandParams,
947
+ ExpandResult,
948
+ DmlExtension,
949
+ BulkUpdatePatch,
950
+ BulkOptions,
951
+ BulkResult,
952
+ BulkBatchError,
953
+ BulkProgress,
954
+ VectorExtension,
955
+ FindNearestParams,
956
+ DistanceMeasure,
957
+ FullTextSearchExtension,
958
+ GeoExtension,
959
+ RawFirestoreExtension,
960
+ RawSqlExtension,
961
+ RealtimeListenExtension,
962
+
777
963
  // Registry
778
964
  RegistryEntry, // includes targetGraph, allowedIn
779
965
  GraphRegistry, // includes lookupByAxbType
@@ -781,9 +967,12 @@ import type {
781
967
 
782
968
  // Dynamic Registry
783
969
  DynamicGraphClient,
970
+ DynamicGraphMethods,
784
971
  DynamicRegistryConfig,
785
972
  NodeTypeData,
786
973
  EdgeTypeData,
974
+ DefineTypeOptions,
975
+ CascadeResult,
787
976
 
788
977
  // Migration
789
978
  MigrationFn,
@@ -791,6 +980,7 @@ import type {
791
980
  StoredMigrationStep,
792
981
  MigrationExecutor,
793
982
  MigrationWriteBack,
983
+ MigrationResult,
794
984
 
795
985
  // Traversal
796
986
  HopDefinition, // includes targetGraph
@@ -801,10 +991,14 @@ import type {
801
991
 
802
992
  // Entity Discovery
803
993
  DiscoveredEntity,
804
- DiscoveryResult,
994
+ DiscoverResult, // return type of discoverEntities()
995
+ DiscoveryResult, // { nodes: Map<...>, edges: Map<...> } — the .result field of DiscoverResult
996
+ DiscoveryWarning,
805
997
  } from 'firegraph';
806
998
  ```
807
999
 
1000
+ > **Note:** Several types are defined in the library but not yet exported from the `'firegraph'` entry point: the parameter and result types for `fullTextSearch()`, `geoSearch()`, and `runEngineTraversal()` (`FullTextSearchParams`, `GeoSearchParams`, `GeoPointLiteral`, `EngineHopSpec`, `EngineTraversalParams`, `EngineTraversalResult`), and the extension interface `EngineTraversalExtension`. Rely on type inference or declare local `Parameters<typeof client.fullTextSearch>[0]`-style helpers until these types are promoted to the public export.
1001
+
808
1002
  ## How It Works
809
1003
 
810
1004
  ### Storage Layout
@@ -832,68 +1026,118 @@ When you call `findEdges`, the query planner decides the strategy:
832
1026
 
833
1027
  ### Traversal Execution
834
1028
 
835
- 1. Start with `sourceUids = [startUid]`
836
- 2. For each hop in sequence:
837
- - Resolve `targetGraph`: check hop override, then registry, then none
838
- - If cross-graph (forward + `targetGraph` + `GraphClient` reader): create a subgraph reader via `reader.subgraph(sourceUid, targetGraph)` for each source
839
- - Fan out: query edges for each source UID (parallel, bounded by semaphore)
840
- - Each `findEdges` call counts as 1 read against the budget
841
- - Apply in-memory `filter` if specified, then apply `limit`
842
- - Collect edges, extract next source UIDs (deduplicated)
843
- - If budget exceeded, mark `truncated` and stop
844
- 3. Return final hop edges as `nodes`, all hop data in `hops`
1029
+ Traversal dispatches through three tiers in order:
1030
+
1031
+ 1. **Engine-level** (Firestore Enterprise, `traversal.serverSide`): collapses the entire hop chain into one nested-Pipeline server-side round trip. Requires every hop to have a positive `limitPerSource`, no JS `filter` predicates, no cross-graph hops, and depth ≤ 5. Counts as `totalReads: 1`. Controlled by `engineTraversal` option (`'auto'` by default). Sets `truncated: true` on any hop whose returned edge count reaches `limitPerSource`; `result.truncated` is `true` when any hop is truncated.
1032
+
1033
+ 2. **Expand fast-path** (`query.join`): one `expand()` call per hop instead of one `findEdges` per source. Counts as 1 read per hop regardless of source-set size.
1034
+
1035
+ 3. **Per-source loop** (all backends): fan-out over source UIDs in parallel (bounded by semaphore). Each `findEdges` call counts as 1 read against the budget.
1036
+
1037
+ For each hop the traversal also: resolves `targetGraph` (hop override → registry → none), creates subgraph readers for cross-graph hops, applies in-memory `filter` + `limit`, deduplicates next source UIDs, and stops with `truncated = true` if the budget is exceeded.
845
1038
 
846
1039
  ## Query Modes
847
1040
 
848
- Firegraph supports two query backends. The mode is set when creating a client:
1041
+ Firegraph ships two Firestore backends that you choose at construction time:
849
1042
 
850
1043
  ```typescript
851
- // Pipeline mode (default) requires Enterprise Firestore
852
- const g = createGraphClient(db, 'graph');
1044
+ import { createGraphClient } from 'firegraph';
1045
+ import { createFirestoreStandardBackend } from 'firegraph/firestore-standard';
1046
+ import { createFirestoreEnterpriseBackend } from 'firegraph/firestore-enterprise';
1047
+
1048
+ // Standard — works on any Firestore project, uses classic .where().get() queries
1049
+ const backend = createFirestoreStandardBackend(db, 'graph');
1050
+ const g = createGraphClient(backend, { registry });
1051
+
1052
+ // Enterprise — uses Firestore Pipelines by default; requires Enterprise Firestore
1053
+ const backend = createFirestoreEnterpriseBackend(db, 'graph');
1054
+ const g = createGraphClient(backend, { registry });
853
1055
 
854
- // Standard mode (opt-in) for emulator or small datasets
855
- const g = createGraphClient(db, 'graph', { queryMode: 'standard' });
1056
+ // Enterprise with classic query path (e.g. to avoid full-collection scans)
1057
+ const backend = createFirestoreEnterpriseBackend(db, 'graph', { defaultQueryMode: 'classic' });
856
1058
  ```
857
1059
 
858
- ### Pipeline Mode (Default)
1060
+ ### Standard Backend (`firegraph/firestore-standard`)
1061
+
1062
+ Uses classic Firestore queries (`.where().get()`). Works on any Firestore project (no Enterprise edition required). Limitations:
1063
+
1064
+ | `data.*` Filters | Risk |
1065
+ | ----------------------------- | --------------------------------- |
1066
+ | Fails without composite index | Query errors for unindexed fields |
1067
+
1068
+ Appropriate for:
1069
+
1070
+ - Any Firestore project (Standard or Enterprise edition)
1071
+ - **Emulator** testing — classic queries work out of the box
1072
+ - Projects that manage their own composite indexes
859
1073
 
860
- Uses the Firestore Pipeline API (`db.pipeline()`). This is the recommended mode for production.
1074
+ ### Enterprise Backend (`firegraph/firestore-enterprise`)
1075
+
1076
+ Uses the Firestore Pipeline API (`db.pipeline()`) by default. Requires **Firestore Enterprise** edition.
861
1077
 
862
1078
  - Enables queries on `data.*` fields without composite indexes
863
- - Requires **Firestore Enterprise** edition
864
- - Pipeline API is currently in Preview
1079
+ - Unlocks additional capabilities: `query.dml`, `traversal.serverSide`, `search.fullText`, `search.geo`
865
1080
 
866
- ### Standard Mode
1081
+ **Emulator auto-fallback:** when `FIRESTORE_EMULATOR_HOST` is detected, the Enterprise backend automatically switches to the classic query path (pipelines aren't supported in the emulator). No configuration needed.
867
1082
 
868
- Uses standard Firestore queries (`.where().get()`). Use only if you understand the limitations:
1083
+ **Transactions** always use the classic query path regardless of `defaultQueryMode`, because Pipeline queries are not transactionally bound.
869
1084
 
870
- | Firestore Edition | With `data.*` Filters | Risk |
871
- | ----------------- | -------------------------------------- | --------------------------------- |
872
- | Enterprise | Full collection scan (no index needed) | High billing on large collections |
873
- | Standard | Fails without composite index | Query errors for unindexed fields |
1085
+ ### SQLite Backend (`firegraph/sqlite`)
874
1086
 
875
- Standard mode is appropriate for:
1087
+ Shared-table SQLite backend for Node.js (`better-sqlite3`) and Cloudflare D1. Supports all four core capabilities plus `query.aggregate`, `query.select`, `query.join`, and `query.dml`. Does not support `search.*`.
876
1088
 
877
- - **Emulator** — the emulator doesn't support pipelines, so firegraph auto-falls back to standard mode when `FIRESTORE_EMULATOR_HOST` is set
878
- - **Small datasets** where full scans are acceptable
879
- - Projects that manage their own composite indexes
1089
+ ```typescript
1090
+ import { createSqliteBackend } from 'firegraph/sqlite';
1091
+ import { createGraphClientFromBackend } from 'firegraph';
880
1092
 
881
- ### Emulator Auto-Fallback
1093
+ const backend = createSqliteBackend(executor, 'graph');
1094
+ const g = createGraphClientFromBackend(backend, { registry });
1095
+ ```
882
1096
 
883
- When `FIRESTORE_EMULATOR_HOST` is detected, firegraph automatically uses standard mode regardless of the `queryMode` setting. No configuration needed.
1097
+ Note: `core.transactions` is only declared when `executor.transaction` is defined `better-sqlite3` provides this, but Cloudflare D1 does not.
884
1098
 
885
- ### Transactions
1099
+ ### Cloudflare Durable Object Backend (`firegraph/cloudflare`)
1100
+
1101
+ Runs inside a Durable Object via `state.storage.sql`. Same capability set as SQLite minus `core.transactions` (the DO's single-threaded executor cannot block on transaction callbacks) and `raw.sql` (the DO SQL surface is hidden behind RPC).
1102
+
1103
+ ```typescript
1104
+ // In your DO class file (workerd bundle):
1105
+ import { FiregraphDO } from 'firegraph/cloudflare';
1106
+ export class MyGraphDO extends FiregraphDO {}
1107
+
1108
+ // In your backend code (Node):
1109
+ import { DORPCBackend, createDOClient } from 'firegraph/cloudflare';
1110
+ const g = createDOClient(env.MY_GRAPH, 'graph', { registry });
1111
+ ```
1112
+
1113
+ `firegraph/cloudflare` also re-exports `createRegistry`, `createMergedRegistry`, `generateId`, `META_NODE_TYPE`, `META_EDGE_TYPE`, and `deleteField()` so workerd-bundled code can build registries without statically importing `@google-cloud/firestore`.
1114
+
1115
+ `createSiblingClient(client, siblingRootKey)` creates a peer root-level `DOGraphClient` for a sibling collection within the same Durable Object — useful when a DO hosts multiple logical graph roots.
1116
+
1117
+ ### Routing Backend (`firegraph/backend`)
1118
+
1119
+ Assembles a single capability-typed backend that routes operations to the appropriate per-subgraph backend. Use when different subgraphs live in different storage systems.
1120
+
1121
+ ```typescript
1122
+ import { createRoutingBackend } from 'firegraph/backend';
1123
+ import { createGraphClientFromBackend } from 'firegraph';
1124
+
1125
+ const backend = createRoutingBackend(defaultBackend, {
1126
+ 'users/*': userBackend,
1127
+ });
1128
+ const g = createGraphClientFromBackend(backend, { registry });
1129
+ ```
886
1130
 
887
- Transactions always use standard Firestore queries, even when the client is in pipeline mode. This is because Pipeline queries are not transactionally bound — they see committed state, not the transaction's isolated view.
1131
+ `firegraph/backend` also exports `StorageBackend`, `BackendCapabilities`, `createCapabilities`, and `intersectCapabilities` for authors implementing custom backends.
888
1132
 
889
1133
  ### Config File
890
1134
 
891
- Set the query mode in `firegraph.config.ts`:
1135
+ Set the default backend in `firegraph.config.ts`:
892
1136
 
893
1137
  ```typescript
894
1138
  export default defineConfig({
895
1139
  entities: './entities',
896
- queryMode: 'pipeline', // or 'standard'
1140
+ queryMode: 'pipeline', // 'pipeline' selects Enterprise, 'standard' selects Standard
897
1141
  });
898
1142
  ```
899
1143