@factoidal/core 0.26.0 → 0.28.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 +19 -0
- package/README.md +44 -0
- package/bin/factoidal.mjs +35 -2
- package/bin/qt4.mjs +49 -0
- package/index.d.ts +16 -3
- package/index.mjs +6 -1
- 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 +1 -1
- package/lib/nodes.mjs +21 -1
- package/lib/sbm11-lws.mjs +53 -10
- package/lib/sync-http-node.mjs +3 -2
- package/lib/version.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.28.0 (2026-10-31)
|
|
6
|
+
|
|
7
|
+
- The public LWS SBM11 reader rejects resource paths that escape its configured container before calling transport. Default Node HTTP reads refuse redirects. Browser default XHR now rejects caller-supplied headers, including authorization; authenticated browser use requires an explicit trusted, redirect-safe transport. Custom transports remain responsible for redirect and credential policy. WebDAV read-only behavior is unchanged.
|
|
8
|
+
- The Lean HTML parser improvements merged in PR269 now reach the package, including processing instructions, namespace/template handling and public parse-html regressions. This is parser support, not a browser layout engine or a claim of complete HTML conformance.
|
|
9
|
+
- Valid zero-second IETF dates no longer emit panic diagnostics. The targeted native fn-parse-ietf-date set passes 105 cases.
|
|
10
|
+
- Bounded slice and positional consumers of fn:generate evaluate finite prefixes without increasing the generation cap. Native fn-generate improves from 13 pass / 3 fail to 15 pass / 1 fail; the constructed-node identity failure remains visible. All 104 fn-slice cases pass. These targeted native results are not whole-catalog WASM conformance claims.
|
|
11
|
+
|
|
12
|
+
Native server executables and the SVG graphics experiment have separate release gates; this package version does not claim new executable or SVG compatibility coverage.
|
|
13
|
+
|
|
14
|
+
## 0.27.0 (2026-10-30)
|
|
15
|
+
|
|
16
|
+
- New CLI commands `factoidal xpath4`, `factoidal xquery` and `factoidal xslt4` expose the existing Lean XML operations. They accept explicit local input files, display JSON or the engine's serialized result, and preserve QT4 error codes such as `XPST0003`. See the README for arguments and examples.
|
|
17
|
+
- The public dataflow API now has `xpath4Eval`, `xqueryEval` and `xslt4Transform` nodes. Their output wires carry serialized XDM or XSLT results. These nodes are marked tested, with `checked: false` evidence; no evaluator correctness theorem is claimed. Existing registry IDs are preserved.
|
|
18
|
+
- Public `xpath4Eval` and `xqueryEval` now install the pure inline `fn:transform` adapter already used by the QT4 harness. It supports supplied source nodes and stylesheet text without fetching external resources. Assertions default to disabled for `fn:transform` and can be enabled explicitly; direct `xslt4Transform` retains its enabled default. A nested `fn:transform` inside a `post-process` callback remains unsupported (`FOXT0004`).
|
|
19
|
+
- QT4 fixes include assertion handling, QName environments and nested-transform resource bases (https://github.com/danbri/factoidal-core/pull/268). The native `fn-transform` set improves from 127 pass / 9 fail / 1 unsupported to 132 / 4 / 1. XPath grapheme behavior remains Unicode 15.1, with 1,189 / 0 / 0 on the pinned QT4 set. These are native measurements, not full WASM conformance claims.
|
|
20
|
+
- Schema-aware `fn:element-to-map` now uses schema components and typed values, including inherited declarations and conversions (https://github.com/danbri/factoidal-core/pull/270). Native QT4 results improve from 186 pass / 35 fail / 0 unsupported to 219 / 2 / 0; the two remaining fixture/specification disagreements remain visible. Schema-driven behavior requires a validating host and is not newly exposed through the pure public API.
|
|
21
|
+
|
|
22
|
+
The XProc atomic-step bridge to the Lean dataflow graph remains available in the Lean tree; this release adds no npm XProc operation. Native server executable publication is separate and remains blocked by the WebDAV regression failures tracked in https://github.com/danbri/factoidal/issues/1202.
|
|
23
|
+
|
|
5
24
|
## 0.26.0 (2026-10-29)
|
|
6
25
|
|
|
7
26
|
- QT4 processor feature switches (https://github.com/danbri/factoidal-core/pull/252): the native qt4tests runner can run as a processor without a feature, so the tests written for such processors run instead of being unsupported; qt4tests in XQuery mode, measured on that PR: `fn` 17188 → 17217 pass, `prod` 13819 → 13823 pass, no new failures.
|
package/README.md
CHANGED
|
@@ -123,6 +123,50 @@ console.log(r.assumptions, r.proved); // what the answer depends on
|
|
|
123
123
|
and marks those steps in the evidence. Details: API.md, "Stages as nodes" and
|
|
124
124
|
"Explaining an entailment" (`explain()` is the same check as one call).
|
|
125
125
|
|
|
126
|
+
### XPath, XQuery and XSLT 4.0
|
|
127
|
+
|
|
128
|
+
`xpath4Eval(xml, expression)`, `xqueryEval(query, xml?)`, and
|
|
129
|
+
`xslt4Transform(stylesheet, xml)` use the same Lean engines as the native
|
|
130
|
+
XML test runners. They also have nodes named `xpath4Eval`, `xqueryEval`, and
|
|
131
|
+
`xslt4Transform`. Each node reads XML context **text** (`''` means no context)
|
|
132
|
+
and requires its one string setting: `expression`, `query`, or `stylesheet`.
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { node, run } from '@factoidal/core';
|
|
136
|
+
|
|
137
|
+
const evaluate = await node('xpath4Eval', { expression: 'count(//person)' });
|
|
138
|
+
const result = await run(evaluate, '<people><person/></people>', { unchecked: true });
|
|
139
|
+
console.log(result.graph.xml); // '1', serialized by Lean
|
|
140
|
+
console.log(result.proved); // false: this adapter is tested, with no stage contract
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
XPath/XQuery nodes output `Qt4Result` (`count`, typed serialized `items`,
|
|
144
|
+
`adaptive`, nullable `xml`), not live XDM values. XSLT nodes output `Xslt4Result`
|
|
145
|
+
(`output`, `method`, `secondary`, `messages`); text or JSON results are not
|
|
146
|
+
mislabelled as XML. There is no implicit result-to-input conversion. Unknown
|
|
147
|
+
or wrongly typed settings fail in Lean. Under the existing node policy these
|
|
148
|
+
tested nodes can run, but return `proved: false` and `checked: false` evidence.
|
|
149
|
+
`unchecked: true` additionally records the explicit opt-in and its reason;
|
|
150
|
+
neither mode claims a proved evaluator contract.
|
|
151
|
+
|
|
152
|
+
The CLI has `factoidal xpath4 EXPRESSION`, `factoidal xquery QUERY` (or
|
|
153
|
+
`--file PATH`), and `factoidal xslt4 STYLESHEET_FILE`. Each accepts
|
|
154
|
+
`--input XML_FILE`; omission supplies no context item. Default output is the
|
|
155
|
+
engine's JSON envelope. XPath/XQuery accept `--format adaptive|xml`; XSLT
|
|
156
|
+
accepts `--format output` for the principal serialization. `--help` describes
|
|
157
|
+
the complete syntax. QT4 failures exit 1 and retain the error QName in JSON
|
|
158
|
+
on stderr; usage failures exit 2. Secondary results stay in the JSON envelope
|
|
159
|
+
and are not written to files.
|
|
160
|
+
|
|
161
|
+
Inline `fn:transform` uses the existing pure XSLT adapter, including its
|
|
162
|
+
`enable-assertions` option (default false). Direct `xslt4Transform` retains
|
|
163
|
+
enabled assertions. Calling `fn:transform` from its own `post-process`
|
|
164
|
+
callback currently fails with `FOXT0004`; callback reentrancy is not provided.
|
|
165
|
+
No stylesheet/module/resource fetch or schema provider
|
|
166
|
+
is added. Schema-dependent PSVI tests and XProc execution remain native/
|
|
167
|
+
host-dependent: the Lean XProc 3.1 reader and atomic dataflow bridge exist,
|
|
168
|
+
but there is no public npm XProc WASM entry point.
|
|
169
|
+
|
|
126
170
|
Formats (generated from `formats()`):
|
|
127
171
|
|
|
128
172
|
<!-- formats-table:start (generated by test/formats-table.test.mjs) -->
|
package/bin/factoidal.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
} from './store.mjs'
|
|
38
38
|
import { sbm11Api, setDefaultSbm11Reader } from '../lib/sbm11.mjs'
|
|
39
39
|
import { nodeDefaultReaderFactory } from '../lib/sbm11-node.mjs'
|
|
40
|
+
import { runQt4, Qt4UsageError } from './qt4.mjs'
|
|
40
41
|
|
|
41
42
|
const EXIT_OK = 0
|
|
42
43
|
const EXIT_FAILURE = 1
|
|
@@ -108,6 +109,9 @@ commands:
|
|
|
108
109
|
sample-store [DIR] pack a small built-in store and print its path
|
|
109
110
|
inspect STORE report what the activated manifest commits
|
|
110
111
|
query STORE [QUERY] evaluate a SPARQL query against a store
|
|
112
|
+
xpath4 EXPRESSION XPath 4.0 over --input XML_FILE (optional)
|
|
113
|
+
xquery QUERY XQuery 4.0 over --input XML_FILE (optional)
|
|
114
|
+
xslt4 STYLESHEET_FILE XSLT 4.0 over --input XML_FILE (optional)
|
|
111
115
|
pack INPUT OUTPUT build one immutable generation from an RDF file
|
|
112
116
|
activate STORE GENERATION make one generation the activated generation
|
|
113
117
|
update STORE [UPDATE] apply a SPARQL Update through the delta log
|
|
@@ -146,6 +150,23 @@ an RDF file with pack and activate, or try a small built-in one:
|
|
|
146
150
|
factoidal query "$(factoidal sample-store)" 'SELECT * WHERE { ?s ?p ?o } LIMIT 5'`
|
|
147
151
|
|
|
148
152
|
const COMMAND_USAGE = {
|
|
153
|
+
xpath4: `factoidal xpath4 - evaluate XPath 4.0
|
|
154
|
+
usage: factoidal xpath4 [EXPRESSION] [--expression TEXT | --file PATH] [--input XML_FILE] [--format json|adaptive|xml]
|
|
155
|
+
Supply exactly one expression source. No --input means no context item.
|
|
156
|
+
Default output is the engine's JSON result, including typed serialized items.
|
|
157
|
+
--format xml fails if XML serialization is unavailable (for example maps).
|
|
158
|
+
Only explicitly named files are read. External resources and schema providers are not supplied.`,
|
|
159
|
+
xquery: `factoidal xquery - evaluate XQuery 4.0
|
|
160
|
+
usage: factoidal xquery [QUERY] [--query TEXT | --file PATH] [--input XML_FILE] [--format json|adaptive|xml]
|
|
161
|
+
Supply exactly one query source. No --input means no context item.
|
|
162
|
+
Default output is the engine's JSON result; display serializations come from Lean.
|
|
163
|
+
External modules/resources and schema providers are not supplied.`,
|
|
164
|
+
xslt4: `factoidal xslt4 - transform with XSLT 4.0
|
|
165
|
+
usage: factoidal xslt4 [STYLESHEET_FILE] [--stylesheet PATH] [--input XML_FILE] [--format json|output]
|
|
166
|
+
Supply exactly one stylesheet file. No --input means no global context item.
|
|
167
|
+
Default JSON includes principal output, method, secondary results and messages.
|
|
168
|
+
--format output prints only the principal output. Secondary results are never written to files.
|
|
169
|
+
External stylesheet modules/resources and schema providers are not supplied.`,
|
|
149
170
|
'sample-store': `factoidal sample-store - pack a small built-in store, print its path
|
|
150
171
|
|
|
151
172
|
usage: factoidal sample-store [DIR] [--json]
|
|
@@ -1132,6 +1153,9 @@ class UsageError extends Error {}
|
|
|
1132
1153
|
const REPEATABLE_OPTIONS = new Set(['cors-origin', 'saml-idp', 'saml-decrypt-key', 'allow-host', 'allow-origin'])
|
|
1133
1154
|
|
|
1134
1155
|
const VALUE_OPTIONS = {
|
|
1156
|
+
xpath4: new Set(['expression', 'file', 'input', 'format']),
|
|
1157
|
+
xquery: new Set(['query', 'file', 'input', 'format']),
|
|
1158
|
+
xslt4: new Set(['stylesheet', 'input', 'format']),
|
|
1135
1159
|
version: new Set([]),
|
|
1136
1160
|
'sample-store': new Set([]),
|
|
1137
1161
|
inspect: new Set(['generation']),
|
|
@@ -2965,7 +2989,16 @@ async function commandLwsClient (positional, options) {
|
|
|
2965
2989
|
return EXIT_OK
|
|
2966
2990
|
}
|
|
2967
2991
|
|
|
2992
|
+
const qt4Host = {
|
|
2993
|
+
loadEngine,
|
|
2994
|
+
readText: (path) => new TextDecoder('utf-8', { fatal: true }).decode(readWhole(path)),
|
|
2995
|
+
out, err,
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2968
2998
|
const COMMANDS = {
|
|
2999
|
+
xpath4: (positional, options) => runQt4('xpath4', positional, options, qt4Host),
|
|
3000
|
+
xquery: (positional, options) => runQt4('xquery', positional, options, qt4Host),
|
|
3001
|
+
xslt4: (positional, options) => runQt4('xslt4', positional, options, qt4Host),
|
|
2969
3002
|
version: (positional, options) => commandVersion(options),
|
|
2970
3003
|
'sample-store': (positional, options) => commandSampleStore(positional, options),
|
|
2971
3004
|
inspect: commandInspect,
|
|
@@ -3017,7 +3050,7 @@ async function main () {
|
|
|
3017
3050
|
try {
|
|
3018
3051
|
parsed = parseArguments(argv.slice(1), VALUE_OPTIONS[command])
|
|
3019
3052
|
} catch (error) {
|
|
3020
|
-
if (error instanceof UsageError) {
|
|
3053
|
+
if (error instanceof UsageError || error instanceof Qt4UsageError) {
|
|
3021
3054
|
err(`factoidal ${command}: ${error.message}`)
|
|
3022
3055
|
return EXIT_USAGE
|
|
3023
3056
|
}
|
|
@@ -3034,7 +3067,7 @@ async function main () {
|
|
|
3034
3067
|
try {
|
|
3035
3068
|
return await COMMANDS[command](parsed.positional, parsed.options)
|
|
3036
3069
|
} catch (error) {
|
|
3037
|
-
if (error instanceof UsageError) {
|
|
3070
|
+
if (error instanceof UsageError || error instanceof Qt4UsageError) {
|
|
3038
3071
|
err(`factoidal ${command}: ${error.message}`)
|
|
3039
3072
|
err('')
|
|
3040
3073
|
err(COMMAND_USAGE[command])
|
package/bin/qt4.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// CLI transport only: read explicit files, call the existing Lean operation,
|
|
2
|
+
// display its JSON/serialization, and preserve the engine's QT4 error QName.
|
|
3
|
+
import { errorFromEnvelope } from '../lib/errors.mjs';
|
|
4
|
+
|
|
5
|
+
export class Qt4UsageError extends Error {}
|
|
6
|
+
|
|
7
|
+
export async function runQt4(command, positional, options, host) {
|
|
8
|
+
const transform = command === 'xslt4';
|
|
9
|
+
const key = transform ? 'stylesheet' : command === 'xpath4' ? 'expression' : 'query';
|
|
10
|
+
const allowed = new Set([key, 'input', 'format', 'help', ...(transform ? [] : ['file'])]);
|
|
11
|
+
for (const option of Object.keys(options)) {
|
|
12
|
+
if (!allowed.has(option)) throw new Qt4UsageError(`unknown option --${option}`);
|
|
13
|
+
}
|
|
14
|
+
if (positional.length > 1) throw new Qt4UsageError('expected at most one positional argument');
|
|
15
|
+
const sources = [positional[0], options[key], options.file].filter((value) => value !== undefined);
|
|
16
|
+
if (sources.length !== 1 || typeof sources[0] !== 'string') {
|
|
17
|
+
throw new Qt4UsageError(`supply exactly one ${key}${transform ? ' file' : ' text or --file PATH'}`);
|
|
18
|
+
}
|
|
19
|
+
if (options.input !== undefined && typeof options.input !== 'string') throw new Qt4UsageError('--input needs a file path');
|
|
20
|
+
const format = options.format ?? 'json';
|
|
21
|
+
const formats = transform ? ['json', 'output'] : ['json', 'adaptive', 'xml'];
|
|
22
|
+
if (!formats.includes(format)) throw new Qt4UsageError(`--format must be ${formats.join(', ')}`);
|
|
23
|
+
try {
|
|
24
|
+
const expression = transform || options.file !== undefined ? host.readText(sources[0]) : sources[0];
|
|
25
|
+
const input = options.input === undefined ? '' : host.readText(options.input);
|
|
26
|
+
const op = transform ? 'xslt4Transform' : command === 'xpath4' ? 'xpath4Eval' : 'xqueryEval';
|
|
27
|
+
const args = command === 'xpath4' ? [input, expression] : [expression, input];
|
|
28
|
+
const result = (await host.loadEngine()).call(op, args);
|
|
29
|
+
if (format === 'json') host.out(JSON.stringify(result, null, 2));
|
|
30
|
+
else {
|
|
31
|
+
const text = result[format];
|
|
32
|
+
if (text === null || typeof text !== 'string') {
|
|
33
|
+
host.err(`factoidal ${command}: result has no ${format} serialization; use --format json`);
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
host.out(text);
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// loadL4.call throws a plain Error with the original envelope. API wrappers
|
|
41
|
+
// normalize it separately; the direct CLI call must preserve it here too.
|
|
42
|
+
const envelope = error?.envelope;
|
|
43
|
+
const problem = envelope && typeof envelope === 'object' && envelope.ok === false
|
|
44
|
+
? errorFromEnvelope(envelope) : error;
|
|
45
|
+
host.err(JSON.stringify({ error: String(problem?.message ?? problem),
|
|
46
|
+
...(problem?.code ? { code: problem.code } : {}), ...(problem?.qtError ? { qtError: problem.qtError } : {}) }));
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -971,6 +971,7 @@ export interface EvidenceRecord {
|
|
|
971
971
|
* `wireOut`: a Dataset for the graph wires, text for String (a writer), the
|
|
972
972
|
* rows for Table (SELECT), a boolean for Answer (ASK), `[canonical, sha256]`
|
|
973
973
|
* for String × String (rdfcStage), the report object for ValidationReport,
|
|
974
|
+
* a Qt4Result for Serialized XDM result, or an Xslt4Result for XSLT result,
|
|
974
975
|
* the explain result for ExplainGraphResult. Since 0.20.0; before, a node
|
|
975
976
|
* whose output was not a Dataset left its input on the wire.
|
|
976
977
|
*/
|
|
@@ -985,7 +986,12 @@ export interface NodePort {
|
|
|
985
986
|
readonly wire: string;
|
|
986
987
|
}
|
|
987
988
|
|
|
988
|
-
/** A node: a registered stage with its parameters.
|
|
989
|
+
/** A node: a registered stage with its parameters. Public XML adapters use
|
|
990
|
+
* `xpath4Eval` with {expression}, `xqueryEval` with {query}, or
|
|
991
|
+
* `xslt4Transform` with {stylesheet}; each reads XML context text and has
|
|
992
|
+
* status tested, checked:false evidence and no proved evaluator contract.
|
|
993
|
+
* Their serialized output types are Qt4Result and Xslt4Result (see Wire).
|
|
994
|
+
*/
|
|
989
995
|
export interface FactoidalNode {
|
|
990
996
|
readonly stage: string;
|
|
991
997
|
readonly kind: string | null;
|
|
@@ -1550,14 +1556,21 @@ export interface Sbm11ReadEvidence {
|
|
|
1550
1556
|
bytes: number;
|
|
1551
1557
|
}
|
|
1552
1558
|
|
|
1553
|
-
/** A synchronous HTTP GET: `(url, headers) -> answer`.
|
|
1559
|
+
/** A trusted synchronous HTTP GET: `(url, headers) -> answer`.
|
|
1560
|
+
* It must refuse redirects before forwarding scoped headers. */
|
|
1554
1561
|
export type Sbm11Transport = (url: string, headers: Record<string, string>) =>
|
|
1555
1562
|
{ status: number; header(name: string): string | null; bytes: Uint8Array };
|
|
1556
1563
|
|
|
1557
1564
|
/** A reader of the SBM11 files held as LWS resources under the container
|
|
1558
1565
|
* `url` (one resource per file, CURRENT a resource too), by HTTP Range GETs
|
|
1559
1566
|
* when the server answers 206, else by whole GETs kept in memory. Node: the
|
|
1560
|
-
* default transport is a fetch in a worker thread
|
|
1567
|
+
* default transport is a fetch in a worker thread that refuses redirects;
|
|
1568
|
+
* close() stops it. `url` must be absolute HTTP(S), without userinfo, query
|
|
1569
|
+
* or fragment. File arguments are raw relative paths; invalid paths return
|
|
1570
|
+
* null before transport and appear in errors().
|
|
1571
|
+
* Browser worker default XHR accepts no caller auth/headers; provide an explicit
|
|
1572
|
+
* trusted transport for those. Its final-URL check cannot prevent an already
|
|
1573
|
+
* sent anonymous request or ambient same-origin browser cookies. */
|
|
1561
1574
|
export function lwsSbm11Reader(url: string, opts?: {
|
|
1562
1575
|
auth?: string; headers?: Record<string, string>; transport?: Sbm11Transport; timeoutMs?: number
|
|
1563
1576
|
}): Sbm11Reader & { evidence(): Sbm11ReadEvidence[]; errors(): string[]; close(): void };
|
package/index.mjs
CHANGED
|
@@ -107,7 +107,12 @@ export { registerSbm11Source, mapReader };
|
|
|
107
107
|
export function lwsSbm11Reader(url, opts = {}) {
|
|
108
108
|
if (typeof opts.transport === 'function') return lwsSbm11ReaderWith(url, opts);
|
|
109
109
|
const transport = nodeSyncTransport({ timeoutMs: opts.timeoutMs });
|
|
110
|
-
|
|
110
|
+
try {
|
|
111
|
+
return lwsSbm11ReaderWith(url, { ...opts, transport, onClose: () => transport.close() });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
transport.close();
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
// The persisted store
|
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-e5057984e388-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/41154673265",
|
|
7
|
+
"gitSha": "e5057984e388be6534fef19cdbb869b5b5652a17",
|
|
8
|
+
"builtAt": "2026-10-31T02:40:08Z",
|
|
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": "5ca09ab0343eac5cfea0295461af8c4ea963a8bf083afc6f3aeadf1f92ef4804",
|
|
15
|
+
"wasmBytes": 30629431,
|
|
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
|
@@ -2696,7 +2696,7 @@ function buildApi(driver) {
|
|
|
2696
2696
|
const nodeApi = buildNodes({
|
|
2697
2697
|
entry, entryResult, requireEntryFn, Dataset,
|
|
2698
2698
|
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
|
|
2699
|
-
datasetMerge,
|
|
2699
|
+
datasetMerge, xpath4Eval, xqueryEval, xslt4Transform,
|
|
2700
2700
|
});
|
|
2701
2701
|
|
|
2702
2702
|
// Controlled Identifiers v1.0 (lib/cid.mjs, issue 1100 items 7 and 9).
|
package/lib/nodes.mjs
CHANGED
|
@@ -66,7 +66,7 @@ import { refusalFor, makeRecord, summarize, unionAssumptions, unionLists } from
|
|
|
66
66
|
export function buildNodes(deps) {
|
|
67
67
|
const { entry, entryResult, requireEntryFn, Dataset,
|
|
68
68
|
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
|
|
69
|
-
datasetMerge } = deps;
|
|
69
|
+
datasetMerge, xpath4Eval, xqueryEval, xslt4Transform } = deps;
|
|
70
70
|
|
|
71
71
|
const reader = (format) => async (wire, params) => {
|
|
72
72
|
if (typeof wire.graph !== 'string') {
|
|
@@ -92,6 +92,15 @@ export function buildNodes(deps) {
|
|
|
92
92
|
// node's output wire (FactoidalNode.wireOut, the engine's), which run()
|
|
93
93
|
// checks (wireHolds); `artefact` goes in the evidence record.
|
|
94
94
|
const RUNNERS = {
|
|
95
|
+
xpath4Eval: async (wire, params) => ({
|
|
96
|
+
graph: await xpath4Eval(wire.graph, params.expression), artefact: { operation: 'xpath4Eval' },
|
|
97
|
+
}),
|
|
98
|
+
xqueryEval: async (wire, params) => ({
|
|
99
|
+
graph: await xqueryEval(params.query, wire.graph), artefact: { operation: 'xqueryEval' },
|
|
100
|
+
}),
|
|
101
|
+
xslt4Transform: async (wire, params) => ({
|
|
102
|
+
graph: await xslt4Transform(params.stylesheet, wire.graph), artefact: { operation: 'xslt4Transform' },
|
|
103
|
+
}),
|
|
95
104
|
ntriplesDocumentReader: reader('ntriples'),
|
|
96
105
|
nquadsDocumentReader: reader('nquads'),
|
|
97
106
|
turtleDocumentReader: reader('turtle'),
|
|
@@ -181,6 +190,17 @@ export function buildNodes(deps) {
|
|
|
181
190
|
case 'Table': return Array.isArray(value);
|
|
182
191
|
case 'String × String':
|
|
183
192
|
return Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === 'string');
|
|
193
|
+
// Transport shapes of the existing Lean serializers, not live XDM nodes
|
|
194
|
+
// or a promise that arbitrary result text is a well-formed XML document.
|
|
195
|
+
case 'Serialized XDM result':
|
|
196
|
+
return value !== null && typeof value === 'object' && Number.isSafeInteger(value.count) && value.count >= 0 &&
|
|
197
|
+
Array.isArray(value.items) && value.items.length === value.count &&
|
|
198
|
+
value.items.every((item) => item && typeof item.type === 'string' && typeof item.value === 'string') &&
|
|
199
|
+
typeof value.adaptive === 'string' && (value.xml === null || typeof value.xml === 'string');
|
|
200
|
+
case 'XSLT result':
|
|
201
|
+
return value !== null && typeof value === 'object' && typeof value.output === 'string' && typeof value.method === 'string' &&
|
|
202
|
+
Array.isArray(value.secondary) && value.secondary.every((item) => item && typeof item.href === 'string' && typeof item.output === 'string') &&
|
|
203
|
+
Array.isArray(value.messages) && value.messages.every((item) => typeof item === 'string');
|
|
184
204
|
default: return value !== undefined && value !== null;
|
|
185
205
|
}
|
|
186
206
|
}
|
package/lib/sbm11-lws.mjs
CHANGED
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
// mixed). The engine checks every byte it decodes against the manifest's
|
|
21
21
|
// Merkle roots (openSbm11's default `verify: 'merkle'`), so it asks for whole
|
|
22
22
|
// 65,536-byte chunks and the `.merkle` sidecars; this file does no check.
|
|
23
|
-
// Limits: no eviction of the in-memory copies, no retry.
|
|
23
|
+
// Limits: no eviction of the in-memory copies, no retry. Resource paths are
|
|
24
|
+
// confined to the configured container. A custom transport must independently
|
|
25
|
+
// refuse redirects before forwarding headers. Default Node fetch refuses them;
|
|
26
|
+
// default worker XHR accepts no caller headers and checks the final response URL
|
|
27
|
+
// only after sending. XHR may still use ambient same-origin browser cookies.
|
|
24
28
|
//
|
|
25
29
|
// EVIDENCE
|
|
26
30
|
// `evidence()` lists each resource read: { resource, etag, lastModified,
|
|
@@ -35,13 +39,41 @@
|
|
|
35
39
|
// browser worker it is a synchronous XMLHttpRequest. A browser window
|
|
36
40
|
// thread has neither: pass `transport` or run the engine in a worker.
|
|
37
41
|
|
|
42
|
+
// This reader grants its transport authority only within one HTTP container.
|
|
43
|
+
// A file is a raw relative filename/path, never a URL reference. Reject both
|
|
44
|
+
// literal traversal and encoded structural characters before attaching headers.
|
|
38
45
|
function containerUrl (url) {
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
let base
|
|
47
|
+
try { base = new URL(String(url)) } catch { throw new TypeError('lwsSbm11Reader: expected an absolute HTTP(S) container URL') }
|
|
48
|
+
if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password || /[?#]/.test(base.href)) {
|
|
49
|
+
throw new TypeError('lwsSbm11Reader: container must be HTTP(S), without userinfo, query or fragment; use opts.auth/headers')
|
|
50
|
+
}
|
|
51
|
+
if (!base.pathname.endsWith('/')) base.pathname += '/'
|
|
52
|
+
return base
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function resourceUrl (base, file) {
|
|
44
|
-
|
|
56
|
+
if (typeof file !== 'string' || file.length === 0 || file.length > 4096) {
|
|
57
|
+
throw new TypeError('invalid relative resource path')
|
|
58
|
+
}
|
|
59
|
+
let probe = file
|
|
60
|
+
for (;;) {
|
|
61
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(probe) || /[\\?#\x00-\x1f\x7f]/.test(probe) ||
|
|
62
|
+
probe.split('/').some(part => part === '' || part === '.' || part === '..') ||
|
|
63
|
+
/%(?:2f|5c|3f|23)/i.test(probe)) {
|
|
64
|
+
throw new TypeError('resource path must remain inside the configured container')
|
|
65
|
+
}
|
|
66
|
+
// Decode only for rejection, never for the requested filename. Each changed
|
|
67
|
+
// pass shortens the string, so nested percent encodings are also bounded.
|
|
68
|
+
const next = probe.replace(/%([a-f0-9]{2})/gi, (_match, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
69
|
+
if (next === probe) break
|
|
70
|
+
probe = next
|
|
71
|
+
}
|
|
72
|
+
const resource = new URL(file.split('/').map(encodeURIComponent).join('/'), base)
|
|
73
|
+
if (resource.origin !== base.origin || !resource.pathname.startsWith(base.pathname)) {
|
|
74
|
+
throw new TypeError('resource URL escaped the configured container')
|
|
75
|
+
}
|
|
76
|
+
return resource.href
|
|
45
77
|
}
|
|
46
78
|
|
|
47
79
|
/** A synchronous GET with XMLHttpRequest (a browser worker). */
|
|
@@ -51,6 +83,9 @@ export function xhrSyncTransport (url, headers) {
|
|
|
51
83
|
x.responseType = 'arraybuffer'
|
|
52
84
|
for (const [k, v] of Object.entries(headers)) x.setRequestHeader(k, v)
|
|
53
85
|
x.send()
|
|
86
|
+
// XHR follows redirects internally. This rejects returned bytes, not the
|
|
87
|
+
// already-sent request. Caller headers require an explicit trusted transport.
|
|
88
|
+
if (x.responseURL !== new URL(url).href) throw new Error('LWS reader: redirected XHR resource refused')
|
|
54
89
|
return {
|
|
55
90
|
status: x.status,
|
|
56
91
|
header: (name) => x.getResponseHeader(name),
|
|
@@ -73,26 +108,27 @@ function unsatisfiedTotal (value) {
|
|
|
73
108
|
* A reader ({ readAt, size, evidence, close }) of the SBM11 files held as
|
|
74
109
|
* LWS resources under the container `url`.
|
|
75
110
|
*
|
|
76
|
-
* @param {string} url
|
|
111
|
+
* @param {string} url absolute HTTP(S), no userinfo/query/fragment; container that holds CURRENT and the generation
|
|
77
112
|
* (or the generation container itself)
|
|
78
113
|
* @param {object} [opts]
|
|
79
|
-
* @param {Function} [opts.transport]
|
|
114
|
+
* @param {Function} [opts.transport] trusted synchronous GET; must constrain redirect/header forwarding
|
|
80
115
|
* @param {string} [opts.auth] a value for the Authorization header, such as
|
|
81
116
|
* `Bearer <access token>` (an LWS access token)
|
|
82
|
-
* @param {object} [opts.headers] further request headers
|
|
117
|
+
* @param {object} [opts.headers] further request headers; browser use requires explicit trusted transport
|
|
83
118
|
*/
|
|
84
119
|
export function lwsSbm11Reader (url, opts = {}) {
|
|
85
120
|
const base = containerUrl(url)
|
|
121
|
+
const extra = { ...(opts.headers || {}) }
|
|
122
|
+
if (opts.auth) extra.authorization = String(opts.auth)
|
|
86
123
|
let transport = opts.transport
|
|
87
124
|
if (typeof transport !== 'function') {
|
|
88
125
|
if (typeof XMLHttpRequest === 'function' && typeof document === 'undefined') {
|
|
126
|
+
if (Object.keys(extra).length > 0) throw new TypeError('lwsSbm11Reader: caller headers require an explicit redirect-safe transport; default XHR cannot constrain forwarding')
|
|
89
127
|
transport = xhrSyncTransport
|
|
90
128
|
} else {
|
|
91
129
|
throw new TypeError('lwsSbm11Reader: no synchronous transport here; pass opts.transport (Node: @factoidal/core gives one)')
|
|
92
130
|
}
|
|
93
131
|
}
|
|
94
|
-
const extra = { ...(opts.headers || {}) }
|
|
95
|
-
if (opts.auth) extra.authorization = String(opts.auth)
|
|
96
132
|
// file -> { url, etag, lastModified, total, whole, requests, partial, bytes }
|
|
97
133
|
const files = new Map()
|
|
98
134
|
const errors = []
|
|
@@ -100,7 +136,12 @@ export function lwsSbm11Reader (url, opts = {}) {
|
|
|
100
136
|
function entry (file) {
|
|
101
137
|
let e = files.get(file)
|
|
102
138
|
if (!e) {
|
|
103
|
-
|
|
139
|
+
let url
|
|
140
|
+
try { url = resourceUrl(base, file) } catch (error) {
|
|
141
|
+
fail(file, error.message)
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
e = { url, etag: null, lastModified: null, total: undefined, whole: null, requests: 0, partial: 0, bytes: 0 }
|
|
104
145
|
files.set(file, e)
|
|
105
146
|
}
|
|
106
147
|
return e
|
|
@@ -136,6 +177,7 @@ export function lwsSbm11Reader (url, opts = {}) {
|
|
|
136
177
|
|
|
137
178
|
function sizeOf (file) {
|
|
138
179
|
const e = entry(file)
|
|
180
|
+
if (e === null) return null
|
|
139
181
|
if (e.total !== undefined) return e.total
|
|
140
182
|
const r = get(e, file, 0, 0)
|
|
141
183
|
if (r === null) return null
|
|
@@ -161,6 +203,7 @@ export function lwsSbm11Reader (url, opts = {}) {
|
|
|
161
203
|
return {
|
|
162
204
|
readAt (file, offset, length) {
|
|
163
205
|
const e = entry(file)
|
|
206
|
+
if (e === null) return null
|
|
164
207
|
if (length === 0) return new Uint8Array(0)
|
|
165
208
|
if (e.whole !== null) {
|
|
166
209
|
return offset + length <= e.whole.length ? e.whole.subarray(offset, offset + length) : null
|
package/lib/sync-http-node.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// The engine is inside WebAssembly and asks for bytes synchronously; Node's
|
|
4
4
|
// fetch is asynchronous. A worker thread makes the fetch; this thread waits
|
|
5
5
|
// on a shared Int32Array (Atomics.wait) and takes the answer with
|
|
6
|
-
// receiveMessageOnPort.
|
|
6
|
+
// receiveMessageOnPort. Redirects are refused before forwarding scoped headers.
|
|
7
|
+
// No RDF or response-header decision is made here.
|
|
7
8
|
//
|
|
8
9
|
// The thread that calls the transport is blocked until the answer comes, so
|
|
9
10
|
// the server must not run on that thread (start it in another process or
|
|
@@ -18,7 +19,7 @@ const flag = new Int32Array(signal)
|
|
|
18
19
|
port.on('message', async (req) => {
|
|
19
20
|
let out
|
|
20
21
|
try {
|
|
21
|
-
const r = await fetch(req.url, { headers: req.headers, redirect: '
|
|
22
|
+
const r = await fetch(req.url, { headers: req.headers, redirect: 'error' })
|
|
22
23
|
const headers = {}
|
|
23
24
|
r.headers.forEach((v, k) => { headers[k] = v })
|
|
24
25
|
out = { status: r.status, headers, bytes: new Uint8Array(await r.arrayBuffer()) }
|
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.28.0",
|
|
4
4
|
"description": "EXPERIMENTAL UNREVIEWED CODE: do not run public services on it without careful independent expert review. RDF parsing and writing (Turtle, TriG, N-Triples, N-Quads, RDF/XML, JSON-LD), SPARQL 1.1 and 1.2, RDFS and OWL reasoning, SHACL and ShEx validation, CSVW and RML mapping, RDFC-1.0, and SPARQL, LWS and Solid servers, for Node, Deno and browsers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sparql",
|