@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/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
|
+
};
|