@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/lib/api.js ADDED
@@ -0,0 +1,2140 @@
1
+ // factoidal — public API layer, shared by index.js (js_of_ocaml) and
2
+ // wasm.js (wasm_of_ocaml).
3
+ //
4
+ // Two ways to reach the engine, tried in order:
5
+ //
6
+ // 1. The npm-entry ABI bundle (factoidal-npm-entry.js /
7
+ // factoidal-npm-entry.wasm.js — built from bin/npm-entry/
8
+ // entry_jsoo.ml). A persistent, function-call ABI: strings in,
9
+ // JSON out. Supports everything including CONSTRUCT, UPDATE and
10
+ // RDFC-1.0 canonicalization.
11
+ //
12
+ // 2. The single-shot CLI bundle (factoidal.js / factoidal.wasm.js),
13
+ // driven argv-style via lib/engine-js.js / lib/engine-wasm.js.
14
+ // Covers parse / SELECT / ASK / serialize today; CONSTRUCT and
15
+ // UPDATE are not reachable through the CLI surface, and
16
+ // canonicalize requires a bundle built after the `factoidal
17
+ // canonicalize` subcommand landed.
18
+ //
19
+ // Operations that need the npm-entry bundle throw an Error whose
20
+ // message contains "pending npm-entry build" when only the CLI bundle
21
+ // is available. All semantic work happens inside the F*-extracted
22
+ // engine either way; this layer only shapes arguments and results.
23
+
24
+ 'use strict';
25
+
26
+ const {
27
+ Dataset,
28
+ dataFactory,
29
+ termFromSrj,
30
+ } = require('../rdfjs.js');
31
+
32
+ const DATA_FORMAT_EXT = {
33
+ turtle: 'ttl',
34
+ ttl: 'ttl',
35
+ ntriples: 'nt',
36
+ nt: 'nt',
37
+ nquads: 'nq',
38
+ nq: 'nq',
39
+ trig: 'trig',
40
+ rdfxml: 'rdf',
41
+ 'rdf-xml': 'rdf',
42
+ rdf: 'rdf',
43
+ jsonld: 'jsonld',
44
+ 'json-ld': 'jsonld',
45
+ // RDF 1.2 opt-in: these select the engine's Mode_12 parsers (triple
46
+ // terms <<( s p o )>>, ~ reifiers, {| |} annotations, VERSION,
47
+ // directional literals "x"@lang--dir). Only reachable via the
48
+ // npm-entry bundle (the entry ABI routes the *12 tag to
49
+ // Parser_*.*_mode Mode_12); the plain names above stay Mode_11 so 1.1
50
+ // output is byte-identical.
51
+ turtle12: 'ttl12',
52
+ ttl12: 'ttl12',
53
+ ntriples12: 'nt12',
54
+ nt12: 'nt12',
55
+ nquads12: 'nq12',
56
+ nq12: 'nq12',
57
+ trig12: 'trig12',
58
+ };
59
+
60
+ // Canonical format tag for the npm-entry ABI. The plain tags are what
61
+ // RDF_Format.format_of_string accepts directly; the *12 tags are
62
+ // intercepted by entry_jsoo's parse_text_to_dataset (before
63
+ // format_of_string) to select Mode_12.
64
+ const DATA_FORMAT_TAG = {
65
+ ttl: 'turtle', nt: 'ntriples', nq: 'nquads', trig: 'trig', rdf: 'rdfxml',
66
+ jsonld: 'jsonld',
67
+ ttl12: 'turtle12', nt12: 'ntriples12', nq12: 'nquads12', trig12: 'trig12',
68
+ };
69
+
70
+ const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
71
+
72
+ function extForFormat(fmt) {
73
+ const key = String(fmt || 'turtle').toLowerCase();
74
+ if (!(key in DATA_FORMAT_EXT)) {
75
+ throw new TypeError(
76
+ `Unknown format '${fmt}'. Expected one of: ` +
77
+ Object.keys(DATA_FORMAT_EXT).join(', ')
78
+ );
79
+ }
80
+ return DATA_FORMAT_EXT[key];
81
+ }
82
+
83
+ function engineError(prefix, res) {
84
+ const msg = (res.stderr || res.stdout ||
85
+ `factoidal exited with code ${res.exitCode}`).trim();
86
+ const err = new Error(`${prefix}: ${msg}`);
87
+ err.exitCode = res.exitCode;
88
+ err.stderr = res.stderr;
89
+ err.stdout = res.stdout;
90
+ return err;
91
+ }
92
+
93
+ function pendingError(what) {
94
+ return new Error(
95
+ `${what} needs the factoidal-npm-entry bundle, which is not present ` +
96
+ '— pending npm-entry build (formal/fstar/build-ocaml.sh js + npm ' +
97
+ 'with bin/npm-entry/entry_jsoo.ml wired in; see bin/npm-entry/README.md).'
98
+ );
99
+ }
100
+
101
+ // Extract the JSON object from CLI stdout defensively (a stray line
102
+ // must not break the parse).
103
+ function jsonFromStdout(stdout, what) {
104
+ const first = stdout.indexOf('{');
105
+ const last = stdout.lastIndexOf('}');
106
+ if (first < 0 || last < first) {
107
+ const err = new Error(`${what}: engine did not produce JSON: ${stdout}`);
108
+ err.stdout = stdout;
109
+ throw err;
110
+ }
111
+ return JSON.parse(stdout.slice(first, last + 1));
112
+ }
113
+
114
+ // Detect the query form for result-shape dispatch. IRIs and comments
115
+ // are blanked first so '#' inside a PREFIX IRI cannot hide the verb.
116
+ // The engine's SPARQL parser remains the authority — this only picks
117
+ // the output shape.
118
+ function sniffQueryForm(sparql) {
119
+ const cleaned = String(sparql)
120
+ .replace(/<[^>]*>/g, ' ')
121
+ .replace(/#[^\n]*/g, ' ');
122
+ const m = cleaned.match(/\b(select|ask|construct|describe)\b/i);
123
+ return m ? m[1].toLowerCase() : 'select';
124
+ }
125
+
126
+ // Pack raw bytes into the one-char-per-byte string js_of_ocaml's fake
127
+ // filesystem (runCli's `files` argument) expects for BINARY content --
128
+ // the same convention browser.js's HDT/COTTAS example cells built by
129
+ // hand before queryHdt() existed (String.fromCharCode per byte).
130
+ // Distinct from bytesToHex below (the npm-entry ABI's string-only wire
131
+ // format for openCottas/toCottas): this is for the CLI's file-based
132
+ // backends (--data-hdt), which read from the CLI bundle's fake
133
+ // filesystem, not the ABI.
134
+ function bytesToLatin1(bytesLike, who) {
135
+ if (typeof bytesLike === 'string') return bytesLike; // already packed
136
+ let u8;
137
+ if (bytesLike instanceof Uint8Array) u8 = bytesLike;
138
+ else if (bytesLike instanceof ArrayBuffer) u8 = new Uint8Array(bytesLike);
139
+ else {
140
+ throw new TypeError(
141
+ `${who}: expected a Uint8Array, Buffer, ArrayBuffer, or an already-packed string`);
142
+ }
143
+ let out = '';
144
+ for (let i = 0; i < u8.length; i += 0x4000) {
145
+ out += String.fromCharCode.apply(null, u8.subarray(i, Math.min(u8.length, i + 0x4000)));
146
+ }
147
+ return out;
148
+ }
149
+
150
+ // SRJ bindings -> Array<Map<string, Term>>
151
+ function bindingsFromSrj(srj) {
152
+ const rows = (srj && srj.results && srj.results.bindings) || [];
153
+ return rows.map((row) => {
154
+ const map = new Map();
155
+ for (const [name, term] of Object.entries(row)) {
156
+ map.set(name, termFromSrj(term));
157
+ }
158
+ return map;
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Build the public API around a driver.
164
+ *
165
+ * @param {object} driver
166
+ * @param {(args: string[], files: Array<{name,content}>) =>
167
+ * ({stdout,stderr,exitCode}|Promise)} driver.runCli
168
+ * @param {() => (object|null|Promise<object|null>)} driver.loadEntry
169
+ * Returns the npm-entry ABI object (factoidalNpmEntry) or null.
170
+ * @param {string} driver.engineName 'js' | 'wasm' (error messages).
171
+ */
172
+ function buildApi(driver) {
173
+ let parseCounter = 0;
174
+ let entryCache; // undefined = not tried; null = unavailable
175
+
176
+ async function entry() {
177
+ if (entryCache === undefined) {
178
+ try {
179
+ entryCache = (await driver.loadEntry()) || null;
180
+ } catch (_) {
181
+ entryCache = null;
182
+ }
183
+ }
184
+ return entryCache;
185
+ }
186
+
187
+ async function run(args, files) {
188
+ return driver.runCli(args, files);
189
+ }
190
+
191
+ function entryResult(jsonText, what) {
192
+ const r = JSON.parse(jsonText);
193
+ if (!r.ok) throw new Error(`${what}: ${r.error}`);
194
+ return r;
195
+ }
196
+
197
+ function freshBnodePrefix() {
198
+ return `p${parseCounter++}_`;
199
+ }
200
+
201
+ // ---------------------------------------------------------------
202
+ // SPARQL 1.1 §17.6 extension functions — issue #463.
203
+ // https://github.com/danbri/factoidal/issues/463
204
+ //
205
+ // Comunica-style: the caller registers (possibly async) JS functions
206
+ // keyed by absolute IRI; a query using an unregistered IRI gets the
207
+ // spec-required error (in expression position: the row's value
208
+ // errors — unbound in SELECT/BIND, row dropped in FILTER).
209
+ //
210
+ // The F*-extracted evaluator is synchronous, so async functions are
211
+ // bridged with a bounded re-evaluation trampoline: each engine call
212
+ // reaches extBridge synchronously; a cache miss on an async function
213
+ // records the promise, returns a pending marker (an engine-side
214
+ // error in THAT pass), and after the pass every pending promise is
215
+ // awaited into the cache and the query re-runs. The per-(iri, args)
216
+ // memoisation is also what honors the F* purity assumption on the
217
+ // extension_function_call hook — within one evaluation every call
218
+ // with the same arguments sees one stable answer.
219
+ //
220
+ // The user function receives an array of SRJ-style term objects
221
+ // ({type:'uri'|'literal'|'bnode', value, datatype?, 'xml:lang'?};
222
+ // {type:'error'} for an errored argument) and returns a term object,
223
+ // a JS primitive (boolean / number / string), a Promise of either,
224
+ // or null/undefined (= error). Thrown errors and rejections map to
225
+ // the §17.6 error too.
226
+ // ---------------------------------------------------------------
227
+ const EXT_PENDING_MARKER = '__FACTOIDAL_EXT_PENDING__';
228
+ const EXT_MAX_ROUNDS = 25;
229
+ const extFunctions = new Map(); // iri -> user fn
230
+ const extInstalled = new Set(); // iris registered into the ABI
231
+ let extCache = new Map(); // key -> normalized result (or null)
232
+ let extPending = []; // [{key, promise}] for this pass
233
+
234
+ function extBridge(iriJs, argsJsonJs) {
235
+ // Called SYNCHRONOUSLY from inside the engine.
236
+ const iri = String(iriJs);
237
+ const argsJson = String(argsJsonJs);
238
+ const key = iri + '' + argsJson;
239
+ if (extCache.has(key)) return extCache.get(key);
240
+ const fn = extFunctions.get(iri);
241
+ if (!fn) return null;
242
+ let out;
243
+ try {
244
+ out = fn(JSON.parse(argsJson));
245
+ } catch (_e) {
246
+ extCache.set(key, null);
247
+ return null;
248
+ }
249
+ if (out && typeof out.then === 'function') {
250
+ extPending.push({ key, promise: out });
251
+ return EXT_PENDING_MARKER;
252
+ }
253
+ out = out === undefined ? null : out;
254
+ extCache.set(key, out);
255
+ return out;
256
+ }
257
+
258
+ async function extEnsureInstalled(what) {
259
+ const e = await entry();
260
+ if (!e) return null;
261
+ if (typeof e.registerExtensionFunction !== 'function') {
262
+ throw new Error(
263
+ `${what}: this npm-entry bundle predates extension functions ` +
264
+ '(issue #463) — rebuild build-ocaml.sh js + npm.');
265
+ }
266
+ for (const iri of extFunctions.keys()) {
267
+ if (!extInstalled.has(iri)) {
268
+ entryResult(e.registerExtensionFunction(iri, extBridge),
269
+ 'registerExtensionFunction');
270
+ extInstalled.add(iri);
271
+ }
272
+ }
273
+ return e;
274
+ }
275
+
276
+ /**
277
+ * Register a custom SPARQL extension function (SPARQL 1.1 §17.6).
278
+ * @param {string} iri absolute IRI the function is invoked by
279
+ * @param {(args: object[]) => any} fn sync or async; see the block
280
+ * comment above for the argument/return contract
281
+ */
282
+ async function registerExtensionFunction(iri, fn) {
283
+ if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
284
+ throw new TypeError(
285
+ 'registerExtensionFunction: iri must be an absolute IRI string');
286
+ }
287
+ if (typeof fn !== 'function') {
288
+ throw new TypeError('registerExtensionFunction: fn must be a function');
289
+ }
290
+ extFunctions.set(iri, fn);
291
+ await extEnsureInstalled('registerExtensionFunction');
292
+ }
293
+
294
+ /** Remove one registered extension function. */
295
+ async function unregisterExtensionFunction(iri) {
296
+ extFunctions.delete(iri);
297
+ const e = await entry();
298
+ if (e && typeof e.unregisterExtensionFunction === 'function'
299
+ && extInstalled.has(iri)) {
300
+ entryResult(e.unregisterExtensionFunction(iri),
301
+ 'unregisterExtensionFunction');
302
+ extInstalled.delete(iri);
303
+ }
304
+ }
305
+
306
+ /** Remove every registered extension function. */
307
+ async function clearExtensionFunctions() {
308
+ extFunctions.clear();
309
+ const e = await entry();
310
+ if (e && typeof e.clearExtensionFunctions === 'function') {
311
+ entryResult(e.clearExtensionFunctions(), 'clearExtensionFunctions');
312
+ }
313
+ extInstalled.clear();
314
+ }
315
+
316
+ /**
317
+ * Bind a SPARQL SERVICE endpoint IRI to a local graph snapshot, so
318
+ * SERVICE <iri> { ... } (and LATERAL { SERVICE ... }) queries
319
+ * resolve against it in-process — the same registry the W3C
320
+ * federated-query suite uses (qt:serviceData). `data` is a Dataset,
321
+ * a raw RDF string (options.format, default turtle), or an array of
322
+ * those. The snapshot is the payload's default graph.
323
+ */
324
+ async function registerServiceEndpoint(iri, data, options) {
325
+ if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
326
+ throw new TypeError(
327
+ 'registerServiceEndpoint: iri must be an absolute IRI string');
328
+ }
329
+ const e = await entry();
330
+ if (!e || typeof e.registerServiceEndpoint !== 'function') {
331
+ throw new Error(
332
+ 'registerServiceEndpoint: this npm-entry bundle predates SERVICE ' +
333
+ 'endpoint registration — rebuild build-ocaml.sh js + npm.');
334
+ }
335
+ const docs = toDocs(data, options);
336
+ const nq = docsToEntryNQuads(e, docs, 'registerServiceEndpoint');
337
+ return entryResult(e.registerServiceEndpoint(iri, nq),
338
+ 'registerServiceEndpoint');
339
+ }
340
+
341
+ /** Remove every registered SERVICE endpoint snapshot. */
342
+ async function clearServiceEndpoints() {
343
+ const e = await entry();
344
+ if (e && typeof e.clearServiceEndpoints === 'function') {
345
+ entryResult(e.clearServiceEndpoints(), 'clearServiceEndpoints');
346
+ }
347
+ }
348
+
349
+ // Run one synchronous engine pass, re-running until no NEW async
350
+ // extension results are pending. With no registered functions this
351
+ // is exactly one pass with zero overhead beyond the length check.
352
+ async function withExtensionRounds(runOnce) {
353
+ extCache = new Map(); // per-evaluation memo (purity contract)
354
+ for (let round = 0; ; round++) {
355
+ extPending = [];
356
+ const result = runOnce();
357
+ if (extPending.length === 0) return result;
358
+ if (round >= EXT_MAX_ROUNDS) {
359
+ throw new Error(
360
+ 'extension functions: async resolution did not converge ' +
361
+ `within ${EXT_MAX_ROUNDS} evaluation rounds`);
362
+ }
363
+ const pend = extPending;
364
+ extPending = [];
365
+ await Promise.all(pend.map(async ({ key, promise }) => {
366
+ try {
367
+ const v = await promise;
368
+ extCache.set(key, v === undefined ? null : v);
369
+ } catch (_e) {
370
+ extCache.set(key, null);
371
+ }
372
+ }));
373
+ }
374
+ }
375
+
376
+ // Normalize the `data` argument of query/update/serialize/
377
+ // canonicalize into engine inputs. Accepts a Dataset, a raw string
378
+ // (with options.format, default turtle), or an array of those —
379
+ // each element is loaded as its own document so blank-node labels
380
+ // stay document-scoped (the per-document renaming is F*'s
381
+ // RDF.Dataset.Merge.rename_dataset_bnodes, applied at engine load).
382
+ function toDocs(data, options) {
383
+ const opts = options || {};
384
+ const items = Array.isArray(data) ? data : [data];
385
+ return items.map((item, i) => {
386
+ if (item instanceof Dataset) {
387
+ return { ext: 'nq', content: item.toNQuads() };
388
+ }
389
+ if (typeof item === 'string') {
390
+ return { ext: extForFormat(opts.format), content: item };
391
+ }
392
+ if (item && typeof item.text === 'string') {
393
+ return { ext: extForFormat(item.format || opts.format), content: item.text };
394
+ }
395
+ throw new TypeError(
396
+ `data[${i}]: expected a Dataset, a string, or {text, format}`);
397
+ });
398
+ }
399
+
400
+ function docsToCliFiles(docs) {
401
+ const files = [];
402
+ const flags = [];
403
+ docs.forEach((d, i) => {
404
+ const name = `/static/data${i}.${d.ext}`;
405
+ files.push({ name, content: d.content });
406
+ flags.push('-d', name);
407
+ });
408
+ return { files, flags };
409
+ }
410
+
411
+ // Normalize toDocs() output into one concatenated N-Quads "dataset
412
+ // handle" string via the entry ABI -- the same per-document
413
+ // parseToDatasetJson-then-concatenate shape query()/update()/
414
+ // canonicalize() each already inline. Needs the entry bundle (the
415
+ // caller must have already checked `e` is non-null); factored out
416
+ // here for the SHACL/ShEx/OWL-closure/RML wrappers below, which all
417
+ // hand the engine a dataset-handle N-Quads string rather than raw
418
+ // Turtle (consistent with every other entry ABI call).
419
+ function docsToEntryNQuads(e, docs, what) {
420
+ let nq = '';
421
+ for (const d of docs) {
422
+ if (d.ext === 'nq') { nq += d.content; continue; }
423
+ const r = entryResult(
424
+ e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
425
+ `${what}(parse)`);
426
+ nq += r.nquads;
427
+ }
428
+ return nq;
429
+ }
430
+
431
+ // A ShEx focus/shape-label argument is either a raw string (an IRI,
432
+ // or "_:label" for a blank node -- the entry ABI's own convention)
433
+ // or an RDF/JS term (NamedNode/BlankNode); accepting both lets a
434
+ // caller pass a term straight out of a query() binding.
435
+ function shexTermToString(t, who) {
436
+ if (typeof t === 'string') return t;
437
+ if (t && typeof t.termType === 'string') {
438
+ if (t.termType === 'BlankNode') return `_:${t.value}`;
439
+ if (t.termType === 'NamedNode') return t.value;
440
+ throw new TypeError(`${who}: expected an IRI or blank node term`);
441
+ }
442
+ throw new TypeError(`${who}: expected a string or an RDF/JS term`);
443
+ }
444
+
445
+ // Everything below is async so both drivers (sync jsoo, async wasm)
446
+ // and both paths (entry, CLI) share one shape.
447
+
448
+ /**
449
+ * Parse one RDF document into a Dataset.
450
+ * @param {string} text
451
+ * @param {{format?: string, baseIRI?: string}} [options]
452
+ * @returns {Promise<Dataset>}
453
+ */
454
+ async function parse(text, options) {
455
+ if (typeof text !== 'string') {
456
+ throw new TypeError('parse: text must be a string');
457
+ }
458
+ const opts = options || {};
459
+ const ext = extForFormat(opts.format);
460
+ const baseIRI = opts.baseIRI || '';
461
+ const bnodePrefix = freshBnodePrefix();
462
+
463
+ const e = await entry();
464
+ if (e) {
465
+ const r = entryResult(
466
+ e.parseToDatasetJson(text, DATA_FORMAT_TAG[ext], baseIRI), 'parse');
467
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: bnodePrefix });
468
+ }
469
+
470
+ const name = `/static/data.${ext}`;
471
+ const args = ['--dump-nq', '-d', name];
472
+ if (baseIRI) args.push('-b', baseIRI);
473
+ const res = await run(args, [{ name, content: text }]);
474
+ if (res.exitCode !== 0) throw engineError('parse failed', res);
475
+ return Dataset.fromNQuads(res.stdout, { blankNodePrefix: bnodePrefix });
476
+ }
477
+
478
+ /**
479
+ * Run a SPARQL 1.1 query.
480
+ * @param {Dataset|string|Array} data
481
+ * @param {string} sparql
482
+ * @param {{format?: string, entail?: 'none'|'RDFS'|'OWL-RL'|'x-rdfscore'|'x-rdfsplus'}} [options]
483
+ * @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
484
+ * SELECT -> Bindings[] (Map of variable name -> RDF/JS Term),
485
+ * ASK -> boolean, CONSTRUCT -> Dataset.
486
+ */
487
+ async function query(data, sparql, options) {
488
+ if (typeof sparql !== 'string') {
489
+ throw new TypeError('query: sparql must be a string');
490
+ }
491
+ const opts = options || {};
492
+ const entail = opts.entail || 'none';
493
+ if (!ENTAIL_VALUES.has(entail)) {
494
+ throw new TypeError(
495
+ `query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
496
+ }
497
+ const form = sniffQueryForm(sparql);
498
+ const docs = toDocs(data, opts);
499
+
500
+ // The npm-entry ABI covers all query forms but has no entailment
501
+ // parameter; entailment closure stays on the CLI path.
502
+ const e = entail === 'none' ? await entry() : null;
503
+ if (e) {
504
+ // The ABI's dataset handle is N-Quads: normalize non-N-Quads
505
+ // documents through parseToDatasetJson first (each document
506
+ // separately, preserving per-document blank-node scoping in F*).
507
+ let nq = '';
508
+ for (const d of docs) {
509
+ if (d.ext === 'nq') { nq += d.content; continue; }
510
+ const r = entryResult(
511
+ e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
512
+ 'query(parse)');
513
+ nq += r.nquads;
514
+ }
515
+ // SPARQL 1.2 opt-in: {sparql12:true} or {version:'1.2'} routes to
516
+ // the entry's queryDataset12 (tokenize_12 parser: triple-term
517
+ // patterns, TRIPLE/isTRIPLE/SUBJECT/PREDICATE/OBJECT, VERSION,
518
+ // lang-dir builtins). Default stays SPARQL 1.1, byte-identical.
519
+ const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
520
+ if (sparql12 && typeof e.queryDataset12 !== 'function') {
521
+ throw new Error(
522
+ 'SPARQL 1.2 requested but this npm-entry bundle predates ' +
523
+ 'queryDataset12 — rebuild build-ocaml.sh js + npm.');
524
+ }
525
+ const r = await withExtensionRounds(() => entryResult(
526
+ (sparql12 ? e.queryDataset12 : e.queryDataset)(nq, sparql), 'query'));
527
+ if (r.kind === 'ask') return r.boolean;
528
+ if (r.kind === 'construct') {
529
+ return Dataset.fromNQuads(r.nquads, {
530
+ blankNodePrefix: freshBnodePrefix(),
531
+ });
532
+ }
533
+ return bindingsFromSrj(r.srj);
534
+ }
535
+
536
+ if (form === 'construct' || form === 'describe') {
537
+ throw pendingError(`${form.toUpperCase()} queries`);
538
+ }
539
+
540
+ const { files, flags } = docsToCliFiles(docs);
541
+ const args = [...flags, '-e', sparql, '-o', 'json'];
542
+ if (entail !== 'none') args.push('--entail', entail);
543
+ const res = await run(args, files);
544
+ if (res.exitCode !== 0) throw engineError('query failed', res);
545
+ const srj = jsonFromStdout(res.stdout, 'query');
546
+ if (form === 'ask' || typeof srj.boolean === 'boolean') {
547
+ return !!srj.boolean;
548
+ }
549
+ return bindingsFromSrj(srj);
550
+ }
551
+
552
+ /**
553
+ * Run a SPARQL 1.1 query against a read-only HDT (Header-Dictionary-
554
+ * Triples) artifact's raw bytes -- factoidal_cli.ml's `--data-hdt`
555
+ * backend (HDT.Triples.fst and the parser modules around it), driven
556
+ * through the same CLI bundle every other function in this file
557
+ * uses. No npm-entry bundle needed -- this is a CLI-only capability.
558
+ * Default graph only, SELECT/ASK only (no CONSTRUCT, no named graphs
559
+ * -- see factoidal_cli.ml's --data-hdt help text).
560
+ * @param {Uint8Array|ArrayBuffer|Buffer|string} hdtBytes whole .hdt
561
+ * file contents (a string is assumed already packed one-char-per-
562
+ * byte, the fake-filesystem convention runCli's `files` expects)
563
+ * @param {string} sparql a SELECT or ASK query
564
+ * @returns {Promise<Array<Map<string, object>>|boolean>}
565
+ */
566
+ async function queryHdt(hdtBytes, sparql) {
567
+ if (typeof sparql !== 'string') {
568
+ throw new TypeError('queryHdt: sparql must be a string');
569
+ }
570
+ const form = sniffQueryForm(sparql);
571
+ if (form === 'construct' || form === 'describe') {
572
+ throw new TypeError(
573
+ `queryHdt: ${form.toUpperCase()} is not supported over --data-hdt (SELECT/ASK only)`);
574
+ }
575
+ const content = bytesToLatin1(hdtBytes, 'queryHdt');
576
+ const name = `/static/hdt${parseCounter++}.hdt`;
577
+ const res = await run(['--data-hdt', name, '-e', sparql, '-o', 'json'], [{ name, content }]);
578
+ if (res.exitCode !== 0) throw engineError('queryHdt failed', res);
579
+ const srj = jsonFromStdout(res.stdout, 'queryHdt');
580
+ if (form === 'ask' || typeof srj.boolean === 'boolean') {
581
+ return !!srj.boolean;
582
+ }
583
+ return bindingsFromSrj(srj);
584
+ }
585
+
586
+ /**
587
+ * Apply a SPARQL 1.1 Update to a dataset, returning the new Dataset.
588
+ * Needs the npm-entry bundle.
589
+ * @param {Dataset|string|Array} data
590
+ * @param {string} updateText
591
+ * @param {{format?: string}} [options]
592
+ * @returns {Promise<Dataset>}
593
+ */
594
+ async function update(data, updateText, options) {
595
+ if (typeof updateText !== 'string') {
596
+ throw new TypeError('update: updateText must be a string');
597
+ }
598
+ const e = await entry();
599
+ if (!e) throw pendingError('SPARQL UPDATE');
600
+ const docs = toDocs(data, options);
601
+ let nq = '';
602
+ for (const d of docs) {
603
+ if (d.ext === 'nq') { nq += d.content; continue; }
604
+ const r = entryResult(
605
+ e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
606
+ 'update(parse)');
607
+ nq += r.nquads;
608
+ }
609
+ const opts = options || {};
610
+ const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
611
+ if (sparql12 && typeof e.updateDataset12 !== 'function') {
612
+ throw new Error(
613
+ 'SPARQL 1.2 UPDATE requested but this npm-entry bundle predates ' +
614
+ 'updateDataset12 — rebuild build-ocaml.sh js + npm.');
615
+ }
616
+ const r = entryResult(
617
+ (sparql12 ? e.updateDataset12 : e.updateDataset)(nq, updateText), 'update');
618
+ return Dataset.fromNQuads(r.nquads, {
619
+ blankNodePrefix: freshBnodePrefix(),
620
+ });
621
+ }
622
+
623
+ /**
624
+ * Serialize a dataset (engine-produced bytes, sorted N-Quads order).
625
+ * @param {Dataset|string|Array} data
626
+ * @param {{format?: 'nquads'|'ntriples'|'turtle', inputFormat?: string}} [options]
627
+ * @returns {Promise<string>}
628
+ * 'turtle' (prefix-compacted, subject-grouped — entry_jsoo.ml's
629
+ * serializeTurtle -> RDF_Turtle_Serialize.turtle_of_graph_auto)
630
+ * needs the npm-entry bundle and flattens every named graph into
631
+ * the default graph (Turtle has no named-graph notion); use
632
+ * 'nquads' when graph names must survive.
633
+ */
634
+ async function serialize(data, options) {
635
+ const opts = options || {};
636
+ const rawOut = String(opts.format || 'nquads').toLowerCase();
637
+ const outFormat = rawOut === 'ttl' ? 'turtle' : rawOut;
638
+ if (outFormat !== 'nquads' && outFormat !== 'ntriples' && outFormat !== 'turtle') {
639
+ throw new TypeError(
640
+ "serialize: format must be 'nquads', 'ntriples', or 'turtle'");
641
+ }
642
+ const docs = toDocs(data, { format: opts.inputFormat });
643
+
644
+ if (outFormat === 'turtle') {
645
+ const e = await entry();
646
+ if (!e) throw pendingError('Turtle serialization');
647
+ requireEntryFn(e, 'serializeTurtle', 'Turtle serialization');
648
+ const nq = docsToEntryNQuads(e, docs, 'serialize(turtle)');
649
+ return entryResult(e.serializeTurtle(nq), 'serialize').turtle;
650
+ }
651
+
652
+ if (outFormat === 'nquads') {
653
+ const e = await entry();
654
+ if (e && docs.every((d) => d.ext === 'nq')) {
655
+ const nq = docs.map((d) => d.content).join('');
656
+ return entryResult(e.serializeNQuads(nq), 'serialize').nquads;
657
+ }
658
+ }
659
+
660
+ const { files, flags } = docsToCliFiles(docs);
661
+ const mode = outFormat === 'nquads' ? '--dump-nq' : '--dump';
662
+ const res = await run([mode, ...flags], files);
663
+ if (res.exitCode !== 0) throw engineError('serialize failed', res);
664
+ return res.stdout;
665
+ }
666
+
667
+ /**
668
+ * RDFC-1.0 canonicalization: canonical blank-node labels + sorted
669
+ * canonical N-Quads.
670
+ * @param {Dataset|string|Array} data
671
+ * @param {{format?: string}} [options]
672
+ * @returns {Promise<string>}
673
+ */
674
+ async function canonicalize(data, options) {
675
+ const docs = toDocs(data, options);
676
+
677
+ const e = await entry();
678
+ if (e) {
679
+ let nq = '';
680
+ for (const d of docs) {
681
+ if (d.ext === 'nq') { nq += d.content; continue; }
682
+ const r = entryResult(
683
+ e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
684
+ 'canonicalize(parse)');
685
+ nq += r.nquads;
686
+ }
687
+ return entryResult(e.canonicalizeToNQuads(nq), 'canonicalize').nquads;
688
+ }
689
+
690
+ const { files, flags } = docsToCliFiles(docs);
691
+ const res = await run(['--canonicalize', ...flags], files);
692
+ if (res.exitCode !== 0) {
693
+ if (/unknown option/.test(res.stderr || '')) {
694
+ throw new Error(
695
+ 'canonicalize: this engine bundle predates the --canonicalize ' +
696
+ 'flag — pending npm-entry build (rebuild via ' +
697
+ 'formal/fstar/build-ocaml.sh js + npm).');
698
+ }
699
+ throw engineError('canonicalize failed', res);
700
+ }
701
+ return res.stdout;
702
+ }
703
+
704
+ /**
705
+ * Enumerate the named graphs of an already-parsed Dataset.
706
+ *
707
+ * Graphs-api design (docs/designissues/2026-07-05-graphs-api-design.md
708
+ * section 1.3): DatasetCore.match(null,null,null,graphNode) already
709
+ * gives per-graph read access, but match() alone cannot answer "what
710
+ * graph names exist" -- this is exactly that enumeration, and only
711
+ * that: it walks quads already produced by the F*-verified parser, so
712
+ * it needs no engine round-trip (no new RDF/SPARQL semantics, rule
713
+ * #11 stays satisfied trivially).
714
+ *
715
+ * @param {Dataset} dataset
716
+ * @returns {Array<[iri: string, graph: Dataset]>}
717
+ * default graph excluded, first-seen order.
718
+ */
719
+ function graphs(dataset) {
720
+ if (!(dataset instanceof Dataset)) {
721
+ throw new TypeError('graphs: expected a Dataset');
722
+ }
723
+ const seen = new Map(); // iri -> graph term (first occurrence)
724
+ for (const q of dataset) {
725
+ const g = q.graph;
726
+ if (!g || g.termType === 'DefaultGraph') continue;
727
+ if (!seen.has(g.value)) seen.set(g.value, g);
728
+ }
729
+ const out = [];
730
+ for (const [iri, gTerm] of seen) {
731
+ out.push([iri, dataset.match(null, null, null, gTerm)]);
732
+ }
733
+ return out;
734
+ }
735
+
736
+ /**
737
+ * RDFC-1.0 canonical hash of a single graph -- the graph-scoped
738
+ * sibling of canonicalize(), gated via capabilities() the same way
739
+ * (docs/designissues/2026-07-05-graphs-api-design.md section 1.3).
740
+ * Accepts a whole dataset or (more usually) one entry of graphs()'s
741
+ * output; either way, every quad's graph component is dropped before
742
+ * canonicalizing, matching RDF.Canonical.fst's
743
+ * canonicalize_named_graph, which projects the named graph into
744
+ * `{ ds_default = g; ds_named = [] }` before reusing
745
+ * canonicalize_to_nquads unmodified.
746
+ *
747
+ * @param {Dataset} datasetOrGraph
748
+ * @returns {Promise<string>} canonical N-Quads text for that graph alone.
749
+ */
750
+ async function canonicalHash(datasetOrGraph) {
751
+ if (!(datasetOrGraph instanceof Dataset)) {
752
+ throw new TypeError(
753
+ 'canonicalHash: expected a Dataset (e.g. one entry of graphs())');
754
+ }
755
+ const asDefaultGraph = new Dataset(
756
+ datasetOrGraph.toArray().map(
757
+ (q) => dataFactory.quad(q.subject, q.predicate, q.object)));
758
+ return canonicalize(asDefaultGraph.toNQuads(), { format: 'nquads' });
759
+ }
760
+
761
+ // Some loaded entry bundles predate one of these exports (e.g. an
762
+ // older/stale wasm-target build) -- `e` itself is truthy (loadEntry
763
+ // succeeded), but `e[fnName]` is undefined, which would otherwise
764
+ // surface as a confusing "e.shaclValidate is not a function". Fail
765
+ // with the same pendingError() shape callers already expect from a
766
+ // missing bundle.
767
+ function requireEntryFn(e, fnName, what) {
768
+ if (typeof e[fnName] !== 'function') throw pendingError(what);
769
+ }
770
+
771
+ /**
772
+ * SHACL Core validation. Needs the npm-entry bundle (SHACL_Validation
773
+ * is only linked into the entry/CLI bundles, not exposed on the
774
+ * argv-driven CLI surface today).
775
+ * @param {Dataset|string|Array} data
776
+ * @param {Dataset|string|Array} shapes
777
+ * @param {{format?: string}} [options] applies to both data and shapes
778
+ * @returns {Promise<{conforms: boolean, report: Dataset}>}
779
+ * report is SHACL_Validation.validation_report_to_graph's graph
780
+ * (sh:conforms + one sh:ValidationResult per violation).
781
+ */
782
+ async function shaclValidate(data, shapes, options) {
783
+ const e = await entry();
784
+ if (!e) throw pendingError('SHACL validation');
785
+ requireEntryFn(e, 'shaclValidate', 'SHACL validation');
786
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'shaclValidate(data)');
787
+ const shapesNq = docsToEntryNQuads(e, toDocs(shapes, options), 'shaclValidate(shapes)');
788
+ const r = entryResult(e.shaclValidate(dataNq, shapesNq), 'shaclValidate');
789
+ return {
790
+ conforms: r.conforms,
791
+ report: Dataset.fromNQuads(r.reportNquads, {
792
+ blankNodePrefix: freshBnodePrefix(),
793
+ }),
794
+ };
795
+ }
796
+
797
+ /**
798
+ * ShEx (Shape Expressions) validation of one focus node against one
799
+ * shape. Needs the npm-entry bundle.
800
+ * @param {Dataset|string|Array} data
801
+ * @param {string} schemaJson the schema, as text -- either ShExJ (a
802
+ * JSON Schema document) or ShExC (the compact human-readable
803
+ * syntax; formal/fstar/Parser.ShExC.fst). Dispatch rule: the schema
804
+ * text's first non-whitespace character decides the format -- '{'
805
+ * means ShExJ, anything else is parsed as ShExC. No separate flag
806
+ * or file-extension hint is needed; a schema whose ShExC text
807
+ * happens to start with whitespace then '{' would misdetect, but no
808
+ * valid ShExC document starts that way (ShExC always opens with a
809
+ * directive keyword, a shape label, or START).
810
+ * @param {string|{termType,value}} focus an IRI, "_:label", or an
811
+ * RDF/JS NamedNode/BlankNode term
812
+ * @param {string|{termType,value}|null} [shape] a shape label (same
813
+ * shapes as focus); omit/null to validate against the schema's own `start`
814
+ * @param {{format?: string}} [options]
815
+ * @returns {Promise<boolean|null>} null means "deferred" -- outside
816
+ * this engine's decidable ShEx fragment, never a guessed answer
817
+ * (see formal/fstar/ShEx.Validation.fst's file header).
818
+ */
819
+ async function shexValidate(data, schemaJson, focus, shape, options) {
820
+ const e = await entry();
821
+ if (!e) throw pendingError('ShEx validation');
822
+ requireEntryFn(e, 'shexValidate', 'ShEx validation');
823
+ if (typeof schemaJson !== 'string') {
824
+ throw new TypeError('shexValidate: schemaJson must be a string');
825
+ }
826
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'shexValidate(data)');
827
+ const focusStr = shexTermToString(focus, 'shexValidate(focus)');
828
+ const shapeStr = shape == null ? '' : shexTermToString(shape, 'shexValidate(shape)');
829
+ const r = entryResult(
830
+ e.shexValidate(dataNq, schemaJson, focusStr, shapeStr), 'shexValidate');
831
+ return r.verdict;
832
+ }
833
+
834
+ const OWL_CLOSURE_MODES = { rdfs: 'RDFS', 'owl-rl': 'OWL-RL', owlrl: 'OWL-RL', owl_rl: 'OWL-RL' };
835
+
836
+ function normalizeClosureMode(mode) {
837
+ const canonical = OWL_CLOSURE_MODES[String(mode || '').toLowerCase()];
838
+ if (!canonical) {
839
+ throw new TypeError(
840
+ `owlClosure: mode must be 'RDFS' or 'OWL-RL' (got '${mode}')`);
841
+ }
842
+ return canonical;
843
+ }
844
+
845
+ /**
846
+ * RDFS or OWL-RL entailment closure, materialized as a new Dataset
847
+ * (input triples + derived triples). Needs the npm-entry bundle.
848
+ * Scope cut: only the default graph is closed over (same cut
849
+ * fn.js's entail() documents for its own CLI-path implementation).
850
+ * @param {Dataset|string|Array} data
851
+ * @param {'RDFS'|'OWL-RL'} mode
852
+ * @param {{format?: string}} [options]
853
+ * @returns {Promise<Dataset>}
854
+ */
855
+ async function owlClosure(data, mode, options) {
856
+ const e = await entry();
857
+ if (!e) throw pendingError('OWL/RDFS closure');
858
+ requireEntryFn(e, 'owlClosure', 'OWL/RDFS closure');
859
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'owlClosure(data)');
860
+ const r = entryResult(
861
+ e.owlClosure(dataNq, normalizeClosureMode(mode)), 'owlClosure');
862
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
863
+ }
864
+
865
+ /**
866
+ * The CERTIFIED core-RDFS closure (RDF.Entailment.RDFS.
867
+ * RhoDFClosure.fst's `rho_df_closure`): rdfs2/3/5/7/9/11 only, with
868
+ * the machine-checked decides-iff (docs/theorem-registry.md).
869
+ * "corerdfs" is this project's API name for the fragment the
870
+ * literature calls ρdf — subPropertyOf/subClassOf/type/domain/range,
871
+ * per Muñoz, Pérez & Gutierrez, "Simple and Efficient Minimal
872
+ * RDFS", J. Web Semantics 7(3), 2009. `rhoDfClosure` remains as an
873
+ * alias so code can be grepped against the theorem registry.
874
+ * Returns the raw certified result, not a Dataset: the N-Triples
875
+ * text is the object the theorems talk about.
876
+ * @param {Dataset|string|Array} data
877
+ * @param {{format?: string}} [options]
878
+ * @returns {Promise<{ok: boolean, ntriples: string}>}
879
+ */
880
+ async function coreRdfsClosure(data, options) {
881
+ const e = await entry();
882
+ if (!e) throw pendingError('certified core-RDFS closure');
883
+ requireEntryFn(e, 'rhoDfClosure', 'certified core-RDFS closure');
884
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'coreRdfsClosure(data)');
885
+ return entryResult(e.rhoDfClosure(dataNq), 'coreRdfsClosure');
886
+ }
887
+ /** Literature-name alias for coreRdfsClosure (ρdf; see above). */
888
+ const rhoDfClosure = coreRdfsClosure;
889
+
890
+ /**
891
+ * Decidable core-RDFS fragment check (`is_rho_df_frag`, tied by an
892
+ * F* lemma to the prop the regime theorems quantify over): does the
893
+ * certified path's guarantee apply to this data? Naming: see
894
+ * coreRdfsClosure above. `rhoDfFragmentCheck` remains as an alias.
895
+ * @param {Dataset|string|Array} data
896
+ * @param {{format?: string}} [options]
897
+ * @returns {Promise<{ok: boolean, fragment: boolean}>}
898
+ */
899
+ async function coreRdfsCheck(data, options) {
900
+ const e = await entry();
901
+ if (!e) throw pendingError('core-RDFS fragment check');
902
+ requireEntryFn(e, 'rhoDfFragmentCheck', 'core-RDFS fragment check');
903
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'coreRdfsCheck(data)');
904
+ return entryResult(e.rhoDfFragmentCheck(dataNq), 'coreRdfsCheck');
905
+ }
906
+ /** Literature-name alias for coreRdfsCheck (ρdf; see above). */
907
+ const rhoDfFragmentCheck = coreRdfsCheck;
908
+
909
+ /**
910
+ * RDFS-Plus closure (RDF.Entailment.RDFSPlus.fst's
911
+ * `rdfs_plus_closure`): the full RDFS step plus the practical OWL
912
+ * subset -- owl:sameAs (symmetry/transitivity/substitution),
913
+ * owl:inverseOf, Symmetric/Transitive/Functional/
914
+ * InverseFunctionalProperty, equivalentClass/Property. The tier the
915
+ * literature calls RDFS-Plus (Allemang & Hendler, "Semantic Web for
916
+ * the Working Ontologist", 2008) or RDFS++ (AllegroGraph). Claim
917
+ * level, weaker than coreRdfsClosure's and stated exactly: every OWL
918
+ * row runs under a PROVED licensing + truth lemma (per-rule
919
+ * certificates in the theorem registry); no chain-level completeness
920
+ * is claimed -- owl:sameAs equality breaks the Herbrand argument the
921
+ * corerdfs completeness theorem uses.
922
+ * @param {Dataset|string|Array} data
923
+ * @param {{format?: string}} [options]
924
+ * @returns {Promise<{ok: boolean, ntriples: string, rounds: number}>}
925
+ */
926
+ async function rdfsPlusClosure(data, options) {
927
+ const e = await entry();
928
+ if (!e) throw pendingError('RDFS-Plus closure');
929
+ requireEntryFn(e, 'rdfsPlusClosure', 'RDFS-Plus closure');
930
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'rdfsPlusClosure(data)');
931
+ return entryResult(e.rdfsPlusClosure(dataNq), 'rdfsPlusClosure');
932
+ }
933
+
934
+ /**
935
+ * OWL tableau materialisation (formal/fstar/Tableau.fst's
936
+ * `tableau_materialise`): add `i rdf:type <ClassExpression>` for
937
+ * every individual the model-construction reasoner can prove is a
938
+ * member of an OWL class expression (someValuesFrom / hasValue /
939
+ * unionOf / intersectionOf, and the named class an equivalentClass
940
+ * restriction defines). This is the same F* function the SPARQL 1.1
941
+ * entailment-regime suite runs under the DL regime. Needs the
942
+ * npm-entry bundle. Default graph only (same scope cut as owlClosure).
943
+ * @param {Dataset|string|Array} data
944
+ * @param {{format?: string}} [options]
945
+ * @returns {Promise<{dataset: Dataset, addedCount: number}>}
946
+ * dataset is input + tableau-derived triples; addedCount is how many
947
+ * the tableau added.
948
+ */
949
+ async function tableauMaterialise(data, options) {
950
+ const e = await entry();
951
+ if (!e) throw pendingError('OWL tableau materialisation');
952
+ requireEntryFn(e, 'tableauMaterialise', 'OWL tableau materialisation');
953
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'tableauMaterialise(data)');
954
+ const r = entryResult(e.tableauMaterialise(dataNq), 'tableauMaterialise');
955
+ return {
956
+ dataset: Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() }),
957
+ addedCount: r.addedCount,
958
+ };
959
+ }
960
+
961
+ /**
962
+ * OWL DL inconsistency verdict. Replays bin/owl-runner's DL pipeline
963
+ * (OWL-RL closure -> Tableau.tableau_materialise -> OWL-RL closure ->
964
+ * is_inconsistent). `rlAlone` is the plain OWL-RL verdict on the same
965
+ * input, so a caller can see the DL>=RL cases the tableau adds: a
966
+ * disjointness clash reached only after the tableau materialises a
967
+ * restriction membership the Datalog closure never derives. Needs the
968
+ * npm-entry bundle. Default graph only.
969
+ * @param {Dataset|string|Array} data
970
+ * @param {{format?: string}} [options]
971
+ * @returns {Promise<{inconsistent: boolean, rlAlone: boolean}>}
972
+ */
973
+ async function tableauDlInconsistent(data, options) {
974
+ const e = await entry();
975
+ if (!e) throw pendingError('OWL tableau DL inconsistency check');
976
+ requireEntryFn(e, 'tableauDlInconsistent', 'OWL tableau DL inconsistency check');
977
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'tableauDlInconsistent(data)');
978
+ const r = entryResult(e.tableauDlInconsistent(dataNq), 'tableauDlInconsistent');
979
+ return { inconsistent: r.inconsistent, rlAlone: r.rlAlone };
980
+ }
981
+
982
+ /**
983
+ * OWL DL consistency verdict via the verified clash-detecting tableau
984
+ * (formal/fstar/Tableau.Refute.fst's `tableau_consistent` over the
985
+ * OWL-RL closure -- the same pure verified chain bin/owl-runner runs
986
+ * under `--regime dl`, minus its native-only z3 counting oracle, which
987
+ * the JS bundle cannot spawn). Needs the npm-entry bundle. Default
988
+ * graph only (same scope cut as tableauDlInconsistent).
989
+ *
990
+ * Three-valued and honest: `consistent` is `false` (a clash on every
991
+ * tableau branch), `true` (a model was constructed with no clash), or
992
+ * `null` -- the refuter ran out of budget before deciding, with
993
+ * `reason` naming the fuel cap. `null` is never collapsed to `false`.
994
+ *
995
+ * @param {Dataset|string|Array} data the ontology + ABox graph
996
+ * @param {{format?: string, fuel?: number|string}} [options] format
997
+ * parses `data` (default 'turtle'); fuel overrides the refutation
998
+ * budget (default 20000).
999
+ * @returns {Promise<{consistent: boolean|null, reason?: string}>}
1000
+ */
1001
+ async function owlIsConsistent(data, options) {
1002
+ const e = await entry();
1003
+ if (!e) throw pendingError('OWL DL consistency check');
1004
+ requireEntryFn(e, 'owlIsConsistent', 'OWL DL consistency check');
1005
+ const opts = options || {};
1006
+ const dataNq = docsToEntryNQuads(e, toDocs(data, opts), 'owlIsConsistent(data)');
1007
+ const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
1008
+ const r = entryResult(e.owlIsConsistent(dataNq, optsJson), 'owlIsConsistent');
1009
+ return r.reason === undefined
1010
+ ? { consistent: r.consistent }
1011
+ : { consistent: r.consistent, reason: r.reason };
1012
+ }
1013
+
1014
+ /**
1015
+ * OWL entailment check: does `premise` entail `conclusion`? Two
1016
+ * verified paths, mirroring bin/owl-runner's PositiveEntailment
1017
+ * dispatch: `via: "closure"` when every conclusion triple is in the
1018
+ * OWL-RL closure of the premise; `via: "refutation"` when the negated
1019
+ * conclusion (Tableau.Refute's `negation_goals`) is refuted on every
1020
+ * goal by the clash-detecting tableau. Needs the npm-entry bundle.
1021
+ * Default graph only. Verified-only chain (no z3).
1022
+ *
1023
+ * Three-valued: `entailed` is `true`, `false`, or `null` (a refutation
1024
+ * goal exhausted its fuel budget -- indeterminate, never a silent
1025
+ * `false`; `reason` names the cap).
1026
+ *
1027
+ * @param {Dataset|string|Array} premise
1028
+ * @param {Dataset|string|Array} conclusion
1029
+ * @param {{format?: string, fuel?: number|string}} [options] format
1030
+ * parses both graphs (default 'turtle'); fuel overrides the
1031
+ * refutation budget (default 20000).
1032
+ * @returns {Promise<{entailed: boolean|null, via: 'closure'|'refutation', reason?: string}>}
1033
+ */
1034
+ async function owlEntails(premise, conclusion, options) {
1035
+ const e = await entry();
1036
+ if (!e) throw pendingError('OWL entailment check');
1037
+ requireEntryFn(e, 'owlEntails', 'OWL entailment check');
1038
+ const opts = options || {};
1039
+ const premiseNq = docsToEntryNQuads(e, toDocs(premise, opts), 'owlEntails(premise)');
1040
+ const conclusionNq = docsToEntryNQuads(e, toDocs(conclusion, opts), 'owlEntails(conclusion)');
1041
+ const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
1042
+ const r = entryResult(e.owlEntails(premiseNq, conclusionNq, optsJson), 'owlEntails');
1043
+ return r.reason === undefined
1044
+ ? { entailed: r.entailed, via: r.via }
1045
+ : { entailed: r.entailed, via: r.via, reason: r.reason };
1046
+ }
1047
+
1048
+ /**
1049
+ * Evaluate an RML mapping graph against one logical source's raw
1050
+ * data, returning the generated triples as a Dataset. Needs the
1051
+ * npm-entry bundle. Scope cut (documented, not silent): every
1052
+ * triples map in `mapping` reads the SAME `sourceData` -- joins
1053
+ * across two DIFFERENT logical sources are not reachable through
1054
+ * this one-source entry point (see bin/rml-runner/rml_runner.ml for
1055
+ * the full multi-source join driver).
1056
+ * @param {Dataset|string|Array} mapping the RML mapping graph (Turtle by default)
1057
+ * @param {string} sourceData raw JSON or CSV text (not RDF)
1058
+ * @param {'json'|'csv'} sourceKind
1059
+ * @param {{format?: string}} [options] applies to `mapping`
1060
+ * @returns {Promise<Dataset>}
1061
+ */
1062
+ async function rmlMap(mapping, sourceData, sourceKind, options) {
1063
+ const e = await entry();
1064
+ if (!e) throw pendingError('RML mapping evaluation');
1065
+ requireEntryFn(e, 'rmlMap', 'RML mapping evaluation');
1066
+ if (typeof sourceData !== 'string') {
1067
+ throw new TypeError('rmlMap: sourceData must be a string');
1068
+ }
1069
+ const kind = String(sourceKind || '').toLowerCase();
1070
+ if (kind !== 'json' && kind !== 'csv') {
1071
+ throw new TypeError("rmlMap: sourceKind must be 'json' or 'csv'");
1072
+ }
1073
+ const mappingNq = docsToEntryNQuads(e, toDocs(mapping, options), 'rmlMap(mapping)');
1074
+ const r = entryResult(e.rmlMap(mappingNq, sourceData, kind), 'rmlMap');
1075
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1076
+ }
1077
+
1078
+ /**
1079
+ * CSVW csv2rdf conversion (w3.org/TR/csv2rdf): convert tabular data
1080
+ * plus an optional CSVW metadata document into a Dataset. Needs the
1081
+ * npm-entry bundle. Scope cut (documented, not silent -- mirrors
1082
+ * rmlMap's one-source cut): every table in a multi-table `tables`
1083
+ * group reads the SAME `csvText`. Datatype `format` facets,
1084
+ * list-valued (`separator`) cells, and full inherited-property
1085
+ * propagation are not yet implemented -- see
1086
+ * docs/designissues/2026-07-05-csvw-program-plan.md for measured
1087
+ * coverage.
1088
+ * @param {string} csvText raw RFC 4180 tabular data (not RDF)
1089
+ * @param {string} [metadataJson] CSVW metadata document (JSON text);
1090
+ * '' / omitted infers the schema from the CSV's own header row
1091
+ * @param {{mode?: 'standard'|'minimal', base?: string, url?: string}}
1092
+ * [options] mode defaults to 'standard' (full csvw:TableGroup/
1093
+ * Table/Row wrapper); base is the resolution base IRI (default
1094
+ * 'file:///'); url is the tabular file's own URL used when the
1095
+ * metadata carries none (default 'table.csv') -- cell predicates
1096
+ * default to `<tableUrl>#<colName>`, so url shapes every emitted
1097
+ * predicate IRI.
1098
+ * @returns {Promise<Dataset>}
1099
+ */
1100
+ async function csvwToRdf(csvText, metadataJson, options) {
1101
+ if (typeof csvText !== 'string') {
1102
+ throw new TypeError('csvwToRdf: csvText must be a string');
1103
+ }
1104
+ const meta = metadataJson == null ? '' : metadataJson;
1105
+ if (typeof meta !== 'string') {
1106
+ throw new TypeError('csvwToRdf: metadataJson must be a string');
1107
+ }
1108
+ const e = await entry();
1109
+ if (!e) throw pendingError('CSVW csv2rdf conversion');
1110
+ requireEntryFn(e, 'csvwToRdf', 'CSVW csv2rdf conversion');
1111
+ const opts = options || {};
1112
+ const optionsJson = JSON.stringify({
1113
+ ...(opts.mode ? { mode: String(opts.mode).toLowerCase() } : {}),
1114
+ ...(opts.base ? { base: opts.base } : {}),
1115
+ ...(opts.url ? { url: opts.url } : {}),
1116
+ });
1117
+ const r = entryResult(e.csvwToRdf(csvText, meta, optionsJson), 'csvwToRdf');
1118
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1119
+ }
1120
+
1121
+ /**
1122
+ * Parse a JSON-LD document into a Dataset, with JSON-LD-specific
1123
+ * options `parse()` has no room for. Needs the npm-entry bundle
1124
+ * (plain `parse(text, {format:'jsonld'})` also works now -- see
1125
+ * bin/npm-entry/entry_jsoo.ml's parseToDatasetJson JSON-LD fix --
1126
+ * this exists for callers that need rdfDirection/expandContext/
1127
+ * processingMode).
1128
+ * @param {string} jsonldText
1129
+ * @param {{base?: string, rdfDirection?: string, expandContext?: string,
1130
+ * processingMode?: string}} [options]
1131
+ * @returns {Promise<Dataset>}
1132
+ */
1133
+ async function jsonldToRdf(jsonldText, options) {
1134
+ if (typeof jsonldText !== 'string') {
1135
+ throw new TypeError('jsonldToRdf: jsonldText must be a string');
1136
+ }
1137
+ const e = await entry();
1138
+ if (!e) throw pendingError('jsonldToRdf');
1139
+ requireEntryFn(e, 'jsonldToRdf', 'jsonldToRdf');
1140
+ const opts = options || {};
1141
+ const optionsJson = JSON.stringify({
1142
+ ...(opts.base ? { base: opts.base } : {}),
1143
+ ...(opts.rdfDirection ? { rdfDirection: opts.rdfDirection } : {}),
1144
+ ...(opts.expandContext ? { expandContext: opts.expandContext } : {}),
1145
+ ...(opts.processingMode ? { processingMode: opts.processingMode } : {}),
1146
+ });
1147
+ const r = entryResult(e.jsonldToRdf(jsonldText, optionsJson), 'jsonldToRdf');
1148
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1149
+ }
1150
+
1151
+ /**
1152
+ * RIF Core forward-chaining saturation, materialized as a new
1153
+ * Dataset (input triples + derived triples, default graph only --
1154
+ * RIF Core has no named-graph notion). Needs the npm-entry bundle.
1155
+ * @param {Dataset|string|Array} data the premise graph
1156
+ * @param {string} rifRulesXml a RIF Core XML rule document
1157
+ * @param {{format?: string}} [options] applies to `data`
1158
+ * @returns {Promise<Dataset>}
1159
+ */
1160
+ async function rifEval(data, rifRulesXml, options) {
1161
+ const e = await entry();
1162
+ if (!e) throw pendingError('RIF Core evaluation');
1163
+ requireEntryFn(e, 'rifEval', 'RIF Core evaluation');
1164
+ if (typeof rifRulesXml !== 'string') {
1165
+ throw new TypeError('rifEval: rifRulesXml must be a string');
1166
+ }
1167
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'rifEval(data)');
1168
+ const r = entryResult(e.rifEval(rifRulesXml, dataNq), 'rifEval');
1169
+ return Dataset.fromNQuads(r.saturatedNquads, {
1170
+ blankNodePrefix: freshBnodePrefix(),
1171
+ });
1172
+ }
1173
+
1174
+ /**
1175
+ * Serialize an RDF dataset as an expanded-form JSON-LD document --
1176
+ * the reverse of jsonldToRdf (entry_jsoo.ml's jsonldFromRdf export ->
1177
+ * the verified JSONLD.FromRdf.from_rdf). Returns the parsed JSON-LD
1178
+ * value (an array of node objects). Needs the npm-entry bundle.
1179
+ * @param {Dataset|string|Array} data
1180
+ * @param {{useNativeTypes?:boolean,useRdfType?:boolean,format?:string}} [options]
1181
+ * @returns {Promise<any>} the JSON-LD document (JCS-canonical, parsed)
1182
+ */
1183
+ async function jsonldFromRdf(data, options) {
1184
+ const e = await entry();
1185
+ if (!e) throw pendingError('jsonldFromRdf');
1186
+ requireEntryFn(e, 'jsonldFromRdf', 'jsonldFromRdf');
1187
+ const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'jsonldFromRdf(data)');
1188
+ const opts = options || {};
1189
+ const optionsJson = JSON.stringify({
1190
+ ...(opts.useNativeTypes ? { useNativeTypes: true } : {}),
1191
+ ...(opts.useRdfType ? { useRdfType: true } : {}),
1192
+ });
1193
+ const r = entryResult(e.jsonldFromRdf(dataNq, optionsJson), 'jsonldFromRdf');
1194
+ return JSON.parse(r.jsonld);
1195
+ }
1196
+
1197
+ /**
1198
+ * did:key resolution (entry_jsoo.ml's didKeyResolve export -> the
1199
+ * verified DID_Key.did_key_document). Resolves a did:key:z6Mk...
1200
+ * (Ed25519) to its DID Document, returned as a Dataset. Needs the
1201
+ * npm-entry bundle.
1202
+ * @param {string} didString a did:key URI
1203
+ * @returns {Promise<Dataset>} the DID Document as RDF
1204
+ */
1205
+ async function didKeyResolve(didString) {
1206
+ if (typeof didString !== 'string') {
1207
+ throw new TypeError('didKeyResolve: didString must be a string');
1208
+ }
1209
+ const e = await entry();
1210
+ if (!e) throw pendingError('didKeyResolve');
1211
+ requireEntryFn(e, 'didKeyResolve', 'did:key resolution');
1212
+ const r = entryResult(e.didKeyResolve(didString), 'didKeyResolve');
1213
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1214
+ }
1215
+
1216
+ /**
1217
+ * Test whether an XML document is well-formed (entry_jsoo.ml's
1218
+ * xmlWellformed export -> Parser_XML.parse_xml_document, the
1219
+ * accept/reject signal bin/xml-runner drives against W3C xmlconf).
1220
+ * The byte-oriented parser has no DOCTYPE/DTD production, so a
1221
+ * document containing a DOCTYPE reports false. Needs the npm-entry
1222
+ * bundle.
1223
+ * @param {string} xmlText
1224
+ * @returns {Promise<boolean>}
1225
+ */
1226
+ async function xmlWellformed(xmlText) {
1227
+ if (typeof xmlText !== 'string') {
1228
+ throw new TypeError('xmlWellformed: xmlText must be a string');
1229
+ }
1230
+ const e = await entry();
1231
+ if (!e) throw pendingError('xmlWellformed');
1232
+ requireEntryFn(e, 'xmlWellformed', 'XML well-formedness');
1233
+ const r = entryResult(e.xmlWellformed(xmlText), 'xmlWellformed');
1234
+ return !!r.wellformed;
1235
+ }
1236
+
1237
+ /**
1238
+ * Evaluate an XPath 1.0 expression over an XML document (entry_jsoo.ml's
1239
+ * xpathEval export -> XPath_Eval.eval_xpath_from_root). Returns the
1240
+ * result envelope: `resultType` ('nodeset'|'string'|'number'|'boolean')
1241
+ * plus, for a node-set, `count`/`stringValue`/`nodes`, else a scalar
1242
+ * `value`. Needs the npm-entry bundle.
1243
+ * @param {string} xmlText
1244
+ * @param {string} xpathExpr
1245
+ * @returns {Promise<object>}
1246
+ */
1247
+ async function xpathEval(xmlText, xpathExpr) {
1248
+ if (typeof xmlText !== 'string' || typeof xpathExpr !== 'string') {
1249
+ throw new TypeError('xpathEval: xmlText and xpathExpr must be strings');
1250
+ }
1251
+ const e = await entry();
1252
+ if (!e) throw pendingError('xpathEval');
1253
+ requireEntryFn(e, 'xpathEval', 'XPath evaluation');
1254
+ return entryResult(e.xpathEval(xmlText, xpathExpr), 'xpathEval');
1255
+ }
1256
+
1257
+ // -----------------------------------------------------------------
1258
+ // VC Data Integrity crypto (eddsa-rdfc-2022) — HACL* wasm backend.
1259
+ // entry_jsoo.ml's vc* exports realise VC_DataIntegrity's four crypto
1260
+ // assume vals via HACL*'s OWN official WebAssembly build. The wasm
1261
+ // backend MUST be initialised before the first primitive runs, or
1262
+ // the assume-val stub throws and `guarded` returns {ok:false} — a
1263
+ // verify NEVER silently succeeds uninitialised (#286, the
1264
+ // throw-on-uninit contract, which entryResult() preserves by
1265
+ // throwing on {ok:false}).
1266
+ //
1267
+ // Init story (Node js + wasm engines): these wrappers AUTO-AWAIT
1268
+ // initHacl() on the first VC call, via the optional driver.initCrypto
1269
+ // hook (idempotent + memoized) — a caller never has to remember the
1270
+ // init step. If the driver supplies no initCrypto hook (or init
1271
+ // rejects), the primitive's own honest {ok:false} error surfaces
1272
+ // rather than a false "valid". Browsers can't auto-init (the
1273
+ // hacl-wasm URL is page-specific) — browser.js documents explicit
1274
+ // init there. See skills/node-crypto-haclstar-vc-wasm-build.
1275
+ // -----------------------------------------------------------------
1276
+
1277
+ let cryptoInitPromise;
1278
+ async function ensureCrypto() {
1279
+ if (typeof driver.initCrypto !== 'function') return;
1280
+ if (!cryptoInitPromise) {
1281
+ cryptoInitPromise = Promise.resolve()
1282
+ .then(() => driver.initCrypto())
1283
+ .catch((err) => { cryptoInitPromise = undefined; throw err; });
1284
+ }
1285
+ await cryptoInitPromise;
1286
+ }
1287
+
1288
+ async function vcEntry(fnName, what) {
1289
+ const e = await entry();
1290
+ if (!e) throw pendingError(what);
1291
+ requireEntryFn(e, fnName, what);
1292
+ await ensureCrypto();
1293
+ return e;
1294
+ }
1295
+
1296
+ /**
1297
+ * SHA-256 of a message string's bytes, as a lowercase hex digest
1298
+ * (entry_jsoo.ml's vcSha256Hex -> VC_DataIntegrity.hash_sha256_hex,
1299
+ * HACL* SHA-2). Needs the npm-entry bundle + the HACL* wasm backend
1300
+ * (auto-initialised).
1301
+ * @param {string} message the message whose bytes are hashed
1302
+ * @returns {Promise<string>} 64-char hex digest
1303
+ */
1304
+ async function vcSha256Hex(message) {
1305
+ if (typeof message !== 'string') {
1306
+ throw new TypeError('vcSha256Hex: message must be a string');
1307
+ }
1308
+ const e = await vcEntry('vcSha256Hex', 'VC SHA-256');
1309
+ return entryResult(e.vcSha256Hex(message), 'vcSha256Hex').sha256;
1310
+ }
1311
+
1312
+ /**
1313
+ * Derive the Ed25519 public key from a 32-byte secret key
1314
+ * (entry_jsoo.ml's vcEd25519SecretToPublic -> HACL* Ed25519).
1315
+ * @param {string} secretKeyHex 32-byte secret key, hex
1316
+ * @returns {Promise<string>} 32-byte public key, hex
1317
+ */
1318
+ async function vcEd25519SecretToPublic(secretKeyHex) {
1319
+ if (typeof secretKeyHex !== 'string') {
1320
+ throw new TypeError('vcEd25519SecretToPublic: secretKeyHex must be a string');
1321
+ }
1322
+ const e = await vcEntry('vcEd25519SecretToPublic', 'VC Ed25519 key derivation');
1323
+ return entryResult(
1324
+ e.vcEd25519SecretToPublic(secretKeyHex), 'vcEd25519SecretToPublic').publicKeyHex;
1325
+ }
1326
+
1327
+ /**
1328
+ * Ed25519 signature over a hex-encoded message (entry_jsoo.ml's
1329
+ * vcEd25519Sign -> HACL* Ed25519).
1330
+ * @param {string} secretKeyHex 32-byte secret key, hex
1331
+ * @param {string} messageHex the message to sign, hex
1332
+ * @returns {Promise<string>} 64-byte signature, hex
1333
+ */
1334
+ async function vcEd25519Sign(secretKeyHex, messageHex) {
1335
+ if (typeof secretKeyHex !== 'string' || typeof messageHex !== 'string') {
1336
+ throw new TypeError('vcEd25519Sign: secretKeyHex and messageHex must be strings');
1337
+ }
1338
+ const e = await vcEntry('vcEd25519Sign', 'VC Ed25519 sign');
1339
+ return entryResult(e.vcEd25519Sign(secretKeyHex, messageHex), 'vcEd25519Sign').signatureHex;
1340
+ }
1341
+
1342
+ /**
1343
+ * Ed25519 verification (entry_jsoo.ml's vcEd25519Verify -> HACL*
1344
+ * Ed25519). A wrong key, tampered signature, altered message, or a
1345
+ * malformed-length input all return false — never an
1346
+ * exception-hidden true.
1347
+ * @param {string} publicKeyHex 32-byte public key, hex
1348
+ * @param {string} messageHex the message, hex
1349
+ * @param {string} signatureHex the 64-byte signature, hex
1350
+ * @returns {Promise<boolean>}
1351
+ */
1352
+ async function vcEd25519Verify(publicKeyHex, messageHex, signatureHex) {
1353
+ if (typeof publicKeyHex !== 'string' || typeof messageHex !== 'string' ||
1354
+ typeof signatureHex !== 'string') {
1355
+ throw new TypeError(
1356
+ 'vcEd25519Verify: publicKeyHex, messageHex and signatureHex must be strings');
1357
+ }
1358
+ const e = await vcEntry('vcEd25519Verify', 'VC Ed25519 verify');
1359
+ return !!entryResult(
1360
+ e.vcEd25519Verify(publicKeyHex, messageHex, signatureHex), 'vcEd25519Verify').valid;
1361
+ }
1362
+
1363
+ /**
1364
+ * Create an eddsa-rdfc-2022 Data Integrity proofValue over an
1365
+ * already-canonicalized document + proof config (entry_jsoo.ml's
1366
+ * vcEddsaCreateFromCanonical -> VC_DataIntegrity.eddsa_rdfc_2022_
1367
+ * create_from_canonical). The two canonical inputs are RDFC-1.0
1368
+ * canonical N-Quads (see canonicalize()).
1369
+ * @param {string} secretKeyHex 32-byte secret key, hex
1370
+ * @param {string} canonicalDocument canonical N-Quads of the document
1371
+ * @param {string} canonicalConfig canonical N-Quads of the proof config
1372
+ * @returns {Promise<string>} the multibase-z (base58btc) proofValue
1373
+ */
1374
+ async function vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig) {
1375
+ if (typeof secretKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1376
+ typeof canonicalConfig !== 'string') {
1377
+ throw new TypeError(
1378
+ 'vcEddsaCreateFromCanonical: secretKeyHex, canonicalDocument and canonicalConfig must be strings');
1379
+ }
1380
+ const e = await vcEntry('vcEddsaCreateFromCanonical', 'VC eddsa-rdfc-2022 proof creation');
1381
+ return entryResult(
1382
+ e.vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig),
1383
+ 'vcEddsaCreateFromCanonical').proofValue;
1384
+ }
1385
+
1386
+ /**
1387
+ * Verify an eddsa-rdfc-2022 proofValue against canonical inputs
1388
+ * (entry_jsoo.ml's vcEddsaVerifyFromCanonical ->
1389
+ * VC_DataIntegrity.eddsa_rdfc_2022_verify_from_canonical). Wrong
1390
+ * key, tampered document/config, or tampered proofValue all return
1391
+ * false.
1392
+ * @param {string} publicKeyHex 32-byte public key, hex
1393
+ * @param {string} canonicalDocument canonical N-Quads of the document
1394
+ * @param {string} canonicalConfig canonical N-Quads of the proof config
1395
+ * @param {string} proofValue the multibase-z proofValue to check
1396
+ * @returns {Promise<boolean>}
1397
+ */
1398
+ async function vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue) {
1399
+ if (typeof publicKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1400
+ typeof canonicalConfig !== 'string' || typeof proofValue !== 'string') {
1401
+ throw new TypeError(
1402
+ 'vcEddsaVerifyFromCanonical: publicKeyHex, canonicalDocument, canonicalConfig and proofValue must be strings');
1403
+ }
1404
+ const e = await vcEntry('vcEddsaVerifyFromCanonical', 'VC eddsa-rdfc-2022 proof verification');
1405
+ return !!entryResult(
1406
+ e.vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue),
1407
+ 'vcEddsaVerifyFromCanonical').verified;
1408
+ }
1409
+
1410
+ /**
1411
+ * VC Data Model 2.0 structural conformance check (entry_jsoo.ml's
1412
+ * vcCheckCredential -> VC_Credential.vc_check_from_string, 117 pass,
1413
+ * 0 fail on the offline vc_stage1 fixture suite). Pure structural
1414
+ * validation — no crypto, so this does NOT go through vcEntry/
1415
+ * ensureCrypto. `v2ctxJson` is the vendored VCDM v2 base context
1416
+ * document's raw JSON text (third_party/contexts/credentials-v2.jsonld,
1417
+ * `@context` value included — the F* side parses the whole document
1418
+ * and only reads its own @context field) and `credentialJson` is the
1419
+ * raw JSON text of the VC/VP document under test.
1420
+ * @param {string} v2ctxJson vendored credentials-v2.jsonld text
1421
+ * @param {string} credentialJson the VC/VP document's raw JSON text
1422
+ * @returns {Promise<{valid: boolean, reason?: string}>}
1423
+ */
1424
+ async function vcCheckCredential(v2ctxJson, credentialJson) {
1425
+ if (typeof v2ctxJson !== 'string' || typeof credentialJson !== 'string') {
1426
+ throw new TypeError('vcCheckCredential: v2ctxJson and credentialJson must be strings');
1427
+ }
1428
+ const e = await entry();
1429
+ if (!e) throw pendingError('vcCheckCredential');
1430
+ requireEntryFn(e, 'vcCheckCredential', 'VC Data Model 2.0 structural check');
1431
+ const r = entryResult(e.vcCheckCredential(v2ctxJson, credentialJson), 'vcCheckCredential');
1432
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1433
+ }
1434
+
1435
+ /**
1436
+ * credentialSubject presence/shape check, VERSION-AGNOSTIC (Track A1,
1437
+ * docs/designissues/2026-07-11-vc-canivc-eecc-plan.md) —
1438
+ * entry_jsoo.ml's vcCheckCredentialSubject ->
1439
+ * VC_Credential.vc_check_credential_subject_from_string. Unlike
1440
+ * vcCheckCredential above, this does NOT require the VCDM 2.0 base
1441
+ * @context to be first (or present at all) — it only checks that a
1442
+ * credential-shaped document (type includes "VerifiableCredential")
1443
+ * has a present, non-empty credentialSubject. Used for documents
1444
+ * under a non-VCDM-2.0 @context (e.g. the vc-di-eddsa Data Integrity
1445
+ * conformance suite's legacy VC 1.1 fixtures) where the full
1446
+ * vcCheckCredential's @context sentinel would (correctly) reject the
1447
+ * document for an unrelated reason.
1448
+ * @param {string} credentialJson the VC/VP document's raw JSON text
1449
+ * @returns {Promise<{valid: boolean, reason?: string}>}
1450
+ */
1451
+ async function vcCheckCredentialSubject(credentialJson) {
1452
+ if (typeof credentialJson !== 'string') {
1453
+ throw new TypeError('vcCheckCredentialSubject: credentialJson must be a string');
1454
+ }
1455
+ const e = await entry();
1456
+ if (!e) throw pendingError('vcCheckCredentialSubject');
1457
+ requireEntryFn(e, 'vcCheckCredentialSubject', 'credentialSubject presence check');
1458
+ const r = entryResult(e.vcCheckCredentialSubject(credentialJson), 'vcCheckCredentialSubject');
1459
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1460
+ }
1461
+
1462
+ /**
1463
+ * DATA_LOSS_DETECTION_ERROR check (Track A1, same plan doc) —
1464
+ * entry_jsoo.ml's vcCheckNoDataLoss ->
1465
+ * VC_Credential.vc_check_no_data_loss_from_string. Rejects a
1466
+ * credential-shaped document whose `type`/`@type` entries or
1467
+ * `credentialSubject` property keys include one that a lenient
1468
+ * JSON-LD processor would silently drop (VC Data Integrity spec,
1469
+ * "Securing Data Losslessly"). `credentialJson` MUST already have
1470
+ * any remote @context IRI the caller recognizes inlined to the real
1471
+ * context object — this engine build has no remote-context loader
1472
+ * registered, so a still-remote IRI string in "@context" fails
1473
+ * context processing honestly rather than silently skipping the
1474
+ * check.
1475
+ * @param {string} credentialJson the VC/VP document's raw JSON text,
1476
+ * @context already inlined
1477
+ * @returns {Promise<{valid: boolean, reason?: string}>}
1478
+ */
1479
+ async function vcCheckNoDataLoss(credentialJson) {
1480
+ if (typeof credentialJson !== 'string') {
1481
+ throw new TypeError('vcCheckNoDataLoss: credentialJson must be a string');
1482
+ }
1483
+ const e = await entry();
1484
+ if (!e) throw pendingError('vcCheckNoDataLoss');
1485
+ requireEntryFn(e, 'vcCheckNoDataLoss', 'DATA_LOSS_DETECTION_ERROR check');
1486
+ const r = entryResult(e.vcCheckNoDataLoss(credentialJson), 'vcCheckNoDataLoss');
1487
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1488
+ }
1489
+
1490
+ /**
1491
+ * relatedResource digest verification (VCDM 2.0 §5.3, vc20-api Track
1492
+ * A4) — entry_jsoo.ml's vcCheckRelatedResourceDigests ->
1493
+ * VC_Credential.vc_check_related_resource_digests_from_string. The
1494
+ * engine does no I/O, so the caller supplies a known-resource digest
1495
+ * registry: a JSON array of {"id": <resource URL>, "digestsHex":
1496
+ * [<lowercase hex>, ...]} entries computed from the caller's VENDORED
1497
+ * copies of each resource's content bytes. A relatedResource entry
1498
+ * whose id is in the registry and whose declared digestSRI/
1499
+ * digestMultibase matches none of that id's digests is rejected (the
1500
+ * spec's digest-mismatch error); an id absent from the registry, or a
1501
+ * digest algorithm the registry contract doesn't cover (anything
1502
+ * outside sha256/sha384), is unverifiable offline and passes. All
1503
+ * decode/match semantics are F*-verified (VC.Credential.fst).
1504
+ * @param {string} registryJson the digest registry's raw JSON text
1505
+ * @param {string} credentialJson the VC/VP document's raw JSON text
1506
+ * @returns {Promise<{valid: boolean, reason?: string}>}
1507
+ */
1508
+ async function vcCheckRelatedResourceDigests(registryJson, credentialJson) {
1509
+ if (typeof registryJson !== 'string' || typeof credentialJson !== 'string') {
1510
+ throw new TypeError('vcCheckRelatedResourceDigests: registryJson and credentialJson must be strings');
1511
+ }
1512
+ const e = await entry();
1513
+ if (!e) throw pendingError('vcCheckRelatedResourceDigests');
1514
+ requireEntryFn(e, 'vcCheckRelatedResourceDigests', 'relatedResource digest check');
1515
+ const r = entryResult(e.vcCheckRelatedResourceDigests(registryJson, credentialJson), 'vcCheckRelatedResourceDigests');
1516
+ return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1517
+ }
1518
+
1519
+ // -----------------------------------------------------------------
1520
+ // Typed "engine" functions (#74 npm FP surface). Each is a pure,
1521
+ // string/JSON-in, JSON-out wrapper over one F*-extracted engine
1522
+ // exposed by entry_jsoo.ml. No logic lives on the JS side — the
1523
+ // transform/eval/validate/CAS math is all verified F*; these bind
1524
+ // the entry ABI to a typed Promise. All need the npm-entry bundle.
1525
+ // -----------------------------------------------------------------
1526
+
1527
+ /**
1528
+ * XSLT 1.0 transform (entry_jsoo.ml's xsltTransform export ->
1529
+ * XSLT.Transform.transform). Applies `stylesheetXml` to `sourceXml`
1530
+ * and returns the serialized result tree. Needs the npm-entry bundle.
1531
+ * @param {string} stylesheetXml an XSLT stylesheet document
1532
+ * @param {string} sourceXml the source XML document
1533
+ * @returns {Promise<string>} the transform output (serialized XML/text)
1534
+ */
1535
+ async function xsltTransform(stylesheetXml, sourceXml) {
1536
+ if (typeof stylesheetXml !== 'string' || typeof sourceXml !== 'string') {
1537
+ throw new TypeError('xsltTransform: stylesheetXml and sourceXml must be strings');
1538
+ }
1539
+ const e = await entry();
1540
+ if (!e) throw pendingError('xsltTransform');
1541
+ requireEntryFn(e, 'xsltTransform', 'XSLT transform');
1542
+ const r = entryResult(e.xsltTransform(stylesheetXml, sourceXml), 'xsltTransform');
1543
+ return r.output;
1544
+ }
1545
+
1546
+ /**
1547
+ * Evaluate a Content MathML document (entry_jsoo.ml's mathmlEval
1548
+ * export -> MathML.Content.eval_doc_env). `bindings` maps
1549
+ * ci-variable names to value strings; pass {} for a closed
1550
+ * expression. Returns the exact numeric/boolean value or an
1551
+ * `undef` reason (division-by-zero, type error, ...). Needs the
1552
+ * npm-entry bundle.
1553
+ * @param {string} contentMathmlXml
1554
+ * @param {Record<string,string>} [bindings]
1555
+ * @returns {Promise<{kind:'rat',num:number,den:number}|{kind:'bool',value:boolean}|{kind:'undef',reason:string}>}
1556
+ */
1557
+ async function mathmlEval(contentMathmlXml, bindings) {
1558
+ if (typeof contentMathmlXml !== 'string') {
1559
+ throw new TypeError('mathmlEval: contentMathmlXml must be a string');
1560
+ }
1561
+ const b = bindings || {};
1562
+ if (typeof b !== 'object') {
1563
+ throw new TypeError('mathmlEval: bindings must be an object');
1564
+ }
1565
+ const e = await entry();
1566
+ if (!e) throw pendingError('mathmlEval');
1567
+ requireEntryFn(e, 'mathmlEval', 'MathML evaluation');
1568
+ const r = entryResult(
1569
+ e.mathmlEval(contentMathmlXml, JSON.stringify(b)), 'mathmlEval');
1570
+ return r.value;
1571
+ }
1572
+
1573
+ /**
1574
+ * XForms recalculate (entry_jsoo.ml's xformsRecalc export ->
1575
+ * XForms.Bind.recalculate). Applies the model binds (calculate,
1576
+ * constraint, relevant, required, readonly, type MIPs) to the
1577
+ * instance and returns the recomputed instance plus a validity
1578
+ * report per bound node. Needs the npm-entry bundle.
1579
+ * @param {string} instanceXml the XForms instance document
1580
+ * @param {Array<{id?:string,target:string,calculate?:string,constraint?:string,relevant?:string,required?:string,readonly?:string,type?:string}>} binds
1581
+ * @returns {Promise<{instance:string,validity:Array<object>}>}
1582
+ */
1583
+ async function xformsRecalc(instanceXml, binds) {
1584
+ if (typeof instanceXml !== 'string') {
1585
+ throw new TypeError('xformsRecalc: instanceXml must be a string');
1586
+ }
1587
+ if (!Array.isArray(binds)) {
1588
+ throw new TypeError('xformsRecalc: binds must be an array');
1589
+ }
1590
+ const e = await entry();
1591
+ if (!e) throw pendingError('xformsRecalc');
1592
+ requireEntryFn(e, 'xformsRecalc', 'XForms recalculate');
1593
+ const r = entryResult(
1594
+ e.xformsRecalc(instanceXml, JSON.stringify(binds)), 'xformsRecalc');
1595
+ return { instance: r.instance, validity: r.validity };
1596
+ }
1597
+
1598
+ /**
1599
+ * JSON Schema (draft-07) validation (entry_jsoo.ml's
1600
+ * jsonSchemaValidate export -> JSONSchema.Validate.validate).
1601
+ * Returns the verdict — the verified validator gives a definite
1602
+ * pass/fail/unsupported, not a per-keyword error list, so `errors`
1603
+ * carries a single reason string when not a definite pass. Needs
1604
+ * the npm-entry bundle.
1605
+ * @param {string} schemaJson the schema document (JSON text)
1606
+ * @param {string} instanceJson the instance document (JSON text)
1607
+ * @returns {Promise<{valid:boolean,result:'pass'|'fail'|'unsupported',errors:string[]}>}
1608
+ */
1609
+ async function jsonSchemaValidate(schemaJson, instanceJson) {
1610
+ if (typeof schemaJson !== 'string' || typeof instanceJson !== 'string') {
1611
+ throw new TypeError('jsonSchemaValidate: schemaJson and instanceJson must be strings');
1612
+ }
1613
+ const e = await entry();
1614
+ if (!e) throw pendingError('jsonSchemaValidate');
1615
+ requireEntryFn(e, 'jsonSchemaValidate', 'JSON Schema validation');
1616
+ const r = entryResult(
1617
+ e.jsonSchemaValidate(schemaJson, instanceJson), 'jsonSchemaValidate');
1618
+ return { valid: !!r.valid, result: r.result, errors: r.errors || [] };
1619
+ }
1620
+
1621
+ /**
1622
+ * Schematron validation (entry_jsoo.ml's schematronValidate export
1623
+ * -> Schematron.Validate.validate). Returns every finding (failed
1624
+ * assert, fired report, indeterminate) in pattern-then-document
1625
+ * order. Needs the npm-entry bundle.
1626
+ * @param {string} schematronXml the Schematron schema document
1627
+ * @param {string} instanceXml the instance document to check
1628
+ * @returns {Promise<{findings:Array<{type:string,context:string,test:string,message:string,path:string,reason?:string}>}>}
1629
+ */
1630
+ async function schematronValidate(schematronXml, instanceXml) {
1631
+ if (typeof schematronXml !== 'string' || typeof instanceXml !== 'string') {
1632
+ throw new TypeError('schematronValidate: schematronXml and instanceXml must be strings');
1633
+ }
1634
+ const e = await entry();
1635
+ if (!e) throw pendingError('schematronValidate');
1636
+ requireEntryFn(e, 'schematronValidate', 'Schematron validation');
1637
+ const r = entryResult(
1638
+ e.schematronValidate(schematronXml, instanceXml), 'schematronValidate');
1639
+ return { findings: r.findings };
1640
+ }
1641
+
1642
+ // TOAN — a small exact-CAS surface over Math.Expr (E_Int/E_Rat/E_Bool/
1643
+ // E_Sym/E_App). Callers pass an expression as the JSON codec
1644
+ // {int:n} | {rat:[n,d]} | {bool:b} | {sym:name} | {app:name,args:[...]}
1645
+ // and receive Content MathML for the result (via MathML.Present).
1646
+ async function toanCall(fnName, what, ...args) {
1647
+ const e = await entry();
1648
+ if (!e) throw pendingError(fnName);
1649
+ requireEntryFn(e, fnName, what);
1650
+ const r = entryResult(e[fnName](...args), fnName);
1651
+ return r.mathml;
1652
+ }
1653
+
1654
+ /**
1655
+ * Symbolic finite summation (entry_jsoo.ml's toanSummation ->
1656
+ * Math.Series.summation): sum of `body[idx:=lo..hi]`, simplified,
1657
+ * as Content MathML. Needs the npm-entry bundle.
1658
+ * @param {object} bodyExpr the summand, in the expr JSON codec
1659
+ * @param {string} idx the summation index symbol
1660
+ * @param {number} lo inclusive lower bound
1661
+ * @param {number} hi inclusive upper bound
1662
+ * @returns {Promise<string>} Content MathML
1663
+ */
1664
+ async function toanSummation(bodyExpr, idx, lo, hi) {
1665
+ if (typeof idx !== 'string') throw new TypeError('toanSummation: idx must be a string');
1666
+ return toanCall('toanSummation', 'TOAN summation',
1667
+ JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1668
+ }
1669
+
1670
+ /**
1671
+ * Symbolic finite product (entry_jsoo.ml's toanProduct ->
1672
+ * Math.Series.finite_product). Same shape as {@link toanSummation}.
1673
+ * @param {object} bodyExpr the factor, in the expr JSON codec
1674
+ * @param {string} idx the product index symbol
1675
+ * @param {number} lo inclusive lower bound
1676
+ * @param {number} hi inclusive upper bound
1677
+ * @returns {Promise<string>} Content MathML
1678
+ */
1679
+ async function toanProduct(bodyExpr, idx, lo, hi) {
1680
+ if (typeof idx !== 'string') throw new TypeError('toanProduct: idx must be a string');
1681
+ return toanCall('toanProduct', 'TOAN product',
1682
+ JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1683
+ }
1684
+
1685
+ /**
1686
+ * Canonical simplification (entry_jsoo.ml's toanSimplify ->
1687
+ * Math.Simplify.simplify) of an expression, as Content MathML.
1688
+ * @param {object} expr in the expr JSON codec
1689
+ * @returns {Promise<string>} Content MathML
1690
+ */
1691
+ async function toanSimplify(expr) {
1692
+ return toanCall('toanSimplify', 'TOAN simplify', JSON.stringify(expr));
1693
+ }
1694
+
1695
+ /**
1696
+ * Symbolic differentiation (entry_jsoo.ml's toanDiff ->
1697
+ * Math.Diff.diff) of `expr` w.r.t. `variable`, as Content MathML.
1698
+ * @param {object} expr in the expr JSON codec
1699
+ * @param {string} variable the differentiation variable
1700
+ * @returns {Promise<string>} Content MathML
1701
+ */
1702
+ async function toanDiff(expr, variable) {
1703
+ if (typeof variable !== 'string') throw new TypeError('toanDiff: variable must be a string');
1704
+ return toanCall('toanDiff', 'TOAN diff', JSON.stringify(expr), variable);
1705
+ }
1706
+
1707
+ /**
1708
+ * Substitution (entry_jsoo.ml's toanSubst -> Math.Subst.subst):
1709
+ * `expr[variable := value]`, simplified, as Content MathML.
1710
+ * @param {object} expr in the expr JSON codec
1711
+ * @param {string} variable the symbol to replace
1712
+ * @param {object} value the replacement, in the expr JSON codec
1713
+ * @returns {Promise<string>} Content MathML
1714
+ */
1715
+ async function toanSubst(expr, variable, value) {
1716
+ if (typeof variable !== 'string') throw new TypeError('toanSubst: variable must be a string');
1717
+ return toanCall('toanSubst', 'TOAN subst',
1718
+ JSON.stringify(expr), variable, JSON.stringify(value));
1719
+ }
1720
+
1721
+ // Matrix / vector algebra over exact rationals (Math.Matrix). A
1722
+ // matrix is a JSON array of rows; a vector a JSON array of cells;
1723
+ // a cell is an integer or a [num,den] pair. Results render via
1724
+ // Math.Matrix.mres_to_string ("undef" carries a `reason`).
1725
+ async function matrixCall(fnName, what, ...jsonArgs) {
1726
+ const e = await entry();
1727
+ if (!e) throw pendingError(fnName);
1728
+ requireEntryFn(e, fnName, what);
1729
+ const r = entryResult(e[fnName](...jsonArgs.map((a) => JSON.stringify(a))), fnName);
1730
+ return { result: r.result, reason: r.reason || '' };
1731
+ }
1732
+
1733
+ /**
1734
+ * Determinant of a square matrix (entry_jsoo.ml's matrixDeterminant
1735
+ * -> Math.Matrix.dyn_determinant), exact.
1736
+ * @param {Array<Array<number|[number,number]>>} matrix
1737
+ * @returns {Promise<{result:string,reason:string}>}
1738
+ */
1739
+ async function matrixDeterminant(matrix) {
1740
+ if (!Array.isArray(matrix)) throw new TypeError('matrixDeterminant: matrix must be an array of rows');
1741
+ return matrixCall('matrixDeterminant', 'matrix determinant', matrix);
1742
+ }
1743
+
1744
+ /**
1745
+ * Dot / scalar product of two vectors (entry_jsoo.ml's
1746
+ * matrixScalarProduct -> Math.Matrix.dyn_scalarproduct).
1747
+ * @param {Array<number|[number,number]>} a
1748
+ * @param {Array<number|[number,number]>} b
1749
+ * @returns {Promise<{result:string,reason:string}>}
1750
+ */
1751
+ async function matrixScalarProduct(a, b) {
1752
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixScalarProduct: a and b must be arrays');
1753
+ return matrixCall('matrixScalarProduct', 'vector scalar product', a, b);
1754
+ }
1755
+
1756
+ /**
1757
+ * Cross product of two 3-vectors (entry_jsoo.ml's
1758
+ * matrixVectorProduct -> Math.Matrix.dyn_vectorproduct).
1759
+ * @param {Array<number|[number,number]>} a
1760
+ * @param {Array<number|[number,number]>} b
1761
+ * @returns {Promise<{result:string,reason:string}>}
1762
+ */
1763
+ async function matrixVectorProduct(a, b) {
1764
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixVectorProduct: a and b must be arrays');
1765
+ return matrixCall('matrixVectorProduct', 'vector cross product', a, b);
1766
+ }
1767
+
1768
+ /**
1769
+ * Outer product of two vectors (entry_jsoo.ml's matrixOuterProduct
1770
+ * -> Math.Matrix.dyn_outerproduct).
1771
+ * @param {Array<number|[number,number]>} a
1772
+ * @param {Array<number|[number,number]>} b
1773
+ * @returns {Promise<{result:string,reason:string}>}
1774
+ */
1775
+ async function matrixOuterProduct(a, b) {
1776
+ if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixOuterProduct: a and b must be arrays');
1777
+ return matrixCall('matrixOuterProduct', 'vector outer product', a, b);
1778
+ }
1779
+
1780
+ // A `scaled` value as entry_jsoo.ml's scaledJson envelope decodes it:
1781
+ // {mantissa,scale,decimal} strings straight off the wire.
1782
+ function scaledFromJson(s) {
1783
+ return { mantissa: s.mantissa, scale: s.scale, decimal: s.decimal };
1784
+ }
1785
+
1786
+ /**
1787
+ * n+1 evenly spaced samples of the logistic sigmoid
1788
+ * L / (1 + exp(-k*(x - x0))) over [xmin, xmax] (entry_jsoo.ml's
1789
+ * sigmoidPoints -> Math.Sigmoid.sigmoid_points). All arithmetic --
1790
+ * argument reduction, the truncated Taylor series, repeated
1791
+ * squaring, and the x samples themselves -- runs as exact rational
1792
+ * arithmetic inside Math.Sigmoid.fst; see that module's header for
1793
+ * the documented error bound on the returned (rounded) values. This
1794
+ * wrapper only marshals JSON; it never computes exp itself.
1795
+ * @param {{k:number|string,x0:number|string,l:number|string,
1796
+ * xmin:number|string,xmax:number|string,n:number|string}} params
1797
+ * @returns {Promise<Array<{x:{mantissa:string,scale:string,decimal:string},
1798
+ * y:{mantissa:string,scale:string,decimal:string}}>>}
1799
+ */
1800
+ async function sigmoidPoints(params) {
1801
+ if (!params || typeof params !== 'object') {
1802
+ throw new TypeError('sigmoidPoints: params must be an object');
1803
+ }
1804
+ const e = await entry();
1805
+ if (!e) throw pendingError('sigmoid points');
1806
+ requireEntryFn(e, 'sigmoidPoints', 'sigmoid points');
1807
+ const wire = {
1808
+ k: String(params.k), x0: String(params.x0), l: String(params.l),
1809
+ xmin: String(params.xmin), xmax: String(params.xmax), n: String(params.n),
1810
+ };
1811
+ const r = entryResult(e.sigmoidPoints(JSON.stringify(wire)), 'sigmoid points');
1812
+ return r.points.map((p) => ({ x: scaledFromJson(p.x), y: scaledFromJson(p.y) }));
1813
+ }
1814
+
1815
+ /**
1816
+ * Presentation MathML for the sigmoid formula L / (1 + exp(-k*(x - x0))),
1817
+ * engine-serialized (entry_jsoo.ml's sigmoidFormulaMathml ->
1818
+ * MathML.Present.to_presentation_mathml applied to a fixed
1819
+ * Math.Expr.expr) -- never hand-written MathML.
1820
+ * @returns {Promise<string>} a `<math>...</math>` Presentation MathML document
1821
+ */
1822
+ async function sigmoidFormulaMathml() {
1823
+ const e = await entry();
1824
+ if (!e) throw pendingError('sigmoid formula MathML');
1825
+ requireEntryFn(e, 'sigmoidFormulaMathml', 'sigmoid formula MathML');
1826
+ const r = entryResult(e.sigmoidFormulaMathml(), 'sigmoid formula MathML');
1827
+ return r.mathml;
1828
+ }
1829
+
1830
+ // -----------------------------------------------------------------
1831
+ // In-memory COTTAS bytes store (docs/designissues/2026-07-06-
1832
+ // inmemory-bytes-store.md, stage 5). Needs the npm-entry bundle
1833
+ // (bin/npm-entry/entry_jsoo.ml's openCottas/queryCottas/closeCottas/
1834
+ // toCottas exports). Unlike every other operation in this file, the
1835
+ // "dataset" here is NOT an in-heap Dataset: openCottas() returns an
1836
+ // opaque handle string naming an entry the F*-verified COTTAS/Parquet
1837
+ // reader decodes lazily, row-group by row-group, as queryCottas()
1838
+ // touches it -- the whole point of the design (heap-store parity
1839
+ // would defeat the memory win the design doc measures). See
1840
+ // queryCottas's doc comment for the query-shape/entailment/write
1841
+ // divergences from query() this implies.
1842
+ // -----------------------------------------------------------------
1843
+
1844
+ // Accept a hex string, a Uint8Array/Buffer, or a plain ArrayBuffer;
1845
+ // normalize to the lowercase hex string the entry ABI's string-only
1846
+ // wire contract requires (same "strings in, JSON out" ABI every
1847
+ // other entry export uses -- see entry_jsoo.ml's file header).
1848
+ function bytesToHex(bytesLike, who) {
1849
+ if (typeof bytesLike === 'string') {
1850
+ if (!/^[0-9a-fA-F]*$/.test(bytesLike) || bytesLike.length % 2 !== 0) {
1851
+ throw new TypeError(`${who}: string input must be an even-length hex string`);
1852
+ }
1853
+ return bytesLike.toLowerCase();
1854
+ }
1855
+ let u8;
1856
+ if (bytesLike instanceof Uint8Array) u8 = bytesLike;
1857
+ else if (bytesLike instanceof ArrayBuffer) u8 = new Uint8Array(bytesLike);
1858
+ else {
1859
+ throw new TypeError(
1860
+ `${who}: expected a hex string, Uint8Array, Buffer, or ArrayBuffer`);
1861
+ }
1862
+ // Buffer's native hex codec when available (Node); a per-byte
1863
+ // string-concat loop allocates millions of intermediate strings on
1864
+ // a corpus-scale artifact (it measurably dominated the bytes-store
1865
+ // path's RSS at 50,000 quads before this branch existed).
1866
+ if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
1867
+ return Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength).toString('hex');
1868
+ }
1869
+ const HEX = '0123456789abcdef';
1870
+ const parts = new Array(u8.length);
1871
+ for (let i = 0; i < u8.length; i++) {
1872
+ parts[i] = HEX[u8[i] >> 4] + HEX[u8[i] & 15];
1873
+ }
1874
+ return parts.join('');
1875
+ }
1876
+
1877
+ function hexToBytes(hex) {
1878
+ const out = new Uint8Array(hex.length / 2);
1879
+ for (let i = 0; i < out.length; i++) {
1880
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
1881
+ }
1882
+ return out;
1883
+ }
1884
+
1885
+ /**
1886
+ * Open a COTTAS/Parquet artifact's raw bytes as a queryable,
1887
+ * read-only store -- the in-memory-bytes-store design's browser call
1888
+ * site. Needs the npm-entry bundle. The store is NOT materialized
1889
+ * into a heap Dataset: rows are decoded lazily by queryCottas() as a
1890
+ * query actually touches them (measured 64-161 B/quad in the design
1891
+ * doc's native numbers, vs. ~877 B/quad for a fully-parsed heap
1892
+ * Dataset of the same data).
1893
+ *
1894
+ * @param {string|Uint8Array|ArrayBuffer} bytes whole `.cottas` file contents
1895
+ * @returns {Promise<string>} an opaque handle for queryCottas()/closeCottas()
1896
+ */
1897
+ async function openCottas(bytes) {
1898
+ const e = await entry();
1899
+ if (!e) throw pendingError('openCottas (in-memory COTTAS bytes store)');
1900
+ requireEntryFn(e, 'openCottas', 'openCottas');
1901
+ const hex = bytesToHex(bytes, 'openCottas');
1902
+ const r = entryResult(e.openCottas(hex), 'openCottas');
1903
+ return r.handle;
1904
+ }
1905
+
1906
+ /**
1907
+ * Run a SPARQL 1.1 query against a store opened by openCottas().
1908
+ * Needs the npm-entry bundle.
1909
+ *
1910
+ * Divergences from query() (documented, not silent):
1911
+ * - No `entail` option -- bare COTTAS bytes carry no closure step.
1912
+ * - No write/--delta-log overlay -- read-only (design doc §2.4's
1913
+ * write-overlay story composes at the native store_caps layer;
1914
+ * it is not wired into this browser ABI).
1915
+ * - A query SHAPE the backend executor can't push down (rare; the
1916
+ * same honest-failure posture the native `--data-cottas` CLI path
1917
+ * has for run_select_query_backend_dataset/run_ask_query_backend_
1918
+ * dataset returning None) rejects with an Error rather than
1919
+ * silently falling back to a full materialize -- that fallback
1920
+ * would defeat the store's whole memory argument.
1921
+ * - DESCRIBE is not supported (same cut queryDataset's ABI has).
1922
+ *
1923
+ * @param {string} handle from openCottas()
1924
+ * @param {string} sparql
1925
+ * @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
1926
+ * SELECT -> Bindings[], ASK -> boolean, CONSTRUCT -> Dataset
1927
+ * (materialized once, via SPARQL11_Store.materialize_dataset_backend
1928
+ * -- see entry_jsoo.ml's queryCottas doc comment for why CONSTRUCT
1929
+ * alone pays that cost).
1930
+ */
1931
+ async function queryCottas(handle, sparql) {
1932
+ if (typeof handle !== 'string') {
1933
+ throw new TypeError('queryCottas: handle must be the string openCottas() returned');
1934
+ }
1935
+ if (typeof sparql !== 'string') {
1936
+ throw new TypeError('queryCottas: sparql must be a string');
1937
+ }
1938
+ const e = await entry();
1939
+ if (!e) throw pendingError('queryCottas');
1940
+ requireEntryFn(e, 'queryCottas', 'queryCottas');
1941
+ const r = entryResult(e.queryCottas(handle, sparql), 'queryCottas');
1942
+ if (r.kind === 'ask') return r.boolean;
1943
+ if (r.kind === 'construct') {
1944
+ return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1945
+ }
1946
+ return bindingsFromSrj(r.srj);
1947
+ }
1948
+
1949
+ /**
1950
+ * Release a store opened by openCottas(). Drops the handle from this
1951
+ * process's registry only -- it does NOT evict the underlying byte
1952
+ * cache the entry bundle keeps for the process's lifetime (design
1953
+ * doc "Open decisions" item 1: no eviction API exists yet). A page
1954
+ * that opens many short-lived stores still grows that cache for the
1955
+ * tab's lifetime; this is a documented limitation, not a silent leak.
1956
+ * @param {string} handle
1957
+ * @returns {Promise<void>}
1958
+ */
1959
+ async function closeCottas(handle) {
1960
+ if (typeof handle !== 'string') {
1961
+ throw new TypeError('closeCottas: handle must be the string openCottas() returned');
1962
+ }
1963
+ const e = await entry();
1964
+ if (!e) throw pendingError('closeCottas');
1965
+ requireEntryFn(e, 'closeCottas', 'closeCottas');
1966
+ entryResult(e.closeCottas(handle), 'closeCottas');
1967
+ }
1968
+
1969
+ /**
1970
+ * Serialize a dataset to COTTAS/Parquet bytes via the native writer
1971
+ * (RDF.CottasStore.BaseWriter.serialize_cottas_v2 -- the SAME pure
1972
+ * `Tot` F* function `factoidal compact --native-writer` uses), for a
1973
+ * caller to persist (IndexedDB/OPFS) or offer as a download. Needs
1974
+ * the npm-entry bundle. Round-trips through openCottas(): the bytes
1975
+ * this returns are valid input to openCottas() and to the native
1976
+ * `--data-cottas`/`--data-cottas-mem` CLI flags, byte-for-byte.
1977
+ * @param {Dataset|string|Array} data
1978
+ * @param {{format?: string}} [options]
1979
+ * @returns {Promise<Uint8Array>}
1980
+ */
1981
+ async function toCottas(data, options) {
1982
+ const e = await entry();
1983
+ if (!e) throw pendingError('toCottas (native COTTAS serialization)');
1984
+ requireEntryFn(e, 'toCottas', 'toCottas');
1985
+ const nq = docsToEntryNQuads(e, toDocs(data, options), 'toCottas');
1986
+ const r = entryResult(e.toCottas(nq), 'toCottas');
1987
+ return hexToBytes(r.cottasHex);
1988
+ }
1989
+
1990
+ /**
1991
+ * Feature probe, for tests and downstream capability checks.
1992
+ * @returns {Promise<{entry: boolean, construct: boolean,
1993
+ * update: boolean, canonicalize: boolean, graphs: boolean,
1994
+ * canonicalHash: boolean, shacl: boolean, shex: boolean,
1995
+ * owlClosure: boolean, rml: boolean, csvw: boolean, jsonld: boolean,
1996
+ * rif: boolean}>}
1997
+ */
1998
+ let capsCache = null;
1999
+ async function capabilities() {
2000
+ if (capsCache) return capsCache;
2001
+ capsCache = capabilitiesUncached();
2002
+ return capsCache;
2003
+ }
2004
+
2005
+ async function capabilitiesUncached() {
2006
+ const e = await entry();
2007
+ if (e) {
2008
+ return {
2009
+ entry: true, construct: true, update: true, canonicalize: true,
2010
+ graphs: true, canonicalHash: true,
2011
+ // Per-function probes, not a blanket `true` -- an older/stale
2012
+ // loaded bundle (e.g. a wasm-target build that predates one of
2013
+ // these exports) reports its ACTUAL surface rather than
2014
+ // over-promising (see requireEntryFn's doc comment above).
2015
+ shacl: typeof e.shaclValidate === 'function',
2016
+ shex: typeof e.shexValidate === 'function',
2017
+ owlClosure: typeof e.owlClosure === 'function',
2018
+ tableau: typeof e.tableauMaterialise === 'function' &&
2019
+ typeof e.tableauDlInconsistent === 'function',
2020
+ rml: typeof e.rmlMap === 'function',
2021
+ csvw: typeof e.csvwToRdf === 'function',
2022
+ jsonld: typeof e.jsonldToRdf === 'function',
2023
+ jsonldFromRdf: typeof e.jsonldFromRdf === 'function',
2024
+ didKey: typeof e.didKeyResolve === 'function',
2025
+ xml: typeof e.xmlWellformed === 'function',
2026
+ xpath: typeof e.xpathEval === 'function',
2027
+ rif: typeof e.rifEval === 'function',
2028
+ xslt: typeof e.xsltTransform === 'function',
2029
+ mathml: typeof e.mathmlEval === 'function',
2030
+ xforms: typeof e.xformsRecalc === 'function',
2031
+ jsonSchema: typeof e.jsonSchemaValidate === 'function',
2032
+ schematron: typeof e.schematronValidate === 'function',
2033
+ toan: typeof e.toanSummation === 'function',
2034
+ matrix: typeof e.matrixDeterminant === 'function',
2035
+ sigmoid: typeof e.sigmoidPoints === 'function' &&
2036
+ typeof e.sigmoidFormulaMathml === 'function',
2037
+ cottasBytesStore: typeof e.openCottas === 'function' &&
2038
+ typeof e.queryCottas === 'function' && typeof e.toCottas === 'function',
2039
+ // VC Data Integrity crypto surface (probed, not blanket-true --
2040
+ // an older bundle predates the vc* exports). The HACL* wasm
2041
+ // backend still has to be initialised at call time; this flag
2042
+ // only reports that the ABI exports exist.
2043
+ vcCrypto: typeof e.vcEd25519Verify === 'function' &&
2044
+ typeof e.vcEddsaVerifyFromCanonical === 'function',
2045
+ };
2046
+ }
2047
+ // Probe --canonicalize support on the CLI bundle with a 1-quad doc.
2048
+ // canonicalHash rides the same engine support as canonicalize (it is
2049
+ // canonicalize() applied to one graph's triples); graphs() is pure
2050
+ // JS enumeration and needs no engine at all.
2051
+ let canon = false;
2052
+ try {
2053
+ const res = await run(
2054
+ ['--canonicalize', '-d', '/static/cap.nq'],
2055
+ [{ name: '/static/cap.nq',
2056
+ content: '<http://x/s> <http://x/p> "o" .\n' }]);
2057
+ canon = res.exitCode === 0;
2058
+ } catch (_) { canon = false; }
2059
+ return {
2060
+ entry: false, construct: false, update: false, canonicalize: canon,
2061
+ graphs: true, canonicalHash: canon,
2062
+ shacl: false, shex: false, owlClosure: false, tableau: false, rml: false,
2063
+ csvw: false, jsonld: false, jsonldFromRdf: false, didKey: false,
2064
+ xml: false, xpath: false, rif: false, cottasBytesStore: false,
2065
+ xslt: false, mathml: false, xforms: false, jsonSchema: false,
2066
+ schematron: false, toan: false, matrix: false, sigmoid: false, vcCrypto: false,
2067
+ };
2068
+ }
2069
+
2070
+ return {
2071
+ parse,
2072
+ query,
2073
+ queryHdt,
2074
+ update,
2075
+ registerExtensionFunction,
2076
+ unregisterExtensionFunction,
2077
+ clearExtensionFunctions,
2078
+ registerServiceEndpoint,
2079
+ clearServiceEndpoints,
2080
+ serialize,
2081
+ canonicalize,
2082
+ graphs,
2083
+ canonicalHash,
2084
+ shaclValidate,
2085
+ shexValidate,
2086
+ owlClosure,
2087
+ coreRdfsClosure,
2088
+ coreRdfsCheck,
2089
+ rdfsPlusClosure,
2090
+ rhoDfClosure,
2091
+ rhoDfFragmentCheck,
2092
+ tableauMaterialise,
2093
+ tableauDlInconsistent,
2094
+ owlIsConsistent,
2095
+ owlEntails,
2096
+ rmlMap,
2097
+ csvwToRdf,
2098
+ jsonldToRdf,
2099
+ jsonldFromRdf,
2100
+ didKeyResolve,
2101
+ xmlWellformed,
2102
+ xpathEval,
2103
+ rifEval,
2104
+ xsltTransform,
2105
+ mathmlEval,
2106
+ xformsRecalc,
2107
+ jsonSchemaValidate,
2108
+ schematronValidate,
2109
+ toanSummation,
2110
+ toanProduct,
2111
+ toanSimplify,
2112
+ toanDiff,
2113
+ toanSubst,
2114
+ matrixDeterminant,
2115
+ matrixScalarProduct,
2116
+ matrixVectorProduct,
2117
+ matrixOuterProduct,
2118
+ sigmoidPoints,
2119
+ sigmoidFormulaMathml,
2120
+ vcSha256Hex,
2121
+ vcEd25519SecretToPublic,
2122
+ vcEd25519Sign,
2123
+ vcEd25519Verify,
2124
+ vcEddsaCreateFromCanonical,
2125
+ vcEddsaVerifyFromCanonical,
2126
+ vcCheckCredential,
2127
+ vcCheckCredentialSubject,
2128
+ vcCheckNoDataLoss,
2129
+ vcCheckRelatedResourceDigests,
2130
+ openCottas,
2131
+ queryCottas,
2132
+ closeCottas,
2133
+ toCottas,
2134
+ capabilities,
2135
+ Dataset,
2136
+ dataFactory,
2137
+ };
2138
+ }
2139
+
2140
+ module.exports = { buildApi, sniffQueryForm, bindingsFromSrj };