@factoidal/core 0.1.0 → 0.2.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/select.js ADDED
@@ -0,0 +1,492 @@
1
+ // factoidal/select — the backend selector (issue #618).
2
+ //
3
+ // One typed surface, two independently verified engines behind it: the
4
+ // F* extraction (./index.js) and the Lean 4 extraction (./l4-core.js).
5
+ // Owner ruling, 2026-08-26 (issue #618, comment
6
+ // https://github.com/danbri/factoidal/issues/618#issuecomment-5425162574,
7
+ // written up as a spec in
8
+ // https://github.com/danbri/factoidal/issues/618#issuecomment-5425193280):
9
+ //
10
+ // Per-instance option; per-call override on at least the `fn` flavour
11
+ // of the API. Five values: lean, fstar, lean1st, fstar1st,
12
+ // slowcompareboth. `lean`/`fstar` THROW if the requested function is
13
+ // not available in that engine -- a request naming exactly one
14
+ // engine never gets an answer from the other. `lean1st` falls
15
+ // through to F* for functions Lean does not implement (and takes an
16
+ // optional override list of function names to route to F* even when
17
+ // Lean implements them); `fstar1st` mirrors that. `slowcompareboth`
18
+ // runs both and compares.
19
+ //
20
+ // This module is the createSelector() implementation; the sub-question
21
+ // answers the ruling asked the implementation to settle (not invent)
22
+ // are recorded next to the code that embodies each one:
23
+ //
24
+ // 1. observability -> the {engine, backend, value} envelope, below.
25
+ // 2. slowcompareboth disagreement -> returns both + agree:false
26
+ // (never throws for a genuine disagreement); throws only when a
27
+ // capability precondition isn't met, or when a call itself
28
+ // errors on at least one side (a different failure mode, kept
29
+ // distinct -- see call()'s slowcompareboth branch).
30
+ // 3. "same answer" -> RDFC-1.0 isomorphism (via ./fn.js's equals(),
31
+ // which already implements it) for anything Dataset-shaped;
32
+ // SPARQL SELECT bindings are compared as a BAG (order-insensitive,
33
+ // duplicates significant) with blank-node labels renamed to a
34
+ // stable per-side canonical form first -- see compareValues().
35
+ //
36
+ // This module does not change index.js, l4-core.js or fn.js: it is a
37
+ // pure consumer of their existing typed surfaces plus fn.js's
38
+ // FnDataset equals()/fromDataset() for the RDFC-1.0 comparison. Every
39
+ // existing require('factoidal/l4'|'l4-core'|'.'|'./fn') call site is
40
+ // unaffected.
41
+
42
+ 'use strict';
43
+
44
+ const fstarApi = require('./index.js');
45
+ const leanApi = require('./l4-core.js');
46
+ const fn = require('./fn.js');
47
+ const { Dataset } = require('./rdfjs.js');
48
+
49
+ const BACKENDS = Object.freeze(
50
+ ['lean', 'fstar', 'lean1st', 'fstar1st', 'slowcompareboth']);
51
+
52
+ function assertBackend(name, who) {
53
+ if (!BACKENDS.includes(name)) {
54
+ throw new TypeError(
55
+ `${who}: backend must be one of ${BACKENDS.join(', ')} (got ${JSON.stringify(name)})`);
56
+ }
57
+ }
58
+
59
+ // ---------------------------------------------------------------------
60
+ // Capability derivation.
61
+ //
62
+ // index.js and l4-core.js are both built from lib/api.js's buildApi()
63
+ // driver, so capabilities() has ONE shape for both engines
64
+ // (lib/api.js:2010-2073) and every flag in it is computed from
65
+ // `typeof <that engine's real loaded entry object>[opName] ===
66
+ // 'function'` -- not a hand-maintained guess here. A handful of
67
+ // functions are wired to a fixed op name and gated only by
68
+ // requireEntryFn (no capabilities() flag): those are available
69
+ // whenever capabilities().entry is true, for whichever engine's entry
70
+ // actually defines that op. Cross-checked against the Lean side's own
71
+ // dispatch-ABI reflection (`bin/linux-x86_64/l4factoidal ops`, and
72
+ // l4.call('ops', []) at runtime) -- both list exactly these 16 of the
73
+ // engine's 21 ops as the ones lib/api.js's typed wrappers reach:
74
+ // parseToDatasetJson, queryDataset, updateDataset, serializeNQuads,
75
+ // serializeTurtle, canonicalizeToNQuads, owlClosure, owlIsConsistent,
76
+ // owlEntails, rhoDfClosure, rhoDfFragmentCheck, rdfsPlusClosure,
77
+ // clParse, clSerialize, clAlphaNorm, clNormalize. These four CL/IKL
78
+ // ops are the Lean-only entries in capabilityTable(): formal/fstar has
79
+ // no CL/IKL parser, so engineSupports(fstarApi, <any of the four>) is
80
+ // false on the very first `typeof` guard below -- index.js never
81
+ // exports any of the four names at all. The other 5 are real ops on
82
+ // the resolved wasm with no typed-API wrapper in l4-core.js, so they
83
+ // are correctly absent from ALWAYS_IF_ENTRY/CAP_FLAG below and from
84
+ // capabilityTable()'s output, for two different reasons (see
85
+ // l4-core.js's OPS comment for the full text):
86
+ // - clToDataset, queryWithIklService: present in the compiled wasm
87
+ // but DELETED from the engine source, 2026-08-26 (issue #626), and
88
+ // already off the npm surface by owner decision (issue #618). The
89
+ // artifact is ahead of its source until it is rebuilt (issue
90
+ // #627). `clParse`/`clSerialize`/`clAlphaNorm`/`clNormalize` are
91
+ // NOT part of that removal -- none of them reads or produces RDF,
92
+ // so all four are wired above instead.
93
+ // - clFiniteSat: DEFERRED, not excluded (owner decision, 2026-08-26)
94
+ // -- it takes a caller-supplied finite-interpretation JSON
95
+ // encoding that has no user yet, and a typed wrapper would freeze
96
+ // that shape before we know whether it is right. Reachable only
97
+ // through the raw dispatch ABI.
98
+ // - ops, datasetOpen/Query/Update/Serialize/Close: not an owner
99
+ // ruling, just not yet wired (no typed-wrapper shape for a
100
+ // stateful handle exists in lib/api.js today).
101
+ // See docs/designissues/2026-08-22-npm-l4-module-packaging.md's #618
102
+ // section for the note.
103
+ const ALWAYS_IF_ENTRY = new Set([
104
+ 'parse', 'query', 'update', 'serialize', 'canonicalize', 'graphs',
105
+ 'canonicalHash', 'coreRdfsClosure', 'coreRdfsCheck', 'rhoDfClosure',
106
+ 'rhoDfFragmentCheck', 'rdfsPlusClosure', 'owlClosure', 'owlIsConsistent',
107
+ 'owlEntails', 'clParse', 'clSerialize', 'clAlphaNorm', 'clNormalize',
108
+ ]);
109
+
110
+ // Every other routable function's support is reported by a
111
+ // capabilities() family flag.
112
+ const CAP_FLAG = {
113
+ shaclValidate: 'shacl',
114
+ shexValidate: 'shex',
115
+ tableauMaterialise: 'tableau',
116
+ tableauDlInconsistent: 'tableau',
117
+ rmlMap: 'rml',
118
+ csvwToRdf: 'csvw',
119
+ jsonldToRdf: 'jsonld',
120
+ jsonldFromRdf: 'jsonldFromRdf',
121
+ didKeyResolve: 'didKey',
122
+ xmlWellformed: 'xml',
123
+ xpathEval: 'xpath',
124
+ rifEval: 'rif',
125
+ xsltTransform: 'xslt',
126
+ mathmlEval: 'mathml',
127
+ xformsRecalc: 'xforms',
128
+ jsonSchemaValidate: 'jsonSchema',
129
+ schematronValidate: 'schematron',
130
+ toanSummation: 'toan', toanProduct: 'toan', toanSimplify: 'toan',
131
+ toanDiff: 'toan', toanSubst: 'toan',
132
+ matrixDeterminant: 'matrix', matrixScalarProduct: 'matrix',
133
+ matrixVectorProduct: 'matrix', matrixOuterProduct: 'matrix',
134
+ sigmoidPoints: 'sigmoid', sigmoidFormulaMathml: 'sigmoid',
135
+ openCottas: 'cottasBytesStore', queryCottas: 'cottasBytesStore',
136
+ closeCottas: 'cottasBytesStore', toCottas: 'cottasBytesStore',
137
+ vcSha256Hex: 'vcCrypto', vcEd25519SecretToPublic: 'vcCrypto',
138
+ vcEd25519Sign: 'vcCrypto', vcEd25519Verify: 'vcCrypto',
139
+ vcEddsaCreateFromCanonical: 'vcCrypto', vcEddsaVerifyFromCanonical: 'vcCrypto',
140
+ };
141
+
142
+ const ROUTABLE = Object.freeze([...ALWAYS_IF_ENTRY, ...Object.keys(CAP_FLAG)]);
143
+
144
+ async function engineSupports(engineApi, fnName) {
145
+ if (typeof engineApi[fnName] !== 'function') return false;
146
+ let caps;
147
+ try {
148
+ caps = await engineApi.capabilities();
149
+ } catch {
150
+ return false;
151
+ }
152
+ if (ALWAYS_IF_ENTRY.has(fnName)) return !!caps.entry;
153
+ const flag = CAP_FLAG[fnName];
154
+ return flag ? !!caps[flag] : false;
155
+ }
156
+
157
+ /**
158
+ * The capability table (issue #618): for every routable function name,
159
+ * whether the Lean engine and the F* engine each support it right now
160
+ * -- derived live from both engines' capabilities() probes (see
161
+ * engineSupports() above), never hand-written from assumption. This is
162
+ * exactly the table lean1st/fstar1st consult for fall-through, and
163
+ * what makes lean/fstar's throw-on-unavailable correct rather than a
164
+ * guess.
165
+ * @returns {Promise<Record<string, {lean: boolean, fstar: boolean}>>}
166
+ */
167
+ async function capabilityTable() {
168
+ const table = {};
169
+ for (const fnName of ROUTABLE) {
170
+ table[fnName] = {
171
+ lean: await engineSupports(leanApi, fnName),
172
+ fstar: await engineSupports(fstarApi, fnName),
173
+ };
174
+ }
175
+ return table;
176
+ }
177
+
178
+ function engineApiFor(name) {
179
+ return name === 'lean' ? leanApi : fstarApi;
180
+ }
181
+
182
+ async function invoke(engineName, fnName, args) {
183
+ const api = engineApiFor(engineName);
184
+ if (typeof api[fnName] !== 'function') {
185
+ throw new TypeError(
186
+ `factoidal/select: '${fnName}' is not a function on the ${engineName} engine`);
187
+ }
188
+ // api[fnName] (lib/api.js's typed wrapper) already throws a clear
189
+ // capability error when the loaded entry lacks the underlying op --
190
+ // this is what makes backend:'lean'/'fstar' throw instead of
191
+ // silently answering from nowhere (the ruling's "a request naming
192
+ // exactly one engine never gets an answer from the other").
193
+ return api[fnName](...args);
194
+ }
195
+
196
+ // ---------------------------------------------------------------------
197
+ // "Same answer" (sub-question 3): RDFC-1.0 isomorphism for anything
198
+ // Dataset-shaped (reusing fn.js's equals(), which already implements
199
+ // the cheapest-correct-path chain down to canonical-hash comparison);
200
+ // a documented BAG comparison for SPARQL SELECT bindings, since RDFC-1.0
201
+ // canonicalizes RDF graphs/datasets, not solution bindings, and SPARQL
202
+ // results are bags (duplicate rows are significant, order is not
203
+ // significant without ORDER BY).
204
+ // ---------------------------------------------------------------------
205
+
206
+ async function datasetsIsomorphic(a, b) {
207
+ return fn.equals(fn.fromDataset(a), fn.fromDataset(b));
208
+ }
209
+
210
+ function termFingerprint(term) {
211
+ if (!term) return '';
212
+ if (term.termType === 'Literal') {
213
+ return `L:${term.value}^^${term.datatype ? term.datatype.value : ''}` +
214
+ (term.language ? `@${term.language}` : '');
215
+ }
216
+ return `${term.termType[0]}:${term.value}`;
217
+ }
218
+
219
+ // Blank-node labels are per-engine arbitrary; relabel them to a stable
220
+ // b0, b1, ... sequence assigned in first-appearance order (scanning
221
+ // rows in order, variables in sorted-name order within each row) so
222
+ // two independently-run engines' otherwise-identical result sets
223
+ // compare equal. This is NOT RDFC-1.0 (that operates on RDF graphs);
224
+ // it is the analogous idea applied to one result set's bindings.
225
+ function normalizeBindingsBag(rows) {
226
+ const varNames = new Set();
227
+ for (const row of rows) for (const k of row.keys()) varNames.add(k);
228
+ const sortedVars = [...varNames].sort();
229
+ const bnodeLabels = new Map();
230
+ let counter = 0;
231
+ return rows.map((row) => sortedVars.map((v) => {
232
+ const term = row.get(v);
233
+ if (!term) return `${v}=∅`;
234
+ if (term.termType === 'BlankNode') {
235
+ let label = bnodeLabels.get(term.value);
236
+ if (label === undefined) {
237
+ label = `_:b${counter++}`;
238
+ bnodeLabels.set(term.value, label);
239
+ }
240
+ return `${v}=B:${label}`;
241
+ }
242
+ return `${v}=${termFingerprint(term)}`;
243
+ }).join('|')).sort();
244
+ }
245
+
246
+ function bagsEqual(a, b) {
247
+ if (a.length !== b.length) return false;
248
+ const remaining = a.slice().sort();
249
+ const other = b.slice().sort();
250
+ for (let i = 0; i < remaining.length; i++) {
251
+ if (remaining[i] !== other[i]) return false;
252
+ }
253
+ return true;
254
+ }
255
+
256
+ function omit(obj, key) {
257
+ const rest = {};
258
+ for (const k of Object.keys(obj)) if (k !== key) rest[k] = obj[k];
259
+ return rest;
260
+ }
261
+
262
+ function shallowScalarEqual(a, b) {
263
+ const ak = Object.keys(a).sort();
264
+ const bk = Object.keys(b).sort();
265
+ if (ak.length !== bk.length) return false;
266
+ for (let i = 0; i < ak.length; i++) if (ak[i] !== bk[i]) return false;
267
+ return ak.every((k) => JSON.stringify(a[k]) === JSON.stringify(b[k]));
268
+ }
269
+
270
+ // Some typed-API results wrap the Dataset-valued payload in a scalar
271
+ // field rather than returning it bare.
272
+ const DATASET_FIELD_HINT = {
273
+ tableauMaterialise: 'dataset',
274
+ shaclValidate: 'report',
275
+ };
276
+ // ... and some wrap RDF as raw N-Triples/N-Quads TEXT rather than a
277
+ // Dataset -- comparing that text with strict string equality would be
278
+ // wrong (two engines' raw, non-canonical serializations can differ in
279
+ // blank-node labels and line order for the same graph), so these parse
280
+ // the field back into a Dataset first.
281
+ const RDF_TEXT_FIELD_HINT = {
282
+ coreRdfsClosure: 'ntriples',
283
+ rhoDfClosure: 'ntriples',
284
+ rdfsPlusClosure: 'ntriples',
285
+ };
286
+
287
+ /**
288
+ * Decide whether two engines' results for the same call are "the same
289
+ * answer" (sub-question 3). Returns { equal, method } so a caller can
290
+ * see which comparison was used, not just the verdict.
291
+ */
292
+ async function compareValues(fnName, leanValue, fstarValue) {
293
+ if (leanValue instanceof Dataset && fstarValue instanceof Dataset) {
294
+ return {
295
+ equal: await datasetsIsomorphic(leanValue, fstarValue),
296
+ method: 'rdfc1.0-isomorphism',
297
+ };
298
+ }
299
+ if (Array.isArray(leanValue) && Array.isArray(fstarValue)) {
300
+ return {
301
+ equal: bagsEqual(
302
+ normalizeBindingsBag(leanValue), normalizeBindingsBag(fstarValue)),
303
+ method: 'bag-of-bindings',
304
+ };
305
+ }
306
+ if (fnName === 'serialize' &&
307
+ typeof leanValue === 'string' && typeof fstarValue === 'string') {
308
+ try {
309
+ const a = Dataset.fromNQuads(leanValue);
310
+ const b = Dataset.fromNQuads(fstarValue);
311
+ return {
312
+ equal: await datasetsIsomorphic(a, b),
313
+ method: 'rdfc1.0-isomorphism(serialize-as-nquads)',
314
+ };
315
+ } catch {
316
+ // Not N-Quads/N-Triples-shaped text (e.g. Turtle/RDF-XML) -- no
317
+ // isomorphism-aware parse available here; fall through to a
318
+ // labelled strict-string comparison rather than guessing.
319
+ return { equal: leanValue === fstarValue, method: 'strict-equality(non-nquads-text)' };
320
+ }
321
+ }
322
+ const rdfField = RDF_TEXT_FIELD_HINT[fnName];
323
+ if (rdfField && leanValue && fstarValue &&
324
+ typeof leanValue[rdfField] === 'string' && typeof fstarValue[rdfField] === 'string') {
325
+ const a = Dataset.fromNQuads(leanValue[rdfField]);
326
+ const b = Dataset.fromNQuads(fstarValue[rdfField]);
327
+ const dsEqual = await datasetsIsomorphic(a, b);
328
+ const restEqual = shallowScalarEqual(omit(leanValue, rdfField), omit(fstarValue, rdfField));
329
+ return { equal: dsEqual && restEqual, method: `rdfc1.0-isomorphism+scalar-fields(${rdfField})` };
330
+ }
331
+ const dsField = DATASET_FIELD_HINT[fnName];
332
+ if (dsField && leanValue && fstarValue &&
333
+ leanValue[dsField] instanceof Dataset && fstarValue[dsField] instanceof Dataset) {
334
+ const dsEqual = await datasetsIsomorphic(leanValue[dsField], fstarValue[dsField]);
335
+ const restEqual = shallowScalarEqual(omit(leanValue, dsField), omit(fstarValue, dsField));
336
+ return { equal: dsEqual && restEqual, method: `rdfc1.0-isomorphism+scalar-fields(${dsField})` };
337
+ }
338
+ if (leanValue !== null && fstarValue !== null &&
339
+ typeof leanValue === 'object' && typeof fstarValue === 'object') {
340
+ return { equal: shallowScalarEqual(leanValue, fstarValue), method: 'structural-equality' };
341
+ }
342
+ return { equal: leanValue === fstarValue, method: 'strict-equality' };
343
+ }
344
+
345
+ // ---------------------------------------------------------------------
346
+ // The selector.
347
+ // ---------------------------------------------------------------------
348
+
349
+ /**
350
+ * Create a backend selector instance (per-instance option; every
351
+ * method also takes a trailing callOptions with a per-call {backend,
352
+ * overrideFns} override -- the "at least the fn flavour" requirement).
353
+ *
354
+ * @param {object} [options]
355
+ * @param {'lean'|'fstar'|'lean1st'|'fstar1st'|'slowcompareboth'} [options.backend='fstar1st']
356
+ * @param {string[]} [options.overrideFns] lean1st only: function names
357
+ * to route to F* regardless of whether Lean implements them.
358
+ */
359
+ function createSelector(options) {
360
+ const opts = options || {};
361
+ const defaultBackend = opts.backend || 'fstar1st';
362
+ assertBackend(defaultBackend, 'createSelector');
363
+ const defaultOverrideFns = new Set(opts.overrideFns || []);
364
+
365
+ /**
366
+ * Dispatch one call by function name. Returns:
367
+ * - {engine: 'lean'|'fstar', backend, value} for lean/fstar/lean1st/fstar1st
368
+ * - {engine: 'both', backend, agree, comparison, lean, fstar} for
369
+ * slowcompareboth (never thrown for a genuine disagreement --
370
+ * agree:false IS the reportable finding).
371
+ * Throws (never falls back) when:
372
+ * - backend is 'lean'/'fstar' and that engine doesn't implement fnName;
373
+ * - backend is 'slowcompareboth' and EITHER engine doesn't implement
374
+ * fnName (nothing to compare -- use lean1st/fstar1st instead);
375
+ * - the underlying call itself throws on at least one side under
376
+ * slowcompareboth (a different failure mode from "disagreement":
377
+ * the thrown Error carries .lean/.fstar outcome records so both
378
+ * sides are still inspectable).
379
+ */
380
+ async function call(fnName, args, callOptions) {
381
+ const co = callOptions || {};
382
+ const backend = co.backend || defaultBackend;
383
+ assertBackend(backend, `select.call('${fnName}')`);
384
+ const overrideFns = co.overrideFns ? new Set(co.overrideFns) : defaultOverrideFns;
385
+
386
+ if (backend === 'lean') {
387
+ return { engine: 'lean', backend, value: await invoke('lean', fnName, args) };
388
+ }
389
+ if (backend === 'fstar') {
390
+ return { engine: 'fstar', backend, value: await invoke('fstar', fnName, args) };
391
+ }
392
+ if (backend === 'lean1st') {
393
+ if (!overrideFns.has(fnName) && await engineSupports(leanApi, fnName)) {
394
+ return { engine: 'lean', backend, value: await invoke('lean', fnName, args) };
395
+ }
396
+ return { engine: 'fstar', backend, value: await invoke('fstar', fnName, args) };
397
+ }
398
+ if (backend === 'fstar1st') {
399
+ if (!overrideFns.has(fnName) && await engineSupports(fstarApi, fnName)) {
400
+ return { engine: 'fstar', backend, value: await invoke('fstar', fnName, args) };
401
+ }
402
+ return { engine: 'lean', backend, value: await invoke('lean', fnName, args) };
403
+ }
404
+
405
+ // slowcompareboth
406
+ const [leanOk, fstarOk] = await Promise.all([
407
+ engineSupports(leanApi, fnName), engineSupports(fstarApi, fnName),
408
+ ]);
409
+ if (!leanOk || !fstarOk) {
410
+ throw new Error(
411
+ `factoidal/select: slowcompareboth('${fnName}') needs BOTH engines to ` +
412
+ `support the function (lean=${leanOk}, fstar=${fstarOk}); ` +
413
+ "use backend 'lean1st' or 'fstar1st' to fall through instead.");
414
+ }
415
+ const [leanOutcome, fstarOutcome] = await Promise.allSettled([
416
+ invoke('lean', fnName, args), invoke('fstar', fnName, args),
417
+ ]);
418
+ if (leanOutcome.status === 'rejected' || fstarOutcome.status === 'rejected') {
419
+ const err = new Error(
420
+ `factoidal/select: slowcompareboth('${fnName}') -- at least one engine's ` +
421
+ 'call threw (capability check passed, so this is an execution error, ' +
422
+ 'not a missing op); see .lean/.fstar for both outcomes.');
423
+ err.engine = 'both';
424
+ err.backend = 'slowcompareboth';
425
+ err.agree = false;
426
+ err.lean = leanOutcome.status === 'fulfilled'
427
+ ? { ok: true, value: leanOutcome.value }
428
+ : { ok: false, error: String((leanOutcome.reason && leanOutcome.reason.message) || leanOutcome.reason) };
429
+ err.fstar = fstarOutcome.status === 'fulfilled'
430
+ ? { ok: true, value: fstarOutcome.value }
431
+ : { ok: false, error: String((fstarOutcome.reason && fstarOutcome.reason.message) || fstarOutcome.reason) };
432
+ throw err;
433
+ }
434
+ const comparison = await compareValues(fnName, leanOutcome.value, fstarOutcome.value);
435
+ return {
436
+ engine: 'both',
437
+ backend,
438
+ agree: comparison.equal,
439
+ comparison: { method: comparison.method },
440
+ lean: leanOutcome.value,
441
+ fstar: fstarOutcome.value,
442
+ };
443
+ }
444
+
445
+ return {
446
+ backend: defaultBackend,
447
+ overrideFns: [...defaultOverrideFns],
448
+ call,
449
+ capabilityTable,
450
+ // Named sugar over call() for the common typed-API functions --
451
+ // the "fn flavour" per-call override: every one takes a trailing
452
+ // {backend, overrideFns} that overrides this instance's default
453
+ // for that one call only.
454
+ parse: (text, parseOptions, callOptions) => call('parse', [text, parseOptions], callOptions),
455
+ query: (data, sparql, queryOptions, callOptions) => call('query', [data, sparql, queryOptions], callOptions),
456
+ update: (data, updateText, callOptions) => call('update', [data, updateText], callOptions),
457
+ serialize: (data, serializeOptions, callOptions) => call('serialize', [data, serializeOptions], callOptions),
458
+ canonicalize: (data, callOptions) => call('canonicalize', [data], callOptions),
459
+ canonicalHash: (data, callOptions) => call('canonicalHash', [data], callOptions),
460
+ graphs: (data, callOptions) => call('graphs', [data], callOptions),
461
+ owlClosure: (data, mode, callOptions) => call('owlClosure', [data, mode], callOptions),
462
+ owlIsConsistent: (data, owlOptions, callOptions) => call('owlIsConsistent', [data, owlOptions], callOptions),
463
+ owlEntails: (premise, conclusion, owlOptions, callOptions) => call('owlEntails', [premise, conclusion, owlOptions], callOptions),
464
+ coreRdfsClosure: (data, closureOptions, callOptions) => call('coreRdfsClosure', [data, closureOptions], callOptions),
465
+ coreRdfsCheck: (data, closureOptions, callOptions) => call('coreRdfsCheck', [data, closureOptions], callOptions),
466
+ rhoDfClosure: (data, closureOptions, callOptions) => call('rhoDfClosure', [data, closureOptions], callOptions),
467
+ rhoDfFragmentCheck: (data, closureOptions, callOptions) => call('rhoDfFragmentCheck', [data, closureOptions], callOptions),
468
+ rdfsPlusClosure: (data, closureOptions, callOptions) => call('rdfsPlusClosure', [data, closureOptions], callOptions),
469
+ shaclValidate: (data, shapes, shaclOptions, callOptions) => call('shaclValidate', [data, shapes, shaclOptions], callOptions),
470
+ shexValidate: (data, schema, focus, shape, callOptions) => call('shexValidate', [data, schema, focus, shape], callOptions),
471
+ // Lean-only (see ALWAYS_IF_ENTRY's comment above): backend:'fstar'
472
+ // throws, since index.js never exports clParse at all.
473
+ clParse: (clifText, callOptions) => call('clParse', [clifText], callOptions),
474
+ // Lean-only, same reason as clParse.
475
+ clSerialize: (clifText, callOptions) => call('clSerialize', [clifText], callOptions),
476
+ clAlphaNorm: (clifText, callOptions) => call('clAlphaNorm', [clifText], callOptions),
477
+ clNormalize: (clifText, callOptions) => call('clNormalize', [clifText], callOptions),
478
+ // clFiniteSat is intentionally NOT given named sugar here (owner
479
+ // decision, 2026-08-26): deferred, reachable via call('clFiniteSat',
480
+ // [interpJson, clifText], callOptions) instead. See ALWAYS_IF_ENTRY's
481
+ // comment above.
482
+ };
483
+ }
484
+
485
+ module.exports = {
486
+ BACKENDS,
487
+ ROUTABLE,
488
+ createSelector,
489
+ capabilityTable,
490
+ engineSupports,
491
+ compareValues,
492
+ };
package/version.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.1.0",
3
- "gitSha": "4944fdb1cc6d5fcc7a2db2f2a1802f276aac6bee",
4
- "builtAt": "2026-08-22T05:46:06Z",
2
+ "version": "0.2.0",
3
+ "gitSha": "49f8ca4d70bf57c12fea445611e2b01b067bf6ba",
4
+ "builtAt": "2026-08-26T21:45:40Z",
5
5
  "claims": {
6
6
  "schema": "1",
7
7
  "statement": "Proved sound with respect to an independent F* formalization of the W3C RDF/RDFS/OWL semantics, under the stated fragment restrictions and the trust surface recorded in docs/theorem-registry.md — never claimed as a complete formally verified implementation of RDF semantics (docs/theorem-registry.md § Calibrated claims).",