@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/CHANGELOG.md +82 -0
- package/README.md +52 -0
- package/factoidal-npm-entry.js +13657 -30789
- package/factoidal-npm-entry.wasm.assets/{code-7ac046580f1bbdda8dc6.wasm → code-5a7fc68f2ab1323718b8.wasm} +0 -0
- package/factoidal-npm-entry.wasm.js +2 -2
- package/factoidal.js +11448 -25890
- package/fn.js +39 -0
- package/l4-assets/l4factoidal.js +137 -0
- package/l4-assets/l4factoidal.mjs +2 -0
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +16 -0
- package/l4-core.d.ts +159 -0
- package/l4-core.js +188 -0
- package/l4.d.ts +36 -0
- package/l4.js +90 -0
- package/lib/api.js +162 -2
- package/package.json +23 -3
- package/select.d.ts +116 -0
- package/select.js +492 -0
- package/version.json +3 -3
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.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"builtAt": "2026-08-
|
|
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).",
|