eyeling 1.24.4 → 1.24.6

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 (301) hide show
  1. package/HANDBOOK.md +2 -101
  2. package/README.md +2 -2
  3. package/dist/browser/eyeling.browser.js +256 -4
  4. package/examples/annotation.n3 +3 -4
  5. package/{see/examples/n3/backward_recursion.n3 → examples/backward-recursion.n3} +0 -2
  6. package/examples/collection.n3 +1 -2
  7. package/examples/context-association.n3 +56 -30
  8. package/{see/examples/n3/dijkstra_risk_path.n3 → examples/dijkstra-risk-path.n3} +3 -18
  9. package/{see/examples/n3/eco_route_insight.n3 → examples/eco-route-insight.n3} +2 -47
  10. package/{see/examples/n3/genetic_knapsack_selection.n3 → examples/genetic-knapsack-selection.n3} +2 -24
  11. package/{see/examples → examples}/input/annotation.trig +3 -3
  12. package/{see/examples/input/backward_recursion.trig → examples/input/backward-recursion.trig} +2 -2
  13. package/{see/examples/input/builtin_coverage.trig → examples/input/builtin-coverage.trig} +2 -2
  14. package/{see/examples → examples}/input/collection.trig +3 -3
  15. package/{see/examples/input/dijkstra_risk_path.trig → examples/input/dijkstra-risk-path.trig} +4 -4
  16. package/{see/examples/input/eco_route_insight.trig → examples/input/eco-route-insight.trig} +4 -4
  17. package/{see/examples/input/genetic_knapsack_selection.trig → examples/input/genetic-knapsack-selection.trig} +4 -4
  18. package/{see/examples/input/rc_discharge_envelope.trig → examples/input/rc-discharge-envelope.trig} +4 -4
  19. package/{see/examples/input/rdf_dataset.trig → examples/input/rdf-dataset.trig} +4 -4
  20. package/{see/examples/input/rdf_message_flow.trig → examples/input/rdf-message-flow.trig} +3 -3
  21. package/{see/examples/input/rdf_messages.trig → examples/input/rdf-messages.trig} +4 -4
  22. package/{see/examples/input/school_placement_audit.trig → examples/input/school-placement-audit.trig} +4 -4
  23. package/{see/examples/input/smoke_arithmetic.trig → examples/input/smoke-arithmetic.trig} +3 -3
  24. package/{see/examples/input/triple_terms.trig → examples/input/triple-terms.trig} +3 -3
  25. package/examples/output/backward-recursion.n3 +4 -0
  26. package/examples/output/context-association.n3 +9 -0
  27. package/examples/output/dijkstra-risk-path.n3 +3 -0
  28. package/examples/output/eco-route-insight.n3 +3 -0
  29. package/examples/output/genetic-knapsack-selection.n3 +3 -0
  30. package/examples/output/rc-discharge-envelope.n3 +9 -0
  31. package/examples/output/rc-discharge-envelope.txt +9 -0
  32. package/examples/output/rdf-dataset.n3 +5 -0
  33. package/examples/output/rdf-message-flow.n3 +7 -0
  34. package/examples/output/rdf-messages.n3 +7 -0
  35. package/examples/output/school-placement-audit.n3 +3 -0
  36. package/examples/output/smoke-arithmetic.n3 +5 -0
  37. package/examples/output/smoke-arithmetic.txt +5 -0
  38. package/examples/output/triple-terms.n3 +5 -0
  39. package/{see/examples/n3/rc_discharge_envelope.n3 → examples/rc-discharge-envelope.n3} +2 -15
  40. package/{see/examples/n3/rdf_dataset.n3 → examples/rdf-dataset.n3} +2 -11
  41. package/{see/examples/n3/rdf_message_flow.n3 → examples/rdf-message-flow.n3} +9 -75
  42. package/{see/examples/n3/rdf_messages.n3 → examples/rdf-messages.n3} +6 -43
  43. package/{see/examples/n3/school_placement_audit.n3 → examples/school-placement-audit.n3} +2 -14
  44. package/{see/examples/n3/smoke_arithmetic.n3 → examples/smoke-arithmetic.n3} +3 -5
  45. package/{see/examples/n3/triple_terms.n3 → examples/triple-terms.n3} +1 -4
  46. package/eyeling.js +256 -4
  47. package/lib/builtins.js +6 -0
  48. package/lib/lexer.js +250 -4
  49. package/package.json +4 -7
  50. package/test/api.test.js +65 -8
  51. package/test/examples.test.js +22 -2
  52. package/test/package.test.js +16 -2
  53. package/examples/arcling/README.md +0 -11
  54. package/examples/input/annotation.ttl +0 -6
  55. package/examples/input/collection.ttl +0 -13
  56. package/examples/input/reifies.ttl +0 -10
  57. package/examples/input/triple-term.ttl +0 -8
  58. package/examples/output/triple-term.n3 +0 -0
  59. package/examples/reifies.n3 +0 -8
  60. package/examples/triple-term.n3 +0 -7
  61. package/see/README.md +0 -149
  62. package/see/examples/_see.js +0 -249
  63. package/see/examples/age.js +0 -1459
  64. package/see/examples/annotation.js +0 -1320
  65. package/see/examples/backward.js +0 -1405
  66. package/see/examples/backward_recursion.js +0 -1504
  67. package/see/examples/bayes_diagnosis.js +0 -2883
  68. package/see/examples/bayes_therapy.js +0 -4152
  69. package/see/examples/bmi.js +0 -3038
  70. package/see/examples/builtin_coverage.js +0 -2524
  71. package/see/examples/collection.js +0 -1320
  72. package/see/examples/complex.js +0 -3762
  73. package/see/examples/complex_matrix_stability.js +0 -2973
  74. package/see/examples/composition_of_injective_functions_is_injective.js +0 -2170
  75. package/see/examples/control_system.js +0 -1918
  76. package/see/examples/crypto_builtins_tests.js +0 -1489
  77. package/see/examples/delfour.js +0 -3174
  78. package/see/examples/digital_product_passport.js +0 -2856
  79. package/see/examples/dijkstra.js +0 -2070
  80. package/see/examples/dijkstra_risk_path.js +0 -1874
  81. package/see/examples/doc/age.md +0 -27
  82. package/see/examples/doc/annotation.md +0 -24
  83. package/see/examples/doc/backward.md +0 -26
  84. package/see/examples/doc/backward_recursion.md +0 -26
  85. package/see/examples/doc/bayes_diagnosis.md +0 -41
  86. package/see/examples/doc/bayes_therapy.md +0 -40
  87. package/see/examples/doc/bmi.md +0 -38
  88. package/see/examples/doc/builtin_coverage.md +0 -53
  89. package/see/examples/doc/collection.md +0 -24
  90. package/see/examples/doc/complex.md +0 -38
  91. package/see/examples/doc/complex_matrix_stability.md +0 -35
  92. package/see/examples/doc/composition_of_injective_functions_is_injective.md +0 -24
  93. package/see/examples/doc/control_system.md +0 -32
  94. package/see/examples/doc/crypto_builtins_tests.md +0 -27
  95. package/see/examples/doc/delfour.md +0 -37
  96. package/see/examples/doc/digital_product_passport.md +0 -36
  97. package/see/examples/doc/dijkstra.md +0 -28
  98. package/see/examples/doc/dijkstra_risk_path.md +0 -30
  99. package/see/examples/doc/dog.md +0 -28
  100. package/see/examples/doc/eco_route_insight.md +0 -33
  101. package/see/examples/doc/equals.md +0 -26
  102. package/see/examples/doc/equivalence_classes_overlap_implies_same_class.md +0 -24
  103. package/see/examples/doc/euler_identity.md +0 -39
  104. package/see/examples/doc/ev_roundtrip_planner.md +0 -32
  105. package/see/examples/doc/existential_rule.md +0 -24
  106. package/see/examples/doc/expression_eval.md +0 -26
  107. package/see/examples/doc/family_cousins.md +0 -24
  108. package/see/examples/doc/fastpow.md +0 -29
  109. package/see/examples/doc/fibonacci.md +0 -28
  110. package/see/examples/doc/french_cities.md +0 -28
  111. package/see/examples/doc/fundamental_theorem_arithmetic.md +0 -36
  112. package/see/examples/doc/genetic_knapsack_selection.md +0 -29
  113. package/see/examples/doc/goldbach_1000.md +0 -31
  114. package/see/examples/doc/good_cobbler.md +0 -27
  115. package/see/examples/doc/gps.md +0 -35
  116. package/see/examples/doc/gray_code_counter.md +0 -31
  117. package/see/examples/doc/greatest_lower_bound_uniqueness.md +0 -24
  118. package/see/examples/doc/group_inverse_uniqueness.md +0 -24
  119. package/see/examples/doc/hadamard_approx.md +0 -32
  120. package/see/examples/doc/hanoi.md +0 -26
  121. package/see/examples/doc/odrl_dpv_risk_ranked.md +0 -57
  122. package/see/examples/doc/path_discovery.md +0 -33
  123. package/see/examples/doc/rc_discharge_envelope.md +0 -33
  124. package/see/examples/doc/rdf_dataset.md +0 -26
  125. package/see/examples/doc/rdf_message_flow.md +0 -35
  126. package/see/examples/doc/rdf_messages.md +0 -37
  127. package/see/examples/doc/school_placement_audit.md +0 -31
  128. package/see/examples/doc/smoke_arithmetic.md +0 -31
  129. package/see/examples/doc/socrates.md +0 -24
  130. package/see/examples/doc/triple_terms.md +0 -26
  131. package/see/examples/doc/wind_turbine.md +0 -37
  132. package/see/examples/doc/witch.md +0 -28
  133. package/see/examples/dog.js +0 -1436
  134. package/see/examples/eco_route_insight.js +0 -2110
  135. package/see/examples/equals.js +0 -1363
  136. package/see/examples/equivalence_classes_overlap_implies_same_class.js +0 -1792
  137. package/see/examples/euler_identity.js +0 -2038
  138. package/see/examples/ev_roundtrip_planner.js +0 -2562
  139. package/see/examples/existential_rule.js +0 -1363
  140. package/see/examples/expression_eval.js +0 -1798
  141. package/see/examples/family_cousins.js +0 -1586
  142. package/see/examples/fastpow.js +0 -2207
  143. package/see/examples/fibonacci.js +0 -1594
  144. package/see/examples/french_cities.js +0 -1492
  145. package/see/examples/fundamental_theorem_arithmetic.js +0 -2106
  146. package/see/examples/genetic_knapsack_selection.js +0 -1743
  147. package/see/examples/goldbach_1000.js +0 -1798
  148. package/see/examples/good_cobbler.js +0 -1396
  149. package/see/examples/gps.js +0 -2813
  150. package/see/examples/gray_code_counter.js +0 -1641
  151. package/see/examples/greatest_lower_bound_uniqueness.js +0 -1918
  152. package/see/examples/group_inverse_uniqueness.js +0 -1897
  153. package/see/examples/hadamard_approx.js +0 -4417
  154. package/see/examples/hanoi.js +0 -1625
  155. package/see/examples/input/age.trig +0 -27
  156. package/see/examples/input/backward.trig +0 -25
  157. package/see/examples/input/bayes_diagnosis.trig +0 -111
  158. package/see/examples/input/bayes_therapy.trig +0 -130
  159. package/see/examples/input/bmi.trig +0 -28
  160. package/see/examples/input/complex.trig +0 -26
  161. package/see/examples/input/complex_matrix_stability.trig +0 -65
  162. package/see/examples/input/composition_of_injective_functions_is_injective.trig +0 -35
  163. package/see/examples/input/control_system.trig +0 -31
  164. package/see/examples/input/crypto_builtins_tests.trig +0 -25
  165. package/see/examples/input/delfour.trig +0 -90
  166. package/see/examples/input/digital_product_passport.trig +0 -116
  167. package/see/examples/input/dijkstra.trig +0 -34
  168. package/see/examples/input/dog.trig +0 -31
  169. package/see/examples/input/equals.trig +0 -25
  170. package/see/examples/input/equivalence_classes_overlap_implies_same_class.trig +0 -28
  171. package/see/examples/input/euler_identity.trig +0 -34
  172. package/see/examples/input/ev_roundtrip_planner.trig +0 -90
  173. package/see/examples/input/existential_rule.trig +0 -26
  174. package/see/examples/input/expression_eval.trig +0 -41
  175. package/see/examples/input/family_cousins.trig +0 -39
  176. package/see/examples/input/fastpow.trig +0 -25
  177. package/see/examples/input/fibonacci.trig +0 -51
  178. package/see/examples/input/french_cities.trig +0 -38
  179. package/see/examples/input/fundamental_theorem_arithmetic.trig +0 -42
  180. package/see/examples/input/goldbach_1000.trig +0 -53
  181. package/see/examples/input/good_cobbler.trig +0 -24
  182. package/see/examples/input/gps.trig +0 -35
  183. package/see/examples/input/gray_code_counter.trig +0 -33
  184. package/see/examples/input/greatest_lower_bound_uniqueness.trig +0 -29
  185. package/see/examples/input/group_inverse_uniqueness.trig +0 -29
  186. package/see/examples/input/hadamard_approx.trig +0 -32
  187. package/see/examples/input/hanoi.trig +0 -26
  188. package/see/examples/input/odrl_dpv_risk_ranked.trig +0 -107
  189. package/see/examples/input/path-discovery.trig +0 -96448
  190. package/see/examples/input/path_discovery.trig +0 -29
  191. package/see/examples/input/socrates.trig +0 -26
  192. package/see/examples/input/wind_turbine.trig +0 -48
  193. package/see/examples/input/witch.trig +0 -26
  194. package/see/examples/n3/age.n3 +0 -28
  195. package/see/examples/n3/annotation.n3 +0 -7
  196. package/see/examples/n3/backward.n3 +0 -22
  197. package/see/examples/n3/bayes_diagnosis.n3 +0 -122
  198. package/see/examples/n3/bayes_therapy.n3 +0 -149
  199. package/see/examples/n3/bmi.n3 +0 -145
  200. package/see/examples/n3/collection.n3 +0 -3
  201. package/see/examples/n3/complex.n3 +0 -140
  202. package/see/examples/n3/complex_matrix_stability.n3 +0 -113
  203. package/see/examples/n3/composition_of_injective_functions_is_injective.n3 +0 -27
  204. package/see/examples/n3/control_system.n3 +0 -59
  205. package/see/examples/n3/crypto_builtins_tests.n3 +0 -18
  206. package/see/examples/n3/delfour.n3 +0 -167
  207. package/see/examples/n3/digital_product_passport.n3 +0 -156
  208. package/see/examples/n3/dijkstra.n3 +0 -46
  209. package/see/examples/n3/dog.n3 +0 -20
  210. package/see/examples/n3/equals.n3 +0 -11
  211. package/see/examples/n3/equivalence_classes_overlap_implies_same_class.n3 +0 -19
  212. package/see/examples/n3/euler_identity.n3 +0 -41
  213. package/see/examples/n3/ev_roundtrip_planner.n3 +0 -82
  214. package/see/examples/n3/existential_rule.n3 +0 -10
  215. package/see/examples/n3/expression_eval.n3 +0 -21
  216. package/see/examples/n3/family_cousins.n3 +0 -62
  217. package/see/examples/n3/fastpow.n3 +0 -56
  218. package/see/examples/n3/fibonacci.n3 +0 -44
  219. package/see/examples/n3/french_cities.n3 +0 -28
  220. package/see/examples/n3/fundamental_theorem_arithmetic.n3 +0 -84
  221. package/see/examples/n3/goldbach_1000.n3 +0 -66
  222. package/see/examples/n3/good_cobbler.n3 +0 -10
  223. package/see/examples/n3/gps.n3 +0 -70
  224. package/see/examples/n3/gray_code_counter.n3 +0 -53
  225. package/see/examples/n3/greatest_lower_bound_uniqueness.n3 +0 -20
  226. package/see/examples/n3/group_inverse_uniqueness.n3 +0 -19
  227. package/see/examples/n3/hadamard_approx.n3 +0 -43
  228. package/see/examples/n3/hanoi.n3 +0 -16
  229. package/see/examples/n3/odrl_dpv_risk_ranked.n3 +0 -460
  230. package/see/examples/n3/path_discovery.n3 +0 -43
  231. package/see/examples/n3/socrates.n3 +0 -21
  232. package/see/examples/n3/wind_turbine.n3 +0 -85
  233. package/see/examples/n3/witch.n3 +0 -30
  234. package/see/examples/odrl_dpv_risk_ranked.js +0 -5128
  235. package/see/examples/output/age.md +0 -48
  236. package/see/examples/output/annotation.md +0 -43
  237. package/see/examples/output/backward.md +0 -50
  238. package/see/examples/output/backward_recursion.md +0 -54
  239. package/see/examples/output/bayes_diagnosis.md +0 -103
  240. package/see/examples/output/bayes_therapy.md +0 -84
  241. package/see/examples/output/bmi.md +0 -164
  242. package/see/examples/output/builtin_coverage.md +0 -99
  243. package/see/examples/output/collection.md +0 -44
  244. package/see/examples/output/complex.md +0 -61
  245. package/see/examples/output/complex_matrix_stability.md +0 -55
  246. package/see/examples/output/composition_of_injective_functions_is_injective.md +0 -62
  247. package/see/examples/output/control_system.md +0 -61
  248. package/see/examples/output/crypto_builtins_tests.md +0 -68
  249. package/see/examples/output/delfour.md +0 -100
  250. package/see/examples/output/digital_product_passport.md +0 -100
  251. package/see/examples/output/dijkstra.md +0 -74
  252. package/see/examples/output/dijkstra_risk_path.md +0 -76
  253. package/see/examples/output/dog.md +0 -50
  254. package/see/examples/output/eco_route_insight.md +0 -88
  255. package/see/examples/output/equals.md +0 -50
  256. package/see/examples/output/equivalence_classes_overlap_implies_same_class.md +0 -86
  257. package/see/examples/output/euler_identity.md +0 -73
  258. package/see/examples/output/ev_roundtrip_planner.md +0 -79
  259. package/see/examples/output/existential_rule.md +0 -54
  260. package/see/examples/output/expression_eval.md +0 -50
  261. package/see/examples/output/family_cousins.md +0 -187
  262. package/see/examples/output/fastpow.md +0 -36
  263. package/see/examples/output/fibonacci.md +0 -53
  264. package/see/examples/output/french_cities.md +0 -70
  265. package/see/examples/output/fundamental_theorem_arithmetic.md +0 -101
  266. package/see/examples/output/genetic_knapsack_selection.md +0 -66
  267. package/see/examples/output/goldbach_1000.md +0 -58
  268. package/see/examples/output/good_cobbler.md +0 -54
  269. package/see/examples/output/gps.md +0 -102
  270. package/see/examples/output/gray_code_counter.md +0 -68
  271. package/see/examples/output/greatest_lower_bound_uniqueness.md +0 -60
  272. package/see/examples/output/group_inverse_uniqueness.md +0 -60
  273. package/see/examples/output/hadamard_approx.md +0 -510
  274. package/see/examples/output/hanoi.md +0 -51
  275. package/see/examples/output/odrl_dpv_risk_ranked.md +0 -139
  276. package/see/examples/output/path_discovery.md +0 -65
  277. package/see/examples/output/rc_discharge_envelope.md +0 -102
  278. package/see/examples/output/rdf_dataset.md +0 -54
  279. package/see/examples/output/rdf_message_flow.md +0 -198
  280. package/see/examples/output/rdf_messages.md +0 -134
  281. package/see/examples/output/school_placement_audit.md +0 -99
  282. package/see/examples/output/smoke_arithmetic.md +0 -54
  283. package/see/examples/output/socrates.md +0 -55
  284. package/see/examples/output/triple_terms.md +0 -53
  285. package/see/examples/output/wind_turbine.md +0 -108
  286. package/see/examples/output/witch.md +0 -87
  287. package/see/examples/path_discovery.js +0 -1774
  288. package/see/examples/rc_discharge_envelope.js +0 -1993
  289. package/see/examples/rdf_dataset.js +0 -1512
  290. package/see/examples/rdf_message_flow.js +0 -2580
  291. package/see/examples/rdf_messages.js +0 -2176
  292. package/see/examples/school_placement_audit.js +0 -1867
  293. package/see/examples/smoke_arithmetic.js +0 -1483
  294. package/see/examples/socrates.js +0 -1420
  295. package/see/examples/triple_terms.js +0 -1442
  296. package/see/examples/wind_turbine.js +0 -2853
  297. package/see/examples/witch.js +0 -1519
  298. package/see/see.js +0 -2179
  299. package/test/see.test.js +0 -159
  300. /package/{see/examples/n3/builtin_coverage.n3 → examples/builtin-coverage.n3} +0 -0
  301. /package/examples/output/{reifies.n3 → builtin-coverage.n3} +0 -0
