@zakkster/lite-bake-stream 1.3.0 → 1.4.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 +26 -0
- package/README.md +1 -1
- package/SPEC.md +15 -5
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/FileIngest.js +1 -1
- package/src/MultiReader.js +1 -1
- package/src/PreserveReader.js +1 -1
- package/src/PreserveTokenizer.js +1 -1
- package/src/PreserveWriter.js +1 -1
- package/src/RangeReader.js +1 -1
- package/src/Reader.js +1 -1
- package/src/Split.js +1 -1
- package/src/StringTable.js +1 -1
- package/src/Tokenizer.js +19 -2
- package/src/Writer.js +64 -10
- package/src/index.js +1 -1
- package/types/Tokenizer.d.ts +6 -0
- package/types/Writer.d.ts +18 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.4.0] -- 2026-09-01
|
|
6
|
+
|
|
7
|
+
M4 -- the sample window does what its name says, and null stops eating lanes. Two inference-window bugs fixed together (BS-07, BS-20). Suite: 461 tests / 461 pass / 0 fail / 0 todo. Torture: 44/44 fast, 47/47 full (incl. the 64 MB structural scenario), arrayBuffers growth 0. Mutation measurement: the 8-test manifest failed exactly and the 5 canaries passed on baseline src. Findings probes: 19 NOT-REPRODUCED / 0 REPRODUCED / 0 PARTIAL -- the ledger's first all-clear; the probe file is retired.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- BS-07: the sample-and-infer window is now byte-true. The Tokenizer exposes `absOffset` (a getter over the cumulative consumed-input-byte counter that already survived chunk boundaries), and the Writer reads it at record boundaries via a new optional `setInputSource(tokenizer)` hook the Tokenizer calls at construction. The window ends at the first record boundary at or after `sampleBytes` INPUT bytes -- independent of chunking and record count. Previously `setInputByteOffset` had zero callers, so the window measured record count ("first `sampleBytes` records") and `serialize()` on a large input buffered the whole thing columnar until finalize (SPEC invariant 1 violated on the default path). `setInputByteOffset` is now a documented no-op retained until 2.0. A hand-driven Writer with no wired source samples by record count (documented fallback; `null` is not a 0-byte estimate). No sink-protocol change: the hook is a call probed with `typeof`, never a field write, so a frozen sink is untouched (BS-19). See decisions/0007-sample-window-bytes.md.
|
|
12
|
+
- BS-20: JSON `null` is now lane-neutral. In the sample window `onNull` marks the field but sets no kind, so `null + string` infers U32 (was `W_MIXED_LANE_TYPES`) and `null + number` infers F64; an only-null field still infers F64. Post-freeze, `null` is legal on both lanes -- F64 -> `0` (unchanged), U32 -> `""` (index 0, the reserved empty-string entry; was `W_LANE_MISMATCH`). Booleans are UNCHANGED: `true`/`false` on a U32 lane still throw `W_LANE_MISMATCH` (pinned). A field that is null through the whole sample window then sees a string post-freeze still throws `W_LANE_MISMATCH` (the pinned sharp edge; no post-freeze schema evolution). The `test/_verify.js` oracle and the t5 scenarios adopt the same policy in the same change. See decisions/0008-null-policy.md.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- BS-07: `sampleBytes` now bounds INPUT bytes as documented; sample memory is O(`sampleBytes`) (columnar staging plus the shared string table), independent of total input size. Proven structurally at full tier (64 MB default-opts stream: shards commit mid-ingest; the sample buffer is nulled after the window drains) and by a t7 SampleBuffer retention witness with a t9 BREAK control.
|
|
17
|
+
- BS-20: a single `null` in a string field no longer poisons inference or throws on an explicit U32 lane -- real-world API JSON that nulls string fields now compiles.
|
|
18
|
+
|
|
19
|
+
## [1.3.1] -- 2026-09-01
|
|
20
|
+
|
|
21
|
+
M3 remainder (BS-05) -- the UTF-8 door ends the promise-A/behavior-B split with a measured decision. Option A (validate at the tokenizer, refuse invalid UTF-8 with `E_INVALID_UTF8`) was built, proven correctness-green (460/460, torture fast 42/42), and rejected on its pre-committed budget: unicode-heavy throughput regressed 229.7 -> 213.1 MB/s (-7.2%, over the 3% ceiling), because the baseline scan bulk-copies `>= 0x80` bytes with zero inspection so validation is pure added cost, while ASCII stayed within noise. SPEC 5.5 is instead rewritten to match the long-standing behavior (documented-permissive), and the dead code is removed. No bytes move at runtime. See decisions/0006-utf8-door.md for the before/after numbers and the rejected option. Suite: 453 tests / 453 pass / 0 fail / 0 todo. Torture: 42/42 fast, 44/44 full, arrayBuffers growth 0. Inverted mutation check: all 49 battery and pin tests pass on baseline src as well -- zero behavior change, proven.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- SPEC 5.5 and SPEC 7's U32 row rewritten to documented-permissive: string bytes between the quotes are stored byte-exact through U32 lanes regardless of UTF-8 validity; invalid sequences are not a parse error. `get()`/`getString()` decode with replacement (U+FFFD); `shardStringTable(s).bytesAt(i)` returns the exact stored bytes, the schema-mode twin of `PreserveReader.getBytes(i)`. This is a documentation-and-dead-code change only -- no runtime behavior changed, no new public surface (the accessor already existed).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- BS-05: the SPEC no longer promises a UTF-8 parse error the tokenizer never enforced, and `Tokenizer.js` no longer declares an unreachable `E_INVALID_UTF8` code. The byte-exact string contract is now stated truthfully: invalid bytes round-trip byte-exact and decode with U+FFFD, in schema mode exactly as in preserve mode. Lone-surrogate escapes still refuse with `E_UNPAIRED_SURROGATE` (an escape-grammar rule, unrelated to input-UTF-8 validity).
|
|
30
|
+
|
|
5
31
|
## [1.3.0] -- 2026-09-01
|
|
6
32
|
|
|
7
33
|
M2 -- a container is verified at the door or refused at the door. The four readers (`Reader`, `PreserveReader`, `RangeReader`, `MultiReader`) now agree with each other and with the `Writer` about the bytes between them; every malformed container names its refusal with a code, and a standalone SPEC byte-checker (reads SPEC.md offsets only, imports nothing from `src/`) is the contract's executable form. All new validation runs at open/parse/construction time -- the `get`/`syncRange`/`findShards` row loops and both tokenizer byte loops are byte-identical. Suite: 432 tests / 431 pass / 0 fail / 1 todo. Torture: 37/37 fast, 39/39 full, arrayBuffers growth 0. Mutation check: all 52 door tests fail on source revert; all 23 behavior-unchanged pins hold.
|
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.
|
|
16
|
+
**Status:** v1.4.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
|
@@ -274,7 +274,7 @@ The boundary is exercised explicitly by `test/NumericBoundary.test.js`, which pi
|
|
|
274
274
|
|
|
275
275
|
### 5.5 String semantics (v1)
|
|
276
276
|
|
|
277
|
-
-
|
|
277
|
+
- String bytes between the quotes are stored byte-exact through U32 lanes, regardless of UTF-8 validity. Invalid sequences are **not** a parse error; the prior rule declaring them one (and its `E_INVALID_UTF8` code) is withdrawn. `get()` / `getString()` decode with replacement (U+FFFD); `shardStringTable(s).bytesAt(i)` returns the exact stored bytes, as `PreserveReader.getBytes(i)` does in preserve mode.
|
|
278
278
|
- All standard JSON escapes are supported: `\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`, `\uXXXX`.
|
|
279
279
|
- Surrogate pairs (`\uD800`..`\uDBFF` followed by `\uDC00`..`\uDFFF`) are combined and emitted as UTF-8.
|
|
280
280
|
- Unpaired surrogates are a parse error.
|
|
@@ -334,12 +334,14 @@ Given source NDJSON `S` and a schema `Σ` (either explicit or inferred), the LBK
|
|
|
334
334
|
| :-- | :-- | :-- |
|
|
335
335
|
| JSON number in fast-path domain (≤15 sig digits, `|exp| ≤ 22`) | F64 | **bit-exact IEEE 754 double** |
|
|
336
336
|
| JSON number in slow-path domain | F64 | within 1 ULP of the correctly-rounded value |
|
|
337
|
-
| JSON string (any valid UTF-8) | U32 | **byte-exact
|
|
338
|
-
| JSON `true` / `false`
|
|
337
|
+
| JSON string (any bytes, valid UTF-8 or not) | U32 | **byte-exact bytes; validity not asserted** |
|
|
338
|
+
| JSON `true` / `false` | F64 | `1` / `0` (documented coercion) |
|
|
339
|
+
| JSON `null` | F64 | `0` (documented coercion) |
|
|
340
|
+
| JSON `null` | U32 | `""` (index 0, the reserved empty-string entry; BS-20: null is lane-neutral) |
|
|
339
341
|
| absent field (missing in source) | F64 | `0` (default) |
|
|
340
342
|
| absent field | U32 | `""` (index 0 is the reserved empty-string entry) |
|
|
341
|
-
| field in source but NOT in
|
|
342
|
-
| field in
|
|
343
|
+
| field in source but NOT in the schema | -- | silently dropped (matches lite-bake core) |
|
|
344
|
+
| field in the schema with wrong value type (post-freeze), except null | -- | `W_LANE_MISMATCH` error, container not produced; null is lane-legal on both lanes (F64 -> 0, U32 -> "") |
|
|
343
345
|
|
|
344
346
|
The preservation contract is asserted mechanically by three complementary layers:
|
|
345
347
|
|
|
@@ -351,6 +353,14 @@ The boundary between "bit-exact" and "1 ULP" for F64 is pinned by `test/NumericB
|
|
|
351
353
|
|
|
352
354
|
Correctness of the preservation contract is a release gate on par with the zero-GC gate. If either fails, no publish.
|
|
353
355
|
|
|
356
|
+
### 7.1 Sample window (byte-true)
|
|
357
|
+
|
|
358
|
+
In sample-and-infer mode (no explicit schema) the Writer observes the first `sampleBytes` of INPUT before it freezes an inferred schema. The window is byte-true: it ends at the first record boundary AT OR AFTER `sampleBytes` input bytes have been consumed (BS-07). "Input bytes" is the Tokenizer's `absOffset` -- the cumulative count of bytes consumed across every `feed()` chunk, correct across chunk boundaries -- read by the Writer at record boundaries, never in the byte scan. The freeze point is therefore independent of how the input is chunked and of record count: 1000 tiny records or one 5 KB record both freeze at ~`sampleBytes` input bytes, not at a record count.
|
|
359
|
+
|
|
360
|
+
The Writer receives the live Tokenizer through an OPTIONAL capability hook: at construction the Tokenizer calls `sink.setInputSource(tokenizer)` if the method exists. This is a call, never a field write onto the sink, so a frozen sink is untouched (BS-19) and a sink without the method is skipped. The required sink event set is unchanged. A hand-driven Writer with no wired source samples by record count (documented fallback; never a 0-byte estimate). `setInputByteOffset` is a deprecated no-op.
|
|
361
|
+
|
|
362
|
+
Sample memory is O(`sampleBytes`): the columnar staging area plus the shared string table (bounded by string cardinality within the window), independent of total input size. The staging buffer drains to shards at freeze and is released.
|
|
363
|
+
|
|
354
364
|
## 8. Release gates
|
|
355
365
|
|
|
356
366
|
- **M4 GA gate.** JSON conformance corpus (RFC 8259 + JSONTestSuite), fuzzed chunk-boundary splits at every byte position, F64 round-trip parity, container round-trip parity, zero-alloc-per-row proven by lite-gc-profiler after warmup.
|
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.
|
|
11
|
+
v1.4.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.
|
|
17
|
+
## Public API (v1.4.0)
|
|
18
18
|
|
|
19
19
|
Two ingest modes share one top-level API:
|
|
20
20
|
|
|
@@ -49,7 +49,7 @@ Error classes with stable `code`: `TokenizerError`, `WriterError`, `ReaderError`
|
|
|
49
49
|
|
|
50
50
|
- `{ fields: ['id', 'x', 'y'] }` — all-F64 shorthand, back-compat with M2.
|
|
51
51
|
- `{ fields: [{name:'id', laneKind:'f64'}, {name:'tag', laneKind:'u32'}] }` — mixed lanes (M3).
|
|
52
|
-
- Sample-and-infer (default when no schema is passed): first `sampleBytes` of
|
|
52
|
+
- Sample-and-infer (default when no schema is passed): the first `sampleBytes` of INPUT are observed, byte-true (BS-07) -- the window ends at the first record boundary at or after `sampleBytes` input bytes, read from the Tokenizer's `absOffset`, independent of chunking and record count (a hand-driven Writer with no wired source samples by record count). Fields with only numbers become F64, only strings become U32. null is lane-neutral (BS-20): it sets no kind, so null+string infers U32 (no longer "mixed") and null+number infers F64; post-freeze null reads 0 on an F64 lane and "" on a U32 lane. A field that saw both a real number and a real string raises `W_MIXED_LANE_TYPES` at freeze. Sample memory is O(`sampleBytes`): columnar staging plus a shared string table, independent of total input size.
|
|
53
53
|
|
|
54
54
|
## Contract
|
|
55
55
|
|
package/package.json
CHANGED
package/src/FileIngest.js
CHANGED
|
@@ -27,7 +27,7 @@ import { PreserveWriter } from './PreserveWriter.js';
|
|
|
27
27
|
import { PreserveReader } from './PreserveReader.js';
|
|
28
28
|
import { checkOpts } from './Opts.js';
|
|
29
29
|
|
|
30
|
-
export const VERSION = '1.
|
|
30
|
+
export const VERSION = '1.4.0';
|
|
31
31
|
|
|
32
32
|
const U32_MAX = 4294967295;
|
|
33
33
|
const INGEST_OPTS = {
|
package/src/MultiReader.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// M_ROW_OUT_OF_RANGE - rowIdx >= totalRows
|
|
26
26
|
// M_TOO_MANY_ROWS - cumulative row count exceeds Number.MAX_SAFE_INTEGER
|
|
27
27
|
|
|
28
|
-
export const VERSION = '1.
|
|
28
|
+
export const VERSION = '1.4.0';
|
|
29
29
|
|
|
30
30
|
export class MultiReaderError extends Error {
|
|
31
31
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'MultiReaderError'; }
|
package/src/PreserveReader.js
CHANGED
package/src/PreserveTokenizer.js
CHANGED
package/src/PreserveWriter.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { checkOpts } from './Opts.js';
|
|
20
20
|
|
|
21
|
-
export const VERSION = '1.
|
|
21
|
+
export const VERSION = '1.4.0';
|
|
22
22
|
|
|
23
23
|
export class PreserveWriterError extends Error {
|
|
24
24
|
constructor(code, msg) { super(msg); this.code = code; this.name = 'PreserveWriterError'; }
|
package/src/RangeReader.js
CHANGED
package/src/Reader.js
CHANGED
package/src/Split.js
CHANGED
|
@@ -37,7 +37,7 @@ import { Reader, ReaderError } from './Reader.js';
|
|
|
37
37
|
import { StringTable } from './StringTable.js';
|
|
38
38
|
import { checkOpts } from './Opts.js';
|
|
39
39
|
|
|
40
|
-
export const VERSION = '1.
|
|
40
|
+
export const VERSION = '1.4.0';
|
|
41
41
|
|
|
42
42
|
const LF = 0x0A;
|
|
43
43
|
const CONTAINER_HEADER_BYTES = 48;
|
package/src/StringTable.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// and at every reset(), so an absent U32 row cell (which is 0) decodes as ""
|
|
21
21
|
// rather than aliasing the shard's first-interned string (SPEC 3.3, SPEC 7).
|
|
22
22
|
|
|
23
|
-
export const VERSION = '1.
|
|
23
|
+
export const VERSION = '1.4.0';
|
|
24
24
|
|
|
25
25
|
const EMPTY_SLOT = 0xFFFFFFFF; // MUST be unsigned; typed-array reads are unsigned
|
|
26
26
|
const INITIAL_BLOB_BYTES = 64 * 1024;
|
package/src/Tokenizer.js
CHANGED
|
@@ -4,13 +4,19 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Contract: see /SPEC.md section 5.
|
|
6
6
|
//
|
|
7
|
+
// Byte offset (BS-07): `absOffset` (getter) is the cumulative count of input
|
|
8
|
+
// bytes consumed across every feed() chunk; it survives chunk boundaries. A
|
|
9
|
+
// sink may expose setInputSource(tokenizer) to receive the live Tokenizer at
|
|
10
|
+
// construction and read absOffset at record boundaries (the Writer uses this
|
|
11
|
+
// for byte-true sampleBytes). Purely optional; a sink without the method is
|
|
12
|
+
// never called and never mutated (BS-19).
|
|
13
|
+
//
|
|
7
14
|
// Error codes (stable):
|
|
8
15
|
// E_UNEXPECTED_BYTE - byte does not belong in current state
|
|
9
16
|
// E_UNEXPECTED_EOF - end() called mid-token
|
|
10
17
|
// E_INVALID_ESCAPE - unknown \ sequence
|
|
11
18
|
// E_INVALID_HEX - non-hex digit in \uXXXX
|
|
12
19
|
// E_UNPAIRED_SURROGATE - lone high/low surrogate in \u escape
|
|
13
|
-
// E_INVALID_UTF8 - malformed UTF-8 byte sequence
|
|
14
20
|
// E_NUMBER_OVERFLOW - number magnitude exceeds F64 range
|
|
15
21
|
// E_NUMBER_INVALID - malformed number literal
|
|
16
22
|
// E_KEYWORD_MISMATCH - true/false/null spelled wrong
|
|
@@ -24,7 +30,7 @@
|
|
|
24
30
|
|
|
25
31
|
import { checkOpts } from './Opts.js';
|
|
26
32
|
|
|
27
|
-
export const VERSION = '1.
|
|
33
|
+
export const VERSION = '1.4.0';
|
|
28
34
|
|
|
29
35
|
const U32_MAX = 4294967295;
|
|
30
36
|
const TOKENIZER_OPTS = {
|
|
@@ -142,6 +148,12 @@ export class Tokenizer {
|
|
|
142
148
|
this._onNull = sink.onNull ? sink.onNull.bind(sink) : NOOP;
|
|
143
149
|
this._onEnd = sink.onEnd ? sink.onEnd.bind(sink) : NOOP;
|
|
144
150
|
|
|
151
|
+
// Optional capability hook (BS-07): a sink may expose setInputSource to
|
|
152
|
+
// receive this Tokenizer and read `absOffset` at record boundaries. This is
|
|
153
|
+
// a CALL, never a field write onto the sink, so a frozen sink is untouched
|
|
154
|
+
// (BS-19); a sink without the method is skipped. Cold path (construction).
|
|
155
|
+
if (typeof sink.setInputSource === 'function') sink.setInputSource(this);
|
|
156
|
+
|
|
145
157
|
// Terminal state (BS-14): once ended or poisoned, the instance is dead.
|
|
146
158
|
this._ended = false;
|
|
147
159
|
this._poisoned = false;
|
|
@@ -369,6 +381,11 @@ export class Tokenizer {
|
|
|
369
381
|
this._err('E_UNEXPECTED_EOF', 'input ended mid-token (state=' + this._state + ')');
|
|
370
382
|
}
|
|
371
383
|
|
|
384
|
+
// Cumulative input bytes consumed across every feed() chunk (survives chunk
|
|
385
|
+
// boundaries). Read by a Writer sink at record boundaries for byte-true
|
|
386
|
+
// sampleBytes (BS-07); never read on the byte scan.
|
|
387
|
+
get absOffset() { return this._absOffset; }
|
|
388
|
+
|
|
372
389
|
// -------- top-level handling --------
|
|
373
390
|
|
|
374
391
|
_handleTopByte(b) {
|
package/src/Writer.js
CHANGED
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
// Two modes:
|
|
6
6
|
// - Explicit schema: consumer passes { fields: [names...] } or
|
|
7
7
|
// { fields: [{name, laneKind}...] }; no sampling.
|
|
8
|
-
// - Sample-and-infer (default): first `sampleBytes` of
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// - Sample-and-infer (default): the first `sampleBytes` of INPUT (byte-true,
|
|
9
|
+
// BS-07: the window ends at the first record boundary at or after
|
|
10
|
+
// sampleBytes input bytes, read from the Tokenizer's absOffset via
|
|
11
|
+
// setInputSource; hand-driven Writers with no source sample by record
|
|
12
|
+
// count) buffered in a columnar staging area, per-field lane kind inferred
|
|
13
|
+
// from observed value types (number->F64, string->U32-into-string-table).
|
|
14
|
+
// null is lane-neutral (BS-20): it marks the field but sets no kind, so
|
|
15
|
+
// null+string infers U32 and null+number infers F64. A field that saw both
|
|
16
|
+
// a real number and a real string raises W_MIXED_LANE_TYPES at freeze.
|
|
12
17
|
//
|
|
13
18
|
// v1 lane kinds: F64 (numeric) and U32 (string-table index). Per-shard string
|
|
14
19
|
// tables ride immediately after each shard's payload; the shard directory
|
|
@@ -23,8 +28,10 @@
|
|
|
23
28
|
// W_NESTED_UNSUPPORTED - nested object/array inside a record
|
|
24
29
|
// W_EMPTY_INPUT - finalize() with zero records seen
|
|
25
30
|
// W_ROW_STRIDE_ZERO - schema has zero fields (nothing to store)
|
|
26
|
-
// W_MIXED_LANE_TYPES - a field saw both number and string values
|
|
31
|
+
// W_MIXED_LANE_TYPES - a field saw both real number and string values
|
|
32
|
+
// (null excluded -- it is lane-neutral, BS-20)
|
|
27
33
|
// W_LANE_MISMATCH - post-freeze: value type doesn't match schema lane
|
|
34
|
+
// (null is lane-legal on both lanes: F64->0, U32->"")
|
|
28
35
|
// W_UNKNOWN_LANE_KIND - explicit schema declares an unknown laneKind
|
|
29
36
|
// W_DUPLICATE_FIELD - schema declares the same field name twice
|
|
30
37
|
// W_SCHEMA_TOO_WIDE - a field offset exceeds the u16 offset_in_row ceiling
|
|
@@ -36,7 +43,7 @@
|
|
|
36
43
|
import { StringTable } from './StringTable.js';
|
|
37
44
|
import { checkOpts } from './Opts.js';
|
|
38
45
|
|
|
39
|
-
export const VERSION = '1.
|
|
46
|
+
export const VERSION = '1.4.0';
|
|
40
47
|
|
|
41
48
|
const U32_MAX = 4294967295;
|
|
42
49
|
// Post-finalize sentinel for _recordDepth. Chosen = 2 so every post-finalize
|
|
@@ -145,6 +152,15 @@ class SampleBuffer {
|
|
|
145
152
|
meta.columnU32[this._rowCount] = idx;
|
|
146
153
|
}
|
|
147
154
|
|
|
155
|
+
// BS-20: null is lane-neutral. setNull ensures the field exists but leaves its
|
|
156
|
+
// kind untouched, so an only-null field stays K_UNKNOWN (-> F64, reads 0),
|
|
157
|
+
// null+string infers U32, and null+number infers F64. The row's column slot is
|
|
158
|
+
// left at its default 0, which reads back as 0 on an F64 lane and the reserved
|
|
159
|
+
// "" on a U32 lane. No per-field flag: K_UNKNOWN already carries the only-null case.
|
|
160
|
+
setNull(name) {
|
|
161
|
+
this._ensureField(name);
|
|
162
|
+
}
|
|
163
|
+
|
|
148
164
|
_ensureField(name) {
|
|
149
165
|
let meta = this._fieldMeta.get(name);
|
|
150
166
|
if (!meta) {
|
|
@@ -218,6 +234,11 @@ export class Writer {
|
|
|
218
234
|
this._currentFieldIdx = -1;
|
|
219
235
|
this._recordStartByteOffset = 0;
|
|
220
236
|
this._absOffset = 0;
|
|
237
|
+
// BS-07: the live Tokenizer, handed over via setInputSource at construction.
|
|
238
|
+
// When present, the sample window measures INPUT bytes (byte-true) by reading
|
|
239
|
+
// _source.absOffset at record boundaries. null => hand-driven Writer: fall
|
|
240
|
+
// back to record-count sampling (Math.max(1, ...) below). null is not zero.
|
|
241
|
+
this._source = null;
|
|
221
242
|
|
|
222
243
|
this._finalized = false;
|
|
223
244
|
|
|
@@ -237,7 +258,9 @@ export class Writer {
|
|
|
237
258
|
const f = this._rowValueSlotsF64, u = this._rowValueSlotsU32;
|
|
238
259
|
for (let i = 0; i < f.length; i++) { f[i] = 0; u[i] = 0; }
|
|
239
260
|
}
|
|
240
|
-
|
|
261
|
+
// Record-boundary read (BS-07): byte-true when a source is wired, else the
|
|
262
|
+
// dead hand-driven offset (0) that yields record-count sampling. Not the byte scan.
|
|
263
|
+
this._recordStartByteOffset = this._source !== null ? this._source.absOffset : this._absOffset;
|
|
241
264
|
}
|
|
242
265
|
|
|
243
266
|
onEndObject() {
|
|
@@ -286,7 +309,28 @@ export class Writer {
|
|
|
286
309
|
|
|
287
310
|
onTrue() { this.onNumber(1); }
|
|
288
311
|
onFalse() { this.onNumber(0); }
|
|
289
|
-
|
|
312
|
+
|
|
313
|
+
// BS-20: null is lane-neutral. Mirrors onString's guard structure (depth /
|
|
314
|
+
// finalized doors, unknown-field idx<0 return) but dispatches per lane instead
|
|
315
|
+
// of throwing: F64 slot -> 0, U32 slot -> 0 = the reserved "" (index 0, real in
|
|
316
|
+
// every shard since BS-01). onTrue/onFalse stay delegating to onNumber, so a
|
|
317
|
+
// bool on a U32 lane still throws W_LANE_MISMATCH. Non-null values never reach
|
|
318
|
+
// here -- zero added cost on the numeric/string hot path.
|
|
319
|
+
onNull() {
|
|
320
|
+
if (this._recordDepth !== 1) {
|
|
321
|
+
if (this._finalized) throw new WriterError('W_FINALIZED', 'writer already finalized');
|
|
322
|
+
throw new WriterError('W_TOP_LEVEL_NOT_OBJECT', 'top-level null is not an object; LBK1 schema mode stores objects only (SPEC 5.1)');
|
|
323
|
+
}
|
|
324
|
+
if (this._schema) {
|
|
325
|
+
const idx = this._currentFieldIdx;
|
|
326
|
+
if (idx < 0) return;
|
|
327
|
+
if (this._fieldLaneKinds[idx] === LANE_F64) this._rowValueSlotsF64[idx] = 0;
|
|
328
|
+
else this._rowValueSlotsU32[idx] = 0;
|
|
329
|
+
} else {
|
|
330
|
+
if (!this._currentKeyName) return;
|
|
331
|
+
this._sample.setNull(this._currentKeyName);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
290
334
|
|
|
291
335
|
onString(bytes, from, to) {
|
|
292
336
|
if (this._recordDepth !== 1) {
|
|
@@ -469,7 +513,8 @@ export class Writer {
|
|
|
469
513
|
this._totalRows++;
|
|
470
514
|
if (this._currentShardRowCount >= this._currentShardMaxRows) this._finalizeCurrentShard();
|
|
471
515
|
} else {
|
|
472
|
-
const
|
|
516
|
+
const endOff = this._source !== null ? this._source.absOffset : this._absOffset;
|
|
517
|
+
const consumed = Math.max(1, endOff - this._recordStartByteOffset);
|
|
473
518
|
this._sample.commitRow(consumed);
|
|
474
519
|
if (this._sample.byteEstimate >= this._sampleBytes) {
|
|
475
520
|
this._freezeSchemaFromSample();
|
|
@@ -611,7 +656,16 @@ export class Writer {
|
|
|
611
656
|
return this._container;
|
|
612
657
|
}
|
|
613
658
|
|
|
614
|
-
|
|
659
|
+
// Optional capability hook (BS-07): the Tokenizer calls this at construction
|
|
660
|
+
// to hand over the live tokenizer. The Writer then reads src.absOffset at
|
|
661
|
+
// record boundaries so the sample window is byte-true. A CALL only; the
|
|
662
|
+
// Tokenizer is never mutated.
|
|
663
|
+
setInputSource(src) { this._source = src; }
|
|
664
|
+
|
|
665
|
+
// Deprecated (BS-07): superseded by setInputSource. The Writer now reads the
|
|
666
|
+
// live Tokenizer's absOffset at record boundaries, so this hand-driven offset
|
|
667
|
+
// dance is a documented no-op retained for source compatibility until 2.0.
|
|
668
|
+
setInputByteOffset() {}
|
|
615
669
|
|
|
616
670
|
// -------- container assembly --------
|
|
617
671
|
|
package/src/index.js
CHANGED
package/types/Tokenizer.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export class Tokenizer {
|
|
|
33
33
|
feed(chunk: Uint8Array): void;
|
|
34
34
|
/** Signal end of input. Any pending record is flushed to the sink. */
|
|
35
35
|
end(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Cumulative count of input bytes consumed across every `feed()` chunk;
|
|
38
|
+
* survives chunk boundaries. A sink reads this at record boundaries for
|
|
39
|
+
* byte-true `sampleBytes` (BS-07); it is never read on the byte scan.
|
|
40
|
+
*/
|
|
41
|
+
readonly absOffset: number;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
export class TokenizerError extends Error {
|
package/types/Writer.d.ts
CHANGED
|
@@ -22,8 +22,12 @@ export interface WriterOptions {
|
|
|
22
22
|
/** Target output bytes per shard. Default 32 MB. */
|
|
23
23
|
targetShardBytes?: number;
|
|
24
24
|
/**
|
|
25
|
-
* Bytes of
|
|
26
|
-
*
|
|
25
|
+
* Bytes of INPUT to observe before freezing an inferred schema. Only used in
|
|
26
|
+
* sample-and-infer mode (no explicit schema). Byte-true (BS-07): the window
|
|
27
|
+
* ends at the first record boundary at or after `sampleBytes` input bytes,
|
|
28
|
+
* read from the Tokenizer's `absOffset` -- independent of chunking and record
|
|
29
|
+
* count. A hand-driven Writer with no wired source samples by record count.
|
|
30
|
+
* Default = targetShardBytes.
|
|
27
31
|
*/
|
|
28
32
|
sampleBytes?: number;
|
|
29
33
|
}
|
|
@@ -63,7 +67,18 @@ export class Writer implements TokenizerSinkForWriter {
|
|
|
63
67
|
onFalse(): void;
|
|
64
68
|
onNull(): void;
|
|
65
69
|
onEnd(): void;
|
|
66
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Optional capability hook (BS-07): the Tokenizer calls this at construction
|
|
72
|
+
* to hand over the live tokenizer. The Writer then reads `src.absOffset` at
|
|
73
|
+
* record boundaries so the sample window is byte-true. A call only; the
|
|
74
|
+
* Tokenizer is never mutated.
|
|
75
|
+
*/
|
|
76
|
+
setInputSource(src: { readonly absOffset: number }): void;
|
|
77
|
+
/**
|
|
78
|
+
* @deprecated Superseded by `setInputSource` (BS-07). The Writer now reads the
|
|
79
|
+
* live Tokenizer's `absOffset` at record boundaries, so this is a no-op
|
|
80
|
+
* retained for source compatibility until 2.0.
|
|
81
|
+
*/
|
|
67
82
|
setInputByteOffset(n: number): void;
|
|
68
83
|
/**
|
|
69
84
|
* Seal the container and return the LBK1 binary. Call once, after the
|