@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/l4-core.js ADDED
@@ -0,0 +1,188 @@
1
+ // factoidal — Node entry point for the **Lean 4** engine.
2
+ //
3
+ // The same typed public API shape as index.js / wasm.js (parse /
4
+ // query / update / serialize / canonicalize / dataFactory / Dataset /
5
+ // the closure and checker family), served by the Lean 4 extraction
6
+ // compiled to WebAssembly (L4Factoidal — formal/lean4/) instead of
7
+ // the F* extraction. Asset resolution is l4.js's three-step ladder:
8
+ // @factoidal/lean, $FACTOIDAL_L4_ASSETS, then the repository checkout.
9
+ //
10
+ // const factoidal = require('@factoidal/core/l4-core');
11
+ // const ds = await factoidal.parse('<a> <b> "c" .', { format: 'ntriples' });
12
+ //
13
+ // Surface: the subset capabilities() reports. Operations the Lean
14
+ // engine does not implement (SHACL, ShEx, HDT, COTTAS, VC crypto,
15
+ // entailment-regime query modes, tableauMaterialise /
16
+ // tableauDlInconsistent, RML/CSVW/JSON-LD/RIF) reject with a clear
17
+ // engine-capability error rather than silently falling back to the F*
18
+ // engine — the point of this entry is that everything it answers came
19
+ // through the Lean extraction. See ./select.js (factoidal/select) for
20
+ // an explicit, observable lean/fstar/lean1st/fstar1st/slowcompareboth
21
+ // switch built on top of this and ./index.js (issue #618); this module
22
+ // itself never falls back. owlIsConsistent / owlEntails ARE served
23
+ // (the three-valued OWL DL verdict, formal/lean4 issue 586) when the
24
+ // resolved wasm carries the ops — an older bundle answers "unknown op",
25
+ // which surfaces as the entryResult error, so probe the `ops`
26
+ // reflection before relying on them. The export list mirrors wasm.js
27
+ // so callers can swap engines by import path alone.
28
+
29
+ 'use strict';
30
+
31
+ const { buildApi } = require('./lib/api.js');
32
+ const rdfjs = require('./rdfjs.js');
33
+ const l4 = require('./l4.js');
34
+ const pkg = require('./package.json');
35
+
36
+ const NOT_SUPPORTED =
37
+ 'factoidal/l4-core: this operation is not implemented by the Lean 4 ' +
38
+ 'engine (see capabilities()); use the F* engine entry points for it. ' +
39
+ 'If the Lean assets are missing, npm install @factoidal/lean or set ' +
40
+ 'FACTOIDAL_L4_ASSETS.';
41
+
42
+ // The entry-ABI method names lib/api.js dispatches on, mapped onto the
43
+ // Lean wasm module's single dispatch export. Methods absent from this
44
+ // list are deliberately absent from the entry object: api.js
45
+ // typeof-guards each one and capabilities() reports the truth.
46
+ //
47
+ // The resolved wasm's full dispatch surface has 21 ops
48
+ // (`bin/linux-x86_64/l4factoidal ops`); this list wires 16 of them.
49
+ // `clParse` reads CLIF text into a CL syntax tree and reports its
50
+ // shape (sentence count, CL-vs-IKL dialect, canonical re-serialisation)
51
+ // -- it never produces RDF, so it is NOT part of the IKL-to-RDF
52
+ // projection family below. `clSerialize`, `clAlphaNorm` and
53
+ // `clNormalize` join it as the same kind of entry (CLIF text in, CLIF/
54
+ // CL text out, never RDF) -- see lib/api.js's doc comments on each for
55
+ // what their answers are worth (clSerialize's `roundTripProved: false`,
56
+ // clNormalize's `preserves`/`noIntrusion`). All four are Lean-only
57
+ // entries in the typed capability table (formal/fstar has no CL/IKL
58
+ // parser at all); see ./select.js, which throws on `backend:'fstar'`
59
+ // for each rather than answering from Lean silently.
60
+ //
61
+ // `clFiniteSat` (the fifth CL/IKL op) is DEFERRED from this typed
62
+ // layer, not excluded (owner decision, 2026-08-26): it takes a
63
+ // caller-supplied finite-interpretation JSON encoding that has no user
64
+ // yet, and a typed wrapper would freeze that shape before we know
65
+ // whether it is right. It stays reachable only through the raw
66
+ // dispatch ABI (`l4.call('clFiniteSat', [interpJson, clifText])`) --
67
+ // see lib/api.js's comment next to the other three clNormalize/
68
+ // clAlphaNorm/clSerialize wrappers for the same note.
69
+ //
70
+ // The IKL-to-RDF projection ("direction B": `clToDataset`,
71
+ // `queryWithIklService`) was removed from the engine source on
72
+ // 2026-08-26 (https://github.com/danbri/factoidal/issues/626): both
73
+ // ops went through the deleted `CL/ToRdf.lean`, whose content-addressed
74
+ // proposition graph names were never asked for. They had already been held off this JS surface by owner
75
+ // decision (https://github.com/danbri/factoidal/issues/618 — "I don't
76
+ // want npm code for direction b at this stage ... Take it out of npm
77
+ // for now."), and there is now nothing to wire. The compiled wasm
78
+ // artifact still CONTAINS both ops until it is rebuilt
79
+ // (https://github.com/danbri/factoidal/issues/627) — it is ahead of
80
+ // its source. The same #618 ruling is why an x-ikl-<suffix>
81
+ // entailment-regime family is rejected in lib/api.js's query() and
82
+ // fn.js's entail().
83
+ //
84
+ // CORRECTION 2026-08-26: an earlier version of this comment listed
85
+ // `clParse` alongside those two and cited #618 as the reason for all
86
+ // three. That misattributed a decision the owner did not make --
87
+ // `clParse` was unwired only because nobody had decided to wrap it,
88
+ // which is why it is wired above instead of here.
89
+ //
90
+ // A second group is withheld for an unrelated, non-owner reason:
91
+ //
92
+ // - dataset handles (`datasetOpen`/`datasetQuery`/`datasetUpdate`/
93
+ // `datasetSerialize`/`datasetClose`): ordinary RDF dataset handles,
94
+ // not part of the CL/IKL decision above. Held back only because
95
+ // lib/api.js has no typed wrapper shape for a stateful handle yet
96
+ // (every existing typed op is request/response) — a scope
97
+ // judgement, not an owner ruling. Wiring these in is a reasonable
98
+ // follow-up.
99
+ const OPS = [
100
+ 'parseToDatasetJson',
101
+ 'queryDataset',
102
+ 'updateDataset',
103
+ 'serializeNQuads',
104
+ 'serializeTurtle',
105
+ 'canonicalizeToNQuads',
106
+ 'owlClosure',
107
+ 'owlIsConsistent',
108
+ 'owlEntails',
109
+ 'rhoDfClosure',
110
+ 'rhoDfFragmentCheck',
111
+ 'rdfsPlusClosure',
112
+ 'clParse',
113
+ 'clSerialize',
114
+ 'clAlphaNorm',
115
+ 'clNormalize',
116
+ ];
117
+
118
+ async function loadLeanEntry() {
119
+ const eng = await l4.loadL4();
120
+ if (typeof eng.call !== 'function') {
121
+ throw new Error(
122
+ 'factoidal/l4-core: the resolved Lean wasm module predates the ' +
123
+ 'dispatch ABI (no call export); update @factoidal/lean or ' +
124
+ 'FACTOIDAL_L4_ASSETS to a build with l4_call'
125
+ );
126
+ }
127
+ const entry = { abiVersion: '1' };
128
+ for (const op of OPS) {
129
+ // lib/api.js's entryResult() takes the envelope as a JSON STRING
130
+ // (entry_jsoo.ml's wire shape). The Lean loader's call() parses the
131
+ // envelope and throws on {"ok":false}; re-encode both outcomes so
132
+ // this entry object is wire-compatible with the F* one.
133
+ entry[op] = (...args) => {
134
+ try {
135
+ return JSON.stringify(eng.call(op, args.map(String)));
136
+ } catch (err) {
137
+ return JSON.stringify({
138
+ ok: false,
139
+ error: String((err && err.message) || err),
140
+ });
141
+ }
142
+ };
143
+ }
144
+ return entry;
145
+ }
146
+
147
+ const api = buildApi({
148
+ engineName: 'l4',
149
+ loadEntry: loadLeanEntry,
150
+ runCli: async () => ({ exitCode: 1, stdout: '', stderr: NOT_SUPPORTED }),
151
+ });
152
+
153
+ module.exports = {
154
+ parse: api.parse,
155
+ query: api.query,
156
+ update: api.update,
157
+ serialize: api.serialize,
158
+ canonicalize: api.canonicalize,
159
+ graphs: api.graphs,
160
+ canonicalHash: api.canonicalHash,
161
+ owlClosure: api.owlClosure,
162
+ coreRdfsClosure: api.coreRdfsClosure,
163
+ coreRdfsCheck: api.coreRdfsCheck,
164
+ rhoDfClosure: api.rhoDfClosure,
165
+ rhoDfFragmentCheck: api.rhoDfFragmentCheck,
166
+ rdfsPlusClosure: api.rdfsPlusClosure,
167
+ // Lean-only: formal/fstar has no CL/IKL parser, so these are NOT
168
+ // exported from index.js/wasm.js. See the OPS comment above.
169
+ clParse: api.clParse,
170
+ clSerialize: api.clSerialize,
171
+ clAlphaNorm: api.clAlphaNorm,
172
+ clNormalize: api.clNormalize,
173
+ // Served by the Lean engine's dispatch ABI when the resolved wasm
174
+ // carries the ops (see the header note).
175
+ owlIsConsistent: api.owlIsConsistent,
176
+ owlEntails: api.owlEntails,
177
+ // Not implemented by this engine — kept on the surface so a caller
178
+ // swapping engines gets the pinned capability error, not
179
+ // `undefined is not a function`.
180
+ shaclValidate: api.shaclValidate,
181
+ shexValidate: api.shexValidate,
182
+ capabilities: api.capabilities,
183
+ Dataset: rdfjs.Dataset,
184
+ dataFactory: rdfjs.dataFactory,
185
+ engine: 'lean4-wasm',
186
+ available: l4.available,
187
+ version: pkg.version,
188
+ };
package/l4.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ // TypeScript declarations for factoidal/l4 — the Lean 4 (L4Factoidal)
2
+ // engine compiled to WebAssembly, bundled in this package's l4-assets/
3
+ // since issue #618. See l4.js for the asset-resolution ladder and
4
+ // docs/designissues/2026-08-22-npm-l4-module-packaging.md for why the
5
+ // wasm was split out originally and why that no longer applies.
6
+
7
+ /** A term in SPARQL Query Results JSON shape, plus `var` for patterns. */
8
+ export interface L4Term {
9
+ type: 'uri' | 'literal' | 'bnode' | 'var';
10
+ value: string;
11
+ datatype?: string;
12
+ 'xml:lang'?: string;
13
+ }
14
+
15
+ export interface L4Triple {
16
+ subject: L4Term;
17
+ predicate: L4Term;
18
+ object: L4Term;
19
+ }
20
+
21
+ /** SPARQL 1.1 Query Results JSON document. */
22
+ export interface L4ResultsDoc {
23
+ head: { vars: string[] };
24
+ results: { bindings: Array<Record<string, L4Term>> };
25
+ }
26
+
27
+ export interface L4Engine {
28
+ version(): string;
29
+ bgpQuery(triples: L4Triple[], bgp: L4Triple[]): Promise<L4ResultsDoc> | L4ResultsDoc;
30
+ }
31
+
32
+ export const engine: 'lean4-wasm';
33
+ export function available(): boolean;
34
+ export function loadL4(): Promise<L4Engine>;
35
+ export function version(): Promise<string>;
36
+ export function bgpQuery(triples: L4Triple[], bgp: L4Triple[]): Promise<L4ResultsDoc>;
package/l4.js ADDED
@@ -0,0 +1,90 @@
1
+ // factoidal/l4 — the Lean 4 engine (L4Factoidal compiled to wasm32)
2
+ // behind the same package namespace as the F*-extracted engines.
3
+ //
4
+ // const l4 = require('factoidal/l4'); // or import('factoidal/l4')
5
+ // await l4.version(); // "L4Factoidal ..."
6
+ // await l4.bgpQuery(triples, bgp); // SPARQL results JSON
7
+ //
8
+ // BUNDLED since issue #618: `@factoidal/core` now ships the Lean wasm
9
+ // directly under `l4-assets/` (l4factoidal.{js,mjs,wasm} + version.json
10
+ // — l4-assets/version.json, not this package's own version.json, which
11
+ // stays the F* engine's), so one `npm install @factoidal/core` gets
12
+ // both engines. The +23%-tarball objection that shaped the earlier
13
+ // companion-package split
14
+ // (docs/designissues/2026-08-22-npm-l4-module-packaging.md) no longer
15
+ // applies once measured against the real 21-op dispatch surface — see
16
+ // that doc's "issue #618: option A after all" section for the full
17
+ // argument. This module is now a resolver over FOUR sources, first hit
18
+ // wins:
19
+ //
20
+ // 1. this package's own l4-assets/ (the normal case, since #618);
21
+ // 2. the companion package `@factoidal/lean` (never published — kept
22
+ // only as a manual-override path for anyone pinning an older
23
+ // Lean build against a newer core; see npm/factoidal-lean/README.md);
24
+ // 3. $FACTOIDAL_L4_ASSETS — a directory holding
25
+ // l4factoidal.{js,mjs,wasm} (custom deployments);
26
+ // 4. the repository checkout layout (docs/web/hub/assets/l4/) —
27
+ // what the hub tests and in-repo development use.
28
+ //
29
+ // Every source's three files must stay together and keep their names:
30
+ // the Emscripten glue resolves the .wasm sidecar from its own basename
31
+ // (see skills/lean4-wasm-export, "the naming trap").
32
+ //
33
+ // API surface mirrors docs/web/hub/assets/l4/l4factoidal.js (phase-1
34
+ // ABI: version + bgpQuery). New exports appear here when they are
35
+ // added to formal/lean4/Wasm/Exports.lean and the wasm is rebuilt.
36
+
37
+ 'use strict';
38
+
39
+ const { existsSync } = require('node:fs');
40
+ const path = require('node:path');
41
+ const { pathToFileURL } = require('node:url');
42
+
43
+ function resolveLoader() {
44
+ const inPkg = path.join(__dirname, 'l4-assets', 'l4factoidal.js');
45
+ if (existsSync(inPkg)) return inPkg;
46
+ try {
47
+ return require.resolve('@factoidal/lean/l4factoidal.js');
48
+ } catch { /* not installed */ }
49
+ const env = process.env.FACTOIDAL_L4_ASSETS;
50
+ if (env && existsSync(path.join(env, 'l4factoidal.js'))) {
51
+ return path.join(env, 'l4factoidal.js');
52
+ }
53
+ const repo = path.join(__dirname, '..', '..', 'docs', 'web', 'hub', 'assets', 'l4', 'l4factoidal.js');
54
+ if (existsSync(repo)) return repo;
55
+ return null;
56
+ }
57
+
58
+ let l4Promise = null;
59
+
60
+ async function loadL4() {
61
+ if (l4Promise) return l4Promise;
62
+ const loaderPath = resolveLoader();
63
+ if (!loaderPath) {
64
+ throw new Error(
65
+ 'factoidal/l4: Lean engine assets not found. They normally ship in ' +
66
+ "this package's own l4-assets/ directory — if it is missing, this " +
67
+ 'checkout/install is incomplete. Otherwise set FACTOIDAL_L4_ASSETS ' +
68
+ 'to a directory containing l4factoidal.js, l4factoidal.mjs and ' +
69
+ 'l4factoidal.wasm, or install the (unpublished, override-only) ' +
70
+ 'companion package @factoidal/lean.');
71
+ }
72
+ l4Promise = import(pathToFileURL(loaderPath).href).then((m) => m.loadL4());
73
+ return l4Promise;
74
+ }
75
+
76
+ module.exports = {
77
+ engine: 'lean4-wasm',
78
+ /** True when the wasm assets are resolvable without loading them. */
79
+ available: () => resolveLoader() !== null,
80
+ /** Load (once) and return the low-level engine handle. */
81
+ loadL4,
82
+ /** Engine identification string from the Lean side. */
83
+ version: async () => (await loadL4()).version(),
84
+ /**
85
+ * Evaluate a basic graph pattern over an in-memory triple list.
86
+ * Arguments and result use SPARQL Query Results JSON term shapes;
87
+ * see docs/web/hub/36-lean-in-the-browser.md for worked examples.
88
+ */
89
+ bgpQuery: async (triples, bgp) => (await loadL4()).bgpQuery(triples, bgp),
90
+ };
package/lib/api.js CHANGED
@@ -490,6 +490,31 @@ function buildApi(driver) {
490
490
  }
