@factoidal/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +201 -0
  3. package/README.md +514 -0
  4. package/browser-wasm.js +872 -0
  5. package/browser.d.ts +514 -0
  6. package/browser.js +2276 -0
  7. package/factoidal-npm-entry.js +32609 -0
  8. package/factoidal-npm-entry.wasm.assets/code-7ac046580f1bbdda8dc6.wasm +0 -0
  9. package/factoidal-npm-entry.wasm.js +455 -0
  10. package/factoidal.js +27560 -0
  11. package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
  12. package/factoidal.wasm.js +457 -0
  13. package/fn.d.ts +519 -0
  14. package/fn.js +916 -0
  15. package/hacl-init.js +92 -0
  16. package/hacl-wasm/FStar.wasm +0 -0
  17. package/hacl-wasm/Hacl_Bignum.wasm +0 -0
  18. package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
  19. package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
  20. package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
  21. package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
  22. package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
  23. package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
  24. package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
  25. package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
  26. package/hacl-wasm/LowStar_Endianness.wasm +0 -0
  27. package/hacl-wasm/WasmSupport.wasm +0 -0
  28. package/hacl-wasm/api.js +775 -0
  29. package/hacl-wasm/api.json +3787 -0
  30. package/hacl-wasm/layouts.json +1 -0
  31. package/hacl-wasm/loader.js +568 -0
  32. package/hacl-wasm/shell.js +12 -0
  33. package/index.d.ts +1068 -0
  34. package/index.js +237 -0
  35. package/index.mjs +85 -0
  36. package/lib/api.js +2140 -0
  37. package/lib/engine-js.js +165 -0
  38. package/lib/engine-wasm.js +300 -0
  39. package/package.json +101 -0
  40. package/rdfjs.js +540 -0
  41. package/version.json +101 -0
  42. package/wasm.d.ts +130 -0
  43. package/wasm.js +158 -0
