@factoidal/core 0.11.0 → 0.12.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 +382 -0
- package/README.md +63 -3
- package/api.d.ts +2 -3
- package/api.mjs +2 -2
- package/bin/factoidal.mjs +139 -3
- package/browser.d.ts +3 -2
- package/browser.js +1 -2
- package/fn.js +8 -12
- package/index.d.ts +83 -28
- package/index.mjs +1 -2
- 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 +93 -203
- package/lib/dataset.mjs +40 -45
- package/lib/delta-log-hosts.mjs +9 -8
- package/lib/errors.mjs +97 -0
- package/lib/lean-entry.mjs +15 -10
- package/lib/store.mjs +4 -5
- package/lib/version.mjs +1 -1
- package/lws/README.md +70 -3
- package/lws/persist.mjs +237 -6
- package/lws/server.mjs +271 -9
- package/package.json +2 -1
- package/rdfjs.mjs +4 -2
- package/solid/server/index.mjs +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,387 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
One error model for the whole package
|
|
6
|
+
(https://github.com/danbri/factoidal/issues/882 item 4,
|
|
7
|
+
https://github.com/danbri/factoidal/issues/901 KR 3.1). Every op error
|
|
8
|
+
envelope of the engine carries a stable `code`, the input `position` where
|
|
9
|
+
the input defines one, and the `spec` section where one applies; the engine
|
|
10
|
+
decides all three (`formal/lean4/Wasm/Ops/ErrorModel.lean`). The package
|
|
11
|
+
copies them into one class, `FactoidalError`. The README "Errors" table is
|
|
12
|
+
generated from the engine's list.
|
|
13
|
+
|
|
14
|
+
### Breaking changes
|
|
15
|
+
|
|
16
|
+
- `DatasetHandle`, the name of `Dataset` before 0.10.0, is removed. Use
|
|
17
|
+
`Dataset`.
|
|
18
|
+
|
|
19
|
+
- `ParseError` is removed. Every function rejects with `FactoidalError`
|
|
20
|
+
(`code`, `position`, `spec`, `cause`); a parse error has
|
|
21
|
+
`code: 'parse.syntax'` (or `parse.rdf12-required`, `term.invalid-iri`,
|
|
22
|
+
`term.invalid-langtag`) and `err.position.{line, column, offset}` in place
|
|
23
|
+
of `err.line`, `err.column`, `err.offset`. `position.byteOffset` is new.
|
|
24
|
+
- Error messages are the engine's text, with no `<function>: l4factoidal:`
|
|
25
|
+
prefix. A parse with an inferred format no longer appends "(format inferred
|
|
26
|
+
as ...)" to the message; `err.formatInferred` and `err.format` say it.
|
|
27
|
+
- An unknown option key, an unknown format name, an `entail`, `output`,
|
|
28
|
+
`version`, `owlClosure` mode, `rmlMap` source kind or `Dataset.closure`
|
|
29
|
+
regime outside the engine's tables, and options that cannot be combined
|
|
30
|
+
were a `TypeError`; each is now a `FactoidalError` (`option.unknown`,
|
|
31
|
+
`format.unknown`, `option.invalid-value`, `regime.refused`,
|
|
32
|
+
`option.conflict`) decided by the new engine op `apiCheck`. A `TypeError`
|
|
33
|
+
stays only for an argument of the wrong JavaScript type.
|
|
34
|
+
- A method of a disposed `Dataset` was a `TypeError`; it is now the engine's
|
|
35
|
+
`FactoidalError` with code `handle.disposed`. A closed `Store` is the same.
|
|
36
|
+
- `queryRaw` rejected with an `Error` whose message began "SPARQL query
|
|
37
|
+
failed:" and which had `stderr`; it now rejects with the engine's
|
|
38
|
+
`FactoidalError` (`query.syntax` with a position, or `query.failed`).
|
|
39
|
+
- File-system and IndexedDB failures (`openStore`, the delta log) are
|
|
40
|
+
`FactoidalError` with code `host.io` and the original error as `cause`.
|
|
41
|
+
- `@factoidal/core/fn` `entail()` with an `x-ikl-*` regime rejects with code
|
|
42
|
+
`regime.refused` and the message names `query`, the function that refuses.
|
|
43
|
+
|
|
44
|
+
Linked Web Storage kept in files, stage 1 (https://github.com/danbri/factoidal/issues/918;
|
|
45
|
+
design record `docs/designissues/2026-10-22-lws-storage-on-disk.md`), decided in
|
|
46
|
+
Lean (`L4Factoidal/LWS/Journal.lean`, `FileLayout.lean`):
|
|
47
|
+
|
|
48
|
+
- `factoidal lws-serve DIR --storage files` (config member `storage: "files"`,
|
|
49
|
+
`listen({ storage: 'files' })`; the default stays `snapshot`): DIR holds one
|
|
50
|
+
file per resource body under `bodies/`, a journal `lws-journal` with one
|
|
51
|
+
record per change, and a metadata file `lws-files.json` that the journal is
|
|
52
|
+
folded into when it passes max(1 MiB, the size of the metadata file). A
|
|
53
|
+
write no longer rewrites the whole storage: a PUT into a storage of 1,000,
|
|
54
|
+
2,000 and 4,000 resources took 2.70, 2.40 and 2.89 ms, against 42.6, 83.9 and
|
|
55
|
+
170.4 ms with the snapshot (`tests/lws/scaling.mjs`, rows "storage").
|
|
56
|
+
- A journal record is valid only when it is complete and its SHA-256 matches;
|
|
57
|
+
a torn last record is ignored at start (theorem `decodeFrames_torn`).
|
|
58
|
+
Replaying the records of a sequence of steps gives the metadata of the store
|
|
59
|
+
after them (`replay_records`, `persist_replay`).
|
|
60
|
+
- With `FACTOIDAL_LWS_KEY` every body file and journal record is sealed with
|
|
61
|
+
ChaCha20-Poly1305 (HACL*), the file's own name in the associated data, and
|
|
62
|
+
`lws-files.json` as the snapshot is.
|
|
63
|
+
- An existing `lws-state.json` is migrated at the first start in files mode and
|
|
64
|
+
renamed to `lws-state.json.migrated`.
|
|
65
|
+
- New ops `lwsLoad` and `lwsPersist` (byte-region entry only): the engine names
|
|
66
|
+
the files to read and gives every file operation and byte; the host
|
|
67
|
+
(`lws/persist.mjs`) performs them. `tools/lws-touchstone.sh --storage files`.
|
|
68
|
+
- Stage 1 still holds the whole storage in memory; stage 2 (bodies read on GET,
|
|
69
|
+
the metadata in an on-disk index) is an expected failure in
|
|
70
|
+
`tests/lws/scaling.mjs` naming issue 918.
|
|
71
|
+
|
|
72
|
+
Linked Web Storage Search and Type Index Services (`lws10-index`, w3c/lws-protocol
|
|
73
|
+
`9a08ff65`; https://github.com/danbri/factoidal/issues/912), decided in Lean
|
|
74
|
+
(`L4Factoidal/LWS/TypeIndex.lean`, `Search.lean`):
|
|
75
|
+
|
|
76
|
+
- The storage description advertises a `TypeIndexService`
|
|
77
|
+
(`GET /.well-known/lws-type-index`, a paged `TypeIndex`) and a
|
|
78
|
+
`TypeSearchService` (`QUERY /.well-known/lws-type-search` with an
|
|
79
|
+
`application/lws-query+json` filter in conjunctive normal form over `type`
|
|
80
|
+
and indexed relations such as `describedby`; a paged ContainerPage).
|
|
81
|
+
Option `typeIndex: false` of `listen`/`openLwsStorage` turns both off.
|
|
82
|
+
- Types come from the native class, from `Link: <T>; rel="type"` on POST and
|
|
83
|
+
on `Prefer: set-linkset` PUT and PATCH, and from `rdf:type` in Turtle
|
|
84
|
+
content up to 1 MiB. Such a `type` link was refused with 409 before; it is
|
|
85
|
+
now a declared type and appears in the linkset document. A `type` link
|
|
86
|
+
that names an LWS or LDP class is still refused with 409. A PUT or PATCH
|
|
87
|
+
that applies `Prefer: set-linkset` answers `Preference-Applied: set-linkset`.
|
|
88
|
+
- The index is kept by the store on every write and rebuilt in one pass when
|
|
89
|
+
a snapshot is opened. Answers, `totalItems` and the type index hold only
|
|
90
|
+
what the requesting agent may read now. Theorems `ixSearch_eq_specSearch`
|
|
91
|
+
and `searchAnswer_authorized`.
|
|
92
|
+
|
|
93
|
+
Linked Web Storage Access Requests and Grants (LWS core §Access Requests
|
|
94
|
+
and Grants, w3c/lws-protocol `9a08ff65`;
|
|
95
|
+
https://github.com/danbri/factoidal/issues/913), decided in Lean
|
|
96
|
+
(`L4Factoidal/LWS/AccessPolicy.lean`, `AccessGrants.lean`):
|
|
97
|
+
|
|
98
|
+
- The storage description advertises an `AccessRequestService`
|
|
99
|
+
(`/.well-known/lws-access-requests/`) and an `AccessGrantService`
|
|
100
|
+
(`/.well-known/lws-access-grants/`), each with `conformsTo`
|
|
101
|
+
`https://www.w3.org/ns/lws#AccessProfile`. Both endpoints are LWS
|
|
102
|
+
containers: GET lists (paged), POST of an `application/lws+json` document
|
|
103
|
+
answers 201 with `Location`, GET and DELETE act on one member. Option
|
|
104
|
+
`accessGrants: false` of `listen`/`openLwsStorage` turns both off.
|
|
105
|
+
- Any authenticated agent creates an access request; only the storage
|
|
106
|
+
controller (`acl:Control` on the storage root) creates or revokes a grant.
|
|
107
|
+
A document without `type`, `storage` (this storage), `access`, a policy
|
|
108
|
+
`type` with `AccessPolicy`, `action` (`read`, `modify`, `create`,
|
|
109
|
+
`delete`), `assignee`, or with a bad `inbox`, `target` or constraint,
|
|
110
|
+
answers 400.
|
|
111
|
+
- A grant is part of the access policy beside Web Access Control: it permits
|
|
112
|
+
exactly its actions on exactly its targets (a container grant does not
|
|
113
|
+
reach the members) to its assignee (`foaf:Agent` for the public), when
|
|
114
|
+
every constraint holds: `dateTime` (`eq`, `gt`, `gteq`, `lt`, `lteq`),
|
|
115
|
+
`format` and `type` of the resource, `client` (the token's `client_id`)
|
|
116
|
+
with `eq` or `isAnyOf`; `purpose` is accepted and no request satisfies it.
|
|
117
|
+
A move needs `delete` of the resource and `create` in the destination.
|
|
118
|
+
Notification subscriptions and deliveries, and the type index answers,
|
|
119
|
+
see the same access. Theorems `grantsPermit_sound`,
|
|
120
|
+
`policyPermits_excluded`, `recordPermits_expired`, `grantsPermit_revoke`,
|
|
121
|
+
`grantAllows_acl`.
|
|
122
|
+
- A new grant is announced (one `Create` notification) to its `inbox` and
|
|
123
|
+
to the inbox of each access request naming one of its assignees. Grants
|
|
124
|
+
and requests are kept in the storage snapshot (member `access`).
|
|
125
|
+
- The storage controller hears of a new access request
|
|
126
|
+
(https://github.com/danbri/factoidal/issues/920). Core §Access Requests and
|
|
127
|
+
Grants: "When a new access request is submitted, the storage controller
|
|
128
|
+
SHOULD be notified." The draft names no inbox for the controller, so this
|
|
129
|
+
server's is configuration: `listen({ controllerInbox: URL })` or
|
|
130
|
+
`factoidal lws-serve --controller-inbox URL`. When it is set, each new
|
|
131
|
+
access request gives one `Create` notification there (signed, retried, as
|
|
132
|
+
every delivery); when it is not set, nothing is sent. An inbox that is not
|
|
133
|
+
`http` or `https` stops the start.
|
|
134
|
+
|
|
135
|
+
Linked Web Storage moves and notifications, three more rules
|
|
136
|
+
(w3c/lws-protocol `9a08ff65`), decided in Lean:
|
|
137
|
+
|
|
138
|
+
- 301 at the previous identifiers of a moved resource
|
|
139
|
+
(https://github.com/danbri/factoidal/issues/914;
|
|
140
|
+
`L4Factoidal/LWS/MoveRedirect.lean`). §Moving Resources: "Such a server
|
|
141
|
+
SHOULD respond to subsequent requests on the previous identifiers of the
|
|
142
|
+
resource and of its auxiliary resources with 301 Moved Permanently,
|
|
143
|
+
including a `Location` header field pointing at the new identifier." After
|
|
144
|
+
a move, a request to the old path of the resource, of each descendant of a
|
|
145
|
+
moved container, of its `.acl` and `.meta` and of its `?linkset` answers
|
|
146
|
+
301 with the new IRI, when the same request at the new path would be
|
|
147
|
+
authorized (§Privacy Considerations, "Redirects": "Servers SHOULD apply the
|
|
148
|
+
same access control to such responses as to the moved resource itself");
|
|
149
|
+
otherwise the old path answers as before. A chain of moves answers the
|
|
150
|
+
current path. The table holds at most `redirectsMax` pairs (default 256,
|
|
151
|
+
`lws-serve --redirects-max N`), is kept in the snapshot (member `moved`),
|
|
152
|
+
and loses a pair when a new resource is created at its old path or the
|
|
153
|
+
moved resource is deleted. Theorems `redirect_current`, `after_ok`,
|
|
154
|
+
`redirect_after_move`.
|
|
155
|
+
- A webhook inbox that names this server under another host name, an IP
|
|
156
|
+
address or `localhost`, at its port, is refused with 400
|
|
157
|
+
(https://github.com/danbri/factoidal/issues/915;
|
|
158
|
+
`L4Factoidal/LWS/InboxGuard.lean`). The engine compares the inbox
|
|
159
|
+
authority with the base IRI, the bound address and the interface
|
|
160
|
+
addresses; for a host name it answers `lwsStep` with `resolve: {host}`, the
|
|
161
|
+
host resolves the name and steps the request again with `resolved`. The
|
|
162
|
+
host passes `listen: {address, port, addresses}` to `lwsOpen`.
|
|
163
|
+
- A delivery rate limit per subscription (issue 915; the webhook suite:
|
|
164
|
+
"Rate limiting and endpoint verification are suggested to prevent abuse of
|
|
165
|
+
the subscription mechanism"): at most `deliveryLimit` deliveries (default
|
|
166
|
+
10) in a window of `deliveryWindow` seconds (default 60;
|
|
167
|
+
`lws-serve --delivery-limit N --delivery-window S`; `deliveryLimit: 0`
|
|
168
|
+
turns it off). The activities over the limit wait and go, when the window
|
|
169
|
+
ends, in one envelope with an `activity` array (core §Batching
|
|
170
|
+
Notifications). An answer may carry `wake: SECONDS`; the host then calls
|
|
171
|
+
the new op `lwsFlush`. Theorem `admits_le`: at most `deliveryLimit`
|
|
172
|
+
deliveries per subscription in one window. Endpoint verification: the
|
|
173
|
+
drafts define no procedure (no request, no challenge, no expected answer),
|
|
174
|
+
so there is no behaviour to implement.
|
|
175
|
+
|
|
176
|
+
Linked Web Storage, three rules of the editor's draft (w3c/lws-protocol
|
|
177
|
+
`9a08ff65`, 2026-10-19), all decided in Lean:
|
|
178
|
+
|
|
179
|
+
- Container pagination (§LWS Media Type, Pagination;
|
|
180
|
+
https://github.com/danbri/factoidal/issues/887). A container with more
|
|
181
|
+
than `pageSize` members (option of `listen`/`openLwsStorage`, default 100,
|
|
182
|
+
0 turns it off) answers in pages: `Link` relations `first`, `last`, `next`
|
|
183
|
+
(not on the last page) and `prev` (not on the first page), page URIs
|
|
184
|
+
`CONTAINER?page=N`, `totalItems` over the full membership, one `ETag` per
|
|
185
|
+
page. Members are listed in increasing path order, each once (theorem
|
|
186
|
+
`pageItems_concat`).
|
|
187
|
+
- Client-managed links (§Metadata, §Update Resource;
|
|
188
|
+
https://github.com/danbri/factoidal/issues/888): `Link` with
|
|
189
|
+
`Prefer: set-linkset` on PUT (replace) and PATCH (add), `Link` on POST, and
|
|
190
|
+
JSON Patch or PUT of `application/linkset+json` on `PATH?linkset`.
|
|
191
|
+
Server-managed relations (`linkset`, `type`, `format`, `size`, `modified`,
|
|
192
|
+
`up`, `items`, `lws#storage`) answer 409; a malformed link answers 400.
|
|
193
|
+
Client links are kept in the storage snapshot.
|
|
194
|
+
- Notifications (§Notifications and the webhook notification suite
|
|
195
|
+
`lws10-notifications-webhook`;
|
|
196
|
+
https://github.com/danbri/factoidal/issues/890). The storage description
|
|
197
|
+
advertises a `NotificationService` (`subscriptionType`
|
|
198
|
+
`["WebhookSubscription"]`, endpoint `/.well-known/lws-subscriptions/`) and
|
|
199
|
+
an Ed25519 `JsonWebKey` verification method. An authenticated POST of a
|
|
200
|
+
`WebhookSubscription` (`topic`, `inbox`, optional `expires`) answers 201;
|
|
201
|
+
each topic must be readable by the agent (403 otherwise). Every successful
|
|
202
|
+
write gives Activity Streams 2.0 `Create` (with `target`), `Update` or
|
|
203
|
+
`Delete` (with `origin`) activities, sent only to subscribers who may read
|
|
204
|
+
the resource at that time, as an `application/lws+json` POST to the inbox,
|
|
205
|
+
signed with HTTP Message Signatures (RFC 9421) and `Content-Digest`
|
|
206
|
+
(RFC 9530). The endpoint lists the agent's subscriptions; GET and DELETE
|
|
207
|
+
manage one. A delivery answered 5xx, 408, 425, 429 or not at all is tried
|
|
208
|
+
up to three times; 410 or three failed deliveries deactivate the
|
|
209
|
+
subscription. Subscriptions and the signing key are kept in the storage
|
|
210
|
+
snapshot. The host makes the requests the engine names and reports each
|
|
211
|
+
status to the new op `lwsDelivered`; `listen`/`openLwsStorage` take
|
|
212
|
+
`notifications: false` to turn it off.
|
|
213
|
+
- Moving resources (§Moving Resources;
|
|
214
|
+
https://github.com/danbri/factoidal/issues/914). The storage description
|
|
215
|
+
describes a `MoveResource` capability (`appliesTo` DataResource and
|
|
216
|
+
Container, `preservesIdentifier` false). A JSON Patch (or PUT) of a
|
|
217
|
+
resource's linkset that changes its `up` link moves it to that container,
|
|
218
|
+
with its descendants when it is a container, in one step: the answer is
|
|
219
|
+
200 with the new linkset and `Content-Location`, both containers get a new
|
|
220
|
+
ETag, and a subscriber hears one `Move` activity (`origin`, `target`). A
|
|
221
|
+
destination that is missing or not a container, a cycle, the storage
|
|
222
|
+
root, a name already taken, another storage or a removed `up` link is
|
|
223
|
+
409. The agent needs the delete of every moved resource and the create in
|
|
224
|
+
the destination. `listen`/`openLwsStorage` take `moves: false` to turn it
|
|
225
|
+
off. Theorem `move_tree`: a move keeps the containment hierarchy a tree.
|
|
226
|
+
- Three server options, each off by default and decided in Lean
|
|
227
|
+
configuration, so that every Touchstone test that one of our
|
|
228
|
+
configurations can satisfy runs (`tools/lws-touchstone.sh --config all`):
|
|
229
|
+
`lws-serve --moves all|data|off` (`moves: "data"` describes `appliesTo`
|
|
230
|
+
`[DataResource]` and answers a change of a container's `up` link with 409;
|
|
231
|
+
`"off"` describes no `MoveResource`), `--no-linkset-put` (`linksetPut:
|
|
232
|
+
false`: a linkset's `Allow` leaves out PUT and a PUT answers 405 with
|
|
233
|
+
`Allow`, as lws10-core §Metadata requires when PUT is not advertised) and
|
|
234
|
+
`--paginate-always` (`paginateAlways: true`: a container listing that fits
|
|
235
|
+
on one page is still one paginated page, with `rel="first"` and
|
|
236
|
+
`rel="last"` and no `rel="next"` or `rel="prev"`). An unknown `moves`
|
|
237
|
+
value stops the start.
|
|
238
|
+
- Fixed: on a paginated container, `If-Match` with the `ETag` of its first
|
|
239
|
+
page (which a GET of the container URI answers) failed with 412; the
|
|
240
|
+
first page's tags are now current tags of the container.
|
|
241
|
+
|
|
242
|
+
Encryption at rest of the LWS and Solid storage snapshot, off by default
|
|
243
|
+
(https://github.com/danbri/factoidal/issues/921; owner, 2026-10-21: "Yes env
|
|
244
|
+
var please. Again off by default but good to show we can."):
|
|
245
|
+
|
|
246
|
+
- When the environment variable `FACTOIDAL_LWS_KEY` holds a 32-byte key (64
|
|
247
|
+
hex digits or base64url), `lws-serve` and `solid-serve` write
|
|
248
|
+
`DIR/lws-state.json` as format `lws-snapshot-sealed-1`: format id, cipher
|
|
249
|
+
id `chacha20-poly1305`, key id (first 8 bytes of SHA-256 of the key), a
|
|
250
|
+
12-byte nonce new for every write, and the ChaCha20-Poly1305 (RFC 8439,
|
|
251
|
+
HACL*) ciphertext of the snapshot, with the header as associated data.
|
|
252
|
+
`listen`/`openLwsStorage`/`openSolidStorage` take `storageKey` (text, or
|
|
253
|
+
`null` to read no key).
|
|
254
|
+
- At start the file is authenticated before use. A wrong key, a changed or
|
|
255
|
+
truncated file, a clear-text file with the variable set and a sealed file
|
|
256
|
+
without it each stop the start with a one-line message (exit 1). With the
|
|
257
|
+
variable unset, behaviour is unchanged.
|
|
258
|
+
- It protects the file on disk only: not the memory of a running server, not
|
|
259
|
+
the key in its environment, not the answers to clients.
|
|
260
|
+
- Decided in Lean (`L4Factoidal/LWS/SnapshotSeal.lean`), with theorems over
|
|
261
|
+
any AEAD with RFC 8439 correctness (`unsealDoc_sealDoc`,
|
|
262
|
+
`unsealDoc_authenticated`, `unsealDoc_wrongKey`, ...). `lwsSnapshot` and
|
|
263
|
+
`solidSnapshot` take an optional second argument `{"nonce":…}`.
|
|
264
|
+
- Cost with a key (2026-10-21, about 1,050 resources): a write takes about
|
|
265
|
+
2.7 times as long (133 to 143 ms against 49 to 54 ms), the file is 1.33
|
|
266
|
+
times larger, a restart about 2.1 times as long.
|
|
267
|
+
|
|
268
|
+
### Breaking changes
|
|
269
|
+
|
|
270
|
+
- The storage description has a second service, `NotificationService`, and
|
|
271
|
+
`verificationMethod` and `authentication` members; its `ETag` is a digest
|
|
272
|
+
of the document.
|
|
273
|
+
- A container listing is in increasing path order. Before, it was in the
|
|
274
|
+
order of the store (newest write first).
|
|
275
|
+
- A container with more than 100 members returns only its first 100 members
|
|
276
|
+
unless `pageSize` is set; follow `rel="next"`.
|
|
277
|
+
- PUT on a linkset resource replaces the client links. Before, it answered
|
|
278
|
+
405.
|
|
279
|
+
|
|
280
|
+
### Unstable
|
|
281
|
+
|
|
282
|
+
- ANTICIPATORY AND UNSTABLE, off by default: a type search results resource
|
|
283
|
+
(https://github.com/danbri/factoidal/issues/922). With
|
|
284
|
+
`listen({ searchResults: true })` or `factoidal lws-serve
|
|
285
|
+
--unstable-search-results`, a QUERY answer of the Type Search Service by an
|
|
286
|
+
authenticated agent carries `Content-Location` naming
|
|
287
|
+
`/.well-known/lws-type-search/results/<id>` (RFC 10008 section 2.3).
|
|
288
|
+
lws10-index defines no such resource; its path, lifetime, snapshot member
|
|
289
|
+
and behaviour are likely to change. A GET by the same agent and client lists
|
|
290
|
+
the QUERY's results that a new QUERY would still answer (paged, filtered by
|
|
291
|
+
current read access); anyone else gets 404; after `searchResultsTtl`
|
|
292
|
+
(600 s) 410. The storage description marks it with the extension member
|
|
293
|
+
`https://github.com/danbri/factoidal/issues/922#unstableResultsResource`.
|
|
294
|
+
`lwsStep` request documents may carry `random` (16 bytes, base64); the LWS
|
|
295
|
+
host sends it with every request. Decided in Lean
|
|
296
|
+
(`L4Factoidal/LWS/SearchResults.lean`).
|
|
297
|
+
- Off by default: Merkle hashes of the LWS storage
|
|
298
|
+
(https://github.com/danbri/factoidal/issues/919; owner, 2026-10-21: "As an
|
|
299
|
+
optional, probably default off until we settle on stronger motivating
|
|
300
|
+
usecase but good to have the fundamentals there"). With
|
|
301
|
+
`listen({ merkle: true })` or `factoidal lws-serve --merkle`: a GET or HEAD
|
|
302
|
+
of a resource carries `Repr-Digest: sha-256=:<base64>:` (RFC 9530, SHA-256
|
|
303
|
+
of the stored bytes, HACL*); a container listing carries
|
|
304
|
+
`Factoidal-Merkle-Root` and the member
|
|
305
|
+
`https://github.com/danbri/factoidal/issues/919#merkleRoot` (a Merkle digest
|
|
306
|
+
over its members and their digests); the storage description carries
|
|
307
|
+
`https://github.com/danbri/factoidal/issues/919#storageMerkleRoot`. Under
|
|
308
|
+
authorization the roots go to the storage controller only. Entity tags do
|
|
309
|
+
not change. The hashes are kept by the store on every write, delete and
|
|
310
|
+
move (only the containers above the change are recomputed) and rebuilt when
|
|
311
|
+
a storage is opened; no hash is in the snapshot. Decided in Lean
|
|
312
|
+
(`L4Factoidal/LWS/Merkle.lean`, `MerkleHttp.lean`), with theorems
|
|
313
|
+
`MkState.dig_eq_recompute` (the kept digests equal a recomputation from the
|
|
314
|
+
store) and `change_reaches_ancestor` (a change reaches every container
|
|
315
|
+
above it, with collision-freedom of SHA-256 as a stated hypothesis).
|
|
316
|
+
Cost (2026-10-21, wasm in Node): a write into a container of 2,000 members
|
|
317
|
+
takes about 6.3 ms with the option on against 0.9 ms off (linear in the
|
|
318
|
+
members of each container above the write); a reopen of 2,000 resources
|
|
319
|
+
with the hashes rebuilt takes 246 ms.
|
|
320
|
+
|
|
321
|
+
### Performance
|
|
322
|
+
|
|
323
|
+
- LWS and Solid PUT and GET of a large body
|
|
324
|
+
(https://github.com/danbri/factoidal/issues/903): one 100,000-triple
|
|
325
|
+
N-Triples body (7.7 MB) took about 17 s to PUT and the GET stopped the
|
|
326
|
+
engine with out of memory; now the PUT takes 85 ms and the GET 40 ms
|
|
327
|
+
(`npm run test:costs`). The cause was the FNV-1a digest of the entity tag,
|
|
328
|
+
computed in `Nat`: on wasm32 each byte made heap numbers. It now runs in
|
|
329
|
+
`UInt32` (theorem `fnv1a32_eq_spec`: same value as the `Nat` definition).
|
|
330
|
+
Peak memory of the 100,000-triple PUT and GET fell from 2,640 MiB to
|
|
331
|
+
288 MiB. Scaling check: `node tests/lws/scaling.mjs`.
|
|
332
|
+
|
|
333
|
+
### Fixed
|
|
334
|
+
|
|
335
|
+
Seven defects of `lws-serve` that the Touchstone LWS conformance harness found
|
|
336
|
+
(https://github.com/danbri/factoidal/issues/911; record:
|
|
337
|
+
`docs/designissues/2026-10-21-lws-touchstone-run.md`). All are decided in
|
|
338
|
+
Lean (`formal/lean4/L4Factoidal/LWS/`):
|
|
339
|
+
|
|
340
|
+
- A POST or PUT with no content and no `Content-Type` is accepted
|
|
341
|
+
(`Operations.writeMediaType`). Before, it answered 400, so the draft's own
|
|
342
|
+
container-creation example (`POST`, `Content-Length: 0`, the
|
|
343
|
+
`lws#Container` type link) failed. An empty data resource made this way
|
|
344
|
+
has type `application/octet-stream`. Content without `Content-Type` still
|
|
345
|
+
answers 400.
|
|
346
|
+
- The Turtle form of a container uses `http://schema.org/size`, as the
|
|
347
|
+
`application/lws+json` context and the core draft's examples do. Before,
|
|
348
|
+
it used `https://schema.org/size`, so the two forms were not RDF
|
|
349
|
+
isomorphic. The WG's `lws10-vocab/vocabulary.yml` declares
|
|
350
|
+
`https://schema.org/`; the conflict is recorded in `Binding.containerTurtle`.
|
|
351
|
+
- A container's `ETag` carries the FNV-1a digest of its listing in the
|
|
352
|
+
negotiated media type (`Binding.containerTag`). Before, it was the
|
|
353
|
+
container's clock only, and a host with a real clock sets the clock to
|
|
354
|
+
the current second, so two changes in one second gave one strong tag.
|
|
355
|
+
`Last-Modified` is unchanged.
|
|
356
|
+
- The `lws#storage` link of a 401 and the default token audience are the
|
|
357
|
+
storage description IRI `<base>.well-known/lws-storage`, the IRI every
|
|
358
|
+
other response links and the description's `id` (`Authorization.storageIri`).
|
|
359
|
+
Before, both were the storage root `<base>`.
|
|
360
|
+
- `If-Unmodified-Since` is evaluated for every method (412 when the resource
|
|
361
|
+
changed after the date), and `If-Modified-Since` and `If-Unmodified-Since`
|
|
362
|
+
are compared as instants in all three HTTP-date formats of RFC 9110
|
|
363
|
+
§5.6.7 (`HttpDate.parseHttpDate`; theorem `unixOfCivil_civilOfUnix`).
|
|
364
|
+
Before, `If-Unmodified-Since` was ignored and `If-Modified-Since` matched
|
|
365
|
+
only the exact `Last-Modified` text.
|
|
366
|
+
- A POST names its new resource with the first free name of the hint (or
|
|
367
|
+
`res<clock>`), `-1`, `-2`, ... (`Operations.childPathFor`, theorem
|
|
368
|
+
`childPathFor_fresh`). Before, a clash got the clock as its one suffix,
|
|
369
|
+
and under a real clock the third POST to one container in one second
|
|
370
|
+
REPLACED the second resource.
|
|
371
|
+
- The Turtle form of a container gives a contained container no
|
|
372
|
+
`dcterms:modified`: the `application/lws+json` context defines
|
|
373
|
+
`modified` for a DataResource only, so the JSON item's `modified` of a
|
|
374
|
+
container is no triple, and the two forms must be RDF isomorphic.
|
|
375
|
+
- `factoidal lws-serve --page-size N` passes `N` to the engine's
|
|
376
|
+
`pageSize`.
|
|
377
|
+
- Result: Touchstone (221 tests) runs with no failure, open 70 pass, 0
|
|
378
|
+
fail, 151 inapplicable; with authorization 146 pass, 0 fail, 75
|
|
379
|
+
inapplicable.
|
|
380
|
+
|
|
381
|
+
Breaking: an LWS access token must name the storage description IRI in
|
|
382
|
+
`aud` (or the `--audience` given). A token for the storage root IRI is now
|
|
383
|
+
refused with 401 `invalid_token`.
|
|
384
|
+
|
|
3
385
|
## 0.11.0 — 2026-10-21
|
|
4
386
|
|
|
5
387
|
No RDF, SPARQL, datatype or IRI decision is made in JavaScript any more
|
package/README.md
CHANGED
|
@@ -70,6 +70,62 @@ Formats (generated from `formats()`):
|
|
|
70
70
|
<!-- formats-table:end -->
|
|
71
71
|
|
|
72
72
|
Missing writers (HDT, N3, RDFa): https://github.com/danbri/factoidal/issues/883.
|
|
73
|
+
|
|
74
|
+
## Errors
|
|
75
|
+
|
|
76
|
+
Every function rejects with one class, `FactoidalError`, except that an
|
|
77
|
+
argument of the wrong JavaScript type is a `TypeError`. The engine decides
|
|
78
|
+
three fields: `code` (below), `position` (`{line, column, offset,
|
|
79
|
+
byteOffset}` where the input defines one: `offset` is a UTF-16 index into
|
|
80
|
+
your string, `byteOffset` counts UTF-8 bytes) and `spec` (the specification
|
|
81
|
+
section of the condition). `cause` holds the underlying error of `host.io`.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { parse, FactoidalError } from '@factoidal/core';
|
|
85
|
+
try { await parse('<http://e/a> <http://e/p> .', 'turtle'); }
|
|
86
|
+
catch (e) { if (e instanceof FactoidalError) console.log(e.code, e.position.line, e.spec); }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Codes (generated from `engineTables().errorCodes`):
|
|
90
|
+
|
|
91
|
+
<!-- errors-table:start (generated by test/errors.test.mjs) -->
|
|
92
|
+
| code | specification | meaning |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `parse.syntax` | <https://www.w3.org/TR/rdf12-turtle/#sec-grammar> | The input text does not match the grammar of its RDF syntax. |
|
|
95
|
+
| `parse.rdf12-required` | <https://www.w3.org/TR/rdf12-concepts/#section-triple-terms> | The text uses an RDF 1.2 form (triple term, reified triple, base direction) and was read as RDF 1.1. |
|
|
96
|
+
| `term.invalid-iri` | <https://www.w3.org/TR/rdf12-concepts/#section-IRIs> | A term that must be an IRI is not one. |
|
|
97
|
+
| `term.invalid-langtag` | <https://www.w3.org/TR/rdf12-concepts/#section-Graph-Literal> | A language tag is not well-formed (BCP 47). |
|
|
98
|
+
| `term.invalid` | <https://www.w3.org/TR/sparql11-results-json/#select-encode-terms> | A term or quad given as JSON is not an RDF term or quad. |
|
|
99
|
+
| `query.syntax` | <https://www.w3.org/TR/sparql11-query/#grammar> | The SPARQL query does not match the SPARQL grammar. |
|
|
100
|
+
| `query.failed` | <https://www.w3.org/TR/sparql11-query/#sparqlDefinition> | The SPARQL query parsed but its evaluation was refused. |
|
|
101
|
+
| `update.syntax` | <https://www.w3.org/TR/sparql11-update/#grammar> | The SPARQL update does not match the SPARQL Update grammar. |
|
|
102
|
+
| `update.failed` | <https://www.w3.org/TR/sparql11-update/#updateLanguage> | The SPARQL update parsed but was refused. |
|
|
103
|
+
| `format.unknown` | - | The format name is not one the engine reads or writes. |
|
|
104
|
+
| `serialize.unsupported-rdf12` | <https://www.w3.org/TR/rdf12-concepts/#section-triple-terms> | The data holds an RDF 1.2 term and the requested RDF 1.1 output has no form for it. |
|
|
105
|
+
| `serialize.failed` | - | The writer refused the data. |
|
|
106
|
+
| `canon.unsupported-rdf12` | <https://www.w3.org/TR/rdf-canon/#canon-algorithm> | RDFC-1.0 defines no canonical labelling for RDF 1.2 triple terms or directional strings. |
|
|
107
|
+
| `canon.failed` | <https://www.w3.org/TR/rdf-canon/#canon-algorithm> | Canonicalization was refused. |
|
|
108
|
+
| `owl.unsupported-rdf12` | <https://www.w3.org/TR/owl2-mapping-to-rdf/#Mapping_from_RDF_Graphs_to_the_Structural_Specification> | OWL 2 Direct Semantics has no rule for RDF 1.2 triple terms or directional strings. |
|
|
109
|
+
| `reason.failed` | - | A closure or entailment check was refused. |
|
|
110
|
+
| `regime.refused` | <https://www.w3.org/TR/sparql11-entailment/> | The entailment regime is not implemented. |
|
|
111
|
+
| `validate.failed` | - | A validation (SHACL, ShEx, JSON Schema, Schematron) could not run on its inputs. |
|
|
112
|
+
| `xml.failed` | <https://www.w3.org/TR/xml/#sec-well-formed> | An XML, XPath, XSLT, MathML or XForms input was refused. |
|
|
113
|
+
| `crypto.failed` | - | A cryptographic or Verifiable Credentials operation refused its inputs. |
|
|
114
|
+
| `store.failed` | - | A persisted-store, pack or HDT operation was refused. |
|
|
115
|
+
| `registry.failed` | <https://www.w3.org/TR/sparql11-query/#extensionFunctions> | An extension-function or SERVICE registration was refused. |
|
|
116
|
+
| `handle.unknown` | - | The handle was never issued by this engine. |
|
|
117
|
+
| `handle.disposed` | - | The handle was issued and then closed. |
|
|
118
|
+
| `option.unknown` | - | The options object has a key the function does not take. |
|
|
119
|
+
| `option.invalid-value` | - | An option or mode argument has a value outside its table. |
|
|
120
|
+
| `option.conflict` | - | Two options were given that cannot be combined. |
|
|
121
|
+
| `capacity.exceeded` | - | The input is above a fixed size limit of the engine. |
|
|
122
|
+
| `call.arity` | - | The op was called with the wrong number of arguments. |
|
|
123
|
+
| `call.arguments` | - | The arguments document is not a JSON array of strings. |
|
|
124
|
+
| `op.unknown` | - | The loaded engine has no op of this name. |
|
|
125
|
+
| `op.failed` | - | Any other refusal of an op. |
|
|
126
|
+
| `host.io` | - | The host could not read or write a file, a store or a log (no engine decision). |
|
|
127
|
+
<!-- errors-table:end -->
|
|
128
|
+
|
|
73
129
|
Full API: [API.md](https://github.com/danbri/factoidal-core/blob/main/npm/factoidal/API.md).
|
|
74
130
|
Changes: [CHANGELOG.md](CHANGELOG.md).
|
|
75
131
|
|
|
@@ -97,10 +153,11 @@ Official test suites, pass / fail / total, 2026-10-09 [B] unless marked.
|
|
|
97
153
|
| SPARQL 1.1 Query, Update, Protocol | 631 / 0 / 631 |
|
|
98
154
|
| SPARQL 1.2 | 254 / 0 / 254 |
|
|
99
155
|
| SPARQL Protocol, Graph Store, Service Description against `serve` | 50 / 0 / 50 [P] |
|
|
156
|
+
| Linked Web Storage 1.0, Touchstone (another implementer's harness), `lws-serve` | 215 / 0 / 221 (6 not applicable) [L] |
|
|
100
157
|
| RDF 1.1 N-Triples, N-Quads, Turtle, TriG, RDF/XML | 992 / 0 / 992 |
|
|
101
158
|
| RDF 1.2 syntax suites | all pass |
|
|
102
159
|
| RDF 1.1 Semantics | 39 / 0 / 39 |
|
|
103
|
-
| RDF 1.2 Semantics |
|
|
160
|
+
| RDF 1.2 Semantics | 29 / 0 / 29 [S] |
|
|
104
161
|
| JSON-LD 1.1 toRdf, expand, flatten | 910 / 0 / 910 |
|
|
105
162
|
| JSON-LD 1.1 compact, fromRdf | 298 / 0 / 300 (2 not run) |
|
|
106
163
|
| JSON-LD 1.1 Framing | 29 / 63 / 92 |
|
|
@@ -111,7 +168,7 @@ Official test suites, pass / fail / total, 2026-10-09 [B] unless marked.
|
|
|
111
168
|
| SHACL 1.0 Core, SHACL-SPARQL | 120 / 0 / 120 |
|
|
112
169
|
| SHACL 1.2 Core | 103 / 30 / 133 |
|
|
113
170
|
| ShEx 2 validation, ShExC | 1624 / 0 / 1624 |
|
|
114
|
-
| CSVW | 821 /
|
|
171
|
+
| CSVW | 821 / 0 / 821 (1 defective W3C test excluded) [C] |
|
|
115
172
|
| RIF Core | 43 / 0 / 46 (3 undecided) |
|
|
116
173
|
| RML Core | 60 / 0 / 76 (16 not attempted) |
|
|
117
174
|
| XML 1.0 (xmlconf) | 1861 / 0 (516 not run) |
|
|
@@ -124,10 +181,13 @@ Official test suites, pass / fail / total, 2026-10-09 [B] unless marked.
|
|
|
124
181
|
[B] https://github.com/danbri/factoidal-core/blob/03e2e6deee8146ce7e6fdc3f8f34b8245af5ab54/docs/designissues/2026-10-09-lean-conformance-baseline.md
|
|
125
182
|
[P] https://github.com/danbri/factoidal-core/blob/9a4045f1572a032b53ccb867f8395fe0c8d97bb9/docs/designissues/2026-10-10-store-serve.md
|
|
126
183
|
[X] https://github.com/danbri/factoidal-core/blob/cb871c2de8354362ae6226eb57fb6c549624ea1a/docs/designissues/2026-10-10-xml-profile-widened.md
|
|
184
|
+
[L] 2026-10-22, https://github.com/danbri/factoidal-core/blob/2b9d2e39af53be31ed92463b58d3e132e65c4fe4/docs/designissues/2026-10-21-lws-touchstone-run.md (with server options, 220 of 221 pass in at least one configuration)
|
|
185
|
+
[S] 2026-10-21, the current W3C manifest (29 active tests), https://github.com/danbri/factoidal-core/blob/2b9d2e39af53be31ed92463b58d3e132e65c4fe4/docs/claude-rules/current-state.md
|
|
186
|
+
[C] 2026-10-21, validation test308, https://github.com/danbri/factoidal/issues/700
|
|
127
187
|
|
|
128
188
|
## Limits
|
|
129
189
|
|
|
130
|
-
- Size: 2.
|
|
190
|
+
- Size: 2.9 MB packed, 14.0 MB unpacked; one 13.2 MB wasm module.
|
|
131
191
|
- `factoidal update` and `compact` exit 3; updates work through `serve`
|
|
132
192
|
(https://github.com/danbri/factoidal/issues/641).
|
|
133
193
|
- Fuseki: read-only administration API, one dataset per process, no
|
package/api.d.ts
CHANGED
|
@@ -42,11 +42,10 @@ export function buildApi(driver: {
|
|
|
42
42
|
export function entryFromEngine(engine: object): Record<string, unknown>;
|
|
43
43
|
|
|
44
44
|
export const Dataset: typeof import('./index').Dataset;
|
|
45
|
-
export const DatasetHandle: typeof import('./index').DatasetHandle;
|
|
46
45
|
export const Store: typeof import('./index').Store;
|
|
47
46
|
export const dataFactory: import('./index').DataFactory;
|
|
48
|
-
/**
|
|
49
|
-
export const
|
|
47
|
+
/** The one error class; see index.d.ts's FactoidalError. */
|
|
48
|
+
export const FactoidalError: typeof import('./index').FactoidalError;
|
|
50
49
|
/** Package version string, e.g. '0.9.0'. */
|
|
51
50
|
export const version: string;
|
|
52
51
|
|
package/api.mjs
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
//
|
|
19
19
|
// openStore() here takes a reader function only: there is no file system.
|
|
20
20
|
|
|
21
|
-
import { buildApi,
|
|
21
|
+
import { buildApi, FactoidalError, Store } from './lib/api.mjs';
|
|
22
22
|
import { entryFromEngine } from './lib/lean-entry.mjs';
|
|
23
23
|
import { dataFactory } from './rdfjs.mjs';
|
|
24
24
|
import { Dataset } from './lib/dataset.mjs';
|
|
@@ -52,5 +52,5 @@ export function createApi(engine, options) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export { buildApi, entryFromEngine,
|
|
55
|
+
export { buildApi, entryFromEngine, FactoidalError, Store, Dataset, dataFactory, version };
|
|
56
56
|
export default createApi;
|