eyeling 1.23.3 → 1.23.5

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/HANDBOOK.md CHANGED
@@ -1902,6 +1902,8 @@ node eyeling.js --builtin examples/builtin/queens.js examples/queens.n3
1902
1902
 
1903
1903
  Examples that do not need a custom builtin should not add a matching file under `examples/builtin/`. Examples that do need one should ship it there and let the examples test runner load it uniformly. For example, `examples/sudoku.n3` is paired with `examples/builtin/sudoku.js`, and `examples/queens.n3` is paired with `examples/builtin/queens.js`.
1904
1904
 
1905
+ The browser playground follows the same convention for URL-loaded repository examples: when a loaded URL looks like `.../examples/name.n3`, the playground tries to fetch `.../examples/builtin/name.js` and register it before reasoning. If no matching builtin file exists, the N3 program runs normally.
1906
+
1905
1907
  ### 14.2 The bundled Node CLI/runtime (`eyeling.js`)
1906
1908
 
1907
1909
  The bundle contains the whole engine. The CLI path is the “canonical behavior”:
@@ -1965,7 +1967,7 @@ It deliberately does **not** expose `loadBuiltinModule(...)`, because loading bu
1965
1967
 
1966
1968
  For browser apps, prefer running Eyeling in a **Web Worker** and importing `eyeling/browser` there.
1967
1969
 
1968
- ### 14.3 `lib/entry.js`: bundler-friendly exports
1970
+ ### 14.4 `lib/entry.js`: bundler-friendly exports
1969
1971
 
1970
1972
  `lib/entry.js` exports:
1971
1973
 
@@ -1974,14 +1976,14 @@ For browser apps, prefer running Eyeling in a **Web Worker** and importing `eyel
1974
1976
 
1975
1977
  `rdfjs` is a small built-in RDF/JS `DataFactory`, so browser / worker code can construct quads without pulling in another package first.
1976
1978
 
1977
- ### 14.4 JavaScript API
1979
+ ### 14.5 JavaScript API
1978
1980
 
1979
1981
  Eyeling exposes two JavaScript entry styles:
1980
1982
 
1981
1983
  - `reason(...)` from `index.js` when you want the same text output as the CLI
1982
1984
  - `reasonStream(...)` / `reasonRdfJs(...)` from the Node bundle or `eyeling/browser` when you want in-process reasoning and structured RDF/JS results
1983
1985
 
1984
- #### 14.4.1 npm helper: `reason(...)`
1986
+ #### 14.5.1 npm helper: `reason(...)`
1985
1987
 
1986
1988
  The npm `reason(...)` function does something intentionally simple and robust:
1987
1989
 
@@ -2034,7 +2036,7 @@ Notes:
2034
2036
  - By default, the npm helper keeps output machine-friendly (`proofComments: false`).
2035
2037
  - Use this path when you want CLI-equivalent behavior inside JavaScript.
2036
2038
 
2037
- #### 14.4.2 RDF-JS and Eyeling rule-object interoperability
2039
+ #### 14.5.2 RDF-JS and Eyeling rule-object interoperability
2038
2040
 
2039
2041
  The JavaScript APIs accept four input styles:
2040
2042
 
@@ -2112,7 +2114,7 @@ console.log(out);
2112
2114
 
2113
2115
  You can also pass a full AST bundle directly, for example `[prefixes, triples, forwardRules, backwardRules]`.
2114
2116
 
2115
- #### 14.4.3 In-process bundle API: `reasonStream(...)` and `reasonRdfJs(...)`
2117
+ #### 14.5.3 In-process bundle API: `reasonStream(...)` and `reasonRdfJs(...)`
2116
2118
 
2117
2119
  Use the bundle entry if you want structured results while the engine is running instead of final CLI text after the fact.
2118
2120
 
@@ -2151,7 +2153,7 @@ Use these entry points when you need one or more of the following:
2151
2153
  - derived results consumed as RDF/JS quads
2152
2154
  - streaming derived RDF/JS quads during reasoning
2153
2155
 
2154
- ### 14.5 Choosing the right entry point
2156
+ ### 14.6 Choosing the right entry point
2155
2157
 
2156
2158
  A practical rule of thumb:
2157
2159
 
@@ -2392,7 +2394,7 @@ Run it explicitly like this:
2392
2394
  eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2393
2395
  ```
