provident-storeroom 1.0.0 → 1.0.1

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.
@@ -0,0 +1,1251 @@
1
+ # Using `provident-storeroom` — the installed package
2
+
3
+ **This guide is for someone who has just run `npm install provident-storeroom` and has never seen the repository
4
+ it comes from.** It describes the package you now have in `node_modules`: how it is reached, what its surface
5
+ does, how an address is shaped, how the tiers behave, what each refusal means, and — the part most worth your
6
+ time — **§12, which lists the behaviours you can break and tells you exactly where this package's promises stop.**
7
+
8
+ **The two guides that used to describe this package are archived and wrong.** They are
9
+ `package-use.md` and `electron-integration.md` under `archive/pre-strata-docs-2026-10-24/docs/guides/`, and they
10
+ describe the `0.2.0`/`0.3.0`-era surface; the surface has moved under them twice more since. **Nothing in this file
11
+ is copied from them.** Where a figure here is a declaration, it is cited; where it is a measurement, the
12
+ measurement is named.
13
+
14
+ ---
15
+
16
+ ## §0 · What this guide describes, and where its figures come from
17
+
18
+ **THE VERSION THIS GUIDE DESCRIBES IS `0.5.0`** (`bindings/js/package.json`'s `version` field). **AND NOTHING HERE
19
+ WAS DRIVEN AGAINST A PUBLISHED TARBALL:** every behavioural claim below is either read from the package's own
20
+ **declaration of record** (`bindings/js/index.d.ts`), from a **kept contract** under `docs/specs/`, or from a
21
+ measurement taken on this repository's checkout and named as one. **No `npm install` was run by the pass that
22
+ wrote this file against the registry, so a registry-installed `0.5.0` is described here and not witnessed.**
23
+
24
+ **⟶ CORRECTED 2026-10-26 — THE VERSION THIS GUIDE DESCRIBES IS `1.0.0`, NOT `0.5.0`, AND THE SENTENCE ABOVE STANDS
25
+ BYTE-FOR-BYTE AS THE READING OF THE DAY THIS FILE WAS WRITTEN AND IS READ THROUGH THIS CLAUSE** (`RCA-8(g)`
26
+ ANNOTATE-BESIDE; `DEF-2026-10-02`: no whole-file rewrite). **`bindings/js/package.json`'s `version` field read at its
27
+ own bytes this date is `"version": "1.0.0"`, and all five `optionalDependencies` pins are `1.0.0` with it** — **so
28
+ every `0.5.0` this guide prints is the figure of its writing date and reads `1.0.0` from here on: `§2.2`'s five exact
29
+ pins, `§8.1`'s *"an installed `0.5.0`"*, and `§11`'s manifest table.** **AND THE ONE THING THIS PASS DID NOT WITNESS
30
+ IS UNMOVED: no `npm install` was run against the registry, so a registry-installed `1.0.0` is described here from
31
+ the manifest and from measurements on this checkout, and is still not witnessed.**
32
+
33
+ **Every figure carries one of four marks, and the four are not the same thing:**
34
+
35
+ | the mark | what it means |
36
+ |---|---|
37
+ | **`path §section`** | **declared** — the clause says it, and it is the authority for that subject |
38
+ | **`path` alone, or a row id such as `DD-28`** | **declared** at a contract's own named row |
39
+ | **`archive/…`** | declared in a file that has been **archived** by this branch's documentation pass; cited so the reference resolves, not as the current home of anything |
40
+ | **`measured 2026-10-26`** | **taken on this checkout** — either read at a file's own bytes, or driven against the checkout's own built addon through the published entry, and reported raw |
41
+
42
+ **AND ONE RESOLUTION RULE, SO NO CITATION HERE DANGLES: a bare `write-and-read.md`, `crossing.md`,
43
+ `store-model.md`, `seams-and-lifecycle.md`, `parameter-semantics.md`, `resolution.md`, `kernel-surface.md` or
44
+ `package.md` in a citation is one of this project's ARCHIVED `docs/specs/` parts, and resolves under
45
+ `archive/pre-strata-docs-2026-10-24/docs/specs/<unit>/` — the archive prefix is printed where each is first cited
46
+ and is elided afterwards only to keep the prose readable.**
47
+
48
+ **AND THE SHORT NAMES THIS FILE USES, so every citation resolves from any line:**
49
+
50
+ | the short name | the file it is |
51
+ |---|---|
52
+ | **`index.d.ts`** | `bindings/js/index.d.ts` — **the declaration of record** |
53
+ | **`README.md`** | `bindings/js/README.md` — the readme that ships in the package |
54
+ | **`surface.md`** | `docs/specs/storeroom-js/surface.md` |
55
+ | **`js/contract.md`** | `docs/specs/storeroom-js/contract.md` |
56
+ | **`native/contract.md`** | `docs/specs/storeroom-native/contract.md` |
57
+ | **`json-crossing/contract.md`** | `docs/specs/storeroom-json-crossing/contract.md` |
58
+ | **`held-listener/contract.md`** | `docs/specs/storeroom-held-listener-identity/contract.md` |
59
+ | **`publish/contract.md`** | `docs/specs/storeroom-publish/contract.md` |
60
+
61
+ **AND THE ROW IDS, WHICH ARE STABLE NAMES AND ARE CITED INSTEAD OF A LINE NUMBER ON PURPOSE:** **`DD-n`, `PAR-…`
62
+ and `P-…` are the contracts' own named rows** (each contract ids its own families at its `§0.2` or `§0.3`);
63
+ **`A…` ids are the ARCHIVED authority log's rows** (`archive/pre-strata-docs-2026-10-24/docs/decisions.md`); and
64
+ **`D-…` ids are `strata/decisions.md`'s own rulings**, which outrank every other document in this tree.
65
+
66
+ **HOW TO CHECK WHICH VERSION YOU ACTUALLY HAVE, at your own install — because a guide's claim about a version is
67
+ worth nothing to you if you cannot falsify it:**
68
+
69
+ 1. **What you have:** read `node_modules/provident-storeroom/package.json` and check `"version"`, then read its
70
+ five `optionalDependencies` values. **All six figures must be the SAME version** — the main `version` and the
71
+ five exact pins move together and never singly (`bindings/js/package.json`, measured 2026-10-26; the coupling
72
+ rule is `PUB-DD-10` at `docs/specs/storeroom-publish/contract.md §0.3`).
73
+ 2. **Which realm you are in:** you must be in **Node ESM**. **No other realm is claimed** (§1).
74
+ 3. **What is actually loaded:** construct a store and read the empty address. If the loader resolved a
75
+ differently-versioned platform package, that is where it shows.
76
+
77
+ **THE SHAPE OF THIS GUIDE, IN ONE LINE: one module name, one instance with fourteen own members, one thrown
78
+ refusal form, three tiers, two population routes for constraints, and a value domain narrower than JSON's.** The
79
+ rest is the consequences.
80
+
81
+ ---
82
+
83
+ ## §1 · What you get
84
+
85
+ `provident-storeroom` is **a native Rust register, reached through a Node-API addon, behind a thin ES-module
86
+ facade.** The package itself is small — an ESM entry, its own parts, a readme and a hand-authored type declaration
87
+ — and **the binary does not travel in it**: it arrives in a separate per-platform package (§2).
88
+
89
+ **THE CLAIMED REALM IS `{ Node ESM }` AND NOTHING ELSE** (`bindings/js/README.md`, opening clause; `A70`; and
90
+ `archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-js/package.md §1`–`§2`, archived). **Node CJS, Electron's
91
+ `main`/`preload`/`renderer`, the browser, `worker_threads` and any IPC route are each UNCLAIMED** — the package's
92
+ own readme enumerates them one by one and describes no other realm as supported.
93
+
94
+ **The licence of record is `AGPL-3.0`** (`bindings/js/package.json`'s `license` field, measured 2026-10-26), and a
95
+ copy of the repository's `LICENSE` travels with every published artefact, landed byte-for-byte by the release's
96
+ own assembly step (`docs/specs/storeroom-publish/contract.md §2.3` item 2; `PUB-DD-11`). **Three spellings of the
97
+ licence stand in this repository and they are not identical:** the root `Cargo.toml` says `GPL-3.0-or-later`, the
98
+ binding's `Cargo.toml` says `AGPL-3.0-or-later`, and the published manifest says `AGPL-3.0`. **Do not assume the
99
+ root crate's spelling is a typo** — no clause of the tree says so, and the packaging decision it reflects is not
100
+ yours to reverse. **The manifest's field is the one that governs your use of what you installed.**
101
+
102
+ **AND ONE THING THAT IS ABSENT ON PURPOSE, SAID FIRST BECAUSE YOU WILL LOOK FOR IT: there is no release verb.**
103
+ No `dispose()`, no `close()` and no finalizer is a member (`bindings/js/index.d.ts`, the `StoreInstance`
104
+ declaration; `A74`). **The instance's state is released with the instance and by nothing else — construct one store
105
+ and keep it for the process's life.**
106
+
107
+ **The five decisions this guide exists to inform** — the checklist the archived guide carried and that is still
108
+ true of this surface:
109
+
110
+ - **Decide the address scheme before you write code.** `['domain', id, 'field']` — pick it once, write it down, and
111
+ never let two subsystems invent two schemes. **The addresses are your schema** (§4).
112
+ - **Decide the tiers per data class** — what must survive a reboot (`file`), what may survive a reload (`mem`),
113
+ what may vanish (`temp`) (§5).
114
+ - **Set `journalLength` against your undo promise**, not against a guess: it bounds the journal, which is the
115
+ whole undo window (§5.4).
116
+ - **Decide the value shapes.** They must be crossing-compatible, so they are shapes you would send over an IPC
117
+ boundary. Serialize once, at the boundary (§10).
118
+ - **Wrap every verb in one refusal→data mapper.** Refusals are thrown plain objects, and one place to branch on
119
+ `status` is the whole design (§8).
120
+
121
+ ---
122
+
123
+ ## §2 · How you reach it: install, and the three rungs
124
+
125
+ ### §2.1 One name to install
126
+
127
+ ```sh
128
+ npm install provident-storeroom
129
+ ```
130
+
131
+ **That installs the package and — if your platform is covered — its binary, as a dependency of its own.** You do
132
+ not install five packages; you install one and npm resolves the one your runtime needs.
133
+
134
+ ### §2.2 The five platform packages, named and pinned
135
+
136
+ **The entry resolves its artefact in a declared order, and the first rung is the scoped per-platform package**
137
+ (`docs/specs/storeroom-publish/contract.md §1.1`; `bindings/js/README.md`, *How the addon is resolved*). **The
138
+ names are a closed set of five and are never built by concatenating tokens** (`P-PUB-2`; `publish/contract.md §1.3`):
139
+
140
+ - `@auspicion/provident-storeroom-js-linux-x64-gnu`
141
+ - `@auspicion/provident-storeroom-js-linux-arm64-gnu`
142
+ - `@auspicion/provident-storeroom-js-darwin-x64`
143
+ - `@auspicion/provident-storeroom-js-darwin-arm64`
144
+ - `@auspicion/provident-storeroom-js-win32-x64-msvc`
145
+
146
+ **Measured 2026-10-26 at `bindings/js/package.json`: all five stand in `optionalDependencies`, each pinned to
147
+ `0.5.0` — an exact version, five times, with no caret, no tilde and no range.** **THE PINS ARE EXACT ON PURPOSE AND
148
+ YOU SHOULD NOT "RELAX" THEM:** npm's dedupe could otherwise satisfy the loader with a differently-versioned copy
149
+ hoisted into your tree, and the exact pins are what closes that window (`PUB-DD-10`'s `alt` cell, which prices a
150
+ range and refuses it).
151
+
152
+ **⟶ AND THE FIGURE IS CORRECTED AT THIS DATE: each of the five is pinned to `1.0.0`, not `0.5.0`** (`bindings/js/
153
+ package.json`, read at its own bytes 2026-10-26; `RCA-8(g)` ANNOTATE-BESIDE — the sentence above stands and is read
154
+ THROUGH this one). **THE READING IT TEACHES IS UNMOVED AND IS THE POINT: five EXACT pins, no caret, no tilde and no
155
+ range, all six figures moving together (§0 item 1; `PUB-DD-10`).** **`1.0.0` is the package's own version (§0 and
156
+ §11), so the main `version` and the five pins are one figure and a consumer who reads a `0.5.0` anywhere in a
157
+ manifest has the wrong package.**
158
+
159
+ ### §2.3 The three rungs
160
+
161
+ 1. **RUNG ONE — the scoped platform package for this runtime's `process.platform` × `process.arch` key.** This is
162
+ the rung that fires in a published install. **Every throw at this rung is a MISS and not an error** — it falls
163
+ through rather than reporting (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-publish/resolution.md
164
+ §2.1`–`§2.2`, archived).
165
+ 2. **RUNG TWO — the module-relative addon beside the entry** (`build/Release/storeroom_js_binding.node`), computed
166
+ from the entry module's own location and from no other source: **no environment variable, no configuration
167
+ key, no path relative to the working directory and no search of a parent directory.** **This is the
168
+ checkout-only rung** — a `files`-restricted tarball does not carry it — and no version is compared here
169
+ (`bindings/js/README.md`; `resolution.md §3.2`, archived).
170
+ 3. **RUNG THREE — the declared absent-addon form.** Failing both, `storeCreate` throws the one refusal carrying
171
+ the store's `FilePathUnreachable`, **and no instance is returned.**
172
+
173
+ **THERE IS NO FALLBACK AND NO JS-SIDE STORE.** An absent or failed addon refuses; it never degrades to a JavaScript
174
+ store, never absorbs state silently and never returns a partial instance (`bindings/js/README.md`; `PUB-DD-1`,
175
+ `PUB-DD-2`; `resolution.md §5.3`, archived).
176
+
177
+ **AND THE UNCOVERED-TARGET ANSWER IS A DECLARED LIMITATION, NOT AN ERROR YOU CAN FIX AT THE INSTALL:** a
178
+ `process.platform` × `process.arch` pair outside the five covered keys gets **no binary** — rung one is not
179
+ attempted at all, because no name exists for that pair — and a published install carries no `build.rs`, no
180
+ `Cargo.toml`, no `src/**` and **no `postinstall` build-on-install step.** **Your only route is to build the store
181
+ crate from its source yourself; a covered consumer needs no Rust toolchain, an uncovered one does**
182
+ (`publish/contract.md §2.1` item 3; `PUB-DD-1`).
183
+
184
+ ---
185
+
186
+ ## §3 · The surface: one module name and fourteen members
187
+
188
+ ```js
189
+ import { storeCreate } from 'provident-storeroom';
190
+ ```
191
+
192
+ **That is the whole module namespace — ONE name, `storeCreate`, and nothing else.** There is **no** module-level
193
+ `read`, `write`, `listen` or any other verb (`bindings/js/index.d.ts`'s preamble; `surface.md §2.9` item 1's `A127`
194
+ clause, the module count `11 → 1` ✓; measured 2026-10-26: the imported namespace's keys are `["storeCreate"]`).
195
+
196
+ ### §3.1 The instance, and its fourteen own members
197
+
198
+ ```js
199
+ const store = storeCreate({
200
+ journalLength: 256, // REQUIRED — a positive integer, bounds the journal and so the undo window
201
+ listeners: [], // REQUIRED, even when empty — an absent `listeners` is refused
202
+ filePath: '/…', // OPTIONAL — what makes the `file` tier real, and what construction occupies
203
+ constraints: [], // OPTIONAL — your own list; NEVER persisted (§7.2)
204
+ });
205
+ ```
206
+
207
+ **`storeCreate` answers an instance carrying exactly FOURTEEN own members** — `10 + 1 + 1 + 2 = 14` ✓ — and they
208
+ are these fourteen and no others (`bindings/js/index.d.ts`, `StoreInstance`;
209
+ `docs/specs/storeroom-held-listener-identity/contract.md §2.7` item 9):
210
+
211
+ | member | its declared shape | one line |
212
+ |---|---|---|
213
+ | `read` | `(address, tier?) => unknown` | a leaf's literal, a branch's assembled value, or the whole register |
214
+ | `readLiteral` | `(address, tier?) => unknown` | the same, narrowed to a leaf — **a branch refuses** |
215
+ | `write` | `(address, tier, value, commit?) => WriteRecord` | `commit` is the kernel's own position; leaving it out is the supported form |
216
+ | `delete` | `(address, tier?) => DeleteRecord` | **a method name, not an identifier binding** — `store.delete(…)` |
217
+ | `rollback` | `(opId) => RollbackRecord` | walks backward and undoes **that op and everything after it** in scope |
218
+ | `listen` | `(handle, addresses) => Handle` | **you spell the handle**; the address position is a **SET**; answers the standing handle |
219
+ | `unlisten` | `(handle) => void` | withdraws the store-side entry, and the registry entry where the identity stands nowhere |
220
+ | `register` | `(handle, listener) => void` | one handle, one function — a second registration replaces the first |
221
+ | `removeListener` | `(handle) => void` | the entry removed whole and its store entries withdrawn |
222
+ | `addConstraint` | `(pair, constraint) => ConstraintIdentity` | the ADD route; **overwrites at a held pair and throws nothing for it** |
223
+ | `constraintOn` | `(pair, constraint) => void` | enable — idempotent; throws at a pair the store does not carry |
224
+ | `constraintOff` | `(pair, constraint) => void` | disable — idempotent; **not a removal** |
225
+ | `removeConstraint` | `(pair) => void` | **the one act that deletes an identity** |
226
+ | `constraints` | `() => ConstraintPair[]` | the registered pairs, `{address, id}` only — **no function, no identity, no order, no `enabled`** |
227
+
228
+ **Measured 2026-10-26, on an instance built in memory: `Object.keys(store)` and
229
+ `Object.getOwnPropertyNames(store)` are both this same fourteen-name list, and the `read` member's own descriptor
230
+ reads `writable: true, enumerable: true, configurable: true`.** **THERE IS NO `on`, NO `off` AND NO `settle`.** `on` and
231
+ `off` were merged away — registration is `listen` then `register`, and withdrawal is `unlisten` — and `settle`
232
+ was removed whole when the kernel became the only write path (`index.d.ts`, the member-set clauses of 2026-10-18
233
+ and 2026-10-19; `A127`). **A guide or a README that declares a `settle` member is describing a surface that no
234
+ longer exists.**
235
+
236
+ ### §3.2 The private-state rule
237
+
238
+ **The instance is the handle you act through, and no own member is a data member.** There is no `store.data`, no
239
+ `state` field and no getter over the register; **`read(address)` is the route.** A consumer that expects
240
+ `store.data`, `store.store` or a `state` property is reading an object that does not exist
241
+ (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-js/surface.md §2.1` (e) item 1, archived at its
242
+ operative clause; `A117` (D6)).
243
+
244
+ ### §3.3 The `commit` position, and why you should leave it out
245
+
246
+ **`write`'s fourth position is the optional `commit`** — `write(address, tier, value, commit?)` — and **an absent
247
+ `commit` is in domain**: the write opens or joins a commit all the same (`surface.md §2.2` clause 3;
248
+ `index.d.ts`, the `write` member). **Passing one is the kernel's own route into a write, and a caller leaving the
249
+ position out is the supported form.** **Every form at that position other than an object carrying a `pending`
250
+ member present and not `undefined` throws the class test's own `TypeError`, whose `status` member is ABSENT** — and
251
+ **a refused position stops the write: no verb is reached and no byte moves** (`surface.md §2.4` item 9 (g) item 1).
252
+
253
+ **And one reading that catches people: a SUPPLIED `undefined` at a `tier` position is not the same call as no
254
+ position at all.** It is a PRESENT value, and the store answers `AddressMalformed` for it; only the call that
255
+ supplies nothing there folds to the absent tier (`parameter-semantics.md §2.6`, `PAR-JS-6`, archived; `index.d.ts`,
256
+ `TierSet`).
257
+
258
+ ---
259
+
260
+ ## §4 · Addresses
261
+
262
+ ```ts
263
+ type Address = readonly (string | number)[];
264
+ ```
265
+
266
+ **An address is a path from the register, walked one segment at a time.** A segment is either a `string` key or a
267
+ **non-negative integer index** (`bindings/js/index.d.ts`, the `Address` declaration; `PAR-JS-5`).
268
+
269
+ ```js
270
+ store.read([]) // THE WHOLE REGISTER — measured 2026-10-26: `{}` on a fresh store
271
+ store.read(['users']) // a branch: its assembled value
272
+ store.read(['users', 0, 'name']) // a leaf's literal
273
+ store.readLiteral(['users', 0]) // a leaf only — a branch refuses with `ReadTargetIsBranch`
274
+ ```
275
+
276
+ **Five rules, each of which a reader guesses wrong at least once:**
277
+
278
+ 1. **The empty address resolves to the register, and the register is itself a branch** — so read it with `read`,
279
+ never `readLiteral`.
280
+ 2. **The empty string `''` is a legal key, and there is no delimited address form.** `'a.b'` is not an address; it
281
+ is a single string key that happens to carry a dot, and an address is an ARRAY of segments
282
+ (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-js/crossing.md §1.3`, `AddressArg` rows 2 and 6,
283
+ archived).
284
+ 3. **An integral segment is in domain up to `Number.MAX_SAFE_INTEGER`.** Anything integral above `2^53 − 1` is
285
+ `AddressMalformed` — refused, never folded onto a nearby address (`crossing.md §1.3`, the `AddressArg` row 3
286
+ range clause).
287
+ 4. **An index aimed at the register is `AddressNotFound`** — the register is keyed by names and holds no index
288
+ semantics (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-native/store-model.md §2.2`, arm
289
+ `L-INT-REG`).
290
+ 5. **An array with a hole is not an address.** A sparse address array is `AddressMalformed`, because the two
291
+ readings of a hole disagree and neither may be chosen silently (`crossing.md §1.4` item 2).
292
+
293
+ **And the sentence that saves the most time: a nested write does not create its parent.** Writing beneath a path
294
+ does not bring the path into being; the parent must already stand, and the failure is the write's own
295
+ `AddressNotFound` naming the container it looked in (`archive/pre-strata-docs-2026-10-24` — the archived
296
+ `write-and-read.md` `§3.2`, items 1–2, arm `F-3`). **So create the shape you mean, then fill it** —
297
+ either write the parent first, or **write a whole structured value at the key, which mints the branch tree beneath
298
+ it in one call** (`write-and-read.md §3.4`, arm `W-4`).
299
+
300
+ **A branch assembles one member per child key**, a branch whose container is an array assembles a **dense array**,
301
+ and **an empty container reads `{}` or `[]` — the two are distinct, and that distinctness is what makes the
302
+ container's kind observable at a read** (`write-and-read.md §4.2`, arms `R-2`/`R-3`). **A write whose value kind
303
+ contradicts the container's answers `ContainerKindConflict` rather than converting** (`write-and-read.md §3.4`,
304
+ arm `F-4`). **That is a design decision you make once per key, not something the store repairs for you.**
305
+
306
+ ---
307
+
308
+ ## §5 · Tiers
309
+
310
+ **The tier order is `file` > `mem` > `temp` in persistence**, and what each means for you is lifetime: `temp` is
311
+ shadowed by writes to `mem`, `mem` lives for the session, `file` is for anything that must be there on the next
312
+ boot (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-native/write-and-read.md §3.1.1`).
313
+
314
+ ### §5.1 The tier order decides which version a read lands on — it is not a layered stack
315
+
316
+ **Writing at a tier does not add a version beside the old one: the write purges the versions it outranks, and one
317
+ version is left holding the key** (`write-and-read.md §3.4`, arms `W-6` and `W-1`; §3.2 item 3). **A read narrowed
318
+ to a tier the key no longer holds refuses `TierNotFound`, and the message names the tiers that ARE present**
319
+ (`write-and-read.md §4.2`, arm `R-4`'s message obligation).
320
+
321
+ **There is one state in which two versions DO stand at one key, and it is the exception that makes the rule
322
+ readable: a SHADOW.** When the key holds a version at a tier MORE persistent than the write's, the newer version
323
+ is added alongside and the more persistent one wins the unqualified read. **So "one version per key" is not the
324
+ invariant; "the write's own tier decides which versions survive it" is** (`write-and-read.md §3.4`, arm `W-5`).
325
+
326
+ **Whether declared semantics and shipped behaviour agree at every arm and every tier combination is not a
327
+ consumer guide's question, and this guide does not turn the rule above into a promise about any particular
328
+ pair — it states what to rely on: the order `file` > `mem` > `temp`, the `TierNotFound` refusal with its named
329
+ tiers, and the arity rule immediately below.**
330
+
331
+ ### §5.2 The tier argument's arity differs by verb
332
+
333
+ **READERS take absent, ONE token, or an ARRAY of tokens read as an ANY-MATCH set. WRITERS AND DELETES TAKE EXACTLY
334
+ ONE TOKEN** — an array at `write` or `delete` is `OutOfDomainTier` (`bindings/js/index.d.ts`, the `TierSet`
335
+ declaration, which says so directly; `crossing.md §1.3`, `TierArg` rows 2 and 3).
336
+
337
+ | verb | `tier` absent | one token | an array | `null` / `undefined` |
338
+ |---|---|---|---|---|
339
+ | `read` · `readLiteral` | legal — the lowest persistence present | legal | **legal — an any-match set** | `AddressMalformed` |
340
+ | `write` | `AddressMalformed` | legal | **`OutOfDomainTier`** | `AddressMalformed` |
341
+ | `delete` | legal — every tier at the target | legal | **`OutOfDomainTier`** | `AddressMalformed` |
342
+
343
+ **Asking "do I have this anywhere durable or in memory?" is a legitimate question — `store.read(addr, ['file',
344
+ 'mem'])` answers a node matching ANY of them. A write is going somewhere, and an array there would be a choice you
345
+ did not make.**
346
+
347
+ **And the tier's domain is three lowercase tokens: `file` · `mem` · `temp`.** `'secure'`, `'FILE'`, `'disk'`,
348
+ `'session'` and `''` are each outside it and each answers `OutOfDomainTier`; **`null` and `undefined` are not
349
+ tier-shaped values at all, so they answer `AddressMalformed` rather than `OutOfDomainTier`**
350
+ (`write-and-read.md §3.1.1`, and its `null`/`undefined` column clause).
351
+
352
+ **★ AND `listen` HAS NO TIER AT ALL** — it takes a handle and an address SET, and nothing else (`index.d.ts`, the
353
+ `listen` member; `held-listener/contract.md §2.2` item 1's arity `2`). **The archived guide's tier table carries a `listen` row
354
+ and it is superseded: the verb's positions are `(handle, addresses)` and a tier-shaped argument at position two
355
+ is a set-position class refusal, not a tier.**
356
+
357
+ ### §5.3 `write` and `delete` do not treat one tier alike
358
+
359
+ **A `write` at a tier purges the versions it outranks; a `delete` at a tier removes THAT tier's version and
360
+ leaves every other tier's version at that key untouched** (`write-and-read.md §4.4`, arm `D-1`). **A `delete` with
361
+ no tier removes every version at the target and all downstream of each; a delete of the empty address empties the
362
+ register; and a delete that removes nothing is a SUCCESS — it answers `tiers: []` rather than throwing**
363
+ (`write-and-read.md §4.3`, `§4.4` arms `D-2`, `D-3` and `D-5`).
364
+
365
+ ### §5.4 Undo is bounded by the journal, and `rollback` is not a single-op undo
366
+
367
+ **`rollback` walks backward through the journal, from the newest in-scope entry to the oldest, and the scope is
368
+ every operation touching the same subtree as the named op — not the last op alone. There is no partial or subset
369
+ rollback** (`write-and-read.md §6.4`, arms `M-5` and `M-8`). **Its answer's `undoneOps` is the LIST of the ids the
370
+ reversal actually undid — a report of reversals PERFORMED, newest first, and never a census of in-scope entries**
371
+ (`index.d.ts`, the `RollbackRecord` declaration; `write-and-read.md §6.4` item 3 (i)). **`restoredTo` is the base
372
+ state the addressed subtree now stands at, `{ present, value }`.**
373
+
374
+ **Three more things about the journal, each of which changes how you use it:**
375
+
376
+ - **The journal is not readable, enumerable, addressable or deletable by any other call.** There is no
377
+ `journalEntries()`, no `journalLength()` and no `clearJournal()`; **`rollback` is its only consumer, and the op
378
+ crosses to you as a returned value on every write** (`write-and-read.md §6.3`).
379
+ - **`journalLength` is your whole undo window.** The store compacts entries into a base state as the journal
380
+ fills, and **a rollback whose candidate set includes at least one compacted entry fails `RollbackCompacted` AND
381
+ UNDOES NOTHING — the floor is all-or-nothing** (`write-and-read.md §6.1` `M-4`; `§6.4` `M-6`).
382
+ - **A rollback appends no journal entry, and an undone entry is marked `compacted: true`.** **The `compacted` flag
383
+ is ONE predicate with TWO grounds** — compressed into the base state, or undone by a rollback — **and the record
384
+ carries no member to tell them apart** (`write-and-read.md §6.4` `M-9`).
385
+
386
+ **★ A REVERSAL IS ALSO JUDGED BY YOUR CONSTRAINTS — AT THE STATE IT LEAVES, AND THAT ARM IS DECLARED BUT NOT
387
+ YET CARRIED, WHICH THIS GUIDE SAYS PLAINLY RATHER THAN LETTING YOU DISCOVER IT.** **The ruling** (`D-38`,
388
+ verbatim: *"Assert the constraints on rollback"*) **makes a `rollback` whose TERMINAL state violates a registered
389
+ constraint FAIL, answering `{status: 'ConstraintPassRefused', message, log}` at rank `20` — the same member a
390
+ constraint-refused `write` answers (`§8.1`).** **IT IS JUDGED AT THE TERMINAL STATE AND NEVER PER UNWINDING
391
+ STEP**, because a per-step check is unsound: a reversal passes through intermediate states no constraint would
392
+ approve, and only the state you are left standing in is a state a constraint can be asked about. **A LAWFUL
393
+ REVERSAL IS UNCHANGED** — same `{opId, undoneOps, restoredTo}`, no status and no throw. **MEASURED 2026-10-26:
394
+ the shipped `rollback` still consults no constraint** — a refusal registered over the address a reversal
395
+ restores is not evaluated, and the reversal succeeds — **so treat this paragraph as the ruling's declaration
396
+ and NOT as today's behaviour**; the arm's carrier is owed at the `rollback` member and is the only part of this
397
+ guide's constraint story that is not yet observable.
398
+
399
+ **⟶ AND THE CARRIER THAT PARAGRAPH SAYS IS OWED HAS LANDED (2026-10-26), SO THE RULING IS NOW TODAY'S BEHAVIOUR — WITH FOUR NARROWINGS YOU SHOULD MEET BEFORE YOU RELY ON IT.** `RCA-8(g)` ANNOTATE-BESIDE: **the ★ paragraph above stands BYTE-FOR-BYTE and is read THROUGH this clause — so do its two measured readings, *"the shipped `rollback` still consults no constraint"* and *"the arm's carrier is owed at the `rollback` member"*: each was true of the tree it was measured on and each is FALSE of the shipped tree from this date.** **WHAT YOU NOW GET: a `rollback` whose terminal state a registered constraint refuses throws `{status: 'ConstraintPassRefused', message, log}` at rank `20`, and a lawful reversal answers its unchanged record** — same `{opId, undoneOps, restoredTo}`, no throw, no new member. **WHAT THAT PROMISE DOES NOT COVER — FOUR LINES, EACH WITH ITS REASON:**
400
+ 1. **A reversal INSIDE an open commit is NOT judged.** If you reverse from inside a constraint body, a repair, a listener, or the kernel's own failing-attempt floor call, the state the reversal leaves is not the state that stands (the open window moves it again, and the caller sees nothing yet) — **and judging it would invoke your constraint bodies MID-ATTEMPT and break `D-40`'s promise that a failed attempt leaves the store as it found it.** **The judgement is the AT-REST reversal's: your own `rollback` at the surface, where the state it leaves is the state you then read.**
401
+ 2. **A DISABLED pair is NOT judged.** A pair you `constraintOff`'d still appears in `constraints()`, but the judgement asks only pairs that are enabled — **because a disabled pair cannot fire, so nothing it would refuse can have run.** Re-enable it and it is asked again at the next reversal.
402
+ 3. **A pair registered STRAIGHT THROUGH THE KERNEL'S `K-1` is NOT judged at a reversal.** The judgement reads the package's own registry, which the package's declared registration routes write — `cfg.constraints` at construction and `addConstraint` after it — while a pair placed by a kernel-direct call is evaluated at writes but is **not** in the state the judgement reads (and `constraints()` does not list it either). **Register through the package's routes and the promise covers you.**
403
+ 4. **WHEN IT REFUSES AND NO ATTEMPT STANDS, `log` IS ONE `constraint-check` ENTRY — NOT `[]`.** The `log` member is normally *"the attempt's own message log"*, and `[]` is its value where a failure's cause has no attempt; **a reversal's judgement is one of those causes, and here the refusal AUTHORS ITS OWN RECORD instead of reporting an absence:** `log.length === 1`, `log[0].kind === 'constraint-check'`, `log[0].id` the refusing pair's own id, `log[0].state` `null`, `log[0].opId` the reversal's own op id or `null`. **So do not read an empty array as *"nothing failed"* at this arm, and do not read this one entry as an attempt's history — it is the refusal's.**
404
+ **AND ONE THING ABOUT A FAILING REPAIR, WHICH IS THE SAME SHAPE AND IS DECLARED RATHER THAN DISCOVERED: THE FLOOR CALL RESTORES THE ADDRESS THE ATTEMPT WROTE; AN ADDRESS A REPAIR LANDED AT BESIDE IT IS NOT RESTORED.** `D-40`'s sentence — *"a failed attempt leaves the store exactly as it found it"* — is measured true of the attempt's own address and false of a beside address: the store's rollback scope is the addressed subtree at or after the named op's own position, so a repair that writes elsewhere leaves THAT value standing after the failed attempt. **If you need the beside address's pre-attempt value, keep it yourself: the store's own floor call does not put it back** (`docs/specs/storeroom-js/contract.md §2.6`'s clause of 2026-10-26, which is where the four narrowings, the `log` arm and this spatial scope are declared; `E2E-C-9` cell 6 in `tests/e2e/blind-constraints-v2.mjs` is the measured reach).
405
+
406
+ ---
407
+
408
+ ## §6 · Watching for changes: the listener surface
409
+
410
+ **Listeners fire on `write` and `delete`, and never on a read.** A read is not a change, and a store that fired on
411
+ reads would fire on every render; **nor does a boot or any read-driven traversal fire a listener**
412
+ (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-native/write-and-read.md §6.5`).
413
+
414
+ ```js
415
+ const handle = store.listen('greeting-watch', [['greeting']]); // YOU spell the handle; the addresses are a SET
416
+ store.register(handle, (address) => { // handle + your function
417
+ console.log('changed at', address); // the ONE argument is the write's address
418
+ });
419
+
420
+ store.write(['greeting'], 'mem', 'hello');
421
+
422
+ store.unlisten(handle); // withdraw the registration
423
+ ```
424
+
425
+ **Measured 2026-10-26: `store.listen('h1', [['b']])` answered the string `'h1'`, and `store.register('h1', () =>
426
+ {})` answered `undefined`.** **Each address in the set is itself an array of segments — `[['b']]` is a set of one
427
+ address — and `[]` (the empty set) is in the domain.**
428
+
429
+ ### §6.1 `listen` takes an address SET and answers the handle
430
+
431
+ **THE SIGNATURE IS `listen(handle: Handle, addresses: readonly Address[]): Handle`** — arity `2`, position `1` a
432
+ non-empty `string` you spell, position `2` an ARRAY of addresses
433
+ (`docs/specs/storeroom-held-listener-identity/contract.md §2.2` items 1 and 2; `bindings/js/index.d.ts`'s `listen`
434
+ member, amended 2026-10-24).
435
+
436
+ **Four rules about that second position, and they are the ones a reader gets wrong:**
437
+
438
+ 1. **A SINGLE address supplied as a bare array-of-segments is NOT an address set.** `listen('h', ['a'])` puts the
439
+ STRING `'a'` where an address belongs — a string is not a segment list — so that call is a set-position class
440
+ refusal, not a registration. **You mean `listen('h', [['a']])`** (`held-listener/contract.md §2.2` item 2: *"the
441
+ position is a set and a segment list is a different shape"*). **The outside-value set of the set position is: ABSENT · `null` · `undefined` · a non-array · an
442
+ array carrying a member outside the address domain · an array carrying a nested array-of-arrays that is not
443
+ itself an address — and the declared answer for each is the one thrown form carrying the store's
444
+ `AddressMalformed`.**
445
+ 2. **THE EMPTY ARRAY IS IN DOMAIN.** `listen('h', [])` is a lawful call.
446
+ 3. **THE SET IS AN ASSIGNMENT, NOT A UNION.** `listen(h, S2)` after `listen(h, S1)` **destroys `S1` wholly and
447
+ installs `S2`**, with no intermediate state observable. **You cannot narrow a live handle's standing by
448
+ omitting an address you previously named** (`held-listener/contract.md §2.2` items 3 and 4; `(R1)`).
449
+ 4. **THE CALL ANSWERS THE HANDLE.** On success `listen` returns the handle it watches under — your own spelling,
450
+ returned as the identity the store holds for that entry — and **every failure remains the one thrown form**
451
+ (`D-22`, verbatim: *"Listen should return the handle on success, or error message on fail. Discard the noop
452
+ prefix"*; `held-listener/contract.md §2.2`'s 2026-10-25 clause). **A caller that ignores the return is unaffected by the
453
+ return's existence.**
454
+
455
+ ### §6.2 `register`, `unlisten`, `removeListener`
456
+
457
+ **`register(handle, listener)` installs the function under a handle this instance holds an entry for; a second
458
+ `register` for one handle REPLACES the first, and the replaced function is dropped without notice**
459
+ (`surface.md §2.4` rule 2). **Measured 2026-10-26: `store.register('h9', () => {})` at a handle no `listen` wrote
460
+ and no `cfg.listeners` adopted threw the ONE PLAIN FORM — `{status: 'AddressMalformed', message, log}`, whose
461
+ message says *"this instance holds no listener entry for it — a registration names a handle `listen` wrote or
462
+ `cfg.listeners` adopted"*.** **That is the declared reading and not a `TypeError`:** the registration site's
463
+ refusal *"is a rejection under `A76` and travels in this section's single form, with its `status` drawn from the
464
+ store's closed set"* (`surface.md §2.5` item 5 (g)'s operative reading of 2026-10-17; `index.d.ts`'s `register`
465
+ block). **A `TypeError` from a store-reaching verb is itself treated as a finding at that site, which is how you
466
+ can tell the two apart.**
467
+
468
+ **`unlisten(handle)` withdraws the registration and performs the package's own half too:** the store-side entry
469
+ named by the call is withdrawn, and the registry's entry for the identity is removed **where the identity no
470
+ longer stands anywhere** (`surface.md §2.4` item 7 (b)'s step list, merged into this one member;
471
+ `held-listener/contract.md §5.5`'s fourth added entry). **Over a value outside the domain it is a PLAIN NO-OP SUCCESS** — a
472
+ fabricated handle, a blank token, a prior session's token — producing no code and removing no other registration.
473
+
474
+ **`removeListener(handle)` is the stronger member and it is not the same act: the listener entry is removed WHOLE
475
+ and its store entries are withdrawn, and every arm that is not a REAL removal THROWS** (`held-listener/contract.md §2.7` item 2;
476
+ `PAR-HL-14`, `PAR-HL-15`; `index.d.ts`). **Measured 2026-10-26: `removeListener` at a handle the instance holds no
477
+ entry for threw the one plain form with `AddressMalformed` and the message *"…removeListener's first argument is
478
+ 'h9', and this instance holds no listener entry for it"*.** **So `unlisten` is a no-op and `removeListener` is not:
479
+ choose the one whose answer you want when the handle is stale.**
480
+
481
+ ### §6.3 The store invokes nothing
482
+
483
+ **The store returns the triggered set and invokes no callback.** The invocation is the CLIENT's act, and the
484
+ package's per-instance registry is the handle→function map (`write-and-read.md §3.5`'s amended record and the
485
+ store-side clause it cites; `surface.md §2.4` items 5–6). **That is why a registration is a member of the
486
+ instance and not a function you hand to the store.**
487
+
488
+ **Four rules, and they are the package's rather than yours to reimplement:**
489
+
490
+ 1. **An unregistered handle fires nothing, silently** — no warning, no log, no code. **The silence is an
491
+ assertable observable** (`surface.md §2.4` rule 1).
492
+ 2. **`triggered` membership is decided by address and by nothing else:** a handle appears exactly where **its
493
+ listener's address is a PREFIX of the triggering address** (`surface.md §2.3` item 5). **So a listener bound at
494
+ a strict descendant of a deleted address does NOT fire.**
495
+ 3. **A registration made during a firing window is not invoked during that window.** A delivery's recipient set
496
+ is read from the array as it stands when the window opens (`surface.md §2.4`, the delivery-window clause).
497
+ 4. **One invocation per distinct handle.** A handle bound at two addresses that both match fires ONCE
498
+ (`held-listener/contract.md §2.4` item 6; `surface.md`, *"a lawful duplicate fires once per distinct handle"*).
499
+
500
+ ### §6.4 What a record's `triggered` and `watched` carry
501
+
502
+ **A trigger is a PAIR, `{handle, address}` — not a bare handle.** **`address` is the address the listener was
503
+ BOUND at, never the address the write targeted**, and the two are told apart: **the invocation's one argument is
504
+ the write's address** (`index.d.ts`, `TriggerPair`; `held-listener/contract.md §2.4` items 1 and 7; `PAR-HL-19`–`PAR-HL-21`).
505
+ **There is no parallel `triggeredAddresses` array and none may be added.**
506
+
507
+ - **`write` answers `{op, watched, triggered}`** (`index.d.ts`, `WriteRecord`). **`op` is the store's own journal
508
+ record and not a number** — `{opId, kind, address, tier, before, after, seq, compacted}` — and **the op's
509
+ `opId` is a string** and is the token `rollback` accepts (`index.d.ts`, `JournalOp`). **`watched` carries
510
+ ADDRESSES where `triggered` carries pairs — two types on purpose, so a call site cannot confuse the two lists**
511
+ (`index.d.ts`, `WriteRecord`; `write-and-read.md §3.5`).
512
+ - **`delete` answers `{address, tiers, triggered}`** — `tiers` naming the tiers whose versions were REMOVED, and
513
+ `address` being your own array (`index.d.ts`, `DeleteRecord`).
514
+ - **`rollback` answers `{opId, undoneOps, restoredTo}`** (`index.d.ts`, `RollbackRecord`).
515
+ - **Neither list's ORDER is declared.** Compare them as SETS, never as sequences (`DD-22`, `DD-37`;
516
+ `held-listener/contract.md §2.4` item 6's no-order rule).
517
+ - **`triggered` is always a list and may be empty; it is never absent.** A delete that removes nothing is a
518
+ SUCCESS carrying `tiers: []` and `triggered: []` (`write-and-read.md §4.4`, arm `D-3`).
519
+ - **The kernel's own `pending` list carries the same pair element as `triggered`**, because `pending` is seeded as
520
+ a clone of the store's returned set (`index.d.ts`, `CommitRecord`; `held-listener/contract.md §2.4` item 3).
521
+
522
+ **And one store behaviour that shows up in the records if you use integer addresses: a delete of an indexed
523
+ version of an array container RENUMBERS the surviving anchors in container order from `0`, each keeping its tier
524
+ and its node** (`write-and-read.md §4.4`'s renumbering clause).
525
+
526
+ ---
527
+
528
+ ## §7 · Constraining: the pair, the handle, and the two routes
529
+
530
+ ### §7.1 A pair is `{address, id}`, and the `id` IS its handle
531
+
532
+ **A constraint is addressed by a PAIR — `{ address, id }` — where `address` is an address in §4's domain and `id`
533
+ is a NON-EMPTY `string`** (`bindings/js/index.d.ts`, `ConstraintPair`; `PAR-JS-24`).
534
+
535
+ **★ AND THE PAIR'S `id` IS ITS HANDLE, INSTANCE-SCOPED AND UNIQUE. YOU NAME IT.** `D-23`: *"Setting a constraint
536
+ should expect a handle name to be used for handle and process/error logging purposes"*; `D-24`: *"The handle
537
+ becomes the id — the handle needs to be unique regardless, and setting a duplicate handle is an overwrite"*;
538
+ `D-29`: *"Handle becomes the internal ID"*. **One `id` names one pair, one identity and one handle** — the older
539
+ `(id, address-set)` keying is superseded, and **there is only ever one pair per `id`.**
540
+
541
+ **AND THE `address` MEMBER'S TYPE MOVES BY ACT, which is a fact worth knowing before you write a helper:** at the
542
+ REGISTRATION act the pair names a single address; on the `constraints()` read and at `removeConstraint` it is the
543
+ pair's own SET of addresses (`index.d.ts`, `ConstraintPair`; `held-listener/contract.md §2.6` items 4–5). **Measured 2026-10-26:
544
+ after `store.addConstraint({address: ['a'], id: 'c1'}, () => true)`, `store.constraints()` answered
545
+ `[{address: [['a']], id: 'c1'}]` — the element's `address` is a one-member set, and the element carries exactly
546
+ `address` and `id`.**
547
+
548
+ ### §7.2 The two population routes, and the function
549
+
550
+ **There are exactly TWO ways to populate constraints, and neither substitutes for the other**
551
+ (`surface.md §2.12` item 1; `index.d.ts`).
552
+
553
+ **ROUTE ONE — the batch at construction, `cfg.constraints`.** **The element is THREE members —
554
+ `{ address, id, constraint }` — AND THE FUNCTION IS REQUIRED** (`D-26`, verbatim: *"The batch in cfg.constraints
555
+ must supply functions"*). **The pair is the element's addressing and the function is its substance**, and **the
556
+ batch ARMS the function at construction** — not merely validates it and defers (`D-27`, verbatim: *"Functions are
557
+ armed"*) — through the same act the add route performs. **The store never sees the function**: a JavaScript
558
+ function cannot cross into Rust, so the crossing carries the pair and **the package arms the substance**.
559
+
560
+ **A batch element that carries no function — absent, `undefined` or a non-function — is REFUSED at construction,
561
+ under the store's existing `ConstraintListInvalid`, in the one thrown form, BEFORE ANY FILESYSTEM ACT**
562
+ (`D-26` item 5; `D-27`'s closing clause; `bindings/js/src/entry/frame.mjs`, whose `constraintEntry` validates the
563
+ member and whose construction walk refuses the element — read 2026-10-26). **So the state the older prose called
564
+ *"a pair with no function"* HAS NO PRODUCER ON THIS ROUTE:** a batch element is always carried.
565
+
566
+ **ROUTE TWO — the add route after creation, `addConstraint(pair, constraint)`.** **It CARRIES the function as its
567
+ own argument, and at a pair already held the supplied constraint OVERWRITES the held one under the SAME identity
568
+ the pair was already added under — the call throws nothing for the mere fact that the pair was held** (`D-24`;
569
+ `index.d.ts`, `addConstraint`).
570
+
571
+ **★ AND THE LIST IS NEVER PERSISTED — not in the file, not as a register key and not in any journal entry.**
572
+ **The importing application supplies it at EVERY startup from its own information, and the store holds the pair's
573
+ `address` and `id` for the instance's life only** (`A118`; `native/contract.md §7.2` item 6; `index.d.ts`,
574
+ `StoreConfig.constraints`). **It is validated at TWO instants, both answering `ConstraintListInvalid` with no
575
+ instance: at CONSTRUCTION, and at a boot over an existing `filePath`, where the LOADED state is validated against
576
+ the list you supplied** (`native/contract.md §8.1` #16's amended trigger clause). **So a constraint list is code
577
+ your application carries, not state your store stores.**
578
+
579
+ **★ AND A REPEATED HANDLE ANSWERS DIFFERENTLY AT THE TWO ROUTES, WHICH IS A DIFFERENCE OF ACT AND NOT AN
580
+ INCONSISTENCY** (`D-27`'s own table):
581
+
582
+ | the act | a repeated handle |
583
+ |---|---|
584
+ | **`cfg.constraints`, the batch** | **REJECTED** — the list is ONE supplied act, so an internal ambiguity is malformed input and not a second write |
585
+ | **`addConstraint(pair, constraint)`** | **OVERWRITE** — acts are separate in time, so a later one replaces the earlier |
586
+
587
+ **At construction that means TWO refusals the store cannot take and the package owns:** no two elements of one
588
+ `cfg.constraints` may name one `id`, **and no element's `id` may equal a `handle` this construction's `listeners`
589
+ adopts — a cross-KIND collision, rejected regardless of kind** (`D-27`, verbatim: *"Reject duplicate handles in
590
+ batch - regardless of listener/constraint type"*; `index.d.ts`, `StoreConfig.constraints`). **Both are refused
591
+ before any filesystem act, under `ConstraintListInvalid`; the package mints no code.**
592
+
593
+ ### §7.3 A cross-kind duplicate is REJECTED, in both directions
594
+
595
+ **A duplicate handle of the SAME kind is an overwrite. A duplicate handle ACROSS KINDS — a listener where a
596
+ constraint stands, or a constraint where a listener stands — is REJECTED** (`D-25`, verbatim: *"Attempting to
597
+ overwrite a listener with a constraint or vice versa is rejected"*; `D-29`: *"Listen cannot overwrite constraint,
598
+ only other listeners"*).
599
+
600
+ **Measured 2026-10-26, from the constraint-then-listener direction:** with `c1` armed at `['a']`,
601
+ `store.listen('h1', [['a']])` threw the **one plain form** — `Object.keys` `["status","message","log"]`, `status`
602
+ `'AddressMalformed'`, `log` `[]` — whose message names BOTH sides and carries the shared rule sentence **"a
603
+ listener and a constraint do not share one"**. **The form is the surface's one thrown form and NOT a `TypeError`**
604
+ (`index.d.ts`, `addConstraint`'s and `register`'s cross-kind clauses). **No state moves: the rejection is taken
605
+ after the positions' class tests and before any store call.**
606
+
607
+ ### §7.4 The toggles, and the read
608
+
609
+ **`constraintOn(pair, constraint)` ENABLES and `constraintOff(pair, constraint)` DISABLES. Both are IDEMPOTENT,
610
+ both take the `constraint` position REQUIRED (it is a no-op on every success — the caller supplies as a function
611
+ the identity it holds at that pair, and the entry never substitutes one), and both THROW at a pair the entry does
612
+ not carry** (`index.d.ts`, the two toggle members; `surface.md §2.12` items 2–3).
613
+
614
+ **A toggle is NOT a removal.** A disabled pair is still registered and still carried; a write the disabled
615
+ constraint would have refused is ACCEPTED. **`removeConstraint(pair)` is the ONE act that deletes an identity:
616
+ the pair, its N address rows, the kernel's registration and the store's home entry all go, and a fresh
617
+ `addConstraint` at that pair afterwards answers a NEW identity** (`held-listener/contract.md §2.6` item 3; `§2.7` item 3;
618
+ `index.d.ts`).
619
+
620
+ **`constraints()` is the registered-pair read, and it reports PAIRS with four declared exclusions**
621
+ (`index.d.ts`, the `constraints` member, items 1–4; `surface.md §2.4` item 9 (f)'s `constraints` clause):
622
+
623
+ 1. **NO constraint function is returned** — a disabled pair makes the hazard sharp, since the caller's function is
624
+ kept in the entry while the kernel holds a stub.
625
+ 2. **NO opaque identity is exposed** — the pair is your own datum; `ConstraintIdentity` is the package's opaque
626
+ handle for it, and the two are not the same datum. **Compare identities, never contents.**
627
+ 3. **NO ORDER is promised** — compare the returned pairs **as a SET and never as a sequence.**
628
+ 4. **A DISABLED PAIR DOES APPEAR, and there is therefore NO `enabled` member on an element and NO way to read a
629
+ pair's state off this return.** `constraintOff` is a toggle and not a removal; **an absence here means nothing
630
+ was ever registered.**
631
+
632
+ **The read takes no argument, makes no store call, appends no journal entry and changes no node**
633
+ (`index.d.ts`).
634
+
635
+ **⟶ AND EXCLUSION 3 ABOVE IS RETIRED, 2026-10-26, ON `D-39`'S `DEF-R` RULING — VERBATIM *"add deterministic
636
+ ordering"* — WHICH RULES THAT *"that exclusion is superseded and the order becomes part of what the method
637
+ promises"*** (`strata/decisions.md`; `RCA-8(g)` ANNOTATE-BESIDE — the four exclusions above stand BYTE-FOR-BYTE and
638
+ are read THROUGH this clause). **`constraints()` EMITS IN REGISTRATION SEQUENCE — THE ORDER THE PAIRS WERE
639
+ REGISTERED — DETERMINISTICALLY AND STABLY ACROSS EVERY CALL AND EVERY INTERVENING ACT; THE ORDER IS PART OF WHAT
640
+ THE METHOD PROMISES.** **So you MAY now compare the returned array element by element, and the sequence you read
641
+ is the sequence your own calls stood in: a pair from `cfg.constraints` in its list position, then each
642
+ `addConstraint` in the order you made it; a pass, a refusal, a toggle and a `rollback` move none of it.** **THE
643
+ OTHER THREE EXCLUSIONS STAND UNCHANGED: no function, no opaque identity, and a disabled pair still appears with no
644
+ `enabled` member beside it.**
645
+
646
+ ### §7.5 ★ Reads check NO constraint
647
+
648
+ **The constraint mechanism is unconditional on the WRITE/COMMIT path, over the triggered handle set — and
649
+ `read`, `readLiteral` and a branch's assembly CONSULT NO CONSTRAINT, refuse nothing on a constraint's verdict, and
650
+ filter nothing on one.** **There is no read-side enforcement arm, none is declared, and none is owed**
651
+ (`strata/decisions.md` `D-1-bis`, where the architect refused the read-side reading by name, reading (a)):
652
+ *"a is correct"*.
653
+
654
+ **So a constraint cannot be used as a read filter, a field-level permission, or a projection. It is a gate on
655
+ writes and on nothing else** — and **reads consult no constraint's verdict, filter nothing on one, and refuse
656
+ nothing because of one.**
657
+
658
+ ---
659
+
660
+ ## §8 · Refusals
661
+
662
+ **One failure surface, everywhere, and it is THROWN:**
663
+
664
+ ```js
665
+ try {
666
+ const v = store.read(['users', 0, 'name']);
667
+ } catch (e) {
668
+ // e is a PLAIN OBJECT — never an Error subclass, never a rejected promise, never a tagged union
669
+ const { status, message, log } = e;
670
+ }
671
+ ```
672
+
673
+ **A refusal is a thrown object literal carrying exactly `{status, message, log}`** — **never an `Error` subclass,
674
+ never a returned tagged union, never a `Promise` rejection**, so `try/catch` is the whole error model
675
+ (`bindings/js/index.d.ts`, `StoreRefusal`; `surface.md §2.5`'s clause of 2026-10-16, the crossing form's one home;
676
+ `js/contract.md §1.4`).
677
+
678
+ **★ AND THE `log` MEMBER IS ALWAYS PRESENT, and it is a plain own enumerable data member of that same object** —
679
+ written with an object literal, not `defineProperty`, not a getter, not on a prototype and not behind a `Symbol`.
680
+ **So `Object.keys(caught)` reads exactly `["log","message","status"]` and its length is `3` on EVERY arm. It is
681
+ never absent, never `null` and never a non-array; `[]` is its declared value where the failure's cause has no
682
+ attempt at all, and every failure whose attempt exists carries that attempt's own entries**
683
+ (`index.d.ts`, `StoreRefusal` items 4, 9 and 10; `surface.md §2.5`).
684
+
685
+ **Measured 2026-10-26, on refusals from `read`, `readLiteral`, `write`, `delete`, `rollback`, `listen`,
686
+ `unlisten`, `register` and `removeListener`: every one was an `Object` whose `Object.keys` was
687
+ `["status","message","log"]`, whose `log` was `[]`, and which was not an `Error` instance.**
688
+
689
+ **EVERY REFUSAL NAMES ITSELF** — by a `status` from the store's own closed set, and by a **human `message` that
690
+ carries the address, the domain, and what was found instead.** The message is generated from the code and the
691
+ failing trigger's arguments and never hand-authored at a call site
692
+ (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-native/seams-and-lifecycle.md §8.2`, archived);
693
+ **`index.d.ts` carries both, verbatim from the store.**
694
+
695
+ ### §8.1 The set is TWENTY and open by design
696
+
697
+ **⟶ RE-DERIVED AGAIN 2026-10-26 — THE SET IS TWENTY, ON THE ARCHITECT'S TWO RULINGS, VERBATIM: *"Expand the
698
+ returned fail-state list to include the refusal"* and *"Assert the constraints on rollback"*.** **`StoreStatus`
699
+ carries TWENTY members, plus the open escape arm `(string & {})`** — ranks `1`…`19` stand exactly as the two
700
+ paragraphs below print them, and **the appended term is `ConstraintPassRefused` at rank `20`: THE ONE STATUS A
701
+ CONSTRAINT REFUSAL ANSWERS WITH.** **Two arms and one member:** a `write` whose registered constraint refuses it
702
+ (`{status: 'ConstraintPassRefused', message, log}`, with the kernel's own token still riding the attempt's
703
+ `log[].state` — `KC-3`, `KC-5`, `KC-6`, `KC-8` as the case is), **and** a `rollback` whose terminal state a
704
+ registered constraint refuses (`§5.4`). **`FileCommitFailed` KEEPS rank `15` AND IS NOW THE **SAVE'S** CODE
705
+ ALONE** — it is produced by a failing `file`-tier save and by nothing else — **so a constraint refusal never
706
+ answers it again**, and the status stops naming a layer that did not fail. The algebra reads
707
+ `11 + 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 20` ✓. **Every paragraph and every table row below is the reading of its
708
+ own date and is read THROUGH this clause** (the guide's figures move with the set's own membership, which is
709
+ this section's own rule).
710
+
711
+ **⟶ RE-DERIVED 2026-10-26 TO THE ARCHITECT'S RULING AND NOT TO THE BYTES** (`DEF-S` and `DEF-N`; the architect,
712
+ verbatim: *"Expand the returned error set for both S/N"*). **`StoreStatus` carries NINETEEN members, plus the open
713
+ escape arm `(string & {})`** — ranks `1`…`17` stand exactly as the paragraph below prints them, and the two
714
+ appended terms are `ConstraintBodyThrew` at rank `18` (a constraint BODY's throw, which the package's crossing
715
+ converts into the one failure form from a value the KERNEL marked as a body's own) and
716
+ `ConstraintRefusedAtRegistration` at rank `19` (the REGISTRATION VERDICT: a well-formed pair whose body answered
717
+ non-`true` at registration or at a re-enable). **`ConstraintListInvalid` stays at rank `16` and names the SHAPE
718
+ arm alone — a malformed entry — so a pair that is `{address: ['a'], id: 'c1'}` is never refused with it.** The
719
+ algebra reads `11 + 2 + 1 + 1 + 1 + 1 + 1 + 1 = 19` ✓. The paragraph and the table below are the 2026-10-23
720
+ reading and stand as their own date's (the guide's figures move with the set's own membership, which is the
721
+ `§8.1` rule the same paragraph states two sentences later).
722
+
723
+ **`StoreStatus` carries SEVENTEEN members, plus an open escape arm `(string & {})`**
724
+ (`bindings/js/index.d.ts`, the `StoreStatus` declaration; `docs/specs/storeroom-native/contract.md §8.1`'s
725
+ 2026-10-23 amendment, whose own words are *"the operative set is `17` members ranked `1`…`17`"*; the algebra
726
+ `11 + 2 + 1 + 1 + 1 + 1 = 17` ✓). **`ValueRefusedAtBoundary` is written at rank `17`, LAST of the seventeen and
727
+ BEFORE the escape arm**, and `ConstraintListInvalid` stands at rank `16`. **The store implements seventeen
728
+ (`src/error.rs`'s `CODES`).**
729
+
730
+ | rank | status |
731
+ |---|---|
732
+ | 1–11 | `AddressMalformed` · `AddressNotFound` · `LeafHasNoDownstream` · `TierNotFound` · `OutOfDomainTier` · `ParentAnchorMonotonicityViolation` · `ContainerKindConflict` · `ReadTargetIsBranch` · `WriteParentNotLink` · `JournalOpUnknown` · `RollbackCompacted` |
733
+ | 12–16 | `ConstructionArgumentMissing` · `FilePathUnreachable` · `ValueNotAdmitted` · `FileCommitFailed` · `ConstraintListInvalid` |
734
+ | 17 | `ValueRefusedAtBoundary` |
735
+ | 18–19 | `ConstraintBodyThrew` · `ConstraintRefusedAtRegistration` (`DEF-S`, `DEF-N`, 2026-10-26) |
736
+ | 20 | `ConstraintPassRefused` (`DEF-H`, `DEF-W`, 2026-10-26) — a constraint's refusal of a `write`, and of a `rollback` whose terminal state it refuses |
737
+ | escape | `(string & {})` |
738
+
739
+ **THE ESCAPE ARM IS LOAD-BEARING RATHER THAN LOOSENESS.** The store's set has already moved three times — fourteen
740
+ to fifteen, fifteen to sixteen, and sixteen to seventeen — and an immutably-enumerated union would mis-type a LAWFUL
741
+ refusal the moment the set moves again; **the arm is what keeps a member the store has added typeable in an
742
+ installed `0.5.0`** (`index.d.ts`, `StoreStatus`; `PUB-DD-3`). **It has just done so: the 2026-10-26 ruling
743
+ added `ConstraintBodyThrew` and `ConstraintRefusedAtRegistration` at ranks `18` and `19`, and an installed copy
744
+ whose union stopped at `17` would mistype both.** **So: do
745
+ not `switch` on these without a `default`, and NEVER treat an unknown `status` as success.** The set is the
746
+ store's to close; **the package mints no member, widens no set and renames none.**
747
+
748
+ **AND `ValueRefusedAtBoundary` IS THE ONE YOU WILL MEET ON THE VALUE ROUTE:** the package's own walk decides a
749
+ value's class **before any store verb is called** and answers this member with the failing target's spelling
750
+ (§10). **`ValueNotAdmitted` stays at rank `14` and is the STORE's own answer on the native route — a pass that
751
+ reads rank `17` as a rename of rank `14` has destroyed a member.**
752
+
753
+ **⟶ AND THREE FIGURES OF THIS SECTION ARE CORRECTED AT THIS DATE, EACH AT ITS OWN SITE, SO THE NINETEEN ABOVE IS
754
+ NOT LEFT DISAGREEING WITH ITS OWN NEIGHBOURS** (`RCA-8(g)` ANNOTATE-BESIDE: every sentence above stands BYTE-FOR-
755
+ BYTE and is read THROUGH this clause). **(i) *"The store implements seventeen (`src/error.rs`'s `CODES`)"* READS
756
+ NINETEEN**: `src/error.rs`'s `CODES` is `[&str; 19]`, ranks `18` and `19` being the two this set's 2026-10-26 ruling
757
+ appended — **so the declaration of record and the crate agree at nineteen, which is the row above.** **(ii)
758
+ *"installed `0.5.0`"* READS `1.0.0`** (`bindings/js/package.json`, read at its own bytes; §0's correction clause).
759
+ **(iii) THE SET HAS NOW MOVED FOUR TIMES, NOT THREE**: fourteen to fifteen, fifteen to sixteen, sixteen to
760
+ seventeen, **and seventeen to nineteen** — **the escape arm's argument is strengthened rather than weakened by the
761
+ fourth move, which is exactly the arm's own evidence.** **NO TABLE ROW, NO RANK AND NO TRIGGER MOVES, and the two
762
+ appended members are the ones the table's `18–19` row already prints.**
763
+
764
+ **⟶ AND TWO FIGURES OF THAT CORRECTION ARE THEMSELVES RE-READ AT THE SHIPPED TREE, 2026-10-26, BECAUSE THE SET MOVED AGAIN AFTER THE CORRECTION WAS FILED AND ITS OWN ALGEBRA MOVED WITH IT.** `RCA-8(g)` ANNOTATE-BESIDE: **the correction's sentences — including its *"READS NINETEEN"* and its *"FOUR TIMES, NOT THREE"* — stand BYTE-FOR-BYTE and are read THROUGH this clause; THIS clause re-reads two figures and moves no site, no rank and no trigger.** **(i) *"`src/error.rs`'s `CODES` is `[&str; 19]`"* READS `[&str; 20]`** — read at the crate's own bytes — **ranks `18`, `19` and `20` being the three the 2026-10-26 rulings appended, so the declaration of record and the crate agree at TWENTY, which is this section's own heading and its table's own `20` row.** **(ii) THE SET HAS NOW MOVED FIVE TIMES, NOT FOUR:** fourteen to fifteen, fifteen to sixteen, sixteen to seventeen, **seventeen to nineteen, and nineteen to twenty** — **the fifth move being `ConstraintPassRefused` at rank `20`, appended on the architect's ruling *"Assert the constraints on rollback"* — and the escape arm's argument is strengthened rather than weakened by the fifth move, exactly as it was by the fourth.** **NO TABLE ROW, NO RANK AND NO TRIGGER MOVES BY THIS CLAUSE: the heading's algebra `11 + 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 20` ✓ is the one figure of this section that stood right before it and stands right after it.**
765
+
766
+ ### §8.2 The two states that share `FilePathUnreachable`, and what each means for you
767
+
768
+ **`FilePathUnreachable` is the one member that answers for two unrelated failures, and a handler that treats them
769
+ as one "file problem" will retry a construction that cannot succeed, or discard a running store.**
770
+
771
+ | the state | when you meet it | what it means |
772
+ |---|---|---|
773
+ | **`FilePathUnreachable`** | **at `storeCreate`** — no addon loaded at either rung (§2.3), or a provided `filePath` that failed the construction-time reachability test | **NO INSTANCE WAS RETURNED AND NOTHING WAS CREATED.** There is no store to retry against |
774
+ | **`FileCommitFailed`** | **on a `file`-tier write's save, on an instance that is running** | **THE INSTANCE IS FINE AND THE WRITE FAILED — AFTER THE STORE'S OWN JOURNAL ROLLBACK ALREADY RAN** (§9.2) |
775
+
776
+ **The refused `filePath` forms, each answering `FilePathUnreachable` with nothing created:** an absent parent
777
+ directory; a path that is a directory; an existing file that is not writable; a name occupied by a DANGLING
778
+ SYMLINK; a parent on a read-only mount (`EROFS`); and a create the process is denied (`EACCES`)
779
+ (`docs/specs/storeroom-native/contract.md §8.1` #13's amended trigger clause; `§10.4` part 5).
780
+
781
+ ### §8.3 The refusal that is not `{status, message, log}` — declared, and narrow
782
+
783
+ **Two positions throw a `TypeError` instead of the one form, and both are DECLARED rather than accidental:**
784
+
785
+ 1. **`write`'s `commit` position.** Every form at that position outside the admitted one throws **the class
786
+ test's own `TypeError`, whose `status` member is ABSENT and whose STRING `message` member is REQUIRED**
787
+ (`surface.md §2.4` item 9 (g) item 1; `index.d.ts`, the `write` member). **A refused position stops the write:
788
+ no verb is reached and no byte moves.**
789
+ 2. **The three constraint methods' argument positions.** **Every outside form of either position throws ONE
790
+ `TypeError`, whose `status` member is ABSENT, and nothing is added, enabled, disabled or otherwise changed**
791
+ (`index.d.ts`, `addConstraint`; `surface.md §2.4` item 5's constraint-argument rule (ii);
792
+ `parameter-semantics.md §2.25`, archived).
793
+
794
+ **Measured 2026-10-26, and it is the sharpest of these:** `store.constraintOn({address: ['a'], id: 'c1'}, () =>
795
+ true)` at a pair this entry does not carry threw a **`TypeError`** whose `Object.keys` was `["log"]`, whose
796
+ `status` was **absent**, and whose message read *"the pair at the first argument of `constraintOn` carries no
797
+ constraint in this instance, and a toggle names a pair the entry carries"*. **So the `TypeError` carries `log` and
798
+ no `status`, and no clause declares that member set** (`docs/L1/open-items-2026-10-25.md §2`'s item (d) records
799
+ the residue and its owner, `role_spec_writer`).
800
+
801
+ **THE PRACTICAL RULE, AND IT IS THE WHOLE OF WHAT YOU NEED: catch broad, branch on `status` when it is present,
802
+ and never assume a caught value has one.** **A `TypeError` whose `status` is absent is either a declared
803
+ class-test site above or your own bug — and it is never a store refusal.**
804
+
805
+ ---
806
+
807
+ ## §9 · The `file` tier
808
+
809
+ ### §9.1 Construction, `filePath`, and the file the store leaves behind
810
+
811
+ **`filePath` is OPTIONAL and its absence is in domain: the register is created at `mem`.** **The register's own
812
+ tier is fixed at creation and never moves** — `file` where a `filePath` was provided and passed the probe, `mem`
813
+ otherwise (`docs/specs/storeroom-native/contract.md §10.4`; the archived `write-and-read.md` `§3.2`'s register-tier
814
+ clauses). **That matters because it is the parent anchor
815
+ the monotonic clause is evaluated against at a register key: in a `mem`-register store the `file` tier is
816
+ unreachable at register keys, and through the chain below them.**
817
+
818
+ **★ AND A PROVIDED, ABSENT PATH IS CREATED AT CONSTRUCTION — EXCLUSIVELY, ZERO-LENGTH AND EMPTY, with no register
819
+ key, no journal entry and no registration committed** (`native/contract.md §10.4` part 3; `DD-28`'s amended cell). **So
820
+ `storeCreate` is itself a filesystem act: if your own arrangement of the path depends on its absence, the store
821
+ will have occupied it before you look.**
822
+
823
+ **AN EXISTING FILE IS NEVER TRUNCATED, AND THAT IS DELIBERATE:** the primitive is an exclusive create, and its
824
+ `AlreadyExists` falls through to a write-mode open that writes no byte — **so `storeCreate` over an existing
825
+ store's file is safe, and it is not a reset** (`native/contract.md §10.4` parts 2 and 6).
826
+
827
+ **AND EVERY CONSTRUCTION ARGUMENT IS VALIDATED BEFORE ANY FILESYSTEM ACT**, so a refused construction leaves
828
+ **no instance, no file, no directory and no byte** — which holds for the later-argument failures too, and is what
829
+ the ordering buys (`native/contract.md §10.4` part 4; `§8.1` #1 and #12).
830
+
831
+ **THE FILE IS THE STORE'S, NOT YOURS.** Do not read, edit, parse or migrate it by hand; its shape is the store's
832
+ own and a hand-migration is a corruption waiting to happen. **If you need a migration, read the old value through
833
+ `read` and write the new one through `write`** (`A117`/`A127`'s private-state rule; `write-and-read.md §7.2`).
834
+
835
+ **★ AND ONE NAMED LIMITATION WITH NO RULE, so you do not discover it as a mystery: a `filePath` naming a SYMLINK to
836
+ a real writable file is ACCEPTED at construction, and the first `file`-tier write's save replaces the link with a
837
+ regular file.** **Three remedies exist and all are priced; the choice is the architect's and no clause declares
838
+ one** (`archive/pre-strata-docs-2026-10-24/docs/defects.md`, `DEF-2026-10-13-D`; `strata/brief.md §6`). **If you
839
+ point `filePath` at a symlink deliberately, know that the store will end up holding a regular file at that name
840
+ and the link will be gone.**
841
+
842
+ ### §9.2 Durability is a `file`-tier WRITE's act — and the terminal arm's save is a different save
843
+
844
+ **`DD-28`'s sentence is law: *"`file`-TIER DURABILITY IS PRODUCED BY A SUCCESSFUL `file`-TIER `write` ONLY"***
845
+ (`docs/specs/storeroom-native/contract.md` `DD-28`). **So `store.write(addr, 'file', value)` COMMITS, with no
846
+ kernel attempt and no terminal arm in the path, and a fresh `storeCreate` over the same `filePath` resolves the
847
+ address** (`strata/decisions.md` `D-32`, where the architect chose the native reading, `"a"`). **That correction is
848
+ the single largest of the archived guide's errors: it taught that the write verb performs no file I/O on any of
849
+ its arms, and that a bare `file`-tier write therefore does not persist.**
850
+
851
+ **AND `surface.md §2.11` clause 1 IS READ THROUGH THAT RULING RATHER THAN REPEALED:** *"the verb performs no file
852
+ I/O on any of its arms"* becomes **"the write verb performs no file I/O OF ITS OWN beyond the store's declared
853
+ write"**. **The clause still governs the PACKAGE's layer — it adds no second write path, no scratch copy and no
854
+ package-authored image — and it no longer denies the store's own declared act** (`D-32`, which also supersedes
855
+ nothing of clause 2).
856
+
857
+ **★ AND CLAUSE 2 IS UNTOUCHED AND STILL BINDS, which is why BOTH saves stand:** on the **kernel path**, the
858
+ terminal arm performs the store's `commit_file` **as its first act on the PASSING arm, before any delivered
859
+ listener body runs, keyed by the settled record's own `op`** — and **that is the KERNEL path's save of a whole
860
+ attempt, not a second spelling of the bare write's.** **Both acts stand; the bare write's and the terminal arm's
861
+ are different saves of different scopes, and the same bytes may lawfully be committed by both** (`surface.md
862
+ §2.11` clauses 2–4; `D-32`).
863
+
864
+ **So: no listener body runs before the pass, and none runs before the save on the passing arm.** **A failing pass
865
+ performs NO save at all** — the bytes at `filePath` after it are byte-identical to the bytes that stood there
866
+ before the write (`surface.md §2.11` clause 4 (i)).
867
+
868
+ ### §9.3 A `delete` and a `rollback` perform no file I/O
869
+
870
+ **A `delete` — at ANY tier scope, INCLUDING one that removes `file`-tier versions — performs no file I/O, and a
871
+ `rollback` performs none either.** **The bytes at `filePath` remain the bytes of the last successful `file`-tier
872
+ write, and a fresh construction at the same `filePath` re-boots from those bytes — so the version the delete
873
+ removed in memory STANDS AGAIN after the re-boot.** **That re-boot is the store's own declared boot and not a
874
+ defect** (`DD-28`; `native/contract.md §7.2` item 6; `write-and-read.md §4.3`'s closing clause; `§6.4` `M-9`).
875
+
876
+ **What this means for you, in one line: durability is a property of `file`-tier WRITES. If you delete something
877
+ durable and the process then restarts, expect it back — and delete it again after the boot, or write over it.**
878
+
879
+ ### §9.4 When the save itself fails
880
+
881
+ **On a `file`-tier write's save that fails — the act itself failing, not a constraint — the store rolls the write
882
+ back in the journal AUTOMATICALLY, and the caller receives `FileCommitFailed` AFTER that rollback has run and
883
+ never the rolled-back state.** **The package compensates for nothing** (`surface.md §2.11` clause 5;
884
+ `write-and-read.md §6.4` item 8; `native/contract.md §8.1` rank #15).
885
+
886
+ **So `FileCommitFailed` is the one refusal to take seriously: surface it to the user rather than retrying blindly,
887
+ because the store's own recovery has already happened by the time you see it.**
888
+
889
+ ---
890
+
891
+ ## §10 · The value domain
892
+
893
+ **THE CROSSING IS A DEEP COPY IN BOTH DIRECTIONS.** When you write, the store copies your value into itself; when
894
+ you read, it copies out. **Neither side holds a reference to the other's data, at any verb, at any argument
895
+ position, at any return position** — so you may drop your reference immediately after a write, and a value you
896
+ read is yours to mutate (`docs/specs/storeroom-json-crossing/contract.md §4`'s copy discipline, and `§5`'s
897
+ copy-out form).
898
+
899
+ ### §10.1 The admitted tree
900
+
901
+ **The admitted tree is the bare JSON tree and nothing else** — a **string** that is a sequence of Unicode scalar
902
+ values (no lone surrogate), a **finite** number (`-0` normalizes to `0`), a **boolean**, a **dense, genuine
903
+ `Array`** whose elements are admitted values, and a **plain object** whose own enumerable string-keyed members
904
+ carry admitted values (`json-crossing/contract.md §1`, its bullets).
905
+
906
+ **★ AND THE ONE MEMBER JSON CAN SPELL AND THIS STORE CANNOT: `null` IS NOT ADMITTED.** *"`null` is not admitted,
907
+ although JSON can spell it, because the store's own taxonomy makes `StoreValue::Null` an outside class"* — and
908
+ `undefined` is not admitted either (`json-crossing/contract.md §1`'s closing bullet of that list; `A140`, `A147`,
909
+ `A148`; `src/value.rs`'s admitted/outside taxonomy). **So a JSON payload carrying `null` anywhere in it refuses as
910
+ a WHOLE value — and there is no envelope, no marker and no `{"v":…}`/`{"e":…}` spelling that rescues it** (the
911
+ envelope is retired: `A148` Q1). **And a plain object from ANY realm is admitted** — a `vm` object, a
912
+ worker's object, a `structuredClone`d one (`json-crossing/contract.md §1`'s 2026-10-24 clause, item (1)(a); `§3`'s
913
+ `p` classification `plain`/`foreign`/`null`).
914
+
915
+ **A value is a PLAIN OBJECT when its own prototype is ONE LINK from the value and ends the chain, and that
916
+ prototype carries the standard `Object.prototype` member set** (`json-crossing/contract.md §1`'s 2026-10-24 clause, items
917
+ (1)(a)–(c)). **That is why `Object.create({ a: 1 })` is REFUSED:** its prototype's own prototype is an
918
+ `Object.prototype`, so the value's prototype is not terminal, and it is classified as **`a class instance or
919
+ another prototype-carrying non-plain object`** — the same row a class instance answers (`json-crossing/contract.md §2` row 14;
920
+ `§1` clause's item (1)(b)). **The two refusals are ONE row because they are one shape condition, not because the
921
+ package is being blunt.** A `Object.create(null)` tree, by contrast, is ADMITTED.
922
+
923
+ **An ARRAY is admitted by the same three conditions on its own chain — `value → Array.prototype →
924
+ Object.prototype → null` — and `Array.isArray` alone may not stand as the test** (`json-crossing/contract.md §1` clause of
925
+ 2026-10-24, item (2)). **So a `vm` array, a worker's array and a `structuredClone`d array are admitted by the same
926
+ rule as this realm's own.**
927
+
928
+ ### §10.2 What refuses, and with which status
929
+
930
+ **Every refusal on this route is `ValueRefusedAtBoundary`, decided by the package's own walk BEFORE ANY STORE
931
+ VERB IS CALLED, and it carries the failing target's spelling at its own value position**
932
+ (`json-crossing/contract.md §2`'s class table, whose rows all answer that one status; `§4`'s 2026-10-23 clause, item (4)).
933
+
934
+ **The classes, from `json-crossing/contract.md §2`'s rows — SEVENTEEN rows, and the contract's own 2026-10-24 clause
935
+ makes every one of its census sites read `seventeen`/`17` (read at their own bytes 2026-10-26; no cell of this guide
936
+ drove one):**
937
+
938
+ - **`null` and `undefined`** (rows 1–2) are refused as VALUES, always and everywhere — **and an object member
939
+ whose value is `undefined` refuses the WHOLE value.** The member is not dropped and its key is not skipped,
940
+ because any other answer would drop a member silently (`json-crossing/contract.md §1`'s admitted tree; `DD-8`).
941
+ - **a function** (row 3) · **a symbol** (row 4) · **a `bigint`** (row 6) · **a non-finite number** —
942
+ `NaN`, `Infinity`, `-Infinity` (row 7).
943
+ - **a string carrying a LONE SURROGATE** (rows 9 and 10) — **at a value, and at an object member's KEY**, which
944
+ is why a key you built from a truncated astral character refuses the value around it.
945
+ - **cyclic values** (row 11) — refused on the walk's own ancestor stack.
946
+ - **anything that is not a plain collection** (rows 12–14): a `Map`, a `Set`, a weak collection, a typed array, an
947
+ `ArrayBuffer`, a `DataView`, a `Date`, a `RegExp`, a `Promise`, an `Error`, **a `Proxy`**, a class instance, an
948
+ object wrapper around a primitive (`new String('a')`, `new Number(1)`).
949
+ - **a SPARSE ARRAY** (row 16) — **an array is dense if and only if every index in `0…length − 1` is an OWN
950
+ property; an index whose only carrier is the prototype is a HOLE, and a prototype-carried index is never read**
951
+ (`json-crossing/contract.md §2`'s 2026-10-24 clause, item (5)).
952
+ - **an object or array carrying an own ACCESSOR** (row 15) — **and note the consequence: a getter cannot occur on
953
+ an admitted value, so the copy reads each member exactly once as a structural property rather than as a promise
954
+ about your getter** (`json-crossing/contract.md §4`'s read-once clause).
955
+ - **an object or array carrying an own SYMBOL-keyed member** (row 5).
956
+ - **a class instance, and any other prototype-carrying non-plain object** (row 14) — `Object.create({a: 1})`
957
+ included, for the reason §10.1 gives.
958
+ - **a class extending `Array`** (row 13).
959
+ - **★ an ARRAY CARRYING A NON-INDEX OWN MEMBER** (row 17) — **`const a = [1]; a.foo = 3` is REFUSED, not stored
960
+ without its member.** **The predicate is exact: every string in `Object.keys(a)` must be a canonical index in
961
+ `0…length − 1`**, so a named member on an array has no JSON spelling and the package will not drop it for you
962
+ (`json-crossing/contract.md §2`'s 2026-10-24 clause, items (1)–(8); the arm is decided in the walk —
963
+ `bindings/js/src/entry/carrier.mjs`, read 2026-10-26).
964
+
965
+ ### §10.3 The two ceilings, and there is no knob for either
966
+
967
+ **The walk refuses a value whose nesting depth exceeds `126`, and it refuses one whose node count exceeds
968
+ `1,000,000`.** **BOTH FIGURES ARE THE OPERATIVE ADMISSION CEILING ON BOTH ROUTES, AND DEPTH `127` AND BEYOND IS
969
+ REFUSED AT THE PACKAGE'S WALK, BEFORE ANY STORE VERB, as the depth class `a value over the walk ceiling` with the
970
+ status `ValueRefusedAtBoundary`** (`json-crossing/contract.md §4`'s 2026-10-23 clause, item (2); `§8` row 7's type `integer,
971
+ 0…126`).
972
+
973
+ **AND THE NODE FIGURE IS A VISIT COUNT, WHICH IS THE ONE THING ABOUT IT THAT SURPRISES PEOPLE:** the walk counts
974
+ its root as its first node, so **a value of `n` own nodes counts `n + 1`, the declared admitted own-node range is
975
+ `0…999,999`, and a value of `1,000,000` own nodes is REFUSED** (`json-crossing/contract.md §8` row 8; `§4`'s clause of
976
+ 2026-10-24 on `F-RA-4`). **A shared sub-value is counted ONCE PER POSITION it occupies, not once in the caller's
977
+ graph: a dense root array of `1,000,000` elements is `1,000,000` own nodes and is REFUSED, while an acyclic diamond
978
+ carrying two distinct objects counts three visits and is ADMITTED.** **The ceiling's subject is the crossing's own
979
+ work — the visits the walk performs and the nodes the copy allocates — and a census of your graph would bound
980
+ neither.**
981
+
982
+ **THE PACKAGE SHIPS NO CONFIGURATION KNOB FOR EITHER BOUND** — *"the package ships no configuration knob for
983
+ either bound"* is `DD-10`'s own sentence (`js/contract.md` `DD-10`). **AND TWO OF THAT ROW'S OTHER TERMS ARE
984
+ SUPERSEDED, NAMED SO YOU DO NOT READ THEM OFF IT:** the depth figure it prints is `64`, re-derived to the operative
985
+ `126` by `json-crossing/contract.md §4`'s 2026-10-23 clause, **and the status it prints is `ValueNotAdmitted`, where
986
+ the package's walk now answers `ValueRefusedAtBoundary`** (§8.1's rank `17`). **The row's SUBJECT — one bound per
987
+ call, neither configurable — is what this guide relies on, and it is unmoved.**
988
+
989
+ ### §10.4 The practical rule
990
+
991
+ **If you are storing something that came from a form, a JSON payload or an IPC message, you are already inside
992
+ the domain.** The failures come from storing *live* things — a class instance, a `Date` you meant to serialize, a
993
+ DOM node, a `Buffer`. **Serialize at the boundary, once, consciously** — **and if it cannot cross IPC, it cannot
994
+ be stored, which is one rule for both boundaries** (`strata/brief.md §5` item 2).
995
+
996
+ ---
997
+
998
+ ## §11 · The installed package, exactly
999
+
1000
+ **These are the manifest's own facts, measured 2026-10-26 at `bindings/js/package.json` — not a summary of a
1001
+ contract.**
1002
+
1003
+ | the field | its value |
1004
+ |---|---|
1005
+ | `name` / `version` | `provident-storeroom` / `0.5.0` |
1006
+ | `type` | `module` — **the entry is ESM and there is no CJS arm** |
1007
+ | `license` | `AGPL-3.0` |
1008
+ | `engines` | `{ "node": ">=18.0.0" }` |
1009
+ | `exports` | **ONE arm** — `"."` → `{ "types": "./index.d.ts", "import": "./index.mjs" }` |
1010
+ | `optionalDependencies` | **five names, each pinned exactly to `0.5.0`** (§2.2) |
1011
+ | `files` | **five rows**: `index.mjs` · `README.md` · `index.d.ts` · `src/entry/` · `src/constraint-kernel.mjs` |
1012
+
1013
+ **⟶ AND TWO OF THOSE ROWS ARE CORRECTED AT THIS DATE: `version` is `1.0.0` and each of the five
1014
+ `optionalDependencies` is pinned to `1.0.0`** (`bindings/js/package.json`, read at its own bytes 2026-10-26;
1015
+ `RCA-8(g)` ANNOTATE-BESIDE — the table above stands as the reading of its own date and is read THROUGH this
1016
+ clause). **`name`, `type`, `license`, `engines`, `exports` and `files` are UNMOVED and were re-read at the same
1017
+ bytes; the five `files` rows are still a fact of this tree and not a rule (the ★ paragraph below).**
1018
+
1019
+ **★ THE `exports` MAP HAS ONE ARM, AND THAT IS A PROMISE ABOUT WHAT IS REACHABLE:** **the addon is NOT reachable
1020
+ as a subpath and neither is the constraint kernel.** **There is no wildcard export, no deep import to the addon
1021
+ and no CJS arm** (`bindings/js/README.md`; `docs/specs/storeroom-publish/contract.md §1.3`;
1022
+ `archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-js/package.md §1.3`, archived). **The addon is reached
1023
+ INTERNALLY by the package's own `createRequire`-style step and is not an entry point: a consumer that spells the
1024
+ addon's path directly is outside every clause of the contract** — the load succeeds or it does not, and nothing
1025
+ about it is yours to configure.
1026
+
1027
+ **★ AND THE `files` ALLOWLIST IS A CONSEQUENCE OF REACHABILITY, NOT A DECLARED COUNT.** **The rule that binds is:
1028
+ whatever the entry requires by path must ship in the tarball.** **The strict count is lifted and no hand-maintained
1029
+ arity replaces it** (`strata/decisions.md` `D-3` and `D-4`; `D-4`'s own words: *"The strict count is lifted, the
1030
+ delivery module and the e2e/blind scenario testing will determine the required publish shape"*). **THE FIVE ROWS
1031
+ ABOVE ARE THEREFORE A FACT OF THIS TREE AND NOT A RULE:** the hub imports its parts from `src/entry/` and the
1032
+ barrel, so those paths must ship; **when the entry requires nothing by path, nothing is owed — and neither you
1033
+ nor a later release should read "five" as a contract.**
1034
+
1035
+ **What else no published tarball carries, each for a declared reason** (`publish/contract.md §2.1`; `P-PUB-7`'s surviving
1036
+ prohibitions, which the lifted count did NOT touch):
1037
+
1038
+ - **no `.node` binary, at any key, on any target** — the binary travels in the platform packages;
1039
+ - **no `build/Release` path**;
1040
+ - **no `postinstall` build-on-install step.**
1041
+
1042
+ **AND THE LICENCE ROW IS A SEPARATE QUESTION, ANSWERED SEPARATELY:** **measured 2026-10-26, the five allowlist rows
1043
+ carry no `LICENSE` row, and that is right — the licence TEXT travels at assembly time instead, into all six
1044
+ artefacts, and a `files` row changes no byte of the tarball** (`publish/contract.md §2.3`; `PUB-DD-11`; and the fact that
1045
+ makes the choice cheap). **Whether a row is ever added is EVIDENCE-CONDITIONAL and not rule-bound:** `delivery`
1046
+ adds it **only if an e2e/install scenario shows a consumer or a scanner needs the row** (`strata/decisions.md`
1047
+ `D-8`, reading (iii)). **So do not read the row's absence as a prohibition and do not read its future presence as
1048
+ a change of licence.**
1049
+
1050
+ **⟶ AND FOUR FIGURES OF THIS SECTION ARE RE-READ AT THE SHIPPED TREE, 2026-10-26, EACH AT ITS OWN SITE, SO THE ALLOWLIST IS NOT LEFT AT A COUNT THE MANIFEST NO LONGER CARRIES.** `RCA-8(g)` ANNOTATE-BESIDE: **every table row, every ★ paragraph and every figure above stands BYTE-FOR-BYTE and is read THROUGH this clause; THIS clause re-reads four figures and moves no rule of theirs — the reachability rule, the licence reading's ground and the `engines` floor all stand whole.** **(i) THE `files` ROW READS SIX ROWS, NOT FIVE** — `bindings/js/package.json`'s `files` array, read at its own bytes: `index.mjs` · `README.md` · `README.guide.md` · `index.d.ts` · `src/entry/` · `src/constraint-kernel.mjs` — **`6 = 4 + 2` ✓, the four root files plus the entry directory and the kernel** — and **the sixth row is the consumer guide's landed copy, `README.guide.md`, staged byte-for-byte from `docs/guides/package-use.md` by the release's assembly and imported by nothing** (`scripts/publish.sh`'s guide stage; `distribution.md §4.2` item 3's documented-text class). **THE TWO *"FIVE"* READINGS ABOVE — the table's own *"five rows"* and the ★ paragraph's *"THE FIVE ROWS ABOVE"* — are the reading of their own date and are read THROUGH this clause; and so is the licence paragraph's *"the five allowlist rows"*, whose subject is UNMOVED: NONE of the SIX rows is a `LICENSE` row, and none will be unless `D-8`'s evidence condition is met.** **THE RULE THE COUNT CARRIES IS UNMOVED EITHER WAY: whatever the entry requires by path must ship, and the count is a fact of this tree and not a contract.** **(ii) THE PACKED SET READS NINETEEN MEMBERS BARE AND TWENTY ASSEMBLED** — measured at the shipped packers: **the bare tarball carries `19` files** (`1` `package.json` + `4` root files + `13` entry modules + `1` kernel, `1 + 4 + 13 + 1 = 19` ✓, the guide standing among the four root files), **and the assembled artefact carries `20`, the nineteen plus `LICENSE`, which lands only in the assembled tree** — so a consumer comparing a bare `npm pack` tree with the release's artefact should expect exactly one member's difference and it is the licence. **(iii) AND THE GUIDE ITSELF NOW SHIPS:** **this document's canonical text reaches `node_modules/provident-storeroom/README.guide.md` in an installed package, beside `README.md`** — so a reader of the installed tree holds the document this file is, at that one path, and the landing adds no code, no export and no import. **(iv) THE STATUS SET IS TWENTY** — the figure `§8.1`'s heading and table print, and the one `§8.1`'s own dated clause and `§12.3`'s entry still spell `nineteen`: **each is read through the correction of this date at its own site, and the crate's own `CODES` is `[&str; 20]` at its bytes**, so the declaration of record and the crate agree at twenty.**
1051
+
1052
+ **AND THE `engines` FLOOR IS ONE DECLARATION WITH ONE READER — YOURS.** `>=18.0.0` is what the artefact loads on
1053
+ and it is honest rather than provisional: the addon's highest-referenced Node-API class is the `napi_*` pair at
1054
+ **Node-API version 6** (`Node 12.17.0` / `10.20.0`), read at `bindings/js/build.rs:35`–`:36`, and the ESM entry uses
1055
+ only `node:` builtins — so **raising the floor would be a false restriction rather than a correction**
1056
+ (`archive/pre-strata-docs-2026-10-24/docs/specs/storeroom-publish/parameter-semantics.md §2.6`, `PAR-PUB-6`,
1057
+ archived). **And the one other version number in that file is not a runtime floor at all:** `bindings/js/build.rs`
1058
+ pins `24.11.0` as **the Windows import library's fixed, reproducible byte** — a build input for one platform's
1059
+ linker, exporting the `napi_*` symbols and needing to equal no consumer's runtime (`build.rs:41`). **Every other
1060
+ Node version number in this package's repository is addressed to its publisher or to that linker and not to you.**
1061
+
1062
+ ---
1063
+
1064
+ ## §12 · ★ Behaviours a client can BREAK — declared OUT OF SCOPE
1065
+
1066
+ **This chapter is the most valuable part of this guide, and it is not a list of bugs. It is the list of places
1067
+ where the package's promises STOP — where a client who does something unusual gets a raw host error, a silent
1068
+ wrong answer, or their own code back, and where the project has RULED that this is acceptable rather than
1069
+ defended.**
1070
+
1071
+ **THE RULING BEHIND ALMOST ALL OF IT IS ONE SENTENCE, AND IT IS THE ARCHITECT'S** (`strata/decisions.md` `D-35`,
1072
+ verbatim): *"The client will be able to break the JS code if they want to, the node_modules import is still in
1073
+ client scope. Guard against parameter access and at the Rust boundary."* **So the package defends exactly two
1074
+ things — what it reads off a value YOU supplied, and everything crossing into Rust — and nothing else about your
1075
+ own realm is defended.** **Each entry below says what breaks, why it is out of scope with its ruling, and what to
1076
+ do instead.**
1077
+
1078
+ ### §12.1 Deleting or redefining `Array.prototype` members
1079
+
1080
+ **WHAT BREAKS:** the package reads arrays IT MINTED with the realm's own prototype methods, so **`delete
1081
+ Array.prototype.slice` makes a LAWFUL `write` throw a RAW HOST `TypeError`** — **measured, and the measurement is
1082
+ the ruling's: this guide did not re-take it** — and `Array.prototype.map`/`filter`/`includes` deletions do the same
1083
+ at other sites. **The most exposed site is `written.triggered`**, the copy of the store's returned trigger set that
1084
+ the commit path takes.
1085
+ **WHY IT IS OUT OF SCOPE:** `D-36`, verbatim: *"Presume the client isn't going to vandalize Array.prototype"* —
1086
+ the ~8 such sites are **not debts and are not fixed**, and the premise is the realm's: **the client will not
1087
+ delete or redefine `Array.prototype`'s members.** **WHAT TO DO INSTEAD:** do not patch `Array.prototype` in a
1088
+ process that holds a store. **The distinction the ruling draws is provenance — a value YOU supplied is read by
1089
+ own-property forms and crossed as a null-prototype node; a value the package minted is read with the realm's
1090
+ methods, which is ordinary JavaScript** (`D-36`; `bindings/js/src/entry/kernel-commit.mjs`, the `written.triggered`
1091
+ copy).
1092
+
1093
+ ### §12.2 Detaching or rebinding a store member
1094
+
1095
+ **⟶ CORRECTED 2026-10-26, AND THIS IS THE ONE ENTRY OF THIS CHAPTER WHOSE READING NO LONGER BINDS AT THE
1096
+ DETACHMENT HALF: a detached call is now REFUSED. THE AS-FILED TEXT BELOW STANDS BYTE FOR BYTE AND IS READ
1097
+ THROUGH THIS CLAUSE — `RCA-8(g)` ANNOTATE-BESIDE — so a client who quoted it is not left with a silent
1098
+ reversal.**
1099
+
1100
+ **WHAT REPLACED IT, MEASURED ON THE FIXED TREE:** **every one of the fourteen members that CAN refuse a
1101
+ detached call now refuses it in the ONE plain form** — `{status: 'AddressMalformed', message: "the receiver
1102
+ position is not the store object this addon's storeCreate returned", log: []}` — **and moves no state**.
1103
+ **The twelve that carry the receiver arm answer those exact bytes**: the seven store-reaching verbs (`read`,
1104
+ `readLiteral`, `write`, `delete`, `rollback`, `listen`, `unlisten`), the four constraint members
1105
+ (`addConstraint`, `removeConstraint`, `constraintOn`, `constraintOff`, which until this date ANSWERED — two
1106
+ of them editing the store's pair registry — or threw the constraint methods' `TypeError`) and
1107
+ `removeListener`. **`register` keeps its own domain refusal** (the handle-domain arm, `AddressMalformed`, its
1108
+ own message) where the handle is not one the instance holds, **which is the arm the ruling keeps rather than
1109
+ replaces**; a detached `register` whose handle the entry DOES hold still answers and still moves that entry,
1110
+ because that member's re-install path hands the entry's own published receiver rather than the call's.
1111
+ **`constraints()` still answers a detached read** — its arity is `0`, its own clause declares it *"THROWS
1112
+ NOTHING"* (`docs/specs/storeroom-js/surface.md §2.4` item 9 (f)), and it performs no store call and changes
1113
+ no node, so the state half of the promise holds at it and the refusal half has nothing to refuse.
1114
+
1115
+ **WHY IT IS NO LONGER OUT OF SCOPE:** the architect *"Run the fix by rejecting the detached functions."*
1116
+ **The detached call is not defended because your realm is yours — it is refused because the ANSWER it was
1117
+ giving was a state edit against a store the call had already lost: `constraints()` read what a detached
1118
+ `addConstraint` had carried, and a bound write at that address evaluated a function nobody had lawfully
1119
+ registered. A refusal is what the seven verbs had always answered; the four outliers were the defect.**
1120
+
1121
+ **WHAT BREAKS:** **`const { read } = store; read(['x'])` resolves NO receiver.** **Measured 2026-10-26:** the seven
1122
+ store-reaching verbs (`read`, `readLiteral`, `write`, `delete`, `rollback`, `listen`, `unlisten`) answer the one
1123
+ declared refusal — `{status: 'AddressMalformed', message: "the receiver position is not the store object this
1124
+ addon's storeCreate returned", log: []}`; `register` and `removeListener` answer their own domain refusals in the
1125
+ same form; **and three members do NOT refuse for the detachment at all** — detached `addConstraint` and
1126
+ `removeConstraint` **ANSWERED and edited the real store's pair registry**, detached `constraints()` **answered it**,
1127
+ and the detached toggles threw the constraint methods' `TypeError`. **A REBOUND member is worse: `store.read = fn`
1128
+ succeeds — the `read` member's own descriptor reads `writable: true, enumerable: true, configurable: true` — the
1129
+ call answers your own function, and the package raises nothing and prints nothing** (measured 2026-10-26:
1130
+ descriptor, call and console all read).
1131
+ **WHY IT IS OUT OF SCOPE:** `D-35` — a member is an own property of an object in your own realm, and the package
1132
+ defends parameter access and the Rust boundary, not the mutability of its own product. **WHAT TO DO INSTEAD:**
1133
+ **always call a member on the receiver — never destructure one, never pass one as a callback, and never assign to
1134
+ one.** **And do not rely on the detachment being detected:** for three of the fourteen it is not detected at all,
1135
+ and for two more the answer is the constraint methods' `TypeError` rather than the one form.
1136
+ **⟶ AND THE REBOUND HALF IS UNMOVED AND IS STILL OUT OF SCOPE** (`D-35`, `D-36`): `store.read = fn` still succeeds,
1137
+ still answers your own function, and the package still raises nothing — **rebinding is client scope and is not
1138
+ detected, before this correction and after it.** **The detachment half is what moved: do not rely on a detached
1139
+ call being DETECTED as a call at all — it is refused before it reads or edits anything, which is the answer that
1140
+ keeps `constraints()` and the store's own home entry exactly as they stood.**
1141
+
1142
+ **⟶ AND WHAT IS TRUE AFTER THE DETACHED-CALL REJECTION, IN ONE QUOTABLE LINE — THE ENTRY'S OWN SUMMARY, ADDED
1143
+ BECAUSE THE AS-FILED TEXT BESIDE IT STILL READS AS THE OLD WORLD** (`RCA-8(g)` ANNOTATE-BESIDE; nothing above is
1144
+ withdrawn): **DETACHING IS REFUSED; REBINDING IS THE CLIENT'S OWN FUNCTION AND IS STILL UNDETECTED.** **Detaching —
1145
+ `const { read } = store; read(['x'])` — answers `{status: 'AddressMalformed', message: "the receiver position is not
1146
+ the store object this addon's storeCreate returned", log: []}` at every member that can refuse it, and moves no
1147
+ state; `register` keeps its own domain refusal and `constraints()` answers, because it takes no argument, makes no
1148
+ store call and changes no node.** **Rebinding — `store.read = fn` — succeeds, answers YOUR function, and is still
1149
+ undetected: it is client scope by `D-35`, and it is the half of this entry that remains OUT OF SCOPE.**
1150
+
1151
+ ### §12.3 Freezing or sealing a store
1152
+
1153
+ **WHAT BREAKS:** `Object.freeze(store)` **before** the store's first kernel act, then a lawful `write` or
1154
+ `addConstraint`, throws a **raw host `TypeError`** — *"Cannot define property `Symbol(provident-storeroom.kernel)`,
1155
+ object is not extensible"* — **with `status` absent, not one of the seventeen, and not the one form.** **It is
1156
+ ORDER-DEPENDENT:** freeze *after* any kernel act and the same write succeeds. **WHY IT IS OUT OF SCOPE:** `D-35`
1157
+ classifies it as client-scope breakage, not a defect — **the ruling declines the fix and accepts the form**:
1158
+ *"a raw host `TypeError` where a lawful call used to answer is still worth a note, not a fix."* **WHAT TO DO
1159
+ INSTEAD:** **do not freeze or seal a store.** If you need an immutable handle, wrap it — a plain object holding
1160
+ the instance and closing over it, never the instance itself.
1161
+
1162
+ **⟶ AND THE SET SIZE THIS ENTRY PRINTS IS CORRECTED AT THIS DATE: it is NINETEEN, not seventeen** (§8.1; the algebra
1163
+ `11 + 2 + 1 + 1 + 1 + 1 + 1 + 1 = 19` ✓; `RCA-8(g)` ANNOTATE-BESIDE — the sentence above stands and is read THROUGH
1164
+ this one). **THE ENTRY'S OWN POINT IS UNMOVED AND IS SHARPER AT NINETEEN: a raw host `TypeError` carries NO `status`
1165
+ member at all, so it is not any member of the closed set, whatever the set's size — and a consumer who reads a
1166
+ caught value by its `status` will find none here (§8.3).**
1167
+
1168
+ **⟶ AND THAT CORRECTED SIZE IS ITSELF RE-READ: the set is TWENTY, not nineteen** (§8.1's heading and its table's `20` row; the algebra `11 + 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 = 20` ✓; `RCA-8(g)` ANNOTATE-BESIDE — the correction above stands and is read THROUGH this one). **THE ENTRY'S OWN POINT IS UNMOVED AND SHARPER AT TWENTY: a raw host `TypeError` carries NO `status` member at all, so it is not any member of the closed set, WHATEVER THE SET'S SIZE — and a consumer who reads a caught value by its `status` will find none here (§8.3).**
1169
+
1170
+ ### §12.4 Reaching the kernel record on the store
1171
+
1172
+ **WHAT BREAKS:** **the kernel's record is an own symbol-keyed property of the store and it is MUTABLE.**
1173
+ `Object.getOwnPropertySymbols(store)` yields the slot, and the record carries `pairs`, `handles`, `live`, `closed`
1174
+ and `store`. **`store[SYM].pairs.clear()` makes an ARMED constraint stop refusing — the value lands — while
1175
+ `constraints()` still reports the pair.** **WHY IT IS OUT OF SCOPE:** `D-35` — it is your own realm and your own
1176
+ object; the ruling stands `cdd2467` and declines the revert that would have re-privatised the record. **WHAT TO DO
1177
+ INSTEAD:** **treat the symbol slot as private.** Do not enumerate a store's symbols, do not write to one, and do
1178
+ not read a store's internals to reconstruct state — **`read(address)` and `constraints()` are the routes, and a
1179
+ constraint's enforcement is not yours to toggle by hand.**
1180
+
1181
+ ### §12.5 Prototype forgery on a registered symbol
1182
+
1183
+ **WHAT BREAKS:** **a getter at `Object.prototype[Symbol.for('provident-storeroom.kernel')]` supplies ONE record to
1184
+ EVERY store in the process** — measured in the record at `D-35`: `a[SYM] === b[SYM]` with a shared `attempts`
1185
+ counter of `2` after one write each. **The key is guessable because `Symbol.for` is a REGISTERED symbol, and the
1186
+ store's own read of the slot walks the prototype chain.** **WHY IT IS OUT OF SCOPE:** `D-35`'s third vector of the
1187
+ same client-scope class — **a getter you install on a prototype in your own realm is your own act, and own-property
1188
+ discipline cannot help a READ.** **WHAT TO DO INSTEAD:** **do not install anything on `Object.prototype` or
1189
+ `Array.prototype` in a process that holds a store** — and if a dependency does, that is the dependency's defect
1190
+ reaching your store, not the store's.
1191
+
1192
+ ### §12.6 Values you read back inherit your realm's prototypes
1193
+
1194
+ **WHAT BREAKS:** **the values the crate mints on the way out carry the realm's `Object.prototype`** (and
1195
+ `Array.prototype` for arrays), so **a client's own prototype pollution APPEARS on what it reads back** — a
1196
+ `store.read()` answer inherits any member you have added to `Object.prototype`. **WHY IT IS OUT OF SCOPE:** the
1197
+ contract **does not pin the copy-out's prototype at all**: *"the contract pins the tree and not the allocation"*,
1198
+ and **no cell may assert a prototype identity on the copy-out, in either direction**
1199
+ (`docs/specs/storeroom-json-crossing/contract.md §5`'s 2026-10-24 clause, on `F-RA-15`; the same clause's ground is
1200
+ `D-35`'s client-scope line). **WHAT TO DO INSTEAD:** **do not pollute `Object.prototype`** — and **do not assert
1201
+ that a read-back's prototype is or is not a particular object.** **What IS declared is that the answer is a FRESH
1202
+ tree sharing no mutable member with the store or with another crossing**, which is the guarantee worth relying on
1203
+ (`json-crossing/contract.md §5`, item (3)(c)).
1204
+
1205
+ ### §12.7 A skip is not a green, and your own throws are not refusals
1206
+
1207
+ **WHAT BREAKS:** **a `skipped` cell is a cell that DID NOT RUN** — *"a skip is not a green"*, in the authority
1208
+ log's own words: *"`skipped 1` is a cell that did not run"* (`strata/decisions.md`, the examiner's findings) —
1209
+ **and a client's own throw is not the package's form.** **A listener body that throws is NOT CAUGHT, NOT RETRIED
1210
+ AND NOT REPORTED:** the package does not wrap your callback, and what escapes is your own throwable, not
1211
+ `{status, message, log}` (`docs/specs/storeroom-js/surface.md §2.7` item 4). **WHY IT IS OUT OF SCOPE:** `D-35` —
1212
+ **anything that enters the package from your side and leaves it is your shape, and the one thrown form is the
1213
+ package's answer to its OWN refusals.** **WHAT TO DO INSTEAD:** **catch inside your listener if you want a
1214
+ listener's failure to be yours**, and **never count a skipped test as a passed one** — and never treat a caught
1215
+ value with no `status` as a store refusal (§8.3).
1216
+
1217
+ ---
1218
+
1219
+ ## §13 · What this guide does not cover
1220
+
1221
+ **Stated rather than left to be inferred, because a guide that hides its own edges is the next thing to fail
1222
+ quietly.**
1223
+
1224
+ - **IT DOES NOT COVER ELECTRON, AND THE STORY IT DOES NOT TELL IS NOT ABSENT FROM THE REPOSITORY.** The Electron
1225
+ integration story — main-process-only execution, a data-only preload bridge, `asarUnpack` for `**/*.node`, code
1226
+ signing, a `filePath` under `app.getPath('userData')`, and the verification ladder *envelope* → *loaded in dev*
1227
+ → *loaded in the packaged app* → *installed from the registry* — lives in the archived
1228
+ `archive/pre-strata-docs-2026-10-24/docs/guides/electron-integration.md`. **It describes a superseded surface
1229
+ exactly as this guide's predecessor does, and §2.3's rungs and §11's manifest facts are what it predates.** **A
1230
+ consumer who has not measured their own Electron packaging has measured nothing about it** — `LIVE / INSTALL` is
1231
+ open in this project and the `APP` layer is `NONE` (`strata/brief.md §6`).
1232
+ - **IT DOES NOT COVER THE RELEASED-BYTES LAYER.** Nothing here was driven against a published tarball, and the
1233
+ layer that would do it — an install-shaped battery — is declared and not the thing this file reports on.
1234
+ **`D-4`'s publish shape is evidence-derived, and the evidence is not this guide's.**
1235
+ - **IT DOES NOT DESCRIBE AN APPLICATION.** It says nothing about how you should structure a store, what your
1236
+ schema should be, or whether any program works. It describes the package a consumer installs and what it
1237
+ answers.
1238
+ - **IT DOES NOT SETTLE THE TIER-EVICTION QUESTION AS A SEMANTIC CLAIM.** §5.1 states the rule and what to rely on;
1239
+ whether declared semantics and shipped behaviour agree at every arm and every tier combination is a question
1240
+ for the clauses and not a consumer guide's.
1241
+ - **IT DOES NOT REPORT THE `A112` ORDERING MEASUREMENT.** The law is that the surface runs the triggered
1242
+ constraints first and the other listeners only once every constraint has passed, and §9.2 states it as the law
1243
+ (`surface.md §2.11`). **Whether delivery now follows the pass is `OWED` a measurement on this branch and is not
1244
+ claimed here either way** (`strata/decisions.md` `D-1-bis`'s closing clause; `strata/brief.md §6`).
1245
+ - **IT DOES NOT RE-DERIVE ANY FIGURE IT CITES.** Where a contract's own text and its later annotate-beside clauses
1246
+ print two readings, this guide cites the operative one and names the superseded one where a consumer would
1247
+ otherwise be misled — **and where it cites an archived file, it says so.**
1248
+ - **AND IT STATES NO MEASUREMENT OF ITS OWN beyond the two of 2026-10-26 this file names.** **It is a consumer
1249
+ document, not a test report:** the declared documents are the instrument for questions of semantics, and a
1250
+ driven run is the instrument for questions of behaviour. **This guide tries to say which is which, and it is not
1251
+ the instrument that decides between them.**