eyeling 1.17.2 → 1.18.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/eyeling.js CHANGED
@@ -9,6 +9,474 @@
9
9
  const __cache = Object.create(null);
10
10
 
11
11
  // ---- bundled modules ----
12
+ __modules["lib/builtin-sudoku.js"] = function(require, module, exports){
13
+ 'use strict';
14
+
15
+ module.exports = function registerSudokuBuiltins(api) {
16
+ const { registerBuiltin, internLiteral, termToJsString, unifyTerm, terms } = api;
17
+ const { Var } = terms;
18
+
19
+ const SUDOKU_NS = 'http://example.org/sudoku-builtin#';
20
+ const __sudokuReportCache = new Map();
21
+ const __SUDOKU_ALL = 0x1ff;
22
+
23
+ function makeStringLiteral(str) {
24
+ return internLiteral(JSON.stringify(str));
25
+ }
26
+
27
+ function digitMask(v) {
28
+ return 1 << (v - 1);
29
+ }
30
+
31
+ function boxIndex(r, c) {
32
+ return Math.floor(r / 3) * 3 + Math.floor(c / 3);
33
+ }
34
+
35
+ function popcount(mask) {
36
+ let n = 0;
37
+ while (mask) {
38
+ mask &= mask - 1;
39
+ n += 1;
40
+ }
41
+ return n;
42
+ }
43
+
44
+ function maskToDigits(mask) {
45
+ const out = [];
46
+ for (let d = 1; d <= 9; d += 1) if (mask & digitMask(d)) out.push(d);
47
+ return out;
48
+ }
49
+
50
+ function formatBoard(cells) {
51
+ let out = '';
52
+ for (let r = 0; r < 9; r += 1) {
53
+ if (r > 0 && r % 3 === 0) out += '\n';
54
+ for (let c = 0; c < 9; c += 1) {
55
+ if (c > 0 && c % 3 === 0) out += '| ';
56
+ const v = cells[r * 9 + c];
57
+ out += v === 0 ? '. ' : `${String(v)} `;
58
+ }
59
+ out += '\n';
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function parsePuzzle(input) {
65
+ const filtered = [];
66
+ for (const ch of input) {
67
+ if (/\s/.test(ch) || ch === '|' || ch === '+') continue;
68
+ filtered.push(ch);
69
+ }
70
+ if (filtered.length !== 81) {
71
+ return { error: `Expected exactly 81 cells after removing whitespace, but found ${filtered.length}.` };
72
+ }
73
+ const cells = new Array(81).fill(0);
74
+ for (let i = 0; i < 81; i += 1) {
75
+ const ch = filtered[i];
76
+ if (ch >= '1' && ch <= '9') cells[i] = ch.charCodeAt(0) - 48;
77
+ else if (ch === '0' || ch === '.' || ch === '_') cells[i] = 0;
78
+ else return { error: `Unexpected character '${ch}' at position ${i + 1}.` };
79
+ }
80
+ return { cells };
81
+ }
82
+
83
+ function attachMethods(state) {
84
+ state.place = function place(idx, value) {
85
+ if (this.cells[idx] !== 0) return this.cells[idx] === value;
86
+ const row = Math.floor(idx / 9);
87
+ const col = idx % 9;
88
+ const bx = boxIndex(row, col);
89
+ const bit = digitMask(value);
90
+ if (((this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]) & bit) !== 0) return false;
91
+ this.cells[idx] = value;
92
+ this.rowUsed[row] |= bit;
93
+ this.colUsed[col] |= bit;
94
+ this.boxUsed[bx] |= bit;
95
+ return true;
96
+ };
97
+
98
+ state.candidates = function candidates(idx) {
99
+ const row = Math.floor(idx / 9);
100
+ const col = idx % 9;
101
+ const bx = boxIndex(row, col);
102
+ return __SUDOKU_ALL & ~(this.rowUsed[row] | this.colUsed[col] | this.boxUsed[bx]);
103
+ };
104
+
105
+ state.clone = function clone() {
106
+ return attachMethods({
107
+ cells: this.cells.slice(),
108
+ rowUsed: this.rowUsed.slice(),
109
+ colUsed: this.colUsed.slice(),
110
+ boxUsed: this.boxUsed.slice(),
111
+ moves: this.moves.slice(),
112
+ });
113
+ };
114
+
115
+ return state;
116
+ }
117
+
118
+ function stateFromPuzzle(cells) {
119
+ const state = attachMethods({
120
+ cells: new Array(81).fill(0),
121
+ rowUsed: new Array(9).fill(0),
122
+ colUsed: new Array(9).fill(0),
123
+ boxUsed: new Array(9).fill(0),
124
+ moves: [],
125
+ });
126
+
127
+ for (let idx = 0; idx < 81; idx += 1) {
128
+ const value = cells[idx];
129
+ if (value === 0) continue;
130
+ if (value < 1 || value > 9) {
131
+ return { error: `Cell ${idx + 1} contains ${value}, but only digits 1-9 or 0/. are allowed.` };
132
+ }
133
+ if (!state.place(idx, value)) {
134
+ const row = Math.floor(idx / 9) + 1;
135
+ const col = (idx % 9) + 1;
136
+ return { error: `The given clues already conflict at row ${row}, column ${col}.` };
137
+ }
138
+ }
139
+
140
+ return { state };
141
+ }
142
+
143
+ function summarizeMoves(moves, limit) {
144
+ if (!moves.length) return 'no placements were needed';
145
+ const parts = [];
146
+ for (const mv of moves.slice(0, limit)) {
147
+ const row = Math.floor(mv.index / 9) + 1;
148
+ const col = (mv.index % 9) + 1;
149
+ const mode = mv.forced ? 'forced' : 'guess';
150
+ parts.push(`r${row}c${col}=${mv.value}: ${mode}`);
151
+ }
152
+ if (moves.length > limit) parts.push(`… and ${moves.length - limit} more placements`);
153
+ return parts.join(', ');
154
+ }
155
+
156
+ function unitIsComplete(values) {
157
+ let seen = 0;
158
+ for (const v of values) {
159
+ if (v < 1 || v > 9) return false;
160
+ const bit = digitMask(v);
161
+ if (seen & bit) return false;
162
+ seen |= bit;
163
+ }
164
+ return seen === __SUDOKU_ALL;
165
+ }
166
+
167
+ function replayMovesAreLegal(puzzleCells, moves) {
168
+ const init = stateFromPuzzle(puzzleCells);
169
+ if (init.error) return false;
170
+ const state = init.state;
171
+ for (const mv of moves) {
172
+ if (state.cells[mv.index] !== 0) return false;
173
+ const maskNow = state.candidates(mv.index);
174
+ if (maskNow !== mv.candidatesMask) return false;
175
+ if ((maskNow & digitMask(mv.value)) === 0) return false;
176
+ if (mv.forced && popcount(maskNow) !== 1) return false;
177
+ if (!state.place(mv.index, mv.value)) return false;
178
+ }
179
+ return true;
180
+ }
181
+
182
+ function propagateSingles(state, stats) {
183
+ for (;;) {
184
+ let progress = false;
185
+ for (let idx = 0; idx < 81; idx += 1) {
186
+ if (state.cells[idx] !== 0) continue;
187
+ const mask = state.candidates(idx);
188
+ const count = popcount(mask);
189
+ if (count === 0) return false;
190
+ if (count === 1) {
191
+ const digit = maskToDigits(mask)[0];
192
+ state.moves.push({ index: idx, value: digit, candidatesMask: mask, forced: true });
193
+ if (!state.place(idx, digit)) return false;
194
+ stats.forcedMoves += 1;
195
+ progress = true;
196
+ }
197
+ }
198
+ if (!progress) return true;
199
+ }
200
+ }
201
+
202
+ function selectUnfilledCell(state) {
203
+ let best = null;
204
+ for (let idx = 0; idx < 81; idx += 1) {
205
+ if (state.cells[idx] !== 0) continue;
206
+ const mask = state.candidates(idx);
207
+ const count = popcount(mask);
208
+ if (best === null || count < best.count) best = { idx, mask, count };
209
+ if (count === 2) break;
210
+ }
211
+ return best;
212
+ }
213
+
214
+ function solve(state, stats, depth) {
215
+ stats.recursiveNodes += 1;
216
+ if (depth > stats.maxDepth) stats.maxDepth = depth;
217
+ const current = state.clone();
218
+ if (!propagateSingles(current, stats)) {
219
+ stats.backtracks += 1;
220
+ return null;
221
+ }
222
+ const best = selectUnfilledCell(current);
223
+ if (!best) return current;
224
+ for (const digit of maskToDigits(best.mask)) {
225
+ const next = current.clone();
226
+ const candidatesMask = next.candidates(best.idx);
227
+ next.moves.push({ index: best.idx, value: digit, candidatesMask, forced: false });
228
+ stats.guessedMoves += 1;
229
+ if (!next.place(best.idx, digit)) continue;
230
+ const solved = solve(next, stats, depth + 1);
231
+ if (solved) return solved;
232
+ }
233
+ stats.backtracks += 1;
234
+ return null;
235
+ }
236
+
237
+ function countSolutions(state, limit, countRef) {
238
+ if (countRef.count >= limit) return;
239
+ const current = state.clone();
240
+ const dummy = {
241
+ givens: 0,
242
+ blanks: 0,
243
+ forcedMoves: 0,
244
+ guessedMoves: 0,
245
+ recursiveNodes: 0,
246
+ backtracks: 0,
247
+ maxDepth: 0,
248
+ };
249
+ if (!propagateSingles(current, dummy)) return;
250
+ const best = selectUnfilledCell(current);
251
+ if (!best) {
252
+ countRef.count += 1;
253
+ return;
254
+ }
255
+ for (const digit of maskToDigits(best.mask)) {
256
+ if (countRef.count >= limit) return;
257
+ const next = current.clone();
258
+ if (next.place(best.idx, digit)) countSolutions(next, limit, countRef);
259
+ }
260
+ }
261
+
262
+ function computeReport(term) {
263
+ const raw = termToJsString(term);
264
+ if (raw === null) return null;
265
+ if (__sudokuReportCache.has(raw)) return __sudokuReportCache.get(raw);
266
+
267
+ const parsed = parsePuzzle(raw);
268
+ if (parsed.error) {
269
+ const rep = { status: 'invalid-input', error: parsed.error, raw, normalized: null };
270
+ __sudokuReportCache.set(raw, rep);
271
+ return rep;
272
+ }
273
+
274
+ const normalized = parsed.cells.join('');
275
+ const init = stateFromPuzzle(parsed.cells);
276
+ if (init.error) {
277
+ const rep = {
278
+ status: 'illegal-clues',
279
+ error: init.error,
280
+ raw,
281
+ normalized,
282
+ givens: parsed.cells.filter((v) => v !== 0).length,
283
+ blanks: parsed.cells.filter((v) => v === 0).length,
284
+ puzzleText: formatBoard(parsed.cells),
285
+ };
286
+ __sudokuReportCache.set(raw, rep);
287
+ return rep;
288
+ }
289
+
290
+ const initial = init.state;
291
+ const stats = {
292
+ givens: parsed.cells.filter((v) => v !== 0).length,
293
+ blanks: parsed.cells.filter((v) => v === 0).length,
294
+ forcedMoves: 0,
295
+ guessedMoves: 0,
296
+ recursiveNodes: 0,
297
+ backtracks: 0,
298
+ maxDepth: 0,
299
+ };
300
+
301
+ const solved = solve(initial, stats, 0);
302
+ if (!solved) {
303
+ const rep = {
304
+ status: 'unsatisfiable',
305
+ raw,
306
+ normalized,
307
+ givens: stats.givens,
308
+ blanks: stats.blanks,
309
+ recursiveNodes: stats.recursiveNodes,
310
+ backtracks: stats.backtracks,
311
+ puzzleText: formatBoard(parsed.cells),
312
+ };
313
+ __sudokuReportCache.set(raw, rep);
314
+ return rep;
315
+ }
316
+
317
+ const countRef = { count: 0 };
318
+ countSolutions(initial, 2, countRef);
319
+
320
+ const givensPreserved = parsed.cells.every((v, i) => v === 0 || v === solved.cells[i]);
321
+ const noBlanks = solved.cells.every((v) => v >= 1 && v <= 9);
322
+ const rowsComplete = Array.from({ length: 9 }, (_, r) =>
323
+ unitIsComplete(solved.cells.slice(r * 9, r * 9 + 9)),
324
+ ).every(Boolean);
325
+ const colsComplete = Array.from({ length: 9 }, (_, c) =>
326
+ unitIsComplete(Array.from({ length: 9 }, (_, r) => solved.cells[r * 9 + c])),
327
+ ).every(Boolean);
328
+ const boxesComplete = Array.from({ length: 9 }, (_, b) => {
329
+ const br = Math.floor(b / 3) * 3;
330
+ const bc = (b % 3) * 3;
331
+ const vals = [];
332
+ for (let dr = 0; dr < 3; dr += 1) {
333
+ for (let dc = 0; dc < 3; dc += 1) vals.push(solved.cells[(br + dr) * 9 + (bc + dc)]);
334
+ }
335
+ return unitIsComplete(vals);
336
+ }).every(Boolean);
337
+ const replayLegal = replayMovesAreLegal(parsed.cells, solved.moves);
338
+ const proofPathGuessCount = solved.moves.filter((m) => !m.forced).length;
339
+ const storyConsistent =
340
+ stats.recursiveNodes >= 1 &&
341
+ stats.maxDepth <= stats.blanks &&
342
+ solved.moves.length === stats.blanks &&
343
+ proofPathGuessCount <= stats.guessedMoves;
344
+
345
+ const rep = {
346
+ status: 'ok',
347
+ raw,
348
+ normalized,
349
+ givens: stats.givens,
350
+ blanks: stats.blanks,
351
+ forcedMoves: stats.forcedMoves,
352
+ guessedMoves: stats.guessedMoves,
353
+ recursiveNodes: stats.recursiveNodes,
354
+ backtracks: stats.backtracks,
355
+ maxDepth: stats.maxDepth,
356
+ unique: countRef.count === 1,
357
+ solution: solved.cells.join(''),
358
+ puzzleText: formatBoard(parsed.cells),
359
+ solutionText: formatBoard(solved.cells),
360
+ moveSummary: summarizeMoves(solved.moves, 8),
361
+ moveCount: solved.moves.length,
362
+ givensPreserved,
363
+ noBlanks,
364
+ rowsComplete,
365
+ colsComplete,
366
+ boxesComplete,
367
+ replayLegal,
368
+ storyConsistent,
369
+ };
370
+
371
+ __sudokuReportCache.set(raw, rep);
372
+ return rep;
373
+ }
374
+
375
+ function reportFieldAsTerm(report, field) {
376
+ if (!report) return null;
377
+ if (field === 'status') return makeStringLiteral(report.status);
378
+ if (field === 'error') return report.error ? makeStringLiteral(report.error) : null;
379
+ if (field === 'normalizedPuzzle') return report.normalized ? makeStringLiteral(report.normalized) : null;
380
+ if (field === 'solution') return report.solution ? makeStringLiteral(report.solution) : null;
381
+ if (field === 'puzzleText') return report.puzzleText ? makeStringLiteral(report.puzzleText) : null;
382
+ if (field === 'solutionText') return report.solutionText ? makeStringLiteral(report.solutionText) : null;
383
+ if (field === 'moveSummary') return report.moveSummary ? makeStringLiteral(report.moveSummary) : null;
384
+ if (field === 'givensPreservedText')
385
+ return report.givensPreserved === undefined ? null : makeStringLiteral(report.givensPreserved ? 'OK' : 'failed');
386
+ if (field === 'noBlanksText')
387
+ return report.noBlanks === undefined ? null : makeStringLiteral(report.noBlanks ? 'OK' : 'failed');
388
+ if (field === 'rowsCompleteText')
389
+ return report.rowsComplete === undefined ? null : makeStringLiteral(report.rowsComplete ? 'OK' : 'failed');
390
+ if (field === 'colsCompleteText')
391
+ return report.colsComplete === undefined ? null : makeStringLiteral(report.colsComplete ? 'OK' : 'failed');
392
+ if (field === 'boxesCompleteText')
393
+ return report.boxesComplete === undefined ? null : makeStringLiteral(report.boxesComplete ? 'OK' : 'failed');
394
+ if (field === 'replayLegalText')
395
+ return report.replayLegal === undefined ? null : makeStringLiteral(report.replayLegal ? 'OK' : 'failed');
396
+ if (field === 'storyConsistentText')
397
+ return report.storyConsistent === undefined ? null : makeStringLiteral(report.storyConsistent ? 'OK' : 'failed');
398
+
399
+ const boolFields = [
400
+ 'unique',
401
+ 'givensPreserved',
402
+ 'noBlanks',
403
+ 'rowsComplete',
404
+ 'colsComplete',
405
+ 'boxesComplete',
406
+ 'replayLegal',
407
+ 'storyConsistent',
408
+ ];
409
+ if (boolFields.includes(field))
410
+ return report[field] === undefined ? null : internLiteral(report[field] ? 'true' : 'false');
411
+
412
+ const numberFields = [
413
+ 'givens',
414
+ 'blanks',
415
+ 'forcedMoves',
416
+ 'guessedMoves',
417
+ 'recursiveNodes',
418
+ 'backtracks',
419
+ 'maxDepth',
420
+ 'moveCount',
421
+ ];
422
+ if (numberFields.includes(field)) return report[field] === undefined ? null : internLiteral(String(report[field]));
423
+
424
+ return null;
425
+ }
426
+
427
+ function evalSudokuField(goal, subst, field) {
428
+ const report = computeReport(goal.s);
429
+ if (!report) return [];
430
+ const term = reportFieldAsTerm(report, field);
431
+ if (!term) return [];
432
+ if (goal.o instanceof Var) {
433
+ const s2 = { ...subst };
434
+ s2[goal.o.name] = term;
435
+ return [s2];
436
+ }
437
+ const s2 = unifyTerm(goal.o, term, subst);
438
+ return s2 !== null ? [s2] : [];
439
+ }
440
+
441
+ const fields = [
442
+ 'status',
443
+ 'error',
444
+ 'normalizedPuzzle',
445
+ 'solution',
446
+ 'givens',
447
+ 'blanks',
448
+ 'forcedMoves',
449
+ 'guessedMoves',
450
+ 'recursiveNodes',
451
+ 'backtracks',
452
+ 'maxDepth',
453
+ 'unique',
454
+ 'givensPreserved',
455
+ 'noBlanks',
456
+ 'rowsComplete',
457
+ 'colsComplete',
458
+ 'boxesComplete',
459
+ 'replayLegal',
460
+ 'storyConsistent',
461
+ 'givensPreservedText',
462
+ 'noBlanksText',
463
+ 'rowsCompleteText',
464
+ 'colsCompleteText',
465
+ 'boxesCompleteText',
466
+ 'replayLegalText',
467
+ 'storyConsistentText',
468
+ 'moveSummary',
469
+ 'puzzleText',
470
+ 'solutionText',
471
+ 'moveCount',
472
+ ];
473
+
474
+ for (const field of fields) {
475
+ registerBuiltin(SUDOKU_NS + field, ({ goal, subst }) => evalSudokuField(goal, subst, field));
476
+ }
477
+ };
478
+
479
+ };
12
480
  __modules["lib/builtins.js"] = function(require, module, exports){
13
481
  /**
14
482
  * Eyeling Reasoner — builtins
@@ -73,6 +541,152 @@ function __useNumericCacheKey(key) {
73
541
  return typeof key === 'string' && key.length <= MAX_NUMERIC_CACHE_KEY_LEN;
74
542
  }
75
543
 
544
+ // ---------------------------------------------------------------------------
545
+ // Custom builtin registry
546
+ // ---------------------------------------------------------------------------
547
+ const __customBuiltinHandlers = new Map(); // predicate IRI -> evaluator(ctx) => deltas[]
548
+ const __loadedBuiltinModuleIds = new Set();
549
+
550
+ function __validateBuiltinIri(iri) {
551
+ if (typeof iri !== 'string' || !iri) {
552
+ throw new TypeError('Custom builtin IRI must be a non-empty string');
553
+ }
554
+ }
555
+
556
+ function registerBuiltin(iri, handler) {
557
+ __validateBuiltinIri(iri);
558
+ if (typeof handler !== 'function') {
559
+ throw new TypeError(`Custom builtin ${iri} must be registered with a function handler`);
560
+ }
561
+ __customBuiltinHandlers.set(iri, handler);
562
+ return handler;
563
+ }
564
+
565
+ function unregisterBuiltin(iri) {
566
+ return __customBuiltinHandlers.delete(iri);
567
+ }
568
+
569
+ function listBuiltinIris() {
570
+ return Array.from(__customBuiltinHandlers.keys()).sort();
571
+ }
572
+
573
+ function __buildBuiltinRegistrationApi() {
574
+ return {
575
+ registerBuiltin,
576
+ unregisterBuiltin,
577
+ listBuiltinIris,
578
+ internIri,
579
+ internLiteral,
580
+ literalParts,
581
+ termToJsString,
582
+ termToJsStringDecoded,
583
+ termToN3,
584
+ iriValue,
585
+ unifyTerm,
586
+ applySubstTerm,
587
+ applySubstTriple,
588
+ proveGoals,
589
+ isGroundTerm,
590
+ computeConclusionFromFormula,
591
+ skolemIriFromGroundTerm,
592
+ parseBooleanLiteralInfo,
593
+ parseNumericLiteralInfo,
594
+ parseXsdDecimalToBigIntScale,
595
+ pow10n,
596
+ normalizeLiteralForFastKey,
597
+ literalsEquivalentAsXsdString,
598
+ materializeRdfLists,
599
+ terms: { Literal, Iri, Var, Blank, ListTerm, OpenListTerm, GraphTerm, Triple, Rule },
600
+ ns: { RDF_NS, XSD_NS, CRYPTO_NS, MATH_NS, TIME_NS, LIST_NS, LOG_NS, STRING_NS },
601
+ };
602
+ }
603
+
604
+ function registerBuiltinModule(mod, origin = '<builtin-module>') {
605
+ if (!mod) throw new TypeError(`Builtin module ${origin} did not export anything`);
606
+
607
+ const api = __buildBuiltinRegistrationApi();
608
+
609
+ if (typeof mod === 'function') {
610
+ mod(api);
611
+ return true;
612
+ }
613
+
614
+ if (typeof mod.register === 'function') {
615
+ mod.register(api);
616
+ return true;
617
+ }
618
+
619
+ const candidates = [];
620
+ if (mod && typeof mod.builtins === 'object' && mod.builtins) candidates.push(mod.builtins);
621
+ if (mod && typeof mod.default === 'object' && mod.default) candidates.push(mod.default);
622
+ candidates.push(mod);
623
+
624
+ let registeredAny = false;
625
+ for (const obj of candidates) {
626
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) continue;
627
+ for (const [iri, handler] of Object.entries(obj)) {
628
+ if (typeof handler !== 'function') continue;
629
+ registerBuiltin(iri, handler);
630
+ registeredAny = true;
631
+ }
632
+ if (registeredAny) return true;
633
+ }
634
+
635
+ throw new TypeError(
636
+ `Builtin module ${origin} must export a function, a { register() } object, or an object mapping predicate IRIs to handlers`,
637
+ );
638
+ }
639
+
640
+ function loadBuiltinModule(specifier, options = {}) {
641
+ if (typeof require !== 'function') {
642
+ throw new Error('Custom builtin modules can only be loaded when require() is available');
643
+ }
644
+ if (typeof specifier !== 'string' || !specifier) {
645
+ throw new TypeError('Builtin module specifier must be a non-empty string');
646
+ }
647
+
648
+ const path = require('node:path');
649
+ const resolved = options && options.resolveFrom ? path.resolve(options.resolveFrom, specifier) : specifier;
650
+ const moduleId = String(resolved);
651
+ if (__loadedBuiltinModuleIds.has(moduleId)) return moduleId;
652
+
653
+ const loaded = require(resolved);
654
+ registerBuiltinModule(loaded, moduleId);
655
+ __loadedBuiltinModuleIds.add(moduleId);
656
+ return moduleId;
657
+ }
658
+
659
+ function __evalRegisteredBuiltin(pv, goal, subst, facts, backRules, depth, varGen, maxResults) {
660
+ const handler = __customBuiltinHandlers.get(pv);
661
+ if (typeof handler !== 'function') return null;
662
+
663
+ const ctx = {
664
+ iri: pv,
665
+ goal,
666
+ subst,
667
+ facts,
668
+ backRules,
669
+ depth,
670
+ varGen,
671
+ maxResults,
672
+ api: __buildBuiltinRegistrationApi(),
673
+ };
674
+
675
+ try {
676
+ const out = handler(ctx);
677
+ if (out == null) return [];
678
+ if (!Array.isArray(out)) {
679
+ throw new TypeError(`Custom builtin ${pv} must return an array of substitution deltas`);
680
+ }
681
+ return out;
682
+ } catch (err) {
683
+ if (err && typeof err === 'object' && typeof err.message === 'string') {
684
+ err.message = `Error in custom builtin ${pv}: ${err.message}`;
685
+ }
686
+ throw err;
687
+ }
688
+ }
689
+
76
690
  //
77
691
  // Engine hooks (injected once by makeBuiltins)
78
692
  //
@@ -1478,6 +2092,9 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
1478
2092
  if (pv !== allow1 && pv !== allow2) return [];
1479
2093
  }
1480
2094
 
2095
+ const registeredBuiltinResult = __evalRegisteredBuiltin(pv, g, subst, facts, backRules, depth, varGen, maxResults);
2096
+ if (registeredBuiltinResult !== null) return registeredBuiltinResult;
2097
+
1481
2098
  // -----------------------------------------------------------------
1482
2099
  // 4.1 crypto: builtins
1483
2100
  // -----------------------------------------------------------------
@@ -3689,6 +4306,8 @@ function isBuiltinPred(p) {
3689
4306
  return true;
3690
4307
  }
3691
4308
 
4309
+ if (__customBuiltinHandlers.has(v)) return true;
4310
+
3692
4311
  return (
3693
4312
  v.startsWith(CRYPTO_NS) ||
3694
4313
  v.startsWith(MATH_NS) ||
@@ -3846,6 +4465,11 @@ function listHasTriple(list, tr) {
3846
4465
 
3847
4466
  module.exports = {
3848
4467
  makeBuiltins,
4468
+ registerBuiltin,
4469
+ unregisterBuiltin,
4470
+ registerBuiltinModule,
4471
+ loadBuiltinModule,
4472
+ listBuiltinIris,
3849
4473
  // shared helpers used by engine/explain
3850
4474
  parseBooleanLiteralInfo,
3851
4475
  parseNumericLiteralInfo,
@@ -3922,7 +4546,7 @@ function main() {
3922
4546
  argv.push(a);
3923
4547
  continue;
3924
4548
  }
3925
- // Combined short flags (no flag in eyeling takes a value)
4549
+ // Combined short flags (the long --builtin option takes a value)
3926
4550
  for (const ch of a.slice(1)) argv.push('-' + ch);
3927
4551
  }
3928
4552
  const prog = String(process.argv[1] || 'eyeling')
@@ -3934,6 +4558,7 @@ function main() {
3934
4558
  `Usage: ${prog} [options] <file.n3>\n\n` +
3935
4559
  `Options:\n` +
3936
4560
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
4561
+ ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
3937
4562
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
3938
4563
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
3939
4564
  ` -h, --help Show this help and exit.\n` +
@@ -3956,6 +4581,27 @@ function main() {
3956
4581
  process.exit(0);
3957
4582
  }
3958
4583
 
4584
+ const builtinModules = [];
4585
+ const positional = [];
4586
+ for (let i = 0; i < argv.length; i++) {
4587
+ const a = argv[i];
4588
+ if (a === '--builtin') {
4589
+ const next = argv[i + 1];
4590
+ if (!next || next.startsWith('-')) {
4591
+ console.error('Error: --builtin expects a module path.');
4592
+ process.exit(1);
4593
+ }
4594
+ builtinModules.push(next);
4595
+ i += 1;
4596
+ continue;
4597
+ }
4598
+ if (typeof a === 'string' && a.startsWith('--builtin=')) {
4599
+ builtinModules.push(a.slice('--builtin='.length));
4600
+ continue;
4601
+ }
4602
+ if (!a.startsWith('-')) positional.push(a);
4603
+ }
4604
+
3959
4605
  const showAst = argv.includes('--ast') || argv.includes('-a');
3960
4606
  const streamMode = argv.includes('--stream') || argv.includes('-t');
3961
4607
 
@@ -3980,7 +4626,6 @@ function main() {
3980
4626
  }
3981
4627
 
3982
4628
  // Positional args (the N3 file)
3983
- const positional = argv.filter((a) => !a.startsWith('-'));
3984
4629
  if (positional.length === 0) {
3985
4630
  printHelp(false);
3986
4631
  process.exit(0);
@@ -3991,6 +4636,16 @@ function main() {
3991
4636
  process.exit(1);
3992
4637
  }
3993
4638
 
4639
+ for (const spec of builtinModules) {
4640
+ try {
4641
+ if (typeof engine.loadBuiltinModule === 'function')
4642
+ engine.loadBuiltinModule(spec, { resolveFrom: process.cwd() });
4643
+ } catch (e) {
4644
+ console.error(`Error loading builtin module ${JSON.stringify(spec)}: ${e && e.message ? e.message : String(e)}`);
4645
+ process.exit(1);
4646
+ }
4647
+ }
4648
+
3994
4649
  const filePath = positional[0];
3995
4650
  let text;
3996
4651
  try {
@@ -4754,6 +5409,11 @@ const { liftBlankRuleVars } = require('./rules');
4754
5409
 
4755
5410
  const {
4756
5411
  makeBuiltins,
5412
+ registerBuiltin,
5413
+ unregisterBuiltin,
5414
+ registerBuiltinModule,
5415
+ loadBuiltinModule,
5416
+ listBuiltinIris,
4757
5417
  // helpers used by engine core
4758
5418
  parseBooleanLiteralInfo,
4759
5419
  parseNumericLiteralInfo,
@@ -5240,6 +5900,10 @@ const { evalBuiltin, isBuiltinPred } = makeBuiltins({
5240
5900
  termsEqualNoIntDecimal,
5241
5901
  });
5242
5902
 
5903
+ try {
5904
+ registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
5905
+ } catch (_) {}
5906
+
5243
5907
  // Initialize proof/output helpers (implemented in lib/explain.js).
5244
5908
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
5245
5909
  applySubstTerm,
@@ -7908,6 +8572,7 @@ function reasonStream(input, opts = {}) {
7908
8572
  enforceHttps = false,
7909
8573
  rdfjs = false,
7910
8574
  dataFactory = null,
8575
+ builtinModules = null,
7911
8576
  } = opts;
7912
8577
 
7913
8578
  const parsedInput = normalizeParsedReasonerInputSync(input);
@@ -7917,6 +8582,12 @@ function reasonStream(input, opts = {}) {
7917
8582
  deref.setEnforceHttpsEnabled(!!enforceHttps);
7918
8583
  proofCommentsEnabled = !!proof;
7919
8584
 
8585
+ if (Array.isArray(builtinModules)) {
8586
+ for (const spec of builtinModules) loadBuiltinModule(spec);
8587
+ } else if (typeof builtinModules === 'string' && builtinModules) {
8588
+ loadBuiltinModule(builtinModules);
8589
+ }
8590
+
7920
8591
  let prefixes, triples, frules, brules, logQueryRules;
7921
8592
 
7922
8593
  if (parsedInput) {
@@ -8141,6 +8812,11 @@ module.exports = {
8141
8812
  setTracePrefixes,
8142
8813
  getDeterministicSkolemEnabled,
8143
8814
  setDeterministicSkolemEnabled,
8815
+ registerBuiltin,
8816
+ unregisterBuiltin,
8817
+ registerBuiltinModule,
8818
+ loadBuiltinModule,
8819
+ listBuiltinIris,
8144
8820
  };
8145
8821
 
8146
8822
  };