eyeling 1.33.0 → 1.33.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +9 -1
  2. package/bin/eyeling.cjs +1 -1
  3. package/dist/browser/eyeling.browser.js +2 -12
  4. package/examples/eyelang/INTEGRATION.md +7 -0
  5. package/examples/eyelang/README.md +4 -0
  6. package/examples/eyelang/output/vulnerability-impact.pl +20 -0
  7. package/examples/eyelang/vulnerability-impact.pl +70 -0
  8. package/examples/output/vulnerability-impact.n3 +22 -0
  9. package/examples/vulnerability-impact.n3 +80 -0
  10. package/eyeling-builtins.ttl +1 -1
  11. package/eyeling.js +2 -12
  12. package/index.js +1 -1
  13. package/lib/builtins.js +1 -11
  14. package/lib/cli.js +1 -1
  15. package/lib/eyelang/bin.js +1 -1
  16. package/lib/eyelang/builtins/{aggregation.mjs → aggregation.js} +1 -1
  17. package/lib/eyelang/builtins/{alphametic.mjs → alphametic.js} +1 -1
  18. package/lib/eyelang/builtins/{arithmetic.mjs → arithmetic.js} +1 -1
  19. package/lib/eyelang/builtins/{core.mjs → core.js} +1 -1
  20. package/lib/eyelang/builtins/{formula.mjs → formula.js} +1 -1
  21. package/lib/eyelang/builtins/{lists.mjs → lists.js} +1 -1
  22. package/lib/eyelang/builtins/{matrix.mjs → matrix.js} +1 -1
  23. package/lib/eyelang/builtins/{n3.mjs → n3.js} +4 -4
  24. package/lib/eyelang/builtins/{number-theory.mjs → number-theory.js} +1 -1
  25. package/lib/eyelang/builtins/{portfolio.mjs → portfolio.js} +1 -1
  26. package/lib/eyelang/builtins/{registry.mjs → registry.js} +14 -14
  27. package/lib/eyelang/builtins/{search.mjs → search.js} +2 -2
  28. package/lib/eyelang/builtins/{strings.mjs → strings.js} +1 -1
  29. package/lib/eyelang/builtins/{sudoku.mjs → sudoku.js} +1 -1
  30. package/lib/eyelang/{cli.mjs → cli.js} +5 -5
  31. package/lib/eyelang/{explain.mjs → explain.js} +4 -4
  32. package/lib/eyelang/{index.mjs → index.js} +12 -12
  33. package/lib/eyelang/package.json +3 -0
  34. package/lib/eyelang/{parser.mjs → parser.js} +1 -1
  35. package/lib/eyelang/{program.mjs → program.js} +3 -3
  36. package/lib/eyelang/{rdf.mjs → rdf.js} +1 -1
  37. package/lib/eyelang/{solver.mjs → solver.js} +4 -4
  38. package/package.json +3 -3
  39. package/test/builtins.test.js +15 -2
  40. package/test/eyelang/run-conformance.mjs +1 -1
  41. package/test/eyelang/run-examples.mjs +1 -1
  42. package/test/eyelang/run-regression.mjs +4 -4
  43. package/test/eyelang/test-style.mjs +10 -7
  44. package/test/eyelang.test.js +1 -1
  45. package/test/run.js +186 -37
  46. /package/lib/eyelang/builtins/{control.mjs → control.js} +0 -0
  47. /package/lib/eyelang/{hash.mjs → hash.js} +0 -0
  48. /package/lib/eyelang/{term.mjs → term.js} +0 -0
package/README.md CHANGED
@@ -631,6 +631,13 @@ Use the CLI option when running `.pl` programs:
631
631
  eyeling --engine eyelang examples/eyelang/ancestor.pl
632
632
  ```
633
633
 
634
+ A paired example shows the same vulnerability-impact scenario in both engines:
635
+
636
+ ```bash
637
+ eyeling examples/vulnerability-impact.n3
638
+ eyeling --engine eyelang examples/eyelang/vulnerability-impact.pl
639
+ ```
640
+
634
641
  Use the CommonJS convenience API when the rest of your application already imports `eyeling`:
635
642
 
636
643
  ```js
@@ -791,7 +798,7 @@ Eyeling provides datatype built-ins in the namespace `https://eyereasoner.github
791
798
 
792
799
  Supported operations include:
793
800
 
794
- - `dt:datatype`, `dt:lexicalForm`, and `dt:language` for literal inspection;
801
+ - `dt:datatype`, `dt:lexicalForm`, and `dt:language` for literal inspection. `dt:datatype` returns the literal's actual datatype IRI only, so RDF string literals return `xsd:string`; it does not return `rdfs:Literal`, which is a class of literals rather than a datatype IRI;
795
802
  - `dt:validForDatatype` and `dt:invalidForDatatype` for lexical validity and datatype membership checks, either as `?literal dt:validForDatatype ?datatype` tests or as tuple-to-boolean checks like `(?literal ?datatype) dt:validForDatatype true`;
796
803
  - `dt:sameValueAs` and `dt:differentValueFrom` for value-space equality and inequality;
797
804
  - `dt:canonicalLiteral` for canonical literal production.
@@ -1171,6 +1178,7 @@ The repository contains more than two hundred N3 examples under `examples/`, plu
1171
1178
  | `examples/age.n3` | Literal propagation. |
1172
1179
  | `examples/family-cousins.n3` | Multi-hop relational inference. |
1173
1180
  | `examples/dijkstra.n3` | Graph/path reasoning. |
1181
+ | `examples/vulnerability-impact.n3` and `examples/eyelang/vulnerability-impact.pl` | Paired N3 and eyelang dependency-risk example. |
1174
1182
  | `examples/list-map.n3` | List processing. |
1175
1183
  | `examples/string-builtins-tests.n3` | String built-ins. |
1176
1184
  | `examples/math-builtins-tests.n3` | Numeric built-ins. |
package/bin/eyeling.cjs CHANGED
@@ -37,7 +37,7 @@ function normalizeEngineName(value) {
37
37
  }
38
38
 
39
39
  async function runEyelangCli(argv) {
40
- const cliUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'eyelang', 'cli.mjs')).href;
40
+ const cliUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'eyelang', 'cli.js')).href;
41
41
  const cli = await import(cliUrl);
42
42
  await cli.main(argv);
43
43
  }
@@ -1888,17 +1888,7 @@ function evalDatatypeInspectionBuiltin(g, subst, kind) {
1888
1888
  const dt = literalDatatypeIri(g.s);
1889
1889
  if (dt === null) return [];
1890
1890
  valueTerm = internIri(dt);
1891
- const out = evalBindBuiltinObject(g.o, valueTerm, subst);
1892
-
1893
- // OWL 2 RL datatype rules often need common string literals to also
1894
- // participate in generic literal comparisons, while application rules still
1895
- // ask for their precise datatype explicitly. When the datatype output is
1896
- // unbound, expose rdfs:Literal as an additional super-datatype answer for
1897
- // xsd:string and rdf:langString; bound-object calls remain strictly exact.
1898
- if (g.o instanceof Var && (dt === XSD_STRING_DT || dt === RDF_LANGSTRING_DT)) {
1899
- out.push(...evalBindBuiltinObject(g.o, internIri(RDFS_LITERAL_DT), subst));
1900
- }
1901
- return out;
1891
+ return evalBindBuiltinObject(g.o, valueTerm, subst);
1902
1892
  } else if (kind === 'lexicalForm') {
1903
1893
  valueTerm = makeStringLiteral(literalLexicalValue(g.s.value));
1904
1894
  } else if (kind === 'language') {
@@ -6493,7 +6483,7 @@ async function main() {
6493
6483
  `Options:\n` +
6494
6484
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
6495
6485
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
6496
- ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
6486
+ ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
6497
6487
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
6498
6488
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
6499
6489
  ` -h, --help Show this help and exit.\n` +
@@ -6,6 +6,13 @@ This directory contains eyelang examples that can be exercised through Eyeling's
6
6
  eyeling --engine eyelang examples/eyelang/ancestor.pl
7
7
  ```
