@factoidal/core 0.19.0 → 0.20.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 +16 -0
- package/README.md +21 -13
- package/browser.d.ts +13 -0
- package/browser.js +14 -0
- package/index.d.ts +220 -20
- package/index.mjs +14 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +251 -23
- package/lib/dataset.mjs +96 -12
- package/lib/lean-entry.mjs +30 -12
- package/lib/nodes.mjs +92 -14
- package/lib/sbm11-lws.mjs +4 -2
- package/lib/sbm11.mjs +16 -6
- package/lib/version.mjs +1 -1
- package/package.json +1 -1
package/lib/nodes.mjs
CHANGED
|
@@ -49,7 +49,7 @@ import { refusalFor, makeRecord, summarize, unionAssumptions, unionLists } from
|
|
|
49
49
|
*/
|
|
50
50
|
export function buildNodes(deps) {
|
|
51
51
|
const { entry, entryResult, requireEntryFn, Dataset,
|
|
52
|
-
parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf } = deps;
|
|
52
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf } = deps;
|
|
53
53
|
|
|
54
54
|
const reader = (format) => async (wire, params) => {
|
|
55
55
|
if (typeof wire.graph !== 'string') {
|
|
@@ -62,13 +62,18 @@ export function buildNodes(deps) {
|
|
|
62
62
|
graph: await closure(wire.graph, regime, params),
|
|
63
63
|
artefact: { regime },
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// A writer's output wire is String: the text goes on the wire, so the next
|
|
66
|
+
// node (a reader, say) reads it. Before 0.20.0 the input Dataset stayed on
|
|
67
|
+
// the wire and only the artefact held the text.
|
|
68
|
+
const writer = (format) => async (wire, params) => {
|
|
69
|
+
const text = await serialize(wire.graph, { ...params, format });
|
|
70
|
+
return { graph: text, artefact: { format, text } };
|
|
71
|
+
};
|
|
68
72
|
|
|
69
73
|
// Stage name (stageList `name`) -> the package function that runs it.
|
|
70
|
-
// A runner answers `{ graph
|
|
71
|
-
//
|
|
74
|
+
// A runner answers `{ graph, artefact }`: `graph` is the value of the
|
|
75
|
+
// node's output wire (FactoidalNode.wireOut, the engine's), which run()
|
|
76
|
+
// checks (wireHolds); `artefact` goes in the evidence record.
|
|
72
77
|
const RUNNERS = {
|
|
73
78
|
ntriplesDocumentReader: reader('ntriples'),
|
|
74
79
|
nquadsDocumentReader: reader('nquads'),
|
|
@@ -83,23 +88,35 @@ export function buildNodes(deps) {
|
|
|
83
88
|
const { shapes, ...opts } = params;
|
|
84
89
|
if (shapes === undefined) throw new TypeError('node shaclValidator: params.shapes is required');
|
|
85
90
|
const r = await shaclValidate(wire.graph, shapes, opts);
|
|
86
|
-
|
|
91
|
+
const report = { conforms: r.conforms, report: r.report };
|
|
92
|
+
return { graph: report, artefact: report };
|
|
87
93
|
},
|
|
94
|
+
// SELECT gives a Table (the rows), ASK an Answer (a boolean), CONSTRUCT
|
|
95
|
+
// and DESCRIBE a Dataset: the engine picks the wire from the query form
|
|
96
|
+
// (op pipelineCheckNodes), and the value here follows it.
|
|
88
97
|
shippedEvaluator: async (wire, params) => {
|
|
89
98
|
const { query: sparql, ...opts } = params;
|
|
90
99
|
if (typeof sparql !== 'string') throw new TypeError('node shippedEvaluator: params.query must be a string');
|
|
91
100
|
const answer = await query(wire.graph, sparql, opts);
|
|
92
101
|
return answer instanceof Dataset ? { graph: answer, artefact: { form: 'graph' } }
|
|
93
|
-
: { artefact: { answer } };
|
|
102
|
+
: { graph: answer, artefact: { answer } };
|
|
94
103
|
},
|
|
95
104
|
ntriplesDocumentWriter: writer('ntriples'),
|
|
96
105
|
nquadsDocumentWriter: writer('nquads'),
|
|
97
106
|
turtleDocumentWriter: writer('turtle'),
|
|
98
107
|
trigDocumentWriter: writer('trig'),
|
|
99
108
|
rdfxmlDocumentWriter: writer('rdfxml'),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
// String × String: the canonical N-Quads text and its SHA-256 hex
|
|
110
|
+
// (RdfcContract, Dataflow/PhaseE.lean). canonicalHash is the SHA-256 of
|
|
111
|
+
// the text canonicalize answers (op datasetCanonicalHash).
|
|
112
|
+
rdfcStage: async (wire, params) => {
|
|
113
|
+
if (!(wire.graph instanceof Dataset)) {
|
|
114
|
+
throw new TypeError(`node rdfcStage: the stage takes a Dataset on its wire (got ${describe(wire.graph)})`);
|
|
115
|
+
}
|
|
116
|
+
const canonical = await canonicalize(wire.graph, params);
|
|
117
|
+
const sha256 = await canonicalHash(wire.graph);
|
|
118
|
+
return { graph: [canonical, sha256], artefact: { canonical, sha256 } };
|
|
119
|
+
},
|
|
103
120
|
'CSVW (tabular data to RDF)': async (wire, params) => {
|
|
104
121
|
if (typeof wire.graph !== 'string') {
|
|
105
122
|
throw new TypeError(`node CSVW: the stage takes CSV text on its wire (got ${typeof wire.graph})`);
|
|
@@ -113,10 +130,30 @@ export function buildNodes(deps) {
|
|
|
113
130
|
if (description === undefined) {
|
|
114
131
|
throw new TypeError('node rdfsExplainGraphStage: params.description is required');
|
|
115
132
|
}
|
|
116
|
-
|
|
133
|
+
const result = await explain(wire.graph, description, { ...opts, profile: 'rdfs' });
|
|
134
|
+
return { graph: result, artefact: result };
|
|
117
135
|
},
|
|
118
136
|
};
|
|
119
137
|
|
|
138
|
+
// Whether `value` represents the wire type `wire` (a wire name of the
|
|
139
|
+
// engine's registry, Dataflow/Wire.lean) in this package: a Dataset for the
|
|
140
|
+
// graph wires, text for String, and so on. A wire this table does not name
|
|
141
|
+
// needs a value that is not null or undefined.
|
|
142
|
+
function wireHolds(wire, value) {
|
|
143
|
+
switch (wire) {
|
|
144
|
+
case 'String': return typeof value === 'string';
|
|
145
|
+
case 'Dataset': case 'Graph': case 'Dataset × PrefixList': return value instanceof Dataset;
|
|
146
|
+
case 'Answer': return typeof value === 'boolean';
|
|
147
|
+
case 'Table': return Array.isArray(value);
|
|
148
|
+
case 'String × String':
|
|
149
|
+
return Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === 'string');
|
|
150
|
+
default: return value !== undefined && value !== null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const describe = (v) => (v instanceof Dataset ? 'a Dataset' : Array.isArray(v) ? 'an array'
|
|
155
|
+
: v === null ? 'null' : typeof v);
|
|
156
|
+
|
|
120
157
|
let stageCache = null;
|
|
121
158
|
let engine = null; // the entry stages() loaded; node() calls stages(), so pipe() has it
|
|
122
159
|
|
|
@@ -167,6 +204,33 @@ export function buildNodes(deps) {
|
|
|
167
204
|
throw new TypeError(`node: setting ${path} must be a JSON value or a Dataset (got ${typeof v})`);
|
|
168
205
|
}
|
|
169
206
|
|
|
207
|
+
// The node's own copy of its settings, taken when node() checks them: plain
|
|
208
|
+
// objects and arrays copied and frozen at every depth, a Dataset cloned. A
|
|
209
|
+
// later change to the caller's objects does not reach the node, so the
|
|
210
|
+
// settings that run are the ones the engine checked and digested.
|
|
211
|
+
function snapshot(v) {
|
|
212
|
+
if (v instanceof Dataset) return typeof v.clone === 'function' ? v.clone() : v;
|
|
213
|
+
if (Array.isArray(v)) return Object.freeze(v.map(snapshot));
|
|
214
|
+
if (v !== null && typeof v === 'object') {
|
|
215
|
+
const out = {};
|
|
216
|
+
for (const [k, x] of Object.entries(v)) if (x !== undefined) out[k] = snapshot(x);
|
|
217
|
+
return Object.freeze(out);
|
|
218
|
+
}
|
|
219
|
+
return v;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// A node's settings must still be the ones it was checked with: a Dataset
|
|
223
|
+
// in them (the clone node() made) can still be changed through
|
|
224
|
+
// node.params. run() refuses such a node instead of running settings that
|
|
225
|
+
// its evidence record would misdescribe.
|
|
226
|
+
function assertSettingsUnchanged(n) {
|
|
227
|
+
const now = JSON.stringify(marshal(n.params, 'params'));
|
|
228
|
+
if (now !== JSON.stringify(n.settingsWire)) {
|
|
229
|
+
throw new FactoidalError(`run: the settings of node ${n.stage} changed after node() checked them ` +
|
|
230
|
+
'(make a new node with node() for new settings)', { code: 'op.failed' });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
170
234
|
const hasNodeCheck = () => !!engine && typeof engine.pipelineCheckNodes === 'function';
|
|
171
235
|
|
|
172
236
|
// The engine's check of nodes with settings (op pipelineCheckNodes).
|
|
@@ -193,7 +257,10 @@ export function buildNodes(deps) {
|
|
|
193
257
|
throw new FactoidalError(`node: no registered stage named '${name}' (see stages())`,
|
|
194
258
|
{ code: 'option.invalid-value' });
|
|
195
259
|
}
|
|
196
|
-
|
|
260
|
+
// The copy first, then its wire form: the engine checks and digests
|
|
261
|
+
// exactly the settings the node keeps.
|
|
262
|
+
const own = snapshot({ ...(params || {}) });
|
|
263
|
+
const settingsWire = snapshot(marshal(own, 'params'));
|
|
197
264
|
// The engine's reading of the settings, when it has the op.
|
|
198
265
|
let s = null;
|
|
199
266
|
if (hasNodeCheck()) s = nodeCheck([{ stage: info.name, settingsWire }]).stages[0];
|
|
@@ -209,7 +276,7 @@ export function buildNodes(deps) {
|
|
|
209
276
|
wireOut: s ? s.wireOut : info.wireOut,
|
|
210
277
|
runnable: info.runnable,
|
|
211
278
|
bound: info.bound,
|
|
212
|
-
params:
|
|
279
|
+
params: own,
|
|
213
280
|
settingsWire,
|
|
214
281
|
settings: s ? s.settings : null,
|
|
215
282
|
settingsDigest: s ? s.settingsDigest : null,
|
|
@@ -300,6 +367,7 @@ export function buildNodes(deps) {
|
|
|
300
367
|
for (const n of nodes) {
|
|
301
368
|
const why = refusalFor(n, unchecked, Object.prototype.hasOwnProperty.call(RUNNERS, n.stage));
|
|
302
369
|
if (why) throw why;
|
|
370
|
+
assertSettingsUnchanged(n);
|
|
303
371
|
}
|
|
304
372
|
const c = check(nodes);
|
|
305
373
|
if (c && !c.wellTyped && !unchecked) {
|
|
@@ -314,6 +382,16 @@ export function buildNodes(deps) {
|
|
|
314
382
|
let owned = null; // a Dataset an earlier node of this run made
|
|
315
383
|
for (const n of nodes) {
|
|
316
384
|
const out = await RUNNERS[n.stage](wire, n.params);
|
|
385
|
+
// The value on the output wire must be one of the node's wireOut, the
|
|
386
|
+
// wire the engine's check typed: a well-typed pipeline then cannot fail
|
|
387
|
+
// between nodes for want of the value the next node reads. The output
|
|
388
|
+
// wires per setting are the engine's when it has op pipelineCheckNodes;
|
|
389
|
+
// with an older engine the registry wire is generic, so not checked.
|
|
390
|
+
const value = out.graph === undefined ? wire.graph : out.graph;
|
|
391
|
+
if (hasNodeCheck() && !wireHolds(n.wireOut, value)) {
|
|
392
|
+
throw new FactoidalError(`run: node ${n.stage} declares the output wire ${n.wireOut} ` +
|
|
393
|
+
`but its runner gave ${describe(value)}`, { code: 'op.failed' });
|
|
394
|
+
}
|
|
317
395
|
const record = makeRecord(n, out.artefact, unchecked);
|
|
318
396
|
let graph = wire.graph;
|
|
319
397
|
if (out.graph !== undefined && out.graph !== wire.graph) {
|
package/lib/sbm11-lws.mjs
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
// representation: it is kept in memory and every later read of that file is
|
|
18
18
|
// cut from it, with no further request. A changed entity tag between two
|
|
19
19
|
// reads of one file refuses the read (the bytes of two versions are never
|
|
20
|
-
// mixed).
|
|
21
|
-
//
|
|
20
|
+
// mixed). The engine checks every byte it decodes against the manifest's
|
|
21
|
+
// Merkle roots (openSbm11's default `verify: 'merkle'`), so it asks for whole
|
|
22
|
+
// 65,536-byte chunks and the `.merkle` sidecars; this file does no check.
|
|
23
|
+
// Limits: no eviction of the in-memory copies, no retry.
|
|
22
24
|
//
|
|
23
25
|
// EVIDENCE
|
|
24
26
|
// `evidence()` lists each resource read: { resource, etag, lastModified,
|
package/lib/sbm11.mjs
CHANGED
|
@@ -111,17 +111,24 @@ function envelope (raw) {
|
|
|
111
111
|
export function sbm11Api (loadEngine) {
|
|
112
112
|
/**
|
|
113
113
|
* Open an SBM11 generation (a generation directory, or a collection root
|
|
114
|
-
* whose CURRENT file names one). Returns { generation, terms,
|
|
115
|
-
* options), close() }. `query` answers { path, rows, format,
|
|
116
|
-
* `result` is the text `l4sbm11 query` writes (SPARQL JSON
|
|
117
|
-
* ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its
|
|
118
|
-
* SELECT and ASK.
|
|
114
|
+
* whose CURRENT file names one). Returns { generation, terms, verify,
|
|
115
|
+
* query(sparql, options), close() }. `query` answers { path, rows, format,
|
|
116
|
+
* result, json }: `result` is the text `l4sbm11 query` writes (SPARQL JSON
|
|
117
|
+
* for SELECT and ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its
|
|
118
|
+
* parse for SELECT and ASK.
|
|
119
|
+
*
|
|
120
|
+
* `options.verify`: 'merkle' (the default: the engine checks every byte
|
|
121
|
+
* it decodes against the manifest's Merkle roots, and refuses a changed
|
|
122
|
+
* file with its name) or 'none' (lengths only). The engine decides; this
|
|
123
|
+
* file passes the string.
|
|
119
124
|
*/
|
|
120
125
|
async function openSbm11 (source, options = {}) {
|
|
121
126
|
installBridge()
|
|
122
127
|
if (options.reader) registerSbm11Source(source, options.reader)
|
|
123
128
|
const eng = await loadEngine()
|
|
124
|
-
const
|
|
129
|
+
const openArgs = [String(source)]
|
|
130
|
+
if (options.verify != null) openArgs.push(JSON.stringify({ verify: String(options.verify) }))
|
|
131
|
+
const opened = envelope(eng.call('sbm11Open', openArgs))
|
|
125
132
|
const handle = opened.handle
|
|
126
133
|
let open = true
|
|
127
134
|
const reader = options.reader || readers.get(String(source))
|
|
@@ -135,6 +142,9 @@ export function sbm11Api (loadEngine) {
|
|
|
135
142
|
},
|
|
136
143
|
generation: opened.generation,
|
|
137
144
|
terms: Number(opened.terms),
|
|
145
|
+
/** how the engine checks reads: 'merkle' or 'none' (undefined from a
|
|
146
|
+
* wasm module built before the check) */
|
|
147
|
+
verify: opened.verify,
|
|
138
148
|
query (sparql, qopts = {}) {
|
|
139
149
|
if (!open) throw new Error('sbm11: the store is closed')
|
|
140
150
|
const o = {}
|
package/lib/version.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@factoidal/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "EXPERIMENTAL UNREVIEWED CODE: do not run public services on it without careful independent expert review. RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|