@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/NOTICE +31 -0
  3. package/README.md +131 -1
  4. package/bin/engine.mjs +124 -0
  5. package/bin/factoidal.mjs +894 -0
  6. package/bin/pack-host.mjs +341 -0
  7. package/bin/pack-worker.mjs +31 -0
  8. package/bin/pack.mjs +174 -0
  9. package/bin/store.mjs +238 -0
  10. package/l4-assets/l4factoidal.js +125 -5
  11. package/l4-assets/l4factoidal.mjs +1 -1
  12. package/l4-assets/l4factoidal.wasm +0 -0
  13. package/l4-assets/package.json +4 -0
  14. package/l4-assets/version.json +5 -5
  15. package/l4.d.ts +14 -0
  16. package/l4.js +15 -1
  17. package/package.json +17 -2
  18. package/sample-store/CURRENT +1 -0
  19. package/sample-store/gen-1/manifest.sbm2 +0 -0
  20. package/sample-store/gen-1/manifest.tsv +14 -0
  21. package/sample-store/gen-1/predicate-0.ibk3 +0 -0
  22. package/sample-store/gen-1/predicate-0.ibk3.merkle +1 -0
  23. package/sample-store/gen-1/predicate-0.ibk3.oli2 +0 -0
  24. package/sample-store/gen-1/predicate-0.ibk3.oli2.merkle +1 -0
  25. package/sample-store/gen-1/predicate-0.ibk3.sri2 +0 -0
  26. package/sample-store/gen-1/predicate-0.ibk3.sri2.merkle +1 -0
  27. package/sample-store/gen-1/predicate-0.ibk3.tli1 +0 -0
  28. package/sample-store/gen-1/predicate-0.ibk3.tli1.merkle +0 -0
  29. package/sample-store/gen-1/predicate-1.ibk3 +0 -0
  30. package/sample-store/gen-1/predicate-1.ibk3.merkle +0 -0
  31. package/sample-store/gen-1/predicate-1.ibk3.oli2 +0 -0
  32. package/sample-store/gen-1/predicate-1.ibk3.oli2.merkle +0 -0
  33. package/sample-store/gen-1/predicate-1.ibk3.sri2 +0 -0
  34. package/sample-store/gen-1/predicate-1.ibk3.sri2.merkle +1 -0
  35. package/sample-store/gen-1/predicate-1.ibk3.tli1 +0 -0
  36. package/sample-store/gen-1/predicate-1.ibk3.tli1.merkle +1 -0
  37. package/sample-store/gen-1/predicate-10.ibk3 +0 -0
  38. package/sample-store/gen-1/predicate-10.ibk3.merkle +1 -0
  39. package/sample-store/gen-1/predicate-10.ibk3.oli2 +0 -0
  40. package/sample-store/gen-1/predicate-10.ibk3.oli2.merkle +0 -0
  41. package/sample-store/gen-1/predicate-10.ibk3.sri2 +0 -0
  42. package/sample-store/gen-1/predicate-10.ibk3.sri2.merkle +1 -0
  43. package/sample-store/gen-1/predicate-10.ibk3.tli1 +0 -0
  44. package/sample-store/gen-1/predicate-10.ibk3.tli1.merkle +1 -0
  45. package/sample-store/gen-1/predicate-11.ibk3 +0 -0
  46. package/sample-store/gen-1/predicate-11.ibk3.merkle +0 -0
  47. package/sample-store/gen-1/predicate-11.ibk3.oli2 +0 -0
  48. package/sample-store/gen-1/predicate-11.ibk3.oli2.merkle +1 -0
  49. package/sample-store/gen-1/predicate-11.ibk3.sri2 +0 -0
  50. package/sample-store/gen-1/predicate-11.ibk3.sri2.merkle +1 -0
  51. package/sample-store/gen-1/predicate-11.ibk3.tli1 +0 -0
  52. package/sample-store/gen-1/predicate-11.ibk3.tli1.merkle +1 -0
  53. package/sample-store/gen-1/predicate-12.ibk3 +0 -0
  54. package/sample-store/gen-1/predicate-12.ibk3.merkle +1 -0
  55. package/sample-store/gen-1/predicate-12.ibk3.oli2 +0 -0
  56. package/sample-store/gen-1/predicate-12.ibk3.oli2.merkle +1 -0
  57. package/sample-store/gen-1/predicate-12.ibk3.sri2 +0 -0
  58. package/sample-store/gen-1/predicate-12.ibk3.sri2.merkle +1 -0
  59. package/sample-store/gen-1/predicate-12.ibk3.tli1 +0 -0
  60. package/sample-store/gen-1/predicate-12.ibk3.tli1.merkle +1 -0
  61. package/sample-store/gen-1/predicate-2.ibk3 +0 -0
  62. package/sample-store/gen-1/predicate-2.ibk3.merkle +1 -0
  63. package/sample-store/gen-1/predicate-2.ibk3.oli2 +0 -0
  64. package/sample-store/gen-1/predicate-2.ibk3.oli2.merkle +1 -0
  65. package/sample-store/gen-1/predicate-2.ibk3.sri2 +0 -0
  66. package/sample-store/gen-1/predicate-2.ibk3.sri2.merkle +0 -0
  67. package/sample-store/gen-1/predicate-2.ibk3.tli1 +0 -0
  68. package/sample-store/gen-1/predicate-2.ibk3.tli1.merkle +0 -0
  69. package/sample-store/gen-1/predicate-3.ibk3 +0 -0
  70. package/sample-store/gen-1/predicate-3.ibk3.merkle +1 -0
  71. package/sample-store/gen-1/predicate-3.ibk3.oli2 +0 -0
  72. package/sample-store/gen-1/predicate-3.ibk3.oli2.merkle +1 -0
  73. package/sample-store/gen-1/predicate-3.ibk3.sri2 +0 -0
  74. package/sample-store/gen-1/predicate-3.ibk3.sri2.merkle +1 -0
  75. package/sample-store/gen-1/predicate-3.ibk3.tli1 +0 -0
  76. package/sample-store/gen-1/predicate-3.ibk3.tli1.merkle +1 -0
  77. package/sample-store/gen-1/predicate-4.ibk3 +0 -0
  78. package/sample-store/gen-1/predicate-4.ibk3.merkle +0 -0
  79. package/sample-store/gen-1/predicate-4.ibk3.oli2 +0 -0
  80. package/sample-store/gen-1/predicate-4.ibk3.oli2.merkle +1 -0
  81. package/sample-store/gen-1/predicate-4.ibk3.sri2 +0 -0
  82. package/sample-store/gen-1/predicate-4.ibk3.sri2.merkle +1 -0
  83. package/sample-store/gen-1/predicate-4.ibk3.tli1 +0 -0
  84. package/sample-store/gen-1/predicate-4.ibk3.tli1.merkle +2 -0
  85. package/sample-store/gen-1/predicate-5.ibk3 +0 -0
  86. package/sample-store/gen-1/predicate-5.ibk3.merkle +1 -0
  87. package/sample-store/gen-1/predicate-5.ibk3.oli2 +0 -0
  88. package/sample-store/gen-1/predicate-5.ibk3.oli2.merkle +1 -0
  89. package/sample-store/gen-1/predicate-5.ibk3.sri2 +0 -0
  90. package/sample-store/gen-1/predicate-5.ibk3.sri2.merkle +1 -0
  91. package/sample-store/gen-1/predicate-5.ibk3.tli1 +0 -0
  92. package/sample-store/gen-1/predicate-5.ibk3.tli1.merkle +1 -0
  93. package/sample-store/gen-1/predicate-6.ibk3 +0 -0
  94. package/sample-store/gen-1/predicate-6.ibk3.merkle +1 -0
  95. package/sample-store/gen-1/predicate-6.ibk3.oli2 +0 -0
  96. package/sample-store/gen-1/predicate-6.ibk3.oli2.merkle +1 -0
  97. package/sample-store/gen-1/predicate-6.ibk3.sri2 +0 -0
  98. package/sample-store/gen-1/predicate-6.ibk3.sri2.merkle +1 -0
  99. package/sample-store/gen-1/predicate-6.ibk3.tli1 +0 -0
  100. package/sample-store/gen-1/predicate-6.ibk3.tli1.merkle +0 -0
  101. package/sample-store/gen-1/predicate-7.ibk3 +0 -0
  102. package/sample-store/gen-1/predicate-7.ibk3.merkle +0 -0
  103. package/sample-store/gen-1/predicate-7.ibk3.oli2 +0 -0
  104. package/sample-store/gen-1/predicate-7.ibk3.oli2.merkle +2 -0
  105. package/sample-store/gen-1/predicate-7.ibk3.sri2 +0 -0
  106. package/sample-store/gen-1/predicate-7.ibk3.sri2.merkle +1 -0
  107. package/sample-store/gen-1/predicate-7.ibk3.tli1 +0 -0
  108. package/sample-store/gen-1/predicate-7.ibk3.tli1.merkle +1 -0
  109. package/sample-store/gen-1/predicate-8.ibk3 +0 -0
  110. package/sample-store/gen-1/predicate-8.ibk3.merkle +1 -0
  111. package/sample-store/gen-1/predicate-8.ibk3.oli2 +0 -0
  112. package/sample-store/gen-1/predicate-8.ibk3.oli2.merkle +1 -0
  113. package/sample-store/gen-1/predicate-8.ibk3.sri2 +0 -0
  114. package/sample-store/gen-1/predicate-8.ibk3.sri2.merkle +1 -0
  115. package/sample-store/gen-1/predicate-8.ibk3.tli1 +0 -0
  116. package/sample-store/gen-1/predicate-8.ibk3.tli1.merkle +1 -0
  117. package/sample-store/gen-1/predicate-9.ibk3 +0 -0
  118. package/sample-store/gen-1/predicate-9.ibk3.merkle +1 -0
  119. package/sample-store/gen-1/predicate-9.ibk3.oli2 +0 -0
  120. package/sample-store/gen-1/predicate-9.ibk3.oli2.merkle +1 -0
  121. package/sample-store/gen-1/predicate-9.ibk3.sri2 +0 -0
  122. package/sample-store/gen-1/predicate-9.ibk3.sri2.merkle +3 -0
  123. package/sample-store/gen-1/predicate-9.ibk3.tli1 +0 -0
  124. package/sample-store/gen-1/predicate-9.ibk3.tli1.merkle +2 -0
  125. package/sample-store.d.ts +16 -0
  126. package/sample-store.mjs +37 -0
  127. package/store-host/deno.mjs +262 -0
  128. package/store-host/errors.mjs +55 -0
  129. package/store-host/index.mjs +247 -0
  130. package/store-host/node.mjs +273 -0
  131. package/store-host/paths.mjs +77 -0
  132. package/version.json +22 -21
