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/HANDBOOK.md CHANGED
@@ -1891,9 +1891,19 @@ A few practical defaults are worth remembering:
1891
1891
  Custom builtins can be loaded explicitly from the CLI:
1892
1892
 
1893
1893
  ```bash
1894
- npx eyeling --builtin lib/builtin-sudoku.js examples/sudoku.n3
1894
+ npx eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
1895
1895
  ```
1896
1896
 
1897
+ Example-specific builtins live under `examples/builtin/`. When the examples test runner sees `examples/builtin/<stem>.js` next to `examples/<stem>.n3`, it auto-loads that builtin for the matching example by running the same command shape a user would run manually:
1898
+
1899
+ ```bash
1900
+ node eyeling.js --builtin examples/builtin/queens.js examples/queens.n3
1901
+ ```
1902
+
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
+
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
+
1897
1907
  ### 14.2 The bundled Node CLI/runtime (`eyeling.js`)
1898
1908
 
1899
1909
  The bundle contains the whole engine. The CLI path is the “canonical behavior”:
@@ -2376,14 +2386,16 @@ That API keeps the extension boundary explicit: custom builtins get the operatio
2376
2386
 
2377
2387
  ### 16.6 A shipped example: the Sudoku builtin
2378
2388
 
2379
- The repository now ships a Sudoku builtin module (`lib/builtin-sudoku.js`) and a matching example program (`sudoku.n3`).
2389
+ The repository ships a Sudoku example program (`examples/sudoku.n3`) together with its example-specific builtin module (`examples/builtin/sudoku.js`).
2380
2390
 
2381
- So this works out of the box:
2391
+ Run it explicitly like this:
2382
2392
 
2383
2393
  ```bash
2384
- eyeling sudoku.n3
2394
+ eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2385
2395
  ```
2386
2396
 
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.
2398
+
2387
2399
  That example is useful for two reasons:
2388
2400
 
2389
2401
  - it shows a realistic domain-specific builtin implemented outside the core builtin switchboard
@@ -2567,10 +2579,10 @@ It also supports **custom builtin modules**.
2567
2579
  - From JavaScript: `reason({ builtinModules: ['./my-builtins.js'] }, input)`
2568
2580
  - Programmatically in-process: `registerBuiltin(...)`, `registerBuiltinModule(...)`, `loadBuiltinModule(...)`
2569
2581
 
2570
- A concrete shipped example is the Sudoku builtin and the root-level `sudoku.n3` program:
2582
+ A concrete shipped example is the Sudoku builtin paired with `examples/sudoku.n3`:
2571
2583
 
2572
2584
  ```bash
2573
- eyeling sudoku.n3
2585
+ eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2574
2586
  ```
2575
2587
 
2576
2588
  References:
