@factoidal/core 0.2.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 +43 -0
- package/README.md +97 -1
- package/bin/engine.mjs +124 -0
- package/bin/factoidal.mjs +658 -0
- package/bin/store.mjs +206 -0
- package/l4-assets/l4factoidal.js +65 -5
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/package.json +4 -0
- package/l4-assets/version.json +5 -5
- package/l4.d.ts +14 -0
- package/l4.js +15 -1
- package/package.json +8 -1
- package/store-host/deno.mjs +199 -0
- package/store-host/errors.mjs +55 -0
- package/store-host/index.mjs +208 -0
- package/store-host/node.mjs +212 -0
- package/store-host/paths.mjs +77 -0
- package/version.json +22 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
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
|
+
|
|
3
46
|
## 0.2.0 — both engines in one package, with a backend selector
|
|
4
47
|
|
|
5
48
|
- The Lean 4 engine (`L4Factoidal`, wasm) now ships INSIDE
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @factoidal/core
|
|
2
2
|
|
|
3
|
-
>
|
|
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).
|
|
@@ -312,6 +312,16 @@ Two engines now ship in one package. `factoidal` and `factoidal/wasm`
|
|
|
312
312
|
are the F\*-extracted engine, unchanged. The subpaths below are the
|
|
313
313
|
Lean 4 engine (`L4Factoidal`, compiled to wasm).
|
|
314
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
|
+
|
|
315
325
|
```js
|
|
316
326
|
const l4 = require('factoidal/l4-core'); // Lean engine, same API shape
|
|
317
327
|
const { select } = require('factoidal/select'); // choose an engine per call
|
|
@@ -358,6 +368,92 @@ model-theory modules run to about 22,000 lines — but only these reach
|
|
|
358
368
|
JavaScript today. Everything else in the Lean tree is used through
|
|
359
369
|
`parse`/`query`/`closure`, or not exposed at all.
|
|
360
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
|
+
|
|
361
457
|
## API (draft)
|
|
362
458
|
|
|
363
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
|
+
}
|