package/see/see.js DELETED
@@ -1,2179 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
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
-
14
- const crypto = require('crypto');
15
- const fs = require('fs');
16
- const path = require('path');
17
- const { spawnSync } = require('child_process');
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.
21
- const ROOT = __dirname;
22
- const EXAMPLES_DIR = path.join(ROOT, 'examples');
23
- const INPUT_DIR = path.join(EXAMPLES_DIR, 'input');
24
- const OUTPUT_DIR = path.join(EXAMPLES_DIR, 'output');
25
- const DOC_DIR = path.join(EXAMPLES_DIR, 'doc');
26
-
27
- function usage() {
28
- return `SEE Notation3-to-JavaScript compiler
29
-
30
- Usage:
31
- node see.js generate <example.n3> [--name <slug>] [--force]
32
- node see.js render <example.n3>
33
- node see.js inspect <example.n3>
34
-
35
- What generate writes:
36
- examples/<name>.js Specialized JavaScript derivation program
37
- examples/input/<name>.trig RDF 1.2 TriG input evidence dataset
38
- examples/output/<name>.md Snapshot produced by the specialized JS
39
- examples/doc/<name>.md Human-readable compilation notes
40
-
41
- This is intentionally not a reasoner bridge. see.js parses the program N3 once,
42
- emits its source facts as formal TriG evidence, compiles the supported
43
- rule/query/fuse subset into JavaScript, and the resulting examples/<name>.js
44
- loads the TriG evidence directly and performs the forward derivation itself.`;
45
- }
46
-
47
- function ensureDir(dir) {
48
- fs.mkdirSync(dir, { recursive: true });
49
- }
50
- function readText(file) {
51
- return fs.readFileSync(file, 'utf8');
52
- }
53
- function writeText(file, text, force) {
54
- if (!force && fs.existsSync(file)) {
55
- throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
56
- }
57
- ensureDir(path.dirname(file));
58
- fs.writeFileSync(file, text, 'utf8');
59
- }
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
- }
66
-
67
- function slugify(value) {
68
- const base = String(value || 'example')
69
- .replace(/\.[^.]+$/, '')
70
- .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
71
- .toLowerCase()
72
- .replace(/[^a-z0-9]+/g, '_')
73
- .replace(/^_+|_+$/g, '');
74
- return base || 'example';
75
- }
76
- function titleFromSlug(slug) {
77
- return slug
78
- .split(/[_-]+/)
79
- .filter(Boolean)
80
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
81
- .join(' ');
82
- }
83
-
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
- }
93
- function parseHeader(n3, fallbackTitle) {
94
- const raw = [];
95
- for (const line of n3.split(/\r?\n/)) {
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
- }
104
- if (/^\s*$/.test(line)) continue;
105
- break;
106
- }
107
- const useful = raw.map((line) => line.trim()).filter((line) => !isSeparator(line));
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
- };
117
- }
118
-
119
- function removeComments(n3) {
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');
144
- }
145
-
146
- function decodeEscapes(value) {
147
- return value.replace(/\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[nrtbf"'\\])/g, (all, esc) => {
148
- if (esc === 'n') return '\n';
149
- if (esc === 'r') return '\r';
150
- if (esc === 't') return '\t';
151
- if (esc === 'b') return '\b';
152
- if (esc === 'f') return '\f';
153
- if (esc === '"') return '"';
154
- if (esc === "'") return "'";
155
- if (esc === '\\') return '\\';
156
- if (esc.startsWith('u')) return String.fromCharCode(Number.parseInt(esc.slice(1), 16));
157
- if (esc.startsWith('U')) return String.fromCodePoint(Number.parseInt(esc.slice(1), 16));
158
- return all;
159
- });
160
- }
161
-
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
- }
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.
187
- function tokenize(source) {
188
- const s = removeComments(source);
189
- const tokens = [];
190
- let i = 0;
191
- const isWs = (ch) => /\s/.test(ch);
192
- const one = new Set(['{', '}', '[', ']', '(', ')', ';', ',', '.']);
193
- while (i < s.length) {
194
- const ch = s[i];
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
- }
209
- if (s.startsWith('=>', i) || s.startsWith('<=', i) || s.startsWith('^^', i)) {
210
- tokens.push({ type: s.slice(i, i + 2), value: s.slice(i, i + 2) });
211
- i += 2;
212
- continue;
213
- }
214
- if (/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/.test(s.slice(i)) && /[+\-0-9.]/.test(ch)) {
215
- const m = s.slice(i).match(/^[+-]?(?:\d+\.\d*|\d*\.\d+|\d+)(?:[eE][+-]?\d+)?/)[0];
216
- // Do not steal the dot that terminates a previous integer triple; this branch starts at the number itself.
217
- tokens.push(classifyToken(m));
218
- i += m.length;
219
- continue;
220
- }
221
- if (one.has(ch)) {
222
- tokens.push({ type: ch, value: ch });
223
- i += 1;
224
- continue;
225
- }
226
- if (ch === '"') {
227
- let out = '',
228
- escaped = false;
229
- i += 1;
230
- while (i < s.length) {
231
- const c = s[i++];
232
- if (escaped) {
233
- out += `\\${c}`;
234
- escaped = false;
235
- continue;
236
- }
237
- if (c === '\\') {
238
- escaped = true;
239
- continue;
240
- }
241
- if (c === '"') break;
242
- out += c;
243
- }
244
- tokens.push({ type: 'string', value: decodeEscapes(out) });
245
- continue;
246
- }
247
- if (ch === '<') {
248
- let out = '';
249
- i += 1;
250
- while (i < s.length && s[i] !== '>') out += s[i++];
251
- if (s[i] !== '>') throw new Error('Unterminated IRI');
252
- i += 1;
253
- tokens.push({ type: 'iri', value: `<${out}>` });
254
- continue;
255
- }
256
- let out = '';
257
- while (i < s.length && !isWs(s[i]) && !one.has(s[i])) {
258
- if (s.startsWith('=>', i) || s.startsWith('<=', i) || s.startsWith('^^', i)) break;
259
- out += s[i++];
260
- }
261
- if (out.length) tokens.push(classifyToken(out));
262
- }
263
- return tokens;
264
- }
265
-
266
- function classifyToken(raw) {
267
- if (raw === '@prefix') return { type: '@prefix', value: raw };
268
- if (/^VERSION$/i.test(raw)) return { type: 'VERSION', value: raw };
269
- if (raw === 'a') return { type: 'qname', value: 'rdf:type' };
270
- if (raw.startsWith('?')) return { type: 'var', value: raw.slice(1) };
271
- if (/^(true|false)$/i.test(raw)) return { type: 'boolean', value: /^true$/i.test(raw) };
272
- if (/^[+-]?\d+$/.test(raw)) return { type: 'number', value: Number.parseInt(raw, 10) };
273
- if (/^[+-]?(?:\d+\.\d*|\d*\.\d+)(?:[eE][+-]?\d+)?$/.test(raw) || /^[+-]?\d+[eE][+-]?\d+$/.test(raw))
274
- return { type: 'number', value: Number(raw) };
275
- return { type: 'qname', value: raw };
276
- }
277
-
278
- class Parser {
279
- constructor(tokens) {
280
- this.tokens = tokens;
281
- this.pos = 0;
282
- this.blankCounter = 0;
283
- }
284
- eof() {
285
- return this.pos >= this.tokens.length;
286
- }
287
- peek(value = undefined, offset = 0) {
288
- const tok = this.tokens[this.pos + offset];
289
- if (value === undefined) return tok;
290
- return tok && tok.type === value;
291
- }
292
- isTermToken(tok) {
293
- return tok && ['qname', 'iri', 'var', 'string', 'number', 'boolean', '<<(', '(', '{', '['].includes(tok.type);
294
- }
295
- isNamedGraphStart() {
296
- const tok = this.peek();
297
- if (tok?.type === 'qname' && /^GRAPH$/i.test(tok.value)) {
298
- return this.isTermToken(this.peek(undefined, 1)) && this.peek('{', 2);
299
- }
300
- return (tok?.type === 'qname' || tok?.type === 'iri') && this.peek('{', 1);
301
- }
302
- parseNamedGraphFact() {
303
- if (this.peek()?.type === 'qname' && /^GRAPH$/i.test(this.peek().value)) this.next();
304
- const name = this.parseTerm('fact', []);
305
- const atoms = this.parseFormula('fact');
306
- this.accept('.');
307
- return { s: name, p: I('log:nameOf'), o: { kind: 'formula', atoms } };
308
- }
309
- next() {
310
- if (this.eof()) throw new Error('Unexpected end of input');
311
- return this.tokens[this.pos++];
312
- }
313
- accept(type) {
314
- if (this.peek(type)) return this.next();
315
- return null;
316
- }
317
- expect(type) {
318
- const tok = this.next();
319
- if (tok.type !== type) throw new Error(`Expected ${type}, got ${tok.type} (${tok.value})`);
320
- return tok;
321
- }
322
- freshBlank(prefix = 'b') {
323
- this.blankCounter += 1;
324
- return `_${prefix}${this.blankCounter}`;
325
- }
326
- skipPrefix() {
327
- this.expect('@prefix');
328
- // Prefix declaration is irrelevant after QName compaction; skip until final dot.
329
- while (!this.eof() && !this.accept('.')) this.next();
330
- }
331
- skipVersion() {
332
- this.expect('VERSION');
333
- if (this.peek('string')) this.next();
334
- this.accept('.');
335
- }
336
- parseProgram() {
337
- const facts = [],
338
- rules = [],
339
- queries = [],
340
- prefixes = {};
341
- while (!this.eof()) {
342
- if (this.accept('@prefix')) {
343
- this.pos -= 1;
344
- const start = this.pos;
345
- this.skipPrefix();
346
- const slice = this.tokens
347
- .slice(start, this.pos)
348
- .map((tok) => tok.value)
349
- .join(' ');
350
- const m = slice.match(/@prefix\s+([^\s]*)\s+<([^>]+)>/);
351
- if (m) prefixes[(m[1] || ':').replace(/:$/, '')] = m[2];
352
- continue;
353
- }
354
- if (this.peek('VERSION')) {
355
- this.skipVersion();
356
- continue;
357
- }
358
- if (this.isNamedGraphStart()) {
359
- facts.push(this.parseNamedGraphFact());
360
- continue;
361
- }
362
- if (this.peek('{')) {
363
- const lhs = this.parseFormula('body');
364
- if (this.accept('=>')) {
365
- if (
366
- (this.peek('qname') && this.tokens[this.pos].value === 'false') ||
367
- (this.peek('boolean') && this.tokens[this.pos].value === false)
368
- ) {
369
- this.next();
370
- this.accept('.');
371
- rules.push({ kind: 'fuse', id: rules.length + 1, body: lhs });
372
- } else {
373
- const head = this.parseFormula('head');
374
- this.accept('.');
375
- rules.push({ kind: 'rule', id: rules.length + 1, body: lhs, head });
376
- }
377
- } else if (this.accept('<=')) {
378
- if (
379
- (this.peek('qname') && this.tokens[this.pos].value === 'true') ||
380
- (this.peek('boolean') && this.tokens[this.pos].value === true)
381
- ) {
382
- this.next();
383
- this.accept('.');
384
- rules.push({ kind: 'backward', id: rules.length + 1, body: [], head: lhs });
385
- } else {
386
- const rhs = this.parseFormula('body');
387
- this.accept('.');
388
- rules.push({ kind: 'backward', id: rules.length + 1, body: rhs, head: lhs });
389
- }
390
- } else if (this.peek('.') || this.eof()) {
391
- this.accept('.');
392
- facts.push(...lhs);
393
- } else {
394
- const subject = { kind: 'formula', atoms: lhs };
395
- const triples = this.parseStatementRest('fact', subject);
396
- this.accept('.');
397
- for (const triple of triples) {
398
- if (
399
- triple.s?.kind === 'formula' &&
400
- triple.p?.kind === 'iri' &&
401
- triple.p.value === 'log:query' &&
402
- triple.o?.kind === 'formula'
403
- ) {
404
- queries.push({ id: queries.length + 1, premise: triple.s.atoms, conclusion: triple.o.atoms });
405
- } else {
406
- facts.push(triple);
407
- }
408
- }
409
- }
410
- } else {
411
- facts.push(...this.parseStatement('fact'));
412
- this.accept('.');
413
- }
414
- }
415
- return { facts, rules, queries, prefixes };
416
- }
417
- parseFormula(mode) {
418
- this.expect('{');
419
- const atoms = [];
420
- while (!this.accept('}')) {
421
- if (this.eof()) throw new Error('Unclosed formula');
422
- atoms.push(...this.parseStatement(mode));
423
- this.accept('.');
424
- }
425
- return atoms;
426
- }
427
- parseStatement(mode) {
428
- const triples = [];
429
- const subject = this.parseTerm(mode, triples);
430
- return this.parseStatementRest(mode, subject, triples);
431
- }
432
- parseStatementRest(mode, subject, triples = []) {
433
- while (!this.eof() && !['.', '}'].includes(this.peek()?.type)) {
434
- if (this.accept(';')) {
435
- if (['.', '}'].includes(this.peek()?.type)) break;
436
- }
437
- const predicate = this.parsePredicate();
438
- while (true) {
439
- const object = this.parseTerm(mode, triples);
440
- triples.push({ s: subject, p: predicate, o: object });
441
- if (!this.accept(',')) break;
442
- }
443
- if (!this.accept(';')) break;
444
- if (['.', '}'].includes(this.peek()?.type)) break;
445
- }
446
- return triples;
447
- }
448
- parsePredicate() {
449
- const tok = this.next();
450
- if (tok.type === 'var') return V(tok.value);
451
- if (tok.type !== 'qname' && tok.type !== 'iri') throw new Error(`Expected predicate, got ${tok.type}`);
452
- if (tok.value === '=') return I('owl:sameAs');
453
- return I(tok.value);
454
- }
455
- parseTerm(mode, sink) {
456
- const tok = this.next();
457
- if (tok.type === 'var') return V(tok.value);
458
- if (tok.type === 'string') {
459
- if (this.accept('^^')) this.next();
460
- return L(tok.value);
461
- }
462
- if (tok.type === 'number' || tok.type === 'boolean') return L(tok.value);
463
- if (tok.type === 'iri' || tok.type === 'qname') return I(tok.value);
464
- if (tok.type === '<<(') {
465
- const subject = this.parseTerm(mode, sink);
466
- const predicate = this.parsePredicate();
467
- const object = this.parseTerm(mode, sink);
468
- this.expect(')>>');
469
- return { kind: 'triple', s: subject, p: predicate, o: object };
470
- }
471
- if (tok.type === '(') {
472
- const items = [];
473
- while (!this.accept(')')) items.push(this.parseTerm(mode, sink));
474
- return List(items);
475
- }
476
- if (tok.type === '{') {
477
- const atoms = [];
478
- while (!this.accept('}')) {
479
- if (this.eof()) throw new Error('Unclosed nested formula');
480
- atoms.push(...this.parseStatement(mode));
481
- this.accept('.');
482
- }
483
- return { kind: 'formula', atoms };
484
- }
485
- if (tok.type === '[') {
486
- const id =
487
- mode === 'body'
488
- ? V(this.freshBlank('bodyBlank'))
489
- : Blank(this.freshBlank(mode === 'head' ? 'headBlank' : 'blank'));
490
- if (!this.accept(']')) {
491
- while (true) {
492
- const predicate = this.parsePredicate();
493
- while (true) {
494
- const object = this.parseTerm(mode, sink);
495
- sink.push({ s: id, p: predicate, o: object });
496
- if (!this.accept(',')) break;
497
- }
498
- if (this.accept(']')) break;
499
- this.expect(';');
500
- }
501
- }
502
- return id;
503
- }
504
- throw new Error(`Expected term, got ${tok.type}`);
505
- }
506
- }
507
-
508
- // parseN3 separates the source file into four compiler inputs:
509
- // facts -> serialized as examples/input/<name>.trig
510
- // rules -> compiled into JavaScript fixpoint code
511
- // queries -> rendered as selected output checks
512
- // prefixes -> carried into generated TriG evidence
513
- function parseN3(n3) {
514
- const parser = new Parser(tokenize(n3));
515
- return parser.parseProgram();
516
- }
517
-
518
- function termToJsComment(term) {
519
- if (term.kind === 'iri') return term.value;
520
- if (term.kind === 'lit') return JSON.stringify(term.value);
521
- if (term.kind === 'var') return `?${term.value}`;
522
- if (term.kind === 'blank') return term.value;
523
- if (term.kind === 'list') return `(${term.items.map(termToJsComment).join(' ')})`;
524
- if (term.kind === 'triple') return `<<( ${termToJsComment(term.s)} ${termToJsComment(term.p)} ${termToJsComment(term.o)} )>>`;
525
- if (term.kind === 'formula') return `{ ${term.atoms.map(atomToComment).join(' . ')} }`;
526
- return String(term.value ?? term);
527
- }
528
- function atomToComment(atom) {
529
- return `${termToJsComment(atom.s)} ${termToJsComment(atom.p)} ${termToJsComment(atom.o)}`;
530
- }
531
-
532
- function compilationStats(program) {
533
- const predicates = new Set();
534
- const builtins = new Set();
535
- for (const atom of [
536
- ...program.facts,
537
- ...program.rules.flatMap((r) => [...(r.body || []), ...(r.head || [])]),
538
- ...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
539
- ]) {
540
- const p = atom.p?.value;
541
- if (p) predicates.add(p);
542
- if (/^(math|string|list|log|crypto):/.test(p)) builtins.add(p);
543
- }
544
- return {
545
- facts: program.facts.length,
546
- rules: program.rules.filter((r) => r.kind === 'rule').length,
547
- backwardRules: program.rules.filter((r) => r.kind === 'backward').length,
548
- fuses: program.rules.filter((r) => r.kind === 'fuse').length,
549
- queries: (program.queries || []).length,
550
- predicates: predicates.size,
551
- builtins: [...builtins].sort(),
552
- };
553
- }
554
-
555
- // Source facts are emitted as RDF 1.2 TriG. Formulas that appear as objects are
556
- // lifted into named graphs so the generated runner can load evidence directly
557
- // from .trig without going through an intermediate conversion step.
558
- function trigString(value) {
559
- return JSON.stringify(String(value));
560
- }
561
- function trigNumber(value) {
562
- if (Object.is(value, -0)) return '0';
563
- if (Number.isInteger(value)) return String(value);
564
- return Number(value.toPrecision(15)).toString();
565
- }
566
- function inputLiteralToN3(value) {
567
- if (typeof value === 'string') return trigString(value);
568
- if (typeof value === 'number') return trigNumber(value);
569
- if (typeof value === 'boolean') return value ? 'true' : 'false';
570
- return trigString(value);
571
- }
572
- function inputTermToN3(term) {
573
- if (!term) return 'undefined';
574
- if (term.kind === 'iri') return term.value;
575
- if (term.kind === 'lit') return inputLiteralToN3(term.value);
576
- if (term.kind === 'var') return '?' + term.value;
577
- if (term.kind === 'blank') return term.value.startsWith('_:') ? term.value : '_:' + term.value.replace(/^_+/, '');
578
- if (term.kind === 'list') return '(' + term.items.map(inputTermToN3).join(' ') + ')';
579
- if (term.kind === 'triple') return '<<( ' + inputTermToN3(term.s) + ' ' + inputTermToN3(term.p) + ' ' + inputTermToN3(term.o) + ' )>>';
580
- if (term.kind === 'formula') return '{ ' + term.atoms.map(inputAtomToN3).join(' . ') + ' }';
581
- return String(term.value ?? term);
582
- }
583
- function inputAtomToN3(atom) {
584
- return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + inputTermToN3(atom.o);
585
- }
586
- function inputTermHasTripleTerm(term) {
587
- if (!term) return false;
588
- if (term.kind === 'triple') return true;
589
- if (term.kind === 'list') return term.items.some(inputTermHasTripleTerm);
590
- if (term.kind === 'formula') return term.atoms.some(inputAtomHasTripleTerm);
591
- return false;
592
- }
593
- function inputAtomHasTripleTerm(atom) {
594
- return inputTermHasTripleTerm(atom.s) || inputTermHasTripleTerm(atom.p) || inputTermHasTripleTerm(atom.o);
595
- }
596
- function programHasTripleTerms(program) {
597
- return [
598
- ...(program.facts || []),
599
- ...(program.rules || []).flatMap((r) => [...(r.body || []), ...(r.head || [])]),
600
- ...(program.queries || []).flatMap((q) => [...(q.premise || []), ...(q.conclusion || [])]),
601
- ].some(inputAtomHasTripleTerm);
602
- }
603
- function formulaBlock(label, atoms) {
604
- const lines = [label + ' {'];
605
- for (const atom of atoms) lines.push(' ' + inputAtomToN3(atom) + ' .');
606
- lines.push('}');
607
- return lines.join('\n');
608
- }
609
- function atomToTrig(atom, state) {
610
- if (atom.o && atom.o.kind === 'formula') {
611
- if (atom.p && atom.p.kind === 'iri' && atom.p.value === 'log:nameOf') {
612
- state.graphs.push(formulaBlock(inputTermToN3(atom.s), atom.o.atoms));
613
- return null;
614
- }
615
- state.formulaCounter += 1;
616
- const label = `in:formula${state.formulaCounter}`;
617
- state.graphs.push(formulaBlock(label, atom.o.atoms));
618
- return inputTermToN3(atom.s) + ' ' + inputTermToN3(atom.p) + ' ' + label + ' .';
619
- }
620
- return inputAtomToN3(atom) + ' .';
621
- }
622
- function inputFactsToTrig(facts) {
623
- const state = { formulaCounter: 0, graphs: [] };
624
- const triples = [];
625
- for (const atom of facts) {
626
- const line = atomToTrig(atom, state);
627
- if (line) triples.push(line);
628
- }
629
- return { triples, graphs: state.graphs };
630
- }
631
- function prefixLines(prefixes) {
632
- const merged = { ...(prefixes || {}) };
633
- if (!Object.hasOwn(merged, 'log')) merged.log = 'http://www.w3.org/2000/10/swap/log#';
634
- if (!Object.hasOwn(merged, 'see')) merged.see = 'https://example.org/see#';
635
- if (!Object.hasOwn(merged, 'in')) merged.in = 'https://example.org/see/input#';
636
- return Object.entries(merged).map(([prefix, iri]) => `@prefix ${prefix ? prefix + ':' : ':'} <${iri}> .`);
637
- }
638
- function generateInputTrig(n3Path, name, title, header, stats, program) {
639
- const { triples, graphs } = inputFactsToTrig(program.facts);
640
- const metadata = [
641
- 'in:metadata {',
642
- ' in:run a see:InputDataset .',
643
- ` in:run see:name ${trigString(name)} .`,
644
- ` in:run see:title ${trigString(title)} .`,
645
- ` in:run see:sourceFile ${trigString(path.relative(ROOT, path.resolve(n3Path)))} .`,
646
- ` in:run see:sourceSHA256 ${trigString(stats.sourceHash)} .`,
647
- ` in:run see:description ${trigString(header.description || '')} .`,
648
- ' in:run see:compiler "see.js N3-to-JS compiler" .',
649
- ` in:run see:inputFacts ${stats.facts} .`,
650
- ` in:run see:compiledRules ${stats.rules} .`,
651
- ` in:run see:compiledBackwardRules ${stats.backwardRules} .`,
652
- ` in:run see:compiledFuses ${stats.fuses} .`,
653
- ` in:run see:compiledQueries ${stats.queries} .`,
654
- '}',
655
- ].join('\n');
656
- const sections = [
657
- ...(programHasTripleTerms(program) ? ['VERSION "1.2"', ''] : []),
658
- ...prefixLines(program.prefixes),
659
- '',
660
- '# Formal SEE input evidence in RDF 1.2 TriG.',
661
- '# The generated runner reads this TriG evidence directly.',
662
- '',
663
- triples.length ? triples.join('\n') : '# No source facts were present in the N3 program.',
664
- ];
665
- if (graphs.length) sections.push('', graphs.join('\n\n'));
666
- sections.push('', metadata, '');
667
- return sections.join('\n');
668
- }
669
- // The runtime below is copied verbatim into each generated example. Keep it
670
- // dependency-light: generated examples should be executable with Node alone plus
671
- // the local examples/_see.js TriG loader.
672
- function runtimeSource() {
673
- return String.raw`
674
- const crypto = require('crypto');
675
-
676
- function canonical(term) {
677
- if (term.kind === 'list') return ['list', term.items.map(canonical)];
678
- if (term.kind === 'triple') return ['triple', canonical(term.s), canonical(term.p), canonical(term.o)];
679
- if (term.kind === 'formula') return ['formula', term.atoms.map((a) => [canonical(a.s), canonical(a.p), canonical(a.o)])];
680
- return [term.kind, term.value];
681
- }
682
- function factKey(f) { return JSON.stringify([canonical(f.s), canonical(f.p), canonical(f.o)]); }
683
- function termIndexKey(t) { return JSON.stringify(canonical(t)); }
684
- function compoundIndexKey() { return Array.from(arguments).map(termIndexKey).join('\u001f'); }
685
- function termIsConcrete(t) {
686
- if (!t || t.kind === 'var') return false;
687
- if (t.kind === 'list') return t.items.every(termIsConcrete);
688
- if (t.kind === 'triple') return termIsConcrete(t.s) && termIsConcrete(t.p) && termIsConcrete(t.o);
689
- if (t.kind === 'formula') return t.atoms.every((a) => termIsConcrete(a.s) && termIsConcrete(a.p) && termIsConcrete(a.o));
690
- return true;
691
- }
692
- function isVar(t) { return t && t.kind === 'var'; }
693
- function isIri(t, iri) { return t && t.kind === 'iri' && t.value === iri; }
694
- function lit(value) { return { kind: 'lit', value }; }
695
- function blank(value) { return { kind: 'blank', value }; }
696
- function list(items) { return { kind: 'list', items }; }
697
- function cloneTerm(t) { return JSON.parse(JSON.stringify(t)); }
698
- function primitive(t) {
699
- if (!t) return undefined;
700
- if (t.kind === 'lit') return t.value;
701
- if (t.kind === 'iri') return t.value.replace(/^:/, '');
702
- if (t.kind === 'blank') return t.value;
703
- if (t.kind === 'list') return t.items.map(primitive);
704
- if (t.kind === 'triple') return termToN3(t);
705
- if (t.kind === 'formula') return termToN3(t);
706
- return undefined;
707
- }
708
- function literalToN3(value) {
709
- if (typeof value === 'string') return JSON.stringify(value);
710
- if (typeof value === 'number') {
711
- if (Object.is(value, -0)) return '0';
712
- if (Number.isInteger(value)) return String(value);
713
- return Number(value.toPrecision(15)).toString();
714
- }
715
- if (typeof value === 'boolean') return value ? 'true' : 'false';
716
- return JSON.stringify(value);
717
- }
718
- function termToN3(t) {
719
- if (!t) return 'undefined';
720
- if (t.kind === 'iri') return t.value;
721
- if (t.kind === 'lit') return literalToN3(t.value);
722
- if (t.kind === 'var') return '?' + t.value;
723
- if (t.kind === 'blank') return t.value.startsWith('_:') ? t.value : '_:' + t.value.replace(/^_+/, '');
724
- if (t.kind === 'list') return '(' + t.items.map(termToN3).join(' ') + ')';
725
- if (t.kind === 'triple') return '<<( ' + termToN3(t.s) + ' ' + termToN3(t.p) + ' ' + termToN3(t.o) + ' )>>';
726
- if (t.kind === 'formula') return '{ ' + t.atoms.map(atomToN3).join(' . ') + ' }';
727
- return String(t.value ?? t);
728
- }
729
- function atomToN3(f) { return termToN3(f.s) + ' ' + termToN3(f.p) + ' ' + termToN3(f.o); }
730
- function display(t) {
731
- const value = primitive(t);
732
- if (Array.isArray(value)) return value.map(String).join(' ');
733
- return String(value);
734
- }
735
- function deepEqual(a, b) {
736
- if (a?.kind === 'lit' && b?.kind === 'lit' && typeof a.value === 'number' && typeof b.value === 'number') {
737
- return Math.abs(a.value - b.value) < 1e-12;
738
- }
739
- return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
740
- }
741
- function resolve(term, env, seen = new Set()) {
742
- if (term.kind === 'var' && Object.hasOwn(env, term.value)) {
743
- if (seen.has(term.value)) return term;
744
- seen.add(term.value);
745
- return resolve(env[term.value], env, seen);
746
- }
747
- if (term.kind === 'list') return list(term.items.map((item) => resolve(item, env, seen)));
748
- if (term.kind === 'triple') return { kind: 'triple', s: resolve(term.s, env), p: resolve(term.p, env), o: resolve(term.o, env) };
749
- 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) })) };
750
- return term;
751
- }
752
- function unify(a, b, env) {
753
- a = resolve(a, env);
754
- b = resolve(b, env);
755
- if (a.kind === 'var') return { ...env, [a.value]: b };
756
- if (b.kind === 'var') return { ...env, [b.value]: a };
757
- if (a.kind === 'list' || b.kind === 'list') {
758
- if (a.kind !== 'list' || b.kind !== 'list' || a.items.length !== b.items.length) return null;
759
- let out = env;
760
- for (let i = 0; i < a.items.length; i += 1) {
761
- out = unify(a.items[i], b.items[i], out);
762
- if (!out) return null;
763
- }
764
- return out;
765
- }
766
- if (a.kind === 'triple' || b.kind === 'triple') {
767
- if (a.kind !== 'triple' || b.kind !== 'triple') return null;
768
- let out = unify(a.s, b.s, env);
769
- if (!out) return null;
770
- out = unify(a.p, b.p, out);
771
- if (!out) return null;
772
- return unify(a.o, b.o, out);
773
- }
774
- return deepEqual(a, b) ? env : null;
775
- }
776
- function bind(pattern, value, env) { return unify(pattern, value, env); }
777
- function matchFact(atom, fact, env) {
778
- let out = unify(atom.p, fact.p, env); if (!out) return null;
779
- out = unify(atom.s, fact.s, out); if (!out) return null;
780
- return unify(atom.o, fact.o, out);
781
- }
782
- function atomIsGround(atom, env) {
783
- return termIsGround(atom.s, env) && termIsGround(atom.p, env) && termIsGround(atom.o, env);
784
- }
785
- function termIsGround(t, env) {
786
- const r = resolve(t, env);
787
- if (r.kind === 'var') return false;
788
- if (r.kind === 'list') return r.items.every((item) => termIsGround(item, env));
789
- if (r.kind === 'triple') return termIsGround(r.s, env) && termIsGround(r.p, env) && termIsGround(r.o, env);
790
- if (r.kind === 'formula') return r.atoms.every((atom) => atomIsGround(atom, env));
791
- return true;
792
- }
793
- function stringValue(t) {
794
- const r = t;
795
- if (!r) return null;
796
- if (r.kind === 'lit') return String(r.value);
797
- if (r.kind === 'iri') return r.value.replace(/[<>]/g, '');
798
- if (r.kind === 'blank' || r.kind === 'var') return null;
799
- return termToN3(r);
800
- }
801
- function xsdDurationSeconds(value) {
802
- const m = String(value || '').match(/^(-)?P(?:(\d+(?:\.\d+)?)Y)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)W)?(?:(\d+(?:\.\d+)?)D)?(?:T(?:(\d+(?:\.\d+)?)H)?(?:(\d+(?:\.\d+)?)M)?(?:(\d+(?:\.\d+)?)S)?)?$/i);
803
- if (!m) return null;
804
- const sign = m[1] ? -1 : 1;
805
- const years = Number(m[2] || 0);
806
- const months = Number(m[3] || 0);
807
- const weeks = Number(m[4] || 0);
808
- const days = Number(m[5] || 0);
809
- const hours = Number(m[6] || 0);
810
- const minutes = Number(m[7] || 0);
811
- const seconds = Number(m[8] || 0);
812
-
813
- // xsd:duration years/months are calendar-relative. SEE uses Gregorian averages
814
- // for standalone numeric comparisons such as "age above P80Y".
815
- return sign * (
816
- years * 365.2425 * 86400 +
817
- months * (365.2425 / 12) * 86400 +
818
- weeks * 7 * 86400 +
819
- days * 86400 +
820
- hours * 3600 +
821
- minutes * 60 +
822
- seconds
823
- );
824
- }
825
-
826
- function toNumberMaybe(t) {
827
- const v = primitive(t);
828
- if (typeof v === 'number') return Number.isFinite(v) || Number.isNaN(v) ? v : null;
829
- if (typeof v === 'boolean') return v ? 1 : 0;
830
- if (typeof v !== 'string') return null;
831
- const dur = xsdDurationSeconds(v);
832
- if (dur !== null) return dur;
833
- const d = Date.parse(v);
834
- if (/^\d{4}-\d{2}-\d{2}(?:T|$)/.test(v) && Number.isFinite(d)) return d / 1000;
835
- const n = Number(v);
836
- return Number.isNaN(n) ? null : n;
837
- }
838
- function toNumber(t) {
839
- const n = toNumberMaybe(t);
840
- if (n === null || !Number.isFinite(n)) throw new Error('Expected numeric value, got ' + JSON.stringify(primitive(t)));
841
- return n;
842
- }
843
- function toIntegerMaybe(t) {
844
- const n = toNumberMaybe(t);
845
- if (n === null || !Number.isFinite(n) || !Number.isInteger(n)) return null;
846
- return n;
847
- }
848
- function parseDate(t) {
849
- const v = primitive(t);
850
- if (typeof v !== 'string') return null;
851
- const ms = Date.parse(v);
852
- return Number.isFinite(ms) ? new Date(ms) : null;
853
- }
854
- function dateTimeLiteral(date) { return lit(date.toISOString().replace(/\.000Z$/, 'Z')); }
855
- function durationLiteral(seconds) { return lit('PT' + Number(seconds.toPrecision(15)).toString() + 'S'); }
856
- function termFrom(value) {
857
- if (Array.isArray(value)) return list(value.map(termFrom));
858
- if (value && typeof value === 'object' && value.kind) return value;
859
- return lit(value);
860
- }
861
- function allBoundList(term, env) {
862
- const r = resolve(term, env);
863
- if (r.kind !== 'list') throw new Error('Expected N3 list');
864
- if (r.items.some((item) => resolve(item, env).kind === 'var')) return null;
865
- return r.items.map((item) => resolve(item, env));
866
- }
867
- function bindResult(pattern, value, env) {
868
- return bind(pattern, termFrom(value), env);
869
- }
870
- function bindTermResult(pattern, term, env) {
871
- const out = bind(pattern, term, env);
872
- return out ? [out] : [];
873
- }
874
- function succeedIf(ok, env) { return ok ? [env] : []; }
875
- function compareNumericTerms(a, b) {
876
- const na = toNumberMaybe(a), nb = toNumberMaybe(b);
877
- if (na === null || nb === null) return null;
878
- if (Number.isNaN(na) || Number.isNaN(nb)) return NaN;
879
- return na < nb ? -1 : na > nb ? 1 : 0;
880
- }
881
- function comparisonOperands(s, o, env) {
882
- const left = resolve(s, env), right = resolve(o, env);
883
- if (left.kind === 'list' && left.items.length === 2 && (right.kind === 'lit' ? right.value === true : right.kind === 'iri' && right.value === 'true')) {
884
- return [resolve(left.items[0], env), resolve(left.items[1], env)];
885
- }
886
- return [left, right];
887
- }
888
- function bindNumericOutput(pattern, value, env) {
889
- if (!Number.isFinite(value)) return [];
890
- const normalized = Number.isInteger(value) ? value : Number(value.toPrecision(15));
891
- return bindTermResult(pattern, lit(normalized), env);
892
- }
893
- function unaryMath(name, s, o, env, fwd, inv) {
894
- const left = resolve(s, env), right = resolve(o, env);
895
- if (left.kind !== 'var') return bindNumericOutput(o, fwd(toNumber(left)), env);
896
- if (right.kind !== 'var' && inv) return bindNumericOutput(s, inv(toNumber(right)), env);
897
- if (left.kind === 'var' && right.kind === 'var') return [env];
898
- return [];
899
- }
900
- function builtinMath(name, s, o, env) {
901
- const left = resolve(s, env);
902
- const right = resolve(o, env);
903
- const testCmp = (pred) => {
904
- const [a, b] = comparisonOperands(s, o, env);
905
- if (a.kind === 'var' || b.kind === 'var') return [];
906
- const cmp = compareNumericTerms(a, b);
907
- if (cmp === null || Number.isNaN(cmp)) return [];
908
- return succeedIf(pred(cmp), env);
909
- };
910
- if (name === 'math:lessThan') return testCmp((c) => c < 0);
911
- if (name === 'math:notLessThan') return testCmp((c) => c >= 0);
912
- if (name === 'math:greaterThan') return testCmp((c) => c > 0);
913
- if (name === 'math:notGreaterThan') return testCmp((c) => c <= 0);
914
- if (name === 'math:equalTo') return testCmp((c) => c === 0);
915
- if (name === 'math:notEqualTo') {
916
- const [a, b] = comparisonOperands(s, o, env);
917
- if (a.kind === 'var' || b.kind === 'var') return [];
918
- const cmp = compareNumericTerms(a, b);
919
- if (cmp !== null) return succeedIf(Number.isNaN(cmp) || cmp !== 0, env);
920
- return succeedIf(!deepEqual(a, b), env);
921
- }
922
- if (name === 'math:negation') return unaryMath(name, s, o, env, (x) => -x, (x) => -x);
923
- if (name === 'math:absoluteValue') {
924
- if (left.kind === 'var' && right.kind === 'var') return [env];
925
- if (left.kind !== 'var') return bindNumericOutput(o, Math.abs(toNumber(left)), env);
926
- const n = toNumber(right);
927
- const outs = [];
928
- for (const v of n === 0 ? [0] : [n, -n]) {
929
- const out = bind(s, lit(v), env); if (out) outs.push(out);
930
- }
931
- return outs;
932
- }
933
- if (name === 'math:degrees') return unaryMath(name, s, o, env, (x) => x * 180 / Math.PI, (x) => x * Math.PI / 180);
934
- const unary = {
935
- 'math:sin': [Math.sin, Math.asin], 'math:cos': [Math.cos, Math.acos], 'math:tan': [Math.tan, Math.atan],
936
- 'math:asin': [Math.asin, Math.sin], 'math:acos': [Math.acos, Math.cos], 'math:atan': [Math.atan, Math.tan],
937
- 'math:sinh': [Math.sinh, Math.asinh], 'math:cosh': [Math.cosh, Math.acosh], 'math:tanh': [Math.tanh, Math.atanh],
938
- 'math:rounded': [Math.round, null], 'math:floor': [Math.floor, null]
939
- };
940
- if (Object.hasOwn(unary, name)) {
941
- const [fwd, inv] = unary[name];
942
- if (typeof fwd !== 'function') return [];
943
- return unaryMath(name, s, o, env, fwd, typeof inv === 'function' ? inv : null);
944
- }
945
- if (name === 'math:exponentiation') {
946
- const l = resolve(s, env);
947
- if (l.kind !== 'list' || l.items.length !== 2) return [];
948
- const base = resolve(l.items[0], env);
949
- const exponent = resolve(l.items[1], env);
950
- const result = resolve(o, env);
951
- if (base.kind !== 'var' && exponent.kind !== 'var') return bindNumericOutput(o, Math.pow(toNumber(base), toNumber(exponent)), env);
952
- if (base.kind !== 'var' && result.kind !== 'var') {
953
- const b = toNumber(base), r = toNumber(result);
954
- if (b > 0 && b !== 1 && r > 0) return bindNumericOutput(l.items[1], Math.log(r) / Math.log(b), env);
955
- return [];
956
- }
957
- if (exponent.kind !== 'var' && result.kind !== 'var') {
958
- const e = toNumber(exponent), r = toNumber(result);
959
- if (e !== 0) return bindNumericOutput(l.items[0], Math.pow(r, 1 / e), env);
960
- return [];
961
- }
962
- return [];
963
- }
964
- if (name === 'math:product' || name === 'math:sum') {
965
- const vals = allBoundList(s, env); if (!vals) return [];
966
- if (vals.length === 2) {
967
- const [aT, bT] = vals;
968
- const aD = parseDate(aT);
969
- if (name === 'math:sum' && aD) {
970
- const secs = toNumberMaybe(bT);
971
- if (secs !== null && Number.isFinite(secs)) return bindTermResult(o, dateTimeLiteral(new Date(aD.getTime() + secs * 1000)), env);
972
- }
973
- }
974
- const value = name === 'math:product' ? vals.reduce((a, x) => a * toNumber(x), 1) : vals.reduce((a, x) => a + toNumber(x), 0);
975
- return bindNumericOutput(o, value, env);
976
- }
977
- if (name === 'math:difference' || name === 'math:quotient' || name === 'math:integerQuotient' || name === 'math:remainder') {
978
- const vals = allBoundList(s, env); if (!vals || vals.length !== 2) return [];
979
- const aD = parseDate(vals[0]), bD = parseDate(vals[1]);
980
- if (name === 'math:difference' && aD && bD) return bindTermResult(o, durationLiteral((aD.getTime() - bD.getTime()) / 1000), env);
981
- if (name === 'math:difference' && aD && !bD) {
982
- const secs = toNumberMaybe(vals[1]);
983
- if (secs !== null && Number.isFinite(secs)) return bindTermResult(o, dateTimeLiteral(new Date(aD.getTime() - secs * 1000)), env);
984
- }
985
- const a = toNumber(vals[0]), b = toNumber(vals[1]);
986
- if ((name === 'math:quotient' || name === 'math:integerQuotient' || name === 'math:remainder') && b === 0) return [];
987
- if (name === 'math:difference') return bindNumericOutput(o, a - b, env);
988
- if (name === 'math:quotient') return bindNumericOutput(o, a / b, env);
989
- if (!Number.isInteger(a) || !Number.isInteger(b)) return [];
990
- if (name === 'math:integerQuotient') return bindNumericOutput(o, Math.trunc(a / b), env);
991
- return bindNumericOutput(o, a % b, env);
992
- }
993
- return null;
994
- }
995
- function applyFormat(fmt, args) {
996
- let i = 0;
997
- return String(fmt).replace(/%([-0]?\d+)?(?:\.(\d+))?([%sdiufFeEgGc])/g, (m, width, precision, conv) => {
998
- if (conv === '%') return '%';
999
- const arg = args[i++];
1000
- if (conv === 's') return String(arg ?? '');
1001
- if (conv === 'c') return String.fromCodePoint(Number(arg));
1002
- if ('diu'.includes(conv)) return String(Math.trunc(Number(arg)));
1003
- const n = Number(arg);
1004
- if (conv === 'f' || conv === 'F') return n.toFixed(precision == null ? 6 : Number(precision));
1005
- if (conv === 'e' || conv === 'E') return n.toExponential(precision == null ? 6 : Number(precision));
1006
- if (conv === 'g' || conv === 'G') return n.toPrecision(precision == null ? 6 : Number(precision)).replace(/\.0+(e|$)/i, '$1');
1007
- return m;
1008
- });
1009
- }
1010
- function compileRegex(pattern, flags = '') {
1011
- try { return new RegExp(String(pattern), flags); }
1012
- catch { return null; }
1013
- }
1014
- function builtinString(name, s, o, env) {
1015
- const left = resolve(s, env), right = resolve(o, env);
1016
- function output(value) { return bindTermResult(o, lit(String(value)), env); }
1017
- const str = (term) => stringValue(resolve(term, env));
1018
- if (name === 'string:concatenation') {
1019
- const vals = allBoundList(s, env); if (!vals) return [];
1020
- return output(vals.map(stringValue).join(''));
1021
- }
1022
- if (name === 'string:format') {
1023
- const vals = allBoundList(s, env); if (!vals || vals.length === 0) return [];
1024
- return output(applyFormat(stringValue(vals[0]), vals.slice(1).map(stringValue)));
1025
- }
1026
- if (name === 'string:length') { const a = str(s); return a === null ? [] : bindTermResult(o, lit(a.length), env); }
1027
- if (name === 'string:charAt') {
1028
- const vals = allBoundList(s, env); if (!vals || vals.length !== 2) return [];
1029
- const a = stringValue(vals[0]); const idx = Math.trunc(toNumber(vals[1]));
1030
- return output(idx < 0 || idx >= a.length ? '' : a.charAt(idx));
1031
- }
1032
- if (name === 'string:setCharAt') {
1033
- const vals = allBoundList(s, env); if (!vals || vals.length !== 3) return [];
1034
- const a = stringValue(vals[0]); const idx = Math.trunc(toNumber(vals[1])); const ch = String(stringValue(vals[2]) || '').charAt(0);
1035
- if (idx < 0 || idx >= a.length) return output(a);
1036
- return output(a.slice(0, idx) + ch + a.slice(idx + 1));
1037
- }
1038
- const a = str(s), b = str(o);
1039
- if (name === 'string:contains') return a === null || b === null ? [] : succeedIf(a.includes(b), env);
1040
- if (name === 'string:containsIgnoringCase') return a === null || b === null ? [] : succeedIf(a.toLowerCase().includes(b.toLowerCase()), env);
1041
- if (name === 'string:startsWith') return a === null || b === null ? [] : succeedIf(a.startsWith(b), env);
1042
- if (name === 'string:endsWith') return a === null || b === null ? [] : succeedIf(a.endsWith(b), env);
1043
- if (name === 'string:equalIgnoringCase') return a === null || b === null ? [] : succeedIf(a.toLowerCase() === b.toLowerCase(), env);
1044
- if (name === 'string:notEqualIgnoringCase') return a === null || b === null ? [] : succeedIf(a.toLowerCase() !== b.toLowerCase(), env);
1045
- if (name === 'string:greaterThan') return a === null || b === null ? [] : succeedIf(a > b, env);
1046
- if (name === 'string:lessThan') return a === null || b === null ? [] : succeedIf(a < b, env);
1047
- if (name === 'string:notGreaterThan') return a === null || b === null ? [] : succeedIf(a <= b, env);
1048
- if (name === 'string:notLessThan') return a === null || b === null ? [] : succeedIf(a >= b, env);
1049
- if (name === 'string:matches' || name === 'string:notMatches') {
1050
- if (a === null || b === null) return [];
1051
- const re = compileRegex(b); if (!re) return [];
1052
- const ok = re.test(a);
1053
- return succeedIf(name === 'string:matches' ? ok : !ok, env);
1054
- }
1055
- if (name === 'string:replace') {
1056
- const vals = allBoundList(s, env); if (!vals || vals.length !== 3) return [];
1057
- const data = stringValue(vals[0]), pattern = stringValue(vals[1]), replacement = stringValue(vals[2]);
1058
- const re = compileRegex(pattern, 'g'); if (!re) return [];
1059
- return output(data.replace(re, replacement));
1060
- }
1061
- if (name === 'string:scrape') {
1062
- const vals = allBoundList(s, env); if (!vals || vals.length !== 2) return [];
1063
- const data = stringValue(vals[0]), pattern = stringValue(vals[1]);
1064
- const re = compileRegex(pattern); if (!re) return [];
1065
- const m = data.match(re); if (!m || m.length < 2) return [];
1066
- return output(m[1]);
1067
- }
1068
- return null;
1069
- }
1070
- function builtinCrypto(name, s, o, env) {
1071
- const algos = { 'crypto:sha': 'sha1', 'crypto:md5': 'md5', 'crypto:sha256': 'sha256', 'crypto:sha512': 'sha512' };
1072
- if (!Object.hasOwn(algos, name)) return null;
1073
- if (!termIsGround(s, env)) return [];
1074
- const digest = crypto.createHash(algos[name]).update(String(primitive(resolve(s, env))), 'utf8').digest('hex');
1075
- return bindTermResult(o, lit(digest), env);
1076
- }
1077
- function formulaContains(graph, patternAtoms, env, rules, depth) {
1078
- const localGraph = graph && graph.facts ? graph : makeGraph(graph?.atoms || graph?.facts || []);
1079
- return evalBody(patternAtoms, [env], localGraph, rules, depth + 1).length > 0;
1080
- }
1081
- function builtinLog(name, s, o, env, graph, rules, depth) {
1082
- if (name === 'log:equalTo') return bindTermResult(s, resolve(o, env), env);
1083
- if (name === 'log:notEqualTo') {
1084
- const out = unify(resolve(s, env), resolve(o, env), env);
1085
- return out ? [] : [env];
1086
- }
1087
- if (name === 'log:conjunction') {
1088
- const vals = allBoundList(s, env); if (!vals) return [];
1089
- const merged = [], seen = new Set();
1090
- for (const part of vals) {
1091
- if (part.kind === 'lit' && part.value === true) continue;
1092
- if (part.kind !== 'formula') return [];
1093
- for (const atom of part.atoms) { const k = factKey(atom); if (!seen.has(k)) { seen.add(k); merged.push(atom); } }
1094
- }
1095
- return bindTermResult(o, { kind: 'formula', atoms: merged }, env);
1096
- }
1097
- if (name === 'log:includes' || name === 'log:notIncludes') {
1098
- const scope = resolve(s, env), pattern = resolve(o, env);
1099
- if (pattern.kind !== 'formula') return [];
1100
- const local = scope.kind === 'formula' ? makeGraph(scope.atoms) : graph;
1101
- const ok = evalBody(pattern.atoms, [env], local, rules, depth + 1).length > 0;
1102
- return succeedIf(name === 'log:includes' ? ok : !ok, env);
1103
- }
1104
- if (name === 'log:rawType') {
1105
- const rt = resolve(s, env);
1106
- if (rt.kind === 'var') return [];
1107
- const ty = rt.kind === 'formula' ? 'log:Formula' : rt.kind === 'lit' ? 'log:Literal' : rt.kind === 'list' ? 'rdf:List' : 'log:Other';
1108
- return bindTermResult(o, { kind: 'iri', value: ty }, env);
1109
- }
1110
- if (name === 'log:dtlit') {
1111
- const l = resolve(s, env), obj = resolve(o, env);
1112
- if (l.kind === 'list' && l.items.length === 2) {
1113
- const lex = stringValue(resolve(l.items[0], env));
1114
- const dt = resolve(l.items[1], env);
1115
- if (lex === null || dt.kind !== 'iri') return [];
1116
- return bindTermResult(o, lit(lex), env);
1117
- }
1118
- if (obj.kind === 'var') return [];
1119
- return stringValue(obj) === null ? [] : [env];
1120
- }
1121
- if (name === 'log:outputString') return [env];
1122
- if (name === 'log:collectAllIn') {
1123
- const l = resolve(s, env); if (l.kind !== 'list' || l.items.length !== 3) return [];
1124
- const [valueTemplate, clause, outList] = l.items;
1125
- const scope = resolve(o, env);
1126
- const local = scope.kind === 'formula' ? makeGraph(scope.atoms) : graph;
1127
- if (clause.kind !== 'formula') return [];
1128
- const sols = evalBody(clause.atoms, [env], local, rules, depth + 1);
1129
- return bindTermResult(outList, list(sols.map((sol) => resolve(valueTemplate, sol))), env);
1130
- }
1131
- if (name === 'log:forAllIn') {
1132
- const l = resolve(s, env); if (l.kind !== 'list' || l.items.length !== 2) return [];
1133
- const [where, then] = l; if (where.kind !== 'formula' || then.kind !== 'formula') return [];
1134
- const scope = resolve(o, env);
1135
- const local = scope.kind === 'formula' ? makeGraph(scope.atoms) : graph;
1136
- const sols = evalBody(where.atoms, [{}], local, rules, depth + 1);
1137
- for (const sol of sols) if (!evalBody(then.atoms, [sol], local, rules, depth + 1).length) return [];
1138
- return [env];
1139
- }
1140
- if (['log:content', 'log:semantics', 'log:semanticsOrError', 'log:parsedAsN3'].includes(name)) {
1141
- throw new Error(name + ' requires dereferencing or parsing at runtime and is intentionally not available in offline specialized SEE output');
1142
- }
1143
- return null;
1144
- }
1145
- function splitList(items, parts, env, start = 0, idx = 0) {
1146
- if (idx === parts.length) return start === items.length ? [env] : [];
1147
- const outs = [];
1148
- const last = idx === parts.length - 1;
1149
- for (let end = start; end <= items.length; end += 1) {
1150
- if (last && end !== items.length) continue;
1151
- const out = bind(parts[idx], list(items.slice(start, end)), env);
1152
- if (out) outs.push(...splitList(items, parts, out, end, idx + 1));
1153
- }
1154
- return outs;
1155
- }
1156
- function termSortKey(t) {
1157
- if (t.kind === 'lit') { const n = Number(t.value); return Number.isNaN(n) ? '1:' + String(t.value) : '0:' + n.toString().padStart(20, '0'); }
1158
- if (t.kind === 'iri') return '2:' + t.value;
1159
- if (t.kind === 'list') return '3:' + t.items.map(termSortKey).join('|');
1160
- return '9:' + JSON.stringify(canonical(t));
1161
- }
1162
- function builtinList(name, s, o, env, graph, rules, depth) {
1163
- const predName = name === 'rdf:first' ? 'list:first' : name === 'rdf:rest' ? 'list:rest' : name;
1164
- const left = resolve(s, env);
1165
- const right = resolve(o, env);
1166
- const listItems = (term) => term.kind === 'list' ? term.items.map((x) => resolve(x, env)) : null;
1167
- if (predName === 'list:first') {
1168
- const xs = listItems(left); if (!xs || !xs.length) return [];
1169
- return bindTermResult(o, xs[0], env);
1170
- }
1171
- if (predName === 'list:rest') {
1172
- const xs = listItems(left); if (!xs || !xs.length) return [];
1173
- return bindTermResult(o, list(xs.slice(1)), env);
1174
- }
1175
- if (predName === 'list:firstRest') {
1176
- const outs = [];
1177
- if (left.kind === 'list' && left.items.length > 0 && !left.items.some((x) => resolve(x, env).kind === 'var')) {
1178
- const items = left.items.map((x) => resolve(x, env));
1179
- const out = bind(o, list([items[0], list(items.slice(1))]), env); if (out) outs.push(out);
1180
- }
1181
- if (right.kind === 'list' && right.items.length === 2) {
1182
- const rest = resolve(right.items[1], env);
1183
- const first = resolve(right.items[0], env);
1184
- if (rest.kind === 'list') { const out = bind(s, list([first, ...rest.items]), env); if (out) outs.push(out); }
1185
- }
1186
- return outs;
1187
- }
1188
- if (predName === 'list:append') {
1189
- if (left.kind !== 'list') return [];
1190
- if (right.kind === 'list') return splitList(right.items, left.items, env);
1191
- const outElems = [];
1192
- for (const part of left.items) { const p = resolve(part, env); if (p.kind !== 'list') return []; outElems.push(...p.items); }
1193
- return bindTermResult(o, list(outElems), env);
1194
- }
1195
- if (predName === 'list:iterate') {
1196
- const xs = listItems(left); if (!xs) return [];
1197
- const outs = [];
1198
- for (let i = 0; i < xs.length; i += 1) { const out = bind(o, list([lit(i), xs[i]]), env); if (out) outs.push(out); }
1199
- return outs;
1200
- }
1201
- if (predName === 'list:last') {
1202
- const xs = listItems(left); if (!xs || !xs.length) return [];
1203
- return bindTermResult(o, xs[xs.length - 1], env);
1204
- }
1205
- if (predName === 'list:memberAt') {
1206
- if (left.kind !== 'list' || left.items.length !== 2) return [];
1207
- const arr = resolve(left.items[0], env), idx = resolve(left.items[1], env);
1208
- if (arr.kind !== 'list') return [];
1209
- const outs = [];
1210
- for (let i = 0; i < arr.items.length; i += 1) {
1211
- let out = idx.kind === 'var' ? bind(left.items[1], lit(i), env) : (Number(idx.value) === i ? env : null);
1212
- if (out) out = bind(o, arr.items[i], out);
1213
- if (out) outs.push(out);
1214
- }
1215
- return outs;
1216
- }
1217
- if (predName === 'list:remove') {
1218
- if (left.kind !== 'list' || left.items.length !== 2) return [];
1219
- const arr = resolve(left.items[0], env), item = resolve(left.items[1], env);
1220
- if (arr.kind !== 'list' || item.kind === 'var') return [];
1221
- return bindTermResult(o, list(arr.items.filter((x) => !deepEqual(resolve(x, env), item))), env);
1222
- }
1223
- if (predName === 'list:member') {
1224
- const xs = listItems(left); if (!xs) return [];
1225
- return xs.map((x) => bind(o, x, env)).filter(Boolean);
1226
- }
1227
- if (predName === 'list:in') {
1228
- const xs = listItems(right); if (!xs) return [];
1229
- return xs.map((x) => bind(s, x, env)).filter(Boolean);
1230
- }
1231
- if (predName === 'list:length') {
1232
- const xs = listItems(left); if (!xs) return [];
1233
- return bindTermResult(o, lit(xs.length), env);
1234
- }
1235
- if (predName === 'list:notMember') {
1236
- const xs = listItems(left); if (!xs) return [];
1237
- return xs.some((x) => unify(o, x, env)) ? [] : [env];
1238
- }
1239
- if (predName === 'list:reverse') {
1240
- if (left.kind === 'list') return bindTermResult(o, list([...left.items].reverse()), env);
1241
- if (right.kind === 'list') return bindTermResult(s, list([...right.items].reverse()), env);
1242
- return [];
1243
- }
1244
- if (predName === 'list:sort') {
1245
- const source = left.kind === 'list' ? left : right.kind === 'list' ? right : null;
1246
- if (!source || source.items.some((x) => resolve(x, env).kind === 'var')) return [];
1247
- const sorted = [...source.items].sort((a, b) => termSortKey(a).localeCompare(termSortKey(b)));
1248
- return left.kind === 'list' ? bindTermResult(o, list(sorted), env) : bindTermResult(s, list(sorted), env);
1249
- }
1250
- if (predName === 'list:map') {
1251
- if (left.kind !== 'list' || left.items.length !== 2) return [];
1252
- const arr = resolve(left.items[0], env), pred = resolve(left.items[1], env);
1253
- if (arr.kind !== 'list' || pred.kind !== 'iri') return [];
1254
- const values = [];
1255
- for (let i = 0; i < arr.items.length; i += 1) {
1256
- const valueVar = { kind: 'var', value: '__map_' + depth + '_' + i };
1257
- const matches = evalAtom({ s: arr.items[i], p: pred, o: valueVar }, env, graph, rules, depth + 1);
1258
- for (const m of matches) { const v = resolve(valueVar, m); if (v.kind !== 'var') values.push(v); }
1259
- }
1260
- return bindTermResult(o, list(values), env);
1261
- }
1262
- return null;
1263
- }
1264
- function parseDateTimeParts(t) {
1265
- const v = primitive(t);
1266
- if (typeof v !== 'string') return null;
1267
- const m = v.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:\d{2})?$/);
1268
- if (!m) return null;
1269
- return { year: Number(m[1]), month: Number(m[2]), day: Number(m[3]), hour: Number(m[4]), minute: Number(m[5]), second: Number(m[6]), timeZone: m[7] || '' };
1270
- }
1271
- function builtinTime(name, s, o, env) {
1272
- if (name === 'time:localTime') return bindTermResult(o, lit(new Date().toISOString().replace(/\.000Z$/, 'Z')), env);
1273
- const parts = parseDateTimeParts(resolve(s, env)); if (!parts) return [];
1274
- const key = name.slice('time:'.length);
1275
- if (['year','month','day','hour','minute','second'].includes(key)) return bindTermResult(o, lit(parts[key]), env);
1276
- if (key === 'timeZone') return bindTermResult(o, lit(parts.timeZone || 'Z'), env);
1277
- return null;
1278
- }
1279
- function evalBuiltin(atom, env, graph, rules, depth) {
1280
- const pred = resolve(atom.p, env);
1281
- if (pred.kind !== 'iri') return null;
1282
- const name = pred.value;
1283
- if (name.startsWith('math:')) return builtinMath(name, atom.s, atom.o, env);
1284
- if (name.startsWith('string:')) return builtinString(name, atom.s, atom.o, env);
1285
- if (name.startsWith('crypto:')) return builtinCrypto(name, atom.s, atom.o, env);
1286
- if (name.startsWith('list:') || name === 'rdf:first' || name === 'rdf:rest') return builtinList(name, atom.s, atom.o, env, graph, rules, depth);
1287
- if (name.startsWith('time:')) return builtinTime(name, atom.s, atom.o, env);
1288
- if (name.startsWith('log:')) return builtinLog(name, atom.s, atom.o, env, graph, rules, depth);
1289
- return null;
1290
- }
1291
- function renameTerm(term, suffix) {
1292
- if (term.kind === 'var') return { kind: 'var', value: term.value + suffix };
1293
- if (term.kind === 'list') return list(term.items.map((item) => renameTerm(item, suffix)));
1294
- if (term.kind === 'formula') return { kind: 'formula', atoms: term.atoms.map((a) => renameAtom(a, suffix)) };
1295
- return cloneTerm(term);
1296
- }
1297
- function renameAtom(atom, suffix) {
1298
- return { s: renameTerm(atom.s, suffix), p: renameTerm(atom.p, suffix), o: renameTerm(atom.o, suffix) };
1299
- }
1300
- let renameCounter = 0;
1301
- function evalBackward(atom, env, graph, rules, depth) {
1302
- if (depth > 200) throw new Error('Backward rule recursion limit exceeded');
1303
- const outs = [];
1304
- for (const rule of rules) {
1305
- if (rule.kind !== 'backward') continue;
1306
- for (const headAtom of rule.head) {
1307
- const suffix = '__rule' + rule.id + '_' + depth + '_' + (++renameCounter);
1308
- const renamedHead = renameAtom(headAtom, suffix);
1309
- let out = unify(atom.p, renamedHead.p, env); if (!out) continue;
1310
- out = unify(atom.s, renamedHead.s, out); if (!out) continue;
1311
- out = unify(atom.o, renamedHead.o, out); if (!out) continue;
1312
- const renamedBody = rule.body.map((bodyAtom) => renameAtom(bodyAtom, suffix));
1313
- outs.push(...evalBody(renamedBody, [out], graph, rules, depth + 1));
1314
- }
1315
- }
1316
- return outs;
1317
- }
1318
- function evalAtom(atom, env, graph, rules, depth = 0) {
1319
- const builtin = evalBuiltin(atom, env, graph, rules, depth);
1320
- if (builtin !== null) return builtin;
1321
- const outs = [];
1322
- for (const fact of candidateFactsForAtom(atom, env, graph)) {
1323
- const out = matchFact(atom, fact, env);
1324
- if (out) outs.push(out);
1325
- }
1326
- outs.push(...evalBackward(atom, env, graph, rules, depth + 1));
1327
- return outs;
1328
- }
1329
- function evalBody(atoms, envs, graph, rules, depth = 0) {
1330
- let out = envs;
1331
- for (const atom of atoms) {
1332
- const next = [];
1333
- for (const env of out) next.push(...evalAtom(atom, env, graph, rules, depth));
1334
- out = next;
1335
- if (!out.length) break;
1336
- }
1337
- return out;
1338
- }
1339
- function envSignature(env) {
1340
- return crypto.createHash('sha1').update(JSON.stringify(Object.keys(env).sort().map((k) => [k, canonical(env[k])]))).digest('hex').slice(0, 12);
1341
- }
1342
- function instantiate(term, env, ruleId) {
1343
- if (term.kind === 'var') {
1344
- if (!Object.hasOwn(env, term.value)) throw new Error('Unbound variable in rule head: ?' + term.value);
1345
- return cloneTerm(resolve(env[term.value], env));
1346
- }
1347
- if (term.kind === 'blank') return blank('_:r' + ruleId + '_' + envSignature(env) + '_' + term.value.replace(/^_/, ''));
1348
- if (term.kind === 'list') return list(term.items.map((item) => instantiate(item, env, ruleId)));
1349
- 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) };
1350
- 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) })) };
1351
- return cloneTerm(term);
1352
- }
1353
- function supportFactsForBody(body, env, graph) {
1354
- const seen = new Set();
1355
- const out = [];
1356
- for (const atom of body || []) {
1357
- const pred = resolve(atom.p, env);
1358
- if (pred.kind === 'iri') {
1359
- const builtin = pred.value.startsWith('math:') || pred.value.startsWith('string:') || pred.value.startsWith('crypto:') ||
1360
- pred.value.startsWith('list:') || pred.value.startsWith('time:') || pred.value.startsWith('log:') ||
1361
- pred.value === 'rdf:first' || pred.value === 'rdf:rest';
1362
- if (builtin) continue;
1363
- }
1364
- for (const fact of candidateFactsForAtom(atom, env, graph)) {
1365
- if (!matchFact(atom, fact, env)) continue;
1366
- const key = factKey(fact);
1367
- if (!seen.has(key)) { seen.add(key); out.push(fact); }
1368
- }
1369
- }
1370
- return out;
1371
- }
1372
- function makeIndex() { return { p: new Map(), spByP: new Map(), poByP: new Map(), s: null, o: null }; }
1373
- function pushIndex(map, key, fact) {
1374
- let bucket = map.get(key);
1375
- if (!bucket) { bucket = []; map.set(key, bucket); }
1376
- bucket.push(fact);
1377
- }
1378
- function makeGraph(facts) {
1379
- const graph = { facts: [], keys: new Set(), index: makeIndex() };
1380
- for (const fact of facts) addFact(graph, fact);
1381
- return graph;
1382
- }
1383
- function ensureFlatIndex(graph, kind, termSelector) {
1384
- if (!graph.index[kind]) {
1385
- const map = new Map();
1386
- for (const fact of graph.facts) pushIndex(map, termIndexKey(termSelector(fact)), fact);
1387
- graph.index[kind] = map;
1388
- }
1389
- return graph.index[kind];
1390
- }
1391
- function ensurePredicateTermIndex(graph, byPredicate, pKey, pFacts, termSelector) {
1392
- let map = graph.index[byPredicate].get(pKey);
1393
- if (!map) {
1394
- map = new Map();
1395
- for (const fact of pFacts) pushIndex(map, termIndexKey(termSelector(fact)), fact);
1396
- graph.index[byPredicate].set(pKey, map);
1397
- }
1398
- return map;
1399
- }
1400
- function addFact(graph, fact) {
1401
- const key = factKey(fact);
1402
- if (graph.keys.has(key)) return false;
1403
- graph.keys.add(key);
1404
- graph.facts.push(fact);
1405
- const pKey = termIndexKey(fact.p);
1406
- pushIndex(graph.index.p, pKey, fact);
1407
- if (graph.index.s) pushIndex(graph.index.s, termIndexKey(fact.s), fact);
1408
- if (graph.index.o) pushIndex(graph.index.o, termIndexKey(fact.o), fact);
1409
- const sp = graph.index.spByP.get(pKey);
1410
- if (sp) pushIndex(sp, termIndexKey(fact.s), fact);
1411
- const po = graph.index.poByP.get(pKey);
1412
- if (po) pushIndex(po, termIndexKey(fact.o), fact);
1413
- return true;
1414
- }
1415
- function candidateFactsForAtom(atom, env, graph) {
1416
- if (!graph || !graph.index) return graph && graph.facts ? graph.facts : [];
1417
- const s = resolve(atom.s, env), p = resolve(atom.p, env), o = resolve(atom.o, env);
1418
- const sg = termIsConcrete(s), pg = termIsConcrete(p), og = termIsConcrete(o);
1419
- if (pg) {
1420
- const pKey = termIndexKey(p);
1421
- const pFacts = graph.index.p.get(pKey) || [];
1422
- if (sg) return ensurePredicateTermIndex(graph, 'spByP', pKey, pFacts, (f) => f.s).get(termIndexKey(s)) || [];
1423
- if (og) return ensurePredicateTermIndex(graph, 'poByP', pKey, pFacts, (f) => f.o).get(termIndexKey(o)) || [];
1424
- return pFacts;
1425
- }
1426
- if (sg) return ensureFlatIndex(graph, 's', (f) => f.s).get(termIndexKey(s)) || [];
1427
- if (og) return ensureFlatIndex(graph, 'o', (f) => f.o).get(termIndexKey(o)) || [];
1428
- return graph.facts;
1429
- }
1430
-
1431
- function isBuiltinPredicateValue(name) {
1432
- return typeof name === 'string' && (name.startsWith('math:') || name.startsWith('string:') || name.startsWith('crypto:') ||
1433
- name.startsWith('list:') || name.startsWith('time:') || name.startsWith('log:') || name === 'rdf:first' || name === 'rdf:rest');
1434
- }
1435
- function collectFormulaDependencyKeys(term, keys) {
1436
- if (!term) return true;
1437
- if (term.kind === 'formula') {
1438
- for (const atom of term.atoms || []) {
1439
- if (!collectAtomDependencyKeys(atom, keys)) return false;
1440
- }
1441
- return true;
1442
- }
1443
- if (term.kind === 'list') {
1444
- for (const item of term.items || []) if (!collectFormulaDependencyKeys(item, keys)) return false;
1445
- }
1446
- return true;
1447
- }
1448
- function collectAtomDependencyKeys(atom, keys) {
1449
- const pred = atom && atom.p;
1450
- if (!pred || pred.kind === 'var') return false;
1451
- if (pred.kind === 'iri') {
1452
- if (!isBuiltinPredicateValue(pred.value)) keys.add(termIndexKey(pred));
1453
- else if (pred.value.startsWith('log:')) {
1454
- if (!collectFormulaDependencyKeys(atom.s, keys)) return false;
1455
- if (!collectFormulaDependencyKeys(atom.o, keys)) return false;
1456
- }
1457
- }
1458
- return true;
1459
- }
1460
- function ruleDependencyPredicateKeys(rule) {
1461
- const keys = new Set();
1462
- for (const atom of rule.body || []) {
1463
- if (!collectAtomDependencyKeys(atom, keys)) return null;
1464
- }
1465
- return keys;
1466
- }
1467
- function producedFactsCanAffectRules(producedPredicates, ruleDependencies) {
1468
- if (!producedPredicates || !producedPredicates.size) return false;
1469
- for (const deps of ruleDependencies) {
1470
- if (deps === null) return true;
1471
- for (const key of producedPredicates) if (deps.has(key)) return true;
1472
- }
1473
- return false;
1474
- }
1475
- function saturate(initialFacts, rules) {
1476
- const graph = makeGraph(initialFacts);
1477
- const trace = [];
1478
- const maxIterations = 10000;
1479
- const activeRules = (rules || []).filter((rule) => rule.kind !== 'backward');
1480
- const ruleDependencies = activeRules.map(ruleDependencyPredicateKeys);
1481
- for (let iter = 0; iter < maxIterations; iter += 1) {
1482
- let changed = false;
1483
- const producedPredicates = new Set();
1484
- for (const rule of activeRules) {
1485
- const matches = evalBody(rule.body, [{}], graph, rules);
1486
- if (rule.kind === 'fuse') {
1487
- if (matches.length) throw new Error('N3 fuse failed in compiled rule #' + rule.id);
1488
- continue;
1489
- }
1490
- for (const env of matches) {
1491
- const supportFacts = supportFactsForBody(rule.body, env, graph);
1492
- const produced = [];
1493
- for (const atom of rule.head) {
1494
- const fact = { s: instantiate(atom.s, env, rule.id), p: instantiate(atom.p, env, rule.id), o: instantiate(atom.o, env, rule.id) };
1495
- if (addFact(graph, fact)) {
1496
- changed = true;
1497
- produced.push(fact);
1498
- producedPredicates.add(termIndexKey(fact.p));
1499
- }
1500
- }
1501
- if (produced.length) {
1502
- trace.push({
1503
- rule: rule.id,
1504
- produced: produced.length,
1505
- producedFacts: produced.map(codeFact).map((x) => x.replace(/ \.$/, '')),
1506
- supportFacts: supportFacts.map(codeFact).map((x) => x.replace(/ \.$/, ''))
1507
- });
1508
- }
1509
- }
1510
- }
1511
- if (!changed) break;
1512
- if (!producedFactsCanAffectRules(producedPredicates, ruleDependencies)) break;
1513
- if (iter === maxIterations - 1) throw new Error('Compiled derivation did not reach a fixpoint');
1514
- }
1515
- return { graph, trace };
1516
- }
1517
- function queryFacts(graph, queries, rules) {
1518
- const out = [];
1519
- const seen = new Set();
1520
- for (const query of queries || []) {
1521
- const matches = evalBody(query.premise || [], [{}], graph, rules);
1522
- for (const env of matches) {
1523
- for (const atom of query.conclusion || []) {
1524
- const fact = { s: instantiate(atom.s, env, 'q' + query.id), p: instantiate(atom.p, env, 'q' + query.id), o: instantiate(atom.o, env, 'q' + query.id) };
1525
- if (!atomIsGround(fact, {})) throw new Error('Unbound variable in log:query projection');
1526
- const key = factKey(fact);
1527
- if (!seen.has(key)) { seen.add(key); out.push(fact); }
1528
- }
1529
- }
1530
- }
1531
- return out;
1532
- }
1533
- function outputStrings(graph) {
1534
- return graph.facts
1535
- .filter((f) => isIri(f.p, 'log:outputString'))
1536
- .sort((a, b) => display(a.s).localeCompare(display(b.s)))
1537
- .map((f) => String(primitive(f.o)));
1538
- }
1539
- function initialKeys(initialFacts) {
1540
- if (!initialFacts) return new Set();
1541
- if (initialFacts.__seeKeySet) return initialFacts.__seeKeySet;
1542
- const keys = new Set((initialFacts || []).map(factKey));
1543
- try { Object.defineProperty(initialFacts, '__seeKeySet', { value: keys, enumerable: false }); } catch (_) {}
1544
- return keys;
1545
- }
1546
- function derivedFacts(graph, initialFacts) {
1547
- const base = initialKeys(initialFacts);
1548
- return graph.facts.filter((f) => !base.has(factKey(f)));
1549
- }
1550
- function codeFact(f) { return atomToN3(f) + ' .'; }
1551
- function unquoteLiteral(text) {
1552
- const m = String(text || '').match(/^\"([\s\S]*)\"$/);
1553
- if (!m) return String(text || '');
1554
- return m[1].replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\\\/g, '\\');
1555
- }
1556
- function readablePredicateName(term) {
1557
- return String(term || '')
1558
- .replace(/^[:<#]*/, '')
1559
- .replace(/[>#]*$/g, '')
1560
- .replace(/^./, (ch) => ch.toLowerCase());
1561
- }
1562
- function compactValidationValue(value) {
1563
- return String(value || '').replace(/^OK\s*-\s*/i, '').trim();
1564
- }
1565
- function compactSupportFactText(fact) {
1566
- const text = String(fact || '')
1567
- .replace(/===\s*Reason\s+Why\s*===/gi, '=== Explanation ===')
1568
- .replace(/===\s*Review\s*===/gi, '=== Explanation ===');
1569
- const report = text.match(/^(.*?\blog:outputString)\s+/);
1570
- if (report) return report[1] + ' "[authored report]"';
1571
-
1572
- return text.replace(/\s{2,}/g, ' ').trim();
1573
- }
1574
- function compactRuleComment(comment) {
1575
- return compactSupportFactText(comment);
1576
- }
1577
- function shortTerm(t) { return termToN3(t); }
1578
- function factSentence(f) {
1579
- const s = shortTerm(f.s), p = shortTerm(f.p), o = shortTerm(f.o);
1580
- if (isIri(f.p, 'rdf:type')) return s + ' is a ' + o + '.';
1581
- if (isIri(f.p, 'rdfs:subClassOf')) return s + ' is a subclass of ' + o + '.';
1582
- if (isIri(f.p, ':is') || /(^|:)is$/.test(p)) return s + ' is ' + o + '.';
1583
- return s + ' ' + p + ' ' + o + '.';
1584
- }
1585
- function previewItems(items, limit = 2) {
1586
- const list = Array.isArray(items) ? items.filter(Boolean).map(compactRuleComment) : [];
1587
- const shown = list.slice(0, limit).join('; ');
1588
- const more = list.length > limit ? '; … +' + (list.length - limit) + ' more' : '';
1589
- return shown + more;
1590
- }
1591
- function describeRule(rule) {
1592
- if (!rule) return 'compiled rule';
1593
- const body = rule.bodyComment || [];
1594
- const head = rule.headComment || [];
1595
- if (rule.kind === 'fuse') {
1596
- const preview = previewItems(body, 2) || 'forbidden pattern';
1597
- return 'Fuse ' + rule.id + ' guards against ' + preview;
1598
- }
1599
- const bodyCount = body.length;
1600
- const headCount = head.length;
1601
- if (bodyCount <= 2 && headCount <= 2) {
1602
- return 'Rule ' + rule.id + ': ' + (previewItems(body, 2) || 'true') + ' => ' + (previewItems(head, 2) || 'false');
1603
- }
1604
- return 'Rule ' + rule.id + ' (' + bodyCount + ' premise pattern(s) => ' + headCount + ' conclusion pattern(s))';
1605
- }
1606
- function traceApplications(trace, rules, limit = 6) {
1607
- const byId = new Map((rules || []).map((r) => [r.id, r]));
1608
- const grouped = new Map();
1609
- for (const step of trace || []) {
1610
- const facts = Array.isArray(step.producedFacts) ? step.producedFacts : [];
1611
- if (!facts.length) continue;
1612
- const key = String(step.rule);
1613
- if (!grouped.has(key)) grouped.set(key, { rule: byId.get(step.rule), facts: [], supportFacts: [] });
1614
- const entry = grouped.get(key);
1615
- for (const fact of facts) if (!entry.facts.includes(fact)) entry.facts.push(fact);
1616
- for (const fact of Array.isArray(step.supportFacts) ? step.supportFacts : []) {
1617
- if (!entry.supportFacts.includes(fact)) entry.supportFacts.push(fact);
1618
- }
1619
- }
1620
- return Array.from(grouped.values()).slice(0, limit);
1621
- }
1622
- function supportIndex(trace) {
1623
- const index = new Map();
1624
- for (const step of trace || []) {
1625
- const facts = Array.isArray(step.producedFacts) ? step.producedFacts : [];
1626
- for (const fact of facts) {
1627
- if (!index.has(fact)) {
1628
- index.set(fact, { rule: step.rule, supportFacts: Array.isArray(step.supportFacts) ? step.supportFacts : [] });
1629
- }
1630
- }
1631
- }
1632
- return index;
1633
- }
1634
- function supportTreeLines(fact, supportMap, sourceKeys, ruleMap, opts = {}) {
1635
- const lines = [];
1636
- const maxDepth = opts.maxDepth ?? 4;
1637
- const maxChildren = opts.maxChildren ?? 4;
1638
- const maxLines = opts.maxLines ?? 40;
1639
- function walk(item, depth, seen) {
1640
- if (lines.length >= maxLines) return;
1641
- const indent = ' '.repeat(depth);
1642
- if (/\blog:outputString\b/.test(String(item || ''))) {
1643
- const support = supportMap.get(item);
1644
- const rule = support ? ruleMap.get(support.rule) : null;
1645
- const label = support ? (rule ? 'Rule ' + rule.id : 'compiled rule #' + support.rule) : 'no recorded rule support';
1646
- lines.push(indent + '- ' + compactSupportFactText(item) + ' . _(authored report, ' + label + ')_');
1647
- return;
1648
- }
1649
- if (sourceKeys.has(item)) {
1650
- lines.push(indent + '- ' + compactSupportFactText(item) + ' . _(source)_');
1651
- return;
1652
- }
1653
- const support = supportMap.get(item);
1654
- if (!support) {
1655
- lines.push(indent + '- ' + compactSupportFactText(item) + ' . _(no recorded rule support)_');
1656
- return;
1657
- }
1658
- const rule = ruleMap.get(support.rule);
1659
- const label = rule ? 'Rule ' + rule.id : 'compiled rule #' + support.rule;
1660
- lines.push(indent + '- ' + compactSupportFactText(item) + ' . _(derived by ' + label + ')_');
1661
- if (depth >= maxDepth) {
1662
- if (support.supportFacts && support.supportFacts.length) lines.push(indent + ' - support omitted beyond depth ' + maxDepth);
1663
- return;
1664
- }
1665
- const children = (support.supportFacts || []).slice(0, maxChildren);
1666
- const childIndent = indent + ' ';
1667
- if (!children.length) {
1668
- lines.push(childIndent + '- no graph premises; built-ins/constants satisfied the rule.');
1669
- return;
1670
- }
1671
- for (const child of children) {
1672
- if (seen.has(child)) {
1673
- lines.push(childIndent + '- ' + compactSupportFactText(child) + ' . _(already shown)_');
1674
- continue;
1675
- }
1676
- const nextSeen = new Set(seen);
1677
- nextSeen.add(child);
1678
- walk(child, depth + 1, nextSeen);
1679
- if (lines.length >= maxLines) break;
1680
- }
1681
- if ((support.supportFacts || []).length > children.length) {
1682
- lines.push(childIndent + '- ... ' + ((support.supportFacts || []).length - children.length) + ' more premise fact(s)');
1683
- }
1684
- }
1685
- walk(fact, 0, new Set([fact]));
1686
- if (lines.length >= maxLines) lines.push('- … support tree truncated after ' + maxLines + ' line(s)');
1687
- return lines;
1688
- }
1689
- function conclusionSupportSection(selected, trace, rules, initialFacts, limit = 6) {
1690
- const supportMap = supportIndex(trace);
1691
- const sourceKeys = new Set((initialFacts || []).map((f) => codeFact(f).replace(/ \.$/, '')));
1692
- const ruleMap = new Map((rules || []).map((r) => [r.id, r]));
1693
- const facts = [];
1694
- const seen = new Set();
1695
- for (const f of selected || []) {
1696
- const key = codeFact(f).replace(/ \.$/, '');
1697
- if (!seen.has(key)) { seen.add(key); facts.push(key); }
1698
- }
1699
- const focus = facts.slice(-limit).reverse();
1700
- if (!focus.length) return [];
1701
- const lines = [];
1702
- lines.push('Selected explanation support:');
1703
- for (const fact of focus) {
1704
- lines.push(...supportTreeLines(fact, supportMap, sourceKeys, ruleMap).map((line) => ' ' + line));
1705
- }
1706
- return lines;
1707
- }
1708
- function evidenceSummaryLine(mode) {
1709
- if (mode === 'query') return 'The query-selected facts are serialized in the Formal TriG Output section.';
1710
- if (mode === 'formula') return 'The formula-valued facts are serialized in the Formal TriG Output section.';
1711
- return 'The selected facts are serialized in the Formal TriG Output section.';
1712
- }
1713
- function renderStructuredOutput({ title, graph, queries = [], rules = [], initialFacts = [], trace = [], mode = 'derived' }) {
1714
- let selected = [];
1715
- if (mode === 'query') selected = queryFacts(graph, queries, rules);
1716
- else if (mode === 'formula') selected = graph.facts.filter((f) => f.o && f.o.kind === 'formula');
1717
- else selected = derivedFacts(graph, initialFacts);
1718
- if (!selected.length) selected = graph.facts.slice(0, 30);
1719
-
1720
- const derived = derivedFacts(graph, initialFacts);
1721
- const keyFact = selected[selected.length - 1];
1722
- const lines = [];
1723
- lines.push('# ' + title);
1724
- lines.push('');
1725
- lines.push('## Entailment');
1726
- if (mode === 'query') {
1727
- lines.push('The compiled query selected ' + selected.length + ' fact(s) after the rule closure was computed.');
1728
- } else if (mode === 'formula') {
1729
- lines.push('The derivation produced ' + selected.length + ' formula-valued entailment(s).');
1730
- } else {
1731
- lines.push('The derivation produced ' + derived.length + ' new fact(s) from ' + initialFacts.length + ' stated fact(s).');
1732
- }
1733
- if (keyFact) lines.push('Main entailment: **' + factSentence(keyFact) + '**');
1734
- const bullets = selected.slice(-6).reverse();
1735
- if (bullets.length) {
1736
- lines.push('');
1737
- lines.push('Selected entailments:');
1738
- for (const fact of bullets) lines.push('- ' + codeFact(fact));
1739
- }
1740
- lines.push('');
1741
- lines.push('## Explanation');
1742
- const ordinaryRules = (rules || []).filter((r) => r.kind !== 'fuse').length;
1743
- const fuses = (rules || []).filter((r) => r.kind === 'fuse').length;
1744
- lines.push('Starts with ' + initialFacts.length + ' source fact(s), applies ' + ordinaryRules + ' rule(s), and reaches a fixpoint.');
1745
- if (queries.length) lines.push('The log:query projection then keeps only the matching fact(s) shown above.');
1746
- if (fuses) lines.push('The run also validates ' + fuses + ' fuse(s) for forbidden patterns.');
1747
- const apps = traceApplications(trace, rules);
1748
- if (apps.length) {
1749
- lines.push('');
1750
- lines.push('Derivation steps:');
1751
- const sourceKeys = new Set((initialFacts || []).map((f) => codeFact(f).replace(/ \.$/, '')));
1752
- for (const app of apps) {
1753
- const produced = app.facts.slice(0, 4).map((f) => compactSupportFactText(f) + ' .').join(', ');
1754
- const more = app.facts.length > 4 ? ', … +' + (app.facts.length - 4) + ' more' : '';
1755
- lines.push('- ' + describeRule(app.rule) + ' derives ' + produced + more);
1756
- if (app.supportFacts && app.supportFacts.length) {
1757
- const used = app.supportFacts.slice(0, 4).map((fact) => compactSupportFactText(fact) + ' . ' + (sourceKeys.has(fact) ? '_(source)_' : '_(derived)_')).join('; ');
1758
- const omitted = app.supportFacts.length > 4 ? '; … +' + (app.supportFacts.length - 4) + ' more premise fact(s)' : '';
1759
- lines.push(' - Uses: ' + used + omitted);
1760
- } else {
1761
- lines.push(' - Uses: no graph premises; built-ins/constants satisfied the rule.');
1762
- }
1763
- }
1764
- }
1765
- const supportLines = conclusionSupportSection(selected, trace, rules, initialFacts);
1766
- if (supportLines.length) {
1767
- lines.push('');
1768
- lines.push(...supportLines);
1769
- }
1770
- lines.push('');
1771
- lines.push(evidenceSummaryLine(mode));
1772
- return lines.join('\n') + '\n';
1773
- }
1774
- function renderRawOutput(graph, queries = [], rules = [], initialFacts = []) {
1775
- const outs = outputStrings(graph);
1776
- if (outs.length) return outs.join('');
1777
- if (queries && queries.length) {
1778
- const selected = queryFacts(graph, queries, rules);
1779
- if (selected.length) return selected.map((f) => codeFact(f)).join('\n') + '\n';
1780
- return '';
1781
- }
1782
- const formulaFacts = graph.facts.filter((f) => f.o && f.o.kind === 'formula');
1783
- if (formulaFacts.length) return formulaFacts.map((f) => codeFact(f)).join('\n') + '\n';
1784
- const derived = derivedFacts(graph, initialFacts);
1785
- const selected = derived.length ? derived : graph.facts.slice(0, 30);
1786
- return selected.map((f) => codeFact(f)).join('\n') + '\n';
1787
- }
1788
- function dedupeExplanationHeadings(text) {
1789
- let seen = false;
1790
- return String(text || '').replace(/^##\s+Explanation\s*$/gmi, () => {
1791
- if (seen) return '';
1792
- seen = true;
1793
- return '## Explanation';
1794
- });
1795
- }
1796
- function normalizePublicReport(markdown, title) {
1797
- let text = String(markdown || '').trimEnd();
1798
- if (!/^\s*#\s+/m.test(text)) text = '# ' + title + '\n\n' + text;
1799
- if (!/^##\s+Entailment\s*$/mi.test(text)) {
1800
- text = text.replace(/^(#\s+[^\n]+\n*)/, '$1\n## Entailment\n');
1801
- }
1802
- if (!/^##\s+Explanation\s*$/mi.test(text)) {
1803
- text += '\n\n## Explanation\nNo additional explanation was provided by the generated output.';
1804
- }
1805
- text = text.replace(/^##\s+([^\n]+?)\s*$/gm, (line, heading) => {
1806
- const normalized = heading.trim().toLowerCase();
1807
- if (normalized === 'insight' || normalized === 'conclusion' || normalized === 'entailment' || normalized === 'explanation') return '## ' + (normalized === 'explanation' ? 'Explanation' : 'Entailment');
1808
- return '**' + heading.trim() + '**';
1809
- });
1810
- text = dedupeExplanationHeadings(text);
1811
- return text.trimEnd() + '\n';
1812
- }
1813
- function markdownize(raw, title) {
1814
- let text = String(raw || '');
1815
- text = text
1816
- .replace(/===\s*Answer\s*===/g, '## Entailment')
1817
- .replace(/===\s*Reason\s+Why\s*===/gi, '## Explanation')
1818
- .replace(/===\s*Explanation\s*===/gi, '## Explanation')
1819
- .replace(/===\s*([^=]+?)\s*===/g, (_, h) => '**' + h.trim() + '**');
1820
- text = text.replace(/^C(\d+)\s+OK\s*-\s*/gm, 'C$1: ');
1821
- text = dedupeExplanationHeadings(text);
1822
- if (!text.trim()) text = '## Entailment\nNo log:outputString facts were derived.\n\n## Explanation\nThe compiled derivation did not produce authored report text.';
1823
- return normalizePublicReport(text, title);
1824
- }
1825
- function authoredSupportAppendix(graph, queries, rules, initialFacts, trace) {
1826
- const derived = derivedFacts(graph, initialFacts);
1827
- let selected = [];
1828
- if (queries && queries.length) selected = queryFacts(graph, queries, rules);
1829
- else if (graph.facts.some((f) => f.o && f.o.kind === 'formula')) selected = graph.facts.filter((f) => f.o && f.o.kind === 'formula');
1830
- else selected = derived;
1831
- const lines = [];
1832
- const ordinaryRules = (rules || []).filter((r) => r.kind !== 'fuse').length;
1833
- const apps = traceApplications(trace, rules, 6);
1834
- const supportLines = conclusionSupportSection(selected, trace, rules, initialFacts, 4);
1835
- if (!apps.length && !supportLines.length) return '';
1836
- lines.push('**Generated derivation support**');
1837
- lines.push('');
1838
- lines.push('Compiled support: ' + initialFacts.length + ' source fact(s), ' + ordinaryRules + ' rule(s), fixpoint reached before rendering.');
1839
- if (apps.length) {
1840
- lines.push('');
1841
- lines.push('Derivation steps:');
1842
- const sourceKeys = new Set((initialFacts || []).map((f) => codeFact(f).replace(/ \.$/, '')));
1843
- for (const app of apps) {
1844
- const produced = app.facts.slice(0, 4).map((f) => compactSupportFactText(f) + ' .').join(', ');
1845
- const more = app.facts.length > 4 ? ', … +' + (app.facts.length - 4) + ' more' : '';
1846
- lines.push('- ' + describeRule(app.rule) + ' derives ' + produced + more);
1847
- if (app.supportFacts && app.supportFacts.length) {
1848
- const used = app.supportFacts.slice(0, 4).map((fact) => compactSupportFactText(fact) + ' . ' + (sourceKeys.has(fact) ? '_(source)_' : '_(derived)_')).join('; ');
1849
- const omitted = app.supportFacts.length > 4 ? '; … +' + (app.supportFacts.length - 4) + ' more premise fact(s)' : '';
1850
- lines.push(' - Uses: ' + used + omitted);
1851
- } else {
1852
- lines.push(' - Uses: no graph premises; built-ins/constants satisfied the rule.');
1853
- }
1854
- }
1855
- }
1856
- if (supportLines.length) {
1857
- lines.push('');
1858
- lines.push(...supportLines);
1859
- }
1860
- return lines.join('\n');
1861
- }
1862
- function appendAuthoredExplanation(markdown, graph, queries, rules, initialFacts, trace) {
1863
- const appendix = authoredSupportAppendix(graph, queries, rules, initialFacts, trace);
1864
- if (!appendix) return markdown;
1865
- return markdown.trimEnd() + '\n\n' + appendix + '\n';
1866
- }
1867
- function renderPresentation(graph, queries, rules, initialFacts, title, trace) {
1868
- const outs = outputStrings(graph);
1869
- if (outs.length) return appendAuthoredExplanation(markdownize(outs.join(''), title), graph, queries, rules, initialFacts, trace);
1870
- if (queries && queries.length) return renderStructuredOutput({ title, graph, queries, rules, initialFacts, trace, mode: 'query' });
1871
- if (graph.facts.some((f) => f.o && f.o.kind === 'formula')) return renderStructuredOutput({ title, graph, queries, rules, initialFacts, trace, mode: 'formula' });
1872
- return renderStructuredOutput({ title, graph, queries, rules, initialFacts, trace, mode: 'derived' });
1873
- }
1874
- `;
1875
- }
1876
-
1877
- function generateExampleJs(name, title, program, stats, doc) {
1878
- const rulesWithComments = program.rules.map((rule) => ({
1879
- ...rule,
1880
- bodyComment: (rule.body || []).map(atomToComment),
1881
- headComment: (rule.head || []).map(atomToComment),
1882
- }));
1883
- const queriesWithComments = (program.queries || []).map((query) => ({
1884
- ...query,
1885
- premiseComment: (query.premise || []).map(atomToComment),
1886
- conclusionComment: (query.conclusion || []).map(atomToComment),
1887
- }));
1888
- return `#!/usr/bin/env node
1889
- 'use strict';
1890
- const fs = require('fs');
1891
- const path = require('path');
1892
- const { fail, loadInput } = require('./_see');
1893
- ${runtimeSource()}
1894
- const NAME = ${JSON.stringify(name)};
1895
- const TITLE = ${JSON.stringify(title)};
1896
- const EXPECTED_INPUT_FACTS = ${stats.facts};
1897
- const RULES = ${js(rulesWithComments)};
1898
- const QUERIES = ${js(queriesWithComments)};
1899
- const DOC_MARKDOWN = ${JSON.stringify(doc)};
1900
- function seeMetadata(data) { return (data && data.__see) || {}; }
1901
- function trustedDerivation(data) { const meta = seeMetadata(data); const facts = data && Array.isArray(data.facts) ? data.facts : []; const expectedFacts = EXPECTED_INPUT_FACTS || Number(meta.InputFacts || 0); if (meta.SourceSHA256 && meta.SourceSHA256 !== ${JSON.stringify(stats.sourceHash)}) throw new Error('input evidence does not match the N3 source compiled into this example'); const result = saturate(facts, RULES); const rawOutput = renderRawOutput(result.graph, QUERIES, RULES, facts); fail('Compiled N3 derivation failed', { 'input evidence metadata is present and matches compiled source': meta.SourceSHA256 === ${JSON.stringify(stats.sourceHash)}, 'input evidence facts were loaded': expectedFacts > 0 ? facts.length === expectedFacts : facts.length >= 0, 'compiled rules were loaded': RULES.length === ${stats.rules + stats.backwardRules + stats.fuses}, 'compiled query directives were loaded': QUERIES.length === ${stats.queries}, 'a derivation fixpoint was reached': result.graph.facts.length >= facts.length, 'query or output facts were produced': rawOutput.length > 0 }); return { ...result, rawOutput, inputFacts: facts }; }
1902
- function snapshotMarkdown(markdown) { return markdown.split(/\\n/).map((line) => line ? line + ' \\n' : '\\n').join(''); }
1903
- function prefixLinesFromTrig(trig) {
1904
- const out = [];
1905
- const seen = new Set();
1906
- for (const rawLine of String(trig || '').split(String.fromCharCode(10))) {
1907
- const trimmed = rawLine.replace(String.fromCharCode(13), '').trim();
1908
- if (!trimmed.toLowerCase().startsWith('@prefix ')) continue;
1909
- if (!seen.has(trimmed)) { seen.add(trimmed); out.push(trimmed); }
1910
- }
1911
- if (!out.some((line) => line.toLowerCase().startsWith('@prefix rdf:'))) out.push('@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .');
1912
- return out;
1913
- }
1914
- function formalOutputFacts(graph, queries, rules, initialFacts) {
1915
- const base = initialKeys(initialFacts);
1916
- const out = [];
1917
- const seen = new Set();
1918
- const add = (fact) => {
1919
- if (!fact) return;
1920
- const key = factKey(fact);
1921
- if (!seen.has(key)) { seen.add(key); out.push(fact); }
1922
- };
1923
- if (queries && queries.length) {
1924
- for (const fact of queryFacts(graph, queries, rules)) add(fact);
1925
- return out;
1926
- }
1927
- for (const fact of derivedFacts(graph, initialFacts)) add(fact);
1928
- if (!out.length) {
1929
- for (const fact of graph.facts) if (fact.o && fact.o.kind === 'formula' && !base.has(factKey(fact))) add(fact);
1930
- }
1931
- return out;
1932
- }
1933
- function termHasTripleTerm(term) {
1934
- if (!term) return false;
1935
- if (term.kind === 'triple') return true;
1936
- if (term.kind === 'list') return term.items.some(termHasTripleTerm);
1937
- if (term.kind === 'formula') return term.atoms.some(atomHasTripleTerm);
1938
- return false;
1939
- }
1940
- function atomHasTripleTerm(atom) { return termHasTripleTerm(atom.s) || termHasTripleTerm(atom.p) || termHasTripleTerm(atom.o); }
1941
- function factsHaveTripleTerms(facts) { return (facts || []).some(atomHasTripleTerm); }
1942
- function trigHasVersion12(trig) { return /^\s*(?:@version|VERSION)\s+["']1\.2["']/mi.test(String(trig || '')); }
1943
- function trigGraphBlock(label, atoms) {
1944
- const lines = [label + ' {'];
1945
- for (const atom of atoms || []) lines.push(' ' + atomToN3(atom) + ' .');
1946
- lines.push('}');
1947
- return lines;
1948
- }
1949
- function formalFactToTrigLines(fact, state) {
1950
- if (fact.o && fact.o.kind === 'formula') {
1951
- if (isIri(fact.p, 'log:nameOf')) return trigGraphBlock(termToN3(fact.s), fact.o.atoms);
1952
- state.formulaCounter += 1;
1953
- state.needOutPrefix = true;
1954
- const label = 'out:formula' + state.formulaCounter;
1955
- return [termToN3(fact.s) + ' ' + termToN3(fact.p) + ' ' + label + ' .', '', ...trigGraphBlock(label, fact.o.atoms)];
1956
- }
1957
- return [codeFact(fact)];
1958
- }
1959
- function trigMetadataBlock(trig) {
1960
- const lines = String(trig || '').split(String.fromCharCode(10));
1961
- const out = [];
1962
- let depth = 0;
1963
- let active = false;
1964
- for (const line of lines) {
1965
- const trimmed = line.trim();
1966
- if (!active && !trimmed.startsWith('in:metadata')) continue;
1967
- active = true;
1968
- out.push(line.replace(String.fromCharCode(13), ''));
1969
- depth += (line.match(/\{/g) || []).length;
1970
- depth -= (line.match(/\}/g) || []).length;
1971
- if (active && depth <= 0) break;
1972
- }
1973
- return out.length ? out.join(String.fromCharCode(10)).trimEnd() : '';
1974
- }
1975
- function formalOutputToTrig(facts, trig) {
1976
- const state = { formulaCounter: 0, needOutPrefix: false };
1977
- const body = [];
1978
- for (const fact of facts || []) {
1979
- body.push(...formalFactToTrigLines(fact, state));
1980
- }
1981
- const metadata = trigMetadataBlock(trig);
1982
- if (metadata) {
1983
- if (body.length) body.push('');
1984
- body.push(metadata);
1985
- }
1986
- if (!body.length) return '';
1987
- const prefixes = prefixLinesFromTrig(trig);
1988
- if (state.needOutPrefix && !prefixes.some((line) => line.toLowerCase().startsWith('@prefix out:'))) prefixes.push('@prefix out: <https://example.org/see/output#> .');
1989
- const nl = String.fromCharCode(10);
1990
- const version = factsHaveTripleTerms(facts) ? 'VERSION "1.2"' + nl + nl : '';
1991
- return version + prefixes.join(nl) + nl + nl + body.join(nl);
1992
- }
1993
- function appendFormalTrigOutput(markdown, graph, queries, rules, initialFacts, data) {
1994
- const trig = formalOutputToTrig(formalOutputFacts(graph, queries, rules, initialFacts), data && data.trig);
1995
- if (!trig) return markdown;
1996
- const nl = String.fromCharCode(10);
1997
- const fence = String.fromCharCode(96).repeat(3);
1998
- const fenced = trig.trimEnd().replace(new RegExp(fence, 'g'), '\` \` \`');
1999
- return markdown.trimEnd() + nl + nl + '## Formal TriG Output' + nl + nl + fence + 'trig' + nl + fenced + nl + fence + nl;
2000
- }
2001
- function outputMarkdown() { const data = loadInput(NAME); const result = trustedDerivation(data); const markdown = renderPresentation(result.graph, QUERIES, RULES, result.inputFacts, TITLE, result.trace); return snapshotMarkdown(appendFormalTrigOutput(markdown, result.graph, QUERIES, RULES, result.inputFacts, data)); }
2002
- function documentationMarkdown() { return DOC_MARKDOWN; }
2003
- function writeArtefacts() { const outputDir = path.join(__dirname, 'output'); const docDir = path.join(__dirname, 'doc'); fs.mkdirSync(outputDir, { recursive: true }); fs.mkdirSync(docDir, { recursive: true }); fs.writeFileSync(path.join(outputDir, NAME + '.md'), outputMarkdown(), 'utf8'); fs.writeFileSync(path.join(docDir, NAME + '.md'), documentationMarkdown(), 'utf8'); }
2004
- function main(argv = process.argv.slice(2)) { if (argv.includes('--write') || argv.includes('--write-files') || argv.includes('--snapshot')) { writeArtefacts(); return; } if (argv.includes('--doc')) { process.stdout.write(documentationMarkdown()); return; } process.stdout.write(outputMarkdown()); }
2005
- if (require.main === module) main();
2006
- module.exports = { trustedDerivation, outputMarkdown, documentationMarkdown, writeArtefacts };
2007
- `;
2008
- }
2009
-
2010
- // Documentation is generated from compilation metadata rather than hand-written
2011
- // per example, keeping examples/output and examples/doc reproducible snapshots.
2012
- function generateDoc(name, title, header, stats) {
2013
- const description = header.description
2014
- ? `
2015
- ${header.description}
2016
- `
2017
- : '';
2018
- const builtins = stats.builtins.length ? stats.builtins.map((b) => `- \`${b}\``).join('\n') : '- none';
2019
- 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`;
2020
- }
2021
-
2022
- function runNode(file, cwd = ROOT, args = []) {
2023
- const result = spawnSync(process.execPath, [file, ...args], { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
2024
- if (result.status !== 0)
2025
- throw new Error(`generated example failed:
2026
- ${result.stderr || result.stdout}`);
2027
- return result.stdout;
2028
- }
2029
-
2030
- // compile is pure with respect to the repository: it reads one source .n3 file
2031
- // and returns all generated text. The generate/render commands decide whether
2032
- // those artefacts are written to disk or executed from a temporary directory.
2033
- function compile(n3Path, options = {}) {
2034
- const absolute = path.resolve(n3Path);
2035
- const n3 = readText(absolute);
2036
- const name = options.name || slugify(path.basename(absolute));
2037
- const title = parseHeader(n3, titleFromSlug(name)).title;
2038
- const header = parseHeader(n3, titleFromSlug(name));
2039
- const program = parseN3(n3);
2040
- const stats = compilationStats(program);
2041
- stats.sourceHash = sha256(n3);
2042
- const inputTrig = generateInputTrig(absolute, name, title, header, stats, program);
2043
- const doc = generateDoc(name, title, header, stats);
2044
- const exampleJs = generateExampleJs(name, title, program, stats, doc);
2045
- return { name, title, program, stats, inputTrig, exampleJs, doc };
2046
- }
2047
-
2048
- function inputNameCandidates(name) {
2049
- const out = [name];
2050
- const dashed = name.replace(/_/g, '-');
2051
- if (!out.includes(dashed)) out.push(dashed);
2052
- return out;
2053
- }
2054
- function inputFactsFromTrigText(trig) {
2055
- const m = String(trig || '').match(/\bsee:inputFacts\s+([0-9]+)\s*\./);
2056
- return m ? Number.parseInt(m[1], 10) : null;
2057
- }
2058
- function inputCandidateScore(file) {
2059
- try {
2060
- const stat = fs.statSync(file);
2061
- const text = fs.readFileSync(file, 'utf8');
2062
- const facts = inputFactsFromTrigText(text);
2063
- return { facts: facts ?? -1, size: stat.size };
2064
- } catch (_) {
2065
- return { facts: -1, size: -1 };
2066
- }
2067
- }
2068
- // Rules-only examples can reuse an externally authored TriG evidence file. The
2069
- // scoring prefers the candidate that advertises the most input facts, then the
2070
- // larger file, so dashed public datasets such as path-discovery.trig win over
2071
- // empty generated placeholders.
2072
- function existingExternalInputName(name) {
2073
- const candidates = inputNameCandidates(name)
2074
- .map((base, order) => ({ base, order, file: path.join(INPUT_DIR, `${base}.trig`) }))
2075
- .filter((c) => fs.existsSync(c.file))
2076
- .map((c) => ({ ...c, score: inputCandidateScore(c.file) }));
2077
- if (!candidates.length) return null;
2078
- candidates.sort((a, b) => b.score.facts - a.score.facts || b.score.size - a.score.size || a.order - b.order);
2079
- return candidates[0].base;
2080
- }
2081
- // generate writes the checked-in artefacts and immediately executes the new
2082
- // example with --write so examples/output and examples/doc remain in sync.
2083
- function generate(n3Path, options = {}) {
2084
- const compiled = compile(n3Path, options);
2085
- const jsFile = path.join(EXAMPLES_DIR, `${compiled.name}.js`);
2086
- const externalInputName = compiled.stats.facts === 0 ? existingExternalInputName(compiled.name) : null;
2087
- const inputBaseName = externalInputName || compiled.name;
2088
- const inputTrigFile = path.join(INPUT_DIR, `${inputBaseName}.trig`);
2089
- const outputFile = path.join(OUTPUT_DIR, `${compiled.name}.md`);
2090
- const docFile = path.join(DOC_DIR, `${compiled.name}.md`);
2091
- if (!options.force) {
2092
- const protectedInputs = externalInputName ? [] : [inputTrigFile];
2093
- for (const file of [outputFile, docFile, ...protectedInputs]) {
2094
- if (fs.existsSync(file))
2095
- throw new Error(`${path.relative(ROOT, file)} already exists; pass --force to overwrite`);
2096
- }
2097
- }
2098
- writeText(jsFile, compiled.exampleJs, options.force);
2099
- fs.chmodSync(jsFile, 0o755);
2100
- if (!externalInputName) writeText(inputTrigFile, compiled.inputTrig, true);
2101
- runNode(jsFile, ROOT, ['--write']);
2102
- const output = readText(outputFile);
2103
- return { ...compiled, files: { jsFile, inputTrigFile, outputFile, docFile }, output };
2104
- }
2105
-
2106
- // render is the non-mutating companion to generate. It compiles into a small
2107
- // temporary /see-shaped tree, runs the generated example, and returns Markdown.
2108
- function render(n3Path) {
2109
- const tmpName = `_see_tmp_${process.pid}`;
2110
- const compiled = compile(n3Path, { name: tmpName });
2111
- const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'see-compile-'));
2112
- const tmpSeeDir = path.join(tmpDir, 'see');
2113
- const examplesDir = path.join(tmpSeeDir, 'examples');
2114
- ensureDir(path.join(examplesDir, 'input'));
2115
- fs.copyFileSync(path.join(EXAMPLES_DIR, '_see.js'), path.join(examplesDir, '_see.js'));
2116
- fs.copyFileSync(path.join(ROOT, 'see.js'), path.join(tmpSeeDir, 'see.js'));
2117
- const jsFile = path.join(examplesDir, `${tmpName}.js`);
2118
- const trigFile = path.join(examplesDir, 'input', `${tmpName}.trig`);
2119
- fs.writeFileSync(jsFile, compiled.exampleJs, 'utf8');
2120
- fs.writeFileSync(trigFile, compiled.inputTrig, 'utf8');
2121
- try {
2122
- return runNode(jsFile, tmpSeeDir);
2123
- } finally {
2124
- fs.rmSync(tmpDir, { recursive: true, force: true });
2125
- }
2126
- }
2127
-
2128
- function parseArgs(argv) {
2129
- const args = [...argv];
2130
- const opts = { force: false };
2131
- const command = args.shift();
2132
- const file = args.shift();
2133
- while (args.length) {
2134
- const arg = args.shift();
2135
- if (arg === '--force') opts.force = true;
2136
- else if (arg === '--name') opts.name = slugify(args.shift());
2137
- else throw new Error(`Unknown argument: ${arg}`);
2138
- }
2139
- return { command, file, opts };
2140
- }
2141
-
2142
- function main() {
2143
- const { command, file, opts } = parseArgs(process.argv.slice(2));
2144
- if (!command || command === 'help' || command === '--help') {
2145
- console.log(usage());
2146
- return;
2147
- }
2148
- if (!file) throw new Error(`Missing <example.n3>\n\n${usage()}`);
2149
- if (command === 'generate') {
2150
- const result = generate(file, opts);
2151
- console.log(`generated ${path.relative(ROOT, result.files.jsFile)}`);
2152
- if (result.files.inputTrigFile) console.log(`generated ${path.relative(ROOT, result.files.inputTrigFile)}`);
2153
- console.log(`generated ${path.relative(ROOT, result.files.outputFile)}`);
2154
- console.log(`generated ${path.relative(ROOT, result.files.docFile)}`);
2155
- console.log(
2156
- `compiled ${result.stats.facts} facts, ${result.stats.rules} forward rules, ${result.stats.backwardRules} backward rules, ${result.stats.fuses} fuses, ${result.stats.queries} queries`,
2157
- );
2158
- } else if (command === 'render') {
2159
- process.stdout.write(render(file));
2160
- } else if (command === 'inspect') {
2161
- const result = compile(file, opts);
2162
- console.log(
2163
- `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`,
2164
- );
2165
- } else {
2166
- throw new Error(`Unknown command: ${command}\n\n${usage()}`);
2167
- }
2168
- }
2169
-
2170
- if (require.main === module) {
2171
- try {
2172
- main();
2173
- } catch (err) {
2174
- console.error(err.stack || err.message);
2175
- process.exit(1);
2176
- }
2177
- }
2178
-
2179
- module.exports = { compile, generate, parseN3, render, tokenize };