@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.js DELETED
@@ -1,2300 +0,0 @@
1
- // factoidal — public API layer, shared by index.js (js_of_ocaml) and
2
- // wasm.js (wasm_of_ocaml).
3
- //
4
- // Two ways to reach the engine, tried in order:
5
- //
6
- // 1. The npm-entry ABI bundle (factoidal-npm-entry.js /
7
- // factoidal-npm-entry.wasm.js — built from bin/npm-entry/
8
- // entry_jsoo.ml). A persistent, function-call ABI: strings in,
9
- // JSON out. Supports everything including CONSTRUCT, UPDATE and
10
- // RDFC-1.0 canonicalization.
11
- //
12
- // 2. The single-shot CLI bundle (factoidal.js / factoidal.wasm.js),
13
- // driven argv-style via lib/engine-js.js / lib/engine-wasm.js.
14
- // Covers parse / SELECT / ASK / serialize today; CONSTRUCT and
15
- // UPDATE are not reachable through the CLI surface, and
16
- // canonicalize requires a bundle built after the `factoidal
17
- // canonicalize` subcommand landed.
18
- //
19
- // Operations that need the npm-entry bundle throw an Error whose
20
- // message contains "pending npm-entry build" when only the CLI bundle
21
- // is available. All semantic work happens inside the F*-extracted
22
- // engine either way; this layer only shapes arguments and results.
23
-
24
- 'use strict';
25
-
26
- const {
27
- Dataset,
28
- dataFactory,
29
- termFromSrj,
30
- } = require('../rdfjs.js');
31
-
32
- const DATA_FORMAT_EXT = {
33
- turtle: 'ttl',
34
- ttl: 'ttl',
35
- ntriples: 'nt',
36
- nt: 'nt',
37
- nquads: 'nq',
38
- nq: 'nq',
39
- trig: 'trig',
40
- rdfxml: 'rdf',
41
- 'rdf-xml': 'rdf',
42
- rdf: 'rdf',
43
- jsonld: 'jsonld',
44
- 'json-ld': 'jsonld',
45
- // RDF 1.2 opt-in: these select the engine's Mode_12 parsers (triple
46
- // terms <<( s p o )>>, ~ reifiers, {| |} annotations, VERSION,
47
- // directional literals "x"@lang--dir). Only reachable via the
48
- // npm-entry bundle (the entry ABI routes the *12 tag to
49
- // Parser_*.*_mode Mode_12); the plain names above stay Mode_11 so 1.1
50
- // output is byte-identical.
51
- turtle12: 'ttl12',
52
- ttl12: 'ttl12',
53
- ntriples12: 'nt12',
54
- nt12: 'nt12',
55
- nquads12: 'nq12',
56
- nq12: 'nq12',
57
- trig12: 'trig12',
58
- };
59
-
60
- // Canonical format tag for the npm-entry ABI. The plain tags are what
61
- // RDF_Format.format_of_string accepts directly; the *12 tags are
62
- // intercepted by entry_jsoo's parse_text_to_dataset (before
63
- // format_of_string) to select Mode_12.
64
- const DATA_FORMAT_TAG = {
65
- ttl: 'turtle', nt: 'ntriples', nq: 'nquads', trig: 'trig', rdf: 'rdfxml',
66
- jsonld: 'jsonld',
67
- ttl12: 'turtle12', nt12: 'ntriples12', nq12: 'nquads12', trig12: 'trig12',
68
- };
69
-
70
- const ENTAIL_VALUES = new Set(['none', 'RDFS', 'OWL-RL', 'x-rdfscore', 'x-rdfsplus']);
71
-
72
- function extForFormat(fmt) {
73
- const key = String(fmt || 'turtle').toLowerCase();
74
- if (!(key in DATA_FORMAT_EXT)) {
75
- throw new TypeError(
76
- `Unknown format '${fmt}'. Expected one of: ` +
77
- Object.keys(DATA_FORMAT_EXT).join(', ')
78
- );
79
- }
80
- return DATA_FORMAT_EXT[key];
81
- }
82
-
83
- function engineError(prefix, res) {
84
- const msg = (res.stderr || res.stdout ||
85
- `factoidal exited with code ${res.exitCode}`).trim();
86
- const err = new Error(`${prefix}: ${msg}`);
87
- err.exitCode = res.exitCode;
88
- err.stderr = res.stderr;
89
- err.stdout = res.stdout;
90
- return err;
91
- }
92
-
93
- function pendingError(what) {
94
- return new Error(
95
- `${what} needs the factoidal-npm-entry bundle, which is not present ` +
96
- '— pending npm-entry build (formal/fstar/build-ocaml.sh js + npm ' +
97
- 'with bin/npm-entry/entry_jsoo.ml wired in; see bin/npm-entry/README.md).'
98
- );
99
- }
100
-
101
- // Extract the JSON object from CLI stdout defensively (a stray line
102
- // must not break the parse).
103
- function jsonFromStdout(stdout, what) {
104
- const first = stdout.indexOf('{');
105
- const last = stdout.lastIndexOf('}');
106
- if (first < 0 || last < first) {
107
- const err = new Error(`${what}: engine did not produce JSON: ${stdout}`);
108
- err.stdout = stdout;
109
- throw err;
110
- }
111
- return JSON.parse(stdout.slice(first, last + 1));
112
- }
113
-
114
- // Detect the query form for result-shape dispatch. IRIs and comments
115
- // are blanked first so '#' inside a PREFIX IRI cannot hide the verb.
116
- // The engine's SPARQL parser remains the authority — this only picks
117
- // the output shape.
118
- function sniffQueryForm(sparql) {
119
- const cleaned = String(sparql)
120
- .replace(/<[^>]*>/g, ' ')
121
- .replace(/#[^\n]*/g, ' ');
122
- const m = cleaned.match(/\b(select|ask|construct|describe)\b/i);
123
- return m ? m[1].toLowerCase() : 'select';
124
- }
125
-
126
- // Pack raw bytes into the one-char-per-byte string js_of_ocaml's fake
127
- // filesystem (runCli's `files` argument) expects for BINARY content --
128
- // the same convention browser.js's HDT/COTTAS example cells built by
129
- // hand before queryHdt() existed (String.fromCharCode per byte).
130
- // Distinct from bytesToHex below (the npm-entry ABI's string-only wire
131
- // format for openCottas/toCottas): this is for the CLI's file-based
132
- // backends (--data-hdt), which read from the CLI bundle's fake
133
- // filesystem, not the ABI.
134
- function bytesToLatin1(bytesLike, who) {
135
- if (typeof bytesLike === 'string') return bytesLike; // already packed
136
- let u8;
137
- if (bytesLike instanceof Uint8Array) u8 = bytesLike;
138
- else if (bytesLike instanceof ArrayBuffer) u8 = new Uint8Array(bytesLike);
139
- else {
140
- throw new TypeError(
141
- `${who}: expected a Uint8Array, Buffer, ArrayBuffer, or an already-packed string`);
142
- }
143
- let out = '';
144
- for (let i = 0; i < u8.length; i += 0x4000) {
145
- out += String.fromCharCode.apply(null, u8.subarray(i, Math.min(u8.length, i + 0x4000)));
146
- }
147
- return out;
148
- }
149
-
150
- // SRJ bindings -> Array<Map<string, Term>>
151
- function bindingsFromSrj(srj) {
152
- const rows = (srj && srj.results && srj.results.bindings) || [];
153
- return rows.map((row) => {
154
- const map = new Map();
155
- for (const [name, term] of Object.entries(row)) {
156
- map.set(name, termFromSrj(term));
157
- }
158
- return map;
159
- });
160
- }
161
-
162
- /**
163
- * Build the public API around a driver.
164
- *
165
- * @param {object} driver
166
- * @param {(args: string[], files: Array<{name,content}>) =>
167
- * ({stdout,stderr,exitCode}|Promise)} driver.runCli
168
- * @param {() => (object|null|Promise<object|null>)} driver.loadEntry
169
- * Returns the npm-entry ABI object (factoidalNpmEntry) or null.
170
- * @param {string} driver.engineName 'js' | 'wasm' (error messages).
171
- */
172
- function buildApi(driver) {
173
- let parseCounter = 0;
174
- let entryCache; // undefined = not tried; null = unavailable
175
-
176
- async function entry() {
177
- if (entryCache === undefined) {
178
- try {
179
- entryCache = (await driver.loadEntry()) || null;
180
- } catch (_) {
181
- entryCache = null;
182
- }
183
- }
184
- return entryCache;
185
- }
186
-
187
- async function run(args, files) {
188
- return driver.runCli(args, files);
189
- }
190
-
191
- function entryResult(jsonText, what) {
192
- const r = JSON.parse(jsonText);
193
- if (!r.ok) throw new Error(`${what}: ${r.error}`);
194
- return r;
195
- }
196
-
197
- function freshBnodePrefix() {
198
- return `p${parseCounter++}_`;
199
- }
200
-
201
- // ---------------------------------------------------------------
202
- // SPARQL 1.1 §17.6 extension functions — issue #463.
203
- // https://github.com/danbri/factoidal/issues/463
204
- //
205
- // Comunica-style: the caller registers (possibly async) JS functions
206
- // keyed by absolute IRI; a query using an unregistered IRI gets the
207
- // spec-required error (in expression position: the row's value
208
- // errors — unbound in SELECT/BIND, row dropped in FILTER).
209
- //
210
- // The F*-extracted evaluator is synchronous, so async functions are
211
- // bridged with a bounded re-evaluation trampoline: each engine call
212
- // reaches extBridge synchronously; a cache miss on an async function
213
- // records the promise, returns a pending marker (an engine-side
214
- // error in THAT pass), and after the pass every pending promise is
215
- // awaited into the cache and the query re-runs. The per-(iri, args)
216
- // memoisation is also what honors the F* purity assumption on the
217
- // extension_function_call hook — within one evaluation every call
218
- // with the same arguments sees one stable answer.
219
- //
220
- // The user function receives an array of SRJ-style term objects
221
- // ({type:'uri'|'literal'|'bnode', value, datatype?, 'xml:lang'?};
222
- // {type:'error'} for an errored argument) and returns a term object,
223
- // a JS primitive (boolean / number / string), a Promise of either,
224
- // or null/undefined (= error). Thrown errors and rejections map to
225
- // the §17.6 error too.
226
- // ---------------------------------------------------------------
227
- const EXT_PENDING_MARKER = '__FACTOIDAL_EXT_PENDING__';
228
- const EXT_MAX_ROUNDS = 25;
229
- const extFunctions = new Map(); // iri -> user fn
230
- const extInstalled = new Set(); // iris registered into the ABI
231
- let extCache = new Map(); // key -> normalized result (or null)
232
- let extPending = []; // [{key, promise}] for this pass
233
-
234
- function extBridge(iriJs, argsJsonJs) {
235
- // Called SYNCHRONOUSLY from inside the engine.
236
- const iri = String(iriJs);
237
- const argsJson = String(argsJsonJs);
238
- const key = iri + '' + argsJson;
239
- if (extCache.has(key)) return extCache.get(key);
240
- const fn = extFunctions.get(iri);
241
- if (!fn) return null;
242
- let out;
243
- try {
244
- out = fn(JSON.parse(argsJson));
245
- } catch (_e) {
246
- extCache.set(key, null);
247
- return null;
248
- }
249
- if (out && typeof out.then === 'function') {
250
- extPending.push({ key, promise: out });
251
- return EXT_PENDING_MARKER;
252
- }
253
- out = out === undefined ? null : out;
254
- extCache.set(key, out);
255
- return out;
256
- }
257
-
258
- async function extEnsureInstalled(what) {
259
- const e = await entry();
260
- if (!e) return null;
261
- if (typeof e.registerExtensionFunction !== 'function') {
262
- throw new Error(
263
- `${what}: this npm-entry bundle predates extension functions ` +
264
- '(issue #463) — rebuild build-ocaml.sh js + npm.');
265
- }
266
- for (const iri of extFunctions.keys()) {
267
- if (!extInstalled.has(iri)) {
268
- entryResult(e.registerExtensionFunction(iri, extBridge),
269
- 'registerExtensionFunction');
270
- extInstalled.add(iri);
271
- }
272
- }
273
- return e;
274
- }
275
-
276
- /**
277
- * Register a custom SPARQL extension function (SPARQL 1.1 §17.6).
278
- * @param {string} iri absolute IRI the function is invoked by
279
- * @param {(args: object[]) => any} fn sync or async; see the block
280
- * comment above for the argument/return contract
281
- */
282
- async function registerExtensionFunction(iri, fn) {
283
- if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
284
- throw new TypeError(
285
- 'registerExtensionFunction: iri must be an absolute IRI string');
286
- }
287
- if (typeof fn !== 'function') {
288
- throw new TypeError('registerExtensionFunction: fn must be a function');
289
- }
290
- extFunctions.set(iri, fn);
291
- await extEnsureInstalled('registerExtensionFunction');
292
- }
293
-
294
- /** Remove one registered extension function. */
295
- async function unregisterExtensionFunction(iri) {
296
- extFunctions.delete(iri);
297
- const e = await entry();
298
- if (e && typeof e.unregisterExtensionFunction === 'function'
299
- && extInstalled.has(iri)) {
300
- entryResult(e.unregisterExtensionFunction(iri),
301
- 'unregisterExtensionFunction');
302
- extInstalled.delete(iri);
303
- }
304
- }
305
-
306
- /** Remove every registered extension function. */
307
- async function clearExtensionFunctions() {
308
- extFunctions.clear();
309
- const e = await entry();
310
- if (e && typeof e.clearExtensionFunctions === 'function') {
311
- entryResult(e.clearExtensionFunctions(), 'clearExtensionFunctions');
312
- }
313
- extInstalled.clear();
314
- }
315
-
316
- /**
317
- * Bind a SPARQL SERVICE endpoint IRI to a local graph snapshot, so
318
- * SERVICE <iri> { ... } (and LATERAL { SERVICE ... }) queries
319
- * resolve against it in-process — the same registry the W3C
320
- * federated-query suite uses (qt:serviceData). `data` is a Dataset,
321
- * a raw RDF string (options.format, default turtle), or an array of
322
- * those. The snapshot is the payload's default graph.
323
- */
324
- async function registerServiceEndpoint(iri, data, options) {
325
- if (typeof iri !== 'string' || !/^[A-Za-z][A-Za-z0-9+.-]*:/.test(iri)) {
326
- throw new TypeError(
327
- 'registerServiceEndpoint: iri must be an absolute IRI string');
328
- }
329
- const e = await entry();
330
- if (!e || typeof e.registerServiceEndpoint !== 'function') {
331
- throw new Error(
332
- 'registerServiceEndpoint: this npm-entry bundle predates SERVICE ' +
333
- 'endpoint registration — rebuild build-ocaml.sh js + npm.');
334
- }
335
- const docs = toDocs(data, options);
336
- const nq = docsToEntryNQuads(e, docs, 'registerServiceEndpoint');
337
- return entryResult(e.registerServiceEndpoint(iri, nq),
338
- 'registerServiceEndpoint');
339
- }
340
-
341
- /** Remove every registered SERVICE endpoint snapshot. */
342
- async function clearServiceEndpoints() {
343
- const e = await entry();
344
- if (e && typeof e.clearServiceEndpoints === 'function') {
345
- entryResult(e.clearServiceEndpoints(), 'clearServiceEndpoints');
346
- }
347
- }
348
-
349
- // Run one synchronous engine pass, re-running until no NEW async
350
- // extension results are pending. With no registered functions this
351
- // is exactly one pass with zero overhead beyond the length check.
352
- async function withExtensionRounds(runOnce) {
353
- extCache = new Map(); // per-evaluation memo (purity contract)
354
- for (let round = 0; ; round++) {
355
- extPending = [];
356
- const result = runOnce();
357
- if (extPending.length === 0) return result;
358
- if (round >= EXT_MAX_ROUNDS) {
359
- throw new Error(
360
- 'extension functions: async resolution did not converge ' +
361
- `within ${EXT_MAX_ROUNDS} evaluation rounds`);
362
- }
363
- const pend = extPending;
364
- extPending = [];
365
- await Promise.all(pend.map(async ({ key, promise }) => {
366
- try {
367
- const v = await promise;
368
- extCache.set(key, v === undefined ? null : v);
369
- } catch (_e) {
370
- extCache.set(key, null);
371
- }
372
- }));
373
- }
374
- }
375
-
376
- // Normalize the `data` argument of query/update/serialize/
377
- // canonicalize into engine inputs. Accepts a Dataset, a raw string
378
- // (with options.format, default turtle), or an array of those —
379
- // each element is loaded as its own document so blank-node labels
380
- // stay document-scoped (the per-document renaming is F*'s
381
- // RDF.Dataset.Merge.rename_dataset_bnodes, applied at engine load).
382
- function toDocs(data, options) {
383
- const opts = options || {};
384
- const items = Array.isArray(data) ? data : [data];
385
- return items.map((item, i) => {
386
- if (item instanceof Dataset) {
387
- return { ext: 'nq', content: item.toNQuads() };
388
- }
389
- if (typeof item === 'string') {
390
- return { ext: extForFormat(opts.format), content: item };
391
- }
392
- if (item && typeof item.text === 'string') {
393
- return { ext: extForFormat(item.format || opts.format), content: item.text };
394
- }
395
- throw new TypeError(
396
- `data[${i}]: expected a Dataset, a string, or {text, format}`);
397
- });
398
- }
399
-
400
- function docsToCliFiles(docs) {
401
- const files = [];
402
- const flags = [];
403
- docs.forEach((d, i) => {
404
- const name = `/static/data${i}.${d.ext}`;
405
- files.push({ name, content: d.content });
406
- flags.push('-d', name);
407
- });
408
- return { files, flags };
409
- }
410
-
411
- // Normalize toDocs() output into one concatenated N-Quads "dataset
412
- // handle" string via the entry ABI -- the same per-document
413
- // parseToDatasetJson-then-concatenate shape query()/update()/
414
- // canonicalize() each already inline. Needs the entry bundle (the
415
- // caller must have already checked `e` is non-null); factored out
416
- // here for the SHACL/ShEx/OWL-closure/RML wrappers below, which all
417
- // hand the engine a dataset-handle N-Quads string rather than raw
418
- // Turtle (consistent with every other entry ABI call).
419
- function docsToEntryNQuads(e, docs, what) {
420
- let nq = '';
421
- for (const d of docs) {
422
- if (d.ext === 'nq') { nq += d.content; continue; }
423
- const r = entryResult(
424
- e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
425
- `${what}(parse)`);
426
- nq += r.nquads;
427
- }
428
- return nq;
429
- }
430
-
431
- // A ShEx focus/shape-label argument is either a raw string (an IRI,
432
- // or "_:label" for a blank node -- the entry ABI's own convention)
433
- // or an RDF/JS term (NamedNode/BlankNode); accepting both lets a
434
- // caller pass a term straight out of a query() binding.
435
- function shexTermToString(t, who) {
436
- if (typeof t === 'string') return t;
437
- if (t && typeof t.termType === 'string') {
438
- if (t.termType === 'BlankNode') return `_:${t.value}`;
439
- if (t.termType === 'NamedNode') return t.value;
440
- throw new TypeError(`${who}: expected an IRI or blank node term`);
441
- }
442
- throw new TypeError(`${who}: expected a string or an RDF/JS term`);
443
- }
444
-
445
- // Everything below is async so both drivers (sync jsoo, async wasm)
446
- // and both paths (entry, CLI) share one shape.
447
-
448
- /**
449
- * Parse one RDF document into a Dataset.
450
- * @param {string} text
451
- * @param {{format?: string, baseIRI?: string}} [options]
452
- * @returns {Promise<Dataset>}
453
- */
454
- async function parse(text, options) {
455
- if (typeof text !== 'string') {
456
- throw new TypeError('parse: text must be a string');
457
- }
458
- const opts = options || {};
459
- const ext = extForFormat(opts.format);
460
- const baseIRI = opts.baseIRI || '';
461
- const bnodePrefix = freshBnodePrefix();
462
-
463
- const e = await entry();
464
- if (e) {
465
- const r = entryResult(
466
- e.parseToDatasetJson(text, DATA_FORMAT_TAG[ext], baseIRI), 'parse');
467
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: bnodePrefix });
468
- }
469
-
470
- const name = `/static/data.${ext}`;
471
- const args = ['--dump-nq', '-d', name];
472
- if (baseIRI) args.push('-b', baseIRI);
473
- const res = await run(args, [{ name, content: text }]);
474
- if (res.exitCode !== 0) throw engineError('parse failed', res);
475
- return Dataset.fromNQuads(res.stdout, { blankNodePrefix: bnodePrefix });
476
- }
477
-
478
- /**
479
- * Run a SPARQL 1.1 query.
480
- * @param {Dataset|string|Array} data
481
- * @param {string} sparql
482
- * @param {{format?: string, entail?: 'none'|'RDFS'|'OWL-RL'|'x-rdfscore'|'x-rdfsplus'}} [options]
483
- * @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
484
- * SELECT -> Bindings[] (Map of variable name -> RDF/JS Term),
485
- * ASK -> boolean, CONSTRUCT -> Dataset.
486
- */
487
- async function query(data, sparql, options) {
488
- if (typeof sparql !== 'string') {
489
- throw new TypeError('query: sparql must be a string');
490
- }
491
- const opts = options || {};
492
- const entail = opts.entail || 'none';
493
- // x-ikl-* is NOT IMPLEMENTED. It is not withheld by policy.
494
- //
495
- // CORRECTION 2026-08-26: this comment previously cited the owner's
496
- // direction-B ruling (danbri/factoidal#618) as the reason. That
497
- // ruling was about the IKL-to-RDF projection, not this regime
498
- // family, and citing it here misstated a decision the owner did
499
- // not make. The family is the OWNER'S design
500
- // (danbri/factoidal#581); its Lean dispatch was deleted on
501
- // 2026-08-26 as collateral of the projection purge
502
- // (danbri/factoidal#626), not as its target. The semantics
503
- // survived -- Unified/ClBridge.lean's asserted_merge_sound is the
504
- // regime's soundness statement -- so restoring it is a dispatch
505
- // branch, not a redesign. Checked explicitly (not just left out of
506
- // ENTAIL_VALUES below) so a future ENTAIL_VALUES edit can't
507
- // reopen this without deliberately removing this check too; see
508
- // test/select.test.js's regression test.
509
- if (/^x-ikl/i.test(entail)) {
510
- throw new TypeError(
511
- `query: entail '${entail}' is not implemented. The x-ikl-* ` +
512
- "entailment regimes are the owner's design " +
513
- "(danbri/factoidal#581); the Lean engine's dispatch for them " +
514
- 'was deleted on 2026-08-26 as collateral of the IKL-to-RDF ' +
515
- 'projection purge (danbri/factoidal#626). This is not a ' +
516
- 'policy exclusion.');
517
- }
518
- if (!ENTAIL_VALUES.has(entail)) {
519
- throw new TypeError(
520
- `query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
521
- }
522
- const form = sniffQueryForm(sparql);
523
- const docs = toDocs(data, opts);
524
-
525
- // The npm-entry ABI covers all query forms but has no entailment
526
- // parameter; entailment closure stays on the CLI path.
527
- const e = entail === 'none' ? await entry() : null;
528
- if (e) {
529
- // The ABI's dataset handle is N-Quads: normalize non-N-Quads
530
- // documents through parseToDatasetJson first (each document
531
- // separately, preserving per-document blank-node scoping in F*).
532
- let nq = '';
533
- for (const d of docs) {
534
- if (d.ext === 'nq') { nq += d.content; continue; }
535
- const r = entryResult(
536
- e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
537
- 'query(parse)');
538
- nq += r.nquads;
539
- }
540
- // SPARQL 1.2 opt-in: {sparql12:true} or {version:'1.2'} routes to
541
- // the entry's queryDataset12 (tokenize_12 parser: triple-term
542
- // patterns, TRIPLE/isTRIPLE/SUBJECT/PREDICATE/OBJECT, VERSION,
543
- // lang-dir builtins). Default stays SPARQL 1.1, byte-identical.
544
- const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
545
- if (sparql12 && typeof e.queryDataset12 !== 'function') {
546
- throw new Error(
547
- 'SPARQL 1.2 requested but this npm-entry bundle predates ' +
548
- 'queryDataset12 — rebuild build-ocaml.sh js + npm.');
549
- }
550
- const r = await withExtensionRounds(() => entryResult(
551
- (sparql12 ? e.queryDataset12 : e.queryDataset)(nq, sparql), 'query'));
552
- if (r.kind === 'ask') return r.boolean;
553
- if (r.kind === 'construct') {
554
- return Dataset.fromNQuads(r.nquads, {
555
- blankNodePrefix: freshBnodePrefix(),
556
- });
557
- }
558
- return bindingsFromSrj(r.srj);
559
- }
560
-
561
- if (form === 'construct' || form === 'describe') {
562
- throw pendingError(`${form.toUpperCase()} queries`);
563
- }
564
-
565
- const { files, flags } = docsToCliFiles(docs);
566
- const args = [...flags, '-e', sparql, '-o', 'json'];
567
- if (entail !== 'none') args.push('--entail', entail);
568
- const res = await run(args, files);
569
- if (res.exitCode !== 0) throw engineError('query failed', res);
570
- const srj = jsonFromStdout(res.stdout, 'query');
571
- if (form === 'ask' || typeof srj.boolean === 'boolean') {
572
- return !!srj.boolean;
573
- }
574
- return bindingsFromSrj(srj);
575
- }
576
-
577
- /**
578
- * Run a SPARQL 1.1 query against a read-only HDT (Header-Dictionary-
579
- * Triples) artifact's raw bytes -- factoidal_cli.ml's `--data-hdt`
580
- * backend (HDT.Triples.fst and the parser modules around it), driven
581
- * through the same CLI bundle every other function in this file
582
- * uses. No npm-entry bundle needed -- this is a CLI-only capability.
583
- * Default graph only, SELECT/ASK only (no CONSTRUCT, no named graphs
584
- * -- see factoidal_cli.ml's --data-hdt help text).
585
- * @param {Uint8Array|ArrayBuffer|Buffer|string} hdtBytes whole .hdt
586
- * file contents (a string is assumed already packed one-char-per-
587
- * byte, the fake-filesystem convention runCli's `files` expects)
588
- * @param {string} sparql a SELECT or ASK query
589
- * @returns {Promise<Array<Map<string, object>>|boolean>}
590
- */
591
- async function queryHdt(hdtBytes, sparql) {
592
- if (typeof sparql !== 'string') {
593
- throw new TypeError('queryHdt: sparql must be a string');
594
- }
595
- const form = sniffQueryForm(sparql);
596
- if (form === 'construct' || form === 'describe') {
597
- throw new TypeError(
598
- `queryHdt: ${form.toUpperCase()} is not supported over --data-hdt (SELECT/ASK only)`);
599
- }
600
- const content = bytesToLatin1(hdtBytes, 'queryHdt');
601
- const name = `/static/hdt${parseCounter++}.hdt`;
602
- const res = await run(['--data-hdt', name, '-e', sparql, '-o', 'json'], [{ name, content }]);
603
- if (res.exitCode !== 0) throw engineError('queryHdt failed', res);
604
- const srj = jsonFromStdout(res.stdout, 'queryHdt');
605
- if (form === 'ask' || typeof srj.boolean === 'boolean') {
606
- return !!srj.boolean;
607
- }
608
- return bindingsFromSrj(srj);
609
- }
610
-
611
- /**
612
- * Apply a SPARQL 1.1 Update to a dataset, returning the new Dataset.
613
- * Needs the npm-entry bundle.
614
- * @param {Dataset|string|Array} data
615
- * @param {string} updateText
616
- * @param {{format?: string}} [options]
617
- * @returns {Promise<Dataset>}
618
- */
619
- async function update(data, updateText, options) {
620
- if (typeof updateText !== 'string') {
621
- throw new TypeError('update: updateText must be a string');
622
- }
623
- const e = await entry();
624
- if (!e) throw pendingError('SPARQL UPDATE');
625
- const docs = toDocs(data, options);
626
- let nq = '';
627
- for (const d of docs) {
628
- if (d.ext === 'nq') { nq += d.content; continue; }
629
- const r = entryResult(
630
- e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
631
- 'update(parse)');
632
- nq += r.nquads;
633
- }
634
- const opts = options || {};
635
- const sparql12 = opts.sparql12 === true || String(opts.version || '') === '1.2';
636
- if (sparql12 && typeof e.updateDataset12 !== 'function') {
637
- throw new Error(
638
- 'SPARQL 1.2 UPDATE requested but this npm-entry bundle predates ' +
639
- 'updateDataset12 — rebuild build-ocaml.sh js + npm.');
640
- }
641
- const r = entryResult(
642
- (sparql12 ? e.updateDataset12 : e.updateDataset)(nq, updateText), 'update');
643
- return Dataset.fromNQuads(r.nquads, {
644
- blankNodePrefix: freshBnodePrefix(),
645
- });
646
- }
647
-
648
- /**
649
- * Serialize a dataset (engine-produced bytes, sorted N-Quads order).
650
- * @param {Dataset|string|Array} data
651
- * @param {{format?: 'nquads'|'ntriples'|'turtle', inputFormat?: string}} [options]
652
- * @returns {Promise<string>}
653
- * 'turtle' (prefix-compacted, subject-grouped — entry_jsoo.ml's
654
- * serializeTurtle -> RDF_Turtle_Serialize.turtle_of_graph_auto)
655
- * needs the npm-entry bundle and flattens every named graph into
656
- * the default graph (Turtle has no named-graph notion); use
657
- * 'nquads' when graph names must survive.
658
- */
659
- async function serialize(data, options) {
660
- const opts = options || {};
661
- const rawOut = String(opts.format || 'nquads').toLowerCase();
662
- const outFormat = rawOut === 'ttl' ? 'turtle' : rawOut;
663
- if (outFormat !== 'nquads' && outFormat !== 'ntriples' && outFormat !== 'turtle') {
664
- throw new TypeError(
665
- "serialize: format must be 'nquads', 'ntriples', or 'turtle'");
666
- }
667
- const docs = toDocs(data, { format: opts.inputFormat });
668
-
669
- if (outFormat === 'turtle') {
670
- const e = await entry();
671
- if (!e) throw pendingError('Turtle serialization');
672
- requireEntryFn(e, 'serializeTurtle', 'Turtle serialization');
673
- const nq = docsToEntryNQuads(e, docs, 'serialize(turtle)');
674
- return entryResult(e.serializeTurtle(nq), 'serialize').turtle;
675
- }
676
-
677
- if (outFormat === 'nquads') {
678
- const e = await entry();
679
- if (e) {
680
- // Non-N-Quads documents normalize through parseToDatasetJson
681
- // (docsToEntryNQuads), same as the turtle branch above — so
682
- // entry-only drivers (e.g. l4-core.js) serve this path too.
683
- const nq = docs.every((d) => d.ext === 'nq')
684
- ? docs.map((d) => d.content).join('')
685
- : docsToEntryNQuads(e, docs, 'serialize(nquads)');
686
- return entryResult(e.serializeNQuads(nq), 'serialize').nquads;
687
- }
688
- }
689
-
690
- const { files, flags } = docsToCliFiles(docs);
691
- const mode = outFormat === 'nquads' ? '--dump-nq' : '--dump';
692
- const res = await run([mode, ...flags], files);
693
- if (res.exitCode !== 0) throw engineError('serialize failed', res);
694
- return res.stdout;
695
- }
696
-
697
- /**
698
- * RDFC-1.0 canonicalization: canonical blank-node labels + sorted
699
- * canonical N-Quads.
700
- * @param {Dataset|string|Array} data
701
- * @param {{format?: string}} [options]
702
- * @returns {Promise<string>}
703
- */
704
- async function canonicalize(data, options) {
705
- const docs = toDocs(data, options);
706
-
707
- const e = await entry();
708
- if (e) {
709
- let nq = '';
710
- for (const d of docs) {
711
- if (d.ext === 'nq') { nq += d.content; continue; }
712
- const r = entryResult(
713
- e.parseToDatasetJson(d.content, DATA_FORMAT_TAG[d.ext], ''),
714
- 'canonicalize(parse)');
715
- nq += r.nquads;
716
- }
717
- return entryResult(e.canonicalizeToNQuads(nq), 'canonicalize').nquads;
718
- }
719
-
720
- const { files, flags } = docsToCliFiles(docs);
721
- const res = await run(['--canonicalize', ...flags], files);
722
- if (res.exitCode !== 0) {
723
- if (/unknown option/.test(res.stderr || '')) {
724
- throw new Error(
725
- 'canonicalize: this engine bundle predates the --canonicalize ' +
726
- 'flag — pending npm-entry build (rebuild via ' +
727
- 'formal/fstar/build-ocaml.sh js + npm).');
728
- }
729
- throw engineError('canonicalize failed', res);
730
- }
731
- return res.stdout;
732
- }
733
-
734
- /**
735
- * Enumerate the named graphs of an already-parsed Dataset.
736
- *
737
- * Graphs-api design (docs/designissues/2026-07-05-graphs-api-design.md
738
- * section 1.3): DatasetCore.match(null,null,null,graphNode) already
739
- * gives per-graph read access, but match() alone cannot answer "what
740
- * graph names exist" -- this is exactly that enumeration, and only
741
- * that: it walks quads already produced by the F*-verified parser, so
742
- * it needs no engine round-trip (no new RDF/SPARQL semantics, rule
743
- * #11 stays satisfied trivially).
744
- *
745
- * @param {Dataset} dataset
746
- * @returns {Array<[iri: string, graph: Dataset]>}
747
- * default graph excluded, first-seen order.
748
- */
749
- function graphs(dataset) {
750
- if (!(dataset instanceof Dataset)) {
751
- throw new TypeError('graphs: expected a Dataset');
752
- }
753
- const seen = new Map(); // iri -> graph term (first occurrence)
754
- for (const q of dataset) {
755
- const g = q.graph;
756
- if (!g || g.termType === 'DefaultGraph') continue;
757
- if (!seen.has(g.value)) seen.set(g.value, g);
758
- }
759
- const out = [];
760
- for (const [iri, gTerm] of seen) {
761
- out.push([iri, dataset.match(null, null, null, gTerm)]);
762
- }
763
- return out;
764
- }
765
-
766
- /**
767
- * RDFC-1.0 canonical hash of a single graph -- the graph-scoped
768
- * sibling of canonicalize(), gated via capabilities() the same way
769
- * (docs/designissues/2026-07-05-graphs-api-design.md section 1.3).
770
- * Accepts a whole dataset or (more usually) one entry of graphs()'s
771
- * output; either way, every quad's graph component is dropped before
772
- * canonicalizing, matching RDF.Canonical.fst's
773
- * canonicalize_named_graph, which projects the named graph into
774
- * `{ ds_default = g; ds_named = [] }` before reusing
775
- * canonicalize_to_nquads unmodified.
776
- *
777
- * @param {Dataset} datasetOrGraph
778
- * @returns {Promise<string>} canonical N-Quads text for that graph alone.
779
- */
780
- async function canonicalHash(datasetOrGraph) {
781
- if (!(datasetOrGraph instanceof Dataset)) {
782
- throw new TypeError(
783
- 'canonicalHash: expected a Dataset (e.g. one entry of graphs())');
784
- }
785
- const asDefaultGraph = new Dataset(
786
- datasetOrGraph.toArray().map(
787
- (q) => dataFactory.quad(q.subject, q.predicate, q.object)));
788
- return canonicalize(asDefaultGraph.toNQuads(), { format: 'nquads' });
789
- }
790
-
791
- // Some loaded entry bundles predate one of these exports (e.g. an
792
- // older/stale wasm-target build) -- `e` itself is truthy (loadEntry
793
- // succeeded), but `e[fnName]` is undefined, which would otherwise
794
- // surface as a confusing "e.shaclValidate is not a function". Fail
795
- // with the same pendingError() shape callers already expect from a
796
- // missing bundle.
797
- function requireEntryFn(e, fnName, what) {
798
- if (typeof e[fnName] !== 'function') throw pendingError(what);
799
- }
800
-
801
- /**
802
- * SHACL Core validation. Needs the npm-entry bundle (SHACL_Validation
803
- * is only linked into the entry/CLI bundles, not exposed on the
804
- * argv-driven CLI surface today).
805
- * @param {Dataset|string|Array} data
806
- * @param {Dataset|string|Array} shapes
807
- * @param {{format?: string}} [options] applies to both data and shapes
808
- * @returns {Promise<{conforms: boolean, report: Dataset}>}
809
- * report is SHACL_Validation.validation_report_to_graph's graph
810
- * (sh:conforms + one sh:ValidationResult per violation).
811
- */
812
- async function shaclValidate(data, shapes, options) {
813
- const e = await entry();
814
- if (!e) throw pendingError('SHACL validation');
815
- requireEntryFn(e, 'shaclValidate', 'SHACL validation');
816
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'shaclValidate(data)');
817
- const shapesNq = docsToEntryNQuads(e, toDocs(shapes, options), 'shaclValidate(shapes)');
818
- const r = entryResult(e.shaclValidate(dataNq, shapesNq), 'shaclValidate');
819
- return {
820
- conforms: r.conforms,
821
- report: Dataset.fromNQuads(r.reportNquads, {
822
- blankNodePrefix: freshBnodePrefix(),
823
- }),
824
- };
825
- }
826
-
827
- /**
828
- * ShEx (Shape Expressions) validation of one focus node against one
829
- * shape. Needs the npm-entry bundle.
830
- * @param {Dataset|string|Array} data
831
- * @param {string} schemaJson the schema, as text -- either ShExJ (a
832
- * JSON Schema document) or ShExC (the compact human-readable
833
- * syntax; formal/fstar/Parser.ShExC.fst). Dispatch rule: the schema
834
- * text's first non-whitespace character decides the format -- '{'
835
- * means ShExJ, anything else is parsed as ShExC. No separate flag
836
- * or file-extension hint is needed; a schema whose ShExC text
837
- * happens to start with whitespace then '{' would misdetect, but no
838
- * valid ShExC document starts that way (ShExC always opens with a
839
- * directive keyword, a shape label, or START).
840
- * @param {string|{termType,value}} focus an IRI, "_:label", or an
841
- * RDF/JS NamedNode/BlankNode term
842
- * @param {string|{termType,value}|null} [shape] a shape label (same
843
- * shapes as focus); omit/null to validate against the schema's own `start`
844
- * @param {{format?: string}} [options]
845
- * @returns {Promise<boolean|null>} null means "deferred" -- outside
846
- * this engine's decidable ShEx fragment, never a guessed answer
847
- * (see formal/fstar/ShEx.Validation.fst's file header).
848
- */
849
- async function shexValidate(data, schemaJson, focus, shape, options) {
850
- const e = await entry();
851
- if (!e) throw pendingError('ShEx validation');
852
- requireEntryFn(e, 'shexValidate', 'ShEx validation');
853
- if (typeof schemaJson !== 'string') {
854
- throw new TypeError('shexValidate: schemaJson must be a string');
855
- }
856
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'shexValidate(data)');
857
- const focusStr = shexTermToString(focus, 'shexValidate(focus)');
858
- const shapeStr = shape == null ? '' : shexTermToString(shape, 'shexValidate(shape)');
859
- const r = entryResult(
860
- e.shexValidate(dataNq, schemaJson, focusStr, shapeStr), 'shexValidate');
861
- return r.verdict;
862
- }
863
-
864
- const OWL_CLOSURE_MODES = { rdfs: 'RDFS', 'owl-rl': 'OWL-RL', owlrl: 'OWL-RL', owl_rl: 'OWL-RL' };
865
-
866
- function normalizeClosureMode(mode) {
867
- const canonical = OWL_CLOSURE_MODES[String(mode || '').toLowerCase()];
868
- if (!canonical) {
869
- throw new TypeError(
870
- `owlClosure: mode must be 'RDFS' or 'OWL-RL' (got '${mode}')`);
871
- }
872
- return canonical;
873
- }
874
-
875
- /**
876
- * RDFS or OWL-RL entailment closure, materialized as a new Dataset
877
- * (input triples + derived triples). Needs the npm-entry bundle.
878
- * Scope cut: only the default graph is closed over (same cut
879
- * fn.js's entail() documents for its own CLI-path implementation).
880
- * @param {Dataset|string|Array} data
881
- * @param {'RDFS'|'OWL-RL'} mode
882
- * @param {{format?: string}} [options]
883
- * @returns {Promise<Dataset>}
884
- */
885
- async function owlClosure(data, mode, options) {
886
- const e = await entry();
887
- if (!e) throw pendingError('OWL/RDFS closure');
888
- requireEntryFn(e, 'owlClosure', 'OWL/RDFS closure');
889
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'owlClosure(data)');
890
- const r = entryResult(
891
- e.owlClosure(dataNq, normalizeClosureMode(mode)), 'owlClosure');
892
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
893
- }
894
-
895
- /**
896
- * The CERTIFIED core-RDFS closure (RDF.Entailment.RDFS.
897
- * RhoDFClosure.fst's `rho_df_closure`): rdfs2/3/5/7/9/11 only, with
898
- * the machine-checked decides-iff (docs/theorem-registry.md).
899
- * "corerdfs" is this project's API name for the fragment the
900
- * literature calls ρdf — subPropertyOf/subClassOf/type/domain/range,
901
- * per Muñoz, Pérez & Gutierrez, "Simple and Efficient Minimal
902
- * RDFS", J. Web Semantics 7(3), 2009. `rhoDfClosure` remains as an
903
- * alias so code can be grepped against the theorem registry.
904
- * Returns the raw certified result, not a Dataset: the N-Triples
905
- * text is the object the theorems talk about.
906
- * @param {Dataset|string|Array} data
907
- * @param {{format?: string}} [options]
908
- * @returns {Promise<{ok: boolean, ntriples: string}>}
909
- */
910
- async function coreRdfsClosure(data, options) {
911
- const e = await entry();
912
- if (!e) throw pendingError('certified core-RDFS closure');
913
- requireEntryFn(e, 'rhoDfClosure', 'certified core-RDFS closure');
914
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'coreRdfsClosure(data)');
915
- return entryResult(e.rhoDfClosure(dataNq), 'coreRdfsClosure');
916
- }
917
- /** Literature-name alias for coreRdfsClosure (ρdf; see above). */
918
- const rhoDfClosure = coreRdfsClosure;
919
-
920
- /**
921
- * Decidable core-RDFS fragment check (`is_rho_df_frag`, tied by an
922
- * F* lemma to the prop the regime theorems quantify over): does the
923
- * certified path's guarantee apply to this data? Naming: see
924
- * coreRdfsClosure above. `rhoDfFragmentCheck` remains as an alias.
925
- * @param {Dataset|string|Array} data
926
- * @param {{format?: string}} [options]
927
- * @returns {Promise<{ok: boolean, fragment: boolean}>}
928
- */
929
- async function coreRdfsCheck(data, options) {
930
- const e = await entry();
931
- if (!e) throw pendingError('core-RDFS fragment check');
932
- requireEntryFn(e, 'rhoDfFragmentCheck', 'core-RDFS fragment check');
933
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'coreRdfsCheck(data)');
934
- return entryResult(e.rhoDfFragmentCheck(dataNq), 'coreRdfsCheck');
935
- }
936
- /** Literature-name alias for coreRdfsCheck (ρdf; see above). */
937
- const rhoDfFragmentCheck = coreRdfsCheck;
938
-
939
- /**
940
- * RDFS-Plus closure (RDF.Entailment.RDFSPlus.fst's
941
- * `rdfs_plus_closure`): the full RDFS step plus the practical OWL
942
- * subset -- owl:sameAs (symmetry/transitivity/substitution),
943
- * owl:inverseOf, Symmetric/Transitive/Functional/
944
- * InverseFunctionalProperty, equivalentClass/Property. The tier the
945
- * literature calls RDFS-Plus (Allemang & Hendler, "Semantic Web for
946
- * the Working Ontologist", 2008) or RDFS++ (AllegroGraph). Claim
947
- * level, weaker than coreRdfsClosure's and stated exactly: every OWL
948
- * row runs under a PROVED licensing + truth lemma (per-rule
949
- * certificates in the theorem registry); no chain-level completeness
950
- * is claimed -- owl:sameAs equality breaks the Herbrand argument the
951
- * corerdfs completeness theorem uses.
952
- * @param {Dataset|string|Array} data
953
- * @param {{format?: string}} [options]
954
- * @returns {Promise<{ok: boolean, ntriples: string, rounds: number}>}
955
- */
956
- async function rdfsPlusClosure(data, options) {
957
- const e = await entry();
958
- if (!e) throw pendingError('RDFS-Plus closure');
959
- requireEntryFn(e, 'rdfsPlusClosure', 'RDFS-Plus closure');
960
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'rdfsPlusClosure(data)');
961
- return entryResult(e.rdfsPlusClosure(dataNq), 'rdfsPlusClosure');
962
- }
963
-
964
- /**
965
- * OWL tableau materialisation (formal/fstar/Tableau.fst's
966
- * `tableau_materialise`): add `i rdf:type <ClassExpression>` for
967
- * every individual the model-construction reasoner can prove is a
968
- * member of an OWL class expression (someValuesFrom / hasValue /
969
- * unionOf / intersectionOf, and the named class an equivalentClass
970
- * restriction defines). This is the same F* function the SPARQL 1.1
971
- * entailment-regime suite runs under the DL regime. Needs the
972
- * npm-entry bundle. Default graph only (same scope cut as owlClosure).
973
- * @param {Dataset|string|Array} data
974
- * @param {{format?: string}} [options]
975
- * @returns {Promise<{dataset: Dataset, addedCount: number}>}
976
- * dataset is input + tableau-derived triples; addedCount is how many
977
- * the tableau added.
978
- */
979
- async function tableauMaterialise(data, options) {
980
- const e = await entry();
981
- if (!e) throw pendingError('OWL tableau materialisation');
982
- requireEntryFn(e, 'tableauMaterialise', 'OWL tableau materialisation');
983
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'tableauMaterialise(data)');
984
- const r = entryResult(e.tableauMaterialise(dataNq), 'tableauMaterialise');
985
- return {
986
- dataset: Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() }),
987
- addedCount: r.addedCount,
988
- };
989
- }
990
-
991
- /**
992
- * OWL DL inconsistency verdict. Replays bin/owl-runner's DL pipeline
993
- * (OWL-RL closure -> Tableau.tableau_materialise -> OWL-RL closure ->
994
- * is_inconsistent). `rlAlone` is the plain OWL-RL verdict on the same
995
- * input, so a caller can see the DL>=RL cases the tableau adds: a
996
- * disjointness clash reached only after the tableau materialises a
997
- * restriction membership the Datalog closure never derives. Needs the
998
- * npm-entry bundle. Default graph only.
999
- * @param {Dataset|string|Array} data
1000
- * @param {{format?: string}} [options]
1001
- * @returns {Promise<{inconsistent: boolean, rlAlone: boolean}>}
1002
- */
1003
- async function tableauDlInconsistent(data, options) {
1004
- const e = await entry();
1005
- if (!e) throw pendingError('OWL tableau DL inconsistency check');
1006
- requireEntryFn(e, 'tableauDlInconsistent', 'OWL tableau DL inconsistency check');
1007
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'tableauDlInconsistent(data)');
1008
- const r = entryResult(e.tableauDlInconsistent(dataNq), 'tableauDlInconsistent');
1009
- return { inconsistent: r.inconsistent, rlAlone: r.rlAlone };
1010
- }
1011
-
1012
- /**
1013
- * OWL DL consistency verdict via the verified clash-detecting tableau
1014
- * (formal/fstar/Tableau.Refute.fst's `tableau_consistent` over the
1015
- * OWL-RL closure -- the same pure verified chain bin/owl-runner runs
1016
- * under `--regime dl`, minus its native-only z3 counting oracle, which
1017
- * the JS bundle cannot spawn). Needs the npm-entry bundle. Default
1018
- * graph only (same scope cut as tableauDlInconsistent).
1019
- *
1020
- * Three-valued and honest: `consistent` is `false` (a clash on every
1021
- * tableau branch), `true` (a model was constructed with no clash), or
1022
- * `null` -- the refuter ran out of budget before deciding, with
1023
- * `reason` naming the fuel cap. `null` is never collapsed to `false`.
1024
- *
1025
- * @param {Dataset|string|Array} data the ontology + ABox graph
1026
- * @param {{format?: string, fuel?: number|string}} [options] format
1027
- * parses `data` (default 'turtle'); fuel overrides the refutation
1028
- * budget (default 20000).
1029
- * @returns {Promise<{consistent: boolean|null, reason?: string}>}
1030
- */
1031
- async function owlIsConsistent(data, options) {
1032
- const e = await entry();
1033
- if (!e) throw pendingError('OWL DL consistency check');
1034
- requireEntryFn(e, 'owlIsConsistent', 'OWL DL consistency check');
1035
- const opts = options || {};
1036
- const dataNq = docsToEntryNQuads(e, toDocs(data, opts), 'owlIsConsistent(data)');
1037
- const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
1038
- const r = entryResult(e.owlIsConsistent(dataNq, optsJson), 'owlIsConsistent');
1039
- return r.reason === undefined
1040
- ? { consistent: r.consistent }
1041
- : { consistent: r.consistent, reason: r.reason };
1042
- }
1043
-
1044
- /**
1045
- * OWL entailment check: does `premise` entail `conclusion`? Two
1046
- * verified paths, mirroring bin/owl-runner's PositiveEntailment
1047
- * dispatch: `via: "closure"` when every conclusion triple is in the
1048
- * OWL-RL closure of the premise; `via: "refutation"` when the negated
1049
- * conclusion (Tableau.Refute's `negation_goals`) is refuted on every
1050
- * goal by the clash-detecting tableau. Needs the npm-entry bundle.
1051
- * Default graph only. Verified-only chain (no z3).
1052
- *
1053
- * Three-valued: `entailed` is `true`, `false`, or `null` (a refutation
1054
- * goal exhausted its fuel budget -- indeterminate, never a silent
1055
- * `false`; `reason` names the cap).
1056
- *
1057
- * @param {Dataset|string|Array} premise
1058
- * @param {Dataset|string|Array} conclusion
1059
- * @param {{format?: string, fuel?: number|string}} [options] format
1060
- * parses both graphs (default 'turtle'); fuel overrides the
1061
- * refutation budget (default 20000).
1062
- * @returns {Promise<{entailed: boolean|null, via: 'closure'|'refutation', reason?: string}>}
1063
- */
1064
- async function owlEntails(premise, conclusion, options) {
1065
- const e = await entry();
1066
- if (!e) throw pendingError('OWL entailment check');
1067
- requireEntryFn(e, 'owlEntails', 'OWL entailment check');
1068
- const opts = options || {};
1069
- const premiseNq = docsToEntryNQuads(e, toDocs(premise, opts), 'owlEntails(premise)');
1070
- const conclusionNq = docsToEntryNQuads(e, toDocs(conclusion, opts), 'owlEntails(conclusion)');
1071
- const optsJson = JSON.stringify(opts.fuel != null ? { fuel: String(opts.fuel) } : {});
1072
- const r = entryResult(e.owlEntails(premiseNq, conclusionNq, optsJson), 'owlEntails');
1073
- return r.reason === undefined
1074
- ? { entailed: r.entailed, via: r.via }
1075
- : { entailed: r.entailed, via: r.via, reason: r.reason };
1076
- }
1077
-
1078
- /**
1079
- * Evaluate an RML mapping graph against one logical source's raw
1080
- * data, returning the generated triples as a Dataset. Needs the
1081
- * npm-entry bundle. Scope cut (documented, not silent): every
1082
- * triples map in `mapping` reads the SAME `sourceData` -- joins
1083
- * across two DIFFERENT logical sources are not reachable through
1084
- * this one-source entry point (see bin/rml-runner/rml_runner.ml for
1085
- * the full multi-source join driver).
1086
- * @param {Dataset|string|Array} mapping the RML mapping graph (Turtle by default)
1087
- * @param {string} sourceData raw JSON or CSV text (not RDF)
1088
- * @param {'json'|'csv'} sourceKind
1089
- * @param {{format?: string}} [options] applies to `mapping`
1090
- * @returns {Promise<Dataset>}
1091
- */
1092
- async function rmlMap(mapping, sourceData, sourceKind, options) {
1093
- const e = await entry();
1094
- if (!e) throw pendingError('RML mapping evaluation');
1095
- requireEntryFn(e, 'rmlMap', 'RML mapping evaluation');
1096
- if (typeof sourceData !== 'string') {
1097
- throw new TypeError('rmlMap: sourceData must be a string');
1098
- }
1099
- const kind = String(sourceKind || '').toLowerCase();
1100
- if (kind !== 'json' && kind !== 'csv') {
1101
- throw new TypeError("rmlMap: sourceKind must be 'json' or 'csv'");
1102
- }
1103
- const mappingNq = docsToEntryNQuads(e, toDocs(mapping, options), 'rmlMap(mapping)');
1104
- const r = entryResult(e.rmlMap(mappingNq, sourceData, kind), 'rmlMap');
1105
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1106
- }
1107
-
1108
- /**
1109
- * CSVW csv2rdf conversion (w3.org/TR/csv2rdf): convert tabular data
1110
- * plus an optional CSVW metadata document into a Dataset. Needs the
1111
- * npm-entry bundle. Scope cut (documented, not silent -- mirrors
1112
- * rmlMap's one-source cut): every table in a multi-table `tables`
1113
- * group reads the SAME `csvText`. Datatype `format` facets,
1114
- * list-valued (`separator`) cells, and full inherited-property
1115
- * propagation are not yet implemented -- see
1116
- * docs/designissues/2026-07-05-csvw-program-plan.md for measured
1117
- * coverage.
1118
- * @param {string} csvText raw RFC 4180 tabular data (not RDF)
1119
- * @param {string} [metadataJson] CSVW metadata document (JSON text);
1120
- * '' / omitted infers the schema from the CSV's own header row
1121
- * @param {{mode?: 'standard'|'minimal', base?: string, url?: string}}
1122
- * [options] mode defaults to 'standard' (full csvw:TableGroup/
1123
- * Table/Row wrapper); base is the resolution base IRI (default
1124
- * 'file:///'); url is the tabular file's own URL used when the
1125
- * metadata carries none (default 'table.csv') -- cell predicates
1126
- * default to `<tableUrl>#<colName>`, so url shapes every emitted
1127
- * predicate IRI.
1128
- * @returns {Promise<Dataset>}
1129
- */
1130
- async function csvwToRdf(csvText, metadataJson, options) {
1131
- if (typeof csvText !== 'string') {
1132
- throw new TypeError('csvwToRdf: csvText must be a string');
1133
- }
1134
- const meta = metadataJson == null ? '' : metadataJson;
1135
- if (typeof meta !== 'string') {
1136
- throw new TypeError('csvwToRdf: metadataJson must be a string');
1137
- }
1138
- const e = await entry();
1139
- if (!e) throw pendingError('CSVW csv2rdf conversion');
1140
- requireEntryFn(e, 'csvwToRdf', 'CSVW csv2rdf conversion');
1141
- const opts = options || {};
1142
- const optionsJson = JSON.stringify({
1143
- ...(opts.mode ? { mode: String(opts.mode).toLowerCase() } : {}),
1144
- ...(opts.base ? { base: opts.base } : {}),
1145
- ...(opts.url ? { url: opts.url } : {}),
1146
- });
1147
- const r = entryResult(e.csvwToRdf(csvText, meta, optionsJson), 'csvwToRdf');
1148
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1149
- }
1150
-
1151
- /**
1152
- * Parse a JSON-LD document into a Dataset, with JSON-LD-specific
1153
- * options `parse()` has no room for. Needs the npm-entry bundle
1154
- * (plain `parse(text, {format:'jsonld'})` also works now -- see
1155
- * bin/npm-entry/entry_jsoo.ml's parseToDatasetJson JSON-LD fix --
1156
- * this exists for callers that need rdfDirection/expandContext/
1157
- * processingMode).
1158
- * @param {string} jsonldText
1159
- * @param {{base?: string, rdfDirection?: string, expandContext?: string,
1160
- * processingMode?: string}} [options]
1161
- * @returns {Promise<Dataset>}
1162
- */
1163
- async function jsonldToRdf(jsonldText, options) {
1164
- if (typeof jsonldText !== 'string') {
1165
- throw new TypeError('jsonldToRdf: jsonldText must be a string');
1166
- }
1167
- const e = await entry();
1168
- if (!e) throw pendingError('jsonldToRdf');
1169
- requireEntryFn(e, 'jsonldToRdf', 'jsonldToRdf');
1170
- const opts = options || {};
1171
- const optionsJson = JSON.stringify({
1172
- ...(opts.base ? { base: opts.base } : {}),
1173
- ...(opts.rdfDirection ? { rdfDirection: opts.rdfDirection } : {}),
1174
- ...(opts.expandContext ? { expandContext: opts.expandContext } : {}),
1175
- ...(opts.processingMode ? { processingMode: opts.processingMode } : {}),
1176
- });
1177
- const r = entryResult(e.jsonldToRdf(jsonldText, optionsJson), 'jsonldToRdf');
1178
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1179
- }
1180
-
1181
- /**
1182
- * RIF Core forward-chaining saturation, materialized as a new
1183
- * Dataset (input triples + derived triples, default graph only --
1184
- * RIF Core has no named-graph notion). Needs the npm-entry bundle.
1185
- * @param {Dataset|string|Array} data the premise graph
1186
- * @param {string} rifRulesXml a RIF Core XML rule document
1187
- * @param {{format?: string}} [options] applies to `data`
1188
- * @returns {Promise<Dataset>}
1189
- */
1190
- async function rifEval(data, rifRulesXml, options) {
1191
- const e = await entry();
1192
- if (!e) throw pendingError('RIF Core evaluation');
1193
- requireEntryFn(e, 'rifEval', 'RIF Core evaluation');
1194
- if (typeof rifRulesXml !== 'string') {
1195
- throw new TypeError('rifEval: rifRulesXml must be a string');
1196
- }
1197
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'rifEval(data)');
1198
- const r = entryResult(e.rifEval(rifRulesXml, dataNq), 'rifEval');
1199
- return Dataset.fromNQuads(r.saturatedNquads, {
1200
- blankNodePrefix: freshBnodePrefix(),
1201
- });
1202
- }
1203
-
1204
- /**
1205
- * Serialize an RDF dataset as an expanded-form JSON-LD document --
1206
- * the reverse of jsonldToRdf (entry_jsoo.ml's jsonldFromRdf export ->
1207
- * the verified JSONLD.FromRdf.from_rdf). Returns the parsed JSON-LD
1208
- * value (an array of node objects). Needs the npm-entry bundle.
1209
- * @param {Dataset|string|Array} data
1210
- * @param {{useNativeTypes?:boolean,useRdfType?:boolean,format?:string}} [options]
1211
- * @returns {Promise<any>} the JSON-LD document (JCS-canonical, parsed)
1212
- */
1213
- async function jsonldFromRdf(data, options) {
1214
- const e = await entry();
1215
- if (!e) throw pendingError('jsonldFromRdf');
1216
- requireEntryFn(e, 'jsonldFromRdf', 'jsonldFromRdf');
1217
- const dataNq = docsToEntryNQuads(e, toDocs(data, options), 'jsonldFromRdf(data)');
1218
- const opts = options || {};
1219
- const optionsJson = JSON.stringify({
1220
- ...(opts.useNativeTypes ? { useNativeTypes: true } : {}),
1221
- ...(opts.useRdfType ? { useRdfType: true } : {}),
1222
- });
1223
- const r = entryResult(e.jsonldFromRdf(dataNq, optionsJson), 'jsonldFromRdf');
1224
- return JSON.parse(r.jsonld);
1225
- }
1226
-
1227
- /**
1228
- * did:key resolution (entry_jsoo.ml's didKeyResolve export -> the
1229
- * verified DID_Key.did_key_document). Resolves a did:key:z6Mk...
1230
- * (Ed25519) to its DID Document, returned as a Dataset. Needs the
1231
- * npm-entry bundle.
1232
- * @param {string} didString a did:key URI
1233
- * @returns {Promise<Dataset>} the DID Document as RDF
1234
- */
1235
- async function didKeyResolve(didString) {
1236
- if (typeof didString !== 'string') {
1237
- throw new TypeError('didKeyResolve: didString must be a string');
1238
- }
1239
- const e = await entry();
1240
- if (!e) throw pendingError('didKeyResolve');
1241
- requireEntryFn(e, 'didKeyResolve', 'did:key resolution');
1242
- const r = entryResult(e.didKeyResolve(didString), 'didKeyResolve');
1243
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
1244
- }
1245
-
1246
- /**
1247
- * Test whether an XML document is well-formed (entry_jsoo.ml's
1248
- * xmlWellformed export -> Parser_XML.parse_xml_document, the
1249
- * accept/reject signal bin/xml-runner drives against W3C xmlconf).
1250
- * The byte-oriented parser has no DOCTYPE/DTD production, so a
1251
- * document containing a DOCTYPE reports false. Needs the npm-entry
1252
- * bundle.
1253
- * @param {string} xmlText
1254
- * @returns {Promise<boolean>}
1255
- */
1256
- async function xmlWellformed(xmlText) {
1257
- if (typeof xmlText !== 'string') {
1258
- throw new TypeError('xmlWellformed: xmlText must be a string');
1259
- }
1260
- const e = await entry();
1261
- if (!e) throw pendingError('xmlWellformed');
1262
- requireEntryFn(e, 'xmlWellformed', 'XML well-formedness');
1263
- const r = entryResult(e.xmlWellformed(xmlText), 'xmlWellformed');
1264
- return !!r.wellformed;
1265
- }
1266
-
1267
- /**
1268
- * Evaluate an XPath 1.0 expression over an XML document (entry_jsoo.ml's
1269
- * xpathEval export -> XPath_Eval.eval_xpath_from_root). Returns the
1270
- * result envelope: `resultType` ('nodeset'|'string'|'number'|'boolean')
1271
- * plus, for a node-set, `count`/`stringValue`/`nodes`, else a scalar
1272
- * `value`. Needs the npm-entry bundle.
1273
- * @param {string} xmlText
1274
- * @param {string} xpathExpr
1275
- * @returns {Promise<object>}
1276
- */
1277
- async function xpathEval(xmlText, xpathExpr) {
1278
- if (typeof xmlText !== 'string' || typeof xpathExpr !== 'string') {
1279
- throw new TypeError('xpathEval: xmlText and xpathExpr must be strings');
1280
- }
1281
- const e = await entry();
1282
- if (!e) throw pendingError('xpathEval');
1283
- requireEntryFn(e, 'xpathEval', 'XPath evaluation');
1284
- return entryResult(e.xpathEval(xmlText, xpathExpr), 'xpathEval');
1285
- }
1286
-
1287
- /**
1288
- * Parse Common Logic Interchange Format text (ISO/IEC 24707:2018),
1289
- * with the IKL `that`-operator extension (entry_jsoo.ml's clParse
1290
- * export -> L4Factoidal's CL/Clif.lean reader). Reads CLIF into a CL
1291
- * syntax tree and reports its shape; it never produces RDF -- the
1292
- * IKL-to-RDF projection that used to accompany it is deleted
1293
- * (danbri/factoidal#626). Lean 4 only: formal/fstar has no CL/IKL parser, so this
1294
- * function is absent from index.js/wasm.js -- see capabilities() /
1295
- * factoidal/select's capability table.
1296
- * @param {string} clifText
1297
- * @returns {Promise<{ok: boolean, sentences: number, pureCL: boolean,
1298
- * normalized: string}>} `pureCL` is a DIALECT flag, not a validity
1299
- * or quality signal: true while the text stays inside ISO/IEC
1300
- * 24707 Common Logic, false once it uses IKL's `that` operator.
1301
- * Both values are returned only for text that parsed; a CLIF text
1302
- * that fails to parse rejects instead (e.g. a bare `(that S)` used
1303
- * as a proposition rather than a term -- see the GUIDE).
1304
- */
1305
- async function clParse(clifText) {
1306
- if (typeof clifText !== 'string') {
1307
- throw new TypeError('clParse: clifText must be a string');
1308
- }
1309
- const e = await entry();
1310
- if (!e) throw pendingError('clParse');
1311
- requireEntryFn(e, 'clParse', 'Common Logic / IKL parse');
1312
- return entryResult(e.clParse(clifText), 'clParse');
1313
- }
1314
-
1315
- /**
1316
- * Read Common Logic Interchange Format text and write it back out in
1317
- * the canonical spacing of the CLIF writer (entry_jsoo.ml's
1318
- * clSerialize export -> L4Factoidal's CL/Clif.lean reader/writer
1319
- * pair). Lean 4 only: formal/fstar has no CL/IKL parser, so this
1320
- * function is absent from index.js/wasm.js -- see capabilities() /
1321
- * factoidal/select's capability table.
1322
- *
1323
- * `roundTripProved` is always `false`. The round-trip lemma
1324
- * `clif_roundTrip` (`CL/ClifAdequacy.lean`) is an OPEN lemma: the
1325
- * fragment boundary `marksLexable` is MEASURED, not proved. The
1326
- * field is in the envelope, unmodified, so a caller does not have to
1327
- * go and find that out.
1328
- * @param {string} clifText
1329
- * @returns {Promise<{ok: boolean, clif: string, sentences: number,
1330
- * roundTripProved: false}>}
1331
- */
1332
- async function clSerialize(clifText) {
1333
- if (typeof clifText !== 'string') {
1334
- throw new TypeError('clSerialize: clifText must be a string');
1335
- }
1336
- const e = await entry();
1337
- if (!e) throw pendingError('clSerialize');
1338
- requireEntryFn(e, 'clSerialize', 'Common Logic / IKL serialize');
1339
- return entryResult(e.clSerialize(clifText), 'clSerialize');
1340
- }
1341
-
1342
- /**
1343
- * Alpha-normalise Common Logic Interchange Format text: the canonical
1344
- * representative of each sentence's bound-variable-renaming
1345
- * equivalence class (entry_jsoo.ml's clAlphaNorm export ->
1346
- * L4Factoidal's `CL/Alpha.lean`, `Sentence.alphaNorm`). Bound names
1347
- * become `v1`, `v2`, ... in traversal order, so two sentences that
1348
- * differ only in bound-variable names produce byte-identical output
1349
- * -- IKL GUIDE Appendix B condition (1): renaming a bound variable
1350
- * does not change the proposition expressed. Lean 4 only: formal/fstar
1351
- * has no CL/IKL parser, so this function is absent from
1352
- * index.js/wasm.js -- see capabilities() / factoidal/select's
1353
- * capability table.
1354
- * @param {string} clifText
1355
- * @returns {Promise<{ok: boolean, clif: string, sentences: number}>}
1356
- */
1357
- async function clAlphaNorm(clifText) {
1358
- if (typeof clifText !== 'string') {
1359
- throw new TypeError('clAlphaNorm: clifText must be a string');
1360
- }
1361
- const e = await entry();
1362
- if (!e) throw pendingError('clAlphaNorm');
1363
- requireEntryFn(e, 'clAlphaNorm', 'Common Logic / IKL alpha-normalise');
1364
- return entryResult(e.clAlphaNorm(clifText), 'clAlphaNorm');
1365
- }
1366
-
1367
- /**
1368
- * Hayes's satisfiability-preserving reduction of IKL to Common Logic
1369
- * (entry_jsoo.ml's clNormalize export -> L4Factoidal's
1370
- * `CL/Normalize.lean`, `normalizeText`; danbri/factoidal#625), over a
1371
- * whole text: one head text and one shared tail, with the
1372
- * proposition-name counter running across the text. Lean 4 only:
1373
- * formal/fstar has no CL/IKL parser, so this function is absent from
1374
- * index.js/wasm.js -- see capabilities() / factoidal/select's
1375
- * capability table.
1376
- *
1377
- * Two limits, both real, both in the answer, neither hidden:
1378
- * - `preserves: "satisfiability"` -- the reduction preserves
1379
- * satisfiability, NOT equivalence. It suits entailment and
1380
- * consistency testing; it is not a transformation to apply to
1381
- * data you intend to keep.
1382
- * - `noIntrusion` IS the proof hypothesis `CL.noIntrSs [] []`
1383
- * decides, not a paraphrase of it. The transformation runs either
1384
- * way; when `noIntrusion` is `false`, the output is still
1385
- * produced, but `tails_satisfiable` / `normalize_preserves` do
1386
- * not cover that case.
1387
- * @param {string} clifText
1388
- * @returns {Promise<{ok: boolean, head: string[], tail: string[],
1389
- * clif: string, sentences: number, thatCount: number,
1390
- * noIntrusion: boolean, preserves: 'satisfiability',
1391
- * provedUnder: string}>}
1392
- */
1393
- async function clNormalize(clifText) {
1394
- if (typeof clifText !== 'string') {
1395
- throw new TypeError('clNormalize: clifText must be a string');
1396
- }
1397
- const e = await entry();
1398
- if (!e) throw pendingError('clNormalize');
1399
- requireEntryFn(e, 'clNormalize', 'Common Logic / IKL normalize');
1400
- return entryResult(e.clNormalize(clifText), 'clNormalize');
1401
- }
1402
-
1403
- // clFiniteSat (entry_jsoo.ml's clFiniteSat -> L4Factoidal's
1404
- // CL/FiniteSatTheorems.lean) is DEFERRED, not excluded, from this
1405
- // typed layer (owner decision, 2026-08-26): it takes a caller-supplied
1406
- // finite-interpretation JSON encoding (see Wasm/Ops/CL.lean's header
1407
- // for the wire format) that has no user yet, and a typed wrapper here
1408
- // would freeze that shape before anyone knows whether it is right. It
1409
- // stays reachable through the raw dispatch ABI (`l4.call('clFiniteSat',
1410
- // [interpJson, clifText])` / factoidal/select's `call('clFiniteSat',
1411
- // ...)`), which needs no shape commitment on this layer.
1412
-
1413
- // -----------------------------------------------------------------
1414
- // VC Data Integrity crypto (eddsa-rdfc-2022) — HACL* wasm backend.
1415
- // entry_jsoo.ml's vc* exports realise VC_DataIntegrity's four crypto
1416
- // assume vals via HACL*'s OWN official WebAssembly build. The wasm
1417
- // backend MUST be initialised before the first primitive runs, or
1418
- // the assume-val stub throws and `guarded` returns {ok:false} — a
1419
- // verify NEVER silently succeeds uninitialised (#286, the
1420
- // throw-on-uninit contract, which entryResult() preserves by
1421
- // throwing on {ok:false}).
1422
- //
1423
- // Init story (Node js + wasm engines): these wrappers AUTO-AWAIT
1424
- // initHacl() on the first VC call, via the optional driver.initCrypto
1425
- // hook (idempotent + memoized) — a caller never has to remember the
1426
- // init step. If the driver supplies no initCrypto hook (or init
1427
- // rejects), the primitive's own honest {ok:false} error surfaces
1428
- // rather than a false "valid". Browsers can't auto-init (the
1429
- // hacl-wasm URL is page-specific) — browser.js documents explicit
1430
- // init there. See skills/node-crypto-haclstar-vc-wasm-build.
1431
- // -----------------------------------------------------------------
1432
-
1433
- let cryptoInitPromise;
1434
- async function ensureCrypto() {
1435
- if (typeof driver.initCrypto !== 'function') return;
1436
- if (!cryptoInitPromise) {
1437
- cryptoInitPromise = Promise.resolve()
1438
- .then(() => driver.initCrypto())
1439
- .catch((err) => { cryptoInitPromise = undefined; throw err; });
1440
- }
1441
- await cryptoInitPromise;
1442
- }
1443
-
1444
- async function vcEntry(fnName, what) {
1445
- const e = await entry();
1446
- if (!e) throw pendingError(what);
1447
- requireEntryFn(e, fnName, what);
1448
- await ensureCrypto();
1449
- return e;
1450
- }
1451
-
1452
- /**
1453
- * SHA-256 of a message string's bytes, as a lowercase hex digest
1454
- * (entry_jsoo.ml's vcSha256Hex -> VC_DataIntegrity.hash_sha256_hex,
1455
- * HACL* SHA-2). Needs the npm-entry bundle + the HACL* wasm backend
1456
- * (auto-initialised).
1457
- * @param {string} message the message whose bytes are hashed
1458
- * @returns {Promise<string>} 64-char hex digest
1459
- */
1460
- async function vcSha256Hex(message) {
1461
- if (typeof message !== 'string') {
1462
- throw new TypeError('vcSha256Hex: message must be a string');
1463
- }
1464
- const e = await vcEntry('vcSha256Hex', 'VC SHA-256');
1465
- return entryResult(e.vcSha256Hex(message), 'vcSha256Hex').sha256;
1466
- }
1467
-
1468
- /**
1469
- * Derive the Ed25519 public key from a 32-byte secret key
1470
- * (entry_jsoo.ml's vcEd25519SecretToPublic -> HACL* Ed25519).
1471
- * @param {string} secretKeyHex 32-byte secret key, hex
1472
- * @returns {Promise<string>} 32-byte public key, hex
1473
- */
1474
- async function vcEd25519SecretToPublic(secretKeyHex) {
1475
- if (typeof secretKeyHex !== 'string') {
1476
- throw new TypeError('vcEd25519SecretToPublic: secretKeyHex must be a string');
1477
- }
1478
- const e = await vcEntry('vcEd25519SecretToPublic', 'VC Ed25519 key derivation');
1479
- return entryResult(
1480
- e.vcEd25519SecretToPublic(secretKeyHex), 'vcEd25519SecretToPublic').publicKeyHex;
1481
- }
1482
-
1483
- /**
1484
- * Ed25519 signature over a hex-encoded message (entry_jsoo.ml's
1485
- * vcEd25519Sign -> HACL* Ed25519).
1486
- * @param {string} secretKeyHex 32-byte secret key, hex
1487
- * @param {string} messageHex the message to sign, hex
1488
- * @returns {Promise<string>} 64-byte signature, hex
1489
- */
1490
- async function vcEd25519Sign(secretKeyHex, messageHex) {
1491
- if (typeof secretKeyHex !== 'string' || typeof messageHex !== 'string') {
1492
- throw new TypeError('vcEd25519Sign: secretKeyHex and messageHex must be strings');
1493
- }
1494
- const e = await vcEntry('vcEd25519Sign', 'VC Ed25519 sign');
1495
- return entryResult(e.vcEd25519Sign(secretKeyHex, messageHex), 'vcEd25519Sign').signatureHex;
1496
- }
1497
-
1498
- /**
1499
- * Ed25519 verification (entry_jsoo.ml's vcEd25519Verify -> HACL*
1500
- * Ed25519). A wrong key, tampered signature, altered message, or a
1501
- * malformed-length input all return false — never an
1502
- * exception-hidden true.
1503
- * @param {string} publicKeyHex 32-byte public key, hex
1504
- * @param {string} messageHex the message, hex
1505
- * @param {string} signatureHex the 64-byte signature, hex
1506
- * @returns {Promise<boolean>}
1507
- */
1508
- async function vcEd25519Verify(publicKeyHex, messageHex, signatureHex) {
1509
- if (typeof publicKeyHex !== 'string' || typeof messageHex !== 'string' ||
1510
- typeof signatureHex !== 'string') {
1511
- throw new TypeError(
1512
- 'vcEd25519Verify: publicKeyHex, messageHex and signatureHex must be strings');
1513
- }
1514
- const e = await vcEntry('vcEd25519Verify', 'VC Ed25519 verify');
1515
- return !!entryResult(
1516
- e.vcEd25519Verify(publicKeyHex, messageHex, signatureHex), 'vcEd25519Verify').valid;
1517
- }
1518
-
1519
- /**
1520
- * Create an eddsa-rdfc-2022 Data Integrity proofValue over an
1521
- * already-canonicalized document + proof config (entry_jsoo.ml's
1522
- * vcEddsaCreateFromCanonical -> VC_DataIntegrity.eddsa_rdfc_2022_
1523
- * create_from_canonical). The two canonical inputs are RDFC-1.0
1524
- * canonical N-Quads (see canonicalize()).
1525
- * @param {string} secretKeyHex 32-byte secret key, hex
1526
- * @param {string} canonicalDocument canonical N-Quads of the document
1527
- * @param {string} canonicalConfig canonical N-Quads of the proof config
1528
- * @returns {Promise<string>} the multibase-z (base58btc) proofValue
1529
- */
1530
- async function vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig) {
1531
- if (typeof secretKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1532
- typeof canonicalConfig !== 'string') {
1533
- throw new TypeError(
1534
- 'vcEddsaCreateFromCanonical: secretKeyHex, canonicalDocument and canonicalConfig must be strings');
1535
- }
1536
- const e = await vcEntry('vcEddsaCreateFromCanonical', 'VC eddsa-rdfc-2022 proof creation');
1537
- return entryResult(
1538
- e.vcEddsaCreateFromCanonical(secretKeyHex, canonicalDocument, canonicalConfig),
1539
- 'vcEddsaCreateFromCanonical').proofValue;
1540
- }
1541
-
1542
- /**
1543
- * Verify an eddsa-rdfc-2022 proofValue against canonical inputs
1544
- * (entry_jsoo.ml's vcEddsaVerifyFromCanonical ->
1545
- * VC_DataIntegrity.eddsa_rdfc_2022_verify_from_canonical). Wrong
1546
- * key, tampered document/config, or tampered proofValue all return
1547
- * false.
1548
- * @param {string} publicKeyHex 32-byte public key, hex
1549
- * @param {string} canonicalDocument canonical N-Quads of the document
1550
- * @param {string} canonicalConfig canonical N-Quads of the proof config
1551
- * @param {string} proofValue the multibase-z proofValue to check
1552
- * @returns {Promise<boolean>}
1553
- */
1554
- async function vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue) {
1555
- if (typeof publicKeyHex !== 'string' || typeof canonicalDocument !== 'string' ||
1556
- typeof canonicalConfig !== 'string' || typeof proofValue !== 'string') {
1557
- throw new TypeError(
1558
- 'vcEddsaVerifyFromCanonical: publicKeyHex, canonicalDocument, canonicalConfig and proofValue must be strings');
1559
- }
1560
- const e = await vcEntry('vcEddsaVerifyFromCanonical', 'VC eddsa-rdfc-2022 proof verification');
1561
- return !!entryResult(
1562
- e.vcEddsaVerifyFromCanonical(publicKeyHex, canonicalDocument, canonicalConfig, proofValue),
1563
- 'vcEddsaVerifyFromCanonical').verified;
1564
- }
1565
-
1566
- /**
1567
- * VC Data Model 2.0 structural conformance check (entry_jsoo.ml's
1568
- * vcCheckCredential -> VC_Credential.vc_check_from_string, 117 pass,
1569
- * 0 fail on the offline vc_stage1 fixture suite). Pure structural
1570
- * validation — no crypto, so this does NOT go through vcEntry/
1571
- * ensureCrypto. `v2ctxJson` is the vendored VCDM v2 base context
1572
- * document's raw JSON text (third_party/contexts/credentials-v2.jsonld,
1573
- * `@context` value included — the F* side parses the whole document
1574
- * and only reads its own @context field) and `credentialJson` is the
1575
- * raw JSON text of the VC/VP document under test.
1576
- * @param {string} v2ctxJson vendored credentials-v2.jsonld text
1577
- * @param {string} credentialJson the VC/VP document's raw JSON text
1578
- * @returns {Promise<{valid: boolean, reason?: string}>}
1579
- */
1580
- async function vcCheckCredential(v2ctxJson, credentialJson) {
1581
- if (typeof v2ctxJson !== 'string' || typeof credentialJson !== 'string') {
1582
- throw new TypeError('vcCheckCredential: v2ctxJson and credentialJson must be strings');
1583
- }
1584
- const e = await entry();
1585
- if (!e) throw pendingError('vcCheckCredential');
1586
- requireEntryFn(e, 'vcCheckCredential', 'VC Data Model 2.0 structural check');
1587
- const r = entryResult(e.vcCheckCredential(v2ctxJson, credentialJson), 'vcCheckCredential');
1588
- return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1589
- }
1590
-
1591
- /**
1592
- * credentialSubject presence/shape check, VERSION-AGNOSTIC (Track A1,
1593
- * docs/designissues/2026-07-11-vc-canivc-eecc-plan.md) —
1594
- * entry_jsoo.ml's vcCheckCredentialSubject ->
1595
- * VC_Credential.vc_check_credential_subject_from_string. Unlike
1596
- * vcCheckCredential above, this does NOT require the VCDM 2.0 base
1597
- * @context to be first (or present at all) — it only checks that a
1598
- * credential-shaped document (type includes "VerifiableCredential")
1599
- * has a present, non-empty credentialSubject. Used for documents
1600
- * under a non-VCDM-2.0 @context (e.g. the vc-di-eddsa Data Integrity
1601
- * conformance suite's legacy VC 1.1 fixtures) where the full
1602
- * vcCheckCredential's @context sentinel would (correctly) reject the
1603
- * document for an unrelated reason.
1604
- * @param {string} credentialJson the VC/VP document's raw JSON text
1605
- * @returns {Promise<{valid: boolean, reason?: string}>}
1606
- */
1607
- async function vcCheckCredentialSubject(credentialJson) {
1608
- if (typeof credentialJson !== 'string') {
1609
- throw new TypeError('vcCheckCredentialSubject: credentialJson must be a string');
1610
- }
1611
- const e = await entry();
1612
- if (!e) throw pendingError('vcCheckCredentialSubject');
1613
- requireEntryFn(e, 'vcCheckCredentialSubject', 'credentialSubject presence check');
1614
- const r = entryResult(e.vcCheckCredentialSubject(credentialJson), 'vcCheckCredentialSubject');
1615
- return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1616
- }
1617
-
1618
- /**
1619
- * DATA_LOSS_DETECTION_ERROR check (Track A1, same plan doc) —
1620
- * entry_jsoo.ml's vcCheckNoDataLoss ->
1621
- * VC_Credential.vc_check_no_data_loss_from_string. Rejects a
1622
- * credential-shaped document whose `type`/`@type` entries or
1623
- * `credentialSubject` property keys include one that a lenient
1624
- * JSON-LD processor would silently drop (VC Data Integrity spec,
1625
- * "Securing Data Losslessly"). `credentialJson` MUST already have
1626
- * any remote @context IRI the caller recognizes inlined to the real
1627
- * context object — this engine build has no remote-context loader
1628
- * registered, so a still-remote IRI string in "@context" fails
1629
- * context processing honestly rather than silently skipping the
1630
- * check.
1631
- * @param {string} credentialJson the VC/VP document's raw JSON text,
1632
- * @context already inlined
1633
- * @returns {Promise<{valid: boolean, reason?: string}>}
1634
- */
1635
- async function vcCheckNoDataLoss(credentialJson) {
1636
- if (typeof credentialJson !== 'string') {
1637
- throw new TypeError('vcCheckNoDataLoss: credentialJson must be a string');
1638
- }
1639
- const e = await entry();
1640
- if (!e) throw pendingError('vcCheckNoDataLoss');
1641
- requireEntryFn(e, 'vcCheckNoDataLoss', 'DATA_LOSS_DETECTION_ERROR check');
1642
- const r = entryResult(e.vcCheckNoDataLoss(credentialJson), 'vcCheckNoDataLoss');
1643
- return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1644
- }
1645
-
1646
- /**
1647
- * relatedResource digest verification (VCDM 2.0 §5.3, vc20-api Track
1648
- * A4) — entry_jsoo.ml's vcCheckRelatedResourceDigests ->
1649
- * VC_Credential.vc_check_related_resource_digests_from_string. The
1650
- * engine does no I/O, so the caller supplies a known-resource digest
1651
- * registry: a JSON array of {"id": <resource URL>, "digestsHex":
1652
- * [<lowercase hex>, ...]} entries computed from the caller's VENDORED
1653
- * copies of each resource's content bytes. A relatedResource entry
1654
- * whose id is in the registry and whose declared digestSRI/
1655
- * digestMultibase matches none of that id's digests is rejected (the
1656
- * spec's digest-mismatch error); an id absent from the registry, or a
1657
- * digest algorithm the registry contract doesn't cover (anything
1658
- * outside sha256/sha384), is unverifiable offline and passes. All
1659
- * decode/match semantics are F*-verified (VC.Credential.fst).
1660
- * @param {string} registryJson the digest registry's raw JSON text
1661
- * @param {string} credentialJson the VC/VP document's raw JSON text
1662
- * @returns {Promise<{valid: boolean, reason?: string}>}
1663
- */
1664
- async function vcCheckRelatedResourceDigests(registryJson, credentialJson) {
1665
- if (typeof registryJson !== 'string' || typeof credentialJson !== 'string') {
1666
- throw new TypeError('vcCheckRelatedResourceDigests: registryJson and credentialJson must be strings');
1667
- }
1668
- const e = await entry();
1669
- if (!e) throw pendingError('vcCheckRelatedResourceDigests');
1670
- requireEntryFn(e, 'vcCheckRelatedResourceDigests', 'relatedResource digest check');
1671
- const r = entryResult(e.vcCheckRelatedResourceDigests(registryJson, credentialJson), 'vcCheckRelatedResourceDigests');
1672
- return r.valid ? { valid: true } : { valid: false, reason: r.reason };
1673
- }
1674
-
1675
- // -----------------------------------------------------------------
1676
- // Typed "engine" functions (#74 npm FP surface). Each is a pure,
1677
- // string/JSON-in, JSON-out wrapper over one F*-extracted engine
1678
- // exposed by entry_jsoo.ml. No logic lives on the JS side — the
1679
- // transform/eval/validate/CAS math is all verified F*; these bind
1680
- // the entry ABI to a typed Promise. All need the npm-entry bundle.
1681
- // -----------------------------------------------------------------
1682
-
1683
- /**
1684
- * XSLT 1.0 transform (entry_jsoo.ml's xsltTransform export ->
1685
- * XSLT.Transform.transform). Applies `stylesheetXml` to `sourceXml`
1686
- * and returns the serialized result tree. Needs the npm-entry bundle.
1687
- * @param {string} stylesheetXml an XSLT stylesheet document
1688
- * @param {string} sourceXml the source XML document
1689
- * @returns {Promise<string>} the transform output (serialized XML/text)
1690
- */
1691
- async function xsltTransform(stylesheetXml, sourceXml) {
1692
- if (typeof stylesheetXml !== 'string' || typeof sourceXml !== 'string') {
1693
- throw new TypeError('xsltTransform: stylesheetXml and sourceXml must be strings');
1694
- }
1695
- const e = await entry();
1696
- if (!e) throw pendingError('xsltTransform');
1697
- requireEntryFn(e, 'xsltTransform', 'XSLT transform');
1698
- const r = entryResult(e.xsltTransform(stylesheetXml, sourceXml), 'xsltTransform');
1699
- return r.output;
1700
- }
1701
-
1702
- /**
1703
- * Evaluate a Content MathML document (entry_jsoo.ml's mathmlEval
1704
- * export -> MathML.Content.eval_doc_env). `bindings` maps
1705
- * ci-variable names to value strings; pass {} for a closed
1706
- * expression. Returns the exact numeric/boolean value or an
1707
- * `undef` reason (division-by-zero, type error, ...). Needs the
1708
- * npm-entry bundle.
1709
- * @param {string} contentMathmlXml
1710
- * @param {Record<string,string>} [bindings]
1711
- * @returns {Promise<{kind:'rat',num:number,den:number}|{kind:'bool',value:boolean}|{kind:'undef',reason:string}>}
1712
- */
1713
- async function mathmlEval(contentMathmlXml, bindings) {
1714
- if (typeof contentMathmlXml !== 'string') {
1715
- throw new TypeError('mathmlEval: contentMathmlXml must be a string');
1716
- }
1717
- const b = bindings || {};
1718
- if (typeof b !== 'object') {
1719
- throw new TypeError('mathmlEval: bindings must be an object');
1720
- }
1721
- const e = await entry();
1722
- if (!e) throw pendingError('mathmlEval');
1723
- requireEntryFn(e, 'mathmlEval', 'MathML evaluation');
1724
- const r = entryResult(
1725
- e.mathmlEval(contentMathmlXml, JSON.stringify(b)), 'mathmlEval');
1726
- return r.value;
1727
- }
1728
-
1729
- /**
1730
- * XForms recalculate (entry_jsoo.ml's xformsRecalc export ->
1731
- * XForms.Bind.recalculate). Applies the model binds (calculate,
1732
- * constraint, relevant, required, readonly, type MIPs) to the
1733
- * instance and returns the recomputed instance plus a validity
1734
- * report per bound node. Needs the npm-entry bundle.
1735
- * @param {string} instanceXml the XForms instance document
1736
- * @param {Array<{id?:string,target:string,calculate?:string,constraint?:string,relevant?:string,required?:string,readonly?:string,type?:string}>} binds
1737
- * @returns {Promise<{instance:string,validity:Array<object>}>}
1738
- */
1739
- async function xformsRecalc(instanceXml, binds) {
1740
- if (typeof instanceXml !== 'string') {
1741
- throw new TypeError('xformsRecalc: instanceXml must be a string');
1742
- }
1743
- if (!Array.isArray(binds)) {
1744
- throw new TypeError('xformsRecalc: binds must be an array');
1745
- }
1746
- const e = await entry();
1747
- if (!e) throw pendingError('xformsRecalc');
1748
- requireEntryFn(e, 'xformsRecalc', 'XForms recalculate');
1749
- const r = entryResult(
1750
- e.xformsRecalc(instanceXml, JSON.stringify(binds)), 'xformsRecalc');
1751
- return { instance: r.instance, validity: r.validity };
1752
- }
1753
-
1754
- /**
1755
- * JSON Schema (draft-07) validation (entry_jsoo.ml's
1756
- * jsonSchemaValidate export -> JSONSchema.Validate.validate).
1757
- * Returns the verdict — the verified validator gives a definite
1758
- * pass/fail/unsupported, not a per-keyword error list, so `errors`
1759
- * carries a single reason string when not a definite pass. Needs
1760
- * the npm-entry bundle.
1761
- * @param {string} schemaJson the schema document (JSON text)
1762
- * @param {string} instanceJson the instance document (JSON text)
1763
- * @returns {Promise<{valid:boolean,result:'pass'|'fail'|'unsupported',errors:string[]}>}
1764
- */
1765
- async function jsonSchemaValidate(schemaJson, instanceJson) {
1766
- if (typeof schemaJson !== 'string' || typeof instanceJson !== 'string') {
1767
- throw new TypeError('jsonSchemaValidate: schemaJson and instanceJson must be strings');
1768
- }
1769
- const e = await entry();
1770
- if (!e) throw pendingError('jsonSchemaValidate');
1771
- requireEntryFn(e, 'jsonSchemaValidate', 'JSON Schema validation');
1772
- const r = entryResult(
1773
- e.jsonSchemaValidate(schemaJson, instanceJson), 'jsonSchemaValidate');
1774
- return { valid: !!r.valid, result: r.result, errors: r.errors || [] };
1775
- }
1776
-
1777
- /**
1778
- * Schematron validation (entry_jsoo.ml's schematronValidate export
1779
- * -> Schematron.Validate.validate). Returns every finding (failed
1780
- * assert, fired report, indeterminate) in pattern-then-document
1781
- * order. Needs the npm-entry bundle.
1782
- * @param {string} schematronXml the Schematron schema document
1783
- * @param {string} instanceXml the instance document to check
1784
- * @returns {Promise<{findings:Array<{type:string,context:string,test:string,message:string,path:string,reason?:string}>}>}
1785
- */
1786
- async function schematronValidate(schematronXml, instanceXml) {
1787
- if (typeof schematronXml !== 'string' || typeof instanceXml !== 'string') {
1788
- throw new TypeError('schematronValidate: schematronXml and instanceXml must be strings');
1789
- }
1790
- const e = await entry();
1791
- if (!e) throw pendingError('schematronValidate');
1792
- requireEntryFn(e, 'schematronValidate', 'Schematron validation');
1793
- const r = entryResult(
1794
- e.schematronValidate(schematronXml, instanceXml), 'schematronValidate');
1795
- return { findings: r.findings };
1796
- }
1797
-
1798
- // TOAN — a small exact-CAS surface over Math.Expr (E_Int/E_Rat/E_Bool/
1799
- // E_Sym/E_App). Callers pass an expression as the JSON codec
1800
- // {int:n} | {rat:[n,d]} | {bool:b} | {sym:name} | {app:name,args:[...]}
1801
- // and receive Content MathML for the result (via MathML.Present).
1802
- async function toanCall(fnName, what, ...args) {
1803
- const e = await entry();
1804
- if (!e) throw pendingError(fnName);
1805
- requireEntryFn(e, fnName, what);
1806
- const r = entryResult(e[fnName](...args), fnName);
1807
- return r.mathml;
1808
- }
1809
-
1810
- /**
1811
- * Symbolic finite summation (entry_jsoo.ml's toanSummation ->
1812
- * Math.Series.summation): sum of `body[idx:=lo..hi]`, simplified,
1813
- * as Content MathML. Needs the npm-entry bundle.
1814
- * @param {object} bodyExpr the summand, in the expr JSON codec
1815
- * @param {string} idx the summation index symbol
1816
- * @param {number} lo inclusive lower bound
1817
- * @param {number} hi inclusive upper bound
1818
- * @returns {Promise<string>} Content MathML
1819
- */
1820
- async function toanSummation(bodyExpr, idx, lo, hi) {
1821
- if (typeof idx !== 'string') throw new TypeError('toanSummation: idx must be a string');
1822
- return toanCall('toanSummation', 'TOAN summation',
1823
- JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1824
- }
1825
-
1826
- /**
1827
- * Symbolic finite product (entry_jsoo.ml's toanProduct ->
1828
- * Math.Series.finite_product). Same shape as {@link toanSummation}.
1829
- * @param {object} bodyExpr the factor, in the expr JSON codec
1830
- * @param {string} idx the product index symbol
1831
- * @param {number} lo inclusive lower bound
1832
- * @param {number} hi inclusive upper bound
1833
- * @returns {Promise<string>} Content MathML
1834
- */
1835
- async function toanProduct(bodyExpr, idx, lo, hi) {
1836
- if (typeof idx !== 'string') throw new TypeError('toanProduct: idx must be a string');
1837
- return toanCall('toanProduct', 'TOAN product',
1838
- JSON.stringify(bodyExpr), idx, String(lo), String(hi));
1839
- }
1840
-
1841
- /**
1842
- * Canonical simplification (entry_jsoo.ml's toanSimplify ->
1843
- * Math.Simplify.simplify) of an expression, as Content MathML.
1844
- * @param {object} expr in the expr JSON codec
1845
- * @returns {Promise<string>} Content MathML
1846
- */
1847
- async function toanSimplify(expr) {
1848
- return toanCall('toanSimplify', 'TOAN simplify', JSON.stringify(expr));
1849
- }
1850
-
1851
- /**
1852
- * Symbolic differentiation (entry_jsoo.ml's toanDiff ->
1853
- * Math.Diff.diff) of `expr` w.r.t. `variable`, as Content MathML.
1854
- * @param {object} expr in the expr JSON codec
1855
- * @param {string} variable the differentiation variable
1856
- * @returns {Promise<string>} Content MathML
1857
- */
1858
- async function toanDiff(expr, variable) {
1859
- if (typeof variable !== 'string') throw new TypeError('toanDiff: variable must be a string');
1860
- return toanCall('toanDiff', 'TOAN diff', JSON.stringify(expr), variable);
1861
- }
1862
-
1863
- /**
1864
- * Substitution (entry_jsoo.ml's toanSubst -> Math.Subst.subst):
1865
- * `expr[variable := value]`, simplified, as Content MathML.
1866
- * @param {object} expr in the expr JSON codec
1867
- * @param {string} variable the symbol to replace
1868
- * @param {object} value the replacement, in the expr JSON codec
1869
- * @returns {Promise<string>} Content MathML
1870
- */
1871
- async function toanSubst(expr, variable, value) {
1872
- if (typeof variable !== 'string') throw new TypeError('toanSubst: variable must be a string');
1873
- return toanCall('toanSubst', 'TOAN subst',
1874
- JSON.stringify(expr), variable, JSON.stringify(value));
1875
- }
1876
-
1877
- // Matrix / vector algebra over exact rationals (Math.Matrix). A
1878
- // matrix is a JSON array of rows; a vector a JSON array of cells;
1879
- // a cell is an integer or a [num,den] pair. Results render via
1880
- // Math.Matrix.mres_to_string ("undef" carries a `reason`).
1881
- async function matrixCall(fnName, what, ...jsonArgs) {
1882
- const e = await entry();
1883
- if (!e) throw pendingError(fnName);
1884
- requireEntryFn(e, fnName, what);
1885
- const r = entryResult(e[fnName](...jsonArgs.map((a) => JSON.stringify(a))), fnName);
1886
- return { result: r.result, reason: r.reason || '' };
1887
- }
1888
-
1889
- /**
1890
- * Determinant of a square matrix (entry_jsoo.ml's matrixDeterminant
1891
- * -> Math.Matrix.dyn_determinant), exact.
1892
- * @param {Array<Array<number|[number,number]>>} matrix
1893
- * @returns {Promise<{result:string,reason:string}>}
1894
- */
1895
- async function matrixDeterminant(matrix) {
1896
- if (!Array.isArray(matrix)) throw new TypeError('matrixDeterminant: matrix must be an array of rows');
1897
- return matrixCall('matrixDeterminant', 'matrix determinant', matrix);
1898
- }
1899
-
1900
- /**
1901
- * Dot / scalar product of two vectors (entry_jsoo.ml's
1902
- * matrixScalarProduct -> Math.Matrix.dyn_scalarproduct).
1903
- * @param {Array<number|[number,number]>} a
1904
- * @param {Array<number|[number,number]>} b
1905
- * @returns {Promise<{result:string,reason:string}>}
1906
- */
1907
- async function matrixScalarProduct(a, b) {
1908
- if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixScalarProduct: a and b must be arrays');
1909
- return matrixCall('matrixScalarProduct', 'vector scalar product', a, b);
1910
- }
1911
-
1912
- /**
1913
- * Cross product of two 3-vectors (entry_jsoo.ml's
1914
- * matrixVectorProduct -> Math.Matrix.dyn_vectorproduct).
1915
- * @param {Array<number|[number,number]>} a
1916
- * @param {Array<number|[number,number]>} b
1917
- * @returns {Promise<{result:string,reason:string}>}
1918
- */
1919
- async function matrixVectorProduct(a, b) {
1920
- if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixVectorProduct: a and b must be arrays');
1921
- return matrixCall('matrixVectorProduct', 'vector cross product', a, b);
1922
- }
1923
-
1924
- /**
1925
- * Outer product of two vectors (entry_jsoo.ml's matrixOuterProduct
1926
- * -> Math.Matrix.dyn_outerproduct).
1927
- * @param {Array<number|[number,number]>} a
1928
- * @param {Array<number|[number,number]>} b
1929
- * @returns {Promise<{result:string,reason:string}>}
1930
- */
1931
- async function matrixOuterProduct(a, b) {
1932
- if (!Array.isArray(a) || !Array.isArray(b)) throw new TypeError('matrixOuterProduct: a and b must be arrays');
1933
- return matrixCall('matrixOuterProduct', 'vector outer product', a, b);
1934
- }
1935
-
1936
- // A `scaled` value as entry_jsoo.ml's scaledJson envelope decodes it:
1937
- // {mantissa,scale,decimal} strings straight off the wire.
1938
- function scaledFromJson(s) {
1939
- return { mantissa: s.mantissa, scale: s.scale, decimal: s.decimal };
1940
- }
1941
-
1942
- /**
1943
- * n+1 evenly spaced samples of the logistic sigmoid
1944
- * L / (1 + exp(-k*(x - x0))) over [xmin, xmax] (entry_jsoo.ml's
1945
- * sigmoidPoints -> Math.Sigmoid.sigmoid_points). All arithmetic --
1946
- * argument reduction, the truncated Taylor series, repeated
1947
- * squaring, and the x samples themselves -- runs as exact rational
1948
- * arithmetic inside Math.Sigmoid.fst; see that module's header for
1949
- * the documented error bound on the returned (rounded) values. This
1950
- * wrapper only marshals JSON; it never computes exp itself.
1951
- * @param {{k:number|string,x0:number|string,l:number|string,
1952
- * xmin:number|string,xmax:number|string,n:number|string}} params
1953
- * @returns {Promise<Array<{x:{mantissa:string,scale:string,decimal:string},
1954
- * y:{mantissa:string,scale:string,decimal:string}}>>}
1955
- */
1956
- async function sigmoidPoints(params) {
1957
- if (!params || typeof params !== 'object') {
1958
- throw new TypeError('sigmoidPoints: params must be an object');
1959
- }
1960
- const e = await entry();
1961
- if (!e) throw pendingError('sigmoid points');
1962
- requireEntryFn(e, 'sigmoidPoints', 'sigmoid points');
1963
- const wire = {
1964
- k: String(params.k), x0: String(params.x0), l: String(params.l),
1965
- xmin: String(params.xmin), xmax: String(params.xmax), n: String(params.n),
1966
- };
1967
- const r = entryResult(e.sigmoidPoints(JSON.stringify(wire)), 'sigmoid points');
1968
- return r.points.map((p) => ({ x: scaledFromJson(p.x), y: scaledFromJson(p.y) }));
1969
- }
1970
-
1971
- /**
1972
- * Presentation MathML for the sigmoid formula L / (1 + exp(-k*(x - x0))),
1973
- * engine-serialized (entry_jsoo.ml's sigmoidFormulaMathml ->
1974
- * MathML.Present.to_presentation_mathml applied to a fixed
1975
- * Math.Expr.expr) -- never hand-written MathML.
1976
- * @returns {Promise<string>} a `<math>...</math>` Presentation MathML document
1977
- */
1978
- async function sigmoidFormulaMathml() {
1979
- const e = await entry();
1980
- if (!e) throw pendingError('sigmoid formula MathML');
1981
- requireEntryFn(e, 'sigmoidFormulaMathml', 'sigmoid formula MathML');
1982
- const r = entryResult(e.sigmoidFormulaMathml(), 'sigmoid formula MathML');
1983
- return r.mathml;
1984
- }
1985
-
1986
- // -----------------------------------------------------------------
1987
- // In-memory COTTAS bytes store (docs/designissues/2026-07-06-
1988
- // inmemory-bytes-store.md, stage 5). Needs the npm-entry bundle
1989
- // (bin/npm-entry/entry_jsoo.ml's openCottas/queryCottas/closeCottas/
1990
- // toCottas exports). Unlike every other operation in this file, the
1991
- // "dataset" here is NOT an in-heap Dataset: openCottas() returns an
1992
- // opaque handle string naming an entry the F*-verified COTTAS/Parquet
1993
- // reader decodes lazily, row-group by row-group, as queryCottas()
1994
- // touches it -- the whole point of the design (heap-store parity
1995
- // would defeat the memory win the design doc measures). See
1996
- // queryCottas's doc comment for the query-shape/entailment/write
1997
- // divergences from query() this implies.
1998
- // -----------------------------------------------------------------
1999
-
2000
- // Accept a hex string, a Uint8Array/Buffer, or a plain ArrayBuffer;
2001
- // normalize to the lowercase hex string the entry ABI's string-only
2002
- // wire contract requires (same "strings in, JSON out" ABI every
2003
- // other entry export uses -- see entry_jsoo.ml's file header).
2004
- function bytesToHex(bytesLike, who) {
2005
- if (typeof bytesLike === 'string') {
2006
- if (!/^[0-9a-fA-F]*$/.test(bytesLike) || bytesLike.length % 2 !== 0) {
2007
- throw new TypeError(`${who}: string input must be an even-length hex string`);
2008
- }
2009
- return bytesLike.toLowerCase();
2010
- }
2011
- let u8;
2012
- if (bytesLike instanceof Uint8Array) u8 = bytesLike;
2013
- else if (bytesLike instanceof ArrayBuffer) u8 = new Uint8Array(bytesLike);
2014
- else {
2015
- throw new TypeError(
2016
- `${who}: expected a hex string, Uint8Array, Buffer, or ArrayBuffer`);
2017
- }
2018
- // Buffer's native hex codec when available (Node); a per-byte
2019
- // string-concat loop allocates millions of intermediate strings on
2020
- // a corpus-scale artifact (it measurably dominated the bytes-store
2021
- // path's RSS at 50,000 quads before this branch existed).
2022
- if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
2023
- return Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength).toString('hex');
2024
- }
2025
- const HEX = '0123456789abcdef';
2026
- const parts = new Array(u8.length);
2027
- for (let i = 0; i < u8.length; i++) {
2028
- parts[i] = HEX[u8[i] >> 4] + HEX[u8[i] & 15];
2029
- }
2030
- return parts.join('');
2031
- }
2032
-
2033
- function hexToBytes(hex) {
2034
- const out = new Uint8Array(hex.length / 2);
2035
- for (let i = 0; i < out.length; i++) {
2036
- out[i] = parseInt(hex.substr(i * 2, 2), 16);
2037
- }
2038
- return out;
2039
- }
2040
-
2041
- /**
2042
- * Open a COTTAS/Parquet artifact's raw bytes as a queryable,
2043
- * read-only store -- the in-memory-bytes-store design's browser call
2044
- * site. Needs the npm-entry bundle. The store is NOT materialized
2045
- * into a heap Dataset: rows are decoded lazily by queryCottas() as a
2046
- * query actually touches them (measured 64-161 B/quad in the design
2047
- * doc's native numbers, vs. ~877 B/quad for a fully-parsed heap
2048
- * Dataset of the same data).
2049
- *
2050
- * @param {string|Uint8Array|ArrayBuffer} bytes whole `.cottas` file contents
2051
- * @returns {Promise<string>} an opaque handle for queryCottas()/closeCottas()
2052
- */
2053
- async function openCottas(bytes) {
2054
- const e = await entry();
2055
- if (!e) throw pendingError('openCottas (in-memory COTTAS bytes store)');
2056
- requireEntryFn(e, 'openCottas', 'openCottas');
2057
- const hex = bytesToHex(bytes, 'openCottas');
2058
- const r = entryResult(e.openCottas(hex), 'openCottas');
2059
- return r.handle;
2060
- }
2061
-
2062
- /**
2063
- * Run a SPARQL 1.1 query against a store opened by openCottas().
2064
- * Needs the npm-entry bundle.
2065
- *
2066
- * Divergences from query() (documented, not silent):
2067
- * - No `entail` option -- bare COTTAS bytes carry no closure step.
2068
- * - No write/--delta-log overlay -- read-only (design doc §2.4's
2069
- * write-overlay story composes at the native store_caps layer;
2070
- * it is not wired into this browser ABI).
2071
- * - A query SHAPE the backend executor can't push down (rare; the
2072
- * same honest-failure posture the native `--data-cottas` CLI path
2073
- * has for run_select_query_backend_dataset/run_ask_query_backend_
2074
- * dataset returning None) rejects with an Error rather than
2075
- * silently falling back to a full materialize -- that fallback
2076
- * would defeat the store's whole memory argument.
2077
- * - DESCRIBE is not supported (same cut queryDataset's ABI has).
2078
- *
2079
- * @param {string} handle from openCottas()
2080
- * @param {string} sparql
2081
- * @returns {Promise<Array<Map<string, object>>|boolean|Dataset>}
2082
- * SELECT -> Bindings[], ASK -> boolean, CONSTRUCT -> Dataset
2083
- * (materialized once, via SPARQL11_Store.materialize_dataset_backend
2084
- * -- see entry_jsoo.ml's queryCottas doc comment for why CONSTRUCT
2085
- * alone pays that cost).
2086
- */
2087
- async function queryCottas(handle, sparql) {
2088
- if (typeof handle !== 'string') {
2089
- throw new TypeError('queryCottas: handle must be the string openCottas() returned');
2090
- }
2091
- if (typeof sparql !== 'string') {
2092
- throw new TypeError('queryCottas: sparql must be a string');
2093
- }
2094
- const e = await entry();
2095
- if (!e) throw pendingError('queryCottas');
2096
- requireEntryFn(e, 'queryCottas', 'queryCottas');
2097
- const r = entryResult(e.queryCottas(handle, sparql), 'queryCottas');
2098
- if (r.kind === 'ask') return r.boolean;
2099
- if (r.kind === 'construct') {
2100
- return Dataset.fromNQuads(r.nquads, { blankNodePrefix: freshBnodePrefix() });
2101
- }
2102
- return bindingsFromSrj(r.srj);
2103
- }
2104
-
2105
- /**
2106
- * Release a store opened by openCottas(). Drops the handle from this
2107
- * process's registry only -- it does NOT evict the underlying byte
2108
- * cache the entry bundle keeps for the process's lifetime (design
2109
- * doc "Open decisions" item 1: no eviction API exists yet). A page
2110
- * that opens many short-lived stores still grows that cache for the
2111
- * tab's lifetime; this is a documented limitation, not a silent leak.
2112
- * @param {string} handle
2113
- * @returns {Promise<void>}
2114
- */
2115
- async function closeCottas(handle) {
2116
- if (typeof handle !== 'string') {
2117
- throw new TypeError('closeCottas: handle must be the string openCottas() returned');
2118
- }
2119
- const e = await entry();
2120
- if (!e) throw pendingError('closeCottas');
2121
- requireEntryFn(e, 'closeCottas', 'closeCottas');
2122
- entryResult(e.closeCottas(handle), 'closeCottas');
2123
- }
2124
-
2125
- /**
2126
- * Serialize a dataset to COTTAS/Parquet bytes via the native writer
2127
- * (RDF.CottasStore.BaseWriter.serialize_cottas_v2 -- the SAME pure
2128
- * `Tot` F* function `factoidal compact --native-writer` uses), for a
2129
- * caller to persist (IndexedDB/OPFS) or offer as a download. Needs
2130
- * the npm-entry bundle. Round-trips through openCottas(): the bytes
2131
- * this returns are valid input to openCottas() and to the native
2132
- * `--data-cottas`/`--data-cottas-mem` CLI flags, byte-for-byte.
2133
- * @param {Dataset|string|Array} data
2134
- * @param {{format?: string}} [options]
2135
- * @returns {Promise<Uint8Array>}
2136
- */
2137
- async function toCottas(data, options) {
2138
- const e = await entry();
2139
- if (!e) throw pendingError('toCottas (native COTTAS serialization)');
2140
- requireEntryFn(e, 'toCottas', 'toCottas');
2141
- const nq = docsToEntryNQuads(e, toDocs(data, options), 'toCottas');
2142
- const r = entryResult(e.toCottas(nq), 'toCottas');
2143
- return hexToBytes(r.cottasHex);
2144
- }
2145
-
2146
- /**
2147
- * Feature probe, for tests and downstream capability checks.
2148
- * @returns {Promise<{entry: boolean, construct: boolean,
2149
- * update: boolean, canonicalize: boolean, graphs: boolean,
2150
- * canonicalHash: boolean, shacl: boolean, shex: boolean,
2151
- * owlClosure: boolean, rml: boolean, csvw: boolean, jsonld: boolean,
2152
- * rif: boolean}>}
2153
- */
2154
- let capsCache = null;
2155
- async function capabilities() {
2156
- if (capsCache) return capsCache;
2157
- capsCache = capabilitiesUncached();
2158
- return capsCache;
2159
- }
2160
-
2161
- async function capabilitiesUncached() {
2162
- const e = await entry();
2163
- if (e) {
2164
- return {
2165
- entry: true, construct: true, update: true, canonicalize: true,
2166
- graphs: true, canonicalHash: true,
2167
- // Per-function probes, not a blanket `true` -- an older/stale
2168
- // loaded bundle (e.g. a wasm-target build that predates one of
2169
- // these exports) reports its ACTUAL surface rather than
2170
- // over-promising (see requireEntryFn's doc comment above).
2171
- shacl: typeof e.shaclValidate === 'function',
2172
- shex: typeof e.shexValidate === 'function',
2173
- owlClosure: typeof e.owlClosure === 'function',
2174
- tableau: typeof e.tableauMaterialise === 'function' &&
2175
- typeof e.tableauDlInconsistent === 'function',
2176
- rml: typeof e.rmlMap === 'function',
2177
- csvw: typeof e.csvwToRdf === 'function',
2178
- jsonld: typeof e.jsonldToRdf === 'function',
2179
- jsonldFromRdf: typeof e.jsonldFromRdf === 'function',
2180
- didKey: typeof e.didKeyResolve === 'function',
2181
- xml: typeof e.xmlWellformed === 'function',
2182
- xpath: typeof e.xpathEval === 'function',
2183
- rif: typeof e.rifEval === 'function',
2184
- xslt: typeof e.xsltTransform === 'function',
2185
- mathml: typeof e.mathmlEval === 'function',
2186
- xforms: typeof e.xformsRecalc === 'function',
2187
- jsonSchema: typeof e.jsonSchemaValidate === 'function',
2188
- schematron: typeof e.schematronValidate === 'function',
2189
- toan: typeof e.toanSummation === 'function',
2190
- matrix: typeof e.matrixDeterminant === 'function',
2191
- sigmoid: typeof e.sigmoidPoints === 'function' &&
2192
- typeof e.sigmoidFormulaMathml === 'function',
2193
- cottasBytesStore: typeof e.openCottas === 'function' &&
2194
- typeof e.queryCottas === 'function' && typeof e.toCottas === 'function',
2195
- // VC Data Integrity crypto surface (probed, not blanket-true --
2196
- // an older bundle predates the vc* exports). The HACL* wasm
2197
- // backend still has to be initialised at call time; this flag
2198
- // only reports that the ABI exports exist.
2199
- vcCrypto: typeof e.vcEd25519Verify === 'function' &&
2200
- typeof e.vcEddsaVerifyFromCanonical === 'function',
2201
- };
2202
- }
2203
- // Probe --canonicalize support on the CLI bundle with a 1-quad doc.
2204
- // canonicalHash rides the same engine support as canonicalize (it is
2205
- // canonicalize() applied to one graph's triples); graphs() is pure
2206
- // JS enumeration and needs no engine at all.
2207
- let canon = false;
2208
- try {
2209
- const res = await run(
2210
- ['--canonicalize', '-d', '/static/cap.nq'],
2211
- [{ name: '/static/cap.nq',
2212
- content: '<http://x/s> <http://x/p> "o" .\n' }]);
2213
- canon = res.exitCode === 0;
2214
- } catch (_) { canon = false; }
2215
- return {
2216
- entry: false, construct: false, update: false, canonicalize: canon,
2217
- graphs: true, canonicalHash: canon,
2218
- shacl: false, shex: false, owlClosure: false, tableau: false, rml: false,
2219
- csvw: false, jsonld: false, jsonldFromRdf: false, didKey: false,
2220
- xml: false, xpath: false, rif: false, cottasBytesStore: false,
2221
- xslt: false, mathml: false, xforms: false, jsonSchema: false,
2222
- schematron: false, toan: false, matrix: false, sigmoid: false, vcCrypto: false,
2223
- };
2224
- }
2225
-
2226
- return {
2227
- parse,
2228
- query,
2229
- queryHdt,
2230
- update,
2231
- registerExtensionFunction,
2232
- unregisterExtensionFunction,
2233
- clearExtensionFunctions,
2234
- registerServiceEndpoint,
2235
- clearServiceEndpoints,
2236
- serialize,
2237
- canonicalize,
2238
- graphs,
2239
- canonicalHash,
2240
- shaclValidate,
2241
- shexValidate,
2242
- owlClosure,
2243
- coreRdfsClosure,
2244
- coreRdfsCheck,
2245
- rdfsPlusClosure,
2246
- rhoDfClosure,
2247
- rhoDfFragmentCheck,
2248
- tableauMaterialise,
2249
- tableauDlInconsistent,
2250
- owlIsConsistent,
2251
- owlEntails,
2252
- rmlMap,
2253
- csvwToRdf,
2254
- jsonldToRdf,
2255
- jsonldFromRdf,
2256
- didKeyResolve,
2257
- xmlWellformed,
2258
- xpathEval,
2259
- clParse,
2260
- clSerialize,
2261
- clAlphaNorm,
2262
- clNormalize,
2263
- rifEval,
2264
- xsltTransform,
2265
- mathmlEval,
2266
- xformsRecalc,
2267
- jsonSchemaValidate,
2268
- schematronValidate,
2269
- toanSummation,
2270
- toanProduct,
2271
- toanSimplify,
2272
- toanDiff,
2273
- toanSubst,
2274
- matrixDeterminant,
2275
- matrixScalarProduct,
2276
- matrixVectorProduct,
2277
- matrixOuterProduct,
2278
- sigmoidPoints,
2279
- sigmoidFormulaMathml,
2280
- vcSha256Hex,
2281
- vcEd25519SecretToPublic,
2282
- vcEd25519Sign,
2283
- vcEd25519Verify,
2284
- vcEddsaCreateFromCanonical,
2285
- vcEddsaVerifyFromCanonical,
2286
- vcCheckCredential,
2287
- vcCheckCredentialSubject,
2288
- vcCheckNoDataLoss,
2289
- vcCheckRelatedResourceDigests,
2290
- openCottas,
2291
- queryCottas,
2292
- closeCottas,
2293
- toCottas,
2294
- capabilities,
2295
- Dataset,
2296
- dataFactory,
2297
- };
2298
- }
2299
-
2300
- module.exports = { buildApi, sniffQueryForm, bindingsFromSrj };