8
8
 
9
+ For a paired example, compare the N3 and eyelang versions of the same dependency-risk scenario:
10
+
11
+ ```bash
12
+ eyeling examples/vulnerability-impact.n3
13
+ eyeling --engine eyelang examples/eyelang/vulnerability-impact.pl
14
+ ```
15
+
9
16
  The eyelang conformance corpus is kept under `test/eyelang/` and can be run with:
10
17
 
11
18
  ```bash
@@ -1,5 +1,8 @@
1
1
  # eyelang
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/eyelang.svg)](https://www.npmjs.com/package/eyelang)
4
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.1242549108-blue.svg)](https://doi.org/10.5281/zenodo.20342331)
5
+
3
6
  eyelang is a small rule engine for Prolog-style Horn clauses over ordinary terms, lists, arithmetic, strings, and finite search. The command-line executable is `eyelang`.
4
7
 
5
8
  Programs write relations directly, for example `ancestor(pat, emma)` or `status(case1, accepted)`. eyelang output is ordinary eyelang syntax: by default, the CLI materializes selected answer facts and prints those facts only. Pass `--proof` (or `-p`) when you also want each answer followed by a `why/2` explanation fact that records the proof. Programs may add `materialize(Name, Arity).` declarations to focus output on selected predicates.
@@ -512,6 +515,7 @@ The repository includes examples for recursion, graph reachability, finite searc
512
515
  | [`traveling-salesman.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/traveling-salesman.pl) | Finds an optimal traveling-salesman tour. | [`output/traveling-salesman.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/traveling-salesman.pl) |
513
516
  | [`turing.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/turing.pl) | Simulates a binary-increment Turing machine. | [`output/turing.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/turing.pl) |
514
517
  | [`vector-similarity.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/vector-similarity.pl) | Computes dot product, norm, and cosine similarity. | [`output/vector-similarity.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/vector-similarity.pl) |
518
+ | [`vulnerability-impact.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/vulnerability-impact.pl) | Analyzes vulnerable transitive dependencies and urgent patch impact. | [`output/vulnerability-impact.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/vulnerability-impact.pl) |
515
519
  | [`witch.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/witch.pl) | Derives the classic “burn the witch” N3 rule chain. | [`output/witch.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/witch.pl) |
516
520
  | [`wolf-goat-cabbage.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/wolf-goat-cabbage.pl) | Solves the wolf-goat-cabbage river crossing. | [`output/wolf-goat-cabbage.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/wolf-goat-cabbage.pl) |
517
521
  | [`zebra.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/zebra.pl) | Solves the zebra logic puzzle. | [`output/zebra.pl`](https://github.com/eyereasoner/eyelang/blob/main/examples/output/zebra.pl) |
@@ -0,0 +1,20 @@
1
+ uses(webapp, api).
2
+ uses(worker, queue).
3
+ uses(frontend, ui).
4
+ uses(webapp, auth).
5
+ uses(worker, openssl).
6
+ uses(frontend, css).
7
+ uses(webapp, openssl).
8
+ impacted_by(webapp, api).
9
+ impacted_by(worker, queue).
10
+ impacted_by(frontend, ui).
11
+ impacted_by(webapp, auth).
12
+ impacted_by(worker, openssl).
13
+ impacted_by(frontend, css).
14
+ impacted_by(webapp, openssl).
15
+ risk(worker, high).
16
+ risk(webapp, high).
17
+ mitigate(worker, cve_2026_0001).
18
+ mitigate(webapp, cve_2026_0001).
19
+ urgent_patch(webapp, openssl_3_0_14).
20
+ urgent_patch(worker, openssl_3_0_14).
@@ -0,0 +1,70 @@
1
+ % Vulnerability impact analysis over a transitive dependency graph.
2
+ %
3
+ % This mirrors examples/vulnerability-impact.n3 using eyelang rules.
4
+
5
+ % Output declarations: materialize/2 selects the relations written to this example's golden output.
6
+ materialize(uses, 2).
7
+ materialize(impacted_by, 2).
8
+ materialize(risk, 2).
9
+ materialize(mitigate, 2).
10
+ materialize(urgent_patch, 2).
11
+
12
+ % Program structure: facts set up the scenario, and rules derive the materialized conclusions.
13
+ service(webapp).
14
+ service(worker).
15
+ service(frontend).
16
+
17
+ critical(webapp).
18
+ critical(worker).
19
+
20
+ depends_on(webapp, api).
21
+ depends_on(worker, queue).
22
+ depends_on(frontend, ui).
23
+ depends_on(api, auth).
24
+ depends_on(auth, openssl).
25
+ depends_on(queue, openssl).
26
+ depends_on(ui, css).
27
+
28
+ vulnerability(openssl, cve_2026_0001).
29
+ cvss(cve_2026_0001, 9.8).
30
+ fixed_by(cve_2026_0001, openssl_3_0_14).
31
+
32
+ vulnerability(css, cve_2026_0002).
33
+ cvss(cve_2026_0002, 5.4).
34
+ fixed_by(cve_2026_0002, css_2_1_1).
35
+
36
+ % Derivation rules: each rule below contributes one logical step toward the displayed results.
37
+ uses(Service, Dependency) :-
38
+ service(Service),
39
+ depends_on(Service, Dependency).
40
+
41
+ uses(Service, Dependency) :-
42
+ service(Service),
43
+ depends_on(Service, Intermediate),
44
+ depends_on(Intermediate, Dependency).
45
+
46
+ uses(Service, Dependency) :-
47
+ service(Service),
48
+ depends_on(Service, Intermediate),
49
+ depends_on(Intermediate, Next),
50
+ depends_on(Next, Dependency).
51
+
52
+ impacted_by(Service, Dependency) :-
53
+ uses(Service, Dependency).
54
+
55
+ exposed_to(Service, Vulnerability) :-
56
+ uses(Service, Package),
57
+ vulnerability(Package, Vulnerability),
58
+ cvss(Vulnerability, Score),
59
+ gt(Score, 7.0).
60
+
61
+ risk(Service, high) :-
62
+ exposed_to(Service, _Vulnerability).
63
+
64
+ mitigate(Service, Vulnerability) :-
65
+ exposed_to(Service, Vulnerability).
66
+
67
+ urgent_patch(Service, Patch) :-
68
+ critical(Service),
69
+ exposed_to(Service, Vulnerability),
70
+ fixed_by(Vulnerability, Patch).
@@ -0,0 +1,22 @@
1
+ @prefix : <https://example.org/vulnerability-impact#> .
2
+
3
+ :webapp :uses :api .
4
+ :worker :uses :queue .
5
+ :frontend :uses :ui .
6
+ :webapp :uses :auth .
7
+ :worker :uses :openssl .
8
+ :frontend :uses :css .
9
+ :webapp :uses :openssl .
10
+ :worker :risk :high .
11
+ :worker :mitigate :CVE_2026_0001 .
12
+ :webapp :risk :high .
13
+ :webapp :mitigate :CVE_2026_0001 .
14
+ :webapp :urgentPatch :openssl_3_0_14 .
15
+ :worker :urgentPatch :openssl_3_0_14 .
16
+ :webapp :impactedBy :api .
17
+ :worker :impactedBy :queue .
18
+ :frontend :impactedBy :ui .
19
+ :webapp :impactedBy :auth .
20
+ :worker :impactedBy :openssl .
21
+ :frontend :impactedBy :css .
22
+ :webapp :impactedBy :openssl .
@@ -0,0 +1,80 @@
1
+ # ===========================================================
2
+ # Vulnerability impact analysis over a transitive dependency graph
3
+ # ===========================================================
4
+
5
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
6
+ @prefix : <https://example.org/vulnerability-impact#>.
7
+
8
+ # Services.
9
+ :webapp a :Service; :critical true; :dependsOn :api.
10
+ :worker a :Service; :critical true; :dependsOn :queue.
11
+ :frontend a :Service; :dependsOn :ui.
12
+
13
+ # Component dependencies.
14
+ :api :dependsOn :auth.
15
+ :auth :dependsOn :openssl.
16
+ :queue :dependsOn :openssl.
17
+ :ui :dependsOn :css.
18
+
19
+ # Vulnerability data.
20
+ :openssl :vulnerability :CVE_2026_0001.
21
+ :CVE_2026_0001 :cvss 9.8; :fixedBy :openssl_3_0_14.
22
+
23
+ :css :vulnerability :CVE_2026_0002.
24
+ :CVE_2026_0002 :cvss 5.4; :fixedBy :css_2_1_1.
25
+
26
+ # A service uses its direct dependencies and the components they pull in.
27
+ {
28
+ ?S a :Service.
29
+ ?S :dependsOn ?D.
30
+ } => {
31
+ ?S :uses ?D.
32
+ }.
33
+
34
+ {
35
+ ?S a :Service.
36
+ ?S :dependsOn ?M.
37
+ ?M :dependsOn ?D.
38
+ } => {
39
+ ?S :uses ?D.
40
+ }.
41
+
42
+ {
43
+ ?S a :Service.
44
+ ?S :dependsOn ?M.
45
+ ?M :dependsOn ?N.
46
+ ?N :dependsOn ?D.
47
+ } => {
48
+ ?S :uses ?D.
49
+ }.
50
+
51
+ # Materialize the dependency closure for impact review.
52
+ {
53
+ ?S :uses ?D.
54
+ } => {
55
+ ?S :impactedBy ?D.
56
+ }.
57
+
58
+ # A service is high risk when any reachable component has a severe vulnerability.
59
+ {
60
+ ?S :uses ?Pkg.
61
+ ?Pkg :vulnerability ?V.
62
+ ?V :cvss ?Score.
63
+ ?Score math:greaterThan 7.0.
64
+ } => {
65
+ ?S :risk :high.
66
+ ?S :mitigate ?V.
67
+ }.
68
+
69
+ # Critical services with a severe reachable vulnerability get the available patch first.
70
+ {
71
+ ?S a :Service.
72
+ ?S :critical true.
73
+ ?S :uses ?Pkg.
74
+ ?Pkg :vulnerability ?V.
75
+ ?V :cvss ?Score.
76
+ ?Score math:greaterThan 7.0.
77
+ ?V :fixedBy ?Patch.
78
+ } => {
79
+ ?S :urgentPatch ?Patch.
80
+ }.
@@ -45,7 +45,7 @@ ex:aliasOf a rdf:Property .
45
45
  # --- dt: datatype ---------------------------------------------------
46
46
 
47
47
  dt:datatype a ex:Builtin ; ex:kind ex:Function ;
48
- rdfs:comment "Extracts the datatype IRI of a literal. Plain RDF string literals return xsd:string; language-tagged strings return rdf:langString; shorthand numeric/boolean literals return their inferred XSD datatype." .
48
+ rdfs:comment "Extracts the datatype IRI of a literal. Plain RDF string literals return xsd:string; language-tagged strings return rdf:langString; shorthand numeric/boolean literals return their inferred XSD datatype. It returns the literal's actual datatype IRI only; rdfs:Literal is a class and is not returned by dt:datatype." .
49
49
 
50
50
  dt:lexicalForm a ex:Builtin ; ex:kind ex:Function ;
51
51
  rdfs:comment "Extracts the literal lexical form as an xsd:string/plain string value, preserving the original lexical spelling such as leading zeroes." .
package/eyeling.js CHANGED
@@ -1888,17 +1888,7 @@ function evalDatatypeInspectionBuiltin(g, subst, kind) {
1888
1888
  const dt = literalDatatypeIri(g.s);
1889
1889
  if (dt === null) return [];
1890
1890
  valueTerm = internIri(dt);
1891
- const out = evalBindBuiltinObject(g.o, valueTerm, subst);
1892
-
1893
- // OWL 2 RL datatype rules often need common string literals to also
1894
- // participate in generic literal comparisons, while application rules still
1895
- // ask for their precise datatype explicitly. When the datatype output is
1896
- // unbound, expose rdfs:Literal as an additional super-datatype answer for
1897
- // xsd:string and rdf:langString; bound-object calls remain strictly exact.
1898
- if (g.o instanceof Var && (dt === XSD_STRING_DT || dt === RDF_LANGSTRING_DT)) {
1899
- out.push(...evalBindBuiltinObject(g.o, internIri(RDFS_LITERAL_DT), subst));
1900
- }
1901
- return out;
1891
+ return evalBindBuiltinObject(g.o, valueTerm, subst);
1902
1892
  } else if (kind === 'lexicalForm') {
1903
1893
  valueTerm = makeStringLiteral(literalLexicalValue(g.s.value));
1904
1894
  } else if (kind === 'language') {
@@ -6493,7 +6483,7 @@ async function main() {
6493
6483
  `Options:\n` +
6494
6484
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
6495
6485
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
6496
- ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
6486
+ ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
6497
6487
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
6498
6488
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
6499
6489
  ` -h, --help Show this help and exit.\n` +
package/index.js CHANGED
@@ -85,7 +85,7 @@ function reasonEyelangSync(opt = {}, input = '') {
85
85
  }
86
86
 
87
87
  async function loadEyelangModule() {
88
- return import(pathToFileURL(path.join(__dirname, 'lib', 'eyelang', 'index.mjs')).href);
88
+ return import(pathToFileURL(path.join(__dirname, 'lib', 'eyelang', 'index.js')).href);
89
89
  }
90
90
 
91
91
  async function runEyelang(input = '', opt = {}) {
package/lib/builtins.js CHANGED
@@ -1877,17 +1877,7 @@ function evalDatatypeInspectionBuiltin(g, subst, kind) {
1877
1877
  const dt = literalDatatypeIri(g.s);
1878
1878
  if (dt === null) return [];
1879
1879
  valueTerm = internIri(dt);
1880
- const out = evalBindBuiltinObject(g.o, valueTerm, subst);
1881
-
1882
- // OWL 2 RL datatype rules often need common string literals to also
1883
- // participate in generic literal comparisons, while application rules still
1884
- // ask for their precise datatype explicitly. When the datatype output is
1885
- // unbound, expose rdfs:Literal as an additional super-datatype answer for
1886
- // xsd:string and rdf:langString; bound-object calls remain strictly exact.
1887
- if (g.o instanceof Var && (dt === XSD_STRING_DT || dt === RDF_LANGSTRING_DT)) {
1888
- out.push(...evalBindBuiltinObject(g.o, internIri(RDFS_LITERAL_DT), subst));
1889
- }
1890
- return out;
1880
+ return evalBindBuiltinObject(g.o, valueTerm, subst);
1891
1881
  } else if (kind === 'lexicalForm') {
1892
1882
  valueTerm = makeStringLiteral(literalLexicalValue(g.s.value));
1893
1883
  } else if (kind === 'language') {
package/lib/cli.js CHANGED
@@ -805,7 +805,7 @@ async function main() {
805
805
  `Options:\n` +
806
806
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
807
807
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
808
- ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
808
+ ` --engine <n3|eyelang> Select the N3 engine (default) or eyelang engine.\n` +
809
809
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
810
810
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
811
811
  ` -h, --help Show this help and exit.\n` +
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- import('./cli.mjs').then(({ main }) => main(process.argv.slice(2))).catch((error) => {
4
+ import('./cli.js').then(({ main }) => main(process.argv.slice(2))).catch((error) => {
5
5
  console.error(`eyelang: ${error && error.message ? error.message : String(error)}`);
6
6
  process.exit(1);
7
7
  });
@@ -1,6 +1,6 @@
1
1
  // Aggregation builtins that run a inner goal and collect, count, sum, or select the best answers.
2
2
  // Each handler clones the solver so the inner goal can enumerate independently of the outer goal.
3
- import { compareTerms, copyResolved, isDecimalInteger, lexicalValue, listFromItems, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.mjs';
3
+ import { compareTerms, copyResolved, isDecimalInteger, lexicalValue, listFromItems, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.js';
4
4
 
5
5
  export const aggregationBuiltins = {
6
6
  register(registry) {
@@ -2,7 +2,7 @@
2
2
  // It evaluates column constraints right-to-left, so examples such as
3
3
  // SEND+MORE=MONEY and DONALD+GERALD=ROBERT do not need to express digit search
4
4
  // with many relational select/3 and arithmetic goals.
5
- import { atom, deref, listFromItems, numberTerm, properListItems, unify } from '../term.mjs';
5
+ import { atom, deref, listFromItems, numberTerm, properListItems, unify } from '../term.js';
6
6
 
7
7
  export const alphameticBuiltins = {
8
8
  register(registry) {
@@ -1,6 +1,6 @@
1
1
  // Numeric builtins for integer-preserving arithmetic, floating point functions, comparisons, and ranges.
2
2
  // The code keeps BigInt paths where possible so large eyelang integers remain exact.
3
- import { compareIntegerText, deref, isDecimalInteger, lexicalValue, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.mjs';
3
+ import { compareIntegerText, deref, isDecimalInteger, lexicalValue, numberTerm, numberTextFromDouble, parseFiniteNumber, unify } from '../term.js';
4
4
 
5
5
  const unaryNames = ['neg', 'abs', 'sin', 'cos', 'asin', 'acos', 'rounded', 'log'];
6
6
  const binaryNames = ['add', 'sub', 'mul', 'div', 'mod', 'max', 'min', 'pow'];
@@ -1,6 +1,6 @@
1
1
  // Core relational builtins that do not naturally belong to arithmetic, strings, lists, or aggregation.
2
2
  // They are deterministic filters/projections and should avoid enumerating additional answers.
3
- import { stringTerm, lexicalValue, unify } from '../term.mjs';
3
+ import { stringTerm, lexicalValue, unify } from '../term.js';
4
4
 
5
5
  function* ok(env) { yield env; }
6
6
 
@@ -1,6 +1,6 @@
1
1
  // Formula builtins that treat conjunctions as first-class data terms.
2
2
  // These are used by examples that construct or inspect rule bodies programmatically.
3
- import { atom, deref, isConjunction, unify } from '../term.mjs';
3
+ import { atom, deref, isConjunction, unify } from '../term.js';
4
4
 
5
5
  export const formulaBuiltins = {
6
6
  register(registry) {
@@ -1,6 +1,6 @@
1
1
  // List builtins for proper lists, selection, membership, sorting, and indexing.
2
2
  // Several predicates support both checking and generation, so the argument modes are handled explicitly.
3
- import { compareTerms, copyResolved, deref, isCons, lexicalValue, listFromItems, numberTerm, properListItems, unify } from '../term.mjs';
3
+ import { compareTerms, copyResolved, deref, isCons, lexicalValue, listFromItems, numberTerm, properListItems, unify } from '../term.js';
4
4
 
5
5
  export const listBuiltins = {
6
6
  register(registry) {
@@ -1,7 +1,7 @@
1
1
  // Small dense-matrix arithmetic builtins.
2
2
  // These are reusable numeric kernels for ground matrices; when arguments are not
3
3
  // ground proper numeric lists, user-defined matrix predicates remain available.
4
- import { deref, isDecimalInteger, listFromItems, numberTerm, numberTextFromDouble, parseFiniteNumber, properListItems, unify } from '../term.mjs';
4
+ import { deref, isDecimalInteger, listFromItems, numberTerm, numberTextFromDouble, parseFiniteNumber, properListItems, unify } from '../term.js';
5
5
 
6
6
  export const matrixBuiltins = {
7
7
  register(registry) {
@@ -2,10 +2,10 @@
2
2
  // RDF parser lowers body triples with math:, string:, list:, crypto:, time:, and
3
3
  // selected log: predicates to n3_* eyelang builtins. The handlers work on the
4
4
  // explicit RDF term representation from src/rdf.js.
5
- import { hashHex } from '../hash.mjs';
6
- import { compareLexicalOrNumeric } from './arithmetic.mjs';
7
- import { deref, isDecimalInteger, listFromItems, parseFiniteNumber, properListItems, termToString, unify } from '../term.mjs';
8
- import { RDF_DIR_LANG_STRING, RDF_LANG_STRING, XSD_BOOLEAN, XSD_DECIMAL, XSD_DOUBLE, XSD_INTEGER, XSD_STRING, rdfIri, rdfLiteral } from '../rdf.mjs';
5
+ import { hashHex } from '../hash.js';
6
+ import { compareLexicalOrNumeric } from './arithmetic.js';
7
+ import { deref, isDecimalInteger, listFromItems, parseFiniteNumber, properListItems, termToString, unify } from '../term.js';
8
+ import { RDF_DIR_LANG_STRING, RDF_LANG_STRING, XSD_BOOLEAN, XSD_DECIMAL, XSD_DOUBLE, XSD_INTEGER, XSD_STRING, rdfIri, rdfLiteral } from '../rdf.js';
9
9
 
10
10
  const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
11
11
  const XSD_DATE_TIME = `${XSD_NS}dateTime`;
@@ -2,7 +2,7 @@
2
2
  // arithmetic algorithms with many small recursive rule calls.
3
3
  // The predicates are intentionally general: extended GCD, Collatz trajectories,
4
4
  // Kaprekar iteration counts, and Goldbach pair generation.
5
- import { deref, listFromItems, numberTerm, unify } from '../term.mjs';
5
+ import { deref, listFromItems, numberTerm, unify } from '../term.js';
6
6
 
7
7
  export const numberTheoryBuiltins = {
8
8
  register(registry) {
@@ -1,7 +1,7 @@
1
1
  // Reusable bounded-subset optimizer for finite knapsack-style examples.
2
2
  // Items are p(Name, Value, Cost, Risk) terms; the builtin enumerates every
3
3
  // subset whose total cost/risk stays within the supplied caps.
4
- import { deref, listFromItems, numberTerm, properListItems, unify } from '../term.mjs';
4
+ import { deref, listFromItems, numberTerm, properListItems, unify } from '../term.js';
5
5
 
6
6
  export const portfolioBuiltins = {
7
7
  register(registry) {
@@ -1,19 +1,19 @@
1
1
  // Registry for builtins and their execution metadata.
2
2
  // The solver uses the metadata to know when a builtin is deterministic, mode-ready, or should fall back to user clauses.
3
- import { arithmeticBuiltins } from './arithmetic.mjs';
4
- import { coreBuiltins } from './core.mjs';
5
- import { stringBuiltins } from './strings.mjs';
6
- import { listBuiltins } from './lists.mjs';
7
- import { aggregationBuiltins } from './aggregation.mjs';
8
- import { formulaBuiltins } from './formula.mjs';
9
- import { controlBuiltins } from './control.mjs';
10
- import { sudokuBuiltins } from './sudoku.mjs';
11
- import { portfolioBuiltins } from './portfolio.mjs';
12
- import { searchBuiltins } from './search.mjs';
13
- import { numberTheoryBuiltins } from './number-theory.mjs';
14
- import { matrixBuiltins } from './matrix.mjs';
15
- import { alphameticBuiltins } from './alphametic.mjs';
16
- import { n3Builtins } from './n3.mjs';
3
+ import { arithmeticBuiltins } from './arithmetic.js';
4
+ import { coreBuiltins } from './core.js';
5
+ import { stringBuiltins } from './strings.js';
6
+ import { listBuiltins } from './lists.js';
7
+ import { aggregationBuiltins } from './aggregation.js';
8
+ import { formulaBuiltins } from './formula.js';
9
+ import { controlBuiltins } from './control.js';
10
+ import { sudokuBuiltins } from './sudoku.js';
11
+ import { portfolioBuiltins } from './portfolio.js';
12
+ import { searchBuiltins } from './search.js';
13
+ import { numberTheoryBuiltins } from './number-theory.js';
14
+ import { matrixBuiltins } from './matrix.js';
15
+ import { alphameticBuiltins } from './alphametic.js';
16
+ import { n3Builtins } from './n3.js';
17
17
 
18
18
  export class BuiltinRegistry {
19
19
  constructor() {
@@ -2,8 +2,8 @@
2
2
  // of their time in small relational generators. These predicates are generic
3
3
  // entry points (graph, CNF, QMC, range, and n-queens helpers), not compiled
4
4
  // replacements for particular example predicate names.
5
- import { atom, compound, deref, listFromItems, numberTerm, properListItems, unify } from '../term.mjs';
6
- import { compareLexicalOrNumeric } from './arithmetic.mjs';
5
+ import { atom, compound, deref, listFromItems, numberTerm, properListItems, unify } from '../term.js';
6
+ import { compareLexicalOrNumeric } from './arithmetic.js';
7
7
 
8
8
  export const searchBuiltins = {
9
9
  register(registry) {
@@ -1,6 +1,6 @@
1
1
  // String and atom conversion builtins.
2
2
  // They mostly project from already-ground terms to avoid guessing string domains.
3
- import { atom, lexicalValue, stringTerm, unify } from '../term.mjs';
3
+ import { atom, lexicalValue, stringTerm, unify } from '../term.js';
4
4
 
5
5
  export const stringBuiltins = {
6
6
  register(registry) {
@@ -1,6 +1,6 @@
1
1
  // Sudoku-specific helpers used by the example suite.
2
2
  // The cover9/1 optimization prunes invalid row blocks early while leaving the declarative fallback available.
3
- import { deref, lexicalValue, listFromItems, numberTerm, properListItems, unify } from '../term.mjs';
3
+ import { deref, lexicalValue, listFromItems, numberTerm, properListItems, unify } from '../term.js';
4
4
 
5
5
  export const sudokuBuiltins = {
6
6
  register(registry) {
@@ -87,10 +87,10 @@ export async function main(argv) {
87
87
  async function loadEngine() {
88
88
  if (engineModule == null) {
89
89
  const [term, program, solver, registry] = await Promise.all([
90
- import('./term.mjs'),
91
- import('./program.mjs'),
92
- import('./solver.mjs'),
93
- import('./builtins/registry.mjs'),
90
+ import('./term.js'),
91
+ import('./program.js'),
92
+ import('./solver.js'),
93
+ import('./builtins/registry.js'),
94
94
  ]);
95
95
  engineModule = { ...term, ...program, ...solver, ...registry };
96
96
  }
@@ -98,7 +98,7 @@ async function loadEngine() {
98
98
  }
99
99
 
100
100
  async function loadExplanation() {
101
- if (explanationModule == null) explanationModule = await import('./explain.mjs');
101
+ if (explanationModule == null) explanationModule = await import('./explain.js');
102
102
  return explanationModule;
103
103
  }
104
104
 
@@ -2,10 +2,10 @@
2
2
  // The explanation printer replays a successful goal against the program and emits
3
3
  // ordinary eyelang facts with nested proof terms. Explanations are therefore both
4
4
  // human-readable and machine-readable.
5
- import { COMPOUND, Env, Term, VAR, deref, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.mjs';
6
- import { selectClauseCandidates } from './program.mjs';
7
- import { createDefaultRegistry } from './builtins/registry.mjs';
8
- import { Solver, nextFreshId } from './solver.mjs';
5
+ import { COMPOUND, Env, Term, VAR, deref, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.js';
6
+ import { selectClauseCandidates } from './program.js';
7
+ import { createDefaultRegistry } from './builtins/registry.js';
8
+ import { Solver, nextFreshId } from './solver.js';
9
9
 
10
10
  export function whyProof(program, goal, options = {}) {
11
11
  const maxDepth = options.maxDepth ?? 256;
@@ -1,17 +1,17 @@
1
1
  // Public JavaScript API surface for embedders and the browser playground.
2
2
  // The CLI imports the same parser, program, solver, and term primitives from here.
3
- export { Program, makeProgram } from './program.mjs';
4
- export { parseClauses, parseProgramText } from './parser.mjs';
5
- export { parseRdfClauses, rdfToEyelang, clausesToEyelang, rdfIri, rdfBlank, rdfLiteral, rdfTripleTerm, rdfGoal } from './rdf.mjs';
6
- export { Solver } from './solver.mjs';
7
- export * from './term.mjs';
8
- export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './builtins/registry.mjs';
3
+ export { Program, makeProgram } from './program.js';
4
+ export { parseClauses, parseProgramText } from './parser.js';
5
+ export { parseRdfClauses, rdfToEyelang, clausesToEyelang, rdfIri, rdfBlank, rdfLiteral, rdfTripleTerm, rdfGoal } from './rdf.js';
6
+ export { Solver } from './solver.js';
7
+ export * from './term.js';
8
+ export { BuiltinRegistry, createDefaultRegistry, getDefaultRegistry } from './builtins/registry.js';
9
9
 
10
- import { Env, copyResolved, termIsGround, termToString } from './term.mjs';
11
- import { Program } from './program.mjs';
12
- import { Solver } from './solver.mjs';
13
- import { whyNoProof, whyProof } from './explain.mjs';
14
- import { getDefaultRegistry } from './builtins/registry.mjs';
10
+ import { Env, copyResolved, termIsGround, termToString } from './term.js';
11
+ import { Program } from './program.js';
12
+ import { Solver } from './solver.js';
13
+ import { whyNoProof, whyProof } from './explain.js';
14
+ import { getDefaultRegistry } from './builtins/registry.js';
15
15
 
16
16
  export function run(source, options = {}) {
17
17
  const includeWhy = options.proof === true || options.why === true || options.explain === true;
@@ -45,4 +45,4 @@ function appendExplanation(output, program, resolved, registry) {
45
45
  if (!proof.ok) output.push(whyNoProof(resolved));
46
46
  }
47
47
 
48
- export * from './explain.mjs';
48
+ export * from './explain.js';
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -1,6 +1,6 @@
1
1
  // Tokenizer and recursive-descent parser for the eyelang source language.
2
2
  // It preserves the compact Prolog-like syntax while producing Term objects for the solver.
3
- import { atom, compound, cons, emptyList, numberTerm, stringTerm, variable } from './term.mjs';
3
+ import { atom, compound, cons, emptyList, numberTerm, stringTerm, variable } from './term.js';
4
4
 
5
5
  const TOK = {
6
6
  EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
@@ -1,8 +1,8 @@
1
1
  // Program representation and clause indexing.
2
2
  // Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
3
- import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar, termToString } from './term.mjs';
4
- import { parseClauses } from './parser.mjs';
5
- import { filenameLooksRdf, parseRdfClauses } from './rdf.mjs';
3
+ import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar, termToString } from './term.js';
4
+ import { parseClauses } from './parser.js';
5
+ import { filenameLooksRdf, parseRdfClauses } from './rdf.js';
6
6
 
7
7
  export class Program {
8
8
  constructor(clauses = [], options = {}) {
@@ -17,7 +17,7 @@
17
17
  // N3 rules of the form `{ ... } => { ... } .` become Horn clauses whose heads
18
18
  // and bodies are rdf/3 goals. RDF 1.2 reified triples and annotation syntax are
19
19
  // expanded to rdf:reifies triples.
20
- import { atom, compound, listFromItems, numberTerm, stringTerm, termToString, variable } from './term.mjs';
20
+ import { atom, compound, listFromItems, numberTerm, stringTerm, termToString, variable } from './term.js';
21
21
 
22
22
  const TOKEN = {
23
23
  EOF: 'eof', IRI: 'iri', STRING: 'string', NUMBER: 'number', NAME: 'name', BNODE: 'bnode', VAR: 'var', LANG: 'lang', ATWORD: 'atword',
@@ -1,8 +1,8 @@
1
1
  // Depth-first eyelang solver with builtin dispatch, memoization, and guarded recursion handling.
2
2
  // Most semantic decisions still flow through unification; optimizations only select candidates earlier.
3
- import { COMPOUND, Env, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.mjs';
4
- import { createDefaultRegistry } from './builtins/registry.mjs';
5
- import { selectClauseCandidates } from './program.mjs';
3
+ import { COMPOUND, Env, flattenConjunction, freshTerm, termToString, unify, variantTerms } from './term.js';
4
+ import { createDefaultRegistry } from './builtins/registry.js';
5
+ import { selectClauseCandidates } from './program.js';
6
6
 
7
7
  let freshCounter = 0;
8
8
 
@@ -234,4 +234,4 @@ function importResolved(term, env) {
234
234
  }
235
235
 
236
236
  // Avoid circular import surprises in older Node loaders.
237
- import * as termModuleCache from './term.mjs';
237
+ import * as termModuleCache from './term.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.33.0",
3
+ "version": "1.33.2",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
@@ -87,8 +87,8 @@
87
87
  "./package.json": "./package.json",
88
88
  "./eyelang": {
89
89
  "types": "./eyelang.d.ts",
90
- "import": "./lib/eyelang/index.mjs",
91
- "default": "./lib/eyelang/index.mjs"
90
+ "import": "./lib/eyelang/index.js",
91
+ "default": "./lib/eyelang/index.js"
92
92
  }
93
93
  },
94
94
  "typesVersions": {
@@ -199,7 +199,6 @@ const cases = [
199
199
  { "11"^^xsd:integer dt:differentValueFrom "12"^^xsd:integer . } => { :different :numeric true } .
200
200
  { "hello@en"^^rdf:PlainLiteral dt:differentValueFrom "bye@en"^^rdf:PlainLiteral . } => { :different :plainLiteral true } .
201
201
  { "a" dt:differentValueFrom "b" . } => { :different :string true } .
202
- { "a" dt:datatype ?sd . ?sd <http://www.w3.org/2000/10/swap/log#notEqualTo> xsd:string . } => { :string :comparisonDatatype ?sd } .
203
202
  { "<a/>"^^rdf:XMLLiteral dt:differentValueFrom "<b/>"^^rdf:XMLLiteral . } => { :different :xmlLiteral true } .
204
203
 
205
204
  { ("1"^^xsd:integer xsd:integer) dt:validForDatatype true . } => { :tuple :valid true } .
@@ -252,7 +251,6 @@ const cases = [
252
251
  assert.match(out, /:different :numeric true \./);
253
252
  assert.match(out, /:different :plainLiteral true \./);
254
253
  assert.match(out, /:different :string true \./);
255
- assert.match(out, /:string :comparisonDatatype rdfs:Literal \./);
256
254
  assert.match(out, /:different :xmlLiteral true \./);
257
255
  assert.match(out, /:tuple :valid true \./);
258
256
  assert.match(out, /:tuple :invalidBoolean true \./);
@@ -265,6 +263,21 @@ const cases = [
265
263
  assert.match(out, /:canonical :dateTime "2026-06-10T12:00:00Z"\^\^xsd:dateTime \./);
266
264
  assert.match(out, /:canonical :midnightRollover "2027-01-01T00:00:00Z"\^\^xsd:dateTime \./);
267
265
  assert.match(out, /:sameAs :reflexive true \./);
266
+
267
+ const datatypeOut = runReason(`
268
+ @prefix : <http://example.org/datatype-tests#> .
269
+ @prefix dt: <https://eyereasoner.github.io/eyeling/datatype#> .
270
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
271
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
272
+ :s :plainName "Alice" .
273
+ :s :typedName "Alice"^^xsd:string .
274
+ { :s :plainName ?value . ?value dt:datatype ?datatype . } => { :plain :detectedDatatype ?datatype } .
275
+ { :s :typedName ?value . ?value dt:datatype ?datatype . } => { :typed :detectedDatatype ?datatype } .
276
+ `);
277
+ assert.match(datatypeOut, /:plain :detectedDatatype xsd:string \./);
278
+ assert.match(datatypeOut, /:typed :detectedDatatype xsd:string \./);
279
+ assert.doesNotMatch(datatypeOut, /:plain :detectedDatatype rdfs:Literal \./);
280
+ assert.doesNotMatch(datatypeOut, /:typed :detectedDatatype rdfs:Literal \./);
268
281
  },
269
282
  },
270
283
  {
@@ -4,7 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
- import { Program, run } from '../../lib/eyelang/index.mjs';
7
+ import { Program, run } from '../../lib/eyelang/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { TestReporter, isMainModule } from './test-style.mjs';
10
10
 
@@ -4,7 +4,7 @@
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { spawnSync } from 'node:child_process';
7
- import { Program, run } from '../../lib/eyelang/index.mjs';
7
+ import { Program, run } from '../../lib/eyelang/index.js';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { TestReporter, isMainModule } from './test-style.mjs';
10
10
 
@@ -31,11 +31,11 @@ import {
31
31
  variantTerms,
32
32
  parseProgramText,
33
33
  rdfToEyelang,
34
- } from '../../lib/eyelang/index.mjs';
35
- import { parseGoalText } from '../../lib/eyelang/parser.mjs';
36
- import { selectClauseCandidates } from '../../lib/eyelang/program.mjs';
34
+ } from '../../lib/eyelang/index.js';
35
+ import { parseGoalText } from '../../lib/eyelang/parser.js';
36
+ import { selectClauseCandidates } from '../../lib/eyelang/program.js';
37
37
  import { TestReporter, isMainModule } from './test-style.mjs';
38
- import { hashHex } from '../../lib/eyelang/hash.mjs';
38
+ import { hashHex } from '../../lib/eyelang/hash.js';
39
39
 
40
40
  const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
41
41
  const packageRoot = path.resolve(testRoot, '..', '..');
@@ -1,17 +1,20 @@
1
1
  // Shared test output helpers.
2
2
  // The runners use this small reporter so individual suites and `npm test` share
3
- // one compact, Eyesharl-like style with a continuous grey test number.
3
+ // one compact Eyeling-style layout: colored OK/FAIL, sequence number, test description, and dimmed timing.
4
4
  import process from 'node:process';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import path from 'node:path';
7
7
 
8
- const useColor = Boolean(process.stdout.isTTY) && process.env.NO_COLOR == null;
8
+ const useColor = process.env.NO_COLOR == null && (
9
+ Boolean(process.stdout.isTTY) ||
10
+ Boolean(process.env.FORCE_COLOR && process.env.FORCE_COLOR !== '0')
11
+ );
9
12
 
10
13
  export const colors = {
11
14
  green: useColor ? '\x1b[32m' : '',
12
15
  red: useColor ? '\x1b[31m' : '',
13
16
  yellow: useColor ? '\x1b[33m' : '',
14
- grey: useColor ? '\x1b[90m' : '',
17
+ dim: useColor ? '\x1b[2m' : '',
15
18
  reset: useColor ? '\x1b[0m' : '',
16
19
  };
17
20
 
@@ -45,7 +48,7 @@ export class TestReporter {
45
48
  const total = this.total - this.currentSection.totalAtStart;
46
49
  const ms = nowMs() - this.currentSection.startedAt;
47
50
  const suite = label ?? defaultSectionLabel(this.currentSection.name);
48
- this.stdout.write(`${colors.green}OK${colors.reset} ${ok}/${total} ${suite} tests passed ${colors.grey}(${ms} ms)${colors.reset}\n`);
51
+ this.stdout.write(`${colors.green}OK${colors.reset} ${ok}/${total} ${suite} tests passed ${colors.dim}(${ms} ms)${colors.reset}\n`);
49
52
  }
50
53
 
51
54
  test(name, run) {
@@ -57,10 +60,10 @@ export class TestReporter {
57
60
  run();
58
61
  const ms = nowMs() - startedAt;
59
62
  this.ok++;
60
- this.stdout.write(`${colors.grey}${nr}${colors.reset} ${colors.green}OK${colors.reset} ${name} ${colors.grey}(${ms} ms)${colors.reset}\n`);
63
+ this.stdout.write(`${colors.green}OK${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
61
64
  } catch (error) {
62
65
  const ms = nowMs() - startedAt;
63
- this.stderr.write(`${colors.grey}${nr}${colors.reset} ${colors.red}FAIL${colors.reset} ${name} ${colors.grey}(${ms} ms)${colors.reset}\n`);
66
+ this.stderr.write(`${colors.red}FAIL${colors.reset} ${nr} ${name} ${colors.dim}(${ms} ms)${colors.reset}\n`);
64
67
  this.stderr.write(`${error?.stack ?? String(error)}\n`);
65
68
  throw error;
66
69
  }
@@ -69,7 +72,7 @@ export class TestReporter {
69
72
  totalLine() {
70
73
  const ms = nowMs() - this.startedAt;
71
74
  this.stdout.write(`\n${colors.yellow}== Total${colors.reset}\n`);
72
- this.stdout.write(`${colors.green}OK${colors.reset} ${this.ok}/${this.total} tests passed ${colors.grey}(${ms} ms)${colors.reset}\n`);
75
+ this.stdout.write(`${colors.green}OK${colors.reset} ${this.ok}/${this.total} tests passed ${colors.dim}(${ms} ms)${colors.reset}\n`);
73
76
  }
74
77
  }
75
78
 
@@ -47,7 +47,7 @@ out(X) :- in(X).
47
47
  assert.match(directCli.stdout, /ancestor\(jan, emma\)\./);
48
48
 
49
49
  // Sanity check the ESM file URL too; this catches nested package type regressions.
50
- const esm = await import(pathToFileURL(`${process.cwd()}/lib/eyelang/index.mjs`).href);
50
+ const esm = await import(pathToFileURL(`${process.cwd()}/lib/eyelang/index.js`).href);
51
51
  assert.equal(typeof esm.Program, 'function');
52
52
 
53
53
  pass(1, 'eyelang second-engine integration passed', Date.now() - startedAt);
package/test/run.js CHANGED
@@ -5,57 +5,206 @@ const cp = require('node:child_process');
5
5
 
6
6
  const { C, formatDuration } = require('./report');
7
7
 
8
+ const node = process.execPath;
9
+
10
+ // Run test files directly instead of through `npm run …`. This keeps the
11
+ // output in the compact Eyeling reporter style and avoids npm's script banners
12
+ // between the colored OK/FAIL test lines.
8
13
  const sections = [
9
- ['Build bundle', 'npm', ['run', 'build']],
10
- ['Packlist checks', 'npm', ['run', 'test:packlist']],
11
- ['API tests', 'npm', ['run', 'test:api']],
12
- ['Streaming RDF Messages tests', 'npm', ['run', 'test:stream-messages']],
13
- ['Builtin contract tests', 'npm', ['run', 'test:builtins']],
14
- ['Store tests', 'npm', ['run', 'test:store']],
15
- ['Examples tests', 'npm', ['run', 'test:examples']],
16
- ['Proof examples tests', 'npm', ['run', 'test:examples:proof']],
17
- ['Manifest tests', 'npm', ['run', 'test:manifest']],
18
- ['RDF 1.2 syntax tests', 'npm', ['run', 'test:rdf12']],
19
- ['Playground tests', 'npm', ['run', 'test:playground']],
20
- ['Eyelang second-engine tests', 'npm', ['run', 'test:eyelang']],
21
- ['Package tests', 'npm', ['run', 'test:package']],
14
+ ['Build bundle', node, ['tools/bundle.js']],
15
+ ['Packlist checks', node, ['test/packlist.test.js']],
16
+ ['API tests', node, ['test/api.test.js']],
17
+ ['Streaming RDF Messages tests', node, ['test/stream_messages.test.js']],
18
+ ['Builtin contract tests', node, ['test/builtins.test.js']],
19
+ ['Store tests', node, ['test/store.test.js']],
20
+ ['Examples tests', node, ['test/examples.test.js']],
21
+ ['Proof examples tests', node, ['test/examples.test.js', '--proof-only']],
22
+ ['Manifest tests', node, ['test/manifest.test.js']],
23
+ ['RDF 1.2 syntax tests', node, ['test/rdf12.test.js']],
24
+ ['Playground tests', node, ['test/playground.test.js']],
25
+ ['Eyelang second-engine tests', node, ['test/eyelang.test.js']],
26
+ ['Eyelang corpus tests', node, ['test/eyelang/run-all.mjs']],
27
+ ['Package tests', node, ['test/package.test.js']],
22
28
  ];
23
29
 
30
+ const aggregate = {
31
+ total: 0,
32
+ ok: 0,
33
+ incomplete: 0,
34
+ nonconform: 0,
35
+ crashed: 0,
36
+ };
37
+
24
38
  function sectionLine(kind, label, ms) {
25
39
  const suffix = typeof ms === 'number' ? ` (${formatDuration(ms)})` : '';
26
40
  console.log(`${C.y}==${C.n} ${kind} ${label}${suffix}`);
27
41
  }
28
42
 
43
+ function stripAnsi(s) {
44
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '');
45
+ }
46
+
47
+ function makeSectionSummary() {
48
+ return {
49
+ // Normal Eyeling tests use report.js: "OK 001 ..." / "FAIL 001 ...".
50
+ reportOk: 0,
51
+ reportFail: 0,
52
+ // Older corpus runners used: "1 OK ..." / "1 FAIL ...". Keep
53
+ // parsing that shape so aggregate totals remain robust across local runs.
54
+ numberedOk: 0,
55
+ numberedFail: 0,
56
+ // notation3tests prints a suite-level line with the real manifest count.
57
+ manifestTotal: null,
58
+ };
59
+ }
60
+
61
+ function parseLineForSummary(rawLine, summary) {
62
+ const line = stripAnsi(rawLine).trimEnd();
63
+ if (!line) return;
64
+
65
+ if (/^OK\s+\d{3}\s/.test(line)) {
66
+ summary.reportOk++;
67
+ return;
68
+ }
69
+ if (/^FAIL\s+\d{3}\s/.test(line)) {
70
+ summary.reportFail++;
71
+ return;
72
+ }
73
+ if (/^\d+\s+OK\s+/.test(line)) {
74
+ summary.numberedOk++;
75
+ return;
76
+ }
77
+ if (/^\d+\s+FAIL\s+/.test(line)) {
78
+ summary.numberedFail++;
79
+ return;
80
+ }
81
+
82
+ const manifest = line.match(
83
+ /^TOTAL\s+\[COUNT:(\d+)\]\s+OK:\s*[^0-9]*(\d+)[^0-9]+INCOMPLETE:\s*[^0-9]*(\d+)[^0-9]+NONCONFORM:\s*[^0-9]*(\d+)[^0-9]+CRASHED:\s*[^0-9]*(\d+)/,
84
+ );
85
+ if (manifest) {
86
+ summary.manifestTotal = {
87
+ count: Number(manifest[1]),
88
+ ok: Number(manifest[2]),
89
+ incomplete: Number(manifest[3]),
90
+ nonconform: Number(manifest[4]),
91
+ crashed: Number(manifest[5]),
92
+ };
93
+ }
94
+ }
95
+
96
+ function makeStreamingParser(summary) {
97
+ let buffer = '';
98
+ return {
99
+ feed(chunk) {
100
+ buffer += String(chunk);
101
+ const lines = buffer.split(/\r?\n/);
102
+ buffer = lines.pop() || '';
103
+ for (const line of lines) parseLineForSummary(line, summary);
104
+ },
105
+ finish() {
106
+ if (buffer) parseLineForSummary(buffer, summary);
107
+ buffer = '';
108
+ },
109
+ };
110
+ }
111
+
112
+ function addSectionToAggregate(summary) {
113
+ if (summary.manifestTotal) {
114
+ aggregate.total += summary.manifestTotal.count;
115
+ aggregate.ok += summary.manifestTotal.ok;
116
+ aggregate.incomplete += summary.manifestTotal.incomplete;
117
+ aggregate.nonconform += summary.manifestTotal.nonconform;
118
+ aggregate.crashed += summary.manifestTotal.crashed;
119
+ return;
120
+ }
121
+
122
+ const ok = summary.reportOk + summary.numberedOk;
123
+ const failed = summary.reportFail + summary.numberedFail;
124
+ aggregate.total += ok + failed;
125
+ aggregate.ok += ok;
126
+ aggregate.nonconform += failed;
127
+ }
128
+
129
+ function scoreLine() {
130
+ const score = aggregate.total === 0 ? 0 : (100 * aggregate.ok) / aggregate.total;
131
+ return `TOTAL [COUNT:${aggregate.total}] OK: ⭐${aggregate.ok}⭐ INCOMPLETE: ⭐${aggregate.incomplete}⭐ NONCONFORM: ⭐${aggregate.nonconform}⭐ CRASHED: ⭐${aggregate.crashed}⭐ => SCORE: ⭐⭐⭐${score.toFixed(1)}⭐⭐⭐`;
132
+ }
133
+
134
+ function childEnv() {
135
+ const env = { ...process.env };
136
+ if (!env.NO_COLOR && C.g && !env.FORCE_COLOR) env.FORCE_COLOR = '1';
137
+ return env;
138
+ }
139
+
29
140
  function runSection(label, cmd, args) {
30
141
  console.log('');
31
142
  sectionLine('Start', label);
32
143
  const startedAt = Date.now();
33
- const r = cp.spawnSync(cmd, args, {
34
- cwd: process.cwd(),
35
- env: process.env,
36
- shell: process.platform === 'win32',
37
- stdio: 'inherit',
144
+ const summary = makeSectionSummary();
145
+ const stdoutParser = makeStreamingParser(summary);
146
+ const stderrParser = makeStreamingParser(summary);
147
+
148
+ return new Promise((resolve) => {
149
+ const child = cp.spawn(cmd, args, {
150
+ cwd: process.cwd(),
151
+ env: childEnv(),
152
+ shell: process.platform === 'win32',
153
+ stdio: ['inherit', 'pipe', 'pipe'],
154
+ });
155
+
156
+ child.stdout.on('data', (chunk) => {
157
+ stdoutParser.feed(chunk);
158
+ process.stdout.write(chunk);
159
+ });
160
+ child.stderr.on('data', (chunk) => {
161
+ stderrParser.feed(chunk);
162
+ process.stderr.write(chunk);
163
+ });
164
+
165
+ child.on('error', (err) => {
166
+ stdoutParser.finish();
167
+ stderrParser.finish();
168
+ addSectionToAggregate(summary);
169
+ aggregate.total++;
170
+ aggregate.crashed++;
171
+ const elapsed = Date.now() - startedAt;
172
+ console.error(`${C.r}FAIL${C.n} ${label}: ${err.message || String(err)}`);
173
+ sectionLine('End', `${label} failed`, elapsed);
174
+ console.log('');
175
+ resolve(1);
176
+ });
177
+
178
+ child.on('close', (code, signal) => {
179
+ stdoutParser.finish();
180
+ stderrParser.finish();
181
+ addSectionToAggregate(summary);
182
+ const elapsed = Date.now() - startedAt;
183
+ const status = typeof code === 'number' ? code : 1;
184
+ if (status !== 0 && summary.reportFail + summary.numberedFail === 0 && !summary.manifestTotal) {
185
+ aggregate.total++;
186
+ aggregate.crashed++;
187
+ }
188
+ const suffix = signal ? ` (${signal})` : '';
189
+ sectionLine('End', status === 0 ? `${label} passed` : `${label} failed${suffix}`, elapsed);
190
+ console.log('');
191
+ resolve(status);
192
+ });
38
193
  });
39
- const elapsed = Date.now() - startedAt;
40
- if (r.error) {
41
- console.error(`${C.r}FAIL${C.n} ${label}: ${r.error.message || String(r.error)}`);
42
- sectionLine('End', `${label} failed`, elapsed);
43
- console.log('');
44
- return 1;
45
- }
46
- const status = typeof r.status === 'number' ? r.status : 1;
47
- sectionLine('End', status === 0 ? `${label} passed` : `${label} failed`, elapsed);
48
- console.log('');
49
- return status;
50
194
  }
51
195
 
52
- let status = 0;
53
- for (const [label, cmd, args] of sections) {
54
- const sectionStatus = runSection(label, cmd, args);
55
- if (sectionStatus !== 0) {
56
- status = sectionStatus;
57
- break;
196
+ (async function main() {
197
+ let status = 0;
198
+ for (const [label, cmd, args] of sections) {
199
+ const sectionStatus = await runSection(label, cmd, args);
200
+ if (sectionStatus !== 0) {
201
+ status = sectionStatus;
202
+ break;
203
+ }
58
204
  }
59
- }
60
205
 
61
- process.exit(status);
206
+ console.log('');
207
+ console.log(scoreLine());
208
+
209
+ process.exit(status);
210
+ })();
File without changes
File without changes