@@ -8,478 +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
385
- ? null
386
- : makeStringLiteral(report.givensPreserved ? 'OK' : 'failed');
387
- if (field === 'noBlanksText')
388
- return report.noBlanks === undefined ? null : makeStringLiteral(report.noBlanks ? 'OK' : 'failed');
389
- if (field === 'rowsCompleteText')
390
- return report.rowsComplete === undefined ? null : makeStringLiteral(report.rowsComplete ? 'OK' : 'failed');
391
- if (field === 'colsCompleteText')
392
- return report.colsComplete === undefined ? null : makeStringLiteral(report.colsComplete ? 'OK' : 'failed');
393
- if (field === 'boxesCompleteText')
394
- return report.boxesComplete === undefined ? null : makeStringLiteral(report.boxesComplete ? 'OK' : 'failed');
395
- if (field === 'replayLegalText')
396
- return report.replayLegal === undefined ? null : makeStringLiteral(report.replayLegal ? 'OK' : 'failed');
397
- if (field === 'storyConsistentText')
398
- return report.storyConsistent === undefined
399
- ? null
400
- : makeStringLiteral(report.storyConsistent ? 'OK' : 'failed');
401
-
402
- const boolFields = [
403
- 'unique',
404
- 'givensPreserved',
405
- 'noBlanks',
406
- 'rowsComplete',
407
- 'colsComplete',
408
- 'boxesComplete',
409
- 'replayLegal',
410
- 'storyConsistent',
411
- ];
412
- if (boolFields.includes(field))
413
- return report[field] === undefined ? null : internLiteral(report[field] ? 'true' : 'false');
414
-
415
- const numberFields = [
416
- 'givens',
417
- 'blanks',
418
- 'forcedMoves',
419
- 'guessedMoves',
420
- 'recursiveNodes',
421
- 'backtracks',
422
- 'maxDepth',
423
- 'moveCount',
424
- ];
425
- if (numberFields.includes(field))
426
- return report[field] === undefined ? null : internLiteral(String(report[field]));
427
-
428
- return null;
429
- }
430
-
431
- function evalSudokuField(goal, subst, field) {
432
- const report = computeReport(goal.s);
433
- if (!report) return [];
434
- const term = reportFieldAsTerm(report, field);
435
- if (!term) return [];
436
- if (goal.o instanceof Var) {
437
- const s2 = { ...subst };
438
- s2[goal.o.name] = term;
439
- return [s2];
440
- }
441
- const s2 = unifyTerm(goal.o, term, subst);
442
- return s2 !== null ? [s2] : [];
443
- }
444
-
445
- const fields = [
446
- 'status',
447
- 'error',
448
- 'normalizedPuzzle',
449
- 'solution',
450
- 'givens',
451
- 'blanks',
452
- 'forcedMoves',
453
- 'guessedMoves',
454
- 'recursiveNodes',
455
- 'backtracks',
456
- 'maxDepth',
457
- 'unique',
458
- 'givensPreserved',
459
- 'noBlanks',
460
- 'rowsComplete',
461
- 'colsComplete',
462
- 'boxesComplete',
463
- 'replayLegal',
464
- 'storyConsistent',
465
- 'givensPreservedText',
466
- 'noBlanksText',
467
- 'rowsCompleteText',
468
- 'colsCompleteText',
469
- 'boxesCompleteText',
470
- 'replayLegalText',
471
- 'storyConsistentText',
472
- 'moveSummary',
473
- 'puzzleText',
474
- 'solutionText',
475
- 'moveCount',
476
- ];
477
-
478
- for (const field of fields) {
479
- registerBuiltin(SUDOKU_NS + field, ({ goal, subst }) => evalSudokuField(goal, subst, field));
480
- }
481
- };
482
- };
483
11
  __modules['lib/builtins.js'] = function (require, module, exports) {
484
12
  /**
485
13
  * Eyeling Reasoner — builtins
@@ -6394,10 +5922,6 @@
6394
5922
  termsEqualNoIntDecimal,
6395
5923
  });
6396
5924
 
6397
- try {
6398
- registerBuiltinModule(require('./builtin-sudoku'), './builtin-sudoku');
6399
- } catch (_) {}
6400
-
6401
5925
  // Initialize proof/output helpers (implemented in lib/explain.js).
6402
5926
  const { printExplanation, collectOutputStringsFromFacts } = makeExplain({
6403
5927
  applySubstTerm,
@@ -9178,10 +8702,21 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
9178
8702
  deref.setEnforceHttpsEnabled(!!enforceHttps);
9179
8703
  proofCommentsEnabled = !!proof;
9180
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
+
9181
8714
  if (Array.isArray(builtinModules)) {
9182
- for (const spec of builtinModules) loadBuiltinModule(spec);
9183
- } else if (typeof builtinModules === 'string' && builtinModules) {
9184
- loadBuiltinModule(builtinModules);
8715
+ for (let i = 0; i < builtinModules.length; i += 1) {
8716
+ registerBuiltinModuleOption(builtinModules[i], i);
8717
+ }
8718
+ } else {
8719
+ registerBuiltinModuleOption(builtinModules, 0);
9185
8720
  }
9186
8721
 
9187
8722
  let prefixes, triples, frules, brules, logQueryRules;
@@ -9465,6 +9000,11 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
9465
9000
  tripleToN3: engine.tripleToN3,
9466
9001
  collectOutputStringsFromFacts: engine.collectOutputStringsFromFacts,
9467
9002
  prettyPrintQueryTriples: engine.prettyPrintQueryTriples,
9003
+ registerBuiltin: engine.registerBuiltin,
9004
+ unregisterBuiltin: engine.unregisterBuiltin,
9005
+ registerBuiltinModule: engine.registerBuiltinModule,
9006
+ loadBuiltinModule: engine.loadBuiltinModule,
9007
+ listBuiltinIris: engine.listBuiltinIris,
9468
9008
  getEnforceHttpsEnabled: engine.getEnforceHttpsEnabled,
9469
9009
  setEnforceHttpsEnabled: engine.setEnforceHttpsEnabled,
9470
9010
  getProofCommentsEnabled: engine.getProofCommentsEnabled,