eyeling 1.21.9 → 1.22.0

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.
package/eyeling.js CHANGED
@@ -809,6 +809,56 @@ function __builtinCollectVarsInTriples(triples, out) {
809
809
  for (const tr of triples) __builtinCollectVarsInTriple(tr, out);
810
810
  }
811
811
 
812
+ function __existentializeBlankTerm(t, mapping, varGen) {
813
+ if (t instanceof Blank) {
814
+ let v = mapping[t.label];
815
+ if (v === undefined) {
816
+ const n = Array.isArray(varGen) && typeof varGen[0] === 'number' ? varGen[0]++ : Object.keys(mapping).length + 1;
817
+ v = new Var(`__qb_${n}`);
818
+ mapping[t.label] = v;
819
+ }
820
+ return v;
821
+ }
822
+ if (t instanceof ListTerm) return t;
823
+ if (t instanceof OpenListTerm) return t;
824
+ if (t instanceof GraphTerm) {
825
+ let changed = false;
826
+ const triples = t.triples.map((tr) => {
827
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
828
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
829
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
830
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
831
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
832
+ });
833
+ return changed ? new GraphTerm(triples) : t;
834
+ }
835
+ return t;
836
+ }
837
+
838
+ function __existentializeBlankTriples(triples, varGen) {
839
+ const mapping = Object.create(null);
840
+ let changed = false;
841
+ const out = triples.map((tr) => {
842
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
843
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
844
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
845
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
846
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
847
+ });
848
+ return changed ? out : triples;
849
+ }
850
+
851
+ function __prepareQuotedPatternTriples(rawTriples, subst, varGen) {
852
+ const ex = __existentializeBlankTriples(Array.from(rawTriples), varGen);
853
+ let changed = false;
854
+ const out = ex.map((tr) => {
855
+ const tr2 = applySubstTriple(tr, subst);
856
+ if (tr2 !== tr) changed = true;
857
+ return tr2;
858
+ });
859
+ return changed ? out : ex;
860
+ }
861
+
812
862
  function literalHasLangTag(lit) {
813
863
  // True iff the literal is a quoted string literal with a language tag suffix,
814
864
  // e.g. "hello"@en or """hello"""@en.
@@ -4001,7 +4051,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4001
4051
  const keepVars = new Set();
4002
4052
  if (g.s instanceof GraphTerm) __builtinCollectVarsInTriples(g.s.triples, keepVars);
4003
4053
 
4004
- const goalVariants = __expandScopedVarPredicateGoals(Array.from(g.o.triples));
4054
+ const goalTriples = __prepareQuotedPatternTriples(goal.o.triples, subst, varGen);
4055
+ const goalVariants = __expandScopedVarPredicateGoals(goalTriples);
4005
4056
  const out = [];
4006
4057
  for (const variant of goalVariants) {
4007
4058
  const sols = proveGoals(
@@ -4083,7 +4134,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4083
4134
 
4084
4135
  const visited2 = [];
4085
4136
  const sols = proveGoals(
4086
- Array.from(g.o.triples),
4137
+ __prepareQuotedPatternTriples(goal.o.triples, subst, varGen),
4087
4138
  { ...subst },
4088
4139
  scopeFacts,
4089
4140
  scopeBackRules,
@@ -4177,15 +4228,12 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4177
4228
  }
4178
4229
 
4179
4230
  const visited2 = [];
4180
- const sols = proveGoals(
4181
- Array.from(clauseTerm.triples),
4182
- {},
4183
- scopeFacts,
4184
- scopeBackRules,
4185
- depth + 1,
4186
- visited2,
4187
- varGen,
4188
- );
4231
+ const rawClauseTerm = goal.s instanceof ListTerm ? goal.s.elems[1] : clauseTerm;
4232
+ const clauseGoals =
4233
+ rawClauseTerm instanceof GraphTerm
4234
+ ? __prepareQuotedPatternTriples(rawClauseTerm.triples, subst, varGen)
4235
+ : __prepareQuotedPatternTriples(clauseTerm.triples, subst, varGen);
4236
+ const sols = proveGoals(clauseGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
4189
4237
 
4190
4238
  const collected = sols.map((sBody) => applySubstTerm(valueTempl, sBody));
4191
4239
  const collectedList = new ListTerm(collected);
@@ -4238,28 +4286,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
4238
4286
  scopeFacts = snap;
4239
4287
  }
4240
4288
 
4289
+ const rawWhereClause = goal.s instanceof ListTerm ? goal.s.elems[0] : whereClause;
4290
+ const rawThenClause = goal.s instanceof ListTerm ? goal.s.elems[1] : thenClause;
4291
+ const whereGoals =
4292
+ rawWhereClause instanceof GraphTerm
4293
+ ? __prepareQuotedPatternTriples(rawWhereClause.triples, subst, varGen)
4294
+ : __prepareQuotedPatternTriples(whereClause.triples, subst, varGen);
4295
+ const thenGoals =
4296
+ rawThenClause instanceof GraphTerm
4297
+ ? __prepareQuotedPatternTriples(rawThenClause.triples, subst, varGen)
4298
+ : __prepareQuotedPatternTriples(thenClause.triples, subst, varGen);
4299
+
4241
4300
  const visited1 = [];
4242
- const sols1 = proveGoals(
4243
- Array.from(whereClause.triples),
4244
- {},
4245
- scopeFacts,
4246
- scopeBackRules,
4247
- depth + 1,
4248
- visited1,
4249
- varGen,
4250
- );
4301
+ const sols1 = proveGoals(whereGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited1, varGen);
4251
4302
 
4252
4303
  for (const s1 of sols1) {
4253
4304
  const visited2 = [];
4254
- const sols2 = proveGoals(
4255
- Array.from(thenClause.triples),
4256
- s1,
4257
- scopeFacts,
4258
- scopeBackRules,
4259
- depth + 1,
4260
- visited2,
4261
- varGen,
4262
- );
4305
+ const sols2 = proveGoals(thenGoals, s1, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
4263
4306
  if (!sols2.length) return [];
4264
4307
  }
4265
4308
  return [outSubst];
@@ -12696,14 +12739,26 @@ function liftBlankRuleVars(premise, conclusion) {
12696
12739
  return new Var(name);
12697
12740
  }
12698
12741
 
12742
+ function copyQuotedTerm(t) {
12743
+ // Quoted formulas are data terms with their own local blank scope.
12744
+ // Copy them structurally so later in-place rewrites cannot mutate shared AST,
12745
+ // but do not lift their blank nodes into rule-body variables.
12746
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(copyQuotedTerm));
12747
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(copyQuotedTerm), t.tailVar);
12748
+ if (t instanceof GraphTerm) {
12749
+ const triples = t.triples.map(
12750
+ (tr) => new Triple(copyQuotedTerm(tr.s), copyQuotedTerm(tr.p), copyQuotedTerm(tr.o)),
12751
+ );
12752
+ return new GraphTerm(triples);
12753
+ }
12754
+ return t;
12755
+ }
12756
+
12699
12757
  function convertTerm(t) {
12700
12758
  if (t instanceof Blank) return blankToVar(t.label);
12701
12759
  if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertTerm));
12702
12760
  if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertTerm), t.tailVar);
