@jarenjs/play 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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @jarenjs/play
2
+
3
+ **A JSON-engine playground — understand an engine before you compose it.**
4
+
5
+ Pick an engine (JSONPath, JSON Pointer, JSON Patch, `$query`, JSLT,
6
+ markdown, mermaid, …), feed it a source input and one or more datasets from
7
+ a curated example library, and watch it run. Where `@jarenjs/studio` is for
8
+ building an application out of many files, `@jarenjs/play` is for
9
+ learning one engine standalone — a reference bench you experiment on first.
10
+
11
+ Two layers, the suite's convention:
12
+
13
+ - **the engine** (`@jarenjs/play`) — headless: an engine registry, a
14
+ curated example library, and a pure `runExample(engineId, source, data)
15
+ → { ok, panels, timing, error }`. Wraps the real shipped compilers; knows
16
+ nothing of the DOM.
17
+ - **the component** (`@jarenjs/play/component`) — the playground UI, a
18
+ `createPlayComponent()` factory (like `@jarenjs/calc`): a JSLT view
19
+ (example rail, source editors, a dataset switcher, the run stage) the
20
+ host composes.
21
+
22
+ Play is a **student tool**: it opens calm — one clean result per run — and
23
+ drills deeper on demand. An engine's rich explainers (match cards, the
24
+ compiled program, a geometry-free AST, a canonical round-trip) are `deep`
25
+ result panels behind a quiet **"Explain ▸"** depth toggle: revealed beside
26
+ the answer on desktop, as a full-pane swap with a ← back on a phone.
27
+
28
+ ## The model
29
+
30
+ An engine's inputs are heterogeneous — a selector needs one JSON document,
31
+ a patch needs a target, markdown needs no data at all. Each engine is a
32
+ **descriptor** (the panes it consumes + a pure `run`); each **example**
33
+ presets those panes plus a **list of datasets**:
34
+
35
+ ```js
36
+ { engine: 'path',
37
+ source: { selector: '$.store.book[*].title' },
38
+ datasets: [ { label: 'store', data: { data: '{ … }' } },
39
+ { label: 'catalog', data: { data: '{ … }' } } ] }
40
+ ```
41
+
42
+ The datasets list length is the whole story: **0** → the engine takes no
43
+ data (markdown/mermaid); **1** → one dataset; **N** → a switcher that runs
44
+ the *same* source over each shape. Adding an engine is a descriptor plus
45
+ examples — the picker, panes and switcher all derive from the descriptor.
46
+ See [docs/PLAY-FORMAT.md](docs/PLAY-FORMAT.md).
47
+
48
+ ## Install
49
+
50
+ ```
51
+ npm install @jarenjs/play
52
+ ```
53
+
54
+ Zero third-party runtime dependencies — only other `@jarenjs/*` packages.
55
+ Node ≥ 24, ESM.
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @file The playground COMPONENT — `createPlayComponent(options)`, the
3
+ * suite's factory convention (like `@jarenjs/calc` / `@jarenjs/studio`). It
4
+ * hands the host the JSLT view (`rules` + `mode` + `modes`), the pure
5
+ * derivation (`viewModel`), and the headless engine surface its run loop
6
+ * binds (`runExample`, the registry, the example library). The reducer
7
+ * `play/*` actions and the debounced live-run are wired at the host.
8
+ */
9
+ import { ENGINES, EXAMPLES, engineIds, runExample } from '../index.js';
10
+ import { playViewModel } from './viewmodel.js';
11
+ import { playRules, playModes, PLAY_MODE, PLAY_BASE } from './view.js';
12
+ /**
13
+ * Build the playground component.
14
+ *
15
+ * The three host seams are configured ONCE here and become defaults for
16
+ * every `runExample` call the returned component makes; a per-call option
17
+ * of the same name still wins, so a caller can vary one run. Configuring
18
+ * them at the factory and having them silently ignored is the trap this
19
+ * shape exists to close — a host that registered its operator packs here
20
+ * would otherwise watch `$mean` and `$npv` go missing at run time.
21
+ *
22
+ * @param {{ operators?: { toOptions: () => any },
23
+ * renderers?: Record<string, Function>, validate?: Function }} [options]
24
+ * `operators` reaches the query/jslt engines, `renderers` the visual
25
+ * engines (markdown/mermaid/charts/mdx), `validate` the JSON Schema one
26
+ */
27
+ export declare function createPlayComponent(options?: {
28
+ operators?: {
29
+ toOptions: () => any;
30
+ };
31
+ renderers?: Record<string, Function>;
32
+ validate?: Function;
33
+ }): {
34
+ mode: string;
35
+ rules: ({
36
+ match: string;
37
+ mode: string;
38
+ body: {
39
+ $if: (string | (string | {
40
+ class: string;
41
+ })[] | {
42
+ $if: (string | (string | (string | {}[] | {}[] | {
43
+ class: string;
44
+ })[] | {
45
+ class: string;
46
+ })[] | {
47
+ $if: (string | (string | {
48
+ class: string;
49
+ })[] | {
50
+ $if: (string | (string | {
51
+ $apply: string;
52
+ }[] | {
53
+ class: string;
54
+ })[] | (string | {}[] | {
55
+ class: string;
56
+ })[])[];
57
+ })[];
58
+ })[];
59
+ })[];
60
+ };
61
+ } | {
62
+ match: string;
63
+ mode: string;
64
+ body: {}[];
65
+ })[];
66
+ modes: Readonly<{
67
+ play: {
68
+ unmatched: string;
69
+ };
70
+ }>;
71
+ viewModel: typeof playViewModel;
72
+ engines: Readonly<Record<string, import("../index.js").EngineDescriptor>>;
73
+ examples: readonly import("../index.js").PlayExample[];
74
+ engineIds: typeof engineIds;
75
+ runExample: (engineId: any, source: any, data: any, perCall?: {}) => import("../index.js").PlayResult;
76
+ operators: {
77
+ toOptions: () => any;
78
+ } | undefined;
79
+ };
80
+ export { playViewModel, playRules, playModes, PLAY_MODE, PLAY_BASE, ENGINES, EXAMPLES, engineIds, runExample, };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @file The playground as a JSLT view — the chrome is a document, rendered
3
+ * by the same engine as the rest of the suite. One `play` mode, rules
4
+ * matched by their ABSOLUTE slice path (`$.ui.play`), `$apply` and body
5
+ * references RELATIVE to the matched node. The host mounts the view model
6
+ * at `$.ui.play`. No imperative islands — the whole playground is data.
7
+ */
8
+ /** The one mode this view uses. */
9
+ export declare const PLAY_MODE = "play";
10
+ /** The slice the host mounts the view model at. */
11
+ export declare const PLAY_BASE = "$.ui.play";
12
+ /** The modes the host merges into the site stylesheet. */
13
+ export declare const playModes: Readonly<{
14
+ play: {
15
+ unmatched: string;
16
+ };
17
+ }>;
18
+ /** The playground's JSLT rules — spread into the site stylesheet. */
19
+ export declare const playRules: ({
20
+ match: string;
21
+ mode: string;
22
+ body: {
23
+ $if: (string | (string | {
24
+ class: string;
25
+ })[] | {
26
+ $if: (string | (string | (string | {}[] | {}[] | {
27
+ class: string;
28
+ })[] | {
29
+ class: string;
30
+ })[] | {
31
+ $if: (string | (string | {
32
+ class: string;
33
+ })[] | {
34
+ $if: (string | (string | {
35
+ $apply: string;
36
+ }[] | {
37
+ class: string;
38
+ })[] | (string | {}[] | {
39
+ class: string;
40
+ })[])[];
41
+ })[];
42
+ })[];
43
+ })[];
44
+ };
45
+ } | {
46
+ match: string;
47
+ mode: string;
48
+ body: {}[];
49
+ })[];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @param {{ play?: any }} state
3
+ * @returns {any}
4
+ */
5
+ export declare function playViewModel(state: {
6
+ play?: any;
7
+ }): any;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @file The playground's engine descriptors — each wraps a real shipped
3
+ * `@jarenjs` compiler into a pure `run(source, data, options) → PlayResult`.
4
+ * `sourcePanes` are the engine input(s); `dataPanes` are the JSON it runs
5
+ * against (empty for source-only engines, which land in a later order).
6
+ * Registered operators reach the `query`/`jslt` engines through
7
+ * `options.operators` (a `.toOptions()` registry) — the host opt-in.
8
+ */
9
+ /** @type {import('./index.js').EngineDescriptor[]} */
10
+ export declare const ENGINE_LIST: import('./index.js').EngineDescriptor[];
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @file The playground's curated example library — the canonical home for
3
+ * the suite's engine examples (folded from the website + more). Each
4
+ * example presets its engine's source pane(s) and a LIST of datasets: one
5
+ * dataset shows a single run; several turn on a switcher, so the same
6
+ * source runs over each shape (see the `-shapes` / `-inputs` examples).
7
+ */
8
+ /** @type {import('./index.js').PlayExample[]} */
9
+ export declare const EXAMPLE_LIST: import('./index.js').PlayExample[];
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @file Display formatting shared by the engine layer (the deep "how it
3
+ * ran" stat cards) and the component layer (the stage's timing line).
4
+ */
5
+ /**
6
+ * A measured duration as a stage label. Anything that is not a number —
7
+ * including the `null` an engine reports for a phase it does not have, or
8
+ * one the host never measured — is an em dash rather than a fabricated
9
+ * `0`, which would read as "ran in no time" instead of "not measured".
10
+ * Sub-hundredth-millisecond work prints as a floor: two decimals is the
11
+ * resolution the cards claim, and `0.00 ms` would over-claim it.
12
+ * @param {unknown} ms
13
+ * @returns {string}
14
+ */
15
+ export declare function formatMs(ms: unknown): string;
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @file `@jarenjs/play` — a JSON-engine playground. Pick an engine, feed
3
+ * it a source input and one or more datasets, and watch it run — a way to
4
+ * understand an engine standalone before composing it in the studio.
5
+ *
6
+ * The model (PLAY-FORMAT.md): each engine is a DESCRIPTOR — the panes it
7
+ * consumes (`sourcePanes` = the input; `dataPanes` = the JSON it runs
8
+ * against, possibly none) plus a pure `run`. Each EXAMPLE presets those
9
+ * source panes plus a LIST of datasets: 0 = the engine takes no data
10
+ * (markdown/mermaid), 1 = one dataset, N = a dataset switcher (the same
11
+ * source over several shapes). The engine registry and the example library
12
+ * fill in over the next orders; this is the headless core the component
13
+ * renders.
14
+ */
15
+ export type EnginePane = {
16
+ /**
17
+ * - the pane's id (e.g. 'selector', 'data')
18
+ */
19
+ key: string;
20
+ label: string;
21
+ control?: 'code' | 'text';
22
+ };
23
+ export type OptionPane = {
24
+ key: string;
25
+ label: string;
26
+ choices: Array<{
27
+ value: string;
28
+ label: string;
29
+ }>;
30
+ /**
31
+ * - the value used until the user picks another
32
+ */
33
+ default: string;
34
+ };
35
+ export type Panel = {
36
+ /**
37
+ * - unique within the result (the tab key)
38
+ */
39
+ id: string;
40
+ /**
41
+ * - the tab label (defaults to `id`)
42
+ */
43
+ label?: string;
44
+ kind: 'code' | 'view' | 'table' | 'note' | 'cards';
45
+ /**
46
+ * - `deep` panels are the engine's rich
47
+ * explainers, hidden until the student drills in via the depth toggle;
48
+ * defaults to `simple` (the calm default view)
49
+ */
50
+ depth?: 'simple' | 'deep';
51
+ /**
52
+ * - `code` / `note`: the text body
53
+ */
54
+ text?: string;
55
+ /**
56
+ * - `view`: a host-rendered vnode, spliced verbatim
57
+ */
58
+ vnode?: any;
59
+ /**
60
+ * - `table`: the header labels
61
+ */
62
+ columns?: string[];
63
+ /**
64
+ * - `table`: cells, row-major
65
+ */
66
+ rows?: Array<Array<any>>;
67
+ /**
68
+ * - `note`: the callout tone
69
+ */
70
+ tone?: 'ok' | 'warn' | 'info';
71
+ /**
72
+ * `cards`: a row of stat cards (matches, compile/run timings, …)
73
+ */
74
+ items?: Array<{
75
+ title: string;
76
+ value: string;
77
+ note?: string;
78
+ }>;
79
+ };
80
+ export type PlayResult = {
81
+ ok: boolean;
82
+ timing: {
83
+ compileMs: number;
84
+ runMs: number;
85
+ } | null;
86
+ error: {
87
+ message: string;
88
+ code?: string;
89
+ path?: string;
90
+ } | null;
91
+ /**
92
+ * - the result screens (`[]` on error); a single
93
+ * `code` panel for most engines, several for the richer ones
94
+ */
95
+ panels: Panel[];
96
+ };
97
+ export type EngineDescriptor = {
98
+ id: string;
99
+ label: string;
100
+ /**
101
+ * - one line describing the engine
102
+ */
103
+ lead?: string;
104
+ /**
105
+ * - the engine INPUT pane(s)
106
+ */
107
+ sourcePanes: EnginePane[];
108
+ /**
109
+ * - the JSON it runs against (may be [])
110
+ */
111
+ dataPanes: EnginePane[];
112
+ /**
113
+ * - live mode selects (may be absent)
114
+ */
115
+ optionPanes?: OptionPane[];
116
+ run: (source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult;
117
+ };
118
+ export type RunOptions = {
119
+ /**
120
+ * - a host operator registry ({ toOptions() }) for query/jslt/jtlt
121
+ */
122
+ operators?: any;
123
+ /**
124
+ * - the current option-pane values
125
+ */
126
+ config?: Record<string, string>;
127
+ /**
128
+ * host-injected vnode renderers keyed by engine id — the visual engines
129
+ * (markdown/mermaid/charts) delegate their rendering here (the hybrid seam),
130
+ * so the package owns the descriptors + examples but stays dependency-light.
131
+ * A renderer receives the source text plus its second argument — the
132
+ * option-pane config for the visual engines, the PARSED data document for
133
+ * `mdx` — and returns the preview vnode, or `{ vnode, deep }` where `deep`
134
+ * is extra `Panel`s (AST, canonical round-trip) the host derives — shown
135
+ * only behind the depth toggle
136
+ */
137
+ renderers?: Record<string, (source: string, config?: any) => any>;
138
+ /**
139
+ * host-injected JSON Schema validator (the same seam) — the `validate`
140
+ * engine delegates here so
141
+ */
142
+ validate?: (schemaText: string, data: any, locale: string) => {
143
+ schemaError: string | null;
144
+ draft: string;
145
+ compileMs: number | null;
146
+ validateMs: number | null;
147
+ valid: boolean | null;
148
+ errors: any[];
149
+ };
150
+ };
151
+ export type PlayExample = {
152
+ id: string;
153
+ label: string;
154
+ /**
155
+ * - an engine id
156
+ */
157
+ engine: string;
158
+ /**
159
+ * - presets the source pane(s)
160
+ */
161
+ source: Record<string, string>;
162
+ /**
163
+ * the JSON to run against — `[]` for a source-only engine (josl/csv/md),
164
+ * one for a single run, several for a switcher
165
+ */
166
+ datasets: Array<{
167
+ label: string;
168
+ data: Record<string, string>;
169
+ }>;
170
+ /**
171
+ * - presets option-pane values
172
+ */
173
+ config?: Record<string, string>;
174
+ };
175
+ /**
176
+ * @typedef {Object} EnginePane
177
+ * @property {string} key - the pane's id (e.g. 'selector', 'data')
178
+ * @property {string} label
179
+ * @property {'code' | 'text'} [control]
180
+ */
181
+ /**
182
+ * @typedef {Object} OptionPane
183
+ * A live select above the editors — a mode an engine runs in (JOSL vs TOML,
184
+ * CSV strict vs repair). Its value lives in the host's `config` slice.
185
+ * @property {string} key
186
+ * @property {string} label
187
+ * @property {Array<{ value: string, label: string }>} choices
188
+ * @property {string} default - the value used until the user picks another
189
+ */
190
+ /**
191
+ * @typedef {Object} Panel
192
+ * A single result SCREEN. Every ok run yields at least one; a multi-screen
193
+ * engine (CSV: a summary note, the parsed table, the CSV round-trip) yields
194
+ * several, which the component shows behind a tab strip.
195
+ * @property {string} id - unique within the result (the tab key)
196
+ * @property {string} [label] - the tab label (defaults to `id`)
197
+ * @property {'code' | 'view' | 'table' | 'note' | 'cards'} kind
198
+ * @property {'simple' | 'deep'} [depth] - `deep` panels are the engine's rich
199
+ * explainers, hidden until the student drills in via the depth toggle;
200
+ * defaults to `simple` (the calm default view)
201
+ * @property {string} [text] - `code` / `note`: the text body
202
+ * @property {any} [vnode] - `view`: a host-rendered vnode, spliced verbatim
203
+ * @property {string[]} [columns] - `table`: the header labels
204
+ * @property {Array<Array<any>>} [rows] - `table`: cells, row-major
205
+ * @property {'ok' | 'warn' | 'info'} [tone] - `note`: the callout tone
206
+ * @property {Array<{ title: string, value: string, note?: string }>} [items]
207
+ * `cards`: a row of stat cards (matches, compile/run timings, …)
208
+ */
209
+ /**
210
+ * @typedef {Object} PlayResult
211
+ * @property {boolean} ok
212
+ * @property {{ compileMs: number, runMs: number } | null} timing
213
+ * @property {{ message: string, code?: string, path?: string } | null} error
214
+ * @property {Panel[]} panels - the result screens (`[]` on error); a single
215
+ * `code` panel for most engines, several for the richer ones
216
+ */
217
+ /**
218
+ * @typedef {Object} EngineDescriptor
219
+ * @property {string} id
220
+ * @property {string} label
221
+ * @property {string} [lead] - one line describing the engine
222
+ * @property {EnginePane[]} sourcePanes - the engine INPUT pane(s)
223
+ * @property {EnginePane[]} dataPanes - the JSON it runs against (may be [])
224
+ * @property {OptionPane[]} [optionPanes] - live mode selects (may be absent)
225
+ * @property {(source: Record<string, string>, data: Record<string, string>, options?: RunOptions) => PlayResult} run
226
+ */
227
+ /**
228
+ * @typedef {Object} RunOptions
229
+ * @property {any} [operators] - a host operator registry ({ toOptions() }) for query/jslt/jtlt
230
+ * @property {Record<string, string>} [config] - the current option-pane values
231
+ * @property {Record<string, (source: string, config?: any) => any>} [renderers]
232
+ * host-injected vnode renderers keyed by engine id — the visual engines
233
+ * (markdown/mermaid/charts) delegate their rendering here (the hybrid seam),
234
+ * so the package owns the descriptors + examples but stays dependency-light.
235
+ * A renderer receives the source text plus its second argument — the
236
+ * option-pane config for the visual engines, the PARSED data document for
237
+ * `mdx` — and returns the preview vnode, or `{ vnode, deep }` where `deep`
238
+ * is extra `Panel`s (AST, canonical round-trip) the host derives — shown
239
+ * only behind the depth toggle
240
+ * @property {(schemaText: string, data: any, locale: string) => { schemaError: string|null, draft: string, compileMs: number|null, validateMs: number|null, valid: boolean|null, errors: any[] }} [validate]
241
+ * host-injected JSON Schema validator (the same seam) — the `validate`
242
+ * engine delegates here so @jarenjs/validate + the locale packs stay in the host
243
+ */
244
+ /**
245
+ * @typedef {Object} PlayExample
246
+ * @property {string} id
247
+ * @property {string} label
248
+ * @property {string} engine - an engine id
249
+ * @property {Record<string, string>} source - presets the source pane(s)
250
+ * @property {Array<{ label: string, data: Record<string, string> }>} datasets
251
+ * the JSON to run against — `[]` for a source-only engine (josl/csv/md),
252
+ * one for a single run, several for a switcher
253
+ * @property {Record<string, string>} [config] - presets option-pane values
254
+ */
255
+ /** The registered engines, by id. */
256
+ export declare const ENGINES: Readonly<Record<string, EngineDescriptor>>;
257
+ /** The ids of the registered engines, in registration order. */
258
+ export declare function engineIds(): string[];
259
+ /** The curated example library (the canonical home for the suite's engine
260
+ * examples). */
261
+ export declare const EXAMPLES: readonly PlayExample[];
262
+ /**
263
+ * Run one engine over a source + data. An unknown engine (or a throwing
264
+ * runner) yields an error Result — this never throws.
265
+ * @param {string} engineId
266
+ * @param {Record<string, string>} source
267
+ * @param {Record<string, string>} data
268
+ * @param {RunOptions} [options] - operators (query/jslt), the option-pane
269
+ * config, and host renderers (markdown/mermaid/charts)
270
+ * @returns {PlayResult}
271
+ */
272
+ export declare function runExample(engineId: string, source: Record<string, string>, data: Record<string, string>, options?: RunOptions): PlayResult;
@@ -0,0 +1,156 @@
1
+ # PLAY-FORMAT — the engine descriptor and example contract
2
+
3
+ `@jarenjs/play` models every engine the same way, so the playground UI
4
+ (the example picker, the source panes, the dataset switcher, the run stage)
5
+ is engine-agnostic and adding an engine is data, not UI code.
6
+
7
+ ## §1 The engine descriptor
8
+
9
+ ```
10
+ EngineDescriptor = {
11
+ id: string, // 'path' | 'pointer' | 'query' | 'jslt' | …
12
+ label: string,
13
+ lead?: string, // one line describing the engine
14
+ sourcePanes: EnginePane[], // the engine INPUT (usually one)
15
+ dataPanes: EnginePane[], // the JSON it runs against (may be empty)
16
+ run: (source, data) => PlayResult,
17
+ }
18
+ EnginePane = { key: string, label: string, control?: 'code' | 'text' }
19
+ ```
20
+
21
+ - `sourcePanes` are the engine's input(s): a JSONPath `selector`, a JSLT
22
+ `stylesheet`, a JSON `patch`, a query + its `externals`, a markdown
23
+ `source`. Editable in the playground.
24
+ - `dataPanes` are the JSON the engine runs against: usually one `data`; a
25
+ patch's is its `target`; markdown / mermaid / charts have **none** — the
26
+ source is everything.
27
+ - `run(source, data)` is PURE and NEVER throws: it wraps the real shipped
28
+ compiler and returns a `PlayResult`. `source` / `data` are maps keyed
29
+ by the pane `key`s, holding the raw text.
30
+
31
+ ## §2 The result
32
+
33
+ ```
34
+ PlayResult = {
35
+ ok: boolean,
36
+ // EITHER half may be null: an engine with no compile step (patch merge
37
+ // and diff) and a phase the host did not report are both "no number",
38
+ // and the stage omits that clause rather than printing `0 ms` — which
39
+ // read as "it was free". Only measured phases are ever shown.
40
+ timing: { compileMs: number | null, runMs: number | null } | null,
41
+ error: { message: string, code?, path? } | null,
42
+ panels: Panel[], // the result SCREENS ([] on error)
43
+ }
44
+
45
+ Panel = {
46
+ id: string, // unique in the result (the tab key)
47
+ label?: string, // the tab label (defaults to id)
48
+ kind: 'code' | 'view' | 'table' | 'note' | 'cards',
49
+ depth?: 'simple' | 'deep', // 'deep' → hidden behind the depth toggle
50
+ // per kind:
51
+ text?: string, // code / note body
52
+ vnode?: any, // view: a host-rendered vnode, spliced verbatim
53
+ columns?: string[], rows?: any[][], // table
54
+ tone?: 'ok' | 'warn' | 'info', // note callout tone
55
+ items?: Array<{ title, value, note? }>, // cards: a row of stat cards
56
+ }
57
+ ```
58
+
59
+ Every ok run yields **at least one** panel — most engines a single `code`
60
+ panel. The `simple` panels are the calm default: the component shows one
61
+ inline, or — for more than one — a tab strip above the active panel body.
62
+ The `deep` panels are the engine's rich explainers (match cards, a
63
+ geometry-free AST, a compiled program, a canonical round-trip): they stay
64
+ hidden until the learner opens the **depth toggle** ("Explain ▸"), which
65
+ reveals them as their own tab row beside the answer on desktop and as a
66
+ full-pane swap (with a ← back) on a phone. **CSV** is the richest example:
67
+ a `note` summary is the calm answer; the parsed `table`, the dialect and
68
+ repairs tables and the round-trip `code` ride behind the toggle. The
69
+ playground owns this small render vocabulary (code block, spliced vnode,
70
+ table, callout, stat cards, coded error line), so it never depends on a
71
+ host's node helpers.
72
+
73
+ ## §3 The example, and the dataset problem
74
+
75
+ ```
76
+ PlayExample = {
77
+ id: string,
78
+ label: string,
79
+ engine: string, // an engine id
80
+ source: Record<paneKey, string>, // presets the source pane(s)
81
+ datasets: Array<{ label: string, data: Record<paneKey, string> }>,
82
+ }
83
+ ```
84
+
85
+ An example presets the source, and carries a **list** of named datasets.
86
+ The list length is the answer to "one data or many":
87
+
88
+ - **0 datasets** — the engine takes no data (markdown / mermaid): no data
89
+ pane, no switcher.
90
+ - **1 dataset** — one data pane, no switcher.
91
+ - **N datasets** — a **dataset switcher** appears: run the same source over
92
+ dataset A vs. B vs. C without touching the source (e.g. one JSONPath
93
+ selector across three document shapes). Switching keeps the source;
94
+ editing a dataset re-runs.
95
+
96
+ ## §4 Extending
97
+
98
+ A new engine is one `EngineDescriptor` (registered in `ENGINES`) plus its
99
+ `PlayExample`s (added to `EXAMPLES`). No UI changes: the picker groups
100
+ examples by engine, the panes render from `sourcePanes`/`dataPanes`, and
101
+ the switcher appears whenever an example has ≥ 2 datasets.
102
+
103
+ ## §5 Host-injected seams (the dependency-light contract)
104
+
105
+ Engines whose real work is heavy or opinionated keep the package free of that
106
+ weight by delegating to a host-injected function on `RunOptions`, the same
107
+ shape for each:
108
+
109
+ - `renderers[id]` — the visual engines (markdown / mermaid / charts / mdx)
110
+ hand their source to a host renderer that returns a spliced `view` vnode —
111
+ or `{ vnode, deep, compileMs?, runMs? }`, where `deep` is extra panels only
112
+ the host can derive (the JSON AST, the canonical round-trip), shown behind
113
+ the depth toggle — so the package never imports `@jarenjs/md`, `/mermaid`
114
+ or `/charts`. The renderer's second argument is the option-pane config,
115
+ except `mdx`, which receives the PARSED data document (markdown × data is
116
+ a two-input engine). A renderer that reports `compileMs`/`runMs` is
117
+ believed; one that does not leaves the package holding a single
118
+ wall-clock number for the whole call, which it attributes to the RUN and
119
+ leaves the compile `null` — it never invents a figure it did not measure.
120
+ - `validate` — the JSON Schema engine hands `(schemaText, data, locale)` to a
121
+ host validator that returns `{ valid, errors, draft, compileMs, validateMs,
122
+ schemaError }` (errors already localized), so the compiled validator and the
123
+ `@jarenjs/locales` packs stay in the host.
124
+ - `operators` — a registry threaded to the `query`/`jslt`/`jtlt` engines.
125
+
126
+ A missing seam is an honest error Result (`PLAY_NO_RENDERER` /
127
+ `PLAY_NO_VALIDATOR`), never a throw. Read-only result panels are
128
+ self-contained; interactive panels (a generated form) are host-wired.
129
+
130
+ ## §6 The session file
131
+
132
+ A session is saveable, shareable and — because a share link has a length
133
+ ceiling — **downloadable**. The file is a small self-describing envelope:
134
+
135
+ ```
136
+ PlaySessionFile = {
137
+ $play: '0.1',
138
+ name: string, // the session title
139
+ session: { engine, exampleId, source, data, config },
140
+ }
141
+ ```
142
+
143
+ Two rules make the round trip safe. Reading is **liberal**: a bare
144
+ `session` object loads too, because that is what a share token decodes to
145
+ and what a hand-written file is likely to be. Reading is also
146
+ **untrusting**: every field goes through the same coercion a share token
147
+ does, so a foreign or hostile document lands as safe defaults rather than
148
+ reaching an engine, and a document naming no engine is refused outright —
149
+ the session in progress survives a bad file instead of being replaced by
150
+ it.
151
+
152
+ The host supplies the two capabilities (a download and a file picker); a
153
+ host that has neither still runs the surface, and the affordances say so
154
+ rather than failing silently. This is the path that makes the oversized-
155
+ share refusal honest: the link is declined, and the same session is
156
+ offered as a file the import side can read back.