@factoidal/core 0.15.0 → 0.16.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.16.0 (2026-10-26)
6
+
7
+ - Node settings in the evidence (https://github.com/danbri/factoidal/issues/975). The params of `node()` are the node's settings. With a wasm module that has the new op `pipelineCheckNodes` (Lean `Wasm/Ops/PipelineNodes.lean`, `Dataflow/NodeSettings.lean`), each evidence record carries `settings` (canonical: keys sorted, defaults of absent settings that narrow the contract such as `lenient: false` and `entail: 'none'`, a Dataset value as `{nquadsSha256}`) and `settingsDigest` (`sha256:` and the hex SHA-256 of the canonical text). A setting outside the proved domain of a proved stage (a lenient reader, an evaluator with `entail` other than `none` or with `output`, a key the stage's settings table does not name) makes the node's status `unproved`, with `registryStatus` and `outside` (each setting and the reason) in the record, so `run()` reports `proved: false`. A reader's `format` must name the stage's syntax (`ttl12` selects RDF 1.2 mode, reported as `derived.mode`); an evaluator's query must parse, else `node()` rejects. Design record: `docs/designissues/2026-10-25-pipeline-settings-and-provenance.md`.
8
+ - The output wire of a `shippedEvaluator` node follows its query form: SELECT gives `Table`, ASK `Answer`, CONSTRUCT and DESCRIBE `Dataset` (owner, 2026-10-25: "select give table? Sure"). So reader, CONSTRUCT, CONSTRUCT is well typed, and reader, SELECT, CONSTRUCT is not. Proved: every answer that meets the evaluator contract has the wire of its query form (`Dataflow.evalContract_wire`) (https://github.com/danbri/factoidal/issues/1026). The Lean wire enumeration has a new wire `table`.
9
+ - CSVW is a registered, bound stage: `node('CSVW (tabular data to RDF)', { metadata, mode, base, url })` runs `csvwToRdf` on CSV text and gives a Dataset and an evidence record with status `tested` (the W3C csv2rdf suite; no Lean contract, so `checked: false`) (https://github.com/danbri/factoidal/issues/975). `stages()` marks a stage `bound` only when the engine also reports it runnable.
10
+ - With the wasm of 0.15.0 or older (for example a mismatched local install), records have no settings, the evaluator's wire is the registry's `Answer`, and the CSVW stage stays `missing` and unbound.
11
+
12
+ - `packSbm11(text, { syntax, base })` packs an RDF document into the files of one SBM11 generation in memory and resolves to a `Map` from file name to bytes (data files, `.merkle` sidecars, `manifest.sbm11`; no `CURRENT`); `mapReader(files)` is a reader over such files for `openSbm11(source, { reader })`. The wasm op is `sbm11PackFiles` (Lean `Storage/Sbm11/PackBytes.lean`), one call per file. The files are byte for byte those of the native `l4sbm11-pack` for the same quads: 23 of 23 files for `test/fixtures/sbm11-tiny`, and all files of 605 of 605 W3C SPARQL 1.0 and 1.1 inputs (`SBM11_PACKFILES=1 tests/sbm11/census.sh`) (https://github.com/danbri/factoidal/issues/1009 stage 2). On the wasm of 0.15.0 or older, `packSbm11` rejects with `code: 'SBM11_NO_PACK_OP'`. The whole document and every file are in memory at once. Not yet: `factoidal pack` on SBM11, and a pack that streams its input.
13
+ - SPARQL: the inverse path `^p` keeps a pair whose object is a literal. SPARQL 1.1 §9.3 and §18.4 make `?x ^p ?y` the same as `?y p ?x`, so `?x` can bind a literal; up to 0.15.0 both evaluators (the in-memory one and the SBM11 id path) dropped those pairs, and `SELECT ?x ?y WHERE { ?x ^<http://e/p> ?y }` over `<http://e/s> <http://e/p> "v" .` gave no row. Apache Jena ARQ 5.6.0 gives the same rows as the fix (https://github.com/danbri/factoidal/issues/1023). Tests: `test/inverse-path-literal-1023.test.mjs` and the `#guard`s in `SPARQL/QueryTests.lean`. The F* tree has the same fix.
14
+ - SPARQL: a `FROM` or `FROM NAMED` IRI that names no graph of the dataset gives a warning of class `dataset-iri-not-found` in the `warnings` member of the query envelope (`class`, `clause` (`FROM` or `FROM NAMED`), `iri`, `message`), and on standard error in the native `l4factoidal query` command. The rows do not change: the clause adds no graph, as before (https://github.com/danbri/factoidal/issues/1032). Lean: `SPARQL.datasetWarnings` beside `applyDataset`, whose merge theorem is unchanged; `#guard`s in `SPARQL/QueryTests.lean` and `Wasm/Ops/Query.lean`. Tests: `test/from-iri-not-found.test.mjs` (plain assertions; https://github.com/danbri/factoidal/issues/1038 records why an expected failure that waits for a rebuild blocked the build of this release).
15
+ - Tests, no engine change: GeoSPARQL `geo:wktLiteral` values made with `STRDT(CONCAT("POINT(", STR(?lon), " ", STR(?lat), ")"), geo:wktLiteral)` in SELECT, in a sub-SELECT that feeds `geof:sfWithin`, and by an INSERT whose stored literal `geof:sfWithin` then tests; from a CSVW column whose datatype object is `{"@id": "http://www.opengis.net/ont/geosparql#wktLiteral", "base": "string"}`; and from an RML object map with `rml:datatype` or a `rml:datatypeMap` with `rml:constant`. `test/wkt-generation.test.mjs` (6 pass on the 0.15.0 wasm) and `#guard`s in `Wasm/Ops/Query.lean` and `Wasm/Ops/Formats.lean`.
16
+
17
+ - Tests, no engine change: `test/sparql-string-arg-types.test.mjs` holds 6 expected failures for SPARQL 1.1 string functions that accept arguments the specification makes type errors, and for SUBSTR rounding (https://github.com/danbri/factoidal/issues/1036), found by porting Apache Jena ARQ expression tests (`docs/designissues/2026-10-25-jena-style-facade-feasibility.md`).
18
+
3
19
  ## 0.15.0
4
20
 
5
21
  - `openSbm11(source)` and `registerSbm11Source(source, reader)` are back, with the wasm ops `sbm11Open`, `sbm11Query`, `sbm11Close`: SPARQL over an SBM11 generation (the Shardborough store) in Node and in the browser, reading each file at an offset through a host reader (https://github.com/danbri/factoidal/issues/1009, https://github.com/danbri/factoidal-core/pull/93). Fix: in 0.14.0 `sbm11Open` trapped (wasm `unreachable`) because the C host-read functions had a signature that differs from the generated one; the wasm build now fails on any such mismatch. Not yet: the Merkle check of each read, a generation with a delta log (refused at open), packing SBM11 from npm.
package/browser.d.ts CHANGED
@@ -41,6 +41,8 @@ export {
41
41
  queryStore,
42
42
  listStores,
43
43
  openSbm11,
44
+ packSbm11,
45
+ mapReader,
44
46
  registerSbm11Source,
45
47
  lwsSbm11Reader,
46
48
  deltaLogOpen,
package/browser.js CHANGED
@@ -26,7 +26,7 @@ import { buildApi } from './lib/api.mjs';
26
26
  import { entryFromEngine } from './lib/lean-entry.mjs';
27
27
  import { browserEngine } from './lib/browser-engine.mjs';
28
28
  import { indexedDbDeltaLogStore } from './lib/delta-log-hosts.mjs';
29
- import { sbm11Api, registerSbm11Source } from './lib/sbm11.mjs';
29
+ import { sbm11Api, registerSbm11Source, mapReader } from './lib/sbm11.mjs';
30
30
  import { lwsSbm11Reader } from './lib/sbm11-lws.mjs';
31
31
 
32
32
  const l4Engine = browserEngine('l4factoidal', new URL('./l4-assets/', import.meta.url).href);
@@ -65,8 +65,12 @@ const api = buildApi({
65
65
  // (lib/sbm11.mjs; https://github.com/danbri/factoidal/issues/1009). In a
66
66
  // browser every source needs a registered synchronous reader, for example
67
67
  // over OPFS FileSystemSyncAccessHandle in a dedicated worker.
68
- export const openSbm11 = sbm11Api(loadEngine).openSbm11;
69
- export { registerSbm11Source };
68
+ const sbm11 = sbm11Api(loadEngine);
69
+ export const openSbm11 = sbm11.openSbm11;
70
+ // Pack a document into the files of an SBM11 generation in memory, and a
71
+ // reader over such files (issue 1009 stage 2).
72
+ export const packSbm11 = sbm11.packSbm11;
73
+ export { registerSbm11Source, mapReader };
70
74
  // A reader of an SBM11 generation held as LWS resources (issue 1010 KR3). In
71
75
  // a browser worker its default transport is a synchronous XMLHttpRequest.
72
76
  export { lwsSbm11Reader };
package/index.d.ts CHANGED
@@ -701,14 +701,31 @@ export interface StageInfo {
701
701
  note: string;
702
702
  /** false exactly when status is 'missing': such a stage does not run. */
703
703
  runnable: boolean;
704
- /** true when this package has a function that runs the stage as a node. */
704
+ /**
705
+ * true when this package has a function that runs the stage as a node AND
706
+ * the engine reports the stage runnable (an older engine that reports a
707
+ * stage 'missing' leaves it unbound).
708
+ */
705
709
  bound: boolean;
706
710
  }
707
711
 
712
+ /**
713
+ * The status of a node: the registry status, or 'unproved' when the stage is
714
+ * 'proved' and one of the node's settings is outside its proved domain (op
715
+ * pipelineCheckNodes, Lean Dataflow/NodeSettings.lean).
716
+ */
717
+ export type NodeStatus = StageInfo['status'] | 'unproved';
718
+
719
+ /** A setting outside the proved domain of its stage, with the reason. */
720
+ export interface OutsideSetting {
721
+ readonly setting: string;
722
+ readonly reason: string;
723
+ }
724
+
708
725
  /** One evidence record: the node that ran and what it produced. */
709
726
  export interface EvidenceRecord {
710
727
  stage: string;
711
- status: StageInfo['status'];
728
+ status: NodeStatus;
712
729
  contract: string | null;
713
730
  assumptions: string[];
714
731
  /** true iff `status` is 'proved'. */
@@ -719,6 +736,19 @@ export interface EvidenceRecord {
719
736
  reason?: string;
720
737
  /** The node's own output beside the graph: the explain result, the SHACL report, the written text, ... */
721
738
  artefact: unknown;
739
+ /**
740
+ * The node's settings in canonical form (keys sorted at every depth; the
741
+ * defaults of absent settings that narrow the contract, e.g. `lenient:
742
+ * false`, `entail: 'none'`; a Dataset value as `{nquadsSha256}`). Present
743
+ * when the engine has the op pipelineCheckNodes.
744
+ */
745
+ settings?: Record<string, unknown>;
746
+ /** `sha256:` and the hex SHA-256 of the canonical settings text. */
747
+ settingsDigest?: string;
748
+ /** With status 'unproved': the registry status ('proved'). */
749
+ registryStatus?: StageInfo['status'];
750
+ /** With status 'unproved': the settings outside the proved domain. */
751
+ outside?: OutsideSetting[];
722
752
  }
723
753
 
724
754
  /** The value on a wire. */
@@ -731,14 +761,31 @@ export interface Wire<G = unknown> {
731
761
  export interface FactoidalNode {
732
762
  readonly stage: string;
733
763
  readonly kind: string | null;
734
- readonly status: StageInfo['status'];
764
+ /** The engine's status of this node with its settings (see NodeStatus). */
765
+ readonly status: NodeStatus;
766
+ /** The registry status of the stage. */
767
+ readonly registryStatus: StageInfo['status'];
735
768
  readonly contract: string | null;
736
769
  readonly assumptions: readonly string[];
737
770
  readonly wireIn: string;
771
+ /**
772
+ * The output wire with these settings: for shippedEvaluator, 'Table' for
773
+ * SELECT, 'Answer' for ASK, 'Dataset' for CONSTRUCT and DESCRIBE (Lean
774
+ * formWire, evalContract_wire). With an older engine, the registry wire.
775
+ */
738
776
  readonly wireOut: string;
739
777
  readonly runnable: boolean;
740
778
  readonly bound: boolean;
779
+ /** The settings as given (passed to the package function that runs the stage). */
741
780
  readonly params: Readonly<Record<string, unknown>>;
781
+ /** The settings as sent to the engine: JSON values, a Dataset as {$nquads}. */
782
+ readonly settingsWire: Readonly<Record<string, unknown>>;
783
+ /** The engine's canonical settings, or null with an older engine. */
784
+ readonly settings: Readonly<Record<string, unknown>> | null;
785
+ readonly settingsDigest: string | null;
786
+ readonly outside: readonly OutsideSetting[];
787
+ /** Values the engine read from the settings: `form` of a query, `mode` ('1.1' | '1.2') of a reader. */
788
+ readonly derived: Readonly<Record<string, string>> | null;
742
789
  }
743
790
 
744
791
  /** The engine's check of a list of stages (op pipelineCheck, Lean pipelineReport). */
@@ -755,9 +802,17 @@ export interface PipelineCheck {
755
802
  readonly assumptions: readonly string[];
756
803
  readonly contract: { readonly theorem: string; readonly stages: readonly (string | null)[] };
757
804
  readonly stages: readonly {
758
- readonly name: string; readonly status: StageInfo['status']; readonly checked: boolean;
805
+ readonly name: string; readonly status: NodeStatus; readonly checked: boolean;
759
806
  readonly contract: string | null; readonly assumptions: readonly string[];
760
807
  readonly wireIn: string; readonly wireOut: string;
808
+ /** The fields below come from the op pipelineCheckNodes only. */
809
+ readonly registryStatus?: StageInfo['status'];
810
+ readonly settings?: Record<string, unknown>;
811
+ readonly settingsDigest?: string;
812
+ /** The role of each setting: 'parameter' | 'selects-wire' | 'inert' | 'narrows' | 'unlisted'. */
813
+ readonly roles?: Readonly<Record<string, string>>;
814
+ readonly outside?: readonly OutsideSetting[];
815
+ readonly derived?: Readonly<Record<string, string>>;
761
816
  }[];
762
817
  }
763
818
 
@@ -1136,6 +1191,18 @@ export function openSbm11(source: string, options?: { reader?: Sbm11Reader }): P
1136
1191
  /** Serve `source` with `reader`; `null` removes it. */
1137
1192
  export function registerSbm11Source(source: string, reader: Sbm11Reader | null): void;
1138
1193
 
1194
+ /** Pack an RDF document into the files of one SBM11 generation, in memory
1195
+ * (wasm op `sbm11PackFiles`, https://github.com/danbri/factoidal/issues/1009
1196
+ * stage 2): file name to bytes, the files `l4sbm11-pack` writes for the same
1197
+ * quads, without `CURRENT`. Rejects with `code: 'SBM11_NO_PACK_OP'` on a wasm
1198
+ * module that has no such op. */
1199
+ export function packSbm11(text: string | Uint8Array,
1200
+ options: { syntax: 'turtle' | 'trig' | 'nquads' | 'ntriples' | 'rdfxml' | 'jsonld' | string; base?: string }
1201
+ ): Promise<Map<string, Uint8Array>>;
1202
+
1203
+ /** A reader over files held in memory (for example the result of `packSbm11`). */
1204
+ export function mapReader(files: Map<string, Uint8Array> | Record<string, Uint8Array>): Sbm11Reader;
1205
+
1139
1206
  /** One LWS resource an SBM11 reader read (https://github.com/danbri/factoidal/issues/1010). */
1140
1207
  export interface Sbm11ReadEvidence {
1141
1208
  resource: string;
@@ -1570,6 +1637,8 @@ export function capabilities(): Promise<{
1570
1637
  stages: boolean;
1571
1638
  /** The op pipelineCheck: pipe() and run() use the engine's type check and assumption union. */
1572
1639
  pipelineCheck: boolean;
1640
+ /** The op pipelineCheckNodes: node settings in evidence, and the evaluator's output wire from the query form. */
1641
+ pipelineCheckNodes: boolean;
1573
1642
  tableau: boolean;
1574
1643
  rml: boolean;
1575
1644
  csvw: boolean;
package/index.mjs CHANGED
@@ -21,7 +21,7 @@ import { readFileSync } from 'node:fs';
21
21
  import { join } from 'node:path';
22
22
  import { buildApi } from './lib/api.mjs';
23
23
  import { entryFromEngine } from './lib/lean-entry.mjs';
24
- import { sbm11Api, registerSbm11Source, setDefaultSbm11Reader } from './lib/sbm11.mjs';
24
+ import { sbm11Api, registerSbm11Source, setDefaultSbm11Reader, mapReader } from './lib/sbm11.mjs';
25
25
  import { nodeDefaultReaderFactory } from './lib/sbm11-node.mjs';
26
26
  import { lwsSbm11Reader as lwsSbm11ReaderWith } from './lib/sbm11-lws.mjs';
27
27
  import { nodeSyncTransport } from './lib/sync-http-node.mjs';
@@ -74,8 +74,12 @@ export const clearServiceEndpoints = api.clearServiceEndpoints;
74
74
  // (lib/sbm11.mjs; https://github.com/danbri/factoidal/issues/1009). In Node a
75
75
  // source with no registered reader is a directory.
76
76
  setDefaultSbm11Reader(nodeDefaultReaderFactory());
77
- export const openSbm11 = sbm11Api(loadEngine).openSbm11;
78
- export { registerSbm11Source };
77
+ const sbm11 = sbm11Api(loadEngine);
78
+ export const openSbm11 = sbm11.openSbm11;
79
+ // Pack a document into the files of an SBM11 generation in memory, and a
80
+ // reader over such files (issue 1009 stage 2).
81
+ export const packSbm11 = sbm11.packSbm11;
82
+ export { registerSbm11Source, mapReader };
79
83
 
80
84
  // A reader of an SBM11 generation held as LWS resources (lib/sbm11-lws.mjs;
81
85
  // https://github.com/danbri/factoidal/issues/1010 KR3). In Node the default
@@ -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 = "06c67c273257";
40
+ const WASM_VERSION = "1b10444e4416";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
Binary file
@@ -2,17 +2,17 @@
2
2
  "engine": "lean4",
3
3
  "note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
4
4
  "stamped": true,
5
- "release": "danbri/factoidal-builds build-e5d9a26f3b19-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40198875683",
7
- "gitSha": "e5d9a26f3b1931ed70fc3f6a8df3fc78ff1699a4",
8
- "builtAt": "2026-10-25T14:18:06Z",
5
+ "release": "danbri/factoidal-builds build-0069da82efd7-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40281606727",
7
+ "gitSha": "0069da82efd75cc3bfe10785ef8853bd84931957",
8
+ "builtAt": "2026-10-26T08:11:18Z",
9
9
  "leanToolchain": "leanprover/lean4:v4.33.1",
10
10
  "emscripten": "6.0.8",
11
11
  "abiVersion": "1",
12
12
  "engineVersion": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
13
13
  "version": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
14
- "wasmSha256": "06c67c273257118abb3cebdbc249e27d20ba98e87b278b7291c1a9934faecdc4",
15
- "wasmBytes": 16918397,
14
+ "wasmSha256": "1b10444e44160b8689ebd5375c074509196930b1927ce4c9fbc829a73d9b58ac",
15
+ "wasmBytes": 17034490,
16
16
  "claims": {
17
17
  "source": "formal/lean4 (L4Factoidal): no sorry, no user axioms, no native_decide; W3C behaviour pinned by build-time #guard",
18
18
  "suitesAtBuildSha": "see docs/test-results and formal/lean4/PORT_NOTES.md at gitSha"
package/lib/api.mjs CHANGED
@@ -684,7 +684,15 @@ function buildApi(driver) {
684
684
  if (r.kind === 'construct' || r.kind === 'describe') {
685
685
  return c.fromNQuads(r.nquads, ds.prefixes);
686
686
  }
687
- return bindingsFromSrj(r.srj);
687
+ const rows = bindingsFromSrj(r.srj);
688
+ // The engine's warnings (class dataset-iri-not-found for a FROM or FROM
689
+ // NAMED IRI that names no graph, https://github.com/danbri/factoidal/issues/1032),
690
+ // passed through unchanged as a non-enumerable `warnings` property, so
691
+ // the rows themselves are unchanged.
692
+ if (Array.isArray(r.warnings) && r.warnings.length > 0) {
693
+ Object.defineProperty(rows, 'warnings', { value: r.warnings, enumerable: false });
694
+ }
695
+ return rows;
688
696
  }
689
697
 
690
698
  async function shaclOnHandle(c, ds, shapes, opts) {
@@ -2140,6 +2148,7 @@ function buildApi(driver) {
2140
2148
  explain: has('explainGraph'),
2141
2149
  stages: has('stageList'),
2142
2150
  pipelineCheck: has('pipelineCheck'),
2151
+ pipelineCheckNodes: has('pipelineCheckNodes'),
2143
2152
  entailment: has('queryRaw'),
2144
2153
  tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
2145
2154
  rml: has('rmlMap'),
@@ -2176,7 +2185,7 @@ function buildApi(driver) {
2176
2185
  // The node-composition API (lib/nodes.mjs, issue 1002 step 2).
2177
2186
  const nodeApi = buildNodes({
2178
2187
  entry, entryResult, requireEntryFn, Dataset,
2179
- parse, closure, shaclValidate, query, serialize, canonicalize, explain,
2188
+ parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf,
2180
2189
  });
2181
2190
 
2182
2191
  return {
package/lib/evidence.mjs CHANGED
@@ -16,6 +16,10 @@ export const isChecked = (status) => status === 'proved';
16
16
  /** The reason text of an unchecked step. */
17
17
  export function reasonOf(n) {
18
18
  if (n.status === 'missing') return 'no contract';
19
+ if (n.status === 'unproved' && n.outside && n.outside.length > 0) {
20
+ return `status unproved (registry status ${n.registryStatus || 'proved'}); settings outside ` +
21
+ `the proved domain: ${n.outside.map((o) => `${o.setting} (${o.reason})`).join('; ')}`;
22
+ }
19
23
  return n.assumptions.length > 0
20
24
  ? `status ${n.status}; assumptions: ${n.assumptions.join(', ')}`
21
25
  : `status ${n.status}`;
@@ -43,7 +47,10 @@ export function refusalFor(n, unchecked, hasRunner) {
43
47
  /**
44
48
  * One evidence record. `checked` is true iff the status is `proved`.
45
49
  * A step that ran under the opt-in and is not checked also carries
46
- * `unchecked: true` and `reason`.
50
+ * `unchecked: true` and `reason`. When the engine read the node's settings
51
+ * (op pipelineCheckNodes), the record also carries `settings` (canonical)
52
+ * and `settingsDigest`, and, when a setting left the proved domain,
53
+ * `registryStatus` and `outside`; its `status` is then `unproved`.
47
54
  */
48
55
  export function makeRecord(n, artefact, unchecked) {
49
56
  const checked = isChecked(n.status);
@@ -55,6 +62,14 @@ export function makeRecord(n, artefact, unchecked) {
55
62
  checked,
56
63
  artefact: artefact === undefined ? null : artefact,
57
64
  };
65
+ if (n.settingsDigest) {
66
+ base.settings = n.settings;
67
+ base.settingsDigest = n.settingsDigest;
68
+ }
69
+ if (n.outside && n.outside.length > 0) {
70
+ base.registryStatus = n.registryStatus;
71
+ base.outside = n.outside;
72
+ }
58
73
  if (unchecked && !checked) {
59
74
  base.unchecked = true;
60
75
  base.reason = reasonOf(n);
package/lib/nodes.mjs CHANGED
@@ -26,6 +26,18 @@
26
26
  // built before the op, pipe() and run() fall back to the union of the
27
27
  // nodes' fields and report `wellTyped: null`.
28
28
  //
29
+ // Node settings (https://github.com/danbri/factoidal/issues/975,
30
+ // https://github.com/danbri/factoidal/issues/1026): the params of node()
31
+ // are the node's settings. When the engine has the op pipelineCheckNodes
32
+ // (formal/lean4/Wasm/Ops/PipelineNodes.lean), the engine decides, per node:
33
+ // the settings in canonical form with their SHA-256 digest, the role of each
34
+ // setting in the stage's contract, the settings outside the proved domain
35
+ // (a proved stage with one is `unproved`), and the output wire the settings
36
+ // select (SELECT gives a Table, ASK an Answer, CONSTRUCT and DESCRIBE a
37
+ // Dataset, so a CONSTRUCT node can feed the next node). This file only marshals: a Dataset setting is sent as
38
+ // {"$nquads": its N-Quads text}. With an older engine, records carry no
39
+ // settings and the evaluator's output wire is the registry's.
40
+ //
29
41
  // ES module, no Node built-ins: the browser loads this file as it is.
30
42
 
31
43
  import { FactoidalError } from './errors.mjs';
@@ -37,7 +49,7 @@ import { refusalFor, makeRecord, summarize, unionAssumptions, unionLists } from
37
49
  */
38
50
  export function buildNodes(deps) {
39
51
  const { entry, entryResult, requireEntryFn, Dataset,
40
- parse, closure, shaclValidate, query, serialize, canonicalize, explain } = deps;
52
+ parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf } = deps;
41
53
 
42
54
  const reader = (format) => async (wire, params) => {
43
55
  if (typeof wire.graph !== 'string') {
@@ -88,6 +100,14 @@ export function buildNodes(deps) {
88
100
  rdfcStage: async (wire, params) => ({
89
101
  artefact: { canonical: await canonicalize(wire.graph, params) },
90
102
  }),
103
+ 'CSVW (tabular data to RDF)': async (wire, params) => {
104
+ if (typeof wire.graph !== 'string') {
105
+ throw new TypeError(`node CSVW: the stage takes CSV text on its wire (got ${typeof wire.graph})`);
106
+ }
107
+ const { metadata, ...opts } = params;
108
+ const meta = metadata === undefined || typeof metadata === 'string' ? metadata : JSON.stringify(metadata);
109
+ return { graph: await csvwToRdf(wire.graph, meta, opts), artefact: { tabular: 'csv' } };
110
+ },
91
111
  rdfsExplainGraphStage: async (wire, params) => {
92
112
  const { description, ...opts } = params;
93
113
  if (description === undefined) {
@@ -116,7 +136,10 @@ export function buildNodes(deps) {
116
136
  theorems: Object.freeze([...s.theorems]),
117
137
  assumptions: Object.freeze([...s.assumptions]),
118
138
  tests: Object.freeze([...s.tests]),
119
- bound: Object.prototype.hasOwnProperty.call(RUNNERS, s.name),
139
+ // bound: the package has a runner AND the engine lets the stage
140
+ // run. A runner for a stage that an older engine reports as
141
+ // `missing` is not bound (the engine's registry decides).
142
+ bound: s.runnable && Object.prototype.hasOwnProperty.call(RUNNERS, s.name),
120
143
  })));
121
144
  })();
122
145
  stageCache.catch(() => { stageCache = null; });
@@ -126,6 +149,32 @@ export function buildNodes(deps) {
126
149
 
127
150
  const NODE = Symbol('factoidal.node');
128
151
 
152
+ // The settings as the engine reads them: JSON values, a Dataset as
153
+ // {"$nquads": text}. Anything else is refused.
154
+ function marshal(v, path) {
155
+ if (v === null || typeof v === 'string' || typeof v === 'boolean') return v;
156
+ if (typeof v === 'number') {
157
+ if (!Number.isFinite(v)) throw new TypeError(`node: setting ${path} is not a finite number`);
158
+ return v;
159
+ }
160
+ if (v instanceof Dataset) return { $nquads: v.toNQuads() };
161
+ if (Array.isArray(v)) return v.map((x, i) => marshal(x, `${path}[${i}]`));
162
+ if (typeof v === 'object' && Object.getPrototypeOf(v) === Object.prototype) {
163
+ const out = {};
164
+ for (const [k, x] of Object.entries(v)) if (x !== undefined) out[k] = marshal(x, `${path}.${k}`);
165
+ return out;
166
+ }
167
+ throw new TypeError(`node: setting ${path} must be a JSON value or a Dataset (got ${typeof v})`);
168
+ }
169
+
170
+ const hasNodeCheck = () => !!engine && typeof engine.pipelineCheckNodes === 'function';
171
+
172
+ // The engine's check of nodes with settings (op pipelineCheckNodes).
173
+ function nodeCheck(items) {
174
+ return entryResult(engine.pipelineCheckNodes(JSON.stringify(
175
+ items.map((n) => ({ stage: n.stage, settings: n.settingsWire })))), 'pipelineCheckNodes');
176
+ }
177
+
129
178
  /**
130
179
  * A node for the registered stage `name`, with its parameters (the
131
180
  * second argument of the package function it calls, e.g. `{shapes}`,
@@ -144,18 +193,28 @@ export function buildNodes(deps) {
144
193
  throw new FactoidalError(`node: no registered stage named '${name}' (see stages())`,
145
194
  { code: 'option.invalid-value' });
146
195
  }
196
+ const settingsWire = marshal({ ...(params || {}) }, 'params');
197
+ // The engine's reading of the settings, when it has the op.
198
+ let s = null;
199
+ if (hasNodeCheck()) s = nodeCheck([{ stage: info.name, settingsWire }]).stages[0];
147
200
  return Object.freeze({
148
201
  [NODE]: true,
149
202
  stage: info.name,
150
203
  kind: info.kind,
151
- status: info.status,
204
+ status: s ? s.status : info.status,
205
+ registryStatus: info.status,
152
206
  contract: info.contract,
153
207
  assumptions: info.assumptions,
154
208
  wireIn: info.wireIn,
155
- wireOut: info.wireOut,
209
+ wireOut: s ? s.wireOut : info.wireOut,
156
210
  runnable: info.runnable,
157
211
  bound: info.bound,
158
212
  params: Object.freeze({ ...(params || {}) }),
213
+ settingsWire,
214
+ settings: s ? s.settings : null,
215
+ settingsDigest: s ? s.settingsDigest : null,
216
+ outside: Object.freeze(s ? s.outside.map((o) => Object.freeze({ ...o })) : []),
217
+ derived: s ? Object.freeze({ ...s.derived }) : null,
159
218
  });
160
219
  }
161
220
 
@@ -174,8 +233,11 @@ export function buildNodes(deps) {
174
233
  * or null when the loaded engine has no such op.
175
234
  */
176
235
  function check(nodes) {
177
- if (!engine || typeof engine.pipelineCheck !== 'function') return null;
178
- const r = entryResult(engine.pipelineCheck(JSON.stringify(nodes.map((n) => n.stage))), 'pipelineCheck');
236
+ let r;
237
+ if (hasNodeCheck()) r = nodeCheck(nodes);
238
+ else if (engine && typeof engine.pipelineCheck === 'function') {
239
+ r = entryResult(engine.pipelineCheck(JSON.stringify(nodes.map((n) => n.stage))), 'pipelineCheck');
240
+ } else return null;
179
241
  return Object.freeze({
180
242
  wellTyped: r.wellTyped,
181
243
  wireIn: r.wireIn,
@@ -184,7 +246,11 @@ export function buildNodes(deps) {
184
246
  conversions: Object.freeze(r.conversions.map((c) => Object.freeze({ ...c }))),
185
247
  assumptions: Object.freeze([...r.assumptions]),
186
248
  contract: Object.freeze({ theorem: r.contract.theorem, stages: Object.freeze([...r.contract.stages]) }),
187
- stages: Object.freeze(r.stages.map((s) => Object.freeze({ ...s, assumptions: Object.freeze([...s.assumptions]) }))),
249
+ stages: Object.freeze(r.stages.map((s) => Object.freeze({
250
+ ...s,
251
+ assumptions: Object.freeze([...s.assumptions]),
252
+ ...(s.outside ? { outside: Object.freeze(s.outside.map((o) => Object.freeze({ ...o }))) } : {}),
253
+ }))),
188
254
  });
189
255
  }
190
256
 
package/lib/sbm11.mjs CHANGED
@@ -68,6 +68,32 @@ export function setDefaultSbm11Reader (factory) {
68
68
  defaultReader = factory
69
69
  }
70
70
 
71
+ /**
72
+ * A reader over files held in memory: `files` is a Map (or a plain object)
73
+ * from file name to Uint8Array, for example the result of packSbm11. It
74
+ * moves bytes only; a read past the end of a file, or of a file it does not
75
+ * hold, answers null, and the engine refuses it with the file name.
76
+ */
77
+ export function mapReader (files) {
78
+ const m = files instanceof Map ? files : new Map(Object.entries(files || {}))
79
+ return {
80
+ readAt (file, offset, length) {
81
+ const b = m.get(file)
82
+ if (!b || offset < 0 || length < 0 || offset + length > b.length) return null
83
+ return b.subarray(offset, offset + length)
84
+ },
85
+ size (file) {
86
+ const b = m.get(file)
87
+ return b ? b.length : null
88
+ }
89
+ }
90
+ }
91
+
92
+ function opsReflection (eng) {
93
+ const r = eng.call('ops', [])
94
+ return typeof r === 'string' ? JSON.parse(r) : r
95
+ }
96
+
71
97
  function envelope (raw) {
72
98
  const j = typeof raw === 'string' ? JSON.parse(raw) : raw
73
99
  if (!j || j.ok !== true) {
@@ -151,5 +177,44 @@ export function sbm11Api (loadEngine) {
151
177
  }
152
178
  }
153
179
  }
154
- return { openSbm11 }
180
+ /**
181
+ * Pack an RDF document into the files of one SBM11 generation, in memory
182
+ * (the wasm op sbm11PackFiles, formal/lean4/Wasm/Ops/Sbm11Pack.lean;
183
+ * https://github.com/danbri/factoidal/issues/1009 stage 2). `text` is a
184
+ * string or UTF-8 bytes; `options.syntax` is turtle, trig, nquads,
185
+ * ntriples, rdfxml or jsonld; `options.base` the base IRI. Resolves to a
186
+ * Map from file name to Uint8Array: the files `l4sbm11-pack` writes for the
187
+ * same quads (data files, `.merkle` sidecars, `manifest.sbm11`), without
188
+ * `CURRENT`. Open them with `openSbm11(name, { reader: mapReader(files) })`,
189
+ * or write each under its name into one directory. The engine names every
190
+ * file and computes every byte (iron rule 7).
191
+ */
192
+ async function packSbm11 (text, options = {}) {
193
+ const syntax = options.syntax ?? options.format
194
+ if (typeof syntax !== 'string' || syntax === '') {
195
+ throw new TypeError('packSbm11: options.syntax is required (turtle, trig, nquads, ntriples, rdfxml or jsonld)')
196
+ }
197
+ const eng = await loadEngine()
198
+ const refl = opsReflection(eng)
199
+ const blobIoOps = Array.isArray(refl && refl.blobIoOps) ? refl.blobIoOps : []
200
+ if (typeof eng.callBlobIO !== 'function' || !blobIoOps.includes('sbm11PackFiles')) {
201
+ const e = new Error('packSbm11: this wasm module has no sbm11PackFiles op (it was built before ' +
202
+ 'https://github.com/danbri/factoidal/issues/1009 stage 2)')
203
+ e.code = 'SBM11_NO_PACK_OP'
204
+ throw e
205
+ }
206
+ const bytes = typeof text === 'string' ? new TextEncoder().encode(text) : new Uint8Array(text)
207
+ const base = options.base == null ? '' : String(options.base)
208
+ const files = new Map()
209
+ let count = 1
210
+ for (let i = 0; i < count; i++) {
211
+ const answer = eng.callBlobIO('sbm11PackFiles', [syntax, base, String(i)], bytes)
212
+ const j = envelope(answer.envelope)
213
+ count = Number(j.count)
214
+ files.set(j.name, answer.bytes)
215
+ }
216
+ return files
217
+ }
218
+
219
+ return { openSbm11, packSbm11 }
155
220
  }
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.15.0';
5
+ export const version = '0.16.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "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",