@factoidal/core 0.1.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +201 -0
  3. package/README.md +514 -0
  4. package/browser-wasm.js +872 -0
  5. package/browser.d.ts +514 -0
  6. package/browser.js +2276 -0
  7. package/factoidal-npm-entry.js +32609 -0
  8. package/factoidal-npm-entry.wasm.assets/code-7ac046580f1bbdda8dc6.wasm +0 -0
  9. package/factoidal-npm-entry.wasm.js +455 -0
  10. package/factoidal.js +27560 -0
  11. package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
  12. package/factoidal.wasm.js +457 -0
  13. package/fn.d.ts +519 -0
  14. package/fn.js +916 -0
  15. package/hacl-init.js +92 -0
  16. package/hacl-wasm/FStar.wasm +0 -0
  17. package/hacl-wasm/Hacl_Bignum.wasm +0 -0
  18. package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
  19. package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
  20. package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
  21. package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
  22. package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
  23. package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
  24. package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
  25. package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
  26. package/hacl-wasm/LowStar_Endianness.wasm +0 -0
  27. package/hacl-wasm/WasmSupport.wasm +0 -0
  28. package/hacl-wasm/api.js +775 -0
  29. package/hacl-wasm/api.json +3787 -0
  30. package/hacl-wasm/layouts.json +1 -0
  31. package/hacl-wasm/loader.js +568 -0
  32. package/hacl-wasm/shell.js +12 -0
  33. package/index.d.ts +1068 -0
  34. package/index.js +237 -0
  35. package/index.mjs +85 -0
  36. package/lib/api.js +2140 -0
  37. package/lib/engine-js.js +165 -0
  38. package/lib/engine-wasm.js +300 -0
  39. package/package.json +101 -0
  40. package/rdfjs.js +540 -0
  41. package/version.json +101 -0
  42. package/wasm.d.ts +130 -0
  43. package/wasm.js +158 -0
