@lossless.org/nosqldb 10.10.0 → 11.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.smartconfig.json +21 -19
  2. package/dist_rust/rustdb_linux_amd64 +0 -0
  3. package/dist_rust/rustdb_linux_amd64.tsrust-build.json +5 -5
  4. package/dist_rust/rustdb_linux_amd64_musl +0 -0
  5. package/dist_rust/rustdb_linux_amd64_musl.tsrust-build.json +5 -5
  6. package/dist_rust/rustdb_linux_arm64 +0 -0
  7. package/dist_rust/rustdb_linux_arm64.tsrust-build.json +5 -5
  8. package/dist_rust/rustdb_linux_arm64_musl +0 -0
  9. package/dist_rust/rustdb_linux_arm64_musl.tsrust-build.json +5 -5
  10. package/dist_rust/rustdb_macos_amd64 +0 -0
  11. package/dist_rust/rustdb_macos_amd64.tsrust-build.json +5 -5
  12. package/dist_rust/rustdb_macos_arm64 +0 -0
  13. package/dist_rust/rustdb_macos_arm64.tsrust-build.json +5 -5
  14. package/dist_ts/00_commitinfo_data.js +2 -2
  15. package/dist_ts/ts_local/classes.localnosqldb.d.ts +15 -4
  16. package/dist_ts/ts_local/classes.localnosqldb.js +31 -11
  17. package/dist_ts/ts_nosqldb/index.d.ts +3 -3
  18. package/dist_ts/ts_nosqldb/index.js +5 -4
  19. package/dist_ts/ts_nosqldb/rust-db-bridge.d.ts +125 -2
  20. package/dist_ts/ts_nosqldb/rust-db-bridge.js +261 -46
  21. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.d.ts +69 -3
  22. package/dist_ts/ts_nosqldb/server/classes.nosqldbserver.js +91 -12
  23. package/dist_ts/ts_nosqldb/server/index.d.ts +2 -2
  24. package/dist_ts/ts_nosqldb/server/index.js +2 -2
  25. package/dist_ts/ts_nosqldb/stopped-storage.d.ts +9 -1
  26. package/dist_ts/ts_nosqldb/stopped-storage.js +1 -1
  27. package/dist_ts/ts_nosqldb/storage-rehearsal.js +1 -1
  28. package/dist_ts/ts_nosqldb/storage-root-alignment.js +7 -3
  29. package/dist_ts/ts_nosqldb/storage-root-relocation.js +2 -2
  30. package/package.json +20 -16
  31. package/readme.md +469 -666
  32. package/third-party-notices.md +40 -8
  33. package/third_party_licenses/inventory.json +6 -6
  34. package/ts/00_commitinfo_data.ts +1 -1
  35. package/ts/ts_local/classes.localnosqldb.ts +35 -12
  36. package/ts/ts_nosqldb/index.ts +7 -2
  37. package/ts/ts_nosqldb/rust-db-bridge.ts +291 -48
  38. package/ts/ts_nosqldb/server/classes.nosqldbserver.ts +151 -15
  39. package/ts/ts_nosqldb/server/index.ts +2 -2
  40. package/ts/ts_nosqldb/stopped-storage.ts +9 -1
  41. package/ts/ts_nosqldb/storage-rehearsal.ts +1 -1
  42. package/ts/ts_nosqldb/storage-root-alignment.ts +6 -2
  43. package/ts/ts_nosqldb/storage-root-relocation.ts +1 -1
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lossless.org/nosqldb
2
2
 
3
- A MongoDB-wire-compatible embedded database server powered by a native Rust MVCC engine. It supports the documented command surface through the official `mongodb` driver without an external MongoDB server. Memory and file storage use the same paged engine, including the operation log, point-in-time revert and debug dashboard. Matching executables are bundled with the package.
3
+ A MongoDB-wire-compatible database server powered by its own native Rust MVCC engine, engineered from scratch in a clean room with a public API surface derived from the Apache-2.0-licensed official MongoDB Node.js driver, and built for every scale: from a test fixture or a database embedded in a single application to large enterprise deployments. Its goal is to beat MongoDB on performance and on features. It supports the documented command surface through the official `mongodb` driver without an external MongoDB server. Memory and file storage use the same format-3 store, including the operation log, point-in-time revert and debug dashboard. Matching executables are bundled with the package.
4
4
 
5
5
  ## Issue Reporting and Security
6
6
 
@@ -12,32 +12,29 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
12
12
  pnpm add @lossless.org/nosqldb
