eyeling 1.24.0 → 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 (165) hide show
  1. package/HANDBOOK.md +98 -0
  2. package/README.md +4 -4
  3. package/dist/browser/eyeling.browser.js +103 -0
  4. package/eyeling.js +103 -0
  5. package/lib/lexer.js +103 -0
  6. package/package.json +1 -1
  7. package/see/README.md +13 -11
  8. package/see/examples/_see.js +33 -2
  9. package/see/examples/age.js +37 -11
  10. package/see/examples/annotation.js +37 -11
  11. package/see/examples/backward.js +37 -11
  12. package/see/examples/backward_recursion.js +37 -11
  13. package/see/examples/bayes_diagnosis.js +37 -11
  14. package/see/examples/bayes_therapy.js +37 -11
  15. package/see/examples/bmi.js +37 -11
  16. package/see/examples/builtin_coverage.js +37 -11
  17. package/see/examples/collection.js +37 -11
  18. package/see/examples/complex.js +37 -11
  19. package/see/examples/complex_matrix_stability.js +37 -11
  20. package/see/examples/composition_of_injective_functions_is_injective.js +37 -11
  21. package/see/examples/control_system.js +37 -11
  22. package/see/examples/crypto_builtins_tests.js +37 -11
  23. package/see/examples/delfour.js +37 -11
  24. package/see/examples/digital_product_passport.js +37 -11
  25. package/see/examples/dijkstra.js +37 -11
  26. package/see/examples/dijkstra_risk_path.js +37 -11
  27. package/see/examples/doc/age.md +1 -1
  28. package/see/examples/doc/annotation.md +1 -1
  29. package/see/examples/doc/backward.md +1 -1
  30. package/see/examples/doc/backward_recursion.md +1 -1
  31. package/see/examples/doc/bayes_diagnosis.md +1 -1
  32. package/see/examples/doc/bayes_therapy.md +1 -1
  33. package/see/examples/doc/bmi.md +1 -1
  34. package/see/examples/doc/builtin_coverage.md +1 -1
  35. package/see/examples/doc/collection.md +1 -1
  36. package/see/examples/doc/complex.md +1 -1
  37. package/see/examples/doc/complex_matrix_stability.md +1 -1
  38. package/see/examples/doc/composition_of_injective_functions_is_injective.md +1 -1
  39. package/see/examples/doc/control_system.md +1 -1
  40. package/see/examples/doc/crypto_builtins_tests.md +1 -1
  41. package/see/examples/doc/delfour.md +1 -1
  42. package/see/examples/doc/digital_product_passport.md +1 -1
  43. package/see/examples/doc/dijkstra.md +1 -1
  44. package/see/examples/doc/dijkstra_risk_path.md +1 -1
  45. package/see/examples/doc/dog.md +1 -1
  46. package/see/examples/doc/eco_route_insight.md +1 -1
  47. package/see/examples/doc/equals.md +1 -1
  48. package/see/examples/doc/equivalence_classes_overlap_implies_same_class.md +1 -1
  49. package/see/examples/doc/euler_identity.md +1 -1
  50. package/see/examples/doc/ev_roundtrip_planner.md +1 -1
  51. package/see/examples/doc/existential_rule.md +1 -1
  52. package/see/examples/doc/expression_eval.md +1 -1
  53. package/see/examples/doc/family_cousins.md +1 -1
  54. package/see/examples/doc/fastpow.md +1 -1
  55. package/see/examples/doc/fibonacci.md +1 -1
  56. package/see/examples/doc/french_cities.md +1 -1
  57. package/see/examples/doc/fundamental_theorem_arithmetic.md +1 -1
  58. package/see/examples/doc/genetic_knapsack_selection.md +1 -1
  59. package/see/examples/doc/goldbach_1000.md +1 -1
  60. package/see/examples/doc/good_cobbler.md +1 -1
  61. package/see/examples/doc/gps.md +1 -1
  62. package/see/examples/doc/gray_code_counter.md +1 -1
  63. package/see/examples/doc/greatest_lower_bound_uniqueness.md +1 -1
  64. package/see/examples/doc/group_inverse_uniqueness.md +1 -1
  65. package/see/examples/doc/hadamard_approx.md +1 -1
  66. package/see/examples/doc/hanoi.md +1 -1
  67. package/see/examples/doc/odrl_dpv_risk_ranked.md +1 -1
  68. package/see/examples/doc/path_discovery.md +1 -1
  69. package/see/examples/doc/rc_discharge_envelope.md +1 -1
  70. package/see/examples/doc/rdf_message_flow.md +1 -1
  71. package/see/examples/doc/rdf_messages.md +1 -1
  72. package/see/examples/doc/school_placement_audit.md +1 -1
  73. package/see/examples/doc/smoke_arithmetic.md +1 -1
  74. package/see/examples/doc/socrates.md +1 -1
  75. package/see/examples/doc/triple_terms.md +26 -0
  76. package/see/examples/doc/wind_turbine.md +1 -1
  77. package/see/examples/doc/witch.md +1 -1
  78. package/see/examples/dog.js +37 -11
  79. package/see/examples/eco_route_insight.js +37 -11
  80. package/see/examples/equals.js +37 -11
  81. package/see/examples/equivalence_classes_overlap_implies_same_class.js +37 -11
  82. package/see/examples/euler_identity.js +37 -11
  83. package/see/examples/ev_roundtrip_planner.js +37 -11
  84. package/see/examples/existential_rule.js +37 -11
  85. package/see/examples/expression_eval.js +37 -11
  86. package/see/examples/family_cousins.js +37 -11
  87. package/see/examples/fastpow.js +37 -11
  88. package/see/examples/fibonacci.js +37 -11
  89. package/see/examples/french_cities.js +37 -11
  90. package/see/examples/fundamental_theorem_arithmetic.js +37 -11
  91. package/see/examples/genetic_knapsack_selection.js +37 -11
  92. package/see/examples/goldbach_1000.js +37 -11
  93. package/see/examples/good_cobbler.js +37 -11
  94. package/see/examples/gps.js +37 -11
  95. package/see/examples/gray_code_counter.js +37 -11
  96. package/see/examples/greatest_lower_bound_uniqueness.js +37 -11
  97. package/see/examples/group_inverse_uniqueness.js +37 -11
  98. package/see/examples/hadamard_approx.js +37 -11
  99. package/see/examples/hanoi.js +37 -11
  100. package/see/examples/input/triple_terms.trig +28 -0
  101. package/see/examples/n3/triple_terms.n3 +23 -0
  102. package/see/examples/odrl_dpv_risk_ranked.js +37 -11
  103. package/see/examples/output/age.md +3 -3
  104. package/see/examples/output/annotation.md +4 -4
  105. package/see/examples/output/backward.md +3 -3
  106. package/see/examples/output/backward_recursion.md +3 -3
  107. package/see/examples/output/bayes_diagnosis.md +1 -1
  108. package/see/examples/output/bayes_therapy.md +1 -1
  109. package/see/examples/output/bmi.md +1 -1
  110. package/see/examples/output/builtin_coverage.md +3 -3
  111. package/see/examples/output/collection.md +3 -3
  112. package/see/examples/output/complex.md +4 -4
  113. package/see/examples/output/complex_matrix_stability.md +1 -1
  114. package/see/examples/output/composition_of_injective_functions_is_injective.md +3 -3
  115. package/see/examples/output/control_system.md +3 -3
  116. package/see/examples/output/crypto_builtins_tests.md +3 -3
  117. package/see/examples/output/delfour.md +1 -1
  118. package/see/examples/output/digital_product_passport.md +1 -1
  119. package/see/examples/output/dijkstra.md +3 -3
  120. package/see/examples/output/dijkstra_risk_path.md +1 -1
  121. package/see/examples/output/dog.md +3 -3
  122. package/see/examples/output/eco_route_insight.md +1 -1
  123. package/see/examples/output/equals.md +3 -3
  124. package/see/examples/output/equivalence_classes_overlap_implies_same_class.md +3 -3
  125. package/see/examples/output/euler_identity.md +3 -3
  126. package/see/examples/output/ev_roundtrip_planner.md +1 -1
  127. package/see/examples/output/existential_rule.md +3 -3
  128. package/see/examples/output/expression_eval.md +3 -3
  129. package/see/examples/output/family_cousins.md +3 -3
  130. package/see/examples/output/fastpow.md +1 -1
  131. package/see/examples/output/fibonacci.md +1 -1
  132. package/see/examples/output/french_cities.md +3 -3
  133. package/see/examples/output/fundamental_theorem_arithmetic.md +1 -1
  134. package/see/examples/output/genetic_knapsack_selection.md +1 -1
  135. package/see/examples/output/goldbach_1000.md +1 -1
  136. package/see/examples/output/good_cobbler.md +4 -4
  137. package/see/examples/output/gps.md +1 -1
  138. package/see/examples/output/gray_code_counter.md +1 -1
  139. package/see/examples/output/greatest_lower_bound_uniqueness.md +3 -3
  140. package/see/examples/output/group_inverse_uniqueness.md +3 -3
  141. package/see/examples/output/hadamard_approx.md +3 -3
  142. package/see/examples/output/hanoi.md +3 -3
  143. package/see/examples/output/odrl_dpv_risk_ranked.md +3 -3
  144. package/see/examples/output/path_discovery.md +3 -3
  145. package/see/examples/output/rc_discharge_envelope.md +1 -1
  146. package/see/examples/output/rdf_message_flow.md +1 -1
  147. package/see/examples/output/rdf_messages.md +1 -1
  148. package/see/examples/output/school_placement_audit.md +1 -1
  149. package/see/examples/output/smoke_arithmetic.md +1 -1
  150. package/see/examples/output/socrates.md +3 -3
  151. package/see/examples/output/triple_terms.md +53 -0
  152. package/see/examples/output/wind_turbine.md +1 -1
  153. package/see/examples/output/witch.md +3 -3
  154. package/see/examples/path_discovery.js +37 -11
  155. package/see/examples/rc_discharge_envelope.js +37 -11
  156. package/see/examples/rdf_message_flow.js +37 -11
  157. package/see/examples/rdf_messages.js +37 -11
  158. package/see/examples/school_placement_audit.js +37 -11
  159. package/see/examples/smoke_arithmetic.js +37 -11
  160. package/see/examples/socrates.js +37 -11
  161. package/see/examples/triple_terms.js +1442 -0
  162. package/see/examples/wind_turbine.js +37 -11
  163. package/see/examples/witch.js +37 -11
  164. package/see/see.js +455 -94
  165. package/test/api.test.js +20 -0
