eyeling 1.33.1 → 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 (45) hide show
  1. package/README.md +8 -0
  2. package/bin/eyeling.cjs +1 -1
  3. package/dist/browser/eyeling.browser.js +1 -1
  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.js +1 -1
  11. package/index.js +1 -1
  12. package/lib/cli.js +1 -1
  13. package/lib/eyelang/bin.js +1 -1
  14. package/lib/eyelang/builtins/{aggregation.mjs → aggregation.js} +1 -1
  15. package/lib/eyelang/builtins/{alphametic.mjs → alphametic.js} +1 -1
  16. package/lib/eyelang/builtins/{arithmetic.mjs → arithmetic.js} +1 -1
  17. package/lib/eyelang/builtins/{core.mjs → core.js} +1 -1
  18. package/lib/eyelang/builtins/{formula.mjs → formula.js} +1 -1
  19. package/lib/eyelang/builtins/{lists.mjs → lists.js} +1 -1
  20. package/lib/eyelang/builtins/{matrix.mjs → matrix.js} +1 -1
  21. package/lib/eyelang/builtins/{n3.mjs → n3.js} +4 -4
  22. package/lib/eyelang/builtins/{number-theory.mjs → number-theory.js} +1 -1
  23. package/lib/eyelang/builtins/{portfolio.mjs → portfolio.js} +1 -1
  24. package/lib/eyelang/builtins/{registry.mjs → registry.js} +14 -14
  25. package/lib/eyelang/builtins/{search.mjs → search.js} +2 -2
  26. package/lib/eyelang/builtins/{strings.mjs → strings.js} +1 -1
  27. package/lib/eyelang/builtins/{sudoku.mjs → sudoku.js} +1 -1
  28. package/lib/eyelang/{cli.mjs → cli.js} +5 -5
  29. package/lib/eyelang/{explain.mjs → explain.js} +4 -4
  30. package/lib/eyelang/{index.mjs → index.js} +12 -12
  31. package/lib/eyelang/package.json +3 -0
  32. package/lib/eyelang/{parser.mjs → parser.js} +1 -1
  33. package/lib/eyelang/{program.mjs → program.js} +3 -3
  34. package/lib/eyelang/{rdf.mjs → rdf.js} +1 -1
  35. package/lib/eyelang/{solver.mjs → solver.js} +4 -4
  36. package/package.json +3 -3
  37. package/test/eyelang/run-conformance.mjs +1 -1
  38. package/test/eyelang/run-examples.mjs +1 -1
  39. package/test/eyelang/run-regression.mjs +4 -4
  40. package/test/eyelang/test-style.mjs +10 -7
  41. package/test/eyelang.test.js +1 -1
  42. package/test/run.js +186 -37
  43. /package/lib/eyelang/builtins/{control.mjs → control.js} +0 -0
  44. /package/lib/eyelang/{hash.mjs → hash.js} +0 -0
  45. /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
@@ -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
  }
@@ -6483,7 +6483,7 @@ async function main() {
6483
6483
  `Options:\n` +
6484
6484
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
6485
6485
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
6486
- ` --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` +
6487
6487
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
6488
6488
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
6489
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
+ }.
package/eyeling.js CHANGED
@@ -6483,7 +6483,7 @@ async function main() {
6483
6483
  `Options:\n` +
6484
6484
  ` -a, --ast Print parsed AST as JSON and exit.\n` +
6485
6485
  ` --builtin <module.js> Load a custom builtin module (repeatable).\n` +
6486
- ` --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` +
6487
6487
  ` -d, --deterministic-skolem Make log:skolem stable across reasoning runs.\n` +
6488
6488
  ` -e, --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
6489
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/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.1",
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": {
@@ -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