quario 0.1.0 → 0.3.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,8 +12,9 @@
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
- import { err, locate } from "./locate.js";
17
+ import { err, fault, locate } from "./locate.js";
17
18
  import { BLOCKED, NAME, record } from "./names.js";
18
19
  import { plan } from "./plan.js";
19
20
  import { aggregateValue, startRunners, withPage, withRow } from "./scope.js";
@@ -22,30 +23,25 @@ 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";
27
+ export { format } from "./format.js";
26
28
  export { isDiagnostic } from "./locate.js";
27
29
 
28
30
  /** @typedef {import("./scope.js").Scope} Scope */
29
31
 
30
32
  /** @type {(data: any) => any} */
31
33
  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) => {
34
+ // Every aggregate is computed before any is injected, so results never depend
35
+ // on declaration order; a sibling read inside an aggregate expression is always
36
+ // the pre-injection null (see SCHEMA.md, deliberate asymmetries). Hence the two
37
+ // passes: one to fold, one to write the values onto the report root.
38
+ /** @type {(aggs: any, rows: any[], base: any, root: any) => Scope} */
39
+ let foldAggs = (aggs, rows, base, root) => {
41
40
  /** @type {Scope} */
42
41
  let aggregates = {};
43
42
  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
43
  for (let [name] of aggs) root[name] = aggregates[name];
44
+ return aggregates;
49
45
  };
50
46
  /** @type {(band: any, perPage: (items: any) => any) => any} */
51
47
  let maybePage = (band, perPage) => band && perPage(band);
@@ -58,23 +54,6 @@ let pageOf = (pageHeader, pageFooter, perPage) => {
58
54
  );
59
55
  };
60
56
 
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
57
  /** @type {(name: any) => void} */
