@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.
- package/CHANGELOG.md +121 -0
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/browser-wasm.js +872 -0
- package/browser.d.ts +514 -0
- package/browser.js +2276 -0
- package/factoidal-npm-entry.js +32609 -0
- package/factoidal-npm-entry.wasm.assets/code-7ac046580f1bbdda8dc6.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +455 -0
- package/factoidal.js +27560 -0
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +457 -0
- package/fn.d.ts +519 -0
- package/fn.js +916 -0
- package/hacl-init.js +92 -0
- package/hacl-wasm/FStar.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
- package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
- package/hacl-wasm/LowStar_Endianness.wasm +0 -0
- package/hacl-wasm/WasmSupport.wasm +0 -0
- package/hacl-wasm/api.js +775 -0
- package/hacl-wasm/api.json +3787 -0
- package/hacl-wasm/layouts.json +1 -0
- package/hacl-wasm/loader.js +568 -0
- package/hacl-wasm/shell.js +12 -0
- package/index.d.ts +1068 -0
- package/index.js +237 -0
- package/index.mjs +85 -0
- package/lib/api.js +2140 -0
- package/lib/engine-js.js +165 -0
- package/lib/engine-wasm.js +300 -0
- package/package.json +101 -0
- package/rdfjs.js +540 -0
- package/version.json +101 -0
- package/wasm.d.ts +130 -0
- 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 };
|