12703
- if (t instanceof GraphTerm) {
12704
- const triples = t.triples.map((tr) => new Triple(convertTerm(tr.s), convertTerm(tr.p), convertTerm(tr.o)));
12705
- return new GraphTerm(triples);
12706
- }
12761
+ if (t instanceof GraphTerm) return copyQuotedTerm(t);
12707
12762
  return t;
12708
12763
  }
12709
12764
 
package/lib/builtins.js CHANGED
@@ -330,6 +330,56 @@ function __builtinCollectVarsInTriples(triples, out) {
330
330
  for (const tr of triples) __builtinCollectVarsInTriple(tr, out);
331
331
  }
332
332
 
333
+ function __existentializeBlankTerm(t, mapping, varGen) {
334
+ if (t instanceof Blank) {
335
+ let v = mapping[t.label];
336
+ if (v === undefined) {
337
+ const n = Array.isArray(varGen) && typeof varGen[0] === 'number' ? varGen[0]++ : Object.keys(mapping).length + 1;
338
+ v = new Var(`__qb_${n}`);
339
+ mapping[t.label] = v;
340
+ }
341
+ return v;
342
+ }
343
+ if (t instanceof ListTerm) return t;
344
+ if (t instanceof OpenListTerm) return t;
345
+ if (t instanceof GraphTerm) {
346
+ let changed = false;
347
+ const triples = t.triples.map((tr) => {
348
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
349
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
350
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
351
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
352
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
353
+ });
354
+ return changed ? new GraphTerm(triples) : t;
355
+ }
356
+ return t;
357
+ }
358
+
359
+ function __existentializeBlankTriples(triples, varGen) {
360
+ const mapping = Object.create(null);
361
+ let changed = false;
362
+ const out = triples.map((tr) => {
363
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
364
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
365
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
366
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
367
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
368
+ });
369
+ return changed ? out : triples;
370
+ }
371
+
372
+ function __prepareQuotedPatternTriples(rawTriples, subst, varGen) {
373
+ const ex = __existentializeBlankTriples(Array.from(rawTriples), varGen);
374
+ let changed = false;
375
+ const out = ex.map((tr) => {
376
+ const tr2 = applySubstTriple(tr, subst);
377
+ if (tr2 !== tr) changed = true;
378
+ return tr2;
379
+ });
380
+ return changed ? out : ex;
381
+ }
382
+
333
383
  function literalHasLangTag(lit) {
334
384
  // True iff the literal is a quoted string literal with a language tag suffix,
335
385
  // e.g. "hello"@en or """hello"""@en.
@@ -3522,7 +3572,8 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3522
3572
  const keepVars = new Set();
3523
3573
  if (g.s instanceof GraphTerm) __builtinCollectVarsInTriples(g.s.triples, keepVars);
3524
3574
 
3525
- const goalVariants = __expandScopedVarPredicateGoals(Array.from(g.o.triples));
3575
+ const goalTriples = __prepareQuotedPatternTriples(goal.o.triples, subst, varGen);
3576
+ const goalVariants = __expandScopedVarPredicateGoals(goalTriples);
3526
3577
  const out = [];
3527
3578
  for (const variant of goalVariants) {
3528
3579
  const sols = proveGoals(
@@ -3604,7 +3655,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3604
3655
 
3605
3656
  const visited2 = [];
3606
3657
  const sols = proveGoals(
3607
- Array.from(g.o.triples),
3658
+ __prepareQuotedPatternTriples(goal.o.triples, subst, varGen),
3608
3659
  { ...subst },
3609
3660
  scopeFacts,
3610
3661
  scopeBackRules,
@@ -3698,15 +3749,12 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3698
3749
  }
3699
3750
 
3700
3751
  const visited2 = [];
3701
- const sols = proveGoals(
3702
- Array.from(clauseTerm.triples),
3703
- {},
3704
- scopeFacts,
3705
- scopeBackRules,
3706
- depth + 1,
3707
- visited2,
3708
- varGen,
3709
- );
3752
+ const rawClauseTerm = goal.s instanceof ListTerm ? goal.s.elems[1] : clauseTerm;
3753
+ const clauseGoals =
3754
+ rawClauseTerm instanceof GraphTerm
3755
+ ? __prepareQuotedPatternTriples(rawClauseTerm.triples, subst, varGen)
3756
+ : __prepareQuotedPatternTriples(clauseTerm.triples, subst, varGen);
3757
+ const sols = proveGoals(clauseGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
3710
3758
 
3711
3759
  const collected = sols.map((sBody) => applySubstTerm(valueTempl, sBody));
3712
3760
  const collectedList = new ListTerm(collected);
@@ -3759,28 +3807,23 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
3759
3807
  scopeFacts = snap;
3760
3808
  }
3761
3809
 
3810
+ const rawWhereClause = goal.s instanceof ListTerm ? goal.s.elems[0] : whereClause;
3811
+ const rawThenClause = goal.s instanceof ListTerm ? goal.s.elems[1] : thenClause;
3812
+ const whereGoals =
3813
+ rawWhereClause instanceof GraphTerm
3814
+ ? __prepareQuotedPatternTriples(rawWhereClause.triples, subst, varGen)
3815
+ : __prepareQuotedPatternTriples(whereClause.triples, subst, varGen);
3816
+ const thenGoals =
3817
+ rawThenClause instanceof GraphTerm
3818
+ ? __prepareQuotedPatternTriples(rawThenClause.triples, subst, varGen)
3819
+ : __prepareQuotedPatternTriples(thenClause.triples, subst, varGen);
3820
+
3762
3821
  const visited1 = [];
3763
- const sols1 = proveGoals(
3764
- Array.from(whereClause.triples),
3765
- {},
3766
- scopeFacts,
3767
- scopeBackRules,
3768
- depth + 1,
3769
- visited1,
3770
- varGen,
3771
- );
3822
+ const sols1 = proveGoals(whereGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited1, varGen);
3772
3823
 
3773
3824
  for (const s1 of sols1) {
3774
3825
  const visited2 = [];
3775
- const sols2 = proveGoals(
3776
- Array.from(thenClause.triples),
3777
- s1,
3778
- scopeFacts,
3779
- scopeBackRules,
3780
- depth + 1,
3781
- visited2,
3782
- varGen,
3783
- );
3826
+ const sols2 = proveGoals(thenGoals, s1, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
3784
3827
  if (!sols2.length) return [];
3785
3828
  }
3786
3829
  return [outSubst];
package/lib/rules.js CHANGED
@@ -25,14 +25,26 @@ function liftBlankRuleVars(premise, conclusion) {
25
25
  return new Var(name);
26
26
  }
27
27
 
28
+ function copyQuotedTerm(t) {
29
+ // Quoted formulas are data terms with their own local blank scope.
30
+ // Copy them structurally so later in-place rewrites cannot mutate shared AST,
31
+ // but do not lift their blank nodes into rule-body variables.
32
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(copyQuotedTerm));
33
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(copyQuotedTerm), t.tailVar);
34
+ if (t instanceof GraphTerm) {
35
+ const triples = t.triples.map(
36
+ (tr) => new Triple(copyQuotedTerm(tr.s), copyQuotedTerm(tr.p), copyQuotedTerm(tr.o)),
37
+ );
38
+ return new GraphTerm(triples);
39
+ }
40
+ return t;
41
+ }
42
+
28
43
  function convertTerm(t) {
29
44
  if (t instanceof Blank) return blankToVar(t.label);
30
45
  if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertTerm));
31
46
  if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertTerm), t.tailVar);