package/bin/store.mjs ADDED
@@ -0,0 +1,238 @@
1
+ // Driving the three WebAssembly store operations from a JavaScript host.
2
+ // https://github.com/danbri/factoidal/issues/641
3
+ //
4
+ // WHAT THIS FILE IS ALLOWED TO DO
5
+ // Read CURRENT, read a manifest file by name, read the artifact files the
6
+ // engine's plan named, concatenate their bytes, and hand them over. It
7
+ // never parses a manifest, never verifies a digest, never decodes a block
8
+ // and never decides which artifact answers a query. Every one of those is
9
+ // a format decision and it lives in `formal/lean4/Wasm/Ops/Store.lean`
10
+ // (iron rule 7 of CLAUDE.md). A reviewer who finds a magic number, a
11
+ // field offset or a hash in this file has found a rule violation.
12
+ //
13
+ // The operations and their envelopes are described in
14
+ // `docs/designissues/2026-09-03-wasm-shardborough-store-ops.md`.
15
+
16
+ import { openCollection, readWhole } from '../store-host/index.mjs'
17
+ import { joinPath } from '../store-host/paths.mjs'
18
+ import { hexOfBytes } from './engine.mjs'
19
+
20
+ /** The manifest file names a generation directory can carry, in the order
21
+ * `Harness.ShardMerklePread.readManifest` tries them. */
22
+ const MANIFEST_NAMES = ['manifest.sbm2', 'manifest.sbm1']
23
+
24
+ /**
25
+ * An error the store operations reported. `capName` is set when the
26
+ * refusal was one of the operation's caps; `message` is always the
27
+ * engine's own words.
28
+ */
29
+ export class StoreOperationError extends Error {
30
+ constructor (message, detail = {}) {
31
+ super(message)
32
+ this.name = 'StoreOperationError'
33
+ this.capValue = detail.capValue === undefined ? null : detail.capValue
34
+ this.capLimit = detail.capLimit === undefined ? null : detail.capLimit
35
+ this.digestKey = detail.digestKey === undefined ? null : detail.digestKey
36
+ this.stackLimit = detail.stackLimit === true
37
+ }
38
+ }
39
+
40
+ // The engine's refusals arrive as an Error whose message is
41
+ // "l4factoidal: <the operation's own text>". These two patterns say only
42
+ // WHICH refusal it was, so the command can add a next step; the text the
43
+ // user sees is always the engine's, never a rewrite of it.
44
+ const CAP_PATTERN = /the plan selects (\d+) (?:artifacts|artifact bytes|rows), the cap is (\d+)/
45
+ const DIGEST_PATTERN = /artifact '([^']*)' does not match the SHA-256/
46
+
47
+ function asStoreError (error) {
48
+ const raw = error && error.message ? String(error.message) : String(error)
49
+ const message = raw.replace(/^l4factoidal:\s*/, '')
50
+ // Not a refusal by the engine: the host runtime ran out of call stack
51
+ // inside the wasm module. Some evaluator paths recurse once per row.
52
+ // Measured 2026-09-03 against the committed wasm, on a 6455-row
53
+ // generation: `SELECT ?s ?p ?o WHERE { ?s ?p ?o }` and the same query
54
+ // with `ORDER BY` overflow under Node's default WebAssembly frame
55
+ // budget, while `SELECT *`, and either query with a LIMIT, do not.
56
+ // `node --stack-size=4000` clears all of them, and Deno clears them at
57
+ // its own default.
58
+ if (error instanceof RangeError || message.indexOf('call stack size exceeded') >= 0) {
59
+ return new StoreOperationError(message, { stackLimit: true })
60
+ }
61
+ const cap = CAP_PATTERN.exec(message)
62
+ if (cap !== null) {
63
+ return new StoreOperationError(message,
64
+ { capValue: Number(cap[1]), capLimit: Number(cap[2]) })
65
+ }
66
+ const digest = DIGEST_PATTERN.exec(message)
67
+ if (digest !== null) {
68
+ return new StoreOperationError(message, { digestKey: digest[1] })
69
+ }
70
+ return new StoreOperationError(message)
71
+ }
72
+
73
+ /**
74
+ * What to print when the runtime, not the engine, ran out of call stack.
75
+ *
76
+ * One copy of this text, used by every command that can hit the frame
77
+ * budget. `remedy` is the one line that differs: a query can be made
78
+ * smaller with a LIMIT, a pack cannot.
79
+ *
80
+ * The pack path normally never reaches this, because it runs on a worker
81
+ * thread with a raised stack (`bin/pack-host.mjs`,
82
+ * https://github.com/danbri/factoidal/issues/649). It is what a reader
83
+ * sees when the worker route is refused with --no-worker, is unavailable
84
+ * on their platform, or is not enough.
85
+ *
86
+ * @param {string} remedy
87
+ * @returns {string[]} the lines, in order, for stderr
88
+ */
89
+ export function stackLimitAdvice (remedy) {
90
+ return [
91
+ 'The runtime ran out of call stack inside the engine, not the store.',
92
+ 'Some engine paths recurse once per row or per input chunk, and enough',
93
+ "of them exceed the runtime's default WebAssembly frame budget.",
94
+ remedy
95
+ ]
96
+ }
97
+
98
+ /** The remedies for each command that can run out of frames. */
99
+ export const STACK_REMEDY = {
100
+ query: 'Raise it with node --stack-size=4000, add a LIMIT, or use Deno.',
101
+ pack: 'Raise it with node --stack-size=8000, or ' +
102
+ 'deno run --v8-flags=--stack-size=8000.'
103
+ }
104
+
105
+ /**
106
+ * Open a store and return its manifest bytes.
107
+ *
108
+ * With no `generationName` the activated generation is opened through
109
+ * CURRENT. With one, that generation is opened directly, which is how a
110
+ * generation that has not been activated is inspected.
111
+ *
112
+ * @param {string} root the collection root that holds CURRENT
113
+ * @param {string|null} generationName
114
+ * @returns {{root: string, generation: string, generationDir: string,
115
+ * manifestName: string, manifest: Uint8Array,
116
+ * manifestHex: string, activated: boolean}}
117
+ */
118
+ export function openStore (root, generationName = null) {
119
+ if (typeof generationName !== 'string') {
120
+ const opened = openCollection(root)
121
+ return { ...opened, manifestHex: hexOfBytes(opened.manifest), activated: true }
122
+ }
123
+ const generationDir = joinPath(root, generationName)
124
+ for (const manifestName of MANIFEST_NAMES) {
125
+ let manifest
126
+ try {
127
+ manifest = readWhole(joinPath(generationDir, manifestName))
128
+ } catch (_error) {
129
+ continue
130
+ }
131
+ return {
132
+ root,
133
+ generation: generationName,
134
+ generationDir,
135
+ manifestName,
136
+ manifest,
137
+ manifestHex: hexOfBytes(manifest),
138
+ activated: false
139
+ }
140
+ }
141
+ throw new StoreOperationError(
142
+ `${generationDir} has none of ${MANIFEST_NAMES.join(', ')}`)
143
+ }
144
+
145
+ /** `storeManifestInspect` — decode one manifest. */
146
+ export function inspectManifest (engine, store) {
147
+ try {
148
+ return engine.call('storeManifestInspect', [store.manifestHex])
149
+ } catch (error) {
150
+ throw asStoreError(error)
151
+ }
152
+ }
153
+
154
+ /** `storeQueryPlan` — the artifact keys this query needs, and the mode. */
155
+ export function planQuery (engine, store, sparql) {
156
+ try {
157
+ return engine.call('storeQueryPlan', [store.manifestHex, sparql])
158
+ } catch (error) {
159
+ throw asStoreError(error)
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Ask the engine to decide the caps before any file is read.
165
+ *
166
+ * `storeQuery` checks its three caps against the manifest's own
167
+ * declarations BEFORE it looks at the artifact descriptors, so a call
168
+ * carrying an empty descriptor list gets the cap decision without moving
169
+ * a byte. The caps themselves stay where they are defined
170
+ * (`Wasm/Ops/Store.lean`); this host holds none of their values.
171
+ *
172
+ * @returns the envelope when the plan needs no artifact at all, else null
173
+ * @throws {StoreOperationError} when a cap refused the plan
174
+ */
175
+ function capDecision (engine, store, sparql) {
176
+ try {
177
+ return engine.callBlob('storeQuery',
178
+ [store.manifestHex, sparql, '[]'], new Uint8Array(0))
179
+ } catch (error) {
180
+ const refusal = asStoreError(error)
181
+ if (refusal.message.indexOf('no bytes were supplied for artifact') >= 0) {
182
+ return null
183
+ }
184
+ throw refusal
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Evaluate one SPARQL query against one generation.
190
+ *
191
+ * The sequence is: plan, cap decision, read exactly the artifacts the
192
+ * plan named, concatenate them into one buffer, and call `storeQuery`
193
+ * with a `{"key","offset","len"}` window per artifact. The buffer is
194
+ * written straight into the wasm heap by `engine.callBlob` with no
195
+ * encoding, and the engine bounds-checks every window.
196
+ *
197
+ * @returns {{plan: object, result: object, blobBytes: number,
198
+ * artifacts: {key: string, offset: number, len: number}[]}}
199
+ */
200
+ export function queryStore (engine, store, sparql) {
201
+ const plan = planQuery(engine, store, sparql)
202
+ const empty = capDecision(engine, store, sparql)
203
+ if (empty !== null) {
204
+ return { plan, result: empty, blobBytes: 0, artifacts: [] }
205
+ }
206
+ const chunks = plan.keys.map((key) => readWhole(joinPath(store.generationDir, key)))
207
+ let total = 0
208
+ for (const chunk of chunks) total += chunk.length
209
+ const blob = new Uint8Array(total)
210
+ const artifacts = []
211
+ let offset = 0
212
+ for (let index = 0; index < chunks.length; index += 1) {
213
+ blob.set(chunks[index], offset)
214
+ artifacts.push({ key: plan.keys[index], offset, len: chunks[index].length })
215
+ offset += chunks[index].length
216
+ }
217
+ let result
218
+ try {
219
+ result = engine.callBlob('storeQuery',
220
+ [store.manifestHex, sparql, JSON.stringify(artifacts)], blob)
221
+ } catch (error) {
222
+ throw asStoreError(error)
223
+ }
224
+ return { plan, result, blobBytes: total, artifacts }
225
+ }
226
+
227
+ /**
228
+ * `serializeTurtle` — the engine's own Turtle writer, over the N-Quads a
229
+ * CONSTRUCT answered. Named graphs are flattened into the default graph
230
+ * on this path; `--format nquads` is the fidelity-preserving one.
231
+ */
232
+ export function turtleOfNQuads (engine, nquads) {
233
+ try {
234
+ return engine.call('serializeTurtle', [nquads]).turtle
235
+ } catch (error) {
236
+ throw asStoreError(error)
237
+ }
238
+ }
@@ -37,7 +37,7 @@
37
37
  // bytes change.
38
38
 
39
39
  // Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
40
- const WASM_VERSION = "91fb323ec932";
40
+ const WASM_VERSION = "125d391e0ccc";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
@@ -72,8 +72,6 @@ export function loadL4() {
72
72
  const Module = await createModule(moduleArg);
73
73
 
74
74
  const cVersion = Module.cwrap('l4_version_c', 'number', []);
75
- const cBgpQuery = Module.cwrap('l4_bgp_query_c', 'number', ['string', 'string']);
76
- const cCall = Module.cwrap('l4_call_c', 'number', ['string', 'string']);
77
75
  const cFree = Module.cwrap('l4_free_result', null, ['number']);
78
76
  const cInit = Module.cwrap('l4_init', 'number', []);
79
77
 
@@ -89,6 +87,27 @@ export function loadL4() {
89
87
 
90
88
  const asJson = (v) => (typeof v === 'string' ? v : JSON.stringify(v));
91
89
 
90
+ // Emscripten's cwrap `string` converter uses the WebAssembly stack.
91
+ // Multi-megabyte RDF/block requests can therefore overflow STACK_SIZE
92
+ // before Lean sees them. Allocate input UTF-8 on the wasm heap instead;
93
+ // the C shim copies each input into a Lean String synchronously, so these
94
+ // buffers can be released as soon as the exported call returns.
95
+ const callWithHeapStrings = (fn, texts) => {
96
+ const pointers = [];
97
+ try {
98
+ for (const text of texts) {
99
+ const size = Module.lengthBytesUTF8(text) + 1;
100
+ const ptr = Module._malloc(size);
101
+ if (!ptr) throw new Error('l4factoidal: could not allocate a WASM input buffer');
102
+ Module.stringToUTF8(text, ptr, size);
103
+ pointers.push(ptr);
104
+ }
105
+ return fn(...pointers);
106
+ } finally {
107
+ for (let i = pointers.length - 1; i >= 0; i--) Module._free(pointers[i]);
108
+ }
109
+ };
110
+
92
111
  return {
93
112
  /** The Lean-side ABI version string. */
94
113
  version() { return take(cVersion()); },
@@ -105,7 +124,9 @@ export function loadL4() {
105
124
  * @throws if the Lean side reports a decoding error
106
125
  */
107
126
  bgpQuery(data, bgp) {
108
- const parsed = JSON.parse(take(cBgpQuery(asJson(data), asJson(bgp))));
127
+ const resultPtr = callWithHeapStrings(Module._l4_bgp_query_c,
128
+ [asJson(data), asJson(bgp)]);
129
+ const parsed = JSON.parse(take(resultPtr));
109
130
  if (parsed.error) throw new Error(`l4factoidal: ${parsed.error}`);
110
131
  return parsed;
111
132
  },
@@ -122,11 +143,110 @@ export function loadL4() {
122
143
  * @throws if the Lean side reports {"ok":false,"error":...}
123
144
  */
124
145
  call(op, args) {
125
- const parsed = JSON.parse(take(cCall(op, JSON.stringify(args))));
146
+ const resultPtr = callWithHeapStrings(Module._l4_call_c,
147
+ [op, JSON.stringify(args)]);
148
+ const parsed = JSON.parse(take(resultPtr));
126
149
  if (parsed.ok === false) throw new Error(`l4factoidal: ${parsed.error}`);
127
150
  return parsed;
128
151
  },
129
152
 
153
+ /**
154
+ * The dispatch ABI, plus ONE contiguous byte region.
155
+ *
156
+ * For ops whose input is block bytes rather than text
157
+ * (`storeQuery`; the `ops` reflection lists them under
158
+ * `blobOps`). The bytes are written straight into the wasm heap
159
+ * with no encoding — no hex, no base64 — and copied once into a
160
+ * Lean ByteArray on the Lean side. Which bytes belong to which
161
+ * artifact is said in `args`, as {"key","offset","len"} windows
162
+ * into the region; Lean bounds-checks every one of them, so this
163
+ * call cannot pass a stale or out-of-range pointer.
164
+ *
165
+ * @param op the method name, e.g. "storeQuery"
166
+ * @param args array of positional STRING arguments
167
+ * @param blob Uint8Array (or ArrayBuffer) of the concatenated bytes
168
+ * @returns the parsed {"ok":true,...} envelope
169
+ * @throws if the Lean side reports {"ok":false,"error":...}
170
+ */
171
+ callBlob(op, args, blob) {
172
+ const bytes = blob instanceof Uint8Array ? blob : new Uint8Array(blob ?? 0);
173
+ const blobPtr = bytes.length > 0 ? Module._malloc(bytes.length) : 0;
174
+ if (bytes.length > 0 && !blobPtr) {
175
+ throw new Error('l4factoidal: could not allocate a WASM blob buffer');
176
+ }
177
+ try {
178
+ if (bytes.length > 0) Module.HEAPU8.set(bytes, blobPtr);
179
+ const resultPtr = callWithHeapStrings(
180
+ (opPtr, argsPtr) => Module._l4_call_blob_c(opPtr, argsPtr, blobPtr, bytes.length),
181
+ [op, JSON.stringify(args)]);
182
+ const parsed = JSON.parse(take(resultPtr));
183
+ if (parsed.ok === false) throw new Error(`l4factoidal: ${parsed.error}`);
184
+ return parsed;
185
+ } finally {
186
+ if (blobPtr) Module._free(blobPtr);
187
+ }
188
+ },
189
+
190
+ /**
191
+ * The dispatch ABI, plus ONE byte region IN and ONE byte region
192
+ * OUT.
193
+ *
194
+ * For the ops of `L4Wasm.blobIoOpNames` (the `ops` envelope lists
195
+ * them under `blobIoOps`), whose RESULT is bytes rather than
196
+ * text. The bytes leave the module raw — no hex, no base64 — and
197
+ * are copied out of the wasm heap into a fresh Uint8Array before
198
+ * the module's buffer is released. The copy is required: the heap
199
+ * is detached and replaced when the module grows, so a subarray
200
+ * view of it can go stale between calls.
201
+ *
202
+ * Every other op answers as `call` does, with an empty region.
203
+ *
204
+ * @param op the method name, e.g. "blobEcho"
205
+ * @param args array of positional STRING arguments
206
+ * @param blobIn Uint8Array (or ArrayBuffer) carried IN; may be omitted
207
+ * @returns { envelope, bytes } — the parsed {"ok":true,...}
208
+ * envelope and a Uint8Array of the out region
209
+ * @throws if the Lean side reports {"ok":false,"error":...}
210
+ */
211
+ callBlobIO(op, args, blobIn) {
212
+ const bytes = blobIn instanceof Uint8Array
213
+ ? blobIn
214
+ : new Uint8Array(blobIn ?? 0);
215
+ // Two 32-bit out parameters, uint8_t **out_ptr and size_t
216
+ // *out_len, in one 8-byte cell.
217
+ const outCell = Module._malloc(8);
218
+ if (!outCell) throw new Error('l4factoidal: could not allocate the out-parameter cell');
219
+ const blobPtr = bytes.length > 0 ? Module._malloc(bytes.length) : 0;
220
+ if (bytes.length > 0 && !blobPtr) {
221
+ Module._free(outCell);
222
+ throw new Error('l4factoidal: could not allocate a WASM blob buffer');
223
+ }
224
+ let outPtr = 0;
225
+ try {
226
+ Module.setValue(outCell, 0, 'i32');
227
+ Module.setValue(outCell + 4, 0, 'i32');
228
+ if (bytes.length > 0) Module.HEAPU8.set(bytes, blobPtr);
229
+ const resultPtr = callWithHeapStrings(
230
+ (opPtr, argsPtr) => Module._l4_call_blob_io_c(
231
+ opPtr, argsPtr, blobPtr, bytes.length, outCell, outCell + 4),
232
+ [op, JSON.stringify(args)]);
233
+ outPtr = Module.getValue(outCell, 'i32') >>> 0;
234
+ const outLen = Module.getValue(outCell + 4, 'i32') >>> 0;
235
+ const envelope = JSON.parse(take(resultPtr));
236
+ if (envelope.ok === false) throw new Error(`l4factoidal: ${envelope.error}`);
237
+ // slice() copies; HEAPU8 is replaced wholesale when the
238
+ // module's memory grows, so a view would not survive.
239
+ const region = outPtr !== 0 && outLen > 0
240
+ ? Module.HEAPU8.slice(outPtr, outPtr + outLen)
241
+ : new Uint8Array(0);
242
+ return { envelope, bytes: region };
243
+ } finally {
244
+ if (outPtr) Module._l4_free_blob(outPtr);
245
+ if (blobPtr) Module._free(blobPtr);
246
+ Module._free(outCell);
247
+ }
248
+ },
249
+
130
250
  /** Escape hatch for tests: the raw Emscripten module. */
131
251
  _module: Module,
132
252
  };