@factoidal/core 0.13.0 → 0.14.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 +15 -0
- package/README.md +54 -8
- package/bin/factoidal.mjs +6 -6
- package/browser.d.ts +7 -0
- package/browser.js +8 -0
- package/index.d.ts +214 -11
- package/index.mjs +8 -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 +54 -3
- package/lib/dataset.mjs +8 -0
- package/lib/evidence.mjs +84 -0
- package/lib/nodes.mjs +215 -0
- package/lib/sbm11-node.mjs +61 -0
- package/lib/sbm11.mjs +148 -0
- package/lib/version.mjs +1 -1
- package/package.json +1 -1
- package/sample-store.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
**Checked pipelines.** Each component (reader, store, query evaluator, writer, reasoner, validator, proof checker) has one interface type whose contract against the W3C specifications is part of the type. A component cannot be added without a contract; a contract that is not proved yet is a named assumption, listed and counted in CI. A theorem states that the SPARQL answer agrees with the SPARQL 1.1 semantics for data from any source (a parsed document, a Dataset after edits, an update, a reasoner's output, a union of stores), given the listed assumptions. Status and gaps: https://github.com/danbri/factoidal/issues/975.
|
|
6
|
+
|
|
7
|
+
- `explain(sources, description, { profile: "rdfs" })`: whether the sources RDFS-entail a description graph (its blank nodes are variables), with a checked derivation (`bundle`) when it does and the reason (`rejection`) when not (https://github.com/danbri/factoidal/issues/1002).
|
|
8
|
+
- `run(pipeline, input, { unchecked: true })`: runs nodes whose stage is not proved; each evidence record has `checked`, and the result has `proved` and `uncheckedSteps`. Without the option a stage with no contract does not run.
|
|
9
|
+
- `Dataset.warnings`: a language tag that is not well-formed BCP 47 is kept and reported with its line, column and offset; the writers write it unchanged (https://github.com/danbri/factoidal/issues/1004).
|
|
10
|
+
- SPARQL `SELECT *` on the store path returns the variables in query order, the same header as the in-memory path (https://github.com/danbri/factoidal/issues/1014).
|
|
11
|
+
- SBM11 in npm (`openSbm11`) is **not** in this release. Its wasm op `sbm11Open` traps (`unreachable`) in the release build, so the ops are not dispatched and the functions are not exported; the native `l4sbm11` commands are unchanged (https://github.com/danbri/factoidal/issues/1009).
|
|
12
|
+
- `factoidal pack`, `factoidal sample-store` and `sampleStorePath()` write the IBK5 layout (wire version 10, manifest layout `quad-ibk5-ptd2-lgi4-gbi1-merkle-v0`) when no `--layout` is given; the default was IBK3. Owner, 2026-10-25: "yes IBK5 not IBK3 as default sounds sensible, assuming no major downside" (https://github.com/danbri/factoidal/issues/1009). `factoidal serve` over such a store takes changes to every graph; over an IBK3 store a change to a named graph answered 501. `--layout ibk3` still writes IBK3. `sampleStoreFacts.layout` is now `'ibk5'`.
|
|
13
|
+
- `stages()`, `node()`, `pipe()`, `run()`: the registered Lean stages as nodes (https://github.com/danbri/factoidal/issues/1002, step 2). `stages()` is the engine's op `stageList`, read from the Lean stage registry: name, interface type, wire types, status, contract and named assumptions of each stage. A value on a wire is `{graph, evidence}`; each node adds one evidence record `{stage, status, contract, assumptions, artefact}`, and `run()` reports the union of the named assumptions of the nodes. A stage with status `missing` does not run. Needs a wasm build with the op `stageList`.
|
|
14
|
+
- `factoidal pack` writes a set of rows in every layout (https://github.com/danbri/factoidal/issues/1006). Correction to 0.13.0: its entry says that the stores keep one copy of a repeated triple, but in 0.13.0 that was true for the IBK4 and IBK5 layouts only, and only on read. The default layout (IBK3) kept a repeated triple, and kept `"a"@en` and `"a"@EN` as two rows, so `COUNT(*)` counted both; the IBK4 and IBK5 packers also wrote the repeated rows. Now each packer keeps the distinct triples it has written and writes a triple only once (per graph on the quad layouts). A source with no repeated triple gives the same generation as before. The packer's memory now grows with the number of distinct triples of the source. Proved: `Dataflow.packedRows_keyNodup`; tests: `test/pack-is-a-set-1006.test.mjs`.
|
|
15
|
+
- The N-Triples, N-Quads, Turtle and TriG writers give a fresh valid label to a blank-node label outside BLANK_NODE_LABEL (the same label for each occurrence, never a label of another blank node), and refuse a language tag outside LANGTAG with an error that names the tag (https://github.com/danbri/factoidal/issues/986).
|
|
16
|
+
- A language tag is kept in lower case (RDF 1.2 Concepts §3.4: `"x"@en` and `"x"@EN` are one RDF term). Every reader, `STRLANG`, `STRLANGDIR` and the term constructors write the tag in lower case, so `LANG()` returns it in lower case, and a SPARQL answer no longer depends on the order of the triple patterns when the data spells one tag in two ways (https://github.com/danbri/factoidal/issues/1001).
|
|
17
|
+
|
|
3
18
|
## 0.13.0
|
|
4
19
|
|
|
5
20
|
**A parsed document is a set of quads** (RDF 1.1 Concepts §3 and §4;
|
package/README.md
CHANGED
|
@@ -52,6 +52,50 @@ RDF 1.2 input uses `turtle12`, `ntriples12`, `nquads12` or `trig12`.
|
|
|
52
52
|
"Terms given as strings".
|
|
53
53
|
- RDF/JS DataFactory and DatasetCore (`/rdfjs`), a functional API (`/fn`)
|
|
54
54
|
|
|
55
|
+
## Checked pipelines
|
|
56
|
+
|
|
57
|
+
Each component kind has one interface type whose contract against the W3C
|
|
58
|
+
specifications is part of the type: readers, stores, the SPARQL evaluator,
|
|
59
|
+
writers, reasoners, validators and the proof checker. A contract that is not
|
|
60
|
+
proved yet is a named assumption; CI fails when the list of assumptions grows
|
|
61
|
+
without a stated reason. A theorem states that the SPARQL answer agrees with
|
|
62
|
+
the SPARQL 1.1 semantics for data from any source (a parsed document, an
|
|
63
|
+
edited Dataset, an update, a reasoner's output, a union of stores), given the
|
|
64
|
+
listed assumptions. No new command-line verbs. Status:
|
|
65
|
+
https://github.com/danbri/factoidal/issues/975.
|
|
66
|
+
|
|
67
|
+
The same components are nodes you can compose. A value on a wire is
|
|
68
|
+
`{graph, evidence}`; each node adds one evidence record; the result lists the
|
|
69
|
+
union of the named assumptions of the nodes, so a pipeline says exactly what
|
|
70
|
+
its answer depends on.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { stages, node, pipe, run } from "@factoidal/core";
|
|
74
|
+
|
|
75
|
+
const sources = `@prefix ex: <http://example.org/> .
|
|
76
|
+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
77
|
+
ex:alice ex:worksFor ex:acme .
|
|
78
|
+
ex:worksFor rdfs:domain ex:Employee .
|
|
79
|
+
ex:Employee rdfs:subClassOf ex:Person .`;
|
|
80
|
+
const description = `@prefix ex: <http://example.org/> .
|
|
81
|
+
_:x a ex:Person . _:x ex:worksFor ex:acme .`;
|
|
82
|
+
|
|
83
|
+
console.log((await stages()).length); // registered components, each with status and contract
|
|
84
|
+
const p = pipe(
|
|
85
|
+
await node("turtleDocumentReader"),
|
|
86
|
+
await node("rdfsReasoner"),
|
|
87
|
+
await node("rdfsExplainGraphStage", { description }),
|
|
88
|
+
);
|
|
89
|
+
const r = await run(p, sources);
|
|
90
|
+
console.log(r.evidence.map((e) => e.stage)); // one record per node
|
|
91
|
+
console.log(r.evidence[2].artefact.entailed); // true, with a checked derivation
|
|
92
|
+
console.log(r.assumptions, r.proved); // what the answer depends on
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`run(p, input, { unchecked: true })` also runs components that are not proved,
|
|
96
|
+
and marks those steps in the evidence. Details: API.md, "Stages as nodes" and
|
|
97
|
+
"Explaining an entailment" (`explain()` is the same check as one call).
|
|
98
|
+
|
|
55
99
|
Formats (generated from `formats()`):
|
|
56
100
|
|
|
57
101
|
<!-- formats-table:start (generated by test/formats-table.test.mjs) -->
|
|
@@ -135,7 +179,7 @@ Changes: [CHANGELOG.md](CHANGELOG.md).
|
|
|
135
179
|
```console
|
|
136
180
|
$ npx factoidal serve --file data.ttl /ds # SPARQL endpoint at /ds/sparql
|
|
137
181
|
$ npx factoidal serve --tdb2 DIR /ds # serve a Jena TDB2 database
|
|
138
|
-
$ npx factoidal pack data.ttl store/gen-1 && npx factoidal activate store gen-1
|
|
182
|
+
$ npx factoidal pack data.ttl store/gen-1 && npx factoidal activate store gen-1 # IBK5 quad blocks
|
|
139
183
|
$ npx factoidal query store 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'
|
|
140
184
|
$ npx factoidal query "$(npx factoidal sample-store)" 'SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }'
|
|
141
185
|
$ npx factoidal lws-serve DIR --issuer IRI # Linked Web Storage
|
|
@@ -150,13 +194,15 @@ $ npx factoidal solid-serve DIR # Solid Protocol
|
|
|
150
194
|
Official test suites, pass / fail / total, 2026-10-09 [B] unless marked.
|
|
151
195
|
|
|
152
196
|
The OWL DL rows below are historical measurements of the legacy harness;
|
|
153
|
-
they do not measure the current `owlIsConsistent` API. That API
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
197
|
+
they do not measure the current `owlIsConsistent` API. That API checks only
|
|
198
|
+
an admitted fragment of the default graph (atomic classes,
|
|
199
|
+
`owl:someValuesFrom`, `owl:TransitiveProperty`, and annotations, axiom
|
|
200
|
+
annotations included). It returns `true` for a checked finite model, `false`
|
|
201
|
+
for a checked contradiction and `null` for unsupported input, exhausted
|
|
202
|
+
`fuel` or an incomplete check. Other restrictions, negative property
|
|
203
|
+
assertions, imports and equivalentClass are unsupported. `owlEntails`
|
|
204
|
+
answers `false` only with a checked countermodel; its positive verdicts use a
|
|
205
|
+
separate legacy path that does not have this source-to-verdict proof. See the
|
|
160
206
|
[OWL API contract](https://github.com/danbri/factoidal-core/blob/main/npm/factoidal/API.md)
|
|
161
207
|
and the [assurance record](https://github.com/danbri/factoidal-core/blob/main/docs/20261022-owl-pr-reconciliation.md).
|
|
162
208
|
|
package/bin/factoidal.mjs
CHANGED
|
@@ -224,9 +224,9 @@ INPUT is an RDF file. OUTPUT is the generation directory to create; it is
|
|
|
224
224
|
normally STORE/gen-N. Packing does not activate: run activate next.
|
|
225
225
|
|
|
226
226
|
options:
|
|
227
|
-
--layout LAYOUT
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
--layout LAYOUT ibk5 (quads, wire version 10: RDF 1.2 terms,
|
|
228
|
+
out-of-line literals, zone maps; default), ibk4
|
|
229
|
+
(quads, wire version 9) or ibk3 (triples)
|
|
230
230
|
--syntax SYNTAX turtle, trig or nquads; default from the file extension
|
|
231
231
|
--chunk-bytes N Merkle chunk size; default is the engine's
|
|
232
232
|
--batch-bytes N ibk4 only: source bytes between publication batches;
|
|
@@ -319,7 +319,7 @@ activated generation's delta log as one batch before the response is sent,
|
|
|
319
319
|
and is visible to the next request. An IBK3 (triples) generation accepts
|
|
320
320
|
changes to the default graph only, because its delta log is replayed and
|
|
321
321
|
compacted for that graph only; a change to a named graph answers 501. A
|
|
322
|
-
quad generation (ibk4, ibk5) accepts every graph. Fold the log into a new
|
|
322
|
+
quad generation (ibk4, ibk5, the default) accepts every graph. Fold the log into a new
|
|
323
323
|
generation with l4block-shard-compact, activate it, and restart the server.
|
|
324
324
|
|
|
325
325
|
The server makes no protocol decision: the request framing, routing,
|
|
@@ -1266,7 +1266,7 @@ function currentDirectory () {
|
|
|
1266
1266
|
}
|
|
1267
1267
|
|
|
1268
1268
|
function packLayout (options) {
|
|
1269
|
-
if (typeof options.layout !== 'string') return '
|
|
1269
|
+
if (typeof options.layout !== 'string') return 'ibk5'
|
|
1270
1270
|
const layout = options.layout.toLowerCase()
|
|
1271
1271
|
if (PACK_LAYOUTS.indexOf(layout) >= 0) return layout
|
|
1272
1272
|
throw new UsageError(`--layout ${options.layout} is not one of ${PACK_LAYOUTS.join(', ')}`)
|
|
@@ -1528,7 +1528,7 @@ async function commandServe (positional, options) {
|
|
|
1528
1528
|
const { tmpdir } = await import('node:os')
|
|
1529
1529
|
const { join } = await import('node:path')
|
|
1530
1530
|
const root = mkdtempSync(join(tmpdir(), 'factoidal-serve-'))
|
|
1531
|
-
const packOptions = { quiet: true, layout:
|
|
1531
|
+
const packOptions = { quiet: true, layout: packLayout(options) }
|
|
1532
1532
|
let rc = await commandPack([options.file, join(root, 'gen-1')], packOptions)
|
|
1533
1533
|
if (rc !== 0) return rc
|
|
1534
1534
|
rc = await commandActivate([root, 'gen-1'], { quiet: true })
|
package/browser.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type {
|
|
|
11
11
|
DataFormat, EntailRegime, Bindings, DataInput, QueryInput, ParseOptions,
|
|
12
12
|
QueryOptions, SerializeOptions, StoreReader, OpenStoreOptions,
|
|
13
13
|
DeltaLogHandle, DeltaLogInspection,
|
|
14
|
+
Sbm11Reader, Sbm11Answer, Sbm11Store,
|
|
15
|
+
ExplainResult, StageInfo, EvidenceRecord, Wire, FactoidalNode, Pipeline, RunResult, RunOptions,
|
|
14
16
|
} from './index';
|
|
15
17
|
|
|
16
18
|
export {
|
|
@@ -50,6 +52,11 @@ export {
|
|
|
50
52
|
owlClosure,
|
|
51
53
|
coreRdfsClosure,
|
|
52
54
|
coreRdfsCheck,
|
|
55
|
+
explain,
|
|
56
|
+
stages,
|
|
57
|
+
node,
|
|
58
|
+
pipe,
|
|
59
|
+
run,
|
|
53
60
|
rdfsPlusClosure,
|
|
54
61
|
rhoDfClosure,
|
|
55
62
|
rhoDfFragmentCheck,
|
package/browser.js
CHANGED
|
@@ -59,6 +59,9 @@ const api = buildApi({
|
|
|
59
59
|
deltaLogStore: indexedDbDeltaLogStore(),
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
// SBM11 (openSbm11, registerSbm11Source) is not exported in 0.14.0: the op
|
|
63
|
+
// sbm11Open traps in the wasm build (https://github.com/danbri/factoidal/issues/1009).
|
|
64
|
+
|
|
62
65
|
// RDF and SPARQL
|
|
63
66
|
export const parse = api.parse;
|
|
64
67
|
export const query = api.query;
|
|
@@ -102,6 +105,11 @@ export const shexValidate = api.shexValidate;
|
|
|
102
105
|
export const owlClosure = api.owlClosure;
|
|
103
106
|
export const coreRdfsClosure = api.coreRdfsClosure;
|
|
104
107
|
export const coreRdfsCheck = api.coreRdfsCheck;
|
|
108
|
+
export const explain = api.explain;
|
|
109
|
+
export const stages = api.stages;
|
|
110
|
+
export const node = api.node;
|
|
111
|
+
export const pipe = api.pipe;
|
|
112
|
+
export const run = api.run;
|
|
105
113
|
export const rdfsPlusClosure = api.rdfsPlusClosure;
|
|
106
114
|
export const rhoDfClosure = api.rhoDfClosure;
|
|
107
115
|
export const rhoDfFragmentCheck = api.rhoDfFragmentCheck;
|
package/index.d.ts
CHANGED
|
@@ -184,7 +184,8 @@ export interface ParseDiagnostic {
|
|
|
184
184
|
* 'unterminated-string', 'bad-literal', 'bad-blank-node',
|
|
185
185
|
* 'bad-prefixed-name', 'bad-directive', 'bad-graph-name', 'rdf12-syntax',
|
|
186
186
|
* 'unclosed-group', 'nesting-limit', 'unexpected-token', 'internal' or
|
|
187
|
-
* 'too-many-errors'.
|
|
187
|
+
* 'too-many-errors'. A warning (`Dataset.warnings`) has the class
|
|
188
|
+
* 'ill-formed-language-tag'.
|
|
188
189
|
*/
|
|
189
190
|
class: string;
|
|
190
191
|
}
|
|
@@ -250,6 +251,13 @@ export class Dataset implements Iterable<Quad> {
|
|
|
250
251
|
readonly prefixes: Record<string, string>;
|
|
251
252
|
/** Diagnostics of a `{lenient:true}` parse; else empty. */
|
|
252
253
|
readonly diagnostics: ParseDiagnostic[];
|
|
254
|
+
/**
|
|
255
|
+
* Warnings of the parse of text: one per language tag that is not
|
|
256
|
+
* well-formed under BCP 47 section 2.2.9 (class 'ill-formed-language-tag',
|
|
257
|
+
* the position of the tag's first character). The reader keeps the triple
|
|
258
|
+
* and the writers write the tag unchanged. Empty otherwise.
|
|
259
|
+
*/
|
|
260
|
+
readonly warnings: ParseDiagnostic[];
|
|
253
261
|
/** The parse format tag, when the data was read from text. */
|
|
254
262
|
readonly format: string | undefined;
|
|
255
263
|
/** True when `format` was inferred because the caller gave none. */
|
|
@@ -627,6 +635,159 @@ export function coreRdfsCheck(
|
|
|
627
635
|
options?: { format?: DataFormat; baseIRI?: string }
|
|
628
636
|
): Promise<{ ok: boolean; fragment: boolean }>;
|
|
629
637
|
|
|
638
|
+
/** The evidence bundle and verdict of explain(). */
|
|
639
|
+
export interface ExplainResult {
|
|
640
|
+
profile: 'rdfs';
|
|
641
|
+
/** true: the checker accepted `bundle`; the sources RDFS-entail the description. */
|
|
642
|
+
entailed: boolean;
|
|
643
|
+
bundle: null | {
|
|
644
|
+
sources: string;
|
|
645
|
+
description: string;
|
|
646
|
+
mapping: Array<{ bnode: string; term: string }>;
|
|
647
|
+
steps: Array<{ rule: string; conclusion: string; premises: number[] }>;
|
|
648
|
+
support: number[];
|
|
649
|
+
};
|
|
650
|
+
/** Named assumptions that `entailed: false` relies on. */
|
|
651
|
+
assumptions: string[];
|
|
652
|
+
rejection:
|
|
653
|
+
| null
|
|
654
|
+
| { kind: 'noMatch'; index: number; triple: string }
|
|
655
|
+
| { kind: 'noJointMapping' }
|
|
656
|
+
| { kind: 'checkerRefused'; reason: string };
|
|
657
|
+
entailedMeans: string;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Is `description` (blank nodes shared across its triples) RDFS-entailed by
|
|
662
|
+
* `sources`? Full engine only; the description is default-graph only. Text
|
|
663
|
+
* inputs are Turtle unless `format` says otherwise.
|
|
664
|
+
* https://github.com/danbri/factoidal/issues/1002
|
|
665
|
+
*/
|
|
666
|
+
export function explain(
|
|
667
|
+
sources: DataInput,
|
|
668
|
+
description: DataInput,
|
|
669
|
+
options?: {
|
|
670
|
+
profile?: 'rdfs';
|
|
671
|
+
format?: DataFormat;
|
|
672
|
+
baseIRI?: string;
|
|
673
|
+
sourcesFormat?: DataFormat;
|
|
674
|
+
descriptionFormat?: DataFormat;
|
|
675
|
+
}
|
|
676
|
+
): Promise<ExplainResult>;
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* One registered stage, as the engine's op stageList reads it from the Lean
|
|
680
|
+
* registry (L4Factoidal.Dataflow.registry).
|
|
681
|
+
* https://github.com/danbri/factoidal/issues/1002
|
|
682
|
+
*/
|
|
683
|
+
export interface StageInfo {
|
|
684
|
+
/** The stage declaration relative to L4Factoidal.Dataflow (or L4Factoidal); the area when there is none. */
|
|
685
|
+
name: string;
|
|
686
|
+
/** The full Lean declaration name, or null (status tested or missing). */
|
|
687
|
+
declaration: string | null;
|
|
688
|
+
area: string;
|
|
689
|
+
dir: string;
|
|
690
|
+
/** The interface type of the declaration (Stage, RdfReader, SoundReasoner, ...), or null. */
|
|
691
|
+
kind: string | null;
|
|
692
|
+
wireIn: string;
|
|
693
|
+
wireOut: string;
|
|
694
|
+
status: 'proved' | 'tested' | 'assumed' | 'missing';
|
|
695
|
+
/** The contract declaration (full Lean name), or null. */
|
|
696
|
+
contract: string | null;
|
|
697
|
+
theorems: string[];
|
|
698
|
+
/** The named assumptions of the stage (full Lean names). */
|
|
699
|
+
assumptions: string[];
|
|
700
|
+
tests: string[];
|
|
701
|
+
note: string;
|
|
702
|
+
/** false exactly when status is 'missing': such a stage does not run. */
|
|
703
|
+
runnable: boolean;
|
|
704
|
+
/** true when this package has a function that runs the stage as a node. */
|
|
705
|
+
bound: boolean;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** One evidence record: the node that ran and what it produced. */
|
|
709
|
+
export interface EvidenceRecord {
|
|
710
|
+
stage: string;
|
|
711
|
+
status: StageInfo['status'];
|
|
712
|
+
contract: string | null;
|
|
713
|
+
assumptions: string[];
|
|
714
|
+
/** true iff `status` is 'proved'. */
|
|
715
|
+
checked: boolean;
|
|
716
|
+
/** true on a step that ran under run()'s `unchecked` opt-in and is not checked. */
|
|
717
|
+
unchecked?: true;
|
|
718
|
+
/** With `unchecked`: the status and named assumptions, or 'no contract' for a missing stage. */
|
|
719
|
+
reason?: string;
|
|
720
|
+
/** The node's own output beside the graph: the explain result, the SHACL report, the written text, ... */
|
|
721
|
+
artefact: unknown;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** The value on a wire. */
|
|
725
|
+
export interface Wire<G = unknown> {
|
|
726
|
+
graph: G;
|
|
727
|
+
evidence: EvidenceRecord[];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** A node: a registered stage with its parameters. */
|
|
731
|
+
export interface FactoidalNode {
|
|
732
|
+
readonly stage: string;
|
|
733
|
+
readonly kind: string | null;
|
|
734
|
+
readonly status: StageInfo['status'];
|
|
735
|
+
readonly contract: string | null;
|
|
736
|
+
readonly assumptions: readonly string[];
|
|
737
|
+
readonly wireIn: string;
|
|
738
|
+
readonly wireOut: string;
|
|
739
|
+
readonly runnable: boolean;
|
|
740
|
+
readonly bound: boolean;
|
|
741
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** A pipeline: nodes in order; `assumptions` is the union of theirs. */
|
|
745
|
+
export interface Pipeline {
|
|
746
|
+
readonly nodes: readonly FactoidalNode[];
|
|
747
|
+
readonly assumptions: readonly string[];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** The result of run(): the last graph, one record per node, the assumption union. */
|
|
751
|
+
export interface RunResult<G = unknown> extends Wire<G> {
|
|
752
|
+
assumptions: string[];
|
|
753
|
+
/** true iff every evidence record is checked (status 'proved'). */
|
|
754
|
+
proved: boolean;
|
|
755
|
+
/** The stage names of the records that are not checked. */
|
|
756
|
+
uncheckedSteps: string[];
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Options of run(). */
|
|
760
|
+
export interface RunOptions {
|
|
761
|
+
/**
|
|
762
|
+
* Let stages that are not 'proved' run, and 'missing' stages too when the
|
|
763
|
+
* package has a runner for them. Each such step's record says so, and the
|
|
764
|
+
* result's `proved` is false. A stage with no runner still rejects (op.unknown).
|
|
765
|
+
*/
|
|
766
|
+
unchecked?: boolean;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** The engine's stage list (op stageList, full engine). */
|
|
770
|
+
export function stages(): Promise<StageInfo[]>;
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* A node for the registered stage `name`. `params` are passed to the
|
|
774
|
+
* package function that runs it: reader options; `{shapes}` for
|
|
775
|
+
* shaclValidator; `{query}` for shippedEvaluator; `{description}` for
|
|
776
|
+
* rdfsExplainGraphStage. An unknown name rejects with a FactoidalError.
|
|
777
|
+
*/
|
|
778
|
+
export function node(name: string, params?: Record<string, unknown>): Promise<FactoidalNode>;
|
|
779
|
+
|
|
780
|
+
/** Compose nodes and pipelines left to right. */
|
|
781
|
+
export function pipe(...nodes: Array<FactoidalNode | Pipeline>): Pipeline;
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Run a node or pipeline on `input` (a Wire, or the graph: text for a
|
|
785
|
+
* reader, else a Dataset or data input). Rejects before any node runs when
|
|
786
|
+
* a node's stage has status 'missing' (unless `options.unchecked`) or has
|
|
787
|
+
* no package function.
|
|
788
|
+
*/
|
|
789
|
+
export function run(pipeline: FactoidalNode | Pipeline, input: unknown, options?: RunOptions): Promise<RunResult>;
|
|
790
|
+
|
|
630
791
|
/** Literature-name alias for coreRdfsClosure (ρdf). */
|
|
631
792
|
export function rhoDfClosure(
|
|
632
793
|
data: DataInput,
|
|
@@ -681,15 +842,17 @@ export function tableauDlInconsistent(
|
|
|
681
842
|
|
|
682
843
|
/**
|
|
683
844
|
* Checked consistency for the original default graph in a restricted
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
845
|
+
* OWL fragment (named subclass/class assertions, Thing/Nothing,
|
|
846
|
+
* owl:someValuesFrom restrictions as a superclass or a class assertion,
|
|
847
|
+
* owl:TransitiveProperty, sameAs/differentFrom, declarations, annotations
|
|
848
|
+
* and annotated axioms).
|
|
849
|
+
* true requires a checked finite model and has no `reason`; false requires
|
|
850
|
+
* an accepted refutation certificate; null means unsupported-syntax,
|
|
851
|
+
* budget-out or incomplete-search. Named graphs do not contribute axioms.
|
|
852
|
+
* RDF 1.2 terms are refused. Other restrictions, object/data property
|
|
853
|
+
* axioms, imports and equivalentClass are unsupported. `fuel` bounds the
|
|
854
|
+
* model search and the certificate checker (default 20000 rounds).
|
|
855
|
+
* This is not complete OWL DL support.
|
|
693
856
|
*/
|
|
694
857
|
export function owlIsConsistent(
|
|
695
858
|
data: DataInput,
|
|
@@ -703,7 +866,9 @@ export function owlIsConsistent(
|
|
|
703
866
|
* implementation returns `true` for those positive results and `null`
|
|
704
867
|
* when it cannot establish entailment. `reason` distinguishes budget-out,
|
|
705
868
|
* incomplete-search, unsupported-conclusion and unsupported-punning.
|
|
706
|
-
*
|
|
869
|
+
* `false` (via `refutation`, reason `countermodel: ...`) requires a checked
|
|
870
|
+
* finite model of the premise in which the conclusion fails; both graphs
|
|
871
|
+
* must be in the fragment that `owlIsConsistent` reads.
|
|
707
872
|
* The punning refusal is limited; full OWL DL recognition and universal
|
|
708
873
|
* Direct Semantics soundness are not established by this API.
|
|
709
874
|
*/
|
|
@@ -897,6 +1062,42 @@ export class Store {
|
|
|
897
1062
|
*/
|
|
898
1063
|
export function openStore(source: string | StoreReader, options?: OpenStoreOptions): Promise<Store>;
|
|
899
1064
|
|
|
1065
|
+
/** A synchronous reader of the files of one SBM11 generation directory (or
|
|
1066
|
+
* collection root): the engine names every file and checks every answer. */
|
|
1067
|
+
export interface Sbm11Reader {
|
|
1068
|
+
readAt(file: string, offset: number, length: number): Uint8Array | null;
|
|
1069
|
+
size(file: string): number | null;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/** The answer of one SBM11 query. */
|
|
1073
|
+
export interface Sbm11Answer {
|
|
1074
|
+
/** `path=ids` or `path=reference reason="..." ...` */
|
|
1075
|
+
path: string;
|
|
1076
|
+
rows: number;
|
|
1077
|
+
/** `srj` (SELECT, ASK) or `ntriples` (CONSTRUCT, DESCRIBE) */
|
|
1078
|
+
format: 'srj' | 'ntriples';
|
|
1079
|
+
/** the bytes `l4sbm11 query` writes, as text */
|
|
1080
|
+
readonly result: string;
|
|
1081
|
+
/** the same answer as UTF-8 bytes */
|
|
1082
|
+
readonly bytes: Uint8Array;
|
|
1083
|
+
bytesRead: number;
|
|
1084
|
+
reads: number;
|
|
1085
|
+
/** the parsed SPARQL JSON for SELECT and ASK */
|
|
1086
|
+
readonly json: any;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** An open SBM11 generation. */
|
|
1090
|
+
export interface Sbm11Store {
|
|
1091
|
+
handle: string;
|
|
1092
|
+
/** the generation CURRENT names, or "" when the source is a generation */
|
|
1093
|
+
generation: string;
|
|
1094
|
+
terms: number;
|
|
1095
|
+
query(sparql: string, options?: { base?: string; sparql12?: boolean; reference?: boolean }): Sbm11Answer;
|
|
1096
|
+
close(): void;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
|
|
900
1101
|
/** One query against a store: open with the query's plan, query, close. */
|
|
901
1102
|
export function queryStore(
|
|
902
1103
|
source: string | StoreReader,
|
|
@@ -1302,6 +1503,8 @@ export function capabilities(): Promise<{
|
|
|
1302
1503
|
shacl: boolean;
|
|
1303
1504
|
shex: boolean;
|
|
1304
1505
|
owlClosure: boolean;
|
|
1506
|
+
explain: boolean;
|
|
1507
|
+
stages: boolean;
|
|
1305
1508
|
tableau: boolean;
|
|
1306
1509
|
rml: boolean;
|
|
1307
1510
|
csvw: boolean;
|
package/index.mjs
CHANGED
|
@@ -66,6 +66,9 @@ export const clearExtensionFunctions = api.clearExtensionFunctions;
|
|
|
66
66
|
export const registerServiceEndpoint = api.registerServiceEndpoint;
|
|
67
67
|
export const clearServiceEndpoints = api.clearServiceEndpoints;
|
|
68
68
|
|
|
69
|
+
// SBM11 (openSbm11, registerSbm11Source) is not exported in 0.14.0: the op
|
|
70
|
+
// sbm11Open traps in the wasm build (https://github.com/danbri/factoidal/issues/1009).
|
|
71
|
+
|
|
69
72
|
// The persisted store
|
|
70
73
|
export const openStore = api.openStore;
|
|
71
74
|
export const queryStore = api.queryStore;
|
|
@@ -86,6 +89,11 @@ export const shexValidate = api.shexValidate;
|
|
|
86
89
|
export const owlClosure = api.owlClosure;
|
|
87
90
|
export const coreRdfsClosure = api.coreRdfsClosure;
|
|
88
91
|
export const coreRdfsCheck = api.coreRdfsCheck;
|
|
92
|
+
export const explain = api.explain;
|
|
93
|
+
export const stages = api.stages;
|
|
94
|
+
export const node = api.node;
|
|
95
|
+
export const pipe = api.pipe;
|
|
96
|
+
export const run = api.run;
|
|
89
97
|
export const rdfsPlusClosure = api.rdfsPlusClosure;
|
|
90
98
|
export const rhoDfClosure = api.rhoDfClosure;
|
|
91
99
|
export const rhoDfFragmentCheck = api.rhoDfFragmentCheck;
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -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-
|
|
6
|
-
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/
|
|
7
|
-
"gitSha": "
|
|
8
|
-
"builtAt": "2026-10-
|
|
5
|
+
"release": "danbri/factoidal-builds build-159a7adfbd01-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40192154339",
|
|
7
|
+
"gitSha": "159a7adfbd0155fc560583bc028a54f4508cd4ee",
|
|
8
|
+
"builtAt": "2026-10-25T12:48:55Z",
|
|
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": "
|
|
15
|
-
"wasmBytes":
|
|
14
|
+
"wasmSha256": "2e65b7c2fda13352e80f73ee224d3614551455f7361fa8b1596bb5f702653feb",
|
|
15
|
+
"wasmBytes": 15069305,
|
|
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
|
@@ -29,6 +29,7 @@ import { openGeneration, readRegion, manifestKeys, hexOfBytes } from './store.mj
|
|
|
29
29
|
import { fullEngineMessage } from './lean-entry.mjs';
|
|
30
30
|
import { memoryDeltaLogStore } from './delta-log-hosts.mjs';
|
|
31
31
|
import { FactoidalError, errorFromEnvelope, envelopeResult } from './errors.mjs';
|
|
32
|
+
import { buildNodes } from './nodes.mjs';
|
|
32
33
|
|
|
33
34
|
// The format names, regimes and query outputs are the engine's tables (op
|
|
34
35
|
// engineTables, formal/lean4/Wasm/Ops/Handles.lean), read once per engine:
|
|
@@ -530,7 +531,10 @@ function buildApi(driver) {
|
|
|
530
531
|
},
|
|
531
532
|
missing: (what, op) => missingOpError(what, e, op),
|
|
532
533
|
wrap(r, prefixes, extra) {
|
|
533
|
-
|
|
534
|
+
// `warnings` (the reader's language-tag warnings) is not an info
|
|
535
|
+
// member: openText() and the lenient parse() pass the warnings of the
|
|
536
|
+
// caller's text in `extra`, with their offsets in UTF-16 units.
|
|
537
|
+
const { ok: _ok, handle, count, prefixes: own, warnings: _w, ...info } = r;
|
|
534
538
|
return new Dataset(INTERNAL, c, handle, count,
|
|
535
539
|
prefixes !== undefined ? prefixes : own, { ...(extra || {}), info });
|
|
536
540
|
},
|
|
@@ -595,7 +599,10 @@ function buildApi(driver) {
|
|
|
595
599
|
}
|
|
596
600
|
throw err;
|
|
597
601
|
}
|
|
598
|
-
return c.wrap(r, undefined, {
|
|
602
|
+
return c.wrap(r, undefined, {
|
|
603
|
+
format: tag, formatInferred: inferred,
|
|
604
|
+
warnings: utf16Diagnostics(text, r.warnings),
|
|
605
|
+
});
|
|
599
606
|
}
|
|
600
607
|
|
|
601
608
|
// The Dataset a consumer reads: `data` itself when it is a Dataset, else
|
|
@@ -741,6 +748,7 @@ function buildApi(driver) {
|
|
|
741
748
|
const opened = entryResult(c.e.datasetOpen(r.nquads, 'nquads', ''), 'parse');
|
|
742
749
|
return c.wrap(opened, r.prefixes, {
|
|
743
750
|
diagnostics: utf16Diagnostics(text, r.diagnostics),
|
|
751
|
+
warnings: utf16Diagnostics(text, r.warnings),
|
|
744
752
|
format: tag, formatInferred: inferred,
|
|
745
753
|
});
|
|
746
754
|
}
|
|
@@ -1023,6 +1031,36 @@ function buildApi(driver) {
|
|
|
1023
1031
|
/** Literature-name alias for coreRdfsCheck. */
|
|
1024
1032
|
const rhoDfFragmentCheck = coreRdfsCheck;
|
|
1025
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Is `description` (a graph whose blank nodes are shared variables)
|
|
1036
|
+
* RDFS-entailed by `sources`? Returns the evidence bundle when it is
|
|
1037
|
+
* (https://github.com/danbri/factoidal/issues/1002). Op explainGraph, full
|
|
1038
|
+
* engine only. `entailed: false` also relies on the named assumption in
|
|
1039
|
+
* `assumptions`. Text inputs are Turtle unless `format` says otherwise.
|
|
1040
|
+
* @param {Dataset|string|Array} sources
|
|
1041
|
+
* @param {Dataset|string|Array} description default graph only
|
|
1042
|
+
* @param {{profile?: 'rdfs', format?: string, baseIRI?: string,
|
|
1043
|
+
* sourcesFormat?: string, descriptionFormat?: string}} [options]
|
|
1044
|
+
* @returns {Promise<{profile: string, entailed: boolean, bundle: object|null,
|
|
1045
|
+
* assumptions: string[], rejection: object|null, entailedMeans: string}>}
|
|
1046
|
+
*/
|
|
1047
|
+
async function explain(sources, description, options) {
|
|
1048
|
+
const o = optionsOf('explain', options);
|
|
1049
|
+
if (o.profile !== undefined && o.profile !== 'rdfs') {
|
|
1050
|
+
throw new TypeError(`explain: unsupported profile '${o.profile}' (only 'rdfs')`);
|
|
1051
|
+
}
|
|
1052
|
+
const { profile: _profile, sourcesFormat, descriptionFormat, ...rest } = o;
|
|
1053
|
+
// The engine's option table has no entry for explain; the options here are read-only hints.
|
|
1054
|
+
const opts = rest;
|
|
1055
|
+
const fmt = opts.format || 'turtle';
|
|
1056
|
+
const srcNq = await nquadsOf(sources, { ...opts, format: sourcesFormat || fmt }, 'explain(sources)');
|
|
1057
|
+
const descNq = await nquadsOf(description, { ...opts, format: descriptionFormat || fmt }, 'explain(description)');
|
|
1058
|
+
const e = await entry();
|
|
1059
|
+
requireEntryFn(e, 'explainGraph', 'explain');
|
|
1060
|
+
const { ok: _ok, ...out } = entryResult(e.explainGraph(srcNq, descNq), 'explain');
|
|
1061
|
+
return out;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1026
1064
|
/**
|
|
1027
1065
|
* RDFS-Plus closure (RDFS with owl:sameAs, inverseOf, Symmetric,
|
|
1028
1066
|
* Transitive, Functional and InverseFunctional properties,
|
|
@@ -2078,7 +2116,7 @@ function buildApi(driver) {
|
|
|
2078
2116
|
* @returns {Promise<{entry: boolean, construct: boolean,
|
|
2079
2117
|
* update: boolean, canonicalize: boolean, graphs: boolean,
|
|
2080
2118
|
* canonicalHash: boolean, shacl: boolean, shex: boolean,
|
|
2081
|
-
* owlClosure: boolean, rml: boolean, csvw: boolean, jsonld: boolean,
|
|
2119
|
+
* owlClosure: boolean, explain: boolean, stages: boolean, rml: boolean, csvw: boolean, jsonld: boolean,
|
|
2082
2120
|
* rif: boolean}>}
|
|
2083
2121
|
*/
|
|
2084
2122
|
let capsCache = null;
|
|
@@ -2099,6 +2137,8 @@ function buildApi(driver) {
|
|
|
2099
2137
|
shacl: has('shaclValidate'),
|
|
2100
2138
|
shex: has('shexValidate'),
|
|
2101
2139
|
owlClosure: has('owlClosure'),
|
|
2140
|
+
explain: has('explainGraph'),
|
|
2141
|
+
stages: has('stageList'),
|
|
2102
2142
|
entailment: has('queryRaw'),
|
|
2103
2143
|
tableau: has('tableauMaterialise') && has('tableauDlInconsistent'),
|
|
2104
2144
|
rml: has('rmlMap'),
|
|
@@ -2132,6 +2172,12 @@ function buildApi(driver) {
|
|
|
2132
2172
|
};
|
|
2133
2173
|
}
|
|
2134
2174
|
|
|
2175
|
+
// The node-composition API (lib/nodes.mjs, issue 1002 step 2).
|
|
2176
|
+
const nodeApi = buildNodes({
|
|
2177
|
+
entry, entryResult, requireEntryFn, Dataset,
|
|
2178
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, explain,
|
|
2179
|
+
});
|
|
2180
|
+
|
|
2135
2181
|
return {
|
|
2136
2182
|
parse,
|
|
2137
2183
|
query,
|
|
@@ -2154,6 +2200,11 @@ function buildApi(driver) {
|
|
|
2154
2200
|
owlClosure,
|
|
2155
2201
|
coreRdfsClosure,
|
|
2156
2202
|
coreRdfsCheck,
|
|
2203
|
+
explain,
|
|
2204
|
+
stages: nodeApi.stages,
|
|
2205
|
+
node: nodeApi.node,
|
|
2206
|
+
pipe: nodeApi.pipe,
|
|
2207
|
+
run: nodeApi.run,
|
|
2157
2208
|
rdfsPlusClosure,
|
|
2158
2209
|
rhoDfClosure,
|
|
2159
2210
|
rhoDfFragmentCheck,
|
package/lib/dataset.mjs
CHANGED
|
@@ -152,6 +152,7 @@ class Dataset {
|
|
|
152
152
|
this._prefixes = Object.freeze({ ...(prefixes || {}) });
|
|
153
153
|
const x = extra || {};
|
|
154
154
|
this._diagnostics = Object.freeze((x.diagnostics || []).slice());
|
|
155
|
+
this._warnings = Object.freeze((x.warnings || []).slice());
|
|
155
156
|
this._format = x.format;
|
|
156
157
|
this._formatInferred = !!x.formatInferred;
|
|
157
158
|
this._info = Object.freeze({ ...(x.info || {}) });
|
|
@@ -169,6 +170,13 @@ class Dataset {
|
|
|
169
170
|
get prefixes() { return this._prefixes; }
|
|
170
171
|
/** Diagnostics of a lenient parse ({lenient:true}); else empty. */
|
|
171
172
|
get diagnostics() { return this._diagnostics; }
|
|
173
|
+
/**
|
|
174
|
+
* Warnings of the parse: one per language tag that is not well-formed
|
|
175
|
+
* under BCP 47 (class 'ill-formed-language-tag'). The triple is kept and
|
|
176
|
+
* the writers write the tag unchanged. Empty for a Dataset not read from
|
|
177
|
+
* text.
|
|
178
|
+
*/
|
|
179
|
+
get warnings() { return this._warnings; }
|
|
172
180
|
/** The syntax the data was read from, when it was read from text. */
|
|
173
181
|
get format() { return this._format; }
|
|
174
182
|
/** True when `format` was inferred because the caller gave none. */
|
package/lib/evidence.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The evidence logic of the node-composition API (issue 1002), kept apart
|
|
2
|
+
// from the engine so it runs, and is tested, without the wasm.
|
|
3
|
+
//
|
|
4
|
+
// Rule (owner-approved, issue 975 comment 6393905605 item 3): a stage that
|
|
5
|
+
// is not `proved` may run only after an explicit opt-in, and its evidence
|
|
6
|
+
// record says so. A result never reports a guarantee the path lacks:
|
|
7
|
+
// `proved` is true only when every record is `checked`.
|
|
8
|
+
//
|
|
9
|
+
// ES module, no Node built-ins.
|
|
10
|
+
|
|
11
|
+
import { FactoidalError } from './errors.mjs';
|
|
12
|
+
|
|
13
|
+
/** A stage is checked exactly when its registry status is `proved`. */
|
|
14
|
+
export const isChecked = (status) => status === 'proved';
|
|
15
|
+
|
|
16
|
+
/** The reason text of an unchecked step. */
|
|
17
|
+
export function reasonOf(n) {
|
|
18
|
+
if (n.status === 'missing') return 'no contract';
|
|
19
|
+
return n.assumptions.length > 0
|
|
20
|
+
? `status ${n.status}; assumptions: ${n.assumptions.join(', ')}`
|
|
21
|
+
: `status ${n.status}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The refusal for node `n` before anything runs, or null. `hasRunner` is
|
|
26
|
+
* whether the package has a function that runs the stage.
|
|
27
|
+
* Without the opt-in a `missing` stage refuses (op.failed). With it, a stage
|
|
28
|
+
* runs when it has a runner; one with no runner refuses (op.unknown).
|
|
29
|
+
*/
|
|
30
|
+
export function refusalFor(n, unchecked, hasRunner) {
|
|
31
|
+
if (!n.runnable && !unchecked) {
|
|
32
|
+
return new FactoidalError(`run: stage '${n.stage}' has status ${n.status}: ` +
|
|
33
|
+
'no contract and no test suite, so it does not run as a node ' +
|
|
34
|
+
'(pass { unchecked: true } to run it without a guarantee)', { code: 'op.failed' });
|
|
35
|
+
}
|
|
36
|
+
if (!hasRunner) {
|
|
37
|
+
return new FactoidalError(`run: stage '${n.stage}' has no package function that runs it`,
|
|
38
|
+
{ code: 'op.unknown' });
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One evidence record. `checked` is true iff the status is `proved`.
|
|
45
|
+
* A step that ran under the opt-in and is not checked also carries
|
|
46
|
+
* `unchecked: true` and `reason`.
|
|
47
|
+
*/
|
|
48
|
+
export function makeRecord(n, artefact, unchecked) {
|
|
49
|
+
const checked = isChecked(n.status);
|
|
50
|
+
const base = {
|
|
51
|
+
stage: n.stage,
|
|
52
|
+
status: n.status,
|
|
53
|
+
contract: n.contract,
|
|
54
|
+
assumptions: n.assumptions,
|
|
55
|
+
checked,
|
|
56
|
+
artefact: artefact === undefined ? null : artefact,
|
|
57
|
+
};
|
|
58
|
+
if (unchecked && !checked) {
|
|
59
|
+
base.unchecked = true;
|
|
60
|
+
base.reason = reasonOf(n);
|
|
61
|
+
}
|
|
62
|
+
return Object.freeze(base);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A record is checked; a record without `checked` (older producer) falls back to its status. */
|
|
66
|
+
export const recordChecked = (r) => (typeof r.checked === 'boolean' ? r.checked : isChecked(r.status));
|
|
67
|
+
|
|
68
|
+
/** The union of the named assumptions of the records, first-seen order. */
|
|
69
|
+
export function unionAssumptions(records) {
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const r of records) {
|
|
73
|
+
for (const a of r.assumptions) {
|
|
74
|
+
if (!seen.has(a)) { seen.add(a); out.push(a); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `{ assumptions, proved, uncheckedSteps }` of a record list. */
|
|
81
|
+
export function summarize(records) {
|
|
82
|
+
const uncheckedSteps = records.filter((r) => !recordChecked(r)).map((r) => r.stage);
|
|
83
|
+
return { assumptions: unionAssumptions(records), proved: uncheckedSteps.length === 0, uncheckedSteps };
|
|
84
|
+
}
|
package/lib/nodes.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// The node-composition API (https://github.com/danbri/factoidal/issues/1002,
|
|
2
|
+
// step 2): registered Lean stages as nodes, each carrying its contract name
|
|
3
|
+
// and its proof status, with evidence flowing on the wires.
|
|
4
|
+
//
|
|
5
|
+
// The stage list is the engine's (op stageList, formal/lean4/Wasm/Ops/
|
|
6
|
+
// StageList.lean), read from the Lean registry
|
|
7
|
+
// L4Factoidal.Dataflow.registry: name, interface type, wire types, status,
|
|
8
|
+
// contract, named assumptions, and whether the stage may run at all
|
|
9
|
+
// (`runnable`, false for status `missing`). This file holds no copy of it.
|
|
10
|
+
//
|
|
11
|
+
// What this file adds is the binding of a stage NAME to the package
|
|
12
|
+
// function that runs it (RUNNERS below; each is the function that
|
|
13
|
+
// foundation/entrypoints.toml maps to that stage), and the wiring: a value
|
|
14
|
+
// on a wire is `{ graph, evidence }`, and each node appends one evidence
|
|
15
|
+
// record `{ stage, status, contract, assumptions, checked, artefact }` (plus
|
|
16
|
+
// `unchecked` and `reason` for a step run under run()'s `unchecked` opt-in;
|
|
17
|
+
// the record logic is in evidence.mjs). The result of
|
|
18
|
+
// a run also reports the union of the named assumptions of the nodes it
|
|
19
|
+
// passed: the premise list of the pipeline's soundness statement
|
|
20
|
+
// (Dataflow/EndToEnd.lean) as the user sees it.
|
|
21
|
+
//
|
|
22
|
+
// ES module, no Node built-ins: the browser loads this file as it is.
|
|
23
|
+
|
|
24
|
+
import { FactoidalError } from './errors.mjs';
|
|
25
|
+
import { refusalFor, makeRecord, summarize, unionAssumptions } from './evidence.mjs';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {object} deps the package functions and the engine entry
|
|
29
|
+
* @returns {{stages: Function, node: Function, pipe: Function, run: Function}}
|
|
30
|
+
*/
|
|
31
|
+
export function buildNodes(deps) {
|
|
32
|
+
const { entry, entryResult, requireEntryFn, Dataset,
|
|
33
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, explain } = deps;
|
|
34
|
+
|
|
35
|
+
const reader = (format) => async (wire, params) => {
|
|
36
|
+
if (typeof wire.graph !== 'string') {
|
|
37
|
+
throw new TypeError(`node: a reader takes text on its wire (got ${typeof wire.graph})`);
|
|
38
|
+
}
|
|
39
|
+
const ds = await parse(wire.graph, { format, ...params });
|
|
40
|
+
return { graph: ds, artefact: { format } };
|
|
41
|
+
};
|
|
42
|
+
const reasoner = (regime) => async (wire, params) => ({
|
|
43
|
+
graph: await closure(wire.graph, regime, params),
|
|
44
|
+
artefact: { regime },
|
|
45
|
+
});
|
|
46
|
+
const writer = (format) => async (wire, params) => ({
|
|
47
|
+
artefact: { format, text: await serialize(wire.graph, { ...params, format }) },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Stage name (stageList `name`) -> the package function that runs it.
|
|
51
|
+
// A runner answers `{ graph?, artefact }`; no `graph` passes the input
|
|
52
|
+
// graph on unchanged.
|
|
53
|
+
const RUNNERS = {
|
|
54
|
+
ntriplesDocumentReader: reader('ntriples'),
|
|
55
|
+
nquadsDocumentReader: reader('nquads'),
|
|
56
|
+
turtleDocumentReader: reader('turtle'),
|
|
57
|
+
trigDocumentReader: reader('trig'),
|
|
58
|
+
rdfxmlDocumentReader: reader('rdfxml'),
|
|
59
|
+
jsonldDocumentReader: reader('jsonld'),
|
|
60
|
+
rdfsReasoner: reasoner('rdfs'),
|
|
61
|
+
owlRlReasoner: reasoner('owl-rl'),
|
|
62
|
+
rhoDfReasoner: reasoner('rho-df'),
|
|
63
|
+
shaclValidator: async (wire, params) => {
|
|
64
|
+
const { shapes, ...opts } = params;
|
|
65
|
+
if (shapes === undefined) throw new TypeError('node shaclValidator: params.shapes is required');
|
|
66
|
+
const r = await shaclValidate(wire.graph, shapes, opts);
|
|
67
|
+
return { artefact: { conforms: r.conforms, report: r.report } };
|
|
68
|
+
},
|
|
69
|
+
shippedEvaluator: async (wire, params) => {
|
|
70
|
+
const { query: sparql, ...opts } = params;
|
|
71
|
+
if (typeof sparql !== 'string') throw new TypeError('node shippedEvaluator: params.query must be a string');
|
|
72
|
+
const answer = await query(wire.graph, sparql, opts);
|
|
73
|
+
return answer instanceof Dataset ? { graph: answer, artefact: { form: 'graph' } }
|
|
74
|
+
: { artefact: { answer } };
|
|
75
|
+
},
|
|
76
|
+
ntriplesDocumentWriter: writer('ntriples'),
|
|
77
|
+
nquadsDocumentWriter: writer('nquads'),
|
|
78
|
+
turtleDocumentWriter: writer('turtle'),
|
|
79
|
+
trigDocumentWriter: writer('trig'),
|
|
80
|
+
rdfxmlDocumentWriter: writer('rdfxml'),
|
|
81
|
+
rdfcStage: async (wire, params) => ({
|
|
82
|
+
artefact: { canonical: await canonicalize(wire.graph, params) },
|
|
83
|
+
}),
|
|
84
|
+
rdfsExplainGraphStage: async (wire, params) => {
|
|
85
|
+
const { description, ...opts } = params;
|
|
86
|
+
if (description === undefined) {
|
|
87
|
+
throw new TypeError('node rdfsExplainGraphStage: params.description is required');
|
|
88
|
+
}
|
|
89
|
+
return { artefact: await explain(wire.graph, description, { ...opts, profile: 'rdfs' }) };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let stageCache = null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The engine's stage list (op stageList), one element per registry entry.
|
|
97
|
+
* @returns {Promise<object[]>}
|
|
98
|
+
*/
|
|
99
|
+
async function stages() {
|
|
100
|
+
if (!stageCache) {
|
|
101
|
+
stageCache = (async () => {
|
|
102
|
+
const e = await entry();
|
|
103
|
+
requireEntryFn(e, 'stageList', 'stages');
|
|
104
|
+
const r = entryResult(e.stageList(), 'stages');
|
|
105
|
+
return Object.freeze(r.stages.map((s) => Object.freeze({
|
|
106
|
+
...s,
|
|
107
|
+
theorems: Object.freeze([...s.theorems]),
|
|
108
|
+
assumptions: Object.freeze([...s.assumptions]),
|
|
109
|
+
tests: Object.freeze([...s.tests]),
|
|
110
|
+
bound: Object.prototype.hasOwnProperty.call(RUNNERS, s.name),
|
|
111
|
+
})));
|
|
112
|
+
})();
|
|
113
|
+
stageCache.catch(() => { stageCache = null; });
|
|
114
|
+
}
|
|
115
|
+
return stageCache;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const NODE = Symbol('factoidal.node');
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A node for the registered stage `name`, with its parameters (the
|
|
122
|
+
* second argument of the package function it calls, e.g. `{shapes}`,
|
|
123
|
+
* `{query}`, `{description}`, reader options).
|
|
124
|
+
* @param {string} name a stageList `name`
|
|
125
|
+
* @param {object} [params]
|
|
126
|
+
* @returns {Promise<object>}
|
|
127
|
+
*/
|
|
128
|
+
async function node(name, params) {
|
|
129
|
+
if (typeof name !== 'string') throw new TypeError('node: name must be a string');
|
|
130
|
+
if (params !== undefined && (params === null || typeof params !== 'object')) {
|
|
131
|
+
throw new TypeError('node: params must be an object');
|
|
132
|
+
}
|
|
133
|
+
const info = (await stages()).find((s) => s.name === name);
|
|
134
|
+
if (!info) {
|
|
135
|
+
throw new FactoidalError(`node: no registered stage named '${name}' (see stages())`,
|
|
136
|
+
{ code: 'option.invalid-value' });
|
|
137
|
+
}
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
[NODE]: true,
|
|
140
|
+
stage: info.name,
|
|
141
|
+
kind: info.kind,
|
|
142
|
+
status: info.status,
|
|
143
|
+
contract: info.contract,
|
|
144
|
+
assumptions: info.assumptions,
|
|
145
|
+
wireIn: info.wireIn,
|
|
146
|
+
wireOut: info.wireOut,
|
|
147
|
+
runnable: info.runnable,
|
|
148
|
+
bound: info.bound,
|
|
149
|
+
params: Object.freeze({ ...(params || {}) }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function nodesOf(items, who) {
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const n of items) {
|
|
156
|
+
if (n && n[NODE]) out.push(n);
|
|
157
|
+
else if (n && Array.isArray(n.nodes)) out.push(...nodesOf(n.nodes, who));
|
|
158
|
+
else throw new TypeError(`${who}: expected nodes from node() or pipelines from pipe()`);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A pipeline: the nodes in order (pipelines are flattened).
|
|
165
|
+
* `assumptions` is the union of the nodes' named assumptions.
|
|
166
|
+
* @returns {{nodes: object[], assumptions: string[]}}
|
|
167
|
+
*/
|
|
168
|
+
function pipe(...items) {
|
|
169
|
+
const nodes = nodesOf(items, 'pipe');
|
|
170
|
+
if (nodes.length === 0) throw new TypeError('pipe: no nodes');
|
|
171
|
+
return Object.freeze({ nodes: Object.freeze(nodes), assumptions: Object.freeze(unionAssumptions(nodes)) });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isWire = (v) => v !== null && typeof v === 'object' && !(v instanceof Dataset) &&
|
|
175
|
+
Object.prototype.hasOwnProperty.call(v, 'graph') && Array.isArray(v.evidence);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run a node or a pipeline. `input` is a wire value `{graph, evidence}`
|
|
179
|
+
* or the graph itself (text for a reader, else a Dataset or data input).
|
|
180
|
+
* `options.unchecked: true` lets stages that are not `proved` run,
|
|
181
|
+
* `missing` ones too when the package has a runner for them; each such
|
|
182
|
+
* step's record says so (evidence.mjs). Without it, a `missing` stage
|
|
183
|
+
* refuses before any node runs.
|
|
184
|
+
* @returns {Promise<{graph: any, evidence: object[], assumptions: string[],
|
|
185
|
+
* proved: boolean, uncheckedSteps: string[]}>}
|
|
186
|
+
*/
|
|
187
|
+
async function run(pipeline, input, options) {
|
|
188
|
+
if (options !== undefined && (options === null || typeof options !== 'object')) {
|
|
189
|
+
throw new TypeError('run: options must be an object');
|
|
190
|
+
}
|
|
191
|
+
const unchecked = !!(options && options.unchecked);
|
|
192
|
+
const nodes = nodesOf([pipeline], 'run');
|
|
193
|
+
for (const n of nodes) {
|
|
194
|
+
const why = refusalFor(n, unchecked, Object.prototype.hasOwnProperty.call(RUNNERS, n.stage));
|
|
195
|
+
if (why) throw why;
|
|
196
|
+
}
|
|
197
|
+
let wire = isWire(input) ? { graph: input.graph, evidence: [...input.evidence] }
|
|
198
|
+
: { graph: input, evidence: [] };
|
|
199
|
+
let owned = null; // a Dataset an earlier node of this run made
|
|
200
|
+
for (const n of nodes) {
|
|
201
|
+
const out = await RUNNERS[n.stage](wire, n.params);
|
|
202
|
+
const record = makeRecord(n, out.artefact, unchecked);
|
|
203
|
+
let graph = wire.graph;
|
|
204
|
+
if (out.graph !== undefined && out.graph !== wire.graph) {
|
|
205
|
+
if (owned && owned === wire.graph && typeof owned.dispose === 'function') owned.dispose();
|
|
206
|
+
graph = out.graph;
|
|
207
|
+
owned = out.graph instanceof Dataset ? out.graph : null;
|
|
208
|
+
}
|
|
209
|
+
wire = { graph, evidence: [...wire.evidence, record] };
|
|
210
|
+
}
|
|
211
|
+
return { graph: wire.graph, evidence: wire.evidence, ...summarize(wire.evidence) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { stages, node, pipe, run };
|
|
215
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The Node reader of an SBM11 generation directory for lib/sbm11.mjs: the
|
|
2
|
+
// source is a directory path, and a file of the generation is read with
|
|
3
|
+
// fs.readSync at an offset (https://github.com/danbri/factoidal/issues/1009
|
|
4
|
+
// KR1). It moves bytes only (iron rule 7); the engine checks every length.
|
|
5
|
+
//
|
|
6
|
+
// One open file descriptor per file, kept while the reader lives: a query
|
|
7
|
+
// makes many small reads of the same few files.
|
|
8
|
+
|
|
9
|
+
import { closeSync, fstatSync, openSync, readSync, statSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
|
|
12
|
+
export function nodeDirectoryReader (dir) {
|
|
13
|
+
const fds = new Map()
|
|
14
|
+
function fdOf (file) {
|
|
15
|
+
let fd = fds.get(file)
|
|
16
|
+
if (fd === undefined) {
|
|
17
|
+
fd = openSync(join(dir, file), 'r')
|
|
18
|
+
fds.set(file, fd)
|
|
19
|
+
}
|
|
20
|
+
return fd
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
readAt (file, offset, length) {
|
|
24
|
+
const out = new Uint8Array(length)
|
|
25
|
+
const fd = fdOf(file)
|
|
26
|
+
let done = 0
|
|
27
|
+
while (done < length) {
|
|
28
|
+
const n = readSync(fd, out, done, length - done, offset + done)
|
|
29
|
+
if (n === 0) break
|
|
30
|
+
done += n
|
|
31
|
+
}
|
|
32
|
+
return done === length ? out : null
|
|
33
|
+
},
|
|
34
|
+
size (file) {
|
|
35
|
+
try {
|
|
36
|
+
const fd = fds.get(file)
|
|
37
|
+
const st = fd === undefined ? statSync(join(dir, file)) : fstatSync(fd)
|
|
38
|
+
return st.isFile() ? st.size : null
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
close () {
|
|
44
|
+
for (const fd of fds.values()) closeSync(fd)
|
|
45
|
+
fds.clear()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A default-reader factory: one reader per directory, kept. */
|
|
51
|
+
export function nodeDefaultReaderFactory () {
|
|
52
|
+
const byDir = new Map()
|
|
53
|
+
return (source) => {
|
|
54
|
+
let r = byDir.get(source)
|
|
55
|
+
if (!r) {
|
|
56
|
+
r = nodeDirectoryReader(source)
|
|
57
|
+
byDir.set(source, r)
|
|
58
|
+
}
|
|
59
|
+
return r
|
|
60
|
+
}
|
|
61
|
+
}
|
package/lib/sbm11.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// SPARQL over an SBM11 generation, in Node and in the browser, through the
|
|
2
|
+
// wasm ops sbm11Open / sbm11Query / sbm11Close
|
|
3
|
+
// (formal/lean4/Wasm/Ops/Sbm11.lean; https://github.com/danbri/factoidal/issues/1009 KR1).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE IS ALLOWED TO DO
|
|
6
|
+
// Move bytes. The engine asks for (source, file, offset, length) through the
|
|
7
|
+
// bridge globalThis.__factoidalReadAt, and for the size of a file through
|
|
8
|
+
// globalThis.__factoidalFileSize (the EM_JS bodies in
|
|
9
|
+
// formal/lean4/ffi/l4_hostread.c call them synchronously). This file finds
|
|
10
|
+
// the reader that serves `source` and answers. It never reads a manifest,
|
|
11
|
+
// checks a length or a digest, or decides which file a query needs: the
|
|
12
|
+
// engine names every file and checks every answer against the manifest
|
|
13
|
+
// (formal/lean4/L4Factoidal/Storage/Sbm11/HostSession.lean). Iron rule 7.
|
|
14
|
+
//
|
|
15
|
+
// READERS
|
|
16
|
+
// A reader is { readAt(file, offset, length) -> Uint8Array, size(file) ->
|
|
17
|
+
// number | null }, both synchronous: the engine is inside WebAssembly and
|
|
18
|
+
// cannot wait for a promise. Node: lib/sbm11-node.mjs reads a directory with
|
|
19
|
+
// fs. A browser worker: a reader over OPFS FileSystemSyncAccessHandle
|
|
20
|
+
// (stage 4 of https://github.com/danbri/factoidal/issues/1007). A caller
|
|
21
|
+
// registers a reader under a source name; a source with no registered reader
|
|
22
|
+
// goes to the default reader (Node: the directory of that name).
|
|
23
|
+
|
|
24
|
+
const readers = new Map()
|
|
25
|
+
let defaultReader = null
|
|
26
|
+
let bridgeInstalled = false
|
|
27
|
+
|
|
28
|
+
function readerFor (source) {
|
|
29
|
+
const r = readers.get(source)
|
|
30
|
+
if (r) return r
|
|
31
|
+
if (defaultReader) return defaultReader(source)
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function installBridge () {
|
|
36
|
+
if (bridgeInstalled) return
|
|
37
|
+
bridgeInstalled = true
|
|
38
|
+
// A thrown error and a wrong length both reach Lean as "the host gave no
|
|
39
|
+
// bytes"; the engine then refuses the read with the file name.
|
|
40
|
+
globalThis.__factoidalReadAt = (source, file, offset, length) => {
|
|
41
|
+
const r = readerFor(source)
|
|
42
|
+
if (!r) return null
|
|
43
|
+
return r.readAt(file, offset, length)
|
|
44
|
+
}
|
|
45
|
+
globalThis.__factoidalFileSize = (source, file) => {
|
|
46
|
+
const r = readerFor(source)
|
|
47
|
+
if (!r) return null
|
|
48
|
+
return r.size(file)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Serve `source` with `reader` ({ readAt(file, offset, length), size(file) },
|
|
54
|
+
* both synchronous). `null` removes the registration.
|
|
55
|
+
*/
|
|
56
|
+
export function registerSbm11Source (source, reader) {
|
|
57
|
+
installBridge()
|
|
58
|
+
if (reader === null) { readers.delete(String(source)); return }
|
|
59
|
+
if (!reader || typeof reader.readAt !== 'function' || typeof reader.size !== 'function') {
|
|
60
|
+
throw new TypeError('registerSbm11Source: reader needs readAt(file, offset, length) and size(file)')
|
|
61
|
+
}
|
|
62
|
+
readers.set(String(source), reader)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The reader for a source with no registration: `(source) => reader | null`. */
|
|
66
|
+
export function setDefaultSbm11Reader (factory) {
|
|
67
|
+
installBridge()
|
|
68
|
+
defaultReader = factory
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function envelope (raw) {
|
|
72
|
+
const j = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
73
|
+
if (!j || j.ok !== true) {
|
|
74
|
+
const e = new Error((j && j.error) || 'sbm11: unknown error')
|
|
75
|
+
e.code = 'SBM11'
|
|
76
|
+
throw e
|
|
77
|
+
}
|
|
78
|
+
return j
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The SBM11 functions over an engine loader: `loadEngine()` resolves to the
|
|
83
|
+
* engine handle (`call(op, args)`).
|
|
84
|
+
*/
|
|
85
|
+
export function sbm11Api (loadEngine) {
|
|
86
|
+
/**
|
|
87
|
+
* Open an SBM11 generation (a generation directory, or a collection root
|
|
88
|
+
* whose CURRENT file names one). Returns { generation, terms, query(sparql,
|
|
89
|
+
* options), close() }. `query` answers { path, rows, format, result, json }:
|
|
90
|
+
* `result` is the text `l4sbm11 query` writes (SPARQL JSON for SELECT and
|
|
91
|
+
* ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its parse for
|
|
92
|
+
* SELECT and ASK.
|
|
93
|
+
*/
|
|
94
|
+
async function openSbm11 (source, options = {}) {
|
|
95
|
+
installBridge()
|
|
96
|
+
if (options.reader) registerSbm11Source(source, options.reader)
|
|
97
|
+
const eng = await loadEngine()
|
|
98
|
+
const opened = envelope(eng.call('sbm11Open', [String(source)]))
|
|
99
|
+
const handle = opened.handle
|
|
100
|
+
let open = true
|
|
101
|
+
return {
|
|
102
|
+
handle,
|
|
103
|
+
generation: opened.generation,
|
|
104
|
+
terms: Number(opened.terms),
|
|
105
|
+
query (sparql, qopts = {}) {
|
|
106
|
+
if (!open) throw new Error('sbm11: the store is closed')
|
|
107
|
+
const o = {}
|
|
108
|
+
if (qopts.base != null) o.base = String(qopts.base)
|
|
109
|
+
if (qopts.sparql12) o.sparql12 = true
|
|
110
|
+
if (qopts.reference) o.reference = true
|
|
111
|
+
const args = [handle, String(sparql), JSON.stringify(o)]
|
|
112
|
+
// The byte-region entry returns the result bytes outside the
|
|
113
|
+
// envelope; a large answer is then not escaped into a JSON string
|
|
114
|
+
// and parsed again (Wasm/Ops/Sbm11.lean sbm11QueryBlob).
|
|
115
|
+
let j, bytes
|
|
116
|
+
if (typeof eng.callBlobIO === 'function') {
|
|
117
|
+
const answer = eng.callBlobIO('sbm11Query', args, new Uint8Array(0))
|
|
118
|
+
j = envelope(answer.envelope)
|
|
119
|
+
bytes = answer.bytes
|
|
120
|
+
} else {
|
|
121
|
+
j = envelope(eng.call('sbm11Query', args))
|
|
122
|
+
}
|
|
123
|
+
let text = bytes === undefined ? j.result : undefined
|
|
124
|
+
return {
|
|
125
|
+
path: j.path,
|
|
126
|
+
rows: Number(j.rows),
|
|
127
|
+
format: j.format,
|
|
128
|
+
bytesRead: Number(j.bytesRead),
|
|
129
|
+
reads: Number(j.reads),
|
|
130
|
+
/** the answer as bytes (UTF-8) */
|
|
131
|
+
get bytes () { return bytes ?? new TextEncoder().encode(text) },
|
|
132
|
+
/** the answer as text */
|
|
133
|
+
get result () {
|
|
134
|
+
if (text === undefined) text = new TextDecoder().decode(bytes)
|
|
135
|
+
return text
|
|
136
|
+
},
|
|
137
|
+
get json () { return j.format === 'srj' ? JSON.parse(this.result) : undefined }
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
close () {
|
|
141
|
+
if (!open) return
|
|
142
|
+
open = false
|
|
143
|
+
envelope(eng.call('sbm11Close', [handle]))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { openSbm11 }
|
|
148
|
+
}
|
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.14.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",
|
package/sample-store.mjs
CHANGED
|
@@ -27,7 +27,7 @@ ex:c3 a skos:Concept ; skos:inScheme ex:scheme ; skos:prefLabel "SECAM"@en ; sko
|
|
|
27
27
|
export const sampleStoreFacts = Object.freeze({
|
|
28
28
|
triples: 15,
|
|
29
29
|
source: 'SAMPLE_TURTLE in sample-store.mjs (written for this package)',
|
|
30
|
-
layout: '
|
|
30
|
+
layout: 'ibk5'
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
/**
|