@zakkster/lite-stream 1.1.1 → 1.2.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
@@ -7,6 +7,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ---
9
9
 
10
+ ## 1.2.0 -- 2026-09-02
11
+
12
+ **The law's gate, able to fail.** A torture harness that proves the package's
13
+ own claims -- including the zero-steady-state-allocation sentence that has been
14
+ unverified since 1.0.0 -- plus the drift guards that keep docs, exports, and
15
+ ASCII honest, a `VERSION` export, and the LS-11 packaging cleanup. No behavior
16
+ change: the only `Stream.js` code change is the additive `VERSION` const.
17
+
18
+ ### Added
19
+
20
+ - **Torture harness** -- `test/torture.mjs` + `test/torture/s0-s8`, run by
21
+ `npm run torture` (`node --expose-gc test/torture.mjs`): metamorphic laws
22
+ (s0), degenerate values and hostile iterators (s1), async-iteration protocol
23
+ conformance (s2), API abuse with a decided policy per case (s3), seeded
24
+ fuzz-vs-oracle (s4), the allocation gate (s5, `@zakkster/lite-gc-profiler`,
25
+ `maxMajor` 0 with per-scenario byte budgets), a retention soak (s6,
26
+ `@zakkster/lite-leak` WeakRef census), the StreamQuery conformance corpus and
27
+ TimeoutError duck-pact (s7), and the controls tier (s8). Prints `ok`, exits
28
+ 0/1; `TORTURE_SEED` replays a failure; `STREAM_TORTURE_BREAK=<s0..s8|1>` arms
29
+ each tier's injected breakage so every tier's control exits non-zero.
30
+ - **Scripts** -- `torture`, `torture:control` (all-tiers break lane), and
31
+ `test:floor`. `verify` gains `torture`. **LS-09 amendment:** the floor lane
32
+ runs `npm test` ONLY against the pinned peer -- `@zakkster/lite-leak` peers
33
+ `@zakkster/lite-signal >= 1.5.0`, so pinning `1.2.2` alongside it would put
34
+ lite-leak outside its supported peer range; torture (which imports lite-leak)
35
+ runs against the resolved latest only. Fail closed: never run a tool outside
36
+ its peer range.
37
+ - **`VERSION` export** (LS-05) -- a string that always equals the installed
38
+ `package.json` version. Declared in `Stream.d.ts`; documented in llms.txt and
39
+ the README API reference with no embedded version literal. Never re-exported
40
+ by consumers (suite ruling). The `Stream.d.ts` `TimeoutError` type declaration
41
+ was also added (the shipped runtime export had no type; the surface-guard
42
+ requires three-way parity).
43
+ - **`test/09-guards.test.mjs`** (5 tests) -- ascii-guard (every `files[]` +
44
+ `test/`/`bench/`/`demo/` byte is printable ASCII or LF, empty allowlist) and
45
+ surface-guard (runtime exports == llms.txt import list == `Stream.d.ts` value
46
+ exports; `VERSION` string-equals `package.json` version; the declared peer is
47
+ present in both docs). Each family carries an inline failing control.
48
+ - **devDependencies** -- `@zakkster/lite-leak ^1.10.0` and
49
+ `@zakkster/lite-gc-profiler ^1.16.0` (torture only; not runtime deps).
50
+
51
+ ### Changed
52
+
53
+ - **`files[]` drops `ROADMAP.md`** (LS-11) -- the roadmap is a working doc, not
54
+ a shipped artifact. llms.txt's Files section and the CHANGELOG [1.1.0]
55
+ forward-reference were reworded so tarball readers are not pointed at an
56
+ unshipped file.
57
+ - **`test` / `test:gc` globs scoped to `test/*.test.mjs`** so the `node:test`
58
+ runner does not discover the torture files.
59
+
60
+ ### Fixed
61
+
62
+ - Docs and packaging only; no runtime behavior changed.
63
+
64
+ ### Known issues
65
+
66
+ - **LS-13** (S1, opened this release, fix targeted for 1.3.0) -- a `next()`
67
+ result object whose `done` or `value` is a throwing getter escapes the pump's
68
+ `.then` onFulfilled uncaught: it surfaces as an unhandledRejection and
69
+ `fromAsyncIterable` never reaches a terminal state (it should transition to
70
+ the error state). Torture tier s1 reproduces it with a bounded sentinel and a
71
+ scoped rejection capture and prints it as `TODO(LS3, LS-13)` in the gate
72
+ output; it does not fail the gate. This refutes the "iterator throws -> error
73
+ state" triplet leg for the throwing-getter shape specifically.
74
+
75
+ ### Testing
76
+
77
+ - **90 tests** across `test/01-*` through `test/09-*` (up from 85), 0 fail,
78
+ 0 skip under `npm test` and `npm run test:gc`.
79
+ - `npm run torture` -> `ok`, ~1.8s wall (node v26.3.1, macOS arm64,
80
+ 2026-09-02); zero steady-state majors in every s5 scenario.
81
+ - `npm run test:floor` -> FLOOR-PASS (`npm test` 90/0 against
82
+ `@zakkster/lite-signal@1.2.2`) and LATEST-PASS (`npm test` 90/0 + torture ok
83
+ against the resolved `@zakkster/lite-signal@1.5.0`). The `^1.2.0` peer floor
84
+ is now a tested fact, not a claim.
85
+ - s5 budget doctrine: per-scenario `maxBytesPerOp` budgets (32 B/op for the
86
+ caught-up toAsyncIterable and pipeToSignal steady pump; 16 B/op for the
87
+ by-design fromAsyncIterable state-object and waiter-heavy paths), each ~3x the
88
+ measured 3-pass peak; the global `maxMajor` rule never widens.
89
+
90
+ ---
91
+
10
92
  ## 1.1.1 -- 2026-09-01
11
93
 
12
94
  **Truth patch. No API change, no behavior change; the `Stream.js` diff is
@@ -65,9 +147,9 @@ corrected, and the previously dark GC tier now gates on the default test run.
65
147
  ## 1.1.0 -- 2026-07-10
66
148
 
67
149
  **Additive `toAsyncIterable` enrichment.** Six additions, no breaking
68
- changes. All 1.0.0 callers continue to work without modification. See
69
- ROADMAP.md's "Shipped in 1.1.0" section for the triggering signals that
70
- justified each addition.
150
+ changes. All 1.0.0 callers continue to work without modification. Each
151
+ addition below records the triggering signal that justified it; the
152
+ `Added` and `Changed` entries in this section stand on their own.
71
153
 
72
154
  ### Added
73
155
 
package/README.md CHANGED
@@ -249,6 +249,16 @@ Treat this as the secondary API for classic "consume signal changes"
249
249
  patterns; for the streaming pipeline case (paginated APIs, SSE, pubsub)
250
250
  the forward direction is what most consumers want.
251
251
 
252
+ ### VERSION
253
+
254
+ ```ts
255
+ VERSION: string
256
+ ```
257
+
258
+ A string that always equals the installed `package.json` version. Consumers
259
+ do NOT re-export it (suite ruling); the surface-guard asserts the equality on
260
+ every test run.
261
+
252
262
  ## Modes: latest vs buffer
253
263
 
254
264
  Pick `"latest"` when you only care about the most recent value:
@@ -486,7 +496,7 @@ target.
486
496
 
487
497
  ### Tier 1 -- behavior (unit tests, fast)
488
498
 
489
- 85 tests across `test/01-*` through `test/08-*`:
499
+ 90 tests across `test/01-*` through `test/09-*`:
490
500
 
491
501
  - `01-from-async-iterable-latest.test.mjs` -- state shape, lifecycle,
492
502
  Iterable acceptance variants, pre-aborted, subscriber observability
@@ -506,6 +516,11 @@ target.
506
516
  (LS-01: disposing a result/target signal does NOT stop the pump;
507
517
  LS-04: a pending `next()` on a disposed source never settles, `timeout`
508
518
  is the only escape hatch)
519
+ - `09-guards.test.mjs` -- drift guards: ascii-guard (every shipped +
520
+ test/bench/demo byte is printable ASCII or LF) and surface-guard (runtime
521
+ exports == llms.txt == Stream.d.ts, VERSION == package.json version, the
522
+ declared peer is present in both docs). Each family carries an inline
523
+ failing control proving the guard can fail.
509
524
 
