eyeling 1.24.1 → 1.24.2

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.
Files changed (64) hide show
  1. package/HANDBOOK.md +98 -0
  2. package/dist/browser/eyeling.browser.js +103 -0
  3. package/eyeling.js +103 -0
  4. package/lib/lexer.js +103 -0
  5. package/package.json +1 -1
  6. package/see/README.md +3 -0
  7. package/see/examples/_see.js +33 -2
  8. package/see/examples/age.js +27 -1
  9. package/see/examples/annotation.js +27 -1
  10. package/see/examples/backward.js +27 -1
  11. package/see/examples/backward_recursion.js +27 -1
  12. package/see/examples/bayes_diagnosis.js +27 -1
  13. package/see/examples/bayes_therapy.js +27 -1
  14. package/see/examples/bmi.js +27 -1
  15. package/see/examples/builtin_coverage.js +27 -1
  16. package/see/examples/collection.js +27 -1
  17. package/see/examples/complex.js +27 -1
  18. package/see/examples/complex_matrix_stability.js +27 -1
  19. package/see/examples/composition_of_injective_functions_is_injective.js +27 -1
  20. package/see/examples/control_system.js +27 -1
  21. package/see/examples/crypto_builtins_tests.js +27 -1
  22. package/see/examples/delfour.js +27 -1
  23. package/see/examples/digital_product_passport.js +27 -1
  24. package/see/examples/dijkstra.js +27 -1
  25. package/see/examples/dijkstra_risk_path.js +27 -1
  26. package/see/examples/doc/triple_terms.md +26 -0
  27. package/see/examples/dog.js +27 -1
  28. package/see/examples/eco_route_insight.js +27 -1
  29. package/see/examples/equals.js +27 -1
  30. package/see/examples/equivalence_classes_overlap_implies_same_class.js +27 -1
  31. package/see/examples/euler_identity.js +27 -1
  32. package/see/examples/ev_roundtrip_planner.js +27 -1
  33. package/see/examples/existential_rule.js +27 -1
  34. package/see/examples/expression_eval.js +27 -1
  35. package/see/examples/family_cousins.js +27 -1
  36. package/see/examples/fastpow.js +27 -1
  37. package/see/examples/fibonacci.js +27 -1
  38. package/see/examples/french_cities.js +27 -1
  39. package/see/examples/fundamental_theorem_arithmetic.js +27 -1
  40. package/see/examples/genetic_knapsack_selection.js +27 -1
  41. package/see/examples/goldbach_1000.js +27 -1
  42. package/see/examples/good_cobbler.js +27 -1
  43. package/see/examples/gps.js +27 -1
  44. package/see/examples/gray_code_counter.js +27 -1
  45. package/see/examples/greatest_lower_bound_uniqueness.js +27 -1
  46. package/see/examples/group_inverse_uniqueness.js +27 -1
  47. package/see/examples/hadamard_approx.js +27 -1
  48. package/see/examples/hanoi.js +27 -1
  49. package/see/examples/input/triple_terms.trig +28 -0
  50. package/see/examples/n3/triple_terms.n3 +23 -0
  51. package/see/examples/odrl_dpv_risk_ranked.js +27 -1
  52. package/see/examples/output/triple_terms.md +53 -0
  53. package/see/examples/path_discovery.js +27 -1
  54. package/see/examples/rc_discharge_envelope.js +27 -1
  55. package/see/examples/rdf_message_flow.js +27 -1
  56. package/see/examples/rdf_messages.js +27 -1
  57. package/see/examples/school_placement_audit.js +27 -1
  58. package/see/examples/smoke_arithmetic.js +27 -1
  59. package/see/examples/socrates.js +27 -1
  60. package/see/examples/triple_terms.js +1442 -0
  61. package/see/examples/wind_turbine.js +27 -1
  62. package/see/examples/witch.js +27 -1
  63. package/see/see.js +75 -2
  64. package/test/api.test.js +20 -0
