provident-storeroom 1.0.0 → 1.1.0

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