quario 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * definition lives in `./plan.js`; what the two of them share lives in the
13
13
  * siblings beside it, one seam each. See SCHEMA.md for the document shape.
14
14
  */
15
+ import { relocate as relocateQuery } from "padvinder";
15
16
  import { MARKING, verify } from "./license.js";
16
17
  import { err, locate } from "./locate.js";
17
18
  import { BLOCKED, NAME, record } from "./names.js";
@@ -22,30 +23,24 @@ import { opt } from "./stream.js";
22
23
  // The event stream's public seam, single-sourced in ./stream.js, and the
23
24
  // diagnostic predicate in ./locate.js. Re-exported here because a consumer
24
25
  // imports them from the package, not from a file inside it.
25
- export { breathe, isReportBand, text, typed, walk } from "./stream.js";
26
+ export { breathe, display, isReportBand, text, typed, walk } from "./stream.js";
26
27
  export { isDiagnostic } from "./locate.js";
27
28
 
28
29
  /** @typedef {import("./scope.js").Scope} Scope */
29
30
 
30
31
  /** @type {(data: any) => any} */
31
32
  let inputOf = (data) => data ?? {};
32
- /** @type {(rows: any[], where: any, base: any) => any[]} */
33
- let filterRows = (rows, where, base) =>
34
- where ? rows.filter((row) => where(withRow(base, row))) : rows;
35
- /** @type {(rows: any[], sort: any, base: any) => any[]} */
36
- let sortRows = (rows, sort, base) => (sort ? sort(rows, base) : rows);
37
- /** @type {(rows: any[], n: number | null) => any[]} */
38
- let takeRows = (rows, n) => (n == null ? rows : rows.slice(0, n));
39
- /** @type {(aggs: any, rows: any[], base: any) => Scope} */
40
- let foldAggs = (aggs, rows, base) => {
33
+ // Every aggregate is computed before any is injected, so results never depend
34
+ // on declaration order; a sibling read inside an aggregate expression is always
35
+ // the pre-injection null (see SCHEMA.md, deliberate asymmetries). Hence the two
36
+ // passes: one to fold, one to write the values onto the report root.
37
+ /** @type {(aggs: any, rows: any[], base: any, root: any) => Scope} */
38
+ let foldAggs = (aggs, rows, base, root) => {
41
39
  /** @type {Scope} */
42
40
  let aggregates = {};
43
41
  for (let [name, fold] of aggs) aggregates[name] = aggregateValue(fold, rows, base);
44
- return aggregates;
45
- };
46
- /** @type {(root: any, aggregates: Scope, aggs: any) => void} */
47
- let injectAggs = (root, aggregates, aggs) => {
48
42
  for (let [name] of aggs) root[name] = aggregates[name];
43
+ return aggregates;
49
44
  };
50
45
  /** @type {(band: any, perPage: (items: any) => any) => any} */
51
46
  let maybePage = (band, perPage) => band && perPage(band);
@@ -58,23 +53,6 @@ let pageOf = (pageHeader, pageFooter, perPage) => {
58
53
  );
59
54
  };
60
55
 
61
- /**
62
- * @type {(parts: any, ctx: any) => Generator<object, void, undefined>}
63
- */
64
- function* reportEvents(parts, ctx) {
65
- let { header, empty, band, footer, marking, columns } = parts;
66
- let { root, aggregates, page, rows, base, runners } = ctx;
67
- yield opt(
68
- { type: "report-start", params: root.params, aggregates },
69
- { page, columns, marking: marking() },
70
- );
71
- yield* header(base);
72
- if (!rows.length && empty) yield* empty(base);
73
- else yield* band(rows, base, runners);
74
- yield* footer(base);
75
- yield { type: "report-end" };
76
- }
77
-
78
56
  /** @type {(name: any) => void} */
