@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 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 | 40 / 7 / 47 |
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 / 1 / 822 |
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.5 MB packed, 12.4 MB unpacked; one 11.7 MB wasm module.
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
- /** Position-carrying parse failure; see index.d.ts's ParseError. */
49
- export const ParseError: typeof import('./index').ParseError;
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, ParseError, DatasetHandle, Store } from './lib/api.mjs';
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, ParseError, DatasetHandle, Store, Dataset, dataFactory, version };
55
+ export { buildApi, entryFromEngine, FactoidalError, Store, Dataset, dataFactory, version };
56
56
  export default createApi;