@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/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
- const writer = (format) => async (wire, params) => ({
66
- artefact: { format, text: await serialize(wire.graph, { ...params, format }) },
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?, artefact }`; no `graph` passes the input
71
- // graph on unchanged.
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
- return { artefact: { conforms: r.conforms, report: r.report } };
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
- rdfcStage: async (wire, params) => ({
101
- artefact: { canonical: await canonicalize(wire.graph, params) },
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
- return { artefact: await explain(wire.graph, description, { ...opts, profile: 'rdfs' }) };
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
- const settingsWire = marshal({ ...(params || {}) }, 'params');
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: Object.freeze({ ...(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). Limits: no Merkle check here (the engine's Verify.none path), no
21
- // eviction of the in-memory copies, no retry.
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, query(sparql,
115
- * options), close() }. `query` answers { path, rows, format, result, json }:
116
- * `result` is the text `l4sbm11 query` writes (SPARQL JSON for SELECT and
117
- * ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its parse for
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 opened = envelope(eng.call('sbm11Open', [String(source)]))
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
@@ -2,4 +2,4 @@
2
2
  // needs import attributes, which not every runtime this package targets
3
3
  // reads). test/dts-drift.test.js checks that it equals package.json's
4
4
  // `version`.
5
- export const version = '0.19.0';
5
+ export const version = '0.20.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.19.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",