32
- if (t instanceof GraphTerm) {
33
- const triples = t.triples.map((tr) => new Triple(convertTerm(tr.s), convertTerm(tr.p), convertTerm(tr.o)));
34
- return new GraphTerm(triples);
35
- }
47
+ if (t instanceof GraphTerm) return copyQuotedTerm(t);
36
48
  return t;
37
49
  }
38
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyeling",
3
- "version": "1.21.9",
3
+ "version": "1.22.0",
4
4
  "description": "A minimal Notation3 (N3) reasoner in JavaScript.",
5
5
  "main": "./index.js",
6
6
  "keywords": [
package/test/api.test.js CHANGED
@@ -2035,6 +2035,123 @@ _:x :hates { _:foo :making :mess }.
2035
2035
  },
2036
2036
  expect: [/:x :value "world" \./m],
2037
2037
  },
2038
+
2039
+ {
2040
+ name: '241 regression: quoted-formula blanks in rule bodies stay blank through log:conjunction',
2041
+ opt: { proofComments: false },
2042
+ input: `@prefix log: <http://www.w3.org/2000/10/swap/log#> .
2043
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2044
+ @prefix : <http://example.org/ns#> .
2045
+
2046
+ {
2047
+ ( { ?S a :Subject } { [] a :Thing } ) log:conjunction ?Z.
2048
+ }
2049
+ =>
2050
+ {
2051
+ :result :is ?Z.
2052
+ }.
2053
+ `,
2054
+ expect: [/:result\s+:is\s+\{[\s\S]*\?S\s+a\s+:Subject\s*\.[\s\S]*_:(?:b\d+)\s+a\s+:Thing\s*\.[\s\S]*\}\s*\./m],
2055
+ notExpect: [/\?_b\d+\s+a\s+:Thing\s*\./],
2056
+ },
2057
+ {
2058
+ name: '242 regression: log:includes existentializes blank nodes inside quoted formula patterns',
2059
+ opt: { proofComments: false },
2060
+ input: `@prefix : <http://example.org/> .
2061
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2062
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
2063
+
2064
+ :doc :graph {
2065
+ :perm :duty [
2066
+ :action :inform ;
2067
+ :constraint [
2068
+ :kind :notice ;
2069
+ :days 3
2070
+ ]
2071
+ ]
2072
+ } .
2073
+
2074
+ {
2075
+ :doc :graph ?G .
2076
+ ?G log:includes {
2077
+ :perm :duty [
2078
+ :action :inform ;
2079
+ :constraint [
2080
+ :kind :notice ;
2081
+ :days ?D
2082
+ ]
2083
+ ]
2084
+ } .
2085
+ }
2086
+ =>
2087
+ {
2088
+ :result :days ?D ;
2089
+ :status :matched .
2090
+ }.
2091
+ `,
2092
+ expect: [
2093
+ /:result\s+:days\s+3(?:\s*\^\^<http:\/\/www\.w3\.org\/2001\/XMLSchema#integer>)?\s*\./,
2094
+ /:result\s+:status\s+:matched\s*\./,
2095
+ ],
2096
+ },
2097
+ {
2098
+ name: '243a regression: collectAllIn treats quoted-formula blanks existentially',
2099
+ opt: { proofComments: false },
2100
+ input: `@prefix : <http://example.org/> .
2101
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2102
+
2103
+ :a :p [ :q 1 ] .
2104
+ :b :p [ :q 2 ] .
2105
+
2106
+ {
2107
+ ( ?s { ?s :p [ :q 1 ] . } ?xs ) log:collectAllIn _:scope .
2108
+ ?xs log:equalTo ( :a ) .
2109
+ }
2110
+ =>
2111
+ {
2112
+ :test :is true .
2113
+ }.
2114
+ `,
2115
+ expect: [/:(?:test)\s+:(?:is)\s+true\s*\./],
2116
+ },
2117
+
2118
+ {
2119
+ name: '243 regression: quoted formulas remain isolated from collectAllIn rule-body rewrites',
2120
+ opt: { proofComments: false },
2121
+ input: `@prefix : <http://example.org/jade-eigen-loom#> .
2122
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
2123
+ @prefix list: <http://www.w3.org/2000/10/swap/list#> .
2124
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
2125
+
2126
+ :PCA1 :points (
2127
+ [ :id 1 ; :x 2.0 ; :y 1.0 ]
2128
+ [ :id 2 ; :x 3.0 ; :y 2.0 ]
2129
+ [ :id 3 ; :x 4.0 ; :y 3.2 ]
2130
+ [ :id 4 ; :x 5.0 ; :y 5.1 ]
2131
+ [ :id 5 ; :x 6.0 ; :y 7.9 ]
2132
+ [ :id 6 ; :x 7.0 ; :y 13.0 ]
2133
+ [ :id 7 ; :x 20.0 ; :y -3.0 ]
2134
+ ) .
2135
+
2136
+ {
2137
+ :PCA1 :points ?pts .
2138
+ ?pts list:length ?n .
2139
+ ( ?x { ?pts list:member ?p . ?p :x ?x . } ?xs ) log:collectAllIn _:m1 .
2140
+ ?xs math:sum ?sumX .
2141
+ (?sumX ?n) math:quotient ?meanX .
2142
+ }
2143
+ =>
2144
+ {
2145
+ :result :xs ?xs ; :sumX ?sumX ; :meanX ?meanX .
2146
+ }.
2147
+ `,
2148
+ expect: [
2149
+ /:result\s+:xs\s+\(2\.0 3\.0 4\.0 5\.0 6\.0 7\.0 20\.0\)\s*\./,
2150
+ /:result\s+:sumX\s+"47"\^\^xsd:decimal\s*\./,
2151
+ /:result\s+:meanX\s+"6\.714285714285714"\^\^xsd:decimal\s*\./,
2152
+ ],
2153
+ notExpect: [/:result\s+:sumX\s+"329"\^\^xsd:decimal\s*\./, /:result\s+:meanX\s+"47"\^\^xsd:decimal\s*\./],
2154
+ },
2038
2155
  ];