510
525
  Run via `npm test`.
511
526
 
@@ -525,10 +540,23 @@ retained as an alias.
525
540
  `bench/bench.mjs` -- six scenarios; throughput and B/op retained. Run via
526
541
  `npm run bench`.
527
542
 
543
+ ### Tier 4 -- torture (retention + alloc gate + controls)
544
+
545
+ `npm run torture` (`node --expose-gc test/torture.mjs`) runs tiers s0-s8
546
+ sequentially -- metamorphic laws, degenerate values, protocol conformance,
547
+ API abuse, seeded fuzz-vs-oracle, the s5 per-scenario allocation gate
548
+ (maxMajor 0), a lite-leak retention soak, the StreamQuery conformance corpus,
549
+ and the controls tier. It prints `ok` and exits 0 on success.
550
+ `STREAM_TORTURE_BREAK=<s0..s8|1>` arms one tier's (or every tier's) injected
551
+ breakage so the control lane exits non-zero; `npm run torture:control` runs
552
+ the all-tiers form. `npm run test:floor` runs `npm test` against the peer
553
+ floor `@zakkster/lite-signal@1.2.2` and then `npm test` + torture against the
554
+ resolved latest, printing both verdicts.
555
+
528
556
  No `package-lock.json` is committed: devDependencies float within their
529
557
  stated ranges, and correctness against the peer floor is proven by running
530
- the suite against `@zakkster/lite-signal@1.2.2` rather than by pinning a
531
- lockfile.
558
+ `npm run test:floor` against `@zakkster/lite-signal@1.2.2` rather than by
559
+ pinning a lockfile.
532
560
 
533
561
  ## What this is not
534
562
 
package/Stream.d.ts CHANGED
@@ -135,3 +135,16 @@ export function toAsyncIterable<T>(
135
135
  sig: Signal<T> | Computed<T>,
136
136
  opts?: ToAsyncIterableOptions
137
137
  ): AsyncIterable<T> & { readonly droppedCount: number };
138
+
139
+ /**
140
+ * Thrown when the `timeout` option on `toAsyncIterable` elapses. Structurally
141
+ * identical to `@zakkster/lite-await`'s TimeoutError (`e.name === "TimeoutError"`
142
+ * duck-checks work across both packages); not imported -- lite-stream stays zero-dep.
143
+ */
144
+ export declare class TimeoutError extends Error {
145
+ readonly name: "TimeoutError";
146
+ readonly timeout: number;
147
+ }
148
+
149
+ /** Package version string; always equals the installed package.json version. */
150
+ export declare const VERSION: string;
package/Stream.js CHANGED
@@ -1,4 +1,4 @@
1
- // @zakkster/lite-stream 1.1.1
1
+ // @zakkster/lite-stream 1.2.0
2
2
  //
3
3
  // Zero-GC bridge between async iterators and @zakkster/lite-signal. The
4
4
  // multi-shot dual of lite-await's fromPromise: project an async source of N
@@ -873,3 +873,7 @@ export {
873
873
  toAsyncIterable,
874
874
  TimeoutError
875
875
  };
876
+
877
+ // Always equals the installed package.json version (single source of truth is
878
+ // package.json; the /release drill bumps both sites in the same commit).
879
+ export const VERSION = "1.2.0";
package/llms.txt CHANGED
@@ -14,7 +14,7 @@ Peer dep: `@zakkster/lite-signal ^1.2.0`.
14
14
 
15
15
  ## Imports
16
16
 
17
- import { fromAsyncIterable, pipeToSignal, toAsyncIterable, TimeoutError } from "@zakkster/lite-stream";
17
+ import { fromAsyncIterable, pipeToSignal, toAsyncIterable, TimeoutError, VERSION } from "@zakkster/lite-stream";
18
18
 
19
19
  ## Exports
20
20
 
@@ -120,6 +120,12 @@ Structurally identical to `@zakkster/lite-await`'s TimeoutError so
120
120
  `e.name === "TimeoutError"` duck-checks work across both packages. Not
121
121
  imported from lite-await -- lite-stream stays zero-dep.
122
122
 
123
+ ### `VERSION`
124
+
125
+ String, always equals the installed package.json version. Not re-exported
126
+ by consumers (suite ruling); the surface-guard asserts the equality on
127
+ every test run.
128
+
123
129
  ## Cleanup termination triplet
124
130
 
125
131
  `fromAsyncIterable` ends on exactly three paths, all structural:
@@ -278,7 +284,7 @@ Per-yield, `fromAsyncIterable` latest mode allocates one wrapper state
278
284
  object; buffer mode adds one snapshot array. `toAsyncIterable` `mode:
279
285
  "latest"` allocates zero on the steady-state hot path when the consumer
280
286
  is caught up; `mode: "buffer"` also zero when no waiter is pending
281
- (structural claim; gated by the alloc tier in 1.2.0).
287
+ (gated: test/torture.mjs s5, per-scenario budgets).
282
288
 
283
289
  ## Files
284
290
 
@@ -287,7 +293,6 @@ is caught up; `mode: "buffer"` also zero when no waiter is pending
287
293
  - `README.md` -- full docs with integration recipes
288
294
  - `llms.txt` -- this file
289
295
  - `CHANGELOG.md` -- version history
290
- - `ROADMAP.md` -- deferred features
291
296
 
292
297
  ## Author
293
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zakkster/lite-stream",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Zero-GC bridge between async iterators and @zakkster/lite-signal. Project async streams (paginated APIs, SSE, network frames, pubsub topics) into signals. Bounded buffering with explicit overflow diagnostics, structural cleanup on three termination paths (iterator done, iterator throws, AbortSignal abort). The multi-shot dual of lite-await's fromPromise.",
5
5
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
6
6
  "license": "MIT",
@@ -22,14 +22,16 @@
22
22
  "README.md",
23
23
  "llms.txt",
24
24
  "LICENSE.txt",
25
- "CHANGELOG.md",
26
- "ROADMAP.md"
25
+ "CHANGELOG.md"
27
26
  ],
28
27
  "scripts": {
29
- "test": "node --expose-gc --test --test-reporter=spec",
30
- "test:gc": "node --expose-gc --test --test-reporter=spec",
28
+ "test": "node --expose-gc --test --test-reporter=spec test/*.test.mjs",
29
+ "test:gc": "node --expose-gc --test --test-reporter=spec test/*.test.mjs",
31
30
  "bench": "node --expose-gc bench/bench.mjs",
32
- "verify": "npm test && npm run test:gc && npm run bench"
31
+ "torture": "node --expose-gc test/torture.mjs",
32
+ "torture:control": "for t in s0 s1 s2 s3 s4 s5 s6 s7 s8; do if STREAM_TORTURE_BREAK=$t node --expose-gc test/torture.mjs >/dev/null 2>&1; then echo \"control $t FAILED-TO-FAIL\"; exit 1; else echo \"control $t failed-as-expected\"; fi; done",
33
+ "test:floor": "echo '== floor lane: pin @zakkster/lite-signal@1.2.2 (torture NOT run here -- lite-leak peers lite-signal>=1.5.0, LS-09 amendment) ==' && npm i --no-save --no-package-lock @zakkster/lite-signal@1.2.2 --legacy-peer-deps && npm test && echo 'FLOOR-PASS: npm test green @ lite-signal@1.2.2' && echo '== latest lane: restore @zakkster/lite-signal@^1.2.0 ==' && npm i --no-save --no-package-lock @zakkster/lite-signal@^1.2.0 && npm test && npm run torture && echo 'LATEST-PASS: npm test + torture green @ lite-signal latest'",
34
+ "verify": "npm test && npm run test:gc && npm run bench && npm run torture"
33
35
  },
34
36
  "keywords": [
35
37
  "stream",
@@ -66,6 +68,8 @@
66
68
  "@zakkster/lite-signal": "^1.2.0"
67
69
  },
68
70
  "devDependencies": {
71
+ "@zakkster/lite-gc-profiler": "^1.16.0",
72
+ "@zakkster/lite-leak": "^1.10.0",
69
73
  "@zakkster/lite-signal": "^1.2.2"
70
74
  }
71
75
  }