package/see/see.js CHANGED
@@ -1,11 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ // SEE, Specialized Eyeling Executables, compiles a small, practical Notation3
5
+ // subset into standalone JavaScript examples. The compiler runs once at
6
+ // generation time: it extracts source facts into formal TriG evidence and bakes
7
+ // the supported rules, queries, and fuses into the generated runner.
8
+ //
9
+ // The generated examples intentionally do not call Eyeling or another reasoner
10
+ // at runtime. They read their .trig evidence directly and perform a local
11
+ // fixpoint derivation, which makes the resulting programs easy to inspect,
12
+ // snapshot, and publish as self-contained executable explanations.
13
+
4
14
  const crypto = require('crypto');
5
15
  const fs = require('fs');
6
16
  const path = require('path');
7
17
  const { spawnSync } = require('child_process');
8
18
 
19
+ // All SEE-owned artefacts stay below /see/examples so the directory can be
20
+ // generated, tested, and documented from the eyeling repository root.
9
21
  const ROOT = __dirname;
10
22
  const EXAMPLES_DIR = path.join(ROOT, 'examples');
11
23
  const INPUT_DIR = path.join(EXAMPLES_DIR, 'input');
@@ -32,8 +44,12 @@ rule/query/fuse subset into JavaScript, and the resulting examples/<name>.js
32
44
  loads the TriG evidence directly and performs the forward derivation itself.`;
33
45
  }
34
46
 
35
- function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); }
36
- function readText(file) { return fs.readFileSync(file, 'utf8'); }
47
+ function ensureDir(dir) {
48
+ fs.mkdirSync(dir, { recursive: true });
49
+ }
50
+ function readText(file) {
51
+ return fs.readFileSync(file, 'utf8');
52
+ }
37
53
  function writeText(file, text, force) {
38
54
  if (!force && fs.existsSync(file)) {
39
55
  throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
@@ -41,8 +57,12 @@ function writeText(file, text, force) {
41
57
  ensureDir(path.dirname(file));
42
58
  fs.writeFileSync(file, text, 'utf8');
43
59
  }
44
- function sha256(text) { return crypto.createHash('sha256').update(text, 'utf8').digest('hex'); }
45
- function js(value) { return JSON.stringify(value, null, 2); }
60
+ function sha256(text) {
61
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
62
+ }
63
+ function js(value) {
64
+ return JSON.stringify(value, null, 2);
65
+ }
46
66
 
47
67
  function slugify(value) {
48
68
  const base = String(value || 'example')
@@ -54,38 +74,73 @@ function slugify(value) {
54
74
  return base || 'example';
55
75
  }
56
76
  function titleFromSlug(slug) {
57
- return slug.split(/[_-]+/).filter(Boolean)
58
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(' ');
77
+ return slug
78
+ .split(/[_-]+/)
79
+ .filter(Boolean)
80
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
81
+ .join(' ');
59
82
  }
60
83
 
61
- function stripComment(line) { return line.replace(/^\s*#\s?/, '').trimEnd(); }
62
- function isSeparator(line) { const t = line.trim(); return /^[-=]{3,}$/.test(t) || t === ''; }
84
+ // A leading comment block in each source .n3 file becomes the public example
85
+ // title and description used in generated documentation and metadata.
86
+ function stripComment(line) {
87
+ return line.replace(/^\s*#\s?/, '').trimEnd();
88
+ }
89
+ function isSeparator(line) {
90
+ const t = line.trim();
91
+ return /^[-=]{3,}$/.test(t) || t === '';
92
+ }
63
93
  function parseHeader(n3, fallbackTitle) {
64
94
  const raw = [];
65
95
  for (const line of n3.split(/\r?\n/)) {
66
- if (/^\s*#/.test(line)) { raw.push(stripComment(line)); continue; }
67
- if (/^\s*$/.test(line) && raw.length) { raw.push(''); continue; }
96
+ if (/^\s*#/.test(line)) {
97
+ raw.push(stripComment(line));
98
+ continue;
99
+ }
100
+ if (/^\s*$/.test(line) && raw.length) {
101
+ raw.push('');
102
+ continue;
103
+ }
68
104
  if (/^\s*$/.test(line)) continue;
69
105
  break;
70
106
  }
71
107
  const useful = raw.map((line) => line.trim()).filter((line) => !isSeparator(line));
72
- return { title: useful[0] || fallbackTitle, description: useful.slice(1).join('\n').replace(/\n{3,}/g, '\n\n').trim(), headerComments: raw };
108
+ return {
109
+ title: useful[0] || fallbackTitle,
110
+ description: useful
111
+ .slice(1)
112
+ .join('\n')
113
+ .replace(/\n{3,}/g, '\n\n')
114
+ .trim(),
115
+ headerComments: raw,
116
+ };
73
117
  }
74
118
 
75
119
  function removeComments(n3) {
76
- return n3.split(/\r?\n/).map((line) => {
77
- let inString = false, escaped = false, inIri = false;
78
- for (let i = 0; i < line.length; i += 1) {
79
- const ch = line[i];
80
- if (escaped) { escaped = false; continue; }
81
- if (ch === '\\' && inString) { escaped = true; continue; }
82
- if (ch === '"' && !inIri) inString = !inString;
83
- if (ch === '<' && !inString) inIri = true;
84
- if (ch === '>' && inIri) inIri = false;
85
- if (ch === '#' && !inString && !inIri) return line.slice(0, i);
86
- }
87
- return line;
88
- }).join('\n');
120
+ return n3
121
+ .split(/\r?\n/)
122
+ .map((line) => {
123
+ let inString = false,
124
+ escaped = false,
125
+ inIri = false;
126
+ for (let i = 0; i < line.length; i += 1) {
127
+ const ch = line[i];
128
+ if (escaped) {
129
+ escaped = false;
130
+ continue;
131
+ }
132
+ if (ch === '\\' && inString) {
133
+ escaped = true;
134
+ continue;
135
+ }
136
+ if (ch === '"' && !inIri) inString = !inString;
137
+ if (ch === '<' && !inString) inIri = true;
138
+ if (ch === '>' && inIri) inIri = false;
139
+ if (ch === '#' && !inString && !inIri) return line.slice(0, i);
140
+ }
141
+ return line;
142
+ })
143
+ .join('\n');
89
144
  }
90
145
 
91
146
  function decodeEscapes(value) {
@@ -104,13 +159,31 @@ function decodeEscapes(value) {
104
159
  });
105
160
  }
106
161
 
107
- function t(kind, value) { return { kind, value }; }
108
- function I(value) { return t('iri', value); }
109
- function V(value) { return t('var', value); }
110
- function L(value) { return t('lit', value); }
111
- function List(items) { return { kind: 'list', items }; }
112
- function Blank(id) { return { kind: 'blank', value: id }; }
162
+ // Internal terms use a tiny AST shared by the compiler and the generated
163
+ // runtime. Variables are stored without the leading '?'; IRIs and compact
164
+ // QNames are preserved as source-facing strings for readable snapshots.
165
+ function t(kind, value) {
166
+ return { kind, value };
167
+ }
168
+ function I(value) {
169
+ return t('iri', value);
170
+ }
171
+ function V(value) {
172
+ return t('var', value);
173
+ }
174
+ function L(value) {
175
+ return t('lit', value);
176
+ }
177
+ function List(items) {
178
+ return { kind: 'list', items };
179
+ }
180
+ function Blank(id) {
181
+ return { kind: 'blank', value: id };
182
+ }
113
183
 
184
+ // This tokenizer/parser is deliberately smaller than a complete N3 parser. It
185
+ // accepts the SEE example subset: triples, lists, blank-node property lists,
186
+ // quoted formulas, RDF 1.2 triple terms, implication arrows, variables, literals, and prefix/version lines.
114
187
  function tokenize(source) {
115
188
  const s = removeComments(source);
116
189
  const tokens = [];
@@ -119,9 +192,24 @@ function tokenize(source) {
119
192
  const one = new Set(['{', '}', '[', ']', '(', ')', ';', ',', '.']);
120
193
  while (i < s.length) {
121
194
  const ch = s[i];
122
- if (isWs(ch)) { i += 1; continue; }
195
+ if (isWs(ch)) {
196
+ i += 1;
197
+ continue;
198
+ }
199
+ if (s.startsWith('<<(', i)) {
200
+ tokens.push({ type: '<<(', value: '<<(' });
201
+ i += 3;
202
+ continue;
203
+ }
204
+ if (s.startsWith(')>>', i)) {
205
+ tokens.push({ type: ')>>', value: ')>>' });
206
+ i += 3;
207
+ continue;
208
+ }
123
209
  if (s.startsWith('=>', i) || s.startsWith('<=', i) || s.startsWith('^^', i)) {
124
- tokens.push({ type: s.slice(i, i + 2), value: s.slice(i, i + 2) }); i += 2; continue;
210
+ tokens.push({ type: s.slice(i, i + 2), value: s.slice(i, i + 2) });
211
+ i += 2;
212
+ continue;
125
213
  }
126
214
  if (/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/.test(s.slice(i)) && /[+\-0-9.]/.test(ch)) {
127
215
  const m = s.slice(i).match(/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/)[0];
@@ -130,13 +218,26 @@ function tokenize(source) {
130
218
  i += m.length;
131
219
  continue;
132
220
  }
133
- if (one.has(ch)) { tokens.push({ type: ch, value: ch }); i += 1; continue; }
221
+ if (one.has(ch)) {
222
+ tokens.push({ type: ch, value: ch });
223
+ i += 1;
224
+ continue;
225
+ }
134
226
  if (ch === '"') {
135
- let out = '', escaped = false; i += 1;
227
+ let out = '',
228
+ escaped = false;
229
+ i += 1;
136
230
  while (i < s.length) {
137
231
  const c = s[i++];
138
- if (escaped) { out += `\\${c}`; escaped = false; continue; }
139
- if (c === '\\') { escaped = true; continue; }
232
+ if (escaped) {
233
+ out += `\\${c}`;
234
+ escaped = false;
235
+ continue;
236
+ }
237
+ if (c === '\\') {
238
+ escaped = true;
239
+ continue;
240
+ }
140
241
  if (c === '"') break;
141
242
  out += c;
142
243
  }
@@ -144,7 +245,8 @@ function tokenize(source) {
144
245
  continue;
145
246
  }
146
247
  if (ch === '<') {
147
- let out = ''; i += 1;
248
+ let out = '';
249
+ i += 1;
148
250
  while (i < s.length && s[i] !== '>') out += s[i++];
149
251
  if (s[i] !== '>') throw new Error('Unterminated IRI');
150
252
  i += 1;
@@ -163,11 +265,13 @@ function tokenize(source) {
163
265
 
164
266
  function classifyToken(raw) {
165
267
  if (raw === '@prefix') return { type: '@prefix', value: raw };
268
+ if (/^VERSION$/i.test(raw)) return { type: 'VERSION', value: raw };
166
269
  if (raw === 'a') return { type: 'qname', value: 'rdf:type' };
167
270
  if (raw.startsWith('?')) return { type: 'var', value: raw.slice(1) };
168
271
  if (/^(true|false)$/i.test(raw)) return { type: 'boolean', value: /^true$/i.test(raw) };
169
272
  if (/^[+-]?\d+$/.test(raw)) return { type: 'number', value: Number.parseInt(raw, 10) };
170
- if (/^[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(raw) || /^[+-]?\d+[eE][+-]?\d+$/.test(raw)) return { type: 'number', value: Number(raw) };
273
+ if (/^[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(raw) || /^[+-]?\d+[eE][+-]?\d+$/.test(raw))
274
+ return { type: 'number', value: Number(raw) };
171
275
  return { type: 'qname', value: raw };
172
276
  }
173
277
 
@@ -177,7 +281,9 @@ class Parser {
177
281
  this.pos = 0;
178
282
  this.blankCounter = 0;
179
283
  }
180
- eof() { return this.pos >= this.tokens.length; }
284
+ eof() {
285
+ return this.pos >= this.tokens.length;
286
+ }
181
287
  peek(value = undefined) {
182
288
  const tok = this.tokens[this.pos];
183
289
  if (value === undefined) return tok;
@@ -187,46 +293,77 @@ class Parser {
187
293
  if (this.eof()) throw new Error('Unexpected end of input');
188
294
  return this.tokens[this.pos++];
189
295
  }
190
- accept(type) { if (this.peek(type)) return this.next(); return null; }
296
+ accept(type) {
297
+ if (this.peek(type)) return this.next();
298
+ return null;
299
+ }
191
300
  expect(type) {
192
301
  const tok = this.next();
193
302
  if (tok.type !== type) throw new Error(`Expected ${type}, got ${tok.type} (${tok.value})`);
194
303
  return tok;
195
304
  }
196
- freshBlank(prefix = 'b') { this.blankCounter += 1; return `_${prefix}${this.blankCounter}`; }
305
+ freshBlank(prefix = 'b') {
306
+ this.blankCounter += 1;
307
+ return `_${prefix}${this.blankCounter}`;
308
+ }
197
309
  skipPrefix() {
198
310
  this.expect('@prefix');
199
311
  // Prefix declaration is irrelevant after QName compaction; skip until final dot.
200
312
  while (!this.eof() && !this.accept('.')) this.next();
201
313
  }
314
+ skipVersion() {
315
+ this.expect('VERSION');
316
+ if (this.peek('string')) this.next();
317
+ this.accept('.');
318
+ }
202
319
  parseProgram() {
203
- const facts = [], rules = [], queries = [], prefixes = {};
320
+ const facts = [],
321
+ rules = [],
322
+ queries = [],
323
+ prefixes = {};
204
324
  while (!this.eof()) {
205
325
  if (this.accept('@prefix')) {
206
326
  this.pos -= 1;
207
327
  const start = this.pos;
208
328
  this.skipPrefix();
209
- const slice = this.tokens.slice(start, this.pos).map((tok) => tok.value).join(' ');
329
+ const slice = this.tokens
330
+ .slice(start, this.pos)
331
+ .map((tok) => tok.value)
332
+ .join(' ');
210
333
  const m = slice.match(/@prefix\s+([^\s]*)\s+<([^>]+)>/);
211
334
  if (m) prefixes[(m[1] || ':').replace(/:$/, '')] = m[2];
212
335
  continue;
213
336
  }
337
+ if (this.peek('VERSION')) {
338
+ this.skipVersion();
339
+ continue;
340
+ }
214
341
  if (this.peek('{')) {
215
342
  const lhs = this.parseFormula('body');
216
343
  if (this.accept('=>')) {
217
- if ((this.peek('qname') && this.tokens[this.pos].value === 'false') || (this.peek('boolean') && this.tokens[this.pos].value === false)) {
218
- this.next(); this.accept('.');
344
+ if (
345
+ (this.peek('qname') && this.tokens[this.pos].value === 'false') ||
346
+ (this.peek('boolean') && this.tokens[this.pos].value === false)
347
+ ) {
348
+ this.next();
349
+ this.accept('.');
219
350
  rules.push({ kind: 'fuse', id: rules.length + 1, body: lhs });
220
351
  } else {
221
- const head = this.parseFormula('head'); this.accept('.');
352
+ const head = this.parseFormula('head');
353
+ this.accept('.');
222
354
  rules.push({ kind: 'rule', id: rules.length + 1, body: lhs, head });
223
355
  }
224
356
  } else if (this.accept('<=')) {
225
- if ((this.peek('qname') && this.tokens[this.pos].value === 'true') || (this.peek('boolean') && this.tokens[this.pos].value === true)) {
226
- this.next(); this.accept('.');
357
+ if (
358
+ (this.peek('qname') && this.tokens[this.pos].value === 'true') ||
359
+ (this.peek('boolean') && this.tokens[this.pos].value === true)
360
+ ) {
361
+ this.next();
362
+ this.accept('.');
227
363
  rules.push({ kind: 'backward', id: rules.length + 1, body: [], head: lhs });
228
364
  } else {
229
- const rhs = this.parseFormula('body'); this.accept('.');
365
+ const rhs = this.parseFormula('body');
366
+ this.accept('.');
230
367
  rules.push({ kind: 'backward', id: rules.length + 1, body: rhs, head: lhs });
231
368
  }
232
369
  } else {
@@ -234,7 +371,12 @@ class Parser {
234
371
  const triples = this.parseStatementRest('fact', subject);
235
372
  this.accept('.');
236
373
  for (const triple of triples) {
237
- if (triple.s?.kind === 'formula' && triple.p?.kind === 'iri' && triple.p.value === 'log:query' && triple.o?.kind === 'formula') {
374
+ if (
375
+ triple.s?.kind === 'formula' &&
376
+ triple.p?.kind === 'iri' &&
377
+ triple.p.value === 'log:query' &&
378
+ triple.o?.kind === 'formula'
379
+ ) {
238
380
  queries.push({ id: queries.length + 1, premise: triple.s.atoms, conclusion: triple.o.atoms });
239
381
  } else {
240
382
  facts.push(triple);
@@ -295,6 +437,13 @@ class Parser {
295
437
  }
296
438
  if (tok.type === 'number' || tok.type === 'boolean') return L(tok.value);
297
439
  if (tok.type === 'iri' || tok.type === 'qname') return I(tok.value);
440
+ if (tok.type === '<<(') {
441
+ const subject = this.parseTerm(mode, sink);
442
+ const predicate = this.parsePredicate();
443
+ const object = this.parseTerm(mode, sink);
444
+ this.expect(')>>');
445
+ return { kind: 'triple', s: subject, p: predicate, o: object };
446
+ }
298
447
  if (tok.type === '(') {
299
448
  const items = [];
300
449
  while (!this.accept(')')) items.push(this.parseTerm(mode, sink));
@@ -310,7 +459,10 @@ class Parser {
310
459
  return { kind: 'formula', atoms };
311
460
  }
312
461
  if (tok.type === '[') {
313
- const id = mode === 'body' ? V(this.freshBlank('bodyBlank')) : Blank(this.freshBlank(mode === 'head' ? 'headBlank' : 'blank'));
462
+ const id =
463
+ mode === 'body'
464
+ ? V(this.freshBlank('bodyBlank'))
465
+ : Blank(this.freshBlank(mode === 'head' ? 'headBlank' : 'blank'));
314
466
  if (!this.accept(']')) {
315
467
  while (true) {
316
468
  const predicate = this.parsePredicate();
@@ -329,6 +481,11 @@ class Parser {
329
481
  }
330
482
  }
331
483
 
484
+ // parseN3 separates the source file into four compiler inputs:
485
+ // facts -> serialized as examples/input/<name>.trig
486
+ // rules -> compiled into JavaScript fixpoint code
487
+ // queries -> rendered as selected output checks
488
+ // prefixes -> carried into generated TriG evidence
332
489
  function parseN3(n3) {
333
490
  const parser = new Parser(tokenize(n3));
334
491
  return parser.parseProgram();
@@ -340,15 +497,22 @@ function termToJsComment(term) {
340
497
  if (term.kind === 'var') return `?${term.value}`;
341
498
  if (term.kind === 'blank') return term.value;
342
499
  if (term.kind === 'list') return `(${term.items.map(termToJsComment).join(' ')})`;
500
+ if (term.kind === 'triple') return `<<( ${termToJsComment(term.s)} ${termToJsComment(term.p)} ${termToJsComment(term.o)} )>>`;
343
501
  if (term.kind === 'formula') return `{ ${term.atoms.map(atomToComment).join(' . ')} }`;
344
502
  return String(term.value ?? term);
345
503
  }
346
- function atomToComment(atom) { return `${termToJsComment(atom.s)} ${termToJsComment(atom.p)} ${termToJsComment(atom.o)}`; }
504
+ function atomToComment(atom) {
505
+ return `${termToJsComment(atom.s)} ${termToJsComment(atom.p)} ${termToJsComment(atom.o)}`;
506
+ }
347
507
 
348
508
  function compilationStats(program) {
349
509
  const predicates = new Set();
350
510
  const builtins = new Set();
351
- for (const atom of [...program.facts, ...program.rules.flatMap((r) => [...(r.body || []), ...(r.head || [])]), ...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])])]) {
511
+ for (const atom of [
512
+ ...program.facts,
513
+ ...program.rules.flatMap((r) => [...(r.body || []), ...(r.head || [])]),
514
+ ...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
515
+ ]) {
352
516
  const p = atom.p?.value;
353
517
  if (p) predicates.add(p);
354
518
  if (/^(math|string|list|log|crypto):/.test(p)) builtins.add(p);
@@ -364,23 +528,130 @@ function compilationStats(program) {
364
528
  };
365
529
  }
366
530
 
367
-
368
- function trigString(value) { return JSON.stringify(String(value)); }
369
- function trigNumber(value) { if (Object.is(value, -0)) return '0'; if (Number.isInteger(value)) return String(value); return Number(value.toPrecision(15)).toString(); }
370
- function inputLiteralToN3(value) { if (typeof value === 'string') return trigString(value); if (typeof value === 'number') return trigNumber(value); if (typeof value === 'boolean') return value ? 'true' : 'false'; return trigString(value); }
371
- function inputTermToN3(term) { if (!term) return 'undefined'; if (term.kind === 'iri') return term.value; if (term.kind === 'lit') return inputLiteralToN3(term.value); if (term.kind === 'var') return '?' + term.value; if (term.kind === 'blank') return term.value.startsWith('_:') ? term.value : '_:' + term.value.replace(/^_+/, ''); if (term.kind === 'list') return '(' + term.items.map(inputTermToN3).join(' ') + ')'; if (term.kind === 'formula') return '{ ' + term.atoms.map(inputAtomToN3).join(' . ') + ' }'; return String(term.value ?? term); }
372
- function inputAtomToN3(atom) { return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + inputTermToN3(atom.o); }
373
- function formulaBlock(label, atoms) { const lines = [label + ' {']; for (const atom of atoms) lines.push(' ' + inputAtomToN3(atom) + ' .'); lines.push('}'); return lines.join('\n'); }
374
- function atomToTrig(atom, state) { if (atom.o && atom.o.kind === 'formula') { if (atom.p && atom.p.kind === 'iri' && atom.p.value === 'log:nameOf') { state.graphs.push(formulaBlock(inputTermToN3(atom.s), atom.o.atoms)); return null; } state.formulaCounter += 1; const label = `in:formula${state.formulaCounter}`; state.graphs.push(formulaBlock(label, atom.o.atoms)); return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + label + ' .'; } return inputAtomToN3(atom) + ' .'; }
375
- function inputFactsToTrig(facts) { const state = { formulaCounter: 0, graphs: [] }; const triples = []; for (const atom of facts) { const line = atomToTrig(atom, state); if (line) triples.push(line); } return { triples, graphs: state.graphs }; }
376
- function prefixLines(prefixes) { const merged = { ...(prefixes || {}) }; if (!Object.hasOwn(merged, 'log')) merged.log = 'http://www.w3.org/2000/10/swap/log#'; if (!Object.hasOwn(merged, 'see')) merged.see = 'https://example.org/see#'; if (!Object.hasOwn(merged, 'in')) merged.in = 'https://example.org/see/input#'; return Object.entries(merged).map(([prefix, iri]) => `@prefix ${prefix ? prefix + ':' : ':'} <${iri}> .`); }
377
- function generateInputTrig(n3Path, name, title, header, stats, program) { const { triples, graphs } = inputFactsToTrig(program.facts); const metadata = ['in:metadata {', ' in:run a see:InputDataset .', ` in:run see:name ${trigString(name)} .`, ` in:run see:title ${trigString(title)} .`, ` in:run see:sourceFile ${trigString(path.relative(ROOT, path.resolve(n3Path)))} .`, ` in:run see:sourceSHA256 ${trigString(stats.sourceHash)} .`, ` in:run see:description ${trigString(header.description || '')} .`, ' in:run see:compiler "see.js N3-to-JS compiler" .', ` in:run see:inputFacts ${stats.facts} .`, ` in:run see:compiledRules ${stats.rules} .`, ` in:run see:compiledBackwardRules ${stats.backwardRules} .`, ` in:run see:compiledFuses ${stats.fuses} .`, ` in:run see:compiledQueries ${stats.queries} .`, '}'].join('\n'); const sections = [...prefixLines(program.prefixes), '', '# Formal SEE input evidence in RDF 1.2 TriG.', '# The generated runner reads this TriG evidence directly.', '', triples.length ? triples.join('\n') : '# No source facts were present in the N3 program.']; if (graphs.length) sections.push('', graphs.join('\n\n')); sections.push('', metadata, ''); return sections.join('\n'); }
531
+ // Source facts are emitted as RDF 1.2 TriG. Formulas that appear as objects are
532
+ // lifted into named graphs so the generated runner can load evidence directly
533
+ // from .trig without going through an intermediate n3gen conversion step.
534
+ function trigString(value) {
535
+ return JSON.stringify(String(value));
536
+ }
537
+ function trigNumber(value) {
538
+ if (Object.is(value, -0)) return '0';
539
+ if (Number.isInteger(value)) return String(value);
540
+ return Number(value.toPrecision(15)).toString();
541
+ }
542
+ function inputLiteralToN3(value) {
543
+ if (typeof value === 'string') return trigString(value);
544
+ if (typeof value === 'number') return trigNumber(value);
545
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
546
+ return trigString(value);
547
+ }
548
+ function inputTermToN3(term) {
549
+ if (!term) return 'undefined';
550
+ if (term.kind === 'iri') return term.value;
551
+ if (term.kind === 'lit') return inputLiteralToN3(term.value);
552
+ if (term.kind === 'var') return '?' + term.value;
553
+ if (term.kind === 'blank') return term.value.startsWith('_:') ? term.value : '_:' + term.value.replace(/^_+/, '');
554
+ if (term.kind === 'list') return '(' + term.items.map(inputTermToN3).join(' ') + ')';
555
+ if (term.kind === 'triple') return '<<( ' + inputTermToN3(term.s) + ' ' + inputTermToN3(term.p) + ' ' + inputTermToN3(term.o) + ' )>>';
556
+ if (term.kind === 'formula') return '{ ' + term.atoms.map(inputAtomToN3).join(' . ') + ' }';
557
+ return String(term.value ?? term);
558
+ }
559
+ function inputAtomToN3(atom) {
560
+ return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + inputTermToN3(atom.o);
561
+ }
562
+ function inputTermHasTripleTerm(term) {
563
+ if (!term) return false;
564
+ if (term.kind === 'triple') return true;
565
+ if (term.kind === 'list') return term.items.some(inputTermHasTripleTerm);
566
+ if (term.kind === 'formula') return term.atoms.some(inputAtomHasTripleTerm);
567
+ return false;
568
+ }
569
+ function inputAtomHasTripleTerm(atom) {
570
+ return inputTermHasTripleTerm(atom.s) || inputTermHasTripleTerm(atom.p) || inputTermHasTripleTerm(atom.o);
571
+ }
572
+ function programHasTripleTerms(program) {
573
+ return [
574
+ ...(program.facts || []),
575
+ ...(program.rules || []).flatMap((r) => [...(r.body || []), ...(r.head || [])]),
576
+ ...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
577
+ ].some(inputAtomHasTripleTerm);
578
+ }
579
+ function formulaBlock(label, atoms) {
580
+ const lines = [label + ' {'];
581
+ for (const atom of atoms) lines.push(' ' + inputAtomToN3(atom) + ' .');
582
+ lines.push('}');
583
+ return lines.join('\n');
584
+ }
585
+ function atomToTrig(atom, state) {
586
+ if (atom.o && atom.o.kind === 'formula') {
587
+ if (atom.p && atom.p.kind === 'iri' && atom.p.value === 'log:nameOf') {
588
+ state.graphs.push(formulaBlock(inputTermToN3(atom.s), atom.o.atoms));
589
+ return null;
590
+ }
591
+ state.formulaCounter += 1;
592
+ const label = `in:formula${state.formulaCounter}`;
593
+ state.graphs.push(formulaBlock(label, atom.o.atoms));
594
+ return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + label + ' .';
595
+ }
596
+ return inputAtomToN3(atom) + ' .';
597
+ }
598
+ function inputFactsToTrig(facts) {
599
+ const state = { formulaCounter: 0, graphs: [] };
600
+ const triples = [];
601
+ for (const atom of facts) {
602
+ const line = atomToTrig(atom, state);
603
+ if (line) triples.push(line);
604
+ }
605
+ return { triples, graphs: state.graphs };
606
+ }
607
+ function prefixLines(prefixes) {
608
+ const merged = { ...(prefixes || {}) };
609
+ if (!Object.hasOwn(merged, 'log')) merged.log = 'http://www.w3.org/2000/10/swap/log#';
610
+ if (!Object.hasOwn(merged, 'see')) merged.see = 'https://example.org/see#';
611
+ if (!Object.hasOwn(merged, 'in')) merged.in = 'https://example.org/see/input#';
612
+ return Object.entries(merged).map(([prefix, iri]) => `@prefix ${prefix ? prefix + ':' : ':'} <${iri}> .`);
613
+ }
614
+ function generateInputTrig(n3Path, name, title, header, stats, program) {
615
+ const { triples, graphs } = inputFactsToTrig(program.facts);
616
+ const metadata = [
617
+ 'in:metadata {',
618
+ ' in:run a see:InputDataset .',
619
+ ` in:run see:name ${trigString(name)} .`,
620
+ ` in:run see:title ${trigString(title)} .`,
621
+ ` in:run see:sourceFile ${trigString(path.relative(ROOT, path.resolve(n3Path)))} .`,
622
+ ` in:run see:sourceSHA256 ${trigString(stats.sourceHash)} .`,
623
+ ` in:run see:description ${trigString(header.description || '')} .`,
624
+ ' in:run see:compiler "see.js N3-to-JS compiler" .',
625
+ ` in:run see:inputFacts ${stats.facts} .`,
626
+ ` in:run see:compiledRules ${stats.rules} .`,
627
+ ` in:run see:compiledBackwardRules ${stats.backwardRules} .`,
628
+ ` in:run see:compiledFuses ${stats.fuses} .`,
629
+ ` in:run see:compiledQueries ${stats.queries} .`,
630
+ '}',
631
+ ].join('\n');
632
+ const sections = [
633
+ ...(programHasTripleTerms(program) ? ['VERSION "1.2"', ''] : []),
634
+ ...prefixLines(program.prefixes),
635
+ '',
636
+ '# Formal SEE input evidence in RDF 1.2 TriG.',
637
+ '# The generated runner reads this TriG evidence directly.',
638
+ '',
639
+ triples.length ? triples.join('\n') : '# No source facts were present in the N3 program.',
640
+ ];
641
+ if (graphs.length) sections.push('', graphs.join('\n\n'));
642
+ sections.push('', metadata, '');
643
+ return sections.join('\n');
644
+ }
645
+ // The runtime below is copied verbatim into each generated example. Keep it
646
+ // dependency-light: generated examples should be executable with Node alone plus
647
+ // the local examples/_see.js TriG loader.
378
648
  function runtimeSource() {
379
649
  return String.raw`
