@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/fn.js ADDED
@@ -0,0 +1,916 @@
1
+ // factoidal/fn — a strictly functional dataset API.
2
+ //
3
+ // Every extracted engine operation is a value-to-value function (F*
4
+ // has no mutation to expose); RDF/JS DatasetCore's add/delete is
5
+ // wrapper-side skeuomorphism on top of that. This module makes the JS
6
+ // contract match the verified one: FnDataset is a frozen snapshot, all
7
+ // operations are free functions that return new values, and content
8
+ // identity (RDFC-1.0 canonical hash) is exposed as a first-class,
9
+ // memoized value so callers can build dataflow graphs that skip
10
+ // recompute when nothing actually changed.
11
+ //
12
+ // Design rationale, cost model, and the dataflow (cell/derive) pattern
13
+ // are documented in
14
+ // docs/designissues/2026-07-05-functional-dataset-api.md. Read that
15
+ // first if any decision below looks arbitrary — none of them are.
16
+ //
17
+ // This module is additive: it does not change rdfjs.js or lib/api.js.
18
+ // It wraps the already-built JS-engine api (./index.js) — no engine
19
+ // plumbing is forked here, only a frozen data shape and pure
20
+ // composition on top of it.
21
+
22
+ 'use strict';
23
+
24
+ const crypto = require('node:crypto');
25
+
26
+ const engineApi = require('./index.js');
27
+ const {
28
+ Dataset,
29
+ dataFactory,
30
+ quadToNQuads,
31
+ quadsToNQuads,
32
+ } = require('./rdfjs.js');
33
+
34
+ // ---------------------------------------------------------------------
35
+ // FnDataset — a frozen snapshot of a quad set.
36
+ //
37
+ // Internal representation is exactly rdfjs.js's model (an array of
38
+ // already-frozen RDF/JS Quad terms) — the same representation
39
+ // lib/api.js's toDocs() already treats as the engine's interchange
40
+ // handle via toNQuads(). FnDataset adds nothing to that representation
41
+ // except: (a) freezing the wrapper itself so no method can mutate it,
42
+ // (b) de-duplication on construction so every FnDataset is a proper
43
+ // *set* of quads (no duplicate tokens), and (c) lazy, memoized
44
+ // derived values (raw N-Quads text, canonical hash) held in
45
+ // module-level WeakMaps rather than object fields — Object.freeze(ds)
46
+ // stays true for the whole lifetime of ds; the memo tables are a side
47
+ // channel, not a hole in the immutability contract.
48
+ // ---------------------------------------------------------------------
49
+
50
+ // ds (FnDataset) -> string, the cheap non-canonical N-Quads
51
+ // serialization. Computed once per instance, cached by identity.
52
+ const NQUADS_CACHE = new WeakMap();
53
+ // ds -> string (once settled) | Promise<string> (in flight), the
54
+ // RDFC-1.0 canonical N-Quads text. Shared with HASH_CACHE's input so
55
+ // two callers awaiting hash(ds) concurrently trigger one canonicalize
56
+ // call, not two.
57
+ const CANON_CACHE = new WeakMap();
58
+ // ds -> string (once settled) | Promise<string> (in flight), the
59
+ // sha256 hex digest of the canonical N-Quads text.
60
+ const HASH_CACHE = new WeakMap();
61
+
62
+ function assertFnDataset(v, who) {
63
+ if (!(v instanceof FnDataset)) {
64
+ throw new TypeError(`${who}: expected an FnDataset`);
65
+ }
66
+ return v;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------
70
+ // Backend interface — capabilities-style, mirroring lib/api.js's
71
+ // `driver` shape (buildApi(driver) there; FnDataset(backend) here).
72
+ // FnDataset does NOT assume its representation is an in-memory quad
73
+ // array or an N-Quads string; it holds an opaque `_backend` satisfying
74
+ // this interface:
75
+ //
76
+ // size : number
77
+ // quads() : Iterable<Quad> -- read access; may materialize
78
+ // precomputedHash : string | null -- skip canonicalize() if set
79
+ //
80
+ // Exactly one backend exists today, arrayBackend, below. It exists so
81
+ // that a future on-disk (COTTAS) backend or a sidecar-hash backend can
82
+ // implement this same three-member interface without any free
83
+ // function in this module changing shape — see the design doc's
84
+ // "future backends" section for what those would look like and why
85
+ // filter()/mapQuads() below (which fully materialize via toArray())
86
+ // are the concrete piece of debt such a backend would need to repay
87
+ // with pushdown, not something this slice claims to have solved.
88
+ // ---------------------------------------------------------------------
89
+
90
+ function arrayBackend(quads) {
91
+ return {
92
+ kind: 'array',
93
+ size: quads.length,
94
+ quads: () => quads,
95
+ precomputedHash: null,
96
+ };
97
+ }
98
+
99
+ // Build a de-duplicated FnDataset from any iterable of RDF/JS quads.
100
+ // Every public constructor path (fromDataset, union, difference,
101
+ // filter, mapQuads, query's CONSTRUCT wrapping, builder().finish())
102
+ // funnels through this, so "FnDataset quads never repeat" is an
103
+ // invariant, not a hope. Always produces an arrayBackend today — a
104
+ // COTTAS-backed constructor would be a sibling entry point, not a
105
+ // change to this one.
106
+ function makeFnDataset(quadsIterable) {
107
+ const seen = new Set();
108
+ const out = [];
109
+ for (const q of quadsIterable) {
110
+ const frozen = dataFactory.fromQuad(q); // normalizes + (re)freezes
111
+ const key = quadToNQuads(frozen);
112
+ if (seen.has(key)) continue;
113
+ seen.add(key);
114
+ out.push(frozen);
115
+ }
116
+ return new FnDataset(arrayBackend(Object.freeze(out)));
117
+ }
118
+
119
+ class FnDataset {
120
+ /** @private use fromDataset/parse/union/... instead of `new`. */
121
+ constructor(backend) {
122
+ this._backend = backend;
123
+ Object.freeze(this);
124
+ }
125
+
126
+ get size() {
127
+ return this._backend.size;
128
+ }
129
+
130
+ [Symbol.iterator]() {
131
+ return this._backend.quads()[Symbol.iterator]();
132
+ }
133
+
134
+ /** A fresh array of this dataset's (frozen) quads. Materializes. */
135
+ toArray() {
136
+ return [...this._backend.quads()];
137
+ }
138
+
139
+ /**
140
+ * Raw N-Quads text, arrival order, not canonicalized. Cheap (pure
141
+ * JS serialization, no engine round-trip) for the array backend;
142
+ * memoized. This is *identity-adjacent*, not identity — two
143
+ * isomorphic datasets with different blank-node labels serialize to
144
+ * different text here. Use hash()/canonicalize() for
145
+ * label-independent identity.
146
+ */
147
+ toNQuads() {
148
+ let cached = NQUADS_CACHE.get(this);
149
+ if (cached === undefined) {
150
+ cached = quadsToNQuads(this.toArray());
151
+ NQUADS_CACHE.set(this, cached);
152
+ }
153
+ return cached;
154
+ }
155
+
156
+ /** Read-only quad-pattern match (RDF/JS DatasetCore shape), no engine call. */
157
+ match(subject, predicate, object, graph) {
158
+ return makeFnDataset(this.toArray().filter((q) =>
159
+ (!subject || subject.equals(q.subject)) &&
160
+ (!predicate || predicate.equals(q.predicate)) &&
161
+ (!object || object.equals(q.object)) &&
162
+ (!graph || graph.equals(q.graph))));
163
+ }
164
+ }
165
+
166
+ /** The empty dataset — the identity element for union() and difference(). */
167
+ const EMPTY = new FnDataset(arrayBackend(Object.freeze([])));
168
+
169
+ // ---------------------------------------------------------------------
170
+ // Interop with the mutable RDF/JS layer.
171
+ // ---------------------------------------------------------------------
172
+
173
+ /** Snapshot a (possibly mutable) RDF/JS Dataset into an FnDataset. */
174
+ function fromDataset(dataset) {
175
+ if (!(dataset instanceof Dataset)) {
176
+ throw new TypeError('fromDataset: expected an RDF/JS Dataset');
177
+ }
178
+ return makeFnDataset(dataset);
179
+ }
180
+
181
+ /** Materialize an FnDataset into a fresh, independently-mutable Dataset. */
182
+ function toDataset(ds) {
183
+ assertFnDataset(ds, 'toDataset');
184
+ return new Dataset(ds.toArray());
185
+ }
186
+
187
+ // ---------------------------------------------------------------------
188
+ // Streaming seam: builder() / fromChunks().
189
+ //
190
+ // This module's `parse()` needs the whole document as one string
191
+ // (mirroring index.js's parse()); a future streaming RDF parser would
192
+ // instead produce quads incrementally, batch by batch, without ever
193
+ // holding the whole document in memory at once. builder() is the
194
+ // intended integration point for that: a mutable accumulator that
195
+ // FINALIZES into a frozen FnDataset. Everything upstream of finish()
196
+ // (a hypothetical streaming Turtle/N-Quads reader feeding addChunk()
197
+ // per parsed batch) is not part of this slice; everything downstream —
198
+ // every op in this module — only ever sees the finished, immutable
199
+ // value finish() returns, never the accumulator itself. The
200
+ // implementation below is deliberately the trivial in-memory case
201
+ // (accumulate an array, dedup once at finish()); it exists so the
202
+ // call shape is settled before a real streaming parser exists.
203
+ // ---------------------------------------------------------------------
204
+
205
+ /**
206
+ * A mutable accumulator for incrementally-arriving quads. addChunk()
207
+ * accepts one quad or an iterable of quads (so a parser can hand over
208
+ * whatever batch size is convenient); finish() de-duplicates once and
209
+ * returns a frozen FnDataset. Calling either method after finish() throws.
210
+ */
211
+ function builder() {
212
+ let pending = [];
213
+ let finished = false;
214
+ return {
215
+ addChunk(chunk) {
216
+ if (finished) throw new Error('builder: addChunk() called after finish()');
217
+ if (chunk && typeof chunk.termType === 'string') {
218
+ pending.push(chunk); // a single Quad
219
+ } else {
220
+ for (const q of chunk) pending.push(q); // an iterable of Quads
221
+ }
222
+ },
223
+ finish() {
224
+ if (finished) throw new Error('builder: finish() already called');
225
+ finished = true;
226
+ const result = makeFnDataset(pending);
227
+ pending = null; // release; no further mutation is possible anyway
228
+ return result;
229
+ },
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Sugar over builder(): consume a (sync or async) iterable of chunks
235
+ * into one finished FnDataset. A streaming parser exposing an async
236
+ * iterator of quad batches plugs in here directly.
237
+ */
238
+ async function fromChunks(chunks) {
239
+ const b = builder();
240
+ for await (const chunk of chunks) b.addChunk(chunk);
241
+ return b.finish();
242
+ }
243
+
244
+ // ---------------------------------------------------------------------
245
+ // Parsing, algebra, query — all free functions.
246
+ // ---------------------------------------------------------------------
247
+
248
+ /** Parse one RDF document into an FnDataset. Mirrors index.js's parse(). */
249
+ async function parse(text, options) {
250
+ return fromDataset(await engineApi.parse(text, options));
251
+ }
252
+
253
+ /** Set union, first-seen order (a's quads, then b's not already present). */
254
+ function union(a, b) {
255
+ assertFnDataset(a, 'union'); assertFnDataset(b, 'union');
256
+ return makeFnDataset([...a, ...b]);
257
+ }
258
+
259
+ /** Quads in a that are not in b. */
260
+ function difference(a, b) {
261
+ assertFnDataset(a, 'difference'); assertFnDataset(b, 'difference');
262
+ const bKeys = new Set(b.toArray().map(quadToNQuads));
263
+ return makeFnDataset(a.toArray().filter((q) => !bKeys.has(quadToNQuads(q))));
264
+ }
265
+
266
+ /** Keep quads matching quadPred(quad) => boolean. */
267
+ function filter(ds, quadPred) {
268
+ assertFnDataset(ds, 'filter');
269
+ if (typeof quadPred !== 'function') {
270
+ throw new TypeError('filter: quadPred must be a function');
271
+ }
272
+ // A subset of an already-deduplicated set has no new duplicates to
273
+ // remove; still normalize each quad for the frozen-terms guarantee.
274
+ return new FnDataset(arrayBackend(Object.freeze(
275
+ ds.toArray().filter(quadPred).map((q) => dataFactory.fromQuad(q)))));
276
+ }
277
+
278
+ /**
279
+ * Transform every quad with f(quad) => quad. Output is re-deduplicated
280
+ * (mapQuads can collapse distinct inputs onto the same output quad —
281
+ * e.g. a rename that merges two subjects — and FnDataset is always a
282
+ * set, never a multiset).
283
+ */
284
+ function mapQuads(ds, f) {
285
+ assertFnDataset(ds, 'mapQuads');
286
+ if (typeof f !== 'function') {
287
+ throw new TypeError('mapQuads: f must be a function');
288
+ }
289
+ return makeFnDataset(ds.toArray().map(f));
290
+ }
291
+
292
+ /**
293
+ * Run a SPARQL 1.1 query. SELECT -> Bindings[]; ASK -> boolean;
294
+ * CONSTRUCT -> FnDataset. Delegates entirely to index.js's query() —
295
+ * same capability gating (CONSTRUCT needs the npm-entry bundle; its
296
+ * absence throws the existing "pending npm-entry build" Error).
297
+ */
298
+ async function query(ds, sparql, options) {
299
+ assertFnDataset(ds, 'query');
300
+ const result = await engineApi.query(toDataset(ds), sparql, options);
301
+ return result instanceof Dataset ? fromDataset(result) : result;
302
+ }
303
+
304
+ /**
305
+ * Materialize an entailment closure as a new FnDataset.
306
+ *
307
+ * Reuses query()'s existing entailment support rather than adding new
308
+ * engine plumbing — but *not* via CONSTRUCT: the npm-entry ABI's
309
+ * queryDataset has no entailment parameter (entailment closure stays
310
+ * on the CLI path, per lib/api.js), and the CLI path does not support
311
+ * CONSTRUCT at all, so "CONSTRUCT ... WHERE ... { entail }" is not
312
+ * reachable through the existing API today. Instead this runs
313
+ * `SELECT ?s ?p ?o WHERE { ?s ?p ?o }` with the entailment option
314
+ * (exactly the combination test/api.test.js's "query: entail RDFS
315
+ * infers subclass instances" already exercises) and repackages each
316
+ * solution row as a quad — pure JS reshaping of already-materialized
317
+ * bindings, not new RDF/SPARQL semantics, so it works with the CLI
318
+ * bundle alone (no npm-entry bundle required).
319
+ *
320
+ * Scope limitation (documented, not silent): like a bare
321
+ * `WHERE { ?s ?p ?o }` in SPARQL generally, this closes over the
322
+ * *default graph* only — named-graph triples are not restated. A
323
+ * named-graph-aware entail() would need `GRAPH ?g { ?s ?p ?o }` and
324
+ * per-graph closure semantics that the underlying query() does not
325
+ * define today.
326
+ */
327
+ // Case-insensitive regime aliases ('rdfs', 'owl-rl', 'owlrl', 'owl_rl')
328
+ // normalized to the engine's exact-case ENTAIL_VALUES spelling ('RDFS',
329
+ // 'OWL-RL'); anything else (including already-correct casing, or a
330
+ // genuinely invalid value) passes through unchanged so engineApi.query's
331
+ // own validation reports the honest error rather than this function
332
+ // guessing at what the caller meant.
333
+ const ENTAIL_REGIME_ALIASES = {
334
+ none: 'none', rdfs: 'RDFS', 'owl-rl': 'OWL-RL', owlrl: 'OWL-RL', owl_rl: 'OWL-RL',
335
+ };
336
+
337
+ function normalizeEntailRegime(regime) {
338
+ const key = String(regime == null ? 'none' : regime).toLowerCase();
339
+ return ENTAIL_REGIME_ALIASES[key] || regime;
340
+ }
341
+
342
+ async function entail(ds, regime) {
343
+ assertFnDataset(ds, 'entail');
344
+ const rows = await engineApi.query(
345
+ toDataset(ds), 'SELECT ?s ?p ?o WHERE { ?s ?p ?o }',
346
+ { entail: normalizeEntailRegime(regime) });
347
+ return makeFnDataset(
348
+ rows.map((row) => dataFactory.quad(row.get('s'), row.get('p'), row.get('o'))));
349
+ }
350
+
351
+ /**
352
+ * SHACL Core validation. Needs the npm-entry engine bundle. Neither
353
+ * argument is mutated or consumed -- both stay valid FnDatasets after
354
+ * the call, same as every other op in this module.
355
+ *
356
+ * @param {FnDataset} ds the data graph
357
+ * @param {FnDataset} shapes the shapes graph
358
+ * @returns {Promise<{conforms: boolean, report: FnDataset}>} report is
359
+ * SHACL_Validation.validation_report_to_graph's graph (sh:conforms +
360
+ * one sh:ValidationResult per violation).
361
+ */
362
+ async function validate(ds, shapes) {
363
+ assertFnDataset(ds, 'validate');
364
+ assertFnDataset(shapes, 'validate');
365
+ const r = await engineApi.shaclValidate(toDataset(ds), toDataset(shapes));
366
+ return { conforms: r.conforms, report: fromDataset(r.report) };
367
+ }
368
+
369
+ /**
370
+ * ShEx (Shape Expressions) validation of one focus node against one
371
+ * shape. Needs the npm-entry engine bundle.
372
+ *
373
+ * @param {FnDataset} ds the data graph
374
+ * @param {string} schema ShExJ (JSON Schema form), as text
375
+ * @param {string|{termType,value}} focus an IRI, "_:label", or an
376
+ * RDF/JS NamedNode/BlankNode term
377
+ * @param {string|{termType,value}|null} [shape] a shape label; omit/
378
+ * null to validate against the schema's own `start`
379
+ * @returns {Promise<boolean|null>} null means "deferred" -- outside
380
+ * this engine's decidable ShEx fragment, never a guessed answer.
381
+ */
382
+ async function shex(ds, schema, focus, shape) {
383
+ assertFnDataset(ds, 'shex');
384
+ return engineApi.shexValidate(toDataset(ds), schema, focus, shape);
385
+ }
386
+
387
+ /**
388
+ * Evaluate an RML mapping graph against one logical source's raw
389
+ * data, materializing the generated triples as a new FnDataset. Needs
390
+ * the npm-entry engine bundle. Scope cut (documented, not silent):
391
+ * every triples map in `mapping` reads the SAME `source` -- joins
392
+ * across two different logical sources are not reachable through this
393
+ * one-source entry point (see engineApi.rmlMap's doc comment).
394
+ *
395
+ * @param {FnDataset} mapping the RML mapping graph
396
+ * @param {string} source raw JSON or CSV text (not RDF)
397
+ * @param {'json'|'csv'} kind
398
+ * @returns {Promise<FnDataset>}
399
+ */
400
+ async function fromMapping(mapping, source, kind) {
401
+ assertFnDataset(mapping, 'fromMapping');
402
+ return fromDataset(await engineApi.rmlMap(toDataset(mapping), source, kind));
403
+ }
404
+
405
+ /**
406
+ * CSVW csv2rdf conversion (w3.org/TR/csv2rdf): convert raw tabular
407
+ * data plus an optional CSVW metadata document into a new FnDataset.
408
+ * Needs the npm-entry engine bundle. Unlike fromMapping there is no
409
+ * FnDataset input -- both arguments are raw text (CSVW's metadata
410
+ * format is JSON, not RDF). Scope cut (documented, not silent --
411
+ * mirrors fromMapping's one-source cut): every table in a multi-table
412
+ * `tables` group reads the SAME `csv` text. Datatype `format` facets,
413
+ * list-valued (`separator`) cells, and full inherited-property
414
+ * propagation are not yet implemented -- see engineApi.csvwToRdf's
415
+ * doc comment and the CSVW program plan for measured coverage.
416
+ *
417
+ * @param {string} csv raw RFC 4180 tabular data (not RDF)
418
+ * @param {string} [metadata] CSVW metadata document (JSON text);
419
+ * '' / omitted infers the schema from the CSV's own header row
420
+ * @param {{mode?: 'standard'|'minimal', base?: string, url?: string}}
421
+ * [options] see engineApi.csvwToRdf
422
+ * @returns {Promise<FnDataset>}
423
+ */
424
+ async function fromCsvw(csv, metadata, options) {
425
+ return fromDataset(await engineApi.csvwToRdf(csv, metadata, options));
426
+ }
427
+
428
+ /**
429
+ * RIF Core forward-chaining saturation, materialized as a new
430
+ * FnDataset (input triples + derived triples, default graph only --
431
+ * RIF Core has no named-graph notion). Needs the npm-entry engine
432
+ * bundle.
433
+ *
434
+ * @param {FnDataset} ds the premise graph
435
+ * @param {string} rules a RIF Core XML rule document
436
+ * @returns {Promise<FnDataset>}
437
+ */
438
+ async function rif(ds, rules) {
439
+ assertFnDataset(ds, 'rif');
440
+ return fromDataset(await engineApi.rifEval(toDataset(ds), rules));
441
+ }
442
+
443
+ /**
444
+ * The CERTIFIED core-RDFS closure
445
+ * (formal/fstar/RDF.Entailment.RDFS.RhoDFClosure.fst's
446
+ * `rho_df_closure`): rdfs2/3/5/7/9/11 only, with the machine-checked
447
+ * decides-iff (docs/theorem-registry.md). "corerdfs" is this project's
448
+ * API name for the fragment the literature calls ρdf —
449
+ * subPropertyOf/subClassOf/type/domain/range (Muñoz, Pérez &
450
+ * Gutierrez, "Simple and Efficient Minimal RDFS", J. Web Semantics
451
+ * 7(3), 2009). Mirrors engineApi.coreRdfsClosure's `{ok, ntriples}`
452
+ * result, with `ntriples` parsed into an FnDataset via
453
+ * Dataset.fromNQuads — pure JS, no extra engine round-trip. Needs the
454
+ * npm-entry engine bundle. `rhoDfClosure` is kept below as a
455
+ * literature-name alias.
456
+ * @param {FnDataset} ds
457
+ * @param {{format?: string}} [options]
458
+ * @returns {Promise<{ok: boolean, dataset: FnDataset}>}
459
+ */
460
+ async function coreRdfsClosure(ds, options) {
461
+ assertFnDataset(ds, 'coreRdfsClosure');
462
+ const r = await engineApi.coreRdfsClosure(toDataset(ds), options);
463
+ return { ok: r.ok, dataset: fromDataset(Dataset.fromNQuads(r.ntriples)) };
464
+ }
465
+ /** Literature-name alias for coreRdfsClosure (ρdf; see above). */
466
+ const rhoDfClosure = coreRdfsClosure;
467
+
468
+ /**
469
+ * Decidable core-RDFS fragment check (`is_rho_df_frag`, tied by an F*
470
+ * lemma to the prop the regime theorems quantify over): does the
471
+ * certified coreRdfsClosure guarantee apply to `ds`? Needs the
472
+ * npm-entry engine bundle. `rhoDfFragmentCheck` is kept below as a
473
+ * literature-name alias.
474
+ * @param {FnDataset} ds
475
+ * @param {{format?: string}} [options]
476
+ * @returns {Promise<{ok: boolean, fragment: boolean}>}
477
+ */
478
+ async function coreRdfsCheck(ds, options) {
479
+ assertFnDataset(ds, 'coreRdfsCheck');
480
+ return engineApi.coreRdfsCheck(toDataset(ds), options);
481
+ }
482
+ /** Literature-name alias for coreRdfsCheck (ρdf; see above). */
483
+ const rhoDfFragmentCheck = coreRdfsCheck;
484
+
485
+ /**
486
+ * RDFS-Plus closure (RDF.Entailment.RDFSPlus.fst's
487
+ * `rdfs_plus_closure`): the full RDFS step plus the practical OWL
488
+ * subset — owl:sameAs, owl:inverseOf, Symmetric/Transitive/Functional/
489
+ * InverseFunctionalProperty, equivalentClass/Property ("RDFS-Plus",
490
+ * Allemang & Hendler 2008; "RDFS++", AllegroGraph). Every OWL row
491
+ * carries a proved licensing + truth lemma; no chain-level
492
+ * completeness claim (theorem registry) — weaker than
493
+ * coreRdfsClosure's decides-iff guarantee. Needs the npm-entry engine
494
+ * bundle.
495
+ * @param {FnDataset} ds
496
+ * @param {{format?: string}} [options]
497
+ * @returns {Promise<{ok: boolean, dataset: FnDataset, rounds: number}>}
498
+ */
499
+ async function rdfsPlusClosure(ds, options) {
500
+ assertFnDataset(ds, 'rdfsPlusClosure');
501
+ const r = await engineApi.rdfsPlusClosure(toDataset(ds), options);
502
+ return {
503
+ ok: r.ok,
504
+ dataset: fromDataset(Dataset.fromNQuads(r.ntriples)),
505
+ rounds: r.rounds,
506
+ };
507
+ }
508
+
509
+ /**
510
+ * OWL tableau materialisation (formal/fstar/Tableau.fst's
511
+ * `tableau_materialise`): add `i rdf:type <ClassExpression>` for every
512
+ * individual the model-construction reasoner proves is a member of an
513
+ * OWL class expression (someValuesFrom / hasValue / unionOf /
514
+ * intersectionOf, and the named class an equivalentClass restriction
515
+ * defines). Needs the npm-entry engine bundle. Default graph only.
516
+ *
517
+ * @param {FnDataset} ds the ontology + ABox graph
518
+ * @returns {Promise<{dataset: FnDataset, addedCount: number}>} dataset
519
+ * is input + tableau-derived triples; addedCount is how many the
520
+ * tableau added.
521
+ */
522
+ async function tableauMaterialise(ds) {
523
+ assertFnDataset(ds, 'tableauMaterialise');
524
+ const r = await engineApi.tableauMaterialise(toDataset(ds));
525
+ return { dataset: fromDataset(r.dataset), addedCount: r.addedCount };
526
+ }
527
+
528
+ /**
529
+ * OWL DL inconsistency verdict (bin/owl-runner's DL pipeline: OWL-RL
530
+ * closure -> tableau materialise -> OWL-RL closure -> is_inconsistent).
531
+ * `rlAlone` is the plain OWL-RL verdict on the same input, so a caller
532
+ * can see the cases the tableau adds. Needs the npm-entry engine
533
+ * bundle. Default graph only.
534
+ *
535
+ * @param {FnDataset} ds the ontology + ABox graph
536
+ * @returns {Promise<{inconsistent: boolean, rlAlone: boolean}>}
537
+ */
538
+ async function tableauDlInconsistent(ds) {
539
+ assertFnDataset(ds, 'tableauDlInconsistent');
540
+ return engineApi.tableauDlInconsistent(toDataset(ds));
541
+ }
542
+
543
+ /**
544
+ * OWL DL consistency verdict via the verified clash-detecting tableau
545
+ * (formal/fstar/Tableau.Refute.fst's `tableau_consistent` over the
546
+ * OWL-RL closure -- the pure verified chain bin/owl-runner runs under
547
+ * `--regime dl`, minus its native-only z3 oracle). Needs the npm-entry
548
+ * bundle. Default graph only.
549
+ *
550
+ * `consistent` is three-valued: `false` (a clash on every tableau
551
+ * branch), `true` (a model was constructed), or `null` -- the refuter
552
+ * exhausted its fuel budget before deciding, with `reason` naming the
553
+ * cap. `null` is never collapsed to `false`.
554
+ *
555
+ * @param {FnDataset|string} ontology the ontology + ABox graph -- an
556
+ * FnDataset, or raw RDF text (Turtle by default, per `options.format`)
557
+ * @param {{format?: string, fuel?: number|string}} [options]
558
+ * @returns {Promise<{consistent: boolean|null, reason?: string}>}
559
+ */
560
+ async function owlIsConsistent(ontology, options) {
561
+ const data = ontology instanceof FnDataset ? toDataset(ontology) : ontology;
562
+ return engineApi.owlIsConsistent(data, options);
563
+ }
564
+
565
+ /**
566
+ * OWL entailment check: does `premise` entail `conclusion`? Verified
567
+ * two-path dispatch (`via: "closure"` for the OWL-RL closure path,
568
+ * `via: "refutation"` for negate-and-refute), mirroring bin/owl-runner.
569
+ * Needs the npm-entry bundle. Default graph only. `entailed` is
570
+ * three-valued (`true` / `false` / `null` budget-out, `reason` names the
571
+ * cap).
572
+ *
573
+ * @param {FnDataset|string} premise an FnDataset or raw RDF text
574
+ * @param {FnDataset|string} conclusion an FnDataset or raw RDF text
575
+ * @param {{format?: string, fuel?: number|string}} [options]
576
+ * @returns {Promise<{entailed: boolean|null, via: 'closure'|'refutation', reason?: string}>}
577
+ */
578
+ async function owlEntails(premise, conclusion, options) {
579
+ const p = premise instanceof FnDataset ? toDataset(premise) : premise;
580
+ const c = conclusion instanceof FnDataset ? toDataset(conclusion) : conclusion;
581
+ return engineApi.owlEntails(p, c, options);
582
+ }
583
+
584
+ /**
585
+ * RDFC-1.0 canonicalization: canonical N-Quads text. Needs the
586
+ * npm-entry bundle (same gating as index.js's canonicalize()).
587
+ */
588
+ async function canonicalize(ds) {
589
+ assertFnDataset(ds, 'canonicalize');
590
+ let cached = CANON_CACHE.get(ds);
591
+ if (cached === undefined) {
592
+ const p = engineApi.canonicalize(toDataset(ds)).then((text) => {
593
+ CANON_CACHE.set(ds, text); // replace the in-flight promise
594
+ return text;
595
+ });
596
+ CANON_CACHE.set(ds, p);
597
+ cached = p;
598
+ }
599
+ return cached;
600
+ }
601
+
602
+ /**
603
+ * sha256 hex digest of canonicalize(ds). O(n log n) once (the
604
+ * canonicalization cost — see the design doc's cost model) and O(1)
605
+ * on every call after, memoized by FnDataset identity in a
606
+ * module-level WeakMap so the frozen object itself never changes.
607
+ * Concurrent callers before the first result lands share one
608
+ * in-flight computation.
609
+ *
610
+ * Backend hook: if `ds`'s backend already carries a
611
+ * `precomputedHash` (e.g. a COTTAS backend reading a `.c14n.sha256`
612
+ * sidecar — see the design doc's "future backends" section), that
613
+ * value is returned directly and canonicalize() is never called. The
614
+ * array backend never sets this, so today this is always a live
615
+ * computation; the check exists so a future backend gets the
616
+ * shortcut for free, without hash()'s callers changing.
617
+ */
618
+ async function hash(ds) {
619
+ assertFnDataset(ds, 'hash');
620
+ if (ds._backend.precomputedHash) return ds._backend.precomputedHash;
621
+ const cached = HASH_CACHE.get(ds);
622
+ if (typeof cached === 'string') return cached;
623
+ if (cached) return cached; // already in flight
624
+ const p = canonicalize(ds).then((canon) => {
625
+ const hex = crypto.createHash('sha256').update(canon, 'utf8')
626
+ .digest('hex');
627
+ HASH_CACHE.set(ds, hex); // replace the in-flight promise
628
+ return hex;
629
+ });
630
+ HASH_CACHE.set(ds, p);
631
+ return p;
632
+ }
633
+
634
+ /** Already-settled hash, if any (including a backend's precomputedHash) — never triggers computation. */
635
+ function peekHash(ds) {
636
+ if (ds._backend.precomputedHash) return ds._backend.precomputedHash;
637
+ const c = HASH_CACHE.get(ds);
638
+ return typeof c === 'string' ? c : null;
639
+ }
640
+
641
+ function hasBlankNode(ds) {
642
+ for (const q of ds) {
643
+ if (q.subject.termType === 'BlankNode') return true;
644
+ if (q.object.termType === 'BlankNode') return true;
645
+ if (q.graph.termType === 'BlankNode') return true;
646
+ }
647
+ return false;
648
+ }
649
+
650
+ /**
651
+ * Structural equality, cheapest-correct-path first (see the design
652
+ * doc's cost model section for the full argument):
653
+ *
654
+ * 1. Different sizes -> not equal, O(1).
655
+ * 2. Both sides already have a memoized hash -> compare those, O(1).
656
+ * 3. Exact quad-set match (same terms, same labels) -> equal, O(n).
657
+ * This is sound regardless of blank nodes: identical tokens can
658
+ * never be a false positive.
659
+ * 4. No blank nodes anywhere -> the step-3 negative is authoritative
660
+ * (ground quads have no relabeling to hide behind) -> not equal.
661
+ * 5. Otherwise (blank nodes present, step 3 was inconclusive) ->
662
+ * the only sound test left is RDFC-1.0 canonical-hash equality,
663
+ * which is exactly the O(n log n) canonicalize() cost this module
664
+ * tries to avoid paying twice.
665
+ */
666
+ async function equals(a, b) {
667
+ assertFnDataset(a, 'equals'); assertFnDataset(b, 'equals');
668
+ if (a === b) return true;
669
+ if (a.size !== b.size) return false;
670
+ const ha = peekHash(a);
671
+ const hb = peekHash(b);
672
+ if (ha !== null && hb !== null) return ha === hb;
673
+ const aKeys = new Set(a.toArray().map(quadToNQuads));
674
+ const bTokens = b.toArray().map(quadToNQuads);
675
+ const exact = bTokens.length === aKeys.size &&
676
+ bTokens.every((t) => aKeys.has(t));
677
+ if (exact) return true;
678
+ if (!hasBlankNode(a) && !hasBlankNode(b)) return false;
679
+ const [h1, h2] = await Promise.all([hash(a), hash(b)]);
680
+ return h1 === h2;
681
+ }
682
+
683
+ /**
684
+ * Open a COTTAS/Parquet artifact's raw bytes as a queryable, read-only
685
+ * store (docs/designissues/2026-07-06-inmemory-bytes-store.md, browser
686
+ * call site). Needs the npm-entry engine bundle. Unlike every other
687
+ * constructor in this module, the returned object is NOT an FnDataset:
688
+ * the whole point of the design is that rows decode lazily as a query
689
+ * touches them, never materializing the corpus into heap FnDataset
690
+ * quads (measured 64-161 B/quad vs. an FnDataset/heap Dataset's
691
+ * ~877 B/quad for the same data, in the design doc's native numbers).
692
+ *
693
+ * @param {string|Uint8Array|ArrayBuffer} bytes whole `.cottas` file contents
694
+ * @returns {Promise<{handle: string,
695
+ * query: (sparql: string) => Promise<Array<Map<string,object>>|boolean|FnDataset>,
696
+ * close: () => Promise<void>}>}
697
+ * query() divergences from fn.query() (documented, not silent): no
698
+ * entail option, no write/--delta-log overlay (read-only), and a
699
+ * query shape the backend executor can't push down rejects instead
700
+ * of silently falling back to a full materialize (see
701
+ * engineApi.queryCottas's doc comment). CONSTRUCT still returns an
702
+ * FnDataset (materialized once for that query only).
703
+ */
704
+ async function openCottas(bytes) {
705
+ const handle = await engineApi.openCottas(bytes);
706
+ let closed = false;
707
+ return {
708
+ handle,
709
+ async query(sparql) {
710
+ if (closed) throw new Error('openCottas store: query() called after close()');
711
+ const result = await engineApi.queryCottas(handle, sparql);
712
+ return result instanceof Dataset ? fromDataset(result) : result;
713
+ },
714
+ async close() {
715
+ if (closed) return;
716
+ closed = true;
717
+ await engineApi.closeCottas(handle);
718
+ },
719
+ };
720
+ }
721
+
722
+ /**
723
+ * Serialize an FnDataset to COTTAS/Parquet bytes via the native writer
724
+ * (the same pure `Tot` F* function `factoidal compact --native-writer`
725
+ * uses). Needs the npm-entry engine bundle. Round-trips through
726
+ * openCottas(): feeding the result straight back into openCottas()
727
+ * reproduces the same queryable store.
728
+ *
729
+ * @param {FnDataset} ds
730
+ * @returns {Promise<Uint8Array>}
731
+ */
732
+ async function toCottas(ds) {
733
+ assertFnDataset(ds, 'toCottas');
734
+ return engineApi.toCottas(toDataset(ds));
735
+ }
736
+
737
+ /**
738
+ * Enumerate named graphs (default graph excluded), first-seen order.
739
+ * Reuses index.js's graphs() enumeration; wraps each per-graph
740
+ * Dataset back into an FnDataset.
741
+ */
742
+ function graphs(ds) {
743
+ assertFnDataset(ds, 'graphs');
744
+ return engineApi.graphs(toDataset(ds))
745
+ .map(([iri, graphDataset]) => [iri, fromDataset(graphDataset)]);
746
+ }
747
+
748
+ // ---------------------------------------------------------------------
749
+ // Dataflow seed: cell (mutable input slot) + derive (hash-keyed,
750
+ // memoized pure recompute). Deliberately tiny and dependency-free —
751
+ // see the design doc's §"dataflow pattern" for the worked example
752
+ // this is extracted from.
753
+ // ---------------------------------------------------------------------
754
+
755
+ /** A minimal mutable box holding one dataflow input value. */
756
+ function cell(initial) {
757
+ let value = initial;
758
+ return {
759
+ get: () => value,
760
+ set(v) { value = v; },
761
+ };
762
+ }
763
+
764
+ // A dataflow value's recompute key: FnDataset content-hash for
765
+ // datasets (so two different FnDataset objects with the same content
766
+ // count as "unchanged"), the value itself otherwise (primitives
767
+ // compare by ===, which is exactly what a dataflow graph needs for
768
+ // non-dataset inputs like a SPARQL string or a numeric parameter).
769
+ async function keyOf(value) {
770
+ return value instanceof FnDataset ? hash(value) : value;
771
+ }
772
+
773
+ /**
774
+ * A derived dataflow node: recomputes fn(...inputCells.map(c=>c.get()))
775
+ * only when at least one input's content-key has changed since the
776
+ * last .get(). First call always computes (there is no prior key to
777
+ * compare against).
778
+ */
779
+ function derive(fn, ...inputCells) {
780
+ let lastKeys = null;
781
+ let lastResult;
782
+ return {
783
+ async get() {
784
+ const values = inputCells.map((c) => c.get());
785
+ const keys = await Promise.all(values.map(keyOf));
786
+ if (lastKeys && keys.length === lastKeys.length &&
787
+ keys.every((k, i) => k === lastKeys[i])) {
788
+ return lastResult; // skip recompute — nothing actually changed
789
+ }
790
+ lastResult = await fn(...values);
791
+ lastKeys = keys;
792
+ return lastResult;
793
+ },
794
+ };
795
+ }
796
+
797
+ /**
798
+ * Left-to-right function composition for Observable-style dataflow.
799
+ * `pipe(f, g, h)(x)` is `h(g(f(x)))`, awaiting any step that returns a
800
+ * Promise, so async engine functions (query, entail, xsltTransform, ...)
801
+ * compose with sync ones without the caller threading awaits. With no
802
+ * functions it is the identity. Every step must be a function.
803
+ * @param {...Function} fns
804
+ * @returns {(input: any) => Promise<any>}
805
+ */
806
+ function pipe(...fns) {
807
+ for (const f of fns) {
808
+ if (typeof f !== 'function') {
809
+ throw new TypeError('pipe: every argument must be a function');
810
+ }
811
+ }
812
+ return async (input) => {
813
+ let value = input;
814
+ for (const f of fns) {
815
+ value = await f(value);
816
+ }
817
+ return value;
818
+ };
819
+ }
820
+
821
+ // -----------------------------------------------------------------
822
+ // Typed engine functions (#74). Re-exported unchanged from the JS
823
+ // engine api — each is already a pure, value-in / value-out function
824
+ // over one F*-extracted engine (XSLT, MathML, XForms, JSON Schema,
825
+ // Schematron, TOAN CAS, matrix algebra), so they belong in the
826
+ // functional surface as-is. See index.d.ts / fn.d.ts for signatures.
827
+ // -----------------------------------------------------------------
828
+ const {
829
+ xsltTransform,
830
+ mathmlEval,
831
+ xformsRecalc,
832
+ jsonSchemaValidate,
833
+ schematronValidate,
834
+ toanSummation,
835
+ toanProduct,
836
+ toanSimplify,
837
+ toanDiff,
838
+ toanSubst,
839
+ matrixDeterminant,
840
+ matrixScalarProduct,
841
+ matrixVectorProduct,
842
+ matrixOuterProduct,
843
+ sigmoidPoints,
844
+ sigmoidFormulaMathml,
845
+ vcSha256Hex,
846
+ vcEd25519SecretToPublic,
847
+ vcEd25519Sign,
848
+ vcEd25519Verify,
849
+ vcEddsaCreateFromCanonical,
850
+ vcEddsaVerifyFromCanonical,
851
+ queryHdt,
852
+ } = engineApi;
853
+
854
+ module.exports = {
855
+ FnDataset,
856
+ EMPTY,
857
+ fromDataset,
858
+ toDataset,
859
+ builder,
860
+ fromChunks,
861
+ parse,
862
+ union,
863
+ difference,
864
+ filter,
865
+ mapQuads,
866
+ query,
867
+ entail,
868
+ validate,
869
+ shex,
870
+ fromMapping,
871
+ fromCsvw,
872
+ rif,
873
+ coreRdfsClosure,
874
+ coreRdfsCheck,
875
+ rhoDfClosure,
876
+ rhoDfFragmentCheck,
877
+ rdfsPlusClosure,
878
+ tableauMaterialise,
879
+ tableauDlInconsistent,
880
+ owlIsConsistent,
881
+ owlEntails,
882
+ canonicalize,
883
+ hash,
884
+ equals,
885
+ openCottas,
886
+ toCottas,
887
+ graphs,
888
+ cell,
889
+ derive,
890
+ pipe,
891
+ // Typed engine functions (#74 FP surface).
892
+ xsltTransform,
893
+ mathmlEval,
894
+ xformsRecalc,
895
+ jsonSchemaValidate,
896
+ schematronValidate,
897
+ toanSummation,
898
+ toanProduct,
899
+ toanSimplify,
900
+ toanDiff,
901
+ toanSubst,
902
+ matrixDeterminant,
903
+ matrixScalarProduct,
904
+ matrixVectorProduct,
905
+ matrixOuterProduct,
906
+ sigmoidPoints,
907
+ sigmoidFormulaMathml,
908
+ vcSha256Hex,
909
+ vcEd25519SecretToPublic,
910
+ vcEd25519Sign,
911
+ vcEd25519Verify,
912
+ vcEddsaCreateFromCanonical,
913
+ vcEddsaVerifyFromCanonical,
914
+ queryHdt,
915
+ capabilities: engineApi.capabilities,
916
+ };