@orkestrel/scaffold 0.0.67 → 0.0.69

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 (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +4 -4
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1567 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +507 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +445 -6
  62. package/dist/src/core/index.cjs +38 -16
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +37 -17
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,505 @@
1
+ # IndexedDB
2
+
3
+ > A lean, typed, Promise-based wrapper over the raw browser `IDBDatabase` /
4
+ > `IDBObjectStore` / `IDBIndex` / `IDBTransaction` API — object stores, secondary
5
+ > indexes, native key ranges, promisified cursors, multi-store transactions, and
6
+ > versioned schema upgrades, over `await` instead of raw `IDBRequest` events.
7
+
8
+ Its job is to type IndexedDB's event-driven, callback-shaped, structurally-untyped surface. It exposes exactly what raw IndexedDB offers natively and deliberately nothing else: there is no `where` / `filter` / `order` / aggregate query builder here; that would duplicate a general-purpose query engine this package does not ship. Source: [`src/browser`](../src/browser). Surfaced through the `@src/browser` barrel (published as `@orkestrel/indexeddb`).
9
+
10
+ ## Surface
11
+
12
+ Create a database from a store schema, then read and write through the lazily-connecting store handle it returns.
13
+
14
+ ```ts
15
+ import { createIndexedDBDatabase, rangeFromKey } from '@orkestrel/indexeddb'
16
+
17
+ // A store keyed by `id`, with one secondary index on `age`. `version: 1` creates
18
+ // the schema on first open; omit `version` for auto-managed mode (see Versioned
19
+ // upgrades, later in this guide).
20
+ const db = createIndexedDBDatabase({
21
+ name: 'app',
22
+ version: 1,
23
+ stores: {
24
+ users: { path: 'id', indexes: [{ name: 'byAge', path: 'age' }] },
25
+ },
26
+ })
27
+
28
+ const users = db.store('users') // lazily connects on first use — no explicit open
29
+ await users.set({ id: 'u1', name: 'Ada', age: 36 })
30
+ await users.set([
31
+ { id: 'u2', name: 'Bea', age: 17 },
32
+ { id: 'u3', name: 'Cy', age: 51 },
33
+ ]) // array in → array of keys out (array-first batch)
34
+
35
+ await users.get('u1') // point read by primary key → the row, or undefined
36
+ await users.index('byAge').records(rangeFromKey(18)) // adults, index-backed (O(log n))
37
+ ```
38
+
39
+ ### Database and factory
40
+
41
+ | API | Kind | Summary |
42
+ | ------------------------- | -------- | ------------------------------------------------------------------------------- |
43
+ | `createIndexedDBDatabase` | function | Creates a typed, lazily-connecting IndexedDB database over a store schema. |
44
+ | `IndexedDBDatabase` | class | Represents a browser-native IndexedDB database — a typed, Promise-based handle. |
45
+
46
+ ### Stores, indexes, cursors, transactions
47
+
48
+ | API | Kind | Summary |
49
+ | --------------------------- | ----- | ------------------------------------------------------------------------------------------------ |
50
+ | `IndexedDBStore` | class | Represents an object store — the full keyed CRUD surface plus index, count, and cursor access. |
51
+ | `IndexedDBIndex` | class | Represents a secondary index on a store — a read-only view keyed by an indexed path. |
52
+ | `IndexedDBCursor` | class | Represents a promisified value cursor over an object store or index. |
53
+ | `IndexedDBTransaction` | class | Represents an explicit transaction over one or more stores, with typed scope-bound store access. |
54
+ | `IndexedDBTransactionStore` | class | Represents an object store bound to an explicit transaction, with no implicit per-call commit. |
55
+
56
+ ### Helpers and errors
57
+
58
+ | API | Kind | Summary |
59
+ | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
60
+ | `supportsIndexedDB` | function | Checks whether IndexedDB is available in this environment. |
61
+ | `promisifyRequest` | function | Resolves an `IDBRequest` to its result, rejecting with an `IndexedDBError`. |
62
+ | `promisifyTransaction` | function | Resolves after an `IDBTransaction` commits, rejecting if it errors or aborts. |
63
+ | `readRecord` | function | Reads one record by key from a store or index, narrowing it to a `Row`. |
64
+ | `readRecords` | function | Reads many records from a store or index over an optional key range, narrowing each to a `Row`. |
65
+ | `hasKey` | function | Checks whether a key is present in a store or index. |
66
+ | `createIndex` | function | Creates a secondary index on a store from its `IndexDefinition`. |
67
+ | `wrapCall` | function | Runs a synchronous native IndexedDB call, wrapping a thrown `DOMException` into a typed `IndexedDBError`. |
68
+ | `rangeAboveKey` | function | Builds a key range strictly above one key. |
69
+ | `rangeFromKey` | function | Builds a key range starting at and including one key. |
70
+ | `rangeBelowKey` | function | Builds a key range strictly below one key. |
71
+ | `rangeToKey` | function | Builds a key range ending at and including one key. |
72
+ | `rangePrefix` | function | Builds a key range containing every string with one prefix. |
73
+ | `wrapError` | function | Maps a native IndexedDB `DOMException` to a typed `IndexedDBError`. |
74
+ | `IndexedDBError` | class | Represents an error thrown by the IndexedDB wrapper, carrying a machine-readable `code` and an optional `context` beside the native cause. |
75
+ | `isIndexedDBError` | function | Checks whether a value is an `IndexedDBError`. |
76
+
77
+ `supportsIndexedDB` reads `globalThis.indexedDB`, and `hasKey` is a native `count` greater than 0. `createIndex` is the shared index-DDL leaf both the built-in schema pass and `context.indexes.create` run. `wrapError` is the request boundary every bridge maps a native `DOMException` through.
78
+
79
+ ### Constants
80
+
81
+ A `Shape` cell holds the constant's declared type.
82
+
83
+ | API | Kind | Shape | Summary |
84
+ | ------------- | ----- | ---------------------------------------------- | --------------------------------------------------------------------- |
85
+ | `ERROR_CODES` | const | `Readonly<Record<string, IndexedDBErrorCode>>` | Maps native `DOMException.name` → the wrapper's `IndexedDBErrorCode`. |
86
+
87
+ `wrapError` reads this frozen map at the request boundary, falling back to `UNKNOWN` for a native name it does not carry.
88
+
89
+ ### Types
90
+
91
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`. An extended interface's name comes before `plus`, with the members it adds after.
92
+
93
+ | API | Kind | Shape | Summary |
94
+ | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `Row` | type | `Record<string, unknown>` | Represents a record stored in, and read from, an object store. |
96
+ | `KeyPath` | type | `string \| readonly string[]` | Represents a key path — one field, or several for a compound key. |
97
+ | `IndexDefinition` | interface | `{ name, path, unique?, multiple? }` | Represents the declaration of a secondary index — its name, key path, and uniqueness. |
98
+ | `StoreDefinition` | interface | `{ path?, increment?, indexes? }` | Represents the declaration of one object store — its key path, key generation, and secondary indexes. |
99
+ | `IndexedDBSchema` | type | `Readonly<Record<string, StoreDefinition>>` | Represents a database's schema — a map of store name to its `StoreDefinition`. |
100
+ | `IndexedDBUpgradeContext` | interface | `{ transaction, old, version, stores, indexes }` | Represents the escape hatch into a version-change upgrade, passed to `IndexedDBDatabaseOptions.upgrade`. |
101
+ | `IndexedDBUpgradeStoreManagerInterface` | interface | `{ names } plus create, drop, store` | Represents the store manager of a version-change upgrade. |
102
+ | `IndexedDBUpgradeIndexManagerInterface` | interface | `{} plus create, drop` | Represents the secondary-index manager of a version-change upgrade. |
103
+ | `IndexedDBDatabaseOptions` | interface | `{ name, version?, stores, upgrade? }` | Represents the options for `createIndexedDBDatabase`. |
104
+ | `IndexedDBCursorOptions` | interface | `{ query?, direction? }` | Represents the options for opening a cursor. |
105
+ | `IndexedDBErrorCode` | type | `'NOT_OPEN' \| 'CLOSED' \| 'NOT_FOUND' \| 'CONSTRAINT' \| 'QUOTA' \| 'ABORTED' \| 'DATA' \| 'OPEN' \| 'UPGRADE' \| 'INACTIVE' \| 'READONLY' \| 'INVALID' \| 'UNKNOWN'` | Represents a machine-readable `IndexedDBError` code. |
106
+ | `IndexedDBDatabaseInterface` | interface | `{ database, name, version, stores, open } plus connect, store, read, write, close, drop` | Represents the contract of a typed, lazily-connecting IndexedDB database. |
107
+ | `IndexedDBRecordStoreInterface` | interface | `{} plus get, resolve, records, keys, has, count, set, add, remove, clear, cursor` | Represents the contract the object-store surfaces share — the keyed record verbs, in or out of an explicit transaction. |
108
+ | `IndexedDBStoreInterface` | interface | `IndexedDBRecordStoreInterface plus { name, path, indexes, increment } plus index` | Represents the contract of an object store — the keyed record surface plus the store's own schema metadata and `index` accessor. |
109
+ | `IndexedDBIndexInterface` | interface | `{ name, path, unique, multiple } plus get, resolve, records, keys, primary, has, count, cursor` | Represents the contract of a secondary index — read access by an indexed key path. |
110
+ | `IndexedDBCursorInterface` | interface | `{ cursor, source, key, primary, value, direction } plus continue, seek, advance, update, remove` | Represents the contract of a promisified value cursor for streaming and in-place mutation. |
111
+ | `IndexedDBTransactionInterface` | interface | `{ transaction, mode, stores, active, finished, error } plus store, abort, commit` | Represents the contract of an explicit transaction over one or more stores. |
112
+ | `IndexedDBTransactionStoreInterface` | interface | `IndexedDBRecordStoreInterface plus { store }` | Represents the contract of an object store bound to an explicit transaction. |
113
+
114
+ Values are this package's own `Row` (a record), narrowed from IndexedDB's structured clone with `isRecord` (from `@orkestrel/contract`) at the read boundary — an `as`-free bridge. Keys are the full native `IDBValidKey`, so the wrapper speaks IndexedDB's whole key space.
115
+
116
+ A database connects **lazily**: the first store operation (or an explicit `connect`) opens it; you never wire up `onsuccess` yourself. `version` controls schema creation: pin an explicit number to create any missing stores on a bump, or **omit it** for auto-managed mode, where the database opens at its current version and bumps once on its own to create any declared store the stored schema lacks — so adding a store never needs a manual version bump.
117
+
118
+ ## Methods
119
+
120
+ The public methods of each behavioral interface — one table per type, keyed by its backticked name, every call-signature member listed. Each interface's `readonly` data members are named in the `Shape` column of the [Types](#types) table, earlier. Each class implements its interface exactly, so this doubles as the per-instance method surface (`AGENTS.md` § Documentation contract).
121
+
122
+ `IndexedDBUpgradeContext` carries only readonly data, so no Methods table follows for it. Its managers carry the upgrade's schema verbs: `context.stores` is the store manager, while `IndexedDBDatabaseInterface.stores` is the plain name list.
123
+
124
+ #### `IndexedDBDatabaseInterface`
125
+
126
+ | Method | Returns | Summary |
127
+ | --------- | ------------------------- | ------------------------------------------------------------------------------------ |
128
+ | `connect` | `Promise<IDBDatabase>` | Opens the connection, lazily and idempotently, waiting through a native block. |
129
+ | `store` | `IndexedDBStoreInterface` | Returns a typed handle for one declared store. |
130
+ | `read` | `Promise<void>` | Runs a readonly scope over one or more stores. |
131
+ | `write` | `Promise<void>` | Runs a readwrite scope, committing when it resolves and rolling back when it throws. |
132
+ | `close` | `void` | Retires the handle permanently, releasing the connection and any later open result. |
133
+ | `drop` | `Promise<void>` | Closes and deletes the database, waiting through a native block. |
134
+
135
+ #### `IndexedDBRecordStoreInterface`
136
+
137
+ The keyed record surface `IndexedDBStoreInterface` and `IndexedDBTransactionStoreInterface` both extend, declared once so neither can drift from the other. The keyed verbs batch by their array overload (one in → one out; array in → array out), array-first (`.claude/rules/patterns.md` § Managers § Batch operations). Each extending table that follows repeats these rows, because a consumer holding either interface calls them on it.
138
+
139
+ | Method | Returns | Summary |
140
+ | --------- | ------------------------------------------- | ---------------------------------------------------------------------- |
141
+ | `get` | `Promise<Row \| undefined>` | Reads the record at a primary key, or `undefined` on a miss. |
142
+ | `resolve` | `Promise<Row>` | Reads the record at a primary key, throwing `NOT_FOUND` on a miss. |
143
+ | `records` | `Promise<readonly Row[]>` | Reads the stored records over an optional key range. |
144
+ | `keys` | `Promise<readonly IDBValidKey[]>` | Lists the stored primary keys over an optional key range. |
145
+ | `has` | `Promise<boolean>` | Checks whether a primary key is present. |
146
+ | `count` | `Promise<number>` | Counts the stored records within an optional key range. |
147
+ | `set` | `Promise<IDBValidKey>` | Writes a record, overwriting whatever the key already holds. |
148
+ | `add` | `Promise<IDBValidKey>` | Writes a record, throwing `CONSTRAINT` where the key is already taken. |
149
+ | `remove` | `Promise<void>` | Deletes the record at a primary key. |
150
+ | `clear` | `Promise<void>` | Deletes every record the store holds. |
151
+ | `cursor` | `Promise<IndexedDBCursorInterface \| null>` | Opens a cursor over the records, or resolves `null` where none match. |
152
+
153
+ #### `IndexedDBStoreInterface`
154
+
155
+ `IndexedDBRecordStoreInterface` plus `index`. Each call runs in its own implicit transaction.
156
+
157
+ | Method | Returns | Summary |
158
+ | --------- | ------------------------------------------- | ---------------------------------------------------------------------------- |
159
+ | `get` | `Promise<Row \| undefined>` | Reads the record at a primary key, or `undefined` on a miss. |
160
+ | `resolve` | `Promise<Row>` | Reads the record at a primary key, throwing `NOT_FOUND` on a miss. |
161
+ | `records` | `Promise<readonly Row[]>` | Reads the stored records over an optional key range. |
162
+ | `keys` | `Promise<readonly IDBValidKey[]>` | Lists the stored primary keys over an optional key range. |
163
+ | `has` | `Promise<boolean>` | Checks whether a primary key is present. |
164
+ | `count` | `Promise<number>` | Counts the stored records within an optional key range. |
165
+ | `set` | `Promise<IDBValidKey>` | Writes a record, overwriting whatever the key already holds. |
166
+ | `add` | `Promise<IDBValidKey>` | Writes a record, throwing `CONSTRAINT` where the key is already taken. |
167
+ | `remove` | `Promise<void>` | Deletes the record at a primary key. |
168
+ | `clear` | `Promise<void>` | Deletes every record the store holds. |
169
+ | `index` | `IndexedDBIndexInterface` | Returns a read-only view over one of the store's declared secondary indexes. |
170
+ | `cursor` | `Promise<IndexedDBCursorInterface \| null>` | Opens a cursor over the records, or resolves `null` where none match. |
171
+
172
+ #### `IndexedDBIndexInterface`
173
+
174
+ | Method | Returns | Summary |
175
+ | --------- | ------------------------------------------- | -------------------------------------------------------------------------------- |
176
+ | `get` | `Promise<Row \| undefined>` | Reads the first record for an index key, or `undefined` on a miss. |
177
+ | `resolve` | `Promise<Row>` | Reads the first record for an index key, throwing `NOT_FOUND` on a miss. |
178
+ | `records` | `Promise<readonly Row[]>` | Reads the matching records over an optional index-key range. |
179
+ | `keys` | `Promise<readonly IDBValidKey[]>` | Lists the primary keys of the matching records over an optional index-key range. |
180
+ | `primary` | `Promise<IDBValidKey \| undefined>` | Reads the primary key an index key maps to, or `undefined` on a miss. |
181
+ | `has` | `Promise<boolean>` | Checks whether an index key is present. |
182
+ | `count` | `Promise<number>` | Counts the matching records within an optional index-key range. |
183
+ | `cursor` | `Promise<IndexedDBCursorInterface \| null>` | Opens a read-only cursor over the matches, or resolves `null` where none match. |
184
+
185
+ #### `IndexedDBCursorInterface`
186
+
187
+ Each readonly data member is a snapshot of the position the cursor stopped on, because IndexedDB reuses the live cursor object on every move.
188
+
189
+ | Method | Returns | Summary |
190
+ | ---------- | ------------------------------------------- | --------------------------------------------------------------------------- |
191
+ | `continue` | `Promise<IndexedDBCursorInterface \| null>` | Advances to the next record, or to the next record at or after a given key. |
192
+ | `seek` | `Promise<IndexedDBCursorInterface \| null>` | Advances to a given index key and primary key. |
193
+ | `advance` | `Promise<IndexedDBCursorInterface \| null>` | Skips forward a given number of records. |
194
+ | `update` | `Promise<IDBValidKey>` | Overwrites the record at the current position. |
195
+ | `remove` | `Promise<void>` | Deletes the record at the current position. |
196
+
197
+ #### `IndexedDBTransactionInterface`
198
+
199
+ | Method | Returns | Summary |
200
+ | -------- | ------------------------------------ | --------------------------------------------------------------------------------------------- |
201
+ | `store` | `IndexedDBTransactionStoreInterface` | Returns a scope-bound store, which must be one of the transaction's own stores. |
202
+ | `abort` | `void` | Rolls every write in the transaction back, throwing `INACTIVE` where it has already finished. |
203
+ | `commit` | `void` | Flushes the transaction early, throwing `INACTIVE` where it has already finished. |
204
+
205
+ #### `IndexedDBTransactionStoreInterface`
206
+
207
+ `IndexedDBRecordStoreInterface` bound to an explicit transaction — the same verbs as a store, without `index` and without an implicit per-call commit. Its `store` is the raw `IDBObjectStore` the owning transaction binds.
208
+
209
+ | Method | Returns | Summary |
210
+ | --------- | ------------------------------------------- | ---------------------------------------------------------------------- |
211
+ | `get` | `Promise<Row \| undefined>` | Reads the record at a primary key, or `undefined` on a miss. |
212
+ | `resolve` | `Promise<Row>` | Reads the record at a primary key, throwing `NOT_FOUND` on a miss. |
213
+ | `records` | `Promise<readonly Row[]>` | Reads the stored records over an optional key range. |
214
+ | `keys` | `Promise<readonly IDBValidKey[]>` | Lists the stored primary keys over an optional key range. |
215
+ | `has` | `Promise<boolean>` | Checks whether a primary key is present. |
216
+ | `count` | `Promise<number>` | Counts the stored records within an optional key range. |
217
+ | `set` | `Promise<IDBValidKey>` | Writes a record, overwriting whatever the key already holds. |
218
+ | `add` | `Promise<IDBValidKey>` | Writes a record, throwing `CONSTRAINT` where the key is already taken. |
219
+ | `remove` | `Promise<void>` | Deletes the record at a primary key. |
220
+ | `clear` | `Promise<void>` | Deletes every record the store holds. |
221
+ | `cursor` | `Promise<IndexedDBCursorInterface \| null>` | Opens a cursor over the records, or resolves `null` where none match. |
222
+
223
+ #### `IndexedDBUpgradeStoreManagerInterface`
224
+
225
+ `names` lists the store names the database holds at that point in the upgrade.
226
+
227
+ | Method | Returns | Summary |
228
+ | -------- | ------------------------------------ | ------------------------------------------------------------------------ |
229
+ | `create` | `void` | Creates a store from its definition, within the upgrade transaction. |
230
+ | `drop` | `void` | Deletes a store and everything it holds, within the upgrade transaction. |
231
+ | `store` | `IndexedDBTransactionStoreInterface` | Returns a transaction-bound store for migrating data during the upgrade. |
232
+
233
+ #### `IndexedDBUpgradeIndexManagerInterface`
234
+
235
+ Reached as `context.indexes`; the store a call names must already exist within the current upgrade transaction.
236
+
237
+ | Method | Returns | Summary |
238
+ | -------- | ------- | ------------------------------------------------------------------------------- |
239
+ | `create` | `void` | Creates a secondary index on an existing store, within the upgrade transaction. |
240
+ | `drop` | `void` | Removes a named secondary index from a store, within the upgrade transaction. |
241
+
242
+ ## Contract
243
+
244
+ These invariants hold across `src/browser` ↔ `indexeddb.md`:
245
+
246
+ 1. **DOC ↔ SOURCE bijection.** Every row in the `## Surface` tables is a real export of the wrapper, and every export appears as a Surface row — exhaustive, both directions (`AGENTS.md` § Documentation contract).
247
+ 2. **Native, not a query engine.** The wrapper exposes only what raw IndexedDB offers natively — object stores, secondary indexes, key-range helpers, cursors, and multi-store transactions. It has **no** `where` / `filter` / `order` / aggregate builder; that stays out of scope entirely, deliberately, so the wrapper never grows into a second query DSL.
248
+ 3. **`Row` values, `IDBValidKey` keys.** Reads return this package's own `Row` (narrowed with `isRecord`, never an unchecked cast); writes take a `Row`. Keys are the native `IDBValidKey`.
249
+ 4. **In-line or out-of-line keys.** A store with a `path` keys rows by that field; a store with no `path` is out-of-line and takes an explicit key on `set` / `add` (`set(row, key)`).
250
+ 5. **Batch by the array overload, array-first.** `get` / `resolve` / `has` / `remove` / `set` / `add` take one value for one result or an array for an array of results (`.claude/rules/patterns.md` § Managers § Batch operations). The array overload is declared first because an array is itself both a record and a compound `IDBValidKey`; to act on a single compound key, pass `IDBKeyRange.only([…])` to `records` / `count`.
251
+ 6. **Each standalone call is its own transaction; `read` / `write` are atomic.** A store method opens and commits its own implicit transaction; `db.read` / `db.write` run a scope across stores that commits on resolve and rolls back on a throw. The completion listener is attached BEFORE the scope runs (not after it resolves), so a scope whose last step is a non-IDB `await` — letting the native transaction auto-commit while the scope is still on the stack — still settles `read` / `write` instead of hanging: `complete` can otherwise fire before a listener attached only after the scope returns would ever be wired.
252
+ 7. **`get` / `records` narrow to records; `count` / `has` / `keys` operate on keys.** A store or index counts, tests presence, and lists keys over every stored value regardless of shape, but `get` / `resolve` / `records` narrow each value with `isRecord` — so a store holding a non-record value shows `count` greater than `records().length`, and `has` reads `true` for a key `get` reads back as `undefined` (a miss and a non-record value both read as `undefined` from `get`). A cursor reports the same boundary the same way: `cursor.value` is `Row | undefined` and reads `undefined` for a non-record stored value — see Cursor streaming and in-place mutation, later in this guide.
253
+ 8. **DOC ↔ SOURCE method bijection.** Every method in a `## Methods` table is a real call-signature member of that interface in source, and every public method of each behavioral interface is documented — exhaustive, both directions; and each implementing class exposes exactly its interface's public methods, no more (`AGENTS.md` § Documentation contract).
254
+
255
+ 9. **One atomic upgrade boundary.** The built-in create-missing-stores pass and the custom `upgrade` callback run inside the same versionchange transaction and failure boundary. A synchronous built-in/custom fault, or a custom rejection captured while that transaction remains active, aborts the whole upgrade and rejects `connect()` with `UPGRADE`; no partially created store, index, or migration survives. Its `cause` preserves the initiating value even when that value is `undefined`, and a native schema failure retains the nested typed chain (`UPGRADE` → `CONSTRAINT` → native `ConstraintError`). A failed open clears only its attempt-local state, so the same handle can retry. If auto-commit already occurred but the browser reports success after a failure was recorded, the wrapper closes that result before rejecting, preventing an orphan connection; closing cannot undo the already-committed schema.
256
+ 10. **Blocking is progress; close wins lifecycle races.** Native `blocked` notifications from open/delete requests are not terminal failures, so `connect()` / `drop()` stay pending until the blocker closes and IndexedDB reports success or error. Repeated `connect()` calls share that pending Promise. `close()` permanently retires the handle even while an open is pending: every native database returned later is closed before the pending `connect()` rejects with `CLOSED`, including the second open of an auto-managed missing-store bump. Connection events carry their exact database identity, so a stale close/versionchange event cannot clear a different live connection.
257
+
258
+ ## Patterns
259
+
260
+ ### Feature-detecting before opening a database
261
+
262
+ Checks for IndexedDB support before creating and using a database, so a non-browser runtime or a privacy mode without storage never reaches the open call.
263
+
264
+ ```ts
265
+ import { createIndexedDBDatabase, rangeFromKey, supportsIndexedDB } from '@orkestrel/indexeddb'
266
+
267
+ if (supportsIndexedDB()) {
268
+ const db = createIndexedDBDatabase({
269
+ name: 'app',
270
+ version: 1,
271
+ stores: {
272
+ users: { path: 'id', indexes: [{ name: 'byAge', path: 'age' }] },
273
+ },
274
+ })
275
+ await db.store('users').set({ id: 'u1', name: 'Ada', age: 36 })
276
+ await db.store('users').index('byAge').records(rangeFromKey(18)) // adults, index-backed
277
+ }
278
+ ```
279
+
280
+ ### Index-backed reads with key ranges
281
+
282
+ Reads a primary-key range and an indexed range with the built-in key-range helpers and a native `IDBKeyRange`.
283
+
284
+ ```ts
285
+ import {
286
+ rangeAboveKey,
287
+ rangeBelowKey,
288
+ rangeFromKey,
289
+ rangePrefix,
290
+ rangeToKey,
291
+ } from '@orkestrel/indexeddb'
292
+
293
+ const users = db.store('users')
294
+ await users.records(IDBKeyRange.only('user:1')) // exactly one primary key
295
+ await users.records(rangeAboveKey('user:1')) // keys greater than user:1
296
+ await users.records(rangeBelowKey('user:9')) // keys less than user:9
297
+ await users.records(rangeToKey('user:9')) // keys less than or equal to user:9
298
+ await users.index('byAge').records(IDBKeyRange.bound(18, 65)) // working-age, O(log n)
299
+ await users.index('byAge').count(rangeFromKey(18)) // how many adults
300
+ await users.index('byEmail').get('ada@x.io') // unique-index point lookup
301
+ await users.records(rangePrefix('user:')) // primary-key prefix scan
302
+ ```
303
+
304
+ The single-boundary builders each fix a native boolean argument that reads as nothing at the call site, and `rangePrefix` caps the range at U+FFFF. Pass a native `IDBKeyRange` wherever the native call is already readable — `IDBKeyRange.only` for one key, `IDBKeyRange.bound` for a lower and an upper boundary.
305
+
306
+ ### Cursor streaming and in-place mutation
307
+
308
+ Streams a store's records with a cursor, removing a record in place as the loop passes over it.
309
+
310
+ ```ts
311
+ let cursor = await db.store('users').cursor()
312
+ while (cursor) {
313
+ if (cursor.value?.active === false) await cursor.remove()
314
+ cursor = await cursor.continue()
315
+ }
316
+ ```
317
+
318
+ A `store` cursor runs in a `readwrite` transaction, so `update` / `remove` work; an `index` cursor is read-only and rejects them with a typed `IndexedDBError` (`code: 'READONLY'`, native `ReadOnlyError`). Iterate promptly — an unrelated `await` between `continue` steps lets the transaction auto-commit and ends the loop. `cursor.value` is `Row | undefined`: it narrows the stored value with `isRecord` exactly as `get` does, so a non-record stored value reads `undefined` while the cursor still stops on that position and still exposes its `key` and `primary`. Test `value` before you dereference it.
319
+
320
+ ### Seeking an index cursor to one primary key
321
+
322
+ Seeks an index cursor to the one primary key it needs among several rows sharing the same index key.
323
+
324
+ ```ts
325
+ // Three rows share the index key 30, so `seek` picks the one whose primary key is
326
+ // 'c' — which is what the native `continuePrimaryKey` exists for.
327
+ let cursor = await db.store('users').index('byAge').cursor()
328
+ if (cursor) cursor = await cursor.seek(30, 'c')
329
+ cursor?.primary // 'c'
330
+ ```
331
+
332
+ `seek` is valid only on a cursor whose `source` is an index. It drives the native `continuePrimaryKey`, which IndexedDB defines for an index cursor alone, so a store cursor from `db.store(name).cursor()` throws `InvalidAccessError` — a name `ERROR_CODES` does not map, so it reaches the caller as an `IndexedDBError` of code `UNKNOWN`. Move a store cursor with `continue` or `advance` instead.
333
+
334
+ ### Connection lifecycle: connect, close, drop
335
+
336
+ Connects explicitly, closes the handle to release the connection, then drops the whole database.
337
+
338
+ ```ts
339
+ await db.connect() // idempotent — a later store call would connect lazily anyway
340
+ // ... use the database ...
341
+ db.close() // release the connection, keeping the stored data
342
+ await db.drop() // closes and deletes the whole database
343
+ ```
344
+
345
+ **`close()` permanently retires the handle** — `open` reads `false` afterwards, and a later `connect()` on the SAME `IndexedDBDatabaseInterface` throws `CLOSED` rather than reconnecting; reopening the database means calling `createIndexedDBDatabase` again. If an open was already pending, IndexedDB cannot cancel that native request, but the wrapper closes any database it eventually returns and rejects the pending `connect()` with `CLOSED`, so no orphan connection survives. This includes an auto-managed open between its current-version probe and missing-store version bump. This is different from the **transient** connection yield described under Practices, later in this guide (another tab's `versionchange`, or an abnormal browser-initiated close): those clear the handle's internal latches WITHOUT retiring it, so the very same handle lazily reconnects on its next operation.
346
+
347
+ An open or deletion held up by another live connection remains pending. IndexedDB's native `blocked` notification reports progress, not a terminal error; `connect()` / `drop()` settle only after the blocker closes and the native request succeeds or errors. Repeated `connect()` calls while blocked return the same Promise.
348
+
349
+ ### Reading, testing, and clearing a store
350
+
351
+ Reads with a `NOT_FOUND`-throwing lookup, tests presence of a batch of keys, removes a batch, then clears the store.
352
+
353
+ ```ts
354
+ const users = db.store('users')
355
+ await users.resolve('u1') // like get, but throws NOT_FOUND on a miss
356
+ await users.has(['u1', 'ghost']) // presence per key, batched (array-first)
357
+ await users.remove(['u1', 'u2']) // delete by key, batched
358
+ await users.clear() // empty the whole store
359
+ ```
360
+
361
+ ### Explicit transaction control and cursor movement
362
+
363
+ Drives an explicit transaction scope, advancing and updating a cursor within it before committing early.
364
+
365
+ ```ts
366
+ await db.write('users', async (transaction) => {
367
+ // Every move returns the cursor at its new position and leaves the old wrapper
368
+ // on its own snapshot, so rebind at each step or `update` writes a stale row.
369
+ let cursor = await transaction.store('users').cursor()
370
+ if (cursor) cursor = await cursor.advance(1) // skip forward one record
371
+ if (cursor?.value) await cursor.update({ ...cursor.value, seen: true })
372
+ transaction.commit() // flush early instead of waiting for the scope to resolve
373
+ // transaction.abort() // or roll every write in this scope back
374
+ })
375
+ ```
376
+
377
+ ### The request-boundary helpers directly
378
+
379
+ Drives the request-boundary helpers directly against a raw `IDBObjectStore` and `IDBTransaction`, and maps a raw `DOMException` on its own.
380
+
381
+ ```ts
382
+ import {
383
+ createIndex,
384
+ hasKey,
385
+ promisifyRequest,
386
+ promisifyTransaction,
387
+ readRecord,
388
+ readRecords,
389
+ wrapCall,
390
+ wrapError,
391
+ } from '@orkestrel/indexeddb'
392
+
393
+ await db.read('users', async (transaction) => {
394
+ const native = transaction.store('users').store
395
+ await promisifyRequest(wrapCall(() => native.get('u1'))) // sync throw → IndexedDBError too
396
+ await readRecord(native, 'u1') // narrowed to Row (or undefined) with isRecord
397
+ await readRecords(native) // every record, narrowed the same way
398
+ await hasKey(native, 'u1') // a native count() > 0
399
+ await promisifyTransaction(native.transaction) // resolves after the transaction commits
400
+ })
401
+ wrapError(null) // the same DOMException → IndexedDBError mapping every bridge uses
402
+
403
+ // createIndex is the leaf `context.indexes.create` delegates to inside
404
+ // onupgradeneeded — call it directly only if you are hand-rolling a raw
405
+ // versionchange transaction.
406
+ ```
407
+
408
+ ### Branching on a typed fault
409
+
410
+ Branches on a typed `IndexedDBError` code, falling back to an upsert when an insert collides on a duplicate key.
411
+
412
+ ```ts
413
+ import { IndexedDBError } from '@orkestrel/indexeddb'
414
+
415
+ // Insert if new, fall back to upsert on a duplicate-key collision.
416
+ try {
417
+ await db.store('users').add({ id: 'u1', name: 'Ada' })
418
+ } catch (error) {
419
+ if (error instanceof IndexedDBError && error.code === 'CONSTRAINT') {
420
+ await db.store('users').set({ id: 'u1', name: 'Ada' })
421
+ } else throw error
422
+ }
423
+ ```
424
+
425
+ Every terminal native `DOMException` crosses the request boundary as an `IndexedDBError` carrying a machine-readable `code` (`CONSTRAINT`, `NOT_FOUND`, `QUOTA`, `ABORTED`, `READONLY`, `DATA`, …), so a `catch` branches on `error.code` rather than parsing a message string. A native `blocked` notification is not a terminal exception and has no error code; the operation remains pending. `ReadOnlyError` (a write attempted on a `readonly` transaction — for example mutating through a cursor opened by `index(...).cursor()`, which always runs read-only) maps to `READONLY`; both `DataError` (an invalid key) and `DataCloneError` (a value IndexedDB's structured clone cannot serialize, for example a function) map to `DATA`.
426
+
427
+ ### Narrowing a caught value with `isIndexedDBError`
428
+
429
+ Narrows a caught value of unknown shape to an `IndexedDBError` before reading its `code`.
430
+
431
+ ```ts
432
+ import { isIndexedDBError } from '@orkestrel/indexeddb'
433
+
434
+ try {
435
+ await db.store('users').resolve('ghost')
436
+ } catch (error) {
437
+ if (isIndexedDBError(error) && error.code === 'NOT_FOUND') {
438
+ // handle the miss
439
+ } else throw error
440
+ }
441
+ ```
442
+
443
+ ### Versioned upgrades: dropping a store, indexing an existing store, migrating data
444
+
445
+ > **The auto-commit rule.** A transaction — including the versionchange transaction `upgrade` runs in — commits the moment control returns to the event loop with no pending IndexedDB request. Every step inside `upgrade` (and inside any `read` / `write` scope) must be an awaited IndexedDB request; a non-IDB `await` (a `fetch`, a `setTimeout`, an unrelated Promise) lets the transaction auto-commit out from under you, so any later `IndexedDBTransactionStoreInterface` call on it fails `INACTIVE` — every request-issuing call wraps its synchronous native invocation so this (and a closed-connection `INVALID`) surfaces as a typed `IndexedDBError`, the same as an asynchronous fault. This is also why there is no returnable, held-open transaction handle on this wrapper — IndexedDB itself auto-commits any transaction that isn't driven promptly, so a handle you could stash and use later would be broken by design; `read` / `write` and `upgrade` exist specifically to keep the whole scope on the stack instead. The built-in create-missing pass and the custom callback share one capture boundary, and every schema verb on the upgrade's managers — `context.stores.create` / `context.stores.drop` / `context.stores.store` / `context.indexes.create` / `context.indexes.drop` — goes through `wrapCall`, so synchronous schema faults are retained as the `UPGRADE` error's typed cause rather than escaping as raw `DOMException`s. A custom rejection captured while the versionchange transaction remains active aborts that same transaction and rolls the whole upgrade back. If auto-commit wins first but the rejection is recorded before the open success event, `connect()` still closes that connection and rejects, but the committed schema cannot be reversed. The wrapper cannot retroactively turn an already-resolved `connect()` into a rejection: if the open request succeeded before the custom Promise rejected, that late rejection is outside the recoverable boundary.
446
+ >
447
+ > `upgrade` may return `void` or a `Promise<void>` — an async `upgrade` can `await` the IDB requests it issues through `context.stores.store(...)`, subject to the same rule. A rejection captured while the transaction is active aborts it and rejects the pending `connect()` with a typed `IndexedDBError` (code `UPGRADE`) rather than an unhandled rejection. The initiating value is preserved as `cause`, including an explicitly rejected `undefined`; a failure does not poison the handle, so a later `connect()` starts a fresh attempt.
448
+
449
+ ```ts
450
+ import { createIndexedDBDatabase } from '@orkestrel/indexeddb'
451
+
452
+ const db = createIndexedDBDatabase({
453
+ name: 'app',
454
+ version: 2,
455
+ stores: { users: { path: 'id', indexes: [{ name: 'byName', path: 'name' }] } },
456
+ upgrade: async (context) => {
457
+ // Drop a retired store.
458
+ context.stores.drop('legacy')
459
+ // Create a store the built-in create-missing pass doesn't cover because it
460
+ // isn't declared in `stores` — app-internal bookkeeping, for example.
461
+ context.stores.create('meta', { path: 'key' })
462
+ // The built-in pass only creates missing stores, so an index on an
463
+ // already-existing store goes through the index manager; `drop` is its
464
+ // inverse, over an index a prior version left behind.
465
+ context.indexes.create('users', { name: 'byName', path: 'name' })
466
+ context.indexes.drop('users', 'byRetired')
467
+ // Migrate data in place, awaiting only the IDB requests `store()` issues.
468
+ const store = context.stores.store('users')
469
+ for (const row of await store.records()) {
470
+ await store.set({ ...row, migrated: true })
471
+ }
472
+ },
473
+ })
474
+ await db.connect()
475
+ ```
476
+
477
+ `context.transaction` — the raw versionchange `IDBTransaction` — remains available for anything this wrapper doesn't model directly.
478
+
479
+ ### Practices
480
+
481
+ - **Feature-detect with `supportsIndexedDB`** before opening a database in an environment that may lack storage (a non-browser runtime, a privacy mode).
482
+ - **Declare a `path`** for ordinary stores (in-line keys); omit it only when you mean to pass keys explicitly (out-of-line).
483
+ - **Keep transaction scopes to awaited IndexedDB operations** — an unrelated `await` between steps lets the transaction auto-commit (see the auto-commit rule under Versioned upgrades, earlier in this guide).
484
+ - **Reach for the key-range helpers** instead of reading everything and filtering in JS; an index plus a key range is the wrapper's whole point.
485
+ - **A live connection yields to another tab's upgrade, and recovers from an abnormal close.** The database wires the native `onversionchange` event to close itself, so a second tab (or a second `IndexedDBDatabaseInterface` in the same page) opening at a higher version is never blocked indefinitely. Because that `close()` is self-initiated, the native `close` event does NOT fire for it — so the `onversionchange` handler also clears its own latches directly. The native `onclose` event — fired for an ABNORMAL, browser-initiated close (a crashed connection, storage eviction) that this handle did not request — clears the SAME latches, so either path leaves the handle able to lazily reconnect on its next operation rather than failing `NOT_OPEN` (or a stale, dead connection) forever. Without this, two open tabs over the same database can hang forever, and an abnormal close would permanently wedge the handle.
486
+ - **Storage persistence is app policy, not this wrapper's job.** Whether the browser is allowed to evict a database under storage pressure (`navigator.storage.persist()`) is a call the consuming application makes — this wrapper does not surface a `durability` option or any persistence API; it stays strictly on the raw IndexedDB CRUD/schema surface.
487
+ - **Safari quirks — untested here, Chromium-only CI.** This package's test suite runs against real Chromium only. Safari has historically shipped `getAll` bugs and first-transaction-after-upgrade quirks on some versions; if you must support Safari, verify your exact schema and upgrade path there directly rather than assuming Chromium parity.
488
+
489
+ ## Tests
490
+
491
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/browser` bijection, the `## Methods` ↔ interface bijection, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Feature-detecting before opening a database` fence against the `@example` block of that title (pinned so the titled pair cannot be retired silently), and the README pitch against this guide's tagline.
492
+ - [`tests/src/browser/helpers.test.ts`](../tests/src/browser/helpers.test.ts) — the `supportsIndexedDB` probe, the key-range helpers asserted on the bounds they return, the shared read primitives (`readRecord` / `readRecords` / `hasKey`) over a real store / index (including the non-record `isRecord` boundary), `createIndex` translating an `IndexDefinition` into a native `createIndex` call inside a real `onupgradeneeded` (honouring `unique` / `multiple`), the `promisifyRequest` / `promisifyTransaction` bridges (success + `IndexedDBError` rejection), `wrapCall` over each of its three paths (a returned value passing through, a thrown `DOMException` surfacing as the mapped `IndexedDBError` with the native error as `cause`, and a non-`DOMException` throw rethrown by identity), the `context` an `IndexedDBError` carries beside its `code`, `wrapError` (including `INACTIVE` / `INVALID`), and `isIndexedDBError`.
493
+ - [`tests/src/browser/IndexedDBDatabase.test.ts`](../tests/src/browser/IndexedDBDatabase.test.ts) — the database handle in real Chromium: lazy connect and state, the `store` accessor, atomic `read` / `write` scopes (including settling when the scope ends on a trailing non-IDB `await`, the auto-commit race), `close` / `drop`, the auto-managed schema path, persistence across reopen, the `upgrade` hook (dropping a store, indexing an existing store and a same-upgrade `context.stores.create`d store through `context.indexes.create` — honouring `unique` / `multiple` — removing an index through `context.indexes.drop`, data migration through `context.stores.store`, `context.stores.create`, `old` / `version` / `stores.names`, an async `upgrade` rejection cleanly failing `connect()` with `UPGRADE`, and a synchronous `wrapCall` fault from `context.stores.drop` / `context.indexes.drop` targeting a missing store/index likewise failing `connect()` with `UPGRADE`), built-in auto-managed missing-store creation containing duplicate index names as `UPGRADE` → `CONSTRAINT` → native `ConstraintError` on two distinct same-handle `connect()` retries while suppressing the custom callback, atomic rollback of the version, sentinel data, and failed store, deletion without an orphan connection, synchronous `throw undefined` and asynchronous `Promise.reject(undefined)` failures retaining a present `cause` property, raw blockers proving a versioned open and deletion remain pending until release, repeated blocked connects share one Promise and produce one upgrade/owned connection, `close()` during blocked explicit or auto-managed second opens rejecting `CLOSED` after release without orphaning the native result, and `drop()` directly retiring a pending blocked open before deletion completes without leaving an orphan, a live connection yielding to a second connection's `versionchange`, that yielded handle lazily reconnecting at the new version on its next operation, and an ABNORMAL (non-self-initiated) `onclose` likewise leaving the handle able to lazily reconnect instead of staying invalid forever.
494
+ - [`tests/src/browser/IndexedDBStore.test.ts`](../tests/src/browser/IndexedDBStore.test.ts) — the store reached through `db.store(name)`: metadata getters, the keyed CRUD surface with array-first batch overloads, key-range reads, `index` / `cursor` access, and the `NOT_FOUND` / `CONSTRAINT` / `DATA` (a non-cloneable value) faults.
495
+ - [`tests/src/browser/IndexedDBIndex.test.ts`](../tests/src/browser/IndexedDBIndex.test.ts) — the index reached through `store.index(name)`: metadata getters, the read surface (`get` / `resolve` / `records` / `keys` / `primary` / `has` / `count` / `cursor`), the unique-index lookup + constraint, and the `multiple` (multiEntry) array index.
496
+ - [`tests/src/browser/IndexedDBCursor.test.ts`](../tests/src/browser/IndexedDBCursor.test.ts) — the store/index cursor: the position snapshot (`key` / `primary` / `value` / `direction`), the moves (`continue` / `seek` / `advance`), `seek` on a store cursor rejecting with `UNKNOWN`, in-place `update` / `remove`, an index cursor's `update` / `remove` rejecting with `READONLY`, and a non-record stored value reading `undefined` from `value`.
497
+ - [`tests/src/browser/IndexedDBTransaction.test.ts`](../tests/src/browser/IndexedDBTransaction.test.ts) — the transaction from a `read` / `write` scope: metadata getters, scoped `store` access with its out-of-scope guard, and `abort` / `commit` with their already-finished `INACTIVE` faults.
498
+ - [`tests/src/browser/IndexedDBTransactionStore.test.ts`](../tests/src/browser/IndexedDBTransactionStore.test.ts) — the scoped store reached through `transaction.store(name)`: the same keyed CRUD surface as a standalone store but bound to the owning transaction (so a sequence of reads and writes is atomic), without `index`, and a real `INACTIVE` fault after the owning transaction auto-commits out from under a captured store.
499
+ - [`tests/src/browser/factories.test.ts`](../tests/src/browser/factories.test.ts) — `createIndexedDBDatabase` returns a working `IndexedDBDatabaseInterface` that connects lazily, creates its declared stores and indexes, and round-trips real data.
500
+ - [`tests/src/browser/integration.test.ts`](../tests/src/browser/integration.test.ts) — this guide's flagship fences, transcribed and executed against real Chromium storage: each case runs one fence over a uniquely-named database and asserts the value that fence's comments claim, so a comment the code contradicts fails here. `tests/guides.test.ts` carries the presence guard beside each transcription, proving the transcribed lines are still the documented ones.
501
+
502
+ ## See also
503
+
504
+ - [`AGENTS.md`](../AGENTS.md) — the Documentation contract, and the batch-by-overload rule in `.claude/rules/patterns.md` § Managers.
505
+ - [`README.md`](README.md) — the guides index.