@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.
- package/CHANGELOG.md +121 -0
- package/LICENSE +201 -0
- package/README.md +514 -0
- package/browser-wasm.js +872 -0
- package/browser.d.ts +514 -0
- package/browser.js +2276 -0
- package/factoidal-npm-entry.js +32609 -0
- package/factoidal-npm-entry.wasm.assets/code-7ac046580f1bbdda8dc6.wasm +0 -0
- package/factoidal-npm-entry.wasm.js +455 -0
- package/factoidal.js +27560 -0
- package/factoidal.wasm.assets/code-bbe6099bfb5b10c4c3ab.wasm +0 -0
- package/factoidal.wasm.js +457 -0
- package/fn.d.ts +519 -0
- package/fn.js +916 -0
- package/hacl-init.js +92 -0
- package/hacl-wasm/FStar.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Bignum_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Curve25519_51.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519.wasm +0 -0
- package/hacl-wasm/Hacl_Ed25519_PrecompTable.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_Base.wasm +0 -0
- package/hacl-wasm/Hacl_Hash_SHA2.wasm +0 -0
- package/hacl-wasm/Hacl_IntTypes_Intrinsics.wasm +0 -0
- package/hacl-wasm/LowStar_Endianness.wasm +0 -0
- package/hacl-wasm/WasmSupport.wasm +0 -0
- package/hacl-wasm/api.js +775 -0
- package/hacl-wasm/api.json +3787 -0
- package/hacl-wasm/layouts.json +1 -0
- package/hacl-wasm/loader.js +568 -0
- package/hacl-wasm/shell.js +12 -0
- package/index.d.ts +1068 -0
- package/index.js +237 -0
- package/index.mjs +85 -0
- package/lib/api.js +2140 -0
- package/lib/engine-js.js +165 -0
- package/lib/engine-wasm.js +300 -0
- package/package.json +101 -0
- package/rdfjs.js +540 -0
- package/version.json +101 -0
- package/wasm.d.ts +130 -0
- 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.
|