@factoidal/core 0.7.1 → 0.11.0

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