package/README.md ADDED
@@ -0,0 +1,514 @@
1
+ # @factoidal/core
2
+
3
+ > First published cut (0.1.0). The API surface is early and may
4
+ > change before 1.0. This package was previously developed in-tree
5
+ > under the placeholder names `factoidal` and `@danbri/foafos`; it was
6
+ > never published under those names. See [CHANGELOG.md](CHANGELOG.md).
7
+
8
+ A formally verified RDF/SPARQL engine for JavaScript and WebAssembly.
9
+ The semantics live in [F\*](https://www.fstar-lang.org/) and are
10
+ compiled via OCaml and js_of_ocaml / wasm_of_ocaml into the engine
11
+ bundles shipped here. There is no hand-written SPARQL evaluator — the
12
+ JavaScript you run was extracted from the same `.fst` specifications
13
+ that verify under Z3 and pass the W3C suites.
14
+
15
+ Verification status qualifier: parser and algebra spec verified in
16
+ F\*; the on-disk backend has unverified OCaml-side optimization layers
17
+ being migrated back to F\* (the npm build does not include the
18
+ on-disk backend).
19
+
20
+ ## Why this instead of N3.js / rdflib.js / Comunica?
21
+
22
+ - **Full SPARQL 1.1 client-side** — query and update evaluated in
23
+ your process by conformance-tested code (631 of 631 W3C SPARQL
24
+ tests, 1031 of 1031 RDF parsing tests on the native build of the
25
+ same source); no server, no endpoint.
26
+ - **RDFC-1.0 canonicalization built in**: `canonicalize()` yields
27
+ standard canonical N-Quads (stable under blank-node renaming — for
28
+ content addressing, dataset diffing, cache keys) without a separate
29
+ library.
30
+ - **RDFS / OWL-RL entailment** as a query option (`entail: "RDFS"`).
31
+ - **RDF/JS data model** (`DataFactory`, terms with `.equals`,
32
+ `DatasetCore`) so it composes with the existing ecosystem.
33
+ - Provenance: every release corresponds to a gates-green commit of
34
+ [danbri/factoidal](https://github.com/danbri/factoidal).
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install @factoidal/core
40
+ ```
41
+
42
+ ## Quickstart
43
+
44
+ The API is **async throughout** — every engine call returns a Promise,
45
+ so `await` each one (the engine bundle loads lazily on first use).
46
+
47
+ ```js
48
+ import { parse, query, serialize, canonicalize, dataFactory }
49
+ from "@factoidal/core";
50
+
51
+ // Parse any supported syntax into an RDF/JS DatasetCore.
52
+ // If your data contains RELATIVE IRIs, pass { baseIRI: ... } —
53
+ // see the note below this example.
54
+ const ds = await parse(`
55
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
56
+ _:a foaf:name "Alice" ; foaf:knows _:b .
57
+ _:b foaf:name "Bob" .
58
+ `, { format: "turtle" });
59
+
60
+ // SELECT — bindings are Map<variableName, RDF/JS Term>.
61
+ const rows = await query(ds, `
62
+ PREFIX foaf: <http://xmlns.com/foaf/0.1/>
63
+ SELECT ?name WHERE { ?p foaf:name ?name } ORDER BY ?name
64
+ `);
65
+ for (const b of rows) console.log(b.get("name").value); // Alice, Bob
66
+
67
+ // ASK — plain boolean.
68
+ const yes = await query(ds, "ASK { ?s ?p ?o }"); // true
69
+
70
+ // Canonical N-Quads (RDFC-1.0): identical output for isomorphic
71
+ // inputs regardless of blank-node labels.
72
+ const c14n = await canonicalize(ds);
73
+
74
+ // Round-trip.
75
+ const nq = await serialize(ds, { format: "nquads" });
76
+ ```
77
+
78
+ > ⚠️ **Pass `baseIRI` when your input uses relative IRIs.** In the
79
+ > current build, statements whose relative IRIs cannot be resolved are
80
+ > **dropped without an error**, so a document can parse to fewer
81
+ > triples than it contains (`{ baseIRI: "https://example.org/doc" }`
82
+ > fixes it). A count check after parsing is a cheap guard. Surfacing
83
+ > these drops as a throw or a warnings channel is tracked in the
84
+ > repository issues.
85
+
86
+ CommonJS: `const factoidal = require("@factoidal/core")`.
87
+
88
+ ### WebAssembly entry
89
+
90
+ ```js
91
+ import { parse, query } from "factoidal/wasm";
92
+ // Same API over the wasm_of_ocaml bundle; needs a Wasm-GC engine
93
+ // (Chrome >= 119, Node >= 22). The unit suite asserts byte-parity
94
+ // between the JS and Wasm entries on parse/SELECT/ASK.
95
+ ```
96
+
97
+ ### Load in the browser without npm
98
+
99
+ You don't need `npm install` (or a bundler) to run this in a browser.
100
+ Two options, in preference order:
101
+
102
+ 1. **This site's own mirror (recommended, same-origin, no build step
103
+ for you).** Every push regenerates `docs/npm/factoidal/` from this
104
+ package (`formal/fstar/build-ocaml.sh npm`'s Pages-mirror step), so
105
+ it's always in sync with what's published here:
106
+
107
+ ```html
108
+ <script type="module">
109
+ import { query } from 'https://danbri.github.io/factoidal/npm/factoidal/browser.js';
110
+ const r = await query(dataTtl, 'SELECT * WHERE { ?s ?p ?o }');
111
+ console.log(r.results.bindings);
112
+ </script>
113
+ ```
114
+
115
+ See [`demo-jsonld-playground.html`](../../docs/fstar-extracted/demo-jsonld-playground.html)
116
+ for a working page built exactly this way (loads `browser.js` from
117
+ `../npm/factoidal/browser.js`, a relative same-origin path).
118
+
119
+ 2. **jsDelivr's GitHub proxy** (serves the correct
120
+ `text/javascript` MIME type; no Pages dependency, works from any
121
+ branch/tag):
122
+
123
+ ```html
124
+ <script type="module">
125
+ import { query } from 'https://cdn.jsdelivr.net/gh/danbri/factoidal@claude/main/npm/factoidal/browser.js';
126
+ </script>
127
+ ```
128
+
129
+ **Do not** link `browser.js` (or `factoidal.js`) via
130
+ `raw.githubusercontent.com`. Raw GitHub serves every file as
131
+ `text/plain`, and browsers refuse to execute a `text/plain` response
132
+ as an ES module (`<script type="module">` fails with a MIME-type
133
+ error) — this bites people who copy a "raw" link expecting it to just
134
+ work. Use the Pages mirror or jsDelivr above instead.
135
+
136
+ `browser.js` also exports `toRdf(text, options)` (parse to sorted
137
+ N-Quads — `--dump-nq` under the hood) and `canonicalize(text,
138
+ options)` (RDFC-1.0 — `--canonicalize`) alongside `query()`; both
139
+ default `options.format` to `'jsonld'` since that's the playground's
140
+ use case, but accept the same formats as `query()`'s `dataFormat`.
141
+
142
+ For multi-file, multi-named-graph datasets (several documents, each
143
+ loaded into its own named graph or the default graph) and a choice of
144
+ extraction target, use `queryDataset(files, queryString, options)`:
145
+
146
+ ```js
147
+ import { queryDataset } from 'https://danbri.github.io/factoidal/npm/factoidal/browser.js';
148
+
149
+ const r = await queryDataset(
150
+ [
151
+ { content: defaultGraphTtl },
152
+ { content: peopleTtl, graph: 'urn:x:people' },
153
+ ],
154
+ 'SELECT * WHERE { GRAPH ?g { ?s ?p ?o } }',
155
+ { entail: 'RDFS', engine: 'js' } // or engine: 'wasm'
156
+ );
157
+ ```
158
+
159
+ On `engine: 'wasm'` it merges the named graphs into one TriG document
160
+ (wasm_of_ocaml's `query()` only takes a single data string) via
161
+ `browser-wasm.js`, loaded on demand. Both `query()` and `queryDataset()`
162
+ attach a non-enumerable `engineMs` (bundle-eval wall-clock time) to the
163
+ returned results object — invisible to `JSON.stringify()`/`Object.keys()`
164
+ so it never perturbs a diff against a W3C `.srx`-derived fixture, but
165
+ readable as `result.engineMs` for timing/observability UIs. This is
166
+ what `docs/fstar-extracted/factoidal-sparql-client.js`'s web component
167
+ is built on, rather than duplicating the engine-invocation logic itself.
168
+
169
+ ### Durable browser persistence (delta log)
170
+
171
+ `browser.js` also exports a small IndexedDB-backed durable-UPDATE log —
172
+ see [`docs/designissues/2026-07-06-browser-persistence.md`](../../docs/designissues/2026-07-06-browser-persistence.md)
173
+ for the full design (why IndexedDB and not OPFS for v1, the tab-close/
174
+ crash guarantee mapping, and the quota/eviction honesty section). Every
175
+ byte moved through these functions is produced/consumed by the same
176
+ F\*-verified `RDF_Store_Columnar_DeltaLog`/`RDF_Store_Columnar_DeltaMerge`
177
+ modules the native on-disk delta log uses (`factoidal serve --rw
178
+ --delta-log`); this is a browser-native persistence path, not a mock:
179
+
180
+ ```js
181
+ import { deltaLogOpen, deltaLogAppend, deltaLogMerge } from
182
+ 'https://danbri.github.io/factoidal/npm/factoidal/browser.js';
183
+
184
+ const handle = await deltaLogOpen(); // opens/creates an IndexedDB database
185
+ await deltaLogAppend(handle, 'INSERT DATA { <urn:x:a> <urn:x:p> "1" }');
186
+ await deltaLogAppend(handle, 'INSERT DATA { <urn:x:b> <urn:x:p> "2" }');
187
+
188
+ // ... reload the page, or close and reopen the browser ...
189
+
190
+ const merged = await deltaLogMerge(handle, ''); // '' = empty base dataset
191
+ console.log(merged); // the two INSERT DATA ops, replayed from IndexedDB
192
+ ```
193
+
194
+ Supported update ops: `INSERT DATA`, `DELETE DATA`, `CLEAR`, `DROP`,
195
+ `CREATE` — the same subset the native `--rw` commit path accepts;
196
+ anything else (`DELETE/INSERT WHERE`, `COPY`, `MOVE`, `ADD`) rejects
197
+ with `ok:false` rather than silently no-op'ing. `deltaLogReadAllHex`,
198
+ `deltaLogDestroy`, and the test-only `_deltaLogCorruptLastForTest` round
199
+ out the surface (see `browser.js`'s own JSDoc for each). This is a
200
+ prototype: no compaction and no `navigator.storage.persist()` wiring
201
+ yet (both named as staged next steps in the design doc), and it does
202
+ not yet back a hub demo page — that is separate, follow-on work.
203
+
204
+ ### RDF/JS interop
205
+
206
+ ```js
207
+ import { dataFactory } from "@factoidal/core";
208
+ const { namedNode, literal, quad, blankNode } = dataFactory;
209
+ const q = quad(blankNode("x"),
210
+ namedNode("http://xmlns.com/foaf/0.1/name"),
211
+ literal("Alice", "en"));
212
+ // Terms implement termType/value/language/datatype/.equals per the
213
+ // RDF/JS data-model spec; Dataset implements DatasetCore
214
+ // (add/delete/has/match/size/iteration).
215
+ ```
216
+
217
+ ## Custom extension functions (SPARQL 1.1 §17.6)
218
+
219
+ Register your own functions by IRI (the
220
+ [Comunica model](https://comunica.dev/docs/query/advanced/extension_functions/));
221
+ sync or async both work. The dispatch semantics are F\*-specified:
222
+ built-in function families always win, and a call to an IRI with no
223
+ registered function is the spec-required error (unbound in
224
+ SELECT/BIND position, row dropped in FILTER position). Issue:
225
+ [#463](https://github.com/danbri/factoidal/issues/463).
226
+
227
+ ```js
228
+ import { query, registerExtensionFunction } from "@factoidal/core";
229
+
230
+ await registerExtensionFunction(
231
+ "http://example.org/fn#isAdult",
232
+ async ([age]) => Number(age.value) >= 18 // args are SRJ-style terms
233
+ );
234
+
235
+ const rows = await query(ds, `
236
+ PREFIX fn: <http://example.org/fn#>
237
+ SELECT ?s WHERE { ?s <http://example.org/age> ?a
238
+ FILTER(fn:isAdult(?a)) }`);
239
+ ```
240
+
241
+ Return a JS primitive (`boolean`/`number`/`string`), an SRJ-style term
242
+ object (`{type:'uri'|'literal'|'bnode', value, datatype?, 'xml:lang'?}`),
243
+ or a Promise of either; `null`/`undefined`/a thrown error is the §17.6
244
+ error. Async functions run over the synchronous verified engine
245
+ through a bounded, memoised re-evaluation loop — within one query
246
+ every call with the same arguments sees one stable answer.
247
+
248
+ ## Functional API (fn)
249
+
250
+ `@factoidal/core/fn` is a strictly functional variant of the API above:
251
+ every extracted engine operation is already a value-to-value
252
+ function — `FnDataset` makes the JS surface match that instead of
253
+ papering over it with RDF/JS's mutable `add`/`delete`. Frozen
254
+ snapshots, free functions instead of methods (so they compose), and
255
+ RDFC-1.0 canonical hashes as a first-class, memoized identity — the
256
+ piece that makes dataflow-style recompute-skipping possible. Full
257
+ design rationale (cost model, backend/streaming extension points):
258
+ [`docs/designissues/2026-07-05-functional-dataset-api.md`](../../docs/designissues/2026-07-05-functional-dataset-api.md).
259
+
260
+ ```js
261
+ const { parse, filter, hash, cell, derive } = require('@factoidal/core/fn');
262
+
263
+ const ds = await parse(`
264
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
265
+ _:a foaf:name "Alice" ; a foaf:Person .
266
+ _:b foaf:name "Bob".
267
+ `);
268
+
269
+ // Every op returns a new FnDataset; ds is never touched.
270
+ const people = filter(ds, (q) => q.predicate.value.endsWith('/type'));
271
+
272
+ // Dataflow: a derive() node recomputes only when its input's content
273
+ // hash actually changes -- not when a new (but equal-content)
274
+ // FnDataset object is set into the cell.
275
+ const source = cell(ds);
276
+ const derived = derive((d) => filter(d, (q) => q.predicate.value.endsWith('name')), source);
277
+ await derived.get(); // computes
278
+ source.set(await parse(sameTextAsBefore));
279
+ await derived.get(); // memoized hit -- same content, new object
280
+
281
+ await hash(ds); // sha256 hex of RDFC-1.0 canonical N-Quads
282
+ ```
283
+
284
+ `fromDataset(dataset)` / `toDataset(fnDataset)` convert to and from
285
+ the mutable RDF/JS `Dataset` above, so the two styles compose freely.
286
+ `union`/`difference`/`filter`/`mapQuads` give set algebra without
287
+ methods; `query`/`entail`/`canonicalize`/`graphs` mirror the plain
288
+ API's capability gating (`capabilities()`) exactly. `validate`/`shex`/
289
+ `fromMapping`/`fromCsvw`/`rif` wrap the SHACL/ShEx/RML/CSVW/RIF
290
+ surface below the same way — FnDataset in, FnDataset (or a plain
291
+ verdict) out:
292
+
293
+ ```js
294
+ const { parse, validate, shex, fromMapping, fromCsvw, rif } = require('@factoidal/core/fn');
295
+
296
+ const data = await parse(personTtl);
297
+ const shapes = await parse(shapesTtl);
298
+ const { conforms, report } = await validate(data, shapes); // SHACL
299
+
300
+ const ok = await shex(data, shexSchemaJson, 'http://example.org/alice'); // true|false|null
301
+
302
+ const mapped = await fromMapping(await parse(rmlMappingTtl), csvOrJsonText, 'csv'); // RML
303
+
304
+ const tabular = await fromCsvw(csvText, csvwMetadataJson, { mode: 'minimal' }); // CSVW csv2rdf
305
+
306
+ const saturated = await rif(data, rifRulesXml); // RIF Core forward chaining
307
+ ```
308
+
309
+ ## API (draft)
310
+
311
+ The `factoidal` CLI (`bin/factoidal-cli/factoidal_cli.ml`, built to
312
+ `bin/<platform>/factoidal`) calls the exact same F\*-extracted
313
+ functions as this npm surface — see `tests/local/cli_api_parity.sh`
314
+ for the test that diffs CLI output against the npm API on shared
315
+ fixtures.
316
+
317
+ | Function | Signature (informal) | CLI equivalent | Notes |
318
+ |---|---|---|---|
319
+ | `parse` | `(text, {format?, baseIRI?}) => Dataset` | `factoidal dump-nq -d FILE` (or `dump`/`dump-turtle`) | formats: `turtle`, `ntriples`, `nquads`, `trig`, `rdfxml`, `jsonld`\* — auto-detected where possible. Each call is one document: blank-node labels are scoped per RDF 1.1 |
320
+ | `query` | `(Dataset \| string, sparql, {entail?}) => Bindings[] \| boolean \| Dataset` | `factoidal query -d FILE -e 'SPARQL' [--entail RDFS\|OWL-RL]` | SELECT → array of `Map<var, Term>`; ASK → boolean; CONSTRUCT → Dataset\*\*; `entail: "RDFS" \| "OWL-RL"` |
321
+ | `update` | `(Dataset, sparqlUpdate) => Dataset` | `factoidal update -d FILE -e 'SPARQL update'` | \*\* in-memory; no persistence. (Durable UPDATE against a COTTAS store is a separate path: `factoidal serve --rw --delta-log ...` / `factoidal compact`.) |
322
+ | `serialize` | `(Dataset, {format}) => string` | `factoidal dump-nq FILE` (nquads) / `factoidal dump FILE` (ntriples) / `factoidal dump-turtle FILE` | `nquads`, `ntriples` (sorted); `turtle`\*\* (prefix-compacted, subject-grouped — needs the entry bundle, flattens named graphs into the default graph) |
323
+ | `canonicalize` | `(Dataset \| string) => string` | `factoidal canonicalize FILE` | RDFC-1.0 canonical N-Quads\*\* |
324
+ | `graphs` | `(Dataset) => Array<[iri, Dataset]>` | `factoidal graphs list FILE` | enumerate named graphs (default graph excluded); pure enumeration, no engine round-trip |
325
+ | `canonicalHash` | `(Dataset) => string` | `factoidal graphs hash FILE IRI` | RDFC-1.0 canonical hash of one graph\*\*; graph-scoped sibling of `canonicalize` — typically called with one entry of `graphs()`'s output |
326
+ | `shaclValidate` | `(data, shapes) => {conforms, report: Dataset}` | `factoidal shacl --data FILE --shapes FILE [--json]` (alias: `factoidal validate --shapes FILE FILE`) | \*\* SHACL Core validation; `report` is the `sh:ValidationReport` graph; exit code 0 iff `sh:conforms` |
327
+ | `shexValidate` | `(data, schemaText, focus, shape?) => boolean \| null` | `factoidal shex --data FILE --schema FILE.{json,shex} --node N [--shape S]` | \*\* ShEx (Shape Expressions) validation of one focus node; `schemaText` accepts ShExJ or ShExC — dispatched by the first non-whitespace character (`{` ⇒ ShExJ, else ShExC; the CLI additionally honors a `.shex` file extension); `null` = outside this engine's decidable ShEx fragment, never a guessed answer |
328
+ | `owlClosure` | `(data, mode) => Dataset` | `factoidal entail --data FILE --regime RDFS\|OWL-RL` | \*\* materializes the entailment closure (input + derived triples), default graph only. (`query --entail` applies the same closure internally before evaluating a query, but does not dump it on its own.) |
329
+ | `rmlMap` | `(mapping, sourceData, sourceKind) => Dataset` | `factoidal rml --mapping FILE --source FILE --kind json\|csv` | \*\* evaluates an RML mapping graph against one logical source (`sourceKind: "json" \| "csv"`); every triples map reads the SAME source — cross-source joins are out of scope for this entry point |
330
+ | `csvwToRdf` | `(csvText, metadataJson?, {mode?, base?, url?}) => Dataset` | `factoidal csvw --csv FILE [--metadata FILE] [--minimal] [--base IRI] [--url URL]` | \*\* CSVW csv2rdf conversion; metadata omitted = schema inferred from the CSV header row; `mode: "standard" \| "minimal"` (default standard); every table in a multi-table group reads the SAME csvText |
331
+ | `jsonldToRdf` | `(jsonldText, {base?, rdfDirection?, expandContext?, processingMode?}) => Dataset` | `factoidal jsonld --in FILE [--base IRI]` (or `factoidal dump-nq FILE.jsonld`, format auto-detected) | \*\* JSON-LD parsing with options `parse()` has no room for; plain `parse(text, {format:'jsonld'})` also works for the common case |
332
+ | `jsonldFromRdf` | `(data, {useNativeTypes?, useRdfType?}) => JSON-LD` | `factoidal dump-nq` ← (reverse) | \*\* serializes an RDF dataset as expanded-form JSON-LD (the reverse of `jsonldToRdf`); returns the parsed JSON-LD document (array of node objects), JCS-canonical |
333
+ | `didKeyResolve` | `(didString) => Dataset` | N/A (`did_runner`) | \*\* resolves a `did:key:z6Mk...` (Ed25519) to its DID Document as RDF; non-Ed25519 / malformed inputs reject rather than guess |
334
+ | `xmlWellformed` | `(xmlText) => boolean` | N/A (`xml_runner`) | \*\* XML 1.0 well-formedness check (byte-oriented, no DOCTYPE/DTD production — a document with a DOCTYPE reports `false`) |
335
+ | `xpathEval` | `(xmlText, xpathExpr) => {resultType, value \| nodes, ...}` | N/A | \*\* XPath 1.0 evaluation over an XML document; `resultType` is `'nodeset' \| 'string' \| 'number' \| 'boolean'` |
336
+ | `rifEval` | `(data, rifRulesXml) => Dataset` | `factoidal rif --rules FILE --data FILE` | \*\* RIF Core forward-chaining saturation (materializes input + derived triples); accepts real vendored RIF-XML (`<!DOCTYPE>` + `&rif;`/`&xs;`/`&rdf;` entities) unmodified |
337
+ | `toCottas` | `(data, {format?}) => Uint8Array` | `factoidal compact --native-writer` | \*\* serializes a dataset to COTTAS/Parquet bytes via the native writer; round-trips into `openCottas()` and into the native `--data-cottas`/`--data-cottas-mem` CLI flags byte-for-byte |
338
+ | `openCottas` | `(bytes: string \| Uint8Array \| ArrayBuffer) => handle` | `factoidal query --data-cottas-mem FILE` | \*\* opens a whole `.cottas` artifact's bytes as a queryable, read-only, in-memory store; rows decode lazily as `queryCottas()` touches them (no heap `Dataset`, no full parse) |
339
+ | `queryCottas` | `(handle, sparql) => Bindings[] \| boolean \| Dataset` | `factoidal query --data-cottas-mem FILE -e 'SPARQL'` | \*\* SPARQL over a store opened by `openCottas()`; no `entail` option, no write overlay (read-only), no DESCRIBE |
340
+ | `closeCottas` | `(handle) => void` | N/A | releases a handle from this process's registry; does not evict the underlying byte cache |
341
+ | `queryRaw` | `(input, sparql) => string` | `factoidal query -d FILE -e 'SPARQL' -o json` | SPARQL-Results-JSON string, for callers that want the wire form |
342
+ | `capabilities` | `() => {construct, update, canonicalize, graphs, canonicalHash, shacl, shex, owlClosure, rml, csvw, jsonld, jsonldFromRdf, didKey, xml, xpath, rif, cottasBytesStore, ...}` | N/A | runtime feature probe; the CLI is one fixed native binary, not a runtime bundle whose feature set varies |
343
+ | `dataFactory` | RDF/JS DataFactory | N/A | data-model class, not an engine operation |
344
+ | `Dataset` | RDF/JS DatasetCore | N/A | returned by `parse`; accepted everywhere |
345
+
346
+ The `fn.js` functional layer's own combinators — `union`, `difference`,
347
+ `filter`, `mapQuads`, `equals`, `hash`, `builder`/`fromChunks`,
348
+ `cell`/`derive` — are pure client-side set algebra and dataflow
349
+ plumbing over already-materialized `Dataset`s. They have no CLI
350
+ equivalent by design: there is no engine operation to wrap, only JS
351
+ composition on top of the operations already listed above.
352
+
353
+ \* JSON-LD parsing (expanded form, inline `@context`, `@base`
354
+ resolution, `@reverse`, container maps) works through both `parse()`
355
+ and `jsonldToRdf()` when the npm-entry bundle is loaded. Remote
356
+ `@context` URLs need a `documentLoader`, which this package's entries
357
+ don't register (an honest failure, not a silent wrong answer) —
358
+ tracked against the vendored W3C json-ld-api suite.
359
+ \*\* CONSTRUCT, UPDATE, `canonicalize`, `canonicalHash`,
360
+ `shaclValidate`, `shexValidate`, `owlClosure`, `rmlMap`, `csvwToRdf`,
361
+ `jsonldToRdf`, `jsonldFromRdf`, `didKeyResolve`, `xmlWellformed`, `xpathEval`, `rifEval`, `toCottas`, `openCottas`, `queryCottas`, and
362
+ `closeCottas` are probed via `capabilities()`: they activate
363
+ automatically when the dedicated npm-entry engine bundle is present,
364
+ and the package reports their absence honestly against older bundles
365
+ instead of guessing.
366
+ `canonicalHash` rides the same engine support as `canonicalize` (it
367
+ computes `canonicalize()` over one graph's triples); `graphs` is pure
368
+ JS enumeration and is always available.
369
+
370
+ ### The db API (openCottas/queryCottas/closeCottas/toCottas)
371
+
372
+ The in-memory COTTAS/Parquet bytes store works identically on both
373
+ engines: `require('@factoidal/core')` (js_of_ocaml) and
374
+ `require('factoidal/wasm')` (wasm_of_ocaml, Node ≥ 22) expose the same
375
+ `toCottas`/`openCottas`/`queryCottas`/`closeCottas` functions, backed
376
+ by the same F\*-verified reader
377
+ (`RDF.CottasStore`/`Parquet.Footer`/`SPARQL11_Store`). A store is
378
+ read-only and holds no `entail` option — see the divergence list on
379
+ `queryCottas`'s doc comment in `lib/api.js`. Zstd-compressed COTTAS
380
+ pages are supported under js_of_ocaml (via the vendored `fzstd`
381
+ decompressor baked into `factoidal.js`); under wasm_of_ocaml, Zstd
382
+ decompression is still an identity-stubbed primitive (a documented
383
+ gap, tracked separately) — write bytes with `toCottas()` (which never
384
+ reaches for Zstd on small in-memory writes) rather than a
385
+ Zstd-compressed on-disk fixture if you need a wasm-portable test case.
386
+ The browser entries mirror this: `browser.js`'s `openCottas`/
387
+ `queryCottas`/`closeCottas`/`toCottas` drive the js_of_ocaml npm-entry
388
+ ABI over `fetch()`; `browser-wasm.js` exposes the same four functions
389
+ against the wasm_of_ocaml npm-entry ABI (`factoidal-npm-entry.wasm.js`).
390
+
391
+ ## Capability matrix
392
+
393
+ Every public function, where it runs, and what it needs. "Node" is the
394
+ `require('@factoidal/core')` / `import '@factoidal/core'` entry (`index.js` /
395
+ `index.mjs`); "Browser" is `import 'factoidal/browser'` (`browser.js`);
396
+ "Wasm" is `require('factoidal/wasm')`. **Needs** legend: *CLI bundle* =
397
+ the fresh-eval `factoidal.js` bundle (always present); *entry* = the
398
+ persistent `factoidal-npm-entry.js` ABI bundle (probe with
399
+ `capabilities()`); *HACL\* init* = the wasm crypto backend must be
400
+ initialised (auto on Node, explicit in the browser — see below);
401
+ *IndexedDB* = a browser storage layer.
402
+
403
+ | Function(s) | Node | Browser | Wasm | Needs |
404
+ |---|---|---|---|---|
405
+ | `parse`, `query` (SELECT/ASK), `serialize` (nquads/ntriples), `canonicalize`, `graphs`, `canonicalHash`, `queryHdt`, `queryRaw` | ✓ | ✓\* | ✓ | CLI bundle |
406
+ | `query` (CONSTRUCT), `update`, `serialize` (turtle) | ✓ | ✓\* | ✓ | entry |
407
+ | `shaclValidate`, `shexValidate`, `owlClosure`, `rmlMap`, `csvwToRdf`, `jsonldToRdf`, `jsonldFromRdf`, `didKeyResolve`, `xmlWellformed`, `xpathEval`, `rifEval` | ✓ | ✓ | partial† | entry |
408
+ | `coreRdfsClosure`/`rhoDfClosure`, `coreRdfsCheck`/`rhoDfFragmentCheck`, `rdfsPlusClosure`, `tableauMaterialise`, `tableauDlInconsistent`, `owlIsConsistent`, `owlEntails` | ✓ | ✓ | wrapper only‡ | entry |
409
+ | `xsltTransform`, `mathmlEval`, `xformsRecalc`, `jsonSchemaValidate`, `schematronValidate`, `toan*`, `matrix*` | ✓ | ✓ | partial† | entry |
410
+ | `openCottas`, `queryCottas`, `closeCottas`, `toCottas` | ✓ | ✓ | ✓ | entry |
411
+ | `vcSha256Hex`, `vcEd25519SecretToPublic`, `vcEd25519Sign`, `vcEd25519Verify`, `vcEddsaCreateFromCanonical`, `vcEddsaVerifyFromCanonical` | ✓ | ✓ | ✓ | entry + HACL\* init |
412
+ | `deltaLogOpen`/`Append`/`ReadAllHex`/`Merge`/`Destroy` | ✗ | ✓ | ✗ | entry + IndexedDB |
413
+ | `dataFactory`, `Dataset`, `fn.*` combinators (`union`/`difference`/`filter`/`mapQuads`/`equals`/`hash`/`builder`/`cell`/`derive`/`pipe`) | ✓ | — | ✓ | none (pure JS) |
414
+ | `capabilities` | ✓ | — | ✓ | none (probe) |
415
+
416
+ \* In the browser the same capability is reached through `browser.js`'s
417
+ own function names, which differ from the Node API: SELECT/ASK is
418
+ `query(dataString, sparql, {output})` (returns SPARQL-Results JSON, not
419
+ `Bindings[]`); parse-to-N-Quads is `toRdf()`; multi-graph queries use
420
+ `queryDataset()`. CONSTRUCT/UPDATE/Turtle ride the same `entry` bundle,
421
+ fetched over the network on first use.
422
+ † Wasm (`require('factoidal/wasm')`) today re-exports the core +
423
+ validation/inference/COTTAS set (`parse`/`query`/`update`/`serialize`/
424
+ `canonicalize`/`shaclValidate`/`shexValidate`/`owlClosure`/`rmlMap`/
425
+ `csvwToRdf`/`jsonldToRdf`/`rifEval`/`openCottas`…/`capabilities`); the
426
+ typed-engine `#74` functions and the `vc*`/`did`/`xml`/`xpath` wrappers
427
+ are exposed on the js and browser entries. The underlying wasm ABI
428
+ carries them — the `/wasm` re-export surface is being brought to parity.
429
+ ‡ `coreRdfsClosure`/`rhoDfClosure`/`coreRdfsCheck`/`rhoDfFragmentCheck`/
430
+ `rdfsPlusClosure`/`tableauMaterialise`/`tableauDlInconsistent`/
431
+ `owlIsConsistent`/`owlEntails` are exported from `factoidal/wasm`
432
+ (wrapper wiring — the same `buildApi()` `index.js` uses), but the
433
+ committed `factoidal-npm-entry.wasm.js` ABI bundle predates these
434
+ functions (built before they landed on the ABI) — calling them on the
435
+ wasm engine throws the existing "pending npm-entry bundle" error until
436
+ that bundle is rebuilt via a real `wasm_of_ocaml` build (not a copy).
437
+ `capabilities()` on the wasm engine reports this honestly (`tableau:
438
+ false` etc.) rather than guessing.
439
+
440
+ ### VC crypto: the init story
441
+
442
+ The `vc*` functions run the F\*-extracted VC Data Integrity pipeline
443
+ over HACL\*'s official WebAssembly build, so the wasm backend has to be
444
+ initialised before the first call (a verify against an uninitialised
445
+ backend **throws** — it never silently returns `true`; issue #286).
446
+
447
+ - **Node** (`index.js`/`index.mjs`, `/wasm`, `fn`): the typed wrappers
448
+ **auto-await `initHacl()` on first call** — a caller writes
449
+ `await vcEd25519Verify(pk, msg, sig)` and never touches init.
450
+ - **Browser** (`browser.js`): **explicit init required** — the
451
+ `hacl-wasm` URL is page-specific. Serve `hacl-wasm/` next to the page
452
+ and `await initHacl({ apiUrl })` from `hacl-init.js` once, then call
453
+ the `vc*` wrappers. (Auto-init isn't possible without a URL to fetch.)
454
+
455
+ ### Native-CLI-only capabilities (not on the npm surface, by design)
456
+
457
+ These live only in `bin/factoidal-cli` (the native `factoidal` binary),
458
+ not in the JS package — they are I/O- or process-shaped, not pure
459
+ value transforms:
460
+
461
+ - **SPARQL endpoint / federation.** `factoidal serve` (HTTP endpoint),
462
+ `SERVICE` federation and `--data-cottas`/`--data-hdt` *file-path*
463
+ backends read the filesystem/network at query time. The npm
464
+ `queryHdt`/`openCottas` take *bytes* in-process instead.
465
+ - **Durable UPDATE against on-disk COTTAS.** `factoidal serve --rw
466
+ --delta-log …` + `factoidal compact` — the npm `update()` is
467
+ in-memory only; the browser `deltaLog*` family is the closest
468
+ in-package durable path (IndexedDB, browser-only).
469
+ - **Internal primitives.** `runFactoidalCli` / `loadNpmEntry` (browser)
470
+ are exported but are the low-level bundle drivers, not the intended
471
+ API; the `_*` functions (e.g. `_deltaLogCorruptLastForTest`) are
472
+ test-only and intentionally left untyped.
473
+
474
+ ### GeoSPARQL
475
+
476
+ There is no separate GeoSPARQL function: the `geof:` functions
477
+ (`geof:sfWithin`, `geof:sfDisjoint`, `geof:distance`, `geof:envelope`,
478
+ …) are built into the SPARQL engine and work through ordinary
479
+ `query()` / `fn.query()` — e.g.
480
+ `query(data, 'PREFIX geof: <http://www.opengis.net/def/function/geosparql/> SELECT ?a ?b WHERE { … FILTER(geof:sfWithin(?a, ?b)) }')`.
481
+ Nothing to import; nothing "missing".
482
+
483
+ ## Limits (deliberate, documented)
484
+
485
+ - **In-memory only.** ~1.2 KB RAM per quad (measured); 1M quads ≈
486
+ 1.2 GB. No streaming parse yet — inputs are whole strings.
487
+ - Lenient Turtle parsing: `parse()` cannot yet reject syntax errors
488
+ (bad input can yield an empty dataset).
489
+ - No *write* persistence in the npm build (SPARQL Update stays
490
+ in-memory; durable UPDATE against a COTTAS store on disk is
491
+ native-only today). *Reading* a COTTAS artifact's bytes is available
492
+ in-process via `openCottas`/`queryCottas`/`closeCottas`/`toCottas`
493
+ (both engines, both npm and browser entries) — see "The db API"
494
+ above.
495
+ - Bundle sizes (measured 2026-07-04): JS engine 554 KB, npm entry
496
+ 461 KB, Wasm 43 KB loader + 1.3 MB assets. The Wasm entry trades
497
+ startup cost for throughput.
498
+
499
+ ## Testing
500
+
501
+ `npm test` — unit suite covering the parse/query/serialize surface,
502
+ the RDF/JS contract, canonicalization stability under blank-node
503
+ renaming, and JS↔Wasm byte-parity. The engine underneath is gated on
504
+ every landing by the full W3C suites, a cross-backend parity harness,
505
+ and comparison probes against Apache Jena ARQ — live scores with
506
+ dates and commit links at
507
+ [danbri.github.io/factoidal/test-results](https://danbri.github.io/factoidal/test-results/).
508
+
509
+ ## Provenance & license
510
+
511
+ Built from [danbri/factoidal](https://github.com/danbri/factoidal)
512
+ (Apache-2.0). This package's JavaScript is a thin consumer layer (API
513
+ shaping + RDF/JS conversion) containing no RDF/SPARQL semantics of
514
+ its own — the semantics are F\*-extracted.