package/ROADMAP.md DELETED
@@ -1,711 +0,0 @@
1
- # lite-stream -- enriched roadmap (post-1.1.0)
2
-
3
- Blueprint: `../BLUEPRINT_ROADMAP.md`. Sibling roadmaps this one is wired to:
4
- `../LiteQuery/ROADMAP.md` (the flagship consumer), `../LiteBakeStream/ROADMAP.md`
5
- (the torture-spec model), `../LiteAwait/AWAIT_ROADMAP.md` (the TimeoutError
6
- duck-pact). Ethos carried over unchanged: **reproduce by running, never infer.**
7
- Every finding below was produced by executing code on 2026-09-01, not by
8
- reading it. Reproductions live in `bench/findings-probes-2026-09-01.mjs`.
9
-
10
- Sessions: LS1 (v1.1.1, truth patch) -> LS2 (v1.2.0, the law's gate) ->
11
- LS3 (v1.3.0, the consumer contract) -> LS4 (v1.4.0, Q8 enablement, GATED).
12
-
13
- ---
14
-
15
- ## 0. Ground truth (verified 2026-09-01)
16
-
17
- - Registry: `@zakkster/lite-stream` 1.0.0 and 1.1.0 published; `latest` =
18
- 1.1.0. Local HEAD `e9d2151 1.1.0;` IS the published state; tree clean.
19
- - Surface: 4 named exports -- `fromAsyncIterable`, `pipeToSignal`,
20
- `toAsyncIterable`, `TimeoutError`. NO `VERSION` export (LS-05). `Stream.d.ts`
21
- and `llms.txt` agree with the export block (hand-checked; guard pending LS2).
22
- - Tests: 79. Default `npm test` -> 71 pass, **8 skipped silently** (the entire
23
- GC tier, LS-02). `npm run test:gc` -> 79 pass, 0 skip, 0 fail -- against
24
- lite-signal **1.5.0** AND against the floor **1.2.2** (both run today; the
25
- `^1.2.0` peer claim is TRUE, previously untested -- LS-09).
26
- - Torture: none. No `test/torture.mjs`, no `@zakkster/lite-leak`, no
27
- `@zakkster/lite-gc-profiler`, no seeded fuzz, no controls. The 8 GC tests
28
- use coarse "retain < 2 MB" ceilings, not the law's `maxMajor: 0` gate (LS-03).
29
- - Bench (node v26.3.1, this machine, 2026-09-01): from-latest 61,464 ops/s
30
- (0.35 B/op retained), from-buffer 63,382 (0.46), abort-cycle 29,652 (1.49),
31
- pipe-to-signal 42,382 (0.26), to-async-iterable 200,956 (negative retained),
32
- overflow 204,774 (0.13). Shipped llms.txt claims different numbers with no
33
- provenance on the 1.0 block (LS-08).
34
- - Consumers in the suite: exactly one. `../LiteQuery/StreamQuery.js:36`
35
- imports `pipeToSignal` (peer `^1.0.0`, optional). lite-query also vendors
36
- a 1.0.0 copy for demos (`demo/vendor/lite-stream.js`).
37
- - ASCII: **zero** non-ASCII bytes across Stream.js, Stream.d.ts, all docs,
38
- test/, bench/, demo/. Already clean; LS2's guard keeps it that way.
39
- - Hygiene: no `.gitignore` (added at roadmap-pass time: `node_modules/`,
40
- `package-lock.json` -- same operator ruling as lite-query Q1), no lockfile
41
- (deliberate, now recorded). `files[]` ships ROADMAP.md (LS-11).
42
- - Runtime facts: engines `>=18`; `Symbol.asyncDispose` feature-detected
43
- (present on node 26, its conditional test runs). devDep `^1.2.2` resolves
44
- to lite-signal 1.5.0 at install time.
45
-
46
- ## 1. Shared law (holds in every session)
47
-
48
- 1. Zero runtime deps; `@zakkster/lite-signal` is the single peer. node:test
49
- only. ASCII-only source and docs. Single main file `Stream.js`.
50
- 2. Bounded buffering is this package's own law: "unbounded buffering is a
51
- memory bug pretending to be a feature." Every buffer has a required,
52
- validated ceiling; every drop is counted and observable.
53
- 3. Fail closed on every unverified state. The LS-01 lesson is the local
54
- phrasing: a cleanup path that depends on a peer behavior nobody ever
55
- executed is not a cleanup path, it is a comment.
56
- 4. **AbortSignal is the only cleanup contract.** Dispose-based teardown is
57
- not promised anywhere until a lite-signal disposed-oracle exists (parked).
58
- Docs, tests, and torture pin this in every direction.
59
- 5. No silent skips (the Q-02/AW-14/LS-02 class): the default `npm test` runs
60
- every test the repo owns. Environment-conditional skips (asyncDispose on
61
- node < 20) must name their condition in the skip message and have a
62
- counterpart that runs.
63
- 6. Every module change is proven by `node --expose-gc test/torture.mjs`
64
- (lite-leak + lite-gc-profiler) once LS2 lands. No gate output is a FAIL.
65
- A gate that cannot fail is decorative -- controls are part of the gate.
66
- 7. Docs parity is release-blocking: exports == llms.txt == Stream.d.ts ==
67
- README API section; peer floors in docs == package.json; perf numbers
68
- carry provenance (node version, date, machine class) or do not ship.
69
- 8. Measured numbers are measured once, at the source of truth, and cited
70
- everywhere else (the bake-stream MQ2 discipline).
71
-
72
- ## 2. Verified findings
73
-
74
- Severity: S1 = a shipped claim is false / correctness; S2 = a gate or
75
- guarantee is dark; S3 = drift, hygiene, or docs debt. Every row was
76
- reproduced by running on 2026-09-01; probe bodies live in
77
- `bench/findings-probes-2026-09-01.mjs` (P0..P4).
78
-
79
- | ID | Sev | Finding | Reproduce |
80
- | --- | --- | --- | --- |
81
- | **LS-01** | S1 | **The shipped dispose-teardown claim is false, and it has never been true.** llms.txt (lines 132-134): "If the consumer disposes the result signal ... the pump detects on next `sig.set()` and tears down silently." Reality: lite-signal's `set()` after `dispose()` is a **silent no-op** in 1.5.0 AND 1.2.2 (P0), so the pump's catch-based detection (`Stream.js:343`) is dead code born dead -- it never fired against any published peer in the range. Observed: dispose mid-pump leaves the iterator **pulling forever** (P1: 14 pulls at dispose, 64 and climbing 60ms later, `return()` never called); same for `pipeToSignal` with a disposed target (P2, `onError` never fires). For an infinite source (SSE -- the package's own headline use case) this is an unbounded background fetch loop. The `Stream.js:148-152` docstring even offers dispose as a stop mechanism ("the iterator continues until natural completion" -- an infinite source has none). Sole suite consumer verified SAFE: StreamQuery always `ac.abort()`s via `streamStop` before entry teardown (`StreamQuery.js:156`). | `node bench/findings-probes-2026-09-01.mjs` (P0/P1/P2) |
82
- | **LS-02** | S2 | **Default `npm test` silently skips the entire GC tier.** All 8 tests in `test/06-gc.test.mjs` are `{ skip: !hasGc }` and the `test` script lacks `--expose-gc`; 71/79 green LOOKS complete. The `verify` script runs `test:gc`, but the habitual gate is `npm test`. Third suite recurrence of the green-light-over-a-hole class (Q-02, AW-14). Fix pre-verified: the `test:gc` form runs 79/79 green in 21.5s. | `npm test 2>&1 \| grep skipped` -> 8; `npm run test:gc` -> 0 |
83
- | **LS-03** | S2 | **The pipeline law's torture gate does not exist.** No `test/torture.mjs`, no lite-leak, no lite-gc-profiler, no seeded fuzz, no zero-alloc gate (llms.txt claims "allocates zero on the steady-state hot path" -- nothing gates it), no controls. The GC tests' "retain < 2 MB" ceilings would pass a 1.9 MB leak. | `ls test/torture.mjs` -> ENOENT; `grep -c lite-leak package.json` -> 0 |
84
- | **LS-04** | S3 | **Disposing the SOURCE under `toAsyncIterable` hangs the pending `next()` forever** (P3: still PENDING 50ms after dispose; no resolve, no reject). llms.txt already anti-patterns this as "undefined behavior", so it is documented-away rather than false -- but the observed behavior is now on record, and `timeout` is the only escape hatch. Hardening parked on a lite-signal disposed-oracle (see parking ledger). | probes P3 |
85
- | **LS-05** | S3 | **No `VERSION` const or export.** Suite convention (lite-query, lite-await) and the /release drill's site list expect one in the main file. Current version sites: `package.json`, `Stream.js:1` header comment, CHANGELOG headings. Additive export -> lands with LS2 (1.2.0), never re-exported by consumers (suite ruling, lite-await pass). | `grep -c VERSION Stream.js` -> 0 |
86
- | **LS-06** | S3 | **Shipped source references a document that does not exist.** `Stream.js:25` and `:560` cite `CONTRACT-toAsyncIterable-1.1.md`; no such file in the repo or the tarball. The TimeoutError comment (`Stream.js:44-45`) still narrates its own dev sessions ("Session B's territory ... Session A"). | `ls CONTRACT-toAsyncIterable-1.1.md` -> ENOENT |
87
- | **LS-07** | S3 | **The pump comment describes code that is not there.** `Stream.js:352-353`: "Defer the next pull through the microtask queue so back-to-back synchronous yields don't blow the stack" -- `pump()` is called directly; no deferral exists at that site. Behavior is stack-safe anyway (`.then` handlers unwind): P4 drains a 1,000,000-value sync source in ~80ms with no overflow. Fix is the comment, plus a torture case pinning the law. | probes P4 |
88
- | **LS-08** | S3 | **Shipped perf numbers lack provenance and do not match a current run.** llms.txt's 1.0 block ("178K ops/s ... 235K ... 84K") names no node version, date, or machine; today's run on node v26.3.1 gives 61K/42K/29K for the same scenarios. Not a lie -- a rotting number. Re-measure once with provenance at LS1, cite everywhere (README Benchmarks included). | `npm run bench` vs llms.txt Performance section |
89
- | **LS-09** | S3 | **The peer floor `^1.2.0` was a claim, not a fact -- now verified, must stay verified.** Tests had only ever run against the latest resolution (1.5.0). Run today against pinned 1.2.2: 79/79 green. The floor holds; nothing keeps it holding. LS2's floor matrix (scripted `test:floor`) institutionalizes the check. | `npm i --no-save --no-package-lock @zakkster/lite-signal@1.2.2 && npm run test:gc` |
90
- | **LS-10** | S3 | **No repo hygiene layer.** No `.gitignore` -- a plain `npm install` leaves `node_modules/` (and would leave `package-lock.json`) as untracked noise poised to be committed; no recorded lockfile policy. Mitigated at roadmap-pass time (2-line `.gitignore`, same ruling as lite-query Q1); policy recorded here: no lockfile, devDeps float within their stated ranges, the floor matrix pins what must stay true. | `git status` after `npm install` (pre-fix) |
91
- | **LS-11** | S3 | **ROADMAP.md ships in the tarball, and two shipped docs point into its old shape.** `files[]` includes ROADMAP.md; llms.txt's Files section describes it as "deferred features"; CHANGELOG 1.1.0 cites "ROADMAP.md's 'Shipped in 1.1.0' section" (preserved below under History so the published 1.1.0 pointer stays honest). Ruling to execute at LS2: drop ROADMAP.md from `files[]` (align with lite-query/bake-stream -- the roadmap is a working doc), fix the llms.txt Files line and CHANGELOG forward-refs in the same release. | `npm pack --dry-run \| grep ROADMAP` |
92
- | **LS-12** | S3 | **The two directions disagree about what abort means, and the flagship consumer pays for it.** `fromAsyncIterable`/`pipeToSignal` route abort through `onError` (error framing, `makeAbortError`); `toAsyncIterable` treats abort as graceful done. StreamQuery hand-filters the noise: "pipeToSignal funnels intentional aborts through onError too. Those are not failures" (`StreamQuery.js:139-142`). Each direction is documented locally; the asymmetry as a whole is undocumented and exports a workaround obligation to every consumer. LS3 owns the decision doc (pin the rationale or add an `onAbort` hook -- see the brief). | read `StreamQuery.js:139-142`; compare `Stream.js:261` vs `:815` |
93
-
94
- ### 2b. Cross-package facts (verified against the neighbors' code, not their docs)
95
-
96
- - **lite-signal 1.5.0 has no public disposed-oracle.** Exports: signal,
97
- computed, signalBox, computedBox, effect, dispose, batch, createRegistry,
98
- setDefaultRegistry, CapacityError, getOwner, runWithOwner. Nothing answers
99
- "is this signal disposed?" -- so LS-01's behavioral fix (pump detects
100
- dispose) is IMPOSSIBLE in-library today. Tripwire: if lite-signal ships an
101
- oracle, un-park "dispose-aware pumps" (parking ledger).
102
- - **StreamQuery's exact usage shape** (the contract LS2's conformance tier
103
- freezes): `pipeToSignal(source, entry.data, { signal, transform, onError,
104
- onDone })` where `transform` doubles as a first-value status tap and a
105
- hand-rolled buffer window (`ring.push/shift/slice`, `StreamQuery.js:119-137`),
106
- and `onError` filters `signal.aborted`. Three shipped triggering signals for
107
- LS3, cited by line.
108
- - **TimeoutError duck-pact.** Three packages now carry the shape
109
- (`e.name === "TimeoutError"`, `.timeout` field): lite-await (origin),
110
- lite-stream (structural copy, deliberate zero-dep), lite-query's /await
111
- entry (re-export of lite-await's). The duck-check IS suite vocabulary;
112
- renaming it anywhere is a breaking change everywhere. Pinned by a shape
113
- test in LS2's conformance tier.
114
- - **lite-bake-stream is bytes-domain** (`ReadableStream<Uint8Array>` in,
115
- LBK1 containers out) -- no structural integration with value-domain
116
- lite-stream. One honest recipe candidate, docs-only: RangeReader shard
117
- walk -> async generator -> `fromAsyncIterable` for a UI-reactive lazy
118
- view (parking ledger; needs bake-stream >= MQ1 for abortability).
119
- - **lite-query Q8 (2.0.0, shared streams)** is the design event LS4 waits
120
- for: leader owns one iterator, followers receive frames over lite-channel
121
- and need "push externally-delivered values into entry signals with the
122
- same windowed-view discipline", plus "leader hung (frames stop, channel
123
- alive)" stall detection. Whether those primitives land in lite-stream 1.4.0
124
- or inside lite-query is Q8's session-start spike ruling. Their spike is
125
- our tripwire; recorded on both sides.
126
-
127
- ## 3. The torture suite (`test/torture.mjs`) -- spec
128
-
129
- Model: `../LiteBakeStream/ROADMAP.md` section 3 (itself ported from
130
- `../LiteBvh/test/torture/`), tiers renamed to this package's threat model:
131
- lifecycle leaks, protocol violations, scheduling races, and false zero-GC
132
- claims. Gate: `node --expose-gc test/torture.mjs` prints exactly "ok",
133
- exit 0/1. Control switch: `STREAM_TORTURE_BREAK=1` must exit non-zero.
134
-
135
- ```
136
- test/
137
- torture.mjs # entry: tiers in order, prints "ok", exit 0/1
138
- torture/
139
- harness.mjs # xorshift32 PRNG (TORTURE_SEED replay), check() with
140
- # message thunks, runOpsGate (stabilize:'deep'),
141
- # STREAM_TORTURE_BREAK switch, preflight: exit 2 with
142
- # a clear message when devDeps are missing
143
- s0-laws.mjs # metamorphic invariants
144
- s1-degenerate.mjs # nasty values and nasty iterators
145
- s2-protocol.mjs # async-iteration protocol conformance
146
- s3-abuse.mjs # API misuse: every case has a decided policy
147
- s4-fuzz.mjs # seeded schedule fuzz vs a naive oracle
148
- s5-alloc.mjs # the zero/bounded-alloc gate (gc-profiler)
149
- s6-soak.mjs # lite-leak retention + lifecycle churn
150
- s7-conformance.mjs # consumer corpus + floor matrix + duck-pact
151
- s8-controls.mjs # every tier, deliberately broken, exits non-zero
152
- ```
153
-
154
- Harness rules (verbatim from the blueprint; all apply):
155
- - All scratch allocated once, outside every loop; assertion messages built
156
- only on failure.
157
- - Seeded xorshift32; any failure prints seed + op index, replayable via
158
- `TORTURE_SEED=... node --expose-gc test/torture.mjs`.
159
- - lite-gc-profiler is one-measurement-at-a-time; tiers run sequentially;
160
- read `../lite-gc-profiler/llms.txt` before wiring -- rule names from
161
- memory are how gates rot. Unknown rule keys throw as of 1.10.0; floor the
162
- devDep there.
163
- - lite-leak held-value contract: neither `cleanup` nor `tag` closes over the
164
- tracked target. Never resolve an unexpected `inconclusive` with
165
- `allowInconclusive`.
166
- - `test/` never enters `files[]`; `npm pack --dry-run` proves it.
167
-
168
- ### s0 -- metamorphic laws
169
-
170
- For any seeded op sequence: `state.count` equals values yielded (both modes);
171
- buffer `values.length <= maxBuffer` always; `droppedCount` equals exactly
172
- `max(0, yields - maxBuffer)` when no consumer drains; latest-mode terminal
173
- `value` equals the last yield; toAsyncIterable delivery order equals source
174
- order (filtered) in buffer mode; **timing invariance**: the same logical
175
- value sequence delivered via sync iterables, microtask-resolved promises,
176
- and macrotask-resolved promises produces the same terminal state (count,
177
- last value/values, done, error class) -- the scheduling analog of
178
- bake-stream's re-chunk invariance. Terminal states are frozen: after
179
- done/error, no further mutation ever (already unit-pinned; fuzz-scale it).
180
-
181
- ### s1 -- degenerate values and hostile iterators
182
-
183
- Values: `undefined` yields (state.count is the oracle for "did a value
184
- arrive", value alone is ambiguous -- pin it), null, NaN, -0, frozen objects,
185
- functions, symbols. Sources: 1M-value sync iterable (stack law, P4 baseline
186
- ~80ms), empty iterable (immediate done), iterator with `.next` but no
187
- `Symbol.asyncIterator`, thenable-returning next(), next() returning
188
- non-object (pinned TypeError), result objects with throwing getters
189
- (`{get done() {throw}}`), `return()` that throws (swallowed -- pin),
190
- `return()` that never settles, an iterator that resolves a pull AFTER
191
- stop/abort landed (the `if (stopped)` guards, fuzz-reachable). Aborts:
192
- pre-aborted (pinned), abort inside the first `queueMicrotask` window before
193
- pull one, abort reentrantly from inside `onError`/`onDone`, abort with a
194
- custom `reason` (pinned), double abort.
195
-
196
- ### s2 -- protocol conformance (toAsyncIterable)
197
-
198
- next() after done; `return(value)` passthrough vs pending waiters resolving
199
- `{value: undefined, done: true}` (the documented split); `throw()` rejecting
200
- both waiters and its own call site; multi-waiter FIFO under fuzz (N waiters
201
- x M values, orders compared to a queue oracle); `Promise.all` batch pulls;
202
- `for await` + break; `await using` on node >= 20; the emitInitial x filter x
203
- initial-value matrix (8 cells, each named); filter throwing on the initial
204
- synchronous fire (the late-binding unsubscribe path, `Stream.js:656-663`).
205
-
206
- ### s3 -- API abuse (throw / documented no-op / documented undefined -- pick one per case)
207
-
208
- Mode typos ("Latest", "newest", 5); maxBuffer 0 / -1 / 1.5 / Infinity / NaN /
209
- "5"; `mode: "latest"` + maxBuffer (pinned TypeError); timeout 0 (DECIDE and
210
- pin: fires on the first turn -> first pending next() rejects TimeoutError),
211
- timeout Infinity (non-finite -> RangeError, pin), timeout -1; filter
212
- non-function; non-signal source/target (missing peek, missing subscribe,
213
- missing set, frozen impostor objects); `stop()` twice (pinned) and `stop()`
214
- racing a pending pull; dispose-then-X: every LS-01/LS-04 case lands here as
215
- a NAMED test pinning the documented behavior ("dispose does not stop the
216
- pump; abort does" / "dispose under a consuming iterable hangs -- documented
217
- anti-pattern, timeout is the escape").
218
-
219
- ### s4 -- seeded schedule fuzz vs a naive oracle
220
-
221
- A seeded generator emits op schedules over both directions: producer-set /
222
- consumer-next / filter-toggle-outcome / abort / break / return / timeout-arm,
223
- with micro/macro-task jitter. Oracle: a deliberately naive reference model
224
- (unbounded array + hand bookkeeping of drops/overflow per the documented
225
- semantics). Compare delivered sequences, terminal states, and counter values
226
- cell-for-cell. Divergence prints seed + op index + a minimal replay line.
227
- The multi-waiter FIFO and the ring head/wrap arithmetic are the two
228
- mechanisms this tier exists to break.
229
-
230
- ### s5 -- the alloc gate (the llms.txt claims, gated at last)
231
-
232
- `runOpsGate(fn, { ops })` with `RULES = { maxMajor: 0, maxPauseMs: 4 }`,
233
- stabilize `'deep'`. Scenarios and their DOCUMENTED budgets (the M0 doctrine:
234
- per-scenario budgets, never a widened global rule):
235
- - toAsyncIterable `mode:"latest"`, consumer caught up: llms.txt claims ZERO
236
- steady-state allocation -- gate exactly that (plus the per-next Promise
237
- cost intrinsic to the protocol, measured and documented once).
238
- - toAsyncIterable buffer, no waiter pending: same claim, same gate.
239
- - pipeToSignal steady pump: budget from today's 0.26 B/op retained baseline.
240
- - fromAsyncIterable latest: one state object per yield BY DESIGN -- assert
241
- the documented envelope (today 0.35 B/op retained steady-state), assert
242
- nothing HIDDEN grows (retained slope flat).
243
- - fromAsyncIterable buffer: state object + snapshot array per yield BY
244
- DESIGN -- envelope pinned per maxBuffer tier (8 / 64 / 1024).
245
- - waiter-heavy path (consumer faster than producer): waiter objects are
246
- intrinsic; retained slope must be flat.
247
-
248
- ### s6 -- soak and retention (lite-leak enters here)
249
-
250
- 4096 cycles per surface of: construct -> pump N values -> terminate (each of
251
- the three paths in rotation) -> drop refs; tracker with the standard kernels;
252
- `tracker.size()` returns to 0. Abort-listener accumulation on a SHARED
253
- AbortSignal across 4096 streams (the removeEventListener discipline at
254
- scale). Timer discipline: 4096 timeout-armed iterables that settle before
255
- the deadline -- `clearTimeout` called every time (the llms.txt "negative
256
- retained" bench hints this is right; prove it). The existing 4K-cycle unit
257
- leak tests stay in `test/07`; s6 is the deeper per-commit version.
258
-
259
- ### s7 -- conformance: the consumer corpus, the floor, the pact
260
-
261
- - **StreamQuery corpus**: the exact call shape from `StreamQuery.js:124-154`
262
- (transform-as-tap + hand ring + abort-filtering onError), run against a
263
- seeded source, asserting the semantics lite-query depends on: transform
264
- called once per value BEFORE set, abort fires onError with
265
- `signal.aborted === true` observable, onDone exactly once, stop idempotent.
266
- lite-stream must never drift under its flagship consumer silently. When
267
- LS3 ships the enriched pipeToSignal, the corpus gains the enriched shape
268
- and KEEPS the 1.1.0 shape (both are public API).
269
- - **Floor matrix**: `npm run test:floor` -- scripted: install the peer floor
270
- (`@zakkster/lite-signal@1.2.2`) via `--no-save --no-package-lock`, full
271
- `test:gc` + torture, restore latest, print both verdicts. Run per release
272
- (release drill step), not per commit. LS-09 stays a fact, not a claim.
273
- - **TimeoutError duck-pact**: shape assertions (`name`, `timeout` field,
274
- `instanceof Error`, message prefix) pinning the cross-package duck-check
275
- contract (2b). No lite-await devDep -- the pact is structural.
276
-
277
- ### s8 -- controls
278
-
279
- `STREAM_TORTURE_BREAK=1 node --expose-gc test/torture.mjs` must exit
280
- non-zero via injected breakage, one per tier minimum: an allocating closure
281
- in the s5 caught-up loop; an oracle that ignores droppedCount in s4; a
282
- skipped abort case in s1; a waiter-order swap in s2; a retained-stream ref
283
- in s6; a corpus assertion inverted in s7. A gate that cannot fail is
284
- decorative.
285
-
286
- ## 4. Session order
287
-
288
- ```
289
- LS1 (1.1.1 truth patch)
290
- |
291
- v
292
- LS2 (1.2.0 the law's gate + guards + VERSION + packaging)
293
- |
294
- v
295
- LS3 (1.3.0 the consumer contract: pipeToSignal enrichment + abort decision)
296
- |
297
- v [tripwire: lite-query Q8 session-start spike rules the split]
298
- LS4 (1.4.0 Q8 enablement: stall detection / push-writer / share primitives)
299
- ```
300
-
301
- Serial on purpose: LS1's honest docs are the floor LS2's gates stand on;
302
- LS2's harness is what makes LS3's surface growth safe to ship; LS4 is
303
- design-gated on lite-query Q8 and MUST NOT jump the queue (building shared-
304
- stream primitives before the consumer's failover matrix exists would be
305
- speculative API -- the exact thing the 1.0 deferral discipline refused).
306
-
307
- ## 5. The briefs
308
-
309
- ===============================================================================
310
- # LS1 -- lite-stream v1.1.1 -- the docs tell the truth and the gates run
311
- ===============================================================================
312
-
313
- ```markdown
314
- ---
315
- package: "@zakkster/lite-stream"
316
- version_target: 1.1.1
317
- status: planned
318
- tests_min: 85
319
- skip_max: 1 # only the named asyncDispose environment skip, node < 20
320
- findings: [LS-01, LS-02, LS-04, LS-06, LS-07, LS-08, LS-10]
321
- depends_on: []
322
- ---
323
-
324
- # lite-stream -- no shipped sentence the probes can refute
325
-
326
- PURPOSE
327
- A patch release with zero API change that makes every shipped claim true
328
- and every existing test actually run. The package's core positioning is
329
- "structural cleanup on every termination path"; today one documented path
330
- is a leak generator. Fix the words now, the machinery in LS2/LS3.
331
-
332
- TASKS
333
- - LS-01 docs surgery, all three shipped docs: llms.txt lines 132-134
334
- replaced with the truth ("disposing the result signal does NOT stop the
335
- pump; the iterator keeps pulling. AbortSignal or natural completion are
336
- the only stop mechanisms. See ROADMAP LS-01."); the Stream.js:148-152
337
- docstring stops offering dispose as a stop mechanism; README cleanup
338
- section aligned. The dead catch at Stream.js:343-351 STAYS (belt against
339
- a future lite-signal that throws) but its comment stops claiming it is
340
- the dispose path and starts naming what it actually is: defensive.
341
- - LS-01/LS-04 pinned tests (new test/08-dispose-behavior.test.mjs, ~6
342
- tests): dispose-mid-pump -> pump continues, return() not called (pin the
343
- TRUE behavior so any future change is a conscious one); same for
344
- pipeToSignal; dispose-source under toAsyncIterable -> pending next()
345
- hangs (pin via a bounded race against a sentinel timer); each test cites
346
- LS-01/LS-04 in its name.
347
- - LS-02: default gate becomes `"test": "node --expose-gc --test
348
- --test-reporter=spec"`; `test:gc` kept as an alias for muscle memory;
349
- `verify` unchanged. Assert in-session: 79 -> 79 pass 0 skip (pre-verified
350
- 2026-09-01: 21.5s).
351
- - LS-06: delete the two `CONTRACT-toAsyncIterable-1.1.md` references (the
352
- locked semantics live in llms.txt + this roadmap's s2/s3 tiers now);
353
- rewrite the TimeoutError comment without the Session A/B narrative.
354
- - LS-07: fix the pump comment to state the actual mechanism (".then
355
- handlers unwind the stack; no explicit deferral needed") and cite the 1M
356
- sync-source probe number.
357
- - LS-08: re-run `npm run bench`, stamp llms.txt Performance and README
358
- Benchmarks with node version + date + "your numbers will differ", one
359
- source of truth (llms.txt), README cites it.
360
- - LS-10 (partially done at roadmap-pass time): .gitignore landed; record
361
- the no-lockfile policy line in README's Testing section or CONTRIBUTING
362
- stub -- one sentence, not a document.
363
- - CHANGELOG [1.1.1]: Fixed (docs corrections, each naming its finding),
364
- Added (dispose-behavior pins, gate flag). Facts only.
365
-
366
- HOT PATH
367
- Untouched. `git diff -- Stream.js` shows comment-only hunks; reviewer
368
- proves token-stream identity (the Q2 discipline).
369
-
370
- ASSERTIONS
371
- - npm test -> >= 85 pass, 0 fail, skip <= 1 (asyncDispose env skip only,
372
- named).
373
- - bench/findings-probes-2026-09-01.mjs output UNCHANGED (the probes pin
374
- behavior; this release changes words).
375
- - grep proves no shipped doc contains "tears down" dispose claims; grep
376
- finds the AbortSignal-only sentence in llms.txt AND README.
377
- - npm pack --dry-run: no test/, no bench/, no demo/.
378
-
379
- NON-GOALS
380
- No API change. No VERSION export (that is additive -> LS2). No torture
381
- harness (LS2). No behavior change to the pump.
382
-
383
- DONE WHEN
384
- Every sentence the probes can check is true, the default gate runs
385
- everything, and 1.1.1 is published with the corrected llms.txt.
386
- ```
387
-
388
- ===============================================================================
389
- # LS2 -- lite-stream v1.2.0 -- the law's gate, able to fail
390
- ===============================================================================
391
-
392
- ```markdown
393
- ---
394
- package: "@zakkster/lite-stream"
395
- version_target: 1.2.0
396
- status: planned
397
- tests_min: 90
398
- skip_max: 1
399
- torture: "s0-s8 ok; STREAM_TORTURE_BREAK=1 fails every tier; floor matrix green"
400
- findings: [LS-03, LS-05, LS-09, LS-11]
401
- depends_on: [LS1]
402
- ---
403
-
404
- # lite-stream -- prove the claims, then keep them proven
405
-
406
- PURPOSE
407
- Port the suite-law torture harness (bake-stream M0 discipline, tiers per
408
- section 3), add the drift guards that keep docs/exports/ASCII honest
409
- (lite-query Q2 discipline), export VERSION, and execute the LS-11
410
- packaging ruling. After this session the package can prove its own
411
- headline claims -- including the zero-steady-state-alloc sentence that has
412
- been unverified since 1.0.0.
413
-
414
- TASKS
415
- - test/torture.mjs + test/torture/ per section 3: harness (seeded PRNG,
416
- check thunks, runOpsGate stabilize:'deep', STREAM_TORTURE_BREAK,
417
- preflight exit 2 on missing devDeps), tiers s0-s8, controls for every
418
- tier. devDeps: @zakkster/lite-leak (floor from its llms.txt),
419
- @zakkster/lite-gc-profiler ^1.10.0 (unknown-rule-keys-throw relied on).
420
- Read both llms.txt files before wiring; rule names from memory are how
421
- gates rot.
422
- - package.json: "torture": "node --expose-gc test/torture.mjs";
423
- "torture:control": "STREAM_TORTURE_BREAK=1 node --expose-gc
424
- test/torture.mjs || echo control-failed-as-expected"; "test:floor" per
425
- s7; "verify" gains torture.
426
- - Guards (Q2-modeled, ~5 tests): ascii-guard (walk = files[] parsed at
427
- runtime + test/ + bench/ + demo/; printable ASCII + LF; empty allowlist;
428
- inline failing control) and surface-guard (exports == llms.txt == d.ts
429
- export names; peer names in llms.txt exist in peerDependencies; failing
430
- fixture control). Pre-verified 2026-09-01: repo is already 0 non-ASCII.
431
- - LS-05: `const VERSION = "1.2.0"` exported from Stream.js; string-equals
432
- package.json (release-drill site). Never re-exported by consumers
433
- (suite ruling). Surface-guard and d.ts updated in the same commit.
434
- - LS-11 ruling executes: ROADMAP.md out of files[]; llms.txt Files section
435
- updated; CHANGELOG forward-references fixed (the published 1.1.0 tarball
436
- keeps its pointer -- the History section below preserves the target).
437
- - LS-09: test:floor scripted and run in-session (pre-verified green
438
- 2026-09-01); result recorded in CHANGELOG with provenance. Peer floor
439
- STAYS ^1.2.0 because it is now a tested fact.
440
- - Expected s5 friction, decided in advance (M0 lesson): the per-next
441
- Promise allocation in toAsyncIterable and the by-design per-yield state
442
- objects in fromAsyncIterable get per-scenario documented budgets --
443
- never a widened global rule. A budget that moves is not a gate.
444
-
445
- ASSERTIONS
446
- - npm test >= 90 pass, 0 fail, skip <= 1; torture prints "ok" exit 0;
447
- torture:control exits non-zero FOR EVERY TIER (each control verified
448
- individually in-session).
449
- - The llms.txt zero-steady-state-alloc sentence now has a gate (s5) and
450
- the gate has a control (s8).
451
- - npm pack --dry-run: ROADMAP.md ABSENT, test/ bench/ demo/ absent,
452
- llms.txt + CHANGELOG.md present; VERSION === package.json version.
453
- - Fresh-clone drill: clone to scratch, npm install, npm test, npm run
454
- torture -- all green (the lite-query G10 habit).
455
-
456
- NON-GOALS
457
- No surface growth beyond VERSION. No pipeToSignal enrichment (LS3). No
458
- behavior changes -- if a tier finds a real bug, it lands as a NAMED
459
- failing todo + ledger row and ships fixed in LS3, not silently here.
460
-
461
- DONE WHEN
462
- the law's command runs from the law's path, every control fails, the
463
- floor matrix is scripted, and 1.2.0 is published with VERSION exported
464
- ```
465
-
466
- ===============================================================================
467
- # LS3 -- lite-stream v1.3.0 -- the consumer contract session
468
- ===============================================================================
469
-
470
- ```markdown
471
- ---
472
- package: "@zakkster/lite-stream"
473
- version_target: 1.3.0
474
- status: planned
475
- tests_min: 105
476
- skip_max: 1
477
- torture: "ok + s7 corpus extended to the enriched shape"
478
- findings: [LS-12]
479
- depends_on: [LS2]
480
- consumer_evidence: "StreamQuery.js:119-137 (hand ring), :127 (status tap), :139-142 (abort filter)"
481
- ---
482
-
483
- # lite-stream -- what its flagship consumer already built by hand, offered properly
484
-
485
- PURPOSE
486
- Every addition in this session has a shipped consumer code-site as its
487
- triggering signal -- the 1.0 deferral discipline, satisfied for real.
488
- lite-query's StreamQuery hand-rolls three things around pipeToSignal:
489
- a buffer window (push/shift/slice -- O(n) shift where our ring is O(1)),
490
- a first-value status tap smuggled through `transform`, and an
491
- abort-filtering onError. Offer all three natively; let the consumer
492
- delete code.
493
-
494
- TASKS
495
- - pipeToSignal enrichment (design settled in-session by the planner,
496
- candidates): `mode: "latest" | "buffer"` + `maxBuffer` + a
497
- droppedCount observable (return-object upgrade or stats getter --
498
- decide against the "returns stop fn" 1.x compat constraint: the stop
499
- FUNCTION must remain callable as before; additive properties on it are
500
- the compat-safe shape), and `onValue(v)` tap firing before set (the
501
- status-tap consumer). All additive; 1.1.0 call shapes byte-compatible
502
- (s7 corpus keeps both shapes green).
503
- - LS-12 decision doc, decisions/0001-abort-vocabulary.md (the lite-await
504
- decisions/ convention): EITHER pin the asymmetry as deliberate with the
505
- producer/consumer rationale (from-side: abort interrupts production =
506
- error framing; to-side: abort stops listening = graceful done) and
507
- document it in llms.txt as a contract table -- OR add `onAbort`
508
- callbacks so consumers stop filtering. Recommendation: PIN the
509
- asymmetry + add `onAbort` to pipeToSignal only (the direction with the
510
- shipped workaround); do not touch toAsyncIterable's graceful-done
511
- contract (1.0.0-locked, correct for its side).
512
- - s7 corpus: add the enriched-shape cases; keep the 1.1.0-shape cases
513
- forever.
514
- - Cross-repo rider recorded (not executed here): lite-query parking entry
515
- exists pointing at this session -- after 1.3.0 ships, StreamQuery can
516
- collapse its ring + tap + filter into the enriched call in a lite-query
517
- minor, and refresh its vendored demo copy (still 1.0.0).
518
-
519
- ASSERTIONS
520
- - Old-shape s7 corpus green UNCHANGED against the new build (no drift
521
- under the flagship consumer).
522
- - A StreamQuery-shaped rewrite using the enriched surface (as a TEST, in
523
- this repo) produces identical observable semantics to the hand-rolled
524
- original, including droppedCount parity on overflow.
525
- - Suite >= 105, 0 fail; torture ok; guards green; VERSION bumped.
526
-
527
- NON-GOALS
528
- No fromAsyncIterable changes. No toAsyncIterable changes. No shared/
529
- multicast primitives (LS4, gated). No combinators (never -- History).
530
-
531
- DONE WHEN
532
- lite-query COULD delete StreamQuery.js:119-137 without behavior change,
533
- and 1.3.0 is published with the decision doc on disk
534
- ```
535
-
536
- ===============================================================================
537
- # LS4 -- lite-stream v1.4.0 -- Q8 enablement (GATED, do not start on a hunch)
538
- ===============================================================================
539
-
540
- ```markdown
541
- ---
542
- package: "@zakkster/lite-stream"
543
- version_target: 1.4.0
544
- status: gated
545
- gate: "lite-query Q8 session-start spike has ruled which primitives land here"
546
- tests_min: 120
547
- depends_on: [LS3]
548
- ---
549
-
550
- # lite-stream -- the primitives a one-connection browser needs, if Q8 wants them here
551
-
552
- PURPOSE
553
- lite-query 2.0 (Q8) makes one tab own an upstream iterator while N tabs
554
- consume frames. Three primitive-shaped needs are visible from Q8's brief
555
- today; WHICH of them belong in lite-stream (vs inside lite-query, vs
556
- nowhere) is Q8's session-start spike ruling. This brief is a menu, not a
557
- commitment.
558
-
559
- CANDIDATE MENU (each with its Q8 obligation)
560
- - Idle/stall detection: an `idleTimeout` that resets per value (today's
561
- `timeout` is an overall deadline -- wrong tool for "leader hung, frames
562
- stopped, channel alive"). Smallest candidate, clearest fit.
563
- - Push-writer surface: values that arrive by callback (channel frames),
564
- not by iterator pull, entering a signal with the same mode/maxBuffer/
565
- droppedCount discipline (the follower side's projection). Shape TBD --
566
- possibly just documenting the enriched pipeToSignal over a hand-made
567
- async queue, possibly a real `createSignalWriter`.
568
- - Share/refcount: one source iterator, N signal consumers, last-consumer
569
- teardown. The most API-heavy candidate and the most likely to belong
570
- INSIDE lite-query next to its cache entries instead -- do not build
571
- speculatively.
572
-
573
- ASSERTIONS (floor, if the gate opens)
574
- Whatever ships: torture tiers extended same-session (a primitive without
575
- its soak + fuzz + controls does not ship); Q8's failover matrix consumes
576
- the new surface in the same quarter, or the surface is reverted before
577
- 2.0 -- an enabler nobody enabled is speculative API with a version number.
578
-
579
- DONE WHEN
580
- the Q8 ruling is recorded here (menu -> commitments), and either 1.4.0
581
- ships what Q8 consumes, or this session closes as "not needed in
582
- lite-stream" with the reasoning -- both are wins
583
- ```
584
-
585
- ## 6. How to run it
586
-
587
- In order: LS1 -> LS2 -> LS3 -> LS4 (gated). Pipeline per session: planner ->
588
- coder -> reviewer -> qa; reviewer REJECTED goes back to coder. `status:
589
- planned -> shipped` in the brief's frontmatter on publish, with the /release
590
- drill (fail closed; the drill owns version bumps; VERSION site list from
591
- LS2 on: package.json, Stream.js header, VERSION const, llms.txt if it ever
592
- gains a version string).
593
-
594
- Habits this roadmap is built around (inherited, locally re-proven):
595
- 1. **Reproduce by running, never infer.** Every ledger row above has a
596
- command. LS-01 existed for two shipped versions because a comment was
597
- trusted over a run.
598
- 2. **A green suite that skips its GC tier is a green light over a hole.**
599
- Fourth suite recurrence (Q-02, AR-02, AW-14, LS-02). The fix is always
600
- the same: the default gate runs everything.
601
- 3. **Claims about a peer are facts about a VERSION of a peer.** The dispose
602
- assumption was never true of any version; the floor claim happened to be
603
- true of all of them. Neither status was known until today. The floor
604
- matrix and the conformance corpus exist so both stay known.
605
- 4. **Budgets, not widened rules.** By-design allocations (state objects,
606
- snapshot arrays, protocol promises) get named per-scenario budgets in
607
- s5; the global maxMajor: 0 rule never widens.
608
-
609
- ## Parking ledger (deferred, with reasons and tripwires)
610
-
611
- - **Dispose-aware pumps** (the LS-01 behavioral fix): IMPOSSIBLE in-library
612
- today -- lite-signal 1.5.0 exposes no disposed-oracle (2b). Tripwire:
613
- lite-signal ships one -> un-park, wire into fromAsyncIterable/pipeToSignal
614
- teardown, upgrade the LS1 pinned "pump continues" tests into "pump stops"
615
- tests in the same change. Until then: docs + pins (LS1).
616
- - **toAsyncIterable dispose-source hardening** (LS-04): same tripwire, same
617
- fate (pending next() could reject instead of hang).
618
- - **mapLimit / transform concurrency** on fromAsyncIterable (from the 1.0
619
- ledger): trigger re-grounded -- two or more SUITE consumers writing the
620
- same bounded-concurrency wrapper. None exist today (checked: only
621
- StreamQuery consumes, no concurrency wrapper).
622
- - **toAsyncIterable overflow policies** (drop-newest / throw / block):
623
- trigger unchanged -- a consumer demonstrating drop-oldest causes
624
- observable correctness issues. None known.
625
- - **mergeIterables / splitSignal**: trigger re-grounded from "first Twitch
626
- consumer" to "first suite consumer needing >= 2 async sources into one
627
- reactive surface with shared cancellation". Q8's leader might be that
628
- consumer -- if so it routes through the LS4 menu, not around it.
629
- - **fromEventTarget(target, eventName)**: browser-facing sugar; trigger:
630
- the pattern recurring across suite demos. demo/index.html is the place to
631
- look first (unaudited against the demo-audit law -- run that skill next
632
- time the demo is touched).
633
- - **pipeToSignal backpressure "block"**: requires effect-completion
634
- semantics lite-signal does not have (2b-class cross-package fact).
635
- Tripwire: lite-signal ships them.
636
- - **opts.equals for buffer-mode snapshot dedup**: trigger: a consumer
637
- demonstrating redundant snapshot propagation mattering in a profile.
638
- - **stats() diagnostic surface**: superseded in part by droppedCount/
639
- overflowCount; remainder (yields, startedAt, settledAt) waits for the
640
- lite-query Q7 devtools feed to define what a stream should report --
641
- tripwire: Q7 ships and asks.
642
- - **RangeReader recipe** (bake-stream): docs-only candidate -- lazy shard
643
- walk -> async generator -> fromAsyncIterable; needs bake-stream >= MQ1
644
- (abortable range reads) so the AbortSignal story is end-to-end. Tripwire:
645
- MQ1 ships.
646
- - **propagate: "always"** and **full combinators (map/filter/debounce/...)**:
647
- never -- see History; the async-generator composition answer stands.
648
-
649
- ## History (preserved from the 1.0/1.1 roadmap; the shipped 1.1.0 CHANGELOG points here)
650
-
651
- ### Shipped in 1.1.0 -- toAsyncIterable enrichment
652
-
653
- Six additions, each with the triggering signal that moved it out of
654
- deferred status: `mode: "latest" | "buffer"` (vocabulary match with
655
- lite-query's streamQuery + reactive-state consumers); `filter` (removes the
656
- per-yield wrapper-generator allocation); `timeout` + `TimeoutError` export
657
- (replaces external AbortController boilerplate; structurally identical to
658
- lite-await's class, deliberately not imported -- the zero-dep story);
659
- `Symbol.asyncDispose` on node >= 20 (`await using`); multi-waiter FIFO
660
- queue (latent 1.0.0 bug: `Promise.all([iter.next(), iter.next()])` silently
661
- lost the first resolver); `overflowCount` (mode-neutral alias for
662
- `droppedCount`). Full details: CHANGELOG.md [1.1.0].
663
-
664
- ### Non-goals carried forward verbatim
665
-
666
- No standalone scheduler; no retry/backoff policy (compose lite-await);
667
- no WebSocket/EventSource adapters (they belong to the consuming SDK); no
668
- ReactiveX combinator surface (async generators are the composition story --
669
- inspectable, debuggable, cancellation-transparent); no multi-iterator
670
- orchestration beyond a potential mergeIterables (lite-await's allOf/anyOf/
671
- raceOf own that).
672
-
673
- ### Versioning policy (unchanged)
674
-
675
- Strict semver. 1.x additive only (new exports, new opts fields, new
676
- state-shape fields); breaking waits for 2.0; patches are fixes/docs only.
677
- Every addition lands with: a concrete suite consumer, full-lifecycle tests
678
- (happy + all termination paths), a bench with provenance, and a CHANGELOG
679
- line with a hot-path-cost note.
680
-
681
- ## Progress log
682
-
683
- - 2026-09-01 -- Roadmap enriched to blueprint form. Ground truth verified by
684
- running: 79 tests (71+8 dark under default gate; 79/79 under test:gc,
685
- against lite-signal 1.5.0 and 1.2.2 both), bench re-run with provenance,
686
- five probes written and committed (bench/findings-probes-2026-09-01.mjs).
687
- Ledger LS-01..LS-12 opened; LS-01 (dispose forever-pump, llms.txt claim
688
- false since 1.0.0) is the headline. .gitignore added (LS-10 mitigation,
689
- lite-query Q1 ruling). Cross-package wiring recorded on both sides:
690
- lite-query Q8 spike <-> LS4 gate; LS3 <-> StreamQuery hand-ring rider.
691
- Sessions LS1..LS4 defined. Nothing else changed; the tree still builds
692
- and tests exactly as published 1.1.0.
693
-
694
- - 2026-09-01 -- LS1 (v1.1.1 truth patch) landed in the working tree. LS-01:
695
- the dispose-teardown claim corrected in llms.txt, README (two sites), the
696
- Stream.js fromAsyncIterable + pipeToSignal docstrings, and the package.json
697
- description; the defensive set() catch and its forward comment re-framed as
698
- peer-defense (not disposal detection), and the LS-07 pump comment corrected
699
- to "direct call, no deferral" -- the Stream.js diff is comment-only and the
700
- code-line stream is byte-identical (md5 unchanged, exports unchanged). LS-06:
701
- the three phantom CONTRACT-toAsyncIterable-1.1.md references and the
702
- Session A/B narrative removed from Stream.js and test/07. LS-08: bench
703
- re-measured with provenance (node v26.3.1, macOS arm64, 150ms warmup /
704
- 500ms runs) and consolidated as llms.txt Performance (the single source of
705
- truth); README now cites it with no ops/s digits; the four unreproducible
706
- 1.1 bench scenarios deleted from the docs (no runnable source in the repo).
707
- LS-02: --expose-gc added to the default npm test so the GC tier gates on
708
- every run. Added test/08-dispose-behavior.test.mjs (6 pins, LS-01/LS-04);
709
- LS-10 no-lockfile sentence added to the README. Measured: npm test 85 pass
710
- / 0 fail / 0 skipped; probes P0-P4 structurally unchanged. The LS1 brief's
711
- `status:` is left at planned -- it flips at publish, not here.