380
650
  const crypto = require('crypto');
381
651
 
382
652
  function canonical(term) {
383
653
  if (term.kind === 'list') return ['list', term.items.map(canonical)];
654
+ if (term.kind === 'triple') return ['triple', canonical(term.s), canonical(term.p), canonical(term.o)];
384
655
  if (term.kind === 'formula') return ['formula', term.atoms.map((a) => [canonical(a.s), canonical(a.p), canonical(a.o)])];
385
656
  return [term.kind, term.value];
386
657
  }
@@ -390,6 +661,7 @@ function compoundIndexKey() { return Array.from(arguments).map(termIndexKey).joi
390
661
  function termIsConcrete(t) {
391
662
  if (!t || t.kind === 'var') return false;
392
663
  if (t.kind === 'list') return t.items.every(termIsConcrete);
664
+ if (t.kind === 'triple') return termIsConcrete(t.s) && termIsConcrete(t.p) && termIsConcrete(t.o);
393
665
  if (t.kind === 'formula') return t.atoms.every((a) => termIsConcrete(a.s) && termIsConcrete(a.p) && termIsConcrete(a.o));
394
666
  return true;
395
667
  }
@@ -405,6 +677,7 @@ function primitive(t) {
405
677
  if (t.kind === 'iri') return t.value.replace(/^:/, '');
406
678
  if (t.kind === 'blank') return t.value;
407
679
  if (t.kind === 'list') return t.items.map(primitive);
680
+ if (t.kind === 'triple') return termToN3(t);
408
681
  if (t.kind === 'formula') return termToN3(t);
409
682
  return undefined;
410
683
  }
@@ -425,6 +698,7 @@ function termToN3(t) {
425
698
  if (t.kind === 'var') return '?' + t.value;
426
699
  if (t.kind === 'blank') return t.value.startsWith('_:') ? t.value : '_:' + t.value.replace(/^_+/, '');
427
700
  if (t.kind === 'list') return '(' + t.items.map(termToN3).join(' ') + ')';
701
+ if (t.kind === 'triple') return '<<( ' + termToN3(t.s) + ' ' + termToN3(t.p) + ' ' + termToN3(t.o) + ' )>>';
428
702
  if (t.kind === 'formula') return '{ ' + t.atoms.map(atomToN3).join(' . ') + ' }';
429
703
  return String(t.value ?? t);
430
704
  }
@@ -447,6 +721,7 @@ function resolve(term, env, seen = new Set()) {
447
721
  return resolve(env[term.value], env, seen);
448
722
  }
449
723
  if (term.kind === 'list') return list(term.items.map((item) => resolve(item, env, seen)));
724
+ if (term.kind === 'triple') return { kind: 'triple', s: resolve(term.s, env), p: resolve(term.p, env), o: resolve(term.o, env) };
450
725
  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) })) };