2394
2396
 
2395
- `npm run test:examples` uses the same convention automatically: when it sees `examples/builtin/sudoku.js` next to `examples/sudoku.n3`, it loads that module for the Sudoku example.
2397
+ `npm run test:examples` uses the same convention automatically: when it sees `examples/builtin/sudoku.js` next to `examples/sudoku.n3`, it loads that module for the Sudoku example. The browser playground uses the convention too for URL-loaded repository examples, so loading the raw `examples/sudoku.n3` URL also fetches and registers the matching `examples/builtin/sudoku.js` module.
2396
2398
 
2397
2399
  That example is useful for two reasons:
2398
2400
 
@@ -8702,10 +8702,21 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
8702
8702
  deref.setEnforceHttpsEnabled(!!enforceHttps);
8703
8703
  proofCommentsEnabled = !!proof;
8704
8704
 
8705
+ function registerBuiltinModuleOption(mod, index) {
8706
+ if (!mod) return;
8707
+ if (typeof mod === 'string') {
8708
+ loadBuiltinModule(mod);
8709
+ return;
8710
+ }
8711
+ registerBuiltinModule(mod, `<reasonStream builtinModules[${index}]>`);
8712
+ }
8713
+
8705
8714
  if (Array.isArray(builtinModules)) {
8706
- for (const spec of builtinModules) loadBuiltinModule(spec);
8707
- } else if (typeof builtinModules === 'string' && builtinModules) {
8708
- loadBuiltinModule(builtinModules);
8715
+ for (let i = 0; i < builtinModules.length; i += 1) {
8716
+ registerBuiltinModuleOption(builtinModules[i], i);
8717
+ }
8718
+ } else {
8719
+ registerBuiltinModuleOption(builtinModules, 0);
8709
8720
  }
8710
8721
 
8711
8722
  let prefixes, triples, frules, brules, logQueryRules;
@@ -8989,6 +9000,11 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
8989
9000
  tripleToN3: engine.tripleToN3,
8990
9001
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
8991
9002
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,
9003
+ registerBuiltin: engine.registerBuiltin,
9004
+ unregisterBuiltin: engine.unregisterBuiltin,
9005
+ registerBuiltinModule: engine.registerBuiltinModule,
9006
+ loadBuiltinModule: engine.loadBuiltinModule,
9007
+ listBuiltinIris: engine.listBuiltinIris,
8992
9008
  getEnforceHttpsEnabled: engine.getEnforceHttpsEnabled,
8993
9009
  setEnforceHttpsEnabled: engine.setEnforceHttpsEnabled,
8994
9010
  getProofCommentsEnabled: engine.getProofCommentsEnabled,
@@ -11142,6 +11158,37 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
11142
11158
 
11143
11159
  const __literalPartsCache = new Map(); // lit string -> [lex, dt]
11144
11160
 
