@factoidal/core 0.7.0 → 0.10.0

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