eyeling 1.27.6 → 1.27.7

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 DELETED
@@ -1,3949 +0,0 @@
1
- # Inside Eyeling
2
-
3
- ## A compact Notation3 reasoner in JavaScript — a handbook
4
-
5
- > This handbook is written for a computer science student who wants to understand Eyeling as _code_ and as a _reasoning machine_.
6
- > It is meant to be read linearly, but each chapter stands on its own.
7
-
8
- ## Contents
9
-
10
- - [Preface](#preface)
11
- - [Chapter 1 — The execution model in one picture](#ch01)
12
- - [Chapter 2 — The repository, as a guided reading path](#ch02)
13
- - [Chapter 3 — The data model: terms, triples, formulas, rules](#ch03)
14
- - [Chapter 4 — From characters to AST: lexing and parsing](#ch04)
15
- - [Chapter 5 — Rule normalization: “compile-time” semantics](#ch05)
16
- - [Chapter 6 — Equality, alpha-equivalence, and unification](#ch06)
17
- - [Chapter 7 — Facts as a database: indexing and fast duplicate checks](#ch07)
18
- - [Chapter 8 — Backward chaining: the proof engine](#ch08)
19
- - [Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules](#ch09)
20
- - [Chapter 10 — Scoped closure, priorities, and `log:conclusion`](#ch10)
21
- - [Chapter 11 — Built-ins as a standard library](#ch11)
22
- - [Chapter 12 — Dereferencing and web-like semantics](#ch12)
23
- - [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
24
- - [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
25
- - [Chapter 15 — A worked example: Socrates, step by step](#ch15)
26
- - [Chapter 16 — Extending Eyeling (without breaking it)](#ch16)
27
- - [Epilogue](#epilogue)
28
- - [Appendix A — Eyeling user notes](#app-a)
29
- - [Appendix B — Notation3: when facts can carry their own logic](#app-b)
30
- - [Appendix C — Why N3 fits the Eyeling examples](#app-c)
31
- - [Appendix D — LLM + Eyeling: A Repeatable Logic Toolchain](#app-d)
32
- - [Appendix E — How Eyeling reaches 100% on `notation3tests`](#app-e)
33
- - [Appendix F — The ARC approach: Answer • Reason Why • Check](#app-f)
34
- - [Appendix G — Eyeling and the W3C CG Notation3 Semantics](#app-g)
35
- - [Appendix H — Applied Constructor-Theory and the N3 ARC examples](#app-h)
36
- - [Appendix I — The Eyeling Playground](#app-i)
37
- - [Appendix J — Formalism Is Fine](#app-j)
38
- - [Appendix K — Whitehead-inspired becoming examples](#app-k)
39
-
40
- ---
41
-
42
- <a id="preface"></a>
43
-
44
- ## Preface: what Eyeling is (and what it is not)
45
-
46
- Eyeling is a small Notation3 (N3) reasoner implemented in JavaScript. Its job is to take:
47
-
48
- 1. **Facts** (RDF-like triples), and
49
- 2. **Rules** written in N3’s implication style (`=>` and `<=`),
50
-
51
- and compute consequences until nothing new follows.
52
-
53
- If you have seen Datalog or Prolog, the shape will feel familiar. Eyeling blends both:
54
-
55
- - **Forward chaining** (like Datalog saturation) for `=>` rules.
56
- - **Backward chaining** (like Prolog goal solving) for `<=` rules _and_ for built-in predicates.
57
-
58
- That last point is the heart of Eyeling’s design: _forward rules are executed by proving their bodies using a backward engine_. This lets forward rules depend on computations and “virtual predicates” without explicitly materializing everything as facts.
59
-
60
- Eyeling deliberately keeps the implementation small and dependency-free:
61
-
62
- - the published package includes a Node-oriented bundle (`eyeling.js`) and a dedicated browser bundle (`dist/browser/eyeling.browser.js`)
63
- - the source is organized into `lib/*` modules that read like a miniature compiler + logic engine.
64
-
65
- This handbook is a tour of that miniature system.
66
-
67
- ---
68
-
69
- <a id="ch01"></a>
70
-
71
- ## Chapter 1 — The execution model in one picture
72
-
73
- Let’s name the pieces:
74
-
75
- - A **fact** is a triple `(subject, predicate, object)`.
76
- - A **forward rule** has the form `{ body } => { head }.`
77
- Read: if the body is provable, assert the head.
78
- - A **backward rule** has the form `{ head } <= { body }.`
79
- Read: to prove the head, prove the body.
80
-
81
- Eyeling runs like this:
82
-
83
- 1. Parse the document into:
84
- - an initial fact set `F`
85
- - forward rules `R_f`
86
- - backward rules `R_b`
87
- 2. Repeat until fixpoint:
88
- - for each forward rule `r ∈ R_f`:
89
- - use the backward prover to find substitutions that satisfy `r.body` using:
90
- - the current facts
91
- - backward rules
92
- - built-ins
93
- - for each solution, instantiate and add `r.head`
94
-
95
- A good mental model is:
96
-
97
- > **Forward chaining is “outer control”. Backward chaining is the “query engine” used inside each rule firing.**
98
-
99
- A sketch:
100
-
101
- ```
102
-
103
- FORWARD LOOP (saturation)
104
- for each forward rule r:
105
- solutions = PROVE(r.body) <-- backward reasoning + builtins
106
- for each s in solutions:
107
- emit instantiate(r.head, s)
108
-
109
- ```
110
-
111
- Because `PROVE` can call built-ins (math, string, list, crypto, dereferencing…), forward rules can compute fresh bindings as part of their condition.
112
-
113
- ---
114
-
115
- <a id="ch02"></a>
116
-
117
- ## Chapter 2 — The repository, as a guided reading path
118
-
119
- If you want to follow the code in the same order Eyeling “thinks”, read:
120
-
121
- 1. `lib/prelude.js` — the AST (terms, triples, rules), namespaces, prefix handling.
122
- 2. `lib/lexer.js` — N3/Turtle-ish tokenization.
123
- 3. `lib/parser.js` — parsing tokens into triples, formulas, and rules.
124
- 4. `lib/rules.js` — small rule helpers (rule-local blank lifting and rule utilities).
125
- 5. `lib/engine.js` — the core inference engine:
126
- - equality + alpha equivalence for formulas
127
- - unification + substitutions
128
- - indexing facts and backward rules
129
- - backward goal proving (`proveGoals`) and forward saturation (`forwardChain`)
130
- - scoped-closure machinery (for `log:*In` and includes tests)
131
- - tracing hooks (`lib/trace.js`, `log:trace`)
132
- - time helpers for `time:*` built-ins (`lib/time.js`)
133
- - deterministic Skolem IDs (head existentials + `log:skolem`) (`lib/skolem.js`)
134
- 6. `lib/builtins.js` — builtin predicate evaluation plus shared literal/number/string/list helpers:
135
- - `makeBuiltins(deps)` dependency-injects engine hooks (unification, proving, deref, …)
136
- - and returns `{ evalBuiltin, isBuiltinPred }` back to the engine
137
- - includes `materializeRdfLists(...)`, a small pre-pass that rewrites _anonymous_ `rdf:first`/`rdf:rest` linked lists into concrete N3 list terms so `list:*` builtins can work uniformly
138
- 7. `lib/explain.js` — proof comments + `log:outputString` aggregation (fact ordering and pretty output).
139
- 8. `lib/deref.js` — synchronous dereferencing for `log:content` / `log:semantics` (used by builtins and engine).
140
- 9. `lib/printing.js` — conversion back to N3 text.
141
- 10. `lib/cli.js` + `lib/entry.js` — command-line wiring and bundle entry exports.
142
- 11. `index.js` — the npm API wrapper (spawns the bundled CLI synchronously).
143
-
144
- This is very nearly a tiny compiler pipeline:
145
-
146
- ```
147
-
148
- text → tokens → AST (facts + rules) → engine → derived facts → printer
149
-
150
- ```
151
-
152
- ---
153
-
154
- <a id="ch03"></a>
155
-
156
- ## Chapter 3 — The data model: terms, triples, formulas, rules (`lib/prelude.js`)
157
-
158
- Eyeling uses a small AST. You can think of it as the “instruction set” for the rest of the reasoner.
159
-
160
- ### 3.1 Terms
161
-
162
- A **Term** is one of:
163
-
164
- - `Iri(value)` — an absolute IRI string
165
- - `Literal(value)` — stored as raw lexical form (e.g. `"hi"@en`, `12`, `"2020-01-01"^^<dt>`)
166
- - `Var(name)` — variable name without the leading `?`
167
- - `Blank(label)` — blank node label like `_:b1`
168
- - `ListTerm(elems)` — a concrete N3 list `(a b c)`
169
- - `OpenListTerm(prefix, tailVar)` — a “list with unknown tail”, used for list unification patterns
170
- - `GraphTerm(triples)` — a quoted formula `{ ... }` as a first-class term
171
-
172
- That last one is special: N3 allows formulas as terms, so Eyeling must treat graphs as matchable data.
173
-
174
- ### 3.2 Triples and rules
175
-
176
- A triple is:
177
-
178
- - `Triple(s, p, o)` where each position is a Term.
179
-
180
- A rule is:
181
-
182
- - `Rule(premiseTriples, conclusionTriples, isForward, isFuse, headBlankLabels)`
183
-
184
- Two details matter later:
185
-
186
- 1. **Inference fuse**: a forward rule whose conclusion is the literal `false` acts as a hard failure. (More in Chapter 10.)
187
- 2. **`headBlankLabels`** records which blank node labels occur _explicitly in the head_ of a rule. Those blanks are treated as existentials and get skolemized per firing. (Chapter 9.)
188
-
189
- ### 3.3 Interning
190
-
191
- Eyeling interns IRIs and Literals by string value. Interning is a quiet performance trick with big consequences:
192
-
193
- - repeated IRIs/Literals become pointer-equal
194
- - indexing is cheaper
195
- - comparisons are faster and allocations drop.
196
-
197
- In addition, interned **Iri**/**Literal** terms (and generated **Blank** terms) get a small, non-enumerable integer id `.__tid` that is stable for the lifetime of the process. This `__tid` is used as the engine’s “fast key”:
198
-
199
- - fact indexes (`__byPred` / `__byPS` / `__byPO`) key by `__tid` values **and store fact _indices_** (predicate buckets are keyed by `predicate.__tid`, and PS/PO buckets are keyed by the subject/object `.__tid`; buckets contain integer indices into the `facts` array)
200
- - duplicate detection uses `"sid pid oid"` where each component is a `__tid`
201
- - unification/equality has an early-out when two terms share the same `__tid`
202
-
203
- For blanks, the id is derived from the blank label (so different blank labels remain different existentials).
204
-
205
- Terms are treated as immutable: once interned/created, the code assumes you will not mutate `.value` (or `.label` for blanks).
206
-
207
- ### 3.4 Prefix environment
208
-
209
- `PrefixEnv` holds prefix mappings and a base IRI. It provides:
210
-
211
- - expansion (`ex:foo` → full IRI)
212
- - shrinking for printing (full IRI → `ex:foo` when possible)
213
- - default prefixes for RDF/RDFS/XSD/log/math/string/list/time/genid.
214
-
215
- ---
216
-
217
- <a id="ch04"></a>
218
-
219
- ## Chapter 4 — From characters to AST: lexing and parsing (`lib/lexer.js`, `lib/parser.js`)
220
-
221
- Eyeling’s parser is intentionally pragmatic: it aims to accept “the stuff people actually write” in N3/Turtle, including common shorthand.
222
-
223
- ### 4.1 Lexing: tokens, not magic
224
-
225
- The lexer turns the input into tokens like:
226
-
227
- - punctuation: `{ } ( ) [ ] , ; .`
228
- - operators: `=>`, `<=`, `=`, `!`, `^`
229
- - directives: `@prefix`, `@base`, and also SPARQL-style `PREFIX`, `BASE`
230
- - variables `?x`
231
- - blanks `_:b1`
232
- - IRIREF `<...>`
233
- - qnames `rdf:type`, `:local`
234
- - literals: strings (short and long), numbers, `true`/`false`, `^^` datatypes, `@en` language tags
235
- - `#` comments
236
-
237
- Parsing becomes dramatically simpler because tokenization already decided where strings end, where numbers are, and so on.
238
-
239
- By default, Eyeling parses ordinary N3. Selected RDF/TriG surface syntax is accepted only when RDF compatibility is explicitly enabled with `eyeling -r file.trig`, `eyeling --rdf file.trig`, or API option `{ rdf: true }`.
240
-
241
- The `-r` flag does **not** switch Eyeling to a different RDF dataset reasoner. It adds a parser/printer compatibility layer around the same N3 engine:
242
-
243
- 1. before normal parsing, RDF/TriG surface syntax is normalized into ordinary Eyeling N3 terms;
244
- 2. the usual N3 parser, rule compiler, forward chainer, backward prover, builtins, and output rendering then run on that normalized program;
245
- 3. when possible, final output is printed back in RDF/TriG-compatible syntax.
246
-
247
- This matters because rules still reason over Eyeling's N3 term model. Named graphs become quoted formulas, RDF 1.2 triple terms become singleton quoted formulas, and RDF Message Logs become an explicit replay graph that ordinary N3 rules can consume.
248
-
249
- In RDF compatibility mode, RDF 1.2 triple terms written as `<<( s p o )>>`, plus the reified triple form `<<s p o ~ r>>`, are normalized to Eyeling's existing singleton quoted-formula term `{ s p o }`. A reifier `r` is preserved as `r rdf:reifies { s p o }`. A leading `VERSION "1.2"` or `@version "1.2"` directive is ignored for the same reason. On output, `--rdf` converts a singleton graph term back to `<<( ... )>>` only when its inner triple is valid as an RDF triple term; otherwise it stays in N3 graph-term form. It also prints `log:nameOf` graph-term triples back as TriG named graph blocks. For example:
250
-
251
- ```n3
252
- :observation rdf:reifies <<( :sensor :reports :overheating )>> .
253
- ```
254
-
255
- is treated internally like:
256
-
257
- ```n3
258
- :observation rdf:reifies { :sensor :reports :overheating } .
259
- ```
260
-
261
- RDF/TriG named graph blocks are normalized to ordinary N3 graph terms:
262
-
263
- ```trig
264
- :factoryDataset {
265
- :observation rdf:reifies <<( :sensor :reports :overheating )>> .
266
- }
267
- ```
268
-
269
- is treated internally like:
270
-
271
- ```n3
272
- :factoryDataset log:nameOf {
273
- :observation rdf:reifies { :sensor :reports :overheating } .
274
- } .
275
- ```
276
-
277
- A top-level default graph block is unwrapped into ordinary top-level triples. A named graph block is not merged into the default graph; it remains a quoted formula reachable through `log:nameOf`.
278
-
279
- #### RDF Message Log replay under `-r`
280
-
281
- If RDF compatibility mode sees a top-level message version directive such as:
282
-
283
- ```trig
284
- VERSION "1.2-messages"
285
- ```
286
-
287
- or the corresponding `@version` spelling, Eyeling treats the input as an RDF Message Log before ordinary N3 parsing starts. The accepted message-version strings are `"1.1-messages"`, `"1.2-messages"`, and `"1.2-basic-messages"`.
288
-
289
- At top-level statement boundaries, `MESSAGE` and `@message` delimiters split the document into message chunks. The text before the first delimiter is the first message. An empty chunk is still a message: it is replayed as an empty heartbeat rather than being dropped. Directives and comments alone do not make a message non-empty.
290
-
291
- Each chunk is then normalized separately using the same RDF/TriG compatibility rules. Eyeling also scopes blank-node labels per message, so reusing `_:x` in two different messages does not accidentally identify the same blank node across message boundaries.
292
-
293
- The result of replay is not a hidden side channel. Eyeling materializes ordinary facts using the `eymsg:` vocabulary:
294
-
295
- ```n3
296
- @prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#>.
297
-
298
- ?stream a eymsg:RDFMessageStream;
299
- eymsg:messageCount ?count;
300
- eymsg:orderedEnvelopes ?envelopes;
301
- eymsg:firstEnvelope ?first;
302
- eymsg:lastEnvelope ?last;
303
- eymsg:envelope ?envelope.
304
-
305
- ?envelope a eymsg:MessageEnvelope;
306
- eymsg:offset ?n;
307
- eymsg:payloadKind eymsg:nonEmpty;
308
- eymsg:payloadGraph ?payload;
309
- eymsg:nextEnvelope ?next.
310
- ```
311
-
312
- For an empty message, the envelope has `eymsg:payloadKind eymsg:empty` and no `eymsg:payloadGraph`. For a non-empty message, the payload is exposed as a named graph:
313
-
314
- ```n3
315
- ?payload log:nameOf {
316
- ...the normalized message body...
317
- } .
318
- ```
319
-
320
- That design is deliberate: message payload triples are **not silently merged** into the global fact store. Rules inspect a payload explicitly, usually with `log:nameOf` plus `log:includes`:
321
-
322
- ```n3
323
- {
324
- ?envelope eymsg:payloadGraph ?payload.
325
- ?payload log:nameOf ?payloadGraph.
326
- ?payloadGraph log:includes { :doorA :state :closed }.
327
- } => {
328
- ?envelope :reportsClosed :doorA.
329
- }.
330
- ```
331
-
332
- This is the important mental model for `-r` with RDF Messages: the parser preserves message order and message boundaries as data. Reasoning can then implement stream-like behavior—flow stages, sliding windows, heartbeats, conflict repair, replay audits—without the TriG sidecar having to invent envelope facts by hand.
333
-
334
- This keeps Eyeling's N3 model stable while allowing small RDF 1.1/RDF 1.2 dataset-shaped and message-log-shaped inputs to run through the existing `GraphTerm` machinery when the caller opts in. More exotic future RDF forms should be added only if they can be mapped cleanly onto Eyeling's quoted-formula term model.
335
-
336
-
337
- #### Streaming large RDF Message Logs with `--stream-messages`
338
-
339
- The ordinary `-r` replay described above is intentionally a faithful whole-log replay: it materializes one stream resource, the complete ordered-envelope list, all envelope links, and all payload graphs before reasoning. That is useful for examples, audits, proofs, and rules that need to see the whole log at once, but it is not the right execution model for very large append-only logs.
340
-
341
- For large files where each RDF Message can be processed independently, use:
342
-
343
- ```sh
344
- eyeling -r --stream-messages rules.n3 large-message-log.trig
345
- ```
346
-
347
- `--stream-messages` keeps the rule files loaded once, then reads each RDF Message chunk from the log and runs the rules against a one-message replay view:
348
-
349
- ```n3
350
- ?stream a eymsg:RDFMessageStream;
351
- eymsg:envelope ?envelope;
352
- eymsg:firstEnvelope ?envelope;
353
- eymsg:lastEnvelope ?envelope;
354
- eymsg:orderedEnvelopes (?envelope).
355
-
356
- ?envelope a eymsg:MessageEnvelope;
357
- eymsg:offset ?n;
358
- eymsg:payloadKind eymsg:nonEmpty;
359
- eymsg:payloadGraph ?payload.
360
- ```
361
-
362
- The payload is still scoped behind `?payload log:nameOf { ... }`, so rules use the same `log:nameOf`/`log:includes` pattern as in whole-log replay. The important difference is lifetime: after one message has been parsed, reasoned over, and printed, its facts are discarded before the next message is read. Local files are scanned incrementally instead of first being normalized into one giant N3 document. Remote HTTP(S) message logs are likewise detected from a small prefix and then spooled for streaming, rather than buffered as one large response.
363
-
364
- The browser playground exposes the same idea through the RDF and stream-message toggles. In stream-message mode, the editor is for rules and background knowledge, while a separate **RDF Message Log URL** field points at the log to be fetched and parsed incrementally. That keeps the playground close to the command-line form `eyeling -r --stream-messages rules.n3 messages.trig`: the rules stay small and editable, and the message log can be a large remote resource. For tiny demos, a pasted message log after the rules is still accepted as a fallback. The detector accepts both `VERSION "1.2-messages"` / `MESSAGE` and `@version "1.1-messages" .` / `@message .` forms.
365
-
366
- This mode is meant for production-style message feeds such as MARC-record streams, telemetry streams, or LDES member logs where a consumer checkpoint already tells the application which messages are new. It deliberately does not expose `eymsg:nextEnvelope` links or a complete `eymsg:messageCount`, because those require holding global stream state. Use ordinary `eyeling -r` when your rules need global ordering, sliding windows across several messages, or proof output for the entire replay.
367
-
368
- ### 4.2 Parsing triples, with Turtle-style convenience
369
-
370
- The parser supports:
371
-
372
- - predicate/object lists with `;` and `,`
373
- - blank node property lists `[ :p :o; :q :r ]`
374
- - collections `( ... )` as `ListTerm`
375
- - quoted formulas `{ ... }` as `GraphTerm`
376
- - variables, blanks, literals, qnames, IRIREFs
377
- - keyword-ish sugar like `is ... of` and inverse arrows
378
- - path operators `!` and `^` that may generate helper triples via fresh blanks
379
-
380
- A useful detail: the parser maintains a `pendingTriples` list used when certain syntactic forms expand into helper triples (for example, some path/property-list expansions). It ensures the “surface statement” still emits all required triples even if the subject itself was syntactic sugar.
381
-
382
- ### 4.3 Parsing rules: `=>`, `<=`, and log idioms
383
-
384
- At the top level, the parser recognizes:
385
-
386
- - `{ P } => { C } .` as a forward rule
387
- - `{ H } <= { B } .` as a backward rule
388
-
389
- It also normalizes top-level triples of the form:
390
-
391
- - `{ P } log:implies { C } .`
392
- - `{ H } log:impliedBy { B } .`
393
-
394
- into the same internal Rule objects. That means you can write rules either as operators (`=>`, `<=`) or as explicit `log:` predicates.
395
-
396
- ### 4.4 `true` and `false` as rule endpoints
397
-
398
- Eyeling treats two literals specially in rule positions:
399
-
400
- - `true` stands for the empty formula `{}` (an empty premise or head).
401
- - `false` is used for inference fuses (`{ ... } => false.`).
402
-
403
- So these are valid patterns:
404
-
405
- ```n3
406
- true => { :Program :loaded true }.
407
- { ?x :p :q } => false.
408
- ```
409
-
410
- Internally:
411
-
412
- - `true` becomes “empty triple list”
413
- - `false` becomes “no head triples” _plus_ the `isFuse` flag if forward.
414
-
415
- ---
416
-
417
- <a id="ch05"></a>
418
-
419
- ## Chapter 5 — Rule normalization: “compile-time” semantics (`lib/rules.js`)
420
-
421
- Before rules hit the engine, Eyeling performs one lightweight transformation. A second “make it work” trick—deferring built-ins that cannot run yet—happens later inside the goal prover.
422
-
423
- ### 5.1 Lifting blank nodes in rule bodies into variables
424
-
425
- In N3 practice, blanks in _rule premises_ behave like universally-quantified placeholders. Eyeling implements this by converting `Blank(label)` to `Var(_bN)` in the premise only.
426
-
427
- So a premise like:
428
-
429
- ```n3
430
- { _:x :p ?y. } => { ... }.
431
- ```
432
-
433
- acts like:
434
-
435
- ```n3
436
- { ?_b1 :p ?y. } => { ... }.
437
- ```
438
-
439
- This avoids the “existential in the body” trap and matches how most rule authors expect N3 to behave.
440
-
441
- Blanks in the **conclusion** are _not_ lifted — they remain blanks and later become existentials (Chapter 9).
442
-
443
- ### 5.1.1 Quoted formulas in rule bodies: direct pattern positions vs nested data positions
444
-
445
- There is one important refinement to the “lift blanks in rule bodies” rule when a rule body mentions a quoted formula (`GraphTerm`).
446
-
447
- Eyeling now distinguishes **direct quoted-formula positions** from **nested quoted-formula data**.
448
-
449
- #### Direct quoted-formula positions in a premise triple
450
-
451
- When a quoted formula appears **directly** as the subject, predicate, or object term of a premise triple, Eyeling treats blank nodes inside that quoted formula as **rule-body placeholders** and lifts them to rule variables.
452
-
453
- Example:
454
-
455
- ```n3
456
- { :A :B :C } a :Statement.
457
-
458
- {
459
- { _:X :B :C } a :Statement.
460
- } => {
461
- :result :is true.
462
- }.
463
- ```
464
-
465
- This matches and derives `:result :is true.` because the direct quoted formula `{ _:X :B :C }` is being used as a **pattern-bearing term** in the premise triple.
466
-
467
- This behavior is mainly for interoperability with engines that treat blank nodes in such direct quoted-formula premise positions as pattern placeholders.
468
-
469
- #### Nested quoted formulas remain data
470
-
471
- If the quoted formula is nested **inside another term** in the rule body — for example inside a list used by `log:conjunction` — Eyeling preserves the quoted formula’s own blank-node scope.
472
-
473
- So this rule body:
474
-
475
- ```n3
476
- {
477
- ( { ?S a :Subject } { [] a :Thing } ) log:conjunction ?Z.
478
- } => { ... }.
479
- ```
480
-
481
- must keep the inner `[]` as a **formula-local blank node**. Eyeling treats it as belonging to the quoted graph, not as a rule-body variable that escapes into the surrounding rule.
482
-
483
- That distinction matters because quoted formulas still play **two different roles** in Eyeling:
484
-
485
- 1. **Formula as data** — for example when constructing a formula with `log:conjunction` or storing `{ ... }` inside another data term. In this role, local blanks stay blanks. They print as blank nodes and participate in alpha-equivalence only within that quoted formula.
486
- 2. **Formula as a query pattern** — either through query-like builtins such as `log:includes`, `log:notIncludes`, `log:collectAllIn`, or `log:forAllIn`, or through a **direct quoted-formula premise position** as described above. In that role, the formula’s local blanks may be treated existentially while matching.
487
-
488
- The practical rule is:
489
-
490
- > **Eyeling lifts blanks inside quoted formulas only when the quoted formula appears directly in an ordinary premise triple position.**
491
- >
492
- > For `log:includes` and `log:notIncludes`, quoted formula operands keep their own blank-node scope. The builtin may treat blanks in the goal formula existentially while proving it, but blanks in an explicit scope graph remain formula-local blanks and may be returned as blank nodes rather than synthetic variables such as `?_b1`.
493
-
494
- This keeps `log:conjunction` and formula printing honest, while still allowing direct quoted-formula premise patterns such as `{ _:X :B :C } a :Statement.` to match interoperably.
495
-
496
- ### 5.2 Builtin deferral in forward-rule bodies
497
-
498
- In a depth-first proof, the order of goals matters. Many built-ins only become informative once parts of the triple are **already instantiated** (for example comparisons, pattern tests, and other built-ins that do not normally create bindings).
499
-
500
- If such a builtin runs while its subject/object still contain variables or blanks, it may return **no solutions** (because it cannot decide yet) or only the **empty delta** (`{}`), even though it would succeed (or fail) once other goals have bound the needed values.
501
-
502
- Eyeling supports a runtime deferral mechanism inside `proveGoals(...)`, enabled only when proving the bodies of forward rules.
503
-
504
- What happens when `proveGoals(..., { deferBuiltins: true })` sees a builtin goal:
505
-
506
- - Eyeling evaluates the builtin once.
507
- - If the builtin yields **no deltas**, or only **empty deltas** (`[{}]`), and:
508
- - there are still other goals remaining, and
509
- - the builtin goal still contains variables/blanks, and
510
- - the goal list hasn’t already been rotated too many times,
511
- - then Eyeling **rotates that builtin goal to the end** of the current goal list and continues with the next goal first.
512
-
513
- A small counter (`deferCount`) caps how many rotations can happen (at most the length of the current goal list), so the prover cannot loop forever by endlessly “trying later”.
514
-
515
- There is one extra guard for a small whitelist of built-ins that are considered satisfiable even when both subject and object are completely unbound (see `__builtinIsSatisfiableWhenFullyUnbound`). For these, if evaluation yields no deltas and there is nothing left to bind (either it is the last goal, or deferral has already been exhausted), Eyeling treats the builtin as a vacuous success (`[{}]`) so it does not block the proof.
516
-
517
- This is intentionally enabled for **forward-chaining rule bodies only**. Backward rules keep their normal left-to-right goal order, which can be important for termination on some programs.
518
-
519
- ### 5.3 Materializing anonymous RDF collections into N3 list terms
520
-
521
- Many N3 documents encode lists using RDF’s linked-list vocabulary:
522
-
523
- ```n3
524
- _:c rdf:first :a.
525
- _:c rdf:rest _:d.
526
- _:d rdf:first :b.
527
- _:d rdf:rest rdf:nil.
528
- ```
529
-
530
- Eyeling supports _both_ representations:
531
-
532
- - **Concrete N3 lists** like `(:a :b)` are parsed as `ListTerm([...])` directly.
533
- - **RDF collections** using `rdf:first`/`rdf:rest` can be traversed by list-aware builtins.
534
-
535
- To make list handling simpler and faster, Eyeling runs a small pre-pass called `materializeRdfLists(...)` (implemented in `lib/builtins.js` and invoked by the CLI/entry code). It:
536
-
537
- - scans the **input triples** for well‑formed `rdf:first`/`rdf:rest` chains,
538
- - **rewrites only anonymous (blank-node) list nodes** into concrete `ListTerm(...)`,
539
- - and applies that rewrite consistently across the input triple set and all rule premises/heads.
540
-
541
- Why only blank nodes? Named list nodes (IRIs) must keep their identity, because some programs treat them as addressable resources; Eyeling leaves those as `rdf:first`/`rdf:rest` graphs so list builtins can still walk them when needed.
542
-
543
- ---
544
-
545
- <a id="ch06"></a>
546
-
547
- ## Chapter 6 — Equality, alpha-equivalence, and unification (`lib/engine.js`)
548
-
549
- Once you enter `engine.js`, you enter the “physics layer.” Everything else depends on the correctness of:
550
-
551
- - equality and normalization (especially for literals)
552
- - alpha-equivalence for formulas
553
- - unification and substitution application
554
-
555
- ### 6.1 Two equalities: structural vs alpha-equivalent
556
-
557
- Eyeling has ordinary structural equality (term-by-term) for most terms.
558
-
559
- But **quoted formulas** (`GraphTerm`) demand something stronger. Two formulas should match even if their internal blank/variable names differ, as long as the structure is the same.
560
-
561
- That is alpha-equivalence:
562
-
563
- - `{ _:x :p ?y. }` should match `{ _:z :p ?w. }`
564
-
565
- Eyeling implements alpha-equivalence by checking whether there exists a consistent renaming mapping between the two formulas’ variables/blanks that makes the triples match.
566
-
567
- Important scope nuance: only blanks/variables that are local to the quoted formula participate in alpha-renaming. If a formula is being matched after an outer substitution has already instantiated part of it, those substituted terms are treated as fixed. In other words, alpha-equivalence may rename formula-local placeholders, but it must not rename names that came from the enclosing match. This prevents a substituted outer blank node from being confused with a local blank node inside the quoted formula.
568
-
569
- So `{ _:x :p :o }` obtained by substituting `?A = _:x` into `{ ?A :p :o }` must not alpha-match `{ _:b :p :o }` by renaming `_:x` to `_:b`.
570
-
571
- A related operational detail matters for rule execution: alpha-equivalence is only a **binding-free shortcut** when both quoted formulas are variable-free after substitution. If unbound variables still remain inside the formulas, Eyeling must fall back to structural quoted-formula unification so shared outer rule variables can actually bind. Otherwise a premise such as `?A :has { ?S ?P ?O }` could appear to match while leaving `?S ?P ?O` unbound for later goals.
572
-
573
- ### 6.2 Groundness: “variables inside formulas do not leak”
574
-
575
- Eyeling makes a deliberate choice about _groundness_:
576
-
577
- - a triple is “ground” if it has no free variables in normal positions
578
- - **variables inside a `GraphTerm` do not make the surrounding triple non-ground**
579
-
580
- This is encoded in functions like `isGroundTermInGraph`. It is what makes it possible to assert and store triples that _mention formulas with variables_ as data.
581
-
582
- ### 6.3 Substitutions: chaining and application
583
-
584
- A substitution is a plain JS object:
585
-
586
- ```js
587
- { X: Term, Y: Term, ... }
588
- ```
589
-
590
- When applying substitutions, Eyeling follows **chains**:
591
-
592
- - if `X → Var(Y)` and `Y → Iri(...)`, applying to `X` yields the IRI.
593
-
594
- Chains arise naturally during unification (e.g. when variables unify with other variables) and during rule firing.
595
-
596
- At the API boundary, a substitution is still just a plain object, and unification still produces _delta_ objects (small `{ varName: Term }` maps).
597
- But inside the hot backward-chaining loop (`proveGoals`), Eyeling uses a Prolog-style **trail** to avoid cloning substitutions at every step:
598
-
599
- - keep one **mutable** substitution object during DFS
600
- - when a candidate match yields a delta, **apply the bindings in place**
601
- - record newly-bound variable names on a **trail stack**
602
- - on backtracking, **undo** only the bindings pushed since a saved “mark”
603
-
604
- This keeps the search semantics identical, but removes the “copy a growing object per step” cost that dominates deep/branchy proofs. Returned solutions are emitted as compact plain objects, so callers never observe mutation.
605
-
606
- Implementation details (and why they matter):
607
-
608
- - **`applySubstTerm` is the only “chain chaser”.** It follows `Var → Term` links until it reaches a stable term.
609
- - Unification’s occurs-check prevents most cycles, but `applySubstTerm` still defends against accidental cyclic chains.
610
- - The cycle guard is written to avoid allocating a `Set` in the common case (short chains).
611
- - **Structural sharing is deliberate.** Applying a substitution often changes nothing:
612
- - `applySubstTerm` returns the original term when it is unaffected.
613
- - list/open-list/graph terms are only rebuilt if at least one component changes (lazy copy-on-change).
614
- - `applySubstTriple` returns the original `Triple` when `s/p/o` are unchanged.
615
-
616
- These “no-op returns” are one of the biggest practical performance wins in the engine: backward chaining and forward rule instantiation apply substitutions constantly, so avoiding allocations reduces GC pressure without changing semantics.
617
-
618
- ### 6.4 Unification: the core operation
619
-
620
- Unification is implemented in `unifyTerm` / `unifyTriple`, with support for:
621
-
622
- - variable binding with occurs check
623
- - list unification (elementwise)
624
- - open-list unification (prefix + tail variable)
625
- - formula unification via graph unification:
626
- - fast path: identical triple list
627
- - otherwise: backtracking order-insensitive matching while threading the substitution
628
-
629
- There are two key traits of Eyeling’s graph unification:
630
-
631
- 1. It is _set-like_: order does not matter.
632
- 2. It is _substitution-threaded_: choices made while matching one triple restrict the remaining matches, just like Prolog.
633
-
634
- ### 6.5 Literals: lexical vs semantic equality
635
-
636
- Eyeling keeps literal values as raw strings, but it parses and normalizes where needed:
637
-
638
- - `literalParts(lit)` splits lexical form and datatype IRI
639
- - it recognizes RDF JSON datatype (`rdf:JSON` / `<...rdf#JSON>`)
640
- - it includes caches for numeric parsing, integer parsing (`BigInt`), and numeric metadata.
641
-
642
- This lets built-ins and fast-key indexing treat some different lexical spellings as the same value (for example, normalizing `"abc"` and `"abc"^^xsd:string` in the fast-key path).
643
-
644
- ---
645
-
646
- <a id="ch07"></a>
647
-
648
- ## Chapter 7 — Facts as a database: indexing and fast duplicate checks
649
-
650
- Reasoning is mostly “join-like” operations: match a goal triple against known facts. Doing this naively is too slow, so Eyeling builds indexes on top of a plain array.
651
-
652
- ### 7.1 The fact store
653
-
654
- Facts live in an array `facts: Triple[]`.
655
-
656
- Eyeling attaches hidden (non-enumerable) index fields:
657
-
658
- - `facts.__byPred: Map<predicateId, number[]>` where each entry is an index into `facts` (and `predicateId` is `predicate.__tid`)
659
- - `facts.__byPS: Map<predicateId, Map<lookupKey, number[]>>` where each entry is an index into `facts`
660
- - `facts.__byPO: Map<predicateId, Map<lookupKey, number[]>>` where each entry is an index into `facts`
661
- - `facts.__byPNonFastS` / `facts.__byPNonFastO` for the small fallback set of IRI-predicate facts whose subject/object cannot be fast-keyed (for example variables or quoted formulas)
662
- - `facts.__varPred*` for top-level facts whose predicate is a variable; these are the only non-IRI-predicate facts that can match a ground IRI predicate goal
663
- - `facts.__keySet: Set<string>` for a fast-path `"sid pid oid"` key (all three are `__tid` values)
664
-
665
- `termFastKey(term)` returns a `termId` (`term.__tid`) for **Iri**, **Literal**, **Blank**, and strict-ground list terms, and `null` for quoted graphs, open lists, and variables. Proving-time lookup uses `termLookupKey(term)`, which is aligned with `unifyTerm`: it keeps exact ids for IRIs/blanks/strings, but canonicalizes value-equivalent booleans and numerics such as `true` / `"1"^^xsd:boolean` and `1.0` / `1.00`.
666
-
667
- The duplicate-check “fast key” only exists when `termFastKey` succeeds for all three terms; the proof indexes use the broader `termLookupKey`.
668
-
669
- ### 7.2 Candidate selection: pick the smallest bucket
670
-
671
- When proving a goal with IRI predicate, Eyeling computes candidate facts by:
672
-
673
- 1. restricting to the IRI predicate bucket
674
- 2. if subject or object has a lookup key, using the matching `(p,s)` or `(p,o)` bucket plus only the non-fast fallback facts for that same position
675
- 3. choosing the smaller of the subject-constrained and object-constrained candidate sets when both exist
676
-
677
- Variable-predicate facts are kept in a separate tiny fallback index. Blank/list/formula predicates are not scanned for an IRI predicate goal because they cannot unify with an IRI predicate.
678
-
679
- This is a cheap selectivity heuristic. In type-heavy RDF, `(p,o)` is often extremely selective (e.g., `rdf:type` + a class IRI), so the PO index can be a major speed win.
680
-
681
- The same selectivity idea is also reused by the single-premise forward-rule agenda in `forwardChain`: safe one-premise rules are pre-indexed by predicate / `(p,s)` / `(p,o)` patterns so a newly added fact only checks the small subset of rules that could match it.
682
-
683
- ### 7.3 Duplicate detection with fast keys
684
-
685
- When adding derived facts, Eyeling uses a fast-path duplicate check when possible:
686
-
687
- - If all three terms have a duplicate-check fast key, it checks membership in `facts.__keySet` using the `"sid pid oid"` key.
688
- - Otherwise (lists, quoted graphs, variables), it falls back to structural triple equality.
689
-
690
- This still treats blanks correctly: blanks are _not_ interchangeable; the blank **label** (and thus its `__tid`) is part of the key.
691
-
692
- ---
693
-
694
- <a id="ch08"></a>
695
-
696
- ## Chapter 8 — Backward chaining: the proof engine (`proveGoals`)
697
-
698
- Eyeling’s backward prover is an iterative depth-first search (DFS) that looks a lot like Prolog’s SLD resolution, but written explicitly with a stack to avoid JS recursion limits.
699
-
700
- ### 8.1 Proof states
701
-
702
- A proof state contains:
703
-
704
- - `goals`: remaining goal triples
705
- - `subst`: current substitution
706
- - `depth`: current depth (used for compaction heuristics)
707
- - `visited`: previously-seen goals (loop prevention)
708
-
709
- ### 8.2 The proving loop
710
-
711
- At each step:
712
-
713
- 1. If no goals remain: emit the current substitution as a solution.
714
- 2. Otherwise:
715
- - take the first goal
716
- - apply the current substitution to it
717
- - attempt to satisfy it in three ways:
718
- 1. built-ins
719
- 2. backward rules
720
- 3. facts
721
-
722
- Eyeling’s order is intentional: built-ins often bind variables cheaply; backward rules expand the search tree (and enable recursion); facts are tried last as cheap terminal matches.
723
-
724
- ### 8.3 Built-ins: return _deltas_, not full substitutions
725
-
726
- A built-in is evaluated by the engine via the builtin library in `lib/builtins.js`:
727
-
728
- ```js
729
- deltas = evalBuiltin(goal0, {}, facts, backRules, ...)
730
- for delta in deltas:
731
- mark = trail.length
732
- if applyDeltaToSubst(delta):
733
- dfs(restGoals)
734
- undoTo(mark)
735
- ```
736
-
737
- **Implementation note (performance):** in the core DFS, Eyeling applies builtin (and unification) deltas into a single mutable substitution and uses a **trail** to undo bindings on backtracking. This preserves the meaning of “threading substitutions through a proof”, but avoids allocating and copying full substitution objects on every branch. Empty deltas (`{}`) are genuinely cheap: they do not touch the trail and only incur the control-flow overhead of exploring a branch.
738
-
739
- **Implementation note (performance):** as of this version, Eyeling also avoids allocating short-lived substitution objects when matching goals against **facts** and when unifying a **backward-rule head** with the current goal. Instead of calling the pure `unifyTriple(..., subst)` (which clones the substitution on each variable bind), the prover performs an **in-place unification** directly into the mutable `substMut` store and records only the newly-bound variable names on the trail. This typically reduces GC pressure significantly on reachability / path-search workloads, where unification is executed extremely frequently.
740
-
741
- So built-ins behave like relations that can generate zero, one, or many possible bindings. A list generator might yield many deltas; a numeric test yields zero or one.
742
-
743
- #### 8.3.1 Builtin deferral and “vacuous” solutions
744
-
745
- Conjunction in N3 is order-insensitive, but many builtins are only useful once some variables are bound by _other_ goals in the same body. When `proveGoals` is called from forward chaining, Eyeling enables **builtin deferral**: if a builtin goal cannot make progress yet, it is rotated to the end of the goal list and retried later (with a small cycle guard to avoid infinite rotation).
746
-
747
- “Cannot make progress” includes both cases:
748
-
749
- - the builtin returns **no solutions** (`[]`), and
750
- - the builtin returns only **vacuous solutions** (`[{}]`, i.e., success with _no new bindings_) while the goal still contains unbound vars/blanks.
751
-
752
- That second case matters for “satisfiable but non-enumerating” builtins (e.g., some `log:` helpers) where early vacuous success would otherwise prevent later goals from ever binding the variables the builtin needs.
753
-
754
- ### 8.4 Loop prevention: visited multiset with backtracking
755
-
756
- Eyeling avoids obvious infinite recursion by recording each (substituted) goal it is currently trying in a per-branch _visited_ structure. If the same goal is encountered again on the same proof branch, Eyeling skips it.
757
-
758
- Implementation notes:
759
-
760
- - The visited structure is a `Map` from _goal key_ to a reference count, plus a trail array. This makes it cheap to check (`O(1)` average) and cheap to roll back on backtracking (just like the substitution trail).
761
- - Keys are _structural_. Atoms use stable IDs; lists use element keys; variables use their identity (so two different variables are **not** conflated). This keeps the cycle check conservative and avoids accidental pruning.
762
- - This is not full tabling: it does not memoize answers, it only guards against immediate cycles (the common “A depends on A” loops).
763
-
764
- ### 8.4.1 Minimal completed-goal tabling
765
-
766
- Eyeling has a **very small, deliberately conservative answer table** for backward goals.
767
-
768
- What is cached:
769
-
770
- - only **completed** answer sets
771
- - keyed by the **fully substituted goal list**
772
- - only when the proof is entered from a “top-level” call shape (no active per-branch `visited` context)
773
- - only when the engine is not in a result-limiting mode such as `maxResults`
774
-
775
- What is **not** cached:
776
-
777
- - pending / in-progress goals
778
- - recursive dependency states
779
- - partial answer streams
780
- - branch-local states inside an active recursive proof
781
-
782
- This matters because exposing **pending** answers without dependency propagation would change the meaning of recursive programs. Eyeling therefore caches only results that are already complete and replays them only when the surrounding proof context is equivalent.
783
-
784
- The cache is invalidated whenever any of the following changes:
785
-
786
- - the number of known facts
787
- - the number of backward rules
788
- - the scoped-closure level
789
- - whether a frozen scoped snapshot is active
790
-
791
- So this is **not SLG tabling** and not a general recursion engine. It is best understood as a reuse optimization for repeated backward proofs in a stable proof environment.
792
-
793
- Typical win cases:
794
-
795
- - many repeated `log:query` directives with the **same premise**
796
- - repeated forward-rule body proofs that ask the same completed backward question
797
- - “query-like” workloads where the expensive part is a repeated backward proof and the fact store does not change between calls
798
-
799
- Typical non-win cases:
800
-
801
- - first-time proofs
802
- - recursive subgoals whose value depends on future answers
803
- - workloads where the fact set changes between almost every call
804
-
805
- ### 8.5 Backward rules: indexed by head predicate
806
-
807
- Backward rules are indexed in `backRules.__byHeadPred`. When proving a goal with IRI predicate `p`, Eyeling retrieves:
808
-
809
- - `rules whose head predicate is p`
810
- - plus `__wildHeadPred` for rules whose head predicate is not an IRI (rare, but supported)
811
-
812
- For each candidate rule:
813
-
814
- 1. standardize it apart (fresh variables)
815
- 2. unify the rule head with the goal
816
- 3. append the rule body goals in front of the remaining goals
817
-
818
- That “standardize apart” step is essential. Without it, reusing a rule multiple times would accidentally share variables across invocations, producing incorrect bindings.
819
-
820
- **Implementation note (performance):** `standardizeRule` is called for every backward-rule candidate during proof search.
821
- To reduce allocation pressure, Eyeling reuses a single fresh `Var(...)` object per _original_ variable name within one standardization pass (all occurrences of `?x` in the rule become the same fresh `?x__N` object). This is semantics-preserving — it still “separates” invocations — but it avoids creating many duplicate Var objects when a variable appears repeatedly in a rule body.
822
-
823
- ### 8.6 Substitution size on deep proofs
824
-
825
- The trail-based substitution store removes the biggest accidental quadratic cost (copying a growing substitution object at every step).
826
- In deep and branchy searches, the substitution trail still grows, and long variable-to-variable chains increase the work done by `applySubstTerm`.
827
-
828
- Eyeling currently keeps the full trail as-is during search. When emitting a solution, it runs a lightweight compaction pass (via `gcCollectVarsInGoals(...)` / `gcCompactForGoals(...)`) so only bindings reachable from the answer variables and remaining goals are kept. It still does not perform general substitution composition/normalization during search.
829
-
830
- ---
831
-
832
- <a id="ch09"></a>
833
-
834
- ## Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules (`forwardChain`)
835
-
836
- Forward chaining is Eyeling’s outer control loop. It is where facts get added and the closure grows.
837
-
838
- ### 9.1 The shape of saturation
839
-
840
- Eyeling loops until no new facts are added. Inside that loop, it scans every forward rule and tries to fire it.
841
-
842
- A simplified view:
843
-
844
- ```text
845
- repeat
846
- changed = false
847
- for each forward rule r:
848
- sols = proveGoals(r.premise, facts, backRules)
849
- for each solution s:
850
- for each head triple h in r.conclusion:
851
- inst = applySubst(h, s)
852
- inst = skolemizeHeadBlanks(inst)
853
- if inst is ground and new:
854
- add inst to facts
855
- changed = true
856
- until not changed
857
- ```
858
-
859
- Top-level input triples are kept as parsed (including non-ground triples such as ?X :p :o.). Groundness is enforced when adding derived facts during forward chaining, and when selecting printed/query output triples.
860
-
861
- There is also a narrow fast path for some **single-premise** forward rules. When a rule has exactly one non-builtin premise and that premise cannot also be satisfied through backward rules, `forwardChain` can index the rule by that premise shape and fire it directly from newly added facts. This does **not** replace the general saturation loop; it is only an agenda-style shortcut for the safe one-premise case.
862
-
863
- ### 9.2 Strict-ground head optimization
864
-
865
- There is a nice micro-compiler optimization in `runFixpoint()`:
866
-
867
- If a rule’s head is _strictly ground_ (no vars, no blanks, no open lists, even inside formulas), and it contains no head blanks, then the head does not depend on _which_ body solution you choose.
868
-
869
- In that case:
870
-
871
- - Eyeling only needs **one** proof of the body.
872
- - And if all head triples are already known, it can skip proving the body entirely.
873
-
874
- This is a surprisingly effective optimization for “axiom-like” rules with constant heads.
875
-
876
- ### 9.3 Existentials: skolemizing head blanks
877
-
878
- Blank nodes in the **rule head** represent existentials: “there exists something such that…”
879
-
880
- Eyeling handles this by replacing head blank labels with fresh blank labels of the form:
881
-
882
- - `_:sk_0`, `_:sk_1`, …
883
-
884
- But it does something subtle and important: it caches skolemization per (rule firing, head blank label), so that the _same_ firing instance does not keep generating new blanks across outer iterations.
885
-
886
- The “firing instance” is keyed by a deterministic string derived from the instantiated body (“firingKey”). This stabilizes the closure and prevents “existential churn.”
887
-
888
- **Implementation note (performance):** the firing-instance key is computed in a hot loop, so `firingKey(...)` builds a compact string via concatenation rather than `JSON.stringify`. If you change what counts as a distinct “firing instance”, update the key format and the skolem cache together.
889
-
890
- Implementation: deterministic Skolem IDs live in `lib/skolem.js`; the per-firing cache and head-blank rewriting are implemented in `lib/engine.js`.
891
-
892
- ### 9.4 Inference fuses: `{ ... } => false`
893
-
894
- A rule whose conclusion is `false` is treated as a hard failure. During forward chaining:
895
-
896
- - Eyeling proves the premise (it only needs one solution)
897
- - if the premise is provable, it prints a message and exits with status code 65 (`EX_DATAERR` in Unix `sysexits.h` terminology)
898
-
899
- This is Eyeling’s way to express hard consistency checks and detect inconsistencies.
900
-
901
- ### 9.5 Rule-producing rules (meta-rules)
902
-
903
- Eyeling treats certain derived triples as _new rules_:
904
-
905
- - `log:implies` and `log:impliedBy` where subject/object are formulas
906
- - it also accepts the literal `true` as an empty formula `{}` on either side
907
-
908
- So these are “rule triples”:
909
-
910
- ```n3
911
- { ... } log:implies { ... }.
912
- true log:implies { ... }.
913
- { ... } log:impliedBy true.
914
- ```
915
-
916
- When such a triple is derived in a forward rule head:
917
-
918
- 1. Eyeling adds it as a fact (so you can inspect it), and
919
- 2. it _promotes_ it into a live rule by constructing a new `Rule` object and inserting it into the forward or backward rule list.
920
-
921
- This is meta-programming: your rules can generate new rules during reasoning.
922
-
923
- **Implementation note (performance):** rule triples are often derived repeatedly (especially inside loops).
924
- To keep promotion cheap, Eyeling maintains a `Set` of canonical rule keys for both the forward-rule list and the backward-rule list. Promotion checks membership in O(1) time instead of scanning the rule arrays and doing structural comparisons each time.
925
-
926
- ---
927
-
928
- <a id="ch10"></a>
929
-
930
- ## Chapter 10 — Scoped closure, priorities, and `log:conclusion`
931
-
932
- Some `log:` built-ins talk about “what is included in the closure” or “collect all solutions.” These are tricky in a forward-chaining engine because the closure is _evolving_.
933
-
934
- Eyeling addresses this with a disciplined two-phase strategy and an optional priority mechanism.
935
-
936
- ### 10.1 The two-phase outer loop (Phase A / Phase B)
937
-
938
- Forward chaining runs inside an _outer loop_ that alternates:
939
-
940
- - **Phase A**: scoped built-ins are disabled (they “delay” by failing)
941
-
942
- - Eyeling saturates normally to a fixpoint
943
-
944
- - then Eyeling freezes a snapshot of the saturated facts
945
-
946
- - **Phase B**: scoped built-ins are enabled, but they query only the frozen snapshot
947
-
948
- - Eyeling runs saturation again (new facts can appear due to scoped queries)
949
-
950
- This produces deterministic behavior for scoped operations: they observe a stable snapshot, not a moving target.
951
-
952
- **Implementation note (performance):** the two-phase scheme is only needed when the program actually uses scoped built-ins. If no rule contains `log:collectAllIn`, `log:forAllIn`, `log:includes`, or `log:notIncludes`, Eyeling **skips Phase B entirely** and runs only a single saturation. This avoids re-running the forward fixpoint and can prevent a “query-like” forward rule (one whose body contains an expensive backward proof search) from being executed twice.
953
-
954
- **Implementation note (performance):** in Phase A there is no snapshot, so scoped built-ins (and priority-gated scoped queries) are guaranteed to “delay” by failing.
955
- Instead of proving the entire forward-rule body only to fail at the end, Eyeling precomputes whether a forward rule depends on scoped built-ins and skips it until a snapshot exists and the requested closure level is reached. This can avoid very expensive proof searches in programs that combine recursion with `log:*In` built-ins.
956
-
957
- ### 10.2 Priority-gated closure levels
958
-
959
- Eyeling introduces a `scopedClosureLevel` counter:
960
-
961
- - level 0 means “no snapshot available” (Phase A)
962
- - level 1, 2, … correspond to snapshots produced after each Phase A saturation
963
-
964
- Some built-ins interpret a positive integer literal as a requested priority:
965
-
966
- - `log:collectAllIn` and `log:forAllIn` use the **object position** for priority
967
- - `log:includes` and `log:notIncludes` use the **subject position** for priority
968
-
969
- If a rule requests priority `N`, Eyeling delays that builtin until `scopedClosureLevel >= N`.
970
-
971
- In practice this allows rule authors to write “do not run this scoped query until the closure is stable enough” and is what lets Eyeling iterate safely when rule-producing rules introduce new needs.
972
-
973
- ### 10.3 `log:conclusion`: local deductive closure of a formula
974
-
975
- `log:conclusion` is handled in a particularly elegant way:
976
-
977
- - given a formula `{ ... }` (a `GraphTerm`),
978
- - Eyeling computes the deductive closure _inside that formula_:
979
- - extract rule triples inside it (`log:implies`, `log:impliedBy`)
980
- - run `forwardChain` locally over those triples
981
-
982
- - cache the result in a `WeakMap` so the same formula does not get recomputed
983
-
984
- Notably, `log:impliedBy` inside the formula is treated as forward implication too for closure computation (and also indexed as backward to help proving).
985
-
986
- This makes formulas a little world you can reason about as data.
987
-
988
- ---
989
-
990
- <a id="ch11"></a>
991
-
992
- ## Chapter 11 — Built-ins as a standard library (`lib/builtins.js`)
993
-
994
- Built-ins are where Eyeling stops being “just a Datalog engine” and becomes a practical N3 tool.
995
-
996
- Implementation note: builtin code lives in `lib/builtins.js` and is wired into the prover by the engine via `makeBuiltins(deps)` (dependency injection keeps the modules loosely coupled).
997
-
998
- ### 11.1 How Eyeling recognizes built-ins
999
-
1000
- A predicate is treated as builtin if:
1001
-
1002
- - it is an IRI in one of the builtin namespaces:
1003
- - `crypto:`, `math:`, `log:`, `string:`, `time:`, `list:`
1004
-
1005
- - or it is `rdf:first` / `rdf:rest` (treated as list-like builtins)
1006
- - unless **super restricted mode** is enabled, in which case only `log:implies` and `log:impliedBy` are treated as builtins.
1007
-
1008
- Super restricted mode exists to let you treat all other predicates as ordinary facts/rules without any built-in evaluation.
1009
-
1010
- **Note on `log:query`:** Eyeling also recognizes a special _top-level_ directive of the form `{...} log:query {...}.` to **select which results to print**. This is **not** a builtin predicate (it is not evaluated as part of goal solving); it is handled by the parser/CLI/output layer. See §11.3.5 below and Chapter 13 for details.
1011
-
1012
- ### 11.2 Built-ins return multiple solutions
1013
-
1014
- Every builtin returns a list of substitution _deltas_.
1015
-
1016
- That means built-ins can be:
1017
-
1018
- - **functional** (return one delta binding an output)
1019
- - **tests** (return either `[{}]` for success or `[]` for failure)
1020
- - **generators** (return many deltas)
1021
-
1022
- List operations are a common source of generators; numeric comparisons are tests.
1023
-
1024
- Below is a drop-in replacement for **§11.3 “A tour of builtin families”** that aims to be _fully self-contained_ and to cover **every builtin currently implemented in `lib/builtins.js`** (including the `rdf:first` / `rdf:rest` aliases).
1025
-
1026
- ---
1027
-
1028
- ## 11.3 A tour of builtin families
1029
-
1030
- Eyeling’s builtins are best thought of as _foreign predicates_: they look like ordinary N3 predicates in your rules, but when the engine tries to satisfy a goal whose predicate is a builtin, it does not search the fact store. Instead, it calls a piece of JavaScript that implements the predicate’s semantics.
1031
-
1032
- That one sentence explains a lot of “why does it behave like _that_?”:
1033
-
1034
- - Builtins are evaluated **during backward proof** (goal solving), just like facts and backward rules.
1035
- - A builtin may produce **zero solutions** (fail), **one solution** (deterministic succeed), or **many solutions** (a generator).
1036
- - Most builtins behave like relations, not like functions: they can sometimes run “backwards” (bind the subject from the object) if the implementation supports it.
1037
-
1038
- ### 11.3.0 Reading builtin “signatures” in this handbook
1039
-
1040
- The N3 Builtins tradition often describes builtins using “schema” annotations like:
1041
-
1042
- - `$s+` / `$o+` — input must be bound (or at least not a variable in practice)
1043
- - `$s-` / `$o-` — output position (often a variable that will be bound)
1044
- - `$s?` / `$o?` — may be unbound
1045
- - `$s.i` — list element _i_ inside the subject list
1046
-
1047
- Eyeling is a little more pragmatic: it implements the spirit of these schemas, but it also has several “engineering” conventions that appear across many builtins:
1048
-
1049
- 1. **Variables (`?X`) may be bound** by a builtin if the builtin is written to do so.
1050
- 2. **Blank nodes (`[]` / `_:`)** are frequently treated as “do not care” placeholders. Many builtins accept a blank node in an output position and simply succeed without binding.
1051
- 3. **Fully unbound relations are usually not enumerated.** If both sides are unbound and enumerating solutions would be infinite (or huge), a number of builtins treat that situation as “satisfiable” and succeed once without binding anything. (This is mainly to keep meta-tests and some N3 conformance cases happy.)
1052
-
1053
- With that, we can tour the builtin families as Eyeling actually implements them.
1054
-
1055
- ---
1056
-
1057
- ## 11.3.1 `crypto:` — digest functions (Node-only)
1058
-
1059
- These builtins hash a string and return a lowercase hex digest as a plain string literal.
1060
-
1061
- ### `crypto:sha`, `crypto:md5`, `crypto:sha256`, `crypto:sha512`
1062
-
1063
- **Shape:** `$literal crypto:sha256 $digest`
1064
-
1065
- **Semantics (Eyeling):**
1066
-
1067
- - The **subject must be a literal**. Eyeling takes the literal’s lexical form (stripping quotes) as UTF-8 input.
1068
- - The **object** is unified with a **plain string literal** containing the hex digest.
1069
-
1070
- **Important runtime note:** Eyeling uses Node’s `crypto` module. If `crypto` is not available (e.g., in some browser builds), these builtins simply **fail** (return no solutions).
1071
-
1072
- **Example:**
1073
-
1074
- ```n3
1075
- "hello" crypto:sha256 ?d.
1076
- # ?d becomes "2cf24dba5...<snip>...9824"
1077
- ```
1078
-
1079
- ---
1080
-
1081
- ## 11.3.2 `math:` — numeric and numeric-like relations
1082
-
1083
- Eyeling’s `math:` builtins fall into three broad categories:
1084
-
1085
- 1. **Comparisons**: test-style predicates (`>`, `<`, `=`, …).
1086
- 2. **Arithmetic on numbers**: sums, products, division, rounding, etc.
1087
- 3. **Unary analytic functions**: trig/hyperbolic functions and a few helpers.
1088
-
1089
- A key design choice: Eyeling parses numeric terms fairly strictly, but comparisons accept a wider “numeric-like” domain including durations and date/time values in some cases.
1090
-
1091
- ### 11.3.2.1 Numeric comparisons
1092
-
1093
- These builtins succeed or fail; they do not introduce new bindings.
1094
-
1095
- - `math:greaterThan` (>)
1096
- - `math:lessThan` (<)
1097
- - `math:notGreaterThan` (≤)
1098
- - `math:notLessThan` (≥)
1099
- - `math:equalTo` (=)
1100
- - `math:notEqualTo` (≠)
1101
-
1102
- **Shapes:**
1103
-
1104
- ```n3
1105
- $a math:greaterThan $b.
1106
- $a math:equalTo $b.
1107
- ```
1108
-
1109
- Eyeling also accepts an older cwm-ish variant where the **subject is a 2-element list**:
1110
-
1111
- ```n3
1112
- ( $a $b ) math:greaterThan true. # (supported as a convenience)
1113
- ```
1114
-
1115
- **Accepted term types (Eyeling):**
1116
-
1117
- - Proper XSD numeric literals (`xsd:integer`, `xsd:decimal`, `xsd:float`, `xsd:double`, and integer-derived types).
1118
- - Untyped numeric tokens (`123`, `-4.5`, `1.2e3`) when they look numeric.
1119
- - `xsd:duration` literals (treated as seconds via a simplified model).
1120
- - `xsd:date` and `xsd:dateTime` literals (converted to epoch seconds for comparison).
1121
-
1122
- **Edge cases:**
1123
-
1124
- - `NaN` is treated as **not equal to anything**, including itself, for `math:equalTo`.
1125
- - Comparisons involving non-parsable values simply fail.
1126
-
1127
- These are pure tests. In forward rules, if a test builtin is encountered before its inputs are bound and it fails, Eyeling may **defer** it and try other goals first; once variables become bound, the test is retried.
1128
-
1129
- ---
1130
-
1131
- ### 11.3.2.2 Arithmetic on lists of numbers
1132
-
1133
- These are “function-like” relations where the subject is usually a list and the object is the result.
1134
-
1135
- #### `math:sum`
1136
-
1137
- **Shape:** `( $x1 $x2 ... ) math:sum $total`
1138
-
1139
- - Subject must be a list of numeric terms (the list may be empty or a singleton).
1140
- - Empty list sums to **0**.
1141
- - Computes the numeric sum.
1142
- - Chooses an output datatype based on the “widest” numeric datatype seen among inputs and (optionally) the object position; integers stay integers unless the result is non-integer.
1143
-
1144
- Eyeling also supports a small, EYE-style convenience for timestamp arithmetic:
1145
-
1146
- - **DateTime plus duration/seconds**: `(dateTime durationOrSeconds) math:sum dateTime`
1147
- - `xsd:duration` is interpreted as seconds (same model as `math:difference`).
1148
- - Output is a normalized `xsd:dateTime` in UTC lexical form (`...Z`).
1149
-
1150
- #### `math:product`
1151
-
1152
- **Shape:** `( $x1 $x2 ... ) math:product $total`
1153
-
1154
- - Subject must be a list of numeric terms (the list may be empty or a singleton).
1155
- - Empty list product is **1**.
1156
- - Same datatype conventions as `math:sum`, but multiplies.
1157
-
1158
- #### `math:difference`
1159
-
1160
- This one is more interesting because Eyeling supports a couple of mixed “numeric-like” cases.
1161
-
1162
- **Shape:** `( $a $b ) math:difference $c`
1163
-
1164
- Eyeling supports:
1165
-
1166
- 1. **Numeric subtraction**: `c = a - b`.
1167
- 2. **DateTime difference**: `(dateTime1 dateTime2) math:difference duration`
1168
- - Produces an **`xsd:duration`** in a seconds-only lexical form such as `"PT900S"^^xsd:duration`.
1169
- - This avoids ambiguity around month/year day-length and still plays well with `math:lessThan`, `math:greaterThan`, etc. because Eyeling's numeric comparison builtins treat `xsd:duration` as seconds.
1170
-
1171
- 3. **DateTime minus duration**: `(dateTime durationOrSeconds) math:difference dateTime`
1172
- - Subtracts a duration from a dateTime and yields a new dateTime.
1173
-
1174
- If the types do not fit any supported case, the builtin fails.
1175
-
1176
- #### `math:quotient`
1177
-
1178
- **Shape:** `( $a $b ) math:quotient $q`
1179
-
1180
- - Parses both inputs as numbers.
1181
- - Requires finite values and `b != 0`.
1182
- - Computes `a / b`, picking a suitable numeric datatype for output.
1183
-
1184
- #### `math:integerQuotient`
1185
-
1186
- **Shape:** `( $a $b ) math:integerQuotient $q`
1187
-
1188
- - Intended for integer division with remainder discarded (truncation toward zero).
1189
- - Prefers exact arithmetic using **BigInt** if both inputs are integer literals.
1190
- - Falls back to Number parsing if needed, but still requires integer-like values.
1191
-
1192
- #### `math:remainder`
1193
-
1194
- **Shape:** `( $a $b ) math:remainder $r`
1195
-
1196
- - Integer-only modulus.
1197
- - Uses BigInt when possible; otherwise requires both numbers to still represent integers.
1198
- - Fails on division by zero.
1199
-
1200
- #### `math:rounded`
1201
-
1202
- **Shape:** `$x math:rounded $n`
1203
-
1204
- - Rounds to nearest integer.
1205
- - Tie-breaking follows JavaScript `Math.round`, i.e. halves go toward **+∞** (`-1.5 -> -1`, `1.5 -> 2`).
1206
- - Eyeling emits the integer as an **integer token literal** (and also accepts typed numerics if they compare equal).
1207
-
1208
- ---
1209
-
1210
- ### 11.3.2.3 Exponentiation and unary numeric relations
1211
-
1212
- #### `math:exponentiation`
1213
-
1214
- **Shape:** `( $base $exp ) math:exponentiation $result`
1215
-
1216
- - Forward direction supports two modes:
1217
- - **Exact integer mode (BigInt):** if `$base` and `$exp` are integer literals and `$exp >= 0`, Eyeling computes the exact integer power using BigInt (with a safety cap on the estimated result size to avoid OOM).
1218
- - **Numeric mode (Number):** otherwise, if base and exponent parse as finite Numbers, computes `base ** exp`.
1219
- - Reverse direction (limited): Eyeling can sometimes solve for the exponent if:
1220
- - base and result are numeric, finite, and **positive**
1221
- - base is not 1
1222
- - exponent is unbound In that case it uses logarithms: `exp = log(result) / log(base)`.
1223
-
1224
- This is a pragmatic inversion, not a full algebra system.
1225
-
1226
- The **BigInt exact-integer mode** exists specifically to avoid rule-level “repeat multiply” derivations that can explode memory for large exponents (e.g., the Ackermann example).
1227
-
1228
- #### Unary “math relations” (often invertible)
1229
-
1230
- Eyeling implements these as a shared pattern: if the subject is numeric, compute object; else if the object is numeric, compute subject via an inverse function; if both sides are unbound, succeed once (do not enumerate).
1231
-
1232
- - `math:absoluteValue`
1233
- - `math:negation`
1234
- - `math:degrees` (and implicitly its inverse “radians” conversion)
1235
- - `math:sin`, `math:cos`, `math:tan`
1236
- - `math:asin`, `math:acos`, `math:atan`
1237
- - `math:sinh`, `math:cosh`, `math:tanh` (only if JS provides the functions)
1238
-
1239
- **Example:**
1240
-
1241
- ```n3
1242
- "0"^^xsd:double math:cos ?c. # forward
1243
- ?x math:cos "1"^^xsd:double. # reverse (principal acos)
1244
- ```
1245
-
1246
- Inversion uses principal values (e.g., `asin`, `acos`, `atan`) and does not attempt to enumerate periodic families of solutions.
1247
-
1248
- ---
1249
-
1250
- ## 11.3.3 `time:` — dateTime inspection and “now”
1251
-
1252
- Eyeling’s time builtins work over `xsd:dateTime` lexical forms. They are deliberately simple: they extract components from the lexical form rather than implementing a full time zone database.
1253
-
1254
- Implementation: these helpers live in `lib/time.js` and are called from `lib/engine.js`’s builtin evaluator.
1255
-
1256
- ### Component extractors
1257
-
1258
- - `time:year`
1259
- - `time:month`
1260
- - `time:day`
1261
- - `time:hour`
1262
- - `time:minute`
1263
- - `time:second`
1264
-
1265
- **Shape:** `$dt time:month $m`
1266
-
1267
- **Semantics:**
1268
-
1269
- - Subject must be an `xsd:dateTime` literal in a format Eyeling can parse.
1270
- - Object becomes the corresponding integer component (as an integer token literal).
1271
- - If the object is already a numeric literal, Eyeling accepts it if it matches.
1272
-
1273
- ### `time:timeZone`
1274
-
1275
- **Shape:** `$dt time:timeZone $tz`
1276
-
1277
- Returns the trailing zone designator:
1278
-
1279
- - `"Z"` for UTC, or
1280
- - a string like `"+02:00"` / `"-05:00"`
1281
-
1282
- It yields a **plain string literal** (and also accepts typed `xsd:string` literals).
1283
-
1284
- ### `time:localTime`
1285
-
1286
- **Shape:** `"" time:localTime ?now`
1287
-
1288
- Binds `?now` to the current local time as an `xsd:dateTime` literal.
1289
-
1290
- Two subtle but important engineering choices:
1291
-
1292
- 1. Eyeling memoizes “now” per reasoning run so that repeated uses in one run do not drift.
1293
- 2. Eyeling supports a fixed “now” override (used for deterministic tests).
1294
-
1295
- ---
1296
-
1297
- ## 11.3.4 `list:` — list structure, iteration, and higher-order helpers
1298
-
1299
- Eyeling has a real internal list term (`ListTerm`) that corresponds to N3’s `(a b c)` surface syntax.
1300
-
1301
- ### RDF collections (`rdf:first` / `rdf:rest`) are materialized
1302
-
1303
- N3 and RDF can also express lists as linked blank nodes using `rdf:first` / `rdf:rest` and `rdf:nil`. Eyeling _materializes_ such structures into internal list terms before reasoning so that `list:*` builtins can operate uniformly.
1304
-
1305
- For convenience and compatibility, Eyeling treats:
1306
-
1307
- - `rdf:first` as an alias of `list:first`
1308
- - `rdf:rest` as an alias of `list:rest`
1309
-
1310
- ### Core list destructuring
1311
-
1312
- #### `list:first` (and `rdf:first`)
1313
-
1314
- **Shape:** `(a b c) list:first a`
1315
-
1316
- - Succeeds iff the subject is a **non-empty closed list**.
1317
- - Unifies the object with the first element.
1318
-
1319
- #### `list:rest` (and `rdf:rest`)
1320
-
1321
- **Shape:** `(a b c) list:rest (b c)`
1322
-
1323
- Eyeling supports both:
1324
-
1325
- - closed lists `(a b c)`, and
1326
- - _open lists_ of the form `(a b ... ?T)` internally.
1327
-
1328
- For open lists, “rest” preserves openness:
1329
-
1330
- - Rest of `(a ... ?T)` is `?T`
1331
- - Rest of `(a b ... ?T)` is `(b ... ?T)`
1332
-
1333
- #### `list:firstRest`
1334
-
1335
- This is a very useful “paired” view of a list.
1336
-
1337
- **Forward shape:** `(a b c) list:firstRest (a (b c))`
1338
-
1339
- **Backward shapes (construction):**
1340
-
1341
- - If the object is `(first restList)`, it can construct the list.
1342
- - If `rest` is a variable, Eyeling constructs an open list term.
1343
-
1344
- This is the closest thing to Prolog’s `[H|T]` in Eyeling.
1345
-
1346
- **Implementation note (performance):** `list:firstRest` is a hot builtin in many recursive list-building programs (including path finding). Eyeling constructs the new prefix using pre-sized arrays and simple loops (instead of spread syntax) to reduce transient allocations.
1347
-
1348
- ---
1349
-
1350
- ### Membership and iteration (multi-solution builtins)
1351
-
1352
- These builtins can yield multiple solutions.
1353
-
1354
- #### `list:member`
1355
-
1356
- **Shape:** `(a b c) list:member ?x`
1357
-
1358
- Generates one solution per element, unifying the object with each member.
1359
-
1360
- #### `list:in`
1361
-
1362
- **Shape:** `?x list:in (a b c)`
1363
-
1364
- Same idea, but the list is in the **object** position and the **subject** is unified with each element.
1365
-
1366
- #### `list:iterate`
1367
-
1368
- **Shape:** `(a b c) list:iterate ?pair`
1369
-
1370
- Generates `(index value)` pairs with **0-based indices**:
1371
-
1372
- - `(0 a)`, `(1 b)`, `(2 c)`, …
1373
-
1374
- A nice ergonomic detail: the object may be a pattern such as:
1375
-
1376
- ```n3
1377
- (a b c) list:iterate ( ?i "b" ).
1378
- ```
1379
-
1380
- In that case Eyeling unifies `?i` with `1` and checks the value part appropriately.
1381
-
1382
- #### `list:memberAt`
1383
-
1384
- **Shape:** `( (a b c) 1 ) list:memberAt b`
1385
-
1386
- The subject must be a 2-element list: `(listTerm indexTerm)`.
1387
-
1388
- Eyeling can use this relationally:
1389
-
1390
- - If the index is bound, it can return the value.
1391
- - If the value is bound, it can search for indices that match.
1392
- - If both are variables, it generates pairs (similar to `iterate`, but with separate index/value logic).
1393
-
1394
- Indices are **0-based**.
1395
-
1396
- ---
1397
-
1398
- ### Transformations and queries
1399
-
1400
- #### `list:length`
1401
-
1402
- **Shape:** `(a b c) list:length 3`
1403
-
1404
- Returns the length as an integer token literal.
1405
-
1406
- A small but intentional strictness: if the object is already ground, Eyeling does not accept “integer vs decimal equivalences” here; it wants the exact integer notion.
1407
-
1408
- #### `list:last`
1409
-
1410
- **Shape:** `(a b c) list:last c`
1411
-
1412
- Returns the last element of a non-empty list.
1413
-
1414
- #### `list:reverse`
1415
-
1416
- Reversible in the sense that either side may be the list:
1417
-
1418
- - If subject is a list, object becomes its reversal.
1419
- - If object is a list, subject becomes its reversal.
1420
-
1421
- It does not enumerate arbitrary reversals; it is a deterministic transform once one side is known.
1422
-
1423
- #### `list:remove`
1424
-
1425
- **Shape:** `( (a b a c) a ) list:remove (b c)`
1426
-
1427
- Removes all occurrences of an item from a list.
1428
-
1429
- Important requirement: the item to remove must be **ground** (fully known) before the builtin will run.
1430
-
1431
- #### `list:notMember` (test)
1432
-
1433
- **Shape:** `(a b c) list:notMember x`
1434
-
1435
- Succeeds iff the object cannot be unified with any element of the subject list. As a test, it typically works best once its inputs are bound; in forward rules Eyeling may defer it if it is reached before bindings are available.
1436
-
1437
- #### `list:append`
1438
-
1439
- This is list concatenation, but Eyeling implements it in a usefully relational way.
1440
-
1441
- **Forward shape:** `( (a b) (c) (d e) ) list:append (a b c d e)`
1442
-
1443
- Subject is a list of lists; object is their concatenation.
1444
-
1445
- **Splitting (reverse-ish) mode:** If the **object is a concrete list**, Eyeling tries all ways of splitting it into the given number of parts and unifying each part with the corresponding subject element. This can yield multiple solutions and is handy for logic programming patterns.
1446
-
1447
- #### `list:sort`
1448
-
1449
- Sorts a list into a deterministic order.
1450
-
1451
- - Requires the input list’s elements to be **ground**.
1452
- - Orders literals numerically when both sides look numeric; otherwise compares their lexical strings.
1453
- - Orders lists lexicographically by elements.
1454
- - Orders IRIs by IRI string.
1455
- - Falls back to a stable structural key for mixed cases.
1456
-
1457
- Like `reverse`, this is “reversible” only in the sense that if one side is a list, the other side can be unified with its sorted form.
1458
-
1459
- #### `list:map` (higher-order)
1460
-
1461
- This is one of Eyeling’s most powerful list builtins because it calls back into the reasoner.
1462
-
1463
- **Shape:** `( (x1 x2 x3) ex:pred ) list:map ?outList`
1464
-
1465
- Semantics:
1466
-
1467
- 1. The subject is a 2-element list: `(inputList predicateIri)`.
1468
- 2. `inputList` must be ground.
1469
- 3. For each element `el` in the input list, Eyeling proves the goal:
1470
-
1471
- ```n3
1472
- el predicateIri ?y.
1473
- ```
1474
-
1475
- using _the full engine_ (facts, backward rules, and builtins).
1476
-
1477
- 4. All resulting `?y` values are collected in proof order and concatenated into the output list.
1478
- 5. If an element produces no solutions, it contributes nothing.
1479
-
1480
- This makes `list:map` a compact “query over a list” operator.
1481
-
1482
- ---
1483
-
1484
- ## 11.3.5 `log:` — unification, formulas, scoping, and meta-level control
1485
-
1486
- The `log:` family is where N3 stops being “RDF with rules” and becomes a _meta-logic_. Eyeling supports the core operators you need to treat formulas as terms, reason inside quoted graphs, and compute closures.
1487
-
1488
- ### Equality and inequality
1489
-
1490
- #### `log:equalTo`
1491
-
1492
- **Shape:** `$x log:equalTo $y`
1493
-
1494
- This is simply **term unification**: it succeeds if the two terms can be unified and returns any bindings that result.
1495
-
1496
- #### `log:notEqualTo` (test)
1497
-
1498
- Succeeds iff the terms **cannot** be unified. No new bindings.
1499
-
1500
- ### Working with formulas as terms
1501
-
1502
- In Eyeling, a quoted formula `{ ... }` is represented as a `GraphTerm` whose content is a list of triples (and, when parsed from documents, rule terms can also appear as `log:implies` / `log:impliedBy` triples inside formulas).
1503
-
1504
- #### `log:conjunction`
1505
-
1506
- **Shape:** `( F1 F2 ... ) log:conjunction F`
1507
-
1508
- - Subject is a list of formulas.
1509
- - Object becomes a formula containing all triples from all inputs.
1510
- - Duplicate triples are removed.
1511
- - The literal `true` is treated as the **empty formula** and is ignored in the merge.
1512
-
1513
- #### `log:conclusion`
1514
-
1515
- **Shape:** `F log:conclusion C`
1516
-
1517
- Computes the _deductive closure_ of the formula `F` **using only the information inside `F`**:
1518
-
1519
- - Eyeling starts with all triples inside `F` as facts.
1520
- - It treats `{A} => {B}` (represented internally as a `log:implies` triple between formulas) as a forward rule.
1521
- - It treats `{A} <= {B}` as the corresponding forward direction for closure purposes.
1522
- - Then it forward-chains to a fixpoint _within that local fact set_.
1523
- - The result is returned as a formula containing all derived triples.
1524
-
1525
- Eyeling caches `log:conclusion` results per formula object, so repeated calls with the same formula term are cheap.
1526
-
1527
- ### Dereferencing and parsing (I/O flavored)
1528
-
1529
- These builtins reach outside the current fact set. They are synchronous by design.
1530
-
1531
- #### `log:content`
1532
-
1533
- **Shape:** `<doc> log:content ?txt`
1534
-
1535
- - Dereferences the IRI (fragment stripped) and returns the raw bytes as an `xsd:string` literal.
1536
- - In Node: HTTP(S) is fetched synchronously; non-HTTP is treated as a local file path (including `file://`).
1537
- - In browsers/workers: uses synchronous XHR (subject to CORS).
1538
-
1539
- #### `log:semantics`
1540
-
1541
- **Shape:** `<doc> log:semantics ?formula`
1542
-
1543
- Dereferences and parses the remote/local resource as N3/Turtle-like syntax, returning a formula.
1544
-
1545
- A useful detail: top-level rules in the parsed document are represented _as data_ inside the returned formula using `log:implies` / `log:impliedBy` triples between formula terms. This means you can treat “a document plus its rules” as a single first-class formula object.
1546
-
1547
- #### `log:semanticsOrError`
1548
-
1549
- Like `log:semantics`, but on failure it returns a string literal such as:
1550
-
1551
- - `error(dereference_failed,...)`
1552
- - `error(parse_error,...)`
1553
-
1554
- This is convenient in robust pipelines where you want logic that can react to failures.
1555
-
1556
- #### `log:parsedAsN3`
1557
-
1558
- **Shape:** `" ...n3 text... " log:parsedAsN3 ?formula`
1559
-
1560
- Parses an in-memory string as N3 and returns the corresponding formula.
1561
-
1562
- ### Type inspection
1563
-
1564
- #### `log:rawType`
1565
-
1566
- Returns one of four IRIs:
1567
-
1568
- - `log:Formula` (quoted graph)
1569
- - `log:Literal`
1570
- - `rdf:List` (closed or open list terms)
1571
- - `log:Other` (IRIs, blank nodes, etc.)
1572
-
1573
- ### Literal constructors
1574
-
1575
- These two are classic N3 “bridge” operators between structured data and concrete RDF literal forms.
1576
-
1577
- #### `log:dtlit`
1578
-
1579
- Relates a datatype literal to a pair `(lex datatypeIri)`.
1580
-
1581
- - If object is a literal, it can produce the subject list `(stringLiteral datatypeIri)`.
1582
- - If subject is such a list, it can produce the corresponding datatype literal.
1583
- - If both subject and object are variables, Eyeling treats this as satisfiable and succeeds once.
1584
-
1585
- Language-tagged strings are normalized: they are treated as having datatype `rdf:langString`.
1586
-
1587
- #### `log:langlit`
1588
-
1589
- Relates a language-tagged literal to a pair `(lex langTag)`.
1590
-
1591
- - If object is `"hello"@en`, subject can become `("hello" "en")`.
1592
- - If subject is `("hello" "en")`, object can become `"hello"@en`.
1593
- - Fully unbound succeeds once.
1594
-
1595
- ### Rules as data: introspection
1596
-
1597
- #### `log:implies` and `log:impliedBy`
1598
-
1599
- As _syntax_, Eyeling parses `{A} => {B}` and `{A} <= {B}` into internal forward/backward rules.
1600
-
1601
- As _builtins_, `log:implies` and `log:impliedBy` let you **inspect the currently loaded rule set**:
1602
-
1603
- - `log:implies` enumerates forward rules as `(premiseFormula, conclusionFormula)` pairs.
1604
- - `log:impliedBy` enumerates backward rules similarly.
1605
-
1606
- Each enumerated rule is standardized apart (fresh variable names) before unification so you can safely query over it.
1607
-
1608
- ### Top-level directive: `log:query` (output selection)
1609
-
1610
- **Shape (top level only):**
1611
-
1612
- ```n3
1613
- { ...premise... } log:query { ...conclusion... }.
1614
- ```
1615
-
1616
- `log:query` is best understood as an **output projection**, not as a rule and not as a normal builtin:
1617
-
1618
- - Eyeling still computes the saturated forward closure (facts + rules, including backward-rule proofs where needed).
1619
- - It then proves the **premise formula** as a goal (as if it were fed to `log:includes` in the global scope).
1620
- - For every solution, it instantiates the **conclusion formula** and collects the resulting triples.
1621
- - The final output is the **set of unique ground triples** from those instantiated conclusions.
1622
-
1623
- This is “forward-rule-like” in spirit (premise ⇒ conclusion), but the instantiated conclusion triples are **not added back into the fact store**; they are just what Eyeling prints.
1624
-
1625
- **Implementation note (performance):** repeated top-level `log:query` directives with the **same premise formula** are a good fit for Eyeling’s minimal completed-goal tabling (§8.4.1). The first query still performs the full backward proof; later identical premises can reuse the completed answer set as long as the saturated closure and scoped-query context are unchanged.
1626
-
1627
- **Important details:**
1628
-
1629
- - Only **top-level** `{...} log:query {...}.` directives are recognized. Inside quoted formulas (or inside rule bodies/heads) it is just an ordinary triple.
1630
- - Query-mode output depends on the saturated closure, so it cannot be streamed; `--stream` has no effect when any `log:query` directives are present.
1631
- - If you want _logical_ querying inside a rule/proof, use `log:includes` (and optionally `log:conclusion`) instead.
1632
-
1633
- **Example (project a result set):**
1634
-
1635
- ```n3
1636
- @prefix : <urn:ex:>.
1637
- @prefix log: <http://www.w3.org/2000/10/swap/log#>.
1638
-
1639
- { :a :p ?x } => { :a :q ?x }.
1640
- :a :p :b.
1641
-
1642
- { :a :q ?x } log:query { :result :x ?x }.
1643
- ```
1644
-
1645
- Output (only):
1646
-
1647
- ```n3
1648
- :result :x :b .
1649
- ```
1650
-
1651
- ### Scoped proof inside formulas: `log:includes` and friends
1652
-
1653
- #### `log:includes`
1654
-
1655
- **Shape:** `Scope log:includes GoalFormula`
1656
-
1657
- This proves all triples in `GoalFormula` as goals, returning the substitutions that make them provable.
1658
-
1659
- Eyeling has **two modes**:
1660
-
1661
- 1. **Explicit scope graph**: if `Scope` is a formula `{...}`
1662
- - Eyeling reasons _only inside that formula_ (its triples are the fact store).
1663
- - External rules are not used.
1664
- - Blank nodes inside the explicit scope graph are preserved as graph-local blanks; if a goal variable matches one, the binding is a blank node, not a lifted rule variable.
1665
-
1666
- 2. **Priority-gated global scope**: otherwise
1667
- - Eyeling uses a _frozen snapshot_ of the current global closure.
1668
- - The “priority” is read from the subject if it is a positive integer literal `N`.
1669
- - If the closure level is below `N`, the builtin “delays” by failing at that point in the search.
1670
-
1671
- This priority mechanism exists because Eyeling’s forward chaining runs in outer iterations with a “freeze snapshot then evaluate scoped builtins” phase. The goal is to make scoped meta-builtins stable and deterministic: they query a fixed snapshot rather than chasing a fact store that is being mutated mid-iteration.
1672
-
1673
- Also supported:
1674
-
1675
- - The object may be the literal `true`, meaning the empty formula, which is always included (subject to the priority gating above).
1676
-
1677
- **Important blank-node note:** when the goal formula is used as a **pattern**, Eyeling treats blank nodes that are **local to that quoted formula** as existential placeholders during the proof.
1678
-
1679
- So a pattern such as:
1680
-
1681
- ```n3
1682
- { ?x :p [] }
1683
- ```
1684
-
1685
- means “find an `?x` that has some `:p` value”, not “find the specific blank node label printed here”.
1686
-
1687
- But that existential behavior is intentionally limited:
1688
-
1689
- - it applies only to blanks that are **owned by the quoted formula being proved**
1690
- - it does **not** rename or relax terms that were already supplied by an outer substitution
1691
- - it does **not** turn concrete members of already-bound lists or other already-ground structures into fresh variables
1692
-
1693
- That last point is easy to miss. A builtin may receive a formula after part of it has already been instantiated from outer bindings. Those substituted-in terms are fixed data, not fresh existential placeholders. Keeping that boundary sharp prevents accidental overmatching and keeps numeric/list-oriented examples stable.
1694
-
1695
- #### `log:notIncludes` (test)
1696
-
1697
- Negation-as-failure version: it succeeds iff `log:includes` would yield no solutions (under the same scoping rules).
1698
-
1699
- #### `log:collectAllIn`
1700
-
1701
- **Shape:** `( ValueTemplate WhereFormula OutList ) log:collectAllIn Scope`
1702
-
1703
- - Proves `WhereFormula` in the chosen scope.
1704
- - For each solution, applies it to `ValueTemplate` and collects the instantiated terms into a list.
1705
- - Unifies `OutList` with that list.
1706
- - If `OutList` is a blank node, Eyeling just checks satisfiable without binding/collecting.
1707
-
1708
- As with `log:includes`, blank nodes that are local to `WhereFormula` behave as existential query placeholders while that formula is being proved. But blanks that came from already-bound outer data remain fixed.
1709
-
1710
- This is essentially a list-producing “findall”.
1711
-
1712
- #### `log:forAllIn` (test)
1713
-
1714
- **Shape:** `( WhereFormula ThenFormula ) log:forAllIn Scope`
1715
-
1716
- For every solution of `WhereFormula`, `ThenFormula` must be provable under the bindings of that solution. If any witness fails, the builtin fails. No bindings are returned.
1717
-
1718
- As a pure test (no returned bindings), this typically works best once its inputs are bound; in forward rules Eyeling may defer it if it is reached too early.
1719
-
1720
- ### Skolemization and URI casting
1721
-
1722
- #### `log:skolem`
1723
-
1724
- **Shape:** `$groundTerm log:skolem ?iri`
1725
-
1726
- Deterministically maps a _ground_ term to a Skolem IRI in Eyeling’s well-known namespace. This is extremely useful when you want a repeatable identifier derived from structured content.
1727
-
1728
- #### `log:uri`
1729
-
1730
- Bidirectional conversion between IRIs and their string form:
1731
-
1732
- - If subject is an IRI, object can be unified with a string literal of its IRI.
1733
- - If object is a string literal, subject can be unified with the corresponding IRI — **but** Eyeling rejects strings that cannot be safely serialized as `<...>` in Turtle/N3, and it rejects `_:`-style strings to avoid confusing blank nodes with IRIs.
1734
- - Some “fully unbound / do not-care” combinations succeed once to avoid infinite enumeration.
1735
-
1736
- ### Side effects and output directives
1737
-
1738
- #### `log:trace`
1739
-
1740
- Always succeeds once and prints a debug line to stderr:
1741
-
1742
- ```
1743
- <s> TRACE <o>
1744
- ```
1745
-
1746
- using the current prefix environment for pretty printing.
1747
-
1748
- Implementation: this is implemented by `lib/trace.js` and called from `lib/engine.js`.
1749
-
1750
- #### `log:outputString`
1751
-
1752
- As a goal, this builtin simply checks that the terms are sufficiently bound/usable and then succeeds. The actual “printing” behavior is handled by the CLI:
1753
-
1754
- - When the final closure contains any `log:outputString` triples, the CLI collects all of them from the _saturated_ closure and renders those strings instead of the default N3 output.
1755
- - It sorts them deterministically by the subject “key” and concatenates the string values in that order.
1756
-
1757
- This is a pure test/side-effect marker (it should not drive search; it should merely validate that strings exist once other reasoning has produced them). In forward rules Eyeling may defer it if it is reached before the terms are usable.
1758
-
1759
- ---
1760
-
1761
- ## 11.3.6 `string:` — string casting, tests, and regexes
1762
-
1763
- Eyeling implements string builtins with a deliberate interpretation of “domain is `xsd:string`”:
1764
-
1765
- - Any **IRI** can be cast to a string (its IRI text).
1766
- - Any **literal** can be cast to a string:
1767
- - quoted lexical forms decode N3/Turtle escapes,
1768
- - unquoted lexical tokens are taken as-is (numbers, booleans, dateTimes, …).
1769
-
1770
- - Blank nodes, lists, formulas, and variables are not string-castable (and cause the builtin to fail).
1771
-
1772
- ### Construction and concatenation
1773
-
1774
- #### `string:concatenation`
1775
-
1776
- **Shape:** `( s1 s2 ... ) string:concatenation s`
1777
-
1778
- Casts each element to a string and concatenates.
1779
-
1780
- #### `string:format`
1781
-
1782
- **Shape:** `( fmt a1 a2 ... ) string:format out`
1783
-
1784
- A small `printf`/`sprintf` subset:
1785
-
1786
- - Supports `%%`, `%s`, `%d`/`%i`/`%u`, `%f`/`%F`, `%e`/`%E`, `%g`/`%G`, and `%c`.
1787
- - Supports width and precision, plus the `-` and `0` flags.
1788
- - Unsupported flags/specifiers cause the builtin to fail.
1789
- - Missing `%s` arguments are treated as empty strings.
1790
- - The format string `fmt` itself must be string-castable.
1791
- - Each `%s` argument may be any bound non-variable term:
1792
- - string-castable terms (IRIs and literals) use their direct string value;
1793
- - other bound terms (blank nodes, lists, quoted formulas, …) are rendered as N3.
1794
- - Numeric directives require numerically parseable literals.
1795
-
1796
- ### Length and character utilities (Eyeling extensions)
1797
-
1798
- Eyeling also implements a few **non-standard** `string:` helpers that are handy for string-based algorithms. These are **not** part of the SWAP builtin set, so treat them as Eyeling extensions.
1799
-
1800
- #### `string:length`
1801
-
1802
- **Shape:** `s string:length n`
1803
-
1804
- Casts `s` to a string and returns its length as an integer literal token.
1805
-
1806
- #### `string:charAt`
1807
-
1808
- **Shape:** `( s i ) string:charAt ch`
1809
-
1810
- - `i` is a numeric term, truncated to an integer.
1811
- - Indexing is **0-based** (like JavaScript).
1812
- - If `i` is out of range, `ch` is the empty string `""`.
1813
-
1814
- #### `string:setCharAt`
1815
-
1816
- **Shape:** `( s i ch ) string:setCharAt out`
1817
-
1818
- Returns a copy of `s` with the character at index `i` (0-based) replaced by:
1819
-
1820
- - the **first character** of `ch` if `ch` is non-empty, otherwise
1821
- - the empty string.
1822
-
1823
- If `i` is out of range, `out` is the original string.
1824
-
1825
- ### Containment and prefix/suffix tests
1826
-
1827
- - `string:contains`
1828
- - `string:containsIgnoringCase`
1829
- - `string:startsWith`
1830
- - `string:endsWith`
1831
-
1832
- All are pure tests: they succeed or fail.
1833
-
1834
- ### Case-insensitive equality tests
1835
-
1836
- - `string:equalIgnoringCase`
1837
- - `string:notEqualIgnoringCase`
1838
-
1839
- ### Lexicographic comparisons
1840
-
1841
- - `string:greaterThan`
1842
- - `string:lessThan`
1843
- - `string:notGreaterThan` (≤ in Unicode codepoint order)
1844
- - `string:notLessThan` (≥ in Unicode codepoint order)
1845
-
1846
- These compare JavaScript strings directly, i.e., Unicode code unit order (practically “lexicographic” for many uses, but not locale-aware collation).
1847
-
1848
- ### Regex-based tests and extraction
1849
-
1850
- Eyeling compiles patterns using JavaScript `RegExp`, with a small compatibility layer:
1851
-
1852
- - If the pattern uses Unicode property escapes (like `\p{L}`) or code point escapes (`\u{...}`), Eyeling enables the `/u` flag.
1853
- - In Unicode mode, some “identity escapes” that would be SyntaxErrors in JS are sanitized in a conservative way.
1854
-
1855
- #### `string:matches` / `string:notMatches` (tests)
1856
-
1857
- **Shape:** `data string:matches pattern`
1858
-
1859
- Tests whether `pattern` matches `data`.
1860
-
1861
- #### `string:replace`
1862
-
1863
- **Shape:** `( data pattern replacement ) string:replace out`
1864
-
1865
- - Compiles `pattern` as a global regex (`/g`).
1866
- - Uses JavaScript replacement semantics (so `$1`, `$2`, etc. work).
1867
- - Returns the replaced string.
1868
-
1869
- #### `string:scrape`
1870
-
1871
- **Shape:** `( data pattern ) string:scrape out`
1872
-
1873
- Matches the regex once and returns the **first capturing group** (group 1). If there is no match or no group, it fails.
1874
-
1875
- ## 11.4 `log:outputString` as a controlled side effect
1876
-
1877
- From a logic-programming point of view, printing is awkward: if you print _during_ proof search, you risk producing output along branches that later backtrack, or producing the same line multiple times in different derivations. Eyeling avoids that whole class of problems by treating “output” as **data**.
1878
-
1879
- The predicate `log:outputString` is the only officially supported “side-effect channel”, and even it is handled in two phases. If any final `log:outputString` facts exist, Eyeling renders them automatically as the CLI output:
1880
-
1881
- 1. **During reasoning (declarative phase):**
1882
- `log:outputString` behaves like a pure test builtin (implemented in `lib/builtins.js`): it succeeds when its arguments are well-formed and sufficiently bound (notably, when the object is a string literal that can be emitted). Importantly, it does _not_ print anything at this time. If a rule derives a triple like:
1883
-
1884
- ```n3
1885
- :k log:outputString "Hello\n".
1886
- ```
1887
-
1888
- then that triple simply becomes part of the fact base like any other fact.
1889
-
1890
- 2. **After reasoning (rendering phase):** Once saturation finishes, Eyeling scans the _final closure_ for `log:outputString` facts and renders them deterministically (this post-pass lives in `lib/explain.js`). Concretely, the CLI collects all such triples, orders them in a stable way (using the subject as a key so output order is reproducible), and concatenates their string objects into the final emitted text.
1891
-
1892
- This separation is not just an aesthetic choice; it preserves the meaning of logic search:
1893
-
1894
- - Proof search may explore multiple branches and backtrack. Because output is only rendered from the **final** set of facts, backtracking cannot “un-print” anything and cannot cause duplicated prints from transient branches.
1895
- - Output becomes explainable. If you enable proof comments or inspect the closure, `log:outputString` facts can be traced back to the rules that produced them.
1896
- - Output becomes compositional. You can reason about output strings (e.g., sort them, filter them, derive them conditionally) just like any other data.
1897
-
1898
- In short: Eyeling makes `log:outputString` safe by refusing to treat it as an immediate effect. It is a _declarative output fact_ whose concrete rendering is a final, deterministic post-processing step. If any such facts are present in the final closure, Eyeling renders those strings automatically instead of printing the default N3 result set.
1899
-
1900
- ---
1901
-
1902
- <a id="ch12"></a>
1903
-
1904
- ## Chapter 12 — Dereferencing and web-like semantics (`lib/deref.js`)
1905
-
1906
- Some N3 workflows treat IRIs as pointers to more knowledge. Eyeling supports this with:
1907
-
1908
- - `log:content` — fetch raw text
1909
- - `log:semantics` — fetch and parse into a formula
1910
- - `log:semanticsOrError` — produce either a formula or an error literal
1911
-
1912
- `deref.js` is deliberately synchronous so the engine can remain synchronous.
1913
-
1914
- ### 12.1 Two environments: Node vs browser/worker
1915
-
1916
- - In **Node**, dereferencing can read:
1917
- - HTTP(S) via a subprocess that runs `fetch()` (keeps the engine synchronous)
1918
- - local files (including `file://` URIs) via `fs.readFileSync`
1919
- - in practice, any non-http IRI is treated as a local path for convenience.
1920
-
1921
- - In **browser/worker**, dereferencing uses synchronous XHR (HTTP(S) only), subject to CORS.
1922
- - Many browsers restrict synchronous XHR on the main thread; use a worker (as in `playground.html`) to avoid UI blocking.
1923
-
1924
- ### 12.2 Caching
1925
-
1926
- Dereferencing is cached by IRI-without-fragment (fragments are stripped). There are separate caches for:
1927
-
1928
- - raw content text
1929
- - parsed semantics (GraphTerm)
1930
- - semantics-or-error
1931
-
1932
- This is both a performance and a stability feature: repeated `log:semantics` calls in backward proofs will not keep refetching.
1933
-
1934
- ### 12.3 HTTPS enforcement
1935
-
1936
- Eyeling can optionally rewrite `http://…` to `https://…` before dereferencing (CLI `--enforce-https`, or API option). This is a pragmatic “make more things work in modern environments” knob.
1937
-
1938
- ---
1939
-
1940
- <a id="ch13"></a>
1941
-
1942
- ## Chapter 13 — Printing, proofs, and the user-facing output
1943
-
1944
- Once reasoning is done (or as it happens in streaming mode), Eyeling converts derived facts back to N3.
1945
-
1946
- ### 13.1 Printing terms and triples (`lib/printing.js`)
1947
-
1948
- Printing handles:
1949
-
1950
- - compact qnames via `PrefixEnv`
1951
- - `rdf:type` as `a`
1952
- - `owl:sameAs` as `=`
1953
- - nice formatting for lists and formulas
1954
-
1955
- The printer is intentionally simple; it prints what Eyeling can parse.
1956
-
1957
- ### 13.2 Proof comments: local justifications, not full proof trees
1958
-
1959
- When enabled, Eyeling prints a compact comment block per derived triple:
1960
-
1961
- - the derived triple
1962
- - the instantiated rule body that was provable
1963
- - the schematic forward rule that produced it
1964
-
1965
- It is a “why this triple holds” explanation, not a globally exported proof graph.
1966
-
1967
- Implementation note: the engine records lightweight `DerivedFact` objects during forward chaining, and `lib/explain.js` (via `makeExplain(...)`) is responsible for turning those objects into the human-readable proof comment blocks.
1968
-
1969
- ### 13.3 Streaming derived facts
1970
-
1971
- The engine’s `reasonStream` API can accept an `onDerived` callback. Each time a new forward fact is derived, Eyeling can report it immediately.
1972
-
1973
- This is especially useful in interactive demos (and is the basis of the playground streaming tab).
1974
-
1975
- The same API can now also emit RDF/JS output. When `rdfjs: true` is passed, every `onDerived(...)` payload includes both:
1976
-
1977
- - `triple` — Eyeling’s N3 string form
1978
- - `quad` — the same fact as an RDF/JS default-graph quad
1979
-
1980
- If your closure may contain N3-only terms such as quoted formulas (`GraphTerm`), RDF/JS conversion can fail because those terms have no standard RDF/JS representation. In that case, pass `skipUnsupportedRdfJs: true` to keep the full N3 closure while silently omitting any derived triples that cannot be represented as RDF/JS quads. When this flag is enabled, `onDerived(...)` still fires for every derived fact, but `quad` is only present for the representable ones.
1981
-
1982
- For fully stream-oriented RDF/JS consumers there is also `reasonRdfJs(...)`, which exposes the derived facts as an async iterable of RDF/JS quads. The same `skipUnsupportedRdfJs: true` flag applies there as well.
1983
-
1984
- ---
1985
-
1986
- <a id="ch14"></a>
1987
-
1988
- ## Chapter 14 — Entry points: CLI, bundle exports, and npm API
1989
-
1990
- Eyeling exposes itself in three layers.
1991
-
1992
- ### 14.1 Install and first run
1993
-
1994
- Eyeling targets modern JavaScript runtimes. For the npm package and CLI workflow, use **Node.js 18 or newer**.
1995
-
1996
- Install from npm:
1997
-
1998
- ```bash
1999
- npm i eyeling
2000
- ```
2001
-
2002
- Run a self-contained example from stdin:
2003
-
2004
- ```bash
2005
- echo '@prefix : <http://example.org/> .
2006
- :Socrates a :Man .
2007
- { ?x a :Man } => { ?x a :Mortal } .' | npx eyeling
2008
- ```
2009
-
2010
- You can also pass one or more file paths/URLs, or `-` to read explicitly from stdin. When multiple inputs are given, Eyeling parses each source separately, merges the parsed ASTs, and then runs one reasoning pass over the combined facts and rules. This avoids constructing one giant N3 source string. During the merge path, parse-only artifacts such as source text and token arrays are discarded after the AST has been built, except while formatting syntax errors or when an internal caller explicitly asks to keep those artifacts for debugging.
2011
-
2012
- Show the available options:
2013
-
2014
- ```bash
2015
- npx eyeling --help
2016
- ```
2017
-
2018
- A few practical defaults are worth remembering:
2019
-
2020
- - In normal mode, Eyeling prints **newly derived forward facts**.
2021
- - If the input contains top-level `log:query` directives, Eyeling prints the **query-selected conclusion triples** instead.
2022
- - If the final closure contains any `log:outputString` triples, Eyeling renders those strings instead of emitting the default N3 result set.
2023
-
2024
- Custom builtins can be loaded explicitly from the CLI:
2025
-
2026
- ```bash
2027
- npx eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2028
- ```
2029
-
2030
- 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:
2031
-
2032
- ```bash
2033
- node eyeling.js --builtin examples/builtin/queens.js examples/queens.n3
2034
- ```
2035
-
2036
- 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`.
2037
-
2038
- 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.
2039
-
2040
- ### 14.2 The bundled Node CLI/runtime (`eyeling.js`)
2041
-
2042
- The bundle contains the whole engine. The CLI path is the “canonical behavior”:
2043
-
2044
- - parse one or more input sources; with multiple sources, parse each source independently and merge the ASTs
2045
- - reason to closure
2046
- - print derived triples, or render `log:outputString` strings when present
2047
- - optional proof comments
2048
- - optional streaming
2049
-
2050
- #### 14.2.1 CLI options at a glance
2051
-
2052
- The current CLI supports a small set of flags (see `lib/cli.js`):
2053
-
2054
- - `-a`, `--ast` — print the parsed AST as JSON and exit.
2055
- - `--builtin <module.js>` — load a custom builtin module (repeatable).
2056
- - `-d`, `--deterministic-skolem` — make `log:skolem` stable across runs.
2057
- - `-e`, `--enforce-https` — rewrite `http://…` to `https://…` for dereferencing builtins.
2058
- - `-p`, `--proof` — include per-fact proof explanations as N3 proof graphs.
2059
- - `-s`, `--super-restricted` — disable all builtins except `log:implies` / `log:impliedBy`.
2060
- - `-t`, `--stream` — stream derived triples as soon as they are derived.
2061
- - `-v`, `--version` — print version and exit.
2062
- - `-h`, `--help` — show usage.
2063
- - With no positional argument, Eyeling reads from stdin when input is piped.
2064
- - Use `-` as the input path to read explicitly from stdin.
2065
- - Multiple positional inputs are allowed, for example `eyeling facts.n3 rules.n3`; rules from any input can match facts from any other input after the merge.
2066
-
2067
- ### 14.3 Package entrypoint split for Node, browser, and CLI
2068
-
2069
- The repo now publishes three distinct surfaces instead of forcing browser tooling through the Node-first bundle entry:
2070
-
2071
- - `index.js` remains the **Node API** used by `require('eyeling')` and `import eyeling from 'eyeling'` in Node.
2072
- - `bin/eyeling.cjs` is the **CLI shim** with the shebang. It loads the Node bundle and calls `main()`.
2073
- - `dist/browser/eyeling.browser.js` is the **browser-safe bundle asset** with **no shebang**.
2074
- - `dist/browser/index.mjs` is the **browser import surface** exported as `eyeling/browser`.
2075
-
2076
- That gives the intended mental model:
2077
-
2078
- ```js
2079
- import eyeling from 'eyeling'; // Node
2080
- import eyelingBrowser from 'eyeling/browser'; // Browser / worker
2081
- ```
2082
-
2083
- ```bash
2084
- npx eyeling … # CLI
2085
- ```
2086
-
2087
- The `package.json` `exports` map points the `browser` condition at `dist/browser/index.mjs`, so browser-oriented bundlers stop resolving the package root to the Node wrapper in `index.js`.
2088
-
2089
- `dist/browser/index.mjs` intentionally re-exports only the browser-safe surface:
2090
-
2091
- - `reasonStream(...)`
2092
- - `reasonRdfJs(...)`
2093
- - `rdfjs`
2094
- - `registerBuiltin(...)`
2095
- - `unregisterBuiltin(...)`
2096
- - `registerBuiltinModule(...)`
2097
- - `listBuiltinIris()`
2098
-
2099
- It deliberately does **not** expose `loadBuiltinModule(...)`, because loading builtin files by module specifier is a Node-only pattern. In browsers, custom builtins should be registered directly in-process (for example with `registerBuiltin(...)` or `registerBuiltinModule(...)`).
2100
-
2101
- For browser apps, prefer running Eyeling in a **Web Worker** and importing `eyeling/browser` there.
2102
-
2103
- ### 14.4 `lib/entry.js`: bundler-friendly exports
2104
-
2105
- `lib/entry.js` exports:
2106
-
2107
- - public APIs: `reasonStream`, `reasonRdfJs`, `rdfjs`, `main`, `version`
2108
- - plus a curated set of internals used by the playground (`lex`, `Parser`, `forwardChain`, etc.)
2109
-
2110
- `rdfjs` is a small built-in RDF/JS `DataFactory`, so browser / worker code can construct quads without pulling in another package first.
2111
-
2112
- ### 14.5 JavaScript API
2113
-
2114
- Eyeling exposes two JavaScript entry styles:
2115
-
2116
- - `reason(...)` from `index.js` when you want the same text output as the CLI
2117
- - `reasonStream(...)` / `reasonRdfJs(...)` from the Node bundle or `eyeling/browser` when you want in-process reasoning and structured RDF/JS results
2118
-
2119
- #### 14.5.1 npm helper: `reason(...)`
2120
-
2121
- The npm `reason(...)` function does something intentionally simple and robust:
2122
-
2123
- - normalize the JavaScript input into N3 text
2124
- - write that N3 input to a temp file
2125
- - spawn the bundled CLI (`node eyeling.js ... input.n3`)
2126
- - return stdout (and forward stderr)
2127
-
2128
- This keeps the observable output identical to the CLI while still allowing richer JS-side inputs.
2129
-
2130
- CommonJS:
2131
-
2132
- ```js
2133
- const { reason } = require('eyeling');
2134
-
2135
- const input = `
2136
- @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
2137
- @prefix : <http://example.org/socrates#>.
2138
-
2139
- :Socrates a :Human.
2140
- :Human rdfs:subClassOf :Mortal.
2141
-
2142
- { ?s a ?A. ?A rdfs:subClassOf ?B. } => { ?s a ?B. }.
2143
- `;
2144
-
2145
- console.log(reason({ proof: false }, input));
2146
- ```
2147
-
2148
- ESM:
2149
-
2150
- ```js
2151
- import eyeling from 'eyeling';
2152
-
2153
- const input = `
2154
- @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
2155
- @prefix : <http://example.org/socrates#>.
2156
-
2157
- :Socrates a :Human.
2158
- :Human rdfs:subClassOf :Mortal.
2159
-
2160
- { ?s a ?A. ?A rdfs:subClassOf ?B. } => { ?s a ?B. }.
2161
- `;
2162
-
2163
- console.log(eyeling.reason({ proof: false }, input));
2164
- ```
2165
-
2166
- Notes:
2167
-
2168
- - `reason()` returns the same textual output you would get from the CLI for the same input and options.
2169
- - By default, the npm helper keeps output machine-friendly (`proof: false`).
2170
- - Use this path when you want CLI-equivalent behavior inside JavaScript.
2171
-
2172
- #### 14.5.2 RDF-JS and Eyeling rule-object interoperability
2173
-
2174
- The JavaScript APIs accept four input styles:
2175
-
2176
- 1. plain N3 text
2177
- 2. a multi-source N3 object (`{ sources: [...] }`)
2178
- 3. RDF/JS fact input (`quads`, `facts`, or `dataset`)
2179
- 4. Eyeling rule objects or full AST bundles
2180
-
2181
- If you want to use one N3 source text, pass the whole input as a plain string. If you want to avoid concatenating several N3 sources into one large string, pass them as a source list instead.
2182
-
2183
- For example:
2184
-
2185
- ```js
2186
- const { reason } = require('eyeling');
2187
-
2188
- const out = reason(
2189
- { proof: false },
2190
- {
2191
- sources: [
2192
- '@prefix : <http://example.org/> .\n:Socrates a :Man .\n',
2193
- '@prefix : <http://example.org/> .\n{ ?x a :Man } => { ?x a :Mortal } .\n',
2194
- ],
2195
- },
2196
- );
2197
-
2198
- console.log(out);
2199
- ```
2200
-
2201
- In a source list, each source is parsed with its own blank-node scope and optional base IRI. That means the same explicit blank label, such as `_:x`, in two different sources does not accidentally become the same blank node after merging. Prefix declarations are merged mainly for readable output; IRI expansion has already happened while each source was parsed.
2202
-
2203
- For large inputs, the source-list path is also the preferred memory shape: each source is lexed and parsed independently, then Eyeling keeps the merged facts/rules rather than retaining the original source strings and token arrays. The parser still needs one JavaScript string per source, so source splitting is not a streaming parser, but it avoids both a single giant concatenated N3 string and long-lived token arrays after parsing.
2204
-
2205
- For RDF/JS facts, the graph must be the default graph. Named-graph quads are rejected.
2206
-
2207
- If you already have rules in structured form, Eyeling rule objects can be passed directly in the API:
2208
-
2209
- ```js
2210
- const { reason, rdfjs } = require('eyeling');
2211
-
2212
- const ex = 'http://example.org/';
2213
-
2214
- const rule = {
2215
- _type: 'Rule',
2216
- premise: [
2217
- {
2218
- _type: 'Triple',
2219
- s: { _type: 'Var', name: 'x' },
2220
- p: { _type: 'Iri', value: ex + 'parent' },
2221
- o: { _type: 'Var', name: 'y' },
2222
- },
2223
- ],
2224
- conclusion: [
2225
- {
2226
- _type: 'Triple',
2227
- s: { _type: 'Var', name: 'x' },
2228
- p: { _type: 'Iri', value: ex + 'ancestor' },
2229
- o: { _type: 'Var', name: 'y' },
2230
- },
2231
- ],
2232
- isForward: true,
2233
- isFuse: false,
2234
- headBlankLabels: [],
2235
- };
2236
-
2237
- const out = reason(
2238
- { proof: false },
2239
- {
2240
- quads: [rdfjs.quad(rdfjs.namedNode(ex + 'alice'), rdfjs.namedNode(ex + 'parent'), rdfjs.namedNode(ex + 'bob'))],
2241
- rules: [rule],
2242
- },
2243
- );
2244
-
2245
- console.log(out);
2246
- ```
2247
-
2248
- You can also pass a full AST bundle directly, for example `[prefixes, triples, forwardRules, backwardRules]`.
2249
-
2250
- #### 14.5.3 In-process bundle API: `reasonStream(...)` and `reasonRdfJs(...)`
2251
-
2252
- Use the bundle entry if you want structured results while the engine is running instead of final CLI text after the fact.
2253
-
2254
- `reasonStream(...)` can emit RDF/JS quads while reasoning runs:
2255
-
2256
- ```js
2257
- import eyeling from './eyeling.js';
2258
-
2259
- const result = eyeling.reasonStream(input, {
2260
- proof: false,
2261
- rdfjs: true,
2262
- skipUnsupportedRdfJs: true,
2263
- onDerived: ({ triple, quad }) => {
2264
- if (quad) console.log(quad);
2265
- else console.warn('Skipped non-RDF/JS derived triple:', triple);
2266
- },
2267
- });
2268
- ```
2269
-
2270
- That same path also lets derived results be consumed as an async stream of RDF/JS quads:
2271
-
2272
- ```js
2273
- for await (const quad of eyeling.reasonRdfJs(input, {
2274
- skipUnsupportedRdfJs: true,
2275
- })) {
2276
- console.log(quad);
2277
- }
2278
- ```
2279
-
2280
- Use `skipUnsupportedRdfJs: true` when you want RDF/JS consumers to ignore derived triples that contain N3-only terms such as quoted formulas. This affects only RDF/JS export. The underlying Eyeling closure and `closureN3` output remain unchanged.
2281
-
2282
- Use these entry points when you need one or more of the following:
2283
-
2284
- - RDF/JS quads as fact input
2285
- - Eyeling rule objects passed directly from JavaScript
2286
- - derived results consumed as RDF/JS quads
2287
- - streaming derived RDF/JS quads during reasoning
2288
-
2289
- ### 14.6 Choosing the right entry point
2290
-
2291
- A practical rule of thumb:
2292
-
2293
- - if you want the same final text output as the CLI, use `reason(...)`
2294
- - if you want in-process access to structured facts, quads, or streaming derivations, use `reasonStream(...)` / `reasonRdfJs(...)`
2295
-
2296
- ---
2297
-
2298
- <a id="ch15"></a>
2299
-
2300
- ## Chapter 15 — A worked example: Socrates, step by step
2301
-
2302
- Consider:
2303
-
2304
- ```n3
2305
- @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
2306
- @prefix : <http://example.org/socrates#>.
2307
-
2308
- :Socrates a :Human.
2309
- :Human rdfs:subClassOf :Mortal.
2310
-
2311
- { ?S a ?A. ?A rdfs:subClassOf ?B } => { ?S a ?B }.
2312
- ```
2313
-
2314
- What Eyeling does:
2315
-
2316
- 1. Parsing yields two facts:
2317
- - `(:Socrates rdf:type :Human)`
2318
- - `(:Human rdfs:subClassOf :Mortal)` and one forward rule:
2319
- - premise goals: `?S a ?A`, `?A rdfs:subClassOf ?B`
2320
- - head: `?S a ?B`
2321
-
2322
- 2. Forward chaining scans the rule and calls `proveGoals` on the body.
2323
-
2324
- 3. Proving `?S a ?A` matches the first fact, producing `{ S = :Socrates, A = :Human }`.
2325
-
2326
- 4. With that substitution, the second goal becomes `:Human rdfs:subClassOf ?B`. It matches the second fact, extending to `{ B = :Mortal }`.
2327
-
2328
- 5. Eyeling instantiates the head `?S a ?B` → `:Socrates a :Mortal`.
2329
-
2330
- 6. The triple is ground and not already present, so it is added and (optionally) printed.
2331
-
2332
- That is the whole engine in miniature: unify, compose substitutions, emit head triples.
2333
-
2334
- ---
2335
-
2336
- <a id="ch16"></a>
2337
-
2338
- ## Chapter 16 — Extending Eyeling (without breaking it)
2339
-
2340
- Eyeling is small, which makes it pleasant to extend — but there are a few invariants worth respecting.
2341
-
2342
- The most important update is architectural: **you no longer need to patch `lib/builtins.js` just to add a project-specific builtin**. The preferred path is now to load a custom builtin module, either programmatically or from the CLI. Core builtins still live in `lib/builtins.js`, but user extensions can stay outside the engine.
2343
-
2344
- ### 16.1 The preferred path: custom builtin modules
2345
-
2346
- Eyeling now exposes a small custom-builtin registry.
2347
-
2348
- At runtime, builtin predicates can be added with:
2349
-
2350
- - `registerBuiltin(iri, handler)`
2351
- - `unregisterBuiltin(iri)`
2352
- - `registerBuiltinModule(moduleExport, origin?)`
2353
- - `loadBuiltinModule(specifier, { resolveFrom? })`
2354
- - `listBuiltinIris()`
2355
-
2356
- That means the extension story is:
2357
-
2358
- - keep the engine’s shipped builtins in `lib/builtins.js`
2359
- - keep your own application or domain builtins in a separate `.js` module
2360
- - load that module with `--builtin` or from JavaScript
2361
-
2362
- This is the safest way to extend Eyeling because it avoids forking the builtin dispatcher and keeps upgrades merge-friendly.
2363
-
2364
- ### 16.2 CLI loading: `--builtin`
2365
-
2366
- The CLI accepts a repeatable `--builtin <module.js>` option:
2367
-
2368
- ```bash
2369
- eyeling --builtin ./hello-builtin.js rules.n3
2370
- ```
2371
-
2372
- You can pass it more than once:
2373
-
2374
- ```bash
2375
- eyeling --builtin ./math-extra.js --builtin ./domain-rules.js input.n3
2376
- ```
2377
-
2378
- Each module is loaded before reasoning starts. Paths are resolved from the current working directory.
2379
-
2380
- The same capability is available through the npm wrapper:
2381
-
2382
- ```js
2383
- const { reason } = require('eyeling');
2384
- const out = reason({ builtinModules: ['./hello-builtin.js'] }, n3Text);
2385
- ```
2386
-
2387
- ### 16.2.1 Stability rule for `--builtin`
2388
-
2389
- Eyeling keeps `--builtin` simple.
2390
-
2391
- There is one small helper API passed into builtin modules. That helper object is frozen, its key set is regression-tested, and builtin modules must use one of the documented export forms.
2392
-
2393
- In practice, this means:
2394
-
2395
- - builtin module loading accepts only the documented export forms
2396
- - the helper API exposed by `__buildBuiltinRegistrationApi()` has a fixed key set
2397
- - builtin handlers should return an array of substitution objects
2398
- - accidental helper drift is caught by `test/builtins.test.js`
2399
-
2400
- This is only meant to stop silent breakage. It is **not** a promise that Eyeling can never change the builtin API. If the helper surface ever needs to change, that change should be deliberate, documented, and called out in release notes.
2401
-
2402
- ### 16.3 What a builtin module may export
2403
-
2404
- Eyeling accepts these stable module shapes.
2405
-
2406
- #### A function export
2407
-
2408
- ```js
2409
- module.exports = ({ registerBuiltin, internLiteral, unifyTerm, terms }) => {
2410
- const { Var } = terms;
2411
-
2412
- registerBuiltin('http://example.org/custom#hello', ({ goal, subst }) => {
2413
- const lit = internLiteral('"world"');
2414
- if (goal.o instanceof Var) {
2415
- return [{ ...subst, [goal.o.name]: lit }];
2416
- }
2417
- const s2 = unifyTerm(goal.o, lit, subst);
2418
- return s2 ? [s2] : [];
2419
- });
2420
- };
2421
- ```
2422
-
2423
- #### An object with `register(api)`
2424
-
2425
- ```js
2426
- module.exports = {
2427
- register(api) {
2428
- api.registerBuiltin('http://example.org/custom#ping', ({ subst }) => [subst]);
2429
- },
2430
- };
2431
- ```
2432
-
2433
- #### A plain object mapping predicate IRIs to handlers
2434
-
2435
- ```js
2436
- module.exports = {
2437
- 'http://example.org/custom#ok': ({ subst }) => [subst],
2438
- };
2439
- ```
2440
-
2441
- #### An object with `.builtins`
2442
-
2443
- ```js
2444
- module.exports = {
2445
- builtins: {
2446
- 'http://example.org/custom#ok': ({ subst }) => [subst],
2447
- },
2448
- };
2449
- ```
2450
-
2451
- #### An object with `.default` as a plain object map
2452
-
2453
- This is mainly an ESM/transpiler compatibility form.
2454
-
2455
- ```js
2456
- module.exports = {
2457
- default: {
2458
- 'http://example.org/custom#ok': ({ subst }) => [subst],
2459
- },
2460
- };
2461
- ```
2462
-
2463
- If none of those shapes match, Eyeling rejects the module with a descriptive error.
2464
-
2465
- ### 16.4 The handler contract
2466
-
2467
- Builtin handlers are called with a context object containing:
2468
-
2469
- - `iri` — the predicate IRI string
2470
- - `goal` — the current triple goal
2471
- - `subst` — the current substitution
2472
- - `facts` — the active fact store
2473
- - `backRules` — the backward-rule set
2474
- - `depth` — current proof depth
2475
- - `varGen` — the variable generator state
2476
- - `maxResults` — current result cap
2477
- - `api` — the same registration/helper API used by modules
2478
-
2479
- A handler should return an **array of substitution objects**:
2480
-
2481
- - `[]` means failure / no solutions
2482
- - `[{}]` means success with no new bindings
2483
- - `[{ ...delta }]` means one successful continuation with bindings
2484
- - multiple objects mean a generator builtin
2485
-
2486
- Returning something else is rejected at runtime.
2487
-
2488
- In practice:
2489
-
2490
- - Decide if your builtin is a test, a functional relation, or a generator.
2491
- - Return substitutions (or substitution deltas merged into the current substitution), not printed output.
2492
- - Be cautious with fully-unbound generators: they can explode the search space.
2493
- - If a builtin needs inputs to be bound first, it is fine to fail early and let forward-rule proving retry later in the conjunction.
2494
-
2495
- Custom builtin failures are wrapped so the predicate IRI appears in the thrown error message, which makes debugging much easier from the CLI.
2496
-
2497
- ### 16.5 The helper API exposed to builtin modules
2498
-
2499
- Builtin modules do not need to import internal engine files directly. Eyeling passes a helper API into module registration, and that helper surface is kept intentionally small.
2500
-
2501
- The current helper function set is:
2502
-
2503
- - `registerBuiltin`, `unregisterBuiltin`, `listBuiltinIris`
2504
- - `internIri`, `internLiteral`, `literalParts`
2505
- - `termToJsString`, `termToJsStringDecoded`, `termToN3`, `iriValue`
2506
- - `unifyTerm`, `applySubstTerm`, `applySubstTriple`, `proveGoals`, `isGroundTerm`
2507
- - `computeConclusionFromFormula`, `skolemIriFromGroundTerm`
2508
- - `parseBooleanLiteralInfo`, `parseNumericLiteralInfo`, `parseXsdDecimalToBigIntScale`, `pow10n`
2509
- - `normalizeLiteralForFastKey`, `literalsEquivalentAsXsdString`, `materializeRdfLists`
2510
-
2511
- The stable namespace bags are:
2512
-
2513
- - `terms`: `Literal`, `Iri`, `Var`, `Blank`, `ListTerm`, `OpenListTerm`, `GraphTerm`, `Triple`, `Rule`
2514
- - `ns`: `RDF_NS`, `XSD_NS`, `CRYPTO_NS`, `MATH_NS`, `TIME_NS`, `LIST_NS`, `LOG_NS`, `STRING_NS`
2515
-
2516
- The helper object is frozen and regression-tested so helper additions, removals, and renames do not slip in silently.
2517
-
2518
- That API keeps the extension boundary explicit: custom builtins get the operations they need without reaching into Eyeling’s private module graph.
2519
-
2520
- ### 16.6 A shipped example: the Sudoku builtin
2521
-
2522
- The repository ships a Sudoku example program (`examples/sudoku.n3`) together with its example-specific builtin module (`examples/builtin/sudoku.js`).
2523
-
2524
- Run it explicitly like this:
2525
-
2526
- ```bash
2527
- eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2528
- ```
2529
-
2530
- `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.
2531
-
2532
- That example is useful for two reasons:
2533
-
2534
- - it shows a realistic domain-specific builtin implemented outside the core builtin switchboard
2535
- - it demonstrates the intended deployment model for larger custom relations: keep the N3 logic in the `.n3` file, and keep specialized search/verification code in a loadable builtin module
2536
-
2537
- ### 16.7 When you should still edit `lib/builtins.js`
2538
-
2539
- Editing `lib/builtins.js` is still reasonable when you are:
2540
-
2541
- - adding or fixing a **core** Eyeling builtin
2542
- - changing builtin behavior that should ship as part of Eyeling itself
2543
- - modifying the builtin helper API that custom modules depend on
2544
-
2545
- But if the builtin is project-specific, experimental, or domain-bound, prefer a custom module first.
2546
-
2547
- A small architectural note: `lib/builtins.js` is still initialized by the engine via `makeBuiltins(deps)`. It receives hooks (unification, proving, deref, scoped-closure helpers, …) instead of importing the engine directly, which keeps the module graph acyclic and makes browser bundling easier.
2548
-
2549
- If your builtin needs a stable view of the scoped closure, follow the scoped-builtin pattern:
2550
-
2551
- - read from `facts.__scopedSnapshot`
2552
- - honor `facts.__scopedClosureLevel` and priority gating
2553
-
2554
- ### 16.8 Adding new term shapes
2555
-
2556
- If you add a new Term subclass, you’ll likely need to touch:
2557
-
2558
- - printing (`termToN3`)
2559
- - unification and equality (`unifyTerm`, `termsEqual`, fast keys)
2560
- - variable collection for compaction (`gcCollectVarsInTerm`)
2561
- - groundness checks
2562
-
2563
- ### 16.9 Parser extensions
2564
-
2565
- If you extend parsing, preserve the Rule invariants:
2566
-
2567
- - rule premise is a triple list
2568
- - rule conclusion is a triple list
2569
- - blanks in premise are lifted (or handled consistently)
2570
- - `headBlankLabels` must reflect blanks occurring explicitly in the head _before_ skolemization
2571
-
2572
- ---
2573
-
2574
- <a id="epilogue"></a>
2575
-
2576
- ## Epilogue: the philosophy of this engine
2577
-
2578
- Eyeling’s codebase is compact because it chooses one powerful idea and leans into it:
2579
-
2580
- > **Use backward proving as the “executor” for forward rule bodies.**
2581
-
2582
- That design makes built-ins and backward rules feel like a standard library of relations, while forward chaining still gives you the determinism and “materialized closure” feel of Datalog.
2583
-
2584
- If you remember only one sentence from this handbook, make it this:
2585
-
2586
- **Eyeling is a forward-chaining engine whose rule bodies are solved by a Prolog-like backward prover with built-ins.**
2587
-
2588
- Everything else is engineering detail — interesting, careful, sometimes subtle — but always in service of that core shape.
2589
-
2590
- ---
2591
-
2592
- <a id="app-a"></a>
2593
-
2594
- ## Appendix A — Eyeling user notes
2595
-
2596
- This appendix is a compact, user-facing reference for **running Eyeling** and **writing inputs that work well**. For deeper explanations and implementation details, follow the chapter links in each section.
2597
-
2598
- ### A.1 Install and run
2599
-
2600
- Eyeling is distributed as an npm package.
2601
-
2602
- - Run without installing:
2603
-
2604
- ```bash
2605
- npx eyeling --help
2606
- npx eyeling yourfile.n3
2607
- ```
2608
-
2609
- - Or install globally:
2610
-
2611
- ```bash
2612
- npm i -g eyeling
2613
- eyeling yourfile.n3
2614
- ```
2615
-
2616
- See also: [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14).
2617
-
2618
- ### A.2 What Eyeling prints
2619
-
2620
- By default, Eyeling prints **newly derived forward facts** (the heads of fired `=>` rules), serialized as N3. It does **not** reprint your input facts.
2621
-
2622
- If the input contains one or more **top-level** `log:query` directives:
2623
-
2624
- ```n3
2625
- { ...premise... } log:query { ...conclusion... }.
2626
- ```
2627
-
2628
- Eyeling still computes the saturated forward closure, but it prints only the **unique instantiated conclusion triples** of those `log:query` directives (instead of all newly derived facts). This is useful when you want a forward-rule-like projection of results.
2629
-
2630
- For proof/explanation output and output modes, see:
2631
-
2632
- - [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
2633
-
2634
- ### A.3 CLI quick reference
2635
-
2636
- The authoritative list is always:
2637
-
2638
- ```bash
2639
- eyeling --help
2640
- ```
2641
-
2642
- Usage:
2643
-
2644
- ```bash
2645
- eyeling [options] [file-or-url.n3|- ...]
2646
- ```
2647
-
2648
- Options:
2649
-
2650
- ```
2651
- -a, --ast Print parsed AST as JSON and exit.
2652
- --builtin <module.js> Load a custom builtin module (repeatable).
2653
- -d, --deterministic-skolem Make log:skolem stable across reasoning runs.
2654
- -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
2655
- -h, --help Show this help and exit.
2656
- -p, --proof Enable proof explanations.
2657
- -r, --rdf Enable RDF/TriG input/output compatibility.
2658
- --stream-messages Process RDF Message Logs one message at a time under -r.
2659
- -s, --super-restricted Disable all builtins except => and <=.
2660
- -t, --stream Stream derived triples as soon as they are derived.
2661
- -v, --version Print version and exit.
2662
- ```
2663
-
2664
- Input note: with multiple positional inputs, Eyeling reads and parses each source separately, then merges facts, forward rules, backward rules, and `log:query` directives before reasoning. Blank node labels are scoped per input document.
2665
-
2666
- Note: when `log:query` directives are present, or when the program may produce `log:outputString` facts, Eyeling cannot stream its final user-facing output from partial derivations, so `--stream` has no effect in those cases. In the latter case Eyeling saturates first and then renders the collected output strings.
2667
-
2668
- See also:
2669
-
2670
- - [Chapter 13 — Printing, proofs, and the user-facing output](#ch13)
2671
- - [Chapter 12 — Dereferencing and web-like semantics](#ch12)
2672
-
2673
- ### A.4 N3 syntax notes that matter in practice
2674
-
2675
- Eyeling implements a practical N3 subset centered around facts and rules.
2676
-
2677
- - A **fact** is a triple ending in `.`:
2678
-
2679
- ```n3
2680
- :alice :knows :bob .
2681
- ```
2682
-
2683
- - A **forward rule**:
2684
-
2685
- ```n3
2686
- { ?x :p ?y } => { ?y :q ?x } .
2687
- ```
2688
-
2689
- - A **backward rule**:
2690
-
2691
- ```n3
2692
- { ?x :ancestor ?z } <= { ?x :parent ?z } .
2693
- ```
2694
-
2695
- Quoted graphs/formulas use `{ ... }`. Inside a quoted formula, directive scope matters:
2696
-
2697
- - `@prefix/@base` and `PREFIX/BASE` directives may appear at top level **or inside `{ ... }`**, and apply to the formula they occur in (formula-local scoping).
2698
-
2699
- With `-r, --rdf` / `{ rdf: true }`, Eyeling accepts selected RDF/TriG surface syntax before normal N3 parsing. RDF 1.2 triple-term forms such as `<<( s p o )>>` and `<<s p o ~ r>>` are compatibility spellings for singleton quoted formulas such as `{ s p o }`; feasible singleton graph terms are printed back as RDF 1.2 triple terms. TriG named graph blocks become `log:nameOf` quoted formulas. If a `VERSION "1.2-messages"`-style directive is present, top-level `MESSAGE` delimiters are replayed as `eymsg:` stream/envelope facts and per-message payload graphs. For large independent message logs, `--stream-messages` runs the same payload-inspection style one message at a time. See [Chapter 4.1](#41-lexing-tokens-not-magic) for the full `-r` model and RDF Message handling.
2700
-
2701
- For the formal grammar, see the N3 spec grammar:
2702
-
2703
- - [https://w3c.github.io/N3/spec/#grammar](https://w3c.github.io/N3/spec/#grammar)
2704
-
2705
- See also:
2706
-
2707
- - [Chapter 4 — From characters to AST: lexing and parsing](#ch04)
2708
-
2709
- ### A.5 Builtins
2710
-
2711
- Eyeling supports a built-in “standard library” across namespaces like `log:`, `math:`, `string:`, `list:`, `time:`, `crypto:`.
2712
-
2713
- It also supports **custom builtin modules**.
2714
-
2715
- - From the CLI: `eyeling --builtin ./my-builtins.js input.n3`
2716
- - From JavaScript: `reason({ builtinModules: ['./my-builtins.js'] }, input)`
2717
- - Programmatically in-process: `registerBuiltin(...)`, `registerBuiltinModule(...)`, `loadBuiltinModule(...)`
2718
-
2719
- A concrete shipped example is the Sudoku builtin paired with `examples/sudoku.n3`:
2720
-
2721
- ```bash
2722
- eyeling --builtin examples/builtin/sudoku.js examples/sudoku.n3
2723
- ```
2724
-
2725
- References:
2726
-
2727
- - W3C N3 Built-ins overview: [https://w3c.github.io/N3/reports/20230703/builtins.html](https://w3c.github.io/N3/reports/20230703/builtins.html)
2728
- - Eyeling implementation details: [Chapter 11 — Built-ins as a standard library](#ch11)
2729
- - Extension API and custom module loading: [Chapter 16 — Extending Eyeling (without breaking it)](#ch16)
2730
- - The shipped builtin catalogue: `eyeling-builtins.ttl` (in this repo)
2731
-
2732
- If you are running untrusted inputs, consider `--super-restricted` to disable all builtins except implication.
2733
-
2734
- ### A.6 Skolemization and `log:skolem`
2735
-
2736
- When forward rule heads contain blank nodes (existentials), Eyeling replaces them with generated Skolem IRIs so derived facts are ground.
2737
-
2738
- See:
2739
-
2740
- - [Chapter 9 — Forward chaining: saturation, skolemization, and meta-rules](#ch09)
2741
-
2742
- ### A.7 Networking and `log:semantics`
2743
-
2744
- `log:content`, `log:semantics`, and related builtins dereference IRIs and parse retrieved content. This is powerful, but it is also I/O.
2745
-
2746
- See:
2747
-
2748
- - [Chapter 12 — Dereferencing and web-like semantics](#ch12)
2749
-
2750
- Safety tip:
2751
-
2752
- - Use `--super-restricted` if you want to ensure _no_ dereferencing (and no other builtins) can run.
2753
-
2754
- ### A.8 Embedding Eyeling in JavaScript
2755
-
2756
- If you depend on Eyeling as a library, the package exposes:
2757
-
2758
- - a CLI wrapper API (`reason(...)`), and
2759
- - in-process engine entry points (via the bundle exports).
2760
-
2761
- See:
2762
-
2763
- - [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
2764
-
2765
- ### A.9 Further reading
2766
-
2767
- If you want to go deeper into N3 itself and the logic/programming ideas behind Eyeling, these are good starting points:
2768
-
2769
- N3 / Semantic Web specs and reports:
2770
-
2771
- - [https://w3c.github.io/N3/spec/](https://w3c.github.io/N3/spec/)
2772
- - [https://w3c.github.io/N3/spec/builtins](https://w3c.github.io/N3/spec/builtins)
2773
- - [https://w3c.github.io/N3/spec/semantics](https://w3c.github.io/N3/spec/semantics)
2774
-
2775
- Logic & reasoning background (Wikipedia):
2776
-
2777
- - [https://en.wikipedia.org/wiki/Mathematical_logic](https://en.wikipedia.org/wiki/Mathematical_logic)
2778
- - [https://en.wikipedia.org/wiki/Automated_reasoning](https://en.wikipedia.org/wiki/Automated_reasoning)
2779
- - [https://en.wikipedia.org/wiki/Forward_chaining](https://en.wikipedia.org/wiki/Forward_chaining)
2780
- - [https://en.wikipedia.org/wiki/Backward_chaining](https://en.wikipedia.org/wiki/Backward_chaining)
2781
- - [https://en.wikipedia.org/wiki/Unification\_%28computer_science%29](https://en.wikipedia.org/wiki/Unification_%28computer_science%29)
2782
- - [https://en.wikipedia.org/wiki/Prolog](https://en.wikipedia.org/wiki/Prolog)
2783
- - [https://en.wikipedia.org/wiki/Datalog](https://en.wikipedia.org/wiki/Datalog)
2784
- - [https://en.wikipedia.org/wiki/Skolem_normal_form](https://en.wikipedia.org/wiki/Skolem_normal_form)
2785
-
2786
- ---
2787
-
2788
- <a id="app-b"></a>
2789
-
2790
- ## Appendix B — Notation3: when facts can carry their own logic
2791
-
2792
- RDF succeeded by making a radical design choice feel natural: reduce meaning to small, uniform statements—triples—that can be published, merged, and queried across boundaries. A triple does not presume a database schema, a programming language, or a particular application. It presumes only that names (IRIs) can be shared, and that graphs can be combined.
2793
-
2794
- That strength also marks RDF’s limit. The moment a graph is expected to _do_ something—normalize values, reconcile vocabularies, derive implied relationships, enforce a policy, compute a small transformation—logic tends to migrate into code. The graph becomes an inert substrate while the decisive semantics hide in scripts, services, ETL pipelines, or bespoke rule engines. What remains portable is the data; what often becomes non-portable is the meaning.
2795
-
2796
- Notation3 (N3) sits precisely at that seam. It remains a readable way to write RDF, but it also treats _graphs themselves_ as objects that can be described, matched, and related. The N3 Community Group’s specification presents N3 as an assertion and logic language that extends RDF rather than replacing it: [https://w3c.github.io/N3/spec/](https://w3c.github.io/N3/spec/).
2797
-
2798
- The essential move is quotation: writing a graph inside braces as a thing that can be discussed. Once graphs can be quoted, rules become graph-to-graph transformations. The familiar implication form, `{ … } => { … } .`, reads as a piece of prose: whenever the antecedent pattern holds, the consequent pattern follows. Tim Berners-Lee’s design note frames this as a web-friendly logic with variables and nested graphs: [https://www.w3.org/DesignIssues/Notation3.html](https://www.w3.org/DesignIssues/Notation3.html).
2799
-
2800
- This style of rule-writing makes rules first-class, publishable artifacts. It keeps the unit of exchange stable. Inputs are RDF graphs; outputs are RDF graphs. Inference produces new triples rather than hidden internal state. Rule sets can be versioned alongside data, reviewed as text, and executed by different engines that implement the same semantics. That portability theme runs back to the original W3C Team Submission: [https://www.w3.org/TeamSubmission/n3/](https://www.w3.org/TeamSubmission/n3/).
2801
-
2802
- Practical reasoning also depends on computation: lists, strings, math, comparisons, and the other “small operations” that integration work demands. N3 addresses this by standardizing built-ins—predicates with predefined behavior that can be used inside rule bodies while preserving the declarative, graph-shaped idiom. The built-ins report is here: [https://w3c.github.io/N3/reports/20230703/builtins.html](https://w3c.github.io/N3/reports/20230703/builtins.html).
2803
-
2804
- Testing is where rule languages either converge or fragment. Different implementations can drift on scoping, blank nodes, quantification, and built-in behavior. N3’s recent direction has been toward explicit, testable semantics, documented separately as model-theoretic foundations: [https://w3c.github.io/N3/reports/20230703/semantics.html](https://w3c.github.io/N3/reports/20230703/semantics.html).
2805
-
2806
- In that context, public conformance suites become more than scoreboards: they are the mechanism by which interoperability becomes measurable. The community test suite lives at [https://codeberg.org/phochste/notation3tests/](https://codeberg.org/phochste/notation3tests/), with comparative results published in its report: [https://codeberg.org/phochste/notation3tests/src/branch/main/reports/report.md](https://codeberg.org/phochste/notation3tests/src/branch/main/reports/report.md).
2807
-
2808
- The comparison with older tools is historically instructive. Cwm (Closed World Machine) was an early, influential RDF data processor and forward-chaining reasoner—part of the lineage that treated RDF (often written in N3) as something executable: [https://www.w3.org/2000/10/swap/doc/cwm](https://www.w3.org/2000/10/swap/doc/cwm).
2809
-
2810
- What motivates Notation3, in the end, is architectural restraint. It refuses to let “logic” become merely a private feature of an application stack. It keeps meaning close to the graph: rules are expressed as graph patterns; results are expressed as triples; computation is pulled in through well-defined built-ins rather than arbitrary code. This produces a style of working where integration and inference are not sidecar scripts, but publishable artifacts—documents that can be inspected, shared, tested, and reused.
2811
-
2812
- In that sense, N3 is less a bid to make the web “smarter” than a bid to make meaning _portable_: not only facts that travel, but also the explicit steps by which facts can be connected, extended, and made actionable—without abandoning the simplicity that made triples travel in the first place.
2813
-
2814
- ---
2815
-
2816
- <a id="app-c"></a>
2817
-
2818
- ## Appendix C — Why N3 fits the Eyeling examples
2819
-
2820
- The Eyeling examples combine several things at once. They contain facts about a situation, rules that derive new facts, checks that make the result testable, and an answer that can be shown to a human. That combination matters. It means that Eyeling is not only a data exercise and not only a logic exercise. It needs a notation in which data and rules can remain together.
2821
-
2822
- This raises a practical question: which language fits these examples best?
2823
-
2824
- SQL is a natural candidate when the main task is storing data and querying it. Prolog is a natural candidate when the main task is writing rules and deriving consequences from facts. N3 is interesting because it tries to keep those two sides together. The point of this appendix is not to rank SQL, Prolog, and N3 in general. The point is to explain why N3 works especially well for Eyeling-style examples.
2825
-
2826
- ### What the examples need
2827
-
2828
- A typical Eyeling example is not just a small dataset. It is also not just a set of inference rules. It is a compact artifact in which several layers belong together.
2829
-
2830
- There is usually a description of a situation: products, airports, organisms, policies, signatures, dates, or other entities. There are rules that derive new facts from those inputs. There are explicit checks that say whether the intended conclusions hold. And there is often a final answer or explanation that is part of the example itself.
2831
-
2832
- This is the real design problem. If the language handles only one of these layers well, then the example has to be split up. The data ends up in one notation, the rules in another, the checks somewhere else, and the final answer in yet another place. Once that happens, the example becomes harder to read and harder to maintain.
2833
-
2834
- ### What SQL contributes
2835
-
2836
- SQL is strong when the main task is structured data and queries over that data. It is excellent for tables, filtering, aggregation, joins, and efficient execution. When an Eyeling example is translated into DuckDB, SQL can do a surprising amount. Recursive queries can express route search. Views can express derived facts. Checks can be written as boolean queries. Output can be assembled from query results.
2837
-
2838
- That is useful, and it shows that the examples can be operationalized in a relational setting.
2839
-
2840
- However, SQL is not the original shape of these examples. To get there, the graph-like source has to be mapped into tables, and the rule logic has to be reconstructed with joins, common table expressions, macros, and views. The result can work well, but the conceptual structure becomes more indirect. Data and reasoning are still connected, but they are no longer expressed in the same native form.
2841
-
2842
- In other words, SQL is a good execution target, but it is not always the clearest authoring language for this kind of material.
2843
-
2844
- ### What Prolog contributes
2845
-
2846
- Prolog is strong when the main task is expressing facts and rules directly. An Eyeling example often looks much closer to Prolog than to SQL once the focus shifts to derivation. Facts become predicates. Rules become clauses. Recursive reasoning becomes natural. This makes Prolog a very good target when the aim is to express the logical behavior of an example clearly.
2847
-
2848
- That is why Prolog translations of Eyeling examples often feel much cleaner than SQL translations. The rule layer fits naturally.
2849
-
2850
- However, Prolog is not primarily a graph data notation. Eyeling examples often use a linked-data style in which named entities and relations remain visible as part of the knowledge representation. In Prolog this can certainly be modeled, but it is usually represented as application-specific predicates rather than as a graph-native notation. That means the rule side is natural, while the original data style becomes less central.
2851
-
2852
- So Prolog captures the inference well, but it does not preserve the same linked-data feel as naturally as N3 does.
2853
-
2854
- ### Why N3 fits these examples well
2855
-
2856
- N3 fits Eyeling well because it keeps the data model and the rule model close together.
2857
-
2858
- The facts remain graph-shaped. Entities and relations can be written directly. Rules can be added in the same notation. Checks can be expressed next to the derivations they depend on. Even the final answer can remain part of the same artifact. This allows an example to stay compact from beginning to end.
2859
-
2860
- That compactness is important. It means the reader can inspect one example and see the situation, the derivation, the checks, and the answer without mentally switching between several different layers of representation.
2861
-
2862
- This is the main reason N3 feels like a sweet spot in Eyeling. Compared with SQL, it avoids the split between graph-shaped knowledge and relational encoding. Compared with Prolog, it avoids the split between logic programming and linked-data representation. It keeps both sides close enough that the whole example can stay in one place.
2863
-
2864
- ### Where this matters in practice
2865
-
2866
- This matters most in examples where the structure of the knowledge is part of the point.
2867
-
2868
- In the path-discovery example, the facts describe airports and routes, and the rule describes how a connection can be found through a bounded number of stopovers. In SQL, this becomes a recursive query over tables. In Prolog, it becomes a recursive predicate over facts. In N3, the graph and the rule remain in one notation.
2869
-
2870
- In the barley-seed-becoming example, the facts describe stages, transitions, and constraints, and the rules determine what can and cannot become something else. In SQL and Prolog, this can be translated, but N3 preserves the original structure more directly.
2871
-
2872
- In the delfour example, the same pattern becomes even clearer. The example combines facts about products and household needs, rules about authorization and recommendation, checks over the derived conclusions, and a final human-readable answer. That kind of example is exactly where a language that keeps data, rules, checks, and answers together becomes valuable.
2873
-
2874
- ### Conclusion
2875
-
2876
- N3 is not the best language for every task. SQL is stronger as a database query language. Prolog is stronger as a pure rule language. But the Eyeling examples are not only database exercises and not only rule exercises.
2877
-
2878
- They are compact knowledge artifacts in which facts, rules, checks, and answers belong together.
2879
-
2880
- That is why N3 fits them so well. It is not because N3 wins an abstract language competition. It is because these examples need a form in which data and reasoning can remain unified. For Eyeling, that is exactly what N3 provides.
2881
-
2882
- ---
2883
-
2884
- <a id="app-d"></a>
2885
-
2886
- ## Appendix D — LLM + Eyeling: A Repeatable Logic Toolchain
2887
-
2888
- Eyeling is a deterministic N3 engine: given facts and rules, it derives consequences to a fixpoint using forward rules proved by a backward engine. That makes it a good “meaning boundary” for LLM-assisted workflows: the LLM can draft and refactor N3, but **Eyeling is what decides what follows**.
2889
-
2890
- A practical pattern is to treat the LLM as a **syntax-and-structure generator** and Eyeling as the **semantic validator**.
2891
-
2892
- ### 1) Constrain the LLM to output compilable N3
2893
-
2894
- If the LLM is allowed to emit prose or “almost N3”, you’ll spend your time cleaning up. Instead, require:
2895
-
2896
- - **Only N3** (no explanations in the artifact).
2897
- - A fixed prefix set (or a required `@base`).
2898
- - One artifact per file (facts + rules), optionally with a separate test file.
2899
- - “No invention” rules for IRIs: new symbols must be declared or use a designated namespace.
2900
-
2901
- This is less about prompt craft and more about creating a stable interface between a text generator and a compiler-like consumer.
2902
-
2903
- ### 2) Use Eyeling as the compile check and the semantic check
2904
-
2905
- Run Eyeling immediately after generation:
2906
-
2907
- - **Parse failures** → feed the error back to the LLM and request a corrected N3 file (same vocabulary, minimal diff).
2908
- - **Runtime failures / fuses** → treat as a spec violation, not “the model being creative”.
2909
-
2910
- Eyeling explicitly supports **inference fuses**: a forward rule with head `false` is a hard failure. This is extremely useful as a guardrail when you want “never allow X” constraints to stop the run.
2911
-
2912
- Example fuse:
2913
-
2914
- ```n3
2915
- @prefix : <http://example/> .
2916
-
2917
- { ?u :role :Admin.
2918
- ?u :disabled true.
2919
- } => false.
2920
- ```
2921
-
2922
- If you do not want “stop the world”, derive a `:Violation` fact instead, and keep going.
2923
-
2924
- ### 3) Make the workflow test-driven (golden closures)
2925
-
2926
- The most robust way to keep LLM-generated logic plausible is to make it live under tests:
2927
-
2928
- - Keep tiny **fixtures** (facts) alongside the rules.
2929
- - Run Eyeling to produce the **derived closure** (Eyeling emits only newly derived forward facts by default, can optionally include compact proof comments, and can also use `log:query` directives to project a specific result set).
2930
- - Compare against an expected output (“golden file”) in CI.
2931
-
2932
- This turns rule edits into a normal change-management loop: diffs are explicit, reviewable, and reproducible.
2933
-
2934
- ### 4) Use proofs/traces as the input to the LLM, not the other way around
2935
-
2936
- If you want a natural-language explanation, do not ask the model to “explain the rules from memory”. Instead:
2937
-
2938
- 1. Run Eyeling with proof/trace enabled (Eyeling has explicit tracing hooks and proof-comment support in its output pipeline).
2939
- 2. Give the LLM the **derived triples + proof comments** and ask it to summarize:
2940
- - what was derived,
2941
- - which rule(s) fired,
2942
- - which premises mattered.
2943
-
2944
- This keeps explanations anchored to what Eyeling actually derived.
2945
-
2946
- ### 5) The refinement loop: edits are N3 diffs, not “better prompting”
2947
-
2948
- When output looks wrong, the fix should be a change in the artifact:
2949
-
2950
- - tighten a premise,
2951
- - split one rule into two,
2952
- - add an exception rule,
2953
- - introduce a new predicate to separate concepts,
2954
- - add a fuse or a `:Violation` derivation,
2955
- - add a test case that locks in the intended behavior.
2956
-
2957
- Then regenerate/rewrite **only the N3**, rerun Eyeling, and review the diff.
2958
-
2959
- ### A prompt shape that tends to behave well
2960
-
2961
- A simple structure that keeps the LLM honest:
2962
-
2963
- - “Output **only** N3.”
2964
- - “Use exactly these prefixes.”
2965
- - “Do not introduce new IRIs outside `<base>#*`.”
2966
- - “Include at least N minimal tests as facts in a separate block/file.”
2967
- - “If something is unknown, emit a placeholder fact (`:needsFact`) rather than guessing.”
2968
-
2969
- The point is not that the LLM is “right”; it is that **Eyeling makes the result checkable**, and the artifact becomes a maintainable program rather than a one-off generation.
2970
-
2971
- ---
2972
-
2973
- <a id="app-e"></a>
2974
-
2975
- ## Appendix E — How Eyeling reaches 100% on `notation3tests`
2976
-
2977
- ### E.1 The goal
2978
-
2979
- Eyeling does not treat [notation3tests](https://codeberg.org/phochste/notation3tests/) as a side check.
2980
-
2981
- It treats the suite as an **external semantic contract**.
2982
-
2983
- That means:
2984
-
2985
- - the target is public
2986
- - the target is reproducible
2987
- - the target is outside the local codebase
2988
- - success means interoperability, not self-consistency
2989
-
2990
- ---
2991
-
2992
- ### E.2 The test loop
2993
-
2994
- The workflow is simple and strict:
2995
-
2996
- - clone the external [notation3tests](https://codeberg.org/phochste/notation3tests/) suite
2997
- - package the current Eyeling tree
2998
- - install that package into the suite
2999
- - run the suite’s Eyeling target
3000
- - fix semantics, not cosmetics
3001
-
3002
- This keeps the suite honest and keeps Eyeling honest.
3003
-
3004
- ---
3005
-
3006
- ### E.3 The prompt packet
3007
-
3008
- A typical conformance-fix prompt is not open-ended.
3009
-
3010
- It usually includes a small, repeatable packet:
3011
-
3012
- - the Eyeling source as an attached zip `https://github.com/eyereasoner/eyeling/archive/refs/heads/main.zip`
3013
- - pointers to the failing tests
3014
- - the exact failing output, or the exact command needed to reproduce it
3015
- - a pointer to the N3 spec `https://w3c.github.io/N3/spec/`
3016
- - a pointer to the builtin definitions `https://w3c.github.io/N3/spec/builtins.html`
3017
- - a direct request to fix the issue in the engine
3018
- - a direct request to update `HANDBOOK.md`
3019
-
3020
- The request is usually phrased in a narrow way:
3021
-
3022
- - fix this specific failing conformance case
3023
- - preserve existing passing behavior
3024
- - make the smallest coherent patch
3025
- - add or update a regression test if needed
3026
- - update the handbook so the semantic rule is documented, not just implemented
3027
- - do not stop at making the test green; align the implementation with the spec and explain the semantic reason in `HANDBOOK.md`
3028
-
3029
- The model is not asked to “improve the reasoner” in general.
3030
-
3031
- It is asked to repair one semantic gap against: the code, the failing test, the spec, and the handbook.
3032
-
3033
- ---
3034
-
3035
- ### E.4 The core idea
3036
-
3037
- Eyeling reaches 100% by making the engine match the semantics that the suite exercises.
3038
-
3039
- That means getting these right:
3040
-
3041
- - N3 syntax
3042
- - rule forms
3043
- - quoted formulas
3044
- - variable and blank-node behavior
3045
- - builtin relations
3046
- - closure and duplicate control
3047
-
3048
- The result is not “test gaming.”
3049
-
3050
- The result is semantic alignment.
3051
-
3052
- ---
3053
-
3054
- ### E.5 One rule core, many surfaces
3055
-
3056
- The suite uses different surface forms for the same logical ideas.
3057
-
3058
- Eyeling accepts and normalizes them into one internal rule model:
3059
-
3060
- - `{ P } => { C } .`
3061
- - `{ H } <= { B } .`
3062
- - top-level `log:implies`
3063
- - top-level `log:impliedBy`
3064
-
3065
- That matters because conformance depends on recognizing equivalence across syntax, not just parsing one preferred style.
3066
-
3067
- ---
3068
-
3069
- ### E.6 Normalize first, reason second
3070
-
3071
- A large share of conformance work happens **before** execution.
3072
-
3073
- Eyeling normalizes the tricky parts early:
3074
-
3075
- - body blanks become variables
3076
- - head blanks stay existential
3077
- - RDF collection encodings become list terms
3078
- - rule syntax variants become one rule representation
3079
-
3080
- This removes ambiguity before the engine starts proving anything.
3081
-
3082
- ---
3083
-
3084
- ### E.7 Body blanks vs. head blanks
3085
-
3086
- This is one of the decisive details.
3087
-
3088
- In Eyeling:
3089
-
3090
- - blanks in rule bodies act like placeholders
3091
- - blanks in rule heads act like fresh existentials
3092
-
3093
- That split is essential.
3094
-
3095
- Without it:
3096
-
3097
- - rule matching goes wrong
3098
- - proofs become unstable
3099
- - existential output becomes noisy
3100
- - conformance drops
3101
-
3102
- ---
3103
-
3104
- ### E.8 Builtins must behave like relations
3105
-
3106
- Eyeling does not treat builtins as one-way helper functions.
3107
-
3108
- It treats them as **relations inside proof search**.
3109
-
3110
- That means a builtin can:
3111
-
3112
- - succeed
3113
- - fail
3114
- - bind variables
3115
- - stay satisfiable without yet binding anything
3116
-
3117
- This is critical for the suite, because many builtin cases are really tests of search behavior, not just value computation.
3118
-
3119
- ---
3120
-
3121
- ### E.9 Delay builtins when needed
3122
-
3123
- Some builtins only become useful after neighboring goals bind enough variables.
3124
-
3125
- Eyeling handles that by deferring non-informative builtins inside conjunctions.
3126
-
3127
- So instead of failing too early, the engine:
3128
-
3129
- - rotates the builtin later
3130
- - keeps proving the remaining goals
3131
- - retries once more information exists
3132
-
3133
- This preserves logical behavior while staying operationally efficient.
3134
-
3135
- ---
3136
-
3137
- ### E.10 Formulas are first-class terms
3138
-
3139
- Quoted formulas are not treated as strings.
3140
-
3141
- They are treated as structured logical objects.
3142
-
3143
- That gives Eyeling the machinery it needs for:
3144
-
3145
- - formula matching
3146
- - nested reasoning
3147
- - `log:includes`
3148
- - `log:conclusion`
3149
- - formula comparison by alpha-equivalence
3150
-
3151
- This is a major reason the higher-level N3 tests pass cleanly.
3152
-
3153
- ---
3154
-
3155
- ### E.11 Alpha-equivalence matters
3156
-
3157
- Two formulas that differ only in internal names must still count as the same formula when their structure matches.
3158
-
3159
- Eyeling therefore compares formulas by structure, not by accidental naming.
3160
-
3161
- That removes a common source of false mismatches in:
3162
-
3163
- - quoted formulas
3164
- - nested graphs
3165
- - rule introspection
3166
- - scoped reasoning
3167
-
3168
- ---
3169
-
3170
- ### E.12 Lists must have one meaning
3171
-
3172
- The suite exercises list behavior in more than one spelling.
3173
-
3174
- Eyeling unifies them:
3175
-
3176
- - concrete N3 lists
3177
- - RDF `first/rest` collection encodings
3178
-
3179
- By materializing anonymous RDF collections into list terms, Eyeling gives both forms one semantic path through the engine.
3180
-
3181
- That keeps list reasoning consistent across the whole suite.
3182
-
3183
- ---
3184
-
3185
- ### E.13 Existentials must be stable
3186
-
3187
- A rule head with blanks must not generate endless fresh variants of the same logical result.
3188
-
3189
- Eyeling stabilizes this by skolemizing head blanks per firing instance.
3190
-
3191
- So one logical firing yields:
3192
-
3193
- - one stable witness
3194
- - one stable derived shape
3195
- - one meaningful duplicate check
3196
-
3197
- This is what lets closure reach a real fixpoint.
3198
-
3199
- ---
3200
-
3201
- ### E.14 Duplicate suppression is semantic, not cosmetic
3202
-
3203
- The engine does not merely try to avoid repeated printing.
3204
-
3205
- It tries to avoid repeated derivation of the same fact.
3206
-
3207
- That requires:
3208
-
3209
- - stable term ids
3210
- - indexed fact storage
3211
- - reliable duplicate keys
3212
- - stable existential handling
3213
-
3214
- Without that, a reasoner can look busy forever and still fail conformance.
3215
-
3216
- ---
3217
-
3218
- ### E.15 Closure must really close
3219
-
3220
- Full conformance depends on real saturation behavior.
3221
-
3222
- Eyeling therefore treats closure as:
3223
-
3224
- - repeated rule firing
3225
- - repeated proof over indexed facts
3226
- - duplicate-aware insertion
3227
- - termination at fixpoint
3228
-
3229
- This is what turns the engine from a parser plus demos into a conformance-grade reasoner.
3230
-
3231
- ---
3232
-
3233
- ### E.16 Performance choices support correctness
3234
-
3235
- Several implementation choices are operational, but they directly protect conformance:
3236
-
3237
- - predicate-based indexing
3238
- - subject/object refinement
3239
- - smallest-bucket candidate selection
3240
- - fast duplicate keys
3241
- - skipping already-known ground heads
3242
-
3243
- These choices reduce accidental nontermination and prevent operational noise from becoming semantic failure.
3244
-
3245
- ---
3246
-
3247
- ### E.17 The suite stays external
3248
-
3249
- This is a key discipline.
3250
-
3251
- Eyeling does not define success by a private in-repo imitation of [notation3tests](https://codeberg.org/phochste/notation3tests/).
3252
-
3253
- It runs against the external suite.
3254
-
3255
- That means:
3256
-
3257
- - the compliance test suite is shared
3258
- - the contract is public
3259
- - the result is independently meaningful
3260
-
3261
- A green run says something real.
3262
-
3263
- ---
3264
-
3265
- ### E.18 Every failure becomes an invariant
3266
-
3267
- Eyeling reaches 100% because failures are not patched superficially.
3268
-
3269
- Each failure is turned into an engine rule.
3270
-
3271
- Examples:
3272
-
3273
- - parser failure → broader syntax support
3274
- - list failure → one unified list model
3275
- - formula failure → alpha-equivalence discipline
3276
- - builtin failure → relational evaluation
3277
- - closure failure → stable existential handling
3278
-
3279
- That is how the suite shapes the engine.
3280
-
3281
- ---
3282
-
3283
- ### E.19 Why 100% happens
3284
-
3285
- Eyeling gets to 100% because all the key layers line up:
3286
-
3287
- - the parser accepts the full rule surface
3288
- - normalization removes semantic ambiguity
3289
- - formulas are real terms
3290
- - builtins participate in proof search
3291
- - existential output is stable
3292
- - closure reaches a true fixpoint
3293
- - the public suite remains the judge
3294
-
3295
- Once those pieces are in place, 100% is the visible result of a coherent design.
3296
-
3297
- ---
3298
-
3299
- ### E.20 Final takeaway
3300
-
3301
- Eyeling reaches full [notation3tests](https://codeberg.org/phochste/notation3tests/) conformance by making “pass the suite” and “implement N3 correctly enough to interoperate” the same task.
3302
-
3303
- That is the method:
3304
-
3305
- - external suite
3306
- - one semantic core
3307
- - early normalization
3308
- - relational builtins
3309
- - formula-aware reasoning
3310
- - stable existential output
3311
- - duplicate-safe fixpoint closure
3312
-
3313
- That is why the result is 100%.
3314
-
3315
- ---
3316
-
3317
- <a id="app-f"></a>
3318
-
3319
- ## Appendix F — The ARC approach: Answer • Reason Why • Check
3320
-
3321
- A simple way to write a good Eyeling program is to make it do three things in one file:
3322
-
3323
- > give the answer, say why, and check that it really holds.
3324
-
3325
- That is the ARC approach: **Answer • Reason Why • Check**.
3326
-
3327
- The idea is not to make the program more grand or formal. It is to make it more useful. A bare result is often not enough. A reader also wants to see the small reason that matters, and to know that the program will fail loudly if an important assumption is wrong.
3328
-
3329
- In Eyeling this style comes quite naturally. Facts hold the data. Rules derive the conclusion. `log:outputString` can turn the conclusion into readable output. And a rule that concludes `false` acts as a fuse: if a bad condition becomes provable, the run stops instead of quietly producing a misleading result.
3330
-
3331
- ### F.1 What the three parts mean
3332
-
3333
- The **Answer** is the direct result. It should be short and easy to recognize. In many Eyeling files it is a final recommendation, a route, a computed value, a decision such as `allowed` or `blocked`, or a small report line emitted with `log:outputString`.
3334
-
3335
- The **Reason Why** is the compact explanation. It is not hidden chain-of-thought and it does not need to be long. Usually it is just the witness, threshold, policy, path, or intermediate fact that made the answer follow. A good reason tells the reader what mattered.
3336
-
3337
- The **Check** is the part that keeps the program honest. It should do more than repeat the answer in different words. A good check tests something that could really fail: a structural invariant, a recomputed quantity, a boundary condition, or a rule that derives `false` when the answer would be inconsistent with the inputs.
3338
-
3339
- A short way to remember ARC is this:
3340
-
3341
- > an answer tells you **what** happened, a reason tells you **why**, and a check tells you **whether you should trust it**.
3342
-
3343
- ### F.2 Why this fits Eyeling well
3344
-
3345
- ARC is not an extra subsystem in Eyeling. It is mostly a good habit.
3346
-
3347
- Eyeling already separates data from logic. It already lets you derive readable output instead of printing ad hoc text during proof search. And it already has a very strong notion of validation through inference fuses. So ARC is really just a clean way to organize an ordinary Eyeling file so that a human reader can see the result, the explanation, and the safety net together.
3348
-
3349
- This is especially useful for examples. A newcomer can run the file and see what it does. A maintainer can inspect the few rules that justify the result. And an external developer can tell whether the example merely prints something nice or actually checks itself.
3350
-
3351
- ### F.3 A simple pattern to follow
3352
-
3353
- A practical ARC-style Eyeling file often has four visible layers.
3354
-
3355
- First come the **facts**: the input data, parameters, thresholds, policies, or known relationships. Then comes the **logic**: the rules that derive the internal conclusion. Then comes the **presentation**: rules that turn the result into `log:outputString` lines or other report facts. Finally come the **checks**: rules that validate the result or trigger `false` when an invariant is broken.
3356
-
3357
- You do not have to separate these layers perfectly, but it helps a lot when the file reads in roughly that order.
3358
-
3359
- ### F.4 A tiny template
3360
-
3361
- ```n3
3362
- @prefix : <http://example.org/> .
3363
- @prefix log: <http://www.w3.org/2000/10/swap/log#> .
3364
- @prefix math: <http://www.w3.org/2000/10/swap/math#> .
3365
-
3366
- # Facts
3367
- :case :input 42 .
3368
-
3369
- # Logic
3370
- { :case :input ?n . ?n math:greaterThan 10 . }
3371
- => { :case :decision "allowed" . } .
3372
-
3373
- # Answer
3374
- { :case :decision ?d . }
3375
- => { :answer log:outputString "Answer\n" .
3376
- :answer log:outputString ?d . } .
3377
-
3378
- # Reason Why
3379
- { :case :input ?n . :case :decision ?d . }
3380
- => { :why log:outputString "\nReason Why\n" .
3381
- :why log:outputString "Input satisfied the rule threshold.\n" . } .
3382
-
3383
- # Check
3384
- { :case :decision "allowed" .
3385
- :case :input ?n .
3386
- ?n math:notGreaterThan 10 . }
3387
- => false .
3388
- ```
3389
-
3390
- The exact wording can vary. The important thing is the shape: derive the result, make the key reason visible, and include at least one check that could fail for a real reason.
3391
-
3392
- ### F.5 What a good check looks like
3393
-
3394
- A good check is not a decorative `:ok true` line. It should add real confidence.
3395
-
3396
- Sometimes that means recomputing a quantity from another angle. Sometimes it means checking a witness path instead of only the summary result. Sometimes it means making sure a threshold really was crossed, or that a list or graph has the shape the rest of the program assumes. And sometimes the right check is simply an inference fuse that says: if this contradiction appears, stop.
3397
-
3398
- The point is not to make checks large. The point is to make them real.
3399
-
3400
- ### F.6 ARC and the Insight Economy
3401
-
3402
- One reason ARC matters beyond pedagogy is that it matches a broader way of thinking about data and computation that Ruben Verborgh has called the **Insight Economy**.
3403
-
3404
- The basic claim is simple: raw data is usually the wrong thing to exchange directly. A better system refines source data into a **specific, purpose-limited, time-bound insight** that is useful in one context, loses much of its value when copied without that context, and can be governed more safely than an unrestricted dump of the original data.
3405
-
3406
- That fits Eyeling remarkably well. Eyeling can derive narrow conclusions explicitly, show why they follow, and attach checks that make sure a decision still respects policy, scope, thresholds, or consistency constraints. In other words, an Eyeling program can act as a small governed insight refinery rather than as a black box that merely emits a verdict.
3407
-
3408
- This is also why ARC is a good mental model here. The **Answer** is the bounded insight. The **Reason Why** makes the governing basis visible. The **Check** ensures the result can be trusted for the stated purpose instead of silently drifting beyond it.
3409
-
3410
- ### F.7 ARC-style examples in `examples/`
3411
-
3412
- The following examples are especially useful if you want to see Eyeling files that derive an answer, expose the key reason, and include meaningful checks. Each entry links to both the source example and the corresponding generated output file in `examples/output/`.
3413
-
3414
- #### Insight Economy and governed-data cases
3415
-
3416
- - [`examples/auroracare.n3`](examples/auroracare.n3) · [`examples/output/auroracare.md`](examples/output/auroracare.md) — purpose-based medical data exchange with explicit allow/deny reasoning and checks around role, purpose, and conditions.
3417
- - [`examples/calidor.n3`](examples/calidor.n3) · [`examples/output/calidor.md`](examples/output/calidor.md) — heatwave-response case where private household signals become a narrow, expiring cooling-support insight.
3418
- - [`examples/delfour.n3`](examples/delfour.n3) · [`examples/output/delfour.md`](examples/output/delfour.md) — shopping-assistance case where a private condition becomes a bounded “prefer lower-sugar products” insight.
3419
- - [`examples/flandor.n3`](examples/flandor.n3) · [`examples/output/flandor.md`](examples/output/flandor.md) — macro-economic coordination case for Flanders that turns sensitive local signals into a regional retooling insight.
3420
- - [`examples/medior.n3`](examples/medior.n3) · [`examples/output/medior.md`](examples/output/medior.md) — post-discharge care-coordination case that derives a minimal continuity-bundle insight without sharing the full record.
3421
- - [`examples/parcellocker.n3`](examples/parcellocker.n3) · [`examples/output/parcellocker.md`](examples/output/parcellocker.md) — one-time parcel pickup authorization with a clear permit decision, justification, and misuse checks.
3422
- - [`examples/harborsmr.n3`](examples/harborsmr.n3) · [`examples/output/harborsmr.md`](examples/output/harborsmr.md) — SMR flexibility case where private plant telemetry becomes a narrow, expiring electrolysis-dispatch insight with policy and safety checks.
3423
-
3424
- - [`examples/transistor-switch.n3`](examples/transistor-switch.n3) · [`examples/output/transistor-switch.md`](examples/output/transistor-switch.md) — NPN low-side switch model with exact arithmetic and cutoff-versus-saturation checks.
3425
-
3426
- #### Core ARC-style walkthroughs
3427
-
3428
- - [`examples/bmi.n3`](examples/bmi.n3) · [`examples/output/bmi.md`](examples/output/bmi.md) — Body Mass Index calculation with normalization, WHO category assignment, and boundary checks.
3429
- - [`examples/control-system.n3`](examples/control-system.n3) · [`examples/output/control-system.md`](examples/output/control-system.md) — small control-system example that derives actuator commands and explains feedforward and feedback contributions.
3430
- - [`examples/easter.n3`](examples/easter.n3) · [`examples/output/easter.md`](examples/output/easter.md) — Gregorian Easter computus with a readable explanation and date-window checks.
3431
- - [`examples/french-cities.n3`](examples/french-cities.n3) · [`examples/output/french-cities.md`](examples/output/french-cities.md) — graph reachability over French cities with explicit path reasoning.
3432
- - [`examples/gps.n3`](examples/gps.n3) · [`examples/output/gps.md`](examples/output/gps.md) — tiny route-planning example for western Belgium with route comparison and metric checks.
3433
- - [`examples/resto.n3`](examples/resto.n3) · [`examples/output/resto.md`](examples/output/resto.md) — RESTdesc-style service composition from person and date to a concrete restaurant reservation.
3434
- - [`examples/sudoku.n3`](examples/sudoku.n3) · [`examples/output/sudoku.md`](examples/output/sudoku.md) — Sudoku solver and report generator with consistency checks over the solved grid.
3435
- - [`examples/wind-turbine.n3`](examples/wind-turbine.n3) · [`examples/output/wind-turbine.md`](examples/output/wind-turbine.md) — predictive-maintenance example that turns sensor readings into an auditable inspection decision.
3436
-
3437
- #### Technical and scientific ARC demos
3438
-
3439
- - [`examples/fundamental-theorem-arithmetic.n3`](examples/fundamental-theorem-arithmetic.n3) · [`examples/output/fundamental-theorem-arithmetic.md`](examples/output/fundamental-theorem-arithmetic.md) — smallest-divisor prime factorization of 202692987 with ARC-style existence, uniqueness-up-to-order, and primality checks.
3440
- - [`examples/complex-matrix-stability.n3`](examples/complex-matrix-stability.n3) · [`examples/output/complex-matrix-stability.md`](examples/output/complex-matrix-stability.md) — discrete-time stability classification for three diagonal complex 2×2 matrices via spectral radius and ARC-style checks.
3441
- - [`examples/matrix-mechanics.n3`](examples/matrix-mechanics.n3) · [`examples/output/matrix-mechanics.md`](examples/output/matrix-mechanics.md) — small 2×2 matrix example deriving trace, determinant, products, and a non-zero commutator.
3442
- - [`examples/pn-junction-tunneling.n3`](examples/pn-junction-tunneling.n3) · [`examples/output/pn-junction-tunneling.md`](examples/output/pn-junction-tunneling.md) — semiconductor toy model that explains current-proxy behavior across bias points.
3443
- - [`examples/transistor-switch.n3`](examples/transistor-switch.n3) · [`examples/output/transistor-switch.md`](examples/output/transistor-switch.md) — NPN low-side switch model with exact arithmetic and cutoff-versus-saturation checks.
3444
-
3445
- #### Applied Constructor-Theory ARC examples
3446
-
3447
- - [`examples/act-alarm-bit-interoperability.n3`](examples/act-alarm-bit-interoperability.n3) · [`examples/output/act-alarm-bit-interoperability.md`](examples/output/act-alarm-bit-interoperability.md) — applied constructor-theory information example showing interoperability of an alarm bit across unlike media together with a no-cloning contrast for a quantum token.
3448
- - [`examples/act-docking-abort.n3`](examples/act-docking-abort.n3) · [`examples/output/act-docking-abort.md`](examples/output/act-docking-abort.md) — applied constructor-theory ARC case for a spacecraft docking-abort token covering permutation, copying, measurement, serial and parallel composition, and the impossibility of cloning a quantum seal.
3449
- - [`examples/act-isolation-breach.n3`](examples/act-isolation-breach.n3) · [`examples/output/act-isolation-breach.md`](examples/output/act-isolation-breach.md) — applied constructor-theory ARC case for a biosafety isolation-breach token covering preparation, distinguishability, reversible permutation, copying, measurement, composition, and no-cloning.
3450
- - [`examples/act-gravity-mediator-witness.n3`](examples/act-gravity-mediator-witness.n3) · [`examples/output/act-gravity-mediator-witness.md`](examples/output/act-gravity-mediator-witness.md) — applied constructor-theory witness showing that, under locality and interoperability, entanglement mediated only by gravity implies a non-classical gravitational mediator.
3451
- - ['examples/act-yeast-self-reproduction.n3'](examples/act-yeast-self-reproduction.n3) · ['examples/output/act-yeast-self-reproduction.md'](examples/output/act-yeast-self-reproduction.md) — applied constructor-theory example of a yeast starter culture showing replicator, vehicle, self-reproduction, heritable variation, and natural selection under no-design laws.
3452
- - ['examples/act-barley-seed-lineage.n3'](examples/act-barley-seed-lineage.n3) · ['examples/output/act-barley-seed-lineage.md'](examples/output/act-barley-seed-lineage.md) — applied constructor-theory ARC case showing both possible and impossible lineage tasks under no-design laws, including blocked reproduction, dormancy, and evolvability when key ingredients are missing.
3453
- - ['examples/act-tunnel-junction-wake-switch.n3'](examples/act-tunnel-junction-wake-switch.n3) · ['examples/output/act-tunnel-junction-wake-switch.md'](examples/output/act-tunnel-junction-wake-switch.md) — applied constructor-theory ARC case comparing a tunnel-junction wake switch with a conventional PN junction via explicit can/cannot rules for tunneling, sub-threshold current, negative differential response, and low-bias switching.
3454
- - ['examples/act-photosynthetic-exciton-transfer.n3'](examples/act-photosynthetic-exciton-transfer.n3) · ['examples/output/act-photosynthetic-exciton-transfer.md'](examples/output/act-photosynthetic-exciton-transfer.md) — applied constructor-theory ARC case for quantum-assisted exciton transfer in a photosynthetic antenna, contrasting a tuned complex with a detuned one via explicit can/cannot rules.
3455
- - ['examples/act-sensor-memory-reset.n3'](examples/act-sensor-memory-reset.n3) · ['examples/output/act-sensor-memory-reset.md'](examples/output/act-sensor-memory-reset.md) — applied constructor-theory ARC case showing that a sensor memory reset is possible with a work medium but not with heat alone, highlighting work/heat distinction and irreversibility.
3456
-
3457
- #### Deep-classification stress tests
3458
-
3459
- - [`examples/deep-taxonomy-10.n3`](examples/deep-taxonomy-10.n3) · [`examples/output/deep-taxonomy-10.md`](examples/output/deep-taxonomy-10.md) — ARC-style deep-taxonomy benchmark at depth 10.
3460
- - [`examples/deep-taxonomy-100.n3`](examples/deep-taxonomy-100.n3) · [`examples/output/deep-taxonomy-100.md`](examples/output/deep-taxonomy-100.md) — ARC-style deep-taxonomy benchmark at depth 100.
3461
- - [`examples/deep-taxonomy-1000.n3`](examples/deep-taxonomy-1000.n3) · [`examples/output/deep-taxonomy-1000.md`](examples/output/deep-taxonomy-1000.md) — ARC-style deep-taxonomy benchmark at depth 1000.
3462
- - [`examples/deep-taxonomy-10000.n3`](examples/deep-taxonomy-10000.n3) · [`examples/output/deep-taxonomy-10000.md`](examples/output/deep-taxonomy-10000.md) — ARC-style deep-taxonomy benchmark at depth 10000.
3463
- - [`examples/deep-taxonomy-100000.n3`](examples/deep-taxonomy-100000.n3) · [`examples/output/deep-taxonomy-100000.md`](examples/output/deep-taxonomy-100000.md) — ARC-style deep-taxonomy benchmark at depth 100000.
3464
-
3465
- These files fit together because they all present reasoning in a recognizably ARC-like way: they derive an answer, make the reason visible in a compact report, and include checks that are meant to catch real mistakes. Some are classical logic or numeric examples; others show how Eyeling can express policy-aware, insight-oriented decision flows without collapsing everything into opaque application code.
3466
-
3467
- ### F.8 How to read an ARC-style example
3468
-
3469
- A good way to read one of these files is to start with the question in the comments or input facts. Then find the part that gives the answer. Then trace the few rules that explain why that answer follows. Finally, look for the checks: the validation facts, the recomputation, or the `=> false` fuse that would stop the run if something important were wrong.
3470
-
3471
- That reading order keeps the example grounded in observable behavior rather than in source code alone.
3472
-
3473
- ### F.9 What ARC is not
3474
-
3475
- ARC does not mean wrapping every file in ceremony. It does not mean long prose explanations. It does not mean hiding important assumptions in comments while the executable part stays thin. And it does not mean replacing checks with a confident tone.
3476
-
3477
- A file really follows ARC only when the answer, the explanation, and the validation all live in the program itself.
3478
-
3479
- ### F.10 Why this style is worth using
3480
-
3481
- This style is worth using because it makes an Eyeling file easier to run, easier to inspect, and easier to trust. The result is visible. The key reason is visible. The check is visible. That makes examples better teaching material, makes policy or computation examples easier to audit, and makes the whole file more reusable as a small reasoning artifact instead of an opaque session transcript.
3482
-
3483
- <a id="app-g"></a>
3484
-
3485
- ## Appendix G — Eyeling and the W3C CG Notation3 Semantics
3486
-
3487
- The purpose of this appendix is to say where Eyeling tracks the [W3C CG Notation3 semantics](https://w3c.github.io/N3/spec/semantics) closely, and where Eyeling makes deliberate operational choices of its own.
3488
-
3489
- The comparison point here is the W3C CG Notation3 semantics document, not a claim that Eyeling is trying to be a line-by-line implementation of that document. Eyeling is a working reasoner, so some choices are shaped by execution, indexing, determinism, and the practical habits of N3 authors.
3490
-
3491
- ### G.1 Where Eyeling is strongly aligned
3492
-
3493
- - **Core term model (IRIs, literals, variables, blank nodes, lists, quoted formulas):** The semantics document treats N3 terms as IRIs, literals, variables, lists, and graph terms. Eyeling’s internal model matches that shape directly through `Iri`, `Literal`, `Var`, `Blank`, `ListTerm`, and `GraphTerm`.
3494
-
3495
- - **Quoted formulas need alpha-equivalence / isomorphism:** The semantics document defines isomorphism for graphs and graph terms using consistent renaming. Eyeling implements the same practical idea operationally as alpha-equivalence for `GraphTerm`, with consistent renaming as the criterion for a match.
3496
-
3497
- - **Rules as implication (and `true` as empty formula):** The semantics document gives a special role to `log:implies` and treats `true` and `false` specially, with `true` corresponding to the empty formula. Eyeling follows that shape: it accepts both `{ P } => { C }` and `{ P } log:implies { C }`, and it treats `true` as `{}`.
3498
-
3499
- - **Lists as first-class citizens (not just RDF collections):** The semantics document treats lists as genuine N3 terms. Eyeling does the same through `ListTerm`, and also materializes RDF `rdf:first` / `rdf:rest` chains into list terms so one list model can be used throughout the engine.
3500
-
3501
- ### G.2 Where Eyeling diverges or goes beyond the semantics document
3502
-
3503
- #### G.2.1 Blank nodes in rule bodies: Eyeling chooses common N3 rule-writing practice
3504
-
3505
- The semantics document describes blank nodes as existentially quantified with local scope. Eyeling intentionally rewrites blank nodes in **rule premises** into variables during normalization. In practice this makes body blanks behave like the placeholders many N3 authors expect when they write rules.
3506
-
3507
- That is a real semantic choice. It is useful and intentional, but it is not the same as reading blank nodes as existentials everywhere.
3508
-
3509
- #### G.2.2 Groundness of quoted formulas containing variables
3510
-
3511
- In the semantics document, whether a graph term is ground depends on whether the underlying graph is closed, and nested formulas can still contain free variables when viewed in isolation. Eyeling makes a pragmatic engine choice: variables inside a `GraphTerm` do not make the surrounding triple non-ground. In the handbook this is summarized as “variables inside formulas do not leak.”
3512
-
3513
- That supports indexing, matching, and duplicate checks, but it is not a one-to-one restatement of model-theoretic groundness for graph terms.
3514
-
3515
- #### G.2.3 Eyeling defines operational behavior beyond what the semantics document currently fixes
3516
-
3517
- The semantics document mainly fixes meaning around implication and the core N3 term/formula model. Eyeling goes further and gives operational meaning to a large standard library of builtins and control features. Examples include `math:*`, `string:*`, `list:*`, `time:*`, `log:includes`, `log:notIncludes`, `log:query`, and scoped closure via `log:conclusion`.
3518
-
3519
- So Eyeling is not only implementing the semantics document; it is also defining engine behavior for features that the current document does not fully specify.
3520
-
3521
- #### G.2.4 Inference fuses (`=> false`) are an engine-level procedural feature
3522
-
3523
- The semantics document discusses `false` in relation to implication and constraints. Eyeling turns `{ ... } => false` into an engine-level hard failure with a visible message and exit status 65 (`EX_DATAERR`). That is a practical tooling feature: it lets a rule act like a checked invariant.
3524
-
3525
- This is very useful in real programs, but it is an operational behavior of the reasoner, not something a model-theoretic semantics “executes.”
3526
-
3527
- #### G.2.5 Surface-language coverage is not the same thing as semantic alignment
3528
-
3529
- The semantics document discusses explicit quantification in its abstract syntax. Eyeling mostly exposes implicit quantification through `?x` variables and blank nodes, together with the rule-normalization choices described earlier. The handbook documents the supported surface forms Eyeling actually parses, which may be narrower than the full abstract surface discussed in the semantics document.
3530
-
3531
- So even where the underlying ideas line up, the accepted concrete syntax may still be a proper subset.
3532
-
3533
- ### G.3 The practical takeaway
3534
-
3535
- A good short summary is this:
3536
-
3537
- - Eyeling is strongly aligned with the N3 semantics on the **core ontology of terms, quoted formulas, implication, and lists**.
3538
- - Eyeling makes deliberate, implementation-shaped choices around **rule-body blanks, groundness of quoted formulas, and constraint execution**.
3539
- - Eyeling also defines a wider operational language than the current semantics document, especially through builtins and scoped proof/query features.
3540
-
3541
- So the handbook and the semantics document are best read as complementary. The semantics document explains the abstract shape of Notation3. The handbook explains how a compact working reasoner realizes that shape, and where it chooses a practical execution model over a purely model-theoretic presentation.
3542
-
3543
- <a id="app-h"></a>
3544
-
3545
- ## Appendix H — Applied Constructor-Theory and the N3 ARC examples
3546
-
3547
- This appendix explains the idea behind the **Applied Constructor-Theory** examples collected in the `examples/act-*` files.
3548
-
3549
- The short version is:
3550
-
3551
- > Appendix F explains the **presentation style** of ARC.
3552
- > This appendix explains the **scientific style** of the ACT examples.
3553
-
3554
- In this handbook, **ACT** is used as a practical label for examples that take constructor-theoretic ideas and turn them into concrete, runnable N3 programs. The label is local to this handbook: it is a convenient way to group examples that are about constructor theory in action, not a claim that there is one official file format or one officially standardized subfield called “ACT”.
3555
-
3556
- ### H.1 What constructor theory is trying to do
3557
-
3558
- Constructor theory is a proposal for formulating physics in terms of **which transformations are possible, which are impossible, and why**, rather than only in terms of trajectories and initial conditions.
3559
-
3560
- That shift matters because many scientifically important statements already have that shape:
3561
-
3562
- - information can be copied from one medium to another
3563
- - an accurate self-reproducer can exist under no-design laws
3564
- - a work medium can reset a memory in a way that heat alone cannot
3565
- - a mediator that can entangle two quantum systems cannot be purely classical
3566
-
3567
- Those are not merely predictions of one trajectory. They are statements about a space of **allowed and forbidden tasks**. Constructor theory is designed to make such statements fundamental rather than secondary.
3568
-
3569
- ### H.2 Why this matters for applied examples
3570
-
3571
- The constructor-theory programme is often presented through applications and research themes rather than as a closed symbolic calculus. In practice, that makes it a good fit for example-driven reasoning in Eyeling.
3572
-
3573
- An Eyeling ACT example does not try to reproduce the full mathematical machinery of a physics paper. Instead, it extracts the **task structure** of the claim:
3574
-
3575
- - what is being attempted
3576
- - which resources or media are available
3577
- - which structural conditions make the task possible
3578
- - which missing conditions make the task impossible
3579
- - what small set of checks would make the conclusion auditable
3580
-
3581
- That is exactly the kind of thing N3 rules are good at expressing.
3582
-
3583
- ### H.3 Why N3 fits constructor-theoretic reasoning unusually well
3584
-
3585
- Notation3 is a good match for constructor-theoretic examples for four reasons.
3586
-
3587
- First, N3 rules are naturally relational. They can say:
3588
-
3589
- ```n3
3590
- { ?system :has ?property . } => { ?system :can ?task . } .
3591
- ```
3592
-
3593
- and just as naturally:
3594
-
3595
- ```n3
3596
- { ?system :lacks ?property . } => { ?system :cannot ?task . } .
3597
- ```
3598
-
3599
- That is already close to the “science of can and cannot” idiom.
3600
-
3601
- Second, N3 can keep the explanation close to the answer. The conditions, the derived `:can` / `:cannot` facts, and the final human-readable report can all live in one file.
3602
-
3603
- Third, Eyeling supports `log:outputString`, so the result can be rendered as a compact ARC report rather than as a raw closure dump.
3604
-
3605
- Fourth, Eyeling supports rule-based checks and hard fuses (`=> false`), so the example can state not only the claim but also what would count as a contradiction of the claim.
3606
-
3607
- That combination makes N3 a strong medium for **pedagogical applied constructor theory**: it is executable, inspectable, and naturally counterfactual.
3608
-
3609
- ### H.4 What these ACT examples are — and what they are not
3610
-
3611
- These examples are **not** microscopic simulations.
3612
-
3613
- They do not solve Schrödinger equations, semiconductor transport equations, or full biochemical kinetics. They are closer to **task-logic models**. They capture the counterfactual structure of a scientific claim:
3614
-
3615
- - if these conditions hold, then this task is possible
3616
- - if these conditions are absent, then that task is impossible
3617
- - if the task is possible, what larger conclusion follows
3618
- - if the task is impossible, what stronger claim is ruled out
3619
-
3620
- That is why an ACT example often looks more like a carefully structured scientific argument than like a numerical simulator.
3621
-
3622
- This is a feature, not a bug. The point is to model the **explanatory logic** of the claim in constructor-theoretic form.
3623
-
3624
- ### H.5 The recurring shape of an ACT file in Eyeling
3625
-
3626
- Most of the ACT files in this repository follow the same skeleton.
3627
-
3628
- #### H.5.1 A concrete scenario
3629
-
3630
- Each file starts with a scenario that is tangible enough to picture:
3631
-
3632
- - an alarm bit crossing unlike media
3633
- - a docking abort token
3634
- - a biosafety isolation-breach signal
3635
- - a gravitational mediator witness
3636
- - a yeast or barley lineage
3637
- - a tunnel-junction wake switch
3638
- - a photosynthetic transfer complex
3639
- - a sensor memory that must be reset
3640
-
3641
- The point of the scenario is to stop constructor theory from floating away into abstract slogans.
3642
-
3643
- #### H.5.2 Positive rules: what the system can do
3644
-
3645
- The positive rules derive facts such as:
3646
-
3647
- - `:can :Copy`
3648
- - `:can :Measure`
3649
- - `:can :AccurateSelfReproduction`
3650
- - `:can :EfficientExcitonTransfer`
3651
- - `:can :ReliableResetFromWork`
3652
-
3653
- These are the constructor-theoretic heart of the file. They say which tasks become possible when the right structural conditions are present.
3654
-
3655
- #### H.5.3 Negative rules: what the system cannot do
3656
-
3657
- The negative rules derive facts such as:
3658
-
3659
- - `:cannot :CloneAllStates`
3660
- - `:cannot :AccurateSelfReproduction`
3661
- - `:cannot :AdaptivePersistence`
3662
- - `:cannot :ServeLeakAlarmWakeCircuit`
3663
- - `:cannot :ReadyForReuseFromHeatAlone`
3664
-
3665
- These rules matter just as much as the positive ones. A constructor-theoretic explanation is incomplete if it says only what works and never says what is ruled out.
3666
-
3667
- In practice, the negative rules often provide the sharpest insight in the file.
3668
-
3669
- #### H.5.4 An ARC report
3670
-
3671
- The final rule usually emits a `log:outputString` report with three parts:
3672
-
3673
- - **Answer**
3674
- - **Reason Why**
3675
- - **Check**
3676
-
3677
- That is the Appendix F layer. ARC gives the file a readable surface. Constructor theory gives it the inner scientific logic.
3678
-
3679
- #### H.5.5 Comments that explain the scientific role of each rule block
3680
-
3681
- The better ACT examples are heavily commented. The comments should say not just what the syntax is doing, but what scientific role the block plays:
3682
-
3683
- #### H.5.6 Editorial conventions for ACT files
3684
-
3685
- For this repository, the ACT examples should stay visibly **Eyeling-native**. They should read as compact N3 task-logic models rather than as a second language layer.
3686
-
3687
- A good default order is:
3688
-
3689
- 1. scenario facts;
3690
- 2. positive `:can` rules;
3691
- 3. negative `:cannot` rules;
3692
- 4. checks;
3693
- 5. the final ARC report.
3694
-
3695
- The ARC report should make the decisive contrast explicit: what task is possible, what task is impossible, and which missing ingredient or witness explains the contrast.
3696
-
3697
- - interoperability
3698
- - locality
3699
- - no-cloning
3700
- - replicator–vehicle logic
3701
- - work versus heat
3702
- - irreversibility
3703
- - short-lived quantum assistance
3704
- - blocked lineage closure
3705
-
3706
- That is important because these examples are meant to teach a way of thinking, not only to demonstrate parser coverage.
3707
-
3708
- ### H.6 The main constructor-theory themes represented in the examples
3709
-
3710
- The current ACT examples are listed in Appendix F’s example catalog. This appendix is the conceptual companion to that list.
3711
-
3712
- Here are the main themes those files illustrate.
3713
-
3714
- #### H.6.1 Information as a task-level notion
3715
-
3716
- The alarm-bit, docking-abort, and isolation-breach examples treat information as something that can be copied, permuted, measured, and moved between unlike media.
3717
-
3718
- #### H.6.2 Life as accurate self-reproduction under no-design laws
3719
-
3720
- The yeast and barley files follow the constructor-theory-of-life pattern: replication, self-reproduction, and natural selection are treated as tasks that can be possible under no-design laws when the right structural conditions are present.
3721
-
3722
- These examples are especially good for N3 because the logic is already rule-shaped:
3723
-
3724
- - digital heredity enables accurate copying
3725
- - vehicle structure enables construction and repair
3726
- - variation plus selection enables adaptive persistence
3727
- - missing ingredients block those tasks
3728
-
3729
- #### H.6.3 Thermodynamics as possible and impossible tasks
3730
-
3731
- The sensor-memory-reset example is a compact way to express constructor-theoretic thermodynamics: a work-like resource can drive a reliable reset task that heat alone cannot, and an irreversible degradation path need not have the exact reverse available.
3732
-
3733
- #### H.6.4 Non-classicality witnesses in hybrid systems
3734
-
3735
- The gravity-mediator example shows how a constructor-theoretic application can be expressed as a chain of constraints: if locality and interoperability hold, and a mediator can entangle two quantum systems, then that mediator cannot be purely classical.
3736
-
3737
- That kind of claim is perfect for N3 because it is already naturally expressed as a chain of conditions and consequences rather than as a trajectory simulation.
3738
-
3739
- #### H.6.5 Quantum effects in practical settings
3740
-
3741
- The tunnel-junction and photosynthetic-transfer files show how ACT examples can model quantum effects without pretending to be full microscopic calculations. They capture the counterfactual claim that certain structural conditions make a task possible, while contrast conditions block it.
3742
-
3743
- This is often the right level of abstraction for a reasoning example: detailed enough to be about a real scientific idea, but explicit enough to stay executable and inspectable.
3744
-
3745
- ### H.7 How to read an ACT example well
3746
-
3747
- A good reading order is:
3748
-
3749
- 1. identify the concrete application scenario
3750
- 2. identify the `:can` facts the file is trying to establish
3751
- 3. identify the `:cannot` facts that provide the contrast
3752
- 4. read the final ARC report
3753
- 5. go back and inspect the rule blocks that justify that report
3754
- 6. check whether the file includes explicit validation or a fuse
3755
-
3756
- That order preserves the scientific meaning of the example. You first see the task. Then you see the allowed and forbidden transformations. Only then do you look at the syntax in detail.
3757
-
3758
- ### H.8 What makes a strong ACT example in this repository
3759
-
3760
- A strong ACT example in Eyeling usually has five traits.
3761
-
3762
- It is **concrete**. The reader can picture the system.
3763
-
3764
- It is **counterfactual**. The file derives both a meaningful `:can` and a meaningful `:cannot`.
3765
-
3766
- It is **commented at the scientific level**. The comments explain principles, not just syntax.
3767
-
3768
- It is **ARC-shaped**. The answer, reason, and checks are visible.
3769
-
3770
- And it is **honest about scope**. It does not pretend to be a full physical simulation when it is really a task-logic model.
3771
-
3772
- ### H.9 Why keep these examples in the handbook at all
3773
-
3774
- Because constructor theory can otherwise seem either too abstract or too grand.
3775
-
3776
- The ACT examples solve that by making the ideas runnable. They let a reader see, in a small executable artifact, how a principle about possible and impossible tasks can be turned into explicit rules, explicit contrasts, and explicit checks.
3777
-
3778
- That is valuable even for readers who do not plan to work on constructor theory itself. It shows a wider lesson:
3779
-
3780
- > some scientific explanations are best understood not as “what happened once,” but as “what could be made to happen, what could not, and what structural features make the difference.”
3781
-
3782
- That is exactly the sort of explanation that N3, and Eyeling in particular, can make unusually clear.
3783
-
3784
- <a id="app-i"></a>
3785
-
3786
- ## Appendix I — The Eyeling Playground
3787
-
3788
- The **Eyeling Playground** is the browser-based front end for experimenting with Eyeling without a local install or command-line workflow. It is meant for teaching, quick debugging, live demos, and shareable reasoning examples. Rather than treating reasoning as an offline batch process, the playground makes it interactive: users can edit N3 directly in the browser, load remote N3 from a URL, run reasoning, inspect streamed or rendered output, autosave local state, and create a compact share link when needed.
3789
-
3790
- This appendix explains what the playground is for, how it is structured, and why it matters in practice.
3791
-
3792
- ### I.1 Why the playground exists
3793
-
3794
- Notation3 is expressive, compact, and unusually good at mixing RDF-style data with rules, but the first contact experience can still be awkward for many users. Command-line tools are powerful, but they are not always the best entry point for small experiments, teaching sessions, or public demonstrations.
3795
-
3796
- The playground exists to lower that initial friction. It lets a user:
3797
-
3798
- - open a page,
3799
- - edit or paste a small N3 program,
3800
- - run reasoning immediately,
3801
- - inspect output and errors in place,
3802
- - autosave local work between reloads,
3803
- - and copy a compact share link when the setup should be shared.
3804
-
3805
- That makes the playground useful not only for newcomers, but also for experienced users who want a fast feedback loop for small examples.
3806
-
3807
- ### I.2 Core interaction model
3808
-
3809
- At the center of the playground is an **editable N3 program**. This is the main authoring area for facts, rules, and output-oriented directives.
3810
-
3811
- Alongside that editor is a **Load from URL** field. A remote N3 document can be fetched directly into the playground, which makes it easy to reuse examples stored in a repository or a raw hosted file.
3812
-
3813
- A key recent addition is **background knowledge mode**. When enabled, the N3 loaded from a URL is not written into the editor. Instead, it is stored separately as background knowledge and merged with the editable program only when reasoning runs. This supports a very common workflow:
3814
-
3815
- - keep a stable imported dataset or rule base,
3816
- - keep the local editor small and focused,
3817
- - iterate on local rules, queries, or reporting logic without repeatedly copying the larger imported source.
3818
-
3819
- That separation is helpful both pedagogically and practically. It mirrors real reasoning work, where a user often reasons _over_ a fixed body of data rather than constantly rewriting it.
3820
-
3821
- For repository examples, the playground also follows the same sidecar-input convention as the example test runner. When a loaded URL looks like `.../examples/name.n3`, the playground probes for `.../examples/input/name.trig`. If that companion TriG file exists, it is loaded automatically as background evidence and the run uses RDF/TriG compatibility mode, matching the command-line `-r` behavior used by `npm run test:examples`.
3822
-
3823
- ### I.3 Execution behavior
3824
-
3825
- The playground is designed to feel responsive even when reasoning is not trivial. To do that, it uses a browser execution model that can run inference in a worker rather than blocking the main UI thread. Output is then surfaced back into the page.
3826
-
3827
- The user-facing controls support three main actions:
3828
-
3829
- - **Run reasoning**,
3830
- - **Pause/Resume**,
3831
- - **Stop**.
3832
-
3833
- This matters because the playground is not just a text box plus a submit button. It treats reasoning as a process that can be observed while it happens.
3834
-
3835
- The output behavior also adapts to the kind of N3 program being run. In some cases the natural result is a streamed list of derived triples. In others, such as programs using output-oriented constructs like `log:outputString`, a rendered text result is more appropriate. The playground supports both styles.
3836
-
3837
- For Markdown-oriented `log:outputString` examples, the output pane has two views: a rendered Markdown view and a Markdown source view. Those tabs appear only when the actual output looks like Markdown; Turtle or other plain output stays in the source editor without the Markdown toggle. The rendered view is selected by default for Markdown output, while the source view keeps the exact generated Markdown available for copying, inspection, or comparison.
3838
-
3839
- Repository example reports often contain relative source links that are written for the checked-in files under `examples/output/*.md`. When such an example is loaded into the playground from a raw URL, or restored later from compact/Gist-backed state, the rendered Markdown view resolves those relative links against the corresponding static output page rather than against `/playground`, so links like `../name.n3` and `../input/name.trig` continue to point to the intended example files. The playground preserves this base in shared state when possible and can also recover it from the injected `@base <.../examples/name.n3>` line.
3840
-
3841
- ### I.4 Error handling and explainability
3842
-
3843
- For an interactive reasoning environment, error behavior matters almost as much as successful output. The playground therefore gives particular attention to syntax and runtime feedback.
3844
-
3845
- When an N3 syntax error occurs, the output pane shows the error with line and column information, and the editor highlights the offending line. This shortens the distance between the parser’s complaint and the place where the user needs to fix the program.
3846
-
3847
- The playground also exposes configuration toggles that are especially useful for explanation, RDF compatibility, and browser safety:
3848
-
3849
- - **proof comments**, which make reasoning output more explanatory,
3850
- - **HTTPS dereferencing enforcement**, which helps avoid mixed-content problems when dereferencing from the browser,
3851
- - **RDF/TriG compatibility**, which mirrors command-line `-r, --rdf` for RDF surface syntax and message-log replay,
3852
- - **stream RDF Messages**, which mirrors `--stream-messages` under RDF mode and runs the editor rules over one RDF Message at a time.
3853
-
3854
- For streamed RDF Messages in the playground, put the extraction rules and any small background knowledge in the editor, then provide the large RDF Message Log through the **RDF Message Log URL** field. The URL is preserved in compact share links. A pasted message log in the editor is still useful for small teaching examples, but the URL field is the intended path for large logs because it lets the worker fetch and parse the stream incrementally rather than asking CodeMirror to hold the log text. URL fetches follow HTTP redirects, so short, stable, or repository URLs can redirect to the actual raw resource while the final URL is still used for base handling where needed.
3855
-
3856
- The layout is responsive: on small screens the URL fields and action buttons stack vertically, controls remain tap-friendly, and the editor/output panes scroll internally when their height is capped. That keeps the playground useful on phones for opening shared links, changing toggles, and running small or URL-backed streaming examples.
3857
-
3858
- Together these choices make the playground better suited to live explanation, teaching, and debugging than a minimal browser wrapper would be.
3859
-
3860
- ### I.5 Local state and compact share links
3861
-
3862
- The playground deliberately separates ordinary editing from link sharing.
3863
-
3864
- During normal use, the live browser URL is kept short. Editor content and UI state are autosaved in `localStorage`, so reloading the page can restore local work without continuously rewriting the address bar with a large encoded N3 program.
3865
-
3866
- When a user does want a portable link, the **Copy share link** button creates one on demand:
3867
-
3868
- - unedited examples that were loaded from a URL can be shared as short `?url=...` links,
3869
- - edited programs are shared with a compact compressed `?state=...` payload,
3870
- - default option values are omitted from that payload to keep links small.
3871
-
3872
- If a generated embedded-state link is still very long, the playground reveals **Create Gist share**. That option asks for a GitHub token with gist permission, stores the compact playground state as a secret Gist JSON file, and copies a small `?stateurl=...` playground link that fetches the state file client-side. The token is stored only in that browser's `localStorage`, the Gist is created by a POST request with `referrerPolicy: "no-referrer"`, and the shared playground URL no longer contains the large encoded program.
3873
-
3874
- This keeps everyday use pleasant while preserving the important tutorial and issue-reporting workflow: a link can still capture the imported resource, the local editable overlay, background-knowledge mode, proof mode, and HTTPS-dereferencing mode.
3875
-
3876
- For compatibility, older `?edit=`, `?program=`, `?url=`, compact `?state=`, `?stateurl=`, and hash-based links are accepted when opened. The old `/demo` entry point is also kept as a redirect to the canonical `/playground` page.
3877
-
3878
- ### I.6 What the playground is good for
3879
-
3880
- The playground is especially valuable in four settings.
3881
-
3882
- #### I.6.1 Teaching
3883
-
3884
- Students can begin with a small example and see what changes immediately when they edit a fact or rule. This is a much more direct way to learn N3 than starting from installation instructions.
3885
-
3886
- #### I.6.2 Live demos
3887
-
3888
- A presenter can preload a scenario, show a compact local rule set, run inference, and then share a reproducible link afterward. Background knowledge mode is particularly helpful here because it keeps the visible editor small while still grounding the run in a richer imported source.
3889
-
3890
- #### I.6.3 Debugging small programs
3891
-
3892
- For short reasoning tasks, the playground can be a faster debugging surface than a command-line loop. It is well suited to checking syntax, validating a rule pattern, or inspecting a small proof-oriented run.
3893
-
3894
- #### I.6.4 Sharing examples
3895
-
3896
- A copied share link can capture enough context for another person to reproduce an example quickly, without forcing the live browser URL to carry the full editor content during normal use. This is valuable in issue reports, discussions, teaching material, and public-facing demonstrations.
3897
-
3898
- ### I.7 Limits of the playground
3899
-
3900
- The playground is intentionally lightweight, and it should be understood in that role.
3901
-
3902
- It is not meant to replace the command line for large-scale workloads, benchmarking, or repository-scale automation. Browser memory and execution limits still matter. Likewise, loading remote resources depends on ordinary web constraints such as network access and cross-origin availability.
3903
-
3904
- In short: the playground is best thought of as a compact interactive front end for exploration, communication, and small-to-medium experiments.
3905
-
3906
- ### I.8 Why it matters
3907
-
3908
- The Eyeling Playground shows that N3 reasoning can be made substantially more approachable without flattening the underlying logic into a toy interface. A relatively small set of features — an editor, a URL loader, background knowledge mode, responsive execution, proof toggles, rendered Markdown output, local autosave, and compact share links — is enough to support serious educational and exploratory work.
3909
-
3910
- That is the main value of the playground. It gives Eyeling a public-facing, browser-native environment where reasoning is not hidden behind setup overhead, and where examples can move easily between author, teacher, student, and reviewer.
3911
-
3912
- <a id="app-j"></a>
3913
-
3914
- ## Appendix J — Formalism Is Fine
3915
-
3916
- For Eyeling, formal methods are not an obstacle to practical reasoning. They are part of what makes the system useful. A reasoner is easier to trust when its facts, rules, derivations, and limits can be stated explicitly rather than hidden in application code. That is the sense in which formalism matters here: not as ceremony, but as a way of keeping the behavior of the system inspectable.
3917
-
3918
- Horn logic is fine because it gives a disciplined core. It does not try to express every possible form of reasoning. Instead, it offers a fragment that is small enough to implement clearly and strong enough to support a wide range of real tasks. That trade is often a good one. In a compact reasoner, expressiveness only helps when it does not destroy clarity or operational control.
3919
-
3920
- Notation3 is fine because a logic language also needs a readable surface. Eyeling works with terms, triples, formulas, and rules, but those structures still have to be written, reviewed, debugged, and shared. N3 matters because it keeps the logic close to the page. A rule still looks like something a person can follow. A quoted formula still looks like a graph that can be inspected. That readability is part of what makes the reasoner teachable and portable.
3921
-
3922
- Executable specification is fine because there is real value in keeping semantics and implementation close together. When a specification can be run, it becomes easier to test the intended behavior on concrete inputs, compare outcomes across examples, and find the points where an abstract account is still too vague. Execution does not replace semantics, but it is often the best way to expose whether the semantics is precise enough to guide an implementation.
3923
-
3924
- Herbrand semantics is fine because it gives symbolic reasoning a concrete semantic basis. Instead of beginning with an opaque external domain, it begins with the symbolic constructions themselves and asks what follows from them under the rules. That is a natural fit for Eyeling. The engine reasons over terms, substitutions, triples, formulas, and proof states. Herbrand-style semantics therefore does not feel like an imported philosophical story. It describes the level at which the system actually works.
3925
-
3926
- Gödel incompleteness is fine because the limits of formal systems are not a refutation of formal reasoning. They are part of its shape. Once a system becomes expressive enough, one should expect structural limits on what it can prove about itself. That does not make formal methods less serious. It shows that their boundaries are principled rather than accidental. For a handbook like this one, that is the right lesson: formal systems are valuable not because they say everything, but because they say some things clearly, explicitly, and in a form that can be checked.
3927
-
3928
- Taken together, these positions support a straightforward attitude toward Eyeling. Horn logic is fine. Notation3 is fine. Executable specification is fine. Herbrand semantics is fine. Gödel incompleteness is fine. None of these commitments make the reasoner narrower in a harmful sense. They make it clearer, easier to inspect, and easier to trust. For this project, that is enough.
3929
-
3930
- <a id="app-k"></a>
3931
-
3932
- ## Appendix K — Whitehead-inspired becoming examples
3933
-
3934
- A small family of examples in the repository (`examples/*-becoming.n3`) explores a common idea: that logic can describe not only what **is** the case, but what a thing, system, lineage, or device can **become**. The inspiration is Whiteheadian in a broad sense. The examples do not attempt to formalize Whitehead’s metaphysics as scholarship. Instead, they borrow one guiding intuition from it: reality is often better understood as a structured passage from one state to another than as a mere inventory of static objects.
3935
-
3936
- In N3 terms, this means the examples are written so that rules describe **state-transition potential**. Earlier examples in the handbook often use predicates such as `:can`, `:cannot`, `:supports`, or `:requires`. The becoming family shifts the emphasis toward predicates such as `:canBecome` and `:cannotBecome`, along with intermediate states such as protected dormancy, germination, negative differential response, or adaptive persistence. This is still ordinary Horn-style reasoning. The novelty is not in the engine, but in the modeling style.
3937
-
3938
- The seven current becoming examples span several domains. One is a pure Whiteheadian toy model, where actual occasions prehend a past, respond to a lure of possibility, and become objectively available for future occasions. Others translate the same pattern into engineering revision, developmental genetics, control-systems design, constructor-theoretic task transition, barley-seed lineage renewal, and tunnel-junction wake switching. The common thread is always the same: an entity inherits a prior condition, encounters some enabling or disabling structure, and either reaches a new stabilized state or fails to do so.
3939
-
3940
- That common pattern makes the examples useful pedagogically. They show that Eyeling is not limited to taxonomies, datatype checks, or one-step deductions. It can also express **process descriptions** in a compact symbolic form. A design revision can become a new approved baseline. A cell state can become a differentiated lineage state. A controller can become a validated closed-loop design. A substrate can become a new attribute-state under a possible task. A seed lineage can become a self-renewing cycle. A tunnel junction can become a low-bias wake-serving device.
3941
-
3942
- These examples are also helpful because they keep different levels of abstraction visible. Some of them are deliberately metaphysical, some quasi-biological, some engineering-oriented, and some constructor-theoretic. But they all run through the same reasoner, using the same underlying machinery: terms, triples, forward rules, and closure. That is a quiet but important point. Eyeling does not care whether the domain is philosophy, control theory, genetics, or device physics. What matters is whether the modeled transitions can be stated clearly enough as explicit conditions and consequences.
3943
-
3944
- The becoming examples should therefore be read as **executable schemata** rather than as complete scientific models. They intentionally simplify their domains. The engineering example does not replace design verification. The genetics example does not replace systems biology. The constructor-theory example does not replace the theory itself. And the Whitehead example is not a substitute for reading Whitehead. What the examples do show is that N3 can serve as a clean medium for expressing relational process in a way that remains inspectable, runnable, and easy to vary.
3945
-
3946
- For the handbook, these examples matter for two reasons. First, they provide a concrete demonstration that Eyeling can handle a style of reasoning that feels closer to **becoming, development, and transformation** than to static classification. Second, they show how expressive gains can come from modeling choices rather than from adding new machinery to the engine. The same forward-chaining core that proves `:Socrates a :Mortal` can also prove that a lineage becomes evolvable, that a controller becomes approved, or that a wake switch becomes serviceable under a low-bias regime.
3947
-
3948
- That is why this appendix belongs after Appendix J. “Formalism is fine” not only because it supports rigor, but because it can remain flexible enough to describe worlds in motion. The becoming examples are small demonstrations of that claim. They show that a compact N3 reasoner can host process-oriented models without ceasing to be simple, readable, and executable.
3949
-