451
726
  return term;
452
727
  }
@@ -464,6 +739,14 @@ function unify(a, b, env) {
464
739
  }
465
740
  return out;
466
741
  }
742
+ if (a.kind === 'triple' || b.kind === 'triple') {
743
+ if (a.kind !== 'triple' || b.kind !== 'triple') return null;
744
+ let out = unify(a.s, b.s, env);
745
+ if (!out) return null;
746
+ out = unify(a.p, b.p, out);
747
+ if (!out) return null;
748
+ return unify(a.o, b.o, out);
749
+ }
467
750
  return deepEqual(a, b) ? env : null;
468
751
  }
469
752
  function bind(pattern, value, env) { return unify(pattern, value, env); }
@@ -479,6 +762,7 @@ function termIsGround(t, env) {
479
762
  const r = resolve(t, env);
480
763
  if (r.kind === 'var') return false;
481
764
  if (r.kind === 'list') return r.items.every((item) => termIsGround(item, env));
765
+ if (r.kind === 'triple') return termIsGround(r.s, env) && termIsGround(r.p, env) && termIsGround(r.o, env);
482
766
  if (r.kind === 'formula') return r.atoms.every((atom) => atomIsGround(atom, env));
483
767
  return true;
484
768
  }
@@ -1038,6 +1322,7 @@ function instantiate(term, env, ruleId) {
1038
1322
  }