79
58
  let checkTargetName = (name) => {
80
59
  if (typeof name !== "string" || !NAME.test(name) || BLOCKED.has(name))
@@ -105,9 +84,23 @@ let runTarget = (compile, stream, data) => {
105
84
  * @returns {string[]} All definition problems, in document order.
106
85
  */
107
86
  export function validate(schema, funcs) {
108
- return plan(schema, funcs).problems;
87
+ return plan(schema, funcs).problems.map((problem) => problem.message);
109
88
  }
110
89
 
90
+ // The structured problems, frozen for handing out: the traversal's own objects
91
+ // go to exactly one caller, so freezing here cannot disturb a second reader.
92
+ // The diagnostic stays an engine-minted error and is deliberately not frozen.
93
+ /** @type {(problems: any[]) => readonly any[]} */
94
+ let freezeProblems = (problems) => Object.freeze(problems.map((problem) => Object.freeze(problem)));
95
+
96
+ // The per-node anchor sets, as frozen host data (CONTEXT.md, "Freeze"): which
97
+ // anchors and group handles each compiled source reads, keyed by schema path.
98
+ /** @type {(anchors: Map<string, Set<string>>) => any} */
99
+ let freezeAnchors = (anchors) =>
100
+ Object.freeze(
101
+ Object.fromEntries(Array.from(anchors, ([path, set]) => [path, Object.freeze([...set])])),
102
+ );
103
+
111
104
  /**
112
105
  * Compile a report once, stream structured render events against data many times.
113
106
  *
@@ -125,13 +118,58 @@ export function validate(schema, funcs) {
125
118
  * wording, or false when the render is covered. A thunk, so what it answers
126
119
  * is this render's answer and not the compile's.
127
120
  * @returns {{(data?: any): Generator<object, void, undefined>, names: string[],
128
- * functions: string[], paths: readonly any[]}} Event stream factory.
121
+ * functions: { name: string, arity: number, doc?: string }[],
122
+ * paths: readonly any[]}} Event stream factory.
129
123
  */
130
124
  function events(schema, funcs, options, marking) {
131
125
  // The traversal collects every problem, so a thrown error is the first of the
132
126
  // same list `validate()` would return.
133
127
  let planned = plan(schema, funcs, options);
134
- if (planned.problems.length) throw planned.diagnostic() || SyntaxError(planned.problems[0]);
128
+ if (planned.problems.length)
129
+ throw planned.diagnostic() || SyntaxError(planned.problems[0].message);
130
+ return assemble(planned, schema, marking, options);
131
+ }
132
+
133
+ /** @type {(event: any) => boolean} */
134
+ let occupying = (event) =>
135
+ event.type === "item" || event.type === "image" || event.type === "split-start";
136
+ /** @type {(n: any) => boolean} */
137
+ let nonzeroLead = (n) => Number.isFinite(n) && n !== 0;
138
+ /** @type {(event: any) => string} */
139
+ let eventPath = (event) => event.path || "header";
140
+ /** @type {(event: any) => void} */
141
+ let refuseLead = (event) => {
142
+ if (nonzeroLead(event.style?.spaceBefore))
143
+ fault(eventPath(event) + ".style.spaceBefore", "must be 0 after a height-declared header");
144
+ };
145
+ /** @type {(events: Iterable<any>) => Generator<any>} */
146
+ function* guardPin(events) {
147
+ let pending = true;
148
+ for (let event of events) {
149
+ if (pending && occupying(event)) {
150
+ pending = false;
151
+ refuseLead(event);
152
+ }
153
+ yield event;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Build the event stream factory from a settled traversal — the half of the
159
+ * one compile that runs only when the plan carried no problems, shared by
160
+ * `report()`'s throwing path and the instance's `plan()`.
161
+ *
162
+ * @param {ReturnType<typeof plan>} planned The finished traversal.
163
+ * @param {any} schema The report document, for locating `data` faults.
164
+ * @param {() => string | false} marking This render's unlicensed marking.
165
+ * @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number},
166
+ * locale?: string, currency?: string, timeZone?: string} | undefined} options
167
+ * Host controls closed over this compile.
168
+ * @returns {{(data?: any): Generator<object, void, undefined>, names: string[],
169
+ * functions: { name: string, arity: number, doc?: string }[],
170
+ * paths: readonly any[]}} Event stream factory.
171
+ */
172
+ function assemble(planned, schema, marking, options) {
135
173
  // Null only for a non-object schema, which is itself a collected problem.
136
174
  // `select` and `params` come from the traversal too: it checked them, so
137
175
  // nothing here compiles or clones a second time.
@@ -143,6 +181,7 @@ function events(schema, funcs, options, marking) {
143
181
  take,
144
182
  aggs,
145
183
  running,
184
+ style,
146
185
  header,
147
186
  empty,
148
187
  columns,
@@ -150,6 +189,8 @@ function events(schema, funcs, options, marking) {
150
189
  band,
151
190
  pageHeader,
152
191
  pageFooter,
192
+ headerHeight,
193
+ margin,
153
194
  } = /** @type {NonNullable<typeof planned.compiled>} */ (planned.compiled);
154
195
 
155
196
  // The runner's own faults are located per render: a budget is spent while
@@ -159,7 +200,7 @@ function events(schema, funcs, options, marking) {
159
200
  try {
160
201
  return select(data);
161
202
  } catch (error) {
162
- locate("data", schema.data, error, 0, select.isDiagnostic(error));
203
+ locate("data", schema.data, error, select.isDiagnostic(error) && relocateQuery);
163
204
  }
164
205
  };
165
206
 
@@ -174,12 +215,11 @@ function events(schema, funcs, options, marking) {
174
215
  // aggregates (over the rows that remain). It cannot be lazy because the
175
216
  // report header may interpolate report aggregates; only the banded walk
176
217
  // 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);
218
+ let rows = runSelect(root.input);
219
+ if (where) rows = rows.filter((row) => where(withRow(base, row)));
220
+ if (sort) rows = sort(rows, base);
221
+ if (take != null) rows = rows.slice(0, take);
222
+ let aggregates = foldAggs(aggs, rows, base, root);
183
223
  // The report's runners, started per render like everything else here.
184
224
  let runners = startRunners(running);
185
225
  // Per-render page band closures: a paginated target calls them once per
@@ -188,18 +228,44 @@ function events(schema, funcs, options, marking) {
188
228
  /** @type {(items: any) => (page: any) => any[]} */
189
229
  let perPage = (items) => (page) => [...items(withPage(base, page))];
190
230
  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
- );
231
+ // The banded walk itself, over everything the pre-pass settled above. The
232
+ // marking rides on the opening event — its wording, so that a target owns
233
+ // only where it goes. Read as the event is yielded, so a render drained
234
+ // before verification settles counts as unlicensed (SCHEMA.md, "License
235
+ // keys").
236
+ /** @type {(options: any) => { locale?: string, currency?: string, timeZone?: string }} */
237
+ let intlOf = (host) => ({
238
+ locale: host?.locale,
239
+ currency: host?.currency,
240
+ timeZone: host?.timeZone,
241
+ });
242
+ /** @type {() => Generator<object, void, undefined>} */
243
+ function* walked() {
244
+ yield opt(
245
+ { type: "report-start", params: root.params, aggregates },
246
+ {
247
+ page,
248
+ columns,
249
+ style: style(base),
250
+ marking: marking(),
251
+ ...intlOf(options),
252
+ margin,
253
+ headerHeight,
254
+ },
255
+ );
256
+ yield* header(base);
257
+ let body = !rows.length && empty ? empty(base) : band(rows, base, runners);
258
+ yield* headerHeight != null ? guardPin(body) : body;
259
+ yield* footer(base);
260
+ yield { type: "report-end" };
261
+ }
262
+ return walked();
199
263
  };
200
264
  return Object.assign(emit, {
201
265
  names: Array.from(planned.names),
202
- functions: Array.from(planned.functions),
266
+ // Already signature objects — plan() paired each called name with its
267
+ // registry metadata through xprsn's signatures().
268
+ functions: planned.functions,
203
269
  paths: select.paths,
204
270
  });
205
271
  }
@@ -214,34 +280,56 @@ function events(schema, funcs, options, marking) {
214
280
  * ("Instances and targets").
215
281
  *
216
282
  * @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number},
217
- * license?: string, trust?: {publicKey?: string, release?: string}}} [options]
283
+ * license?: string, trust?: {publicKey?: string, release?: string},
284
+ * locale?: string, currency?: string, timeZone?: string}} [options]
218
285
  * Host configuration. `trust` is internal, not API — see ./license.js.