491
491
  const opts = options || {};
492
492
  const entail = opts.entail || 'none';
493
+ // x-ikl-* is NOT IMPLEMENTED. It is not withheld by policy.
494
+ //
495
+ // CORRECTION 2026-08-26: this comment previously cited the owner's
496
+ // direction-B ruling (danbri/factoidal#618) as the reason. That
497
+ // ruling was about the IKL-to-RDF projection, not this regime
498
+ // family, and citing it here misstated a decision the owner did
499
+ // not make. The family is the OWNER'S design
500
+ // (danbri/factoidal#581); its Lean dispatch was deleted on
501
+ // 2026-08-26 as collateral of the projection purge
502
+ // (danbri/factoidal#626), not as its target. The semantics
503
+ // survived -- Unified/ClBridge.lean's asserted_merge_sound is the
504
+ // regime's soundness statement -- so restoring it is a dispatch
505
+ // branch, not a redesign. Checked explicitly (not just left out of
506
+ // ENTAIL_VALUES below) so a future ENTAIL_VALUES edit can't
507
+ // reopen this without deliberately removing this check too; see
508
+ // test/select.test.js's regression test.
509
+ if (/^x-ikl/i.test(entail)) {
510
+ throw new TypeError(
511
+ `query: entail '${entail}' is not implemented. The x-ikl-* ` +
512
+ "entailment regimes are the owner's design " +
513
+ "(danbri/factoidal#581); the Lean engine's dispatch for them " +
514
+ 'was deleted on 2026-08-26 as collateral of the IKL-to-RDF ' +
515
+ 'projection purge (danbri/factoidal#626). This is not a ' +
516
+ 'policy exclusion.');
517
+ }
493
518
  if (!ENTAIL_VALUES.has(entail)) {
494
519
  throw new TypeError(
495
520
  `query: entail must be one of ${[...ENTAIL_VALUES].join(', ')}`);
@@ -651,8 +676,13 @@ function buildApi(driver) {
651
676
 
652
677
  if (outFormat === 'nquads') {
653
678
  const e = await entry();
654
- if (e && docs.every((d) => d.ext === 'nq')) {
655
- const nq = docs.map((d) => d.content).join('');
679
+ if (e) {
680
+ // Non-N-Quads documents normalize through parseToDatasetJson
681
+ // (docsToEntryNQuads), same as the turtle branch above — so
682
+ // entry-only drivers (e.g. l4-core.js) serve this path too.
683
+ const nq = docs.every((d) => d.ext === 'nq')
684
+ ? docs.map((d) => d.content).join('')
685
+ : docsToEntryNQuads(e, docs, 'serialize(nquads)');
656
686
  return entryResult(e.serializeNQuads(nq), 'serialize').nquads;
657
687
  }
658
688
  }
@@ -1254,6 +1284,132 @@ function buildApi(driver) {
1254
1284
  return entryResult(e.xpathEval(xmlText, xpathExpr), 'xpathEval');
1255
1285
  }
1256
1286
 
1287
+ /**
1288
+ * Parse Common Logic Interchange Format text (ISO/IEC 24707:2018),
1289
+ * with the IKL `that`-operator extension (entry_jsoo.ml's clParse
1290
+ * export -> L4Factoidal's CL/Clif.lean reader). Reads CLIF into a CL
1291
+ * syntax tree and reports its shape; it never produces RDF -- the
1292
+ * IKL-to-RDF projection that used to accompany it is deleted
1293
+ * (danbri/factoidal#626). Lean 4 only: formal/fstar has no CL/IKL parser, so this
1294
+ * function is absent from index.js/wasm.js -- see capabilities() /
1295
+ * factoidal/select's capability table.
1296
+ * @param {string} clifText
1297
+ * @returns {Promise<{ok: boolean, sentences: number, pureCL: boolean,
1298
+ * normalized: string}>} `pureCL` is a DIALECT flag, not a validity
1299
+ * or quality signal: true while the text stays inside ISO/IEC
1300
+ * 24707 Common Logic, false once it uses IKL's `that` operator.
1301
+ * Both values are returned only for text that parsed; a CLIF text
1302
+ * that fails to parse rejects instead (e.g. a bare `(that S)` used
1303
+ * as a proposition rather than a term -- see the GUIDE).
1304
+ */
1305
+ async function clParse(clifText) {
1306
+ if (typeof clifText !== 'string') {
1307
+ throw new TypeError('clParse: clifText must be a string');
1308
+ }
1309
+ const e = await entry();
1310
+ if (!e) throw pendingError('clParse');
1311
+ requireEntryFn(e, 'clParse', 'Common Logic / IKL parse');
1312
+ return entryResult(e.clParse(clifText), 'clParse');
1313
+ }
1314
+
1315
+ /**
1316
+ * Read Common Logic Interchange Format text and write it back out in
1317
+ * the canonical spacing of the CLIF writer (entry_jsoo.ml's
1318
+ * clSerialize export -> L4Factoidal's CL/Clif.lean reader/writer
1319
+ * pair). Lean 4 only: formal/fstar has no CL/IKL parser, so this
1320
+ * function is absent from index.js/wasm.js -- see capabilities() /
1321
+ * factoidal/select's capability table.
1322
+ *
1323
+ * `roundTripProved` is always `false`. The round-trip lemma
1324
+ * `clif_roundTrip` (`CL/ClifAdequacy.lean`) is an OPEN lemma: the
1325
+ * fragment boundary `marksLexable` is MEASURED, not proved. The
1326
+ * field is in the envelope, unmodified, so a caller does not have to
1327
+ * go and find that out.
1328
+ * @param {string} clifText
1329
+ * @returns {Promise<{ok: boolean, clif: string, sentences: number,
1330
+ * roundTripProved: false}>}
1331
+ */
1332
+ async function clSerialize(clifText) {
1333
+ if (typeof clifText !== 'string') {
1334
+ throw new TypeError('clSerialize: clifText must be a string');
1335
+ }
1336
+ const e = await entry();
1337
+ if (!e) throw pendingError('clSerialize');
1338
+ requireEntryFn(e, 'clSerialize', 'Common Logic / IKL serialize');
1339
+ return entryResult(e.clSerialize(clifText), 'clSerialize');
1340
+ }
1341
+
1342
+ /**
1343
+ * Alpha-normalise Common Logic Interchange Format text: the canonical
1344
+ * representative of each sentence's bound-variable-renaming
1345
+ * equivalence class (entry_jsoo.ml's clAlphaNorm export ->
1346
+ * L4Factoidal's `CL/Alpha.lean`, `Sentence.alphaNorm`). Bound names
1347
+ * become `v1`, `v2`, ... in traversal order, so two sentences that
1348
+ * differ only in bound-variable names produce byte-identical output
1349
+ * -- IKL GUIDE Appendix B condition (1): renaming a bound variable
1350
+ * does not change the proposition expressed. Lean 4 only: formal/fstar
1351
+ * has no CL/IKL parser, so this function is absent from
1352
+ * index.js/wasm.js -- see capabilities() / factoidal/select's
1353
+ * capability table.
1354
+ * @param {string} clifText
1355
+ * @returns {Promise<{ok: boolean, clif: string, sentences: number}>}
1356
+ */
1357
+ async function clAlphaNorm(clifText) {
1358
+ if (typeof clifText !== 'string') {
1359
+ throw new TypeError('clAlphaNorm: clifText must be a string');
1360
+ }
1361
+ const e = await entry();
1362
+ if (!e) throw pendingError('clAlphaNorm');
1363
+ requireEntryFn(e, 'clAlphaNorm', 'Common Logic / IKL alpha-normalise');
1364
+ return entryResult(e.clAlphaNorm(clifText), 'clAlphaNorm');
1365
+ }
1366
+
1367
+ /**
1368
+ * Hayes's satisfiability-preserving reduction of IKL to Common Logic
1369
+ * (entry_jsoo.ml's clNormalize export -> L4Factoidal's
1370
+ * `CL/Normalize.lean`, `normalizeText`; danbri/factoidal#625), over a
1371
+ * whole text: one head text and one shared tail, with the
1372
+ * proposition-name counter running across the text. Lean 4 only:
1373
+ * formal/fstar has no CL/IKL parser, so this function is absent from
1374
+ * index.js/wasm.js -- see capabilities() / factoidal/select's
1375
+ * capability table.
1376
+ *
1377
+ * Two limits, both real, both in the answer, neither hidden:
1378
+ * - `preserves: "satisfiability"` -- the reduction preserves
1379
+ * satisfiability, NOT equivalence. It suits entailment and
1380
+ * consistency testing; it is not a transformation to apply to
1381
+ * data you intend to keep.
1382
+ * - `noIntrusion` IS the proof hypothesis `CL.noIntrSs [] []`
1383
+ * decides, not a paraphrase of it. The transformation runs either
1384
+ * way; when `noIntrusion` is `false`, the output is still
1385
+ * produced, but `tails_satisfiable` / `normalize_preserves` do
1386
+ * not cover that case.
1387
+ * @param {string} clifText
1388
+ * @returns {Promise<{ok: boolean, head: string[], tail: string[],
1389
+ * clif: string, sentences: number, thatCount: number,
1390
+ * noIntrusion: boolean, preserves: 'satisfiability',
1391
+ * provedUnder: string}>}
1392
+ */
1393
+ async function clNormalize(clifText) {
1394
+ if (typeof clifText !== 'string') {
1395
+ throw new TypeError('clNormalize: clifText must be a string');
1396
+ }
1397
+ const e = await entry();
1398
+ if (!e) throw pendingError('clNormalize');
1399
+ requireEntryFn(e, 'clNormalize', 'Common Logic / IKL normalize');
1400
+ return entryResult(e.clNormalize(clifText), 'clNormalize');
1401
+ }
1402
+
1403
+ // clFiniteSat (entry_jsoo.ml's clFiniteSat -> L4Factoidal's
1404
+ // CL/FiniteSatTheorems.lean) is DEFERRED, not excluded, from this
1405
+ // typed layer (owner decision, 2026-08-26): it takes a caller-supplied
1406
+ // finite-interpretation JSON encoding (see Wasm/Ops/CL.lean's header
1407
+ // for the wire format) that has no user yet, and a typed wrapper here
1408
+ // would freeze that shape before anyone knows whether it is right. It
1409
+ // stays reachable through the raw dispatch ABI (`l4.call('clFiniteSat',
1410
+ // [interpJson, clifText])` / factoidal/select's `call('clFiniteSat',
1411
+ // ...)`), which needs no shape commitment on this layer.
1412
+
1257
1413
  // -----------------------------------------------------------------
1258
1414
  // VC Data Integrity crypto (eddsa-rdfc-2022) — HACL* wasm backend.
1259
1415
  // entry_jsoo.ml's vc* exports realise VC_DataIntegrity's four crypto
@@ -2100,6 +2256,10 @@ function buildApi(driver) {
2100
2256
  didKeyResolve,
2101
2257
  xmlWellformed,
2102
2258
  xpathEval,
2259
+ clParse,
2260
+ clSerialize,
2261
+ clAlphaNorm,
2262
+ clNormalize,
2103
2263
  rifEval,
2104
2264
  xsltTransform,
2105
2265
  mathmlEval,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.1.0",
4
- "description": "Formally verified RDF/SPARQL engine, extracted from F* specifications, for Node and the browser (JS + Wasm). Parser and algebra spec verified in F*; on-disk backend has unverified OCaml-side optimization layers being migrated back to F*.",
3
+ "version": "0.2.0",
4
+ "description": "Factoidal: a linked information system with graph data and the Web at its heart - RDF parsing, SPARQL 1.1 query, canonicalization and entailment for Node and the browser (JS + Wasm), grounded in F* and Lean 4. Parser and algebra spec verified in F*; on-disk backend has unverified OCaml-side optimization layers being migrated back to F*.",
5
5
  "keywords": [
6
6
  "sparql",
7
7
  "rdf",
@@ -57,7 +57,20 @@
57
57
  "./factoidal.js": "./factoidal.js",
58
58
  "./factoidal.wasm.js": "./factoidal.wasm.js",
59
59
  "./factoidal.wasm.assets/*": "./factoidal.wasm.assets/*",
60
- "./package.json": "./package.json"
60
+ "./package.json": "./package.json",
61
+ "./l4": {
62
+ "types": "./l4.d.ts",
63
+ "default": "./l4.js"
64
+ },
65
+ "./l4-core": {
66
+ "types": "./l4-core.d.ts",
67
+ "default": "./l4-core.js"
68
+ },
69
+ "./l4-assets/*": "./l4-assets/*",
70
+ "./select": {
71
+ "types": "./select.d.ts",
72
+ "default": "./select.js"
73
+ }
61
74
  },
62
75
  "files": [
63
76
  "index.js",
@@ -68,6 +81,13 @@
68
81
  "rdfjs.js",
69
82
  "fn.js",
70
83
  "fn.d.ts",
84
+ "l4.js",
85
+ "l4.d.ts",
86
+ "l4-core.js",
87
+ "l4-core.d.ts",
88
+ "l4-assets/",
89
+ "select.js",
90
+ "select.d.ts",
71
91
  "lib/",
72
92
  "browser.js",
73
93
  "browser.d.ts",
package/select.d.ts ADDED
@@ -0,0 +1,116 @@
1
+ // TypeScript declarations for factoidal/select — the backend selector
2
+ // (issue #618): one typed surface over both the F* engine (./index.js)
3
+ // and the Lean 4 engine (./l4-core.js), with an explicit, observable
4
+ // lean/fstar/lean1st/fstar1st/slowcompareboth switch. See select.js for
5
+ // the full design rationale and the sub-question decisions.
6
+
7
+ export type Backend = 'lean' | 'fstar' | 'lean1st' | 'fstar1st' | 'slowcompareboth';
8
+
9
+ export const BACKENDS: readonly Backend[];
10
+
11
+ /** Function names the selector can route (see select.js's ALWAYS_IF_ENTRY/CAP_FLAG). */
12
+ export const ROUTABLE: readonly string[];
13
+
14
+ export interface CallOptions {
15
+ /** Per-call override of the selector instance's default backend. */
16
+ backend?: Backend;
17
+ /** lean1st/fstar1st only: function names routed to the OTHER engine
18
+ * regardless of what the primary engine implements. */
19
+ overrideFns?: string[];
20
+ }
21
+
22
+ export interface SingleEngineResult<T> {
23
+ engine: 'lean' | 'fstar';
24
+ backend: Backend;
25
+ value: T;
26
+ }
27
+
28
+ export interface CompareBothResult<T> {
29
+ engine: 'both';
30
+ backend: 'slowcompareboth';
31
+ /** false is a reportable finding, not a thrown error. */
32
+ agree: boolean;
33
+ comparison: { method: string };
34
+ lean: T;
35
+ fstar: T;
36
+ }
37
+
38
+ export type SelectResult<T> = SingleEngineResult<T> | CompareBothResult<T>;
39
+
40
+ /** { lean: boolean, fstar: boolean } per routable function name, derived live. */
41
+ export type CapabilityTable = Record<string, { lean: boolean; fstar: boolean }>;
42
+
43
+ export function capabilityTable(): Promise<CapabilityTable>;
44
+ export function engineSupports(engineApi: unknown, fnName: string): Promise<boolean>;
45
+ export function compareValues(
46
+ fnName: string, leanValue: unknown, fstarValue: unknown
47
+ ): Promise<{ equal: boolean; method: string }>;
48
+
49
+ export interface SelectorOptions {
50
+ backend?: Backend;
51
+ overrideFns?: string[];
52
+ }
53
+
54
+ export interface Selector {
55
+ readonly backend: Backend;
56
+ readonly overrideFns: string[];
57
+ call(fnName: string, args: unknown[], callOptions?: CallOptions): Promise<SelectResult<unknown>>;
58
+ capabilityTable(): Promise<CapabilityTable>;
59
+ parse(text: string, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
60
+ query(data: unknown, sparql: string, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
61
+ update(data: unknown, updateText: string, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
62
+ serialize(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<string>>;
63
+ canonicalize(data: unknown, callOptions?: CallOptions): Promise<SelectResult<string>>;
64
+ canonicalHash(data: unknown, callOptions?: CallOptions): Promise<SelectResult<string>>;
65
+ graphs(data: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
66
+ owlClosure(data: unknown, mode: string, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
67
+ owlIsConsistent(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
68
+ owlEntails(premise: unknown, conclusion: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
69
+ coreRdfsClosure(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
70
+ coreRdfsCheck(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
71
+ rhoDfClosure(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
72
+ rhoDfFragmentCheck(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
73
+ rdfsPlusClosure(data: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
74
+ shaclValidate(data: unknown, shapes: unknown, options?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
75
+ shexValidate(data: unknown, schema: string, focus: unknown, shape?: unknown, callOptions?: CallOptions): Promise<SelectResult<unknown>>;
76
+ /** Lean-only: `backend:'fstar'` throws (formal/fstar has no CL/IKL parser). */
77
+ clParse(clifText: string, callOptions?: CallOptions): Promise<SelectResult<{
78
+ ok: boolean;
79
+ sentences: number;
80
+ pureCL: boolean;
81
+ normalized: string;
82
+ }>>;
83
+ /** Lean-only, same reason as clParse. `roundTripProved` is always
84
+ * `false` -- `clif_roundTrip` is an OPEN lemma; see l4-core.d.ts. */
85
+ clSerialize(clifText: string, callOptions?: CallOptions): Promise<SelectResult<{
86
+ ok: boolean;
87
+ clif: string;
88
+ sentences: number;
89
+ roundTripProved: false;
90
+ }>>;
91
+ /** Lean-only, same reason as clParse. Bound-variable-renaming
92
+ * canonical form (IKL GUIDE Appendix B condition (1)). */
93
+ clAlphaNorm(clifText: string, callOptions?: CallOptions): Promise<SelectResult<{
94
+ ok: boolean;
95
+ clif: string;
96
+ sentences: number;
97
+ }>>;
98
+ /** Lean-only, same reason as clParse. Preserves SATISFIABILITY, not
99
+ * equivalence; `noIntrusion` is the proof hypothesis itself, not a
100
+ * paraphrase -- see l4-core.d.ts. */
101
+ clNormalize(clifText: string, callOptions?: CallOptions): Promise<SelectResult<{
102
+ ok: boolean;
103
+ head: string[];
104
+ tail: string[];
105
+ clif: string;
106
+ sentences: number;
107
+ thatCount: number;
108
+ noIntrusion: boolean;
109
+ preserves: 'satisfiability';
110
+ provedUnder: string;
111
+ }>>;
112
+ // clFiniteSat has no named sugar here (owner decision, 2026-08-26):
113
+ // use call('clFiniteSat', [interpJson, clifText], callOptions).
114
+ }
115
+
116
+ export function createSelector(options?: SelectorOptions): Selector;