1039
1323
  if (term.kind === 'blank') return blank('_:r' + ruleId + '_' + envSignature(env) + '_' + term.value.replace(/^_/, ''));
1040
1324
  if (term.kind === 'list') return list(term.items.map((item) => instantiate(item, env, ruleId)));
1325
+ 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) };
1041
1326
  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) })) };
1042
1327
  return cloneTerm(term);
1043
1328
  }
@@ -1413,19 +1698,19 @@ function renderStructuredOutput({ title, graph, queries = [], rules = [], initia
1413
1698
  const lines = [];
1414
1699
  lines.push('# ' + title);
1415
1700
  lines.push('');
1416
- lines.push('## Insight');
1701
+ lines.push('## Entailment');
1417
1702
  if (mode === 'query') {
1418
1703
  lines.push('The compiled query selected ' + selected.length + ' fact(s) after the rule closure was computed.');
1419
1704
  } else if (mode === 'formula') {
1420
- lines.push('The derivation produced ' + selected.length + ' formula-valued conclusion(s).');
1705
+ lines.push('The derivation produced ' + selected.length + ' formula-valued entailment(s).');
1421
1706
  } else {
1422
1707
  lines.push('The derivation produced ' + derived.length + ' new fact(s) from ' + initialFacts.length + ' stated fact(s).');
1423
1708
  }
1424
- if (keyFact) lines.push('Main conclusion: **' + factSentence(keyFact) + '**');
1709
+ if (keyFact) lines.push('Main entailment: **' + factSentence(keyFact) + '**');
1425
1710
  const bullets = selected.slice(-6).reverse();
1426
1711
  if (bullets.length) {
1427
1712
  lines.push('');
1428
- lines.push('Selected conclusions:');
1713
+ lines.push('Selected entailments:');
1429
1714
  for (const fact of bullets) lines.push('- ' + codeFact(fact));
1430
1715
  }
1431
1716
  lines.push('');
@@ -1487,15 +1772,15 @@ function dedupeExplanationHeadings(text) {
1487
1772
  function normalizePublicReport(markdown, title) {
1488
1773
  let text = String(markdown || '').trimEnd();
1489
1774
  if (!/^\s*#\s+/m.test(text)) text = '# ' + title + '\n\n' + text;
1490
- if (!/^##\s+Insight\s*$/mi.test(text)) {
1491
- text = text.replace(/^(#\s+[^\n]+\n*)/, '$1\n## Insight\n');
1775
+ if (!/^##\s+Entailment\s*$/mi.test(text)) {
1776
+ text = text.replace(/^(#\s+[^\n]+\n*)/, '$1\n## Entailment\n');
1492
1777
  }
1493
1778
  if (!/^##\s+Explanation\s*$/mi.test(text)) {
1494
1779
  text += '\n\n## Explanation\nNo additional explanation was provided by the generated output.';
1495
1780
  }
1496
1781
  text = text.replace(/^##\s+([^\n]+?)\s*$/gm, (line, heading) => {
1497
1782
  const normalized = heading.trim().toLowerCase();
1498
- if (normalized === 'insight' || normalized === 'explanation') return '## ' + (normalized === 'insight' ? 'Insight' : 'Explanation');
1783
+ if (normalized === 'insight' || normalized === 'conclusion' || normalized === 'entailment' || normalized === 'explanation') return '## ' + (normalized === 'explanation' ? 'Explanation' : 'Entailment');
1499
1784
  return '**' + heading.trim() + '**';
1500
1785
  });
1501
1786
  text = dedupeExplanationHeadings(text);
@@ -1504,13 +1789,13 @@ function normalizePublicReport(markdown, title) {
1504
1789
  function markdownize(raw, title) {
1505
1790
  let text = String(raw || '');
1506
1791
  text = text
1507
- .replace(/===\s*Answer\s*===/g, '## Insight')
1792
+ .replace(/===\s*Answer\s*===/g, '## Entailment')
1508
1793
  .replace(/===\s*Reason\s+Why\s*===/gi, '## Explanation')
1509
1794
  .replace(/===\s*Explanation\s*===/gi, '## Explanation')
1510
1795
  .replace(/===\s*([^=]+?)\s*===/g, (_, h) => '**' + h.trim() + '**');
1511
1796
  text = text.replace(/^C(\d+)\s+OK\s*-\s*/gm, 'C$1: ');
1512
1797
  text = dedupeExplanationHeadings(text);
1513
- if (!text.trim()) text = '## Insight\nNo log:outputString facts were derived.\n\n## Explanation\nThe compiled derivation did not produce authored report text.';
1798
+ if (!text.trim()) text = '## Entailment\nNo log:outputString facts were derived.\n\n## Explanation\nThe compiled derivation did not produce authored report text.';
1514
1799
  return normalizePublicReport(text, title);
1515
1800
  }
1516
1801
  function authoredSupportAppendix(graph, queries, rules, initialFacts, trace) {
@@ -1565,10 +1850,17 @@ function renderPresentation(graph, queries, rules, initialFacts, title, trace) {
1565
1850
  `;
1566
1851
  }
1567
1852
 
1568
-
1569
1853
  function generateExampleJs(name, title, program, stats, doc) {
1570
- const rulesWithComments = program.rules.map((rule) => ({ ...rule, bodyComment: (rule.body || []).map(atomToComment), headComment: (rule.head || []).map(atomToComment) }));
1571
- const queriesWithComments = (program.queries || []).map((query) => ({ ...query, premiseComment: (query.premise || []).map(atomToComment), conclusionComment: (query.conclusion || []).map(atomToComment) }));
1854
+ const rulesWithComments = program.rules.map((rule) => ({
1855
+ ...rule,
1856
+ bodyComment: (rule.body || []).map(atomToComment),
1857
+ headComment: (rule.head || []).map(atomToComment),
1858
+ }));
1859
+ const queriesWithComments = (program.queries || []).map((query) => ({
1860
+ ...query,
1861
+ premiseComment: (query.premise || []).map(atomToComment),
1862
+ conclusionComment: (query.conclusion || []).map(atomToComment),
1863
+ }));
1572
1864
  return `#!/usr/bin/env node
1573
1865
  'use strict';
1574
1866
  const fs = require('fs');
@@ -1614,6 +1906,16 @@ function formalOutputFacts(graph, queries, rules, initialFacts) {
1614
1906
  }
1615
1907
  return out;
1616
1908
  }
1909
+ function termHasTripleTerm(term) {
1910
+ if (!term) return false;
1911
+ if (term.kind === 'triple') return true;
1912
+ if (term.kind === 'list') return term.items.some(termHasTripleTerm);
1913
+ if (term.kind === 'formula') return term.atoms.some(atomHasTripleTerm);
1914
+ return false;
1915
+ }
1916
+ function atomHasTripleTerm(atom) { return termHasTripleTerm(atom.s) || termHasTripleTerm(atom.p) || termHasTripleTerm(atom.o); }
1917
+ function factsHaveTripleTerms(facts) { return (facts || []).some(atomHasTripleTerm); }
1918
+ function trigHasVersion12(trig) { return /^\s*(?:@version|VERSION)\s+["']1\.2["']/mi.test(String(trig || '')); }
1617
1919
  function trigGraphBlock(label, atoms) {
1618
1920
  const lines = [label + ' {'];
1619
1921
  for (const atom of atoms || []) lines.push(' ' + atomToN3(atom) + ' .');
@@ -1661,7 +1963,8 @@ function formalOutputToTrig(facts, trig) {
1661
1963
  const prefixes = prefixLinesFromTrig(trig);
1662
1964
  if (state.needOutPrefix && !prefixes.some((line) => line.toLowerCase().startsWith('@prefix out:'))) prefixes.push('@prefix out: <https://example.org/see/output#> .');
1663
1965
  const nl = String.fromCharCode(10);
1664
- return prefixes.join(nl) + nl + nl + body.join(nl);
1966
+ const version = factsHaveTripleTerms(facts) ? 'VERSION "1.2"' + nl + nl : '';
1967
+ return version + prefixes.join(nl) + nl + nl + body.join(nl);
1665
1968
  }
1666
1969
  function appendFormalTrigOutput(markdown, graph, queries, rules, initialFacts, data) {
1667
1970
  const trig = formalOutputToTrig(formalOutputFacts(graph, queries, rules, initialFacts), data && data.trig);
@@ -1680,19 +1983,43 @@ module.exports = { trustedDerivation, outputMarkdown, documentationMarkdown, wri
1680
1983
  `;
1681
1984
  }
1682
1985
 
1683
-
1684
- function generateDoc(name, title, header, stats) { const description = header.description ? `
1986
+ // Documentation is generated from compilation metadata rather than hand-written
1987
+ // per example, keeping examples/output and examples/doc reproducible snapshots.
1988
+ function generateDoc(name, title, header, stats) {
1989
+ const description = header.description
1990
+ ? `
1685
1991
  ${header.description}
1686
- ` : ''; const builtins = stats.builtins.length ? stats.builtins.map((b) => `- \`${b}\``).join('\n') : '- none'; return `# ${title}\n\nGenerated by \`see.js\` from a Notation3 source file.\n${description}\n## Compilation summary\n\n- Example name: \`${name}\`\n- Input facts emitted: ${stats.facts}\n- Forward rules compiled: ${stats.rules}\n- Backward predicate rules compiled: ${stats.backwardRules}\n- Fuses compiled: ${stats.fuses}\n- Predicate count: ${stats.predicates}\n\n## Built-ins used\n\n${builtins}\n\n## Runtime model\n\nThe generated \`examples/${name}.js\` is a specialized JavaScript derivation program. For ordinary sources, \`see.js\` emits the source facts as \`examples/input/${name}.trig\`. For rules-only sources, generation can reuse an existing external evidence file such as \`examples/input/${name.replace(/_/g, '-')}.trig\` or \`examples/input/${name}.trig\`. The runner reads that TriG evidence directly and performs a local fixpoint derivation; it does not parse the program source or call an external reasoner.\n\n## Output model\n\nRunning \`node examples/${name}.js\` produces a SEE-style Markdown report with an **Insight** section, an **Explanation** section, and a **Formal TriG Output** section containing the selected derived/query facts.\n`; }
1992
+ `
1993
+ : '';
1994
+ const builtins = stats.builtins.length ? stats.builtins.map((b) => `- \`${b}\``).join('\n') : '- none';
1995
+ return `# ${title}\n\nGenerated by \`see.js\` from a Notation3 source file.\n${description}\n## Compilation summary\n\n- Example name: \`${name}\`\n- Input facts emitted: ${stats.facts}\n- Forward rules compiled: ${stats.rules}\n- Backward predicate rules compiled: ${stats.backwardRules}\n- Fuses compiled: ${stats.fuses}\n- Predicate count: ${stats.predicates}\n\n## Built-ins used\n\n${builtins}\n\n## Runtime model\n\nThe generated \`examples/${name}.js\` is a specialized JavaScript derivation program. For ordinary sources, \`see.js\` emits the source facts as \`examples/input/${name}.trig\`. For rules-only sources, generation can reuse an existing external evidence file such as \`examples/input/${name.replace(/_/g, '-')}.trig\` or \`examples/input/${name}.trig\`. The runner reads that TriG evidence directly and performs a local fixpoint derivation; it does not parse the program source or call an external reasoner.\n\n## Output model\n\nRunning \`node examples/${name}.js\` produces a SEE-style Markdown report with an **Entailment** section, an **Explanation** section, and a **Formal TriG Output** section containing the selected derived/query facts.\n`;
1996
+ }
1687
1997
 
1688
1998
  function runNode(file, cwd = ROOT, args = []) {
1689
1999
  const result = spawnSync(process.execPath, [file, ...args], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
1690
- if (result.status !== 0) throw new Error(`generated example failed:
2000
+ if (result.status !== 0)
2001
+ throw new Error(`generated example failed:
1691
2002
  ${result.stderr || result.stdout}`);
1692
2003
  return result.stdout;
1693
2004
  }
1694
2005
 
1695
- function compile(n3Path, options = {}) { const absolute = path.resolve(n3Path); const n3 = readText(absolute); const name = options.name || slugify(path.basename(absolute)); const title = parseHeader(n3, titleFromSlug(name)).title; const header = parseHeader(n3, titleFromSlug(name)); const program = parseN3(n3); const stats = compilationStats(program); stats.sourceHash = sha256(n3); const inputTrig = generateInputTrig(absolute, name, title, header, stats, program); const doc = generateDoc(name, title, header, stats); const exampleJs = generateExampleJs(name, title, program, stats, doc); return { name, title, program, stats, inputTrig, exampleJs, doc }; }
2006
+ // compile is pure with respect to the repository: it reads one source .n3 file
2007
+ // and returns all generated text. The generate/render commands decide whether
2008
+ // those artefacts are written to disk or executed from a temporary directory.
2009
+ function compile(n3Path, options = {}) {
2010
+ const absolute = path.resolve(n3Path);
2011
+ const n3 = readText(absolute);
2012
+ const name = options.name || slugify(path.basename(absolute));
2013
+ const title = parseHeader(n3, titleFromSlug(name)).title;
2014
+ const header = parseHeader(n3, titleFromSlug(name));
2015
+ const program = parseN3(n3);
2016
+ const stats = compilationStats(program);
2017
+ stats.sourceHash = sha256(n3);
2018
+ const inputTrig = generateInputTrig(absolute, name, title, header, stats, program);
2019
+ const doc = generateDoc(name, title, header, stats);
2020
+ const exampleJs = generateExampleJs(name, title, program, stats, doc);
2021
+ return { name, title, program, stats, inputTrig, exampleJs, doc };
2022
+ }
1696
2023
 
1697
2024
  function inputNameCandidates(name) {
1698
2025
  const out = [name];
@@ -1714,19 +2041,21 @@ function inputCandidateScore(file) {
1714
2041
  return { facts: -1, size: -1 };
1715
2042
  }
1716
2043
  }
2044
+ // Rules-only examples can reuse an externally authored TriG evidence file. The
2045
+ // scoring prefers the candidate that advertises the most input facts, then the
2046
+ // larger file, so dashed public datasets such as path-discovery.trig win over
2047
+ // empty generated placeholders.
1717
2048
  function existingExternalInputName(name) {
1718
2049
  const candidates = inputNameCandidates(name)
1719
2050
  .map((base, order) => ({ base, order, file: path.join(INPUT_DIR, `${base}.trig`) }))
1720
2051
  .filter((c) => fs.existsSync(c.file))
1721
2052
  .map((c) => ({ ...c, score: inputCandidateScore(c.file) }));
1722
2053
  if (!candidates.length) return null;
1723
- candidates.sort((a, b) =>
1724
- (b.score.facts - a.score.facts) ||
1725
- (b.score.size - a.score.size) ||
1726
- (a.order - b.order)
1727
- );
2054
+ candidates.sort((a, b) => b.score.facts - a.score.facts || b.score.size - a.score.size || a.order - b.order);
1728
2055
  return candidates[0].base;
1729
2056
  }
2057
+ // generate writes the checked-in artefacts and immediately executes the new
2058
+ // example with --write so examples/output and examples/doc remain in sync.
1730
2059
  function generate(n3Path, options = {}) {
1731
2060
  const compiled = compile(n3Path, options);
1732
2061
  const jsFile = path.join(EXAMPLES_DIR, `${compiled.name}.js`);
@@ -1738,7 +2067,8 @@ function generate(n3Path, options = {}) {
1738
2067
  if (!options.force) {
1739
2068
  const protectedInputs = externalInputName ? [] : [inputTrigFile];
1740
2069
  for (const file of [outputFile, docFile, ...protectedInputs]) {
1741
- if (fs.existsSync(file)) throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
2070
+ if (fs.existsSync(file))
2071
+ throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
1742
2072
  }
1743
2073
  }
1744
2074
  writeText(jsFile, compiled.exampleJs, options.force);
@@ -1749,7 +2079,27 @@ function generate(n3Path, options = {}) {
1749
2079
  return { ...compiled, files: { jsFile, inputTrigFile, outputFile, docFile }, output };
1750
2080
  }
1751
2081
 
1752
- function render(n3Path) { const tmpName = `_see_tmp_${process.pid}`; const compiled = compile(n3Path, { name: tmpName }); const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'see-compile-')); const tmpSeeDir = path.join(tmpDir, 'see'); const examplesDir = path.join(tmpSeeDir, 'examples'); ensureDir(path.join(examplesDir, 'input')); fs.copyFileSync(path.join(EXAMPLES_DIR, '_see.js'), path.join(examplesDir, '_see.js')); fs.copyFileSync(path.join(ROOT, 'see.js'), path.join(tmpSeeDir, 'see.js')); const jsFile = path.join(examplesDir, `${tmpName}.js`); const trigFile = path.join(examplesDir, 'input', `${tmpName}.trig`); fs.writeFileSync(jsFile, compiled.exampleJs, 'utf8'); fs.writeFileSync(trigFile, compiled.inputTrig, 'utf8'); try { return runNode(jsFile, tmpSeeDir); } finally { fs.rmSync(tmpDir, { recursive: true, force: true }); } }
2082
+ // render is the non-mutating companion to generate. It compiles into a small
2083
+ // temporary /see-shaped tree, runs the generated example, and returns Markdown.
2084
+ function render(n3Path) {
2085
+ const tmpName = `_see_tmp_${process.pid}`;
2086
+ const compiled = compile(n3Path, { name: tmpName });
2087
+ const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'see-compile-'));
2088
+ const tmpSeeDir = path.join(tmpDir, 'see');
2089
+ const examplesDir = path.join(tmpSeeDir, 'examples');
2090
+ ensureDir(path.join(examplesDir, 'input'));
2091
+ fs.copyFileSync(path.join(EXAMPLES_DIR, '_see.js'), path.join(examplesDir, '_see.js'));
2092
+ fs.copyFileSync(path.join(ROOT, 'see.js'), path.join(tmpSeeDir, 'see.js'));
2093
+ const jsFile = path.join(examplesDir, `${tmpName}.js`);
2094
+ const trigFile = path.join(examplesDir, 'input', `${tmpName}.trig`);
2095
+ fs.writeFileSync(jsFile, compiled.exampleJs, 'utf8');
2096
+ fs.writeFileSync(trigFile, compiled.inputTrig, 'utf8');
2097
+ try {
2098
+ return runNode(jsFile, tmpSeeDir);
2099
+ } finally {
2100
+ fs.rmSync(tmpDir, { recursive: true, force: true });
2101
+ }
2102
+ }
1753
2103
 
1754
2104
  function parseArgs(argv) {
1755
2105
  const args = [...argv];
@@ -1767,7 +2117,10 @@ function parseArgs(argv) {
1767
2117
 
1768
2118
  function main() {
1769
2119
  const { command, file, opts } = parseArgs(process.argv.slice(2));
1770
- if (!command || command === 'help' || command === '--help') { console.log(usage()); return; }
2120
+ if (!command || command === 'help' || command === '--help') {
2121
+ console.log(usage());
2122
+ return;
2123
+ }
1771
2124
  if (!file) throw new Error(`Missing <example.n3>\n\n${usage()}`);
1772
2125
  if (command === 'generate') {
1773
2126
  const result = generate(file, opts);
@@ -1775,20 +2128,28 @@ function main() {
1775
2128
  if (result.files.inputTrigFile) console.log(`generated ${path.relative(ROOT, result.files.inputTrigFile)}`);
1776
2129
  console.log(`generated ${path.relative(ROOT, result.files.outputFile)}`);
1777
2130
  console.log(`generated ${path.relative(ROOT, result.files.docFile)}`);
1778
- console.log(`compiled ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`);
2131
+ console.log(
2132
+ `compiled ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`,
2133
+ );
1779
2134
  } else if (command === 'render') {
1780
2135
  process.stdout.write(render(file));
1781
2136
  } else if (command === 'inspect') {
1782
2137
  const result = compile(file, opts);
1783
- console.log(`OK ${result.name}: ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`);
2138
+ console.log(
2139
+ `OK ${result.name}: ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`,
2140
+ );
1784
2141
  } else {
1785
2142
  throw new Error(`Unknown command: ${command}\n\n${usage()}`);
1786
2143
  }
1787
2144
  }
1788
2145
 
1789
2146
  if (require.main === module) {
1790
- try { main(); }
1791
- catch (err) { console.error(err.stack || err.message); process.exit(1); }
2147
+ try {
2148
+ main();
2149
+ } catch (err) {
2150
+ console.error(err.stack || err.message);
2151
+ process.exit(1);
2152
+ }
1792
2153
  }
1793
2154
 
1794
2155
  module.exports = { compile, generate, parseN3, render, tokenize };