@jarenjs/josl 0.34.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/FORMAT.md ADDED
@@ -0,0 +1,235 @@
1
+ # JOSL — JavaScript Obvious Streaming Language
2
+
3
+ **Status: published (format version 0.x — the surface may still evolve).**
4
+
5
+ JOSL is a strict superset of [TOML 1.0.0](https://toml.io/en/v1.0.0) that
6
+ adds JavaScript's obvious value types as first-class citizens and makes
7
+ the most common LLM output shape — a list of records — streamable at the
8
+ root. Every valid TOML 1.0 document is a valid JOSL document with the
9
+ same meaning. The delta is deliberately tiny: a model that knows TOML
10
+ only has to be taught the extensions below.
11
+
12
+ The name follows Tom's lead: *Tom's Obvious Minimal Language* →
13
+ *JavaScript's Obvious Streaming Language*.
14
+
15
+ ## Why
16
+
17
+ - **Streaming.** A truncated TOML/JOSL document is valid up to the last
18
+ complete line; a truncated JSON document is unparseable until the last
19
+ brace closes. Line-oriented, section-chunked syntax lets a consumer act
20
+ on record *N* while an LLM is still emitting record *N+1*, and lets
21
+ syntax errors surface (with line/column and a repair `hint`) the moment
22
+ they are produced.
23
+ - **Fidelity.** JSON has no datetime, no bigint, no regexp, and TOML has
24
+ no null. JavaScript has all four; JOSL makes them literals.
25
+ - **Small-model ergonomics.** Line-oriented key/value syntax with minimal
26
+ escaping is easy for small models to emit reliably; the JOSL delta is
27
+ small enough for a short system prompt to carry.
28
+
29
+ ## The delta over TOML 1.0
30
+
31
+ ### 1. `null`
32
+
33
+ ```toml
34
+ middle-name = null
35
+ ```
36
+
37
+ TOML has no null. In JOSL, `null` in value position is JavaScript's
38
+ `null`. In strict TOML mode (`mode: 'toml'`) it is a syntax error, and
39
+ the writer either errors or omits the key (`onNull: 'omit'`).
40
+
41
+ ### 2. bigint
42
+
43
+ ```toml
44
+ big = 123n
45
+ mask = 0xffff_ffff_ffff_ffffn
46
+ ```
47
+
48
+ JavaScript bigint literal syntax: an integer in any TOML radix with an
49
+ `n` suffix. A fraction or exponent with `n` is an error. Additionally, a
50
+ *plain* integer that exceeds JavaScript's safe-integer range
51
+ auto-promotes to bigint in both modes, so integers always parse
52
+ losslessly; strict TOML mode additionally enforces the spec's signed
53
+ 64-bit range, while the `n` literal syntax itself stays JOSL-only.
54
+
55
+ ### 3. regexp
56
+
57
+ ```toml
58
+ match = /^[a-z]+_\d{2,}$/im
59
+ ```
60
+
61
+ JavaScript regexp literal syntax: body and flags, `/` inside a character
62
+ class needs no escape, `\/` works everywhere. The value is a native
63
+ `RegExp`, validated at parse time (construction only — never executed).
64
+
65
+ ### 4. Root arrays: `[[]]`
66
+
67
+ ```toml
68
+ [[]]
69
+ name = "first record"
70
+ score = 0.92
71
+ [meta]
72
+ source = "model-a"
73
+
74
+ [[]]
75
+ name = "second record"
76
+ score = 0.87
77
+ ```
78
+
79
+ A list of records is the most common LLM output shape, and plain TOML
80
+ cannot express it at the root without a wrapper key. `[[]]` declares the
81
+ document root to be an array and appends one table element per
82
+ occurrence — each `[[]]` completes the previous record, which is exactly
83
+ the streaming boundary a consumer wants.
84
+
85
+ Scoping rule: after `[[]]`, subsequent `[table]` and `[[array]]` headers
86
+ are resolved *relative to the current root element*, so records can have
87
+ subsections. A document that starts with key-value pairs has an object
88
+ root; mixing the two is an error.
89
+
90
+ Root array elements are tables. Root arrays of scalars are intentionally
91
+ not supported (use a wrapper key); this keeps `[[]]` unambiguous.
92
+
93
+ ### Datetimes (inherited, clarified)
94
+
95
+ TOML's four datetime flavours are kept as-is; JOSL pins the JavaScript
96
+ representation:
97
+
98
+ | Literal | JS value |
99
+ | --- | --- |
100
+ | `1979-05-27T07:32:00Z` (offset) | `Date` (offset normalized to instant) |
101
+ | `1979-05-27T07:32:00` (local) | `LocalDateTime` |
102
+ | `1979-05-27` | `LocalDate` |
103
+ | `07:32:00.999` | `LocalTime` |
104
+
105
+ `LocalDate` / `LocalTime` / `LocalDateTime` are small frozen value
106
+ classes that round-trip via `toString()` and serialize under
107
+ `JSON.stringify` via `toJSON()`. Sub-second precision is kept as the
108
+ literal fraction string.
109
+
110
+ ### What was deliberately left out
111
+
112
+ Bytes, sets, references/anchors, comments-as-data, scalar root arrays.
113
+ Every added type is prompt-tax on every model and code-tax on every
114
+ consumer; "obvious" is the load-bearing word. TOML 1.1 features (e.g.
115
+ `\e`, `\x`, second-optional times) are not included until 1.1 ships.
116
+
117
+ ## Streaming semantics
118
+
119
+ The reader consumes chunks that may split *any* token (including a `"""`
120
+ delimiter). Events fire in document order the moment each construct
121
+ completes:
122
+
123
+ | Event | Meaning |
124
+ | --- | --- |
125
+ | `{type:'table', path, line}` | a `[header]` opened |
126
+ | `{type:'table-array', path, line}` | a `[[header]]` appended an element |
127
+ | `{type:'root-item', path, index, line}` | a `[[]]` element started |
128
+ | `{type:'pair', path, key, value, line}` | a key-value completed |
129
+
130
+ Paths are absolute — strings for keys, numbers for array indices — so
131
+ they are directly convertible to JSON Pointers. The partially built root
132
+ is available at any time via `root()`.
133
+
134
+ This is the deliberate opposite of `JSON.parse(text, reviver)`, which
135
+ visits leaves bottom-up, only after the full text has arrived, and never
136
+ tells you where you are.
137
+
138
+ The write side mirrors this: `createStreamWriter` emits text chunks
139
+ event by event (`pair`, `table`, `tableArray`, `rootItem`), and
140
+ `stringifyJoslChunks` streams an existing value one `[[]]` record at a
141
+ time — byte-identical to `stringifyJosl` output.
142
+
143
+ ## JSONX
144
+
145
+ JSONX is the same set of first-class citizens grafted onto JSON, for
146
+ when brace syntax is the better fit:
147
+
148
+ - bigint: `123n`, with the same unsafe-integer auto-promotion
149
+ - regexp: `/pattern/flags`
150
+ - bare RFC 3339 datetimes: `2026-07-18T12:00:00Z`, `2026-07-18`,
151
+ `12:30:00` (all four flavours, same JS mapping as JOSL)
152
+ - non-finite numbers: `inf`, `-inf`, `nan` (the JS spellings
153
+ `Infinity` / `NaN` are also accepted)
154
+ - numeric separators (`1_000_000`) and a leading `+`
155
+
156
+ `mode: 'json'` is bit-compatible strict JSON: parsing matches
157
+ `JSON.parse`, stringifying delegates to `JSON.stringify`. The parser
158
+ reports the same document-order `open` / `value` / `close` events with
159
+ absolute paths.
160
+
161
+ ## CSV
162
+
163
+ CSV is not a JOSL dialect and has no shared grammar with it — it lives in
164
+ this package because it has the same *shape of problem*: a record-oriented
165
+ text format that has to be readable a chunk at a time, and a value model
166
+ that JSON cannot express. What it shares is the machine architecture
167
+ (`parseAll` and `feed`/`end` run one record parser) and the value types.
168
+
169
+ There is no CSV specification worth conforming to. RFC 4180 describes a
170
+ narrow dialect that real exporters routinely violate, and it ships no test
171
+ suite. So the contract here is stated rather than referenced:
172
+
173
+ **Reading is strict by default.** The eight conditions below throw a
174
+ `CsvSyntaxError` with a stable `CSV1xxx` code, a line and a column.
175
+ `repair: true` reads each one the way that loses the least and records it
176
+ under the same code — the modes differ in what happens, never in the
177
+ diagnosis.
178
+
179
+ | code | condition | repair-mode reading |
180
+ | --- | --- | --- |
181
+ | `CSV1001` | a quoted field is never closed | close at end of input |
182
+ | `CSV1002` | text after a closing quote | field closed; absorb the text |
183
+ | `CSV1003` | an unescaped quote inside a quoted field | the quote is literal |
184
+ | `CSV1004` | record shorter than the header | missing columns stay absent |
185
+ | `CSV1005` | record longer than the header | widen the header once |
186
+ | `CSV1006` | a bare carriage return | ends the record |
187
+ | `CSV1007` | a duplicate header name | suffix (`a`, `a_2`) |
188
+ | `CSV1008` | an empty header name | synthesize (`column_3`) |
189
+
190
+ `CSV1002` and `CSV1003` describe the same byte read two ways. The reader
191
+ chooses by scanning for another quote before the next delimiter or
192
+ terminator: one found means the quote is text (`"he said "hi" ok"` keeps
193
+ its content), none found means the field had closed (`"abc"junk,d` keeps
194
+ its two columns). Column count breaks the tie, because a lost field
195
+ boundary corrupts every value after it while a mangled cell corrupts one.
196
+
197
+ **Deliberate readings**, each chosen because the alternative destroys
198
+ information that cannot be recovered downstream:
199
+
200
+ - A blank line is a record of one empty field (RFC 4180 has no blank
201
+ line). `skipEmptyLines` drops them, and repair mode turns it on.
202
+ - A trailing terminator does not produce a final empty record.
203
+ - A quoted cell is never trimmed and never coerced: the quotes are the
204
+ author marking the content as text.
205
+ - A missing column is **absent**, not empty — `undefined` says the record
206
+ did not carry it, `''` would claim it carried nothing.
207
+ - `typed: true` promotes an integer beyond 2^53 to bigint rather than
208
+ rounding, reads unambiguous ISO-8601 as the value classes above, and
209
+ leaves anything with a leading zero a string.
210
+ - A BOM is stripped without comment; it is an encoding mark, not data.
211
+
212
+ **Writing** quotes a field only when it contains the delimiter, the quote
213
+ character, a newline, or edge whitespace a lenient reader might trim. The
214
+ default terminator is CRLF, per RFC 4180 §2.1 and what spreadsheet
215
+ software expects.
216
+
217
+ ## Compliance notes
218
+
219
+ - Strict TOML mode passes the complete official
220
+ [toml-test](https://github.com/toml-lang/toml-test) 1.0.0 suite
221
+ (the git submodule at `benchmark/toml-test-suite/`): all valid cases with
222
+ typed value verification, all invalid cases rejected. The only skips
223
+ are eight byte-level UTF-8 encoding cases, unreachable once input is
224
+ a JS string. `npm run test:josl` runs the suite;
225
+ `npm run benchmark:toml` compares compliance and speed against other
226
+ JS TOML parsers.
227
+ - Integers parse losslessly: plain integers beyond JavaScript's safe
228
+ range promote to bigint in both modes, and strict TOML mode enforces
229
+ the spec's signed 64-bit **range** (the `n` literal *syntax* remains
230
+ JOSL-only).
231
+ - `__proto__` keys are stored as own properties (no prototype
232
+ pollution) in both JOSL and JSONX.
233
+ - Round-trips are faithful for data, not formatting: comments and key
234
+ order aesthetics are not part of the value model, and an array whose
235
+ elements are all plain objects is re-emitted in `[[array]]` form.