2039
2156
 
2040
2157
  let passed = 0;
@@ -3,12 +3,10 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const assert = require('node:assert/strict');
6
- const { pathToFileURL } = require('node:url');
6
+ const { execFileSync } = require('node:child_process');
7
7
 
8
8
  const TTY = process.stdout.isTTY;
9
- const C = TTY
10
- ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
11
- : { g: '', r: '', y: '', dim: '', n: '' };
9
+ const C = TTY ? { g: '', r: '', y: '', dim: '', n: '' } : { g: '', r: '', y: '', dim: '', n: '' };
12
10
  const msTag = (ms) => `${C.dim}(${ms} ms)${C.n}`;
13
11
 
14
12
  function ok(msg) {
@@ -48,10 +46,19 @@ function listCaseDirs(baseDir) {
48
46
  .sort();
49
47
  }
50
48
 
49
+ function findModelPath(caseDir, base) {
50
+ const candidates = [path.join(caseDir, `${base}.model.go`), path.join(caseDir, `${base}.model.mjs`)];
51
+
52
+ for (const candidate of candidates) {
53
+ if (fs.existsSync(candidate)) return candidate;
54
+ }
55
+
56
+ throw new Error(`Missing required arcling model artifact for ${base}`);
57
+ }
58
+
51
59
  function findCaseFiles(caseDir) {
52
60
  const base = path.basename(caseDir);
53
-
54
- const modelPath = path.join(caseDir, `${base}.model.mjs`);
61
+ const modelPath = findModelPath(caseDir, base);
55
62
  const dataPath = path.join(caseDir, `${base}.data.json`);
56
63
  const expectedPath = path.join(caseDir, `${base}.expected.json`);
57
64
 
@@ -64,15 +71,28 @@ function findCaseFiles(caseDir) {
64
71
  return { base, modelPath, dataPath, expectedPath };
65
72
  }
66
73
 
67
- async function loadEvaluate(modelPath) {
68
- const moduleUrl = pathToFileURL(modelPath).href;
69
- const mod = await import(moduleUrl);
74
+ function runModelJson(modelPath, dataPath) {
75
+ const ext = path.extname(modelPath);
76
+
77
+ if (ext === '.go') {
78
+ const stdout = execFileSync('go', ['run', modelPath, dataPath, '--json'], {
79
+ cwd: path.dirname(modelPath),
80
+ encoding: 'utf8',
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ return JSON.parse(stdout);
84
+ }
70
85
 
71
- if (typeof mod.evaluate !== 'function') {
72
- throw new Error(`Model does not export evaluate(data): ${modelPath}`);
86
+ if (ext === '.mjs') {
87
+ const stdout = execFileSync(process.execPath, [modelPath, dataPath, '--json'], {
88
+ cwd: path.dirname(modelPath),
89
+ encoding: 'utf8',
90
+ stdio: ['ignore', 'pipe', 'pipe'],
91
+ });
92
+ return JSON.parse(stdout);
73
93
  }
74
94
 
75
- return mod.evaluate;
95
+ throw new Error(`Unsupported arcling model extension: ${modelPath}`);
76
96
  }
77
97
 
78
98
  function assertArcTextShape(arcText, label) {
@@ -84,17 +104,15 @@ function assertArcTextShape(arcText, label) {
84
104
 
85
105
  async function runCase(caseDir) {
86
106
  const { base, modelPath, dataPath, expectedPath } = findCaseFiles(caseDir);
87
-
88
- const evaluate = await loadEvaluate(modelPath);
89
107
  const data = readJson(dataPath);
90
108
  const expected = readJson(expectedPath);
91
- const actual = await evaluate(data);
109
+ const actual = runModelJson(modelPath, dataPath);
92
110
 
93
111
  assert.equal(actual.allChecksPass, true, `${base}: expected allChecksPass === true`);
94
112
  assertArcTextShape(actual.arcText, base);
95
113
  assert.deepStrictEqual(actual, expected, `${base}: actual result does not match expected JSON`);
96
114
 
97
- return base;
115
+ return { base, caseName: data.caseName, modelPath };
98
116
  }
99
117
 
100
118
  async function main() {