@factoidal/core 0.2.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 +122 -0
- package/NOTICE +31 -0
- package/README.md +131 -1
- package/bin/engine.mjs +124 -0
- package/bin/factoidal.mjs +894 -0
- package/bin/pack-host.mjs +341 -0
- package/bin/pack-worker.mjs +31 -0
- package/bin/pack.mjs +174 -0
- package/bin/store.mjs +238 -0
- package/l4-assets/l4factoidal.js +125 -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 +17 -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 +262 -0
- package/store-host/errors.mjs +55 -0
- package/store-host/index.mjs +247 -0
- package/store-host/node.mjs +273 -0
- package/store-host/paths.mjs +77 -0
- package/version.json +22 -21
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `factoidal` command: pack, activate, query, update and compact a
|
|
3
|
+
// persisted Shardborough store from Node or Deno, with no native binary.
|
|
4
|
+
// https://github.com/danbri/factoidal/issues/641
|
|
5
|
+
//
|
|
6
|
+
// STATE TODAY. The argument surface below is complete and settled, so that
|
|
7
|
+
// wiring the remaining subcommands changes no user-visible syntax.
|
|
8
|
+
// `version`, `inspect` and `query` run: the first needs only host file
|
|
9
|
+
// I/O, and the other two drive the three WebAssembly store operations of
|
|
10
|
+
// `formal/lean4/Wasm/Ops/Store.lean`. `pack`, `activate`, `update` and
|
|
11
|
+
// `compact` parse their arguments, report what they would do, and exit 3;
|
|
12
|
+
// they need operations that do not exist yet.
|
|
13
|
+
//
|
|
14
|
+
// This file reads files by name, moves bytes and renders what the engine
|
|
15
|
+
// answered. It parses no manifest, verifies no digest, decodes no block
|
|
16
|
+
// and chooses no artifact -- every one of those is a format decision and
|
|
17
|
+
// it lives in the Lean source (iron rule 7). The table renderer is a
|
|
18
|
+
// human display of the engine's own SPARQL Query Results JSON, not a
|
|
19
|
+
// second serializer: `--format json`, `--format nquads` and
|
|
20
|
+
// `--format turtle` all print documents the engine produced.
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
StoreHostError, atomicReplace, listGeneration, makeDirectory, readWhole,
|
|
24
|
+
runtime
|
|
25
|
+
} from '../store-host/index.mjs'
|
|
26
|
+
import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
|
|
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'
|
|
31
|
+
import {
|
|
32
|
+
STACK_REMEDY, StoreOperationError, inspectManifest, openStore, planQuery,
|
|
33
|
+
queryStore, stackLimitAdvice, turtleOfNQuads
|
|
34
|
+
} from './store.mjs'
|
|
35
|
+
|
|
36
|
+
const EXIT_OK = 0
|
|
37
|
+
const EXIT_FAILURE = 1
|
|
38
|
+
const EXIT_USAGE = 2
|
|
39
|
+
const EXIT_NOT_WIRED = 3
|
|
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
|
+
|
|
76
|
+
const ISSUE = 'https://github.com/danbri/factoidal/issues/641'
|
|
77
|
+
|
|
78
|
+
const isDeno = typeof globalThis.Deno !== 'undefined'
|
|
79
|
+
const argv = isDeno ? globalThis.Deno.args.slice() : process.argv.slice(2)
|
|
80
|
+
|
|
81
|
+
// Node writes to a PIPE asynchronously, and `process.exit()` drops
|
|
82
|
+
// whatever is still buffered. A SELECT that prints a few megabytes of
|
|
83
|
+
// SPARQL Query Results JSON into `| jq` therefore arrived truncated at
|
|
84
|
+
// the 64 KiB pipe boundary (measured 2026-09-03, 6455 rows). Setting the
|
|
85
|
+
// exit code and letting the process end on its own flushes it.
|
|
86
|
+
function exit (code) {
|
|
87
|
+
if (isDeno) globalThis.Deno.exit(code)
|
|
88
|
+
else process.exitCode = code
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function out (line) { console.log(line) }
|
|
92
|
+
function err (line) { console.error(line) }
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------- usage
|
|
95
|
+
|
|
96
|
+
const USAGE = `factoidal - the Factoidal persisted store, from Node or Deno
|
|
97
|
+
|
|
98
|
+
usage: factoidal <command> [options]
|
|
99
|
+
|
|
100
|
+
commands:
|
|
101
|
+
version print the package and engine versions
|
|
102
|
+
sample-store print the path of the bundled sample store
|
|
103
|
+
inspect STORE report what the activated manifest commits
|
|
104
|
+
query STORE [QUERY] evaluate a SPARQL query against a store
|
|
105
|
+
pack INPUT OUTPUT build one immutable generation from an RDF file
|
|
106
|
+
activate STORE GENERATION make one generation the activated generation
|
|
107
|
+
update STORE [UPDATE] apply a SPARQL Update through the delta log
|
|
108
|
+
compact STORE GENERATION fold the delta log into a new generation
|
|
109
|
+
|
|
110
|
+
global options:
|
|
111
|
+
-h, --help print this text, or a command's own help
|
|
112
|
+
--json machine-readable output where a command has it
|
|
113
|
+
--quiet print results only, no progress lines
|
|
114
|
+
|
|
115
|
+
exit codes:
|
|
116
|
+
0 success 1 failure 2 usage error 3 not yet wired (${ISSUE})
|
|
117
|
+
|
|
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'`
|
|
122
|
+
|
|
123
|
+
const COMMAND_USAGE = {
|
|
124
|
+
version: `factoidal version - print the package and engine versions
|
|
125
|
+
|
|
126
|
+
usage: factoidal version [--json]
|
|
127
|
+
|
|
128
|
+
Prints the npm package version, the Lean engine's WebAssembly digest as
|
|
129
|
+
recorded by its build, and which host-I/O implementation is loaded.`,
|
|
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
|
+
|
|
146
|
+
inspect: `factoidal inspect - report what a store's manifest commits
|
|
147
|
+
|
|
148
|
+
usage: factoidal inspect STORE [--json] [--generation NAME]
|
|
149
|
+
|
|
150
|
+
Reads CURRENT, hands the manifest bytes to the engine's
|
|
151
|
+
storeManifestInspect operation, and prints what it decoded: the wire
|
|
152
|
+
version, the layout, the blank-node publication profile, the term-registry
|
|
153
|
+
version, whether the manifest carries a fixed-chunk Merkle commitment, and
|
|
154
|
+
one row per entry with its predicate, row count, byte length, block kind
|
|
155
|
+
and graph set.
|
|
156
|
+
|
|
157
|
+
options:
|
|
158
|
+
--generation NAME inspect this generation instead of the activated one
|
|
159
|
+
--json print the operation's envelope unchanged`,
|
|
160
|
+
|
|
161
|
+
query: `factoidal query - evaluate a SPARQL query against a store
|
|
162
|
+
|
|
163
|
+
usage: factoidal query STORE [QUERY] [options]
|
|
164
|
+
|
|
165
|
+
QUERY is the query text. Give it as the second argument, or with --query,
|
|
166
|
+
or in a file with --file.
|
|
167
|
+
|
|
168
|
+
The command reads CURRENT and the manifest, asks the engine which
|
|
169
|
+
artifacts the query needs, reads exactly those, and hands their bytes to
|
|
170
|
+
the engine's storeQuery operation. The engine verifies every artifact
|
|
171
|
+
against the SHA-256 the manifest commits before it answers.
|
|
172
|
+
|
|
173
|
+
options:
|
|
174
|
+
--query TEXT the query text
|
|
175
|
+
--file PATH read the query text from a file
|
|
176
|
+
--format FORMAT table (default), json, nquads, turtle
|
|
177
|
+
--limit N print at most N table rows; the total is always named
|
|
178
|
+
--explain print the artifact plan instead of the results
|
|
179
|
+
--generation NAME query this generation instead of the activated one
|
|
180
|
+
--json shorthand for --format json
|
|
181
|
+
--quiet print the result only, no plan line on stderr
|
|
182
|
+
|
|
183
|
+
formats:
|
|
184
|
+
table a human display of the engine's SPARQL Query Results JSON;
|
|
185
|
+
ASK prints true or false, CONSTRUCT prints its N-Triples
|
|
186
|
+
json SELECT prints the engine's SPARQL 1.1 Query Results JSON
|
|
187
|
+
document; ASK and CONSTRUCT print the operation's envelope,
|
|
188
|
+
because the operation answers those two with a boolean and a
|
|
189
|
+
serialized graph rather than with a results document
|
|
190
|
+
nquads CONSTRUCT only: the graph the engine serialized
|
|
191
|
+
turtle CONSTRUCT only: that graph through the engine's own Turtle
|
|
192
|
+
writer, which flattens named graphs into the default graph
|
|
193
|
+
|
|
194
|
+
not available, and why:
|
|
195
|
+
--base IRI the store query operation takes no base argument;
|
|
196
|
+
put a BASE clause in the query text instead
|
|
197
|
+
xml, csv, tsv the engine has no operation that writes the SPARQL
|
|
198
|
+
Results XML, CSV or TSV documents, and writing one
|
|
199
|
+
here would be a second serializer (iron rule 7)
|
|
200
|
+
DESCRIBE the engine answers "DESCRIBE is not supported by the
|
|
201
|
+
npm entry yet"
|
|
202
|
+
|
|
203
|
+
A store carrying a non-empty delta log is not served by this path: the
|
|
204
|
+
operation reads the manifest's committed artifacts only. Use the native
|
|
205
|
+
l4block-* tools for a store with uncompacted updates.`,
|
|
206
|
+
|
|
207
|
+
pack: `factoidal pack - build one immutable generation from an RDF file
|
|
208
|
+
|
|
209
|
+
usage: factoidal pack INPUT OUTPUT [options]
|
|
210
|
+
|
|
211
|
+
INPUT is an RDF file. OUTPUT is the generation directory to create; it is
|
|
212
|
+
normally STORE/gen-N. Packing does not activate: run activate next.
|
|
213
|
+
|
|
214
|
+
options:
|
|
215
|
+
--layout LAYOUT ibk3 (triples, default) or ibk4 (quads)
|
|
216
|
+
--syntax SYNTAX turtle, trig or nquads; default from the file extension
|
|
217
|
+
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
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.`,
|
|
226
|
+
|
|
227
|
+
activate: `factoidal activate - make one generation the activated generation
|
|
228
|
+
|
|
229
|
+
usage: factoidal activate STORE GENERATION [--json]
|
|
230
|
+
|
|
231
|
+
Verifies every artifact of GENERATION and every cross-artifact relation,
|
|
232
|
+
then atomically replaces STORE/CURRENT. A generation that fails
|
|
233
|
+
verification never becomes current.`,
|
|
234
|
+
|
|
235
|
+
update: `factoidal update - apply a SPARQL Update through the delta log
|
|
236
|
+
|
|
237
|
+
usage: factoidal update STORE [UPDATE] [options]
|
|
238
|
+
|
|
239
|
+
UPDATE is the update text. Give it as the second argument, or with
|
|
240
|
+
--update, or in a file with --file. The batch is appended to the
|
|
241
|
+
activated generation's delta log and is visible to the next query.
|
|
242
|
+
|
|
243
|
+
options:
|
|
244
|
+
--update TEXT the update text
|
|
245
|
+
--file PATH read the update text from a file
|
|
246
|
+
--json emit one JSON object`,
|
|
247
|
+
|
|
248
|
+
compact: `factoidal compact - fold the delta log into a new generation
|
|
249
|
+
|
|
250
|
+
usage: factoidal compact STORE GENERATION [options]
|
|
251
|
+
|
|
252
|
+
Reads the activated generation and its committed delta batches, writes
|
|
253
|
+
GENERATION as a new immutable generation under STORE, and records its
|
|
254
|
+
compacted epoch. It does not activate unless --activate is given.
|
|
255
|
+
|
|
256
|
+
options:
|
|
257
|
+
--activate activate the new generation when it verifies
|
|
258
|
+
--json emit one JSON object`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ------------------------------------------------------------- arguments
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Split argv into positional arguments and options. `--name value` and
|
|
265
|
+
* `--name=value` both work; a flag with no value is `true`.
|
|
266
|
+
*/
|
|
267
|
+
function parseArguments (args, valueOptions) {
|
|
268
|
+
const positional = []
|
|
269
|
+
const options = Object.create(null)
|
|
270
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
271
|
+
const argument = args[index]
|
|
272
|
+
if (argument === '--') {
|
|
273
|
+
positional.push(...args.slice(index + 1))
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
if (argument === '-h') {
|
|
277
|
+
options.help = true
|
|
278
|
+
continue
|
|
279
|
+
}
|
|
280
|
+
if (!argument.startsWith('--')) {
|
|
281
|
+
positional.push(argument)
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
const equals = argument.indexOf('=')
|
|
285
|
+
const name = equals < 0 ? argument.slice(2) : argument.slice(2, equals)
|
|
286
|
+
if (equals >= 0) {
|
|
287
|
+
options[name] = argument.slice(equals + 1)
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
if (valueOptions.has(name)) {
|
|
291
|
+
index += 1
|
|
292
|
+
if (index >= args.length) {
|
|
293
|
+
throw new UsageError(`option --${name} needs a value`)
|
|
294
|
+
}
|
|
295
|
+
options[name] = args[index]
|
|
296
|
+
continue
|
|
297
|
+
}
|
|
298
|
+
options[name] = true
|
|
299
|
+
}
|
|
300
|
+
return { positional, options }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
class UsageError extends Error {}
|
|
304
|
+
|
|
305
|
+
const VALUE_OPTIONS = {
|
|
306
|
+
version: new Set([]),
|
|
307
|
+
'sample-store': new Set([]),
|
|
308
|
+
inspect: new Set(['generation']),
|
|
309
|
+
query: new Set(['query', 'file', 'format', 'limit', 'base', 'generation']),
|
|
310
|
+
pack: new Set(['layout', 'syntax', 'chunk-bytes', 'base']),
|
|
311
|
+
activate: new Set([]),
|
|
312
|
+
update: new Set(['update', 'file']),
|
|
313
|
+
compact: new Set([])
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ------------------------------------------------------------- commands
|
|
317
|
+
|
|
318
|
+
function packageDirectory () {
|
|
319
|
+
return fileUrlToPath(new URL('..', import.meta.url).href)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function readJsonFile (path) {
|
|
323
|
+
const bytes = readWhole(path)
|
|
324
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function commandVersion (options) {
|
|
328
|
+
const root = packageDirectory()
|
|
329
|
+
const own = readJsonFile(joinPath(root, 'package.json'))
|
|
330
|
+
let engine = null
|
|
331
|
+
try {
|
|
332
|
+
engine = readJsonFile(joinPath(root, 'l4-assets/version.json'))
|
|
333
|
+
} catch (_error) {
|
|
334
|
+
engine = null
|
|
335
|
+
}
|
|
336
|
+
const report = {
|
|
337
|
+
package: own.name,
|
|
338
|
+
version: own.version,
|
|
339
|
+
host: runtime,
|
|
340
|
+
engine: engine === null
|
|
341
|
+
? null
|
|
342
|
+
: {
|
|
343
|
+
engine: engine.engine,
|
|
344
|
+
version: engine.version,
|
|
345
|
+
gitSha: engine.gitSha,
|
|
346
|
+
leanToolchain: engine.leanToolchain,
|
|
347
|
+
wasmSha256: engine.wasmSha256,
|
|
348
|
+
wasmBytes: engine.wasmBytes
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (options.json) {
|
|
352
|
+
out(JSON.stringify(report, null, 2))
|
|
353
|
+
return EXIT_OK
|
|
354
|
+
}
|
|
355
|
+
out(`${report.package} ${report.version}`)
|
|
356
|
+
out(`host-io ${report.host}`)
|
|
357
|
+
if (report.engine !== null) {
|
|
358
|
+
out(`engine ${report.engine.engine} ${report.engine.version} (${report.engine.leanToolchain})`)
|
|
359
|
+
out(`wasm sha256 ${report.engine.wasmSha256} (${report.engine.wasmBytes} bytes)`)
|
|
360
|
+
} else {
|
|
361
|
+
out('engine (no l4-assets/version.json in this install)')
|
|
362
|
+
}
|
|
363
|
+
return EXIT_OK
|
|
364
|
+
}
|
|
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
|
+
|
|
376
|
+
// ------------------------------------------------------------ rendering
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* One graph name as the manifest reports it. `{"kind":"default"}` is the
|
|
380
|
+
* default graph; the operation marks it that way so a host never has to
|
|
381
|
+
* recognise a reserved IRI.
|
|
382
|
+
*/
|
|
383
|
+
function graphLabel (graph) {
|
|
384
|
+
if (graph.kind === 'default') return 'default'
|
|
385
|
+
if (graph.kind === 'iri') return `<${graph.value}>`
|
|
386
|
+
if (graph.kind === 'bnode') return `_:${graph.value}`
|
|
387
|
+
return JSON.stringify(graph)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* One SPARQL Query Results JSON term as a table cell. This is a display,
|
|
392
|
+
* not a serialization: `--format json` prints the engine's own document
|
|
393
|
+
* and `--format nquads` prints the engine's own N-Triples.
|
|
394
|
+
*/
|
|
395
|
+
function cellOfTerm (term) {
|
|
396
|
+
if (term === undefined || term === null) return ''
|
|
397
|
+
if (term.type === 'uri') return `<${term.value}>`
|
|
398
|
+
if (term.type === 'bnode') return `_:${term.value}`
|
|
399
|
+
if (term.type === 'literal') {
|
|
400
|
+
const lang = term['xml:lang']
|
|
401
|
+
if (typeof lang === 'string' && lang.length > 0) return `"${term.value}"@${lang}`
|
|
402
|
+
if (typeof term.datatype === 'string') return `"${term.value}"^^<${term.datatype}>`
|
|
403
|
+
return `"${term.value}"`
|
|
404
|
+
}
|
|
405
|
+
return JSON.stringify(term)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function plural (count, noun) {
|
|
409
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Print rows as columns padded to their widest cell. */
|
|
413
|
+
function printTable (headers, rows) {
|
|
414
|
+
const widths = headers.map((header) => header.length)
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
for (let index = 0; index < row.length; index += 1) {
|
|
417
|
+
if (row[index].length > widths[index]) widths[index] = row[index].length
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const line = (cells) => cells
|
|
421
|
+
.map((cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index]))
|
|
422
|
+
.join(' ')
|
|
423
|
+
out(line(headers))
|
|
424
|
+
for (const row of rows) out(line(row))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ------------------------------------------------------------- inspect
|
|
428
|
+
|
|
429
|
+
async function commandInspect (positional, options) {
|
|
430
|
+
if (positional.length !== 1) throw new UsageError('inspect needs exactly one STORE')
|
|
431
|
+
const root = positional[0]
|
|
432
|
+
const named = typeof options.generation === 'string' ? options.generation : null
|
|
433
|
+
const store = openStore(root, named)
|
|
434
|
+
const engine = await loadEngine()
|
|
435
|
+
const envelope = inspectManifest(engine, store)
|
|
436
|
+
|
|
437
|
+
if (options.json) {
|
|
438
|
+
// The operation's envelope, unchanged.
|
|
439
|
+
out(JSON.stringify(envelope, null, 2))
|
|
440
|
+
return EXIT_OK
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const files = listGeneration(store.generationDir)
|
|
444
|
+
let directoryBytes = 0
|
|
445
|
+
for (const file of files) directoryBytes += file.size
|
|
446
|
+
|
|
447
|
+
out(`store ${root}`)
|
|
448
|
+
out(`generation ${store.generation}${store.activated ? ' (activated through CURRENT)' : ' (named on the command line)'}`)
|
|
449
|
+
out(`manifest ${store.manifestName}, ${store.manifest.length} bytes, wire version ${envelope.wireVersion}`)
|
|
450
|
+
out(`layout ${envelope.layout}`)
|
|
451
|
+
out(`blank-node profile ${envelope.blankNodeProfile === '' ? '(none recorded)' : envelope.blankNodeProfile}`)
|
|
452
|
+
out(`term registry ${envelope.termRegistryVersion === '' ? '(none recorded)' : envelope.termRegistryVersion}`)
|
|
453
|
+
out(`fixed-chunk Merkle commitment ${envelope.rangeCommitted ? 'yes' : 'no'}`)
|
|
454
|
+
out(`${envelope.entries.length} ${envelope.entries.length === 1 ? 'entry' : 'entries'}, ${envelope.totalBytes} bytes, ${plural(envelope.totalRows, 'row')}`)
|
|
455
|
+
out(`generation directory holds ${files.length} files, ${directoryBytes} bytes`)
|
|
456
|
+
out('')
|
|
457
|
+
printTable(
|
|
458
|
+
['#', 'rows', 'bytes', 'kind', 'graphs', 'predicate'],
|
|
459
|
+
envelope.entries.map((entry) => [
|
|
460
|
+
String(entry.ordinal),
|
|
461
|
+
String(entry.rows),
|
|
462
|
+
String(entry.bytes),
|
|
463
|
+
entry.blockKind,
|
|
464
|
+
entry.graphs.length === 0 ? '-' : entry.graphs.map(graphLabel).join(' '),
|
|
465
|
+
entry.predicate
|
|
466
|
+
]))
|
|
467
|
+
return EXIT_OK
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function notWired (command, detail) {
|
|
471
|
+
err(`factoidal ${command}: not yet wired. ${detail}`)
|
|
472
|
+
err(`This needs a WebAssembly operation from the Lean engine; see ${ISSUE}.`)
|
|
473
|
+
return EXIT_NOT_WIRED
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const QUERY_FORMATS = ['table', 'json', 'nquads', 'turtle']
|
|
477
|
+
const UNAVAILABLE_FORMATS = {
|
|
478
|
+
xml: 'the SPARQL Results XML document',
|
|
479
|
+
csv: 'the SPARQL Results CSV document',
|
|
480
|
+
tsv: 'the SPARQL Results TSV document'
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function queryText (positional, options) {
|
|
484
|
+
if (typeof options.file === 'string') {
|
|
485
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(readWhole(options.file))
|
|
486
|
+
}
|
|
487
|
+
if (positional.length > 1) return positional.slice(1).join(' ')
|
|
488
|
+
if (typeof options.query === 'string') return options.query
|
|
489
|
+
throw new UsageError('query needs QUERY, --query TEXT or --file PATH')
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function queryFormat (options) {
|
|
493
|
+
if (options.json === true && typeof options.format !== 'string') return 'json'
|
|
494
|
+
if (typeof options.format !== 'string') return 'table'
|
|
495
|
+
const format = options.format.toLowerCase()
|
|
496
|
+
if (QUERY_FORMATS.indexOf(format) >= 0) return format
|
|
497
|
+
if (Object.prototype.hasOwnProperty.call(UNAVAILABLE_FORMATS, format)) {
|
|
498
|
+
throw new UsageError(
|
|
499
|
+
`--format ${format} needs an engine operation that writes ` +
|
|
500
|
+
`${UNAVAILABLE_FORMATS[format]}; there is none, and writing one here ` +
|
|
501
|
+
'would be a second serializer. Use --format json.')
|
|
502
|
+
}
|
|
503
|
+
throw new UsageError(`--format ${options.format} is not one of ${QUERY_FORMATS.join(', ')}`)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function queryLimit (options, format) {
|
|
507
|
+
if (typeof options.limit !== 'string') return null
|
|
508
|
+
const limit = Number(options.limit)
|
|
509
|
+
if (!Number.isSafeInteger(limit) || limit < 0) {
|
|
510
|
+
throw new UsageError('--limit needs a non-negative whole number')
|
|
511
|
+
}
|
|
512
|
+
if (format !== 'table') {
|
|
513
|
+
throw new UsageError(
|
|
514
|
+
'--limit truncates the printed table only; it is not carried into the ' +
|
|
515
|
+
"engine's own documents. Put a LIMIT clause in the query instead.")
|
|
516
|
+
}
|
|
517
|
+
return limit
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Report a refusal the store operations made, and what to do about it. */
|
|
521
|
+
function reportStoreFailure (error) {
|
|
522
|
+
err(`factoidal query: ${error.message}`)
|
|
523
|
+
if (error.capLimit !== null) {
|
|
524
|
+
err(`This query needs more of the store than one WebAssembly call may read: ${error.capValue} against a cap of ${error.capLimit}.`)
|
|
525
|
+
err('Narrow the query - bind a predicate, or restrict the graph - or use the native l4block-* tools.')
|
|
526
|
+
} else if (error.stackLimit) {
|
|
527
|
+
for (const line of stackLimitAdvice(STACK_REMEDY.query)) err(line)
|
|
528
|
+
} else if (error.digestKey !== null) {
|
|
529
|
+
err(`The bytes of '${error.digestKey}' in the generation directory are not the bytes the manifest commits.`)
|
|
530
|
+
err('The generation is damaged or was edited after it was packed; repack or restore it.')
|
|
531
|
+
}
|
|
532
|
+
return EXIT_FAILURE
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function commandQuery (positional, options) {
|
|
536
|
+
if (positional.length < 1) throw new UsageError('query needs a STORE')
|
|
537
|
+
if (typeof options.base === 'string') {
|
|
538
|
+
throw new UsageError(
|
|
539
|
+
'the store query operation takes no base argument; put a BASE clause ' +
|
|
540
|
+
'in the query text instead')
|
|
541
|
+
}
|
|
542
|
+
const root = positional[0]
|
|
543
|
+
const sparql = queryText(positional, options)
|
|
544
|
+
const format = queryFormat(options)
|
|
545
|
+
const limit = queryLimit(options, format)
|
|
546
|
+
const quiet = options.quiet === true
|
|
547
|
+
|
|
548
|
+
const named = typeof options.generation === 'string' ? options.generation : null
|
|
549
|
+
const store = openStore(root, named)
|
|
550
|
+
const engine = await loadEngine()
|
|
551
|
+
|
|
552
|
+
if (options.explain === true) {
|
|
553
|
+
let plan
|
|
554
|
+
try {
|
|
555
|
+
plan = planQuery(engine, store, sparql)
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error instanceof StoreOperationError) return reportStoreFailure(error)
|
|
558
|
+
throw error
|
|
559
|
+
}
|
|
560
|
+
if (format === 'json' || options.json === true) {
|
|
561
|
+
out(JSON.stringify(plan, null, 2))
|
|
562
|
+
return EXIT_OK
|
|
563
|
+
}
|
|
564
|
+
out(`layout ${plan.layout} (wire version ${plan.wireVersion})`)
|
|
565
|
+
out(`mode ${plan.mode}`)
|
|
566
|
+
out(`${plural(plan.shards, 'artifact')}, ${plan.bytes} bytes, ${plural(plan.rows, 'row')}`)
|
|
567
|
+
for (const key of plan.keys) out(` ${key}`)
|
|
568
|
+
return EXIT_OK
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
let answer
|
|
572
|
+
try {
|
|
573
|
+
answer = queryStore(engine, store, sparql)
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error instanceof StoreOperationError) return reportStoreFailure(error)
|
|
576
|
+
throw error
|
|
577
|
+
}
|
|
578
|
+
const { plan, result, blobBytes } = answer
|
|
579
|
+
if (!quiet) {
|
|
580
|
+
err(`mode ${result.mode}, ${plural(result.shards, 'artifact')}, ${blobBytes} bytes read, plan declares ${plural(plan.rows, 'block row')}`)
|
|
581
|
+
}
|
|
582
|
+
return renderQueryResult(engine, result, format, limit, quiet)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function renderQueryResult (engine, result, format, limit, quiet) {
|
|
586
|
+
if (result.kind === 'select') {
|
|
587
|
+
const vars = result.srj.head.vars
|
|
588
|
+
const bindings = result.srj.results.bindings
|
|
589
|
+
if (format === 'json') {
|
|
590
|
+
out(JSON.stringify(result.srj, null, 2))
|
|
591
|
+
return EXIT_OK
|
|
592
|
+
}
|
|
593
|
+
if (format !== 'table') {
|
|
594
|
+
err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is a SELECT`)
|
|
595
|
+
return EXIT_FAILURE
|
|
596
|
+
}
|
|
597
|
+
const shown = limit === null ? bindings : bindings.slice(0, limit)
|
|
598
|
+
printTable(vars, shown.map((row) => vars.map((name) => cellOfTerm(row[name]))))
|
|
599
|
+
if (shown.length < bindings.length) {
|
|
600
|
+
err(`showing ${shown.length} of ${bindings.length} rows (--limit ${limit})`)
|
|
601
|
+
} else if (!quiet) {
|
|
602
|
+
err(plural(bindings.length, 'row'))
|
|
603
|
+
}
|
|
604
|
+
return EXIT_OK
|
|
605
|
+
}
|
|
606
|
+
if (result.kind === 'ask') {
|
|
607
|
+
if (format === 'json') {
|
|
608
|
+
// The operation answers ASK with a boolean, not with a results
|
|
609
|
+
// document, so the envelope is what there is to print.
|
|
610
|
+
out(JSON.stringify(result, null, 2))
|
|
611
|
+
return EXIT_OK
|
|
612
|
+
}
|
|
613
|
+
if (format !== 'table') {
|
|
614
|
+
err(`factoidal query: --format ${format} needs a CONSTRUCT query; this one is an ASK`)
|
|
615
|
+
return EXIT_FAILURE
|
|
616
|
+
}
|
|
617
|
+
out(result.boolean ? 'true' : 'false')
|
|
618
|
+
return EXIT_OK
|
|
619
|
+
}
|
|
620
|
+
if (result.kind === 'construct') {
|
|
621
|
+
if (format === 'json') {
|
|
622
|
+
out(JSON.stringify(result, null, 2))
|
|
623
|
+
return EXIT_OK
|
|
624
|
+
}
|
|
625
|
+
if (format === 'turtle') {
|
|
626
|
+
out(turtleOfNQuads(engine, result.nquads))
|
|
627
|
+
return EXIT_OK
|
|
628
|
+
}
|
|
629
|
+
// table and nquads both print what the engine serialized.
|
|
630
|
+
if (result.nquads.length > 0) out(result.nquads.replace(/\n$/, ''))
|
|
631
|
+
return EXIT_OK
|
|
632
|
+
}
|
|
633
|
+
err(`factoidal query: the engine answered an unknown result kind "${result.kind}"`)
|
|
634
|
+
return EXIT_FAILURE
|
|
635
|
+
}
|
|
636
|
+
|
|
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) {
|
|
691
|
+
if (positional.length !== 2) throw new UsageError('pack needs INPUT and OUTPUT')
|
|
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
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function commandActivate (positional, options) {
|
|
755
|
+
if (positional.length !== 2) throw new UsageError('activate needs STORE and GENERATION')
|
|
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
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function commandUpdate (positional, options) {
|
|
810
|
+
if (positional.length < 1) throw new UsageError('update needs a STORE')
|
|
811
|
+
const text = positional.length > 1
|
|
812
|
+
? positional.slice(1).join(' ')
|
|
813
|
+
: (typeof options.update === 'string' ? options.update : null)
|
|
814
|
+
if (text === null && typeof options.file !== 'string') {
|
|
815
|
+
throw new UsageError('update needs UPDATE, --update TEXT or --file PATH')
|
|
816
|
+
}
|
|
817
|
+
return notWired('update', 'The delta-log operations are stage 4 of the milestone.')
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function commandCompact (positional, _options) {
|
|
821
|
+
if (positional.length !== 2) throw new UsageError('compact needs STORE and GENERATION')
|
|
822
|
+
return notWired('compact', 'Compaction is stage 4 of the milestone.')
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const COMMANDS = {
|
|
826
|
+
version: (positional, options) => commandVersion(options),
|
|
827
|
+
'sample-store': (positional, options) => commandSampleStore(options),
|
|
828
|
+
inspect: commandInspect,
|
|
829
|
+
query: commandQuery,
|
|
830
|
+
pack: commandPack,
|
|
831
|
+
activate: commandActivate,
|
|
832
|
+
update: commandUpdate,
|
|
833
|
+
compact: commandCompact
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ----------------------------------------------------------------- main
|
|
837
|
+
|
|
838
|
+
async function main () {
|
|
839
|
+
if (argv.length === 0) {
|
|
840
|
+
out(USAGE)
|
|
841
|
+
return EXIT_USAGE
|
|
842
|
+
}
|
|
843
|
+
const command = argv[0]
|
|
844
|
+
if (command === '--help' || command === '-h' || command === 'help') {
|
|
845
|
+
const topic = argv[1]
|
|
846
|
+
if (typeof topic === 'string' && Object.prototype.hasOwnProperty.call(COMMAND_USAGE, topic)) {
|
|
847
|
+
out(COMMAND_USAGE[topic])
|
|
848
|
+
} else {
|
|
849
|
+
out(USAGE)
|
|
850
|
+
}
|
|
851
|
+
return EXIT_OK
|
|
852
|
+
}
|
|
853
|
+
if (command === '--version') return commandVersion({})
|
|
854
|
+
if (!Object.prototype.hasOwnProperty.call(COMMANDS, command)) {
|
|
855
|
+
err(`factoidal: unknown command "${command}"`)
|
|
856
|
+
err(USAGE)
|
|
857
|
+
return EXIT_USAGE
|
|
858
|
+
}
|
|
859
|
+
let parsed
|
|
860
|
+
try {
|
|
861
|
+
parsed = parseArguments(argv.slice(1), VALUE_OPTIONS[command])
|
|
862
|
+
} catch (error) {
|
|
863
|
+
if (error instanceof UsageError) {
|
|
864
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
865
|
+
return EXIT_USAGE
|
|
866
|
+
}
|
|
867
|
+
throw error
|
|
868
|
+
}
|
|
869
|
+
if (parsed.options.help) {
|
|
870
|
+
out(COMMAND_USAGE[command])
|
|
871
|
+
return EXIT_OK
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
return await COMMANDS[command](parsed.positional, parsed.options)
|
|
875
|
+
} catch (error) {
|
|
876
|
+
if (error instanceof UsageError) {
|
|
877
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
878
|
+
err('')
|
|
879
|
+
err(COMMAND_USAGE[command])
|
|
880
|
+
return EXIT_USAGE
|
|
881
|
+
}
|
|
882
|
+
if (error instanceof StoreHostError) {
|
|
883
|
+
err(`factoidal ${command}: ${error.code}: ${error.message}`)
|
|
884
|
+
return EXIT_FAILURE
|
|
885
|
+
}
|
|
886
|
+
if (error instanceof StoreOperationError) {
|
|
887
|
+
err(`factoidal ${command}: ${error.message}`)
|
|
888
|
+
return EXIT_FAILURE
|
|
889
|
+
}
|
|
890
|
+
throw error
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
exit(await main())
|