@zakkster/lite-bake-stream 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## [1.1.0] -- 2026-08-31
6
+
7
+ ### Added
8
+
9
+ - Torture gate at the pipeline's canonical path: `node --expose-gc test/torture.mjs` prints `ok` on success, exit 0/1. Tiers: t5 (the 37-scenario stress suite, relocated from bench/ with behavior unchanged), t6 (zero-alloc gates with `maxArrayBuffersGrowth: 0` via measureOps `stabilize: 'deep'` over the tokenizer steady state, the StringTable intern hit path, and the Reader cell loop, each with structural buffer asserts), t7 (lite-leak retention soak: 4096 full-tier cycles, tracker size 0, arrayBuffers delta 0.00 MB), t9 (four in-process controls plus the `BAKE_TORTURE_BREAK=1` whole-suite control, proven to exit non-zero). Tiers t0-t4/t8 are registered reserved stubs. Preflight exits 2 with a remedy line when devDependencies are not installed.
10
+ - `decisions/` directory (repo-only, not shipped): 0001-reserved-empty-string.md records the BS-01 fix and its two rejected alternatives.
11
+ - Round-trip regression suite hardening: 284 tests (282 pass / 2 todo). Five permanent guards pin the BS-01 fix; the two remaining todos are registered known issues (BS-05 invalid UTF-8 acceptance, BS-03 duplicate schema field names). Absent-field fuzz generation, -0 sign, duplicate-key last-wins, null/bool lane-policy codes, and preserve-mode byte-exactness pins added.
12
+ - `npm run verify` (test + torture).
13
+
14
+ ### Changed
15
+
16
+ - `npm test` now globs `test/*.test.js`; a new test file runs without editing package.json.
17
+ - `bench/torture.js` is a deprecation shim forwarding to `test/torture.mjs` (same exit codes, same `--tier` flag).
18
+ - devDependencies: added `@zakkster/lite-leak` `^1.10.0`; `@zakkster/lite-gc-profiler` `^1.0.0` -> `^1.16.0` (the `stabilize: 'deep'` option used by the t6 gate landed in 1.16.0).
19
+ - SPEC 3.3: string-table entry 0 is reserved as the empty string (writers MUST emit it); SPEC 7 absent-U32 row now reads `""`. See Fixed.
20
+
21
+ ### Fixed
22
+
23
+ - **BS-01: absent string fields now decode as `""`.** A record that omits a U32-lane field leaves index 0 in that row slot. Index 0 of a shard's string table used to be whichever string interned first in that shard, so `{"s":"zebra"}` followed by `{}` read row 1 back as `"zebra"` -- silently, and differently depending on record order and where shard boundaries fell. `StringTable` now reserves index 0 as the empty string in its constructor and at every `reset()`, so absent U32 cells decode as `""` on every write path: explicit schema, sample-and-infer, and every shard of a multi-shard drain. Containers written before this change keep their old bytes and their old, data-dependent behavior on those bytes; the fix is not retroactive. Readers are unchanged. SPEC 3.3 and the SPEC 7 coercion table are amended to match.
24
+
5
25
  ## [1.0.0] — 2026-07-12
6
26
 
