@factoidal/core 0.34.0 → 0.35.1

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,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.1 (2026-11-11)
4
+
5
+ - Foafnaut (macOS): a heavy page no longer freezes the app. The host keeps one frame of GPU work in flight for heavy frames (three for light ones); after three frames over 250 ms it slows the page's animation and shows "SLOWED: HEAVY PAGE". Quitting or closing the window stops GPU work, so the app quits within seconds (before, up to 209 s on the Spinal Tap page at 2x). `--gpu-frames-in-flight N` sets the limit.
6
+ - Foafnaut: Arabic letters join again on a page that loaded Noto Sans Math first; a font whose GSUB covers the script is preferred. Chinese, Japanese and Korean text breaks between characters, so long runs wrap instead of pushing right-to-left lines out of their cells.
7
+ - BubblePopJS: conversions between byte arrays and the engine's values are faster (Pako about 14% faster on the native engine), with proofs that the fast forms give the same results.
8
+ - XMPP: xmpp-serve logs each connection by its address tag, never the address (--quiet stops it).
9
+
10
+ ## 0.35.0 (2026-11-10)
11
+
12
+ - Foafnaut: partial frames. A frame compiles again only the primitives that changed, keeps a damage rectangle, and redraws only that part of a layer. On the Spinal Tap page, one animated frame went from 351 ms to 3.9 ms of GPU and JS time at 1x (1225 ms to 4.0 ms at 2x); engine time per frame 28.5 ms to 10.9 ms. `retained:false` still gives the full path, and tests check that both give the same bytes.
13
+ - Foafnaut: BubblePopJS, the built-in JavaScript engine, runs page scripts by default everywhere, also on the Hub. QuickJS-NG stays in the builds for comparison. Fixes: `r.exec(s)` in a loop no longer pays for the length of `s` on each call; a block with `let` or `const` may hold an `await`.
14
+ - Foafnaut: a shader gallery page (WebGPU, WGSL), the same results on both JavaScript engines.
15
+ - Foafnaut: the Hub starts faster, and layout is faster on large pages.
16
+ - Foafnaut: on a Retina Mac the page is drawn at full resolution.
17
+ - Foafnaut: iframes; form submission in the page's encoding; colour emoji (COLR/CPAL) in text, SVG text and canvas text; more fonts and SMIL animation.
18
+ - Foafnaut: a click is no longer lost when the engine is busy; pointer input runs before queued engine work, and script hit tests use the frame on screen.
19
+ - Foafnaut (macOS): the app links Lean's runtime without GNU MP. The releases that still contain GNU MP carry its notice.
20
+ - `foafnaut` command: with no channel, it starts the newer of the installed release and the local bleeding-edge build, and says which. `--bleeding-edge` says the commit and age of the local build and warns when it is more than 6 hours old.
21
+ - LWS client: `verifyNotification` accepts a notification whose `Signature-Input` covers the derived component `@query` (RFC 9421 section 2.2.7; https://github.com/danbri/factoidal-core/issues/379). The value comes from the inbox URL: `?` and the query, or `?` alone when the URL has no query. Before, the client read `@query` as a header field and refused the notification with "a covered component is missing", so `createNotificationReceiver` answered 401 to every notification from a storage that signs `@query` (sparq-lws-core; seen with 0.26.0 and 0.34.0). The client derives `@method`, `@scheme`, `@authority`, `@path` and `@query` only. Another name that starts with `@` (`@target-uri`, `@request-target`, `@query-param`) is refused and is not read as a header field. The decision is in Lean (`inboxBase` and `urlQuery`, `L4Factoidal/LWS/Client/Machine.lean`), through the wasm op `lwsClientStep`.
22
+ - `foafnaut` command: `--version` prints the commit and the build time of the app that would run. The app gives the answer, from the record that its build wrote. An app that is too old to answer gives the line "this build is too old to answer --version".
23
+ - `foafnaut` command: a page given as `file:name.html`, with no `//`, is a path relative to the current directory. A `file://` URL is read as a URL, so percent-encoded characters in its path are decoded.
24
+ - XMPP: every IQ result the server sends has a `from` (RFC 6120 section 8.1.2.1): the address asked, or the account's bare JID when the request had no `to`. Theorems `handleIq_send_has_from` and `handleIq_domain_from`.
25
+
3
26
  ## 0.34.0 (2026-11-08)
4
27
 
5
28
  - `foafnaut` command: `--url` and `--html` open a page or a file (`file:` URLs too), and other options are passed to the app.
package/bin/factoidal.mjs CHANGED
@@ -2998,7 +2998,9 @@ async function commandXmppServe (positional, options) {
2998
2998
  dir: positional[0],
2999
2999
  port: servePort(options, useTls && !startTls ? 5223 : 5222),
3000
3000
  host: serveHost(options),
3001
- onError: (error) => { if (!quiet) err(`xmpp-serve: ${error.message}`) }
3001
+ onError: (error) => { if (!quiet) err(`xmpp-serve: ${error.message}`) },
3002
+ // Connection events: an address tag (never an address), closes at a timeout.
3003
+ onLog: (line) => { if (!quiet) err(`xmpp-serve: ${line}`) }
3002
3004
  }
3003
3005
  if (typeof options.domain === 'string') settings.domain = options.domain
3004
3006
  if (typeof options.accounts === 'string') {
package/bin/foafnaut.mjs CHANGED
@@ -5,10 +5,10 @@
5
5
  // current -> versions/<commit>, config.json {"bleedingEdge":"/path/to/App.app"}.
6
6
  import { spawn, spawnSync } from 'node:child_process';
7
7
  import { createHash } from 'node:crypto';
8
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
- import { basename, join, resolve } from 'node:path';
11
- import { pathToFileURL } from 'node:url';
10
+ import { basename, dirname, join, resolve } from 'node:path';
11
+ import { fileURLToPath, pathToFileURL } from 'node:url';
12
12
 
13
13
  const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE | --url URL | --html FILE] [APP_OPTIONS] [-- APP_ARGS]
14
14
 
@@ -23,6 +23,7 @@ const HELP = `foafnaut [--latest|--bleeding-edge] [PAGE | --url URL | --html FIL
23
23
  --install-latest install the newest published build and stop
24
24
  --list list installed versions
25
25
  --which print the app that would run
26
+ --version print the commit and build time of that app (the app answers)
26
27
  --help this text
27
28
 
28
29
  App options, passed to the app as they are:
@@ -33,6 +34,9 @@ App options, passed to the app as they are:
33
34
  --font FILE load this font file
34
35
  --bitmap-font use the baked-in ASCII font only
35
36
  --follow-links open links that a page asks for at load
37
+ --be-liberal-in-what-we-accept draw a page without the features this browser may skip
38
+ (module scripts, unreadable media conditions, stylesheets that did not
39
+ load) and list them in the console (Option-Command-J); the default refuses
36
40
 
37
41
  Also: foafnaut-latest, foafnaut-bleedingedge. Environment: FOAFNAUT_HOME, and
38
42
  FOAFNAUT_APP, which chooses the app outright. Page data goes to the app. Its trace
@@ -108,13 +112,15 @@ async function installLatest() {
108
112
  }
109
113
 
110
114
  // App options that take no value, and those that take one.
111
- const APP_FLAGS = ['--perf-summary', '--check-pixels', '--wasm-engine', '--bitmap-font', '--follow-links', '--idle-checks-only', '--trace-input'];
115
+ const APP_FLAGS = ['--perf-summary', '--check-pixels', '--wasm-engine', '--bitmap-font', '--follow-links', '--idle-checks-only', '--trace-input', '--be-liberal-in-what-we-accept'];
112
116
  const APP_VALUES = ['--native-engine', '--font'];
113
117
 
114
118
  function pageArgs(page) {
115
119
  if (page === undefined) return [];
116
120
  if (/^https?:\/\//i.test(page)) return ['--url', page];
117
- if (/^file:/i.test(page)) page = new URL(page).pathname;
121
+ // file:///abs/path is a URL; file:name.html has no // and is relative to here.
122
+ if (/^file:\/\//i.test(page)) page = fileURLToPath(page);
123
+ else if (/^file:/i.test(page)) page = decodeURIComponent(page.slice(5));
118
124
  const path = resolve(page);
119
125
  if (!existsSync(path)) fail(`no such file: ${page}`);
120
126
  return ['--html', path];
@@ -124,7 +130,7 @@ export async function main(argv, channel = 'current') {
124
130
  const split = argv.indexOf('--');
125
131
  const options = split < 0 ? argv : argv.slice(0, split);
126
132
  const extra = split < 0 ? [] : argv.slice(split + 1);
127
- let page, which = false, verbose = false, detach = false;
133
+ let page, which = false, verbose = false, detach = false, version = false;
128
134
  const app_ = [];
129
135
  const setPage = (value) => { if (page !== undefined) fail('give one page'); if (value === undefined) fail('a page option needs a value'); page = value; };
130
136
  // Each of these picks the app or the action. Two of them have no one meaning.
@@ -136,6 +142,7 @@ export async function main(argv, channel = 'current') {
136
142
  else if (arg === '--latest') channel = 'latest';
137
143
  else if (arg === '--bleeding-edge') channel = 'bleeding-edge';
138
144
  else if (arg === '--which') which = true;
145
+ else if (arg === '--version') version = true;
139
146
  else if (arg === '--verbose' || arg === '-v') verbose = true;
140
147
  else if (arg === '--detach') detach = true;
141
148
  else if (arg === '--install-latest') { await installLatest(); return; }
@@ -157,9 +164,36 @@ export async function main(argv, channel = 'current') {
157
164
  let app = process.env.FOAFNAUT_APP;
158
165
  if (!app && channel === 'latest') app = await installLatest();
159
166
  if (!app && channel === 'bleeding-edge') app = bleedingEdgeApp();
160
- if (!app) app = currentApp() ?? bleedingEdgeApp();
167
+ // With no channel named: the newer of the installed release and the bleeding-edge build
168
+ // (owner, 2026-11-09: plain foafnaut started a release of 6 November, older than the
169
+ // local build, with nothing to say so). Each is dated by its files on disk.
170
+ if (!app) {
171
+ const release = currentApp(), local = existsSync(bleedingEdgeApp()) ? bleedingEdgeApp() : undefined;
172
+ const age = (path) => statSync(path).mtimeMs;
173
+ app = release && local ? (age(local) >= age(release) ? local : release) : (release ?? local ?? bleedingEdgeApp());
174
+ if (release && app === release)
175
+ console.error(`foafnaut: the installed release ${basename(realpathSync(join(home(), 'current')))} (newer than the bleeding-edge build)`);
176
+ else if (release) console.error('foafnaut: the bleeding-edge build (newer than the installed release; --latest for the release)');
177
+ }
161
178
  if (!existsSync(app)) fail(`no app at ${app}. Run foafnaut --install-latest.`);
179
+ // The bleeding-edge build says what it was built from (BUILD-INFO.json beside the app,
180
+ // written when the build is swapped in); an old build is named, so it is not taken for main.
181
+ if (app === bleedingEdgeApp()) {
182
+ try {
183
+ const info = JSON.parse(readFileSync(join(dirname(app), 'BUILD-INFO.json'), 'utf8'));
184
+ const hours = (Date.now() - Date.parse(info.builtAt)) / 3.6e6;
185
+ console.error(`foafnaut: bleeding edge built from ${info.commit}${info.dirty ? " with uncommitted changes" : ""} at ${info.builtAt} (${hours.toFixed(1)} h ago)` +
186
+ (hours > 6 ? '; it may lack newer work on main' : ''));
187
+ } catch { console.error('foafnaut: bleeding edge has no BUILD-INFO.json; its commit is not known'); }
188
+ }
162
189
  if (which) { console.log(app); return; }
190
+ // The app answers for itself. `open` shows no output, so run its program directly.
191
+ if (version) {
192
+ const run = spawnSync(join(dirname(app), 'native-webgpu-html-darwin-arm64'), ['--version'], { encoding: 'utf8' });
193
+ if (run.status === 0) process.stdout.write(run.stdout);
194
+ else console.log(`foafnaut: this build is too old to answer --version (${app})`);
195
+ process.exit(0);
196
+ }
163
197
  const args = ['-n', ...(detach ? [] : ['-W']), app, '--args', ...pageArgs(page), ...(verbose ? ['--trace-input'] : []), ...app_, ...extra];
164
198
  const log = join(homedir(), 'Library', 'Logs', 'foafnaut', 'trace.jsonl');
165
199
  if (verbose) console.error(`foafnaut: app ${app}\nfoafnaut: open ${args.join(' ')}\nfoafnaut: trace ${log}`);
@@ -37,7 +37,7 @@
37
37
  // bytes change.
38
38
 
39
39
  // Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
40
- const WASM_VERSION = "a92a94475fa0";
40
+ const WASM_VERSION = "2e241365bd79";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
Binary file
@@ -2,17 +2,17 @@
2
2
  "engine": "lean4",
3
3
  "note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
4
4
  "stamped": true,
5
- "release": "danbri/factoidal-builds build-20ff52ea9005-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/42520417546",
7
- "gitSha": "20ff52ea90050c8db7bb7135aa0c20ee118183e5",
8
- "builtAt": "2026-11-08T21:35:07Z",
5
+ "release": "danbri/factoidal-builds build-6f57afba14e0-web",
6
+ "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/42968513371",
7
+ "gitSha": "6f57afba14e04ed648358e4693790edff69a6cd3",
8
+ "builtAt": "2026-11-11T07:07:29Z",
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": "a92a94475fa0cfc6fe9aeb28b197e81c3c3162806ecbba562f200fbaea51416b",
15
- "wasmBytes": 32702060,
14
+ "wasmSha256": "2e241365bd7906474320610a65af99641474534fa0fe0e744ed3d9edeef4ee41",
15
+ "wasmBytes": 32703824,
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/version.mjs CHANGED
@@ -2,4 +2,4 @@
2
2
  // needs import attributes, which not every runtime this package targets
3
3
  // reads). test/dts-drift.test.js checks that it equals package.json's
4
4
  // `version`.
5
- export const version = '0.34.0';
5
+ export const version = '0.35.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@factoidal/core",
3
- "version": "0.34.0",
3
+ "version": "0.35.1",
4
4
  "description": "EXPERIMENTAL UNDER-REVIEWED 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",
@@ -28,7 +28,8 @@
28
28
  "author": "Dan Brickley <danbri@danbri.org>",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/danbri/factoidal-builds.git"
31
+ "url": "git+https://github.com/danbri/factoidal.git",
32
+ "directory": "npm/factoidal"
32
33
  },
33
34
  "main": "./index.js",
34
35
  "module": "./index.mjs",