13
13
  ```
14
14
 
15
- ## Moving from @push.rocks/smartdb
15
+ ---
16
16
 
17
- The module has moved to `@lossless.org/nosqldb`. Install the new package and update imports and product-specific names together:
17
+ ## What It Does
18
18
 
19
- | Previous name | New name |
20
- | --- | --- |
21
- | `@push.rocks/smartdb` | `@lossless.org/nosqldb` |
22
- | `SmartdbServer` | `NoSqlDbServer` |
23
- | `LocalSmartDb` | `LocalNoSqlDb` |
24
- | `SmartdbStorageRehearsal` | `NoSqlDbStorageRehearsal` |
25
- | `SmartdbDebugServer` / `SmartdbDebugUi` | `NoSqlDbDebugServer` / `NoSqlDbDebugUi` |
26
- | `ISmartdb*`, `ISmartDb*`, `TSmartdb*`, `TSmartDb*` | `INoSqlDb*`, `TNoSqlDb*` |
27
- | `createSmartDb*`, `normalizeSmartDb*`, `smartDb*` | `createNoSqlDb*`, `normalizeNoSqlDb*`, `noSqlDb*` |
28
- | `SMARTDB_*` environment variables | `NOSQLDB_*` environment variables |
29
- | `<smartdb-debugui>` / `/api/smartdb/*` | `<nosqldb-debugui>` / `/api/nosqldb/*` |
30
- | Benchmark `--engine smartdb` | Benchmark `--engine nosqldb` |
19
+ `@lossless.org/nosqldb` is a **real database server** that speaks the wire protocol used by MongoDB drivers. The core engine is written in Rust for high performance, with a thin TypeScript orchestration layer. Connect with the standard `mongodb` Node.js driver — no mocks, no stubs, no external binaries required.
31
20
 
32
- The `./debugui` and `./debugserver` package subpaths stay the same. The new module exports the new API names; it does not export aliases for the previous names. Update explicit binary overrides to `NOSQLDB_RUST_BINARY`, and rename the release-builder environment variables shown below.
21
+ ### From one process to enterprise scale
33
22
 
34
- This is a package and API rename, not a storage-format conversion. Existing current-format v7 databases, authentication records, allocation identities, export formats, and operation receipts retain their original byte formats. Versioned `smartdb.*` format identifiers, the `smartdb` allocation provider, hash domains, and private locking/workspace names deliberately remain unchanged. Do not edit those values in stored data or rewrite existing receipts. The native `rustdb` executable, `rustdb_*` packaged artifact names, and `.__rustdb_*` storage directories also keep their existing identities. Earlier incompatible storage formats remain subject to the admission restrictions documented below.
23
+ NoSQLDB brings its own clean-room engineered, MongoDB-compatible engine to every stage of an application's life:
35
24
 
36
- ---
25
+ - **Development and tests:** start a server from code, in memory or on disk, with nothing else to install.
26
+ - **Embedded:** ship the database inside your application and reach it over TCP or a Unix socket.
27
+ - **Enterprise scale:** large datasets, many collections and indexes, and many concurrent clients on its own durable MVCC engine.
37
28
 
38
- ## What It Does
29
+ The target is to beat MongoDB on performance and on features: more throughput, lower latency, and less memory and disk per operation, with MongoDB-compatible behaviour wherever applications rely on it, and capabilities beyond it where they help, such as the built-in operation log, point-in-time revert and debug dashboard.
39
30
 
40
- `@lossless.org/nosqldb` is a **real database server** that speaks the wire protocol used by MongoDB drivers. The core engine is written in Rust for high performance, with a thin TypeScript orchestration layer. Connect with the standard `mongodb` Node.js driver — no mocks, no stubs, no external binaries required.
31
+ ### MongoDB compatibility
32
+
33
+ NoSQLDB is an independent implementation, written from scratch to be MongoDB compatible. Its public API surface is derived from the client side: the official MongoDB Node.js driver (`mongodb`, Apache-2.0 licensed) and the commands and replies it exchanges. Compatibility is established by runtime testing against MongoDB through its wire protocol and the official drivers. NoSQLDB contains no MongoDB server code. It is built with the clean-room setup of [AGL](https://code.foss.global/modelprofile.com/agl): the sessions that write its code work in confined containers that hold nothing but this repository. Where this readme says a behaviour, code or message is MongoDB compatible, it was implemented to match what such testing observes.
34
+
35
+ Contributions follow the same clean-room rules: describe behaviour as implemented to be MongoDB compatible, never as the same as a MongoDB version; record provenance as observed by runtime testing against MongoDB; and never read, quote, cite or link MongoDB server source code or its test suites.
36
+
37
+ MongoDB is a trademark of MongoDB, Inc. NoSQLDB is not affiliated with or endorsed by MongoDB, Inc.
41
38
 
42
39
  ### Why NoSQLDB?
43
40
 
@@ -49,7 +46,7 @@ This is a package and API rename, not a storage-format conversion. Existing curr
49
46
  | **Persistence** | Memory or file-based | Full disk engine |
50
47
  | **Debug UI** | Built in | External tooling |
51
48
  | **Point-in-time revert** | Built in | Depends on the database |
52
- | **Perfect for** | Unit tests, CI/CD, prototyping, local dev, embedded | Production at scale |
49
+ | **Built for** | Unit tests, CI/CD, local dev, embedded applications and large-scale production | Production |
53
50
 
54
51
  ### Three Ways to Use It
55
52
 
@@ -252,7 +249,7 @@ Each entry contains:
252
249
 
253
250
  ## 🔢 Document Field Order
254
251
 
255
- Like MongoDB, every stored document leads with `_id`. Inserts, upserts, replacements, `findAndModify`, `$out` and `$merge` move a client-supplied `_id` to the front (the Node.js driver appends its generated `_id` after the caller's fields) and generate a missing one there. Every other field keeps its written order: update operators change existing fields in place and append the fields they create, and a replacement keeps its own field order after `_id`. Documents stored with a trailing `_id` by 10.7.3 and earlier keep it until an update, replacement or `$merge` changes them.
252
+ Every stored document leads with `_id`, implemented to be MongoDB compatible. Inserts, upserts, replacements, `findAndModify`, `$out` and `$merge` move a client-supplied `_id` to the front (the Node.js driver appends its generated `_id` after the caller's fields) and generate a missing one there. Every other field keeps its written order: update operators change existing fields in place and append the fields they create, and a replacement keeps its own field order after `_id`. Documents stored with a trailing `_id` by 10.7.3 and earlier keep it until an update, replacement or `$merge` changes them.
256
253
 
257
254
  ---
258
255
 
@@ -260,7 +257,7 @@ Like MongoDB, every stored document leads with `_id`. Inserts, upserts, replacem
260
257
 
261
258
  The engine detects document rewrites that change nothing and skips them entirely — no storage write, no WAL append, no index update, and no oplog entry. A skipped rewrite still counts as matched (`matchedCount: 1`) but reports `modifiedCount: 0`.
262
259
 
263
- As in MongoDB, a rewrite is skipped only when the document stays identical: the same fields in the same order with the same values and BSON types, at every depth. Only the position of `_id` is ignored, so a document stored with a trailing `_id` by 10.7.3 and earlier is not rewritten by a write that changes nothing. A replacement or `$set` that only reorders fields is written. Explicit changes to `_updatedAt` are persisted and returned just like changes to any other field. Applications decide when to advance their timestamps.
260
+ To be MongoDB compatible, a rewrite is skipped only when the document stays identical: the same fields in the same order with the same values and BSON types, at every depth. Only the position of `_id` is ignored, so a document stored with a trailing `_id` by 10.7.3 and earlier is not rewritten by a write that changes nothing. A replacement or `$set` that only reorders fields is written. Explicit changes to `_updatedAt` are persisted and returned just like changes to any other field. Applications decide when to advance their timestamps.
264
261
 
265
262
  Counters are exposed through `serverStatus`:
266
263
 
@@ -338,6 +335,59 @@ try {
338
335
 
339
336
  The socket parent directory must already exist with the caller's desired access
340
337
  permissions. `RustDbBridge` also accepts `{ binaryPath }` for direct lifecycle use.
338
+ When its `spawn()` returns false, `spawnFailureMessage` says what that means as
339
+ far as the bridge knows: with a `binaryPath`, that the engine there did not
340
+ report ready within the bridge's limit, with the path as given and the time
341
+ waited; without one, that no engine binary was found or that the engine found
342
+ did not report ready, as nothing tells which. If ending that engine fails,
343
+ `spawn()` throws instead of returning false, and `spawnFailureMessage` keeps the
344
+ message. After any other `spawn()`, including one that throws on its own
345
+ `timeoutMs` or `signal`, it is undefined. A `binaryPath` that does not exist
346
+ is refused before `spawn()` returns, with `RustBinaryLocatorError`.
347
+
348
+ #### Engine output
349
+
350
+ The engine writes its log to its standard error. None of it reaches the host
351
+ process's standard error: by default the server keeps only the engine's last
352
+ lines, for errors. To receive the log, pass `engineLog`, which is called once
353
+ for every line, in order, without the line break. `onEngineExit` receives a
354
+ `NoSqlDbEngineExitError` for each unexpected exit of the engine: an exit with a
355
+ non-zero status or by a signal while the server runs and no stop is in
356
+ progress. An exit with status 0, which the engine makes after a graceful
357
+ shutdown it was signalled to make, is not reported; the server counts as
358
+ stopped either way.
359
+
360
+ ```typescript
361
+ import { NoSqlDbServer } from '@lossless.org/nosqldb';
362
+
363
+ const server = new NoSqlDbServer({
364
+ storage: 'file',
365
+ storagePath: '/var/lib/app/nosqldb',
366
+ engineLog: (line) => logger.info(`nosqldb: ${line}`),
367
+ onEngineExit: (error) => logger.error(error.message, error.engineStderrTail),
368
+ });
369
+ ```
370
+
371
+ Whatever is set, these carry the engine's last 64 standard-error lines in their
372
+ field `engineStderrTail`, a frozen array of strings, without any change to
373
+ their message:
374
+
375
+ - every rejection of `start()` of `NoSqlDbServer` and `LocalNoSqlDb`, with an
376
+ empty array when the start failed before it spawned an engine;
377
+ - the `NoSqlDbEngineExitError`, whose message is
378
+ `nosqldb Rust process exited unexpectedly (code=<code>, signal=<signal>)` and
379
+ which has `exitCode` and `signal`;
380
+ - the rejection of a management call that fails because the engine no longer
381
+ runs.
382
+
383
+ A line longer than 1,024 characters keeps its first 1,024, followed by
384
+ ` [<n> more characters]`; `engineLog` receives it whole.
385
+ `noSqlDbEngineStderrTailLimits` exports both bounds. `LocalNoSqlDb` accepts
386
+ both options and passes them to its server. `RustDbBridge` accepts `engineLog`
387
+ as well, and its `engineStderrTail` getter returns the last lines of the
388
+ running or most recently spawned engine. Each function runs in its own
389
+ microtask, after the server has recorded the line or the exit; an exception it
390
+ throws is not caught and reaches the process as an uncaught exception.
341
391
 
342
392
  #### Native memory and file storage
343
393
 
@@ -380,54 +430,68 @@ above is an example, not a universal default. Reopen with the stored SCRAM work
380
430
  factor; bootstrap users may be omitted after initialization. `LocalNoSqlDb`
381
431
  accepts `folderPath`, `socketPath`, `binaryPath` and the same `auth` options.
382
432
 
433
+ The engine allocates its memory with jemalloc, under settings built into the
434
+ engine: two arenas, freed memory returned to the operating system over 5
435
+ seconds (dirty decay 5,000 ms, muzzy decay 0), and on Linux the allocator's
436
+ background threads. At start the engine checks that its allocator runs these
437
+ settings and raises its soft open-files limit to its hard limit (on macOS to at
438
+ most `kern.maxfilesperproc`). When either fails, as the check does when
439
+ `_RJEM_MALLOC_CONF` in its environment changes a setting, the engine does not
440
+ start: it exits with status 71, and `start()` rejects with the reason in
441
+ `engineStderrTail`. The `MIMALLOC_ARENA_EAGER_COMMIT` and
442
+ `MIMALLOC_ALLOW_LARGE_OS_PAGES` variables that earlier releases set for the
443
+ engine are no longer set or read.
444
+
445
+ The performance figures this release states are measured with file storage on
446
+ XFS. ext4 was measured only on a loop-backed substitute, so this release makes
447
+ no latency claim for ext4. Recovery after a crash relies on a filesystem that
448
+ never exposes stale blocks after a crash: XFS, or ext4 with ordered data
449
+ (`data=ordered`, its default mode). ext4 mounted with `data=writeback` is not
450
+ supported. On macOS, file storage runs on APFS; the durability gates and the
451
+ timed gates of this release run on Linux only.
452
+
383
453
  #### Native query and maintenance efficiency
384
454
 
385
455
  Bounded synchronous reads, including principal resolution, publication authority
386
- and query setup, share a retained generation lease. No lease or resolved
387
- authorization is reused across requests or asynchronous suspensions.
456
+ and query setup, share one retained snapshot of the root. No such snapshot or
457
+ resolved authorization is reused across requests or asynchronous suspensions.
388
458
 
389
459
  Filesystem ownership and root bindings are validated once per command for
390
460
  reads: the reads of each wire command or background maintenance turn reuse one
391
- full validation of the root and its bindings that began after the command
392
- started, run by its own first storage read or by a concurrent command, and
393
- every retained generation file's binding is checked once per command. Reads go
394
- through retained descriptors, so a path swap cannot change what they return. A
395
- failed validation revokes every earlier one, so commands still running stop at
396
- their next read. A validation covers reads for at most one second after it
397
- began, measured on a monotonic clock, so a long-running command such as a large
398
- aggregation or sort validates again at its first read after that and fails at
399
- that read once its root or a binding has been replaced. Every mutation,
400
- including commit conflict checks, appends, flushes, compaction, relocation and
401
- directory changes, still validates ownership and bindings before and after
402
- itself, and startup, recovery and native management calls, which run outside
403
- wire commands, validate every access. A root, container or generation file
461
+ full validation of the enclosing owner, the root and its bindings that began
462
+ after the command started, run by its own first storage read or by a
463
+ concurrent command. Reads go through retained descriptors, so a path swap
464
+ cannot change what they return. A failed validation revokes every earlier
465
+ one, so commands still running stop at their next read. A validation covers
466
+ reads for at most one second after it began, measured on a monotonic clock,
467
+ so a long-running command such as a large aggregation or sort validates again
468
+ at its first read after that and fails at that read once its root or a
469
+ binding has been replaced. Every write, whether a commit's, a group's, a
470
+ maintenance operation's or an index build's, validates the enclosing owner
471
+ and every binding before it writes, and a commit's conflict checks validate
472
+ in full before and after themselves, before the commit takes its place in
473
+ the log. After a write, when its outcome can be unknown, the root's bindings
474
+ and each file the write wrote, by its binding and its length, are checked
475
+ through the retained descriptors alone, without opening the owner's path; a
476
+ failure there fences the root, and the next command's first access validates
477
+ the enclosing owner again. Startup, recovery and native management calls,
478
+ which run outside wire commands, validate every access. A root or container
404
479
  renamed or replaced between commands fails the next command's first storage
405
- access. Durability, cancellation and close checks remain enforced.
406
-
407
- Compound indexes combine leading equality fields, if any, with a range on the
408
- next field. For example, `{ serverId: 1, timestamp: 1 }` seeks directly into the
409
- requested server's time interval. Inclusive/exclusive endpoints, descending
410
- components, trailing fields and reverse pagination retain their normal
411
- semantics. A range is built from `$gt`, `$gte`, `$lt` and `$lte`, and from
412
- `$ne: null` or a `$nin` list holding null, which read only the non-null keys as
413
- MongoDB's `[MinKey, undefined) ∪ (null, MaxKey]` bounds do: documents without
414
- the field or with an explicit null are not examined. Ranges are built only on
415
- complete indexes whose values all have a scalar order, and on a list field from
416
- one side of the range only (see **List fields** below); other indexes are not
417
- narrowed. The full predicate is still checked for every selected candidate.
480
+ access, and a data or log file replaced underneath the root fails the next
481
+ write that writes it. Durability, cancellation and close checks remain
482
+ enforced.
418
483
 
419
484
  For a query whose filter references only `_id` and whose projection is exactly
420
485
  `{ _id: 1 }`, the native primary index can supply the original BSON identity
421
486
  without loading the document payload. Range endpoints, order, skip, limit and
422
487
  cursor pagination retain their normal semantics. The engine verifies the index
423
- and unique-owner values, the document frame's existence, and either matching
424
- commit versions or a storage-owned SHA-256 certificate of the exact BSON `_id`.
425
- Updates that keep the same identity leave the primary owner and its certificate
488
+ and unique-owner values, the document record's existence, and the storage-owned
489
+ SHA-256 certificate of the exact BSON `_id` that every document record holds.
490
+ Updates that keep the same identity leave the primary owner and the certificate
426
491
  unchanged, avoiding an extra index rewrite. Numeric BSON width changes update
427
492
  the certificate and primary identity atomically with the body. A covered read
428
- does not checksum an unrequested document payload. Older records without a
429
- usable identity proof use the normal bounded body read; the next actual write
430
- adds the certificate.
493
+ does not checksum an unrequested document payload. An entry whose owner names
494
+ another identity than the certificate uses the normal bounded body read.
431
495
  Secondary-index projections and predicates requiring other fields retain normal
432
496
  document reads. No application cache or configuration flag is required.
433
497
 
@@ -449,27 +513,20 @@ discard their uncommitted changes. An admitted command that reaches the closing
449
513
  session owner fails with `ShutdownInProgress` (code 91), and one still waiting
450
514
  for a database or collection gate fails with `InterruptedAtShutdown` (code
451
515
  11600); a transaction statement carries `TransientTransactionError` with
452
- either, as in MongoDB 8. Successful shutdown releases storage-root
516
+ either, implemented to be MongoDB compatible. Successful shutdown releases storage-root
453
517
  ownership and exits with code 0 even when the parent's management pipe remains
454
518
  open. A second shutdown signal, a drain failure, or the overall 40-second
455
519
  deadline forces exit with code 70; forced exit does not promise drain completion.
456
520
  `NoSqlDbServer.stop()` also succeeds after a confirmed engine exit without
457
521
  sending another management request to the dead child.
458
522
 
459
- **Storage compatibility:** version 10 adds a 40-byte certified document-ordinal
460
- record alongside the legacy 8-byte ordinal. It reads existing stores directly;
461
- startup and reads perform no format rewrite. After version 10 writes certified
462
- records, use version 10 or later to reopen that store. Older binaries reject the
463
- new record representation. Keep a pre-upgrade backup if a binary downgrade is
464
- required. The K2 pages, commit markers and recovery protocol are unchanged.
465
-
466
523
  Maintenance batches catalog work and parks completed reclamation and no-TTL
467
524
  sweeps until the committed position changes. Session cleanup skips its metadata
468
- scan until a session can expire. Compaction uses the persisted generation anchor
469
- as its baseline, so reopening an already-compacted root does not rewrite it.
470
- Compaction runs separately from TTL maintenance, observes shutdown cancellation,
471
- and does not restart a large copy because a short maintenance deadline elapsed.
472
- These maintenance optimizations preserve the existing generation and commit formats.
525
+ scan until a session can expire. The store maintains itself on its own thread:
526
+ it merges its write buffer into the tree, writes checkpoints, prunes the
527
+ tombstones no snapshot needs and moves the live data out of data segments
528
+ that hold much garbage. No pass copies the whole root, and there is no
529
+ compaction to run, resume or restart.
473
530
 
474
531
  #### File startup admission
475
532
 
@@ -480,20 +537,26 @@ The exported type is `TNoSqlDbFileStorageStartup`. Invalid values, including
480
537
  `'migrate'`, reject at construction. Selecting the policy with memory storage
481
538
  also rejects.
482
539
 
483
- Default startup accepts a fresh root or a current native root. Use
484
- `fileStorageStartup: 'existing-only'` when the database must already exist:
485
- missing paths, empty directories and identity-only roots without committed
486
- initialization fail instead of creating an empty database. The native owner
487
- enforces this policy; no TypeScript existence check or fallback can create a
488
- new root. Older binaries reject the new option rather than silently ignoring it.
540
+ Default startup accepts a fresh root or a current native root, and converts a
541
+ root that a 10.x release wrote before it serves it (see **Storage format
542
+ migration** below). Use `fileStorageStartup: 'existing-only'` when the database
543
+ must already exist: missing paths, empty directories and identity-only roots
544
+ without committed initialization fail instead of creating an empty database,
545
+ and a 10.x root that holds a committed database counts as existing and is
546
+ converted as under the default. The native owner enforces this policy; no
547
+ TypeScript existence check or fallback can create a new root. Older binaries
548
+ reject the new option rather than silently ignoring it.
489
549
  Rehearsal startup retains its separate current-format-only configuration.
490
550
 
491
- Foreign, old, mixed and
492
- orphaned migration staging layouts are rejected without conversion. TypeScript
551
+ Foreign and mixed layouts, a 10.x name beside entries of this release without
552
+ the record of a conversion in progress, and a 10.x root that the conversion
553
+ cannot take are rejected before anything is written. TypeScript
493
554
  performs no storage writes before starting the engine. Native format admission
494
555
  uses the owning detector; the exclusive native file-root lease is acquired before
495
556
  initialization, recovery, authentication or index preparation.
496
557
 
558
+ A root that carries a format or a feature that this release does not know, such as a root that a later release wrote, is refused before anything is written, and no root is created in its place or beside it. The start's rejection then carries `responseErrorCode: 'UNKNOWN_ROOT'`, which no other start failure carries, and its message names the cause. A missing storage path is created together with every directory missing above it, each accessible to its owner alone; `'existing-only'` creates none of them.
559
+
497
560
  Only after `await server.start()` succeeds should consumers prepare SmartData
498
561
  models or run application migrations. Start cancellation and deadlines cover the
499
562
  native operation; failed-start cleanup confirms child exit, and `stop()` remains
@@ -501,10 +564,9 @@ retryable if termination cannot yet be confirmed. A second owner of the same
501
564
  root is rejected without disturbing the winner. Callers must also exclude
502
565
  manual edits and lifecycle changes outside NoSQLDB's ownership protocol.
503
566
 
504
- No automatic format or auth-permission migration runs during startup.
505
- Persisted-format conversions belong exclusively in the top-level
506
- `ts_migration/` folder and require their own explicit qualification. This
507
- release contains no old-engine or authentication-schema conversion.
567
+ **Storage format migration.** Every root this release creates is a format-3 root under `.nosqldb-control` and `.nosqldb-store`. A start, under either `fileStorageStartup` policy, converts a root that a 10.x release wrote, which it recognizes by `.__rustdb_internal` and `.__rustdb_mvcc`, and then serves the result. The conversion reads the 10.x root without writing into it, builds the format-3 root beside it under this release's names, verifies it record for record against the 10.x content, and only then removes the 10.x entries. It records its progress in `.nosqldb-conversion`, so a start that is stopped or interrupted during the conversion resumes it at the next start. Before it writes anything it declares the space it needs beside the 10.x root, and the start is refused when the file system does not hold it. It carries the database as of the root's last completed commit, with no history of writes committed before the conversion and no part of an import that had not finished. No earlier release reads a root that this release has created, converted or aligned: keep a stopped copy of the 10.x root if you may need to go back. That copy is a copied root: restored into another directory or onto another machine, it keeps the binding of its old place, and 10.8.1, like this release, refuses to start on it until it is aligned. Alignment refuses a root that holds auth users, so for such a root a copy restored that way is no way back. A start refuses, and leaves the path as it found it, a 10.x root that holds no committed database or only part of one, a 10.x name beside entries of this release without the conversion's record, a 10.x root that a running 10.x process holds or that a rehearsal created, a collection whose recorded count its documents do not hold, and a unique index that would hold two documents under one key once each array member is indexed on its own. A 10.x root that was copied, moved or restored converts too but keeps the binding of its old place; like every copied root, it must be aligned (`alignStoppedStorageRoot`) before a start serves it. No authentication or permission migration runs during startup, and this release contains no authentication-schema conversion.
568
+
569
+ A stopped root that a 10.x release wrote can be read before a start converts it. The stopped operations that only read open it read-only, under its own names and its own lock as a 10.x release's stopped operations did, and change nothing in it: the root audit (`inspectStoppedStorageRoot`), the string and namespace inspections (`inspectOfflineStringValue`, `inspectOfflinePhysicalNamespaces`) and data validation (`--validate-data`), which runs the namespace inspection. Alignment, relocation and rehearsal refuse a 10.x root with `UNSUPPORTED`: start a server on it to convert it, then copy and align the converted root. Every stopped operation refuses a path on which a conversion is in progress, one that holds `.nosqldb-conversion`; the root audit answers it as `unavailable` with `UNSUPPORTED`, and the string and namespace inspections and data validation reject it with `UNSUPPORTED`. A start on that path resumes the conversion and finishes it.
508
570
 
509
571
  ```typescript
510
572
  import { NoSqlDbServer } from '@lossless.org/nosqldb';
@@ -546,6 +608,15 @@ const lean = new NoSqlDbServer({
546
608
  oplog: { enabled: false },
547
609
  });
548
610
 
611
+ // The store's memory budget, which its node cache and write buffer share:
612
+ // 4 MiB to 1 TiB, refused outside that range and never clamped
613
+ // (default: computed from the memory the process may use)
614
+ const budgeted = new NoSqlDbServer({
615
+ storage: 'file',
616
+ storagePath: './data',
617
+ storageBudgetBytes: 256 * 1024 * 1024,
618
+ });
619
+
549
620
  // TLS transport for TCP mode
550
621
  const tlsServer = new NoSqlDbServer({
551
622
  port: 27017,
@@ -608,7 +679,7 @@ the transaction's first statement. Supported reads and writes see the
608
679
  transaction snapshot plus its staged changes. Commit publishes atomically;
609
680
  abort discards staged changes.
610
681
 
611
- Write conflicts follow MongoDB: the first statement to write a document or a
682
+ Write conflicts are implemented to be MongoDB compatible: the first statement to write a document or a
612
683
  unique index key wins, and the later writer is told at its own statement.
613
684
 
614
685
  - A transaction statement that inserts, updates, replaces, deletes or upserts a
@@ -620,8 +691,16 @@ unique index key wins, and the later writer is told at its own statement.
620
691
  `commitTransaction` report `NoSuchTransaction` (251), also labelled
621
692
  `TransientTransactionError`. The transaction that wrote first keeps its
622
693
  documents and commits. The driver's `withTransaction` retries the loser on a
623
- fresh snapshot, so a transaction that writes a hot document and then does more
624
- work makes progress against clients looping short transactions on it.
694
+ fresh snapshot, and each attempt can lose again: no priority, ageing or wait
695
+ rule favours a retrying transaction. So a transaction that writes a hot
696
+ document and then does more work makes progress against clients looping
697
+ short transactions on it only by its odds of winning each attempt, with no
698
+ bound on its attempts or its time. Those per-attempt odds are lower than
699
+ MongoDB's, as observed by runtime testing against MongoDB (`mongo:7.0`,
700
+ 7.0.39): MongoDB releases a committing transaction's documents before it
701
+ acknowledges the commit, while NoSQLDB acknowledges a commit as it releases
702
+ them, after the commit is durable. On a host whose durable sync is slower,
703
+ the odds fall further.
625
704
  - Reads never conflict: they see the snapshot, not another transaction's
626
705
  uncommitted writes. A statement that writes nothing (a filter that matches
627
706
  nothing in the snapshot, an update that leaves the document unchanged) does
@@ -635,33 +714,18 @@ unique index key wins, and the later writer is told at its own statement.
635
714
  finish. Writes to other documents are not delayed. A multi-document write
636
715
  outside a transaction stays all-or-nothing: it publishes nothing until it can
637
716
  publish every document.
638
- - A transaction open for longer than 60 seconds is aborted, as by MongoDB's
639
- transaction reaper: its staged writes are discarded and its cursors, write
640
- claims, transaction capacity and database and collection gates are
641
- released. The server
642
- checks idle sessions once a second; a statement still executing when the
643
- limit passes is not interrupted, and the transaction aborts when that
644
- statement has finished (MongoDB instead interrupts that statement with
645
- `TransactionExceededLifetimeLimitSeconds` (290)). Its later statements, `commitTransaction` and
646
- `abortTransaction` report `NoSuchTransaction` (251); statements and
647
- `commitTransaction` carry `TransientTransactionError`, so `withTransaction`
648
- retries from the start. Before that abort, the next writer that needs one of
649
- its documents aborts it too rather than wait. The limit is fixed at
650
- MongoDB's default `transactionLifetimeLimitSeconds` and is not configurable.
651
- Ending or expiring the session, and stopping the server, release a
652
- transaction's writes as well.
653
717
  - TTL expiry is not a contending writer. It leaves out the documents an open
654
718
  transaction has written, deletes the other expired documents of the index
655
719
  and moves past the held ones. A later pass over the index expires those
656
720
  still expired once the transaction has ended. It never waits for a transaction and never aborts
657
- one, whatever its age. MongoDB's TTL monitor instead retries a conflicting delete until
658
- the transaction ends and leaves aborting it to its transaction reaper.
721
+ one, whatever its age. MongoDB's TTL expiry, as observed by runtime testing, instead retries a conflicting delete until
722
+ the transaction ends and leaves aborting it to the transaction lifetime limit.
659
723
  - A key already visible in the transaction's snapshot is not a conflict:
660
724
  inserting it fails the statement with `DuplicateKey` (11000), as outside a
661
725
  transaction. A collection created inside a transaction has no stored identity
662
726
  until commit, so two transactions creating the same collection both succeed
663
- their statements and the second commit reports `WriteConflict` (112), as on
664
- MongoDB 7.
727
+ their statements and the second commit reports `WriteConflict` (112),
728
+ implemented to be MongoDB compatible.
665
729
 
666
730
  Write intents live in memory only; no transaction survives a restart, so
667
731
  there is nothing to recover.
@@ -681,15 +745,6 @@ options. A new collection or a changed index catalog waits only for the
681
745
  transactions that named that collection, and revalidates under its exclusive
682
746
  collection gate before any publication (see "Database waits" below).
683
747
 
684
- Logical sessions survive socket disconnects. Expiry, `endSessions` and
685
- `killSessions` drain active work and release cursors and namespace gates.
686
- A `commitTransaction` or `abortTransaction` retried while its session is being
687
- ended still replays the transaction's stored outcome, so a committed
688
- transaction is never reported as aborted; a transaction that was still open
689
- when its session ended publishes nothing and reports `NoSuchTransaction` (251).
690
- Rejected new transaction envelopes do not consume session capacity or reset
691
- existing sessions. See the native resource budgets below.
692
-
693
748
  ### Durable Committed Writes
694
749
 
695
750
  `NoSqlDbServer.getCommittedWrites()` and the database-scoped wire command
@@ -726,13 +781,16 @@ an application write; subsequent serving publications start retained coverage
726
781
  after that boundary. A move preserving the provider identity preserves the
727
782
  cursor epoch, but any uncovered native publication still reports a gap.
728
783
 
729
- Opening a 10.8.1 root does not initialize a journal or rewrite any stored record.
730
- The first new serving publication atomically adds optional journal and coverage
731
- records, beginning at the old committed head; earlier history explicitly gaps.
732
- This is additive lazy journal initialization, not a historical rewrite. Engines
733
- before 10.9 do not recognize the added record families, so do not downgrade a
734
- root after 10.9 writes. A deployment pinned to 10.8.1 remains unaffected unless
735
- its storage root is deliberately opened for writes with the new engine.
784
+ A start converts a root that a 10.x release wrote, a 10.8.1 root included, into
785
+ this release's format (see **Storage format migration** under File startup
786
+ admission). The converted root keeps the 10.x root's committed position as its
787
+ head and carries no history of writes committed before the conversion: a cursor
788
+ before that head gets `gap: true`, and publications after the conversion are
789
+ retained from that head on. No earlier release reads the converted root. A
790
+ deployment pinned to 10.8.1 is unaffected until a start of this release opens
791
+ its root; keep a stopped copy of the root before that if you may need to go
792
+ back. For a root that holds auth users, a copy restored into another directory
793
+ is no way back (see **Storage format migration** under File startup admission).
736
794
 
737
795
  Pages are exclusive of `after` and follow commit order. Their `cursor` advances
738
796
  over examined publications even when none belongs to the requested database;
@@ -819,7 +877,7 @@ const released = await server.commitDatabasePublication({
819
877
  });
820
878
  ```
821
879
 
822
- The held barrier survives restart and blocks wire commands, transactions and index restoration for that database while unrelated databases remain available. A blocked wire command fails with `ConflictingOperationInProgress` (code 117, no error label) naming `EFENCE_PUBLICATION_HELD`. `exportHeldDatabase()` is a bounded management-only read of one pinned committed catalog, auth and fence root. It requires the exact still-held receipt and capability, returns the same canonical Extended JSON shape as `exportDatabase()`, and does not release the barrier. Ordinary export and wire access remain closed; native TTL maintenance cannot expire held documents. A wrong, stale or already released receipt fails closed. Native physical compaction preserves the held records. Commit is exact and idempotent. A successful commit removes the raw capability from durable state and retains only protected verification material. Provider/root identity and bounded validation of native fencing records make copied, incomplete or corrupt allocation state fail closed. A higher fencing token compacts resolved older receipts, so long-lived coordinators do not exhaust receipt capacity.
880
+ The held barrier survives restart and blocks wire commands, transactions and index restoration for that database while unrelated databases remain available. A blocked wire command fails with `ConflictingOperationInProgress` (code 117, no error label) naming `EFENCE_PUBLICATION_HELD`. `exportHeldDatabase()` is a bounded management-only read of one pinned committed catalog, auth and fence root. It requires the exact still-held receipt and capability, returns the same canonical Extended JSON shape as `exportDatabase()`, and does not release the barrier. Ordinary export and wire access remain closed; native TTL maintenance cannot expire held documents. A wrong, stale or already released receipt fails closed. Commit is exact and idempotent. A successful commit removes the raw capability from durable state and retains only protected verification material. Provider/root identity and bounded validation of native fencing records make copied, incomplete or corrupt allocation state fail closed. A higher fencing token compacts resolved older receipts, so long-lived coordinators do not exhaust receipt capacity.
823
881
 
824
882
  Durable database publication holds and allocation fencing use native file storage on the supported Linux and macOS filesystems.
825
883
 
@@ -829,7 +887,7 @@ Coordinators that do not yet have their own durable record can call `getDatabase
829
887
 
830
888
  `allocateDatabaseTenant()` is the create-only control-plane API for permanently allocation-managed database names. It is available only when `allocationFencingVersion === 1`, `allocationFencingSupported === true`, and `allocationFencingRequiresDrain === true`. The request requires literal `expectedAbsent: true`, an `INoSqlDbResourceFence`, and the tenant database, username, and password. `roles` is optional and defaults to `['readWrite', 'dbAdmin']`. Under native ownership and database/control admission, NoSQLDB proves catalog and principal absence, then commits the catalog, principal, complete auth allocation binding and fencing receipt at one position. It returns an `INoSqlDbAllocateDatabaseTenantResult`; its `.allocation` field is the durable `INoSqlDbDatabaseAllocationIdentity`. Exact fence replay returns the same allocation ID, generation, principal ID, and receipt digests.
831
889
 
832
- Pass the returned `allocation` identity to every later `ensureDatabaseTenant()`, `importDatabase()`, and `deleteDatabaseTenant()` request for that name. Managed import and delete requests also require the exact allocation `username`; the identity alone is insufficient. Each distinct managed mutation requires a strictly newer fence token; the current token is accepted only for exact receipt replay or continuation, and older receipts become explicitly stale after advancement. Held publication receipts carry the allocation into `commitDatabasePublication()`. Managed requests without it and unmanaged requests with it fail closed. Deprovision drains affected runtime authority, then atomically retires the catalog, exact principal, grants and auth binding with a permanent `deprovisioned` fencing tombstone. A later allocation requires a newer fence token, increments generation, and receives a fresh allocation and principal identity. Wire data access is available only while active and only to the current allocation principal; any other principal is refused with `Unauthorized` (code 13), and while the allocation is not active every wire command on the database is refused with `Unauthorized` naming `EALLOCATION_DEPROVISIONED`. The database/user ownership-changing wire commands `dropDatabase`, `createUser`, `updateUser`, `dropUser`, `grantRolesToUser` and `revokeRolesFromUser` are refused with `Unauthorized` (code 13), as MongoDB 8 refuses a user without the privilege.
890
+ Pass the returned `allocation` identity to every later `ensureDatabaseTenant()`, `importDatabase()`, and `deleteDatabaseTenant()` request for that name. Managed import and delete requests also require the exact allocation `username`; the identity alone is insufficient. Each distinct managed mutation requires a strictly newer fence token; the current token is accepted only for exact receipt replay or continuation, and older receipts become explicitly stale after advancement. Held publication receipts carry the allocation into `commitDatabasePublication()`. Managed requests without it and unmanaged requests with it fail closed. Deprovision drains affected runtime authority, then atomically retires the catalog, exact principal, grants and auth binding with a permanent `deprovisioned` fencing tombstone. A later allocation requires a newer fence token, increments generation, and receives a fresh allocation and principal identity. Wire data access is available only while active and only to the current allocation principal; any other principal is refused with `Unauthorized` (code 13), and while the allocation is not active every wire command on the database is refused with `Unauthorized` naming `EALLOCATION_DEPROVISIONED`. The database/user ownership-changing wire commands `dropDatabase`, `createUser`, `updateUser`, `dropUser`, `grantRolesToUser` and `revokeRolesFromUser` are refused with `Unauthorized` (code 13), the MongoDB-compatible refusal of a user without the privilege.
833
891
 
834
892
  Allocation bindings are native records, excluded from logical database exports and never accepted from import payloads. Startup and stopped-storage inspection require the complete matching fence and auth binding even when no read grant was ever issued. Missing bindings are rejected without reconstruction. This includes allocated roots produced by experimental native versions that did not create the complete binding. This release provides no automatic conversion or downgrade path; never remove or edit private metadata to force admission.
835
893
 
@@ -933,7 +991,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
933
991
  | Method / Property | Type | Description |
934
992
  |---|---|---|
935
993
  | `start(options?)` | `Promise<void>` | Start under one optional cancellation/deadline budget; join partial sidecars before rejection or retain ownership for `stop()` retry if termination fails |
936
- | `stop()` | `Promise<void>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup |
994
+ | `stop()` | `Promise<INoSqlDbServerStopResult>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup; resolves with how the engine ended |
937
995
  | `getConnectionUri()` | `string` | Get the active `mongodb://` URI; before start and after stop, port `0` remains unresolved |
938
996
  | `running` | `boolean` | Whether the server is currently running |
939
997
  | `port` | `number` | Actual bound port while running; otherwise the configured port (TCP mode) |
@@ -961,6 +1019,30 @@ await client.db('admin').command({ usersInfo: 'reader' });
961
1019
  | `deleteDatabaseTenant(params, options?)` | `Promise<INoSqlDbDeleteDatabaseTenantResult>` | Durably delete an exact tenant database/user, optionally leaving publication held; supports fail-stop cancellation and deadlines |
962
1020
  | `commitDatabasePublication(params, options?)` | `Promise<TNoSqlDbCommitDatabasePublicationResult>` | Idempotently release an exact held publication receipt; supports fail-stop cancellation and deadlines |
963
1021
 
1022
+ `stop()` resolves with an `INoSqlDbServerStopResult` that says how the engine
1023
+ ended: `stopRequestSent`, whether a stop request was sent to a running
1024
+ engine; `stopAcknowledged`, whether the engine acknowledged it;
1025
+ `forcedByServer`, whether the server had to signal or kill the engine to end
1026
+ it; and `exitCode` and `exitSignal`, as the engine process's exit reported
1027
+ them, each `null` where there was none. `clean` is true exactly when the
1028
+ request was sent and acknowledged, the server neither signalled nor killed the
1029
+ engine, and the engine exited with code 0 and no signal. After an acknowledged
1030
+ request, `stop()` closes the engine's management input and gives the engine
1031
+ `noSqlDbServerStopGraceMs` (10,000 ms) to exit by itself; only an engine that
1032
+ has not exited by then, or one that did not acknowledge, is ended by the
1033
+ server. A stop when no engine runs resolves with a result that says no request
1034
+ was sent, and concurrent callers share one result. `stop()` rejects only when
1035
+ the bridge's termination fails, as before. `LocalNoSqlDb.stop()` resolves
1036
+ with its server's result.
1037
+
1038
+ A start that meets a storage root, or a feature of one, from a later NoSQLDB
1039
+ release than the one starting refuses it before it writes anything there and
1040
+ creates no root in its place. `start()` of `NoSqlDbServer` and of
1041
+ `LocalNoSqlDb` then rejects with a `NoSqlDbUnknownRootError`, whose `code`
1042
+ is `UNKNOWN_ROOT` and whose message names that cause; its `cause` is the
1043
+ management error that carried the code. A caller tells this refusal apart
1044
+ from every other start failure by the code, never by the message.
1045
+
964
1046
  ### NoSqlDbStorageRehearsal
965
1047
 
966
1048
  `NoSqlDbStorageRehearsal.createDestination(input, options?)` creates an isolated,
@@ -1050,11 +1132,14 @@ publication and validated on retry. Every other record's bytes and version are
1050
1132
  preserved, with unchanged complete non-profile record count and fingerprint.
1051
1133
  Its authority admits no internal relocation or alignment workspace. An
1052
1134
  unrelated staged workspace rejects before native opening, append or recovery.
1053
- Existing v2 rehearsal journals without a plan remain readable: an unfinished
1054
- binding intent explicitly migrates to the v3 journal and acquires a plan before
1055
- its first append, while an already-bound
1056
- stage is verified without rebinding or adding a second commit. Existing native
1057
- manifests, fencing-profile schemas and committed records need no rewrite.
1135
+ This release neither rehearses a root that a 10.x release wrote nor finishes
1136
+ a rehearsal that a 10.x release began, or returns the receipt of one that it
1137
+ finished. A request whose source, destination or unfinished copy is such a
1138
+ root, in whole or in part, fails with `UNSUPPORTED`, and the retry of one
1139
+ whose 10.x roots starts have converted since fails with `UNSUPPORTED` or
1140
+ `SOURCE_CHANGED`; no such request changes either path or the operation
1141
+ workspace. Finish a 10.x rehearsal with that release before a start converts
1142
+ its source, or rehearse the converted root into another destination.
1058
1143
 
1059
1144
  Active source owners, rehearsal-as-source, old or mixed layouts, unknown entries,
1060
1145
  unfinished import or fencing operations, active read grants, unfinished or
@@ -1157,7 +1242,7 @@ underlying server and bridge are constructed by `LocalNoSqlDb.start()`.
1157
1242
  | Method / Property | Type | Description |
1158
1243
  |---|---|---|
1159
1244
  | `start()` | `Promise<ILocalNoSqlDbConnectionInfo>` | Start and return connection info |
1160
- | `stop()` | `Promise<void>` | Stop the server |
1245
+ | `stop()` | `Promise<INoSqlDbServerStopResult>` | Stop the server; resolves with its server's stop result |
1161
1246
  | `getConnectionInfo()` | `ILocalNoSqlDbConnectionInfo` | Get current connection info |
1162
1247
  | `getConnectionUri()` | `string` | Get the connection URI |
1163
1248
  | `getServer()` | `NoSqlDbServer` | Access the underlying server |
@@ -1225,9 +1310,10 @@ source, inspect it for pins, copy it while it remains stopped, and align the cop
1225
1310
  before its first start. Pins cannot prove that an unverified backup is complete;
1226
1311
  a file truncated exactly at a commit boundary can still be internally consistent.
1227
1312
 
1228
- Version 1 requires current production file storage, a clean committed tail, no
1229
- generation newer than the manifest, no auth users, and no fencing, allocation,
1230
- publication-hold, grant or import-stage authority. The complete catalog, auth,
1313
+ Version 1 requires current production file storage, a clean committed tail that
1314
+ no serving open would still repair, no auth users, and no fencing, allocation,
1315
+ publication-hold, grant or import-stage authority. A root that a 10.x release
1316
+ wrote is refused; a start converts it first. The complete catalog, auth,
1231
1317
  index, transaction-outcome and fencing audits must pass. Rehearsals, memory
1232
1318
  storage, unknown files (including `.DS_Store`/AppleDouble), symlinks, hard links
1233
1319
  and unsafe ownership or modes are refused. The operation never repairs them.
@@ -1260,8 +1346,9 @@ existing file is never overwritten. Use the relocation identifier rules for
1260
1346
  `acknowledgeUnverifiedCopy: true` when no source pins survive, never both.
1261
1347
  `binaryPath`, `timeoutMs` and `signal` select the engine and bound the operation.
1262
1348
 
1263
- The following archive recipe was qualified from macOS arm64 APFS to Linux
1264
- x86_64 ext4 and XFS. Run as the service user, with the source stopped throughout,
1349
+ The following archive recipe was qualified on this release's format-3 roots,
1350
+ from macOS arm64 APFS to Linux x86_64 ext4 and XFS (alignment.md records the
1351
+ run). Run as the service user, with the source stopped throughout,
1265
1352
  and retain its inspection pins separately. Start with a new private destination
1266
1353
  parent; preserve the private file/directory modes and do not include filesystem
1267
1354
  metadata sidecars. On macOS use the exact canonical case and Unicode spelling;
@@ -1302,16 +1389,16 @@ include `INVALID_REQUEST`, `NOT_FOUND`, `DESTINATION_CONFLICT`, `STATE_CORRUPT`,
1302
1389
  `abortStoppedStorageRootAlignment(request, options?)` accepts the identical
1303
1390
  request and only rolls back an exact **incomplete** planned append. A complete
1304
1391
  append must finish alignment. If permissions changed after interruption, restore
1305
- the required ownership and modes first. Abort permits changed timestamps while
1306
- still checking original inodes, the full inventory, original bytes and append
1307
- prefix. It durably records abort, truncates/fsyncs, removes its exact reserved
1308
- receipt and cleans up. Retry abort after an interrupted abort; alignment cannot
1309
- override that decision. An absent workspace returns `not-pending`.
1310
-
1311
- Older 8.0.2 engines reject pending workspaces with “startup rejects existing
1312
- file-provider bookkeeping”. Finish or abort with the newer engine; deleting the
1313
- workspace is not a fix. A v8-produced fixture remains readable by v8 after this
1314
- one-profile append. This does **not** allow downgrading v10-created catalogs.
1392
+ the required ownership and modes first. Abort permits a changed change time,
1393
+ which restoring ownership or modes leaves, while still checking every file's
1394
+ identity, length and modification time, the original bytes, and that the planned
1395
+ append's place holds only its own bytes or zeros. It durably records abort,
1396
+ writes zeros where the interrupted append wrote and syncs, removes its exact
1397
+ reserved receipt and cleans up. Retry abort after an interrupted abort; alignment
1398
+ cannot override that decision. An absent workspace returns `not-pending`.
1399
+
1400
+ Finish or abort a pending alignment with this release; deleting the workspace is
1401
+ not a fix. No earlier release reads a root that this release has aligned.
1315
1402
  Qualification results and study corrections are recorded in the repository's
1316
1403
  [alignment design note](https://code.foss.global/lossless.org/nosqldb/src/branch/main/alignment.md).
1317
1404
 
@@ -1324,7 +1411,14 @@ through the complete record-owner audit. A running engine returns `BUSY`.
1324
1411
 
1325
1412
  An `audited` result includes the recorded provider identity, live binding state,
1326
1413
  lineage (`groupId`/`timelineId`), decimal-string LSN and counts, full-record and
1327
- profile-excluded SHA-256 digests, generation inventory and alignment eligibility.
1414
+ profile-excluded SHA-256 digests, `storageFormat: 'format-3'` for a root of this
1415
+ release or the generation inventory of a root that a 10.x release wrote, and
1416
+ alignment eligibility. A format-3 root that a serving open would still repair,
1417
+ such as one whose log ends inside an unfinished write, is ineligible with
1418
+ `RECOVERY_PENDING`. A root that a 10.x release wrote is audited read-only as that
1419
+ release audited it, with the same identity, counts, digests and generation
1420
+ inventory, and is ineligible with `CONVERSION_PENDING` first among its reasons:
1421
+ this release does not align it, and a start converts it first.
1328
1422
  It counts allocation replays as well as fences, receipts, holds and allocations.
1329
1423
  The audit accepts no active grants or import stages. Auth users or authority
1330
1424
  history make a fully audited root ineligible for independent-fork alignment.
@@ -1344,10 +1438,15 @@ the TypeScript interfaces for older engine binaries.
1344
1438
  #### Offline String-Value Inspection
1345
1439
 
1346
1440
  `LocalNoSqlDb.inspectOfflineStringValue(input, options?)` reads one string from a
1347
- stopped current native file root on supported Linux filesystems or macOS APFS.
1441
+ stopped native file root of this release, or read-only from one that a 10.x
1442
+ release wrote, on supported Linux filesystems or macOS APFS.
1348
1443
  It starts only a disposable management sidecar, acquires the existing native
1349
1444
  leases read-only, and pins the committed root. It never starts a listener,
1350
- initializes storage, repairs tails, replays writes or compacts the source.
1445
+ initializes storage or writes to the source: a root of this release is
1446
+ recovered in memory only, as a start would recover it, and a cut log tail or a
1447
+ leftover anchor replacement stays on disk as found. A root that a 10.x release
1448
+ wrote is read up to its committed state, and one that a 10.x process left
1449
+ without a clean stop is refused with `RECOVERY_REQUIRED`.
1351
1450
 
1352
1451
  ```typescript
1353
1452
  import { LocalNoSqlDb } from '@lossless.org/nosqldb';
@@ -1438,7 +1537,7 @@ await debugServer.start();
1438
1537
  await debugServer.stop();
1439
1538
  ```
1440
1539
 
1441
- The UI is bundled as base64-encoded content (via `@git.zone/tsbundle`) and served from memory — no static file directory needed.
1540
+ The UI is bundled as base64-encoded content (via `tsrepo bundle` from `@git.zone/tsrepo`) and served from memory — no static file directory needed.
1442
1541
 
1443
1542
  ### NoSqlDbDebugUi (Web Component)
1444
1543
 
@@ -1490,72 +1589,20 @@ await collection.findOneAndReplace({ _id: id }, { name: 'Replaced' }, { returnDo
1490
1589
 
1491
1590
  ### Query Operators
1492
1591
 
1493
- ```typescript
1494
- // Comparison
1495
- { age: { $eq: 25 } } { age: { $ne: 25 } }
1496
- { age: { $gt: 18 } } { age: { $lt: 65 } }
1497
- { age: { $gte: 18 } } { age: { $lte: 65 } }
1498
- { status: { $in: ['active', 'pending'] } }
1499
- { status: { $nin: ['deleted'] } }
1500
-
1501
- // Logical
1502
- { $and: [{ age: { $gte: 18 } }, { active: true }] }
1503
- { $or: [{ status: 'active' }, { admin: true }] }
1504
- { $not: { status: 'deleted' } }
1505
-
1506
- // Element
1507
- { email: { $exists: true } }
1508
- { type: { $type: 'string' } } { value: { $type: ['int', 'long'] } }
1509
-
1510
- // Array
1511
- { tags: { $all: ['mongodb', 'database'] } }
1512
- { scores: { $elemMatch: { $gte: 80, $lt: 90 } } }
1513
- { labels: { $not: { $elemMatch: { $nin: ['linux', 'gpu'] } } } }
1514
- { items: { $elemMatch: { sku: 'a', qty: { $gt: 2 } } } }
1515
- { items: { $all: [{ $elemMatch: { sku: 'a' } }, { $elemMatch: { qty: 0 } }] } }
1516
- { tags: { $size: 3 } }
1517
-
1518
- // Regex
1519
- { name: /^Al/i } { name: { $regex: '^al', $options: 'i' } }
1520
- { tags: { $in: [/^db/, 'mongodb'] } }
1521
-
1522
- // Aggregation expressions
1523
- { owner: 'o', $expr: { $lte: [{ $add: ['$retainedBytes', '$reservedBytes', size] }, max] } }
1524
- { $expr: { $gt: ['$leaseExpiresAt', '$$NOW'] } }
1525
- ```
1526
-
1527
- `$type` takes a type alias (`'string'`, `'number'`, …), a numeric BSON type
1528
- code or an array of them, and matches an array field through its elements as
1529
- well as `'array'` itself.
1530
-
1531
- `$comment` annotates a filter without constraining it, at the top level and
1532
- in an `$and`, `$or`, `$nor` or `$elemMatch` document; under a field it is
1533
- refused as an unknown operator, as on MongoDB.
1534
-
1535
- A filter is checked as MongoDB 8 checks it, before any document is read and
1536
- with its messages: `$and`, `$or` and `$nor` need a non-empty array of
1537
- documents, `$in`, `$nin` and `$all` an array, and a `$in` or `$nin` member
1538
- whose first field starts with `$`, such as `{ $gt: 1 }`, is refused
1539
- (`cannot nest $ under $in`), each with `BadValue` (2). A DBRef member such as
1540
- `{ $ref: 'users', $id: id }` is a value. Partial index filters and `$pull`
1541
- conditions refuse the query features MongoDB 8 bans there: `$expr` and
1542
- `$jsonSchema` with `QueryFeatureNotAllowed` (224), `$where` and `$text` with
1543
- `BadValue`, and `$near`, `$nearSphere` and `$geoNear` with `Location5626500`.
1544
-
1545
- `$elemMatch` has MongoDB's two forms. An operand whose first key is a query
1592
+ `$elemMatch` has two forms, implemented to be MongoDB compatible. An operand whose first key is a query
1546
1593
  operator applies its operators (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`,
1547
1594
  `$in`, `$nin`, `$exists`, `$type`, `$regex`, `$size`, `$mod`, `$all`, `$not`,
1548
1595
  `$elemMatch`) to each element as a whole; any other operand is a query over
1549
1596
  each embedded document, and a nested array is matched as a document keyed by
1550
1597
  index. A dotted path tests every array it reaches on its own. `$expr` inside
1551
- `$elemMatch` is refused with `BadValue` (2), as on MongoDB.
1598
+ `$elemMatch` is refused with `BadValue` (2), implemented to be MongoDB compatible.
1552
1599
 
1553
1600
  A regular expression matches the same way wherever a filter holds one — as a
1554
1601
  field's value, through `$regex` and `$options`, in a `$in`, `$nin` or `$all`
1555
1602
  list, under `$not`, and as a `$pull` condition: a string or symbol matches when
1556
1603
  the pattern finds a match in it, an array matches through its elements, a
1557
1604
  stored regular expression matches only when its pattern and options are equal,
1558
- and a missing field never matches. `$options` takes MongoDB's flags `i`, `m`,
1605
+ and a missing field never matches. `$options` takes the MongoDB-compatible flags `i`, `m`,
1559
1606
  `s`, `x` and `u`. Patterns run on Rust's `regex` crate rather than PCRE, so
1560
1607
  matching takes time linear in the input, constructs only PCRE has
1561
1608
  (look-around, backreferences, `\Q…\E`, `\Z`, `(?#…)` comments, a `{` that
@@ -1564,28 +1611,16 @@ starts no repetition) are refused as invalid, and a few match differently:
1564
1611
  does not match before a final newline, a possessive quantifier such as `a++`
1565
1612
  is greedy, and `x` ignores whitespace inside a character class too. A
1566
1613
  pattern is checked when the filter is validated, before any document is read
1567
- and even when the collection does not exist, with MongoDB's errors in
1568
- MongoDB's order: options given in both `$regex` and `$options` are
1614
+ and even when the collection does not exist, with MongoDB-compatible errors in
1615
+ a MongoDB-compatible order: options given in both `$regex` and `$options` are
1569
1616
  `Location51074` or `Location51075`, an unknown flag `Location51108`, a null
1570
1617
  byte `BadValue` (2), a pattern longer than 32,764 bytes or one that does not
1571
1618
  compile `Location51091` `Regular expression is invalid: <reason>` (the reason
1572
- is the crate's, such as `unclosed group` where MongoDB says `missing closing
1619
+ is the crate's, such as `unclosed group` where MongoDB, as observed by runtime testing, answers `missing closing
1573
1620
  parenthesis`), and a regular expression as the operand of `$ne`, `$gt`,
1574
1621
  `$gte`, `$lt` or `$lte` `BadValue` (2). A match charges the work budget the
1575
1622
  size of the field's value times the length of the pattern.
1576
1623
 
1577
- `$expr` evaluates an aggregation expression per document in `find`, `count`,
1578
- `distinct`, `update`, `delete`, `findAndModify` and a `$match` stage, and
1579
- matches when the result is truthy. It accepts the operators listed under
1580
- [Aggregation Pipeline](#aggregation-pipeline). Evaluation is charged against
1581
- the same work budget as every other predicate, and an operator the engine does
1582
- not implement is refused by name with `TypeMismatch` (code 14) before any
1583
- document is read. `$$NOW` is one instant per command, as in MongoDB: the
1584
- engine reads the clock once when the command starts, and the command's filter,
1585
- every document of a pipeline update, every aggregation stage, `$lookup` and
1586
- `$unionWith` sub-pipeline, and every `getMore` of its cursor see that instant.
1587
- Filters outside a command, such as a partial index filter, have no `$$NOW`.
1588
-
1589
1624
  ### Update Operators
1590
1625
 
1591
1626
  ```typescript
@@ -1608,8 +1643,8 @@ may be an array of `$addFields`, `$set`, `$project`, `$unset`, `$replaceRoot`
1608
1643
  or `$replaceWith` stages, and any other stage is refused by name. The pipeline
1609
1644
  must produce exactly one document and cannot change `_id`.
1610
1645
 
1611
- An upsert that matches nothing inserts the filter's equality conditions, as
1612
- MongoDB does: top-level fields and `$and` clauses, `$eq`, a single-element
1646
+ An upsert that matches nothing inserts the filter's equality conditions, to be
1647
+ MongoDB compatible: top-level fields and `$and` clauses, `$eq`, a single-element
1613
1648
  `$in`, `$all` members and a single-clause `$or`. A dotted path such as
1614
1649
  `'owner.id'` becomes a nested document, and a regular expression is copied only
1615
1650
  as an `$eq` operand. A replacement takes only `_id` from the filter and fails
@@ -1632,64 +1667,6 @@ const results = await collection.aggregate([
1632
1667
 
1633
1668
  **Supported stages:** `$match`, `$project`, `$group`, `$sort`, `$limit`, `$skip`, `$unwind`, `$lookup`, `$addFields`, `$count`, `$facet`, `$replaceRoot`, `$set`, `$unionWith`, `$out`, `$merge`
1634
1669
 
1635
- **Supported expression operators:**
1636
-
1637
- | Group | Operators |
1638
- | --- | --- |
1639
- | Comparison | `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$cmp` |
1640
- | Arithmetic | `$add`, `$subtract`, `$multiply`, `$divide` |
1641
- | Logical | `$and`, `$or`, `$not` |
1642
- | Conditional and variables | `$cond` (array or `if`/`then`/`else` form), `$ifNull` (two or more operands), `$let` |
1643
- | Type and arrays | `$isNumber`, `$type`, `$in`, `$size`, `$arrayElemAt` |
1644
- | Dates | `$dateAdd` |
1645
- | Collections | `$map`, `$objectToArray`, `$setEquals` |
1646
- | Literals | `$literal` |
1647
-
1648
- plus `$field` paths, `$$variable` references including `$$ROOT` and `$$NOW`,
1649
- and documents or arrays of those. Every other operator is refused by name with
1650
- `TypeMismatch` (code 14) before any document is read; the engine never treats
1651
- an operator it cannot execute as a literal value.
1652
-
1653
- Expressions follow MongoDB's value semantics. A field that is absent is
1654
- *missing*, which is not `null`: `$type` answers `'missing'`, `$ifNull`
1655
- replaces both, and comparisons order values as MongoDB does (MinKey, missing,
1656
- `null`, numbers, strings, objects, arrays, binary data, ObjectIds, booleans,
1657
- dates, timestamps, regular expressions, MaxKey), so `{ $eq: ['$absent', null] }`
1658
- is false. Numbers compare by value across their wire types. A path through an
1659
- array maps over its elements and always answers an array: `'$mailbox.path'`
1660
- over one looked-up document is `['INBOX']`, so `$arrayElemAt` and `$size`
1661
- apply to it. Arithmetic answers `null` when an operand is `null` or missing;
1662
- an int result that overflows widens to a long and a long to a double, a double
1663
- operand makes the result a double, and `$divide` always answers a double.
1664
- `$add` accepts one date and answers a date; date minus date is milliseconds.
1665
- Runtime errors keep MongoDB 7's codes: a non-numeric operand is
1666
- `TypeMismatch` (14), `can't $divide by zero` is `BadValue` (2), a date that
1667
- leaves the 64-bit range is `Overflow` (15), and the array and date operators
1668
- answer MongoDB's location codes (a second date in `$add` 16612, `$in` without
1669
- an array 40081, `$size` without an array 17124, `$arrayElemAt` 28689–28691,
1670
- `$dateAdd` 5166403, 5166405, 5166406 and 5439013, a non-string time zone
1671
- 40517, an unknown unit `FailedToParse` (9)). Decimal128 operands in arithmetic
1672
- are refused with `TypeMismatch` (14) — decimal arithmetic is not
1673
- implemented. `$and`, `$or`, `$cond` and `$ifNull`
1674
- evaluate, and charge, only the operands MongoDB evaluates, so a guard keeps an
1675
- invalid branch from failing the statement. `$dateAdd` computes in UTC with the
1676
- units `millisecond` to `year`, clamping `month`, `quarter` and `year` to the
1677
- end of the target month; a `timezone` other than UTC is refused by name.
1678
- Within a document expression a member that is missing is still written as
1679
- `null`, where MongoDB omits the member.
1680
-
1681
- `$lookup` supports the equality form and a bounded correlated pipeline form with
1682
- `let`, an optional `$match` (any filter, `$expr` included), and an optional
1683
- following `$limit`. A `let` variable that reads an absent local field is bound
1684
- to the missing value, as in MongoDB. Correlated string equality predicates
1685
- (`$expr` with `$eq`, optionally under `$and`) can use a full-key or subset
1686
- foreign equality index to reduce candidates when available. Candidates are
1687
- paged, the complete expression remains authoritative, and `$limit` counts only
1688
- exact expression matches. Other BSON value shapes use the bounded scan path.
1689
- Mixed `localField`/`foreignField` plus `pipeline` lookups and other inner stages
1690
- are rejected explicitly. Complete filter trees are validated before data is
1691
- read, and query nesting plus aggregation work are bounded.
1692
-
1693
1670
  A leading `$match` narrows the pipeline's source load instead of filtering
1694
1671
  after it: only matching documents are materialized, using an equality index when
1695
1672
  one serves the filter and a bounded scan otherwise. Filter shapes the query
@@ -1709,19 +1686,6 @@ describes, as it does on its own.
1709
1686
 
1710
1687
  **Group accumulators:** `$sum`, `$avg`, `$min`, `$max`, `$first`, `$last`, `$push`, `$addToSet`, `$count`
1711
1688
 
1712
- `$group._id` accepts a literal (`null`, a number, a string), a `$field`
1713
- reference, or a document of expressions: `{ _id: { owner: '$owner', repo:
1714
- '$repo' } }` groups by the evaluated pair, one group per distinct composite
1715
- key. Inside a composite `_id`, a member whose field is absent evaluates to
1716
- `null` and stays part of the key, where MongoDB omits the member instead.
1717
- Accumulator operands accept the same expression forms. A field reference
1718
- resolves per document, and a missing field is not a value: `$sum`, `$avg`,
1719
- `$min`, `$max`, `$push` and `$addToSet` skip it, while `$first` and `$last`
1720
- record `null` for it. Every field beside `_id` must hold an accumulator
1721
- document naming exactly one accumulator, such as `{ $sum: 1 }`, and an operator
1722
- outside the two lists above is refused with `TypeMismatch` (code 14) when the
1723
- stage is parsed.
1724
-
1725
1689
  ### Indexes
1726
1690
 
1727
1691
  ```typescript
@@ -1735,77 +1699,8 @@ await collection.dropIndexes(); // drop all except _id
1735
1699
 
1736
1700
  > 🛡️ **Unique indexes are enforced at the engine level.** Duplicate values are rejected with a `DuplicateKey` error (code 11000) *before* the document is written to disk — on `insertOne`, `updateOne`, `findAndModify`, and upserts. Index definitions and entries are committed with native storage and survive restart.
1737
1701
 
1738
- NoSQLDB supports ascending and descending index keys with `name`, `unique`,
1739
- `sparse`, `partialFilterExpression` and `expireAfterSeconds` options. It
1740
- accepts `background` as a validated no-op and index version `v: 2`.
1741
- Unsupported options, including `collation` and `hidden`, are rejected before
1742
- any index in the request is created. A legacy `ns` field is accepted only when
1743
- it names the target namespace. With `ignoreUnknownIndexOptions: true`,
1744
- `createIndexes` removes the fields MongoDB 7 does not know as index options
1745
- (and plugin-specific fields such as `weights` or `bits` on an ascending or
1746
- descending key) before validation, as MongoDB does; options MongoDB knows but
1747
- NoSQLDB does not implement are still rejected. Native maintenance expires eligible BSON
1748
- dates and date arrays in bounded batches, preserving publication holds and
1749
- active snapshots.
1750
-
1751
- **Partial indexes.** An index with a `partialFilterExpression` holds only the
1752
- documents that match the filter, and a unique partial index enforces
1753
- uniqueness among those documents only:
1754
-
1755
- ```typescript
1756
- await jobs.createIndex(
1757
- { activeClaimKey: 1 },
1758
- { unique: true, partialFilterExpression: { activeClaimKey: { $exists: true } } },
1759
- );
1760
- await pulls.createIndex(
1761
- { org: 1, repo: 1, sourceRef: 1, targetRef: 1 },
1762
- { unique: true, partialFilterExpression: { state: 'open' } },
1763
- );
1764
- ```
1765
-
1766
- The filter accepts MongoDB 8's subset: equality (`field: value` or `$eq`),
1767
- `$exists: true`, `$gt`, `$gte`, `$lt`, `$lte`, `$type`, `$in`, whose list may
1768
- hold regular expressions that match as they do in a query, `$all` of values,
1769
- which is the conjunction of their equalities and counts as one level, `$and`
1770
- and `$or`, nested at most four expression levels deep; `$comment` is accepted
1771
- and ignored. Any other expression — `$exists: false`, `$ne`, `$nin`, `$not`,
1772
- `$nor`, a regular expression outside `$in`, an empty `$all` or one holding a
1773
- regular expression or `$elemMatch`, `$elemMatch` and `$size` — and a filter
1774
- combined with `sparse` are refused with `CannotCreateIndex` (code 67);
1775
- `$expr`, `$jsonSchema`, `$where`, `$text` and the `$near` operators are
1776
- refused with the codes listed under [Query Operators](#query-operators), a
1777
- malformed filter, such as an empty `$and`, is `BadValue` (2) and a
1778
- non-document is `TypeMismatch` (14). As in MongoDB, the whole filter is first
1779
- validated as a query, so a malformed operand or an invalid regular expression
1780
- anywhere in it (`Location51091`, `Location51108`) is answered before an
1781
- expression outside the subset.
1782
- An update that moves a document into the filter claims its key, one that moves
1783
- it out releases it, and transactions claim keys the same way, so a racing
1784
- claim is a `WriteConflict` (112) with `TransientTransactionError`.
1785
- `listIndexes`, canonical export and import report the filter exactly as it was
1786
- created. The filter identifies the index: the same name with a different
1787
- filter is `IndexKeySpecsConflict` (86); the same key with an equivalent filter
1788
- under another name is `IndexOptionsConflict` (85); a different filter over the
1789
- same key under another name is a separate index. Two filters are equivalent
1790
- when MongoDB 8 normalises them to the same expression: implicit equality and
1791
- `$eq`, numeric wire types, `$in: [1]` and `1`, an `$or` of equalities on one
1792
- path and their `$in`, `$in` order and duplicates, `$all` and the `$and` of
1793
- its values, nested and flat `$and` or `$or`, `$type` aliases and their codes,
1794
- `$exists: true` and `1`, and a `$comment` or none compare equal, and so do
1795
- conjuncts and `$or` branches on different paths or of different kinds in any
1796
- order. Predicates of one kind on one path keep their written order, so
1797
- `{ $and: [{ a: 1 }, { a: 2 }] }` and its reverse are separate indexes, as are a
1798
- repeated conjunct and the conjunct alone, a document operand with its fields
1799
- in another order, and `$type: 'number'` and the list of the four numeric
1800
- types. The query
1801
- planner never selects a partial index on its own; a `hint` naming one reads
1802
- only its members, as MongoDB's hinted scan does. A partial TTL index expires
1803
- only its members. Engines older than this release refuse a storage root that
1804
- holds a partial index instead of enforcing its uniqueness over every
1805
- document.
1806
-
1807
1702
  **List fields.** An index on a field that holds lists has an entry per element,
1808
- as a MongoDB multikey index, and a compound index an entry per combination of
1703
+ to be compatible with MongoDB multikey indexes, and a compound index an entry per combination of
1809
1704
  its list fields' elements. Equality, `$in` and one-sided ranges on the elements
1810
1705
  read their bounds, and a document with several entries within them is returned
1811
1706
  once. A list matches a range bounded on both sides when one element meets the
@@ -1816,10 +1711,12 @@ through the index. An index with element entries returns no order, so a sorted
1816
1711
  read sorts its matches, and a `hint` naming it is refused. A unique index on a
1817
1712
  list field is unique per element: a document that shares an element with
1818
1713
  another is a `DuplicateKey` (11000), while one document may repeat an element.
1819
- An index whose field already held a list under 10.7.3 or earlier keeps its
1820
- whole-list entries and uniqueness and is maintained but never selected until it
1821
- is dropped and created again. Engines up to 10.7.3 refuse an index with element
1822
- entries instead of maintaining it with whole lists.
1714
+ An index whose field already held a list under 10.7.3 or earlier, which those
1715
+ releases kept with whole-list entries, gets element entries when 11.0.0
1716
+ converts its root at start, and is then selected like any other index; a unique
1717
+ one whose documents share an element refuses the start before anything is
1718
+ written. Engines up to 10.7.3 refuse an index with element entries instead of
1719
+ maintaining it with whole lists.
1823
1720
 
1824
1721
  `find()` and collection `aggregate()` accept `hint` as an index name (including
1825
1722
  `_id_`) or an exact ascending/descending key pattern. Key-pattern order matters;
@@ -1866,7 +1763,7 @@ Nontransaction aggregation pipelines composed of `$match`, `$project`, `$unset`,
1866
1763
  `$set`, `$addFields`, `$replaceRoot`, `$replaceWith`, `$skip`, `$limit`, `$sort`,
1867
1764
  `$group`, `$count`, `$unwind`, `$lookup`, `$unionWith`, and `$facet` also operate
1868
1765
  beyond 10,000 input or output documents.
1869
- Counts use exact collection metadata where applicable, otherwise stream matching
1766
+ Counts use each collection's exact count from its statistics record where applicable, otherwise stream matching
1870
1767
  records. A limit bounds the work needed to find the requested number of matches.
1871
1768
  Without a limit, exact counts continue to EOF. Hints, deadlines and empty-result
1872
1769
  shapes remain supported; signed BSON count overflow and resource exhaustion
@@ -1932,8 +1829,9 @@ transaction and native commit byte admission. `$out` and `$merge` have no
1932
1829
  10,000-document output ceiling; their atomic output is subject to the same write
1933
1830
  budgets and rejects before publication if those budgets are exceeded. Native
1934
1831
  database import uses private staging for larger supported exports, then publishes
1935
- the catalog and receipt atomically. Existing old-engine storage is rejected
1936
- without conversion.
1832
+ the catalog and receipt atomically. A start converts a root that a 10.x release
1833
+ wrote (see **Storage format migration** under File startup admission); any other
1834
+ old storage is rejected without conversion.
1937
1835
 
1938
1836
  ### Comparing performance with MongoDB
1939
1837
 
@@ -1988,40 +1886,23 @@ time-series collections, clustered indexes, encrypted fields, and all general
1988
1886
  view pipelines remain unsupported and are rejected before the database or
1989
1887
  namespace is created.
1990
1888
 
1991
- `create` accepts an `idIndex` only when it describes the primary index every
1992
- collection gets: key `{ _id: 1 }`, name `_id_`, optionally `v: 2` and an `ns`
1993
- naming the new collection. Any other `idIndex` is `InvalidOptions` (72).
1994
-
1995
1889
  **Collection names.** A name with a NUL, an empty name and a name starting
1996
1890
  with `.` that storage cannot hold, such as `.` or `..`, are `InvalidNamespace`
1997
- (73) on every command naming them, with MongoDB 8's messages, before the
1998
- command runs. A name MongoDB 8 accepts that one storage path component cannot
1891
+ (73) on every command naming them, with MongoDB-compatible messages, before the
1892
+ command runs. A name MongoDB accepts that one storage path component cannot
1999
1893
  hold, one with `/`, `\` or a control character or longer than 255 bytes, names
2000
1894
  a collection that cannot exist: reads, `drop`, `viewOn` and a rename source see
2001
1895
  it missing, and commands that create or write it are refused with
2002
1896
  `InvalidNamespace`, or `IllegalOperation` (20) for `$out` or a rename into a
2003
- name longer than 255 bytes. Such a refused write ends its transaction, as
2004
- MongoDB 8's refusal of a namespace longer than 255 bytes does. NoSQLDB limits
2005
- the collection name to 255 bytes where MongoDB 8 limits the whole
1897
+ name longer than 255 bytes. Such a refused write ends its transaction, to be compatible with MongoDB's
1898
+ refusal of a namespace longer than 255 bytes. NoSQLDB limits
1899
+ the collection name to 255 bytes where MongoDB, as observed by runtime testing, limits the whole
2006
1900
  `database.collection`, and it accepts a name such as `.a` that starts with `.`
2007
- but that its storage holds, which MongoDB 8 refuses.
2008
-
2009
- **Importing from MongoDB.** `mongorestore` (MongoDB Database Tools 100.x)
2010
- restores a `mongodump` of ordinary collections with its default flags: the
2011
- documents, validators and supported indexes (unique, compound, sparse,
2012
- partial and TTL). It sends this `idIndex` with every `create` and
2013
- `createIndexes` with each index's `ns` and `ignoreUnknownIndexOptions: true`,
2014
- and it builds several collections' indexes in parallel, which wait for one
2015
- another instead of failing (see "Database waits" below). Exclude the `admin`
2016
- and `config` namespaces (`--nsExclude 'admin.*' --nsExclude 'config.*'`) and create users
2017
- through NoSQLDB's authentication options. Collections or indexes that use
2018
- unsupported features (collation, text, geospatial, hashed or wildcard
2019
- indexes, capped or time-series collections) fail the restore with the
2020
- refusing command's error instead of being restored differently.
1901
+ but that its storage holds, which MongoDB refuses.
2021
1902
 
2022
1903
  ### Collection Validators
2023
1904
 
2024
- `create` and `collMod` accept MongoDB's `validator`, `validationLevel`
1905
+ `create` and `collMod` accept the MongoDB-compatible `validator`, `validationLevel`
2025
1906
  (`strict`, `moderate`, `off`) and `validationAction` (`error`, `warn`), and
2026
1907
  `listCollections` reports them under `options` exactly as set:
2027
1908
 
@@ -2048,7 +1929,7 @@ is `bsonType` (every BSON alias and `number`), `type` (`object`, `array`,
2048
1929
  `additionalProperties` (boolean), `minProperties`, `maxProperties`, `enum`,
2049
1930
  `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `minLength`,
2050
1931
  `maxLength`, `pattern`, `items` (one schema), `minItems`, `maxItems`, `title`
2051
- and `description`, with MongoDB's semantics: a keyword applies only to values
1932
+ and `description`, with MongoDB-compatible semantics: a keyword applies only to values
2052
1933
  of its kind and not at all when `bsonType`/`type` excludes that kind, `enum`
2053
1934
  compares numbers by value and embedded documents regardless of field order,
2054
1935
  NaN fails `minimum` and `maximum`, and lengths count code points. Everything
@@ -2056,9 +1937,9 @@ else is refused when the validator is set, never ignored: `patternProperties`,
2056
1937
  `additionalItems`, `uniqueItems`, `dependencies`, `allOf`, `anyOf`, `oneOf`,
2057
1938
  `not`, `multipleOf`, `encrypt`, `encryptMetadata`, `items` as an array and
2058
1939
  `additionalProperties` as a schema answer `FailedToParse` (9) `$jsonSchema
2059
- keyword '…' is not currently supported`, as MongoDB answers for `$ref`,
1940
+ keyword '…' is not currently supported`, the MongoDB-compatible answer for `$ref`,
2060
1941
  `format`, `default`, `definitions`, `id` and `$schema`; `$jsonSchema` below the
2061
- top level answers `BadValue` (2). Malformed validators answer MongoDB's codes
1942
+ top level answers `BadValue` (2). Malformed validators answer MongoDB-compatible codes
2062
1943
  (2, 9, 14, `Location51091` for an invalid `pattern`). `pattern` uses the same
2063
1944
  regular-expression dialect as `$regex`; constructs only PCRE supports are
2064
1945
  refused as invalid.
@@ -2070,10 +1951,10 @@ written document; `moderate` skips updates to documents that did not satisfy
2070
1951
  the validator before the update; `off` validates nothing; a no-op update and a
2071
1952
  delete are never validated. A refused document answers
2072
1953
  `DocumentValidationFailure` (121), `errmsg: "Document failed validation"`, and
2073
- MongoDB's `errInfo: { failingDocumentId, details }` — as a write error of
1954
+ the MongoDB-compatible `errInfo: { failingDocumentId, details }` — as a write error of
2074
1955
  `insert`/`update`, keeping ordered and unordered batch semantics, and as the
2075
1956
  command error of `findAndModify` and `aggregate`. A refused statement aborts
2076
- its transaction. `details` has MongoDB's shape for every `$jsonSchema` keyword
1957
+ its transaction. `details` has a MongoDB-compatible shape for every `$jsonSchema` keyword
2077
1958
  above and for `$and`, `$or`, `$nor`, `$not`, `$eq`, `$ne`, `$gt`, `$gte`,
2078
1959
  `$lt`, `$lte`, `$exists`, `$type`, `$in`, `$nin`, `$regex`, `$size`, `$all`,
2079
1960
  `$elemMatch`, `$mod` and `$expr`; other operators report only `operatorName`
@@ -2082,12 +1963,12 @@ and `specifiedAs`. `validationAction: 'warn'` writes the document and logs a
2082
1963
  `bypassDocumentValidation` (a boolean or number) on `insert`, `update`,
2083
1964
  `findAndModify` and writing `aggregate` skips validation; with authentication
2084
1965
  it requires `dbAdmin` on the written database, as `collMod` does — `readWrite`
2085
- alone is refused with `Unauthorized` (13), as in MongoDB's built-in roles.
1966
+ alone is refused with `Unauthorized` (13), implemented to be compatible with MongoDB's built-in roles.
2086
1967
 
2087
1968
  `collMod` changes only the options it names; `validator: {}` removes the
2088
1969
  validator; a `null` option is absent; `collMod` without options answers
2089
1970
  `{ ok: 1 }`. A missing collection answers `NamespaceNotFound` (26), a view or
2090
- `system.*` collection `InvalidOptions` (72), MongoDB's other `collMod` options
1971
+ `system.*` collection `InvalidOptions` (72), the other MongoDB `collMod` options
2091
1972
  (`index`, `viewOn`, `pipeline`, `expireAfterSeconds`, `timeseries`,
2092
1973
  `changeStreamPreAndPostImages`, `dryRun`, `cappedSize`, `cappedMax`, …)
2093
1974
  `InvalidOptions` (72) by name, and unknown fields `IDLUnknownField` (40415).
@@ -2100,13 +1981,13 @@ content digest includes them only then, so digests of collections without
2100
1981
  options are unchanged. Engines up to 10.6.1 refuse a storage root or an export
2101
1982
  holding collection options instead of silently not enforcing them.
2102
1983
 
2103
- Differences from MongoDB 7: `create` on an existing collection answers
1984
+ Differences from MongoDB, as observed by runtime testing: `create` on an existing collection answers
2104
1985
  `NamespaceExists` (48) even when the options are identical (MongoDB answers
2105
1986
  `ok`); a validator together with `viewOn` is refused (MongoDB ignores it); an
2106
- invalid `pattern` at the schema root is refused (MongoDB never compiles it
1987
+ invalid `pattern` at the schema root is refused (MongoDB accepts it
2107
1988
  there);
2108
- command errors say `Document failed validation` without MongoDB's
2109
- `Plan executor error during …` prefix; the `warn` log carries `errInfo`, not
1989
+ command errors say `Document failed validation` without the
1990
+ `Plan executor error during …` prefix MongoDB adds; the `warn` log carries `errInfo`, not
2110
1991
  the whole document.
2111
1992
 
2112
1993
  ### Restricted Deny-All Views
@@ -2142,7 +2023,7 @@ replaced by a view keeps that original snapshot for `find`, `count`, and
2142
2023
  with `CommandNotSupportedOnView` (code 166). `dropCollection` is the supported
2143
2024
  DDL exception for removing a logical restricted view.
2144
2025
 
2145
- Definitions are persisted in the physical `system.views` catalog. As in MongoDB,
2026
+ Definitions are persisted in the physical `system.views` catalog. To be MongoDB compatible,
2146
2027
  `listCollections` lists `system.views` as a physical storage collection and the
2147
2028
  logical namespace as type `view` with `{ viewOn, pipeline }`, read-only metadata,
2148
2029
  and no `idIndex`. `system.views` is reserved from direct wire reads,
@@ -2179,9 +2060,6 @@ const estimated = await collection.estimatedDocumentCount();
2179
2060
  const names = await collection.distinct('name');
2180
2061
  ```
2181
2062
 
2182
- The `count` command answers `n` as a 32-bit integer when the count fits and as
2183
- a 64-bit integer above that, as MongoDB does.
2184
-
2185
2063
  ---
2186
2064
 
2187
2065
  ## Wire Protocol Commands
@@ -2196,84 +2074,6 @@ a 64-bit integer above that, as MongoDB does.
2196
2074
  | **Transactions** | `startTransaction`, `commitTransaction`, `abortTransaction` through driver sessions |
2197
2075
  | **Admin** | `ping`, `buildInfo`/`buildinfo`, `listDatabases`, `listCollections`, `drop`, `dropDatabase`, `create`, `collMod` (validation options), `serverStatus`, `dbStats`, `collStats`, `connectionStatus`, `renameCollection` |
2198
2076
 
2199
- Advertises MongoDB wire protocol versions 0–21 and reports MongoDB 7.0.0
2200
- compatibility for its command surface. The documented command surface is tested
2201
- with the official `mongodb` Node.js driver version 7.5.x. Wire limits and error
2202
- replies follow MongoDB 8.0, whose document size tiers are larger than 7.0's;
2203
- where NoSQLDB still differs from MongoDB 8.0, the list at the end of this
2204
- section says so.
2205
-
2206
- OP_MSG parsing rejects unknown required flags, invalid CRC-32C checksums,
2207
- duplicate BSON keys, noncanonical array indexes, malformed section boundaries,
2208
- multiple body sections, duplicate or colliding document-sequence identifiers,
2209
- and missing or non-string `$db` fields. Early command-envelope validation then
2210
- accepts document sequences only as `insert.documents`, `update.updates`, or
2211
- `delete.deletes`, before authentication or command resources are acquired. A
2212
- write command refuses any other sequence as an unknown field of its own,
2213
- `IDLUnknownField` (40415) naming `insert.x`, or `insert` for an empty
2214
- identifier, as MongoDB 8 does; any other command refuses document sequences with
2215
- `InvalidOptions` (72). All three accepted forms are consumed as the
2216
- corresponding bulk command argument. Unknown optional flags are ignored.
2217
-
2218
- As in MongoDB 8, a complete frame that is malformed or over a document limit is
2219
- answered with an error reply to its request id, and the connection stays usable
2220
- for the requests behind it: for example `InvalidBSON` (22) for a bad document
2221
- length or a body cut short, `Overflow` (15) for nesting beyond 100 levels, the
2222
- `Location404xx` codes for malformed sections, `Location51252` for a checksum
2223
- that leaves no room for a section, and `BSONObjectTooLarge` (10334) for an
2224
- oversized document. The 10334 message shows the document's first element as
2225
- MongoDB's does, cut after 160 bytes. In MongoDB 8's order, a document's
2226
- structure (lengths, terminators, element types and booleans) and nesting are
2227
- checked before its size, and a message's sections and BSON before its checksum.
2228
- A defect in a legacy OP_QUERY document, its nesting included, is `InvalidBSON`
2229
- (22), as MongoDB 8 answers it, and bytes after an OP_QUERY's field selector are
2230
- ignored, as MongoDB 8 ignores them. A refused `moreToCome` request gets no
2231
- reply. A frame that cannot be delimited or trusted closes the connection, again
2232
- as in MongoDB: an invalid length prefix, an OP_MSG too short to hold its flags,
2233
- a request whose `responseTo` is not 0, an unknown required flag, more than 128
2234
- sections, or a checksum mismatch.
2235
-
2236
- A message is limited to 48,000,000 bytes. A command body may be up to 16,809,984
2237
- bytes and a document-sequence or legacy OP_QUERY document up to 16,793,600 bytes,
2238
- MongoDB 8's 16 MiB plus 32 KiB and 16 KiB of command overhead; a stored document
2239
- is limited to 16,777,216 bytes, and an insert row over it is that row's
2240
- `BSONObjectTooLarge` write error. An `insert`, `update` or `delete` batch must
2241
- hold 1 to 10,000 operations, the advertised `maxWriteBatchSize`; other batches
2242
- are refused with `InvalidLength` (16), and drivers split larger input batches.
2243
-
2244
- Before any of a request is decoded, its BSON is checked and the heap its decoded
2245
- form takes is computed from the bytes alone, each allocation rounded up as the
2246
- allocator rounds it. A request holds that charge against the server's
2247
- decoded-memory budget, 1 GiB by default, until its command has finished with
2248
- it. A request whose decoded form takes 16 KiB or less, as handshakes, pings
2249
- and ordinary commands do, holds it against a separate 16 MiB share kept beside
2250
- the budget instead, which holds one such request for each of the 1,024
2251
- connections a server accepts, so it never waits behind larger requests. A
2252
- request whose frame is small but whose decoded form is larger, such as a find
2253
- with a long `$in` list, waits in the budget with the large ones. A request
2254
- that needs more than the
2255
- whole budget is refused whole with `BSONObjectTooLarge` (10334) and the message
2256
- "send smaller batches", after its sections and BSON are checked and before its
2257
- checksum. The number of elements, nested documents and arrays a request holds
2258
- is not limited, so a full batch of 10,000 wide documents is accepted as in
2259
- MongoDB. A request that does not fit what the budget has left waits for it
2260
- behind every request of its share that arrived before it, for at most its
2261
- `maxTimeMS` or five seconds from when it arrived. A request still waiting then
2262
- is answered without being decoded or run: with `MaxTimeMSExpired` (50) when its
2263
- `maxTimeMS` is five seconds or less, and otherwise with `LockTimeout` (24),
2264
- which a transaction statement gets with the `TransientTransactionError` label.
2265
- A statement of a running transaction answered so aborts its transaction; a
2266
- `commitTransaction` or `abortTransaction` answered so, or a statement that would
2267
- start its transaction, leaves the transaction as it was. A `moreToCome` request
2268
- still waiting closes its connection. A command's `maxTimeMS` counts from when
2269
- the server had the whole request, so the wait is part of it; a request sent
2270
- while the connection's previous command ran counts from when that command
2271
- finished, as MongoDB reads it only then. `maxTimeMS: 0` means no limit, as in
2272
- MongoDB, so such a request waits the five seconds and gets `LockTimeout`. A
2273
- legacy OP_QUERY command wrapped in `$query` is answered from an envelope that
2274
- misses its `maxTimeMS` and `lsid`, so it waits five seconds and gets
2275
- `LockTimeout` without a transaction label.
2276
-
2277
2077
  The `moreToCome` request flag is accepted only for the exact `endSessions` or
2278
2078
  `killSessions` cleanup envelope: an `admin` command body containing only the
2279
2079
  session array, `writeConcern: { w: 0 }`, and `$db: "admin"`, with no document
@@ -2339,40 +2139,10 @@ The decoded budget also bounds the largest request the server accepts, so a
2339
2139
  smaller one refuses smaller requests with `BSONObjectTooLarge`. Each server has
2340
2140
  its own budgets, so servers in one process do not share them.
2341
2141
 
2342
- NoSQLDB still differs from MongoDB 8.0 on the wire in these points:
2343
-
2344
- - An opcode other than OP_MSG and OP_QUERY closes the connection. MongoDB 8
2345
- ignores most unknown opcodes and answers the legacy `OP_GET_MORE` with error
2346
- 5739101.
2347
- - A legacy OP_QUERY to a collection other than `$cmd` is routed as a `find`,
2348
- where MongoDB 8 answers 5739101.
2349
- - Nesting is limited to 100 levels; MongoDB 8 allows 200.
2350
- - A write batch over 10,000 operations is refused with `InvalidLength` (16);
2351
- MongoDB 8 accepts up to 100,000.
2352
- - A request whose decoded form needs more than the decoded-memory budget, 1 GiB
2353
- by default, is refused with `BSONObjectTooLarge` (10334); MongoDB 8 has no
2354
- such limit.
2355
- - A request that waits for decode memory longer than its `maxTimeMS` or five
2356
- seconds is answered with `MaxTimeMSExpired` (50) or `LockTimeout` (24)
2357
- without being run; MongoDB 8 has no decode budget.
2358
2142
  - A frame over 8 KiB whose bytes average less than 1 MiB per second after a
2359
2143
  10-second grace closes its connection while another connection waits for
2360
- the encoded budget or more than seven eighths of it are charged; MongoDB 8
2144
+ the encoded budget or more than seven eighths of it are charged; MongoDB
2361
2145
  has no such limit.
2362
- - The identifier a `Location40431` or `Location40433` message names is cut
2363
- after 256 bytes; MongoDB 8 names it whole.
2364
- - A duplicate key anywhere in a request is refused with `InvalidBSON` (22);
2365
- MongoDB 8 does not refuse duplicate keys on the wire.
2366
- - A field name or string that is not valid UTF-8 is refused with `InvalidBSON`
2367
- (22); MongoDB 8 does not check UTF-8 on the wire and stores such data.
2368
- - Most `InvalidBSON` (22) messages describe the defect in NoSQLDB's own words.
2369
- - A message with three document sequences gets an error reply, where MongoDB 8
2370
- closes the connection.
2371
- - A command other than `insert`, `update` and `delete` refuses document
2372
- sequences with `InvalidOptions` (72); MongoDB 8 runs it.
2373
- - `moreToCome` is accepted only for the session cleanup envelope above, where
2374
- MongoDB 8 runs any command sent with it.
2375
- - A request that carries a checksum is answered without one.
2376
2146
 
2377
2147
  ### Identifying the Engine
2378
2148
 
@@ -2395,9 +2165,9 @@ is. Every `hello`, `ismaster` and `isMaster` reply carries a self-contained
2395
2165
  `version` is the released `@lossless.org/nosqldb` version the running binary was
2396
2166
  built from: the Rust build compiles it in from the package manifest, so the
2397
2167
  answer belongs to the shipped artifact and cannot drift from it.
2398
- `mongodbCompatibility` is the MongoDB release whose wire protocol and command
2399
- semantics this engine implements — a compatibility statement, never a claim to
2400
- be that build.
2168
+ `mongodbCompatibility` is the compatibility version NoSQLDB reports to drivers
2169
+ and tools for the command surface it implements to be MongoDB compatible — a
2170
+ compatibility value, never a claim to be a MongoDB build.
2401
2171
 
2402
2172
  The handshake carries the identity unconditionally, before authentication, so a
2403
2173
  client can derive its backend from the connection it already has. `buildInfo` is
@@ -2405,7 +2175,7 @@ available to **every authenticated connection**, including a `readWrite`-only
2405
2175
  tenant user and an allocation read grant; it requires no `clusterMonitor` role
2406
2176
  and creates no session state. `buildInfo` answers from build metadata alone and
2407
2177
  therefore still answers on a fenced or closed runtime, while `hello` fails
2408
- closed. It answers in MongoDB's shape so existing tooling keeps working:
2178
+ closed. It answers in a MongoDB-compatible shape so existing tooling keeps working:
2409
2179
 
2410
2180
  ```json
2411
2181
  {
@@ -2431,16 +2201,65 @@ reported, including `version` and `process: "rustdb"`, and now reports the same
2431
2201
  `nosqldb` identity beside them — but it remains a `clusterMonitor` command, so
2432
2202
  least-privilege connections use `hello` or `buildInfo`.
2433
2203
 
2204
+ `serverStatus.engineFigures` reports the storage engine's own figures, each
2205
+ read from a counter or from the allocator's own statistics, so forming them
2206
+ takes no time that grows with the data. Every count is an int64 that is 0 when
2207
+ the server opens its storage and never decreases while it runs. `commitMode` is
2208
+ `buffered`: commits go through the write buffer, the only commit path.
2209
+ `bufferedBytes` is the bytes of committed writes that the buffer holds and no
2210
+ merge has yet written into the tree, the sum of `activeGenerationBytes` and
2211
+ `frozenGenerationBytes`; `commits` counts the publications made durable and
2212
+ visible, and `mergesStarted` and `mergesCompleted` the buffer's merges.
2213
+ `freezes` counts the buffer's generations frozen for a merge, by cause: `size`
2214
+ when the generation reached its size, `idle` when no commit arrived for the
2215
+ quiet period, `age` when its oldest commit reached the age limit, and `prune`
2216
+ when records that history pruning wrote waited for a merge. `backstopStall`
2217
+ counts the commits that waited at the buffer's backstop (`stalls`) and the
2218
+ nanoseconds they waited (`stalledNanoseconds`), and gives the commits waiting
2219
+ there now (`stalledWriters`). A commit waits there when it would take the
2220
+ active generation past its size while a generation is pending, from its
2221
+ freeze until the checkpoint after its merge is durable, or while commits that
2222
+ passed the backstop before it and are not linked yet fill the active
2223
+ generation; and a background commit waits there at its claim on the buffer
2224
+ while another background commit holds one. `refusals` counts the
2225
+ requests for space that the engine refused for lack of free
2226
+ space, a commit's or the engine's own work's (`space`), and the
2227
+ commits refused after waiting at the backstop as long as their limit
2228
+ allows (`backstop`). While a merge cannot get room,
2229
+ `serverStatus.engineFigures.mergeWedge` reports what it lacks (`bytesShort` or
2230
+ `segmentsShort`), and is null again once a later attempt of the merge is
2231
+ admitted; `mergeRefusals` counts the merge attempts refused since the root
2232
+ opened. `restarts` counts the write preparations started again by cause,
2233
+ `catalogChange`, `pruningWindow` and `merge`; one for a conflict on a document
2234
+ or unique key counts under none. `bytesWritten` counts the bytes handed to the
2235
+ file system for the storage files by duty: `redo`, `merge`, `cleaner`,
2236
+ `indexes`, `checkpoint`, `directory` and `fill`. `liveBytes` is the bytes the
2237
+ current state reaches and `fixedBytes` the bytes the engine's files hold apart
2238
+ from data: the anchor, segment headers and pre-sized log space.
2239
+ `directoryBytes` and `cacheResidentBytes` are the memory that the directory of
2240
+ stored frames and the node cache hold, and `cacheBudgetBytes` is the cache
2241
+ budget that `storageEngine.cacheBytes` reports.
2242
+ `serverStatus.engineFigures.pinnedBytes` is the bytes in the storage's data
2243
+ segments that the engine keeps, where it would otherwise free them, only
2244
+ because a live snapshot, such as an open cursor's, can still read superseded
2245
+ data in them: the value its latest maintenance round found, and 0 when no
2246
+ snapshot keeps any. `allocator` states whether a
2247
+ check at run time found the shipped allocator (`verified`), and only then its
2248
+ arena count, its decay times in milliseconds, whether it runs background
2249
+ threads, and the bytes allocated through it and resident; while `verified` is
2250
+ false, those fields are null.
2251
+
2434
2252
  ### Read and Write Concerns and Wire Deadlines
2435
2253
 
2436
2254
  NoSQLDB serves every database from exactly one voting node and states its
2437
2255
  concern profile in those terms instead of ignoring or blanket-rejecting the
2438
2256
  fields:
2439
2257
 
2440
- - **Write concern.** Every acknowledged write is fsynced to the write-ahead log
2441
- and the data file before the response is sent, so `w: 1`, `w: "majority"`
2258
+ - **Write concern.** Every acknowledged write is in the write-ahead log before
2259
+ the response is sent: fsynced there with file storage, while memory storage
2260
+ keeps its log in the process's memory. So `w: 1`, `w: "majority"`
2442
2261
  (the majority of one voter is that voter), and `j: true` are all satisfied by
2443
- the same durable commit. `w: 0` is acknowledged the same way whenever a
2262
+ the same commit. `w: 0` is acknowledged the same way whenever a
2444
2263
  response is expected; the exact `writeConcern: { w: 0 }` `endSessions` and
2445
2264
  `killSessions` envelopes still run without a transport response. `wtimeout`
2446
2265
  bounds a replication wait that does not exist on one node and has no
@@ -2460,65 +2279,6 @@ fields:
2460
2279
  and `aggregate` only, and `afterClusterTime`, `atClusterTime`, and
2461
2280
  `afterOpTime` are rejected with `InvalidOptions` because a single node issues
2462
2281
  no cluster time.
2463
- - **Deadlines.** `maxTimeMS` attaches a cooperative deadline to the command,
2464
- counted from when the server had the whole request, or from when the
2465
- connection's previous command finished for a request sent while it ran. It
2466
- is checked while the request waits for decode memory, at admission, during
2467
- namespace-lock and maintenance-gate waits, before `find`, `count`, and
2468
- `distinct` materialize documents, between
2469
- documents in `insert` and `delete` batches, and immediately before a
2470
- transaction commit is handed to storage; expiry fails with `MaxTimeMSExpired`
2471
- (code 50). A single storage step or an in-progress `update` batch runs to completion
2472
- before the next check, so a write that expires after its first documents
2473
- were published reports the expiry while those documents remain durable,
2474
- exactly like an interrupted MongoDB batch. The official driver transmits a
2475
- transaction's `maxCommitTimeMS` as `maxTimeMS` on `commitTransaction`; a
2476
- commit that expires before its batch is handed to storage publishes nothing,
2477
- records the transaction as aborted, and carries the
2478
- `UnknownTransactionCommitResult` label; the driver's commit retry then sees
2479
- `NoSuchTransaction` with `TransientTransactionError` and restarts the whole
2480
- transaction. `getMore` rejects `maxTimeMS` with `BadValue` because NoSQLDB
2481
- has no awaitData cursors, and a literal `maxCommitTimeMS` field is rejected
2482
- with `InvalidOptions`.
2483
- - **Database waits.** DDL that drops or reshapes collections (`drop`,
2484
- `dropIndexes`, `collMod`, `dropDatabase`, `renameCollection`,
2485
- `$out`/`$merge`) holds its database exclusively; other commands on that
2486
- database wait for it, as they wait for a MongoDB database lock. Creating a
2487
- collection or its indexes (`create`, `createIndexes`) holds only that
2488
- collection exclusively, as MongoDB locks the collection: it waits for the
2489
- open transactions whose statements targeted that collection (a collection
2490
- a transaction reads only through `$lookup` or `$unionWith` is not held; the
2491
- transaction reads its snapshot of it), transaction statements on that
2492
- collection that arrive after it queue behind it, and everything else
2493
- proceeds. Commands outside a transaction take no collection
2494
- gate; writes still queue for the storage writer, which an index build holds
2495
- until its index is ready. A command outside a
2496
- transaction waits first in, first out for as long as it may run: until
2497
- `maxTimeMS` (`MaxTimeMSExpired`, code 50), until its client disconnects or
2498
- shuts down its sending side (the wait ends with `ClientDisconnect`, code
2499
- 279, which a client that still reads receives before the connection closes)
2500
- or until the server shuts down (`InterruptedAtShutdown`, code 11600). A
2501
- database DDL command likewise waits for the open transactions on its
2502
- database to commit or abort, and every command on that database that
2503
- arrives after it queues behind it, including reads of other collections. An
2504
- open transaction therefore delays DDL and the commands behind it at most
2505
- until the transaction lifetime limit (60 seconds, MongoDB's
2506
- `transactionLifetimeLimitSeconds`) aborts it.
2507
- A transaction statement that needs a further database or collection keeps a
2508
- fixed five-second limit and then fails with `LockTimeout` (code 24, labelled
2509
- `TransientTransactionError`), so a transaction never waits without bound while it
2510
- holds its databases and collections; like any failed statement, it aborts
2511
- the transaction and releases them. One transaction targets at most 4,096
2512
- distinct collections across all its databases; statements that revisit a
2513
- collection retain no extra gate. This matches the process-wide collection-lock
2514
- registry's 4,096-live-identity bound; concurrent transactions and collection
2515
- DDL share that registry. A command that finds the shared registry full fails
2516
- with `LockTimeout` (code 24; labelled `TransientTransactionError` in a
2517
- transaction) and can be retried once in-flight operations complete. It bounds process-local locks, not document staging,
2518
- WAL record size or atomic publication. A statement targeting one more fails
2519
- with code 16500 (`Location16500`, MongoDB 8's code name for it) and aborts it. The service operations (export,
2520
- import, tenant and publication fencing) keep the five-second gate-wait limit and report
2521
- `EFENCE_BUSY`.
2522
2282
 
2523
2283
  Callers may therefore rely on a successful NoSQLDB response as a durable,
2524
2284
  single-node acknowledgement. They must not read it as a replica-backed
@@ -2552,7 +2312,7 @@ The Rust engine is organized as a Cargo workspace with 12 focused crates:
2552
2312
  | `rustdb-wire` | Wire protocol parser/encoder (OP_MSG, OP_QUERY, OP_REPLY) |
2553
2313
  | `rustdb-query` | Query matcher, update engine, aggregation, sort, projection |
2554
2314
  | `rustdb-state` | Internal deterministic storage commands, stable retry identities, and durable receipts |
2555
- | `rustdb-kernel` | Native paged committed-log kernel with stable timelines, contiguous LSNs, strict recovery and exact receipt replay |
2315
+ | `rustdb-kernel` | Native storage format 3: a log of commits, a write buffer merged into a tree, checkpoints and a cleaner, with stable timelines, contiguous LSNs and strict recovery; and the format-2 reader of 10.x roots |
2556
2316
  | `rustdb-replication` | Internal non-serving OpenRaft 0.9.25 one-voter fresh-root foundation; not linked into the released `rustdb` server |
2557
2317
  | `rustdb-storage` | Native catalogs, documents, exact counts, staged imports, reclamation and bounded OpLog |
2558
2318
  | `rustdb-index` | Native index records, query plans, uniqueness, bounded builds and TTL |
@@ -2563,57 +2323,35 @@ The Rust engine is organized as a Cargo workspace with 12 focused crates:
2563
2323
 
2564
2324
  The native MVCC engine serves both memory and file storage.
2565
2325
  Its Rust APIs use `mvcc` modules and `Mvcc` type names independently of npm
2566
- versions and persisted format versions. The K2 kernel stores immutable paged
2567
- roots, typed BSON records and checksummed commit markers in one append stream.
2568
- Snapshots pin roots; normal writes append changed pages. Descriptor-relative
2326
+ versions and persisted format versions. The format-3 store writes each
2327
+ publication as one checksummed `COMMIT` frame of typed BSON records to its
2328
+ log, keeps recent writes in an in-memory buffer, merges them into a
2329
+ copy-on-write tree in data segments and writes checkpoints from which
2330
+ recovery replays the log. Snapshots pin a published view, and maintenance
2331
+ frees nothing a pinned snapshot can reach. Descriptor-relative
2569
2332
  access and the enclosing native file-root lease protect the entire lifecycle.
2570
2333
  Ambiguous publication fences the owner until recovery. Confirmed close drains
2571
2334
  requests, sessions, cursors and retained workers before releasing ownership.
2572
2335
 
2573
- The bounded page cache retains validated key prefixes for point lookups when
2574
- large separators cannot fit as complete nodes. Prefix coverage grows with the
2575
- probe length and preserves exact ordering; scans and writes use complete keys.
2576
- This keeps unrelated small-key operations from repeatedly loading wide index
2577
- separators without increasing the cache budget or changing the stored format.
2578
- A collection scan looks up the backpointers and documents of each page's rows
2579
- in one ordered pass over the tree and reads their values in file order. Scans
2580
- and range pages read adjacent values together, up to 64 KiB per system call,
2581
- and still verify each value on its own.
2582
-
2583
- Verified record values have their own bounded cache, 4 MiB by default, so value
2584
- churn never evicts tree pages. A point read admits a value of up to 64 KiB, or
2585
- an eighth of the cache if that is smaller. A range read admits like a point
2586
- read only when it returns a single record and exhausts its range, such as an
2587
- exact unique-index probe; other range reads admit nothing. A query or
2588
- transaction read whose whole index selection is one entry caches its document
2589
- like a point find. The other per-record reads of a selection or pass use
2590
- cached values but admit none: the documents of collection and index scans, the
2591
- identities of covered `_id` scans, the index entries a multi-value `$in`
2592
- probes, and the reads of validation, stopped
2593
- inspection, TTL and authentication passes, including their single-record
2594
- probes. A user listing and a write's own probes of unique owners, index
2595
- entries and natural-order records admit nothing either. A scan, maintenance
2596
- pass or listing therefore cannot flush hot catalog records and documents.
2597
- A commit forgets the cached value of every record it replaces or deletes, so
2598
- a write, however many documents it changes, leaves behind at most what its
2599
- query would have cached as a find. Frames are immutable and append-only, and a
2600
- snapshot reaches only the frames its own tree references: an older snapshot
2601
- that still needs a forgotten value reads it from storage again, and a new
2602
- version is cached once a read looks it up. As with tree pages, a value is
2603
- verified once when it is cached;
2604
- storage corruption of a cached frame is detected after eviction or generation
2605
- retirement.
2606
-
2607
- Both caches evict by CLOCK. Each cached node and value carries a referenced
2608
- bit. Every read sets it on the inner tree nodes it passes through, since each
2609
- lookup beneath such a node passes through it again. On leaves and values, point
2610
- lookups, the range reads of point queries and value admission set it; scans,
2611
- maintenance passes, generation copies and a commit's own reads leave it as they
2612
- find it, and the leaves and values they cache start without it. To make room, a
2613
- hand sweeps the entries in turn, clears each set bit it passes and evicts the
2614
- first entry whose bit is clear. A leaf or value that point reads use therefore
2615
- outlasts the scan entries cached after it by one turn of the hand, and a cache
2616
- hit costs no reordering.
2336
+ The store keeps verified tree nodes in one node cache, half of its memory
2337
+ budget, evicted by CLOCK. The budget is `storageBudgetBytes` where that option
2338
+ of `NoSqlDbServer` is set, from 4 MiB to 1 TiB, and otherwise a sixteenth of
2339
+ the memory the process may use, measured as for the inbound budgets above, from
2340
+ 16 MiB to 4 GiB; `serverStatus.storageEngine.cacheBytes` reports the cache's
2341
+ half. A root the server serves takes it when it is created or opened and keeps it
2342
+ until it closes; the stopped operations' reads of a stopped root, a start's
2343
+ checks of a root before it serves it and a 10.x conversion open the store
2344
+ with 64 MiB. Each cached node carries a referenced bit:
2345
+ inner nodes enter with it set and leaves without it, and a point read sets it
2346
+ on the nodes it uses. To make room, a hand sweeps the entries in turn, clears
2347
+ each set bit it passes and evicts the first entry whose bit is clear. A read
2348
+ that is one row of a pass over many records, such as a scan, a merge or a page
2349
+ of the commit journal, clears no bit and is cached only in place of an
2350
+ unreferenced entry at the hand, so such reads cannot displace the nodes point
2351
+ reads keep. A publication removes the nodes it supersedes from the cache.
2352
+ There is no value cache: a value stored outside its leaf is read from storage
2353
+ each time. A collection scan reads each page of documents as one range read of
2354
+ their bodies in natural order, with no lookup per document.
2617
2355
 
2618
2356
  Database and collection lookups, absent names included, are remembered for the
2619
2357
  newest committed root a read has used, up to 256 names and 1 MiB, so the
@@ -2627,7 +2365,7 @@ one root list its indexes once. The first listing on a root reads the catalog
2627
2365
  whole; a catalog of more than 64 indexes, aliases included, and a listing that
2628
2366
  fails are not kept.
2629
2367
 
2630
- Document, catalog, exact-count, natural-order, index-entry, unique-owner,
2368
+ Document, catalog, statistics, natural-order, index-entry, unique-owner,
2631
2369
  authentication, fencing and retry-receipt records share atomic publications.
2632
2370
  Transactions retain changed documents and immutable read views with byte and
2633
2371
  concurrency admission; they do not copy a complete database snapshot or impose a
@@ -2638,15 +2376,25 @@ generated IDs and zero-match outcomes, across restart. Failed statements do not
2638
2376
  receive success receipts. Session termination drains active work and retires its
2639
2377
  cursors.
2640
2378
 
2641
- Custom indexes build in bounded batches under a writer reservation and become
2642
- Ready together. Startup resumes interrupted builds under native ownership before
2643
- serving. If a resumed build confirms duplicate keys or exceeds its work budget,
2644
- its Building state remains intact and a secret-free warning identifies the need
2645
- for explicit index repair. Reads avoid that index; indexed writes fail closed.
2646
- Use the supported index drop/recreate operation to resolve it. Cancellation,
2647
- corruption and uncertain publication fail startup. Held databases remain closed
2648
- and are skipped during recovery; releasing a hold does not run extra mutations.
2649
- Their interrupted indexes can recover on a later restart.
2379
+ Custom indexes build in the background: `createIndexes` publishes them as
2380
+ building in one short commit and answers once they are ready, while other writes
2381
+ go on. A build sorts its entries off the writer, after reserving the space its
2382
+ sort runs and its output need, with its runs in a private scratch directory
2383
+ inside a file root and in memory on a memory root, and attaches the entries in
2384
+ one commit; it then applies the writes made meanwhile until the writers keep
2385
+ its indexes up to date in their own commits, and makes its indexes ready
2386
+ together in a commit that carries no entry. No read uses its indexes
2387
+ before then. A request for the same indexes joins a running build, and
2388
+ `serverStatus` reports builds under `indexBuilds`. A build that its collection's
2389
+ writes outpace, or that meets a resource limit, fails with an explicit refusal
2390
+ and leaves no index; a duplicate key fails a unique build with `DuplicateKey`
2391
+ (11000). Interrupted builds restart from the beginning under native ownership
2392
+ before serving, and run in the background, so serving does not wait for them.
2393
+ A restarted build that fails removes its indexes, restores a quarantined index
2394
+ it was rebuilding, logs a warning that names neither the index nor a key, and
2395
+ shows as failed in `indexBuilds` until the next build of that index; it is not
2396
+ retried by itself. Held databases remain closed and are skipped during
2397
+ recovery; releasing the hold restarts their interrupted builds.
2650
2398
 
2651
2399
  Index names and aliases, sparse and unique constraints, empty collections,
2652
2400
  restricted-view catalogs and exact BSON survive supported database export/import.
@@ -2665,7 +2413,7 @@ The serving profile is:
2665
2413
  | Auth/sessions | SCRAM-SHA-256, logical sessions, snapshot transactions, commit/abort and durable retryable writes |
2666
2414
  | Management | Health, tenants, grants, attestation, fencing, publication holds, export/import and content digest |
2667
2415
  | Debug | Bounded current-session OpLog, atomic data revert, document/collection pages and dashboard |
2668
- | Maintenance | Bounded TTL expiry, retired record reclamation, orphan import retirement and generation compaction |
2416
+ | Maintenance | Bounded TTL expiry, retired record reclamation, orphan import retirement, and the store's merges, checkpoints, tombstone pruning and cleaning |
2669
2417
  | Stopped storage | Native inspection and relocation; isolated authenticated rehearsal on its qualified platform profile |
2670
2418
  | Import/control admission | 100 MiB encoded import; bounded private staging and a small final catalog/receipt commit. Other control requests: 1 MiB |
2671
2419
  | External cluster | Replication and HA remain unsupported; the one-voter OpenRaft foundation is not linked into serving |
@@ -2674,16 +2422,20 @@ These are operation and resource budgets, not total database/document limits.
2674
2422
  Unsupported options and commands reject explicitly. Imports that cannot fit one
2675
2423
  derived document or index entry within the native commit budget reject before
2676
2424
  publishing the replacement. Reclamation preserves pinned roots and retires parent
2677
- identities only after their children have been handled. Automatic compaction
2678
- requires at least 64 MiB of growth and 50% growth over its prior baseline.
2425
+ identities only after their children have been handled. The store's cleaner
2426
+ moves the live data out of a data segment of which at least a third of the
2427
+ written bytes is garbage, and a segment is removed once no checkpoint or
2428
+ pinned snapshot needs it.
2679
2429
 
2680
- There is one serving engine and no Bitcask fallback. No historical storage or
2681
- authentication conversion runs during startup or rehearsal. Conversion code,
2682
- when explicitly commissioned, belongs exclusively in top-level `ts_migration/`.
2430
+ There is one serving engine and no Bitcask fallback. A start converts a root that
2431
+ a 10.x release wrote into format 3 (see **Storage format migration** under File
2432
+ startup admission); no other storage conversion and no authentication conversion
2433
+ runs during startup or rehearsal. Any other conversion code, when explicitly
2434
+ commissioned, belongs exclusively in top-level `ts_migration/`.
2683
2435
 
2684
2436
  NoSQLDB makes no general MongoDB performance or feature-parity claim.
2685
2437
  Versioned packages contain Linux amd64/arm64 and macOS amd64/arm64 native
2686
- executables through [@git.zone/tsrust](https://www.npmjs.com/package/@git.zone/tsrust).
2438
+ executables through [@git.zone/rsrepo](https://www.npmjs.com/package/@git.zone/rsrepo).
2687
2439
 
2688
2440
  ### Multi-host release artifacts
2689
2441
 
@@ -2705,7 +2457,7 @@ The native distribution terms and qualified library requirements are recorded in
2705
2457
  extract and install these files independently of its compiled JavaScript bundle;
2706
2458
  NoSQLDB does not download engines at startup.
2707
2459
 
2708
- A plain `tsrust` build, as run by `pnpm build` and before `pnpm test`, compiles only the configured targets of the host's own architecture (its GNU and musl engines on Linux); `tsrust --configured-targets` builds the host's complete partition from `targetsByHost`. The development suite checks the packaged engines of the host architecture. Releases use `tsrust matrix` to coordinate both partitions after the version commit exists, then publish only after native verification passes and every exact-commit binary has executed and passed strict provenance assembly.
2460
+ A plain `rsrepo` build, as run by `pnpm build` and before `pnpm test`, compiles only the configured targets of the host's own architecture (its GNU and musl engines on Linux); `rsrepo --configured-targets` builds the host's complete partition from `targetsByHost`. The development suite checks the packaged engines of the host architecture. Releases use `rsrepo matrix` to coordinate both partitions after the version commit exists, then publish only after native verification passes and every exact-commit binary has executed and passed strict provenance assembly.
2709
2461
 
2710
2462
  The release coordinator needs Git, Node.js, pnpm and SSH access to the configured macOS builder. The local Linux amd64 builder needs `aarch64-linux-gnu-gcc`, `x86_64-linux-musl-gcc` and `aarch64-linux-musl-gcc` and a registered Linux arm64 `binfmt` interpreter. Linux file storage requires kernel 5.6 or newer with `openat2` enabled. Cross-architecture file qualification requires a full-system Linux guest or a native host. A linux-user executable probe does not qualify secure filesystem syscalls such as `openat2`. The built-package acceptance script `test/verify-alpine.mjs` exercises authenticated nested document persistence and restart through Node and Deno on Alpine; run it on both Linux architectures before publication. The Apple Silicon builder needs Rosetta. The matrix qualifies macOS first and then Linux; both partitions must pass before artifact assembly. Set these environment variables to authorized builders and their dedicated temporary roots:
2711
2463
 
@@ -2714,11 +2466,11 @@ export NOSQLDB_RELEASE_MACOS_SSH="release-user@mac-builder.example"
2714
2466
  export NOSQLDB_RELEASE_MACOS_TEMP_ROOT="/absolute/dedicated/nosqldb-release"
2715
2467
  ```
2716
2468
 
2717
- `gitzone release` first runs `pnpm run release:check-builders`, before creating a version commit or tag. It verifies the project-local tsrust version, compiles and executes the required architecture probes, and rejects missing cross-toolchains or emulation, an unsuitable macOS host, missing tools, or an unsafe remote temporary root. The release build then:
2469
+ `gitzone release` first runs `pnpm run release:check-builders`, before creating a version commit or tag. It verifies the project-local rsrepo version and rejects missing cross-toolchains or emulation, an unsuitable macOS host, missing tools, or an unsafe remote temporary root. The release build then:
2718
2470
 
2719
2471
  1. Builds the TypeScript package and debug UI bundle from the version commit.
2720
2472
  2. Transfers an exact Git bundle into isolated local Linux and SSH macOS worker workspaces and installs each frozen lockfile.
2721
- 3. Runs the configured `release:verify-native` command on each worker: the full Rust workspace, TypeScript test checks, a `tsrust --configured-targets` build of the worker's partition, the public API suite with the packaged-engine checks for both architectures, and explicit-engine authentication/ownership/rehearsal tests through the worker's other packaged architecture. Tests use short, private disposable filesystem roots under `/var/tmp` on Linux and canonical `/tmp` on macOS. Linux builders must provide `/var/tmp` on a native-supported durable filesystem; tmpfs is rejected. Failures retain diagnostics and prevent assembly.
2473
+ 3. Runs the configured `release:verify-native` command on each worker: the full Rust workspace, TypeScript test checks, a `rsrepo --configured-targets` build of the worker's partition, the public API suite with the packaged-engine checks for both architectures, and explicit-engine authentication/ownership/rehearsal tests through the worker's other packaged architecture. Tests use short, private disposable filesystem roots under `/var/tmp` on Linux and canonical `/tmp` on macOS. Linux builders must provide `/var/tmp` on a native-supported durable filesystem; tmpfs is rejected. Failures retain diagnostics and prevent assembly.
2722
2474
  4. Builds and executes GNU and musl Linux amd64/arm64 plus macOS arm64/amd64 binaries on their assigned workers.
2723
2475
  5. Retrieves bounded artifact sets and transactionally publishes the complete provenance-checked matrix into `dist_rust/`.
2724
2476
 
@@ -2730,13 +2482,38 @@ No host address or path is stored in the repository. A failed run retains its un
2730
2482
 
2731
2483
  - Atomic native publications include data, indexes, authentication, fences and
2732
2484
  durable outcomes. File writes are acknowledged after durable commit.
2733
- - Immutable roots keep reads stable through concurrent writes and compaction.
2485
+ - Snapshots keep reads stable through concurrent writes and maintenance:
2486
+ merges, checkpoints and the cleaner free nothing a snapshot can reach.
2734
2487
  - Checksums, frame identities and contiguous positions validate committed
2735
2488
  history. Only a valid incomplete final append is repairable during owned
2736
2489
  startup; complete or interior corruption is rejected.
2490
+ - A device that loses acknowledged bytes at the end of the log and reads
2491
+ them back as zeros cannot be detected: recovery takes the zeros for the end
2492
+ of the log. The loss reaches back at most to the newest durable
2493
+ checkpoint's redo start. The commits it can lose hold at most half the
2494
+ buffer budget plus two holds, counted as their keys and values plus 64
2495
+ bytes per record: one hold of commits, which is one commit or one group
2496
+ or batch of commits, and one background commit's claim on the buffer.
2497
+ The server sets the budget when it opens a root, to
2498
+ `storageBudgetBytes` where that option is set (4 MiB to 1 TiB), and
2499
+ otherwise to a sixteenth of the memory the process may use, from 16 MiB to
2500
+ 4 GiB: 64 MiB plus the two holds can be lost in a 2 GiB container, and at
2501
+ most 2 GiB plus the two holds unless the option sets a larger budget.
2502
+ Each hold is at most the batch limit, 64 MiB by default.
2737
2503
  - Uncertain publication fences the engine until confirmed recovery.
2738
- - Bounded maintenance reclaims unreachable records and obsolete generations
2739
- after their readers release them.
2504
+ - Bounded maintenance reclaims unreachable records, and segments that hold
2505
+ only superseded data, after their readers release them.
2506
+ - Disk use under live snapshots is unbounded: the store does not limit how
2507
+ long a snapshot stays open. A segment whose data has all been superseded
2508
+ stays on disk while a snapshot that can still read it is open, and
2509
+ `serverStatus` reports those bytes at
2510
+ `serverStatus.engineFigures.pinnedBytes`.
2511
+ - Writes are admitted at full speed until the free space falls to the
2512
+ store's floor: 64 MiB or two segments, whichever is more, beyond what the
2513
+ write buffer holds. There, every write but a delete or a drop is refused
2514
+ with the capacity code 16500 and writes nothing. Deletes and drops are
2515
+ still admitted, down to two segments, while no snapshot older than the
2516
+ newest write is open.
2740
2517
  - A durable session whose records this version cannot read does not stop
2741
2518
  maintenance. The obsolete-outcome sweep passes over it and leaves its records
2742
2519
  in place. It logs an error at most once a minute and counts the session in
@@ -2760,22 +2537,48 @@ deadline, 100,000 filesystem-entry and 1 TiB byte limits, and bounded native
2760
2537
  record/result admission. This is storage integrity evidence; it does not replace
2761
2538
  the full authenticated rehearsal or production ownership protocol.
2762
2539
 
2540
+ A failure names a stable code (`NoSQLDB stopped inspection failed with code …`).
2541
+ Two codes describe states the tool deliberately does not validate:
2542
+
2543
+ - `RECOVERY_REQUIRED` while a stopped relocation or alignment has left its
2544
+ workspace in the root: finish that relocation or alignment, or abort the
2545
+ alignment, with its exact request, then validate again. A root of this
2546
+ release that a crash left needs no start first: the tool reads it as a start
2547
+ would recover it, writing nothing. A root that a 10.x process left without a
2548
+ clean stop also reports `RECOVERY_REQUIRED`; a start of this release converts
2549
+ it as of its last completed commit.
2550
+ - `UNSUPPORTED` while an index build is unfinished: an index that is building,
2551
+ quarantined, or importing into a collection that is not dropped. A start
2552
+ restarts interrupted builds from the beginning in the background, without
2553
+ waiting for them before it serves, so a root stopped before they publish
2554
+ still reports `UNSUPPORTED`, as does one whose database's publication hold
2555
+ keeps them back. A restarted build that fails, for example on duplicate
2556
+ keys, removes its indexes or restores the quarantined index it was
2557
+ rebuilding, which validation reports until that index is dropped or rebuilt.
2558
+
2763
2559
  ---
2764
2560
 
2765
2561
  ## Testing Example
2766
2562
 
2767
- The offline regression `test/test.commit-journal-compatibility.node.ts` creates
2768
- disposable roots with the exact published 10.8.1 executable, then qualifies the
2769
- current engine against them. A normal `pnpm install --frozen-lockfile` provides
2770
- the exact `@lossless.org/nosqldb@10.8.1` package through the pinned development
2771
- alias `@lossless.org/nosqldb-baseline-1081`, with registry integrity in the lockfile.
2772
- The test reads its platform executable directly from `node_modules`, with no
2773
- manual artifact cache or environment variable. It also pins each supported
2774
- executable's published SHA-256
2775
- and fails on missing or substituted artifacts; it never fetches, skips, builds
2776
- the baseline, or changes dependency manifests at runtime. Build the current
2777
- engine normally before running the test. This qualifies opening existing data
2778
- without rewriting its native format, followed by durable journal publications.
2563
+ The offline regression `test/test.commit-journal-compatibility.node.ts` reads
2564
+ `test/format-2/compatibility.tar`, a root that the published 10.8.1 release
2565
+ wrote once from the definition in `test/format-2/roots.ts`
2566
+ (`test/format-2/archives.md` records how). It checks every archive in
2567
+ `test/format-2/` against `SHA256SUMS`, and each of its tests that reads the root extracts that
2568
+ archive into a new directory that only its owner can access, so the root it
2569
+ reads is a copy. It qualifies that
2570
+ the stopped audit and the namespace and string inspections read the 10.x copy
2571
+ without changing a byte: the audit finds it foreign and ineligible, with
2572
+ `CONVERSION_PENDING`, `AUTH_USERS_PRESENT` and the definition's two users, and
2573
+ the inspections give the definition's databases, collections and values. An
2574
+ `existing-only` start converts the copy, leaving only this release's names, and
2575
+ is refused because a copied root is served only after alignment; a second start
2576
+ is refused the same way and changes nothing. The converted root keeps the 10.x
2577
+ root's provider, group, timeline and LSN, its two users and its databases,
2578
+ collections and values, and alignment refuses it with `UNSUPPORTED`, as a dry
2579
+ run and for real, because it holds auth users; it writes no receipt and
2580
+ changes nothing. The test starts only this release's engine; build it normally
2581
+ before running the test.
2779
2582
 
2780
2583
  The commit-journal lifetime regression uses the existing test-support executable
2781
2584
  and its three-second `NOSQLDB_TEST_TRANSACTION_LIFETIME_MS` setting. It compares
@@ -2785,7 +2588,7 @@ qualification executable through `test:before`; production keeps its unchanged
2785
2588
  60-second transaction lifetime and the suite keeps its 60-second file timeout.
2786
2589
 
2787
2590
  ```typescript
2788
- import { expect, tap } from '@git.zone/tstest/tapbundle';
2591
+ import { expect, tap } from '@git.zone/tsrepo/tapbundle';
2789
2592
  import { NoSqlDbServer } from '@lossless.org/nosqldb';
2790
2593
  import { MongoClient } from 'mongodb';
2791
2594