7
27
  First stable release. The LBK1 container format is frozen at `format_version: 1`.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  > Streaming byte-level JSON compiler for [`@zakkster/lite-bake`](https://github.com/PeshoVurtoleta/lite-bake). Zero-GC, tree-shakeable, gigabyte-scale.
15
15
 
16
- **Status:** v1.0.0. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak with zero GC and byte-exact preservation across 590 million cells.
16
+ **Status:** v1.1.0. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak with zero GC and byte-exact preservation across 590 million cells.
17
17
 
18
18
  ## Two modes, one API
19
19
 
package/SPEC.md CHANGED
@@ -95,7 +95,7 @@ u32 offsets[entry_count + 1] // byte offsets into blob, INCLUDING a trailing
95
95
  u8 blob[blob_length_bytes]
96
96
  ```
97
97
 
98
- Padded to 8. Strings are UTF-8, not null-terminated. Length of string `i` is `offsets[i+1] - offsets[i]`; the trailing sentinel makes this valid for `i = entry_count - 1` without a branch. Entry index 0 is not reserved; the empty string is a legitimate entry.
98
+ Padded to 8. Strings are UTF-8, not null-terminated. Length of string `i` is `offsets[i+1] - offsets[i]`; the trailing sentinel makes this valid for `i = entry_count - 1` without a branch. Entry index 0 is reserved: it is always the empty string. Writers MUST emit "" as entry 0 of every shard's local table, including tables that hold no other entries. Readers MAY rely on this for containers written at or after this rule; containers written before it may hold any string at index 0.
99
99
 
100
100
  Per-shard tables are placed immediately after their shard payload in the container, so a single HTTP Range fetch covering `[payload_off, payload_off + payload_len + local_string_len)` retrieves both.
101
101
 
@@ -337,7 +337,7 @@ Given source NDJSON `S` and a schema `Σ` (either explicit or inferred), the LBK
337
337
  | JSON string (any valid UTF-8) | U32 | **byte-exact UTF-8 sequence** |
338
338
  | JSON `true` / `false` / `null` | F64 | `1` / `0` / `0` (documented coercion) |
339
339
  | absent field (missing in source) | F64 | `0` (default) |
340
- | absent field | U32 | string at index 0 of the shard's local table |
340
+ | absent field | U32 | `""` (index 0 is the reserved empty-string entry) |
341
341
  | field in source but NOT in `Σ` | — | silently dropped (matches lite-bake core) |
342
342
  | field in `Σ` with wrong value type (post-freeze) | — | `W_LANE_MISMATCH` error, container not produced |
343
343
 
package/llms.txt CHANGED
@@ -8,13 +8,13 @@ Ingest gigabyte-scale JSON (top-level array or NDJSON) into the `lite-bake` LBK1
8
8
 
9
9
  ## Status
10
10
 
11
- v1.0.0 — stable. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak (M1 MacBook Pro): 98.37M rows, 4.89 GB container, zero major GC, zero minor GC, 499 KB total heap allocation, 590.21M cells verified byte-exact, zero mismatches. Tokenizer benches at 222-237 MB/s (~55% of JSON.parse, with no object graph allocated).
11
+ v1.1.0 — stable. LBK1 format frozen at `format_version: 1`. Qualified on an 8 GB soak (M1 MacBook Pro): 98.37M rows, 4.89 GB container, zero major GC, zero minor GC, 499 KB total heap allocation, 590.21M cells verified byte-exact, zero mismatches. Tokenizer benches at 222-237 MB/s (~55% of JSON.parse, with no object graph allocated).
12
12
 
13
13
  Public API follows semver from 1.0.0. Future additions (I64 lane, columnar payload mode, container-level string table) land via the format's forward-compat seams -- `min_reader_version` on ShardEntry, reserved FieldDescriptor flags, the `metadata_off` block wrapper -- without a format_version bump.
14
14
 
15
15
  See SPEC.md for the LBK1 container format, section 3.6 for zone maps, section 4.3 for the reserved field flags.
16
16
 
17
- ## Public API (v1.0.0)
17
+ ## Public API (v1.1.0)
18
18
 
19
19
  Two ingest modes share one top-level API:
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-bake-stream",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Streaming byte-level JSON to lite-bake binary compiler. Zero-GC, tree-shakeable, gigabyte-scale.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -76,10 +76,11 @@
76
76
  "LICENSE"
77
77
  ],
78
78
  "scripts": {
79
- "test": "node --test test/Tokenizer.test.js test/RoundTrip.test.js test/StringRoundTrip.test.js test/DataPreservation.test.js test/NumericBoundary.test.js test/RangeReader.test.js test/FileIngest.test.js test/ZoneMaps.test.js test/M4.test.js test/M6.test.js test/Conformance.test.js test/Fuzz.test.js test/OffsetOverflow.test.js test/Preserve.test.js",
79
+ "test": "node --test test/*.test.js",
80
80
  "bench": "node --expose-gc bench/bench-tokenizer.js",
81
- "torture": "node --expose-gc bench/torture.js --tier=fast",
82
- "torture:full": "node --expose-gc bench/torture.js --tier=full",
81
+ "torture": "node --expose-gc test/torture.mjs",
82
+ "torture:full": "node --expose-gc test/torture.mjs --tier=full",
83
+ "verify": "npm test && npm run torture",
83
84
  "soak": "node --expose-gc bench/soak.js --mb=100 --verify",
84
85
  "soak:500": "node --expose-gc bench/soak.js --mb=500 --verify",
85
86
  "soak:1gb": "node --expose-gc bench/soak.js --gb=1 --verify",
@@ -112,6 +113,7 @@
112
113
  "access": "public"
113
114
  },
114
115
  "devDependencies": {
115
- "@zakkster/lite-gc-profiler": "^1.0.0"
116
+ "@zakkster/lite-gc-profiler": "^1.16.0",
117
+ "@zakkster/lite-leak": "^1.10.0"
116
118
  }
117
119
  }
package/src/FileIngest.js CHANGED
@@ -26,7 +26,7 @@ import { PreserveTokenizer } from './PreserveTokenizer.js';
26
26
  import { PreserveWriter } from './PreserveWriter.js';
27
27
  import { PreserveReader } from './PreserveReader.js';
28
28
 
29
- export const VERSION = '1.0.0';
29
+ export const VERSION = '1.1.0';
30
30
 
31
31
  // Ingest a ReadableStream<Uint8Array> and return a Reader over the resulting
32
32
  // LBK1 container. Options:
@@ -24,7 +24,7 @@
24
24
  // M_UNKNOWN_FIELD - .get() / .fieldIndex() called with unknown name
25
25
  // M_ROW_OUT_OF_RANGE - rowIdx >= totalRows
26
26
 
27
- export const VERSION = '1.0.0';
27
+ export const VERSION = '1.1.0';
28
28
 
29
29
  export class MultiReaderError extends Error {
30
30
  constructor(code, msg) { super(msg); this.code = code; this.name = 'MultiReaderError'; }
@@ -17,7 +17,7 @@
17
17
  // buffer. It is valid for as long as the Reader instance holds the container.
18
18
  // Do NOT mutate it, and do NOT retain it past the Reader's lifetime.
19
19
 
20
- export const VERSION = '1.0.0';
20
+ export const VERSION = '1.1.0';
21
21
 
22
22
  const CONTAINER_HEADER_BYTES = 48;
23
23
  const SHARD_ENTRY_BYTES = 40;
@@ -26,7 +26,7 @@
26
26
  // E_TRUNCATED - end of input reached mid-record (depth != 0 or in-string)
27
27
  // E_RECORD_TOO_LARGE - single record exceeds maxRecordBytes (default: none)
28
28
 
29
- export const VERSION = '1.0.0';
29
+ export const VERSION = '1.1.0';
30
30
 
31
31
  export class PreserveTokenizerError extends Error {
32
32
  constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveTokenizerError'; }
@@ -16,7 +16,7 @@
16
16
  // Reader dispatch happens via the flag bit; a schema-mode Reader refuses this
17
17
  // container with R_WRONG_MODE.
18
18
 
19
- export const VERSION = '1.0.0';
19
+ export const VERSION = '1.1.0';
20
20
 
21
21
  export class PreserveWriterError extends Error {
22
22
  constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
@@ -35,7 +35,7 @@
35
35
 
36
36
  import { StringTable } from './StringTable.js';
37
37
 
38
- export const VERSION = '1.0.0';
38
+ export const VERSION = '1.1.0';
39
39
 
40
40
  const CONTAINER_HEADER_BYTES = 48;
41
41
  const SHARD_ENTRY_BYTES = 40;
package/src/Reader.js CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { StringTable } from './StringTable.js';
20
20
 
21
- export const VERSION = '1.0.0';
21
+ export const VERSION = '1.1.0';
22
22
 
23
23
  const CONTAINER_HEADER_BYTES = 48;
24
24
  const SHARD_ENTRY_BYTES = 40;
package/src/Split.js CHANGED
@@ -36,7 +36,7 @@ import { Writer, WriterError } from './Writer.js';
36
36
  import { Reader, ReaderError } from './Reader.js';
37
37
  import { StringTable } from './StringTable.js';
38
38
 
39
- export const VERSION = '1.0.0';
39
+ export const VERSION = '1.1.0';
40
40
 
41
41
  const LF = 0x0A;
42
42
  const CONTAINER_HEADER_BYTES = 48;
@@ -15,8 +15,12 @@
15
15
  //
16
16
  // The trailing sentinel makes `len(i) = offsets[i+1] - offsets[i]` uniform for
17
17
  // all i including the last — no branch in the reader hot path.
18
+ //
19
+ // Entry 0 is always the empty string: the table reserves it in the constructor
20
+ // and at every reset(), so an absent U32 row cell (which is 0) decodes as ""
21
+ // rather than aliasing the shard's first-interned string (SPEC 3.3, SPEC 7).
18
22
 
19
- export const VERSION = '1.0.0';
23
+ export const VERSION = '1.1.0';
20
24
 
21
25
  const EMPTY_SLOT = 0xFFFFFFFF; // MUST be unsigned; typed-array reads are unsigned
22
26
  const INITIAL_BLOB_BYTES = 64 * 1024;
@@ -25,6 +29,10 @@ const INITIAL_HASH_CAP = 2048; // load factor target 50%
25
29
  const HASH_MAX_LOAD_NUM = 1; // 50% load factor: num/den = 1/2
26
30
  const HASH_MAX_LOAD_DEN = 2;
27
31
 
32
+ // Reserved entry 0. A zero-length range: no blob bytes, no growth, one hash
33
+ // slot. Module-level so reset() reserves without allocating.
34
+ const EMPTY = new Uint8Array(0);
35
+
28
36
  // FNV-1a 32-bit over a byte range.
29
37
  function fnv1a(bytes, from, to) {
30
38
  let h = 0x811c9dc5 | 0;
@@ -44,6 +52,7 @@ export class StringTable {
44
52
  this._hashCap = INITIAL_HASH_CAP;
45
53
  this._hashMask = INITIAL_HASH_CAP - 1;
46
54
  this._hashKeys = new Uint32Array(INITIAL_HASH_CAP).fill(EMPTY_SLOT);
55
+ this._reserveEmpty();
47
56
  }
48
57
 
49
58
  // Returns u32 index of the string with these bytes, adding a new entry if
@@ -76,6 +85,13 @@ export class StringTable {
76
85
  return true;
77
86
  }
78
87
 
88
+ // Class invariant: index 0 is the empty string. Zero-alloc -- _insertNew with
89
+ // n === 0 appends no bytes, offsets[0]/[1] already exist, and the load-factor
90
+ // check cannot trip at count 1.
91
+ _reserveEmpty() {
92
+ this.intern(EMPTY, 0, 0);
93
+ }
94
+
79
95
  _insertNew(bytes, from, to, slot) {
80
96
  const n = to - from;
81
97
  // Grow blob if needed
@@ -158,6 +174,7 @@ export class StringTable {
158
174
  // (we only touched [0..count], so the fill is bounded)
159
175
  this._offsets[0] = 0;
160
176
  this._hashKeys.fill(EMPTY_SLOT);
177
+ this._reserveEmpty();
161
178
  }
162
179
 
163
180
  // Serialize into the LBK1 per-shard string-table byte layout described at the
package/src/Tokenizer.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // E_TRAILING_INPUT - non-whitespace bytes after top-level value(s) (array mode only)
18
18
  // E_DEPTH_LIMIT - nesting depth exceeded MAX_DEPTH
19
19
 
20
- export const VERSION = '1.0.0';
20
+ export const VERSION = '1.1.0';
21
21
 
22
22
  // ---------- byte constants ----------
23
23
  const B_SPACE = 0x20, B_TAB = 0x09, B_LF = 0x0A, B_CR = 0x0D;
package/src/Writer.js CHANGED
@@ -29,7 +29,7 @@
29
29
 
30
30
  import { StringTable } from './StringTable.js';
31
31
 
32
- export const VERSION = '1.0.0';
32
+ export const VERSION = '1.1.0';
33
33
 
34
34
  const CONTAINER_HEADER_BYTES = 48;
35
35
  const SHARD_ENTRY_BYTES = 40;
package/src/index.js CHANGED
@@ -26,7 +26,7 @@ export {
26
26
  PreserveWriter, PreserveWriterError,
27
27
  PreserveReader, PreserveReaderError,
28
28
  };
29
- export const VERSION = '1.0.0';
29
+ export const VERSION = '1.1.0';
30
30
 
31
31
  const encoder = new TextEncoder();
32
32