package/browser.js ADDED
@@ -0,0 +1,2276 @@
1
+ // factoidal — browser ESM entry.
2
+ //
3
+ // Ship this to the browser as an ES module:
4
+ //
5
+ // <script type="module">
6
+ // import { query } from 'https://unpkg.com/factoidal/browser.js';
7
+ // const r = await query(dataTtl, 'SELECT * WHERE { ?s ?p ?o }');
8
+ // console.log(r.results.bindings);
9
+ // </script>
10
+ //
11
+ // Exposes the same async API as the Node entry point (index.mjs) so a
12
+ // single codebase works on either side.
13
+ //
14
+ // ASSUMPTION: `factoidal.js` is fetched from a URL relative to this
15
+ // module — i.e. it sits next to `browser.js` on the same server. That
16
+ // is how npm packages are typically shipped by unpkg / jsDelivr, and
17
+ // it matches our own GitHub Pages layout under
18
+ // `docs/fstar-extracted/`. If you need to load `factoidal.js` from a
19
+ // different URL, call `setFactoidalUrl(url)` before `query()`.
20
+ //
21
+ // The F*-extracted evaluator itself doesn't use fetch, localStorage,
22
+ // DOM APIs, or anything else browser-specific — it's pure JavaScript
23
+ // that happens to have been authored by a compiler. The only piece of
24
+ // browser plumbing is the fake filesystem we prime via
25
+ // `globalThis.jsoo_fs_tmp`; see README.md for more on that.
26
+
27
+ const DEFAULT_URL = new URL('./factoidal.js', import.meta.url).href;
28
+
29
+ let _factoidalUrl = DEFAULT_URL;
30
+ let _factoidalSrc = null;
31
+ let _fetchPromise = null;
32
+
33
+ /**
34
+ * Override where `factoidal.js` is loaded from. Useful for bundlers
35
+ * that can't sit the artifact next to this module.
36
+ */
37
+ export function setFactoidalUrl(url) {
38
+ _factoidalUrl = url;
39
+ _factoidalSrc = null;
40
+ _fetchPromise = null;
41
+ }
42
+
43
+ /**
44
+ * Current `factoidal.js` source URL. Lets a caller that wants a
45
+ * non-default bundle (e.g. a per-page `js-url` override, or the
46
+ * source-mapped debug bundle from the `jsoo-debug-bundle` skill)
47
+ * check before calling `setFactoidalUrl()`, so it only pays the
48
+ * cache-reset cost when the URL actually changes.
49
+ */
50
+ export function getFactoidalUrl() {
51
+ return _factoidalUrl;
52
+ }
53
+
54
+ function loadFactoidalSource() {
55
+ if (_factoidalSrc) return Promise.resolve(_factoidalSrc);
56
+ if (_fetchPromise) return _fetchPromise;
57
+ _fetchPromise = fetch(_factoidalUrl)
58
+ .then((r) => {
59
+ if (!r.ok) {
60
+ throw new Error(`factoidal.js fetch failed: ${r.status} ${r.statusText}`);
61
+ }
62
+ return r.text();
63
+ })
64
+ .then((text) => { _factoidalSrc = text; return text; });
65
+ return _fetchPromise;
66
+ }
67
+
68
+ const DATA_FORMAT_EXT = {
69
+ turtle: 'ttl',
70
+ ttl: 'ttl',
71
+ ntriples: 'nt',
72
+ nt: 'nt',
73
+ nquads: 'nq',
74
+ nq: 'nq',
75
+ trig: 'trig',
76
+ rdfxml: 'rdf',
77
+ 'rdf-xml': 'rdf',
78
+ rdf: 'rdf',
79
+ jsonld: 'jsonld',
80
+ 'json-ld': 'jsonld',
81
+ };
82
+
83
+ const OUTPUT_FORMATS = new Set(['json', 'csv', 'tsv', 'xml', 'table', 'ntriples']);
84
+ const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
85
+
86
+ function extForFormat(fmt) {
87
+ const key = String(fmt || 'turtle').toLowerCase();
88
+ if (!(key in DATA_FORMAT_EXT)) {
89
+ throw new TypeError(
90
+ `Unknown dataFormat '${fmt}'. Expected one of: ` +
91
+ Object.keys(DATA_FORMAT_EXT).join(', ')
92
+ );
93
+ }
94
+ return DATA_FORMAT_EXT[key];
95
+ }
96
+
97
+ // RDF 1.2 opt-in via a "*12" format name (turtle12/ttl12/nt12/nquads12/
98
+ // trig12/...): strip the "12" suffix to the base ext and flag rdf12 so
99
+ // the caller adds the CLI's --rdf12 flag (Mode_12 loaders: triple terms
100
+ // <<( )>>, ~ reifiers, {| |} annotations, directional literals). A plain
101
+ // format name stays RDF 1.1.
102
+ function extAnd12(fmt) {
103
+ const key = String(fmt || 'turtle').toLowerCase();
104
+ const m = /^(.+)12$/.exec(key);
105
+ if (m && (m[1] in DATA_FORMAT_EXT)) {
106
+ return { ext: DATA_FORMAT_EXT[m[1]], rdf12: true };
107
+ }
108
+ return { ext: extForFormat(key), rdf12: false };
109
+ }
110
+
111
+ // ---------------------------------------------------------------------
112
+ // #240 byte-encoding convention: under js_of_ocaml use-js-string=true
113
+ // (jsoo 6.x default) OCaml strings ARE the host JS strings, using a
114
+ // "bytes-as-JS-chars" convention — each JS string char's low byte is
115
+ // one OCaml byte, built via String.fromCharCode per UTF-8 byte. Any
116
+ // *textual* content handed to the `factoidal.js` bundle (RDF data,
117
+ // SPARQL query text) must be UTF-8-encoded and byte-packed into that
118
+ // convention, or non-ASCII input desyncs BatUTF8 and throws
119
+ // BatUChar.Out_of_range (see #240, and the `jsoo-debug-bundle` skill).
120
+ //
121
+ // This was previously duplicated in
122
+ // docs/fstar-extracted/factoidal-sparql-client.js as an inline
123
+ // `jsToBytesAsChars` helper; it now lives here as the one true
124
+ // implementation, applied automatically by `query()` / `toRdf()` /
125
+ // `canonicalize()` / `queryDataset()` below. It is deliberately NOT
126
+ // applied inside `runFactoidalCli()` itself: that primitive's `files`
127
+ // contents are sometimes genuinely opaque bytes already packed
128
+ // one-char-per-byte by the caller (e.g. the COTTAS/Parquet demo, which
129
+ // reads a binary `.parquet` file and packs it itself) — re-encoding
130
+ // those through TextEncoder would corrupt them. Callers driving
131
+ // `runFactoidalCli()` directly with *text* content should call this
132
+ // first, same as the higher-level helpers do internally.
133
+ // ---------------------------------------------------------------------
134
+ const _textEncoder = (typeof TextEncoder !== 'undefined') ? new TextEncoder() : null;
135
+
136
+ /**
137
+ * UTF-8-encode a JS string and repack it into the "bytes-as-JS-chars"
138
+ * convention the js_of_ocaml bundle expects for text content (RDF
139
+ * data, SPARQL query text) passed via `jsoo_fs_tmp` or CLI argv. A
140
+ * no-op for pure-ASCII input.
141
+ *
142
+ * @param {string} s
143
+ * @returns {string}
144
+ */
145
+ export function encodeTextAsBundleBytes(s) {
146
+ if (typeof s !== 'string') return s;
147
+ let bytes;
148
+ if (_textEncoder) {
149
+ bytes = _textEncoder.encode(s);
150
+ } else {
151
+ // Manual UTF-8 encode for runtimes without TextEncoder.
152
+ const out = [];
153
+ for (let i = 0; i < s.length; i++) {
154
+ const c = s.charCodeAt(i);
155
+ if (c < 0x80) out.push(c);
156
+ else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
157
+ else if (c < 0xd800 || c >= 0xe000) {
158
+ out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
159
+ } else {
160
+ const hi = c, lo = s.charCodeAt(++i);
161
+ const cp = 0x10000 + (((hi & 0x3ff) << 10) | (lo & 0x3ff));
162
+ out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f),
163
+ 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
164
+ }
165
+ }
166
+ bytes = new Uint8Array(out);
167
+ }
168
+ // Pack bytes into a JS string. fromCharCode.apply blows the arg-list
169
+ // limit on long inputs (~64K), so chunk.
170
+ let r = '';
171
+ for (let i = 0; i < bytes.length; i += 0x4000) {
172
+ r += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(bytes.length, i + 0x4000)));
173
+ }
174
+ return r;
175
+ }
176
+
177
+ /**
178
+ * Run one CLI invocation of the js_of_ocaml `factoidal.js` bundle
179
+ * in-browser: argv + a fake filesystem in, {stdout, stderr, exitCode}
180
+ * out. This is the shared primitive `query()` (and `toRdf()` /
181
+ * `canonicalize()` below) are built on — factored out so callers that
182
+ * need a CLI flag combination this module doesn't wrap yet (or a
183
+ * Node-side driver, e.g. lib/engine-js.js's fs-based equivalent) can
184
+ * still drive the same bundle. See jsonld-playground-client.js for an
185
+ * example of code written against this shape so it runs identically
186
+ * under this browser driver and under Node's fs-based driver.
187
+ *
188
+ * `files` contents are passed through byte-for-byte — no automatic
189
+ * UTF-8/#240 encoding here (see `encodeTextAsBundleBytes()` above);
190
+ * callers driving text content directly through this primitive should
191
+ * apply it themselves first.
192
+ *
193
+ * @param {string[]} args CLI arguments (after argv[0]/argv[1]).
194
+ * @param {Array<{name: string, content: string}>} files
195
+ * Documents for the fake filesystem. Names must start with
196
+ * '/static/'.
197
+ * @param {object} [options]
198
+ * @param {(src: string) => string} [options.transformSource]
199
+ * Optional hook applied to the fetched bundle source before
200
+ * each eval, e.g. to splice in the `?jsoo-debug=1` BatUChar
201
+ * instrumentation the sparql-client web component uses for
202
+ * #240 diagnostics. Not cached — safe to vary per call.
203
+ * @returns {Promise<{stdout: string, stderr: string, exitCode: number, engineMs: number}>}
204
+ */
205
+ export async function runFactoidalCli(args, files, options) {
206
+ const opts = options || {};
207
+ let src = await loadFactoidalSource();
208
+ if (typeof opts.transformSource === 'function') {
209
+ src = opts.transformSource(src);
210
+ }
211
+
212
+ // Preserve anything we're about to overwrite.
213
+ const orig = {
214
+ log: console.log,
215
+ error: console.error,
216
+ argv: globalThis.process && globalThis.process.argv,
217
+ exit: globalThis.process && globalThis.process.exit,
218
+ jsooFs: globalThis.jsoo_fs_tmp,
219
+ };
220
+
221
+ const stdoutBuf = [];
222
+ const stderrBuf = [];
223
+ console.log = (...a) => stdoutBuf.push(a.join(' '));
224
+ console.error = (...a) => stderrBuf.push(a.join(' '));
225
+
226
+ globalThis.process = globalThis.process || {};
227
+ globalThis.process.argv = ['node', 'factoidal', ...args];
228
+ // Mount under `/static/`: that path is an MlFakeDevice in js_of_ocaml
229
+ // both in the browser and under Node. `/tmp/` is only fake in the
230
+ // browser; keeping one path keeps the Node and browser drivers in
231
+ // lockstep.
232
+ globalThis.jsoo_fs_tmp = (files || []).map((f) => ({
233
+ name: f.name, content: f.content,
234
+ }));
235
+
236
+ let exitCode = 0;
237
+ const EXIT_SENTINEL = new Error('__factoidal_exit__');
238
+ globalThis.process.exit = (n) => { exitCode = n | 0; throw EXIT_SENTINEL; };
239
+
240
+ function restore() {
241
+ console.log = orig.log;
242
+ console.error = orig.error;
243
+ if (globalThis.process) {
244
+ globalThis.process.argv = orig.argv;
245
+ globalThis.process.exit = orig.exit;
246
+ }
247
+ globalThis.jsoo_fs_tmp = orig.jsooFs;
248
+ }
249
+
250
+ const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
251
+ try {
252
+ (new Function(src))();
253
+ } catch (e) {
254
+ if (e !== EXIT_SENTINEL) { restore(); throw e; }
255
+ } finally {
256
+ restore();
257
+ }
258
+ const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
259
+
260
+ return {
261
+ stdout: stdoutBuf.join('\n'),
262
+ stderr: stderrBuf.join('\n'),
263
+ exitCode,
264
+ engineMs: t1 - t0,
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Run a SPARQL query against an RDF dataset in memory. Same shape as
270
+ * the Node entry point. See index.d.ts for the full type.
271
+ *
272
+ * @param {string} dataString
273
+ * @param {string} queryString
274
+ * @param {object} [options]
275
+ * @param {string} [options.dataFormat='turtle']
276
+ * @param {string} [options.entail='none']
277
+ * @param {string} [options.output='json']
278
+ * @returns {Promise<object|string>}
279
+ */
280
+ export async function query(dataString, queryString, options) {
281
+ if (typeof dataString !== 'string') {
282
+ throw new TypeError('query: dataString must be a string');
283
+ }
284
+ if (typeof queryString !== 'string') {
285
+ throw new TypeError('query: queryString must be a string');
286
+ }
287
+
288
+ const opts = options || {};
289
+ const dataFormat = opts.dataFormat || 'turtle';
290
+ const entail = opts.entail || 'none';
291
+ const output = opts.output || 'json';
292
+ // SPARQL 1.2 opt-in: {sparql12:true} or {version:'1.2'} parses the
293
+ // query with the tokenize_12 parser (--sparql12) and loads the data in
294
+ // Mode_12 (--rdf12), so triple-term patterns / TRIPLE / isTRIPLE /
295
+ // lang-dir builtins work. Default stays SPARQL 1.1, byte-identical.
296
+ const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
297
+
298
+ if (!ENTAIL_VALUES.has(entail)) {
299
+ throw new TypeError(
300
+ `query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`
301
+ );
302
+ }
303
+ if (!OUTPUT_FORMATS.has(output)) {
304
+ throw new TypeError(
305
+ `query: output must be one of ${[...OUTPUT_FORMATS].join(', ')}`
306
+ );
307
+ }
308
+
309
+ // Extension functions / SERVICE endpoint snapshots live in the
310
+ // npm-entry ABI engine, not the CLI bundle — route through it when
311
+ // any registration is active (issue #463 / #57).
312
+ if (abiRegistryActive()) {
313
+ if (entail !== 'none') {
314
+ throw new Error(
315
+ 'query: entailment regimes are not yet supported while ' +
316
+ 'extension functions or SERVICE endpoints are registered');
317
+ }
318
+ return queryViaAbi(dataString, queryString, { dataFormat, output, sparql12 });
319
+ }
320
+
321
+ const { ext, rdf12 } = extAnd12(dataFormat);
322
+ const dataPath = '/static/data.' + ext;
323
+
324
+ const argv = [
325
+ '-d', dataPath,
326
+ '-e', encodeTextAsBundleBytes(queryString),
327
+ '-o', output === 'json' ? 'json' : output,
328
+ ];
329
+ // A 1.2 query's data (triple terms) must load in Mode_12; --sparql12
330
+ // switches the query parser. Adding --rdf12 to 1.1 data is harmless
331
+ // (the Mode_12 loader is a superset).
332
+ if (rdf12 || sparql12) argv.push('--rdf12');
333
+ if (sparql12) argv.push('--sparql12');
334
+ if (entail !== 'none') argv.push('--entail', entail);
335
+
336
+ const { stdout, stderr, exitCode, engineMs } = await runFactoidalCli(
337
+ argv, [{ name: dataPath, content: encodeTextAsBundleBytes(dataString) }]);
338
+
339
+ if (exitCode !== 0) {
340
+ const msg =
341
+ (stderr || stdout || `factoidal exited with code ${exitCode}`).trim();
342
+ const err = new Error('SPARQL query failed: ' + msg);
343
+ err.exitCode = exitCode;
344
+ err.stderr = stderr;
345
+ err.stdout = stdout;
346
+ throw err;
347
+ }
348
+
349
+ if (output !== 'json') return stdout;
350
+
351
+ const firstBrace = stdout.indexOf('{');
352
+ const lastBrace = stdout.lastIndexOf('}');
353
+ if (firstBrace < 0 || lastBrace < firstBrace) {
354
+ const err = new Error(
355
+ 'factoidal did not produce JSON on stdout. Raw output: ' + stdout
356
+ );
357
+ err.stdout = stdout;
358
+ err.stderr = stderr;
359
+ throw err;
360
+ }
361
+ const jsonText = stdout.slice(firstBrace, lastBrace + 1);
362
+ try {
363
+ const parsed = JSON.parse(jsonText);
364
+ // Non-enumerable: doesn't perturb JSON.stringify()/Object.keys()
365
+ // for callers that diff this against W3C .srx-derived fixtures,
366
+ // but is readable by callers that want engine-timing observability
367
+ // (e.g. the sparql-client web component's Details/timing panel).
368
+ Object.defineProperty(parsed, 'engineMs', { value: engineMs, enumerable: false });
369
+ return parsed;
370
+ } catch (e) {
371
+ const err = new Error(
372
+ 'factoidal JSON parse failed: ' + e.message +
373
+ '. Raw output: ' + stdout
374
+ );
375
+ err.stdout = stdout;
376
+ err.stderr = stderr;
377
+ throw err;
378
+ }
379
+ }
380
+
381
+ // Shared plumbing for toRdf() / canonicalize(): both are "parse this
382
+ // document, dump N-Quads" CLI invocations differing only in the mode
383
+ // flag (--dump-nq vs --canonicalize). Errors (bad syntax, or JSON-LD
384
+ // features Parser.JSONLD.fst doesn't yet cover -- e.g. a remote
385
+ // @context URL string, since there is no JSONLD.Loader/fetch step)
386
+ // surface as a rejected promise carrying the engine's own stderr, so
387
+ // callers can render the honest failure instead of inventing one.
388
+ async function dumpNQuads(mode, text, options) {
389
+ const opts = options || {};
390
+ const format = opts.format || 'jsonld';
391
+ const { ext, rdf12 } = extAnd12(format);
392
+ const baseIRI = opts.baseIRI || '';
393
+ const dataPath = '/static/data.' + ext;
394
+
395
+ const argv = [mode, '-d', dataPath];
396
+ if (rdf12) argv.push('--rdf12');
397
+ if (baseIRI) argv.push('-b', encodeTextAsBundleBytes(baseIRI));
398
+
399
+ const { stdout, stderr, exitCode } = await runFactoidalCli(
400
+ argv, [{ name: dataPath, content: encodeTextAsBundleBytes(text) }]);
401
+
402
+ if (exitCode !== 0) {
403
+ const msg = (stderr || stdout || `factoidal exited with code ${exitCode}`).trim();
404
+ const err = new Error(`${mode === '--canonicalize' ? 'canonicalize' : 'toRdf'} failed: ${msg}`);
405
+ err.exitCode = exitCode;
406
+ err.stderr = stderr;
407
+ err.stdout = stdout;
408
+ throw err;
409
+ }
410
+ return stdout;
411
+ }
412
+
413
+ /**
414
+ * Parse a document to RDF and dump sorted N-Quads (RDF.Canonical's
415
+ * `canonical_nquads` -- sorted, not RDFC-1.0 canonical bnode labels;
416
+ * see canonicalize() for that). Default format is 'jsonld' since this
417
+ * export exists mainly for the JSON-LD playground's "toRdf" step, but
418
+ * any DATA_FORMAT_EXT format works.
419
+ *
420
+ * @param {string} text
421
+ * @param {{format?: string, baseIRI?: string}} [options]
422
+ * @returns {Promise<string>} N-Quads text.
423
+ */
424
+ export async function toRdf(text, options) {
425
+ if (typeof text !== 'string') {
426
+ throw new TypeError('toRdf: text must be a string');
427
+ }
428
+ return dumpNQuads('--dump-nq', text, options);
429
+ }
430
+
431
+ /**
432
+ * RDFC-1.0 canonicalization: canonical blank-node labels + sorted
433
+ * canonical N-Quads (RDF.Canonical.fst's canonicalize_to_nquads).
434
+ *
435
+ * @param {string} text
436
+ * @param {{format?: string, baseIRI?: string}} [options]
437
+ * @returns {Promise<string>} canonical N-Quads text.
438
+ */
439
+ export async function canonicalize(text, options) {
440
+ if (typeof text !== 'string') {
441
+ throw new TypeError('canonicalize: text must be a string');
442
+ }
443
+ return dumpNQuads('--canonicalize', text, options);
444
+ }
445
+
446
+ // ---------------------------------------------------------------------
447
+ // Multi-file / multi-engine dataset queries. `query()` above only
448
+ // takes one data string into the default graph. Multi-named-graph
449
+ // pages (e.g. the life-sci demos, which load several Wikidata TTL
450
+ // files each into their own named graph) need more than that; this
451
+ // was previously duplicated per-page in
452
+ // docs/fstar-extracted/factoidal-sparql-client.js (`_getFilePayloads` /
453
+ // the JS-engine argv-building loop / `payloadsToTriG` for the wasm
454
+ // path). It now lives here as the one true implementation.
455
+ // ---------------------------------------------------------------------
456
+
457
+ /**
458
+ * @typedef {object} DatasetFile
459
+ * @property {string} content Document text.
460
+ * @property {string} [dataFormat='turtle'] One of DATA_FORMAT_EXT's keys.
461
+ * @property {string} [graph] Named-graph IRI. Omitted/falsy loads
462
+ * into the default graph (CLI `-d`); otherwise `--named IRI=path`.
463
+ */
464
+
465
+ // Merge N (graph, content) pairs into one TriG document: directives
466
+ // (@prefix/@base/PREFIX/BASE) are hoisted to the top and deduplicated;
467
+ // remaining triples are wrapped in `GRAPH <iri> { ... }` for files with
468
+ // a graph IRI, or left bare (default-graph triples section) otherwise.
469
+ // Used for the wasm engine path, whose query() API takes a single data
470
+ // string. Ported verbatim from factoidal-sparql-client.js's
471
+ // payloadsToTriG. Assumes Turtle-family (`dataFormat: 'turtle'`) input
472
+ // per file — the same assumption the original shim made.
473
+ function mergeFilesToTrig(files) {
474
+ const directives = new Set();
475
+ const blocks = [];
476
+ const dirRE = /^\s*(?:@prefix|@base|PREFIX|BASE)\b[^\n]*\.\s*$/i;
477
+ files.forEach((f) => {
478
+ const bodyLines = [];
479
+ (f.content || '').split(/\r?\n/).forEach((line) => {
480
+ if (dirRE.test(line)) directives.add(line.trim());
481
+ else bodyLines.push(line);
482
+ });
483
+ if (f.graph) {
484
+ blocks.push('GRAPH <' + f.graph + '> {\n' + bodyLines.join('\n') + '\n}\n');
485
+ } else {
486
+ blocks.push(bodyLines.join('\n') + '\n');
487
+ }
488
+ });
489
+ return [...directives].join('\n') + '\n\n' + blocks.join('\n');
490
+ }
491
+
492
+ /**
493
+ * Run a SPARQL query against a multi-file, multi-named-graph dataset,
494
+ * on either the js_of_ocaml (`js`, default) or wasm_of_ocaml (`wasm`)
495
+ * extraction target.
496
+ *
497
+ * @param {DatasetFile[]} files
498
+ * @param {string} queryString
499
+ * @param {object} [options]
500
+ * @param {string} [options.entail='none']
501
+ * @param {string} [options.output='json']
502
+ * @param {'js'|'wasm'} [options.engine='js']
503
+ * @param {string} [options.wasmUrl] Override for browser-wasm.js's
504
+ * `factoidal.wasm.js` URL (see `setFactoidalWasmUrl`). Only consulted
505
+ * when `options.engine === 'wasm'`.
506
+ * @param {(src: string) => string} [options.transformSource] Forwarded
507
+ * to `runFactoidalCli()` on the js engine path only.
508
+ * @returns {Promise<object|string>}
509
+ */
510
+ export async function queryDataset(files, queryString, options) {
511
+ if (!Array.isArray(files) || files.length === 0) {
512
+ throw new TypeError('queryDataset: files must be a non-empty array');
513
+ }
514
+ if (typeof queryString !== 'string') {
515
+ throw new TypeError('queryDataset: queryString must be a string');
516
+ }
517
+
518
+ const opts = options || {};
519
+ const entail = opts.entail || 'none';
520
+ const output = opts.output || 'json';
521
+ const engine = opts.engine || 'js';
522
+
523
+ if (!ENTAIL_VALUES.has(entail)) {
524
+ throw new TypeError(`queryDataset: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
525
+ }
526
+ if (!OUTPUT_FORMATS.has(output)) {
527
+ throw new TypeError(`queryDataset: output must be one of ${[...OUTPUT_FORMATS].join(', ')}`);
528
+ }
529
+
530
+ if (engine === 'wasm') {
531
+ // wasm_of_ocaml's query() only accepts one data string — merge
532
+ // named graphs into TriG first. Dynamically imported so pages that
533
+ // never touch the wasm engine don't pay for loading this module.
534
+ const wasmMod = await import(new URL('./browser-wasm.js', import.meta.url).href);
535
+ // Only reset the module's cached bundle source when the URL
536
+ // actually changes — setFactoidalWasmUrl() unconditionally clears
537
+ // the cache, and this can run once per query.
538
+ if (opts.wasmUrl && typeof wasmMod.setFactoidalWasmUrl === 'function'
539
+ && (typeof wasmMod.getFactoidalWasmUrl !== 'function'
540
+ || wasmMod.getFactoidalWasmUrl() !== opts.wasmUrl)) {
541
+ wasmMod.setFactoidalWasmUrl(opts.wasmUrl);
542
+ }
543
+ const trigText = mergeFilesToTrig(files);
544
+ const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
545
+ const parsed = await wasmMod.query(trigText, queryString, { dataFormat: 'trig', entail, output });
546
+ const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
547
+ if (output === 'json' && parsed && typeof parsed === 'object') {
548
+ Object.defineProperty(parsed, 'engineMs', { value: t1 - t0, enumerable: false });
549
+ }
550
+ return parsed;
551
+ }
552
+ if (engine !== 'js') {
553
+ throw new TypeError(`queryDataset: engine must be 'js' or 'wasm', got '${engine}'`);
554
+ }
555
+
556
+ // js_of_ocaml path — multi-file via jsoo_fs_tmp, one -d/--named per
557
+ // file, same as the JS-engine branch of the old sparql-client shim.
558
+ const cliFiles = [];
559
+ const argv = [];
560
+ files.forEach((f, i) => {
561
+ const ext = extForFormat(f.dataFormat || 'turtle');
562
+ const path = '/static/data-' + i + '.' + ext;
563
+ cliFiles.push({ name: path, content: encodeTextAsBundleBytes(f.content || '') });
564
+ if (f.graph) argv.push('--named', f.graph + '=' + path);
565
+ else argv.push('-d', path);
566
+ });
567
+ argv.push('-e', encodeTextAsBundleBytes(queryString), '-o', output === 'json' ? 'json' : output);
568
+ if (entail !== 'none') argv.push('--entail', entail);
569
+
570
+ const { stdout, stderr, exitCode, engineMs } =
571
+ await runFactoidalCli(argv, cliFiles, { transformSource: opts.transformSource });
572
+
573
+ if (exitCode !== 0) {
574
+ const msg = (stderr || stdout || `factoidal exited with code ${exitCode}`).trim();
575
+ const err = new Error('SPARQL query failed: ' + msg);
576
+ err.exitCode = exitCode;
577
+ err.stderr = stderr;
578
+ err.stdout = stdout;
579
+ throw err;
580
+ }
581
+
582
+ if (output !== 'json') return stdout;
583
+
584
+ const firstBrace = stdout.indexOf('{');
585
+ const lastBrace = stdout.lastIndexOf('}');
586
+ if (firstBrace < 0 || lastBrace < firstBrace) {
587
+ const err = new Error('factoidal did not produce JSON on stdout. Raw output: ' + stdout);
588
+ err.stdout = stdout;
589
+ err.stderr = stderr;
590
+ throw err;
591
+ }
592
+ try {
593
+ const parsed = JSON.parse(stdout.slice(firstBrace, lastBrace + 1));
594
+ Object.defineProperty(parsed, 'engineMs', { value: engineMs, enumerable: false });
595
+ return parsed;
596
+ } catch (e) {
597
+ const err = new Error('factoidal JSON parse failed: ' + e.message + '. Raw output: ' + stdout);
598
+ err.stdout = stdout;
599
+ err.stderr = stderr;
600
+ throw err;
601
+ }
602
+ }
603
+
604
+ // ---------------------------------------------------------------------
605
+ // npm-entry ABI loader (persistent factoidalNpmEntry object, built from
606
+ // bin/npm-entry/entry_jsoo.ml -- see that file's header comment for the
607
+ // full ABI contract). The CLI bundle above (runFactoidalCli / query /
608
+ // toRdf / canonicalize) covers most of the surface with a fresh bundle
609
+ // eval per call; a few operations (RIF Core saturation today) are only
610
+ // exposed through this persistent ABI, so this loader fetches + evals
611
+ // factoidal-npm-entry.js once and reads the `factoidalNpmEntry` object
612
+ // it registers on globalThis -- same registration Node's index.js reads
613
+ // off `module.exports.factoidalNpmEntry` / `globalThis.factoidalNpmEntry`
614
+ // (see npm/factoidal/index.js's loadEntry()).
615
+ // ---------------------------------------------------------------------
616
+
617
+ let _npmEntryUrl = new URL('./factoidal-npm-entry.js', import.meta.url).href;
618
+ let _npmEntryPromise = null;
619
+
620
+ /**
621
+ * Override where `factoidal-npm-entry.js` is loaded from. Same idea as
622
+ * setFactoidalUrl() for the CLI bundle.
623
+ */
624
+ export function setFactoidalNpmEntryUrl(url) {
625
+ _npmEntryUrl = url;
626
+ _npmEntryPromise = null;
627
+ }
628
+
629
+ /**
630
+ * Fetch + evaluate factoidal-npm-entry.js exactly once, returning the
631
+ * `factoidalNpmEntry` ABI object it registers on globalThis. Optional:
632
+ * everything the CLI bundle can do works without it.
633
+ *
634
+ * @returns {Promise<object>} the factoidalNpmEntry ABI object.
635
+ */
636
+ export async function loadNpmEntry() {
637
+ if (_npmEntryPromise) return _npmEntryPromise;
638
+ _npmEntryPromise = fetch(_npmEntryUrl)
639
+ .then((r) => {
640
+ if (!r.ok) {
641
+ throw new Error(
642
+ `factoidal-npm-entry.js fetch failed: ${r.status} ${r.statusText}`);
643
+ }
644
+ return r.text();
645
+ })
646
+ .then((src) => {
647
+ (new Function(src))();
648
+ const abi = globalThis.factoidalNpmEntry;
649
+ if (!abi) {
650
+ throw new Error(
651
+ 'factoidal-npm-entry.js loaded but did not register ' +
652
+ 'factoidalNpmEntry on globalThis');
653
+ }
654
+ return abi;
655
+ });
656
+ return _npmEntryPromise;
657
+ }
658
+
659
+ /* ---------------------------------------------------------------
660
+ SPARQL 1.1 §17.6 extension functions (issue #463) + SERVICE
661
+ endpoint snapshots (issue #57 family) — browser side.
662
+
663
+ Mirrors npm/factoidal/lib/api.js: user functions keyed by IRI, a
664
+ synchronous bridge into the engine's registry (the extracted
665
+ evaluator is synchronous), per-query memoisation, and a bounded
666
+ re-evaluation trampoline for async functions. These run on the
667
+ npm-entry ABI engine, so query()/fn.query routes through the ABI
668
+ (queryViaAbi below) whenever a registration is active.
669
+ --------------------------------------------------------------- */
670
+ const EXT_PENDING_MARKER = '__FACTOIDAL_EXT_PENDING__';
671
+ const EXT_MAX_ROUNDS = 25;
672
+ const extFunctions = new Map(); // iri -> user fn
673
+ const extInstalled = new Set(); // iris registered into the ABI
674
+ let extCache = new Map(); // key -> normalized result (or null)
675
+ let extPending = []; // [{key, promise}] for this pass
676
+ let serviceEndpointsActive = false;
677
+
678
+ function abiEntryResult(jsonText, what) {
679
+ const r = JSON.parse(jsonText);
680
+ if (!r.ok) throw new Error(`${what}: ${r.error}`);
681
+ return r;
682
+ }
683
+
684
+ function extBridge(iriJs, argsJsonJs) {
685
+ // Called SYNCHRONOUSLY from inside the engine.
686
+ const iri = String(iriJs);
687
+ const argsJson = String(argsJsonJs);
688
+ const key = iri + ' ' + argsJson;
689
+ if (extCache.has(key)) return extCache.get(key);
690
+ const fn = extFunctions.get(iri);
691
+ if (!fn) return null;
692
+ let out;
693
+ try {
694
+ out = fn(JSON.parse(argsJson));
695
+ } catch (_e) {
696
+ extCache.set(key, null);
697
+ return null;
698
+ }
699
+ if (out && typeof out.then === 'function') {
700
+ extPending.push({ key, promise: out });
701
+ return EXT_PENDING_MARKER;
702
+ }
703
+ out = out === undefined ? null : out;
704
+ extCache.set(key, out);
705
+ return out;
706
+ }
707
+
708
+ async function withExtensionRounds(runOnce) {
709
+ extCache = new Map(); // per-evaluation memo (purity contract)
710
+ for (let round = 0; ; round++) {
711
+ extPending = [];
712
+ const result = runOnce();
713
+ if (extPending.length === 0) return result;
714
+ if (round >= EXT_MAX_ROUNDS) {
715
+ throw new Error(
716
+ 'extension functions: async resolution did not converge ' +
717
+ `within ${EXT_MAX_ROUNDS} evaluation rounds`);
718
+ }
719
+ const pend = extPending;
720
+ extPending = [];
721
+ await Promise.all(pend.map(async ({ key, promise }) => {
722
+ try {
723
+ const v = await promise;
724
+ extCache.set(key, v === undefined ? null : v);
725
+ } catch (_e) {
726
+ extCache.set(key, null);
727
+ }
728
+ }));
729
+ }
730
+ }
731
+
732
+ /**
733
+ * Register a custom SPARQL extension function (SPARQL 1.1 §17.6,
734
+ * Comunica-style). `fn` may be sync or async; it receives the
735
+ * evaluated arguments as SRJ-style term objects and returns a term
736
+ * object, a JS primitive, a Promise of either, or null/undefined
737
+ * (= the §17.6 error).
738
+ */
739
+ export async function registerExtensionFunction(iri, fn) {
740
+ if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
741
+ throw new TypeError(
742
+ 'registerExtensionFunction: iri must be an absolute IRI string');
743
+ }
744
+ if (typeof fn !== 'function') {
745
+ throw new TypeError('registerExtensionFunction: fn must be a function');
746
+ }
747
+ extFunctions.set(iri, fn);
748
+ const abi = await loadNpmEntry();
749
+ if (typeof abi.registerExtensionFunction !== 'function') {
750
+ throw new Error(
751
+ 'registerExtensionFunction: this npm-entry bundle predates ' +
752
+ 'extension functions (issue #463) — rebuild.');
753
+ }
754
+ if (!extInstalled.has(iri)) {
755
+ abiEntryResult(abi.registerExtensionFunction(iri, extBridge),
756
+ 'registerExtensionFunction');
757
+ extInstalled.add(iri);
758
+ }
759
+ }
760
+
761
+ /** Remove one registered extension function. */
762
+ export async function unregisterExtensionFunction(iri) {
763
+ extFunctions.delete(iri);
764
+ const abi = await loadNpmEntry();
765
+ if (typeof abi.unregisterExtensionFunction === 'function'
766
+ && extInstalled.has(iri)) {
767
+ abiEntryResult(abi.unregisterExtensionFunction(iri),
768
+ 'unregisterExtensionFunction');
769
+ extInstalled.delete(iri);
770
+ }
771
+ }
772
+
773
+ /** Remove every registered extension function. */
774
+ export async function clearExtensionFunctions() {
775
+ extFunctions.clear();
776
+ const abi = await loadNpmEntry();
777
+ if (typeof abi.clearExtensionFunctions === 'function') {
778
+ abiEntryResult(abi.clearExtensionFunctions(), 'clearExtensionFunctions');
779
+ }
780
+ extInstalled.clear();
781
+ }
782
+
783
+ /**
784
+ * Bind a SPARQL SERVICE endpoint IRI to a local graph snapshot so
785
+ * SERVICE <iri> { ... } (and LATERAL { SERVICE ... }) resolve against
786
+ * it in-process — the same registry the W3C federated suite uses.
787
+ * `data` is raw RDF text ({format}, default turtle) or any object
788
+ * with toNQuads(). The snapshot is the payload's default graph.
789
+ */
790
+ export async function registerServiceEndpoint(iri, data, options) {
791
+ if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
792
+ throw new TypeError(
793
+ 'registerServiceEndpoint: iri must be an absolute IRI string');
794
+ }
795
+ const abi = await loadNpmEntry();
796
+ if (typeof abi.registerServiceEndpoint !== 'function') {
797
+ throw new Error(
798
+ 'registerServiceEndpoint: this npm-entry bundle predates SERVICE ' +
799
+ 'endpoint registration — rebuild.');
800
+ }
801
+ let nq;
802
+ if (data && typeof data.toNQuads === 'function') {
803
+ nq = data.toNQuads();
804
+ } else {
805
+ const fmt = (options && options.format) || 'turtle';
806
+ if (fmt === 'nquads' || fmt === 'ntriples') {
807
+ nq = String(data);
808
+ } else {
809
+ nq = abiEntryResult(
810
+ abi.parseToDatasetJson(String(data), fmt, ''),
811
+ 'registerServiceEndpoint(parse)').nquads;
812
+ }
813
+ }
814
+ const r = abiEntryResult(abi.registerServiceEndpoint(iri, nq),
815
+ 'registerServiceEndpoint');
816
+ serviceEndpointsActive = true;
817
+ return r;
818
+ }
819
+
820
+ /** Remove every registered SERVICE endpoint snapshot. */
821
+ export async function clearServiceEndpoints() {
822
+ const abi = await loadNpmEntry();
823
+ if (typeof abi.clearServiceEndpoints === 'function') {
824
+ abiEntryResult(abi.clearServiceEndpoints(), 'clearServiceEndpoints');
825
+ }
826
+ serviceEndpointsActive = false;
827
+ }
828
+
829
+ function abiRegistryActive() {
830
+ return extFunctions.size > 0 || serviceEndpointsActive;
831
+ }
832
+
833
+ // ABI-engine query path, used by query() whenever extension functions
834
+ // or SERVICE endpoints are registered (the CLI bundle is a separate
835
+ // engine instance and cannot see those registrations).
836
+ async function queryViaAbi(dataString, queryString, opts) {
837
+ const abi = await loadNpmEntry();
838
+ if (typeof abi.queryDataset !== 'function') {
839
+ throw new Error('query: npm-entry ABI lacks queryDataset — rebuild.');
840
+ }
841
+ const dataFormat = opts.dataFormat || 'turtle';
842
+ let nq;
843
+ if (dataFormat === 'nquads' || dataFormat === 'ntriples') {
844
+ nq = dataString;
845
+ } else {
846
+ nq = abiEntryResult(
847
+ abi.parseToDatasetJson(dataString, dataFormat, ''),
848
+ 'query(parse)').nquads;
849
+ }
850
+ const qfn = opts.sparql12 ? abi.queryDataset12 : abi.queryDataset;
851
+ const r = await withExtensionRounds(
852
+ () => abiEntryResult(qfn(nq, queryString), 'query'));
853
+ if (r.kind === 'ask') return { head: {}, boolean: r.boolean };
854
+ if (r.kind === 'construct') {
855
+ if (opts.output === 'json') {
856
+ throw new Error(
857
+ 'query: CONSTRUCT with output "json" is not supported on the ' +
858
+ 'extension/SERVICE registry path');
859
+ }
860
+ return r.nquads;
861
+ }
862
+ return r.srj;
863
+ }
864
+
865
+ /**
866
+ * RIF Core smoke saturation, run live: the exact premise graph and
867
+ * two-rule program baked into RIF.Core.Eval.fst as smoke_input_graph /
868
+ * smoke_program, saturated via RIF_Core_Eval.fixpoint in the loaded
869
+ * bundle (bin/npm-entry/entry_jsoo.ml's rifSmoke export). No user
870
+ * input -- a fixed capability probe (issue #274).
871
+ *
872
+ * @returns {Promise<{inputNquads:string, saturatedNquads:string,
873
+ * inputCount:number, derivedCount:number, rounds:number, fuel:number,
874
+ * engineMs:number}>}
875
+ */
876
+ export async function rifSmoke() {
877
+ const abi = await loadNpmEntry();
878
+ if (typeof abi.rifSmoke !== 'function') {
879
+ throw new Error(
880
+ 'rifSmoke: the loaded factoidal-npm-entry bundle predates the RIF exports');
881
+ }
882
+ const parsed = JSON.parse(abi.rifSmoke());
883
+ if (!parsed.ok) throw new Error(parsed.error || 'rifSmoke failed');
884
+ return parsed;
885
+ }
886
+
887
+ /**
888
+ * RIF Core forward-chaining saturation over caller-supplied RIF-XML
889
+ * rules and N-Quads premise data (default graph only -- RIF Core has
890
+ * no named-graph notion). Parsed via Parser_RIFXML.parse_rif_program,
891
+ * saturated via RIF_Core_Eval.fixpoint (bin/npm-entry/entry_jsoo.ml's
892
+ * rifEval export). Import directives in the RIF-XML are not resolved;
893
+ * merge any imported data into dataNQuads yourself first.
894
+ *
895
+ * @param {string} rifXml
896
+ * @param {string} dataNQuads
897
+ * @returns {Promise<{inputNquads:string, saturatedNquads:string,
898
+ * inputCount:number, derivedCount:number, rounds:number, fuel:number,
899
+ * engineMs:number}>}
900
+ */
901
+ export async function rifEval(rifXml, dataNQuads) {
902
+ if (typeof rifXml !== 'string') {
903
+ throw new TypeError('rifEval: rifXml must be a string');
904
+ }
905
+ if (typeof dataNQuads !== 'string') {
906
+ throw new TypeError('rifEval: dataNQuads must be a string');
907
+ }
908
+ const abi = await loadNpmEntry();
909
+ if (typeof abi.rifEval !== 'function') {
910
+ throw new Error(
911
+ 'rifEval: the loaded factoidal-npm-entry bundle predates the RIF exports');
912
+ }
913
+ const parsed = JSON.parse(abi.rifEval(rifXml, dataNQuads));
914
+ if (!parsed.ok) throw new Error(parsed.error || 'rifEval failed');
915
+ return parsed;
916
+ }
917
+
918
+ /**
919
+ * SHACL Core validation (bin/npm-entry/entry_jsoo.ml's shaclValidate
920
+ * export). dataNQuads/shapesNQuads are dataset-handle N-Quads text
921
+ * (default graph only) -- use toRdf()/canonicalize() above to get
922
+ * there from Turtle or another format.
923
+ *
924
+ * @param {string} dataNQuads
925
+ * @param {string} shapesNQuads
926
+ * @returns {Promise<{ok:true,conforms:boolean,reportNquads:string}>}
927
+ */
928
+ export async function shaclValidate(dataNQuads, shapesNQuads) {
929
+ if (typeof dataNQuads !== 'string') {
930
+ throw new TypeError('shaclValidate: dataNQuads must be a string');
931
+ }
932
+ if (typeof shapesNQuads !== 'string') {
933
+ throw new TypeError('shaclValidate: shapesNQuads must be a string');
934
+ }
935
+ const abi = await loadNpmEntry();
936
+ if (typeof abi.shaclValidate !== 'function') {
937
+ throw new Error(
938
+ 'shaclValidate: the loaded factoidal-npm-entry bundle predates the SHACL export');
939
+ }
940
+ const parsed = JSON.parse(abi.shaclValidate(dataNQuads, shapesNQuads));
941
+ if (!parsed.ok) throw new Error(parsed.error || 'shaclValidate failed');
942
+ return parsed;
943
+ }
944
+
945
+ /**
946
+ * ShEx (Shape Expressions) validation of one focus node against one
947
+ * shape (bin/npm-entry/entry_jsoo.ml's shexValidate export).
948
+ * `focus`/`shapeLabel` are an IRI, or "_:label" for a blank node;
949
+ * `shapeLabel` "" validates against the schema's own `start`.
950
+ *
951
+ * @param {string} dataNQuads
952
+ * @param {string} schemaJson the schema, as text -- either ShExJ (a JSON
953
+ * Schema document) or ShExC (the compact human-readable syntax).
954
+ * Dispatch rule: the first non-whitespace character decides the
955
+ * format -- '{' means ShExJ, anything else is parsed as ShExC (no
956
+ * valid ShExC document starts with '{' after whitespace).
957
+ * @param {string} focus
958
+ * @param {string} shapeLabel
959
+ * @returns {Promise<{ok:true,verdict:boolean|null,deferred:boolean}>}
960
+ * verdict null (deferred:true) means outside this engine's decidable
961
+ * ShEx fragment -- never a guessed answer.
962
+ */
963
+ export async function shexValidate(dataNQuads, schemaJson, focus, shapeLabel) {
964
+ if (typeof dataNQuads !== 'string') {
965
+ throw new TypeError('shexValidate: dataNQuads must be a string');
966
+ }
967
+ if (typeof schemaJson !== 'string') {
968
+ throw new TypeError('shexValidate: schemaJson must be a string');
969
+ }
970
+ if (typeof focus !== 'string') {
971
+ throw new TypeError('shexValidate: focus must be a string');
972
+ }
973
+ const abi = await loadNpmEntry();
974
+ if (typeof abi.shexValidate !== 'function') {
975
+ throw new Error(
976
+ 'shexValidate: the loaded factoidal-npm-entry bundle predates the ShEx export');
977
+ }
978
+ const parsed = JSON.parse(abi.shexValidate(dataNQuads, schemaJson, focus, shapeLabel || ''));
979
+ if (!parsed.ok) throw new Error(parsed.error || 'shexValidate failed');
980
+ return parsed;
981
+ }
982
+
983
+ // ---------------------------------------------------------------------
984
+ // VC Data Integrity crypto (eddsa-rdfc-2022) — bin/npm-entry/
985
+ // entry_jsoo.ml's vc* exports, realising VC_DataIntegrity's four crypto
986
+ // assume vals via HACL*'s OWN official WebAssembly build.
987
+ //
988
+ // INIT (browser): unlike the Node entry (which auto-awaits initHacl()),
989
+ // a browser page MUST initialise the HACL* wasm backend itself before
990
+ // the first VC call — the hacl-wasm URL is page-specific. Serve the
991
+ // `hacl-wasm/` directory next to your page and, from hacl-init.js,
992
+ // `await initHacl({ apiUrl: '/path/to/hacl-wasm/api.js' })` once; that
993
+ // stashes the ready backend on globalThis.__factoidalHacl, which the
994
+ // bundle's synchronous stubs read. If the backend is NOT initialised,
995
+ // the F* assume-val stub throws, `guarded` returns {ok:false}, and
996
+ // these wrappers throw — a verify NEVER silently succeeds
997
+ // uninitialised (#286, the throw-on-uninit contract). See
998
+ // skills/node-crypto-haclstar-vc-wasm-build.
999
+ // ---------------------------------------------------------------------
1000
+
1001
+ /**
1002
+ * SHA-256 of a message string's bytes, as a lowercase hex digest
1003
+ * (VC_DataIntegrity.hash_sha256_hex, HACL* SHA-2).
1004
+ * @param {string} message
1005
+ * @returns {Promise<{ok:true, sha256:string}>}
1006
+ */
1007
+ export async function vcSha256Hex(message) {
1008
+ if (typeof message !== 'string') {
1009
+ throw new TypeError('vcSha256Hex: message must be a string');
1010
+ }
1011
+ const abi = await loadNpmEntry();
1012
+ if (typeof abi.vcSha256Hex !== 'function') {
1013
+ throw new Error('vcSha256Hex: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1014
+ }
1015
+ const parsed = JSON.parse(abi.vcSha256Hex(message));
1016
+ if (!parsed.ok) throw new Error(parsed.error || 'vcSha256Hex failed');
1017
+ return parsed;
1018
+ }
1019
+
1020
+ /**
1021
+ * Derive the Ed25519 public key from a 32-byte secret key (hex).
1022
+ * @param {string} secretKeyHex
1023
+ * @returns {Promise<{ok:true, publicKeyHex:string}>}
1024
+ */
1025
+ export async function vcEd25519SecretToPublic(secretKeyHex) {
1026
+ if (typeof secretKeyHex !== 'string') {
1027
+ throw new TypeError('vcEd25519SecretToPublic: secretKeyHex must be a string');
1028
+ }
1029
+ const abi = await loadNpmEntry();
1030
+ if (typeof abi.vcEd25519SecretToPublic !== 'function') {
1031
+ throw new Error('vcEd25519SecretToPublic: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1032
+ }
1033
+ const parsed = JSON.parse(abi.vcEd25519SecretToPublic(secretKeyHex));
1034
+ if (!parsed.ok) throw new Error(parsed.error || 'vcEd25519SecretToPublic failed');
1035
+ return parsed;
1036
+ }
1037
+
1038
+ /**
1039
+ * Ed25519 signature over a hex-encoded message.
1040
+ * @param {string} secretKeyHex 32-byte secret key, hex
1041
+ * @param {string} messageHex the message to sign, hex
1042
+ * @returns {Promise<{ok:true, signatureHex:string}>}
1043
+ */
1044
+ export async function vcEd25519Sign(secretKeyHex, messageHex) {
1045
+ if (typeof secretKeyHex !== 'string' || typeof messageHex !== 'string') {
1046
+ throw new TypeError('vcEd25519Sign: secretKeyHex and messageHex must be strings');
1047
+ }
1048
+ const abi = await loadNpmEntry();
1049
+ if (typeof abi.vcEd25519Sign !== 'function') {
1050
+ throw new Error('vcEd25519Sign: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1051
+ }
1052
+ const parsed = JSON.parse(abi.vcEd25519Sign(secretKeyHex, messageHex));
1053
+ if (!parsed.ok) throw new Error(parsed.error || 'vcEd25519Sign failed');
1054
+ return parsed;
1055
+ }
1056
+
1057
+ /**
1058
+ * Ed25519 verification. Wrong key, tampered signature, altered
1059
+ * message, or a malformed-length input all report valid:false — never
1060
+ * an exception-hidden true.
1061
+ * @param {string} publicKeyHex
1062
+ * @param {string} messageHex
1063
+ * @param {string} signatureHex
1064
+ * @returns {Promise<{ok:true, valid:boolean}>}
1065
+ */
1066
+ export async function vcEd25519Verify(publicKeyHex, messageHex, signatureHex) {
1067
+ if (typeof publicKeyHex !== 'string' || typeof messageHex !== 'string' ||
1068
+ typeof signatureHex !== 'string') {
1069
+ throw new TypeError('vcEd25519Verify: publicKeyHex, messageHex and signatureHex must be strings');
1070
+ }
1071
+ const abi = await loadNpmEntry();
1072
+ if (typeof abi.vcEd25519Verify !== 'function') {
1073
+ throw new Error('vcEd25519Verify: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1074
+ }
1075
+ const parsed = JSON.parse(abi.vcEd25519Verify(publicKeyHex, messageHex, signatureHex));
1076
+ if (!parsed.ok) throw new Error(parsed.error || 'vcEd25519Verify failed');
1077
+ return parsed;
1078
+ }
1079
+
1080
+ /**
1081
+ * Create an eddsa-rdfc-2022 Data Integrity proofValue over
1082
+ * already-canonicalized inputs (RDFC-1.0 canonical N-Quads).
1083
+ * @param {string} secretKeyHex
1084
+ * @param {string} canonicalDocument canonical N-Quads of the document
1085
+ * @param {string} canonicalConfig canonical N-Quads of the proof config
1086
+ * @returns {Promise<{ok:true, proofValue:string}>} multibase-z (base58btc)
1087
+ */
1088
+ export async function vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig) {
1089
+ if (typeof secretKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1090
+ typeof canonicalConfig !== 'string') {
1091
+ throw new TypeError('vcEddsaCreateFromCanonical: all three arguments must be strings');
1092
+ }
1093
+ const abi = await loadNpmEntry();
1094
+ if (typeof abi.vcEddsaCreateFromCanonical !== 'function') {
1095
+ throw new Error('vcEddsaCreateFromCanonical: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1096
+ }
1097
+ const parsed = JSON.parse(abi.vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig));
1098
+ if (!parsed.ok) throw new Error(parsed.error || 'vcEddsaCreateFromCanonical failed');
1099
+ return parsed;
1100
+ }
1101
+
1102
+ /**
1103
+ * Verify an eddsa-rdfc-2022 proofValue against canonical inputs. Wrong
1104
+ * key, tampered document/config, or tampered proofValue all report
1105
+ * verified:false.
1106
+ * @param {string} publicKeyHex
1107
+ * @param {string} canonicalDocument canonical N-Quads of the document
1108
+ * @param {string} canonicalConfig canonical N-Quads of the proof config
1109
+ * @param {string} proofValue the multibase-z proofValue to check
1110
+ * @returns {Promise<{ok:true, verified:boolean}>}
1111
+ */
1112
+ export async function vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue) {
1113
+ if (typeof publicKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1114
+ typeof canonicalConfig !== 'string' || typeof proofValue !== 'string') {
1115
+ throw new TypeError('vcEddsaVerifyFromCanonical: all four arguments must be strings');
1116
+ }
1117
+ const abi = await loadNpmEntry();
1118
+ if (typeof abi.vcEddsaVerifyFromCanonical !== 'function') {
1119
+ throw new Error('vcEddsaVerifyFromCanonical: the loaded factoidal-npm-entry bundle predates the VC crypto exports');
1120
+ }
1121
+ const parsed = JSON.parse(abi.vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue));
1122
+ if (!parsed.ok) throw new Error(parsed.error || 'vcEddsaVerifyFromCanonical failed');
1123
+ return parsed;
1124
+ }
1125
+
1126
+ /**
1127
+ * Resolve a `did:key:z6Mk...` identifier to its DID Document as RDF
1128
+ * (bin/npm-entry/entry_jsoo.ml's didKeyResolve export -> the verified
1129
+ * DID_Key.did_key_document, formal/fstar/DID.Key.fst). Pure and offline:
1130
+ * a did:key IS a multibase+multicodec Ed25519 public key, so resolution
1131
+ * is total function application -- no network, no registry.
1132
+ *
1133
+ * Scope: Ed25519 (the `z6Mk...` prefix, multicodec 0xed01) only. The
1134
+ * X25519 `keyAgreement` verification method is deferred (curve
1135
+ * conversion, not byte encoding). A non-Ed25519 did:key rejects.
1136
+ *
1137
+ * @param {string} didString e.g. "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
1138
+ * @returns {Promise<{ok:true,did:string,nquads:string}>} `nquads` is the
1139
+ * DID Document as N-Triples (verificationMethod, controller,
1140
+ * publicKeyMultibase, authentication, assertionMethod,
1141
+ * capabilityInvocation, capabilityDelegation).
1142
+ */
1143
+ export async function didKeyResolve(didString) {
1144
+ if (typeof didString !== 'string') {
1145
+ throw new TypeError('didKeyResolve: didString must be a string');
1146
+ }
1147
+ const abi = await loadNpmEntry();
1148
+ if (typeof abi.didKeyResolve !== 'function') {
1149
+ throw new Error(
1150
+ 'didKeyResolve: the loaded factoidal-npm-entry bundle predates the did:key export');
1151
+ }
1152
+ const parsed = JSON.parse(abi.didKeyResolve(didString));
1153
+ if (!parsed.ok) throw new Error(parsed.error || 'didKeyResolve failed');
1154
+ return parsed;
1155
+ }
1156
+
1157
+ /**
1158
+ * RDFS or OWL-RL entailment closure (bin/npm-entry/entry_jsoo.ml's
1159
+ * owlClosure export). Default graph only.
1160
+ *
1161
+ * @param {string} dataNQuads
1162
+ * @param {'RDFS'|'OWL-RL'} mode
1163
+ * @returns {Promise<{ok:true,nquads:string}>}
1164
+ */
1165
+ export async function owlClosure(dataNQuads, mode) {
1166
+ if (typeof dataNQuads !== 'string') {
1167
+ throw new TypeError('owlClosure: dataNQuads must be a string');
1168
+ }
1169
+ const abi = await loadNpmEntry();
1170
+ if (typeof abi.owlClosure !== 'function') {
1171
+ throw new Error(
1172
+ 'owlClosure: the loaded factoidal-npm-entry bundle predates the owlClosure export');
1173
+ }
1174
+ const parsed = JSON.parse(abi.owlClosure(dataNQuads, mode));
1175
+ if (!parsed.ok) throw new Error(parsed.error || 'owlClosure failed');
1176
+ return parsed;
1177
+ }
1178
+
1179
+ /**
1180
+ * Certified core-RDFS closure (bin/npm-entry/entry_jsoo.ml's
1181
+ * rhoDfClosure export -> RDF.Entailment.RDFS.RhoDFClosure.
1182
+ * rho_df_closure, the six-rule operator with the decides-iff; see the
1183
+ * theorem registry). "corerdfs" is this project's API name for the
1184
+ * fragment the literature calls ρdf (subPropertyOf/subClassOf/type/
1185
+ * domain/range — Muñoz, Pérez & Gutierrez, "Simple and Efficient
1186
+ * Minimal RDFS", J. Web Semantics 7(3), 2009); `rhoDfClosure` is the
1187
+ * literature-name alias, kept greppable against the registry.
1188
+ *
1189
+ * @param {string} data Turtle or N-Triples text
1190
+ * @returns {Promise<{ok:true,ntriples:string}>}
1191
+ */
1192
+ export async function coreRdfsClosure(data, options) {
1193
+ if (typeof data !== 'string') {
1194
+ throw new TypeError('coreRdfsClosure: data must be a string');
1195
+ }
1196
+ const opts = options || {};
1197
+ // The ABI parses N-Quads only (entry_jsoo.ml dataset_of_nquads);
1198
+ // Turtle passed raw is silently dropped to an empty graph, so convert
1199
+ // first -- same normalisation the node package's api.js does.
1200
+ const nq = await toRdf(data, { format: opts.format || 'turtle', baseIRI: opts.baseIRI });
1201
+ const abi = await loadNpmEntry();
1202
+ if (typeof abi.rhoDfClosure !== 'function') {
1203
+ throw new Error(
1204
+ 'coreRdfsClosure: the loaded factoidal-npm-entry bundle predates the rhoDfClosure export');
1205
+ }
1206
+ const parsed = JSON.parse(abi.rhoDfClosure(nq));
1207
+ if (!parsed.ok) throw new Error(parsed.error || 'coreRdfsClosure failed');
1208
+ return parsed;
1209
+ }
1210
+
1211
+ /** Literature-name alias for coreRdfsClosure (ρdf; see above). */
1212
+ export async function rhoDfClosure(data, options) {
1213
+ return coreRdfsClosure(data, options);
1214
+ }
1215
+
1216
+ /**
1217
+ * Decidable core-RDFS fragment check (bin/npm-entry/entry_jsoo.ml's
1218
+ * rhoDfFragmentCheck export -> is_rho_df_frag, lemma-tied to the
1219
+ * fragment hypothesis the regime theorems quantify over). Naming: see
1220
+ * coreRdfsClosure above; `rhoDfFragmentCheck` is the literature-name
1221
+ * alias.
1222
+ *
1223
+ * @param {string} data Turtle or N-Triples text
1224
+ * @returns {Promise<{ok:true,fragment:boolean}>}
1225
+ */
1226
+ export async function coreRdfsCheck(data, options) {
1227
+ if (typeof data !== 'string') {
1228
+ throw new TypeError('coreRdfsCheck: data must be a string');
1229
+ }
1230
+ const opts = options || {};
1231
+ // Same N-Quads normalisation as coreRdfsClosure above: raw Turtle
1232
+ // would silently check the EMPTY graph and answer fragment:true
1233
+ // vacuously.
1234
+ const nq = await toRdf(data, { format: opts.format || 'turtle', baseIRI: opts.baseIRI });
1235
+ const abi = await loadNpmEntry();
1236
+ if (typeof abi.rhoDfFragmentCheck !== 'function') {
1237
+ throw new Error(
1238
+ 'coreRdfsCheck: the loaded factoidal-npm-entry bundle predates the rhoDfFragmentCheck export');
1239
+ }
1240
+ const parsed = JSON.parse(abi.rhoDfFragmentCheck(nq));
1241
+ if (!parsed.ok) throw new Error(parsed.error || 'coreRdfsCheck failed');
1242
+ return parsed;
1243
+ }
1244
+
1245
+ /** Literature-name alias for coreRdfsCheck (ρdf; see above). */
1246
+ export async function rhoDfFragmentCheck(data, options) {
1247
+ return coreRdfsCheck(data, options);
1248
+ }
1249
+
1250
+ /**
1251
+ * RDFS-Plus closure (bin/npm-entry/entry_jsoo.ml's rdfsPlusClosure
1252
+ * export -> RDF.Entailment.RDFSPlus.rdfs_plus_closure): the full RDFS
1253
+ * step plus the practical OWL subset (owl:sameAs, owl:inverseOf,
1254
+ * Symmetric/Transitive/Functional/InverseFunctionalProperty,
1255
+ * equivalentClass/Property) -- the tier the literature calls
1256
+ * RDFS-Plus (Allemang & Hendler 2008) or RDFS++ (AllegroGraph).
1257
+ * Every OWL row runs under a proved licensing + truth lemma; no
1258
+ * chain-level completeness is claimed (see the theorem registry).
1259
+ *
1260
+ * @param {string} data Turtle or N-Triples text
1261
+ * @returns {Promise<{ok:true,ntriples:string,rounds:number}>}
1262
+ */
1263
+ export async function rdfsPlusClosure(data, options) {
1264
+ if (typeof data !== 'string') {
1265
+ throw new TypeError('rdfsPlusClosure: data must be a string');
1266
+ }
1267
+ const opts = options || {};
1268
+ // Same N-Quads normalisation as coreRdfsClosure above.
1269
+ const nq = await toRdf(data, { format: opts.format || 'turtle', baseIRI: opts.baseIRI });
1270
+ const abi = await loadNpmEntry();
1271
+ if (typeof abi.rdfsPlusClosure !== 'function') {
1272
+ throw new Error(
1273
+ 'rdfsPlusClosure: the loaded factoidal-npm-entry bundle predates the rdfsPlusClosure export');
1274
+ }
1275
+ const parsed = JSON.parse(abi.rdfsPlusClosure(nq));
1276
+ if (!parsed.ok) throw new Error(parsed.error || 'rdfsPlusClosure failed');
1277
+ return parsed;
1278
+ }
1279
+
1280
+ /**
1281
+ * OWL tableau materialisation (bin/npm-entry/entry_jsoo.ml's
1282
+ * tableauMaterialise export -> formal/fstar/Tableau.fst's
1283
+ * tableau_materialise). Default graph only.
1284
+ *
1285
+ * @param {string} dataNQuads
1286
+ * @returns {Promise<{ok:true,nquads:string,addedCount:number}>}
1287
+ */
1288
+ export async function tableauMaterialise(dataNQuads) {
1289
+ if (typeof dataNQuads !== 'string') {
1290
+ throw new TypeError('tableauMaterialise: dataNQuads must be a string');
1291
+ }
1292
+ const abi = await loadNpmEntry();
1293
+ if (typeof abi.tableauMaterialise !== 'function') {
1294
+ throw new Error(
1295
+ 'tableauMaterialise: the loaded factoidal-npm-entry bundle predates the tableauMaterialise export');
1296
+ }
1297
+ const parsed = JSON.parse(abi.tableauMaterialise(dataNQuads));
1298
+ if (!parsed.ok) throw new Error(parsed.error || 'tableauMaterialise failed');
1299
+ return parsed;
1300
+ }
1301
+
1302
+ /**
1303
+ * OWL DL inconsistency verdict (bin/npm-entry/entry_jsoo.ml's
1304
+ * tableauDlInconsistent export): the RL -> tableau -> RL ->
1305
+ * is_inconsistent DL pipeline, with `rlAlone` the plain OWL-RL verdict
1306
+ * on the same input for comparison. Default graph only.
1307
+ *
1308
+ * @param {string} dataNQuads
1309
+ * @returns {Promise<{ok:true,inconsistent:boolean,rlAlone:boolean}>}
1310
+ */
1311
+ export async function tableauDlInconsistent(dataNQuads) {
1312
+ if (typeof dataNQuads !== 'string') {
1313
+ throw new TypeError('tableauDlInconsistent: dataNQuads must be a string');
1314
+ }
1315
+ const abi = await loadNpmEntry();
1316
+ if (typeof abi.tableauDlInconsistent !== 'function') {
1317
+ throw new Error(
1318
+ 'tableauDlInconsistent: the loaded factoidal-npm-entry bundle predates the tableauDlInconsistent export');
1319
+ }
1320
+ const parsed = JSON.parse(abi.tableauDlInconsistent(dataNQuads));
1321
+ if (!parsed.ok) throw new Error(parsed.error || 'tableauDlInconsistent failed');
1322
+ return parsed;
1323
+ }
1324
+
1325
+ /**
1326
+ * OWL DL consistency verdict via the verified clash-detecting tableau
1327
+ * (bin/npm-entry/entry_jsoo.ml's owlIsConsistent export ->
1328
+ * Tableau.Refute.tableau_consistent over the OWL-RL closure). Default
1329
+ * graph only. `consistent` is true|false|null (null = budget-out, with
1330
+ * `reason` naming the fuel cap -- never a silent false).
1331
+ *
1332
+ * @param {string} dataNQuads
1333
+ * @param {string} [optsJson] '' or a JSON object {"fuel":"<nat>"}
1334
+ * @returns {Promise<{ok:true,consistent:boolean|null,reason?:string}>}
1335
+ */
1336
+ export async function owlIsConsistent(dataNQuads, optsJson) {
1337
+ if (typeof dataNQuads !== 'string') {
1338
+ throw new TypeError('owlIsConsistent: dataNQuads must be a string');
1339
+ }
1340
+ const abi = await loadNpmEntry();
1341
+ if (typeof abi.owlIsConsistent !== 'function') {
1342
+ throw new Error(
1343
+ 'owlIsConsistent: the loaded factoidal-npm-entry bundle predates the owlIsConsistent export');
1344
+ }
1345
+ const parsed = JSON.parse(abi.owlIsConsistent(dataNQuads, optsJson || ''));
1346
+ if (!parsed.ok) throw new Error(parsed.error || 'owlIsConsistent failed');
1347
+ return parsed;
1348
+ }
1349
+
1350
+ /**
1351
+ * OWL entailment check via the verified engine (bin/npm-entry/
1352
+ * entry_jsoo.ml's owlEntails export): the OWL-RL closure path
1353
+ * (`via:"closure"`) then negate-and-refute (`via:"refutation"`).
1354
+ * Default graph only. `entailed` is true|false|null (null = budget-out,
1355
+ * `reason` names the fuel cap).
1356
+ *
1357
+ * @param {string} premiseNQuads
1358
+ * @param {string} conclusionNQuads
1359
+ * @param {string} [optsJson] '' or a JSON object {"fuel":"<nat>"}
1360
+ * @returns {Promise<{ok:true,entailed:boolean|null,via:string,reason?:string}>}
1361
+ */
1362
+ export async function owlEntails(premiseNQuads, conclusionNQuads, optsJson) {
1363
+ if (typeof premiseNQuads !== 'string' || typeof conclusionNQuads !== 'string') {
1364
+ throw new TypeError('owlEntails: premiseNQuads and conclusionNQuads must be strings');
1365
+ }
1366
+ const abi = await loadNpmEntry();
1367
+ if (typeof abi.owlEntails !== 'function') {
1368
+ throw new Error(
1369
+ 'owlEntails: the loaded factoidal-npm-entry bundle predates the owlEntails export');
1370
+ }
1371
+ const parsed = JSON.parse(abi.owlEntails(premiseNQuads, conclusionNQuads, optsJson || ''));
1372
+ if (!parsed.ok) throw new Error(parsed.error || 'owlEntails failed');
1373
+ return parsed;
1374
+ }
1375
+
1376
+ /**
1377
+ * Evaluate an RML mapping graph against one logical source's raw data
1378
+ * (bin/npm-entry/entry_jsoo.ml's rmlMap export). Every triples map in
1379
+ * `mappingNQuads` reads the SAME `sourceData` -- joins across two
1380
+ * different logical sources are out of scope for this entry point.
1381
+ *
1382
+ * @param {string} mappingNQuads dataset-handle N-Quads for the RML mapping graph
1383
+ * @param {string} sourceData raw JSON or CSV text (not RDF)
1384
+ * @param {'json'|'csv'} sourceKind
1385
+ * @returns {Promise<{ok:true,nquads:string}>}
1386
+ */
1387
+ export async function rmlMap(mappingNQuads, sourceData, sourceKind) {
1388
+ if (typeof mappingNQuads !== 'string') {
1389
+ throw new TypeError('rmlMap: mappingNQuads must be a string');
1390
+ }
1391
+ if (typeof sourceData !== 'string') {
1392
+ throw new TypeError('rmlMap: sourceData must be a string');
1393
+ }
1394
+ const abi = await loadNpmEntry();
1395
+ if (typeof abi.rmlMap !== 'function') {
1396
+ throw new Error(
1397
+ 'rmlMap: the loaded factoidal-npm-entry bundle predates the RML export');
1398
+ }
1399
+ const parsed = JSON.parse(abi.rmlMap(mappingNQuads, sourceData, sourceKind));
1400
+ if (!parsed.ok) throw new Error(parsed.error || 'rmlMap failed');
1401
+ return parsed;
1402
+ }
1403
+
1404
+ /**
1405
+ * CSVW csv2rdf conversion (bin/npm-entry/entry_jsoo.ml's csvwToRdf
1406
+ * export): raw tabular data + an optional CSVW metadata document to
1407
+ * N-Quads. Every table in a multi-table `tables` group reads the SAME
1408
+ * `csvText`.
1409
+ *
1410
+ * @param {string} csvText raw RFC 4180 tabular data (not RDF)
1411
+ * @param {string} [metadataJson] CSVW metadata document (JSON text);
1412
+ * '' / omitted infers the schema from the CSV's own header row
1413
+ * @param {{mode?:'standard'|'minimal',base?:string,url?:string}} [options]
1414
+ * @returns {Promise<{ok:true,nquads:string}>}
1415
+ */
1416
+ export async function csvwToRdf(csvText, metadataJson, options) {
1417
+ if (typeof csvText !== 'string') {
1418
+ throw new TypeError('csvwToRdf: csvText must be a string');
1419
+ }
1420
+ const meta = metadataJson == null ? '' : metadataJson;
1421
+ if (typeof meta !== 'string') {
1422
+ throw new TypeError('csvwToRdf: metadataJson must be a string');
1423
+ }
1424
+ const abi = await loadNpmEntry();
1425
+ if (typeof abi.csvwToRdf !== 'function') {
1426
+ throw new Error(
1427
+ 'csvwToRdf: the loaded factoidal-npm-entry bundle predates the CSVW export');
1428
+ }
1429
+ const opts = options || {};
1430
+ const optionsJson = JSON.stringify({
1431
+ ...(opts.mode ? { mode: String(opts.mode).toLowerCase() } : {}),
1432
+ ...(opts.base ? { base: opts.base } : {}),
1433
+ ...(opts.url ? { url: opts.url } : {}),
1434
+ });
1435
+ const parsed = JSON.parse(abi.csvwToRdf(csvText, meta, optionsJson));
1436
+ if (!parsed.ok) throw new Error(parsed.error || 'csvwToRdf failed');
1437
+ return parsed;
1438
+ }
1439
+
1440
+ /**
1441
+ * Parse a JSON-LD document with JSON-LD-specific options
1442
+ * (bin/npm-entry/entry_jsoo.ml's jsonldToRdf export) -- plain
1443
+ * `toRdf(text, {format:'jsonld'})` above also works now for the
1444
+ * common case; this exists for rdfDirection/expandContext/
1445
+ * processingMode, which toRdf()'s options have no room for.
1446
+ *
1447
+ * @param {string} jsonldText
1448
+ * @param {{base?:string,rdfDirection?:string,expandContext?:string,
1449
+ * processingMode?:string}} [options]
1450
+ * @returns {Promise<{ok:true,nquads:string}>}
1451
+ */
1452
+ export async function jsonldToRdf(jsonldText, options) {
1453
+ if (typeof jsonldText !== 'string') {
1454
+ throw new TypeError('jsonldToRdf: jsonldText must be a string');
1455
+ }
1456
+ const abi = await loadNpmEntry();
1457
+ if (typeof abi.jsonldToRdf !== 'function') {
1458
+ throw new Error(
1459
+ 'jsonldToRdf: the loaded factoidal-npm-entry bundle predates the jsonldToRdf export');
1460
+ }
1461
+ const opts = options || {};
1462
+ const optionsJson = JSON.stringify({
1463
+ ...(opts.base ? { base: opts.base } : {}),
1464
+ ...(opts.rdfDirection ? { rdfDirection: opts.rdfDirection } : {}),
1465
+ ...(opts.expandContext ? { expandContext: opts.expandContext } : {}),
1466
+ ...(opts.processingMode ? { processingMode: opts.processingMode } : {}),
1467
+ });
1468
+ const parsed = JSON.parse(abi.jsonldToRdf(jsonldText, optionsJson));
1469
+ if (!parsed.ok) throw new Error(parsed.error || 'jsonldToRdf failed');
1470
+ return parsed;
1471
+ }
1472
+
1473
+ /**
1474
+ * Serialize an RDF dataset (N-Quads text) as an expanded-form JSON-LD
1475
+ * document -- the reverse of jsonldToRdf (bin/npm-entry/entry_jsoo.ml's
1476
+ * jsonldFromRdf export -> the verified JSONLD.FromRdf.from_rdf). The
1477
+ * returned `jsonld` string is JCS-canonical JSON.
1478
+ *
1479
+ * @param {string} nquads N-Quads text
1480
+ * @param {{useNativeTypes?:boolean,useRdfType?:boolean}} [options]
1481
+ * @returns {Promise<{ok:true,jsonld:string}>}
1482
+ */
1483
+ export async function jsonldFromRdf(nquads, options) {
1484
+ if (typeof nquads !== 'string') {
1485
+ throw new TypeError('jsonldFromRdf: nquads must be a string');
1486
+ }
1487
+ const abi = await loadNpmEntry();
1488
+ if (typeof abi.jsonldFromRdf !== 'function') {
1489
+ throw new Error(
1490
+ 'jsonldFromRdf: the loaded factoidal-npm-entry bundle predates the jsonldFromRdf export');
1491
+ }
1492
+ const opts = options || {};
1493
+ const optionsJson = JSON.stringify({
1494
+ ...(opts.useNativeTypes ? { useNativeTypes: true } : {}),
1495
+ ...(opts.useRdfType ? { useRdfType: true } : {}),
1496
+ });
1497
+ const parsed = JSON.parse(abi.jsonldFromRdf(nquads, optionsJson));
1498
+ if (!parsed.ok) throw new Error(parsed.error || 'jsonldFromRdf failed');
1499
+ return parsed;
1500
+ }
1501
+
1502
+ /**
1503
+ * Test whether an XML document is well-formed (bin/npm-entry/
1504
+ * entry_jsoo.ml's xmlWellformed export -> Parser_XML.parse_xml_document,
1505
+ * the accept/reject signal bin/xml-runner drives against W3C xmlconf).
1506
+ * The byte-oriented parser has no DOCTYPE/DTD production, so a document
1507
+ * containing a DOCTYPE reports wellformed:false.
1508
+ *
1509
+ * @param {string} xmlText
1510
+ * @returns {Promise<{ok:true,wellformed:boolean}>}
1511
+ */
1512
+ export async function xmlWellformed(xmlText) {
1513
+ if (typeof xmlText !== 'string') {
1514
+ throw new TypeError('xmlWellformed: xmlText must be a string');
1515
+ }
1516
+ const abi = await loadNpmEntry();
1517
+ if (typeof abi.xmlWellformed !== 'function') {
1518
+ throw new Error(
1519
+ 'xmlWellformed: the loaded factoidal-npm-entry bundle predates the xmlWellformed export');
1520
+ }
1521
+ const parsed = JSON.parse(abi.xmlWellformed(xmlText));
1522
+ if (!parsed.ok) throw new Error(parsed.error || 'xmlWellformed failed');
1523
+ return parsed;
1524
+ }
1525
+
1526
+ /**
1527
+ * Evaluate an XPath 1.0 expression over an XML document (bin/npm-entry/
1528
+ * entry_jsoo.ml's xpathEval export -> XPath_Eval.eval_xpath_from_root,
1529
+ * the Stage-1 engine tests/unit/xpath_tests.ml drives). The result
1530
+ * envelope carries `resultType` ('nodeset'|'string'|'number'|'boolean')
1531
+ * plus the value: for a node-set, `count`, `stringValue`, and a `nodes`
1532
+ * array of {kind,name,value}; otherwise a scalar `value`.
1533
+ *
1534
+ * @param {string} xmlText
1535
+ * @param {string} xpathExpr
1536
+ * @returns {Promise<object>}
1537
+ */
1538
+ export async function xpathEval(xmlText, xpathExpr) {
1539
+ if (typeof xmlText !== 'string' || typeof xpathExpr !== 'string') {
1540
+ throw new TypeError('xpathEval: xmlText and xpathExpr must be strings');
1541
+ }
1542
+ const abi = await loadNpmEntry();
1543
+ if (typeof abi.xpathEval !== 'function') {
1544
+ throw new Error(
1545
+ 'xpathEval: the loaded factoidal-npm-entry bundle predates the xpathEval export');
1546
+ }
1547
+ const parsed = JSON.parse(abi.xpathEval(xmlText, xpathExpr));
1548
+ if (!parsed.ok) throw new Error(parsed.error || 'xpathEval failed');
1549
+ return parsed;
1550
+ }
1551
+
1552
+ /**
1553
+ * Open a COTTAS/Parquet artifact's raw bytes as a queryable, read-only
1554
+ * store (bin/npm-entry/entry_jsoo.ml's openCottas export;
1555
+ * docs/designissues/2026-07-06-inmemory-bytes-store.md, browser call
1556
+ * site). Rows decode lazily as queryCottas() touches them -- the
1557
+ * corpus is never parsed into a heap dataset (that would defeat the
1558
+ * memory win the design doc measures).
1559
+ *
1560
+ * @param {string|Uint8Array|ArrayBuffer} bytes whole `.cottas` file contents
1561
+ * @returns {Promise<string>} opaque handle for queryCottas()/closeCottas()
1562
+ */
1563
+ export async function openCottas(bytes) {
1564
+ let hex;
1565
+ if (typeof bytes === 'string') {
1566
+ if (!/^[0-9a-fA-F]*$/.test(bytes) || bytes.length % 2 !== 0) {
1567
+ throw new TypeError('openCottas: string input must be an even-length hex string');
1568
+ }
1569
+ hex = bytes.toLowerCase();
1570
+ } else {
1571
+ const u8 = bytes instanceof Uint8Array ? bytes
1572
+ : bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : null;
1573
+ if (!u8) {
1574
+ throw new TypeError('openCottas: expected a hex string, Uint8Array, or ArrayBuffer');
1575
+ }
1576
+ // Array-join, not per-byte string concat -- the concat loop
1577
+ // allocates millions of intermediate strings on a corpus-scale
1578
+ // artifact (same fix as lib/api.js's bytesToHex).
1579
+ const HEX = '0123456789abcdef';
1580
+ const parts = new Array(u8.length);
1581
+ for (let i = 0; i < u8.length; i++) {
1582
+ parts[i] = HEX[u8[i] >> 4] + HEX[u8[i] & 15];
1583
+ }
1584
+ hex = parts.join('');
1585
+ }
1586
+ const abi = await loadNpmEntry();
1587
+ if (typeof abi.openCottas !== 'function') {
1588
+ throw new Error('openCottas: the loaded factoidal-npm-entry bundle predates the openCottas export');
1589
+ }
1590
+ const parsed = JSON.parse(abi.openCottas(hex));
1591
+ if (!parsed.ok) throw new Error(parsed.error || 'openCottas failed');
1592
+ return parsed.handle;
1593
+ }
1594
+
1595
+ /**
1596
+ * Run a SPARQL 1.1 query against a store opened by openCottas()
1597
+ * (bin/npm-entry/entry_jsoo.ml's queryCottas export). No `entail`
1598
+ * option and no write overlay (read-only) -- see entry_jsoo.ml's
1599
+ * queryCottas doc comment for the full divergence list from query().
1600
+ *
1601
+ * @param {string} handle from openCottas()
1602
+ * @param {string} sparql
1603
+ * @returns {Promise<{ok:true,kind:'select',srj:object}|{ok:true,kind:'ask',boolean:boolean}|{ok:true,kind:'construct',nquads:string}>}
1604
+ */
1605
+ export async function queryCottas(handle, sparql) {
1606
+ if (typeof handle !== 'string') {
1607
+ throw new TypeError('queryCottas: handle must be the string openCottas() returned');
1608
+ }
1609
+ if (typeof sparql !== 'string') {
1610
+ throw new TypeError('queryCottas: sparql must be a string');
1611
+ }
1612
+ const abi = await loadNpmEntry();
1613
+ if (typeof abi.queryCottas !== 'function') {
1614
+ throw new Error('queryCottas: the loaded factoidal-npm-entry bundle predates the queryCottas export');
1615
+ }
1616
+ const parsed = JSON.parse(abi.queryCottas(handle, encodeTextAsBundleBytes(sparql)));
1617
+ if (!parsed.ok) throw new Error(parsed.error || 'queryCottas failed');
1618
+ return parsed;
1619
+ }
1620
+
1621
+ /**
1622
+ * Release a store opened by openCottas() (bin/npm-entry/entry_jsoo.ml's
1623
+ * closeCottas export). Drops the handle from the entry bundle's own
1624
+ * registry only -- does not evict the underlying byte cache (design
1625
+ * doc "Open decisions" item 1: no eviction API exists yet).
1626
+ * @param {string} handle
1627
+ * @returns {Promise<void>}
1628
+ */
1629
+ export async function closeCottas(handle) {
1630
+ const abi = await loadNpmEntry();
1631
+ if (typeof abi.closeCottas !== 'function') {
1632
+ throw new Error('closeCottas: the loaded factoidal-npm-entry bundle predates the closeCottas export');
1633
+ }
1634
+ const parsed = JSON.parse(abi.closeCottas(handle));
1635
+ if (!parsed.ok) throw new Error(parsed.error || 'closeCottas failed');
1636
+ }
1637
+
1638
+ // ---------------------------------------------------------------------
1639
+ // Typed engine functions (#74 npm FP surface, browser side). Each is a
1640
+ // thin, JSON-in/JSON-out wrapper over one F*-extracted engine exposed
1641
+ // by entry_jsoo.ml through loadNpmEntry() -- the same ABI shape
1642
+ // npm/factoidal/lib/api.js's Node-side wrappers use (duplicated here
1643
+ // deliberately: browser.js is hand-maintained ESM with no access to
1644
+ // lib/api.js's CommonJS/node: requires -- see this file's header
1645
+ // comment). No logic lives on the JS side; the transform/eval/
1646
+ // validate/CAS math is all verified F*. All need the npm-entry bundle.
1647
+ // ---------------------------------------------------------------------
1648
+
1649
+ /**
1650
+ * XSLT 1.0 transform (entry_jsoo.ml's xsltTransform export ->
1651
+ * XSLT.Transform.transform). Applies `stylesheetXml` to `sourceXml`
1652
+ * and returns the serialized result tree.
1653
+ *
1654
+ * @param {string} stylesheetXml
1655
+ * @param {string} sourceXml
1656
+ * @returns {Promise<string>}
1657
+ */
1658
+ export async function xsltTransform(stylesheetXml, sourceXml) {
1659
+ if (typeof stylesheetXml !== 'string' || typeof sourceXml !== 'string') {
1660
+ throw new TypeError('xsltTransform: stylesheetXml and sourceXml must be strings');
1661
+ }
1662
+ const abi = await loadNpmEntry();
1663
+ if (typeof abi.xsltTransform !== 'function') {
1664
+ throw new Error('xsltTransform: the loaded factoidal-npm-entry bundle predates the XSLT export');
1665
+ }
1666
+ const parsed = JSON.parse(abi.xsltTransform(stylesheetXml, sourceXml));
1667
+ if (!parsed.ok) throw new Error(parsed.error || 'xsltTransform failed');
1668
+ return parsed.output;
1669
+ }
1670
+
1671
+ /**
1672
+ * Evaluate a Content MathML document (entry_jsoo.ml's mathmlEval
1673
+ * export -> MathML.Content.eval_doc_env). `bindings` maps ci-variable
1674
+ * names to value strings; pass {} (or omit) for a closed expression.
1675
+ *
1676
+ * @param {string} contentMathmlXml
1677
+ * @param {Record<string,string>} [bindings]
1678
+ * @returns {Promise<{kind:'rat',num:number,den:number}|{kind:'bool',value:boolean}|{kind:'undef',reason:string}>}
1679
+ */
1680
+ export async function mathmlEval(contentMathmlXml, bindings) {
1681
+ if (typeof contentMathmlXml !== 'string') {
1682
+ throw new TypeError('mathmlEval: contentMathmlXml must be a string');
1683
+ }
1684
+ const b = bindings || {};
1685
+ const abi = await loadNpmEntry();
1686
+ if (typeof abi.mathmlEval !== 'function') {
1687
+ throw new Error('mathmlEval: the loaded factoidal-npm-entry bundle predates the mathmlEval export');
1688
+ }
1689
+ const parsed = JSON.parse(abi.mathmlEval(contentMathmlXml, JSON.stringify(b)));
1690
+ if (!parsed.ok) throw new Error(parsed.error || 'mathmlEval failed');
1691
+ return parsed.value;
1692
+ }
1693
+
1694
+ /**
1695
+ * XForms recalculate (entry_jsoo.ml's xformsRecalc export ->
1696
+ * XForms.Bind.recalculate): apply the model binds to `instanceXml`,
1697
+ * returning the recomputed instance and a validity report per bound
1698
+ * node.
1699
+ *
1700
+ * @param {string} instanceXml
1701
+ * @param {Array<{id?:string,target:string,calculate?:string,constraint?:string,relevant?:string,required?:string,readonly?:string,type?:string}>} binds
1702
+ * @returns {Promise<{instance:string,validity:Array<object>}>}
1703
+ */
1704
+ export async function xformsRecalc(instanceXml, binds) {
1705
+ if (typeof instanceXml !== 'string') {
1706
+ throw new TypeError('xformsRecalc: instanceXml must be a string');
1707
+ }
1708
+ if (!Array.isArray(binds)) {
1709
+ throw new TypeError('xformsRecalc: binds must be an array');
1710
+ }
1711
+ const abi = await loadNpmEntry();
1712
+ if (typeof abi.xformsRecalc !== 'function') {
1713
+ throw new Error('xformsRecalc: the loaded factoidal-npm-entry bundle predates the xformsRecalc export');
1714
+ }
1715
+ const parsed = JSON.parse(abi.xformsRecalc(instanceXml, JSON.stringify(binds)));
1716
+ if (!parsed.ok) throw new Error(parsed.error || 'xformsRecalc failed');
1717
+ return { instance: parsed.instance, validity: parsed.validity };
1718
+ }
1719
+
1720
+ /**
1721
+ * JSON Schema (draft-07) validation (entry_jsoo.ml's
1722
+ * jsonSchemaValidate export -> JSONSchema.Validate.validate). The
1723
+ * verified validator gives a definite pass/fail/unsupported verdict
1724
+ * rather than a per-keyword error list.
1725
+ *
1726
+ * @param {string} schemaJson
1727
+ * @param {string} instanceJson
1728
+ * @returns {Promise<{valid:boolean,result:'pass'|'fail'|'unsupported',errors:string[]}>}
1729
+ */
1730
+ export async function jsonSchemaValidate(schemaJson, instanceJson) {
1731
+ if (typeof schemaJson !== 'string' || typeof instanceJson !== 'string') {
1732
+ throw new TypeError('jsonSchemaValidate: schemaJson and instanceJson must be strings');
1733
+ }
1734
+ const abi = await loadNpmEntry();
1735
+ if (typeof abi.jsonSchemaValidate !== 'function') {
1736
+ throw new Error('jsonSchemaValidate: the loaded factoidal-npm-entry bundle predates the jsonSchemaValidate export');
1737
+ }
1738
+ const parsed = JSON.parse(abi.jsonSchemaValidate(schemaJson, instanceJson));
1739
+ if (!parsed.ok) throw new Error(parsed.error || 'jsonSchemaValidate failed');
1740
+ return { valid: !!parsed.valid, result: parsed.result, errors: parsed.errors || [] };
1741
+ }
1742
+
1743
+ /**
1744
+ * Schematron validation (entry_jsoo.ml's schematronValidate export ->
1745
+ * Schematron.Validate.validate): every finding (failed assert, fired
1746
+ * report, indeterminate) in pattern-then-document order.
1747
+ *
1748
+ * @param {string} schematronXml
1749
+ * @param {string} instanceXml
1750
+ * @returns {Promise<{findings:Array<{type:string,context:string,test:string,message:string,path:string,reason?:string}>}>}
1751
+ */
1752
+ export async function schematronValidate(schematronXml, instanceXml) {
1753
+ if (typeof schematronXml !== 'string' || typeof instanceXml !== 'string') {
1754
+ throw new TypeError('schematronValidate: schematronXml and instanceXml must be strings');
1755
+ }
1756
+ const abi = await loadNpmEntry();
1757
+ if (typeof abi.schematronValidate !== 'function') {
1758
+ throw new Error('schematronValidate: the loaded factoidal-npm-entry bundle predates the schematronValidate export');
1759
+ }
1760
+ const parsed = JSON.parse(abi.schematronValidate(schematronXml, instanceXml));
1761
+ if (!parsed.ok) throw new Error(parsed.error || 'schematronValidate failed');
1762
+ return { findings: parsed.findings };
1763
+ }
1764
+
1765
+ // TOAN -- a small exact-CAS surface over Math.Expr (E_Int/E_Rat/E_Bool/
1766
+ // E_Sym/E_App). Callers pass an expression as the JSON codec
1767
+ // {int:n} | {rat:[n,d]} | {bool:b} | {sym:name} | {app:name,args:[...]}
1768
+ // and receive Content MathML for the result (via MathML.Present).
1769
+ async function toanCall(fnName, what, ...args) {
1770
+ const abi = await loadNpmEntry();
1771
+ if (typeof abi[fnName] !== 'function') {
1772
+ throw new Error(`${fnName}: the loaded factoidal-npm-entry bundle predates the ${what} export`);
1773
+ }
1774
+ const parsed = JSON.parse(abi[fnName](...args));
1775
+ if (!parsed.ok) throw new Error(parsed.error || `${fnName} failed`);
1776
+ return parsed.mathml;
1777
+ }
1778
+
1779
+ /** Finite summation of `bodyExpr[idx:=lo..hi]`, simplified, as Content MathML. */
1780
+ export async function toanSummation(bodyExpr, idx, lo, hi) {
1781
+ if (typeof idx !== 'string') throw new TypeError('toanSummation: idx must be a string');
1782
+ return toanCall('toanSummation', 'TOAN summation',
1783
+ JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1784
+ }
1785
+
1786
+ /** Finite product of `bodyExpr[idx:=lo..hi]`, simplified, as Content MathML. */
1787
+ export async function toanProduct(bodyExpr, idx, lo, hi) {
1788
+ if (typeof idx !== 'string') throw new TypeError('toanProduct: idx must be a string');
1789
+ return toanCall('toanProduct', 'TOAN product',
1790
+ JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1791
+ }
1792
+
1793
+ /** Canonical simplification of `expr`, as Content MathML. */
1794
+ export async function toanSimplify(expr) {
1795
+ return toanCall('toanSimplify', 'TOAN simplify', JSON.stringify(expr));
1796
+ }
1797
+
1798
+ /** Derivative of `expr` w.r.t. `variable`, as Content MathML. */
1799
+ export async function toanDiff(expr, variable) {
1800
+ if (typeof variable !== 'string') throw new TypeError('toanDiff: variable must be a string');
1801
+ return toanCall('toanDiff', 'TOAN diff', JSON.stringify(expr), variable);
1802
+ }
1803
+
1804
+ /** `expr[variable := value]` simplified, as Content MathML. */
1805
+ export async function toanSubst(expr, variable, value) {
1806
+ if (typeof variable !== 'string') throw new TypeError('toanSubst: variable must be a string');
1807
+ return toanCall('toanSubst', 'TOAN subst',
1808
+ JSON.stringify(expr), variable, JSON.stringify(value));
1809
+ }
1810
+
1811
+ // Matrix / vector algebra over exact rationals (Math.Matrix). A matrix
1812
+ // is a JSON array of rows; a vector a JSON array of cells; a cell is
1813
+ // an integer or a [num,den] pair. Results render via
1814
+ // Math.Matrix.mres_to_string ("undef" carries a `reason`).
1815
+ async function matrixCall(fnName, what, ...jsonArgs) {
1816
+ const abi = await loadNpmEntry();
1817
+ if (typeof abi[fnName] !== 'function') {
1818
+ throw new Error(`${fnName}: the loaded factoidal-npm-entry bundle predates the ${what} export`);
1819
+ }
1820
+ const parsed = JSON.parse(abi[fnName](...jsonArgs.map((a) => JSON.stringify(a))));
1821
+ if (!parsed.ok) throw new Error(parsed.error || `${fnName} failed`);
1822
+ return { result: parsed.result, reason: parsed.reason || '' };
1823
+ }
1824
+
1825
+ /** Exact determinant of a square matrix (Math.Matrix.dyn_determinant). */
1826
+ export async function matrixDeterminant(matrix) {
1827
+ if (!Array.isArray(matrix)) throw new TypeError('matrixDeterminant: matrix must be an array of rows');
1828
+ return matrixCall('matrixDeterminant', 'matrix determinant', matrix);
1829
+ }
1830
+
1831
+ /** Dot product of two vectors (Math.Matrix.dyn_scalarproduct). */
1832
+ export async function matrixScalarProduct(a, b) {
1833
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixScalarProduct: a and b must be arrays');
1834
+ return matrixCall('matrixScalarProduct', 'vector scalar product', a, b);
1835
+ }
1836
+
1837
+ /** Cross product of two 3-vectors (Math.Matrix.dyn_vectorproduct). */
1838
+ export async function matrixVectorProduct(a, b) {
1839
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixVectorProduct: a and b must be arrays');
1840
+ return matrixCall('matrixVectorProduct', 'vector cross product', a, b);
1841
+ }
1842
+
1843
+ /** Outer product of two vectors (Math.Matrix.dyn_outerproduct). */
1844
+ export async function matrixOuterProduct(a, b) {
1845
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixOuterProduct: a and b must be arrays');
1846
+ return matrixCall('matrixOuterProduct', 'vector outer product', a, b);
1847
+ }
1848
+
1849
+ // A `scaled` value as entry_jsoo.ml's scaledJson envelope decodes it:
1850
+ // {mantissa,scale,decimal} strings straight off the wire.
1851
+ function scaledFromJson(s) {
1852
+ return { mantissa: s.mantissa, scale: s.scale, decimal: s.decimal };
1853
+ }
1854
+
1855
+ /**
1856
+ * n+1 evenly spaced samples of the logistic sigmoid
1857
+ * L / (1 + exp(-k*(x - x0))) over [xmin, xmax] (entry_jsoo.ml's
1858
+ * sigmoidPoints export -> Math.Sigmoid.sigmoid_points). All arithmetic
1859
+ * -- argument reduction, the truncated Taylor series, repeated
1860
+ * squaring, and the x samples themselves -- runs as exact rational
1861
+ * arithmetic inside Math.Sigmoid.fst; see that module's header for the
1862
+ * documented error bound on the returned (rounded) values. This
1863
+ * wrapper only marshals JSON; it never computes exp itself.
1864
+ *
1865
+ * @param {{k:number|string,x0:number|string,l:number|string,
1866
+ * xmin:number|string,xmax:number|string,n:number|string}} params
1867
+ * @returns {Promise<Array<{x:{mantissa:string,scale:string,decimal:string},
1868
+ * y:{mantissa:string,scale:string,decimal:string}}>>}
1869
+ */
1870
+ export async function sigmoidPoints(params) {
1871
+ if (!params || typeof params !== 'object') {
1872
+ throw new TypeError('sigmoidPoints: params must be an object');
1873
+ }
1874
+ const abi = await loadNpmEntry();
1875
+ if (typeof abi.sigmoidPoints !== 'function') {
1876
+ throw new Error('sigmoidPoints: the loaded factoidal-npm-entry bundle predates the sigmoidPoints export');
1877
+ }
1878
+ const wire = {
1879
+ k: String(params.k), x0: String(params.x0), l: String(params.l),
1880
+ xmin: String(params.xmin), xmax: String(params.xmax), n: String(params.n),
1881
+ };
1882
+ const parsed = JSON.parse(abi.sigmoidPoints(JSON.stringify(wire)));
1883
+ if (!parsed.ok) throw new Error(parsed.error || 'sigmoidPoints failed');
1884
+ return parsed.points.map((p) => ({ x: scaledFromJson(p.x), y: scaledFromJson(p.y) }));
1885
+ }
1886
+
1887
+ /**
1888
+ * Presentation MathML for the sigmoid formula L / (1 + exp(-k*(x - x0))),
1889
+ * engine-serialized (entry_jsoo.ml's sigmoidFormulaMathml export ->
1890
+ * MathML.Present.to_presentation_mathml applied to a fixed Math.Expr.
1891
+ * expr) -- never hand-written MathML.
1892
+ *
1893
+ * @returns {Promise<string>} a `<math>...</math>` Presentation MathML document
1894
+ */
1895
+ export async function sigmoidFormulaMathml() {
1896
+ const abi = await loadNpmEntry();
1897
+ if (typeof abi.sigmoidFormulaMathml !== 'function') {
1898
+ throw new Error('sigmoidFormulaMathml: the loaded factoidal-npm-entry bundle predates the sigmoidFormulaMathml export');
1899
+ }
1900
+ const parsed = JSON.parse(abi.sigmoidFormulaMathml());
1901
+ if (!parsed.ok) throw new Error(parsed.error || 'sigmoidFormulaMathml failed');
1902
+ return parsed.mathml;
1903
+ }
1904
+
1905
+ // ---------------------------------------------------------------------
1906
+ // HDT (Header-Dictionary-Triples): query a read-only binary RDF
1907
+ // artifact's raw bytes via the CLI's `--data-hdt` backend
1908
+ // (factoidal_cli.ml, HDT.Triples.fst and the parser modules around
1909
+ // it). No npm-entry ABI bundle needed -- this is a CLI-only
1910
+ // capability, built on runFactoidalCli() exactly the way query() is,
1911
+ // so a hub cell no longer needs to build the --data-hdt argv and fake
1912
+ // filesystem entry by hand (see docs/web/hub/24-hdt-header-dictionary-
1913
+ // triples.md's earlier revision for what this replaces). Default
1914
+ // graph only, SELECT/ASK only (no CONSTRUCT, no named graphs -- HDTQ
1915
+ // is a separate, deferred extension; see factoidal_cli.ml's --data-hdt
1916
+ // help text).
1917
+ // ---------------------------------------------------------------------
1918
+
1919
+ let _hdtSeq = 0;
1920
+
1921
+ /**
1922
+ * Run a SPARQL 1.1 query against an HDT artifact's raw bytes. Same
1923
+ * output shape as query(): parsed SPARQL Results JSON when
1924
+ * `options.output` is 'json' (default), the raw output string
1925
+ * otherwise.
1926
+ *
1927
+ * @param {Uint8Array|ArrayBuffer|string} hdtBytes whole .hdt file
1928
+ * contents -- a Uint8Array/ArrayBuffer is packed automatically into
1929
+ * the fake filesystem's one-char-per-byte convention; a string is
1930
+ * assumed already packed.
1931
+ * @param {string} sparql a SELECT or ASK query
1932
+ * @param {{output?: 'json'|'table'|'csv'|'ntriples'}} [options]
1933
+ * @returns {Promise<object|string>}
1934
+ */
1935
+ export async function queryHdt(hdtBytes, sparql, options) {
1936
+ if (typeof sparql !== 'string') {
1937
+ throw new TypeError('queryHdt: sparql must be a string');
1938
+ }
1939
+ const opts = options || {};
1940
+ const output = opts.output || 'json';
1941
+ if (!OUTPUT_FORMATS.has(output)) {
1942
+ throw new TypeError(`queryHdt: output must be one of ${[...OUTPUT_FORMATS].join(', ')}`);
1943
+ }
1944
+
1945
+ let content;
1946
+ if (typeof hdtBytes === 'string') {
1947
+ content = hdtBytes;
1948
+ } else {
1949
+ const u8 = hdtBytes instanceof Uint8Array ? hdtBytes
1950
+ : hdtBytes instanceof ArrayBuffer ? new Uint8Array(hdtBytes) : null;
1951
+ if (!u8) {
1952
+ throw new TypeError('queryHdt: hdtBytes must be a Uint8Array, ArrayBuffer, or an already-packed string');
1953
+ }
1954
+ let s = '';
1955
+ for (let i = 0; i < u8.length; i += 0x4000) {
1956
+ s += String.fromCharCode.apply(null, u8.subarray(i, Math.min(u8.length, i + 0x4000)));
1957
+ }
1958
+ content = s;
1959
+ }
1960
+
1961
+ const path = '/static/hdt' + (_hdtSeq++) + '.hdt';
1962
+ const { stdout, stderr, exitCode, engineMs } = await runFactoidalCli(
1963
+ ['--data-hdt', path, '-e', encodeTextAsBundleBytes(sparql), '-o', output === 'json' ? 'json' : output],
1964
+ [{ name: path, content }]);
1965
+
1966
+ if (exitCode !== 0) {
1967
+ const msg = (stderr || stdout || `factoidal exited with code ${exitCode}`).trim();
1968
+ const err = new Error('HDT query failed: ' + msg);
1969
+ err.exitCode = exitCode;
1970
+ err.stderr = stderr;
1971
+ err.stdout = stdout;
1972
+ throw err;
1973
+ }
1974
+
1975
+ if (output !== 'json') return stdout;
1976
+
1977
+ const firstBrace = stdout.indexOf('{');
1978
+ const lastBrace = stdout.lastIndexOf('}');
1979
+ if (firstBrace < 0 || lastBrace < firstBrace) {
1980
+ const err = new Error('factoidal did not produce JSON on stdout. Raw output: ' + stdout);
1981
+ err.stdout = stdout;
1982
+ err.stderr = stderr;
1983
+ throw err;
1984
+ }
1985
+ const jsonText = stdout.slice(firstBrace, lastBrace + 1);
1986
+ try {
1987
+ const parsed = JSON.parse(jsonText);
1988
+ Object.defineProperty(parsed, 'engineMs', { value: engineMs, enumerable: false });
1989
+ return parsed;
1990
+ } catch (e) {
1991
+ const err = new Error('factoidal JSON parse failed: ' + e.message + '. Raw output: ' + stdout);
1992
+ err.stdout = stdout;
1993
+ err.stderr = stderr;
1994
+ throw err;
1995
+ }
1996
+ }
1997
+
1998
+ /**
1999
+ * Serialize a dataset-handle N-Quads string to COTTAS/Parquet bytes via
2000
+ * the native writer (bin/npm-entry/entry_jsoo.ml's toCottas export;
2001
+ * RDF.CottasStore.BaseWriter.serialize_cottas_v2, the same pure `Tot`
2002
+ * function `factoidal compact --native-writer` uses). Round-trips into
2003
+ * openCottas() byte-for-byte.
2004
+ *
2005
+ * @param {string} nQuads dataset-handle N-Quads text (see toRdf() to get here from another format)
2006
+ * @returns {Promise<Uint8Array>}
2007
+ */
2008
+ export async function toCottas(nQuads) {
2009
+ if (typeof nQuads !== 'string') {
2010
+ throw new TypeError('toCottas: nQuads must be a string');
2011
+ }
2012
+ const abi = await loadNpmEntry();
2013
+ if (typeof abi.toCottas !== 'function') {
2014
+ throw new Error('toCottas: the loaded factoidal-npm-entry bundle predates the toCottas export');
2015
+ }
2016
+ const parsed = JSON.parse(abi.toCottas(nQuads));
2017
+ if (!parsed.ok) throw new Error(parsed.error || 'toCottas failed');
2018
+ const hex = parsed.cottasHex;
2019
+ const out = new Uint8Array(hex.length / 2);
2020
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
2021
+ return out;
2022
+ }
2023
+
2024
+ // ---------------------------------------------------------------------
2025
+ // Durable-UPDATE browser persistence (issue #282's browser realisation
2026
+ // -- see docs/designissues/2026-07-06-browser-persistence.md for the
2027
+ // full design: v1 architecture decision (IndexedDB, not OPFS -- OPFS
2028
+ // sync access handles are worker-only and there is no worker-RPC
2029
+ // layer for the engine yet), the tab-close/crash guarantee mapping,
2030
+ // and the quota/eviction honesty section).
2031
+ //
2032
+ // Every byte written here is exactly what bin/npm-entry/entry_jsoo.ml's
2033
+ // deltaBatchToHex/deltaMergeApplyBrowser exports produce/consume via
2034
+ // the F*-extracted, VERIFIED RDF_Store_Columnar_DeltaLog /
2035
+ // RDF_Store_Columnar_DeltaMerge modules -- this file moves opaque
2036
+ // hex-encoded bytes into/out of IndexedDB only (rule #11: no RDF/
2037
+ // SPARQL semantics here). It does NOT call the native delta_log_append/
2038
+ // _read_all assume-val realisation (that one is wired to Unix syscalls
2039
+ // which, under js_of_ocaml, hit the in-memory jsoo pseudo-FS -- reset
2040
+ // on every bundle eval, NOT persistent across a reload); this is a
2041
+ // wholly separate path.
2042
+ // ---------------------------------------------------------------------
2043
+
2044
+ const DELTA_STORE = 'deltaBatches';
2045
+ const DEFAULT_DELTA_DB_NAME = 'factoidal-delta-log';
2046
+
2047
+ function idbOpen(dbName) {
2048
+ return new Promise((resolve, reject) => {
2049
+ const req = indexedDB.open(dbName, 1);
2050
+ req.onupgradeneeded = () => {
2051
+ const db = req.result;
2052
+ if (!db.objectStoreNames.contains(DELTA_STORE)) {
2053
+ db.createObjectStore(DELTA_STORE, { keyPath: 'seq' });
2054
+ }
2055
+ };
2056
+ req.onsuccess = () => resolve(req.result);
2057
+ req.onerror = () => reject(req.error || new Error('indexedDB.open failed'));
2058
+ req.onblocked = () => reject(new Error('indexedDB.open blocked (another tab holds an open connection at an older version)'));
2059
+ });
2060
+ }
2061
+
2062
+ function reqToPromise(req) {
2063
+ return new Promise((resolve, reject) => {
2064
+ req.onsuccess = () => resolve(req.result);
2065
+ req.onerror = () => reject(req.error || new Error('IndexedDB request failed'));
2066
+ });
2067
+ }
2068
+
2069
+ /**
2070
+ * Open (creating if needed) a browser-persistent delta log backed by
2071
+ * IndexedDB. Returns a handle to pass to the other deltaLog* functions
2072
+ * below. Data written through this handle survives page reloads and
2073
+ * browser restarts -- subject to the browser's own storage-eviction
2074
+ * policy under storage pressure (see the design doc's quota/eviction
2075
+ * section); this function does not itself call
2076
+ * `navigator.storage.persist()` (a named, not-yet-wired gap -- call it
2077
+ * yourself first if you need the "exempt from eviction" request made).
2078
+ *
2079
+ * @param {string} [dbName='factoidal-delta-log']
2080
+ * @returns {Promise<{dbName: string}>}
2081
+ */
2082
+ export async function deltaLogOpen(dbName) {
2083
+ const name = dbName || DEFAULT_DELTA_DB_NAME;
2084
+ const db = await idbOpen(name);
2085
+ db.close();
2086
+ return { dbName: name };
2087
+ }
2088
+
2089
+ /**
2090
+ * Translate one SPARQL Update (INSERT DATA / DELETE DATA / CLEAR /
2091
+ * DROP / CREATE -- the same subset the native --rw commit path
2092
+ * accepts; anything else rejects rather than silently no-op'ing) into
2093
+ * a delta_batch, serialize it (F*-verified,
2094
+ * RDF_Store_Columnar_DeltaLog.serialize_delta_batch), and durably
2095
+ * append it as one IndexedDB record. The commit point is the
2096
+ * transaction's own 'complete' event.
2097
+ *
2098
+ * @param {{dbName: string}} handle from deltaLogOpen()
2099
+ * @param {string} sparqlUpdate
2100
+ * @param {{epoch?: number}} [options]
2101
+ * @returns {Promise<{seq: number, opCount: number}>}
2102
+ */
2103
+ export async function deltaLogAppend(handle, sparqlUpdate, options) {
2104
+ if (!handle || typeof handle.dbName !== 'string') {
2105
+ throw new TypeError('deltaLogAppend: handle must be the object deltaLogOpen() returned');
2106
+ }
2107
+ if (typeof sparqlUpdate !== 'string') {
2108
+ throw new TypeError('deltaLogAppend: sparqlUpdate must be a string');
2109
+ }
2110
+ const opts = options || {};
2111
+ const epoch = opts.epoch || 0;
2112
+ const abi = await loadNpmEntry();
2113
+ if (typeof abi.deltaBatchToHex !== 'function') {
2114
+ throw new Error('deltaLogAppend: the loaded factoidal-npm-entry bundle predates the delta-log export');
2115
+ }
2116
+
2117
+ const db = await idbOpen(handle.dbName);
2118
+ try {
2119
+ const seq = await reqToPromise(db.transaction(DELTA_STORE, 'readonly').objectStore(DELTA_STORE).count());
2120
+
2121
+ const parsed = JSON.parse(abi.deltaBatchToHex(sparqlUpdate, String(seq), String(epoch)));
2122
+ if (!parsed.ok) throw new Error(parsed.error || 'deltaBatchToHex failed');
2123
+
2124
+ await new Promise((resolve, reject) => {
2125
+ // `durability: 'strict'` matters here, not just as a knob: Chrome
2126
+ // changed ITS OWN DEFAULT from 'strict' to 'relaxed' from Chrome
2127
+ // 121 onward (matching Firefox/Safari's prior behavior) for
2128
+ // throughput -- under 'relaxed', `oncomplete` can fire once
2129
+ // changes reach the OS write buffer, before an actual disk flush
2130
+ // (the buffer is "typically flushed every couple seconds", per
2131
+ // Chrome's own devs blog). That is a materially weaker commit
2132
+ // point than the native design's `fsync`-gated "committed means
2133
+ // durable" promise (durable-update-design.md §3.3 step 3) -- so
2134
+ // this call requests 'strict' explicitly rather than silently
2135
+ // inheriting a browser's relaxed default, which would make the
2136
+ // design doc's own honesty claim (§1.3: "the durability strength
2137
+ // is whatever the browser's IndexedDB implementation guarantees")
2138
+ // wrong in the weaker direction without anyone choosing that.
2139
+ const tx = db.transaction(DELTA_STORE, 'readwrite', { durability: 'strict' });
2140
+ tx.objectStore(DELTA_STORE).put({ seq, epoch, hex: parsed.hex });
2141
+ tx.oncomplete = () => resolve();
2142
+ tx.onerror = () => reject(tx.error || new Error('IndexedDB write failed'));
2143
+ tx.onabort = () => reject(tx.error || new Error('IndexedDB write aborted'));
2144
+ });
2145
+
2146
+ return { seq, opCount: parsed.opCount };
2147
+ } finally {
2148
+ db.close();
2149
+ }
2150
+ }
2151
+
2152
+ /**
2153
+ * Read every committed batch back from IndexedDB, in seq order, as a
2154
+ * newline-joined hex-blob string (the wire format deltaLogMerge()
2155
+ * consumes). Exposed mainly for debugging and torn-write test setup;
2156
+ * deltaLogMerge() below is the normal read path.
2157
+ *
2158
+ * @param {{dbName: string}} handle
2159
+ * @returns {Promise<string>}
2160
+ */
2161
+ export async function deltaLogReadAllHex(handle) {
2162
+ const db = await idbOpen(handle.dbName);
2163
+ try {
2164
+ const all = await reqToPromise(db.transaction(DELTA_STORE, 'readonly').objectStore(DELTA_STORE).getAll());
2165
+ all.sort((a, b) => a.seq - b.seq);
2166
+ return all.map((r) => r.hex).join('\n');
2167
+ } finally {
2168
+ db.close();
2169
+ }
2170
+ }
2171
+
2172
+ /**
2173
+ * Read back the durable log and merge it onto a base dataset (parse +
2174
+ * merge-on-read, RDF_Store_Columnar_DeltaMerge.apply_entries_ref via
2175
+ * the deltaMergeApplyBrowser ABI export) -- the "reload the page, read
2176
+ * the log back, reproduce the updated dataset" proof. A batch record
2177
+ * that fails to parse (a torn/corrupt write) is silently skipped,
2178
+ * never partially applied -- see the design doc's torn-write section.
2179
+ *
2180
+ * @param {{dbName: string}} handle
2181
+ * @param {string} baseNQuads dataset-handle N-Quads text (the pre-update graph)
2182
+ * @returns {Promise<string>} merged N-Quads text
2183
+ */
2184
+ export async function deltaLogMerge(handle, baseNQuads) {
2185
+ if (!handle || typeof handle.dbName !== 'string') {
2186
+ throw new TypeError('deltaLogMerge: handle must be the object deltaLogOpen() returned');
2187
+ }
2188
+ if (typeof baseNQuads !== 'string') {
2189
+ throw new TypeError('deltaLogMerge: baseNQuads must be a string');
2190
+ }
2191
+ const abi = await loadNpmEntry();
2192
+ if (typeof abi.deltaMergeApplyBrowser !== 'function') {
2193
+ throw new Error('deltaLogMerge: the loaded factoidal-npm-entry bundle predates the delta-log export');
2194
+ }
2195
+ const hexBlobs = await deltaLogReadAllHex(handle);
2196
+ const parsed = JSON.parse(abi.deltaMergeApplyBrowser(baseNQuads, hexBlobs));
2197
+ if (!parsed.ok) throw new Error(parsed.error || 'deltaMergeApplyBrowser failed');
2198
+ return parsed.nquads;
2199
+ }
2200
+
2201
+ /**
2202
+ * Delete a browser-persistent delta log entirely (test/demo cleanup;
2203
+ * not part of the durability story -- this is a deliberate wipe, not
2204
+ * an eviction).
2205
+ *
2206
+ * @param {{dbName: string}} handle
2207
+ * @returns {Promise<void>}
2208
+ */
2209
+ export async function deltaLogDestroy(handle) {
2210
+ if (!handle || typeof handle.dbName !== 'string') {
2211
+ throw new TypeError('deltaLogDestroy: handle must be the object deltaLogOpen() returned');
2212
+ }
2213
+ await new Promise((resolve, reject) => {
2214
+ const req = indexedDB.deleteDatabase(handle.dbName);
2215
+ req.onsuccess = () => resolve();
2216
+ req.onerror = () => reject(req.error || new Error('indexedDB.deleteDatabase failed'));
2217
+ req.onblocked = () => reject(new Error('indexedDB.deleteDatabase blocked (another open connection)'));
2218
+ });
2219
+ }
2220
+
2221
+ /**
2222
+ * TEST-ONLY: corrupt the most recently written batch record by
2223
+ * truncating its hex string, simulating a torn/partial write. Ordinary
2224
+ * IndexedDB transactions are atomic (see the design doc's §2 table --
2225
+ * this failure mode has no natural browser-native trigger the way a
2226
+ * killed `write()` syscall does natively); this pokes the store
2227
+ * directly to exercise the delta-log parser's checksum/length framing
2228
+ * the same way the native crash-harness pattern does for the on-disk
2229
+ * log. Returns false if the store is empty.
2230
+ *
2231
+ * @param {{dbName: string}} handle
2232
+ * @returns {Promise<boolean>}
2233
+ */
2234
+ export async function _deltaLogCorruptLastForTest(handle) {
2235
+ const db = await idbOpen(handle.dbName);
2236
+ try {
2237
+ const all = await reqToPromise(db.transaction(DELTA_STORE, 'readonly').objectStore(DELTA_STORE).getAll());
2238
+ if (all.length === 0) return false;
2239
+ all.sort((a, b) => a.seq - b.seq);
2240
+ const last = all[all.length - 1];
2241
+ const truncated = last.hex.slice(0, Math.max(0, last.hex.length - 8));
2242
+ await new Promise((resolve, reject) => {
2243
+ const tx = db.transaction(DELTA_STORE, 'readwrite');
2244
+ tx.objectStore(DELTA_STORE).put({ seq: last.seq, epoch: last.epoch, hex: truncated });
2245
+ tx.oncomplete = () => resolve();
2246
+ tx.onerror = () => reject(tx.error || new Error('IndexedDB corrupt-for-test write failed'));
2247
+ });
2248
+ return true;
2249
+ } finally {
2250
+ db.close();
2251
+ }
2252
+ }
2253
+
2254
+ // Best-effort version export for the browser. Consumers that care
2255
+ // about the exact version should import from the package root (which
2256
+ // reads package.json).
2257
+ export const version = '0.1.0';
2258
+
2259
+ export default {
2260
+ query, toRdf, canonicalize, runFactoidalCli, setFactoidalUrl, getFactoidalUrl,
2261
+ encodeTextAsBundleBytes, queryDataset, version,
2262
+ loadNpmEntry, setFactoidalNpmEntryUrl, rifSmoke, rifEval,
2263
+ shaclValidate, shexValidate, didKeyResolve, owlClosure,
2264
+ coreRdfsClosure, coreRdfsCheck, rdfsPlusClosure, rhoDfClosure, rhoDfFragmentCheck,
2265
+ tableauMaterialise, tableauDlInconsistent, owlIsConsistent, owlEntails, rmlMap, jsonldToRdf,
2266
+ jsonldFromRdf, xmlWellformed, xpathEval,
2267
+ deltaLogOpen, deltaLogAppend, deltaLogReadAllHex, deltaLogMerge,
2268
+ deltaLogDestroy, _deltaLogCorruptLastForTest,
2269
+ openCottas, queryCottas, closeCottas, toCottas,
2270
+ xsltTransform, mathmlEval, xformsRecalc, jsonSchemaValidate, schematronValidate,
2271
+ toanSummation, toanProduct, toanSimplify, toanDiff, toanSubst,
2272
+ matrixDeterminant, matrixScalarProduct, matrixVectorProduct, matrixOuterProduct,
2273
+ vcSha256Hex, vcEd25519SecretToPublic, vcEd25519Sign, vcEd25519Verify,
2274
+ vcEddsaCreateFromCanonical, vcEddsaVerifyFromCanonical,
2275
+ queryHdt,
2276
+ };