package/HANDBOOK.md CHANGED
@@ -36,6 +36,7 @@
36
36
  - [Appendix I — The Eyeling Playground](#app-i)
37
37
  - [Appendix J — Formalism Is Fine](#app-j)
38
38
  - [Appendix K — Whitehead-inspired becoming examples](#app-k)
39
+ - [Appendix L — SEE: Specialized Eyeling Executables](#app-l)
39
40
 
40
41
  ---
41
42
 
@@ -236,6 +237,20 @@ The lexer turns the input into tokens like:
236
237
 
237
238
  Parsing becomes dramatically simpler because tokenization already decided where strings end, where numbers are, and so on.
238
239
 
240
+ One compatibility wrinkle is handled in the lexer before normal parsing: RDF 1.2 triple terms written as `<<( s p o )>>` are accepted and normalized to Eyeling's existing singleton quoted-formula term `{ s p o }`. A leading `VERSION "1.2"` or `@version "1.2"` directive is ignored for the same reason. This keeps Eyeling's N3 model stable while allowing small RDF 1.2 triple-term inputs to run through the existing `GraphTerm` machinery. For example:
241
+
242
+ ```n3
243
+ :observation rdf:reifies <<( :sensor :reports :overheating )>> .
244
+ ```
245
+
246
+ is treated internally like:
247
+
248
+ ```n3
249
+ :observation rdf:reifies { :sensor :reports :overheating } .
250
+ ```
251
+
252
+ This is intentionally a compatibility translation, not a new full RDF 1.2 parser layer. Nested or more exotic future triple-term forms should be added only if they can be mapped cleanly onto Eyeling's quoted-formula term model.
253
+
239
254
  ### 4.2 Parsing triples, with Turtle-style convenience
240
255
 
241
256
  The parser supports:
@@ -2561,6 +2576,8 @@ Quoted graphs/formulas use `{ ... }`. Inside a quoted formula, directive scope m
2561
2576
 
2562
2577
  - `@prefix/@base` and `PREFIX/BASE` directives may appear at top level **or inside `{ ... }`**, and apply to the formula they occur in (formula-local scoping).
2563
2578
 
2579
+ Eyeling also accepts the RDF 1.2 triple-term surface form `<<( s p o )>>` as a compatibility spelling for a singleton quoted formula `{ s p o }`. This is useful for inputs that use `rdf:reifies` or other predicates whose objects are RDF 1.2 triple terms, while keeping the rest of Eyeling on its N3 formula-term model.
2580
+
2564
2581
  For the formal grammar, see the N3 spec grammar:
2565
2582
 
2566
2583
  - [https://w3c.github.io/N3/spec/#grammar](https://w3c.github.io/N3/spec/#grammar)
@@ -3794,3 +3811,84 @@ The becoming examples should therefore be read as **executable schemata** rather
3794
3811
  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.
3795
3812
 
3796
3813
  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.
3814
+
3815
+ ---
3816
+
3817
+ <a id="app-l"></a>
3818
+
3819
+ ## Appendix L — SEE: Specialized Eyeling Executables
3820
+
3821
+ SEE, **Specialized Eyeling Executables**, is the companion example system under `see/`. It compiles selected Eyeling-style N3 programs into small, standalone JavaScript runners. The goal is not to replace the main reasoner. The goal is to turn a particular reasoning task into an executable artifact that carries its input evidence, derivation logic, entailment, explanation, and reference output together.
3822
+
3823
+ The short mental model is:
3824
+
3825
+ ```text
3826
+ N3 source + TriG evidence -> specialized JavaScript -> entailment + explanation
3827
+ ```
3828
+
3829
+ ### L.1 Why SEE exists
3830
+
3831
+ The normal Eyeling CLI is a general reasoner: it parses an input file, runs the engine, and prints the resulting closure, selected query output, or rendered `log:outputString` report.
3832
+
3833
+ SEE is narrower and more packageable. For each committed example, `see.js` generates:
3834
+
3835
+ - `see/examples/<name>.js` — a runnable JavaScript derivation program,
3836
+ - `see/examples/input/<name>.trig` — the RDF/TriG evidence read by the runner,
3837
+ - `see/examples/output/<name>.md` — the expected Markdown result,
3838
+ - `see/examples/doc/<name>.md` — a short generated explanation of the compiled example.
3839
+
3840
+ That makes each example easier to audit. A reviewer can inspect the source N3, the generated runner, the input evidence, and the expected entailment/explanation without reconstructing the whole test setup.
3841
+
3842
+ ### L.2 Entailment plus explanation
3843
+
3844
+ SEE reports use an **Entailment** section and an **Explanation** section. The entailment is the selected result of the reasoning task. The explanation records how the compiled runner reached it: source facts, rules, selected support trees, and the formal TriG output when relevant.
3845
+
3846
+ This vocabulary is deliberately more precise than a generic “result”. It says: the program is not merely presenting a suggestion; it is presenting something that follows from the encoded facts, rules, gates, and query projection.
3847
+
3848
+ ### L.3 Direct TriG input
3849
+
3850
+ Generated SEE runners read their committed `.trig` input directly. Earlier versions used an intermediate `.n3` conversion step, but the current design keeps the evidence in TriG and lets the runner parse that committed input. This matters for examples that are closer to RDF data exchange than to hand-written N3 rule files.
3851
+
3852
+ SEE also includes a triple-term example. Its input can contain RDF 1.2 triple terms such as:
3853
+
3854
+ ```trig
3855
+ VERSION "1.2"
3856
+
3857
+ :observation rdf:reifies <<( :sensor :reports :overheating )>> .
3858
+ ```
3859
+
3860
+ Eyeling accepts this surface form by translating the triple term to a singleton quoted formula internally:
3861
+
3862
+ ```n3
3863
+ :observation rdf:reifies { :sensor :reports :overheating } .
3864
+ ```
3865
+
3866
+ That is enough for the SEE example to demonstrate RDF 1.2-style triple terms as input and output without forcing Eyeling to implement a separate full RDF 1.2 syntax model.
3867
+
3868
+ ### L.4 Generation and tests
3869
+
3870
+ The root package scripts treat SEE as part of the normal repository workflow:
3871
+
3872
+ ```bash
3873
+ npm run generate
3874
+ npm run test:see
3875
+ ```
3876
+
3877
+ `npm run generate` refreshes the generated SEE artifacts from `see/examples/n3/*.n3`. `npm run test:see` runs every generated SEE example and compares its Markdown output with the committed reference output.
3878
+
3879
+ In the full test suite, this means SEE is not just documentation. It is executable regression coverage for a broad range of Eyeling behavior: forward rules, backward rules, builtins, fuses, queries, RDF lists, formula-valued terms, generated explanations, and RDF 1.2 triple-term compatibility.
3880
+
3881
+ ### L.5 When to add a SEE example
3882
+
3883
+ A normal Eyeling example is best when you want to demonstrate a language feature or a compact reasoning pattern directly in N3.
3884
+
3885
+ A SEE example is best when you want a more self-contained artifact:
3886
+
3887
+ - the input evidence matters,
3888
+ - the final entailment should be explained in Markdown,
3889
+ - the example should be runnable without an external reasoner at runtime,
3890
+ - the generated JavaScript itself is worth inspecting,
3891
+ - or the example should become stable regression coverage for a specific reasoning workflow.
3892
+
3893
+ In that sense, SEE is an executable publication format for selected Eyeling examples: small enough to read, deterministic enough to test, and explicit enough to audit.
3894
+
@@ -9555,7 +9555,110 @@ function stripQuotes(lex) {
9555
9555
  return lex;
9556
9556
  }
9557
9557
 
9558
+
9559
+ // RDF 1.2 triple terms use <<( s p o )>>. Eyeling's N3 engine does not
9560
+ // implement a new RDF 1.2 term kind; instead, it accepts this syntax as a
9561
+ // compatibility surface and normalizes it to the existing N3 singleton quoted
9562
+ // graph term { s p o }. This keeps ordinary N3 reasoning unchanged while making
9563
+ // RDF 1.2 examples parseable by the current engine.
9564
+ function normalizeRdf12TripleTerms(inputText) {
9565
+ const text = String(inputText ?? '');
9566
+ let i = 0;
9567
+
9568
+ function startsAt(needle, at = i) {
9569
+ return text.startsWith(needle, at);
9570
+ }
9571
+
9572
+ function readString() {
9573
+ const quote = text[i];
9574
+ let out = quote;
9575
+ const long = text.startsWith(quote.repeat(3), i);
9576
+ if (long) {
9577
+ out = quote.repeat(3);
9578
+ i += 3;
9579
+ while (i < text.length) {
9580
+ if (text.startsWith(quote.repeat(3), i)) {
9581
+ out += quote.repeat(3);
9582
+ i += 3;
9583
+ return out;
9584
+ }
9585
+ if (text[i] === '\\' && i + 1 < text.length) {
9586
+ out += text.slice(i, i + 2);
9587
+ i += 2;
9588
+ } else {
9589
+ out += text[i++];
9590
+ }
9591
+ }
9592
+ return out;
9593
+ }
9594
+ i += 1;
9595
+ let escaped = false;
9596
+ while (i < text.length) {
9597
+ const ch = text[i++];
9598
+ out += ch;
9599
+ if (escaped) {
9600
+ escaped = false;
9601
+ } else if (ch === '\\') {
9602
+ escaped = true;
9603
+ } else if (ch === quote) {
9604
+ break;
9605
+ }
9606
+ }
9607
+ return out;
9608
+ }
9609
+
9610
+ function readIri() {
9611
+ let out = text[i++];
9612
+ while (i < text.length) {
9613
+ const ch = text[i++];
9614
+ out += ch;
9615
+ if (ch === '>') break;
9616
+ }
9617
+ return out;
9618
+ }
9619
+
9620
+ function convertUntil(stopToken) {
9621
+ let out = '';
9622
+ while (i < text.length) {
9623
+ if (stopToken && startsAt(stopToken)) {
9624
+ i += stopToken.length;
9625
+ return out;
9626
+ }
9627
+ if (startsAt('<<(')) {
9628
+ i += 3;
9629
+ out += '{ ' + convertUntil(')>>').trim() + ' }';
9630
+ continue;
9631
+ }
9632
+ const ch = text[i];
9633
+ if (ch === '"' || ch === "'") {
9634
+ out += readString();
9635
+ continue;
9636
+ }
9637
+ if (ch === '<') {
9638
+ out += readIri();
9639
+ continue;
9640
+ }
9641
+ if (ch === '#') {
9642
+ while (i < text.length) {
9643
+ const c = text[i++];
9644
+ out += c;
9645
+ if (c === '\n' || c === '\r') break;
9646
+ }
9647
+ continue;
9648
+ }
9649
+ out += ch;
9650
+ i += 1;
9651
+ }
9652
+ if (stopToken) throw new N3SyntaxError(`Unterminated RDF 1.2 triple term, expected ${stopToken}`);
9653
+ return out;
9654
+ }
9655
+
9656
+ const converted = convertUntil(null);
9657
+ return converted.replace(/^\s*(?:@version|VERSION)\s+(["'])1\.2\1\s*\.?\s*(?:#.*)?$/gim, '');
9658
+ }
9659
+
9558
9660
  function lex(inputText) {
9661
+ inputText = normalizeRdf12TripleTerms(inputText);
9559
9662
  const chars = Array.from(inputText);
9560
9663
  const n = chars.length;
9561
9664
  let i = 0;
package/eyeling.js CHANGED
@@ -9555,7 +9555,110 @@ function stripQuotes(lex) {
9555
9555
  return lex;
9556
9556
  }
9557
9557
 
9558
+
9559
+ // RDF 1.2 triple terms use <<( s p o )>>. Eyeling's N3 engine does not
9560
+ // implement a new RDF 1.2 term kind; instead, it accepts this syntax as a
9561
+ // compatibility surface and normalizes it to the existing N3 singleton quoted
9562
+ // graph term { s p o }. This keeps ordinary N3 reasoning unchanged while making
9563
+ // RDF 1.2 examples parseable by the current engine.
9564
+ function normalizeRdf12TripleTerms(inputText) {
9565
+ const text = String(inputText ?? '');
9566
+ let i = 0;
9567
+
9568
+ function startsAt(needle, at = i) {
9569
+ return text.startsWith(needle, at);
9570
+ }
9571
+
9572
+ function readString() {
9573
+ const quote = text[i];
9574
+ let out = quote;
9575
+ const long = text.startsWith(quote.repeat(3), i);
9576
+ if (long) {
9577
+ out = quote.repeat(3);
9578
+ i += 3;
9579
+ while (i < text.length) {
9580
+ if (text.startsWith(quote.repeat(3), i)) {
9581
+ out += quote.repeat(3);
9582
+ i += 3;
9583
+ return out;
9584
+ }
9585
+ if (text[i] === '\\' && i + 1 < text.length) {
9586
+ out += text.slice(i, i + 2);
9587
+ i += 2;
9588
+ } else {
9589
+ out += text[i++];
9590
+ }
9591
+ }
9592
+ return out;
9593
+ }
9594
+ i += 1;
9595
+ let escaped = false;
9596
+ while (i < text.length) {
9597
+ const ch = text[i++];
9598
+ out += ch;
9599
+ if (escaped) {
9600
+ escaped = false;
9601
+ } else if (ch === '\\') {
9602
+ escaped = true;
9603
+ } else if (ch === quote) {
9604
+ break;
9605
+ }
9606
+ }
9607
+ return out;
9608
+ }
9609
+
9610
+ function readIri() {
9611
+ let out = text[i++];
9612
+ while (i < text.length) {
9613
+ const ch = text[i++];
9614
+ out += ch;
9615
+ if (ch === '>') break;
9616
+ }
9617
+ return out;
9618
+ }
9619
+
9620
+ function convertUntil(stopToken) {
9621
+ let out = '';
9622
+ while (i < text.length) {
9623
+ if (stopToken && startsAt(stopToken)) {
9624
+ i += stopToken.length;
9625
+ return out;
9626
+ }
9627
+ if (startsAt('<<(')) {
9628
+ i += 3;
9629
+ out += '{ ' + convertUntil(')>>').trim() + ' }';
9630
+ continue;
9631
+ }
9632
+ const ch = text[i];
9633
+ if (ch === '"' || ch === "'") {
9634
+ out += readString();
9635
+ continue;
9636
+ }
9637
+ if (ch === '<') {
9638
+ out += readIri();
9639
+ continue;
9640
+ }
9641
+ if (ch === '#') {
9642
+ while (i < text.length) {
9643
+ const c = text[i++];
9644
+ out += c;
9645
+ if (c === '\n' || c === '\r') break;
9646
+ }
9647
+ continue;
9648
+ }
9649
+ out += ch;
9650
+ i += 1;
9651
+ }
9652
+ if (stopToken) throw new N3SyntaxError(`Unterminated RDF 1.2 triple term, expected ${stopToken}`);
9653
+ return out;
9654
+ }
9655
+
9656
+ const converted = convertUntil(null);
9657
+ return converted.replace(/^\s*(?:@version|VERSION)\s+(["'])1\.2\1\s*\.?\s*(?:#.*)?$/gim, '');
9658
+ }
9659
+
9558
9660
  function lex(inputText) {
9661
+ inputText = normalizeRdf12TripleTerms(inputText);
9559
9662
  const chars = Array.from(inputText);
9560
9663
  const n = chars.length;
9561
9664
  let i = 0;
package/lib/lexer.js CHANGED
@@ -347,7 +347,110 @@ function stripQuotes(lex) {
347
347
  return lex;
348
348
  }
349
349
 
350
+
351
+ // RDF 1.2 triple terms use <<( s p o )>>. Eyeling's N3 engine does not
352
+ // implement a new RDF 1.2 term kind; instead, it accepts this syntax as a
353
+ // compatibility surface and normalizes it to the existing N3 singleton quoted
354
+ // graph term { s p o }. This keeps ordinary N3 reasoning unchanged while making
355
+ // RDF 1.2 examples parseable by the current engine.
356
+ function normalizeRdf12TripleTerms(inputText) {
357
+ const text = String(inputText ?? '');
358
+ let i = 0;
359
+
360
+ function startsAt(needle, at = i) {
361
+ return text.startsWith(needle, at);
362
+ }
363
+
364
+ function readString() {
365
+ const quote = text[i];
366
+ let out = quote;
367
+ const long = text.startsWith(quote.repeat(3), i);
368
+ if (long) {
369
+ out = quote.repeat(3);
370
+ i += 3;
371
+ while (i < text.length) {
372
+ if (text.startsWith(quote.repeat(3), i)) {
373
+ out += quote.repeat(3);
374
+ i += 3;
375
+ return out;
376
+ }
377
+ if (text[i] === '\\' && i + 1 < text.length) {
378
+ out += text.slice(i, i + 2);
379
+ i += 2;
380
+ } else {
381
+ out += text[i++];
382
+ }
383
+ }
384
+ return out;
385
+ }
386
+ i += 1;
387
+ let escaped = false;
388
+ while (i < text.length) {
389
+ const ch = text[i++];
390
+ out += ch;
391
+ if (escaped) {
392
+ escaped = false;
393
+ } else if (ch === '\\') {
394
+ escaped = true;
395
+ } else if (ch === quote) {
396
+ break;
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+
402
+ function readIri() {
403
+ let out = text[i++];
404
+ while (i < text.length) {
405
+ const ch = text[i++];
406
+ out += ch;
407
+ if (ch === '>') break;
408
+ }
409
+ return out;
410
+ }
411
+
412
+ function convertUntil(stopToken) {
413
+ let out = '';
414
+ while (i < text.length) {
415
+ if (stopToken && startsAt(stopToken)) {
416
+ i += stopToken.length;
417
+ return out;
418
+ }
419
+ if (startsAt('<<(')) {
420
+ i += 3;
421
+ out += '{ ' + convertUntil(')>>').trim() + ' }';
422
+ continue;
423
+ }
424
+ const ch = text[i];
425
+ if (ch === '"' || ch === "'") {
426
+ out += readString();
427
+ continue;
428
+ }
429
+ if (ch === '<') {
430
+ out += readIri();
431
+ continue;
432
+ }
433
+ if (ch === '#') {
434
+ while (i < text.length) {
435
+ const c = text[i++];
436
+ out += c;
437
+ if (c === '\n' || c === '\r') break;
438
+ }
439
+ continue;
440
+ }
441
+ out += ch;
442
+ i += 1;
443
+ }
444
+ if (stopToken) throw new N3SyntaxError(`Unterminated RDF 1.2 triple term, expected ${stopToken}`);
445
+ return out;
446
+ }
447
+
448
+ const converted = convertUntil(null);
449
+ return converted.replace(/^\s*(?:@version|VERSION)\s+(["'])1\.2\1\s*\.?\s*(?:#.*)?$/gim, '');
450
+ }
451
+
350
452
  function lex(inputText) {
453
+ inputText = normalizeRdf12TripleTerms(inputText);
351
454
  const chars = Array.from(inputText);
352
455
  const n = chars.length;
353
456
  let i = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.24.1",
3
+ "version": "1.24.2",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/see/README.md CHANGED
@@ -18,6 +18,8 @@ Each example starts from a Notation3 source and is compiled by `see.js` into a s
18
18
 
19
19
  The trust gate is executable verification. If a required fact is missing, the program fails instead of emitting an unsupported entailment.
20
20
 
21
+ The `triple_terms` example uses RDF 1.2 `<<( ... )>>` triple-term syntax. Eyeling accepts that syntax by normalizing it to the existing N3 singleton graph term `{ ... }`; SEE keeps the committed `.trig` input and formal output in RDF 1.2 triple-term form.
22
+
21
23
 
22
24
  ## Run
23
25
 
@@ -141,5 +143,6 @@ The repository currently contains **50 N3-compiled SEE examples**. Each example
141
143
  | <a id="example-school-placement-route-audit"></a>[School placement route audit](#example-school-placement-route-audit) | compiled from Notation3 by `see.js`; student, school, distance, and policy facts derive an auditable school-placement route decision. | [doc](examples/doc/school_placement_audit.md), [js](examples/school_placement_audit.js), [input](examples/input/school_placement_audit.trig), [output](examples/output/school_placement_audit.md), [n3](examples/n3/school_placement_audit.n3) |
142
144
  | <a id="example-smoke-arithmetic"></a>[Smoke Arithmetic](#example-smoke-arithmetic) | compiled from Notation3 by `see.js`; arithmetic compiler smoke test deriving 6 × 7 through rules and builtins. | [doc](examples/doc/smoke_arithmetic.md), [js](examples/smoke_arithmetic.js), [input](examples/input/smoke_arithmetic.trig), [output](examples/output/smoke_arithmetic.md), [n3](examples/n3/smoke_arithmetic.n3) |
143
145
  | <a id="example-socrates-inference"></a>[Socrates inference](#example-socrates-inference) | compiled from Notation3 by `see.js`; subclass closure with top-level `log:query` projection. | [doc](examples/doc/socrates.md), [js](examples/socrates.js), [input](examples/input/socrates.trig), [output](examples/output/socrates.md), [n3](examples/n3/socrates.n3) |
146
+ | <a id="example-triple-terms"></a>[Triple terms](#example-triple-terms) | compiled from Notation3 by `see.js`; RDF 1.2 TriG triple terms are used as input evidence and as a derived entailment. | [doc](examples/doc/triple_terms.md), [js](examples/triple_terms.js), [input](examples/input/triple_terms.trig), [output](examples/output/triple_terms.md), [n3](examples/n3/triple_terms.n3) |
144
147
  | <a id="example-wind-turbine-envelope"></a>[Wind turbine envelope](#example-wind-turbine-envelope) | compiled from Notation3 by `see.js`; wind-speed samples are classified against cut-in, rated, and cut-out thresholds and accumulated into energy. | [doc](examples/doc/wind_turbine.md), [js](examples/wind_turbine.js), [input](examples/input/wind_turbine.trig), [output](examples/output/wind_turbine.md), [n3](examples/n3/wind_turbine.n3) |
145
148
  | <a id="example-burn-the-witch"></a>[Burn the witch](#example-burn-the-witch) | compiled from Notation3 by `see.js`; rule-chain explanation for the witch entailment. | [doc](examples/doc/witch.md), [js](examples/witch.js), [input](examples/input/witch.trig), [output](examples/output/witch.md), [n3](examples/n3/witch.n3) |
@@ -7,11 +7,31 @@ function lit(value) { return { kind: 'lit', value }; }
7
7
  function blank(value) { return { kind: 'blank', value }; }
8
8
  function list(items) { return { kind: 'list', items }; }
9
9
  function formula(atoms) { return { kind: 'formula', atoms }; }
10
+ function triple(s, p, o) { return { kind: 'triple', s, p, o }; }
10
11
 
11
12
  function readTermToken(text, start = 0) {
12
13
  let i = start;
13
14
  while (/\s/.test(text[i])) i += 1;
14
15
  const begin = i;
16
+ if (text.startsWith('<<(', i)) {
17
+ let depth = 0;
18
+ while (i < text.length) {
19
+ if (text[i] === '"') {
20
+ const [, next] = readTermToken(text, i);
21
+ i = next;
22
+ continue;
23
+ }
24
+ if (text.startsWith('<<(', i)) { depth += 1; i += 3; continue; }
25
+ if (text.startsWith(')>>', i)) {
26
+ depth -= 1;
27
+ i += 3;
28
+ if (depth === 0) break;
29
+ continue;
30
+ }
31
+ i += 1;
32
+ }
33
+ return [text.slice(begin, i), i];
34
+ }
15
35
  if (text[i] === '"') {
16
36
  i += 1;
17
37
  let escaped = false;
@@ -60,7 +80,10 @@ function splitListItems(text) {
60
80
  while (/\s/.test(text[i])) i += 1;
61
81
  if (i >= text.length) break;
62
82
  const start = i;
63
- if (text[i] === '"') {
83
+ if (text.startsWith('<<(', i)) {
84
+ const [, next] = readTermToken(text, i);
85
+ i = next;
86
+ } else if (text[i] === '"') {
64
87
  i += 1;
65
88
  let escaped = false;
66
89
  while (i < text.length) {
@@ -83,9 +106,17 @@ function splitListItems(text) {
83
106
  }
84
107
  return out;
85
108
  }
109
+ function parseTripleTermBody(text) {
110
+ const [s, i1] = readTermToken(text, 0);
111
+ const [p, i2] = readTermToken(text, i1);
112
+ const [o, i3] = readTermToken(text, i2);
113
+ if (!s || !p || !o || text.slice(i3).trim()) throw new Error('bad triple term: ' + text);
114
+ return triple(parseTerm(s), parseTerm(p), parseTerm(o));
115
+ }
86
116
  function parseTerm(text) {
87
117
  const t = String(text || '').trim();
88
118
  if (!t) throw new Error('empty term');
119
+ if (t.startsWith('<<(') && t.endsWith(')>>')) return parseTripleTermBody(t.slice(3, -3).trim());
89
120
  const first = t[0];
90
121
  if (first === '"') return lit(JSON.parse(t));
91
122
  if (first === '(' && t[t.length - 1] === ')') return list(splitListItems(t.slice(1, -1)).map(parseTerm));
@@ -112,7 +143,7 @@ function parseInputTrigFast(trig) {
112
143
  const lines = String(trig || '').split(/\r?\n/);
113
144
  for (let i = 0; i < lines.length; i += 1) {
114
145
  const line = lines[i].trim();
115
- if (!line || line.startsWith('#') || line.toLowerCase().startsWith('@prefix ')) continue;
146
+ if (!line || line.startsWith('#') || line.toLowerCase().startsWith('@prefix ') || /^(@version|version)\s+/i.test(line)) continue;
116
147
  const graphStart = line.match(/^(\S+)\s*\{\s*$/);
117
148
  if (graphStart) {
118
149
  const atoms = [];
@@ -8,6 +8,7 @@ const crypto = require('crypto');
8
8
 
9
9
  function canonical(term) {
10
10
  if (term.kind === 'list') return ['list', term.items.map(canonical)];
11
+ if (term.kind === 'triple') return ['triple', canonical(term.s), canonical(term.p), canonical(term.o)];
11
12
  if (term.kind === 'formula') return ['formula', term.atoms.map((a) => [canonical(a.s), canonical(a.p), canonical(a.o)])];
12
13
  return [term.kind, term.value];
13
14
  }
@@ -17,6 +18,7 @@ function compoundIndexKey() { return Array.from(arguments).map(termIndexKey).joi
17
18
  function termIsConcrete(t) {
18
19
  if (!t || t.kind === 'var') return false;
19
20
  if (t.kind === 'list') return t.items.every(termIsConcrete);
21
+ if (t.kind === 'triple') return termIsConcrete(t.s) && termIsConcrete(t.p) && termIsConcrete(t.o);
20
22
  if (t.kind === 'formula') return t.atoms.every((a) => termIsConcrete(a.s) && termIsConcrete(a.p) && termIsConcrete(a.o));
21
23
  return true;
22
24
  }
@@ -32,6 +34,7 @@ function primitive(t) {
32
34
  if (t.kind === 'iri') return t.value.replace(/^:/, '');
33
35
  if (t.kind === 'blank') return t.value;
34
36
  if (t.kind === 'list') return t.items.map(primitive);
37
+ if (t.kind === 'triple') return termToN3(t);
35
38
  if (t.kind === 'formula') return termToN3(t);
36
39
  return undefined;
37
40
  }
@@ -52,6 +55,7 @@ function termToN3(t) {
52
55
  if (t.kind === 'var') return '?' + t.value;
53
56
  if (t.kind === 'blank') return t.value.startsWith('_:') ? t.value : '_:' + t.value.replace(/^_+/, '');
54
57
  if (t.kind === 'list') return '(' + t.items.map(termToN3).join(' ') + ')';
58
+ if (t.kind === 'triple') return '<<( ' + termToN3(t.s) + ' ' + termToN3(t.p) + ' ' + termToN3(t.o) + ' )>>';
55
59
  if (t.kind === 'formula') return '{ ' + t.atoms.map(atomToN3).join(' . ') + ' }';
56
60
  return String(t.value ?? t);
57
61
  }
@@ -74,6 +78,7 @@ function resolve(term, env, seen = new Set()) {
74
78
  return resolve(env[term.value], env, seen);
75
79
  }
76
80
  if (term.kind === 'list') return list(term.items.map((item) => resolve(item, env, seen)));
81
+ if (term.kind === 'triple') return { kind: 'triple', s: resolve(term.s, env), p: resolve(term.p, env), o: resolve(term.o, env) };
77
82
  if (term.kind === 'formula') return { kind: 'formula', atoms: term.atoms.map((a) => ({ s: resolve(a.s, env), p: resolve(a.p, env), o: resolve(a.o, env) })) };
78
83
  return term;
79
84
  }
@@ -91,6 +96,14 @@ function unify(a, b, env) {
91
96
  }
92
97
  return out;
93
98
  }
99
+ if (a.kind === 'triple' || b.kind === 'triple') {
100
+ if (a.kind !== 'triple' || b.kind !== 'triple') return null;
101
+ let out = unify(a.s, b.s, env);
102
+ if (!out) return null;
103
+ out = unify(a.p, b.p, out);
104
+ if (!out) return null;
105
+ return unify(a.o, b.o, out);
106
+ }
94
107
  return deepEqual(a, b) ? env : null;
95
108
  }
96
109
  function bind(pattern, value, env) { return unify(pattern, value, env); }
@@ -106,6 +119,7 @@ function termIsGround(t, env) {
106
119
  const r = resolve(t, env);
107
120
  if (r.kind === 'var') return false;
108
121
  if (r.kind === 'list') return r.items.every((item) => termIsGround(item, env));
122
+ if (r.kind === 'triple') return termIsGround(r.s, env) && termIsGround(r.p, env) && termIsGround(r.o, env);
109
123
  if (r.kind === 'formula') return r.atoms.every((atom) => atomIsGround(atom, env));
110
124
  return true;
111
125
  }
@@ -665,6 +679,7 @@ function instantiate(term, env, ruleId) {
665
679
  }
666
680
  if (term.kind === 'blank') return blank('_:r' + ruleId + '_' + envSignature(env) + '_' + term.value.replace(/^_/, ''));
667
681
  if (term.kind === 'list') return list(term.items.map((item) => instantiate(item, env, ruleId)));
682
+ if (term.kind === 'triple') return { kind: 'triple', s: instantiate(term.s, env, ruleId), p: instantiate(term.p, env, ruleId), o: instantiate(term.o, env, ruleId) };
668
683
  if (term.kind === 'formula') return { kind: 'formula', atoms: term.atoms.map((a) => ({ s: instantiate(a.s, env, ruleId), p: instantiate(a.p, env, ruleId), o: instantiate(a.o, env, ruleId) })) };
669
684
  return cloneTerm(term);
670
685
  }
@@ -1368,6 +1383,16 @@ function formalOutputFacts(graph, queries, rules, initialFacts) {
1368
1383
  }
1369
1384
  return out;
1370
1385
  }
1386
+ function termHasTripleTerm(term) {
1387
+ if (!term) return false;
1388
+ if (term.kind === 'triple') return true;
1389
+ if (term.kind === 'list') return term.items.some(termHasTripleTerm);
1390
+ if (term.kind === 'formula') return term.atoms.some(atomHasTripleTerm);
1391
+ return false;
1392
+ }
1393
+ function atomHasTripleTerm(atom) { return termHasTripleTerm(atom.s) || termHasTripleTerm(atom.p) || termHasTripleTerm(atom.o); }
1394
+ function factsHaveTripleTerms(facts) { return (facts || []).some(atomHasTripleTerm); }
1395
+ function trigHasVersion12(trig) { return /^s*(?:@version|VERSION)s+["']1.2["']/mi.test(String(trig || '')); }
1371
1396
  function trigGraphBlock(label, atoms) {
1372
1397
  const lines = [label + ' {'];
1373
1398
  for (const atom of atoms || []) lines.push(' ' + atomToN3(atom) + ' .');
@@ -1415,7 +1440,8 @@ function formalOutputToTrig(facts, trig) {
1415
1440
  const prefixes = prefixLinesFromTrig(trig);
1416
1441
  if (state.needOutPrefix && !prefixes.some((line) => line.toLowerCase().startsWith('@prefix out:'))) prefixes.push('@prefix out: <https://example.org/see/output#> .');
1417
1442
  const nl = String.fromCharCode(10);
1418
- return prefixes.join(nl) + nl + nl + body.join(nl);
1443
+ const version = factsHaveTripleTerms(facts) ? 'VERSION "1.2"' + nl + nl : '';
1444
+ return version + prefixes.join(nl) + nl + nl + body.join(nl);
1419
1445
  }
1420
1446
  function appendFormalTrigOutput(markdown, graph, queries, rules, initialFacts, data) {
1421
1447
  const trig = formalOutputToTrig(formalOutputFacts(graph, queries, rules, initialFacts), data && data.trig);