quario 0.0.1 → 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 ADDED
@@ -0,0 +1,290 @@
1
+ /**
2
+ * quario — a tiny, CSP-safe report engine. A report is a JSON document; text
3
+ * cells are sjabloon templates and every other dynamic value is an xprsn
4
+ * expression, so definitions are data, never code. Nothing here turns text into
5
+ * runnable code, so a report renders under a strict Content Security Policy.
6
+ * (The safety suite greps every source file for the forbidden string-to-code
7
+ * constructs, so they must not appear even in a comment.)
8
+ *
9
+ * This module is the public surface of the markup-unaware core: the instance
10
+ * factory, the one compile behind it, and `events()`, whose stream is the seam
11
+ * every render target consumes. The schema traversal that compiles a report
12
+ * definition lives in `./plan.js`; what the two of them share lives in the
13
+ * siblings beside it, one seam each. See SCHEMA.md for the document shape.
14
+ */
15
+ import { relocate as relocateQuery } from "padvinder";
16
+ import { MARKING, verify } from "./license.js";
17
+ import { err, locate } from "./locate.js";
18
+ import { BLOCKED, NAME, record } from "./names.js";
19
+ import { plan } from "./plan.js";
20
+ import { aggregateValue, startRunners, withPage, withRow } from "./scope.js";
21
+ import { opt } from "./stream.js";
22
+
23
+ // The event stream's public seam, single-sourced in ./stream.js, and the
24
+ // diagnostic predicate in ./locate.js. Re-exported here because a consumer
25
+ // imports them from the package, not from a file inside it.
26
+ export { breathe, display, isReportBand, text, typed, walk } from "./stream.js";
27
+ export { isDiagnostic } from "./locate.js";
28
+
29
+ /** @typedef {import("./scope.js").Scope} Scope */
30
+
31
+ /** @type {(data: any) => any} */
32
+ let inputOf = (data) => data ?? {};
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) => {
39
+ /** @type {Scope} */
40
+ let aggregates = {};
41
+ for (let [name, fold] of aggs) aggregates[name] = aggregateValue(fold, rows, base);
42
+ for (let [name] of aggs) root[name] = aggregates[name];
43
+ return aggregates;
44
+ };
45
+ /** @type {(band: any, perPage: (items: any) => any) => any} */
46
+ let maybePage = (band, perPage) => band && perPage(band);
47
+ /** @type {(pageHeader: any, pageFooter: any, perPage: (items: any) => any) => any} */
48
+ let pageOf = (pageHeader, pageFooter, perPage) => {
49
+ if (!(pageHeader || pageFooter)) return null;
50
+ return opt(
51
+ {},
52
+ { header: maybePage(pageHeader, perPage), footer: maybePage(pageFooter, perPage) },
53
+ );
54
+ };
55
+
56
+ /** @type {(name: any) => void} */
57
+ let checkTargetName = (name) => {
58
+ if (typeof name !== "string" || !NAME.test(name) || BLOCKED.has(name))
59
+ err("target.name: expected a plain identifier");
60
+ };
61
+ /** @type {(target: any) => void} */
62
+ let checkTarget = (target) => {
63
+ // oxlint-disable-next-line no-unused-expressions
64
+ record(target) || err("target: expected a { name, compile } target");
65
+ checkTargetName(target.name);
66
+ // oxlint-disable-next-line no-unused-expressions
67
+ typeof target.compile === "function" || err("target.compile: expected a function");
68
+ };
69
+ /** @type {(compile: Function, stream: any, data: any) => any} */
70
+ let runTarget = (compile, stream, data) => {
71
+ let render = compile(stream);
72
+ // oxlint-disable-next-line no-unused-expressions
73
+ typeof render === "function" ||
74
+ err("target.compile: expected compile(stream) to return a renderer");
75
+ return render(data);
76
+ };
77
+
78
+ /**
79
+ * Check a report definition without rendering it.
80
+ *
81
+ * @param {any} schema The report document.
82
+ * @param {Record<string, Function>} [funcs] Functions callable from expressions.
83
+ * @returns {string[]} All definition problems, in document order.
84
+ */
85
+ export function validate(schema, funcs) {
86
+ return plan(schema, funcs).problems.map((problem) => problem.message);
87
+ }
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
+
103
+ /**
104
+ * Compile a report once, stream structured render events against data many times.
105
+ *
106
+ * The render seam every target consumes — markup, spreadsheet, and document
107
+ * exporters all read the same stream. The instance owns the public API; this
108
+ * is where `report()` does its one compile. The returned stream, called with
109
+ * data, generates the banded walk; the data pre-pass runs eagerly per call
110
+ * and event emission is lazy. See SCHEMA.md ("Event stream").
111
+ *
112
+ * @param {any} schema The report document (see SCHEMA.md).
113
+ * @param {Record<string, Function> | undefined} funcs Functions callable from expressions.
114
+ * @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number}} | undefined}
115
+ * options Host controls.
116
+ * @param {() => string | false} marking This render's unlicensed marking — its
117
+ * wording, or false when the render is covered. A thunk, so what it answers
118
+ * is this render's answer and not the compile's.
119
+ * @returns {{(data?: any): Generator<object, void, undefined>, names: string[],
120
+ * functions: { name: string, arity: number, doc?: string }[],
121
+ * paths: readonly any[]}} Event stream factory.
122
+ */
123
+ function events(schema, funcs, options, marking) {
124
+ // The traversal collects every problem, so a thrown error is the first of the
125
+ // same list `validate()` would return.
126
+ let planned = plan(schema, funcs, options);
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) {
145
+ // Null only for a non-object schema, which is itself a collected problem.
146
+ // `select` and `params` come from the traversal too: it checked them, so
147
+ // nothing here compiles or clones a second time.
148
+ let {
149
+ select,
150
+ params,
151
+ where,
152
+ sort,
153
+ take,
154
+ aggs,
155
+ running,
156
+ style,
157
+ header,
158
+ empty,
159
+ columns,
160
+ footer,
161
+ band,
162
+ pageHeader,
163
+ pageFooter,
164
+ } = /** @type {NonNullable<typeof planned.compiled>} */ (planned.compiled);
165
+
166
+ // The runner's own faults are located per render: a budget is spent while
167
+ // traversing this document, not while compiling the path.
168
+ /** @type {(data: any) => any[]} */
169
+ let runSelect = (data) => {
170
+ try {
171
+ return select(data);
172
+ } catch (error) {
173
+ locate("data", schema.data, error, select.isDiagnostic(error) && relocateQuery);
174
+ }
175
+ };
176
+
177
+ /** @type {(data: any) => Generator<any>} */
178
+ let emit = (data) => {
179
+ let root = Object.create(null);
180
+ root.params = params;
181
+ root.input = inputOf(data);
182
+ let base = Object.create(null);
183
+ base["$"] = root;
184
+ // The eager pre-pass: select → filter → report sort → report take →
185
+ // aggregates (over the rows that remain). It cannot be lazy because the
186
+ // report header may interpolate report aggregates; only the banded walk
187
+ // itself streams.
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);
193
+ // The report's runners, started per render like everything else here.
194
+ let runners = startRunners(running);
195
+ // Per-render page band closures: a paginated target calls them once per
196
+ // page with `{ number, total }` (1-based) and receives resolved item
197
+ // events. Unpaginated targets ignore the field.
198
+ /** @type {(items: any) => (page: any) => any[]} */
199
+ let perPage = (items) => (page) => [...items(withPage(base, page))];
200
+ let page = pageOf(pageHeader, pageFooter, perPage);
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();
219
+ };
220
+ return Object.assign(emit, {
221
+ names: Array.from(planned.names),
222
+ // Already signature objects — plan() paired each called name with its
223
+ // registry metadata through xprsn's signatures().
224
+ functions: planned.functions,
225
+ paths: select.paths,
226
+ });
227
+ }
228
+
229
+ /** @typedef {import("./license.js").LicenseInfo} LicenseInfo */
230
+
231
+ /**
232
+ * Create a configured quario instance: the host-level options — license key,
233
+ * query budgets. The key is verified once, here, and `license` settles with
234
+ * the result. `report()` compiles a definition once and every target renders
235
+ * from that one compiled stream through `render(target, data)`. See SCHEMA.md
236
+ * ("Instances and targets").
237
+ *
238
+ * @param {{query?: {maxNodes?: number, maxDepth?: number, maxResults?: number},
239
+ * license?: string, trust?: {publicKey?: string, release?: string}}} [options]
240
+ * Host configuration. `trust` is internal, not API — see ./license.js.
241
+ * @returns {{license: Promise<LicenseInfo>,
242
+ * report: (schema: any, funcs?: Record<string, Function>) => any,
243
+ * plan: (schema: any, funcs?: Record<string, Function>) => any}} The instance.
244
+ */
245
+ export function quario(options) {
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
+ });
269
+ return {
270
+ license: settled,
271
+ /** @type {(schema: any, funcs?: Record<string, Function>) => any} */
272
+ report(schema, funcs) {
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);
283
+ return {
284
+ report: planned.problems.length ? null : wrap(assemble(planned, schema, marking)),
285
+ problems: freezeProblems(planned.problems),
286
+ anchors: freezeAnchors(planned.anchors),
287
+ };
288
+ },
289
+ };
290
+ }
package/lib/license.js ADDED
@@ -0,0 +1,141 @@
1
+ // License keys (LICENSE section 6): offline verification, scoped to one
2
+ // instance, and the wording of the marking an unlicensed render carries.
3
+ // `quario()` calls `verify` once and closes over the result: no mutable state
4
+ // lives at module scope, so nothing one instance does can reach another.
5
+ // Verification is entirely offline, and asynchronous only because WebCrypto
6
+ // is.
7
+
8
+ // Release date of this version, written into the version commit by
9
+ // release-please (`x-release-please-date`). Both constants keep the `let`
10
+ // spelling that `scripts/license/keygen.mjs` matches on, though nothing
11
+ // reassigns them. A key is valid for every version released inside its window
12
+ // (LICENSE section 7), so validity compares ISO date strings and never reads
13
+ // a clock — accepted output stays accepted.
14
+ let RELEASE = "2026-09-01"; // x-release-please-date
15
+ // The verifying half of the signing pair: the 65-byte uncompressed P-256
16
+ // point, base64url. The private half never enters the repo;
17
+ // scripts/license/sign.mjs mints keys against it.
18
+ let PUBKEY =
19
+ "BEyXC0sH4pDoL-gGxLTqHguWi38WK2v7OrAfNvK8_4Ou8yLhcKgX5dzCoj1ojj6ez0B1cwMDPfejthqEQ4wZGjM";
20
+
21
+ // The marking wording, stated once for the whole engine and carried to every
22
+ // target on `report-start` (SCHEMA.md, "License keys"): the targets own where
23
+ // it goes, never what it says. Keep it encodable by WinAnsi — the PDF target
24
+ // draws it through a base-14 face, and the em dash is the one character here
25
+ // that had to be checked. The assertion that would catch a bad one lives in
26
+ // that target's suite (`packages/pdf/test/pdf.test.js`), not this package's.
27
+ export let MARKING = "Unlicensed evaluation — getquario.com";
28
+
29
+ let ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
30
+ let DECODER = new TextDecoder();
31
+
32
+ /** @type {(text: string) => Uint8Array<ArrayBuffer>} */
33
+ let unb64 = (text) => {
34
+ let bin = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
35
+ let bytes = new Uint8Array(bin.length);
36
+ for (let i = 0; i < bytes.length; i++) bytes[i] = bin.charCodeAt(i);
37
+ return bytes;
38
+ };
39
+
40
+ /** @type {(obj: any, name: string, fallback: string) => string} */
41
+ let field = (obj, name, fallback) => (obj && obj[name]) || fallback;
42
+
43
+ /** @type {(key: string) => [string, Uint8Array<ArrayBuffer>]} */
44
+ let parseKey = (key) => {
45
+ if (!key.startsWith("quario_")) throw Error("not a quario_ key");
46
+ let [body, signature, extra] = key.slice(7).split(".");
47
+ if (!signature || extra !== undefined) throw Error("expected <payload>.<signature>");
48
+ return [signature, unb64(body)];
49
+ };
50
+
51
+ /** @type {(value: any) => boolean} */
52
+ let isStr = (value) => typeof value === "string";
53
+
54
+ /** @type {(payload: any) => void} */
55
+ let checkPayload = (payload) => {
56
+ if (!payload || !isStr(payload.id) || !isStr(payload.name))
57
+ throw Error("payload is missing id/name");
58
+ };
59
+
60
+ /** @type {(payload: any) => void} */
61
+ let checkWindow = (payload) => {
62
+ if (!payload.id.startsWith("1-")) throw Error("unsupported key format version");
63
+ if (!ISO_DATE.test(payload.from) || !ISO_DATE.test(payload.to))
64
+ throw Error("payload window is not a pair of ISO dates");
65
+ };
66
+
67
+ /** @type {() => Crypto["subtle"]} */
68
+ let cryptoSubtle = () => {
69
+ let subtle = globalThis.crypto && globalThis.crypto.subtle;
70
+ if (!subtle) throw Error("crypto.subtle is unavailable (secure contexts only)");
71
+ return subtle;
72
+ };
73
+
74
+ /** @type {(subtle: Crypto["subtle"], pubkey: string, signature: string, bytes: BufferSource) => Promise<void>} */
75
+ let importAndVerify = async (subtle, pubkey, signature, bytes) => {
76
+ let curve = { name: "ECDSA", namedCurve: "P-256" };
77
+ let pub = await subtle.importKey("raw", unb64(pubkey), curve, false, ["verify"]);
78
+ let hash = { name: "ECDSA", hash: "SHA-256" };
79
+ if (!(await subtle.verify(hash, pub, unb64(signature), bytes)))
80
+ throw Error("signature does not match");
81
+ };
82
+
83
+ /** @type {(payload: any, release: string) => void} */
84
+ let checkCover = (payload, release) => {
85
+ if (!(payload.from <= release && release <= payload.to))
86
+ throw Error("window does not cover the " + release + " release");
87
+ };
88
+
89
+ /** @type {(error: unknown) => string} */
90
+ let failMsg = (error) => (error instanceof Error ? error.message : String(error));
91
+
92
+ /** @typedef {{ licensed: boolean, licensee?: string, id?: string }} LicenseInfo */
93
+
94
+ /**
95
+ * Verify one instance's license key.
96
+ *
97
+ * `settled` is the instance's public `license` promise and the gate every
98
+ * render awaits; `licensed()` is the same answer readable synchronously, for
99
+ * the marking on the opening event. A render drained before settlement reads
100
+ * `false` and is marked — the shipped targets await settlement first. Every
101
+ * failure (including a missing `crypto.subtle` outside secure contexts)
102
+ * soft-fails to marked output and warns once; the feature set never gates
103
+ * (LICENSE section 6).
104
+ *
105
+ * @param {string | undefined} key The instance's `license` option.
106
+ * @param {{publicKey?: string, release?: string}} [trust] What this instance
107
+ * verifies against, defaulting to the shipped key and release date. Internal
108
+ * — a test seam, not API: the production private key never enters the repo,
109
+ * so suites verify against freshly generated pairs.
110
+ * @returns {{licensed: () => boolean, settled: Promise<LicenseInfo>}}
111
+ */
112
+ export let verify = (key, trust) => {
113
+ let pubkey = field(trust, "publicKey", PUBKEY);
114
+ let release = field(trust, "release", RELEASE);
115
+ let valid = false;
116
+ let settled = (async () => {
117
+ if (!key) return { licensed: false };
118
+ // Every throw below is caught here and turned into marked output, so none
119
+ // of them is a definition error — the engine's thrower means the opposite.
120
+ try {
121
+ let [signature, bytes] = parseKey(key);
122
+ let payload = JSON.parse(DECODER.decode(bytes));
123
+ checkPayload(payload);
124
+ checkWindow(payload);
125
+ await importAndVerify(cryptoSubtle(), pubkey, signature, bytes);
126
+ checkCover(payload, release);
127
+ // Set before this promise resolves: `render` awaits `settled`, so a
128
+ // verified instance must never still read as unlicensed afterwards.
129
+ valid = true;
130
+ return { licensed: true, licensee: payload.name, id: payload.id };
131
+ } catch (error) {
132
+ console.warn(
133
+ "quario: license key rejected (" +
134
+ failMsg(error) +
135
+ ") - output will be marked, see LICENSE section 6",
136
+ );
137
+ return { licensed: false };
138
+ }
139
+ })();
140
+ return { licensed: () => valid, settled };
141
+ };
package/lib/locate.js ADDED
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Located diagnostics: where quario re-throws an engine's error with the
3
+ * band/item path and offending source attached.
4
+ */
5
+ import { isDiagnostic as isQueryDiagnostic } from "padvinder";
6
+ import { isDiagnostic as isXprsnDiagnostic } from "xprsn";
7
+ import { isDiagnostic as isTemplateDiagnostic } from "sjabloon";
8
+
9
+ /**
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.
17
+ *
18
+ * @param {unknown} error The caught value.
19
+ * @returns {boolean} Whether `error` is a located diagnostic.
20
+ */
21
+ export function isDiagnostic(error) {
22
+ return isXprsnDiagnostic(error) || isTemplateDiagnostic(error) || isQueryDiagnostic(error);
23
+ }
24
+
25
+ /** @type {(msg: string) => never} */
26
+ export let err = (msg) => {
27
+ throw Error(msg);
28
+ };
29
+
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");
34
+
35
+ /** @type {(path: string, msg: string) => never} */
36
+ export let fault = (path, msg) => {
37
+ throw Object.assign(Error(path + ": " + msg), { [LOCATION]: { path } });
38
+ };
39
+
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
+ );
67
+ };
package/lib/names.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The engine's closed name vocabulary: what counts as an object worth
3
+ * descending into, what an author may name, and the anchors the engine binds
4
+ * for itself. The traversal checks a report definition against it, and the
5
+ * instance checks a target's `{ name, compile }` shape against the same rules —
6
+ * one spelling of "a plain identifier", wherever a name is offered.
7
+ */
8
+ /** @type {(value: any) => boolean} */
9
+ export let record = (value) => value && typeof value === "object" && !Array.isArray(value);
10
+ export let NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
11
+ // The anchors quario actually binds at evaluation time.
12
+ export let ANCHORS = ["@", "$", "run", "loop", "page"];
13
+ // Refused as group handle names: the anchors above plus the namespace held for
14
+ // future ones. Engine-provided values arrive on a new anchor rather than as new
15
+ // fields on `$` or a group handle (SCHEMA.md, "Stability"), so protecting the
16
+ // anchor namespace is the whole of what the authoring surface has to reserve --
17
+ // aggregate and run names stay the author's, `count` and `index` included.
18
+ export let RESERVED = new Set([...ANCHORS, "report", "table", "data", "meta"]);
19
+ export let BLOCKED = new Set(["__proto__", "constructor", "prototype"]);