@jarenjs/play 0.34.0 → 0.34.2

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 CHANGED
@@ -23,7 +23,13 @@ Play is a **student tool**: it opens calm — one clean result per run — and
23
23
  drills deeper on demand. An engine's rich explainers (match cards, the
24
24
  compiled program, a geometry-free AST, a canonical round-trip) are `deep`
25
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.
26
+ the answer on desktop, as a full-pane swap with a ← back on a phone. And
27
+ when a run fails, the error says **where**: the editor it is about is
28
+ marked invalid and its label carries the compiler's own location — the
29
+ rule of the stylesheet (`at /rules/0/match`), the token of the selector
30
+ (`at position 7`), the line of the document (`at line 2, column 5`) — so
31
+ the learner is pointed at the text to fix, not just told it is wrong
32
+ ([PLAY-FORMAT §2](./docs/PLAY-FORMAT.md#2-the-result)).
27
33
 
28
34
  ## The model
29
35
 
@@ -77,17 +77,58 @@ export type Panel = {
77
77
  note?: string;
78
78
  }>;
79
79
  };
80
+ export type PlayError = {
81
+ /**
82
+ * - the compiler's message, verbatim (a coded
83
+ * error composes `code: reason at path` itself)
84
+ */
85
+ message: string;
86
+ /**
87
+ * - the stable diagnosis code, when there is one
88
+ */
89
+ code?: string;
90
+ /**
91
+ * - the pane KEY (source or data) the error is
92
+ * about: the source pane for a compile or run failure, the data pane
93
+ * whose JSON did not parse
94
+ */
95
+ pane?: string;
96
+ /**
97
+ * - JSON Pointer into that pane's DOCUMENT (a
98
+ * coded error's `docPath`: the failing patch operation, the query
99
+ * construct, the stylesheet rule)
100
+ */
101
+ path?: string;
102
+ /**
103
+ * - JSON Pointer into the DATA the document
104
+ * was applied to, when a runtime error blames both (a patch operation
105
+ * AND the target location it failed at)
106
+ */
107
+ dataPath?: string;
108
+ /**
109
+ * - 0-based offset into that pane's TEXT
110
+ * (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
111
+ */
112
+ position?: number;
113
+ /**
114
+ * - 1-based line (the JOSL / CSV family)
115
+ */
116
+ line?: number;
117
+ /**
118
+ * - 1-based column (with `line`)
119
+ */
120
+ column?: number;
121
+ };
80
122
  export type PlayResult = {
81
123
  ok: boolean;
82
124
  timing: {
83
125
  compileMs: number;
84
126
  runMs: number;
85
127
  } | null;
86
- error: {
87
- message: string;
88
- code?: string;
89
- path?: string;
90
- } | null;
128
+ /**
129
+ * - null on an ok run
130
+ */
131
+ error: PlayError | null;
91
132
  /**
92
133
  * - the result screens (`[]` on error); a single
93
134
  * `code` panel for most engines, several for the richer ones
@@ -206,11 +247,37 @@ export type PlayExample = {
206
247
  * @property {Array<{ title: string, value: string, note?: string }>} [items]
207
248
  * `cards`: a row of stat cards (matches, compile/run timings, …)
208
249
  */
250
+ /**
251
+ * The error half of a Result: what the compiler said, plus WHERE — in the
252
+ * format's own fields, so the view can point at the pane and the location
253
+ * rather than only quoting the message. Every location field is present
254
+ * exactly when the compiler stated it (a field is never fabricated), and
255
+ * `pane` names the editor the location points into; a location into a
256
+ * document the learner never typed (XQuery's generated query document) has
257
+ * no pane. See PLAY-FORMAT §2.
258
+ * @typedef {Object} PlayError
259
+ * @property {string} message - the compiler's message, verbatim (a coded
260
+ * error composes `code: reason at path` itself)
261
+ * @property {string} [code] - the stable diagnosis code, when there is one
262
+ * @property {string} [pane] - the pane KEY (source or data) the error is
263
+ * about: the source pane for a compile or run failure, the data pane
264
+ * whose JSON did not parse
265
+ * @property {string} [path] - JSON Pointer into that pane's DOCUMENT (a
266
+ * coded error's `docPath`: the failing patch operation, the query
267
+ * construct, the stylesheet rule)
268
+ * @property {string} [dataPath] - JSON Pointer into the DATA the document
269
+ * was applied to, when a runtime error blames both (a patch operation
270
+ * AND the target location it failed at)
271
+ * @property {number} [position] - 0-based offset into that pane's TEXT
272
+ * (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
273
+ * @property {number} [line] - 1-based line (the JOSL / CSV family)
274
+ * @property {number} [column] - 1-based column (with `line`)
275
+ */
209
276
  /**
210
277
  * @typedef {Object} PlayResult
211
278
  * @property {boolean} ok
212
279
  * @property {{ compileMs: number, runMs: number } | null} timing
213
- * @property {{ message: string, code?: string, path?: string } | null} error
280
+ * @property {PlayError | null} error - null on an ok run
214
281
  * @property {Panel[]} panels - the result screens (`[]` on error); a single
215
282
  * `code` panel for most engines, several for the richer ones
216
283
  */
@@ -38,10 +38,25 @@ PlayResult = {
38
38
  // and the stage omits that clause rather than printing `0 ms` — which
39
39
  // read as "it was free". Only measured phases are ever shown.
40
40
  timing: { compileMs: number | null, runMs: number | null } | null,
41
- error: { message: string, code?, path? } | null,
41
+ error: PlayError | null,
42
42
  panels: Panel[], // the result SCREENS ([] on error)
43
43
  }
44
44
 
45
+ PlayError = {
46
+ message: string, // what the compiler said, verbatim
47
+ code?: string, // its stable diagnosis code, when it has one
48
+ // WHERE — the location half, in the format's own fields. A field is
49
+ // present exactly when the compiler stated it; nothing is fabricated.
50
+ pane?: string, // the pane KEY the error is about: the source pane
51
+ // for a compile or run failure, the data pane
52
+ // whose JSON did not parse
53
+ path?: string, // JSON Pointer into that pane's DOCUMENT
54
+ dataPath?: string, // JSON Pointer into the DATA the document ran over
55
+ position?: number, // 0-based offset into that pane's TEXT
56
+ line?: number, // 1-based line …
57
+ column?: number, // … and column
58
+ }
59
+
45
60
  Panel = {
46
61
  id: string, // unique in the result (the tab key)
47
62
  label?: string, // the tab label (defaults to id)
@@ -56,6 +71,36 @@ Panel = {
56
71
  }
57
72
  ```
58
73
 
74
+ An error says **where**, because for a learner the location is the lesson:
75
+ which token of the selector, which operation of the patch, which rule of
76
+ the stylesheet. The compilers already carry it, in three shapes, and the
77
+ error copies whichever its compiler stated: the **coded family** (patch,
78
+ `$query`, JSLT, JTLT — `@jarenjs/core`'s `CodedError`) states `path`, a
79
+ JSON Pointer into the document being compiled or run (`/rules/0/match`,
80
+ `/$idiv`, `/0/path`), and a patch *runtime* error adds `dataPath`, the
81
+ target location its operation failed at — two facts, both kept; the
82
+ **syntax family** (JSONPath, JSON Pointer, XQuery) states `position`, a
83
+ 0-based offset into the source text; the **line/column family** (JOSL,
84
+ CSV) states 1-based `line` and `column`. `pane` names the editor the
85
+ location points into, and it is the whole location for a pane whose JSON
86
+ does not parse: the host's `JSON.parse` states its offset only inside
87
+ engine-specific message text, never as a field, so the pane is claimed and
88
+ nothing finer. Two honest gaps: a missing host seam (`PLAY_NO_RENDERER`,
89
+ `PLAY_NO_VALIDATOR`) is nobody's pane, and XQuery compiles a *generated*
90
+ query document, so a location after its parse phase is a pointer into a
91
+ document the learner never typed — stated, with no pane. `message` stays
92
+ verbatim (a coded error composes `code: reason at path` itself), so a
93
+ host with no structured reader still gets everything.
94
+
95
+ The component lands it in two places: the editor the error is about is
96
+ marked `aria-invalid` (the state the renderer owns; the ring is styled off
97
+ it) and its label carries the phrase — `at /rules/0/match`, `at position
98
+ 7`, `at line 2, column 5` — while the stage's error line shows the code
99
+ chip, the message with its own code prefix dropped rather than read twice,
100
+ and the pane's label. A data pane a runtime error merely failed *at* is
101
+ not marked invalid (the op was wrong, not the target) but shows the
102
+ `dataPath` phrase, because that is where the reader looks next.
103
+
59
104
  Every ok run yields **at least one** panel — most engines a single `code`
60
105
  panel. The `simple` panels are the calm default: the component shows one
61
106
  inline, or — for more than one — a tab strip above the active panel body.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/play",
3
3
  "private": false,
4
- "version": "0.34.0",
4
+ "version": "0.34.2",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -56,9 +56,9 @@
56
56
  "prepack": "npm run build:types"
57
57
  },
58
58
  "dependencies": {
59
- "@jarenjs/core": "^0.34.0",
60
- "@jarenjs/josl": "^0.34.0",
61
- "@jarenjs/json": "^0.34.0",
62
- "@jarenjs/validate": "^0.34.0"
59
+ "@jarenjs/core": "^0.34.2",
60
+ "@jarenjs/josl": "^0.34.2",
61
+ "@jarenjs/json": "^0.34.2",
62
+ "@jarenjs/validate": "^0.34.2"
63
63
  }
64
64
  }
@@ -172,7 +172,15 @@ const shell = {
172
172
  ], ''] },
173
173
  ], ''] },
174
174
  ],
175
- ['p', { class: 'error-line' }, ['strong', {}, '$.result.error.code'], ' ', '$.result.error.message']] },
175
+ // the error line: the code chip (only when there is one), the message
176
+ // (its own code prefix dropped), and the editor the error is about —
177
+ // the pane itself is flagged and carries the location, see
178
+ // sourcePane / dataPane
179
+ ['p', { class: 'error-line' },
180
+ { $if: ['$.result.error.code', [['strong', {}, '$.result.error.code'], ' '], ''] },
181
+ '$.result.error.text',
182
+ { $if: ['$.result.error.paneLabel',
183
+ ['span', { class: 'jplay-error-pane' }, ' — ', '$.result.error.paneLabel'], ''] }]] },
176
184
  ['p', { class: 'muted jplay-hint' }, 'Pick an example, or edit the source or data — it runs live.']] },
177
185
  ],
178
186
  ],
@@ -197,15 +205,24 @@ const railExample = {
197
205
  }, '$.label'],
198
206
  };
199
207
 
208
+ /** A pane label: the name, plus the last error's location in this pane
209
+ * (`at /rules/3`, `at position 7`, `at line 2, column 5`) when it has one. */
210
+ const paneLabel = ['span', { class: 'jplay-pane-label muted' }, '$.label',
211
+ { $if: ['$.where', ['span', { class: 'jplay-pane-where' }, ' · ', '$.where'], ''] }];
212
+
213
+ /** The editor the last error is about says so — `aria-invalid` is the
214
+ * state the renderer owns (the ring is styled off it), absent otherwise. */
215
+ const invalid = { $if: ['$.invalid', 'true'] };
216
+
200
217
  /** One source editor (a `text` control renders an input, else a textarea). */
201
218
  const sourcePane = {
202
219
  match: `${PLAY_BASE}.sourcePanes[*]`, mode: PLAY_MODE,
203
220
  body: ['label', { class: 'jplay-pane' },
204
- ['span', { class: 'jplay-pane-label muted' }, '$.label'],
221
+ paneLabel,
205
222
  { $if: [{ $eq: ['$.control', 'text'] },
206
- ['input', { type: 'text', class: 'editor line', spellcheck: 'false', value: '$.value',
223
+ ['input', { type: 'text', class: 'editor line', spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
207
224
  on: { input: { action: 'play/source', with: { key: '$.key' } } } }],
208
- ['textarea', { class: 'editor', rows: 6, spellcheck: 'false', value: '$.value',
225
+ ['textarea', { class: 'editor', rows: 6, spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
209
226
  on: { input: { action: 'play/source', with: { key: '$.key' } } } }]] },
210
227
  ],
211
228
  };
@@ -214,8 +231,8 @@ const sourcePane = {
214
231
  const dataPane = {
215
232
  match: `${PLAY_BASE}.dataPanes[*]`, mode: PLAY_MODE,
216
233
  body: ['label', { class: 'jplay-pane' },
217
- ['span', { class: 'jplay-pane-label muted' }, '$.label'],
218
- ['textarea', { class: 'editor', rows: 8, spellcheck: 'false', value: '$.value',
234
+ paneLabel,
235
+ ['textarea', { class: 'editor', rows: 8, spellcheck: 'false', value: '$.value', 'aria-invalid': invalid,
219
236
  on: { input: { action: 'play/data', with: { key: '$.key' } } } }],
220
237
  ],
221
238
  };
@@ -27,6 +27,59 @@ function formatTiming(timing) {
27
27
  return parts.length === 0 ? null : parts.join(' · ');
28
28
  }
29
29
 
30
+ /**
31
+ * The location an error states, as the phrase a pane label carries — one
32
+ * per family, in the wording the compilers' own messages use: a JSON
33
+ * Pointer (`at /rules/3`; the root pointer `''` reads "at the root"), a
34
+ * 0-based source offset (`at position 7`), or a 1-based line and column
35
+ * (`at line 2, column 5`). Empty when the error states no location.
36
+ */
37
+ function formatWhere(err) {
38
+ if (!err) return '';
39
+ const parts = [];
40
+ if (typeof err.path === 'string') parts.push(`at ${err.path === '' ? 'the root' : err.path}`);
41
+ if (typeof err.position === 'number') parts.push(`at position ${err.position}`);
42
+ if (typeof err.line === 'number') {
43
+ parts.push(`at line ${err.line}${typeof err.column === 'number' ? `, column ${err.column}` : ''}`);
44
+ }
45
+ return parts.join(', ');
46
+ }
47
+
48
+ /**
49
+ * The error's claim on one editor pane. The pane the error names is
50
+ * INVALID (its text is what failed — the compile, the run, or a JSON parse)
51
+ * and carries the location phrase; a data pane is not invalid when a
52
+ * runtime error merely failed AT a location in it (a patch op removing a
53
+ * path the target lacks blames the op, not the target), yet that location
54
+ * is the lesson, so the data pane carries it as `where` without the flag.
55
+ * @returns {{ invalid: boolean, where: string }}
56
+ */
57
+ function paneError(err, key, isData) {
58
+ if (!err) return { invalid: false, where: '' };
59
+ if (err.pane === key) return { invalid: true, where: formatWhere(err) };
60
+ if (isData && typeof err.dataPath === 'string') {
61
+ return { invalid: false, where: `at ${err.dataPath === '' ? 'the root' : err.dataPath}` };
62
+ }
63
+ return { invalid: false, where: '' };
64
+ }
65
+
66
+ /**
67
+ * The stage's error line. `code` is shown as its own chip, so a message
68
+ * that begins with that same code (the coded family composes
69
+ * `code: reason at path`) drops the prefix rather than reading it twice;
70
+ * `paneLabel` names the editor the error is about (the pane's LABEL, for
71
+ * a reader), empty when no pane owns it.
72
+ */
73
+ function deriveError(err, panes) {
74
+ if (!err) return null;
75
+ const code = err.code ?? '';
76
+ const message = err.message ?? '';
77
+ const prefix = `${code}: `;
78
+ const text = code !== '' && message.startsWith(prefix) ? message.slice(prefix.length) : message;
79
+ const pane = typeof err.pane === 'string' ? panes.find((p) => p.key === err.pane) : undefined;
80
+ return { code, message, text, paneLabel: pane?.label ?? '' };
81
+ }
82
+
30
83
  /**
31
84
  * Shape one panel for the view: kind flags for the `$if` dispatch plus the
32
85
  * per-kind content (a `table` becomes column/cell records the JSLT can walk).
@@ -62,7 +115,7 @@ function shapePanel(p) {
62
115
  * else the first — so a fresh result with different screens never strands
63
116
  * the view on a tab that is gone.
64
117
  */
65
- function deriveResult(r, wantedId, deepWanted, deepPick) {
118
+ function deriveResult(r, wantedId, deepWanted, deepPick, panes) {
66
119
  const all = Array.isArray(r.panels) ? r.panels : [];
67
120
  const panels = all.filter((p) => p.depth !== 'deep');
68
121
  const deep = all.filter((p) => p.depth === 'deep');
@@ -75,7 +128,7 @@ function deriveResult(r, wantedId, deepWanted, deepPick) {
75
128
  return {
76
129
  ran: true,
77
130
  ok: r.ok === true,
78
- error: r.error ? { code: r.error.code ?? '', message: r.error.message ?? '' } : null,
131
+ error: deriveError(r.error, panes),
79
132
  timing: formatTiming(r.timing),
80
133
  tabbed: panels.length > 1,
81
134
  tabs: panels.map((p) => ({ id: p.id, label: p.label ?? p.id, active: p.id === activeId })),
@@ -113,11 +166,16 @@ export function playViewModel(state) {
113
166
  examples: exs.map((ex) => ({ id: ex.id, label: ex.label, active: ex.id === s.exampleId })),
114
167
  }));
115
168
 
169
+ // the editors, each carrying the last error's claim on it (an invalid
170
+ // flag + a location phrase) so the pane the learner has to fix says so
171
+ const err = s.result?.error ?? null;
116
172
  const sourcePanes = (engine?.sourcePanes ?? []).map((p) => ({
117
173
  key: p.key, label: p.label, control: p.control ?? 'code', value: s.source?.[p.key] ?? '',
174
+ ...paneError(err, p.key, false),
118
175
  }));
119
176
  const dataPanes = (engine?.dataPanes ?? []).map((p) => ({
120
177
  key: p.key, label: p.label, value: s.data?.[p.key] ?? '',
178
+ ...paneError(err, p.key, true),
121
179
  }));
122
180
 
123
181
  // option panes: live mode selects (josl dialect, csv repair/headers/…);
@@ -133,7 +191,7 @@ export function playViewModel(state) {
133
191
  const datasets = (active?.datasets ?? []).map((ds, i) => ({ index: i, label: ds.label, active: i === datasetIndex }));
134
192
 
135
193
  const r = s.result ?? null;
136
- const result = r === null ? { ran: false } : deriveResult(r, s.panel, s.deep, s.deepPick);
194
+ const result = r === null ? { ran: false } : deriveResult(r, s.panel, s.deep, s.deepPick, [...sourcePanes, ...dataPanes]);
137
195
 
138
196
  // the IDE half: the saveable-session chrome
139
197
  const names = Array.isArray(s.names) ? s.names : [];
package/src/engines.js CHANGED
@@ -27,10 +27,18 @@ const fmt = (v) => (v === undefined ? '(no result)' : JSON.stringify(v, null, 2)
27
27
  const msg = (err) => String(/** @type {any} */ (err)?.message ?? err);
28
28
  const code = (err) => /** @type {any} */ (err)?.code;
29
29
 
30
- /** Parse a pane's JSON text; `{ value }` or `{ error }`. */
31
- function parseJson(text, label) {
30
+ /**
31
+ * Parse a pane's JSON text; `{ value }` or `{ error, pane }`. `pane` is the
32
+ * pane KEY (what the result's error location names); `label` is the word
33
+ * the message uses for it, which differs where the pane's label does (the
34
+ * patch engine's `data` pane is its "target"). The host's `JSON.parse`
35
+ * states the offending offset only inside its engine-specific message
36
+ * text, never as a field — so a parse failure locates the PANE and
37
+ * nothing finer.
38
+ */
39
+ function parseJson(text, pane, label = pane) {
32
40
  try { return { value: JSON.parse((text ?? 'null') === '' ? 'null' : text) }; }
33
- catch (err) { return { error: `${label}: ${msg(err)}` }; }
41
+ catch (err) { return { error: `${label}: ${msg(err)}`, pane }; }
34
42
  }
35
43
 
36
44
  /** A single-`code`-panel Result — the shape most engines return. */
@@ -85,7 +93,7 @@ function visual(id, label, lead, opts = {}) {
85
93
  const t0 = now();
86
94
  let view;
87
95
  try { view = render(source.source ?? '', options?.config); }
88
- catch (err) { return fail(msg(err), code(err)); }
96
+ catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
89
97
  const t1 = now();
90
98
  return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
91
99
  },
@@ -118,8 +126,50 @@ function renderedPanels(view) {
118
126
  : [];
119
127
  return [{ id: 'preview', label: 'Preview', kind: 'view', vnode }, ...deep];
120
128
  }
121
- /** @returns {import('./index.js').PlayResult} */
122
- const fail = (message, c, path) => ({ ok: false, timing: null, error: { message, code: c, path }, panels: [] });
129
+ /**
130
+ * An error Result. `where` is the location half of the format's error
131
+ * `pane` plus whichever of `path`/`dataPath`/`position`/`line`/`column`
132
+ * the compiler stated (see {@link locate}); only stated fields land on
133
+ * the error, so a field's presence means the compiler said it.
134
+ * @returns {import('./index.js').PlayResult}
135
+ */
136
+ const fail = (message, c, where) => ({
137
+ ok: false, timing: null,
138
+ error: { message, ...(c === undefined ? {} : { code: c }), ...where },
139
+ panels: [],
140
+ });
141
+
142
+ /** The error Result of a pane whose JSON did not parse — locates the pane. */
143
+ const failParse = (d) => fail(d.error, undefined, { pane: d.pane });
144
+
145
+ /**
146
+ * The location an engine error carries, in the format's own fields — one
147
+ * adapter over the three shapes the compilers use. The coded family
148
+ * (`@jarenjs/core` `CodedError`: patch, query, JSLT, JTLT) states a
149
+ * `docPath`, a JSON Pointer into the document being compiled or run, and
150
+ * a patch runtime error adds the `dataPath` its operation failed at in the
151
+ * target; the syntax family (JSONPath, JSON Pointer, XQuery) states a
152
+ * 0-based `position` into the source text; the line/column family (JOSL,
153
+ * CSV) states 1-based `line`/`column`. Only what the error carries is
154
+ * copied. `pane` names the editor the phase was working on — the source
155
+ * pane for compile AND run (a runtime location points into the compiled
156
+ * document, which is that pane's text) — and is omitted where the
157
+ * location is into a document the learner never typed (XQuery compiles a
158
+ * GENERATED query document, and a location in it has no pane).
159
+ * @param {unknown} err
160
+ * @param {string} [pane] the pane key the location points into
161
+ */
162
+ function locate(err, pane) {
163
+ const e = /** @type {any} */ (err);
164
+ /** @type {{ pane?: string, path?: string, dataPath?: string, position?: number, line?: number, column?: number }} */
165
+ const where = pane === undefined ? {} : { pane };
166
+ if (typeof e?.docPath === 'string') where.path = e.docPath;
167
+ if (typeof e?.dataPath === 'string') where.dataPath = e.dataPath;
168
+ if (typeof e?.position === 'number') where.position = e.position;
169
+ if (typeof e?.line === 'number') where.line = e.line;
170
+ if (typeof e?.column === 'number') where.column = e.column;
171
+ return where;
172
+ }
123
173
 
124
174
  /** The compile options the query/jslt engines run with (registry-aware). */
125
175
  function compileOptions(options) {
@@ -135,14 +185,14 @@ export const ENGINE_LIST = [
135
185
  sourcePanes: [{ key: 'selector', label: 'Selector', control: 'text' }],
136
186
  dataPanes: [{ key: 'data', label: 'Data' }],
137
187
  run(source, data) {
138
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
188
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
139
189
  let compiled; const t0 = now();
140
190
  try { compiled = compileJSONPath(source.selector ?? ''); }
141
- catch (err) { return fail(msg(err), code(err)); }
191
+ catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
142
192
  const t1 = now();
143
193
  let nodes;
144
194
  try { nodes = compiled.nodes(d.value); }
145
- catch (err) { return fail(msg(err), code(err)); }
195
+ catch (err) { return fail(msg(err), code(err), locate(err, 'selector')); }
146
196
  const t2 = now();
147
197
  return ok(fmt(nodes.map((n) => n.value)), t1 - t0, t2 - t1, [
148
198
  deepCards('how', 'How it matched', [
@@ -164,16 +214,16 @@ export const ENGINE_LIST = [
164
214
  ],
165
215
  dataPanes: [{ key: 'data', label: 'Data' }],
166
216
  run(source, data) {
167
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
217
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
168
218
  const pointer = source.pointer ?? '';
169
219
  const relative = /^\d/.test(pointer);
170
220
  let getter; const t0 = now();
171
221
  try { getter = relative ? compileRelativeJSONPointer(pointer) : compileJSONPointer(pointer); }
172
- catch (err) { return fail(msg(err), code(err)); }
222
+ catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
173
223
  const t1 = now();
174
224
  let value;
175
225
  try { value = relative ? getter(d.value, source.location ?? '') : getter(d.value); }
176
- catch (err) { return fail(msg(err), code(err)); }
226
+ catch (err) { return fail(msg(err), code(err), locate(err, 'pointer')); }
177
227
  const t2 = now();
178
228
  if (value === JSONPOINTER_NOTHING) return ok('(nothing — the pointer addresses no value)', t1 - t0, t2 - t1);
179
229
  return ok(fmt(value), t1 - t0, t2 - t1);
@@ -193,13 +243,13 @@ export const ENGINE_LIST = [
193
243
  }],
194
244
  run(source, data, options) {
195
245
  const mode = options?.config?.mode ?? 'patch';
196
- const target = parseJson(data.data, 'target'); if (target.error) return fail(target.error);
197
- const patch = parseJson(source.patch, 'patch'); if (patch.error) return fail(patch.error);
246
+ const target = parseJson(data.data, 'data', 'target'); if (target.error) return failParse(target);
247
+ const patch = parseJson(source.patch, 'patch'); if (patch.error) return failParse(patch);
198
248
  if (mode === 'merge') {
199
249
  // RFC 7396: null members delete; an unchanged document is the INPUT
200
250
  let out; const t0 = now();
201
251
  try { out = applyMergePatch(target.value, patch.value); }
202
- catch (err) { return fail(msg(err), code(err)); }
252
+ catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
203
253
  const t1 = now();
204
254
  return ok(fmt(out), null, t1 - t0, [
205
255
  deepCards('how', 'How it merged', [
@@ -215,7 +265,7 @@ export const ENGINE_LIST = [
215
265
  jsonPatch = createJSONPatch(target.value, patch.value);
216
266
  mergePatch = createMergePatch(target.value, patch.value);
217
267
  }
218
- catch (err) { return fail(msg(err), code(err)); }
268
+ catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
219
269
  const t1 = now();
220
270
  return okPanels([
221
271
  { id: 'out', label: `JSON Patch (${jsonPatch.length} ops)`, kind: 'code', text: fmt(jsonPatch) },
@@ -224,11 +274,11 @@ export const ENGINE_LIST = [
224
274
  }
225
275
  let apply; const t0 = now();
226
276
  try { apply = compileJSONPatch(patch.value, { changes: true }); }
227
- catch (err) { return fail(msg(err), code(err)); }
277
+ catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
228
278
  const t1 = now();
229
279
  let run;
230
280
  try { run = apply(target.value); }
231
- catch (err) { return fail(msg(err), code(err)); }
281
+ catch (err) { return fail(msg(err), code(err), locate(err, 'patch')); }
232
282
  const t2 = now();
233
283
  return ok(fmt(run.doc), t1 - t0, t2 - t1, [
234
284
  deepCode('changes', 'What changed', fmt(run.changes)),
@@ -240,19 +290,19 @@ export const ENGINE_LIST = [
240
290
  sourcePanes: [{ key: 'query', label: 'Query', control: 'code' }, { key: 'externals', label: 'Externals', control: 'code' }],
241
291
  dataPanes: [{ key: 'data', label: 'Data' }],
242
292
  run(source, data, options) {
243
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
244
- const q = parseJson(source.query, 'query'); if (q.error) return fail(q.error);
293
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
294
+ const q = parseJson(source.query, 'query'); if (q.error) return failParse(q);
245
295
  let externals = {};
246
296
  if (source.externals !== undefined && String(source.externals).trim() !== '') {
247
- const e = parseJson(source.externals, 'externals'); if (e.error) return fail(e.error); externals = e.value;
297
+ const e = parseJson(source.externals, 'externals'); if (e.error) return failParse(e); externals = e.value;
248
298
  }
249
299
  let fn; const t0 = now();
250
300
  try { fn = compileJsonQuery(q.value, compileOptions(options)); }
251
- catch (err) { return fail(msg(err), code(err)); }
301
+ catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
252
302
  const t1 = now();
253
303
  let out;
254
304
  try { out = fn(d.value, externals); }
255
- catch (err) { return fail(msg(err), code(err)); }
305
+ catch (err) { return fail(msg(err), code(err), locate(err, 'query')); }
256
306
  const t2 = now();
257
307
  const items = out === undefined ? 0 : Array.isArray(out) ? out.length : 1;
258
308
  return ok(fmt(out), t1 - t0, t2 - t1, [
@@ -269,15 +319,15 @@ export const ENGINE_LIST = [
269
319
  sourcePanes: [{ key: 'stylesheet', label: 'Stylesheet', control: 'code' }],
270
320
  dataPanes: [{ key: 'data', label: 'Data' }],
271
321
  run(source, data, options) {
272
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
273
- const s = parseJson(source.stylesheet, 'stylesheet'); if (s.error) return fail(s.error);
322
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
323
+ const s = parseJson(source.stylesheet, 'stylesheet'); if (s.error) return failParse(s);
274
324
  let compiled; const t0 = now();
275
325
  try { compiled = compileJsltStylesheet(s.value, compileOptions(options)); }
276
- catch (err) { return fail(msg(err), code(err)); }
326
+ catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
277
327
  const t1 = now();
278
328
  let out;
279
329
  try { out = compiled(d.value); }
280
- catch (err) { return fail(msg(err), code(err)); }
330
+ catch (err) { return fail(msg(err), code(err), locate(err, 'stylesheet')); }
281
331
  const t2 = now();
282
332
  // the identity lesson: a rule that changes nothing hands the INPUT back
283
333
  // (shared, copy-on-write) — worth teaching, so the deep card says which
@@ -295,15 +345,15 @@ export const ENGINE_LIST = [
295
345
  sourcePanes: [{ key: 'template', label: 'Template', control: 'code' }],
296
346
  dataPanes: [{ key: 'data', label: 'Data' }],
297
347
  run(source, data, options) {
298
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
299
- const t = parseJson(source.template, 'template'); if (t.error) return fail(t.error);
348
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
349
+ const t = parseJson(source.template, 'template'); if (t.error) return failParse(t);
300
350
  let render; const t0 = now();
301
351
  try { render = compileJtltStylesheet(t.value, compileOptions(options)); }
302
- catch (err) { return fail(msg(err), code(err)); }
352
+ catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
303
353
  const t1 = now();
304
354
  let out;
305
355
  try { out = render(d.value); }
306
- catch (err) { return fail(msg(err), code(err)); }
356
+ catch (err) { return fail(msg(err), code(err), locate(err, 'template')); }
307
357
  const t2 = now();
308
358
  // JTLT emits TEXT (markdown / xml / source) — show it verbatim, not fmt'd
309
359
  return ok(out === '' ? '(empty)' : out, t1 - t0, t2 - t1, [
@@ -317,20 +367,22 @@ export const ENGINE_LIST = [
317
367
  sourcePanes: [{ key: 'text', label: 'XQuery', control: 'code' }],
318
368
  dataPanes: [{ key: 'data', label: 'Data' }],
319
369
  run(source, data, options) {
320
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
370
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
321
371
  let doc, fn; const t0 = now();
322
- try {
323
- doc = parseXQuery(source.text ?? '');
324
- fn = compileJsonQuery(doc, compileOptions(options));
325
- }
326
- catch (err) { return fail(msg(err), code(err)); }
372
+ // two phases: the text parses (a syntax error locates a position in
373
+ // the XQuery pane), then the GENERATED query document compiles and
374
+ // runs a location in that document has no pane the learner typed
375
+ try { doc = parseXQuery(source.text ?? ''); }
376
+ catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
377
+ try { fn = compileJsonQuery(doc, compileOptions(options)); }
378
+ catch (err) { return fail(msg(err), code(err), locate(err)); }
327
379
  const t1 = now();
328
380
  let out;
329
381
  try {
330
382
  const externals = fn.externals.includes('doc') ? { doc: d.value } : {};
331
383
  out = fn(d.value, externals);
332
384
  }
333
- catch (err) { return fail(msg(err), code(err)); }
385
+ catch (err) { return fail(msg(err), code(err), locate(err)); }
334
386
  const t2 = now();
335
387
  return ok(out === undefined ? '(empty sequence)' : fmt(out), t1 - t0, t2 - t1, [
336
388
  // the machinery: the XQuery text parses to a runnable query DOCUMENT
@@ -355,7 +407,7 @@ export const ENGINE_LIST = [
355
407
  // parser streams document-order events as it reads — capture (capped)
356
408
  // for the "how it streamed" drill-down
357
409
  try { parsed = parseJosl(source.text ?? '', { mode, onEvent: (e) => { if (events.length < 200) events.push(e); } }); }
358
- catch (err) { return fail(msg(err), code(err)); }
410
+ catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
359
411
  const t1 = now();
360
412
  const out = stringifyJsonx(parsed, { indent: 2 });
361
413
  const t2 = now();
@@ -391,7 +443,7 @@ export const ENGINE_LIST = [
391
443
  // strict mode THROWS on the first RFC 4180 violation (with a code);
392
444
  // repair mode reads anyway and lists every fix — the lesson of the tab
393
445
  try { doc = parseCsvDocument(source.text ?? '', opts); }
394
- catch (err) { return fail(msg(err), code(err)); }
446
+ catch (err) { return fail(msg(err), code(err), locate(err, 'text')); }
395
447
  const t1 = now();
396
448
  // one calm SCREEN (the summary note) plus the drill-down: the parsed
397
449
  // records, the sniffed dialect, the repairs, and the CSV round-trip
@@ -461,12 +513,12 @@ export const ENGINE_LIST = [
461
513
  if (typeof validate !== 'function') {
462
514
  return fail('the JSON Schema engine validates in the host — inject options.validate', 'PLAY_NO_VALIDATOR');
463
515
  }
464
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
516
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
465
517
  const locale = options?.config?.locale ?? 'en';
466
518
  let report;
467
519
  try { report = validate(source.schema ?? '', d.value, locale); }
468
- catch (err) { return fail(msg(err), code(err)); }
469
- if (report.schemaError) return fail(report.schemaError, 'SCHEMA');
520
+ catch (err) { return fail(msg(err), code(err), locate(err)); }
521
+ if (report.schemaError) return fail(report.schemaError, 'SCHEMA', { pane: 'schema' });
470
522
  const errs = report.errors ?? [];
471
523
  const summary = (report.valid ? '✓ valid' : `✗ ${errs.length} error${errs.length === 1 ? '' : 's'}`)
472
524
  + ` · ${report.draft} · compiled ${formatMs(report.compileMs)} · validated ${formatMs(report.validateMs)}`;
@@ -494,11 +546,11 @@ export const ENGINE_LIST = [
494
546
  if (typeof render !== 'function') {
495
547
  return fail('the MDX engine renders in the host — inject options.renderers.mdx', 'PLAY_NO_RENDERER');
496
548
  }
497
- const d = parseJson(data.data, 'data'); if (d.error) return fail(d.error);
549
+ const d = parseJson(data.data, 'data'); if (d.error) return failParse(d);
498
550
  const t0 = now();
499
551
  let view;
500
552
  try { view = render(source.source ?? '', d.value); }
501
- catch (err) { return fail(msg(err), code(err)); }
553
+ catch (err) { return fail(msg(err), code(err), locate(err, 'source')); }
502
554
  const t1 = now();
503
555
  return okPanels(renderedPanels(view), ...renderTiming(view, t1 - t0));
504
556
  },
package/src/index.js CHANGED
@@ -54,11 +54,38 @@ import { EXAMPLE_LIST } from './examples.js';
54
54
  * `cards`: a row of stat cards (matches, compile/run timings, …)
55
55
  */
56
56
 
57
+ /**
58
+ * The error half of a Result: what the compiler said, plus WHERE — in the
59
+ * format's own fields, so the view can point at the pane and the location
60
+ * rather than only quoting the message. Every location field is present
61
+ * exactly when the compiler stated it (a field is never fabricated), and
62
+ * `pane` names the editor the location points into; a location into a
63
+ * document the learner never typed (XQuery's generated query document) has
64
+ * no pane. See PLAY-FORMAT §2.
65
+ * @typedef {Object} PlayError
66
+ * @property {string} message - the compiler's message, verbatim (a coded
67
+ * error composes `code: reason at path` itself)
68
+ * @property {string} [code] - the stable diagnosis code, when there is one
69
+ * @property {string} [pane] - the pane KEY (source or data) the error is
70
+ * about: the source pane for a compile or run failure, the data pane
71
+ * whose JSON did not parse
72
+ * @property {string} [path] - JSON Pointer into that pane's DOCUMENT (a
73
+ * coded error's `docPath`: the failing patch operation, the query
74
+ * construct, the stylesheet rule)
75
+ * @property {string} [dataPath] - JSON Pointer into the DATA the document
76
+ * was applied to, when a runtime error blames both (a patch operation
77
+ * AND the target location it failed at)
78
+ * @property {number} [position] - 0-based offset into that pane's TEXT
79
+ * (the syntax family: a JSONPath selector, a JSON Pointer, an XQuery)
80
+ * @property {number} [line] - 1-based line (the JOSL / CSV family)
81
+ * @property {number} [column] - 1-based column (with `line`)
82
+ */
83
+
57
84
  /**
58
85
  * @typedef {Object} PlayResult
59
86
  * @property {boolean} ok
60
87
  * @property {{ compileMs: number, runMs: number } | null} timing
61
- * @property {{ message: string, code?: string, path?: string } | null} error
88
+ * @property {PlayError | null} error - null on an ok run
62
89
  * @property {Panel[]} panels - the result screens (`[]` on error); a single
63
90
  * `code` panel for most engines, several for the richer ones
64
91
  */
package/styles/play.css CHANGED
@@ -70,6 +70,12 @@
70
70
  .jplay-lead { font-size: 0.8rem; }
71
71
  .jplay-pane { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
72
72
  .jplay-pane-label { font-size: 0.7rem; font-family: var(--mono, monospace); }
73
+ /* the last error's claim on a pane: the location rides on the label, and the
74
+ editor it is about carries aria-invalid — the state the renderer owns —
75
+ which is what the ring is styled off (the --fail family, no new hue) */
76
+ .jplay-pane-where { color: var(--fail, #dc2626); }
77
+ .jplay .editor[aria-invalid='true'] { border-color: var(--fail, #dc2626); }
78
+ .jplay-error-pane { color: var(--muted, #64748b); }
73
79
  .jplay-datasets { align-self: flex-start; }
74
80
  /* the validate engine's JSON ↔ form toggle + the generated-form container */
75
81
  .jplay-dataview { align-self: flex-start; }