eyeling 1.24.5 → 1.24.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/HANDBOOK.md +0 -99
  2. package/README.md +2 -2
  3. package/dist/browser/eyeling.browser.js +6 -0
  4. package/{see/examples/n3 → examples}/annotation.n3 +1 -1
  5. package/{see/examples/n3/backward_recursion.n3 → examples/backward-recursion.n3} +0 -2
  6. package/examples/collection.n3 +3 -0
  7. package/examples/context-association.n3 +59 -0
  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/examples/input/context-association.trig +35 -0
  16. package/{see/examples/input/dijkstra_risk_path.trig → examples/input/dijkstra-risk-path.trig} +4 -4
  17. package/{see/examples/input/eco_route_insight.trig → examples/input/eco-route-insight.trig} +4 -4
  18. package/{see/examples/input/genetic_knapsack_selection.trig → examples/input/genetic-knapsack-selection.trig} +4 -4
  19. package/{see/examples/input/rc_discharge_envelope.trig → examples/input/rc-discharge-envelope.trig} +4 -4
  20. package/{see/examples/input/rdf_dataset.trig → examples/input/rdf-dataset.trig} +4 -4
  21. package/{see/examples/input/rdf_message_flow.trig → examples/input/rdf-message-flow.trig} +3 -3
  22. package/{see/examples/input/rdf_messages.trig → examples/input/rdf-messages.trig} +4 -4
  23. package/{see/examples/input/school_placement_audit.trig → examples/input/school-placement-audit.trig} +4 -4
  24. package/{see/examples/input/smoke_arithmetic.trig → examples/input/smoke-arithmetic.trig} +3 -3
  25. package/{see/examples/input/triple_terms.trig → examples/input/triple-terms.trig} +3 -3
  26. package/examples/output/annotation.n3 +0 -0
  27. package/examples/output/backward-recursion.n3 +4 -0
  28. package/examples/output/builtin-coverage.n3 +0 -0
  29. package/examples/output/collection.n3 +0 -0
  30. package/examples/output/context-association.n3 +9 -0
  31. package/examples/output/dijkstra-risk-path.n3 +3 -0
  32. package/examples/output/eco-route-insight.n3 +3 -0
  33. package/examples/output/genetic-knapsack-selection.n3 +3 -0
  34. package/examples/output/rc-discharge-envelope.n3 +9 -0
  35. package/examples/output/rc-discharge-envelope.txt +9 -0
  36. package/examples/output/rdf-dataset.n3 +5 -0
  37. package/examples/output/rdf-message-flow.n3 +7 -0
  38. package/examples/output/rdf-messages.n3 +7 -0
  39. package/examples/output/school-placement-audit.n3 +3 -0
  40. package/examples/output/smoke-arithmetic.n3 +5 -0
  41. package/examples/output/smoke-arithmetic.txt +5 -0
  42. package/examples/output/triple-terms.n3 +5 -0
  43. package/{see/examples/n3/rc_discharge_envelope.n3 → examples/rc-discharge-envelope.n3} +2 -15
  44. package/{see/examples/n3/rdf_dataset.n3 → examples/rdf-dataset.n3} +2 -11
  45. package/{see/examples/n3/rdf_message_flow.n3 → examples/rdf-message-flow.n3} +9 -75
  46. package/{see/examples/n3/rdf_messages.n3 → examples/rdf-messages.n3} +6 -43
  47. package/{see/examples/n3/school_placement_audit.n3 → examples/school-placement-audit.n3} +2 -14
  48. package/{see/examples/n3/smoke_arithmetic.n3 → examples/smoke-arithmetic.n3} +3 -5
  49. package/{see/examples/n3/triple_terms.n3 → examples/triple-terms.n3} +1 -4
  50. package/eyeling.js +6 -0
  51. package/lib/builtins.js +6 -0
  52. package/package.json +4 -7
  53. package/test/api.test.js +25 -8
  54. package/test/examples.test.js +22 -2
  55. package/test/package.test.js +16 -2
  56. package/see/README.md +0 -149
  57. package/see/examples/_see.js +0 -249
  58. package/see/examples/age.js +0 -1459
  59. package/see/examples/annotation.js +0 -1320
  60. package/see/examples/backward.js +0 -1405
  61. package/see/examples/backward_recursion.js +0 -1504
  62. package/see/examples/bayes_diagnosis.js +0 -2883
  63. package/see/examples/bayes_therapy.js +0 -4152
  64. package/see/examples/bmi.js +0 -3038
  65. package/see/examples/builtin_coverage.js +0 -2524
  66. package/see/examples/collection.js +0 -1320
  67. package/see/examples/complex.js +0 -3762
  68. package/see/examples/complex_matrix_stability.js +0 -2973
  69. package/see/examples/composition_of_injective_functions_is_injective.js +0 -2170
  70. package/see/examples/control_system.js +0 -1918
  71. package/see/examples/crypto_builtins_tests.js +0 -1489
  72. package/see/examples/delfour.js +0 -3174
  73. package/see/examples/digital_product_passport.js +0 -2856
  74. package/see/examples/dijkstra.js +0 -2070
  75. package/see/examples/dijkstra_risk_path.js +0 -1874
  76. package/see/examples/doc/age.md +0 -27
  77. package/see/examples/doc/annotation.md +0 -24
  78. package/see/examples/doc/backward.md +0 -26
  79. package/see/examples/doc/backward_recursion.md +0 -26
  80. package/see/examples/doc/bayes_diagnosis.md +0 -41
  81. package/see/examples/doc/bayes_therapy.md +0 -40
  82. package/see/examples/doc/bmi.md +0 -38
  83. package/see/examples/doc/builtin_coverage.md +0 -53
  84. package/see/examples/doc/collection.md +0 -24
  85. package/see/examples/doc/complex.md +0 -38
  86. package/see/examples/doc/complex_matrix_stability.md +0 -35
  87. package/see/examples/doc/composition_of_injective_functions_is_injective.md +0 -24
  88. package/see/examples/doc/control_system.md +0 -32
  89. package/see/examples/doc/crypto_builtins_tests.md +0 -27
  90. package/see/examples/doc/delfour.md +0 -37
  91. package/see/examples/doc/digital_product_passport.md +0 -36
  92. package/see/examples/doc/dijkstra.md +0 -28
  93. package/see/examples/doc/dijkstra_risk_path.md +0 -30
  94. package/see/examples/doc/dog.md +0 -28
  95. package/see/examples/doc/eco_route_insight.md +0 -33
  96. package/see/examples/doc/equals.md +0 -26
  97. package/see/examples/doc/equivalence_classes_overlap_implies_same_class.md +0 -24
  98. package/see/examples/doc/euler_identity.md +0 -39
  99. package/see/examples/doc/ev_roundtrip_planner.md +0 -32
  100. package/see/examples/doc/existential_rule.md +0 -24
  101. package/see/examples/doc/expression_eval.md +0 -26
  102. package/see/examples/doc/family_cousins.md +0 -24
  103. package/see/examples/doc/fastpow.md +0 -29
  104. package/see/examples/doc/fibonacci.md +0 -28
  105. package/see/examples/doc/french_cities.md +0 -28
  106. package/see/examples/doc/fundamental_theorem_arithmetic.md +0 -36
  107. package/see/examples/doc/genetic_knapsack_selection.md +0 -29
  108. package/see/examples/doc/goldbach_1000.md +0 -31
  109. package/see/examples/doc/good_cobbler.md +0 -27
  110. package/see/examples/doc/gps.md +0 -35
  111. package/see/examples/doc/gray_code_counter.md +0 -31
  112. package/see/examples/doc/greatest_lower_bound_uniqueness.md +0 -24
  113. package/see/examples/doc/group_inverse_uniqueness.md +0 -24
  114. package/see/examples/doc/hadamard_approx.md +0 -32
  115. package/see/examples/doc/hanoi.md +0 -26
  116. package/see/examples/doc/odrl_dpv_risk_ranked.md +0 -57
  117. package/see/examples/doc/path_discovery.md +0 -33
  118. package/see/examples/doc/rc_discharge_envelope.md +0 -33
  119. package/see/examples/doc/rdf_dataset.md +0 -26
  120. package/see/examples/doc/rdf_message_flow.md +0 -35
  121. package/see/examples/doc/rdf_messages.md +0 -37
  122. package/see/examples/doc/school_placement_audit.md +0 -31
  123. package/see/examples/doc/smoke_arithmetic.md +0 -31
  124. package/see/examples/doc/socrates.md +0 -24
  125. package/see/examples/doc/triple_terms.md +0 -26
  126. package/see/examples/doc/wind_turbine.md +0 -37
  127. package/see/examples/doc/witch.md +0 -28
  128. package/see/examples/dog.js +0 -1436
  129. package/see/examples/eco_route_insight.js +0 -2110
  130. package/see/examples/equals.js +0 -1363
  131. package/see/examples/equivalence_classes_overlap_implies_same_class.js +0 -1792
  132. package/see/examples/euler_identity.js +0 -2038
  133. package/see/examples/ev_roundtrip_planner.js +0 -2562
  134. package/see/examples/existential_rule.js +0 -1363
  135. package/see/examples/expression_eval.js +0 -1798
  136. package/see/examples/family_cousins.js +0 -1586
  137. package/see/examples/fastpow.js +0 -2207
  138. package/see/examples/fibonacci.js +0 -1594
  139. package/see/examples/french_cities.js +0 -1492
  140. package/see/examples/fundamental_theorem_arithmetic.js +0 -2106
  141. package/see/examples/genetic_knapsack_selection.js +0 -1743
  142. package/see/examples/goldbach_1000.js +0 -1798
  143. package/see/examples/good_cobbler.js +0 -1396
  144. package/see/examples/gps.js +0 -2813
  145. package/see/examples/gray_code_counter.js +0 -1641
  146. package/see/examples/greatest_lower_bound_uniqueness.js +0 -1918
  147. package/see/examples/group_inverse_uniqueness.js +0 -1897
  148. package/see/examples/hadamard_approx.js +0 -4417
  149. package/see/examples/hanoi.js +0 -1625
  150. package/see/examples/input/age.trig +0 -27
  151. package/see/examples/input/backward.trig +0 -25
  152. package/see/examples/input/bayes_diagnosis.trig +0 -111
  153. package/see/examples/input/bayes_therapy.trig +0 -130
  154. package/see/examples/input/bmi.trig +0 -28
  155. package/see/examples/input/complex.trig +0 -26
  156. package/see/examples/input/complex_matrix_stability.trig +0 -65
  157. package/see/examples/input/composition_of_injective_functions_is_injective.trig +0 -35
  158. package/see/examples/input/control_system.trig +0 -31
  159. package/see/examples/input/crypto_builtins_tests.trig +0 -25
  160. package/see/examples/input/delfour.trig +0 -90
  161. package/see/examples/input/digital_product_passport.trig +0 -116
  162. package/see/examples/input/dijkstra.trig +0 -34
  163. package/see/examples/input/dog.trig +0 -31
  164. package/see/examples/input/equals.trig +0 -25
  165. package/see/examples/input/equivalence_classes_overlap_implies_same_class.trig +0 -28
  166. package/see/examples/input/euler_identity.trig +0 -34
  167. package/see/examples/input/ev_roundtrip_planner.trig +0 -90
  168. package/see/examples/input/existential_rule.trig +0 -26
  169. package/see/examples/input/expression_eval.trig +0 -41
  170. package/see/examples/input/family_cousins.trig +0 -39
  171. package/see/examples/input/fastpow.trig +0 -25
  172. package/see/examples/input/fibonacci.trig +0 -51
  173. package/see/examples/input/french_cities.trig +0 -38
  174. package/see/examples/input/fundamental_theorem_arithmetic.trig +0 -42
  175. package/see/examples/input/goldbach_1000.trig +0 -53
  176. package/see/examples/input/good_cobbler.trig +0 -24
  177. package/see/examples/input/gps.trig +0 -35
  178. package/see/examples/input/gray_code_counter.trig +0 -33
  179. package/see/examples/input/greatest_lower_bound_uniqueness.trig +0 -29
  180. package/see/examples/input/group_inverse_uniqueness.trig +0 -29
  181. package/see/examples/input/hadamard_approx.trig +0 -32
  182. package/see/examples/input/hanoi.trig +0 -26
  183. package/see/examples/input/odrl_dpv_risk_ranked.trig +0 -107
  184. package/see/examples/input/path-discovery.trig +0 -96448
  185. package/see/examples/input/path_discovery.trig +0 -29
  186. package/see/examples/input/socrates.trig +0 -26
  187. package/see/examples/input/wind_turbine.trig +0 -48
  188. package/see/examples/input/witch.trig +0 -26
  189. package/see/examples/n3/age.n3 +0 -28
  190. package/see/examples/n3/backward.n3 +0 -22
  191. package/see/examples/n3/bayes_diagnosis.n3 +0 -122
  192. package/see/examples/n3/bayes_therapy.n3 +0 -149
  193. package/see/examples/n3/bmi.n3 +0 -145
  194. package/see/examples/n3/collection.n3 +0 -3
  195. package/see/examples/n3/complex.n3 +0 -140
  196. package/see/examples/n3/complex_matrix_stability.n3 +0 -113
  197. package/see/examples/n3/composition_of_injective_functions_is_injective.n3 +0 -27
  198. package/see/examples/n3/control_system.n3 +0 -59
  199. package/see/examples/n3/crypto_builtins_tests.n3 +0 -18
  200. package/see/examples/n3/delfour.n3 +0 -167
  201. package/see/examples/n3/digital_product_passport.n3 +0 -156
  202. package/see/examples/n3/dijkstra.n3 +0 -46
  203. package/see/examples/n3/dog.n3 +0 -20
  204. package/see/examples/n3/equals.n3 +0 -11
  205. package/see/examples/n3/equivalence_classes_overlap_implies_same_class.n3 +0 -19
  206. package/see/examples/n3/euler_identity.n3 +0 -41
  207. package/see/examples/n3/ev_roundtrip_planner.n3 +0 -82
  208. package/see/examples/n3/existential_rule.n3 +0 -10
  209. package/see/examples/n3/expression_eval.n3 +0 -21
  210. package/see/examples/n3/family_cousins.n3 +0 -62
  211. package/see/examples/n3/fastpow.n3 +0 -56
  212. package/see/examples/n3/fibonacci.n3 +0 -44
  213. package/see/examples/n3/french_cities.n3 +0 -28
  214. package/see/examples/n3/fundamental_theorem_arithmetic.n3 +0 -84
  215. package/see/examples/n3/goldbach_1000.n3 +0 -66
  216. package/see/examples/n3/good_cobbler.n3 +0 -10
  217. package/see/examples/n3/gps.n3 +0 -70
  218. package/see/examples/n3/gray_code_counter.n3 +0 -53
  219. package/see/examples/n3/greatest_lower_bound_uniqueness.n3 +0 -20
  220. package/see/examples/n3/group_inverse_uniqueness.n3 +0 -19
  221. package/see/examples/n3/hadamard_approx.n3 +0 -43
  222. package/see/examples/n3/hanoi.n3 +0 -16
  223. package/see/examples/n3/odrl_dpv_risk_ranked.n3 +0 -460
  224. package/see/examples/n3/path_discovery.n3 +0 -43
  225. package/see/examples/n3/socrates.n3 +0 -21
  226. package/see/examples/n3/wind_turbine.n3 +0 -85
  227. package/see/examples/n3/witch.n3 +0 -30
  228. package/see/examples/odrl_dpv_risk_ranked.js +0 -5128
  229. package/see/examples/output/age.md +0 -48
  230. package/see/examples/output/annotation.md +0 -43
  231. package/see/examples/output/backward.md +0 -50
  232. package/see/examples/output/backward_recursion.md +0 -54
  233. package/see/examples/output/bayes_diagnosis.md +0 -103
  234. package/see/examples/output/bayes_therapy.md +0 -84
  235. package/see/examples/output/bmi.md +0 -164
  236. package/see/examples/output/builtin_coverage.md +0 -99
  237. package/see/examples/output/collection.md +0 -44
  238. package/see/examples/output/complex.md +0 -61
  239. package/see/examples/output/complex_matrix_stability.md +0 -55
  240. package/see/examples/output/composition_of_injective_functions_is_injective.md +0 -62
  241. package/see/examples/output/control_system.md +0 -61
  242. package/see/examples/output/crypto_builtins_tests.md +0 -68
  243. package/see/examples/output/delfour.md +0 -100
  244. package/see/examples/output/digital_product_passport.md +0 -100
  245. package/see/examples/output/dijkstra.md +0 -74
  246. package/see/examples/output/dijkstra_risk_path.md +0 -76
  247. package/see/examples/output/dog.md +0 -50
  248. package/see/examples/output/eco_route_insight.md +0 -88
  249. package/see/examples/output/equals.md +0 -50
  250. package/see/examples/output/equivalence_classes_overlap_implies_same_class.md +0 -86
  251. package/see/examples/output/euler_identity.md +0 -73
  252. package/see/examples/output/ev_roundtrip_planner.md +0 -79
  253. package/see/examples/output/existential_rule.md +0 -54
  254. package/see/examples/output/expression_eval.md +0 -50
  255. package/see/examples/output/family_cousins.md +0 -187
  256. package/see/examples/output/fastpow.md +0 -36
  257. package/see/examples/output/fibonacci.md +0 -53
  258. package/see/examples/output/french_cities.md +0 -70
  259. package/see/examples/output/fundamental_theorem_arithmetic.md +0 -101
  260. package/see/examples/output/genetic_knapsack_selection.md +0 -66
  261. package/see/examples/output/goldbach_1000.md +0 -58
  262. package/see/examples/output/good_cobbler.md +0 -54
  263. package/see/examples/output/gps.md +0 -102
  264. package/see/examples/output/gray_code_counter.md +0 -68
  265. package/see/examples/output/greatest_lower_bound_uniqueness.md +0 -60
  266. package/see/examples/output/group_inverse_uniqueness.md +0 -60
  267. package/see/examples/output/hadamard_approx.md +0 -510
  268. package/see/examples/output/hanoi.md +0 -51
  269. package/see/examples/output/odrl_dpv_risk_ranked.md +0 -139
  270. package/see/examples/output/path_discovery.md +0 -65
  271. package/see/examples/output/rc_discharge_envelope.md +0 -102
  272. package/see/examples/output/rdf_dataset.md +0 -54
  273. package/see/examples/output/rdf_message_flow.md +0 -198
  274. package/see/examples/output/rdf_messages.md +0 -134
  275. package/see/examples/output/school_placement_audit.md +0 -99
  276. package/see/examples/output/smoke_arithmetic.md +0 -54
  277. package/see/examples/output/socrates.md +0 -55
  278. package/see/examples/output/triple_terms.md +0 -53
  279. package/see/examples/output/wind_turbine.md +0 -108
  280. package/see/examples/output/witch.md +0 -87
  281. package/see/examples/path_discovery.js +0 -1774
  282. package/see/examples/rc_discharge_envelope.js +0 -1993
  283. package/see/examples/rdf_dataset.js +0 -1512
  284. package/see/examples/rdf_message_flow.js +0 -2580
  285. package/see/examples/rdf_messages.js +0 -2176
  286. package/see/examples/school_placement_audit.js +0 -1867
  287. package/see/examples/smoke_arithmetic.js +0 -1483
  288. package/see/examples/socrates.js +0 -1420
  289. package/see/examples/triple_terms.js +0 -1442
  290. package/see/examples/wind_turbine.js +0 -2853
  291. package/see/examples/witch.js +0 -1519
  292. package/see/see.js +0 -2179
  293. package/test/see.test.js +0 -159
  294. /package/{see/examples/n3/builtin_coverage.n3 → examples/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 };