@factoidal/core 0.7.0 → 0.10.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 +510 -1
- package/NOTICE +2 -21
- package/README.md +117 -993
- package/api.d.ts +53 -0
- package/api.js +5 -0
- package/api.mjs +56 -0
- package/bin/engine.mjs +38 -19
- package/bin/factoidal.mjs +513 -67
- package/bin/pack.mjs +37 -0
- package/bin/serve-worker.mjs +32 -0
- package/bin/serve.mjs +423 -0
- package/bin/store-worker-host.mjs +1 -1
- package/bin/store.mjs +1 -1
- package/browser.d.ts +110 -507
- package/browser.js +151 -2257
- package/docs/fuseki-replacement.md +108 -0
- package/fn.d.ts +24 -95
- package/fn.js +104 -150
- package/index.d.ts +598 -259
- package/index.js +4 -234
- package/index.mjs +176 -84
- package/l4-assets/l4factoidal.js +47 -7
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +11 -7
- package/lib/api.mjs +2300 -0
- package/lib/browser-engine.mjs +69 -0
- package/lib/dataset.mjs +377 -0
- package/lib/delta-log-hosts.mjs +191 -0
- package/lib/lean-entry.mjs +204 -0
- package/lib/pure.mjs +98 -0
- package/lib/store.mjs +115 -0
- package/lib/version.mjs +5 -0
- package/lws/README.md +97 -0
- package/lws/server.mjs +331 -0
- package/package.json +59 -61
- package/rdfjs.js +5 -536
- package/rdfjs.mjs +594 -0
- package/sample-store.d.ts +8 -16
- package/sample-store.mjs +39 -29
- package/solid/client/README.md +100 -0
- package/solid/client/index.mjs +277 -0
- package/solid/server/README.md +96 -0
- package/solid/server/index.mjs +317 -0
- package/store-host/deno.mjs +8 -0
- package/store-host/index.mjs +12 -0
- package/store-host/node.mjs +8 -0
- package/xmpp/README.md +107 -0
- package/xmpp/index.mjs +229 -0
- package/browser-wasm.js +0 -872
- package/factoidal-npm-entry.js +0 -15477
- package/factoidal-npm-entry.wasm.assets/code-5a7fc68f2ab1323718b8.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +0 -455
- package/factoidal.js +0 -13118
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +0 -457
- package/hacl-init.js +0 -92
- 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 +0 -775
- package/hacl-wasm/api.json +0 -3787
- package/hacl-wasm/layouts.json +0 -1
- package/hacl-wasm/loader.js +0 -568
- package/hacl-wasm/shell.js +0 -12
- package/l4-core.d.ts +0 -159
- package/l4-core.js +0 -188
- package/l4.d.ts +0 -50
- package/l4.js +0 -104
- package/lib/api.js +0 -2300
- package/lib/engine-js.js +0 -165
- package/lib/engine-wasm.js +0 -300
- package/sample-store/CURRENT +0 -1
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +0 -14
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +0 -2
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +0 -2
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +0 -1
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +0 -1
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +0 -3
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +0 -2
- package/select.d.ts +0 -116
- package/select.js +0 -492
- package/version.json +0 -102
- package/wasm.d.ts +0 -130
- package/wasm.js +0 -158
package/lib/api.mjs
ADDED
|
@@ -0,0 +1,2300 @@
|
|
|
1
|
+
// factoidal — the typed public API, over the Lean 4 engine's dispatch ABI.
|
|
2
|
+
//
|
|
3
|
+
// buildApi(driver) wraps an ENTRY OBJECT: one method per dispatch op of
|
|
4
|
+
// the Lean wasm module (formal/lean4/Wasm/Dispatch.lean), each taking
|
|
5
|
+
// string arguments and answering one JSON envelope string,
|
|
6
|
+
// {"ok":true,...} or {"ok":false,"error":...}. lib/lean-entry.mjs builds
|
|
7
|
+
// that object from a loaded engine handle; index.mjs (Node), browser.js
|
|
8
|
+
// and api.mjs (an engine the caller loaded) all pass it in here, so the
|
|
9
|
+
// three entry points share this one file.
|
|
10
|
+
//
|
|
11
|
+
// All semantic work happens inside the Lean engine. This layer shapes
|
|
12
|
+
// arguments (RDF/JS Datasets to N-Quads text, options to JSON) and
|
|
13
|
+
// results (envelopes to Datasets, SPARQL results JSON to Bindings). An
|
|
14
|
+
// operation whose op the loaded wasm does not have rejects with an
|
|
15
|
+
// Error that names the op (missingOpError below).
|
|
16
|
+
//
|
|
17
|
+
// ES module (0.9.0): no Node built-ins, so the browser loads this file
|
|
18
|
+
// as it is.
|
|
19
|
+
//
|
|
20
|
+
// The op names and envelopes are those of the F* npm entry
|
|
21
|
+
// (bin/npm-entry/entry_jsoo.ml) that served this API before 0.9.0; a
|
|
22
|
+
// JSDoc note "entry_jsoo.ml's X" below names that lineage. The Lean op of
|
|
23
|
+
// the same name serves X now (formal/lean4/Wasm/Dispatch.lean lists them).
|
|
24
|
+
|
|
25
|
+
import { dataFactory, termFromSrj, quadToNQuads } from '../rdfjs.mjs';
|
|
26
|
+
import { Dataset, INTERNAL, checkOptions, makeRegistry, WRITER_TAGS } from './dataset.mjs';
|
|
27
|
+
import { version as packageVersion } from './version.mjs';
|
|
28
|
+
import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mjs';
|
|
29
|
+
import { fullEngineMessage } from './lean-entry.mjs';
|
|
30
|
+
import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
|
|
31
|
+
|
|
32
|
+
const DATA_FORMAT_EXT = {
|
|
33
|
+
turtle: 'ttl',
|
|
34
|
+
ttl: 'ttl',
|
|
35
|
+
ntriples: 'nt',
|
|
36
|
+
nt: 'nt',
|
|
37
|
+
nquads: 'nq',
|
|
38
|
+
nq: 'nq',
|
|
39
|
+
trig: 'trig',
|
|
40
|
+
rdfxml: 'rdf',
|
|
41
|
+
'rdf-xml': 'rdf',
|
|
42
|
+
rdf: 'rdf',
|
|
43
|
+
jsonld: 'jsonld',
|
|
44
|
+
'json-ld': 'jsonld',
|
|
45
|
+
// RDF 1.2 opt-in: these select the engine's RDF 1.2 parsers (triple
|
|
46
|
+
// terms <<( s p o )>>, ~ reifiers, {| |} annotations, VERSION,
|
|
47
|
+
// directional literals "x"@lang--dir; Wasm/Ops/Parse.lean's
|
|
48
|
+
// `Mode.rdf12`); the plain names above stay RDF 1.1.
|
|
49
|
+
turtle12: 'ttl12',
|
|
50
|
+
ttl12: 'ttl12',
|
|
51
|
+
ntriples12: 'nt12',
|
|
52
|
+
nt12: 'nt12',
|
|
53
|
+
nquads12: 'nq12',
|
|
54
|
+
nq12: 'nq12',
|
|
55
|
+
trig12: 'trig12',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Canonical format tag for the engine's parse ops (parseToDatasetJson,
|
|
59
|
+
// datasetOpen); the *12 tags select the RDF 1.2 mode.
|
|
60
|
+
const DATA_FORMAT_TAG = {
|
|
61
|
+
ttl: 'turtle', nt: 'ntriples', nq: 'nquads', trig: 'trig', rdf: 'rdfxml',
|
|
62
|
+
jsonld: 'jsonld',
|
|
63
|
+
ttl12: 'turtle12', nt12: 'ntriples12', nq12: 'nquads12', trig12: 'trig12',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
|
|
67
|
+
|
|
68
|
+
// queryRaw's options (op queryRaw, Wasm/Ops/QueryExtras.lean).
|
|
69
|
+
const RAW_OUTPUT_FORMATS = new Set(['json', 'csv', 'tsv', 'xml', 'table', 'ntriples']);
|
|
70
|
+
const RAW_DATA_FORMATS = {
|
|
71
|
+
turtle: 'turtle', ttl: 'turtle', ntriples: 'ntriples', nt: 'ntriples',
|
|
72
|
+
nquads: 'nquads', nq: 'nquads', trig: 'trig', rdfxml: 'rdfxml',
|
|
73
|
+
'rdf-xml': 'rdfxml', rdf: 'rdfxml',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function extForFormat(fmt) {
|
|
77
|
+
const key = String(fmt || 'turtle').toLowerCase();
|
|
78
|
+
if (!(key in DATA_FORMAT_EXT)) {
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
`Unknown format '${fmt}'. Expected one of: ` +
|
|
81
|
+
Object.keys(DATA_FORMAT_EXT).join(', ')
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return DATA_FORMAT_EXT[key];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Where a function takes an options object, a bare string names the
|
|
88
|
+
// format: serialize(ds, 'turtle') is serialize(ds, { format: 'turtle' }).
|
|
89
|
+
// Until 0.8.0 the string was read as an empty options object, so
|
|
90
|
+
// serialize(ds, 'turtle') produced N-Quads with no error (found by the
|
|
91
|
+
// 0.8.0 release probe). Any other non-object value is a caller error and
|
|
92
|
+
// is reported as one, naming the function.
|
|
93
|
+
function optionsOf(fn, options) {
|
|
94
|
+
if (options == null) return {};
|
|
95
|
+
if (typeof options === 'string') return { format: options };
|
|
96
|
+
if (typeof options === 'object') return options;
|
|
97
|
+
throw new TypeError(
|
|
98
|
+
`${fn}: options must be an object or a format string, got ${typeof options}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The loaded wasm has no op for this operation (a build older than this
|
|
102
|
+
// package, or a dispatch table that does not serve it). When the entry is
|
|
103
|
+
// the core engine and the op is one only the full engine serves, the error
|
|
104
|
+
// names the entry that has it (issue 713 stage 4).
|
|
105
|
+
function missingOpError(what, e, op) {
|
|
106
|
+
if (e && e.profile === 'core' && op && Array.isArray(e.fullOnlyOps) &&
|
|
107
|
+
e.fullOnlyOps.includes(op)) {
|
|
108
|
+
return new Error(`${what}: ${fullEngineMessage(`the op '${op}'`)}`);
|
|
109
|
+
}
|
|
110
|
+
return new Error(
|
|
111
|
+
`${what}: the loaded Lean wasm module has no dispatch op for this ` +
|
|
112
|
+
'operation. The module is older than this package; rebuild it with ' +
|
|
113
|
+
'formal/lean4/Wasm/build-wasm.sh, or point FACTOIDAL_L4_ASSETS at a ' +
|
|
114
|
+
'current build.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Detect the query form for result-shape dispatch. IRIs and comments
|
|
118
|
+
// are blanked first so '#' inside a PREFIX IRI cannot hide the verb.
|
|
119
|
+
// The engine's SPARQL parser remains the authority — this only picks
|
|
120
|
+
// the output shape.
|
|
121
|
+
function sniffQueryForm(sparql) {
|
|
122
|
+
const cleaned = String(sparql)
|
|
123
|
+
.replace(/<[^>]*>/g, ' ')
|
|
124
|
+
.replace(/#[^\n]*/g, ' ');
|
|
125
|
+
const m = cleaned.match(/\b(select|ask|construct|describe)\b/i);
|
|
126
|
+
return m ? m[1].toLowerCase() : 'select';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Bytes for an op that reads the byte region (queryHdt, storeOpen): a
|
|
130
|
+
// Uint8Array, an ArrayBuffer, or a string packed one char per byte.
|
|
131
|
+
function bytesToUint8Array(bytesLike, who) {
|
|
132
|
+
if (bytesLike instanceof Uint8Array) return bytesLike;
|
|
133
|
+
if (bytesLike instanceof ArrayBuffer) return new Uint8Array(bytesLike);
|
|
134
|
+
if (typeof bytesLike === 'string') return Uint8Array.from(bytesLike, (c) => c.charCodeAt(0) & 0xff);
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`${who}: expected a Uint8Array, Buffer, ArrayBuffer, or an already-packed string`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// SRJ bindings -> Array<Map<string, Term>>
|
|
140
|
+
// The members of a JSON object are unordered (RFC 8259 section 4), so a
|
|
141
|
+
// binding object does not give the column order; `head.vars` (SPARQL 1.1
|
|
142
|
+
// Query Results JSON Format section 3.1) does. Each Map iterates in
|
|
143
|
+
// `head.vars` order, then any variable `head.vars` does not list.
|
|
144
|
+
function bindingsFromSrj(srj) {
|
|
145
|
+
const rows = (srj && srj.results && srj.results.bindings) || [];
|
|
146
|
+
const vars = (srj && srj.head && Array.isArray(srj.head.vars)) ? srj.head.vars : [];
|
|
147
|
+
return rows.map((row) => {
|
|
148
|
+
const map = new Map();
|
|
149
|
+
for (const name of vars) {
|
|
150
|
+
if (Object.prototype.hasOwnProperty.call(row, name)) map.set(name, termFromSrj(row[name]));
|
|
151
|
+
}
|
|
152
|
+
for (const [name, term] of Object.entries(row)) {
|
|
153
|
+
if (!map.has(name)) map.set(name, termFromSrj(term));
|
|
154
|
+
}
|
|
155
|
+
return map;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------
|
|
160
|
+
// Strict parsing (issue #344) -- ParseError, and the shared envelope
|
|
161
|
+
// unwrapper every engine call routes through.
|
|
162
|
+
// ---------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A parse failure that carries a position: `line`/`column`/`offset`
|
|
166
|
+
* when the failing engine call reported one (a parseToDatasetJson or
|
|
167
|
+
* datasetOpen error over Turtle, TriG, N-Triples or N-Quads), else
|
|
168
|
+
* `undefined`. RDF/XML and JSON-LD failures, and any non-parse engine
|
|
169
|
+
* error, stay plain Error -- entryResult() below decides which one to
|
|
170
|
+
* throw.
|
|
171
|
+
*
|
|
172
|
+
* Units: `line` and `column` are 1-based; `column` counts Unicode scalar
|
|
173
|
+
* values, as the engine reports it. `offset` is a UTF-16 code-unit index
|
|
174
|
+
* into the input JavaScript string, so `text.slice(err.offset)` starts
|
|
175
|
+
* at the error. The engine counts Unicode scalar values;
|
|
176
|
+
* utf16OffsetOf() below converts.
|
|
177
|
+
*/
|
|
178
|
+
class ParseError extends Error {
|
|
179
|
+
constructor(message, info) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.name = 'ParseError';
|
|
182
|
+
const i = info || {};
|
|
183
|
+
this.line = i.line;
|
|
184
|
+
this.column = i.column;
|
|
185
|
+
this.offset = i.offset;
|
|
186
|
+
this.format = i.format;
|
|
187
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
188
|
+
Error.captureStackTrace(this, ParseError);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Unwrap one dispatch-op JSON envelope ({"ok":true,...} |
|
|
194
|
+
// {"ok":false,"error":...}). A `line` or `offset` member on the error
|
|
195
|
+
// envelope means the failing call was a parse (Turtle/TriG/N-Triples/
|
|
196
|
+
// N-Quads carry a position; RDF/XML and JSON-LD carry a message only)
|
|
197
|
+
// -- throw ParseError so a caller can branch on position without
|
|
198
|
+
// string-matching the message. Every other failure stays a plain
|
|
199
|
+
// Error, as before. Pure (no per-driver state), so every buildApi()
|
|
200
|
+
// instance and DatasetHandle share this one function.
|
|
201
|
+
function entryResult(jsonText, what) {
|
|
202
|
+
const r = JSON.parse(jsonText);
|
|
203
|
+
if (!r.ok) {
|
|
204
|
+
if (r.line !== undefined || r.offset !== undefined) {
|
|
205
|
+
throw new ParseError(`${what}: ${r.error}`, {
|
|
206
|
+
line: r.line, column: r.column, offset: r.offset, format: r.format,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
throw new Error(`${what}: ${r.error}`);
|
|
210
|
+
}
|
|
211
|
+
return r;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The Lean engine reports a parse error's `offset` as a count of Unicode
|
|
215
|
+
// scalar values from the start of the input. A JavaScript string is
|
|
216
|
+
// indexed in UTF-16 code units, where a character outside the Basic
|
|
217
|
+
// Multilingual Plane (U+10000 and above) takes two. This converts the
|
|
218
|
+
// engine's count into the UTF-16 index of the same position. A count past
|
|
219
|
+
// the end of the text gives text.length. Iterating a string with for..of
|
|
220
|
+
// yields one element per code point (a lone surrogate is one element, and
|
|
221
|
+
// the engine reads it as one replacement character), so both sides count
|
|
222
|
+
// the same units.
|
|
223
|
+
function utf16OffsetOf(text, scalarOffset) {
|
|
224
|
+
if (typeof text !== 'string' || !Number.isInteger(scalarOffset) || scalarOffset < 0) {
|
|
225
|
+
return scalarOffset;
|
|
226
|
+
}
|
|
227
|
+
let units = 0;
|
|
228
|
+
let scalars = 0;
|
|
229
|
+
for (const ch of text) {
|
|
230
|
+
if (scalars >= scalarOffset) break;
|
|
231
|
+
units += ch.length;
|
|
232
|
+
scalars += 1;
|
|
233
|
+
}
|
|
234
|
+
return units;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The lenient parse's diagnostics (https://github.com/danbri/factoidal/issues/716)
|
|
238
|
+
// with each `offset` converted as utf16OffsetOf() converts a ParseError's, so
|
|
239
|
+
// `text.slice(d.offset)` starts at the error. `line` and `column` are kept as
|
|
240
|
+
// the engine reports them. One pass over `text` for all diagnostics: the
|
|
241
|
+
// offsets are visited in increasing order, whatever order the list has.
|
|
242
|
+
function utf16Diagnostics(text, diagnostics) {
|
|
243
|
+
if (!Array.isArray(diagnostics) || diagnostics.length === 0) return [];
|
|
244
|
+
if (typeof text !== 'string') return diagnostics;
|
|
245
|
+
const order = diagnostics
|
|
246
|
+
.map((d, i) => [d && Number.isInteger(d.offset) && d.offset >= 0 ? d.offset : -1, i])
|
|
247
|
+
.filter(([off]) => off >= 0)
|
|
248
|
+
.sort((a, b) => a[0] - b[0]);
|
|
249
|
+
const units = new Map();
|
|
250
|
+
let k = 0;
|
|
251
|
+
let unitsSoFar = 0;
|
|
252
|
+
let scalars = 0;
|
|
253
|
+
for (const ch of text) {
|
|
254
|
+
while (k < order.length && order[k][0] <= scalars) {
|
|
255
|
+
units.set(order[k][1], unitsSoFar);
|
|
256
|
+
k += 1;
|
|
257
|
+
}
|
|
258
|
+
if (k >= order.length) break;
|
|
259
|
+
unitsSoFar += ch.length;
|
|
260
|
+
scalars += 1;
|
|
261
|
+
}
|
|
262
|
+
for (; k < order.length; k += 1) units.set(order[k][1], unitsSoFar);
|
|
263
|
+
return diagnostics.map((d, i) => (units.has(i) ? { ...d, offset: units.get(i) } : d));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// entryResult() for an op that parsed `text`: a positioned ParseError gets
|
|
267
|
+
// its offset converted to a UTF-16 index into `text`.
|
|
268
|
+
function parseResult(jsonText, what, text) {
|
|
269
|
+
try {
|
|
270
|
+
return entryResult(jsonText, what);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
if (err instanceof ParseError && typeof err.offset === 'number') {
|
|
273
|
+
err.offset = utf16OffsetOf(text, err.offset);
|
|
274
|
+
}
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// DatasetHandle: the name of the engine-resident dataset class before
|
|
280
|
+
// 0.10.0. Deprecated alias of Dataset, kept for 0.10.x only.
|
|
281
|
+
const DatasetHandle = Dataset;
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------
|
|
284
|
+
// The Lean persisted store (Shardborough generations: IBK3, IBK4, IBK5
|
|
285
|
+
// blocks with their index sidecars; skills/shardborough-storage). A
|
|
286
|
+
// Store is a store HANDLE inside the engine (op storeOpen,
|
|
287
|
+
// Wasm/Ops/StoreHandles.lean): every artifact it read was checked against
|
|
288
|
+
// the SHA-256 its manifest commits, once, and every query answers from
|
|
289
|
+
// the decoded, indexed blocks the handle retains. The engine refuses a
|
|
290
|
+
// query whose plan needs a block the handle does not retain, so an answer
|
|
291
|
+
// is never one of a partial generation.
|
|
292
|
+
// ---------------------------------------------------------------------
|
|
293
|
+
class Store {
|
|
294
|
+
constructor(e, env, gen, manifestHex, fromNQuads, withExtensionRounds) {
|
|
295
|
+
this._e = e;
|
|
296
|
+
this._env = env;
|
|
297
|
+
this._gen = gen;
|
|
298
|
+
this._manifestHex = manifestHex;
|
|
299
|
+
this._closed = false;
|
|
300
|
+
this._fromNQuads = fromNQuads;
|
|
301
|
+
this._withExtensionRounds = withExtensionRounds;
|
|
302
|
+
}
|
|
303
|
+
/** The engine's handle id. */
|
|
304
|
+
get handle() { return this._env.handle; }
|
|
305
|
+
/** The generation directory name the store was opened from. */
|
|
306
|
+
get generation() { return this._gen.generation; }
|
|
307
|
+
/** True when the generation was opened through CURRENT. */
|
|
308
|
+
get activated() { return this._gen.activated; }
|
|
309
|
+
/** The generation's identity, as the engine reports it. */
|
|
310
|
+
get identity() { return this._env.identity; }
|
|
311
|
+
/** The manifest layout label, e.g. 'predicate-ibk3-ptd1-sri2-tli1-oli2-merkle-v0'. */
|
|
312
|
+
get layout() { return this._env.layout; }
|
|
313
|
+
/** The manifest wire version. */
|
|
314
|
+
get wireVersion() { return this._env.wireVersion; }
|
|
315
|
+
/** How many artifacts the handle read and verified. */
|
|
316
|
+
get artifacts() { return this._env.artifacts; }
|
|
317
|
+
/** How many artifact bytes the handle read. */
|
|
318
|
+
get bytes() { return this._env.bytes; }
|
|
319
|
+
/** How many rows the handle retains. */
|
|
320
|
+
get rows() { return this._env.rows; }
|
|
321
|
+
/** True once close() has run; every other method rejects after. */
|
|
322
|
+
get closed() { return this._closed; }
|
|
323
|
+
|
|
324
|
+
_assertOpen(who) {
|
|
325
|
+
if (this._closed) throw new Error(`Store.${who}: store handle '${this.handle}' is closed`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* One SPARQL 1.1 query against the retained blocks.
|
|
330
|
+
* @param {string} sparql
|
|
331
|
+
* @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
|
|
332
|
+
*/
|
|
333
|
+
async query(sparql) {
|
|
334
|
+
this._assertOpen('query');
|
|
335
|
+
if (typeof sparql !== 'string') throw new TypeError('Store.query: sparql must be a string');
|
|
336
|
+
const run = () => entryResult(this._e.storeHandleQuery(this.handle, sparql), 'Store.query');
|
|
337
|
+
const r = this._withExtensionRounds ? await this._withExtensionRounds(run) : run();
|
|
338
|
+
if (r.kind === 'ask') return r.boolean;
|
|
339
|
+
if (r.kind === 'construct' || r.kind === 'describe') {
|
|
340
|
+
return this._fromNQuads(r.nquads);
|
|
341
|
+
}
|
|
342
|
+
return bindingsFromSrj(r.srj);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The manifest, decoded by the engine (op storeManifestInspect). */
|
|
346
|
+
async inspect() {
|
|
347
|
+
const r = entryResult(this._e.storeManifestInspect(this._manifestHex), 'Store.inspect');
|
|
348
|
+
const { ok: _ok, ...rest } = r;
|
|
349
|
+
return rest;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Release the handle and the blocks it retains. Idempotent. */
|
|
353
|
+
async close() {
|
|
354
|
+
if (this._closed) return;
|
|
355
|
+
this._closed = true;
|
|
356
|
+
entryResult(this._e.storeHandleClose(this.handle), 'Store.close');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Build the public API around a driver.
|
|
362
|
+
*
|
|
363
|
+
* @param {object} driver
|
|
364
|
+
* @param {() => (object|Promise<object>)} driver.loadEntry
|
|
365
|
+
* Returns the entry object (lib/lean-entry.mjs, entryFromEngine).
|
|
366
|
+
* A rejection is reported by every operation that needs it.
|
|
367
|
+
* @param {string} driver.engineName reported as `engine`.
|
|
368
|
+
* @param {(root: string) => ((path: string) => any)} [driver.storeReaderFor]
|
|
369
|
+
* Turns a collection root path into a store reader (Node: the file
|
|
370
|
+
* system). Without it, openStore() takes a reader function only.
|
|
371
|
+
* @param {object} [driver.deltaLogStore]
|
|
372
|
+
* Where the deltaLog* functions keep their records
|
|
373
|
+
* (lib/delta-log-hosts.mjs): IndexedDB in the browser entries. The
|
|
374
|
+
* default keeps them in the memory of this process.
|
|
375
|
+
*/
|
|
376
|
+
function buildApi(driver) {
|
|
377
|
+
let entryPromise; // memoized; a failed load is retried on the next call
|
|
378
|
+
|
|
379
|
+
// The entry object. A failure to load the engine (assets missing, a
|
|
380
|
+
// wasm that does not instantiate) rejects every operation with that
|
|
381
|
+
// failure's own message; there is no other engine to fall back to.
|
|
382
|
+
async function entry() {
|
|
383
|
+
if (!entryPromise) {
|
|
384
|
+
entryPromise = Promise.resolve()
|
|
385
|
+
.then(() => driver.loadEntry())
|
|
386
|
+
.then((e) => {
|
|
387
|
+
if (!e) throw new Error('factoidal: the engine loader returned no entry object');
|
|
388
|
+
return e;
|
|
389
|
+
})
|
|
390
|
+
.catch((err) => { entryPromise = undefined; throw err; });
|
|
391
|
+
}
|
|
392
|
+
return entryPromise;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// entryResult() is now module-level (see above ParseError) -- pure,
|
|
396
|
+
// shared by every buildApi() instance and by DatasetHandle.
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------
|
|
400
|
+
// SPARQL 1.1 §17.6 extension functions — issue #463.
|
|
401
|
+
// https://github.com/danbri/factoidal/issues/463
|
|
402
|
+
//
|
|
403
|
+
// Comunica-style: the caller registers (possibly async) JS functions
|
|
404
|
+
// keyed by absolute IRI; a query using an unregistered IRI gets the
|
|
405
|
+
// spec-required error (in expression position: the row's value
|
|
406
|
+
// errors — unbound in SELECT/BIND, row dropped in FILTER).
|
|
407
|
+
//
|
|
408
|
+
// The engine's evaluator is synchronous, so async functions are
|
|
409
|
+
// bridged with a bounded re-evaluation trampoline: each engine call
|
|
410
|
+
// reaches extBridge synchronously; a cache miss on an async function
|
|
411
|
+
// records the promise, returns a pending marker (an engine-side
|
|
412
|
+
// error in THAT pass), and after the pass every pending promise is
|
|
413
|
+
// awaited into the cache and the query re-runs. The per-(iri, args)
|
|
414
|
+
// memoisation is also what honours the engine's purity assumption on
|
|
415
|
+
// the extension-function call — within one evaluation every call with
|
|
416
|
+
// the same arguments sees one stable answer.
|
|
417
|
+
//
|
|
418
|
+
// The user function receives an array of SRJ-style term objects
|
|
419
|
+
// ({type:'uri'|'literal'|'bnode', value, datatype?, 'xml:lang'?};
|
|
420
|
+
// {type:'error'} for an errored argument) and returns a term object,
|
|
421
|
+
// a JS primitive (boolean / number / string), a Promise of either,
|
|
422
|
+
// or null/undefined (= error). Thrown errors and rejections map to
|
|
423
|
+
// the §17.6 error too.
|
|
424
|
+
// ---------------------------------------------------------------
|
|
425
|
+
const EXT_PENDING_MARKER = '__FACTOIDAL_EXT_PENDING__';
|
|
426
|
+
const EXT_MAX_ROUNDS = 25;
|
|
427
|
+
const extFunctions = new Map(); // iri -> user fn
|
|
428
|
+
const extInstalled = new Set(); // iris registered into the ABI
|
|
429
|
+
let extCache = new Map(); // key -> normalized result (or null)
|
|
430
|
+
let extPending = []; // [{key, promise}] for this pass
|
|
431
|
+
|
|
432
|
+
function extBridge(iriJs, argsJsonJs) {
|
|
433
|
+
// Called SYNCHRONOUSLY from inside the engine.
|
|
434
|
+
const iri = String(iriJs);
|
|
435
|
+
const argsJson = String(argsJsonJs);
|
|
436
|
+
const key = iri + '\u0000' + argsJson;
|
|
437
|
+
if (extCache.has(key)) return extCache.get(key);
|
|
438
|
+
const fn = extFunctions.get(iri);
|
|
439
|
+
if (!fn) return null;
|
|
440
|
+
let out;
|
|
441
|
+
try {
|
|
442
|
+
out = fn(JSON.parse(argsJson));
|
|
443
|
+
} catch (_e) {
|
|
444
|
+
extCache.set(key, null);
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
if (out && typeof out.then === 'function') {
|
|
448
|
+
extPending.push({ key, promise: out });
|
|
449
|
+
return EXT_PENDING_MARKER;
|
|
450
|
+
}
|
|
451
|
+
out = out === undefined ? null : out;
|
|
452
|
+
extCache.set(key, out);
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function extEnsureInstalled(what) {
|
|
457
|
+
const e = await entry();
|
|
458
|
+
if (typeof e.registerExtensionFunction !== 'function') {
|
|
459
|
+
throw missingOpError(`${what} (extRegister)`);
|
|
460
|
+
}
|
|
461
|
+
for (const iri of extFunctions.keys()) {
|
|
462
|
+
if (!extInstalled.has(iri)) {
|
|
463
|
+
entryResult(e.registerExtensionFunction(iri, extBridge),
|
|
464
|
+
'registerExtensionFunction');
|
|
465
|
+
extInstalled.add(iri);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return e;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Register a custom SPARQL extension function (SPARQL 1.1 §17.6).
|
|
473
|
+
* @param {string} iri absolute IRI the function is invoked by
|
|
474
|
+
* @param {(args: object[]) => any} fn sync or async; see the block
|
|
475
|
+
* comment above for the argument/return contract
|
|
476
|
+
*/
|
|
477
|
+
async function registerExtensionFunction(iri, fn) {
|
|
478
|
+
if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
|
|
479
|
+
throw new TypeError(
|
|
480
|
+
'registerExtensionFunction: iri must be an absolute IRI string');
|
|
481
|
+
}
|
|
482
|
+
if (typeof fn !== 'function') {
|
|
483
|
+
throw new TypeError('registerExtensionFunction: fn must be a function');
|
|
484
|
+
}
|
|
485
|
+
extFunctions.set(iri, fn);
|
|
486
|
+
await extEnsureInstalled('registerExtensionFunction');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Remove one registered extension function. */
|
|
490
|
+
async function unregisterExtensionFunction(iri) {
|
|
491
|
+
extFunctions.delete(iri);
|
|
492
|
+
const e = await entry();
|
|
493
|
+
if (e && typeof e.unregisterExtensionFunction === 'function'
|
|
494
|
+
&& extInstalled.has(iri)) {
|
|
495
|
+
entryResult(e.unregisterExtensionFunction(iri),
|
|
496
|
+
'unregisterExtensionFunction');
|
|
497
|
+
extInstalled.delete(iri);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Remove every registered extension function. */
|
|
502
|
+
async function clearExtensionFunctions() {
|
|
503
|
+
extFunctions.clear();
|
|
504
|
+
const e = await entry();
|
|
505
|
+
if (e && typeof e.clearExtensionFunctions === 'function') {
|
|
506
|
+
entryResult(e.clearExtensionFunctions(), 'clearExtensionFunctions');
|
|
507
|
+
}
|
|
508
|
+
extInstalled.clear();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Bind a SPARQL SERVICE endpoint IRI to a local graph snapshot, so
|
|
513
|
+
* SERVICE <iri> { ... } (and LATERAL { SERVICE ... }) queries
|
|
514
|
+
* resolve against it in-process — the same registry the W3C
|
|
515
|
+
* federated-query suite uses (qt:serviceData). `data` is a Dataset,
|
|
516
|
+
* a raw RDF string (options.format, default turtle), or an array of
|
|
517
|
+
* those. The snapshot is the payload's default graph.
|
|
518
|
+
*/
|
|
519
|
+
async function registerServiceEndpoint(iri, data, options) {
|
|
520
|
+
if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
|
|
521
|
+
throw new TypeError(
|
|
522
|
+
'registerServiceEndpoint: iri must be an absolute IRI string');
|
|
523
|
+
}
|
|
524
|
+
const e = await entry();
|
|
525
|
+
if (typeof e.registerServiceEndpoint !== 'function') {
|
|
526
|
+
throw missingOpError('registerServiceEndpoint');
|
|
527
|
+
}
|
|
528
|
+
const opts = checkOptions('registerServiceEndpoint', optionsOf('registerServiceEndpoint', options), ['format', 'baseIRI']);
|
|
529
|
+
const nq = await nquadsOf(data, opts, 'registerServiceEndpoint');
|
|
530
|
+
return entryResult(e.registerServiceEndpoint(iri, nq),
|
|
531
|
+
'registerServiceEndpoint');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Remove every registered SERVICE endpoint snapshot. */
|
|
535
|
+
async function clearServiceEndpoints() {
|
|
536
|
+
const e = await entry();
|
|
537
|
+
if (e && typeof e.clearServiceEndpoints === 'function') {
|
|
538
|
+
entryResult(e.clearServiceEndpoints(), 'clearServiceEndpoints');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Run one synchronous engine pass, re-running until no NEW async
|
|
543
|
+
// extension results are pending. With no registered functions this
|
|
544
|
+
// is exactly one pass with zero overhead beyond the length check.
|
|
545
|
+
async function withExtensionRounds(runOnce) {
|
|
546
|
+
extCache = new Map(); // per-evaluation memo (purity contract)
|
|
547
|
+
for (let round = 0; ; round++) {
|
|
548
|
+
extPending = [];
|
|
549
|
+
const result = runOnce();
|
|
550
|
+
if (extPending.length === 0) return result;
|
|
551
|
+
if (round >= EXT_MAX_ROUNDS) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
'extension functions: async resolution did not converge ' +
|
|
554
|
+
`within ${EXT_MAX_ROUNDS} evaluation rounds`);
|
|
555
|
+
}
|
|
556
|
+
const pend = extPending;
|
|
557
|
+
extPending = [];
|
|
558
|
+
await Promise.all(pend.map(async ({ key, promise }) => {
|
|
559
|
+
try {
|
|
560
|
+
const v = await promise;
|
|
561
|
+
extCache.set(key, v === undefined ? null : v);
|
|
562
|
+
} catch (_e) {
|
|
563
|
+
extCache.set(key, null);
|
|
564
|
+
}
|
|
565
|
+
}));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// The prefixes a result Dataset inherits from its inputs: the union of
|
|
570
|
+
// every Dataset input's `prefixes`, in input order, the first
|
|
571
|
+
// declaration of a label winning. update() and CONSTRUCT answer a
|
|
572
|
+
// FRESH Dataset built from N-Quads, which carry no prefixes; without
|
|
573
|
+
// this the parse -> update -> serialize round trip would lose the
|
|
574
|
+
// source labels (https://github.com/danbri/factoidal/issues/681).
|
|
575
|
+
function prefixesOfData(data) {
|
|
576
|
+
const items = Array.isArray(data) ? data : [data];
|
|
577
|
+
const out = {};
|
|
578
|
+
for (const item of items) {
|
|
579
|
+
if (item instanceof Dataset && item.prefixes) {
|
|
580
|
+
for (const [label, iri] of Object.entries(item.prefixes)) {
|
|
581
|
+
if (!(label in out)) out[label] = iri;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// A ShEx focus/shape-label argument is either a raw string (an IRI,
|
|
589
|
+
// or "_:label" for a blank node -- the entry ABI's own convention)
|
|
590
|
+
// or an RDF/JS term (NamedNode/BlankNode); accepting both lets a
|
|
591
|
+
// caller pass a term straight out of a query() binding.
|
|
592
|
+
function shexTermToString(t, who) {
|
|
593
|
+
if (typeof t === 'string') return t;
|
|
594
|
+
if (t && typeof t.termType === 'string') {
|
|
595
|
+
if (t.termType === 'BlankNode') return `_:${t.value}`;
|
|
596
|
+
if (t.termType === 'NamedNode') return t.value;
|
|
597
|
+
throw new TypeError(`${who}: expected an IRI or blank node term`);
|
|
598
|
+
}
|
|
599
|
+
throw new TypeError(`${who}: expected a string or an RDF/JS term`);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------
|
|
603
|
+
// Datasets (https://github.com/danbri/factoidal/issues/882 item 1).
|
|
604
|
+
// Every producer answers a Dataset (lib/dataset.mjs): a handle into the
|
|
605
|
+
// engine's dataset store. Every consumer takes a Dataset, or text with
|
|
606
|
+
// an explicit {format}; when the format is omitted the engine infers it
|
|
607
|
+
// (op detectFormat) and the result reports it (Dataset.format,
|
|
608
|
+
// Dataset.formatInferred, or the error message).
|
|
609
|
+
// ---------------------------------------------------------------
|
|
610
|
+
let ctxCache = null;
|
|
611
|
+
const registry = makeRegistry();
|
|
612
|
+
|
|
613
|
+
async function ctx() {
|
|
614
|
+
const e = await entry();
|
|
615
|
+
if (ctxCache && ctxCache.e === e) return ctxCache;
|
|
616
|
+
const c = {
|
|
617
|
+
e,
|
|
618
|
+
registry,
|
|
619
|
+
missing: (what, op) => missingOpError(what, e, op),
|
|
620
|
+
wrap(r, prefixes, extra) {
|
|
621
|
+
const { ok: _ok, handle, count, prefixes: own, ...info } = r;
|
|
622
|
+
return new Dataset(INTERNAL, c, handle, count,
|
|
623
|
+
prefixes !== undefined ? prefixes : own, { ...(extra || {}), info });
|
|
624
|
+
},
|
|
625
|
+
fromNQuads(text, prefixes) {
|
|
626
|
+
requireEntryFn(e, 'datasetOpen', 'Dataset');
|
|
627
|
+
const r = entryResult(e.datasetOpen(text, 'nquads', ''), 'Dataset');
|
|
628
|
+
return c.wrap(r, prefixes || {});
|
|
629
|
+
},
|
|
630
|
+
async queryHandle(ds, sparql, opts) { return queryDatasetHandle(c, ds, sparql, opts); },
|
|
631
|
+
async shacl(ds, shapes, opts) { return shaclOnHandle(c, ds, shapes, opts); },
|
|
632
|
+
async shex(ds, schema, opts) {
|
|
633
|
+
if (!opts || opts.focus == null) {
|
|
634
|
+
throw new TypeError('Dataset.validateShex: options.focus is required');
|
|
635
|
+
}
|
|
636
|
+
return shexOnHandle(c, ds, schema, opts.focus, opts.shape);
|
|
637
|
+
},
|
|
638
|
+
async closure(ds, regime) { return closureOnHandle(c, ds, regime); },
|
|
639
|
+
};
|
|
640
|
+
ctxCache = c;
|
|
641
|
+
return c;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The parse tag of `format`, or the engine's inference when it is omitted.
|
|
645
|
+
function formatTagOf(c, text, format, who) {
|
|
646
|
+
if (format === undefined || format === null || format === '') {
|
|
647
|
+
requireEntryFn(c.e, 'detectFormat', `${who} (format inference)`);
|
|
648
|
+
return { tag: entryResult(c.e.detectFormat(text), who).format, inferred: true };
|
|
649
|
+
}
|
|
650
|
+
return { tag: DATA_FORMAT_TAG[extForFormat(format)], inferred: false };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Open `text` as a Dataset. A parse error names the format and says
|
|
654
|
+
// whether it was inferred.
|
|
655
|
+
function openText(c, text, format, baseIRI, who) {
|
|
656
|
+
const { tag, inferred } = formatTagOf(c, text, format, who);
|
|
657
|
+
requireEntryFn(c.e, 'datasetOpen', who);
|
|
658
|
+
let r;
|
|
659
|
+
try {
|
|
660
|
+
r = parseResult(c.e.datasetOpen(text, tag, baseIRI || ''), who, text);
|
|
661
|
+
} catch (err) {
|
|
662
|
+
if (inferred) {
|
|
663
|
+
err.message += ` (format inferred as '${tag}' because none was given; pass {format} to choose)`;
|
|
664
|
+
err.formatInferred = true;
|
|
665
|
+
}
|
|
666
|
+
err.format = err.format || tag;
|
|
667
|
+
throw err;
|
|
668
|
+
}
|
|
669
|
+
return c.wrap(r, undefined, { format: tag, formatInferred: inferred });
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// The Dataset a consumer reads: `data` itself when it is a Dataset, else
|
|
673
|
+
// a temporary one the caller disposes (`temp` true). An array opens as
|
|
674
|
+
// one dataset: its members' N-Quads, each document in its own blank-node
|
|
675
|
+
// scope.
|
|
676
|
+
function datasetOf(c, data, opts, who) {
|
|
677
|
+
if (data instanceof Dataset) {
|
|
678
|
+
if (data.disposed) data.size; // throws the disposed error
|
|
679
|
+
return { ds: data, temp: false };
|
|
680
|
+
}
|
|
681
|
+
if (typeof data === 'string') {
|
|
682
|
+
return { ds: openText(c, data, opts.format, opts.baseIRI, who), temp: true };
|
|
683
|
+
}
|
|
684
|
+
if (data && typeof data.text === 'string' && !Array.isArray(data)) {
|
|
685
|
+
return { ds: openText(c, data.text, data.format || opts.format, data.baseIRI || opts.baseIRI, who), temp: true };
|
|
686
|
+
}
|
|
687
|
+
if (Array.isArray(data)) {
|
|
688
|
+
let nq = '';
|
|
689
|
+
for (const item of data) {
|
|
690
|
+
const { ds, temp } = datasetOf(c, item, opts, who);
|
|
691
|
+
nq += ds.toNQuads();
|
|
692
|
+
if (temp) ds.dispose();
|
|
693
|
+
}
|
|
694
|
+
return { ds: c.fromNQuads(nq, prefixesOfData(data)), temp: true };
|
|
695
|
+
}
|
|
696
|
+
throw new TypeError(`${who}: expected a Dataset, a string, {text, format}, or an array of those`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function withDataset(data, opts, who, f) {
|
|
700
|
+
const c = await ctx();
|
|
701
|
+
const { ds, temp } = datasetOf(c, data, opts, who);
|
|
702
|
+
try {
|
|
703
|
+
return await f(c, ds);
|
|
704
|
+
} finally {
|
|
705
|
+
if (temp) ds.dispose();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// The N-Quads text of `data`, for the ops that read text only.
|
|
710
|
+
async function nquadsOf(data, opts, who) {
|
|
711
|
+
return withDataset(data, opts, who, (_c, ds) => ds.toNQuads());
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function queryDatasetHandle(c, ds, sparql, opts) {
|
|
715
|
+
if (typeof sparql !== 'string') throw new TypeError('query: sparql must be a string');
|
|
716
|
+
const entail = opts.entail || 'none';
|
|
717
|
+
if (/^x-ikl/i.test(entail)) {
|
|
718
|
+
throw new TypeError(
|
|
719
|
+
`query: entail '${entail}' is not implemented. The x-ikl-* ` +
|
|
720
|
+
"entailment regimes are the owner's design " +
|
|
721
|
+
"(danbri/factoidal#581); the Lean engine's dispatch for them " +
|
|
722
|
+
'was deleted on 2026-08-26 as collateral of the IKL-to-RDF ' +
|
|
723
|
+
'projection purge (danbri/factoidal#626). This is not a ' +
|
|
724
|
+
'policy exclusion.');
|
|
725
|
+
}
|
|
726
|
+
if (!ENTAIL_VALUES.has(entail)) {
|
|
727
|
+
throw new TypeError(`query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
|
|
728
|
+
}
|
|
729
|
+
if (opts.version !== undefined && !['1.1', '1.2'].includes(String(opts.version))) {
|
|
730
|
+
throw new TypeError("query: version must be '1.1' or '1.2'");
|
|
731
|
+
}
|
|
732
|
+
const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
|
|
733
|
+
const e = c.e;
|
|
734
|
+
if (entail !== 'none' || opts.output !== undefined) {
|
|
735
|
+
// queryRaw over the stored dataset: an entailment regime, or a
|
|
736
|
+
// results format (SPARQL 1.1 Query Results JSON, XML, CSV, TSV).
|
|
737
|
+
if (sparql12) {
|
|
738
|
+
throw new TypeError(
|
|
739
|
+
"query: entail/output and SPARQL 1.2 cannot be combined; that path reads SPARQL 1.1");
|
|
740
|
+
}
|
|
741
|
+
const output = opts.output || 'json';
|
|
742
|
+
if (!RAW_OUTPUT_FORMATS.has(output)) {
|
|
743
|
+
throw new TypeError(`query: output must be one of ${[...RAW_OUTPUT_FORMATS].join(', ')}`);
|
|
744
|
+
}
|
|
745
|
+
const form = sniffQueryForm(sparql);
|
|
746
|
+
const graphForm = form === 'construct' || form === 'describe';
|
|
747
|
+
requireEntryFn(e, 'datasetQueryRaw', `query {entail/output}`);
|
|
748
|
+
if (opts.output === undefined) {
|
|
749
|
+
// An entailment regime with the default answer shape.
|
|
750
|
+
const r = entryResult(e.datasetQueryRaw(ds.handle, sparql, JSON.stringify({
|
|
751
|
+
entail, output: graphForm ? 'ntriples' : 'json' })), 'query');
|
|
752
|
+
if (graphForm) return c.fromNQuads(r.text, ds.prefixes);
|
|
753
|
+
const srj = JSON.parse(r.text);
|
|
754
|
+
if (typeof srj.boolean === 'boolean') return srj.boolean;
|
|
755
|
+
return bindingsFromSrj(srj);
|
|
756
|
+
}
|
|
757
|
+
const r = entryResult(e.datasetQueryRaw(ds.handle, sparql,
|
|
758
|
+
JSON.stringify({ entail, output })), 'query');
|
|
759
|
+
return output === 'json' && !graphForm ? JSON.parse(r.text) : r.text;
|
|
760
|
+
}
|
|
761
|
+
const fn = sparql12 ? e.datasetQuery12 : e.datasetQuery;
|
|
762
|
+
if (typeof fn !== 'function') {
|
|
763
|
+
throw missingOpError(`query (datasetQuery${sparql12 ? '12' : ''})`, e,
|
|
764
|
+
sparql12 ? 'datasetQuery12' : 'datasetQuery');
|
|
765
|
+
}
|
|
766
|
+
const r = await withExtensionRounds(() => entryResult(fn(ds.handle, sparql), 'query'));
|
|
767
|
+
if (r.kind === 'ask') return r.boolean;
|
|
768
|
+
if (r.kind === 'construct' || r.kind === 'describe') {
|
|
769
|
+
return c.fromNQuads(r.nquads, ds.prefixes);
|
|
770
|
+
}
|
|
771
|
+
return bindingsFromSrj(r.srj);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function shaclOnHandle(c, ds, shapes, opts) {
|
|
775
|
+
requireEntryFn(c.e, 'datasetShaclValidate', 'SHACL validation');
|
|
776
|
+
const s = datasetOf(c, shapes, opts, 'shaclValidate(shapes)');
|
|
777
|
+
try {
|
|
778
|
+
const r = entryResult(c.e.datasetShaclValidate(ds.handle, s.ds.handle), 'shaclValidate');
|
|
779
|
+
return { conforms: r.conforms, report: c.fromNQuads(r.reportNquads, {}) };
|
|
780
|
+
} finally {
|
|
781
|
+
if (s.temp) s.ds.dispose();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function shexOnHandle(c, ds, schema, focus, shape) {
|
|
786
|
+
requireEntryFn(c.e, 'datasetShexValidate', 'ShEx validation');
|
|
787
|
+
if (typeof schema !== 'string') throw new TypeError('shexValidate: schema must be a string');
|
|
788
|
+
const focusStr = shexTermToString(focus, 'shexValidate(focus)');
|
|
789
|
+
const shapeStr = shape == null ? '' : shexTermToString(shape, 'shexValidate(shape)');
|
|
790
|
+
return entryResult(
|
|
791
|
+
c.e.datasetShexValidate(ds.handle, schema, focusStr, shapeStr), 'shexValidate').verdict;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const CLOSURE_REGIMES = {
|
|
795
|
+
rdfs: 'rdfs', 'owl-rl': 'owl-rl', owlrl: 'owl-rl', owl_rl: 'owl-rl',
|
|
796
|
+
'rho-df': 'rho-df', rhodf: 'rho-df', 'core-rdfs': 'rho-df',
|
|
797
|
+
'rdfs-plus': 'rdfs-plus', rdfsplus: 'rdfs-plus',
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
async function closureOnHandle(c, ds, regime) {
|
|
801
|
+
const r0 = CLOSURE_REGIMES[String(regime || '').toLowerCase()];
|
|
802
|
+
if (!r0) {
|
|
803
|
+
throw new TypeError(
|
|
804
|
+
`closure: regime must be one of rdfs, owl-rl, rho-df, rdfs-plus (got '${regime}')`);
|
|
805
|
+
}
|
|
806
|
+
requireEntryFn(c.e, 'datasetClosure', 'closure');
|
|
807
|
+
const r = entryResult(c.e.datasetClosure(ds.handle, r0), 'closure');
|
|
808
|
+
return c.wrap(r, ds.prefixes);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Everything below is async: the engine loads asynchronously, and an
|
|
812
|
+
// extension function may answer with a Promise.
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Parse one RDF document into a Dataset. Strict by default: a syntax
|
|
816
|
+
* error rejects with a ParseError (`line`/`column`/`offset` for Turtle,
|
|
817
|
+
* TriG, N-Triples and N-Quads). `{lenient:true}` skips each bad statement
|
|
818
|
+
* of Turtle, TriG, N-Triples and N-Quads and lists one diagnostic per
|
|
819
|
+
* skipped statement in `dataset.diagnostics`
|
|
820
|
+
* (https://github.com/danbri/factoidal/issues/716); `maxErrors: n` stops
|
|
821
|
+
* after n diagnostics. With no `format` the engine infers one
|
|
822
|
+
* (`dataset.format`, `dataset.formatInferred`).
|
|
823
|
+
* @param {string} text
|
|
824
|
+
* @param {{format?: string, baseIRI?: string, lenient?: boolean,
|
|
825
|
+
* maxErrors?: number}} [options]
|
|
826
|
+
* @returns {Promise<Dataset>}
|
|
827
|
+
*/
|
|
828
|
+
async function parse(text, options) {
|
|
829
|
+
if (typeof text !== 'string') throw new TypeError('parse: text must be a string');
|
|
830
|
+
const opts = checkOptions('parse', optionsOf('parse', options),
|
|
831
|
+
['format', 'baseIRI', 'lenient', 'maxErrors']);
|
|
832
|
+
const c = await ctx();
|
|
833
|
+
if (opts.lenient) {
|
|
834
|
+
const { tag, inferred } = formatTagOf(c, text, opts.format, 'parse');
|
|
835
|
+
requireEntryFn(c.e, 'parseDocument', 'parse {lenient:true} (parseDocument)');
|
|
836
|
+
const r = parseResult(
|
|
837
|
+
c.e.parseDocument(text, tag, opts.baseIRI || '', JSON.stringify(
|
|
838
|
+
Number.isInteger(opts.maxErrors) && opts.maxErrors >= 0
|
|
839
|
+
? { lenient: true, maxErrors: opts.maxErrors }
|
|
840
|
+
: { lenient: true })),
|
|
841
|
+
'parse', text);
|
|
842
|
+
const opened = entryResult(c.e.datasetOpen(r.nquads, 'nquads', ''), 'parse');
|
|
843
|
+
return c.wrap(opened, r.prefixes, {
|
|
844
|
+
diagnostics: utf16Diagnostics(text, r.diagnostics),
|
|
845
|
+
format: tag, formatInferred: inferred,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return openText(c, text, opts.format, opts.baseIRI, 'parse');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Open `data` as a Dataset: the same as parse() for text; for a Dataset,
|
|
853
|
+
* a clone; for an array, one Dataset of all its members.
|
|
854
|
+
* @param {Dataset|string|Array|{text, format}} data
|
|
855
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
856
|
+
* @returns {Promise<Dataset>}
|
|
857
|
+
*/
|
|
858
|
+
async function openDataset(data, options) {
|
|
859
|
+
const opts = checkOptions('openDataset', optionsOf('openDataset', options), ['format', 'baseIRI']);
|
|
860
|
+
const c = await ctx();
|
|
861
|
+
if (data instanceof Dataset) return data.clone();
|
|
862
|
+
return datasetOf(c, data, opts, 'openDataset').ds;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Run a SPARQL 1.1 (or 1.2) query.
|
|
867
|
+
* @param {Dataset|string|Array} data
|
|
868
|
+
* @param {string} sparql
|
|
869
|
+
* @param {{format?: string, baseIRI?: string,
|
|
870
|
+
* entail?: 'none'|'RDFS'|'OWL-RL'|'x-rdfscore'|'x-rdfsplus',
|
|
871
|
+
* sparql12?: boolean, version?: '1.1'|'1.2',
|
|
872
|
+
* output?: 'json'|'xml'|'csv'|'tsv'|'table'|'ntriples'}} [options]
|
|
873
|
+
* @returns {Promise<Array<Map<string, object>>|boolean|Dataset|string|object>}
|
|
874
|
+
* SELECT -> Bindings[], ASK -> boolean, CONSTRUCT/DESCRIBE -> Dataset;
|
|
875
|
+
* with `output`, the results document (text; 'json' parsed).
|
|
876
|
+
*/
|
|
877
|
+
async function query(data, sparql, options) {
|
|
878
|
+
if (typeof sparql !== 'string') throw new TypeError('query: sparql must be a string');
|
|
879
|
+
const opts = checkOptions('query', optionsOf('query', options),
|
|
880
|
+
['format', 'baseIRI', 'entail', 'sparql12', 'version', 'output']);
|
|
881
|
+
return withDataset(data, opts, 'query', (c, ds) => queryDatasetHandle(c, ds, sparql, opts));
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Run a SPARQL 1.1 query against a read-only HDT file's raw bytes.
|
|
886
|
+
* SELECT and ASK only; default graph only.
|
|
887
|
+
* @param {Uint8Array|ArrayBuffer|Buffer|string} hdtBytes
|
|
888
|
+
* @param {string} sparql
|
|
889
|
+
* @returns {Promise<Array<Map<string, object>>|boolean>}
|
|
890
|
+
*/
|
|
891
|
+
async function queryHdt(hdtBytes, sparql) {
|
|
892
|
+
if (typeof sparql !== 'string') {
|
|
893
|
+
throw new TypeError('queryHdt: sparql must be a string');
|
|
894
|
+
}
|
|
895
|
+
const form = sniffQueryForm(sparql);
|
|
896
|
+
if (form === 'construct' || form === 'describe') {
|
|
897
|
+
throw new TypeError(
|
|
898
|
+
`queryHdt: ${form.toUpperCase()} is not supported over an HDT file (SELECT/ASK only)`);
|
|
899
|
+
}
|
|
900
|
+
const e = await entry();
|
|
901
|
+
requireEntryFn(e, 'queryHdt', 'queryHdt');
|
|
902
|
+
const r = entryResult(e.queryHdt(bytesToUint8Array(hdtBytes, 'queryHdt'), sparql), 'queryHdt');
|
|
903
|
+
if (r.kind === 'ask') return r.boolean;
|
|
904
|
+
return bindingsFromSrj(r.srj);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Apply a SPARQL 1.1 Update and answer a NEW Dataset; a Dataset argument
|
|
909
|
+
* is not changed (use `dataset.update()` to change one in place).
|
|
910
|
+
* @param {Dataset|string|Array} data
|
|
911
|
+
* @param {string} updateText
|
|
912
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
913
|
+
* @returns {Promise<Dataset>}
|
|
914
|
+
*/
|
|
915
|
+
async function update(data, updateText, options) {
|
|
916
|
+
if (typeof updateText !== 'string') throw new TypeError('update: updateText must be a string');
|
|
917
|
+
const opts = checkOptions('update', optionsOf('update', options), ['format', 'baseIRI']);
|
|
918
|
+
const c = await ctx();
|
|
919
|
+
const { ds, temp } = datasetOf(c, data, opts, 'update');
|
|
920
|
+
const out = temp ? ds : ds.clone();
|
|
921
|
+
try {
|
|
922
|
+
return await out.update(updateText);
|
|
923
|
+
} catch (err) {
|
|
924
|
+
out.dispose();
|
|
925
|
+
throw err;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Write a dataset in a concrete syntax.
|
|
931
|
+
* @param {Dataset|string|Array} data
|
|
932
|
+
* @param {{format?: string, version?: '1.1'|'1.2', prefixes?: object,
|
|
933
|
+
* literalShorthand?: boolean, useNativeTypes?: boolean,
|
|
934
|
+
* useRdfType?: boolean, rdfDirection?: string,
|
|
935
|
+
* inputFormat?: string, baseIRI?: string}} [options]
|
|
936
|
+
* format: nquads (default), ntriples, turtle, trig, rdfxml, jsonld,
|
|
937
|
+
* rdfc. inputFormat and baseIRI read text `data`.
|
|
938
|
+
* @returns {Promise<string>}
|
|
939
|
+
*/
|
|
940
|
+
async function serialize(data, options) {
|
|
941
|
+
const opts = checkOptions('serialize', optionsOf('serialize', options),
|
|
942
|
+
['format', 'version', 'prefixes', 'literalShorthand', 'useNativeTypes',
|
|
943
|
+
'useRdfType', 'rdfDirection', 'inputFormat', 'baseIRI']);
|
|
944
|
+
const { inputFormat, baseIRI, ...writeOpts } = opts;
|
|
945
|
+
if (writeOpts.format !== undefined && !WRITER_TAGS[String(writeOpts.format).toLowerCase()]) {
|
|
946
|
+
throw new TypeError(`serialize: unknown format '${writeOpts.format}'. Known formats: ` +
|
|
947
|
+
[...new Set(Object.values(WRITER_TAGS))].join(', '));
|
|
948
|
+
}
|
|
949
|
+
return withDataset(data, { format: inputFormat, baseIRI }, 'serialize',
|
|
950
|
+
(_c, ds) => ds.serialize(writeOpts));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* RDFC-1.0 canonical N-Quads.
|
|
955
|
+
* @param {Dataset|string|Array} data
|
|
956
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
957
|
+
* @returns {Promise<string>}
|
|
958
|
+
*/
|
|
959
|
+
async function canonicalize(data, options) {
|
|
960
|
+
const opts = checkOptions('canonicalize', optionsOf('canonicalize', options), ['format', 'baseIRI']);
|
|
961
|
+
return withDataset(data, opts, 'canonicalize', (_c, ds) => ds.toString('rdfc'));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* The named graphs of a Dataset, each as a Dataset (default graph
|
|
966
|
+
* excluded, first-seen order).
|
|
967
|
+
* @param {Dataset} dataset
|
|
968
|
+
* @returns {Array<[iri: string, graph: Dataset]>}
|
|
969
|
+
*/
|
|
970
|
+
function graphs(dataset) {
|
|
971
|
+
if (!(dataset instanceof Dataset)) throw new TypeError('graphs: expected a Dataset');
|
|
972
|
+
const seen = new Map();
|
|
973
|
+
for (const q of dataset) {
|
|
974
|
+
const g = q.graph;
|
|
975
|
+
if (!g || g.termType === 'DefaultGraph') continue;
|
|
976
|
+
if (!seen.has(g.value)) seen.set(g.value, g);
|
|
977
|
+
}
|
|
978
|
+
const out = [];
|
|
979
|
+
for (const [iri, gTerm] of seen) out.push([iri, dataset.match(null, null, null, gTerm)]);
|
|
980
|
+
return out;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* RDFC-1.0 canonical N-Quads of one graph: every quad's graph name is
|
|
985
|
+
* dropped first. Text, not a digest.
|
|
986
|
+
* @param {Dataset} datasetOrGraph
|
|
987
|
+
* @returns {Promise<string>}
|
|
988
|
+
*/
|
|
989
|
+
async function canonicalHash(datasetOrGraph) {
|
|
990
|
+
if (!(datasetOrGraph instanceof Dataset)) {
|
|
991
|
+
throw new TypeError('canonicalHash: expected a Dataset (e.g. one entry of graphs())');
|
|
992
|
+
}
|
|
993
|
+
const asDefault = datasetOrGraph.toArray()
|
|
994
|
+
.map((q) => quadToNQuads(dataFactory.quad(q.subject, q.predicate, q.object))).join('');
|
|
995
|
+
return canonicalize(asDefault, { format: 'nquads' });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// A wasm module older than this package can lack an op: `e[fnName]` is
|
|
999
|
+
// then undefined. Fail with missingOpError(), which names the op.
|
|
1000
|
+
function requireEntryFn(e, fnName, what) {
|
|
1001
|
+
if (typeof e[fnName] !== 'function') throw missingOpError(what, e, fnName);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* SHACL validation over the default graph of each argument.
|
|
1006
|
+
* @param {Dataset|string|Array} data
|
|
1007
|
+
* @param {Dataset|string|Array} shapes
|
|
1008
|
+
* @param {{format?: string, baseIRI?: string}} [options] read text arguments
|
|
1009
|
+
* @returns {Promise<{conforms: boolean, report: Dataset}>}
|
|
1010
|
+
*/
|
|
1011
|
+
async function shaclValidate(data, shapes, options) {
|
|
1012
|
+
const opts = checkOptions('shaclValidate', optionsOf('shaclValidate', options), ['format', 'baseIRI']);
|
|
1013
|
+
return withDataset(data, opts, 'shaclValidate(data)', (c, ds) => shaclOnHandle(c, ds, shapes, opts));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* ShEx validation of one focus node against one shape (or the schema's
|
|
1018
|
+
* start), over the default graph of `data`. The schema text is ShExJ when
|
|
1019
|
+
* its first non-blank character is '{', ShExC otherwise.
|
|
1020
|
+
* @param {Dataset|string|Array} data
|
|
1021
|
+
* @param {string} schema
|
|
1022
|
+
* @param {string|{termType,value}} focus
|
|
1023
|
+
* @param {string|{termType,value}|null} [shape]
|
|
1024
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1025
|
+
* @returns {Promise<boolean>}
|
|
1026
|
+
*/
|
|
1027
|
+
async function shexValidate(data, schema, focus, shape, options) {
|
|
1028
|
+
const opts = checkOptions('shexValidate', optionsOf('shexValidate', options), ['format', 'baseIRI']);
|
|
1029
|
+
if (typeof schema !== 'string') throw new TypeError('shexValidate: schema must be a string');
|
|
1030
|
+
return withDataset(data, opts, 'shexValidate(data)', (c, ds) => shexOnHandle(c, ds, schema, focus, shape));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Closure of the default graph, as a new Dataset.
|
|
1035
|
+
* @param {Dataset|string|Array} data
|
|
1036
|
+
* @param {'rdfs'|'owl-rl'|'rho-df'|'rdfs-plus'} regime
|
|
1037
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1038
|
+
* @returns {Promise<Dataset>}
|
|
1039
|
+
*/
|
|
1040
|
+
async function closure(data, regime, options) {
|
|
1041
|
+
const opts = checkOptions('closure', optionsOf('closure', options), ['format', 'baseIRI']);
|
|
1042
|
+
return withDataset(data, opts, 'closure', (c, ds) => closureOnHandle(c, ds, regime));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* RDFS or OWL 2 RL closure of the default graph, as a new Dataset.
|
|
1047
|
+
* @param {Dataset|string|Array} data
|
|
1048
|
+
* @param {'RDFS'|'OWL-RL'} mode
|
|
1049
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1050
|
+
* @returns {Promise<Dataset>}
|
|
1051
|
+
*/
|
|
1052
|
+
async function owlClosure(data, mode, options) {
|
|
1053
|
+
const m = String(mode || '').toLowerCase();
|
|
1054
|
+
if (!['rdfs', 'owl-rl', 'owlrl', 'owl_rl'].includes(m)) {
|
|
1055
|
+
throw new TypeError(`owlClosure: mode must be 'RDFS' or 'OWL-RL' (got '${mode}')`);
|
|
1056
|
+
}
|
|
1057
|
+
return closure(data, m, options);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* The core-RDFS (rho-df: subPropertyOf, subClassOf, type, domain, range;
|
|
1062
|
+
* Munoz, Perez and Gutierrez 2009) closure, as a new Dataset.
|
|
1063
|
+
* @param {Dataset|string|Array} data
|
|
1064
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1065
|
+
* @returns {Promise<Dataset>}
|
|
1066
|
+
*/
|
|
1067
|
+
async function coreRdfsClosure(data, options) {
|
|
1068
|
+
return closure(data, 'rho-df', options);
|
|
1069
|
+
}
|
|
1070
|
+
/** Literature-name alias for coreRdfsClosure. */
|
|
1071
|
+
const rhoDfClosure = coreRdfsClosure;
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Is `data` in the core-RDFS fragment the certified closure covers?
|
|
1075
|
+
* @param {Dataset|string|Array} data
|
|
1076
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1077
|
+
* @returns {Promise<{ok: boolean, fragment: boolean}>}
|
|
1078
|
+
*/
|
|
1079
|
+
async function coreRdfsCheck(data, options) {
|
|
1080
|
+
const opts = checkOptions('coreRdfsCheck', optionsOf('coreRdfsCheck', options), ['format', 'baseIRI']);
|
|
1081
|
+
const nq = await nquadsOf(data, opts, 'coreRdfsCheck(data)');
|
|
1082
|
+
const e = await entry();
|
|
1083
|
+
requireEntryFn(e, 'rhoDfFragmentCheck', 'core-RDFS fragment check');
|
|
1084
|
+
return entryResult(e.rhoDfFragmentCheck(nq), 'coreRdfsCheck');
|
|
1085
|
+
}
|
|
1086
|
+
/** Literature-name alias for coreRdfsCheck. */
|
|
1087
|
+
const rhoDfFragmentCheck = coreRdfsCheck;
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* RDFS-Plus closure (RDFS with owl:sameAs, inverseOf, Symmetric,
|
|
1091
|
+
* Transitive, Functional and InverseFunctional properties,
|
|
1092
|
+
* equivalentClass and equivalentProperty), as a new Dataset.
|
|
1093
|
+
* @param {Dataset|string|Array} data
|
|
1094
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1095
|
+
* @returns {Promise<Dataset>}
|
|
1096
|
+
*/
|
|
1097
|
+
async function rdfsPlusClosure(data, options) {
|
|
1098
|
+
return closure(data, 'rdfs-plus', options);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* OWL tableau materialisation over the default graph.
|
|
1103
|
+
* @param {Dataset|string|Array} data
|
|
1104
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1105
|
+
* @returns {Promise<{dataset: Dataset, addedCount: number, budgetHit?: boolean}>}
|
|
1106
|
+
*/
|
|
1107
|
+
async function tableauMaterialise(data, options) {
|
|
1108
|
+
const opts = checkOptions('tableauMaterialise', optionsOf('tableauMaterialise', options), ['format', 'baseIRI']);
|
|
1109
|
+
const nq = await nquadsOf(data, opts, 'tableauMaterialise(data)');
|
|
1110
|
+
const c = await ctx();
|
|
1111
|
+
requireEntryFn(c.e, 'tableauMaterialise', 'OWL tableau materialisation');
|
|
1112
|
+
const r = entryResult(c.e.tableauMaterialise(nq), 'tableauMaterialise');
|
|
1113
|
+
return {
|
|
1114
|
+
dataset: c.fromNQuads(r.nquads, {}),
|
|
1115
|
+
addedCount: r.addedCount,
|
|
1116
|
+
...(r.budgetHit !== undefined ? { budgetHit: r.budgetHit } : {}),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* OWL DL inconsistency verdict (RL closure, one class-expression pass,
|
|
1122
|
+
* RL closure, clash decision); `rlAlone` is the plain OWL-RL verdict.
|
|
1123
|
+
* @param {Dataset|string|Array} data
|
|
1124
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1125
|
+
* @returns {Promise<{inconsistent: boolean, rlAlone: boolean}>}
|
|
1126
|
+
*/
|
|
1127
|
+
async function tableauDlInconsistent(data, options) {
|
|
1128
|
+
const opts = checkOptions('tableauDlInconsistent', optionsOf('tableauDlInconsistent', options), ['format', 'baseIRI']);
|
|
1129
|
+
const nq = await nquadsOf(data, opts, 'tableauDlInconsistent(data)');
|
|
1130
|
+
const e = await entry();
|
|
1131
|
+
requireEntryFn(e, 'tableauDlInconsistent', 'OWL tableau DL inconsistency check');
|
|
1132
|
+
const r = entryResult(e.tableauDlInconsistent(nq), 'tableauDlInconsistent');
|
|
1133
|
+
return { inconsistent: r.inconsistent, rlAlone: r.rlAlone };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* OWL DL consistency: `consistent` is true, false, or null (the refuter's
|
|
1138
|
+
* budget ran out; `reason` names the cap).
|
|
1139
|
+
* @param {Dataset|string|Array} data
|
|
1140
|
+
* @param {{format?: string, baseIRI?: string, fuel?: number|string}} [options]
|
|
1141
|
+
* @returns {Promise<{consistent: boolean|null, reason?: string}>}
|
|
1142
|
+
*/
|
|
1143
|
+
async function owlIsConsistent(data, options) {
|
|
1144
|
+
const opts = checkOptions('owlIsConsistent', optionsOf('owlIsConsistent', options), ['format', 'baseIRI', 'fuel']);
|
|
1145
|
+
const nq = await nquadsOf(data, opts, 'owlIsConsistent(data)');
|
|
1146
|
+
const e = await entry();
|
|
1147
|
+
requireEntryFn(e, 'owlIsConsistent', 'OWL DL consistency check');
|
|
1148
|
+
const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
|
|
1149
|
+
const r = entryResult(e.owlIsConsistent(nq, optsJson), 'owlIsConsistent');
|
|
1150
|
+
return r.reason === undefined
|
|
1151
|
+
? { consistent: r.consistent }
|
|
1152
|
+
: { consistent: r.consistent, reason: r.reason };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Does `premise` entail `conclusion`? `entailed` is true, false, or null
|
|
1157
|
+
* (a refutation goal ran out of budget; `reason` names the cap).
|
|
1158
|
+
* @param {Dataset|string|Array} premise
|
|
1159
|
+
* @param {Dataset|string|Array} conclusion
|
|
1160
|
+
* @param {{format?: string, baseIRI?: string, fuel?: number|string}} [options]
|
|
1161
|
+
* @returns {Promise<{entailed: boolean|null, via: 'closure'|'refutation', reason?: string}>}
|
|
1162
|
+
*/
|
|
1163
|
+
async function owlEntails(premise, conclusion, options) {
|
|
1164
|
+
const opts = checkOptions('owlEntails', optionsOf('owlEntails', options), ['format', 'baseIRI', 'fuel']);
|
|
1165
|
+
const premiseNq = await nquadsOf(premise, opts, 'owlEntails(premise)');
|
|
1166
|
+
const conclusionNq = await nquadsOf(conclusion, opts, 'owlEntails(conclusion)');
|
|
1167
|
+
const e = await entry();
|
|
1168
|
+
requireEntryFn(e, 'owlEntails', 'OWL entailment check');
|
|
1169
|
+
const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
|
|
1170
|
+
const r = entryResult(e.owlEntails(premiseNq, conclusionNq, optsJson), 'owlEntails');
|
|
1171
|
+
return r.reason === undefined
|
|
1172
|
+
? { entailed: r.entailed, via: r.via }
|
|
1173
|
+
: { entailed: r.entailed, via: r.via, reason: r.reason };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Evaluate an RML mapping graph over one logical source's raw text.
|
|
1178
|
+
* Every triples map reads the same `sourceData`.
|
|
1179
|
+
* @param {Dataset|string|Array} mapping the mapping graph
|
|
1180
|
+
* @param {string} sourceData raw JSON or CSV text
|
|
1181
|
+
* @param {'json'|'csv'} sourceKind
|
|
1182
|
+
* @param {{format?: string, baseIRI?: string}} [options] read `mapping` text
|
|
1183
|
+
* @returns {Promise<Dataset>}
|
|
1184
|
+
*/
|
|
1185
|
+
async function rmlMap(mapping, sourceData, sourceKind, options) {
|
|
1186
|
+
if (typeof sourceData !== 'string') throw new TypeError('rmlMap: sourceData must be a string');
|
|
1187
|
+
const kind = String(sourceKind || '').toLowerCase();
|
|
1188
|
+
if (kind !== 'json' && kind !== 'csv') throw new TypeError("rmlMap: sourceKind must be 'json' or 'csv'");
|
|
1189
|
+
const opts = checkOptions('rmlMap', optionsOf('rmlMap', options), ['format', 'baseIRI']);
|
|
1190
|
+
return withDataset(mapping, opts, 'rmlMap(mapping)', (c, ds) => {
|
|
1191
|
+
requireEntryFn(c.e, 'datasetFromRml', 'RML mapping evaluation');
|
|
1192
|
+
return c.wrap(entryResult(c.e.datasetFromRml(ds.handle, sourceData, kind), 'rmlMap'), {});
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* CSVW csv2rdf (https://www.w3.org/TR/csv2rdf/): tabular data plus an
|
|
1198
|
+
* optional metadata document to a Dataset. Every table of a group reads
|
|
1199
|
+
* the same `csvText`; a `tableSchema` URL is not fetched.
|
|
1200
|
+
* @param {string} csvText
|
|
1201
|
+
* @param {string} [metadataJson] '' or omitted: the header row describes the table
|
|
1202
|
+
* @param {{mode?: 'standard'|'minimal', base?: string, url?: string}} [options]
|
|
1203
|
+
* @returns {Promise<Dataset>}
|
|
1204
|
+
*/
|
|
1205
|
+
async function csvwToRdf(csvText, metadataJson, options) {
|
|
1206
|
+
if (typeof csvText !== 'string') throw new TypeError('csvwToRdf: csvText must be a string');
|
|
1207
|
+
const meta = metadataJson == null ? '' : metadataJson;
|
|
1208
|
+
if (typeof meta !== 'string') throw new TypeError('csvwToRdf: metadataJson must be a string');
|
|
1209
|
+
const opts = checkOptions('csvwToRdf', options, ['mode', 'base', 'url']);
|
|
1210
|
+
const c = await ctx();
|
|
1211
|
+
requireEntryFn(c.e, 'datasetFromCsvw', 'CSVW csv2rdf conversion');
|
|
1212
|
+
const optionsJson = JSON.stringify({
|
|
1213
|
+
...(opts.mode ? { mode: String(opts.mode).toLowerCase() } : {}),
|
|
1214
|
+
...(opts.base ? { base: opts.base } : {}),
|
|
1215
|
+
...(opts.url ? { url: opts.url } : {}),
|
|
1216
|
+
});
|
|
1217
|
+
return c.wrap(entryResult(c.e.datasetFromCsvw(csvText, meta, optionsJson), 'csvwToRdf'), {});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* JSON-LD 1.1 to RDF with the JSON-LD options parse() has no room for.
|
|
1222
|
+
* A remote `@context` is not fetched.
|
|
1223
|
+
* @param {string} jsonldText
|
|
1224
|
+
* @param {{base?: string, rdfDirection?: string, expandContext?: string,
|
|
1225
|
+
* processingMode?: string}} [options]
|
|
1226
|
+
* @returns {Promise<Dataset>}
|
|
1227
|
+
*/
|
|
1228
|
+
async function jsonldToRdf(jsonldText, options) {
|
|
1229
|
+
if (typeof jsonldText !== 'string') throw new TypeError('jsonldToRdf: jsonldText must be a string');
|
|
1230
|
+
const opts = checkOptions('jsonldToRdf', options, ['base', 'rdfDirection', 'expandContext', 'processingMode']);
|
|
1231
|
+
const c = await ctx();
|
|
1232
|
+
requireEntryFn(c.e, 'datasetFromJsonLd', 'jsonldToRdf');
|
|
1233
|
+
const optionsJson = JSON.stringify({
|
|
1234
|
+
...(opts.base ? { base: opts.base } : {}),
|
|
1235
|
+
...(opts.rdfDirection ? { rdfDirection: opts.rdfDirection } : {}),
|
|
1236
|
+
...(opts.expandContext ? { expandContext: opts.expandContext } : {}),
|
|
1237
|
+
...(opts.processingMode ? { processingMode: opts.processingMode } : {}),
|
|
1238
|
+
});
|
|
1239
|
+
return c.wrap(entryResult(c.e.datasetFromJsonLd(jsonldText, optionsJson), 'jsonldToRdf'), {},
|
|
1240
|
+
{ format: 'jsonld', formatInferred: false });
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* RIF Core forward-chaining saturation of the default graph, as a new
|
|
1245
|
+
* Dataset (input and derived triples).
|
|
1246
|
+
* @param {Dataset|string|Array} data
|
|
1247
|
+
* @param {string} rifRulesXml
|
|
1248
|
+
* @param {{format?: string, baseIRI?: string}} [options]
|
|
1249
|
+
* @returns {Promise<Dataset>}
|
|
1250
|
+
*/
|
|
1251
|
+
async function rifEval(data, rifRulesXml, options) {
|
|
1252
|
+
if (typeof rifRulesXml !== 'string') throw new TypeError('rifEval: rifRulesXml must be a string');
|
|
1253
|
+
const opts = checkOptions('rifEval', optionsOf('rifEval', options), ['format', 'baseIRI']);
|
|
1254
|
+
return withDataset(data, opts, 'rifEval(data)', (c, ds) => {
|
|
1255
|
+
requireEntryFn(c.e, 'datasetRifEval', 'RIF Core evaluation');
|
|
1256
|
+
return c.wrap(entryResult(c.e.datasetRifEval(rifRulesXml, ds.handle), 'rifEval'), ds.prefixes);
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Expanded JSON-LD of a dataset (JSON-LD 1.1 API section 8), parsed.
|
|
1262
|
+
* The text form is `serialize(data, {format: 'jsonld'})`.
|
|
1263
|
+
* @param {Dataset|string|Array} data
|
|
1264
|
+
* @param {{useNativeTypes?: boolean, useRdfType?: boolean,
|
|
1265
|
+
* rdfDirection?: string, format?: string, baseIRI?: string}} [options]
|
|
1266
|
+
* @returns {Promise<any>}
|
|
1267
|
+
*/
|
|
1268
|
+
async function jsonldFromRdf(data, options) {
|
|
1269
|
+
const opts = checkOptions('jsonldFromRdf', optionsOf('jsonldFromRdf', options),
|
|
1270
|
+
['useNativeTypes', 'useRdfType', 'rdfDirection', 'format', 'baseIRI']);
|
|
1271
|
+
const { format, baseIRI, ...w } = opts;
|
|
1272
|
+
return withDataset(data, { format, baseIRI }, 'jsonldFromRdf(data)',
|
|
1273
|
+
(_c, ds) => JSON.parse(ds.toString({ format: 'jsonld', ...w })));
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* The syntaxes the engine reads and writes (op datasetFormats).
|
|
1278
|
+
* @returns {Promise<Array<{name: string, read11: boolean, read12: boolean,
|
|
1279
|
+
* write11: boolean, write12: boolean, engine: 'core'|'full'}>>}
|
|
1280
|
+
*/
|
|
1281
|
+
async function formats() {
|
|
1282
|
+
const e = await entry();
|
|
1283
|
+
requireEntryFn(e, 'datasetFormats', 'formats');
|
|
1284
|
+
return entryResult(e.datasetFormats(), 'formats').formats;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* did:key resolution (entry_jsoo.ml's didKeyResolve export -> the
|
|
1289
|
+
* verified DID_Key.did_key_document). Resolves a did:key:z6Mk...
|
|
1290
|
+
* (Ed25519) to its DID Document, returned as a Dataset.
|
|
1291
|
+
* @param {string} didString a did:key URI
|
|
1292
|
+
* @returns {Promise<Dataset>} the DID Document as RDF
|
|
1293
|
+
*/
|
|
1294
|
+
async function didKeyResolve(didString) {
|
|
1295
|
+
if (typeof didString !== 'string') {
|
|
1296
|
+
throw new TypeError('didKeyResolve: didString must be a string');
|
|
1297
|
+
}
|
|
1298
|
+
const e = await entry();
|
|
1299
|
+
requireEntryFn(e, 'didKeyResolve', 'did:key resolution');
|
|
1300
|
+
const r = entryResult(e.didKeyResolve(didString), 'didKeyResolve');
|
|
1301
|
+
return (await ctx()).fromNQuads(r.nquads, {});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/**
|
|
1305
|
+
* Test whether an XML document is well-formed (entry_jsoo.ml's
|
|
1306
|
+
* xmlWellformed export -> Parser_XML.parse_xml_document, the
|
|
1307
|
+
* accept/reject signal bin/xml-runner drives against W3C xmlconf).
|
|
1308
|
+
* A DOCTYPE with an internal subset is read (XML 1.0 section 2.8);
|
|
1309
|
+
* an external DTD is not fetched.
|
|
1310
|
+
* @param {string} xmlText
|
|
1311
|
+
* @returns {Promise<boolean>}
|
|
1312
|
+
*/
|
|
1313
|
+
async function xmlWellformed(xmlText) {
|
|
1314
|
+
if (typeof xmlText !== 'string') {
|
|
1315
|
+
throw new TypeError('xmlWellformed: xmlText must be a string');
|
|
1316
|
+
}
|
|
1317
|
+
const e = await entry();
|
|
1318
|
+
requireEntryFn(e, 'xmlWellformed', 'XML well-formedness');
|
|
1319
|
+
const r = entryResult(e.xmlWellformed(xmlText), 'xmlWellformed');
|
|
1320
|
+
return !!r.wellformed;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Evaluate an XPath 1.0 expression over an XML document (entry_jsoo.ml's
|
|
1325
|
+
* xpathEval export -> XPath_Eval.eval_xpath_from_root). Returns the
|
|
1326
|
+
* result envelope: `resultType` ('nodeset'|'string'|'number'|'boolean')
|
|
1327
|
+
* plus, for a node-set, `count`/`stringValue`/`nodes`, else a scalar
|
|
1328
|
+
* `value`.
|
|
1329
|
+
* @param {string} xmlText
|
|
1330
|
+
* @param {string} xpathExpr
|
|
1331
|
+
* @returns {Promise<object>}
|
|
1332
|
+
*/
|
|
1333
|
+
async function xpathEval(xmlText, xpathExpr) {
|
|
1334
|
+
if (typeof xmlText !== 'string' || typeof xpathExpr !== 'string') {
|
|
1335
|
+
throw new TypeError('xpathEval: xmlText and xpathExpr must be strings');
|
|
1336
|
+
}
|
|
1337
|
+
const e = await entry();
|
|
1338
|
+
requireEntryFn(e, 'xpathEval', 'XPath evaluation');
|
|
1339
|
+
return entryResult(e.xpathEval(xmlText, xpathExpr), 'xpathEval');
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Parse Common Logic Interchange Format text (ISO/IEC 24707:2018),
|
|
1344
|
+
* with the IKL `that`-operator extension (entry_jsoo.ml's clParse
|
|
1345
|
+
* export -> L4Factoidal's CL/Clif.lean reader). Reads CLIF into a CL
|
|
1346
|
+
* syntax tree and reports its shape; it never produces RDF -- the
|
|
1347
|
+
* IKL-to-RDF projection that used to accompany it is deleted
|
|
1348
|
+
* (danbri/factoidal#626). Lean 4 only: formal/fstar has no CL/IKL parser, so this
|
|
1349
|
+
* function is absent from index.js/wasm.js -- see capabilities() /
|
|
1350
|
+
* factoidal/select's capability table.
|
|
1351
|
+
* @param {string} clifText
|
|
1352
|
+
* @returns {Promise<{ok: boolean, sentences: number, pureCL: boolean,
|
|
1353
|
+
* normalized: string}>} `pureCL` is a DIALECT flag, not a validity
|
|
1354
|
+
* or quality signal: true while the text stays inside ISO/IEC
|
|
1355
|
+
* 24707 Common Logic, false once it uses IKL's `that` operator.
|
|
1356
|
+
* Both values are returned only for text that parsed; a CLIF text
|
|
1357
|
+
* that fails to parse rejects instead (e.g. a bare `(that S)` used
|
|
1358
|
+
* as a proposition rather than a term -- see the GUIDE).
|
|
1359
|
+
*/
|
|
1360
|
+
async function clParse(clifText) {
|
|
1361
|
+
if (typeof clifText !== 'string') {
|
|
1362
|
+
throw new TypeError('clParse: clifText must be a string');
|
|
1363
|
+
}
|
|
1364
|
+
const e = await entry();
|
|
1365
|
+
requireEntryFn(e, 'clParse', 'Common Logic / IKL parse');
|
|
1366
|
+
return entryResult(e.clParse(clifText), 'clParse');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Read Common Logic Interchange Format text and write it back out in
|
|
1371
|
+
* the canonical spacing of the CLIF writer (entry_jsoo.ml's
|
|
1372
|
+
* clSerialize export -> L4Factoidal's CL/Clif.lean reader/writer
|
|
1373
|
+
* pair). Lean 4 only: formal/fstar has no CL/IKL parser, so this
|
|
1374
|
+
* function is absent from index.js/wasm.js -- see capabilities() /
|
|
1375
|
+
* factoidal/select's capability table.
|
|
1376
|
+
*
|
|
1377
|
+
* `roundTripProved` is always `false`. The round-trip lemma
|
|
1378
|
+
* `clif_roundTrip` (`CL/ClifAdequacy.lean`) is an OPEN lemma: the
|
|
1379
|
+
* fragment boundary `marksLexable` is MEASURED, not proved. The
|
|
1380
|
+
* field is in the envelope, unmodified, so a caller does not have to
|
|
1381
|
+
* go and find that out.
|
|
1382
|
+
* @param {string} clifText
|
|
1383
|
+
* @returns {Promise<{ok: boolean, clif: string, sentences: number,
|
|
1384
|
+
* roundTripProved: false}>}
|
|
1385
|
+
*/
|
|
1386
|
+
async function clSerialize(clifText) {
|
|
1387
|
+
if (typeof clifText !== 'string') {
|
|
1388
|
+
throw new TypeError('clSerialize: clifText must be a string');
|
|
1389
|
+
}
|
|
1390
|
+
const e = await entry();
|
|
1391
|
+
requireEntryFn(e, 'clSerialize', 'Common Logic / IKL serialize');
|
|
1392
|
+
return entryResult(e.clSerialize(clifText), 'clSerialize');
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Alpha-normalise Common Logic Interchange Format text: the canonical
|
|
1397
|
+
* representative of each sentence's bound-variable-renaming
|
|
1398
|
+
* equivalence class (entry_jsoo.ml's clAlphaNorm export ->
|
|
1399
|
+
* L4Factoidal's `CL/Alpha.lean`, `Sentence.alphaNorm`). Bound names
|
|
1400
|
+
* become `v1`, `v2`, ... in traversal order, so two sentences that
|
|
1401
|
+
* differ only in bound-variable names produce byte-identical output
|
|
1402
|
+
* -- IKL GUIDE Appendix B condition (1): renaming a bound variable
|
|
1403
|
+
* does not change the proposition expressed. Lean 4 only: formal/fstar
|
|
1404
|
+
* has no CL/IKL parser, so this function is absent from
|
|
1405
|
+
* index.js/wasm.js -- see capabilities() / factoidal/select's
|
|
1406
|
+
* capability table.
|
|
1407
|
+
* @param {string} clifText
|
|
1408
|
+
* @returns {Promise<{ok: boolean, clif: string, sentences: number}>}
|
|
1409
|
+
*/
|
|
1410
|
+
async function clAlphaNorm(clifText) {
|
|
1411
|
+
if (typeof clifText !== 'string') {
|
|
1412
|
+
throw new TypeError('clAlphaNorm: clifText must be a string');
|
|
1413
|
+
}
|
|
1414
|
+
const e = await entry();
|
|
1415
|
+
requireEntryFn(e, 'clAlphaNorm', 'Common Logic / IKL alpha-normalise');
|
|
1416
|
+
return entryResult(e.clAlphaNorm(clifText), 'clAlphaNorm');
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Hayes's satisfiability-preserving reduction of IKL to Common Logic
|
|
1421
|
+
* (entry_jsoo.ml's clNormalize export -> L4Factoidal's
|
|
1422
|
+
* `CL/Normalize.lean`, `normalizeText`; danbri/factoidal#625), over a
|
|
1423
|
+
* whole text: one head text and one shared tail, with the
|
|
1424
|
+
* proposition-name counter running across the text. Lean 4 only:
|
|
1425
|
+
* formal/fstar has no CL/IKL parser, so this function is absent from
|
|
1426
|
+
* index.js/wasm.js -- see capabilities() / factoidal/select's
|
|
1427
|
+
* capability table.
|
|
1428
|
+
*
|
|
1429
|
+
* Two limits, both real, both in the answer, neither hidden:
|
|
1430
|
+
* - `preserves: "satisfiability"` -- the reduction preserves
|
|
1431
|
+
* satisfiability, NOT equivalence. It suits entailment and
|
|
1432
|
+
* consistency testing; it is not a transformation to apply to
|
|
1433
|
+
* data you intend to keep.
|
|
1434
|
+
* - `noIntrusion` IS the proof hypothesis `CL.noIntrSs [] []`
|
|
1435
|
+
* decides, not a paraphrase of it. The transformation runs either
|
|
1436
|
+
* way; when `noIntrusion` is `false`, the output is still
|
|
1437
|
+
* produced, but `tails_satisfiable` / `normalize_preserves` do
|
|
1438
|
+
* not cover that case.
|
|
1439
|
+
* @param {string} clifText
|
|
1440
|
+
* @returns {Promise<{ok: boolean, head: string[], tail: string[],
|
|
1441
|
+
* clif: string, sentences: number, thatCount: number,
|
|
1442
|
+
* noIntrusion: boolean, preserves: 'satisfiability',
|
|
1443
|
+
* provedUnder: string}>}
|
|
1444
|
+
*/
|
|
1445
|
+
async function clNormalize(clifText) {
|
|
1446
|
+
if (typeof clifText !== 'string') {
|
|
1447
|
+
throw new TypeError('clNormalize: clifText must be a string');
|
|
1448
|
+
}
|
|
1449
|
+
const e = await entry();
|
|
1450
|
+
requireEntryFn(e, 'clNormalize', 'Common Logic / IKL normalize');
|
|
1451
|
+
return entryResult(e.clNormalize(clifText), 'clNormalize');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// clFiniteSat (entry_jsoo.ml's clFiniteSat -> L4Factoidal's
|
|
1455
|
+
// CL/FiniteSatTheorems.lean) is DEFERRED, not excluded, from this
|
|
1456
|
+
// typed layer (owner decision, 2026-08-26): it takes a caller-supplied
|
|
1457
|
+
// finite-interpretation JSON encoding (see Wasm/Ops/CL.lean's header
|
|
1458
|
+
// for the wire format) that has no user yet, and a typed wrapper here
|
|
1459
|
+
// would freeze that shape before anyone knows whether it is right. It
|
|
1460
|
+
// stays reachable through the raw dispatch ABI (`l4.call('clFiniteSat',
|
|
1461
|
+
// [interpJson, clifText])` / factoidal/select's `call('clFiniteSat',
|
|
1462
|
+
// ...)`), which needs no shape commitment on this layer.
|
|
1463
|
+
|
|
1464
|
+
// -----------------------------------------------------------------
|
|
1465
|
+
// VC Data Integrity crypto (eddsa-rdfc-2022), ops of
|
|
1466
|
+
// Wasm/Ops/Vc.lean. Ed25519 is the HACL* C code compiled into the wasm
|
|
1467
|
+
// module (L4Factoidal/Crypto/Ed25519.lean's @[extern] binding);
|
|
1468
|
+
// SHA-256 is the Lean hash. There is no separate crypto module and no
|
|
1469
|
+
// initialisation step.
|
|
1470
|
+
// -----------------------------------------------------------------
|
|
1471
|
+
|
|
1472
|
+
async function vcEntry(fnName, what) {
|
|
1473
|
+
const e = await entry();
|
|
1474
|
+
requireEntryFn(e, fnName, what);
|
|
1475
|
+
return e;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* SHA-256 of a message string's bytes, as a lowercase hex digest
|
|
1480
|
+
* (entry_jsoo.ml's vcSha256Hex -> VC_DataIntegrity.hash_sha256_hex,
|
|
1481
|
+
* the Lean SHA-256).
|
|
1482
|
+
* @param {string} message the message whose bytes are hashed
|
|
1483
|
+
* @returns {Promise<string>} 64-char hex digest
|
|
1484
|
+
*/
|
|
1485
|
+
async function vcSha256Hex(message) {
|
|
1486
|
+
if (typeof message !== 'string') {
|
|
1487
|
+
throw new TypeError('vcSha256Hex: message must be a string');
|
|
1488
|
+
}
|
|
1489
|
+
const e = await vcEntry('vcSha256Hex', 'VC SHA-256');
|
|
1490
|
+
return entryResult(e.vcSha256Hex(message), 'vcSha256Hex').sha256;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Derive the Ed25519 public key from a 32-byte secret key
|
|
1495
|
+
* (entry_jsoo.ml's vcEd25519SecretToPublic -> HACL* Ed25519).
|
|
1496
|
+
* @param {string} secretKeyHex 32-byte secret key, hex
|
|
1497
|
+
* @returns {Promise<string>} 32-byte public key, hex
|
|
1498
|
+
*/
|
|
1499
|
+
async function vcEd25519SecretToPublic(secretKeyHex) {
|
|
1500
|
+
if (typeof secretKeyHex !== 'string') {
|
|
1501
|
+
throw new TypeError('vcEd25519SecretToPublic: secretKeyHex must be a string');
|
|
1502
|
+
}
|
|
1503
|
+
const e = await vcEntry('vcEd25519SecretToPublic', 'VC Ed25519 key derivation');
|
|
1504
|
+
return entryResult(
|
|
1505
|
+
e.vcEd25519SecretToPublic(secretKeyHex), 'vcEd25519SecretToPublic').publicKeyHex;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Ed25519 signature over a hex-encoded message (entry_jsoo.ml's
|
|
1510
|
+
* vcEd25519Sign -> HACL* Ed25519).
|
|
1511
|
+
* @param {string} secretKeyHex 32-byte secret key, hex
|
|
1512
|
+
* @param {string} messageHex the message to sign, hex
|
|
1513
|
+
* @returns {Promise<string>} 64-byte signature, hex
|
|
1514
|
+
*/
|
|
1515
|
+
async function vcEd25519Sign(secretKeyHex, messageHex) {
|
|
1516
|
+
if (typeof secretKeyHex !== 'string' || typeof messageHex !== 'string') {
|
|
1517
|
+
throw new TypeError('vcEd25519Sign: secretKeyHex and messageHex must be strings');
|
|
1518
|
+
}
|
|
1519
|
+
const e = await vcEntry('vcEd25519Sign', 'VC Ed25519 sign');
|
|
1520
|
+
return entryResult(e.vcEd25519Sign(secretKeyHex, messageHex), 'vcEd25519Sign').signatureHex;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Ed25519 verification (entry_jsoo.ml's vcEd25519Verify -> HACL*
|
|
1525
|
+
* Ed25519). A wrong key, tampered signature, altered message, or a
|
|
1526
|
+
* malformed-length input all return false — never an
|
|
1527
|
+
* exception-hidden true.
|
|
1528
|
+
* @param {string} publicKeyHex 32-byte public key, hex
|
|
1529
|
+
* @param {string} messageHex the message, hex
|
|
1530
|
+
* @param {string} signatureHex the 64-byte signature, hex
|
|
1531
|
+
* @returns {Promise<boolean>}
|
|
1532
|
+
*/
|
|
1533
|
+
async function vcEd25519Verify(publicKeyHex, messageHex, signatureHex) {
|
|
1534
|
+
if (typeof publicKeyHex !== 'string' || typeof messageHex !== 'string' ||
|
|
1535
|
+
typeof signatureHex !== 'string') {
|
|
1536
|
+
throw new TypeError(
|
|
1537
|
+
'vcEd25519Verify: publicKeyHex, messageHex and signatureHex must be strings');
|
|
1538
|
+
}
|
|
1539
|
+
const e = await vcEntry('vcEd25519Verify', 'VC Ed25519 verify');
|
|
1540
|
+
return !!entryResult(
|
|
1541
|
+
e.vcEd25519Verify(publicKeyHex, messageHex, signatureHex), 'vcEd25519Verify').valid;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* Create an eddsa-rdfc-2022 Data Integrity proofValue over an
|
|
1546
|
+
* already-canonicalized document + proof config (entry_jsoo.ml's
|
|
1547
|
+
* vcEddsaCreateFromCanonical -> VC_DataIntegrity.eddsa_rdfc_2022_
|
|
1548
|
+
* create_from_canonical). The two canonical inputs are RDFC-1.0
|
|
1549
|
+
* canonical N-Quads (see canonicalize()).
|
|
1550
|
+
* @param {string} secretKeyHex 32-byte secret key, hex
|
|
1551
|
+
* @param {string} canonicalDocument canonical N-Quads of the document
|
|
1552
|
+
* @param {string} canonicalConfig canonical N-Quads of the proof config
|
|
1553
|
+
* @returns {Promise<string>} the multibase-z (base58btc) proofValue
|
|
1554
|
+
*/
|
|
1555
|
+
async function vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig) {
|
|
1556
|
+
if (typeof secretKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
|
|
1557
|
+
typeof canonicalConfig !== 'string') {
|
|
1558
|
+
throw new TypeError(
|
|
1559
|
+
'vcEddsaCreateFromCanonical: secretKeyHex, canonicalDocument and canonicalConfig must be strings');
|
|
1560
|
+
}
|
|
1561
|
+
const e = await vcEntry('vcEddsaCreateFromCanonical', 'VC eddsa-rdfc-2022 proof creation');
|
|
1562
|
+
return entryResult(
|
|
1563
|
+
e.vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig),
|
|
1564
|
+
'vcEddsaCreateFromCanonical').proofValue;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/**
|
|
1568
|
+
* Verify an eddsa-rdfc-2022 proofValue against canonical inputs
|
|
1569
|
+
* (entry_jsoo.ml's vcEddsaVerifyFromCanonical ->
|
|
1570
|
+
* VC_DataIntegrity.eddsa_rdfc_2022_verify_from_canonical). Wrong
|
|
1571
|
+
* key, tampered document/config, or tampered proofValue all return
|
|
1572
|
+
* false.
|
|
1573
|
+
* @param {string} publicKeyHex 32-byte public key, hex
|
|
1574
|
+
* @param {string} canonicalDocument canonical N-Quads of the document
|
|
1575
|
+
* @param {string} canonicalConfig canonical N-Quads of the proof config
|
|
1576
|
+
* @param {string} proofValue the multibase-z proofValue to check
|
|
1577
|
+
* @returns {Promise<boolean>}
|
|
1578
|
+
*/
|
|
1579
|
+
async function vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue) {
|
|
1580
|
+
if (typeof publicKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
|
|
1581
|
+
typeof canonicalConfig !== 'string' || typeof proofValue !== 'string') {
|
|
1582
|
+
throw new TypeError(
|
|
1583
|
+
'vcEddsaVerifyFromCanonical: publicKeyHex, canonicalDocument, canonicalConfig and proofValue must be strings');
|
|
1584
|
+
}
|
|
1585
|
+
const e = await vcEntry('vcEddsaVerifyFromCanonical', 'VC eddsa-rdfc-2022 proof verification');
|
|
1586
|
+
return !!entryResult(
|
|
1587
|
+
e.vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue),
|
|
1588
|
+
'vcEddsaVerifyFromCanonical').verified;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/**
|
|
1592
|
+
* VC Data Model 2.0 structural conformance check (entry_jsoo.ml's
|
|
1593
|
+
* vcCheckCredential -> VC_Credential.vc_check_from_string, 117 pass,
|
|
1594
|
+
* 0 fail on the offline vc_stage1 fixture suite). Pure structural
|
|
1595
|
+
* validation, no crypto. `v2ctxJson` is the vendored VCDM v2 base context
|
|
1596
|
+
* document's raw JSON text (third_party/contexts/credentials-v2.jsonld,
|
|
1597
|
+
* `@context` value included — the engine parses the whole document
|
|
1598
|
+
* and only reads its own @context field) and `credentialJson` is the
|
|
1599
|
+
* raw JSON text of the VC/VP document under test.
|
|
1600
|
+
* @param {string} v2ctxJson vendored credentials-v2.jsonld text
|
|
1601
|
+
* @param {string} credentialJson the VC/VP document's raw JSON text
|
|
1602
|
+
* @returns {Promise<{valid: boolean, reason?: string}>}
|
|
1603
|
+
*/
|
|
1604
|
+
async function vcCheckCredential(v2ctxJson, credentialJson) {
|
|
1605
|
+
if (typeof v2ctxJson !== 'string' || typeof credentialJson !== 'string') {
|
|
1606
|
+
throw new TypeError('vcCheckCredential: v2ctxJson and credentialJson must be strings');
|
|
1607
|
+
}
|
|
1608
|
+
const e = await entry();
|
|
1609
|
+
requireEntryFn(e, 'vcCheckCredential', 'VC Data Model 2.0 structural check');
|
|
1610
|
+
const r = entryResult(e.vcCheckCredential(v2ctxJson, credentialJson), 'vcCheckCredential');
|
|
1611
|
+
return r.valid ? { valid: true } : { valid: false, reason: r.reason };
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
/**
|
|
1615
|
+
* credentialSubject presence/shape check, VERSION-AGNOSTIC (Track A1,
|
|
1616
|
+
* docs/designissues/2026-07-11-vc-canivc-eecc-plan.md) —
|
|
1617
|
+
* entry_jsoo.ml's vcCheckCredentialSubject ->
|
|
1618
|
+
* VC_Credential.vc_check_credential_subject_from_string. Unlike
|
|
1619
|
+
* vcCheckCredential above, this does NOT require the VCDM 2.0 base
|
|
1620
|
+
* @context to be first (or present at all) — it only checks that a
|
|
1621
|
+
* credential-shaped document (type includes "VerifiableCredential")
|
|
1622
|
+
* has a present, non-empty credentialSubject. Used for documents
|
|
1623
|
+
* under a non-VCDM-2.0 @context (e.g. the vc-di-eddsa Data Integrity
|
|
1624
|
+
* conformance suite's legacy VC 1.1 fixtures) where the full
|
|
1625
|
+
* vcCheckCredential's @context sentinel would (correctly) reject the
|
|
1626
|
+
* document for an unrelated reason.
|
|
1627
|
+
* @param {string} credentialJson the VC/VP document's raw JSON text
|
|
1628
|
+
* @returns {Promise<{valid: boolean, reason?: string}>}
|
|
1629
|
+
*/
|
|
1630
|
+
async function vcCheckCredentialSubject(credentialJson) {
|
|
1631
|
+
if (typeof credentialJson !== 'string') {
|
|
1632
|
+
throw new TypeError('vcCheckCredentialSubject: credentialJson must be a string');
|
|
1633
|
+
}
|
|
1634
|
+
const e = await entry();
|
|
1635
|
+
requireEntryFn(e, 'vcCheckCredentialSubject', 'credentialSubject presence check');
|
|
1636
|
+
const r = entryResult(e.vcCheckCredentialSubject(credentialJson), 'vcCheckCredentialSubject');
|
|
1637
|
+
return r.valid ? { valid: true } : { valid: false, reason: r.reason };
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* DATA_LOSS_DETECTION_ERROR check (Track A1, same plan doc) —
|
|
1642
|
+
* entry_jsoo.ml's vcCheckNoDataLoss ->
|
|
1643
|
+
* VC_Credential.vc_check_no_data_loss_from_string. Rejects a
|
|
1644
|
+
* credential-shaped document whose `type`/`@type` entries or
|
|
1645
|
+
* `credentialSubject` property keys include one that a lenient
|
|
1646
|
+
* JSON-LD processor would silently drop (VC Data Integrity spec,
|
|
1647
|
+
* "Securing Data Losslessly"). `credentialJson` MUST already have
|
|
1648
|
+
* any remote @context IRI the caller recognizes inlined to the real
|
|
1649
|
+
* context object — this engine build has no remote-context loader
|
|
1650
|
+
* registered, so a still-remote IRI string in "@context" makes
|
|
1651
|
+
* context processing fail with an error; the check is not skipped.
|
|
1652
|
+
* @param {string} credentialJson the VC/VP document's raw JSON text,
|
|
1653
|
+
* @context already inlined
|
|
1654
|
+
* @returns {Promise<{valid: boolean, reason?: string}>}
|
|
1655
|
+
*/
|
|
1656
|
+
async function vcCheckNoDataLoss(credentialJson) {
|
|
1657
|
+
if (typeof credentialJson !== 'string') {
|
|
1658
|
+
throw new TypeError('vcCheckNoDataLoss: credentialJson must be a string');
|
|
1659
|
+
}
|
|
1660
|
+
const e = await entry();
|
|
1661
|
+
requireEntryFn(e, 'vcCheckNoDataLoss', 'DATA_LOSS_DETECTION_ERROR check');
|
|
1662
|
+
const r = entryResult(e.vcCheckNoDataLoss(credentialJson), 'vcCheckNoDataLoss');
|
|
1663
|
+
return r.valid ? { valid: true } : { valid: false, reason: r.reason };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/**
|
|
1667
|
+
* relatedResource digest verification (VCDM 2.0 §5.3, vc20-api Track
|
|
1668
|
+
* A4) — entry_jsoo.ml's vcCheckRelatedResourceDigests ->
|
|
1669
|
+
* VC_Credential.vc_check_related_resource_digests_from_string. The
|
|
1670
|
+
* engine does no I/O, so the caller supplies a known-resource digest
|
|
1671
|
+
* registry: a JSON array of {"id": <resource URL>, "digestsHex":
|
|
1672
|
+
* [<lowercase hex>, ...]} entries computed from the caller's VENDORED
|
|
1673
|
+
* copies of each resource's content bytes. A relatedResource entry
|
|
1674
|
+
* whose id is in the registry and whose declared digestSRI/
|
|
1675
|
+
* digestMultibase matches none of that id's digests is rejected (the
|
|
1676
|
+
* spec's digest-mismatch error); an id absent from the registry, or a
|
|
1677
|
+
* digest algorithm the registry contract doesn't cover (anything
|
|
1678
|
+
* outside sha256/sha384), is unverifiable offline and passes. All
|
|
1679
|
+
* decode/match semantics are L4Factoidal/VC/Credential.lean's.
|
|
1680
|
+
* @param {string} registryJson the digest registry's raw JSON text
|
|
1681
|
+
* @param {string} credentialJson the VC/VP document's raw JSON text
|
|
1682
|
+
* @returns {Promise<{valid: boolean, reason?: string}>}
|
|
1683
|
+
*/
|
|
1684
|
+
async function vcCheckRelatedResourceDigests(registryJson, credentialJson) {
|
|
1685
|
+
if (typeof registryJson !== 'string' || typeof credentialJson !== 'string') {
|
|
1686
|
+
throw new TypeError('vcCheckRelatedResourceDigests: registryJson and credentialJson must be strings');
|
|
1687
|
+
}
|
|
1688
|
+
const e = await entry();
|
|
1689
|
+
requireEntryFn(e, 'vcCheckRelatedResourceDigests', 'relatedResource digest check');
|
|
1690
|
+
const r = entryResult(e.vcCheckRelatedResourceDigests(registryJson, credentialJson), 'vcCheckRelatedResourceDigests');
|
|
1691
|
+
return r.valid ? { valid: true } : { valid: false, reason: r.reason };
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// -----------------------------------------------------------------
|
|
1695
|
+
// Typed "engine" functions (#74 npm FP surface). Each is a pure,
|
|
1696
|
+
// string/JSON-in, JSON-out wrapper over one dispatch op. No logic
|
|
1697
|
+
// lives on the JS side; these bind the op to a typed Promise.
|
|
1698
|
+
// -----------------------------------------------------------------
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* XSLT 1.0 transform (entry_jsoo.ml's xsltTransform export ->
|
|
1702
|
+
* XSLT.Transform.transform). Applies `stylesheetXml` to `sourceXml`
|
|
1703
|
+
* and returns the serialized result tree.
|
|
1704
|
+
* @param {string} stylesheetXml an XSLT stylesheet document
|
|
1705
|
+
* @param {string} sourceXml the source XML document
|
|
1706
|
+
* @returns {Promise<string>} the transform output (serialized XML/text)
|
|
1707
|
+
*/
|
|
1708
|
+
async function xsltTransform(stylesheetXml, sourceXml) {
|
|
1709
|
+
if (typeof stylesheetXml !== 'string' || typeof sourceXml !== 'string') {
|
|
1710
|
+
throw new TypeError('xsltTransform: stylesheetXml and sourceXml must be strings');
|
|
1711
|
+
}
|
|
1712
|
+
const e = await entry();
|
|
1713
|
+
requireEntryFn(e, 'xsltTransform', 'XSLT transform');
|
|
1714
|
+
const r = entryResult(e.xsltTransform(stylesheetXml, sourceXml), 'xsltTransform');
|
|
1715
|
+
return r.output;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Evaluate a Content MathML document (entry_jsoo.ml's mathmlEval
|
|
1720
|
+
* export -> MathML.Content.eval_doc_env). `bindings` maps
|
|
1721
|
+
* ci-variable names to value strings; pass {} for a closed
|
|
1722
|
+
* expression. Returns the exact numeric/boolean value or an
|
|
1723
|
+
* `undef` reason (division-by-zero, type error, ...).
|
|
1724
|
+
* @param {string} contentMathmlXml
|
|
1725
|
+
* @param {Record<string,string>} [bindings]
|
|
1726
|
+
* @returns {Promise<{kind:'rat',num:number,den:number}|{kind:'bool',value:boolean}|{kind:'undef',reason:string}>}
|
|
1727
|
+
*/
|
|
1728
|
+
async function mathmlEval(contentMathmlXml, bindings) {
|
|
1729
|
+
if (typeof contentMathmlXml !== 'string') {
|
|
1730
|
+
throw new TypeError('mathmlEval: contentMathmlXml must be a string');
|
|
1731
|
+
}
|
|
1732
|
+
const b = bindings || {};
|
|
1733
|
+
if (typeof b !== 'object') {
|
|
1734
|
+
throw new TypeError('mathmlEval: bindings must be an object');
|
|
1735
|
+
}
|
|
1736
|
+
const e = await entry();
|
|
1737
|
+
requireEntryFn(e, 'mathmlEval', 'MathML evaluation');
|
|
1738
|
+
const r = entryResult(
|
|
1739
|
+
e.mathmlEval(contentMathmlXml, JSON.stringify(b)), 'mathmlEval');
|
|
1740
|
+
return r.value;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* XForms recalculate (entry_jsoo.ml's xformsRecalc export ->
|
|
1745
|
+
* XForms.Bind.recalculate). Applies the model binds (calculate,
|
|
1746
|
+
* constraint, relevant, required, readonly, type MIPs) to the
|
|
1747
|
+
* instance and returns the recomputed instance plus a validity
|
|
1748
|
+
* report per bound node.
|
|
1749
|
+
* @param {string} instanceXml the XForms instance document
|
|
1750
|
+
* @param {Array<{id?:string,target:string,calculate?:string,constraint?:string,relevant?:string,required?:string,readonly?:string,type?:string}>} binds
|
|
1751
|
+
* @returns {Promise<{instance:string,validity:Array<object>}>}
|
|
1752
|
+
*/
|
|
1753
|
+
async function xformsRecalc(instanceXml, binds) {
|
|
1754
|
+
if (typeof instanceXml !== 'string') {
|
|
1755
|
+
throw new TypeError('xformsRecalc: instanceXml must be a string');
|
|
1756
|
+
}
|
|
1757
|
+
if (!Array.isArray(binds)) {
|
|
1758
|
+
throw new TypeError('xformsRecalc: binds must be an array');
|
|
1759
|
+
}
|
|
1760
|
+
const e = await entry();
|
|
1761
|
+
requireEntryFn(e, 'xformsRecalc', 'XForms recalculate');
|
|
1762
|
+
const r = entryResult(
|
|
1763
|
+
e.xformsRecalc(instanceXml, JSON.stringify(binds)), 'xformsRecalc');
|
|
1764
|
+
return { instance: r.instance, validity: r.validity };
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* JSON Schema (draft-07) validation (entry_jsoo.ml's
|
|
1769
|
+
* jsonSchemaValidate export -> JSONSchema.Validate.validate).
|
|
1770
|
+
* Returns the verdict — the verified validator gives a definite
|
|
1771
|
+
* pass/fail/unsupported, not a per-keyword error list, so `errors`
|
|
1772
|
+
* carries a single reason string when not a definite pass.
|
|
1773
|
+
* @param {string} schemaJson the schema document (JSON text)
|
|
1774
|
+
* @param {string} instanceJson the instance document (JSON text)
|
|
1775
|
+
* @returns {Promise<{valid:boolean,result:'pass'|'fail'|'unsupported',errors:string[]}>}
|
|
1776
|
+
*/
|
|
1777
|
+
async function jsonSchemaValidate(schemaJson, instanceJson) {
|
|
1778
|
+
if (typeof schemaJson !== 'string' || typeof instanceJson !== 'string') {
|
|
1779
|
+
throw new TypeError('jsonSchemaValidate: schemaJson and instanceJson must be strings');
|
|
1780
|
+
}
|
|
1781
|
+
const e = await entry();
|
|
1782
|
+
requireEntryFn(e, 'jsonSchemaValidate', 'JSON Schema validation');
|
|
1783
|
+
const r = entryResult(
|
|
1784
|
+
e.jsonSchemaValidate(schemaJson, instanceJson), 'jsonSchemaValidate');
|
|
1785
|
+
return { valid: !!r.valid, result: r.result, errors: r.errors || [] };
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* Schematron validation (entry_jsoo.ml's schematronValidate export
|
|
1790
|
+
* -> Schematron.Validate.validate). Returns every finding (failed
|
|
1791
|
+
* assert, fired report, indeterminate) in pattern-then-document
|
|
1792
|
+
* order.
|
|
1793
|
+
* @param {string} schematronXml the Schematron schema document
|
|
1794
|
+
* @param {string} instanceXml the instance document to check
|
|
1795
|
+
* @returns {Promise<{findings:Array<{type:string,context:string,test:string,message:string,path:string,reason?:string}>}>}
|
|
1796
|
+
*/
|
|
1797
|
+
async function schematronValidate(schematronXml, instanceXml) {
|
|
1798
|
+
if (typeof schematronXml !== 'string' || typeof instanceXml !== 'string') {
|
|
1799
|
+
throw new TypeError('schematronValidate: schematronXml and instanceXml must be strings');
|
|
1800
|
+
}
|
|
1801
|
+
const e = await entry();
|
|
1802
|
+
requireEntryFn(e, 'schematronValidate', 'Schematron validation');
|
|
1803
|
+
const r = entryResult(
|
|
1804
|
+
e.schematronValidate(schematronXml, instanceXml), 'schematronValidate');
|
|
1805
|
+
return { findings: r.findings };
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// TOAN — a small exact-CAS surface over Math.Expr (E_Int/E_Rat/E_Bool/
|
|
1809
|
+
// E_Sym/E_App). Callers pass an expression as the JSON codec
|
|
1810
|
+
// {int:n} | {rat:[n,d]} | {bool:b} | {sym:name} | {app:name,args:[...]}
|
|
1811
|
+
// and receive Content MathML for the result (via MathML.Present).
|
|
1812
|
+
async function toanCall(fnName, what, ...args) {
|
|
1813
|
+
const e = await entry();
|
|
1814
|
+
requireEntryFn(e, fnName, what);
|
|
1815
|
+
const r = entryResult(e[fnName](...args), fnName);
|
|
1816
|
+
return r.mathml;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Symbolic finite summation (entry_jsoo.ml's toanSummation ->
|
|
1821
|
+
* Math.Series.summation): sum of `body[idx:=lo..hi]`, simplified,
|
|
1822
|
+
* as Content MathML.
|
|
1823
|
+
* @param {object} bodyExpr the summand, in the expr JSON codec
|
|
1824
|
+
* @param {string} idx the summation index symbol
|
|
1825
|
+
* @param {number} lo inclusive lower bound
|
|
1826
|
+
* @param {number} hi inclusive upper bound
|
|
1827
|
+
* @returns {Promise<string>} Content MathML
|
|
1828
|
+
*/
|
|
1829
|
+
async function toanSummation(bodyExpr, idx, lo, hi) {
|
|
1830
|
+
if (typeof idx !== 'string') throw new TypeError('toanSummation: idx must be a string');
|
|
1831
|
+
return toanCall('toanSummation', 'TOAN summation',
|
|
1832
|
+
JSON.stringify(bodyExpr), idx, String(lo), String(hi));
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
/**
|
|
1836
|
+
* Symbolic finite product (entry_jsoo.ml's toanProduct ->
|
|
1837
|
+
* Math.Series.finite_product). Same shape as {@link toanSummation}.
|
|
1838
|
+
* @param {object} bodyExpr the factor, in the expr JSON codec
|
|
1839
|
+
* @param {string} idx the product index symbol
|
|
1840
|
+
* @param {number} lo inclusive lower bound
|
|
1841
|
+
* @param {number} hi inclusive upper bound
|
|
1842
|
+
* @returns {Promise<string>} Content MathML
|
|
1843
|
+
*/
|
|
1844
|
+
async function toanProduct(bodyExpr, idx, lo, hi) {
|
|
1845
|
+
if (typeof idx !== 'string') throw new TypeError('toanProduct: idx must be a string');
|
|
1846
|
+
return toanCall('toanProduct', 'TOAN product',
|
|
1847
|
+
JSON.stringify(bodyExpr), idx, String(lo), String(hi));
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* Canonical simplification (entry_jsoo.ml's toanSimplify ->
|
|
1852
|
+
* Math.Simplify.simplify) of an expression, as Content MathML.
|
|
1853
|
+
* @param {object} expr in the expr JSON codec
|
|
1854
|
+
* @returns {Promise<string>} Content MathML
|
|
1855
|
+
*/
|
|
1856
|
+
async function toanSimplify(expr) {
|
|
1857
|
+
return toanCall('toanSimplify', 'TOAN simplify', JSON.stringify(expr));
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Symbolic differentiation (entry_jsoo.ml's toanDiff ->
|
|
1862
|
+
* Math.Diff.diff) of `expr` w.r.t. `variable`, as Content MathML.
|
|
1863
|
+
* @param {object} expr in the expr JSON codec
|
|
1864
|
+
* @param {string} variable the differentiation variable
|
|
1865
|
+
* @returns {Promise<string>} Content MathML
|
|
1866
|
+
*/
|
|
1867
|
+
async function toanDiff(expr, variable) {
|
|
1868
|
+
if (typeof variable !== 'string') throw new TypeError('toanDiff: variable must be a string');
|
|
1869
|
+
return toanCall('toanDiff', 'TOAN diff', JSON.stringify(expr), variable);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Substitution (entry_jsoo.ml's toanSubst -> Math.Subst.subst):
|
|
1874
|
+
* `expr[variable := value]`, simplified, as Content MathML.
|
|
1875
|
+
* @param {object} expr in the expr JSON codec
|
|
1876
|
+
* @param {string} variable the symbol to replace
|
|
1877
|
+
* @param {object} value the replacement, in the expr JSON codec
|
|
1878
|
+
* @returns {Promise<string>} Content MathML
|
|
1879
|
+
*/
|
|
1880
|
+
async function toanSubst(expr, variable, value) {
|
|
1881
|
+
if (typeof variable !== 'string') throw new TypeError('toanSubst: variable must be a string');
|
|
1882
|
+
return toanCall('toanSubst', 'TOAN subst',
|
|
1883
|
+
JSON.stringify(expr), variable, JSON.stringify(value));
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
// -----------------------------------------------------------------
|
|
1889
|
+
// The Lean persisted store (see the Store class above).
|
|
1890
|
+
// -----------------------------------------------------------------
|
|
1891
|
+
|
|
1892
|
+
function storeReader(source, who) {
|
|
1893
|
+
if (typeof source === 'function') return source;
|
|
1894
|
+
if (typeof source === 'string') {
|
|
1895
|
+
if (typeof driver.storeReaderFor !== 'function') {
|
|
1896
|
+
throw new TypeError(
|
|
1897
|
+
`${who}: a path needs a file system; here, pass a reader function ` +
|
|
1898
|
+
'(path) => bytes, for example one over fetch()');
|
|
1899
|
+
}
|
|
1900
|
+
return driver.storeReaderFor(source);
|
|
1901
|
+
}
|
|
1902
|
+
throw new TypeError(`${who}: expected a collection root path or a reader function`);
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Open a store: read the generation CURRENT names (or `generation`),
|
|
1907
|
+
* read the artifacts the engine names, and open a store handle over
|
|
1908
|
+
* them (op storeOpen). With no `sparql` and no `keys`, every artifact
|
|
1909
|
+
* the manifest declares is read, so every query the generation can
|
|
1910
|
+
* answer works. With `sparql`, only the artifacts that query's plan
|
|
1911
|
+
* names (op storeQueryPlan) are read. With `keys`, exactly those.
|
|
1912
|
+
* @param {string|((path: string) => any)} source a collection root path
|
|
1913
|
+
* (Node), or a reader from a root-relative path to the file's bytes
|
|
1914
|
+
* @param {{generation?: string, sparql?: string, keys?: string[]}} [options]
|
|
1915
|
+
* @returns {Promise<Store>}
|
|
1916
|
+
*/
|
|
1917
|
+
async function openStore(source, options) {
|
|
1918
|
+
const opts = options || {};
|
|
1919
|
+
const reader = storeReader(source, 'openStore');
|
|
1920
|
+
const e = await entry();
|
|
1921
|
+
requireEntryFn(e, 'storeOpen', 'openStore (storeOpen)');
|
|
1922
|
+
const gen = await openGeneration(reader, opts.generation);
|
|
1923
|
+
const manifestHex = hexOfBytes(gen.manifest);
|
|
1924
|
+
let keys;
|
|
1925
|
+
if (Array.isArray(opts.keys)) {
|
|
1926
|
+
keys = opts.keys.map(String);
|
|
1927
|
+
} else if (typeof opts.sparql === 'string') {
|
|
1928
|
+
const plan = entryResult(e.storeQueryPlan(manifestHex, opts.sparql), 'openStore(plan)');
|
|
1929
|
+
keys = plan.keys.concat(plan.sidecarKeys || [], plan.blobKeys || []);
|
|
1930
|
+
} else {
|
|
1931
|
+
keys = manifestKeys(entryResult(e.storeManifestInspect(manifestHex), 'openStore(inspect)'));
|
|
1932
|
+
}
|
|
1933
|
+
const { blob, artifacts } = await readRegion(reader, gen.generation, keys);
|
|
1934
|
+
const env = entryResult(e.storeOpen(manifestHex, JSON.stringify(artifacts), blob), 'openStore');
|
|
1935
|
+
const c = await ctx();
|
|
1936
|
+
return new Store(e, env, gen, manifestHex, (nq) => c.fromNQuads(nq, {}), withExtensionRounds);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
/**
|
|
1940
|
+
* One query against a store: open it with the artifacts the query's
|
|
1941
|
+
* plan names, query, close.
|
|
1942
|
+
* @param {string|((path: string) => any)} source
|
|
1943
|
+
* @param {string} sparql
|
|
1944
|
+
* @param {{generation?: string}} [options]
|
|
1945
|
+
* @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
|
|
1946
|
+
*/
|
|
1947
|
+
async function queryStore(source, sparql, options) {
|
|
1948
|
+
if (typeof sparql !== 'string') throw new TypeError('queryStore: sparql must be a string');
|
|
1949
|
+
const store = await openStore(source, { generation: (options || {}).generation, sparql });
|
|
1950
|
+
try {
|
|
1951
|
+
return await store.query(sparql);
|
|
1952
|
+
} finally {
|
|
1953
|
+
await store.close();
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/** The store handles this engine instance holds open (op storeHandleList). */
|
|
1958
|
+
async function listStores() {
|
|
1959
|
+
const e = await entry();
|
|
1960
|
+
requireEntryFn(e, 'storeHandleList', 'listStores');
|
|
1961
|
+
const { ok: _ok, ...rest } = entryResult(e.storeHandleList(), 'listStores');
|
|
1962
|
+
return rest;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* One call, serialised answer (op queryRaw): parse `dataString`, close
|
|
1967
|
+
* the default graph under `entail`, run the query, and write the answer
|
|
1968
|
+
* in `output`: 'json' (default; parsed SPARQL results JSON), or 'csv',
|
|
1969
|
+
* 'tsv', 'xml', 'table', 'ntriples' (text). A CONSTRUCT or DESCRIBE
|
|
1970
|
+
* answer is a graph: 'ntriples' or 'table' only.
|
|
1971
|
+
* @param {string} dataString
|
|
1972
|
+
* @param {string} queryString
|
|
1973
|
+
* @param {{dataFormat?: string, entail?: string, output?: string}} [options]
|
|
1974
|
+
* @returns {Promise<object|string>}
|
|
1975
|
+
*/
|
|
1976
|
+
async function queryRaw(dataString, queryString, options) {
|
|
1977
|
+
if (typeof dataString !== 'string') {
|
|
1978
|
+
throw new TypeError('queryRaw: dataString must be a string');
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof queryString !== 'string') {
|
|
1981
|
+
throw new TypeError('queryRaw: queryString must be a string');
|
|
1982
|
+
}
|
|
1983
|
+
const opts = checkOptions('queryRaw', options, ['dataFormat', 'entail', 'output']);
|
|
1984
|
+
const dataFormat = String(opts.dataFormat || 'turtle').toLowerCase();
|
|
1985
|
+
const entail = opts.entail || 'none';
|
|
1986
|
+
const output = opts.output || 'json';
|
|
1987
|
+
if (!(dataFormat in RAW_DATA_FORMATS)) {
|
|
1988
|
+
throw new TypeError(`queryRaw: unknown dataFormat '${opts.dataFormat}'`);
|
|
1989
|
+
}
|
|
1990
|
+
if (!ENTAIL_VALUES.has(entail)) {
|
|
1991
|
+
throw new TypeError(`queryRaw: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
|
|
1992
|
+
}
|
|
1993
|
+
if (!RAW_OUTPUT_FORMATS.has(output)) {
|
|
1994
|
+
throw new TypeError(`queryRaw: output must be one of ${[...RAW_OUTPUT_FORMATS].join(', ')}`);
|
|
1995
|
+
}
|
|
1996
|
+
const e = await entry();
|
|
1997
|
+
requireEntryFn(e, 'queryRaw', 'queryRaw');
|
|
1998
|
+
const r = JSON.parse(e.queryRaw(dataString, queryString,
|
|
1999
|
+
JSON.stringify({ dataFormat: RAW_DATA_FORMATS[dataFormat], entail, output })));
|
|
2000
|
+
if (!r.ok) {
|
|
2001
|
+
const err = new Error('SPARQL query failed: ' + r.error);
|
|
2002
|
+
err.stderr = String(r.error);
|
|
2003
|
+
throw err;
|
|
2004
|
+
}
|
|
2005
|
+
return output === 'json' ? JSON.parse(r.text) : r.text;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// ---------------------------------------------------------------
|
|
2009
|
+
// The durable update log of a browser host (hub post 18,
|
|
2010
|
+
// https://github.com/danbri/factoidal/issues/728).
|
|
2011
|
+
//
|
|
2012
|
+
// One record for each committed SPARQL Update request, kept by the host
|
|
2013
|
+
// store (driver.deltaLogStore: IndexedDB in the browser entries, the
|
|
2014
|
+
// memory of the process by default). The engine makes every decision
|
|
2015
|
+
// about the bytes: op deltaLogEncode writes a record (a DLB1 frame with
|
|
2016
|
+
// its checksum), op deltaLogMerge decodes the records, stops at the first
|
|
2017
|
+
// record that does not verify, and applies the batches before it
|
|
2018
|
+
// (formal/lean4/L4Factoidal/Storage/BrowserLog.lean, with the theorems in
|
|
2019
|
+
// BrowserLogTheorems.lean). This layer moves text between the two. The
|
|
2020
|
+
// names and result shapes are those of the F* browser entry before 0.9.0.
|
|
2021
|
+
// ---------------------------------------------------------------
|
|
2022
|
+
const DEFAULT_DELTA_DB_NAME = 'factoidal-delta-log';
|
|
2023
|
+
const deltaLogStore = driver.deltaLogStore || memoryDeltaLogStore();
|
|
2024
|
+
|
|
2025
|
+
function requireDeltaHandle(handle, who) {
|
|
2026
|
+
if (!handle || typeof handle.dbName !== 'string') {
|
|
2027
|
+
throw new TypeError(`${who}: handle must be the object deltaLogOpen() returned`);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
/**
|
|
2032
|
+
* Open (create when absent) a durable update log. In the browser the log is
|
|
2033
|
+
* an IndexedDB database of that name; under Node it lives in the memory of
|
|
2034
|
+
* the process.
|
|
2035
|
+
* @param {string} [dbName='factoidal-delta-log']
|
|
2036
|
+
* @returns {Promise<{dbName: string}>}
|
|
2037
|
+
*/
|
|
2038
|
+
async function deltaLogOpen(dbName) {
|
|
2039
|
+
const name = dbName == null || dbName === '' ? DEFAULT_DELTA_DB_NAME : String(dbName);
|
|
2040
|
+
await deltaLogStore.open(name);
|
|
2041
|
+
return { dbName: name };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* Append one SPARQL Update request as one record (op deltaLogEncode).
|
|
2046
|
+
* INSERT DATA, DELETE DATA, CLEAR DEFAULT, CLEAR GRAPH, DROP GRAPH and
|
|
2047
|
+
* CREATE GRAPH are admitted; any other form rejects and nothing is
|
|
2048
|
+
* written. `seq` is the record's position in the log.
|
|
2049
|
+
* @param {{dbName: string}} handle
|
|
2050
|
+
* @param {string} sparqlUpdate
|
|
2051
|
+
* @param {{epoch?: number}} [options]
|
|
2052
|
+
* @returns {Promise<{seq: number, opCount: number}>}
|
|
2053
|
+
*/
|
|
2054
|
+
async function deltaLogAppend(handle, sparqlUpdate, options) {
|
|
2055
|
+
requireDeltaHandle(handle, 'deltaLogAppend');
|
|
2056
|
+
if (typeof sparqlUpdate !== 'string') {
|
|
2057
|
+
throw new TypeError('deltaLogAppend: sparqlUpdate must be a string');
|
|
2058
|
+
}
|
|
2059
|
+
const epoch = (options && options.epoch) || 0;
|
|
2060
|
+
if (!Number.isSafeInteger(epoch) || epoch < 0) {
|
|
2061
|
+
throw new TypeError('deltaLogAppend: options.epoch must be a natural number');
|
|
2062
|
+
}
|
|
2063
|
+
const e = await entry();
|
|
2064
|
+
requireEntryFn(e, 'deltaLogEncode', 'deltaLogAppend');
|
|
2065
|
+
let opCount = 0;
|
|
2066
|
+
const { seq } = await deltaLogStore.append(handle.dbName, (nextSeq) => {
|
|
2067
|
+
const r = entryResult(e.deltaLogEncode(sparqlUpdate, String(nextSeq), String(epoch)),
|
|
2068
|
+
'deltaLogAppend');
|
|
2069
|
+
opCount = r.opCount;
|
|
2070
|
+
return { hex: r.hex, epoch };
|
|
2071
|
+
});
|
|
2072
|
+
return { seq, opCount };
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/**
|
|
2076
|
+
* The records of the log, in commit order, as hexadecimal text joined by
|
|
2077
|
+
* "\n": the text deltaLogMerge() hands to the engine.
|
|
2078
|
+
* @param {{dbName: string}} handle
|
|
2079
|
+
* @returns {Promise<string>}
|
|
2080
|
+
*/
|
|
2081
|
+
async function deltaLogReadAllHex(handle) {
|
|
2082
|
+
requireDeltaHandle(handle, 'deltaLogReadAllHex');
|
|
2083
|
+
return (await deltaLogStore.readAll(handle.dbName)).join('\n');
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
/**
|
|
2087
|
+
* Merge on read (op deltaLogMerge): the base dataset with the log's
|
|
2088
|
+
* batches applied in commit order, as N-Quads. A torn final record (an
|
|
2089
|
+
* append that did not complete) is not applied; deltaLogInspect() reports
|
|
2090
|
+
* it. A damaged record, or a record whose checksum verifies but whose
|
|
2091
|
+
* entries the engine cannot decode, makes the merge throw: the records
|
|
2092
|
+
* after it would otherwise be lost without a message
|
|
2093
|
+
* (https://github.com/danbri/factoidal/issues/843).
|
|
2094
|
+
* @param {{dbName: string}} handle
|
|
2095
|
+
* @param {string} baseNQuads the dataset before the log, as N-Quads
|
|
2096
|
+
* @returns {Promise<string>}
|
|
2097
|
+
*/
|
|
2098
|
+
async function deltaLogMerge(handle, baseNQuads) {
|
|
2099
|
+
requireDeltaHandle(handle, 'deltaLogMerge');
|
|
2100
|
+
if (typeof baseNQuads !== 'string') {
|
|
2101
|
+
throw new TypeError('deltaLogMerge: baseNQuads must be a string');
|
|
2102
|
+
}
|
|
2103
|
+
const e = await entry();
|
|
2104
|
+
requireEntryFn(e, 'deltaLogMerge', 'deltaLogMerge');
|
|
2105
|
+
const logHex = await deltaLogReadAllHex(handle);
|
|
2106
|
+
return entryResult(e.deltaLogMerge(baseNQuads, logHex), 'deltaLogMerge').nquads;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* What the log holds (op deltaLogDecode): the number of batches and
|
|
2111
|
+
* entries that verify, whether the log ends with a torn record and its
|
|
2112
|
+
* size in bytes, and {seq, epoch, entries} for each batch. Throws for a
|
|
2113
|
+
* damaged record or a record whose entries the engine cannot decode
|
|
2114
|
+
* (https://github.com/danbri/factoidal/issues/843).
|
|
2115
|
+
* @param {{dbName: string}} handle
|
|
2116
|
+
* @returns {Promise<{batches: number, entries: number, torn: boolean,
|
|
2117
|
+
* tornBytes: number, logBytes: number,
|
|
2118
|
+
* records: Array<{seq: number, epoch: number, entries: number}>}>}
|
|
2119
|
+
*/
|
|
2120
|
+
async function deltaLogInspect(handle) {
|
|
2121
|
+
requireDeltaHandle(handle, 'deltaLogInspect');
|
|
2122
|
+
const e = await entry();
|
|
2123
|
+
requireEntryFn(e, 'deltaLogDecode', 'deltaLogInspect');
|
|
2124
|
+
const { ok: _ok, ...rest } = entryResult(
|
|
2125
|
+
e.deltaLogDecode(await deltaLogReadAllHex(handle)), 'deltaLogInspect');
|
|
2126
|
+
return rest;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* Remove the log and its records.
|
|
2131
|
+
* @param {{dbName: string}} handle
|
|
2132
|
+
* @returns {Promise<void>}
|
|
2133
|
+
*/
|
|
2134
|
+
async function deltaLogDestroy(handle) {
|
|
2135
|
+
requireDeltaHandle(handle, 'deltaLogDestroy');
|
|
2136
|
+
await deltaLogStore.destroy(handle.dbName);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* TEST ONLY: cut the last 4 bytes (8 hex digits) of the last record, as a
|
|
2141
|
+
* write torn by a crash would leave it. IndexedDB transactions are atomic,
|
|
2142
|
+
* so a page has no other way to make one. False when the log is empty.
|
|
2143
|
+
* @param {{dbName: string}} handle
|
|
2144
|
+
* @returns {Promise<boolean>}
|
|
2145
|
+
*/
|
|
2146
|
+
async function _deltaLogCorruptLastForTest(handle) {
|
|
2147
|
+
requireDeltaHandle(handle, '_deltaLogCorruptLastForTest');
|
|
2148
|
+
return deltaLogStore.corruptLast(handle.dbName);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Feature probe, for tests and downstream capability checks.
|
|
2153
|
+
* @returns {Promise<{entry: boolean, construct: boolean,
|
|
2154
|
+
* update: boolean, canonicalize: boolean, graphs: boolean,
|
|
2155
|
+
* canonicalHash: boolean, shacl: boolean, shex: boolean,
|
|
2156
|
+
* owlClosure: boolean, rml: boolean, csvw: boolean, jsonld: boolean,
|
|
2157
|
+
* rif: boolean}>}
|
|
2158
|
+
*/
|
|
2159
|
+
let capsCache = null;
|
|
2160
|
+
async function capabilities() {
|
|
2161
|
+
if (capsCache) return capsCache;
|
|
2162
|
+
capsCache = capabilitiesUncached();
|
|
2163
|
+
return capsCache;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
async function capabilitiesUncached() {
|
|
2167
|
+
const e = await entry();
|
|
2168
|
+
const has = (name) => typeof e[name] === 'function';
|
|
2169
|
+
return {
|
|
2170
|
+
entry: true, construct: true, update: true, canonicalize: true,
|
|
2171
|
+
graphs: true, canonicalHash: true,
|
|
2172
|
+
// Per-op probes: a wasm module older than this package reports the
|
|
2173
|
+
// ops it actually has.
|
|
2174
|
+
shacl: has('shaclValidate'),
|
|
2175
|
+
shex: has('shexValidate'),
|
|
2176
|
+
owlClosure: has('owlClosure'),
|
|
2177
|
+
entailment: has('queryRaw'),
|
|
2178
|
+
tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
|
|
2179
|
+
rml: has('rmlMap'),
|
|
2180
|
+
csvw: has('csvwToRdf'),
|
|
2181
|
+
jsonld: has('jsonldToRdf'),
|
|
2182
|
+
jsonldFromRdf: has('jsonldFromRdf'),
|
|
2183
|
+
didKey: has('didKeyResolve'),
|
|
2184
|
+
xml: has('xmlWellformed'),
|
|
2185
|
+
xpath: has('xpathEval'),
|
|
2186
|
+
rif: has('rifEval'),
|
|
2187
|
+
xslt: has('xsltTransform'),
|
|
2188
|
+
mathml: has('mathmlEval'),
|
|
2189
|
+
xforms: has('xformsRecalc'),
|
|
2190
|
+
jsonSchema: has('jsonSchemaValidate'),
|
|
2191
|
+
schematron: has('schematronValidate'),
|
|
2192
|
+
toan: has('toanSummation'),
|
|
2193
|
+
hdt: has('queryHdt'),
|
|
2194
|
+
// VC Data Integrity: Ed25519 is HACL* compiled into the wasm module,
|
|
2195
|
+
// SHA-256 is the Lean hash; no separate crypto module or init step.
|
|
2196
|
+
vcCrypto: has('vcEd25519Verify') && has('vcEddsaVerifyFromCanonical'),
|
|
2197
|
+
profile: e.profile || 'full',
|
|
2198
|
+
abiVersion: e.abiVersion,
|
|
2199
|
+
datasetHandles: has('datasetOpen') && has('datasetQuery') &&
|
|
2200
|
+
has('datasetUpdate') && has('datasetSerialize') && has('datasetClose'),
|
|
2201
|
+
datasetInEngine: has('datasetClone') && has('datasetClosure') && has('datasetQueryRaw'),
|
|
2202
|
+
sparql12: has('queryDataset12') && has('datasetQuery12'),
|
|
2203
|
+
parseDiagnostics: has('parseDocument'),
|
|
2204
|
+
turtlePrefixes: has('serializeTurtleWith'),
|
|
2205
|
+
store: has('storeOpen') && has('storeHandleQuery'),
|
|
2206
|
+
deltaLog: has('deltaLogEncode') && has('deltaLogDecode') && has('deltaLogMerge'),
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
return {
|
|
2211
|
+
parse,
|
|
2212
|
+
query,
|
|
2213
|
+
queryHdt,
|
|
2214
|
+
update,
|
|
2215
|
+
openDataset,
|
|
2216
|
+
closure,
|
|
2217
|
+
formats,
|
|
2218
|
+
registerExtensionFunction,
|
|
2219
|
+
unregisterExtensionFunction,
|
|
2220
|
+
clearExtensionFunctions,
|
|
2221
|
+
registerServiceEndpoint,
|
|
2222
|
+
clearServiceEndpoints,
|
|
2223
|
+
serialize,
|
|
2224
|
+
canonicalize,
|
|
2225
|
+
graphs,
|
|
2226
|
+
canonicalHash,
|
|
2227
|
+
shaclValidate,
|
|
2228
|
+
shexValidate,
|
|
2229
|
+
owlClosure,
|
|
2230
|
+
coreRdfsClosure,
|
|
2231
|
+
coreRdfsCheck,
|
|
2232
|
+
rdfsPlusClosure,
|
|
2233
|
+
rhoDfClosure,
|
|
2234
|
+
rhoDfFragmentCheck,
|
|
2235
|
+
tableauMaterialise,
|
|
2236
|
+
tableauDlInconsistent,
|
|
2237
|
+
owlIsConsistent,
|
|
2238
|
+
owlEntails,
|
|
2239
|
+
rmlMap,
|
|
2240
|
+
csvwToRdf,
|
|
2241
|
+
jsonldToRdf,
|
|
2242
|
+
jsonldFromRdf,
|
|
2243
|
+
didKeyResolve,
|
|
2244
|
+
xmlWellformed,
|
|
2245
|
+
xpathEval,
|
|
2246
|
+
clParse,
|
|
2247
|
+
clSerialize,
|
|
2248
|
+
clAlphaNorm,
|
|
2249
|
+
clNormalize,
|
|
2250
|
+
rifEval,
|
|
2251
|
+
xsltTransform,
|
|
2252
|
+
mathmlEval,
|
|
2253
|
+
xformsRecalc,
|
|
2254
|
+
jsonSchemaValidate,
|
|
2255
|
+
schematronValidate,
|
|
2256
|
+
toanSummation,
|
|
2257
|
+
toanProduct,
|
|
2258
|
+
toanSimplify,
|
|
2259
|
+
toanDiff,
|
|
2260
|
+
toanSubst,
|
|
2261
|
+
vcSha256Hex,
|
|
2262
|
+
vcEd25519SecretToPublic,
|
|
2263
|
+
vcEd25519Sign,
|
|
2264
|
+
vcEd25519Verify,
|
|
2265
|
+
vcEddsaCreateFromCanonical,
|
|
2266
|
+
vcEddsaVerifyFromCanonical,
|
|
2267
|
+
vcCheckCredential,
|
|
2268
|
+
vcCheckCredentialSubject,
|
|
2269
|
+
vcCheckNoDataLoss,
|
|
2270
|
+
vcCheckRelatedResourceDigests,
|
|
2271
|
+
capabilities,
|
|
2272
|
+
openStore,
|
|
2273
|
+
queryStore,
|
|
2274
|
+
listStores,
|
|
2275
|
+
deltaLogOpen,
|
|
2276
|
+
deltaLogAppend,
|
|
2277
|
+
deltaLogReadAllHex,
|
|
2278
|
+
deltaLogMerge,
|
|
2279
|
+
deltaLogInspect,
|
|
2280
|
+
deltaLogDestroy,
|
|
2281
|
+
_deltaLogCorruptLastForTest,
|
|
2282
|
+
queryRaw,
|
|
2283
|
+
Dataset,
|
|
2284
|
+
DatasetHandle,
|
|
2285
|
+
Store,
|
|
2286
|
+
ParseError,
|
|
2287
|
+
dataFactory,
|
|
2288
|
+
// Metadata (issue #682): the same two fields on every driver's
|
|
2289
|
+
// typed surface, so a caller can log/report which package version
|
|
2290
|
+
// and which engine (js/wasm/entry/l4) answered a call without a
|
|
2291
|
+
// separate require('../package.json').
|
|
2292
|
+
version: packageVersion,
|
|
2293
|
+
engine: driver.engineName,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
export {
|
|
2298
|
+
buildApi, sniffQueryForm, bindingsFromSrj, entryResult, parseResult,
|
|
2299
|
+
utf16OffsetOf, missingOpError, ParseError, DatasetHandle, Store,
|
|
2300
|
+
};
|