@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 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
- > 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).
@@ -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
+ }