@factoidal/core 0.15.0 → 0.17.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/bin/pack-host.mjs CHANGED
@@ -42,11 +42,12 @@
42
42
  // route rescues an in-page packer. See the closing section of
43
43
  // `docs/designissues/2026-09-03-npm-pack-in-wasm.md`.
44
44
 
45
- import { StoreHostError, listGeneration, readWhole } from '../store-host/index.mjs'
45
+ import { StoreHostError, listGeneration, readWhole, writeNew } from '../store-host/index.mjs'
46
46
  import { fileUrlToPath, joinPath } from '../store-host/paths.mjs'
47
47
  import { loadEngine } from './engine.mjs'
48
48
  import { PackError, packFile, packSupported, verifyGeneration } from './pack.mjs'
49
49
  import { StoreOperationError, openStore } from './store.mjs'
50
+ import { sbm11Api } from '../lib/sbm11.mjs'
50
51
 
51
52
  const isDeno = typeof globalThis.Deno !== 'undefined'
52
53
 
@@ -207,6 +208,7 @@ export function isStackOverflow (error) {
207
208
  */
208
209
  export async function packHere (task, onProgress) {
209
210
  const engine = await loadEngine()
211
+ if (task.kind === 'packSbm11') return await packSbm11Here(engine, task)
210
212
  if (!packSupported(engine)) return { notWired: true }
211
213
  if (task.kind === 'activate') {
212
214
  return { report: activateHere(engine, task) }
@@ -221,6 +223,34 @@ export async function packHere (task, onProgress) {
221
223
  return { report }
222
224
  }
223
225
 
226
+ /**
227
+ * Pack one document into the files of one SBM11 generation (op
228
+ * sbm11PackFiles, through lib/sbm11.mjs packSbm11) and write each file
229
+ * under the name the engine gave it. The document is read whole: the op
230
+ * takes the whole document, and refuses one above its limit.
231
+ *
232
+ * @returns {{notWired: true}|{report: object}}
233
+ */
234
+ async function packSbm11Here (engine, task) {
235
+ const { packSbm11 } = sbm11Api(async () => engine)
236
+ const bytes = readWhole(task.input)
237
+ let files
238
+ try {
239
+ files = await packSbm11(bytes, { syntax: task.syntax, base: task.base })
240
+ } catch (error) {
241
+ if (error && error.code === 'SBM11_NO_PACK_OP') return { notWired: true }
242
+ throw error
243
+ }
244
+ const written = []
245
+ let bytesWritten = 0
246
+ for (const [name, fileBytes] of files) {
247
+ writeNew(joinPath(task.output, name), fileBytes)
248
+ written.push(name)
249
+ bytesWritten += fileBytes.length
250
+ }
251
+ return { report: { bytesRead: bytes.length, written, bytesWritten } }
252
+ }
253
+
224
254
  /**
225
255
  * Verify one generation and answer the engine's verdict.
226
256
  *
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
@@ -529,14 +529,15 @@ export function canonicalize(
529
529
  export function graphs(dataset: Dataset): Array<[name: NamedNode | BlankNode, graph: Dataset]>;
530
530
 
531
531
  /**
532
- * RDFC-1.0 canonical form of a single graph -- the graph-scoped
533
- * sibling of canonicalize(). It returns the canonical N-Quads TEXT, not
534
- * a digest: hash the string yourself (for example `vcSha256Hex`) when a
535
- * fixed-length value is needed. Every quad's graph component is dropped
536
- * before canonicalizing, so isomorphic graphs (including under
537
- * blank-node relabeling) give the same string regardless of what
538
- * graph name they were extracted from. Typically called with one
539
- * entry of graphs()'s output.
532
+ * The RDFC-1.0 canonical hash of a single graph: the SHA-256 of its
533
+ * canonical N-Quads document, as 64 lowercase hexadecimal digits (RDF
534
+ * Dataset Canonicalization 1.0, section 4.4 note 2). The engine computes
535
+ * the canonical form and the digest. Every quad's graph component is
536
+ * dropped first, so isomorphic graphs (including under blank-node
537
+ * relabeling) give the same hash whatever graph name they were extracted
538
+ * from. Typically called with one entry of graphs()'s output. For the
539
+ * canonical text, use canonicalize(). Rejects when the input holds an
540
+ * RDF 1.2 triple term or the section 4.4 call budget is exceeded.
540
541
  */
541
542
  export function canonicalHash(datasetOrGraph: Dataset): Promise<string>;
542
543
 
@@ -701,14 +702,31 @@ export interface StageInfo {
701
702
  note: string;
702
703
  /** false exactly when status is 'missing': such a stage does not run. */
703
704
  runnable: boolean;
704
- /** true when this package has a function that runs the stage as a node. */
705
+ /**
706
+ * true when this package has a function that runs the stage as a node AND
707
+ * the engine reports the stage runnable (an older engine that reports a
708
+ * stage 'missing' leaves it unbound).
709
+ */
705
710
  bound: boolean;
706
711
  }
707
712
 
713
+ /**
714
+ * The status of a node: the registry status, or 'unproved' when the stage is
715
+ * 'proved' and one of the node's settings is outside its proved domain (op
716
+ * pipelineCheckNodes, Lean Dataflow/NodeSettings.lean).
717
+ */
718
+ export type NodeStatus = StageInfo['status'] | 'unproved';
719
+
720
+ /** A setting outside the proved domain of its stage, with the reason. */
721
+ export interface OutsideSetting {
722
+ readonly setting: string;
723
+ readonly reason: string;
724
+ }
725
+
708
726
  /** One evidence record: the node that ran and what it produced. */
709
727
  export interface EvidenceRecord {
710
728
  stage: string;
711
- status: StageInfo['status'];
729
+ status: NodeStatus;
712
730
  contract: string | null;
713
731
  assumptions: string[];
714
732
  /** true iff `status` is 'proved'. */
@@ -719,6 +737,19 @@ export interface EvidenceRecord {
719
737
  reason?: string;
720
738
  /** The node's own output beside the graph: the explain result, the SHACL report, the written text, ... */
721
739
  artefact: unknown;
740
+ /**
741
+ * The node's settings in canonical form (keys sorted at every depth; the
742
+ * defaults of absent settings that narrow the contract, e.g. `lenient:
743
+ * false`, `entail: 'none'`; a Dataset value as `{nquadsSha256}`). Present
744
+ * when the engine has the op pipelineCheckNodes.
745
+ */
746
+ settings?: Record<string, unknown>;
747
+ /** `sha256:` and the hex SHA-256 of the canonical settings text. */
748
+ settingsDigest?: string;
749
+ /** With status 'unproved': the registry status ('proved'). */
750
+ registryStatus?: StageInfo['status'];
751
+ /** With status 'unproved': the settings outside the proved domain. */
752
+ outside?: OutsideSetting[];
722
753
  }
723
754
 
724
755
  /** The value on a wire. */
@@ -731,14 +762,31 @@ export interface Wire<G = unknown> {
731
762
  export interface FactoidalNode {
732
763
  readonly stage: string;
733
764
  readonly kind: string | null;
734
- readonly status: StageInfo['status'];
765
+ /** The engine's status of this node with its settings (see NodeStatus). */
766
+ readonly status: NodeStatus;
767
+ /** The registry status of the stage. */
768
+ readonly registryStatus: StageInfo['status'];
735
769
  readonly contract: string | null;
736
770
  readonly assumptions: readonly string[];
737
771
  readonly wireIn: string;
772
+ /**
773
+ * The output wire with these settings: for shippedEvaluator, 'Table' for
774
+ * SELECT, 'Answer' for ASK, 'Dataset' for CONSTRUCT and DESCRIBE (Lean
775
+ * formWire, evalContract_wire). With an older engine, the registry wire.
776
+ */
738
777
  readonly wireOut: string;
739
778
  readonly runnable: boolean;
740
779
  readonly bound: boolean;
780
+ /** The settings as given (passed to the package function that runs the stage). */
741
781
  readonly params: Readonly<Record<string, unknown>>;
782
+ /** The settings as sent to the engine: JSON values, a Dataset as {$nquads}. */
783
+ readonly settingsWire: Readonly<Record<string, unknown>>;
784
+ /** The engine's canonical settings, or null with an older engine. */
785
+ readonly settings: Readonly<Record<string, unknown>> | null;
786
+ readonly settingsDigest: string | null;
787
+ readonly outside: readonly OutsideSetting[];
788
+ /** Values the engine read from the settings: `form` of a query, `mode` ('1.1' | '1.2') of a reader. */
789
+ readonly derived: Readonly<Record<string, string>> | null;
742
790
  }
743
791
 
744
792
  /** The engine's check of a list of stages (op pipelineCheck, Lean pipelineReport). */
@@ -755,9 +803,17 @@ export interface PipelineCheck {
755
803
  readonly assumptions: readonly string[];
756
804
  readonly contract: { readonly theorem: string; readonly stages: readonly (string | null)[] };
757
805
  readonly stages: readonly {
758
- readonly name: string; readonly status: StageInfo['status']; readonly checked: boolean;
806
+ readonly name: string; readonly status: NodeStatus; readonly checked: boolean;
759
807
  readonly contract: string | null; readonly assumptions: readonly string[];
760
808
  readonly wireIn: string; readonly wireOut: string;
809
+ /** The fields below come from the op pipelineCheckNodes only. */
810
+ readonly registryStatus?: StageInfo['status'];
811
+ readonly settings?: Record<string, unknown>;
812
+ readonly settingsDigest?: string;
813
+ /** The role of each setting: 'parameter' | 'selects-wire' | 'inert' | 'narrows' | 'unlisted'. */
814
+ readonly roles?: Readonly<Record<string, string>>;
815
+ readonly outside?: readonly OutsideSetting[];
816
+ readonly derived?: Readonly<Record<string, string>>;
761
817
  }[];
762
818
  }
763
819
 
@@ -1136,6 +1192,18 @@ export function openSbm11(source: string, options?: { reader?: Sbm11Reader }): P
1136
1192
  /** Serve `source` with `reader`; `null` removes it. */
1137
1193
  export function registerSbm11Source(source: string, reader: Sbm11Reader | null): void;
1138
1194
 
1195
+ /** Pack an RDF document into the files of one SBM11 generation, in memory
1196
+ * (wasm op `sbm11PackFiles`, https://github.com/danbri/factoidal/issues/1009
1197
+ * stage 2): file name to bytes, the files `l4sbm11-pack` writes for the same
1198
+ * quads, without `CURRENT`. Rejects with `code: 'SBM11_NO_PACK_OP'` on a wasm
1199
+ * module that has no such op. */
1200
+ export function packSbm11(text: string | Uint8Array,
1201
+ options: { syntax: 'turtle' | 'trig' | 'nquads' | 'ntriples' | 'rdfxml' | 'jsonld' | string; base?: string }
1202
+ ): Promise<Map<string, Uint8Array>>;
1203
+
1204
+ /** A reader over files held in memory (for example the result of `packSbm11`). */
1205
+ export function mapReader(files: Map<string, Uint8Array> | Record<string, Uint8Array>): Sbm11Reader;
1206
+
1139
1207
  /** One LWS resource an SBM11 reader read (https://github.com/danbri/factoidal/issues/1010). */
1140
1208
  export interface Sbm11ReadEvidence {
1141
1209
  resource: string;
@@ -1570,6 +1638,8 @@ export function capabilities(): Promise<{
1570
1638
  stages: boolean;
1571
1639
  /** The op pipelineCheck: pipe() and run() use the engine's type check and assumption union. */
1572
1640
  pipelineCheck: boolean;
1641
+ /** The op pipelineCheckNodes: node settings in evidence, and the evaluator's output wire from the query form. */
1642
+ pipelineCheckNodes: boolean;
1573
1643
  tableau: boolean;
1574
1644
  rml: boolean;
1575
1645
  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 = "ebac38a482d0";
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-fa72ba5bc828-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40439252690",
7
+ "gitSha": "fa72ba5bc828c1eb75572c3436e9d739f76fdd57",
8
+ "builtAt": "2026-10-27T03:13:16Z",
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": "ebac38a482d03d4a6ce1cf4a59164288628fed6957483dd55180743e7a3daf40",
15
+ "wasmBytes": 17071472,
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) {
@@ -876,9 +884,12 @@ function buildApi(driver) {
876
884
  }
877
885
 
878
886
  /**
879
- * RDFC-1.0 canonical N-Quads of one graph: every quad's graph name is
880
- * dropped first (op datasetGraphAsDefault with the union of the graphs).
881
- * Text, not a digest.
887
+ * The RDFC-1.0 canonical hash of one graph: the SHA-256, as 64 lowercase
888
+ * hexadecimal digits, of the canonical N-Quads document (RDF Dataset
889
+ * Canonicalization 1.0, section 4.4 note 2). Every quad's graph name is
890
+ * dropped first (op datasetGraphAsDefault with the union of the graphs);
891
+ * then op datasetCanonicalHash computes the canonical form and the digest
892
+ * in Lean. canonicalize() answers the canonical text.
882
893
  * @param {Dataset} datasetOrGraph
883
894
  * @returns {Promise<string>}
884
895
  */
@@ -889,10 +900,11 @@ function buildApi(driver) {
889
900
  datasetOrGraph.size; // throws the disposed error
890
901
  const c = datasetOrGraph._ctx;
891
902
  requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
903
+ requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
892
904
  const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
893
905
  JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
894
906
  try {
895
- return merged.toString('rdfc');
907
+ return entryResult(c.e.datasetCanonicalHash(merged.handle), 'canonicalHash').hash;
896
908
  } finally {
897
909
  merged.dispose();
898
910
  }
@@ -2140,6 +2152,7 @@ function buildApi(driver) {
2140
2152
  explain: has('explainGraph'),
2141
2153
  stages: has('stageList'),
2142
2154
  pipelineCheck: has('pipelineCheck'),
2155
+ pipelineCheckNodes: has('pipelineCheckNodes'),
2143
2156
  entailment: has('queryRaw'),
2144
2157
  tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
2145
2158
  rml: has('rmlMap'),
@@ -2176,7 +2189,7 @@ function buildApi(driver) {
2176
2189
  // The node-composition API (lib/nodes.mjs, issue 1002 step 2).
2177
2190
  const nodeApi = buildNodes({
2178
2191
  entry, entryResult, requireEntryFn, Dataset,
2179
- parse, closure, shaclValidate, query, serialize, canonicalize, explain,
2192
+ parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf,
2180
2193
  });
2181
2194
 
2182
2195
  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
  }