11161
+ function quotedLiteralEndIndex(str) {
11162
+ if (typeof str !== 'string' || str.length < 2) return -1;
11163
+
11164
+ const quote = str[0];
11165
+ if (quote !== '"' && quote !== "'") return -1;
11166
+
11167
+ const delimLen = str.startsWith(quote.repeat(3)) ? 3 : 1;
11168
+ const delim = quote.repeat(delimLen);
11169
+
11170
+ let i = delimLen;
11171
+ while (i < str.length) {
11172
+ // Stored literals may contain escaped quotes, e.g. \"...\"^^xsd:dateTime
11173
+ // inside a plain string. Those must not terminate the outer lexical form.
11174
+ if (str[i] === '\\') {
11175
+ i += 2;
11176
+ continue;
11177
+ }
11178
+
11179
+ if (delimLen === 1) {
11180
+ if (str[i] === quote) return i + 1;
11181
+ i += 1;
11182
+ continue;
11183
+ }
11184
+
11185
+ if (str.startsWith(delim, i)) return i + delimLen;
11186
+ i += 1;
11187
+ }
11188
+
11189
+ return -1;
11190
+ }
11191
+
11145
11192
  function literalParts(lit) {
11146
11193
  // Avoid caching extremely large literals (notably huge numeric intermediates)
11147
11194
  // to prevent unbounded memory growth.
@@ -11159,24 +11206,21 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
11159
11206
  let lex = lit;
11160
11207
  let dt = null;
11161
11208
 
11162
- const re = /^(['"]{1,3})([\s\S]*?)\1\^\^(.+)$/;
11163
- const match = lit.match(re);
11164
- if (match) {
11165
- lex = match[1] + match[2] + match[1];
11166
- dt = match[3];
11209
+ const lexEnd = quotedLiteralEndIndex(lit);
11210
+ if (lexEnd > 0 && lit.startsWith('^^', lexEnd)) {
11211
+ lex = lit.slice(0, lexEnd);
11212
+ dt = lit.slice(lexEnd + 2);
11167
11213
  if (dt.startsWith('<') && dt.endsWith('>')) {
11168
11214
  dt = dt.slice(1, -1);
11169
11215
  }
11170
11216
  }
11171
11217
 
11172
11218
  // Strip LANGTAG from the lexical form when present.
11173
- if (lex.length >= 2 && lex[0] === '"') {
11174
- const lastQuote = lex.lastIndexOf('"');
11175
- if (lastQuote > 0 && lastQuote < lex.length - 1 && lex[lastQuote + 1] === '@') {
11176
- const lang = lex.slice(lastQuote + 2);
11177
- if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
11178
- lex = lex.slice(0, lastQuote + 1);
11179
- }
11219
+ const langLexEnd = quotedLiteralEndIndex(lex);
11220
+ if (langLexEnd > 0 && lex[0] === '"' && langLexEnd < lex.length && lex[langLexEnd] === '@') {
11221
+ const lang = lex.slice(langLexEnd + 1);
11222
+ if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
11223
+ lex = lex.slice(0, langLexEnd);
11180
11224
  }
11181
11225
  }
11182
11226
 
package/eyeling.js CHANGED
@@ -8679,10 +8679,21 @@ function reasonStream(input, opts = {}) {
8679
8679
  deref.setEnforceHttpsEnabled(!!enforceHttps);
8680
8680
  proofCommentsEnabled = !!proof;
8681
8681
 
8682
+ function registerBuiltinModuleOption(mod, index) {
8683
+ if (!mod) return;
8684
+ if (typeof mod === 'string') {
8685
+ loadBuiltinModule(mod);
8686
+ return;
8687
+ }
8688
+ registerBuiltinModule(mod, `<reasonStream builtinModules[${index}]>`);
8689
+ }
8690
+
8682
8691
  if (Array.isArray(builtinModules)) {
8683
- for (const spec of builtinModules) loadBuiltinModule(spec);
8684
- } else if (typeof builtinModules === 'string' && builtinModules) {
8685
- loadBuiltinModule(builtinModules);
8692
+ for (let i = 0; i < builtinModules.length; i += 1) {
8693
+ registerBuiltinModuleOption(builtinModules[i], i);
8694
+ }
8695
+ } else {
8696
+ registerBuiltinModuleOption(builtinModules, 0);
8686
8697
  }
8687
8698
 
8688
8699
  let prefixes, triples, frules, brules, logQueryRules;
@@ -8967,6 +8978,11 @@ module.exports = {
8967
8978
  tripleToN3: engine.tripleToN3,
8968
8979
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
8969
8980
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,
8981
+ registerBuiltin: engine.registerBuiltin,
8982
+ unregisterBuiltin: engine.unregisterBuiltin,
8983
+ registerBuiltinModule: engine.registerBuiltinModule,
8984
+ loadBuiltinModule: engine.loadBuiltinModule,
8985
+ listBuiltinIris: engine.listBuiltinIris,
8970
8986
  getEnforceHttpsEnabled: engine.getEnforceHttpsEnabled,
8971
8987
  setEnforceHttpsEnabled: engine.setEnforceHttpsEnabled,
8972
8988
  getProofCommentsEnabled: engine.getProofCommentsEnabled,
@@ -11114,6 +11130,37 @@ const MAX_LITERAL_PARTS_CACHE_LEN = 1024;
11114
11130
 
11115
11131
  const __literalPartsCache = new Map(); // lit string -> [lex, dt]
11116
11132
 
11133
+ function quotedLiteralEndIndex(str) {
11134
+ if (typeof str !== 'string' || str.length < 2) return -1;
11135
+
11136
+ const quote = str[0];
11137
+ if (quote !== '"' && quote !== "'") return -1;
11138
+
11139
+ const delimLen = str.startsWith(quote.repeat(3)) ? 3 : 1;
11140
+ const delim = quote.repeat(delimLen);
11141
+
11142
+ let i = delimLen;
11143
+ while (i < str.length) {
11144
+ // Stored literals may contain escaped quotes, e.g. \"...\"^^xsd:dateTime
11145
+ // inside a plain string. Those must not terminate the outer lexical form.
11146
+ if (str[i] === '\\') {
11147
+ i += 2;
11148
+ continue;
11149
+ }
11150
+
11151
+ if (delimLen === 1) {
11152
+ if (str[i] === quote) return i + 1;
11153
+ i += 1;
11154
+ continue;
11155
+ }
11156
+
11157
+ if (str.startsWith(delim, i)) return i + delimLen;
11158
+ i += 1;
11159
+ }
11160
+
11161
+ return -1;
11162
+ }
11163
+
11117
11164
  function literalParts(lit) {
11118
11165
  // Avoid caching extremely large literals (notably huge numeric intermediates)
11119
11166
  // to prevent unbounded memory growth.
@@ -11131,24 +11178,21 @@ function literalParts(lit) {
11131
11178
  let lex = lit;
11132
11179
  let dt = null;
11133
11180
 
11134
- const re = /^(['"]{1,3})([\s\S]*?)\1\^\^(.+)$/;
11135
- const match = lit.match(re);
11136
- if (match) {
11137
- lex = match[1] + match[2] + match[1];
11138
- dt = match[3];
11181
+ const lexEnd = quotedLiteralEndIndex(lit);
11182
+ if (lexEnd > 0 && lit.startsWith('^^', lexEnd)) {
11183
+ lex = lit.slice(0, lexEnd);
11184
+ dt = lit.slice(lexEnd + 2);
11139
11185
  if (dt.startsWith('<') && dt.endsWith('>')) {
11140
11186
  dt = dt.slice(1, -1);
11141
11187
  }
11142
11188
  }
11143
11189
 
11144
11190
  // Strip LANGTAG from the lexical form when present.
11145
- if (lex.length >= 2 && lex[0] === '"') {
11146
- const lastQuote = lex.lastIndexOf('"');
11147
- if (lastQuote > 0 && lastQuote < lex.length - 1 && lex[lastQuote + 1] === '@') {
11148
- const lang = lex.slice(lastQuote + 2);
11149
- if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
11150
- lex = lex.slice(0, lastQuote + 1);
11151
- }
11191
+ const langLexEnd = quotedLiteralEndIndex(lex);
11192
+ if (langLexEnd > 0 && lex[0] === '"' && langLexEnd < lex.length && lex[langLexEnd] === '@') {
11193
+ const lang = lex.slice(langLexEnd + 1);
11194
+ if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
11195
+ lex = lex.slice(0, langLexEnd);
11152
11196
  }
11153
11197
  }
11154
11198
 
package/lib/engine.js CHANGED
@@ -3384,10 +3384,21 @@ function reasonStream(input, opts = {}) {
3384
3384
  deref.setEnforceHttpsEnabled(!!enforceHttps);
3385
3385
  proofCommentsEnabled = !!proof;
3386
3386
 
3387
+ function registerBuiltinModuleOption(mod, index) {
3388
+ if (!mod) return;
3389
+ if (typeof mod === 'string') {
3390
+ loadBuiltinModule(mod);
3391
+ return;
3392
+ }
3393
+ registerBuiltinModule(mod, `<reasonStream builtinModules[${index}]>`);
3394
+ }
3395
+
3387
3396
  if (Array.isArray(builtinModules)) {
3388
- for (const spec of builtinModules) loadBuiltinModule(spec);
3389
- } else if (typeof builtinModules === 'string' && builtinModules) {
3390
- loadBuiltinModule(builtinModules);
3397
+ for (let i = 0; i < builtinModules.length; i += 1) {
3398
+ registerBuiltinModuleOption(builtinModules[i], i);
3399
+ }
3400
+ } else {
3401
+ registerBuiltinModuleOption(builtinModules, 0);
3391
3402
  }
3392
3403
 
3393
3404
  let prefixes, triples, frules, brules, logQueryRules;
package/lib/entry.js CHANGED
@@ -34,6 +34,11 @@ module.exports = {
34
34
  tripleToN3: engine.tripleToN3,
35
35
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
36
36
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,
37
+ registerBuiltin: engine.registerBuiltin,
38
+ unregisterBuiltin: engine.unregisterBuiltin,
39
+ registerBuiltinModule: engine.registerBuiltinModule,
40
+ loadBuiltinModule: engine.loadBuiltinModule,
41
+ listBuiltinIris: engine.listBuiltinIris,
37
42
  getEnforceHttpsEnabled: engine.getEnforceHttpsEnabled,
38
43
  setEnforceHttpsEnabled: engine.setEnforceHttpsEnabled,
39
44
  getProofCommentsEnabled: engine.getProofCommentsEnabled,
package/lib/prelude.js CHANGED
@@ -43,6 +43,37 @@ const MAX_LITERAL_PARTS_CACHE_LEN = 1024;
43
43
 
44
44
  const __literalPartsCache = new Map(); // lit string -> [lex, dt]
45
45
 
46
+ function quotedLiteralEndIndex(str) {
47
+ if (typeof str !== 'string' || str.length < 2) return -1;
48
+
49
+ const quote = str[0];
50
+ if (quote !== '"' && quote !== "'") return -1;
51
+
52
+ const delimLen = str.startsWith(quote.repeat(3)) ? 3 : 1;
53
+ const delim = quote.repeat(delimLen);
54
+
55
+ let i = delimLen;
56
+ while (i < str.length) {
57
+ // Stored literals may contain escaped quotes, e.g. \"...\"^^xsd:dateTime
58
+ // inside a plain string. Those must not terminate the outer lexical form.
59
+ if (str[i] === '\\') {
60
+ i += 2;
61
+ continue;
62
+ }
63
+
64
+ if (delimLen === 1) {
65
+ if (str[i] === quote) return i + 1;
66
+ i += 1;
67
+ continue;
68
+ }
69
+
70
+ if (str.startsWith(delim, i)) return i + delimLen;
71
+ i += 1;
72
+ }
73
+
74
+ return -1;
75
+ }
76
+
46
77
  function literalParts(lit) {
47
78
  // Avoid caching extremely large literals (notably huge numeric intermediates)
48
79
  // to prevent unbounded memory growth.
@@ -60,24 +91,21 @@ function literalParts(lit) {
60
91
  let lex = lit;
61
92
  let dt = null;
62
93
 
63
- const re = /^(['"]{1,3})([\s\S]*?)\1\^\^(.+)$/;
64
- const match = lit.match(re);
65
- if (match) {
66
- lex = match[1] + match[2] + match[1];
67
- dt = match[3];
94
+ const lexEnd = quotedLiteralEndIndex(lit);
95
+ if (lexEnd > 0 && lit.startsWith('^^', lexEnd)) {
96
+ lex = lit.slice(0, lexEnd);
97
+ dt = lit.slice(lexEnd + 2);
68
98
  if (dt.startsWith('<') && dt.endsWith('>')) {
69
99
  dt = dt.slice(1, -1);
70
100
  }
71
101
  }
72
102
 
73
103
  // Strip LANGTAG from the lexical form when present.
74
- if (lex.length >= 2 && lex[0] === '"') {
75
- const lastQuote = lex.lastIndexOf('"');
76
- if (lastQuote > 0 && lastQuote < lex.length - 1 && lex[lastQuote + 1] === '@') {
77
- const lang = lex.slice(lastQuote + 2);
78
- if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
79
- lex = lex.slice(0, lastQuote + 1);
80
- }
104
+ const langLexEnd = quotedLiteralEndIndex(lex);
105
+ if (langLexEnd > 0 && lex[0] === '"' && langLexEnd < lex.length && lex[langLexEnd] === '@') {
106
+ const lang = lex.slice(langLexEnd + 1);
107
+ if (/^[A-Za-z]+(?:-[A-Za-z0-9]+)*$/.test(lang)) {
108
+ lex = lex.slice(0, langLexEnd);
81
109
  }
82
110
  }
83
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.23.3",
3
+ "version": "1.23.5",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -288,6 +288,33 @@ const cases = [
288
288
  assert.equal(String(out).trimEnd(), '^^');
289
289
  },
290
290
  },
291
+ {
292
+ name: '00c quoted string containing typed literal syntax remains plain string',
293
+ opt: { proofComments: false },
294
+ input: `
295
+ @prefix : <http://example.org/>.
296
+
297
+ :s :p """
298
+ @prefix : <http://example.org/>.
299
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
300
+
301
+ :Let :param \"2023-04-01T18:06:04Z\"^^xsd:dateTime .
302
+ """.
303
+
304
+ {
305
+ :s :p ?O.
306
+ }
307
+ =>
308
+ {
309
+ :test :is ?O.
310
+ }.
311
+ `,
312
+ expect: [/^:test\s+:is\s+/m],
313
+ notExpect: [/\^\^<xsd:dateTime/],
314
+ check(out) {
315
+ assert.match(out, /\\"2023-04-01T18:06:04Z\\"\^\^xsd:dateTime \./);
316
+ },
317
+ },
291
318
  {
292
319
  name: '01 forward rule: p -> q',
293
320
  opt: { proofComments: false },
@@ -488,6 +488,8 @@ async function main() {
488
488
  // Intercept CodeMirror + remote GitHub raw URLs (keep test deterministic).
489
489
  const localPkg = fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8');
490
490
  const localEyeling = fs.readFileSync(path.join(ROOT, 'eyeling.js'), 'utf8');
491
+ const localSudoku = fs.readFileSync(path.join(ROOT, 'examples', 'sudoku.n3'), 'utf8');
492
+ const localSudokuBuiltin = fs.readFileSync(path.join(ROOT, 'examples', 'builtin', 'sudoku.js'), 'utf8');
491
493
 
492
494
  const intercept = new Map([
493
495
  // CodeMirror assets (CDN)
@@ -517,6 +519,14 @@ async function main() {
517
519
  'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/eyeling.js',
518
520
  { ct: 'application/javascript', body: localEyeling },
519
521
  ],
522
+ [
523
+ 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3',
524
+ { ct: 'text/plain', body: localSudoku },
525
+ ],
526
+ [
527
+ 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/builtin/sudoku.js',
528
+ { ct: 'application/javascript', body: localSudokuBuiltin },
529
+ ],
520
530
  ]);
521
531
 
522
532
  await cdp.send(
@@ -641,6 +651,21 @@ async function main() {
641
651
  })()`);
642
652
  }
643
653
 
654
+ async function loadUrlIntoEditor(url) {
655
+ const payload = JSON.stringify(String(url));
656
+ await evalInPage(`(() => {
657
+ const input = document.getElementById('n3-uri');
658
+ const asBackground = document.getElementById('load-as-background');
659
+ const btn = document.getElementById('load-uri-btn');
660
+ if (!input) throw new Error('n3-uri input not found');
661
+ if (!btn) throw new Error('load-uri-btn not found');
662
+ input.value = ${payload};
663
+ if (asBackground) asBackground.checked = false;
664
+ btn.click();
665
+ return true;
666
+ })()`);
667
+ }
668
+
644
669
  async function waitForState(label, predicate, timeoutMs = 60000) {
645
670
  const deadline = Date.now() + timeoutMs;
646
671
  let last = { status: '', output: '', highlighted: [] };
@@ -731,6 +756,26 @@ ${JSON.stringify(last, null, 2)}`);
731
756
  );
732
757
  ok('playground renders log:outputString cleanly in Output');
733
758
 
759
+ // 5) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
760
+ await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3');
761
+ await waitForState(
762
+ 'sudoku URL loaded with companion builtin',
763
+ (st) => /loaded n3 into the editor and loaded its example builtin/i.test(String(st.status || '')),
764
+ 20000,
765
+ );
766
+ await clickRun();
767
+ const sudoku = await waitForState(
768
+ 'URL-loaded Sudoku example completion',
769
+ (st) =>
770
+ String(st.status || '')
771
+ .trim()
772
+ .startsWith('Done') && /The puzzle is solved/i.test(String(st.output || '')),
773
+ 60000,
774
+ );
775
+ assert.match(sudoku.output, /Completed grid/i, 'Expected Sudoku rendered output');
776
+ assert.match(sudoku.output, /unique valid Sudoku solution/i, 'Expected Sudoku builtin-backed result');
777
+ ok('playground auto-loads a companion example builtin for URL-loaded Sudoku');
778
+
734
779
  // Ensure no uncaught runtime exceptions.
735
780
  assert.equal(exceptions.length, 0, `Uncaught exceptions in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
736
781