286
+ * `locale` / `currency` / `timeZone` present `format` (docs/adr/0041).
219
287
  * @returns {{license: Promise<LicenseInfo>,
220
- * report: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
288
+ * report: (schema: any, funcs?: Record<string, Function>) => any,
289
+ * plan: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
221
290
  */
222
291
  export function quario(options) {
223
292
  let { licensed, settled } = verify(options?.license, options?.trust);
293
+ // The wording is the instance's, where the key is; the stream is what
294
+ // states it, so nothing wraps a compiled stream to reach its first event.
295
+ /** @type {() => string | false} */
296
+ let marking = () => !licensed() && MARKING;
297
+ // The compiled report around a stream: what `report()` returns, and what
298
+ // `plan()` carries when the document had no problems.
299
+ /** @type {(stream: ReturnType<typeof events>) => any} */
300
+ let wrap = (stream) => ({
301
+ stream,
302
+ names: stream.names,
303
+ functions: stream.functions,
304
+ paths: stream.paths,
305
+ // Every render awaits verification before the target pulls its first
306
+ // event, so no render is marked merely for racing the check and no
307
+ // target carries that concern itself. Target shape problems throw
308
+ // synchronously — definition errors, like everything else — while a
309
+ // render is always a promise, even over a synchronous renderer.
310
+ render: (/** @type {any} */ target, /** @type {any} */ data) => {
311
+ checkTarget(target);
312
+ return settled.then(() => runTarget(target.compile, stream, data));
313
+ },
314
+ });
224
315
  return {
225
316
  license: settled,
226
317
  /** @type {(schema: any, funcs?: Record<string, Function>) => any} */
227
318
  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);
319
+ return wrap(events(schema, funcs, options, marking));
320
+ },
321
+ // The plan (CONTEXT.md): both readings of the one descent, kept.
322
+ // `report()` and `validate()` name what a caller wanted from it; this
323
+ // hands over the whole thing — the compiled report (null while the
324
+ // document has problems), every problem structurally, and the per-node
325
+ // anchor sets — so an editing host pays one traversal per edit, not two.
326
+ /** @type {(schema: any, funcs?: Record<string, Function>) => any} */
327
+ plan(schema, funcs) {
328
+ let planned = plan(schema, funcs, options);
231
329
  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
- },
330
+ report: planned.problems.length ? null : wrap(assemble(planned, schema, marking, options)),
331
+ problems: freezeProblems(planned.problems),
332
+ anchors: freezeAnchors(planned.anchors),
245
333
  };
246
334
  },
247
335
  };
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-02"; // 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
  };