@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 +235 -0
- package/README.md +494 -0
- package/dist/types/cst.d.ts +78 -0
- package/dist/types/csv-machine.d.ts +104 -0
- package/dist/types/csv-stream.d.ts +102 -0
- package/dist/types/csv.d.ts +141 -0
- package/dist/types/errors.d.ts +79 -0
- package/dist/types/gbnf.d.ts +20 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/jsonx-scalar.d.ts +92 -0
- package/dist/types/jsonx-stream.d.ts +163 -0
- package/dist/types/jsonx.d.ts +31 -0
- package/dist/types/machine.d.ts +97 -0
- package/dist/types/parse.d.ts +24 -0
- package/dist/types/stream.d.ts +32 -0
- package/dist/types/stringify.d.ts +63 -0
- package/dist/types/util.d.ts +56 -0
- package/dist/types/values.d.ts +60 -0
- package/dist/types/write.d.ts +92 -0
- package/package.json +104 -0
- package/schemas/jaren-josl-data.schema.json +21 -0
- package/src/cst.js +256 -0
- package/src/csv-machine.js +908 -0
- package/src/csv-stream.js +196 -0
- package/src/csv.js +363 -0
- package/src/errors.js +103 -0
- package/src/gbnf.js +179 -0
- package/src/index.js +50 -0
- package/src/jsonx-scalar.js +326 -0
- package/src/jsonx-stream.js +806 -0
- package/src/jsonx.js +342 -0
- package/src/machine.js +1252 -0
- package/src/parse.js +37 -0
- package/src/stream.js +57 -0
- package/src/stringify.js +341 -0
- package/src/util.js +96 -0
- package/src/values.js +104 -0
- package/src/write.js +226 -0
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.
|