@factoidal/core 0.1.0 → 0.3.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 CHANGED
@@ -1,5 +1,130 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-09-03
4
+
5
+ - The `factoidal` command answers SPARQL against a persisted
6
+ Shardborough store with no native binary. `factoidal inspect STORE`
7
+ decodes the manifest through the engine's `storeManifestInspect`
8
+ operation and prints the wire version, layout, blank-node profile and
9
+ the entry table; `factoidal query STORE 'SELECT ...'` asks the engine
10
+ which artifacts the query needs, reads exactly those, and hands their
11
+ bytes to `storeQuery` through one WebAssembly heap buffer with no
12
+ encoding. Every artifact is verified against the SHA-256 the manifest
13
+ commits before an answer is given. Formats: `table`, `json`,
14
+ `nquads`, `turtle`; `--explain` prints the artifact plan. Runs under
15
+ Node and under Deno (`--allow-read`).
16
+ - The three caps of the store query operation — 64 artifacts, 8388608
17
+ artifact bytes, 100000 rows — are reported with the cap, the value
18
+ and a next step, and are decided before a single file is read.
19
+ Nothing is truncated.
20
+ - `pack`, `activate`, `update` and `compact` still exit 3; they need
21
+ operations that do not exist yet
22
+ (https://github.com/danbri/factoidal/issues/641).
23
+ - A large piped result is no longer truncated: the command sets
24
+ `process.exitCode` instead of calling `process.exit()`, which dropped
25
+ everything past the 64 KiB pipe boundary.
26
+
27
+ ## 0.3.0 — Lean block-worker preview
28
+
29
+ - The bundled Lean-derived WASM artifact exports
30
+ `scanIBK2Predicate(ibk2Hex, predicateIri)` through `factoidal/l4`.
31
+ It validates a canonical IBK2 block and executes its selective predicate
32
+ scan, returning N-Triples plus a row count. This is a narrow physical
33
+ helper for the Shardborough work, not full SPARQL execution inside a storage
34
+ backend and not a high-throughput buffer ABI (the current hex transport is
35
+ intentionally diagnostic).
36
+ - The regenerated Lean artifact is synchronized across the package's
37
+ `l4-assets/` and the Hub build, and is exercised against the music IBK2
38
+ fixture by `tools/wasm-ibk2-smoke.mjs` in the repository.
39
+ - The current-format companion
40
+ `scanIBK3Predicate(ibk3Hex, predicateIri, blankNodeScope)` validates and
41
+ scans complete predicate-local IBK3 artifacts. Its mandatory source/dataset
42
+ scope preserves one blank node across blocks from the same RDF import while
43
+ keeping same-spelled labels in unrelated inputs apart. Hub post 51 composes
44
+ three real IBK3 scans into an editable Lean-WASM SPARQL query.
45
+
46
+ ## 0.2.0 — both engines in one package, with a backend selector
47
+
48
+ - The Lean 4 engine (`L4Factoidal`, wasm) now ships INSIDE
49
+ `@factoidal/core` as `l4-assets/`, alongside the F\*-extracted
50
+ engine. One install gets both. `require('factoidal/l4')` and
51
+ `factoidal/l4-core` are unchanged; the resolver checks in-package
52
+ assets first, then falls through to the old order (companion
53
+ package, `$FACTOIDAL_L4_ASSETS`, repo checkout).
54
+ - `@factoidal/lean` is superseded and was never published.
55
+ - New subpath `factoidal/select`: a backend selector, per-instance
56
+ with per-call override. Values `lean`, `fstar`, `lean1st`,
57
+ `fstar1st`, `slowcompareboth`. A request naming exactly one engine
58
+ never gets an answer from the other -- `lean` and `fstar` throw on a
59
+ function that engine does not implement; `lean1st` / `fstar1st` fall
60
+ through, and `lean1st` also takes a list of functions to route to
61
+ F\* regardless. Every result carries the answering engine.
62
+ `slowcompareboth` runs both and REPORTS disagreement rather than
63
+ throwing; comparison is RDFC-1.0 isomorphism for Dataset-shaped
64
+ results and bag equality (blank nodes relabelled per side) for
65
+ SELECT bindings.
66
+ - Measured on the typed API surface at this release: 15 functions both
67
+ engines answer, 38 F\*-only, 4 Lean-only (out of 57). This measures
68
+ the typed wrapper surface, not the Lean engine's total capability --
69
+ `l4-core.js` wires 16 of the engine's 21 dispatch ops.
70
+ - `clParse` (Common Logic Interchange Format text, ISO/IEC 24707:2018,
71
+ with the IKL `that`-operator extension) is now wired into the typed
72
+ API -- `l4-core.js`/`lib/api.js`/`select.js`, with `.d.ts` types and
73
+ tests. It reads CLIF text and reports its shape (sentence count,
74
+ `pureCL` dialect flag, canonical re-serialisation); it never produces
75
+ RDF. `pureCL` is a DIALECT flag, not a validity or quality signal:
76
+ true while the text stays inside ISO/IEC 24707 Common Logic, false
77
+ once it uses IKL's `that` operator. It is the first Lean-only entry
78
+ on the typed capability table -- formal/fstar has no CL/IKL parser at
79
+ all, so `index.js`/`wasm.js` never export it, `factoidal/select`'s
80
+ `backend:'fstar'` throws for it (never falls back to Lean), and
81
+ `backend:'slowcompareboth'` fails the capability precondition rather
82
+ than comparing one side against nothing.
83
+ - `clSerialize`, `clAlphaNorm` and `clNormalize` join `clParse` on the
84
+ same Lean-only typed surface (owner instruction, 2026-08-26 -- "wire
85
+ into js functional api"), same `.d.ts`-typed, test-covered pattern:
86
+ `clSerialize` reads CLIF text and writes it back in canonical
87
+ spacing, surfacing `roundTripProved: false` unmodified (the
88
+ round-trip lemma `clif_roundTrip`, `CL/ClifAdequacy.lean`, is OPEN --
89
+ the fragment boundary `marksLexable` is measured, not proved).
90
+ `clAlphaNorm` gives each sentence's bound-variable-renaming canonical
91
+ form (IKL GUIDE Appendix B condition (1)). `clNormalize` is Hayes's
92
+ satisfiability-preserving reduction of IKL to Common Logic
93
+ ([#625](https://github.com/danbri/factoidal/issues/625)): it
94
+ surfaces `preserves: "satisfiability"` (not equivalence -- suited to
95
+ entailment/consistency testing, not to transforming data you intend
96
+ to keep) and `noIntrusion` (the proof hypothesis `CL.noIntrSs [] []`
97
+ decides, not a paraphrase of it). `clFiniteSat`, the fifth CL/IKL op,
98
+ is DEFERRED rather than wired: it takes a caller-supplied finite-
99
+ interpretation JSON encoding with no user yet, and a typed wrapper
100
+ would freeze that shape before it is known to be right; it stays
101
+ reachable through the raw dispatch ABI.
102
+ - The IKL-to-RDF projection ops (`clToDataset`, `queryWithIklService`)
103
+ are DELETED from the engine source (danbri/factoidal#626), along with
104
+ the content-addressed proposition graph names they minted. They were
105
+ never exposed through the npm API. `x-ikl-*` entailment regimes are
106
+ still rejected at the JS layer, and the engine no longer defines the
107
+ family either. The two ops remain present in the compiled wasm
108
+ artifact until it is rebuilt (danbri/factoidal#627) -- the artifact
109
+ is ahead of its source. `clParse` is a different op (it never
110
+ produces RDF) and is unaffected.
111
+
112
+ ## 0.2.0 — Lean 4 engine subpath
113
+
114
+ - New `factoidal/l4-core` subpath: the same typed API served by the
115
+ Lean 4-extracted wasm engine instead of the F\*-extracted one
116
+ ([#476](https://github.com/danbri/factoidal/issues/476)). Engine
117
+ assets resolve from the `@factoidal/lean` companion package, the
118
+ `FACTOIDAL_L4_ASSETS` environment variable, or the repository
119
+ checkout, in that order. `capabilities()` reports the Lean engine's
120
+ actual surface; `shaclValidate`/`owlIsConsistent`/`owlEntails`
121
+ raise pinned errors rather than returning wrong answers
122
+ ([#586](https://github.com/danbri/factoidal/issues/586) tracks the
123
+ OWL verdicts).
124
+ - `serialize({ format: "nquads" })` now normalizes through the entry
125
+ ABI instead of the CLI path.
126
+
127
+
3
128
  ## 0.1.0 — First published release (as `@factoidal/core`)
4
129
 
5
130
  - SPARQL 1.1 §17.6 extension functions
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @factoidal/core
2
2
 
3
- > First published cut (0.1.0). The API surface is early and may
3
+ > Release 0.3.0. The API surface is early and may
4
4
  > change before 1.0. This package was previously developed in-tree
5
5
  > under the placeholder names `factoidal` and `@danbri/foafos`; it was
6
6
  > never published under those names. See [CHANGELOG.md](CHANGELOG.md).
@@ -306,6 +306,154 @@ const tabular = await fromCsvw(csvText, csvwMetadataJson, { mode: 'minimal' });
306
306
  const saturated = await rif(data, rifRulesXml); // RIF Core forward chaining
307
307
  ```
308
308
 
309
+ ## Lean engine (new in 0.2.0)
310
+
311
+ Two engines now ship in one package. `factoidal` and `factoidal/wasm`
312
+ are the F\*-extracted engine, unchanged. The subpaths below are the
313
+ Lean 4 engine (`L4Factoidal`, compiled to wasm).
314
+
315
+ `factoidal/l4` also exposes two deliberately narrow physical helpers:
316
+ `scanIBK2Predicate(ibk2Hex, predicateIri)` for the predecessor format and
317
+ `scanIBK3Predicate(ibk3Hex, predicateIri, blankNodeScope)` for the current
318
+ predicate-local format. They validate one canonical RDF block and scan its
319
+ named predicate, returning N-Triples and a row count. The IBK3 source scope
320
+ must be shared across blocks partitioned from one RDF import unit and differ
321
+ across unrelated units; this preserves document-scoped blank-node identity
322
+ when fragments are composed. The hexadecimal argument is a portable
323
+ diagnostic ABI, not the intended high-throughput buffer interface.
324
+
325
+ ```js
326
+ const l4 = require('factoidal/l4-core'); // Lean engine, same API shape
327
+ const { select } = require('factoidal/select'); // choose an engine per call
328
+ ```
329
+
330
+ ### `factoidal/l4-core`
331
+
332
+ Same call shape as the main API — `parse`, `query`, `update`,
333
+ `serialize`, `canonicalize`, `graphs`, `canonicalHash`, `owlClosure`,
334
+ `coreRdfsClosure`, `coreRdfsCheck`, `rhoDfClosure`,
335
+ `rhoDfFragmentCheck`, `rdfsPlusClosure`, `owlIsConsistent` — plus four
336
+ Common Logic / IKL operations that exist only here, because the F\* tree
337
+ has no CL parser.
338
+
339
+ | Function | In → out | What the answer is worth |
340
+ |---|---|---|
341
+ | `clParse(clifText)` | CLIF text → shape report | sentence count, CL-vs-IKL dialect, canonical re-serialisation. Reads CLIF; never produces RDF |
342
+ | `clSerialize(clifText)` | CLIF → CLIF | canonical writer. Returns `roundTripProved: false` — `clif_roundTrip` is an open lemma and the fragment boundary is measured, not proved |
343
+ | `clAlphaNorm(clifText)` | CLIF → CLIF | alpha-equivalence canonical form (IKL Appendix B condition 1) |
344
+ | `clNormalize(clifText)` | CLIF → CLIF | Hayes's IKL-to-CL reduction. Returns `preserves: "satisfiability"` — **not** equivalence — and `noIntrusion`, the proof hypothesis decided rather than assumed |
345
+
346
+ A fifth op, `clFiniteSat(interpJson, clifText)`, is reachable only
347
+ through the raw dispatch ABI: `l4.call('clFiniteSat', [interpJson,
348
+ clifText])`. It is not in the typed layer.
349
+
350
+ ### `factoidal/select`
351
+
352
+ Per-instance backend choice with a per-call override.
353
+
354
+ | Value | Behaviour |
355
+ |---|---|
356
+ | `lean` / `fstar` | that engine only; **throws** if it does not implement the function |
357
+ | `lean1st` / `fstar1st` | prefer that engine, fall through to the other for unimplemented functions |
358
+ | `slowcompareboth` | run both and **report** disagreement rather than throwing |
359
+
360
+ Every result names the engine that answered. `capabilityTable()`
361
+ returns which functions each engine implements.
362
+
363
+ ### What this surface is, honestly
364
+
365
+ Four `String → String` operations plus one through raw dispatch. The
366
+ Lean tree behind them is larger than that — the CL/IKL and unified
367
+ model-theory modules run to about 22,000 lines — but only these reach
368
+ JavaScript today. Everything else in the Lean tree is used through
369
+ `parse`/`query`/`closure`, or not exposed at all.
370
+
371
+ ## The `factoidal` command: querying a persisted store
372
+
373
+ Installing this package puts a `factoidal` command on PATH. It reads a
374
+ **Shardborough** store — the on-disk format the Lean `l4block-*` tools
375
+ write — with no native binary: JavaScript reads the files and moves the
376
+ bytes, and the Lean engine running as WebAssembly makes every format
377
+ decision (parsing the manifest, choosing the blocks, verifying their
378
+ SHA-256, evaluating the SPARQL).
379
+
380
+ > This command is not the native F\* `factoidal` binary that the API
381
+ > table below refers to. That one is `bin/<platform>/factoidal` in the
382
+ > repository and takes subcommands such as `shex` and `compact`. This
383
+ > one takes `version`, `inspect` and `query`.
384
+
385
+ ```console
386
+ $ factoidal inspect ./mystore
387
+ store ./mystore
388
+ generation gen-1 (activated through CURRENT)
389
+ manifest manifest.sbm2, 2372 bytes, wire version 6
390
+ layout predicate-ibk3-ptd1-sri2-tli1-oli2-merkle-v0
391
+ blank-node profile (none recorded)
392
+ term registry local-ibk3-ptd1-v0
393
+ fixed-chunk Merkle commitment yes
394
+ 5 entries, 393775 bytes, 6455 rows
395
+ generation directory holds 42 files, 846592 bytes
396
+
397
+ # rows bytes kind graphs predicate
398
+ 0 1800 110085 IBK3 - http://www.wikidata.org/prop/direct/P31
399
+ 1 719 35535 IBK3 - http://www.wikidata.org/prop/direct/P361
400
+ ...
401
+
402
+ $ factoidal query ./mystore 'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
403
+ mode ibk3-paged-merkle-full-manifest(5), 5 artifacts, 393775 bytes read, plan declares 6455 block rows
404
+ n
405
+ "6455"^^<http://www.w3.org/2001/XMLSchema#integer>
406
+ 1 row
407
+ ```
408
+
409
+ `STORE` is a collection root: the directory holding `CURRENT`. The plan
410
+ line goes to stderr, so stdout carries only the result; `--quiet`
411
+ removes it.
412
+
413
+ | Option | What it does |
414
+ |---|---|
415
+ | `--format table` | default; a human display of the results |
416
+ | `--format json` | SELECT prints the engine's SPARQL 1.1 Query Results JSON; ASK and CONSTRUCT print the operation's envelope |
417
+ | `--format nquads` | CONSTRUCT only: the graph the engine serialized |
418
+ | `--format turtle` | CONSTRUCT only: that graph through the engine's own Turtle writer |
419
+ | `--explain` | print the artifacts the query needs and the open mode, and stop |
420
+ | `--limit N` | print at most N table rows; the total is always named |
421
+ | `--file PATH` | read the query text from a file |
422
+ | `--generation NAME` | read that generation rather than the activated one |
423
+
424
+ Under Deno, run the file directly; `inspect` and `query` need only
425
+ `--allow-read`:
426
+
427
+ ```console
428
+ $ deno run --allow-read node_modules/@factoidal/core/bin/factoidal.mjs query ./mystore 'ASK { ?s ?p ?o }'
429
+ ```
430
+
431
+ ### What the command answers for, and what it does not
432
+
433
+ * **Every artifact is verified.** The engine refuses the whole query
434
+ when a block's bytes do not hash to the SHA-256 the manifest commits,
435
+ and names the artifact.
436
+ * **Three caps.** One call reads at most 64 artifacts, 8388608 artifact
437
+ bytes and 100000 rows. A query over any of them is refused before a
438
+ single file is read, with the cap and the value named. Nothing is
439
+ truncated.
440
+ * **Committed artifacts only.** A store carrying uncompacted delta-log
441
+ updates is not served by this path; use the native `l4block-*` tools.
442
+ * **`pack`, `activate`, `update` and `compact` exit 3.** They need
443
+ WebAssembly operations that do not exist yet
444
+ (https://github.com/danbri/factoidal/issues/641).
445
+ * **Node's WebAssembly frame budget.** Some evaluator paths recurse once
446
+ per row. Measured 2026-09-03 on a 6455-row store, `SELECT ?s ?p ?o
447
+ WHERE { ?s ?p ?o }` overflows the stack under Node's default while
448
+ `SELECT *`, or the same query with a `LIMIT`, does not, and Deno
449
+ clears all of them. The command reports it and exits 1 rather than
450
+ crashing; `node --stack-size=4000` clears it.
451
+
452
+ Measured 2026-09-03 on macOS arm64, the 6455-triple `sequence_variant`
453
+ store, `SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }`, whole process
454
+ including start-up: 220 ms through this command, 33 ms through the
455
+ native `l4block-id-v3-query`.
456
+
309
457
  ## API (draft)
310
458
 
311
459
  The `factoidal` CLI (`bin/factoidal-cli/factoidal_cli.ml`, built to
package/bin/engine.mjs ADDED
@@ -0,0 +1,124 @@
1
+ // Loading the Lean engine (L4Factoidal compiled to WebAssembly) for the
2
+ // `factoidal` command, under Node and under Deno.
3
+ // https://github.com/danbri/factoidal/issues/641
4
+ //
5
+ // This file resolves three files that must stay together and keep their
6
+ // names -- l4factoidal.js, l4factoidal.mjs and l4factoidal.wasm -- and
7
+ // imports the loader. The Emscripten glue resolves the .wasm sidecar from
8
+ // its own basename, so a rename breaks the load on every runtime
9
+ // (skills/lean4-wasm-export, "the naming trap").
10
+ //
11
+ // `npm/factoidal/l4.js` does the same resolution for the CommonJS API. It
12
+ // is not reused here because it is CommonJS and this command must load
13
+ // under Deno without the require() compatibility path.
14
+ //
15
+ // WHAT THIS FILE IS ALLOWED TO DO
16
+ // Find the engine, load it, and turn bytes into the hexadecimal string
17
+ // two of the three store operations take as their small argument. It
18
+ // makes no format decision: it never parses a manifest, verifies a
19
+ // digest, decodes a block, or chooses an artifact.
20
+
21
+ import { readWhole } from '../store-host/index.mjs'
22
+ import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
23
+
24
+ /** Read one environment variable, or null where reading it is refused. */
25
+ function environment (name) {
26
+ try {
27
+ if (typeof globalThis.Deno !== 'undefined' && globalThis.Deno.env) {
28
+ const value = globalThis.Deno.env.get(name)
29
+ return typeof value === 'string' && value.length > 0 ? value : null
30
+ }
31
+ } catch (_error) {
32
+ // Deno without --allow-env. An absent override is the normal case.
33
+ return null
34
+ }
35
+ const value = globalThis.process && globalThis.process.env
36
+ ? globalThis.process.env[name]
37
+ : undefined
38
+ return typeof value === 'string' && value.length > 0 ? value : null
39
+ }
40
+
41
+ function readable (path) {
42
+ try {
43
+ readWhole(path)
44
+ return true
45
+ } catch (_error) {
46
+ return false
47
+ }
48
+ }
49
+
50
+ /** The package directory that holds this command. */
51
+ export function packageDirectory () {
52
+ return fileUrlToPath(new URL('..', import.meta.url).href).replace(/\/$/, '')
53
+ }
54
+
55
+ /**
56
+ * The loader file of the first engine source that exists, or null.
57
+ *
58
+ * Order, first hit wins:
59
+ * 1. this package's own l4-assets/ (what `npm install` gives);
60
+ * 2. $FACTOIDAL_L4_ASSETS (a custom deployment);
61
+ * 3. the repository checkout layout, docs/web/hub/assets/l4/.
62
+ */
63
+ export function resolveEngine () {
64
+ const inPackage = joinPath(packageDirectory(), 'l4-assets/l4factoidal.js')
65
+ if (readable(inPackage)) return inPackage
66
+ const override = environment('FACTOIDAL_L4_ASSETS')
67
+ if (override !== null) {
68
+ const path = joinPath(override, 'l4factoidal.js')
69
+ if (readable(path)) return path
70
+ throw new Error(`FACTOIDAL_L4_ASSETS=${override} holds no l4factoidal.js`)
71
+ }
72
+ const inCheckout = joinPath(packageDirectory(),
73
+ '../../docs/web/hub/assets/l4/l4factoidal.js')
74
+ if (readable(inCheckout)) return inCheckout
75
+ return null
76
+ }
77
+
78
+ let enginePromise = null
79
+
80
+ /**
81
+ * Load the engine once and return its handle: `call(op, args)` and
82
+ * `callBlob(op, args, bytes)`.
83
+ */
84
+ export function loadEngine () {
85
+ if (enginePromise !== null) return enginePromise
86
+ enginePromise = (async () => {
87
+ const loaderPath = resolveEngine()
88
+ if (loaderPath === null) {
89
+ throw new Error(
90
+ 'the Lean engine assets are missing. They normally ship in this ' +
91
+ "package's own l4-assets/ directory; if it is absent this install " +
92
+ 'is incomplete. Otherwise set FACTOIDAL_L4_ASSETS to a directory ' +
93
+ 'holding l4factoidal.js, l4factoidal.mjs and l4factoidal.wasm.')
94
+ }
95
+ const url = loaderPath.startsWith('file://')
96
+ ? loaderPath
97
+ : 'file://' + encodeURI(loaderPath).replace(/#/g, '%23')
98
+ const module = await import(url)
99
+ return module.loadL4()
100
+ })()
101
+ return enginePromise
102
+ }
103
+
104
+ const HEX = (() => {
105
+ const table = new Array(256)
106
+ for (let byte = 0; byte < 256; byte += 1) {
107
+ table[byte] = byte.toString(16).padStart(2, '0')
108
+ }
109
+ return table
110
+ })()
111
+
112
+ /**
113
+ * Lowercase hexadecimal of a byte array. This is a transport encoding for
114
+ * the manifest argument of `storeManifestInspect` and `storeQueryPlan`,
115
+ * not an interpretation of the bytes. Artifact bytes never take this
116
+ * path: they cross raw through `callBlob`.
117
+ * @param {Uint8Array} bytes
118
+ * @returns {string}
119
+ */
120
+ export function hexOfBytes (bytes) {
121
+ const parts = new Array(bytes.length)
122
+ for (let index = 0; index < bytes.length; index += 1) parts[index] = HEX[bytes[index]]
123
+ return parts.join('')
124
+ }