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