@factoidal/core 0.3.0 → 0.4.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 +79 -0
- package/NOTICE +31 -0
- package/README.md +35 -1
- package/bin/factoidal.mjs +250 -14
- package/bin/pack-host.mjs +341 -0
- package/bin/pack-worker.mjs +31 -0
- package/bin/pack.mjs +174 -0
- package/bin/store.mjs +32 -0
- package/l4-assets/l4factoidal.js +61 -1
- package/l4-assets/l4factoidal.mjs +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +4 -4
- package/package.json +10 -2
- package/sample-store/CURRENT +1 -0
- package/sample-store/gen-1/manifest.sbm2 +0 -0
- package/sample-store/gen-1/manifest.tsv +14 -0
- package/sample-store/gen-1/predicate-0.ibk3 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-3.ibk3 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +2 -0
- package/sample-store/gen-1/predicate-5.ibk3 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +2 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +1 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +1 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +3 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
- package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +2 -0
- package/sample-store.d.ts +16 -0
- package/sample-store.mjs +37 -0
- package/store-host/deno.mjs +63 -0
- package/store-host/index.mjs +39 -0
- package/store-host/node.mjs +62 -1
- package/version.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 — 2026-09-04
|
|
4
|
+
|
|
5
|
+
The package builds a store of its own. `pack` and `activate` join
|
|
6
|
+
`inspect` and `query`, so `npm install @factoidal/core` gives a complete
|
|
7
|
+
RDF store — import, activate, query — with no native binary on the
|
|
8
|
+
machine. https://github.com/danbri/factoidal/issues/641
|
|
9
|
+
|
|
10
|
+
- **`factoidal pack INPUT OUTPUT`** builds one immutable Shardborough
|
|
11
|
+
generation from a Turtle, TriG, N-Triples or N-Quads file, streaming
|
|
12
|
+
it in 65,536-byte chunks through the Lean engine's WebAssembly module.
|
|
13
|
+
The generation is BYTE-IDENTICAL to what the native
|
|
14
|
+
`l4block-shard-pack` writes: `diff -r` is empty for the `ibk3` triple
|
|
15
|
+
layout and for the `ibk4` quad layout, on inputs up to 888,949
|
|
16
|
+
triples. `--layout ibk3|ibk4`, `--syntax`, `--base`.
|
|
17
|
+
- **`factoidal activate STORE GENERATION`** verifies every artifact
|
|
18
|
+
against the SHA-256 the manifest commits, and every cross-artifact
|
|
19
|
+
relation, then replaces `CURRENT` atomically. A generation that fails
|
|
20
|
+
verification never becomes current.
|
|
21
|
+
- **`factoidal sample-store`** prints the path of a store this package
|
|
22
|
+
now carries, so a fresh install answers a SPARQL query with nothing
|
|
23
|
+
else to download: 4,434 triples in 13 predicate blocks, five IPTC
|
|
24
|
+
NewsCodes vocabularies under CC BY 4.0. Also exported as
|
|
25
|
+
`@factoidal/core/sample-store`. See NOTICE.
|
|
26
|
+
- The engine gained a raw byte path out of the module
|
|
27
|
+
(`l4_call_blob_io`), so artifact bytes cross the boundary with no
|
|
28
|
+
encoding. Hexadecimal doubled them; base64 was refused. Measured on
|
|
29
|
+
the read path: 242,416 bytes and 96 ms hexadecimal against 4,893 bytes
|
|
30
|
+
and 70 ms raw.
|
|
31
|
+
- The pack hashes with HACL* SHA-256, the same primitive the native
|
|
32
|
+
packer uses. It hashed with the pure Lean SHA-256 in development,
|
|
33
|
+
which cost 3.3 times: 104 s against 31 s on 888,949 triples. Now at
|
|
34
|
+
parity, 29.24 s against the native packer's 29.38 s.
|
|
35
|
+
- `packBegin` takes a base IRI, defaulted by the command to
|
|
36
|
+
`file://<input>` so relative IRIs resolve exactly as the native packer
|
|
37
|
+
resolves them. `--base ''` asks for no base, which makes a relative
|
|
38
|
+
IRI a parse error rather than a silently different term.
|
|
39
|
+
- `pack` on a syntax the streaming fold cannot read now says so by name
|
|
40
|
+
rather than raising an unhandled error.
|
|
41
|
+
- `store-host` gained `readChunk` (a short read means end of file, not
|
|
42
|
+
an error), `writeNew` (create and fsync, refusing an existing file so
|
|
43
|
+
a name collision in a generation is reported) and `makeDirectory`, on
|
|
44
|
+
Node and on Deno both.
|
|
45
|
+
- `factoidal activate` runs on the raised stack too. Verification decodes
|
|
46
|
+
the same blocks the pack encoded, so it recurses as deep; the worker was
|
|
47
|
+
given to `pack` alone at first, and a 112,742-row generation packed
|
|
48
|
+
successfully and then failed to activate with `Maximum call stack size
|
|
49
|
+
exceeded`, leaving a store that could be built and not opened. Found by
|
|
50
|
+
installing the tarball and running the command, which is why that step
|
|
51
|
+
is in the release procedure. `tests/store-host/cli.mjs` now gates
|
|
52
|
+
pack-then-activate on both runtimes.
|
|
53
|
+
- `factoidal pack` no longer needs a runtime flag. The pack fold recurses
|
|
54
|
+
deeper than either runtime's default call stack allows, so an input
|
|
55
|
+
above roughly 0.5 MB ended with `Maximum call stack size exceeded`
|
|
56
|
+
(https://github.com/danbri/factoidal/issues/649). Under Node the pack
|
|
57
|
+
now runs on a `worker_threads` worker with a 64 MiB stack; under Deno
|
|
58
|
+
the command re-executes itself once with
|
|
59
|
+
`--v8-flags=--stack-size=65536`, which needs `--allow-run` and
|
|
60
|
+
`--allow-env` in addition to `--allow-read` and `--allow-write`.
|
|
61
|
+
`gene.ttl`, 17,363,312 bytes and 888,949 triples, packs on the default
|
|
62
|
+
stack of both runtimes, byte-identical to `l4block-shard-pack`.
|
|
63
|
+
`--no-worker` forces the in-process path, which still reports the
|
|
64
|
+
frame budget and the flag that raises it rather than crashing. A
|
|
65
|
+
browser tab has a fixed frame budget and no flag, so this does not
|
|
66
|
+
make an in-page packer possible.
|
|
67
|
+
|
|
68
|
+
Known limits, measured:
|
|
69
|
+
|
|
70
|
+
- `update` and `compact` still exit 3. The delta-log operations are
|
|
71
|
+
stage 4 of https://github.com/danbri/factoidal/issues/641.
|
|
72
|
+
- The `ibk4` quad layout reads the whole source rather than streaming,
|
|
73
|
+
because a quad block commits a graph-set summary over the entire
|
|
74
|
+
input. The wasm packer refuses a quad file above 128 MiB.
|
|
75
|
+
https://github.com/danbri/factoidal/issues/650
|
|
76
|
+
- A query plan is refused above 64 artifacts, 8,388,608 blob bytes or
|
|
77
|
+
100,000 rows. https://github.com/danbri/factoidal/issues/648
|
|
78
|
+
- Packing in a browser tab is limited to about 7,800 distinct terms in
|
|
79
|
+
one block, whatever the file size, and no host flag raises it.
|
|
80
|
+
https://github.com/danbri/factoidal/issues/647
|
|
81
|
+
|
|
3
82
|
## 0.3.0 — 2026-09-03
|
|
4
83
|
|
|
5
84
|
- The `factoidal` command answers SPARQL against a persisted
|
package/NOTICE
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@factoidal/core — third-party content notices
|
|
2
|
+
=============================================
|
|
3
|
+
|
|
4
|
+
The code in this package is Apache-2.0; see LICENSE.
|
|
5
|
+
|
|
6
|
+
sample-store/
|
|
7
|
+
-------------
|
|
8
|
+
|
|
9
|
+
The bundled sample Shardborough store holds five IPTC NewsCodes
|
|
10
|
+
vocabularies, converted from the publisher's RDF/XML and packed into
|
|
11
|
+
IBK3 predicate blocks:
|
|
12
|
+
|
|
13
|
+
spamfstat, videoqualifier, subjectqualifier, videocodec, spct
|
|
14
|
+
|
|
15
|
+
Publisher: International Press Telecommunications Council
|
|
16
|
+
https://iptc.org/
|
|
17
|
+
Licence: Creative Commons Attribution 4.0 International (CC BY 4.0)
|
|
18
|
+
https://creativecommons.org/licenses/by/4.0/
|
|
19
|
+
Obtained through: https://github.com/danbri/skosdex third_party/skos
|
|
20
|
+
|
|
21
|
+
The IRIs, labels and structure are the IPTC's. The block layout, the
|
|
22
|
+
dictionary, the indexes and the manifest are this package's encoding of
|
|
23
|
+
them. Redistribution of the vocabulary content stays under CC BY 4.0.
|
|
24
|
+
|
|
25
|
+
hacl-wasm/
|
|
26
|
+
----------
|
|
27
|
+
|
|
28
|
+
HACL* verified cryptographic primitives, from
|
|
29
|
+
https://github.com/hacl-star/hacl-star, Apache-2.0 / MIT. See the
|
|
30
|
+
provenance recorded in the Factoidal repository under
|
|
31
|
+
skills/node-crypto-haclstar-vc-wasm-build/.
|
package/README.md
CHANGED
|
@@ -380,7 +380,41 @@ SHA-256, evaluating the SPARQL).
|
|
|
380
380
|
> This command is not the native F\* `factoidal` binary that the API
|
|
381
381
|
> table below refers to. That one is `bin/<platform>/factoidal` in the
|
|
382
382
|
> repository and takes subcommands such as `shex` and `compact`. This
|
|
383
|
-
> one takes `version`, `inspect` and `query`.
|
|
383
|
+
> one takes `version`, `sample-store`, `inspect` and `query`.
|
|
384
|
+
|
|
385
|
+
### First query, with nothing else to download
|
|
386
|
+
|
|
387
|
+
The package carries an activated store, so a fresh install answers a
|
|
388
|
+
SPARQL query at once:
|
|
389
|
+
|
|
390
|
+
```console
|
|
391
|
+
$ npm install @factoidal/core
|
|
392
|
+
$ npx factoidal query "$(npx factoidal sample-store)" \
|
|
393
|
+
'SELECT ?c ?l
|
|
394
|
+
WHERE { ?c <http://www.w3.org/2004/02/skos/core#inScheme>
|
|
395
|
+
<http://cv.iptc.org/newscodes/videocodec/> ;
|
|
396
|
+
<http://www.w3.org/2004/02/skos/core#prefLabel> ?l .
|
|
397
|
+
FILTER(langMatches(lang(?l), "en")) }
|
|
398
|
+
LIMIT 4'
|
|
399
|
+
c l
|
|
400
|
+
<http://cv.iptc.org/newscodes/videocodec/c001> "Analogue Black and White"@en-gb
|
|
401
|
+
<http://cv.iptc.org/newscodes/videocodec/c002> "PAL"@en-gb
|
|
402
|
+
<http://cv.iptc.org/newscodes/videocodec/c003> "NTSC"@en-gb
|
|
403
|
+
<http://cv.iptc.org/newscodes/videocodec/c004> "SECAM"@en-gb
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`factoidal sample-store` prints the path; `--json` adds what was
|
|
407
|
+
recorded when the store was packed. From JavaScript:
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
import { sampleStorePath, sampleStoreFacts } from '@factoidal/core/sample-store'
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
The store holds 4,434 triples in 13 predicate blocks: five IPTC
|
|
414
|
+
NewsCodes vocabularies, published by the IPTC under CC BY 4.0 and taken
|
|
415
|
+
from [danbri/skosdex](https://github.com/danbri/skosdex). See `NOTICE`.
|
|
416
|
+
|
|
417
|
+
### Any other store
|
|
384
418
|
|
|
385
419
|
```console
|
|
386
420
|
$ factoidal inspect ./mystore
|
package/bin/factoidal.mjs
CHANGED
|
@@ -20,13 +20,17 @@
|
|
|
20
20
|
// `--format turtle` all print documents the engine produced.
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
-
StoreHostError, listGeneration, readWhole,
|
|
23
|
+
StoreHostError, atomicReplace, listGeneration, makeDirectory, readWhole,
|
|
24
|
+
runtime
|
|
24
25
|
} from '../store-host/index.mjs'
|
|
25
26
|
import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
|
|
26
27
|
import { loadEngine } from './engine.mjs'
|
|
28
|
+
import { sampleStoreFacts, sampleStorePath } from '../sample-store.mjs'
|
|
29
|
+
import { PackError, packSupported, verifyGeneration } from './pack.mjs'
|
|
30
|
+
import { denoReexec, isStackOverflow, runPack } from './pack-host.mjs'
|
|
27
31
|
import {
|
|
28
|
-
StoreOperationError, inspectManifest, openStore, planQuery,
|
|
29
|
-
turtleOfNQuads
|
|
32
|
+
STACK_REMEDY, StoreOperationError, inspectManifest, openStore, planQuery,
|
|
33
|
+
queryStore, stackLimitAdvice, turtleOfNQuads
|
|
30
34
|
} from './store.mjs'
|
|
31
35
|
|
|
32
36
|
const EXIT_OK = 0
|
|
@@ -34,6 +38,41 @@ const EXIT_FAILURE = 1
|
|
|
34
38
|
const EXIT_USAGE = 2
|
|
35
39
|
const EXIT_NOT_WIRED = 3
|
|
36
40
|
|
|
41
|
+
// Progress is reported about every 16 MiB; the packer feeds 65,536 bytes
|
|
42
|
+
// a time, so this is the window that catches one feed per report.
|
|
43
|
+
const FEED_PROGRESS = 65536
|
|
44
|
+
|
|
45
|
+
const PACK_LAYOUTS = ['ibk3', 'ibk4']
|
|
46
|
+
const PACK_SYNTAXES = ['turtle', 'trig', 'nquads', 'ntriples']
|
|
47
|
+
const PACK_SUFFIXES = [
|
|
48
|
+
['.ttl', 'turtle'], ['.turtle', 'turtle'],
|
|
49
|
+
['.trig', 'trig'],
|
|
50
|
+
['.nq', 'nquads'], ['.nquads', 'nquads'],
|
|
51
|
+
['.nt', 'ntriples'], ['.ntriples', 'ntriples']
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
// Suffixes the engine parses elsewhere but the packer's streaming fold does
|
|
55
|
+
// not read. Naming them is better than "cannot tell the syntax from its
|
|
56
|
+
// name", which sends the reader looking for a --syntax value that does not
|
|
57
|
+
// exist.
|
|
58
|
+
const PACK_UNSUPPORTED_SUFFIXES = [
|
|
59
|
+
['.rdf', 'RDF/XML'], ['.owl', 'RDF/XML'], ['.xml', 'RDF/XML'],
|
|
60
|
+
['.jsonld', 'JSON-LD'], ['.json', 'JSON-LD'], ['.n3', 'Notation3'],
|
|
61
|
+
['.csv', 'CSV'], ['.tsv', 'TSV'], ['.hdt', 'HDT']
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/** The parent of a path, and its last component. The `activate` hint
|
|
65
|
+
* printed after a pack needs both; neither is a format decision. */
|
|
66
|
+
function dirOf (path) {
|
|
67
|
+
const cut = path.replace(/[/\\]+$/, '').lastIndexOf('/')
|
|
68
|
+
return cut <= 0 ? '.' : path.slice(0, cut)
|
|
69
|
+
}
|
|
70
|
+
function nameOf (path) {
|
|
71
|
+
const trimmed = path.replace(/[/\\]+$/, '')
|
|
72
|
+
const cut = trimmed.lastIndexOf('/')
|
|
73
|
+
return cut < 0 ? trimmed : trimmed.slice(cut + 1)
|
|
74
|
+
}
|
|
75
|
+
|
|
37
76
|
const ISSUE = 'https://github.com/danbri/factoidal/issues/641'
|
|
38
77
|
|
|
39
78
|
const isDeno = typeof globalThis.Deno !== 'undefined'
|
|
@@ -60,6 +99,7 @@ usage: factoidal <command> [options]
|
|
|
60
99
|
|
|
61
100
|
commands:
|
|
62
101
|
version print the package and engine versions
|
|
102
|
+
sample-store print the path of the bundled sample store
|
|
63
103
|
inspect STORE report what the activated manifest commits
|
|
64
104
|
query STORE [QUERY] evaluate a SPARQL query against a store
|
|
65
105
|
pack INPUT OUTPUT build one immutable generation from an RDF file
|
|
@@ -75,7 +115,10 @@ global options:
|
|
|
75
115
|
exit codes:
|
|
76
116
|
0 success 1 failure 2 usage error 3 not yet wired (${ISSUE})
|
|
77
117
|
|
|
78
|
-
STORE is a collection root: the directory that holds CURRENT
|
|
118
|
+
STORE is a collection root: the directory that holds CURRENT. This package
|
|
119
|
+
carries one, so the first query needs no other download:
|
|
120
|
+
|
|
121
|
+
factoidal query "$(factoidal sample-store)" 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'`
|
|
79
122
|
|
|
80
123
|
const COMMAND_USAGE = {
|
|
81
124
|
version: `factoidal version - print the package and engine versions
|
|
@@ -85,6 +128,21 @@ usage: factoidal version [--json]
|
|
|
85
128
|
Prints the npm package version, the Lean engine's WebAssembly digest as
|
|
86
129
|
recorded by its build, and which host-I/O implementation is loaded.`,
|
|
87
130
|
|
|
131
|
+
'sample-store': `factoidal sample-store - print the bundled store's path
|
|
132
|
+
|
|
133
|
+
usage: factoidal sample-store [--json]
|
|
134
|
+
|
|
135
|
+
Prints the collection root of the Shardborough store this package
|
|
136
|
+
carries, so a fresh install can query something at once:
|
|
137
|
+
|
|
138
|
+
factoidal inspect "$(factoidal sample-store)"
|
|
139
|
+
factoidal query "$(factoidal sample-store)" \\
|
|
140
|
+
'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
|
|
141
|
+
|
|
142
|
+
The store holds five IPTC NewsCodes vocabularies (CC BY 4.0; see NOTICE)
|
|
143
|
+
packed into IBK3 predicate blocks. --json adds what was recorded when it
|
|
144
|
+
was packed.`,
|
|
145
|
+
|
|
88
146
|
inspect: `factoidal inspect - report what a store's manifest commits
|
|
89
147
|
|
|
90
148
|
usage: factoidal inspect STORE [--json] [--generation NAME]
|
|
@@ -157,7 +215,14 @@ options:
|
|
|
157
215
|
--layout LAYOUT ibk3 (triples, default) or ibk4 (quads)
|
|
158
216
|
--syntax SYNTAX turtle, trig or nquads; default from the file extension
|
|
159
217
|
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
160
|
-
--json emit one JSON object
|
|
218
|
+
--json emit one JSON object
|
|
219
|
+
--no-worker pack in this process instead of on a worker thread
|
|
220
|
+
|
|
221
|
+
The pack fold recurses deeper than either runtime's default call stack
|
|
222
|
+
allows, so it runs on a worker thread with a raised stack under Node, and
|
|
223
|
+
under Deno the command re-executes itself once with a raised V8 stack
|
|
224
|
+
(https://github.com/danbri/factoidal/issues/649). --no-worker turns both
|
|
225
|
+
off; a pack above about half a megabyte of input then overflows.`,
|
|
161
226
|
|
|
162
227
|
activate: `factoidal activate - make one generation the activated generation
|
|
163
228
|
|
|
@@ -239,9 +304,10 @@ class UsageError extends Error {}
|
|
|
239
304
|
|
|
240
305
|
const VALUE_OPTIONS = {
|
|
241
306
|
version: new Set([]),
|
|
307
|
+
'sample-store': new Set([]),
|
|
242
308
|
inspect: new Set(['generation']),
|
|
243
309
|
query: new Set(['query', 'file', 'format', 'limit', 'base', 'generation']),
|
|
244
|
-
pack: new Set(['layout', 'syntax', 'chunk-bytes']),
|
|
310
|
+
pack: new Set(['layout', 'syntax', 'chunk-bytes', 'base']),
|
|
245
311
|
activate: new Set([]),
|
|
246
312
|
update: new Set(['update', 'file']),
|
|
247
313
|
compact: new Set([])
|
|
@@ -297,6 +363,16 @@ function commandVersion (options) {
|
|
|
297
363
|
return EXIT_OK
|
|
298
364
|
}
|
|
299
365
|
|
|
366
|
+
function commandSampleStore (options) {
|
|
367
|
+
const path = sampleStorePath()
|
|
368
|
+
if (options.json === true) {
|
|
369
|
+
out(JSON.stringify({ path, ...sampleStoreFacts }, null, 2))
|
|
370
|
+
return EXIT_OK
|
|
371
|
+
}
|
|
372
|
+
out(path)
|
|
373
|
+
return EXIT_OK
|
|
374
|
+
}
|
|
375
|
+
|
|
300
376
|
// ------------------------------------------------------------ rendering
|
|
301
377
|
|
|
302
378
|
/**
|
|
@@ -448,10 +524,7 @@ function reportStoreFailure (error) {
|
|
|
448
524
|
err(`This query needs more of the store than one WebAssembly call may read: ${error.capValue} against a cap of ${error.capLimit}.`)
|
|
449
525
|
err('Narrow the query - bind a predicate, or restrict the graph - or use the native l4block-* tools.')
|
|
450
526
|
} else if (error.stackLimit) {
|
|
451
|
-
|
|
452
|
-
err('Some evaluator paths recurse once per row, and a few thousand rows can')
|
|
453
|
-
err("exceed Node's default WebAssembly frame budget. Raise it with")
|
|
454
|
-
err('node --stack-size=4000, add a LIMIT, or run the query under Deno.')
|
|
527
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.query)) err(line)
|
|
455
528
|
} else if (error.digestKey !== null) {
|
|
456
529
|
err(`The bytes of '${error.digestKey}' in the generation directory are not the bytes the manifest commits.`)
|
|
457
530
|
err('The generation is damaged or was edited after it was packed; repack or restore it.')
|
|
@@ -561,14 +634,176 @@ function renderQueryResult (engine, result, format, limit, quiet) {
|
|
|
561
634
|
return EXIT_FAILURE
|
|
562
635
|
}
|
|
563
636
|
|
|
564
|
-
|
|
637
|
+
/** The syntax tag for an input, from --syntax or from the file name. The
|
|
638
|
+
* engine is what actually decides how to read the bytes; this only picks
|
|
639
|
+
* which of its parsers to name. */
|
|
640
|
+
function packSyntax (input, options) {
|
|
641
|
+
if (typeof options.syntax === 'string') {
|
|
642
|
+
const syntax = options.syntax.toLowerCase()
|
|
643
|
+
if (PACK_SYNTAXES.indexOf(syntax) >= 0) return syntax
|
|
644
|
+
throw new UsageError(`--syntax ${options.syntax} is not one of ${PACK_SYNTAXES.join(', ')}`)
|
|
645
|
+
}
|
|
646
|
+
const lower = input.toLowerCase()
|
|
647
|
+
for (const [suffix, syntax] of PACK_SUFFIXES) {
|
|
648
|
+
if (lower.endsWith(suffix)) return syntax
|
|
649
|
+
}
|
|
650
|
+
for (const [suffix, name] of PACK_UNSUPPORTED_SUFFIXES) {
|
|
651
|
+
if (lower.endsWith(suffix)) {
|
|
652
|
+
throw new UsageError(
|
|
653
|
+
`pack does not read ${name}. The packer's streaming fold reads ` +
|
|
654
|
+
`${PACK_SYNTAXES.join(', ')} only. Convert the file first, for ` +
|
|
655
|
+
"example with: factoidal parse FILE --out nquads")
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
throw new UsageError(
|
|
659
|
+
`cannot tell the syntax of ${input} from its name; give --syntax ` +
|
|
660
|
+
`(${PACK_SYNTAXES.join(', ')})`)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The base IRI relative IRIs in the source resolve against.
|
|
665
|
+
*
|
|
666
|
+
* The native packer uses `file://<input>`, so this matches it by default
|
|
667
|
+
* and byte-identical output needs no flag. `--base` overrides it, and
|
|
668
|
+
* `--base ''` asks for no base, which turns a relative IRI into a parse
|
|
669
|
+
* error rather than a silently different term.
|
|
670
|
+
*/
|
|
671
|
+
function packBase (input, options) {
|
|
672
|
+
if (typeof options.base === 'string') return options.base
|
|
673
|
+
const absolute = input.startsWith('/') ? input : joinPath(currentDirectory(), input)
|
|
674
|
+
return 'file://' + absolute
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** The process's working directory, on Node and on Deno. */
|
|
678
|
+
function currentDirectory () {
|
|
679
|
+
if (isDeno) return globalThis.Deno.cwd()
|
|
680
|
+
return process.cwd()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function packLayout (options) {
|
|
684
|
+
if (typeof options.layout !== 'string') return 'ibk3'
|
|
685
|
+
const layout = options.layout.toLowerCase()
|
|
686
|
+
if (PACK_LAYOUTS.indexOf(layout) >= 0) return layout
|
|
687
|
+
throw new UsageError(`--layout ${options.layout} is not one of ${PACK_LAYOUTS.join(', ')}`)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function commandPack (positional, options) {
|
|
565
691
|
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
566
|
-
|
|
692
|
+
const [input, output] = positional
|
|
693
|
+
const syntax = packSyntax(input, options)
|
|
694
|
+
const layout = packLayout(options)
|
|
695
|
+
// The pack fold needs a bigger call stack than either runtime gives by
|
|
696
|
+
// default (https://github.com/danbri/factoidal/issues/649). Under Node
|
|
697
|
+
// the work runs on a worker thread with a raised stack; under Deno the
|
|
698
|
+
// command re-executes itself once with --v8-flags=--stack-size, and
|
|
699
|
+
// this is where that happens, before any file is opened. --no-worker
|
|
700
|
+
// keeps the in-process path testable.
|
|
701
|
+
const host = { worker: options['no-worker'] !== true }
|
|
702
|
+
const reexec = await denoReexec(host)
|
|
703
|
+
if (reexec !== null) return reexec
|
|
704
|
+
makeDirectory(output)
|
|
705
|
+
const quiet = options.quiet === true
|
|
706
|
+
let answer
|
|
707
|
+
try {
|
|
708
|
+
answer = await runPack(
|
|
709
|
+
{ kind: 'pack', input, output, syntax, layout, base: packBase(input, options) },
|
|
710
|
+
quiet
|
|
711
|
+
? undefined
|
|
712
|
+
: (progress) => {
|
|
713
|
+
if (progress.bytesRead % (16 * 1024 * 1024) < FEED_PROGRESS) {
|
|
714
|
+
err(`${progress.pass}: ${progress.bytesRead} bytes read, ${progress.artifacts} artifacts written`)
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
host)
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (error instanceof PackError || error instanceof StoreHostError) {
|
|
720
|
+
err(`factoidal pack: ${error.message}`)
|
|
721
|
+
return EXIT_FAILURE
|
|
722
|
+
}
|
|
723
|
+
// Everything the engine refuses -- an unknown grammar tag, a parse
|
|
724
|
+
// error, a cap -- arrives as a plain Error carrying the engine's own
|
|
725
|
+
// words. A stack trace here would hide them.
|
|
726
|
+
if (error instanceof Error && typeof error.message === 'string') {
|
|
727
|
+
err(`factoidal pack: ${error.message.replace(/^l4factoidal:\s*/, '')}`)
|
|
728
|
+
// The raised stack was refused, unavailable, or still not enough.
|
|
729
|
+
if (isStackOverflow(error)) {
|
|
730
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.pack)) err(line)
|
|
731
|
+
}
|
|
732
|
+
return EXIT_FAILURE
|
|
733
|
+
}
|
|
734
|
+
throw error
|
|
735
|
+
}
|
|
736
|
+
if (answer.notWired === true) {
|
|
737
|
+
return notWired('pack',
|
|
738
|
+
'This install carries an engine built before the streaming pack ' +
|
|
739
|
+
'operations. Update @factoidal/core, or set FACTOIDAL_L4_ASSETS to ' +
|
|
740
|
+
'a newer build.')
|
|
741
|
+
}
|
|
742
|
+
const report = answer.report
|
|
743
|
+
if (options.json === true) {
|
|
744
|
+
out(JSON.stringify(report, null, 2))
|
|
745
|
+
return EXIT_OK
|
|
746
|
+
}
|
|
747
|
+
out(`packed ${report.bytesRead} bytes of ${syntax} into ${output}`)
|
|
748
|
+
out(`${report.written.length} artifacts, ${report.bytesWritten} bytes, layout ${layout}`)
|
|
749
|
+
if (typeof report.rows === 'number') out(`${plural(report.rows, 'row')}`)
|
|
750
|
+
out(`activate it with: factoidal activate ${dirOf(output)} ${nameOf(output)}`)
|
|
751
|
+
return EXIT_OK
|
|
567
752
|
}
|
|
568
753
|
|
|
569
|
-
function commandActivate (positional,
|
|
754
|
+
async function commandActivate (positional, options) {
|
|
570
755
|
if (positional.length !== 2) throw new UsageError('activate needs STORE and GENERATION')
|
|
571
|
-
|
|
756
|
+
const [root, generation] = positional
|
|
757
|
+
// Verification decodes the same blocks the pack encoded, so it recurses
|
|
758
|
+
// as deep and needs the same raised stack. Measured 2026-09-04: a
|
|
759
|
+
// 112,742-row generation packed successfully and then failed to
|
|
760
|
+
// activate with `Maximum call stack size exceeded`, leaving a store
|
|
761
|
+
// that could be built and not opened
|
|
762
|
+
// (https://github.com/danbri/factoidal/issues/649).
|
|
763
|
+
const host = { worker: options['no-worker'] !== true }
|
|
764
|
+
const reexec = await denoReexec(host)
|
|
765
|
+
if (reexec !== null) return reexec
|
|
766
|
+
let answer
|
|
767
|
+
try {
|
|
768
|
+
answer = await runPack({ kind: 'activate', root, generation }, undefined, host)
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (error instanceof PackError || error instanceof StoreHostError) {
|
|
771
|
+
err(`factoidal activate: ${error.code ? error.code + ': ' : ''}${error.message}`)
|
|
772
|
+
return EXIT_FAILURE
|
|
773
|
+
}
|
|
774
|
+
if (error instanceof Error && typeof error.message === 'string') {
|
|
775
|
+
err(`factoidal activate: ${error.message.replace(/^l4factoidal:\s*/, '')}`)
|
|
776
|
+
if (isStackOverflow(error)) {
|
|
777
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.pack)) err(line)
|
|
778
|
+
}
|
|
779
|
+
return EXIT_FAILURE
|
|
780
|
+
}
|
|
781
|
+
throw error
|
|
782
|
+
}
|
|
783
|
+
if (answer.notWired === true) {
|
|
784
|
+
return notWired('activate',
|
|
785
|
+
'This install carries an engine built before the activation ' +
|
|
786
|
+
'verification operation. Update @factoidal/core.')
|
|
787
|
+
}
|
|
788
|
+
const verdict = answer.report
|
|
789
|
+
if (verdict.ok !== true) {
|
|
790
|
+
err(`factoidal activate: ${verdict.error}`)
|
|
791
|
+
err('The generation is NOT activated; CURRENT is unchanged.')
|
|
792
|
+
return EXIT_FAILURE
|
|
793
|
+
}
|
|
794
|
+
// Only now does the pointer move, and it moves atomically.
|
|
795
|
+
const pointer = new TextEncoder().encode(generation)
|
|
796
|
+
const synced = atomicReplace(joinPath(root, 'CURRENT'), pointer)
|
|
797
|
+
if (options.json === true) {
|
|
798
|
+
out(JSON.stringify({ ...verdict, generation, directorySynced: synced }, null, 2))
|
|
799
|
+
return EXIT_OK
|
|
800
|
+
}
|
|
801
|
+
out(`activated ${generation}: ${verdict.artifacts} artifacts verified, ${verdict.bytes} bytes`)
|
|
802
|
+
if (!synced) {
|
|
803
|
+
err('CURRENT was replaced, but the directory entry was not synced; a ' +
|
|
804
|
+
'crash now could lose the pointer update.')
|
|
805
|
+
}
|
|
806
|
+
return EXIT_OK
|
|
572
807
|
}
|
|
573
808
|
|
|
574
809
|
function commandUpdate (positional, options) {
|
|
@@ -589,6 +824,7 @@ function commandCompact (positional, _options) {
|
|
|
589
824
|
|
|
590
825
|
const COMMANDS = {
|
|
591
826
|
version: (positional, options) => commandVersion(options),
|
|
827
|
+
'sample-store': (positional, options) => commandSampleStore(options),
|
|
592
828
|
inspect: commandInspect,
|
|
593
829
|
query: commandQuery,
|
|
594
830
|
pack: commandPack,
|