79
57
  let checkTargetName = (name) => {
80
58
  if (typeof name !== "string" || !NAME.test(name) || BLOCKED.has(name))
@@ -105,9 +83,23 @@ let runTarget = (compile, stream, data) => {
105
83
  * @returns {string[]} All definition problems, in document order.
106
84
  */
107
85
  export function validate(schema, funcs) {
108
- return plan(schema, funcs).problems;
86
+ return plan(schema, funcs).problems.map((problem) => problem.message);
109
87
  }
110
88
 
89
+ // The structured problems, frozen for handing out: the traversal's own objects
90
+ // go to exactly one caller, so freezing here cannot disturb a second reader.
91
+ // The diagnostic stays an engine-minted error and is deliberately not frozen.
92
+ /** @type {(problems: any[]) => readonly any[]} */
93
+ let freezeProblems = (problems) => Object.freeze(problems.map((problem) => Object.freeze(problem)));
94
+
95
+ // The per-node anchor sets, as frozen host data (CONTEXT.md, "Freeze"): which
96
+ // anchors and group handles each compiled source reads, keyed by schema path.
97
+ /** @type {(anchors: Map<string, Set<string>>) => any} */
98
+ let freezeAnchors = (anchors) =>
99
+ Object.freeze(
100
+ Object.fromEntries(Array.from(anchors, ([path, set]) => [path, Object.freeze([...set])])),
101
+ );
102
+
111
103
  /**
112
104
  * Compile a report once, stream structured render events against data many times.
113
105
  *
@@ -125,13 +117,31 @@ export function validate(schema, funcs) {
125
117
  * wording, or false when the render is covered. A thunk, so what it answers
126
118
  * is this render's answer and not the compile's.
127
119
  * @returns {{(data?: any): Generator<object, void, undefined>, names: string[],
128
- * functions: string[], paths: readonly any[]}} Event stream factory.
120
+ * functions: { name: string, arity: number, doc?: string }[],
121
+ * paths: readonly any[]}} Event stream factory.
129
122
  */
130
123
  function events(schema, funcs, options, marking) {
131
124
  // The traversal collects every problem, so a thrown error is the first of the
132
125
  // same list `validate()` would return.
133
126
  let planned = plan(schema, funcs, options);
134
- if (planned.problems.length) throw planned.diagnostic() || SyntaxError(planned.problems[0]);
127
+ if (planned.problems.length)
128
+ throw planned.diagnostic() || SyntaxError(planned.problems[0].message);
129
+ return assemble(planned, schema, marking);
130
+ }
131
+
132
+ /**
133
+ * Build the event stream factory from a settled traversal — the half of the
134
+ * one compile that runs only when the plan carried no problems, shared by
135
+ * `report()`'s throwing path and the instance's `plan()`.
136
+ *
137
+ * @param {ReturnType<typeof plan>} planned The finished traversal.
138
+ * @param {any} schema The report document, for locating `data` faults.
139
+ * @param {() => string | false} marking This render's unlicensed marking.
140
+ * @returns {{(data?: any): Generator<object, void, undefined>, names: string[],
141
+ * functions: { name: string, arity: number, doc?: string }[],
142
+ * paths: readonly any[]}} Event stream factory.
143
+ */
144
+ function assemble(planned, schema, marking) {
135
145
  // Null only for a non-object schema, which is itself a collected problem.
136
146
  // `select` and `params` come from the traversal too: it checked them, so
137
147
  // nothing here compiles or clones a second time.
@@ -143,6 +153,7 @@ function events(schema, funcs, options, marking) {
143
153
  take,
144
154
  aggs,
145
155
  running,
156
+ style,
146
157
  header,
147
158
  empty,
148
159
  columns,
@@ -159,7 +170,7 @@ function events(schema, funcs, options, marking) {
159
170
  try {
160
171
  return select(data);
161
172
  } catch (error) {
162
- locate("data", schema.data, error, 0, select.isDiagnostic(error));
173
+ locate("data", schema.data, error, select.isDiagnostic(error) && relocateQuery);
163
174
  }
164
175
  };
165
176
 
@@ -174,12 +185,11 @@ function events(schema, funcs, options, marking) {
174
185
  // aggregates (over the rows that remain). It cannot be lazy because the
175
186
  // report header may interpolate report aggregates; only the banded walk
176
187
  // itself streams.
177
- let rows = takeRows(sortRows(filterRows(runSelect(root.input), where, base), sort, base), take);
178
- // Aggregates compute first and inject after, so results never depend on
179
- // declaration order; a sibling read inside an aggregate expression is
180
- // always the pre-injection null (see SCHEMA.md, deliberate asymmetries).
181
- let aggregates = foldAggs(aggs, rows, base);
182
- injectAggs(root, aggregates, aggs);
188
+ let rows = runSelect(root.input);
189
+ if (where) rows = rows.filter((row) => where(withRow(base, row)));
190
+ if (sort) rows = sort(rows, base);
191
+ if (take != null) rows = rows.slice(0, take);
192
+ let aggregates = foldAggs(aggs, rows, base, root);
183
193
  // The report's runners, started per render like everything else here.
184
194
  let runners = startRunners(running);
185
195
  // Per-render page band closures: a paginated target calls them once per
@@ -188,18 +198,30 @@ function events(schema, funcs, options, marking) {
188
198
  /** @type {(items: any) => (page: any) => any[]} */
189
199
  let perPage = (items) => (page) => [...items(withPage(base, page))];
190
200
  let page = pageOf(pageHeader, pageFooter, perPage);
191
- // The marking rides on the opening event — its wording, so that a target
192
- // owns only where it goes. Read here, as the event is yielded, so a
193
- // render drained before verification settles counts as unlicensed
194
- // (SCHEMA.md, "License keys").
195
- return reportEvents(
196
- { header, empty, band, footer, marking, columns },
197
- { root, aggregates, page, rows, base, runners },
198
- );
201
+ // The banded walk itself, over everything the pre-pass settled above. The
202
+ // marking rides on the opening event — its wording, so that a target owns
203
+ // only where it goes. Read as the event is yielded, so a render drained
204
+ // before verification settles counts as unlicensed (SCHEMA.md, "License
205
+ // keys").
206
+ /** @type {() => Generator<object, void, undefined>} */
207
+ function* walked() {
208
+ yield opt(
209
+ { type: "report-start", params: root.params, aggregates },
210
+ { page, columns, style: style(base), marking: marking() },
211
+ );
212
+ yield* header(base);
213
+ if (!rows.length && empty) yield* empty(base);
214
+ else yield* band(rows, base, runners);
215
+ yield* footer(base);
216
+ yield { type: "report-end" };
217
+ }
218
+ return walked();
199
219
  };
200
220
  return Object.assign(emit, {
201
221
  names: Array.from(planned.names),
202
- functions: Array.from(planned.functions),
222
+ // Already signature objects — plan() paired each called name with its
223
+ // registry metadata through xprsn's signatures().
224
+ functions: planned.functions,
203
225
  paths: select.paths,
204
226
  });
205
227
  }
@@ -217,31 +239,51 @@ function events(schema, funcs, options, marking) {
217
239
  * license?: string, trust?: {publicKey?: string, release?: string}}} [options]
218
240
  * Host configuration. `trust` is internal, not API — see ./license.js.
219
241
  * @returns {{license: Promise<LicenseInfo>,
220
- * report: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
242
+ * report: (schema: any, funcs?: Record<string, Function>) => any,
243
+ * plan: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
221
244
  */
222
245
  export function quario(options) {
223
246
  let { licensed, settled } = verify(options?.license, options?.trust);
247
+ // The wording is the instance's, where the key is; the stream is what
248
+ // states it, so nothing wraps a compiled stream to reach its first event.
249
+ /** @type {() => string | false} */
250
+ let marking = () => !licensed() && MARKING;
251
+ // The compiled report around a stream: what `report()` returns, and what
252
+ // `plan()` carries when the document had no problems.
253
+ /** @type {(stream: ReturnType<typeof events>) => any} */
254
+ let wrap = (stream) => ({
255
+ stream,
256
+ names: stream.names,
257
+ functions: stream.functions,
258
+ paths: stream.paths,
259
+ // Every render awaits verification before the target pulls its first
260
+ // event, so no render is marked merely for racing the check and no
261
+ // target carries that concern itself. Target shape problems throw
262
+ // synchronously — definition errors, like everything else — while a
263
+ // render is always a promise, even over a synchronous renderer.
264
+ render: (/** @type {any} */ target, /** @type {any} */ data) => {
265
+ checkTarget(target);
266
+ return settled.then(() => runTarget(target.compile, stream, data));
267
+ },
268
+ });
224
269
  return {
225
270
  license: settled,
226
271
  /** @type {(schema: any, funcs?: Record<string, Function>) => any} */
227
272
  report(schema, funcs) {
228
- // The wording is the instance's, where the key is; the stream is what
229
- // states it, so nothing wraps a compiled stream to reach its first event.
230
- let stream = events(schema, funcs, options, () => !licensed() && MARKING);
273
+ return wrap(events(schema, funcs, options, marking));
274
+ },
275
+ // The plan (CONTEXT.md): both readings of the one descent, kept.
276
+ // `report()` and `validate()` name what a caller wanted from it; this
277
+ // hands over the whole thing — the compiled report (null while the
278
+ // document has problems), every problem structurally, and the per-node
279
+ // anchor sets — so an editing host pays one traversal per edit, not two.
280
+ /** @type {(schema: any, funcs?: Record<string, Function>) => any} */
281
+ plan(schema, funcs) {
282
+ let planned = plan(schema, funcs, options);
231
283
  return {
232
- stream,
233
- names: stream.names,
234
- functions: stream.functions,
235
- paths: stream.paths,
236
- // Every render awaits verification before the target pulls its first
237
- // event, so no render is marked merely for racing the check and no
238
- // target carries that concern itself. Target shape problems throw
239
- // synchronously — definition errors, like everything else — while a
240
- // render is always a promise, even over a synchronous renderer.
241
- render: (/** @type {any} */ target, /** @type {any} */ data) => {
242
- checkTarget(target);
243
- return settled.then(() => runTarget(target.compile, stream, data));
244
- },
284
+ report: planned.problems.length ? null : wrap(assemble(planned, schema, marking)),
285
+ problems: freezeProblems(planned.problems),
286
+ anchors: freezeAnchors(planned.anchors),
245
287
  };
246
288
  },
247
289
  };
package/lib/license.js CHANGED
@@ -11,7 +11,7 @@
11
11
  // reassigns them. A key is valid for every version released inside its window
12
12
  // (LICENSE section 7), so validity compares ISO date strings and never reads
13
13
  // a clock — accepted output stays accepted.
14
- let RELEASE = "2026-08-27"; // x-release-please-date
14
+ let RELEASE = "2026-09-01"; // x-release-please-date
15
15
  // The verifying half of the signing pair: the 65-byte uncompressed P-256
16
16
  // point, base64url. The private half never enters the repo;
17
17
  // scripts/license/sign.mjs mints keys against it.
package/lib/locate.js CHANGED
@@ -1,36 +1,25 @@
1
1
  /**
2
2
  * Located diagnostics: where quario re-throws an engine's error with the
3
- * band/item path and offending source attached, and the register that keeps
4
- * the copy authenticated.
3
+ * band/item path and offending source attached.
5
4
  */
6
5
  import { isDiagnostic as isQueryDiagnostic } from "padvinder";
7
6
  import { isDiagnostic as isXprsnDiagnostic } from "xprsn";
8
7
  import { isDiagnostic as isTemplateDiagnostic } from "sjabloon";
9
8
 
10
- // The engines authenticate their diagnostics by identity (WeakMap/WeakSet), so
11
- // the located copies `locate()` re-throws can't pass the upstream guards.
12
- // quario registers each copy it makes from an authenticated original here,
13
- // giving located diagnostics the same identity-based authentication.
14
- /** @type {WeakSet<any>} */
15
- let DIAG = new WeakSet();
16
-
17
9
  /**
18
- * True when an error is one of the stack's authenticated located diagnostics —
19
- * thrown by quario with an engine (xprsn, sjabloon, or padvinder) original
20
- * behind it, or by an engine directly — i.e. it safely carries the optional
21
- * `code`/`start`/`end`/`limit`/`actual`/`blocks` metadata. Authentication is
22
- * by identity: an error merely shaped like a diagnostic does not pass.
10
+ * True when an error is one of the stack's authenticated diagnostics — thrown
11
+ * by an engine (xprsn, sjabloon, or padvinder) directly, or re-thrown by
12
+ * quario through that engine's `relocate` — i.e. it safely carries the
13
+ * optional `code`/`start`/`end`/`limit`/`actual`/`blocks` metadata.
14
+ * Authentication is by identity: an error merely shaped like a diagnostic does
15
+ * not pass, and a located copy passes because the engine that minted the
16
+ * original made the copy.
23
17
  *
24
18
  * @param {unknown} error The caught value.
25
19
  * @returns {boolean} Whether `error` is a located diagnostic.
26
20
  */
27
21
  export function isDiagnostic(error) {
28
- return (
29
- DIAG.has(/** @type {any} */ (error)) ||
30
- isXprsnDiagnostic(error) ||
31
- isTemplateDiagnostic(error) ||
32
- isQueryDiagnostic(error)
33
- );
22
+ return isXprsnDiagnostic(error) || isTemplateDiagnostic(error) || isQueryDiagnostic(error);
34
23
  }
35
24
 
36
25
  /** @type {(msg: string) => never} */
@@ -38,43 +27,41 @@ export let err = (msg) => {
38
27
  throw Error(msg);
39
28
  };
40
29
 
41
- /** @type {(error: any) => boolean} */
42
- let trustedOf = (error) => error instanceof Error && Object.hasOwn(error, "code");
43
-
44
- /** @type {(path: string, src: any) => string} */
45
- let locationOf = (path, src) => (src === undefined ? path : path + " [" + String(src) + "]");
46
-
47
- /** @type {(path: string, src: any, error: any) => any} */
48
- let wrap = (path, src, error) => {
49
- let location = locationOf(path, src);
50
- if (error instanceof Error)
51
- return new /** @type {any} */ (error.constructor)(location + ": " + error.message);
52
- return new Error(location + ": " + String(error));
53
- };
54
-
55
- /** @type {(located: any, error: any, keys: string[], extra: (value: any) => any) => void} */
56
- let copyOwn = (located, error, keys, extra) => {
57
- for (let key of keys) if (Object.hasOwn(error, key)) located[key] = extra(error[key]);
58
- };
59
-
60
- /** @type {(located: any, error: any) => void} */
61
- let copyBlocks = (located, error) => {
62
- if (Object.hasOwn(error, "blocks"))
63
- Object.defineProperty(located, "blocks", { value: error.blocks, enumerable: true });
64
- };
30
+ // How a located error remembers where it happened as data: the traversal reads
31
+ // this to build a structured problem, so nothing parses a message back apart.
32
+ // A symbol, so the located copy's public shape stays exactly the engine's.
33
+ export let LOCATION = Symbol("quario.location");
65
34
 
66
- /** @type {(located: any, error: any, shift: number) => void} */
67
- let stamp = (located, error, shift) => {
68
- located.code = error.code;
69
- copyOwn(located, error, ["start", "end"], (value) => value + shift);
70
- copyOwn(located, error, ["limit", "actual"], (value) => value);
71
- copyBlocks(located, error);
72
- DIAG.add(located);
35
+ /** @type {(path: string, msg: string) => never} */
36
+ export let fault = (path, msg) => {
37
+ throw Object.assign(Error(path + ": " + msg), { [LOCATION]: { path } });
73
38
  };
74
39
 
75
- /** @type {(path: string, src: any, error: any, shift?: number, trusted?: boolean) => never} */
76
- export let locate = (path, src, error, shift = 0, trusted = trustedOf(error)) => {
77
- let located = wrap(path, src, error);
78
- if (trusted) stamp(located, error, shift);
79
- throw located;
40
+ /**
41
+ * Re-throw `error` located at `path`, with `src` — the offending author
42
+ * source — in the message.
43
+ *
44
+ * A diagnostic is relocated by the engine that authenticated it, so the copy
45
+ * carries every field that engine puts on it and passes that engine's own
46
+ * guard (ADR 0024). `relocate` is that engine's relocation — and a verdict as
47
+ * much as a callback: the call site holds the guard, because two of them are
48
+ * scoped to one evaluator rather than to the module, and `false` is its way of
49
+ * saying the guard rejected this error. Anything else — a host function's
50
+ * throw, quario's own verdict on image bytes — is wrapped as a plain error
51
+ * with no metadata at all.
52
+ *
53
+ * @type {(path: string, src: any, error: any, relocate?: false | ((e: any, o: { prefix: string, offset: number }) => any), offset?: number) => never}
54
+ */
55
+ export let locate = (path, src, error, relocate, offset = 0) => {
56
+ let prefix = (src === undefined ? path : path + " [" + String(src) + "]") + ": ";
57
+ let where = { [LOCATION]: { path, source: src } };
58
+ if (relocate) throw Object.assign(relocate(error, { prefix, offset }), where);
59
+ // A host throw keeps its own class — the located error is the same kind of
60
+ // failure, named where it happened — and gains no diagnostic metadata.
61
+ throw Object.assign(
62
+ error instanceof Error
63
+ ? new /** @type {any} */ (error.constructor)(prefix + error.message)
64
+ : Error(prefix + String(error)),
65
+ where,
66
+ );
80
67
  };