@factoidal/core 0.4.0 → 0.5.1
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 +139 -0
- package/README.md +123 -7
- package/bin/store.mjs +157 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +4 -4
- package/package.json +10 -1
- package/version.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,144 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 — 2026-09-04
|
|
4
|
+
|
|
5
|
+
**A store can be opened once and queried many times.** `storeQuery` is
|
|
6
|
+
stateless: it re-reads, re-verifies and re-decodes the block on every
|
|
7
|
+
call, so a chat bot or a server paid the full cost for every question.
|
|
8
|
+
The new handle pays it once.
|
|
9
|
+
|
|
10
|
+
Measured on a 141-graph store, `skos:prefLabel` block of 5,571,302 bytes
|
|
11
|
+
and 45,806 rows, a DIFFERENT search string every query, load 10:
|
|
12
|
+
|
|
13
|
+
| | stateless | handle |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| first query (open + query) | 1,376 ms | 1,363 ms |
|
|
16
|
+
| second query, different string | 1,376 ms | **95 ms** |
|
|
17
|
+
| tenth query, all different | 1,382 ms | 103 ms |
|
|
18
|
+
| ten queries, total | 13,962 ms | **2,277 ms** |
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { openStoreHandle } from '@factoidal/core/store'
|
|
22
|
+
import { loadEngine } from '@factoidal/core/engine'
|
|
23
|
+
const engine = await loadEngine()
|
|
24
|
+
const store = await openStoreHandle(engine, '/path/to/store')
|
|
25
|
+
const a = store.query('PREFIX skos: … SELECT … ') // 95 ms
|
|
26
|
+
const b = store.query('PREFIX skos: … SELECT … ') // 95 ms
|
|
27
|
+
store.close()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**What it buys**: the per-query SHA-256 verification, block decode,
|
|
31
|
+
dataset build and index build all happen once. **What it does not buy**:
|
|
32
|
+
`CONTAINS` still scans every retained row, so cost stays proportional to
|
|
33
|
+
row count. This is not a search fix. There is still no inverted index.
|
|
34
|
+
|
|
35
|
+
⚠️ **Memory.** A held-open handle is about 170 MiB resident, of which
|
|
36
|
+
roughly 94 MiB is the decoded form of that 5.5 MB packed block — about
|
|
37
|
+
17 times the packed size. Query evaluation peaks at 346 MiB. A process
|
|
38
|
+
holding several stores should watch this; `storeHandleList` reports each
|
|
39
|
+
handle's retained bytes and rows.
|
|
40
|
+
|
|
41
|
+
**For a long-lived process**: several stores may be open at once, keyed
|
|
42
|
+
independently. `storeOpen` REFUSES at its cap and never evicts another
|
|
43
|
+
caller's handle — an eviction policy is a host decision and is not
|
|
44
|
+
implemented. The WebAssembly module is single-threaded, so a host must
|
|
45
|
+
queue overlapping calls; use one module instance per worker thread for
|
|
46
|
+
concurrency. A server would still need a re-open path when a generation
|
|
47
|
+
is replaced on disk, and delta-log overlay support: this path serves the
|
|
48
|
+
manifest's committed artifacts only.
|
|
49
|
+
|
|
50
|
+
Handle answers are gated against the stateless path by comparing ROWS,
|
|
51
|
+
not row counts, in `tools/wasm-store-query-smoke.sh` and
|
|
52
|
+
`Wasm/native-smoke.sh` (85 pass, 0 fail, out of 85; was 79).
|
|
53
|
+
|
|
54
|
+
## 0.5.0 — 2026-09-04
|
|
55
|
+
|
|
56
|
+
**Queries against a persisted store are about six times faster.** Measured
|
|
57
|
+
end to end the way a caller runs them — process start, engine load,
|
|
58
|
+
digest verification, block decode and scan — on a 141-graph store with a
|
|
59
|
+
5,571,302-byte `skos:prefLabel` block of 45,806 rows, at the same machine
|
|
60
|
+
load:
|
|
61
|
+
|
|
62
|
+
| query | 0.4.0 | 0.5.0 |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `CONTAINS` over labels, `LIMIT 8` | 12.03 s | **2.06 s** |
|
|
65
|
+
| the same for a second word | 12.18 s | **2.04 s** |
|
|
66
|
+
| the same for a third | 11.96 s | **2.02 s** |
|
|
67
|
+
|
|
68
|
+
Two causes, both fixed.
|
|
69
|
+
|
|
70
|
+
- **A quadratic byte copy in SHA-256.** `Crypto.processBlocks256` copied
|
|
71
|
+
the whole remaining message once per 64-byte block, so verifying an
|
|
72
|
+
artifact was quadratic in its size. Fitting `t = c*n^k` to the
|
|
73
|
+
admission step gave k = 2.08 before and **k = 0.98 after**. On the
|
|
74
|
+
three blocks of that store, admission went 1,179 / 2,150 / 11,069 ms to
|
|
75
|
+
303 / 413 / 872 ms. Decode and evaluation were linear throughout.
|
|
76
|
+
- **`LIMIT` was not pushed down through `GRAPH`.** A `LIMIT 8` cost what
|
|
77
|
+
a full count cost, because `GRAPH ?g { ... }` fell through to the
|
|
78
|
+
reference evaluator over the whole materialised dataset. The push-down
|
|
79
|
+
now takes one `GRAPH` layer with a constant IRI or a variable.
|
|
80
|
+
`ORDER BY`, `OFFSET`, `DISTINCT`, `REDUCED`, `GROUP BY`, `HAVING`,
|
|
81
|
+
`VALUES` and aggregates still reject, two of them pinned by theorems.
|
|
82
|
+
- A `RangeError: Maximum call stack size exceeded` on `SELECT ... LIMIT 8`
|
|
83
|
+
is gone with it, because the query no longer materialises 45,806 rows
|
|
84
|
+
to return eight.
|
|
85
|
+
|
|
86
|
+
**A correctness fix found while measuring.** The pre-existing bare-BGP
|
|
87
|
+
`LIMIT` push-down could answer SHORT: a repeated variable (`?x ?p ?x`) or
|
|
88
|
+
an RDF-star triple term let the backend stop early on rows the match then
|
|
89
|
+
rejected. It now refuses both shapes.
|
|
90
|
+
|
|
91
|
+
**New exports.** `@factoidal/core/store`, `/pack` and `/engine`. A caller
|
|
92
|
+
can drive the store in process instead of spawning the command:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
import { openStore, queryStore } from '@factoidal/core/store'
|
|
96
|
+
import { loadEngine } from '@factoidal/core/engine'
|
|
97
|
+
const engine = await loadEngine()
|
|
98
|
+
const store = openStore('/path/to/store', null)
|
|
99
|
+
const { result } = queryStore(engine, store, 'PREFIX skos: ... SELECT ...')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**The engine carries a day of OWL work**: OWL RL 1,181 pass, 266 fail
|
|
103
|
+
(out of 1,457) and OWL DL about 1,326 pass, 121 fail (out of 1,457), both
|
|
104
|
+
against a conclusion check corrected to require one functional blank-node
|
|
105
|
+
mapping (RDF 1.1 Semantics §1.5 and the interpolation lemma). A false
|
|
106
|
+
clash was removed — the materialiser had minted one existential witness
|
|
107
|
+
for several obligations, so the engine denied three consistent
|
|
108
|
+
ontologies; ConsistencyTest went 758 pass, 4 fail to 761 pass, 1 fail.
|
|
109
|
+
|
|
110
|
+
**Named graphs now pack at scale.** The IBK4 quad path read the whole
|
|
111
|
+
source file and a 553 MB, 194-graph corpus could not be packed at all. It
|
|
112
|
+
streams now, and a quadratic term that only named graphs paid — a hash
|
|
113
|
+
map copied per quad in `addQuadFast` — is gone. Peak memory per source
|
|
114
|
+
byte fell from 37 and 20 to between 7.4 and 11.4; a 316,816,934-byte,
|
|
115
|
+
194-graph input that used to fail now packs in 750 s at 2.34 GB. Byte
|
|
116
|
+
identity with the previous packer holds by theorem, not only by diff.
|
|
117
|
+
|
|
118
|
+
**Documentation corrected.** The GeoSPARQL section named functions that
|
|
119
|
+
do not exist. Six topological functions are implemented — `geof:sfEquals`,
|
|
120
|
+
`sfDisjoint`, `sfIntersects`, `sfTouches`, `sfWithin`, `sfContains` — and
|
|
121
|
+
they work against a persisted store, verified. There is no
|
|
122
|
+
`geof:distance`, `buffer`, `envelope`, `boundary`, `convexHull`, no
|
|
123
|
+
`relate` with a DE-9IM matrix, no CRS handling beyond the WKT literal and
|
|
124
|
+
no GML. Full text is SPARQL 1.1's own `CONTAINS` / `STRSTARTS` / `REGEX`,
|
|
125
|
+
evaluated per row after a block decodes: **there is no inverted index**.
|
|
126
|
+
|
|
127
|
+
Known limits, measured:
|
|
128
|
+
|
|
129
|
+
- A query is still O(rows) per search string, and nothing is retained
|
|
130
|
+
between queries: `storeQuery` re-reads, re-verifies and re-decodes the
|
|
131
|
+
block every call. A store handle that decodes once is the next step.
|
|
132
|
+
- `ORDER BY ... LIMIT n` still overflows the call stack above about
|
|
133
|
+
14,576 materialised rows.
|
|
134
|
+
https://github.com/danbri/factoidal/issues/653
|
|
135
|
+
- `update` and `compact` still exit 3.
|
|
136
|
+
https://github.com/danbri/factoidal/issues/641
|
|
137
|
+
- A query plan is refused above 64 artifacts, 8,388,608 blob bytes or
|
|
138
|
+
100,000 rows. https://github.com/danbri/factoidal/issues/648
|
|
139
|
+
- The two SHA-256 folds are checked equal by the FIPS 180-4 build-time
|
|
140
|
+
guards and the HACL* differential, not proved.
|
|
141
|
+
|
|
3
142
|
## 0.4.0 — 2026-09-04
|
|
4
143
|
|
|
5
144
|
The package builds a store of its own. `pack` and `activate` join
|
package/README.md
CHANGED
|
@@ -488,6 +488,94 @@ store, `SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }`, whole process
|
|
|
488
488
|
including start-up: 220 ms through this command, 33 ms through the
|
|
489
489
|
native `l4block-id-v3-query`.
|
|
490
490
|
|
|
491
|
+
### Holding a store open: verify once, answer many
|
|
492
|
+
|
|
493
|
+
`queryStore` is stateless. Every call transfers the artifacts again, and
|
|
494
|
+
the engine hashes, decodes and indexes them again. A process that answers
|
|
495
|
+
many questions against one generation — a chat bot, an MCP server, a
|
|
496
|
+
SPARQL endpoint — pays all of that for every question, and none of it
|
|
497
|
+
depends on the question.
|
|
498
|
+
|
|
499
|
+
A **store handle** holds that work. `openStoreHandle` verifies each
|
|
500
|
+
artifact against the SHA-256 the manifest commits, decodes each block and
|
|
501
|
+
indexes the rows, once. `handle.query()` then answers from what it
|
|
502
|
+
retained. Several stores can be open at the same time.
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
import { loadEngine } from '@factoidal/core/bin/engine.mjs'
|
|
506
|
+
import { openStore, openStoreHandle, listStoreHandles } from '@factoidal/core/store'
|
|
507
|
+
|
|
508
|
+
const engine = await loadEngine()
|
|
509
|
+
|
|
510
|
+
// Two stores, open at once, held for the life of the process.
|
|
511
|
+
const skos = openStoreHandle(engine, openStore('./skos-store'))
|
|
512
|
+
const docs = openStoreHandle(engine, openStore('./docs-store'))
|
|
513
|
+
|
|
514
|
+
console.log(listStoreHandles(engine))
|
|
515
|
+
// { ok: true, handles: [ {handle:'s1', bytes:…, rows:…}, {handle:'s2', …} ],
|
|
516
|
+
// bytes: …, rows: …, handleCap: 8, bytesCap: 67108864 }
|
|
517
|
+
|
|
518
|
+
const PREFIX = 'PREFIX skos: <http://www.w3.org/2004/02/skos/core#>'
|
|
519
|
+
|
|
520
|
+
function labelsMatching (handle, needle) {
|
|
521
|
+
const answer = handle.query(`${PREFIX}
|
|
522
|
+
SELECT ?c ?l WHERE {
|
|
523
|
+
GRAPH ?g { ?c skos:prefLabel ?l }
|
|
524
|
+
FILTER(CONTAINS(LCASE(STR(?l)), "${needle.toLowerCase()}"))
|
|
525
|
+
} LIMIT 10`)
|
|
526
|
+
return answer.srj.results.bindings
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Many questions, each a new search string. None of them re-reads a block.
|
|
530
|
+
for (const needle of ['water', 'forest', 'railway', 'volcano']) {
|
|
531
|
+
console.log(needle, labelsMatching(skos, needle).length)
|
|
532
|
+
}
|
|
533
|
+
console.log(docs.query(`${PREFIX} SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }`))
|
|
534
|
+
|
|
535
|
+
skos.close()
|
|
536
|
+
docs.close()
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`openStoreHandle(engine, store, options)` takes:
|
|
540
|
+
|
|
541
|
+
| option | effect |
|
|
542
|
+
|---|---|
|
|
543
|
+
| (none) | open every artifact the manifest declares |
|
|
544
|
+
| `{ sparql }` | open only the artifacts that query's plan names |
|
|
545
|
+
| `{ keys }` | open exactly these artifact keys |
|
|
546
|
+
|
|
547
|
+
`queryStoreHandle(engine, handle, sparql)`, `closeStoreHandle(engine,
|
|
548
|
+
handle)` and `listStoreHandles(engine)` are the same operations for a
|
|
549
|
+
caller that holds only the handle id.
|
|
550
|
+
|
|
551
|
+
**Measured 2026-09-04**, macOS arm64, Node 22, the 141-graph SKOS store,
|
|
552
|
+
`skos:prefLabel` block of 5,571,302 bytes and 45,806 rows, one
|
|
553
|
+
`CONTAINS` query per row, a DIFFERENT search string every time:
|
|
554
|
+
|
|
555
|
+
| | stateless `queryStore` | handle |
|
|
556
|
+
|---|---|---|
|
|
557
|
+
| first query (open + query) | 1376 ms | 1363 ms |
|
|
558
|
+
| second query, different search string | 1376 ms | 95 ms |
|
|
559
|
+
| tenth query, all different | 1382 ms | 103 ms |
|
|
560
|
+
| ten queries, total | 13962 ms | 2277 ms |
|
|
561
|
+
|
|
562
|
+
**What a handle buys and what it does not.** It removes the per-query
|
|
563
|
+
digest check, block decode and index build. It does NOT make search
|
|
564
|
+
sub-linear: `CONTAINS` still scans every retained row, so a query still
|
|
565
|
+
costs time proportional to the row count. A text index is separate work.
|
|
566
|
+
|
|
567
|
+
**Residency.** Retaining a decoded block costs memory. Measured on the
|
|
568
|
+
same store: 76 MiB resident with the engine loaded and no handle, 170 MiB
|
|
569
|
+
with the handle open — about 94 MiB for a 5.5 MB packed block, and
|
|
570
|
+
evaluation peaks higher again (346 MiB during the queries above). The
|
|
571
|
+
caps are on ARTIFACT bytes, which is what the manifest declares: 8 open
|
|
572
|
+
handles, and 67108864 retained artifact bytes across all of them. A cap
|
|
573
|
+
is a refusal naming the cap; no handle is ever evicted to make room for
|
|
574
|
+
another. `listStoreHandles` is how a server sees its own residency.
|
|
575
|
+
|
|
576
|
+
**One call at a time.** The WebAssembly module is single-threaded. Two
|
|
577
|
+
`query()` calls cannot overlap; a server queues them.
|
|
578
|
+
|
|
491
579
|
## API (draft)
|
|
492
580
|
|
|
493
581
|
The `factoidal` CLI (`bin/factoidal-cli/factoidal_cli.ml`, built to
|
|
@@ -653,14 +741,42 @@ value transforms:
|
|
|
653
741
|
API; the `_*` functions (e.g. `_deltaLogCorruptLastForTest`) are
|
|
654
742
|
test-only and intentionally left untyped.
|
|
655
743
|
|
|
656
|
-
### GeoSPARQL
|
|
744
|
+
### GeoSPARQL — six topological functions
|
|
745
|
+
|
|
746
|
+
The `geof:` functions below are built into the SPARQL engine and need no
|
|
747
|
+
import. They work through `query()` / `fn.query()` AND against a
|
|
748
|
+
persisted store through `factoidal query`, because both paths evaluate
|
|
749
|
+
in the same environment.
|
|
750
|
+
|
|
751
|
+
geof:sfEquals geof:sfDisjoint geof:sfIntersects
|
|
752
|
+
geof:sfTouches geof:sfWithin geof:sfContains
|
|
753
|
+
|
|
754
|
+
```sparql
|
|
755
|
+
PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
|
|
756
|
+
PREFIX geo: <http://www.opengis.net/ont/geosparql#>
|
|
757
|
+
SELECT ?a WHERE {
|
|
758
|
+
?a :footprint ?w
|
|
759
|
+
FILTER(geof:sfWithin(?w, "POLYGON((0 0,0 2,2 2,2 0,0 0))"^^geo:wktLiteral))
|
|
760
|
+
}
|
|
761
|
+
```
|
|
657
762
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
`
|
|
662
|
-
|
|
663
|
-
|
|
763
|
+
**What is NOT there**, stated so nobody plans around it: no
|
|
764
|
+
`geof:distance`, `geof:buffer`, `geof:envelope`, `geof:boundary`,
|
|
765
|
+
`geof:convexHull` or any other non-topological measure; no
|
|
766
|
+
`geof:relate` with a DE-9IM matrix; no coordinate reference system
|
|
767
|
+
handling beyond what the WKT literal carries; no GML literals. Geometry
|
|
768
|
+
comes from a WKT parser, so a shapefile, GeoJSON or GML source must be
|
|
769
|
+
converted to `geo:wktLiteral` before it is loaded.
|
|
770
|
+
|
|
771
|
+
### Full text: SPARQL's own functions, no index
|
|
772
|
+
|
|
773
|
+
`CONTAINS`, `STRSTARTS`, `STRENDS` and `REGEX` (SPARQL 1.1 §17.4.3) are
|
|
774
|
+
implemented and are the way to search text. They are evaluated per row
|
|
775
|
+
after a block is decoded — **there is no inverted index and no
|
|
776
|
+
`text:query`-style extension**. Measured 2026-09-04: a `CONTAINS` over
|
|
777
|
+
45,806 `skos:prefLabel` values in one block answers in about 6 seconds.
|
|
778
|
+
That is fine for a vocabulary and will not scale to a large literal
|
|
779
|
+
corpus.
|
|
664
780
|
|
|
665
781
|
## Limits (deliberate, documented)
|
|
666
782
|
|
package/bin/store.mjs
CHANGED
|
@@ -236,3 +236,160 @@ export function turtleOfNQuads (engine, nquads) {
|
|
|
236
236
|
throw asStoreError(error)
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------- handles
|
|
241
|
+
//
|
|
242
|
+
// `queryStore` above is stateless: every call transfers the artifacts again,
|
|
243
|
+
// and the engine hashes, decodes and indexes them again. A process that
|
|
244
|
+
// answers many questions against one generation — a chat bot, an MCP server,
|
|
245
|
+
// a SPARQL endpoint — pays that per question, and none of it depends on the
|
|
246
|
+
// question.
|
|
247
|
+
//
|
|
248
|
+
// A handle holds the verified, decoded, indexed form inside the engine.
|
|
249
|
+
// `storeOpen` verifies every artifact against the SHA-256 the manifest
|
|
250
|
+
// commits, once; `storeHandleQuery` then answers from what it retained. The
|
|
251
|
+
// engine refuses a query whose plan needs a block the handle does not retain,
|
|
252
|
+
// so a handle answer is never a partial generation.
|
|
253
|
+
//
|
|
254
|
+
// What a handle does NOT change: a query still scans every retained row, so
|
|
255
|
+
// its cost is still proportional to the row count. A handle removes the
|
|
256
|
+
// per-query decode; it is not a text index.
|
|
257
|
+
//
|
|
258
|
+
// The stateless path above stays exactly as it was. A one-shot CLI query
|
|
259
|
+
// should not pay to build a handle it will drop.
|
|
260
|
+
|
|
261
|
+
/** The artifact keys a manifest declares, in manifest order. */
|
|
262
|
+
function manifestKeys (engine, store) {
|
|
263
|
+
return inspectManifest(engine, store).entries.map((entry) => entry.key)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Read the named artifacts and concatenate them into one region. */
|
|
267
|
+
function regionOf (store, keys) {
|
|
268
|
+
const chunks = keys.map((key) => readWhole(joinPath(store.generationDir, key)))
|
|
269
|
+
let total = 0
|
|
270
|
+
for (const chunk of chunks) total += chunk.length
|
|
271
|
+
const blob = new Uint8Array(total)
|
|
272
|
+
const artifacts = []
|
|
273
|
+
let offset = 0
|
|
274
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
275
|
+
blob.set(chunks[index], offset)
|
|
276
|
+
artifacts.push({ key: keys[index], offset, len: chunks[index].length })
|
|
277
|
+
offset += chunks[index].length
|
|
278
|
+
}
|
|
279
|
+
return { blob, artifacts }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* An open store held inside the engine. Hold this object for as long as the
|
|
284
|
+
* process answers questions about the generation, then `close()` it.
|
|
285
|
+
*
|
|
286
|
+
* The wasm module is single-threaded: two `query()` calls cannot overlap. A
|
|
287
|
+
* server awaits one before it starts the next.
|
|
288
|
+
*/
|
|
289
|
+
export class StoreHandle {
|
|
290
|
+
constructor (engine, store, envelope) {
|
|
291
|
+
this.engine = engine
|
|
292
|
+
this.store = store
|
|
293
|
+
this.handle = envelope.handle
|
|
294
|
+
this.identity = envelope.identity
|
|
295
|
+
this.layout = envelope.layout
|
|
296
|
+
this.wireVersion = envelope.wireVersion
|
|
297
|
+
this.artifacts = envelope.artifacts
|
|
298
|
+
this.bytes = envelope.bytes
|
|
299
|
+
this.rows = envelope.rows
|
|
300
|
+
this.closed = false
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* One SPARQL query against the retained blocks. The envelope is the one
|
|
305
|
+
* `queryStore` answers with — `kind`, `srj`/`boolean`/`nquads`, plus
|
|
306
|
+
* `shards` and `mode` — so a caller handles both paths alike.
|
|
307
|
+
*/
|
|
308
|
+
query (sparql) {
|
|
309
|
+
if (this.closed) {
|
|
310
|
+
throw new StoreOperationError(`store handle ${this.handle} is closed`)
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
return this.engine.call('storeHandleQuery', [this.handle, sparql])
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw asStoreError(error)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Drop the handle and everything it retained. Idempotent on this object. */
|
|
320
|
+
close () {
|
|
321
|
+
if (this.closed) return
|
|
322
|
+
this.closed = true
|
|
323
|
+
try {
|
|
324
|
+
this.engine.call('storeHandleClose', [this.handle])
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw asStoreError(error)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* `storeOpen` — verify, decode and index a generation once.
|
|
333
|
+
*
|
|
334
|
+
* With no options every artifact the manifest declares is opened, so any
|
|
335
|
+
* query the generation can answer works. With `sparql` only the artifacts
|
|
336
|
+
* that query's plan names are opened, which is what a process asking one
|
|
337
|
+
* query shape wants. With `keys` exactly those artifacts are opened.
|
|
338
|
+
*
|
|
339
|
+
* @param {object} engine the loaded engine (bin/engine.mjs)
|
|
340
|
+
* @param {object} store the result of `openStore`
|
|
341
|
+
* @param {{keys?: string[], sparql?: string}} options
|
|
342
|
+
* @returns {StoreHandle}
|
|
343
|
+
*/
|
|
344
|
+
export function openStoreHandle (engine, store, options = {}) {
|
|
345
|
+
let keys
|
|
346
|
+
if (Array.isArray(options.keys)) {
|
|
347
|
+
keys = options.keys
|
|
348
|
+
} else if (typeof options.sparql === 'string') {
|
|
349
|
+
keys = planQuery(engine, store, options.sparql).keys
|
|
350
|
+
} else {
|
|
351
|
+
keys = manifestKeys(engine, store)
|
|
352
|
+
}
|
|
353
|
+
const { blob, artifacts } = regionOf(store, keys)
|
|
354
|
+
let envelope
|
|
355
|
+
try {
|
|
356
|
+
envelope = engine.callBlobIO('storeOpen',
|
|
357
|
+
[store.manifestHex, JSON.stringify(artifacts)], blob).envelope
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw asStoreError(error)
|
|
360
|
+
}
|
|
361
|
+
return new StoreHandle(engine, store, envelope)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** `storeHandleQuery` — the loose form, for a caller holding only the id. */
|
|
365
|
+
export function queryStoreHandle (engine, handle, sparql) {
|
|
366
|
+
const id = typeof handle === 'string' ? handle : handle.handle
|
|
367
|
+
try {
|
|
368
|
+
return engine.call('storeHandleQuery', [id, sparql])
|
|
369
|
+
} catch (error) {
|
|
370
|
+
throw asStoreError(error)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** `storeHandleClose` — the loose form. */
|
|
375
|
+
export function closeStoreHandle (engine, handle) {
|
|
376
|
+
if (handle instanceof StoreHandle) return handle.close()
|
|
377
|
+
try {
|
|
378
|
+
return engine.call('storeHandleClose', [handle])
|
|
379
|
+
} catch (error) {
|
|
380
|
+
throw asStoreError(error)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* `storeHandleList` — what this process holds open, with the retained bytes
|
|
386
|
+
* and rows and the two residency caps. A server that cannot see its own
|
|
387
|
+
* residency cannot be operated.
|
|
388
|
+
*/
|
|
389
|
+
export function listStoreHandles (engine) {
|
|
390
|
+
try {
|
|
391
|
+
return engine.call('storeHandleList', [])
|
|
392
|
+
} catch (error) {
|
|
393
|
+
throw asStoreError(error)
|
|
394
|
+
}
|
|
395
|
+
}
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"engine": "lean4",
|
|
3
3
|
"note": "The Lean 4 (L4Factoidal) engine's own build provenance and claims -- distinct from ../version.json, which is the F* engine's. Do not merge the two: they come from different toolchains, different git SHAs in general, and carry different proof obligations (issue #618).",
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"gitSha": "
|
|
6
|
-
"builtAt": "2026-09-
|
|
5
|
+
"gitSha": "6613042b8acf3be78541f862f3ad0caf057cbab3",
|
|
6
|
+
"builtAt": "2026-09-04T20:56:42+00:00",
|
|
7
7
|
"leanToolchain": "leanprover/lean4:v4.33.1",
|
|
8
8
|
"emscripten": "emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 6.0.8-git",
|
|
9
9
|
"abiVersion": "1",
|
|
10
|
-
"wasmSha256": "
|
|
11
|
-
"wasmBytes":
|
|
10
|
+
"wasmSha256": "ffbb3a0357051f78c2e9135ae629fc04a46037d540501f7fadd3d3e0e96c96b4",
|
|
11
|
+
"wasmBytes": 5412682,
|
|
12
12
|
"claims": {
|
|
13
13
|
"source": "formal/lean4 (L4Factoidal): no sorry, no user axioms, no native_decide; W3C behaviour pinned by build-time #guard",
|
|
14
14
|
"suitesAtBuildSha": "see docs/test-results and formal/lean4/PORT_NOTES.md at gitSha"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Factoidal: a linked information system with graph data and the Web at its heart - RDF parsing, SPARQL 1.1 query, canonicalization and entailment for Node and the browser (JS + Wasm), grounded in F* and Lean 4. Parser and algebra spec verified in F*; on-disk backend has unverified OCaml-side optimization layers being migrated back to F*.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|
|
@@ -79,6 +79,15 @@
|
|
|
79
79
|
"./sample-store": {
|
|
80
80
|
"types": "./sample-store.d.ts",
|
|
81
81
|
"default": "./sample-store.mjs"
|
|
82
|
+
},
|
|
83
|
+
"./store": {
|
|
84
|
+
"default": "./bin/store.mjs"
|
|
85
|
+
},
|
|
86
|
+
"./pack": {
|
|
87
|
+
"default": "./bin/pack.mjs"
|
|
88
|
+
},
|
|
89
|
+
"./engine": {
|
|
90
|
+
"default": "./bin/engine.mjs"
|
|
82
91
|
}
|
|
83
92
|
},
|
|
84
93
|
"files": [
|
package/version.json
CHANGED