eyeling 1.23.2 → 1.23.4

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/eyeling.js CHANGED
@@ -8,474 +8,6 @@
8
8
  const __cache = Object.create(null);
9
9
 
10
10
  // ---- bundled modules ----
11
- __modules["lib/builtin-sudoku.js"] = function(require, module, exports){
12
- 'use strict';
13
-
14
- module.exports = function registerSudokuBuiltins(api) {
15
- const { registerBuiltin, internLiteral, termToJsString, unifyTerm, terms } = api;
16
- const { Var } = terms;
17
-
18
- const SUDOKU_NS = 'http://example.org/sudoku-builtin#';
19
- const __sudokuReportCache = new Map();
20
- const __SUDOKU_ALL = 0x1ff;
21
-
22
- function makeStringLiteral(str) {
23
- return internLiteral(JSON.stringify(str));
24
- }
25
-
26
- function digitMask(v) {
27
- return 1 << (v - 1);
28
- }
29
-
30
- function boxIndex(r, c) {
31
- return Math.floor(r / 3) * 3 + Math.floor(c / 3);
32
- }
33
-
34
- function popcount(mask) {
35
- let n = 0;
36
- while (mask) {
37
- mask &= mask - 1;
38
- n += 1;
39
- }
40
- return n;
41
- }
42
-
43
- function maskToDigits(mask) {
44
- const out = [];
45
- for (let d = 1; d <= 9; d += 1) if (mask & digitMask(d)) out.push(d);
46
- return out;
47
- }
48
-
49
- function formatBoard(cells) {
50
- let out = '';
51
- for (let r = 0; r < 9; r += 1) {
52
- if (r > 0 && r % 3 === 0) out += '\n';
53
- for (let c = 0; c < 9; c += 1) {
54
- if (c > 0 && c % 3 === 0) out += '| ';
55
- const v = cells[r * 9 + c];
56
- out += v === 0 ? '. ' : `${String(v)} `;
57
- }
58
- out += '\n';
59
- }
60
- return out;
61
- }
62
-
63
- function parsePuzzle(input) {
64
- const filtered = [];
65
- for (const ch of input) {
66
- if (/\s/.test(ch) || ch === '|' || ch === '+') continue;
67
- filtered.push(ch);
68
- }
69
- if (filtered.length !== 81) {
70
- return { error: `Expected exactly 81 cells after removing whitespace, but found ${filtered.length}.` };
71
- }
72
- const cells = new Array(81).fill(0);
73
- for (let i = 0; i < 81; i += 1) {
74
- const ch = filtered[i];
75
- if (ch >= '1' && ch <= '9') cells[i] = ch.charCodeAt(0) - 48;
76
- else if (ch === '0' || ch === '.' || ch === '_') cells[i] = 0;
77
- else return { error: `Unexpected character '${ch}' at position ${i + 1}.` };
78
- }
79
- return { cells };
80
- }
81
-
82
- function attachMethods(state) {
83
- state.place = function place(idx, value) {
84
- if (this.cells[idx] !== 0) return this.cells[idx] === value;
85
- const row = Math.floor(idx / 9);
86
- const col = idx % 9;
87
- const bx = boxIndex(row, col);
88
- const bit = digitMask(value);
89
- if (((this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]) & bit) !== 0) return false;
90
- this.cells[idx] = value;
91
- this.rowUsed[row] |= bit;
92
- this.colUsed[col] |= bit;
93
- this.boxUsed[bx] |= bit;
94
- return true;
95
- };
96
-
97
- state.candidates = function candidates(idx) {
98
- const row = Math.floor(idx / 9);
99
- const col = idx % 9;
100
- const bx = boxIndex(row, col);
101
- return __SUDOKU_ALL & ~(this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]);
102
- };
103
-
104
- state.clone = function clone() {
105
- return attachMethods({
106
- cells: this.cells.slice(),
107
- rowUsed: this.rowUsed.slice(),
108
- colUsed: this.colUsed.slice(),
109
- boxUsed: this.boxUsed.slice(),
110
- moves: this.moves.slice(),
111
- });
112
- };
113
-
114
- return state;
115
- }
116
-
117
- function stateFromPuzzle(cells) {
118
- const state = attachMethods({
119
- cells: new Array(81).fill(0),
120
- rowUsed: new Array(9).fill(0),
121
- colUsed: new Array(9).fill(0),
122
- boxUsed: new Array(9).fill(0),
123
- moves: [],
124
- });
125
-
126
- for (let idx = 0; idx < 81; idx += 1) {
127
- const value = cells[idx];
128
- if (value === 0) continue;
129
- if (value < 1 || value > 9) {
130
- return { error: `Cell ${idx + 1} contains ${value}, but only digits 1-9 or 0/. are allowed.` };
131
- }
132
- if (!state.place(idx, value)) {
133
- const row = Math.floor(idx / 9) + 1;
134
- const col = (idx % 9) + 1;
135
- return { error: `The given clues already conflict at row ${row}, column ${col}.` };
136
- }
137
- }
138
-
139
- return { state };
140
- }
141
-
142
- function summarizeMoves(moves, limit) {
143
- if (!moves.length) return 'no placements were needed';
144
- const parts = [];
145
- for (const mv of moves.slice(0, limit)) {
146
- const row = Math.floor(mv.index / 9) + 1;
147
- const col = (mv.index % 9) + 1;
148
- const mode = mv.forced ? 'forced' : 'guess';
149
- parts.push(`r${row}c${col}=${mv.value}: ${mode}`);
150
- }
151
- if (moves.length > limit) parts.push(`… and ${moves.length - limit} more placements`);
152
- return parts.join(', ');
153
- }
154
-
155
- function unitIsComplete(values) {
156
- let seen = 0;
157
- for (const v of values) {
158
- if (v < 1 || v > 9) return false;
159
- const bit = digitMask(v);
160
- if (seen & bit) return false;
161
- seen |= bit;
162
- }
163
- return seen === __SUDOKU_ALL;
164
- }
165
-
166
- function replayMovesAreLegal(puzzleCells, moves) {
167
- const init = stateFromPuzzle(puzzleCells);
168
- if (init.error) return false;
169
- const state = init.state;
170
- for (const mv of moves) {
171
- if (state.cells[mv.index] !== 0) return false;
172
- const maskNow = state.candidates(mv.index);
173
- if (maskNow !== mv.candidatesMask) return false;
174
- if ((maskNow & digitMask(mv.value)) === 0) return false;
175
- if (mv.forced && popcount(maskNow) !== 1) return false;
176
- if (!state.place(mv.index, mv.value)) return false;
177
- }
178
- return true;
179
- }
180
-
181
- function propagateSingles(state, stats) {
182
- for (;;) {
183
- let progress = false;
184
- for (let idx = 0; idx < 81; idx += 1) {
185
- if (state.cells[idx] !== 0) continue;
186
- const mask = state.candidates(idx);
187
- const count = popcount(mask);
188
- if (count === 0) return false;
189
- if (count === 1) {
190
- const digit = maskToDigits(mask)[0];
191
- state.moves.push({ index: idx, value: digit, candidatesMask: mask, forced: true });
192
- if (!state.place(idx, digit)) return false;
193
- stats.forcedMoves += 1;
194
- progress = true;
195
- }
196
- }
197
- if (!progress) return true;
198
- }
199
- }
200
-
201
- function selectUnfilledCell(state) {
202
- let best = null;
203
- for (let idx = 0; idx < 81; idx += 1) {
204
- if (state.cells[idx] !== 0) continue;
205
- const mask = state.candidates(idx);
206
- const count = popcount(mask);
207
- if (best === null || count < best.count) best = { idx, mask, count };
208
- if (count === 2) break;
209
- }
210
- return best;
211
- }
212
-
213
- function solve(state, stats, depth) {
214
- stats.recursiveNodes += 1;
215
- if (depth > stats.maxDepth) stats.maxDepth = depth;
216
- const current = state.clone();
217
- if (!propagateSingles(current, stats)) {
218
- stats.backtracks += 1;
219
- return null;
220
- }
221
- const best = selectUnfilledCell(current);
222
- if (!best) return current;
223
- for (const digit of maskToDigits(best.mask)) {
224
- const next = current.clone();
225
- const candidatesMask = next.candidates(best.idx);
226
- next.moves.push({ index: best.idx, value: digit, candidatesMask, forced: false });
227
- stats.guessedMoves += 1;
228
- if (!next.place(best.idx, digit)) continue;
229
- const solved = solve(next, stats, depth + 1);
230
- if (solved) return solved;
231
- }
232
- stats.backtracks += 1;
233
- return null;
234
- }
235
-
236
- function countSolutions(state, limit, countRef) {
237
- if (countRef.count >= limit) return;
238
- const current = state.clone();
239
- const dummy = {
240
- givens: 0,
241
- blanks: 0,
242
- forcedMoves: 0,
243
- guessedMoves: 0,
244
- recursiveNodes: 0,
245
- backtracks: 0,
246
- maxDepth: 0,
247
- };
248
- if (!propagateSingles(current, dummy)) return;
249
- const best = selectUnfilledCell(current);
250
- if (!best) {
251
- countRef.count += 1;
252
- return;
253
- }
254
- for (const digit of maskToDigits(best.mask)) {
255
- if (countRef.count >= limit) return;
256
- const next = current.clone();
257
- if (next.place(best.idx, digit)) countSolutions(next, limit, countRef);
258
- }
259
- }
260
-
261
- function computeReport(term) {
262
- const raw = termToJsString(term);
263
- if (raw === null) return null;
264
- if (__sudokuReportCache.has(raw)) return __sudokuReportCache.get(raw);
265
-
266
- const parsed = parsePuzzle(raw);
267
- if (parsed.error) {
268
- const rep = { status: 'invalid-input', error: parsed.error, raw, normalized: null };
269
- __sudokuReportCache.set(raw, rep);
270
- return rep;
271
- }
272
-
273
- const normalized = parsed.cells.join('');
274
- const init = stateFromPuzzle(parsed.cells);
275
- if (init.error) {
276
- const rep = {
277
- status: 'illegal-clues',
278
- error: init.error,
279
- raw,
280
- normalized,
281
- givens: parsed.cells.filter((v) => v !== 0).length,
282
- blanks: parsed.cells.filter((v) => v === 0).length,
283
- puzzleText: formatBoard(parsed.cells),
284
- };
285
- __sudokuReportCache.set(raw, rep);
286
- return rep;
287
- }
288
-
289
- const initial = init.state;
290
- const stats = {
291
- givens: parsed.cells.filter((v) => v !== 0).length,
292
- blanks: parsed.cells.filter((v) => v === 0).length,
293
- forcedMoves: 0,
294
- guessedMoves: 0,
295
- recursiveNodes: 0,
296
- backtracks: 0,
297
- maxDepth: 0,
298
- };
299
-
300
- const solved = solve(initial, stats, 0);
301
- if (!solved) {
302
- const rep = {
303
- status: 'unsatisfiable',
304
- raw,
305
- normalized,
306
- givens: stats.givens,
307
- blanks: stats.blanks,
308
- recursiveNodes: stats.recursiveNodes,
309
- backtracks: stats.backtracks,
310
- puzzleText: formatBoard(parsed.cells),
311
- };
312
- __sudokuReportCache.set(raw, rep);
313
- return rep;
314
- }
315
-
316
- const countRef = { count: 0 };
317
- countSolutions(initial, 2, countRef);
318
-
319
- const givensPreserved = parsed.cells.every((v, i) => v === 0 || v === solved.cells[i]);
320
- const noBlanks = solved.cells.every((v) => v >= 1 && v <= 9);
321
- const rowsComplete = Array.from({ length: 9 }, (_, r) =>
322
- unitIsComplete(solved.cells.slice(r * 9, r * 9 + 9)),
323
- ).every(Boolean);
324
- const colsComplete = Array.from({ length: 9 }, (_, c) =>
325
- unitIsComplete(Array.from({ length: 9 }, (_, r) => solved.cells[r * 9 + c])),
326
- ).every(Boolean);
327
- const boxesComplete = Array.from({ length: 9 }, (_, b) => {
328
- const br = Math.floor(b / 3) * 3;
329
- const bc = (b % 3) * 3;
330
- const vals = [];
331
- for (let dr = 0; dr < 3; dr += 1) {
332
- for (let dc = 0; dc < 3; dc += 1) vals.push(solved.cells[(br + dr) * 9 + (bc + dc)]);
333
- }
334
- return unitIsComplete(vals);
335
- }).every(Boolean);
336
- const replayLegal = replayMovesAreLegal(parsed.cells, solved.moves);
337
- const proofPathGuessCount = solved.moves.filter((m) => !m.forced).length;
338
- const storyConsistent =
339
- stats.recursiveNodes >= 1 &&
340
- stats.maxDepth <= stats.blanks &&
341
- solved.moves.length === stats.blanks &&
342
- proofPathGuessCount <= stats.guessedMoves;
343
-
344
- const rep = {
345
- status: 'ok',
346
- raw,
347
- normalized,
348
- givens: stats.givens,
349
- blanks: stats.blanks,
350
- forcedMoves: stats.forcedMoves,
351
- guessedMoves: stats.guessedMoves,
352
- recursiveNodes: stats.recursiveNodes,
353
- backtracks: stats.backtracks,
354
- maxDepth: stats.maxDepth,
355
- unique: countRef.count === 1,
356
- solution: solved.cells.join(''),
357
- puzzleText: formatBoard(parsed.cells),
358
- solutionText: formatBoard(solved.cells),
359
- moveSummary: summarizeMoves(solved.moves, 8),
360
- moveCount: solved.moves.length,
361
- givensPreserved,
362
- noBlanks,
363
- rowsComplete,
364
- colsComplete,
365
- boxesComplete,
366
- replayLegal,
367
- storyConsistent,
368
- };
369
-
370
- __sudokuReportCache.set(raw, rep);
371
- return rep;
372
- }
373
-
374
- function reportFieldAsTerm(report, field) {
375
- if (!report) return null;
376
- if (field === 'status') return makeStringLiteral(report.status);
377
- if (field === 'error') return report.error ? makeStringLiteral(report.error) : null;
378
- if (field === 'normalizedPuzzle') return report.normalized ? makeStringLiteral(report.normalized) : null;
379
- if (field === 'solution') return report.solution ? makeStringLiteral(report.solution) : null;
380
- if (field === 'puzzleText') return report.puzzleText ? makeStringLiteral(report.puzzleText) : null;
381
- if (field === 'solutionText') return report.solutionText ? makeStringLiteral(report.solutionText) : null;
382
- if (field === 'moveSummary') return report.moveSummary ? makeStringLiteral(report.moveSummary) : null;
383
- if (field === 'givensPreservedText')
384
- return report.givensPreserved === undefined ? null : makeStringLiteral(report.givensPreserved ? 'OK' : 'failed');
385
- if (field === 'noBlanksText')
386
- return report.noBlanks === undefined ? null : makeStringLiteral(report.noBlanks ? 'OK' : 'failed');
387
- if (field === 'rowsCompleteText')
388
- return report.rowsComplete === undefined ? null : makeStringLiteral(report.rowsComplete ? 'OK' : 'failed');
389
- if (field === 'colsCompleteText')
390
- return report.colsComplete === undefined ? null : makeStringLiteral(report.colsComplete ? 'OK' : 'failed');
391
- if (field === 'boxesCompleteText')
392
- return report.boxesComplete === undefined ? null : makeStringLiteral(report.boxesComplete ? 'OK' : 'failed');
393
- if (field === 'replayLegalText')
394
- return report.replayLegal === undefined ? null : makeStringLiteral(report.replayLegal ? 'OK' : 'failed');
395
- if (field === 'storyConsistentText')
396
- return report.storyConsistent === undefined ? null : makeStringLiteral(report.storyConsistent ? 'OK' : 'failed');
397
-
398
- const boolFields = [
399
- 'unique',
400
- 'givensPreserved',
401
- 'noBlanks',
402
- 'rowsComplete',
403
- 'colsComplete',
404
- 'boxesComplete',
405
- 'replayLegal',
406
- 'storyConsistent',
407
- ];
408
- if (boolFields.includes(field))
409
- return report[field] === undefined ? null : internLiteral(report[field] ? 'true' : 'false');
410
-
411
- const numberFields = [
412
- 'givens',
413
- 'blanks',
414
- 'forcedMoves',
415
- 'guessedMoves',
416
- 'recursiveNodes',
417
- 'backtracks',
418
- 'maxDepth',
419
- 'moveCount',
420
- ];
421
- if (numberFields.includes(field)) return report[field] === undefined ? null : internLiteral(String(report[field]));
422
-
423
- return null;
424
- }
425
-
426
- function evalSudokuField(goal, subst, field) {
427
- const report = computeReport(goal.s);
428
- if (!report) return [];
429
- const term = reportFieldAsTerm(report, field);
430
- if (!term) return [];
431
- if (goal.o instanceof Var) {
432
- const s2 = { ...subst };
433
- s2[goal.o.name] = term;
434
- return [s2];
435
- }
436
- const s2 = unifyTerm(goal.o, term, subst);
437
- return s2 !== null ? [s2] : [];
438
- }
439
-
440
- const fields = [
441
- 'status',
442
- 'error',
443
- 'normalizedPuzzle',
444
- 'solution',
445
- 'givens',
446
- 'blanks',
447
- 'forcedMoves',
448
- 'guessedMoves',
449
- 'recursiveNodes',
450
- 'backtracks',
451
- 'maxDepth',
452
- 'unique',
453
- 'givensPreserved',
454
- 'noBlanks',
455
- 'rowsComplete',
456
- 'colsComplete',
457
- 'boxesComplete',
458
- 'replayLegal',
459
- 'storyConsistent',
460
- 'givensPreservedText',
461
- 'noBlanksText',
462
- 'rowsCompleteText',
463
- 'colsCompleteText',
464
- 'boxesCompleteText',
465
- 'replayLegalText',
466
- 'storyConsistentText',
467
- 'moveSummary',
468
- 'puzzleText',
469
- 'solutionText',
470
- 'moveCount',
471
- ];
472
-
473
- for (const field of fields) {
474
- registerBuiltin(SUDOKU_NS + field, ({ goal, subst }) => evalSudokuField(goal, subst, field));
475
- }
476
- };
477
-
478
- };
479
11
  __modules["lib/builtins.js"] = function(require, module, exports){
480
12
  /**
481
13
  * Eyeling Reasoner — builtins
@@ -6373,10 +5905,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
6373
5905
  termsEqualNoIntDecimal,
6374
5906
  });
6375
5907
 
6376
- try {
6377
- registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
6378
- } catch (_) {}
6379
-
6380
5908
  // Initialize proof/output helpers (implemented in lib/explain.js).
6381
5909
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
6382
5910
  applySubstTerm,
@@ -9151,10 +8679,21 @@ function reasonStream(input, opts = {}) {
9151
8679
  deref.setEnforceHttpsEnabled(!!enforceHttps);
9152
8680
  proofCommentsEnabled = !!proof;
9153
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
+
9154
8691
  if (Array.isArray(builtinModules)) {
9155
- for (const spec of builtinModules) loadBuiltinModule(spec);
9156
- } else if (typeof builtinModules === 'string' && builtinModules) {
9157
- loadBuiltinModule(builtinModules);
8692
+ for (let i = 0; i < builtinModules.length; i += 1) {
8693
+ registerBuiltinModuleOption(builtinModules[i], i);
8694
+ }
8695
+ } else {
8696
+ registerBuiltinModuleOption(builtinModules, 0);
9158
8697
  }
9159
8698
 
9160
8699
  let prefixes, triples, frules, brules, logQueryRules;
@@ -9439,6 +8978,11 @@ module.exports = {
9439
8978
  tripleToN3: engine.tripleToN3,
9440
8979
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
9441
8980
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,
8981
+ registerBuiltin: engine.registerBuiltin,
8982
+ unregisterBuiltin: engine.unregisterBuiltin,
8983
+ registerBuiltinModule: engine.registerBuiltinModule,
8984
+ loadBuiltinModule: engine.loadBuiltinModule,
8985
+ listBuiltinIris: engine.listBuiltinIris,
9442
8986
  getEnforceHttpsEnabled: engine.getEnforceHttpsEnabled,
9443
8987
  setEnforceHttpsEnabled: engine.setEnforceHttpsEnabled,
9444
8988
  getProofCommentsEnabled: engine.getProofCommentsEnabled,
@@ -10770,7 +10314,7 @@ function parseN3SourceList(input, opts = {}) {
10770
10314
  if (!isN3SourceListInput(input)) return null;
10771
10315
  const sources = input.sources.map(normalizeN3SourceItem);
10772
10316
  const defaultBaseIri = typeof opts.baseIri === 'string' ? opts.baseIri : '';
10773
- const parsed = sources.map(source =>
10317
+ const parsed = sources.map((source) =>
10774
10318
  parseN3Text(source.text, {
10775
10319
  label: source.label,
10776
10320
  baseIri: source.baseIri || (sources.length === 1 ? defaultBaseIri : ''),
package/lib/engine.js CHANGED
@@ -610,10 +610,6 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
610
610
  termsEqualNoIntDecimal,
611
611
  });
612
612
 
613
- try {
614
- registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
615
- } catch (_) {}
616
-
617
613
  // Initialize proof/output helpers (implemented in lib/explain.js).
618
614
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
619
615
  applySubstTerm,
@@ -3388,10 +3384,21 @@ function reasonStream(input, opts = {}) {
3388
3384
  deref.setEnforceHttpsEnabled(!!enforceHttps);
3389
3385
  proofCommentsEnabled = !!proof;
3390
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
+
3391
3396
  if (Array.isArray(builtinModules)) {
3392
- for (const spec of builtinModules) loadBuiltinModule(spec);
3393
- } else if (typeof builtinModules === 'string' && builtinModules) {
3394
- loadBuiltinModule(builtinModules);
3397
+ for (let i = 0; i < builtinModules.length; i += 1) {
3398
+ registerBuiltinModuleOption(builtinModules[i], i);
3399
+ }
3400
+ } else {
3401
+ registerBuiltinModuleOption(builtinModules, 0);
3395
3402
  }
3396
3403
 
3397
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.23.2",
3
+ "version": "1.23.4",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -96,6 +96,14 @@ function resolveExpectedPath(outputDir, inputFile) {
96
96
  return path.join(outputDir, candidates[0]);
97
97
  }
98
98
 
99
+ function resolveExampleBuiltinPath(root, inputFile) {
100
+ const stem = path.basename(inputFile, path.extname(inputFile));
101
+ const rel = path.join('examples', 'builtin', `${stem}.js`);
102
+ const abs = path.join(root, rel);
103
+ if (!fs.existsSync(abs)) return null;
104
+ return { abs, rel };
105
+ }
106
+
99
107
  function main() {
100
108
  const suiteStart = Date.now();
101
109
 
@@ -170,17 +178,32 @@ function main() {
170
178
  const tmpDir = mkTmpDir();
171
179
  const generatedPath = path.join(tmpDir, 'generated.n3');
172
180
 
173
- // Run eyeling on this file (cwd examplesDir so relative behavior matches old script)
181
+ // Run eyeling on this file. If examples/builtin/<stem>.js exists,
182
+ // load it for the matching examples/<stem>.n3 file. Builtin-backed examples
183
+ // run from the repository root so the command shape matches documented usage:
184
+ // node eyeling.js --builtin examples/builtin/foo.js examples/foo.n3
185
+ const builtin = resolveExampleBuiltinPath(root, file);
174
186
  const outFd = fs.openSync(generatedPath, 'w');
175
-
176
- const r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', file], {
177
- cwd: examplesDir,
178
- stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
179
- maxBuffer: 200 * 1024 * 1024,
180
- encoding: 'utf8',
181
- });
182
-
183
- fs.closeSync(outFd);
187
+ let r;
188
+ try {
189
+ if (builtin) {
190
+ r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', '--builtin', builtin.rel, path.join('examples', file)], {
191
+ cwd: root,
192
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
193
+ maxBuffer: 200 * 1024 * 1024,
194
+ encoding: 'utf8',
195
+ });
196
+ } else {
197
+ r = cp.spawnSync(nodePath, [eyelingJsPath, '-d', file], {
198
+ cwd: examplesDir,
199
+ stdio: ['ignore', outFd, 'pipe'], // stdout -> file, stderr captured
200
+ maxBuffer: 200 * 1024 * 1024,
201
+ encoding: 'utf8',
202
+ });
203
+ }
204
+ } finally {
205
+ fs.closeSync(outFd);
206
+ }
184
207
 
185
208
  const rc = r.status == null ? 1 : r.status;
186
209