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/HANDBOOK.md CHANGED
@@ -307,6 +307,31 @@ This avoids the “existential in the body” trap and matches how most rule aut
307
307
 
308
308
  Blanks in the **conclusion** are _not_ lifted — they remain blanks and later become existentials (Chapter 9).
309
309
 
310
+ ### 5.1.1 Quoted formulas keep their own blank-node scope
311
+
312
+ There is one important exception to the “lift blanks in rule bodies” rule: **do not descend into a quoted formula** (`GraphTerm`) and lift the blanks that appear _inside_ it.
313
+
314
+ So this rule body:
315
+
316
+ ```n3
317
+ {
318
+ ( { ?S a :Subject } { [] a :Thing } ) log:conjunction ?Z.
319
+ } => { ... }.
320
+ ```
321
+
322
+ must keep the inner `[]` as a **formula-local blank node**. Eyeling should treat it as belonging to the quoted graph, not as a rule-body variable that escapes into the surrounding rule.
323
+
324
+ That distinction matters because quoted formulas play **two different roles** in Eyeling:
325
+
326
+ 1. **Formula as data** — for example when constructing a formula with `log:conjunction` or storing `{ ... }` in a triple. In this role, local blanks stay blanks. They print as blank nodes and participate in alpha-equivalence only within that quoted formula.
327
+ 2. **Formula as a query pattern** — for example when `log:includes`, `log:notIncludes`, `log:collectAllIn`, or `log:forAllIn` prove a quoted formula. In that role, the builtin may treat the formula’s **local blanks existentially** while matching.
328
+
329
+ The practical rule is:
330
+
331
+ > **Rule normalization preserves blank-node scope inside quoted formulas; builtins may later interpret those preserved blanks as existential query placeholders when the formula is used as a pattern.**
332
+
333
+ This separation is deliberate. It keeps `log:conjunction` and formula printing honest, while still allowing query-like builtins to match formulas containing local `[]` placeholders.
334
+
310
335
  ### 5.2 Builtin deferral in forward-rule bodies
311
336
 
312
337
  In a depth-first proof, the order of goals matters. Many built-ins only become informative once parts of the triple are **already instantiated** (for example comparisons, pattern tests, and other built-ins that don’t normally create bindings).
@@ -1481,6 +1506,24 @@ Also supported:
1481
1506
 
1482
1507
  - The object may be the literal `true`, meaning the empty formula, which is always included (subject to the priority gating above).
1483
1508
 
1509
+ **Important blank-node note:** when the goal formula is used as a **pattern**, Eyeling treats blank nodes that are **local to that quoted formula** as existential placeholders during the proof.
1510
+
1511
+ So a pattern such as:
1512
+
1513
+ ```n3
1514
+ { ?x :p [] }
1515
+ ```
1516
+
1517
+ means “find an `?x` that has some `:p` value”, not “find the specific blank node label printed here”.
1518
+
1519
+ But that existential behavior is intentionally limited:
1520
+
1521
+ - it applies only to blanks that are **owned by the quoted formula being proved**
1522
+ - it does **not** rename or relax terms that were already supplied by an outer substitution
1523
+ - it does **not** turn concrete members of already-bound lists or other already-ground structures into fresh variables
1524
+
1525
+ That last point is easy to miss. A builtin may receive a formula after part of it has already been instantiated from outer bindings. Those substituted-in terms are fixed data, not fresh existential placeholders. Keeping that boundary sharp prevents accidental overmatching and keeps numeric/list-oriented examples stable.
1526
+
1484
1527
  #### `log:notIncludes` (test)
1485
1528
 
1486
1529
  Negation-as-failure version: it succeeds iff `log:includes` would yield no solutions (under the same scoping rules).
@@ -1494,6 +1537,8 @@ Negation-as-failure version: it succeeds iff `log:includes` would yield no solut
1494
1537
  - Unifies `OutList` with that list.
1495
1538
  - If `OutList` is a blank node, Eyeling just checks satisfiable without binding/collecting.
1496
1539
 
1540
+ As with `log:includes`, blank nodes that are local to `WhereFormula` behave as existential query placeholders while that formula is being proved. But blanks that came from already-bound outer data remain fixed.
1541
+
1497
1542
  This is essentially a list-producing “findall”.
1498
1543
 
1499
1544
  #### `log:forAllIn` (test)
@@ -813,6 +813,57 @@
813
813
  for (const tr of triples) __builtinCollectVarsInTriple(tr, out);
814
814
  }
815
815
 
816
+ function __existentializeBlankTerm(t, mapping, varGen) {
817
+ if (t instanceof Blank) {
818
+ let v = mapping[t.label];
819
+ if (v === undefined) {
820
+ const n =
821
+ Array.isArray(varGen) && typeof varGen[0] === 'number' ? varGen[0]++ : Object.keys(mapping).length + 1;
822
+ v = new Var(`__qb_${n}`);
823
+ mapping[t.label] = v;
824
+ }
825
+ return v;
826
+ }
827
+ if (t instanceof ListTerm) return t;
828
+ if (t instanceof OpenListTerm) return t;
829
+ if (t instanceof GraphTerm) {
830
+ let changed = false;
831
+ const triples = t.triples.map((tr) => {
832
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
833
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
834
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
835
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
836
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
837
+ });
838
+ return changed ? new GraphTerm(triples) : t;
839
+ }
840
+ return t;
841
+ }
842
+
843
+ function __existentializeBlankTriples(triples, varGen) {
844
+ const mapping = Object.create(null);
845
+ let changed = false;
846
+ const out = triples.map((tr) => {
847
+ const s2 = __existentializeBlankTerm(tr.s, mapping, varGen);
848
+ const p2 = __existentializeBlankTerm(tr.p, mapping, varGen);
849
+ const o2 = __existentializeBlankTerm(tr.o, mapping, varGen);
850
+ if (s2 !== tr.s || p2 !== tr.p || o2 !== tr.o) changed = true;
851
+ return s2 === tr.s && p2 === tr.p && o2 === tr.o ? tr : new Triple(s2, p2, o2);
852
+ });
853
+ return changed ? out : triples;
854
+ }
855
+
856
+ function __prepareQuotedPatternTriples(rawTriples, subst, varGen) {
857
+ const ex = __existentializeBlankTriples(Array.from(rawTriples), varGen);
858
+ let changed = false;
859
+ const out = ex.map((tr) => {
860
+ const tr2 = applySubstTriple(tr, subst);
861
+ if (tr2 !== tr) changed = true;
862
+ return tr2;
863
+ });
864
+ return changed ? out : ex;
865
+ }
866
+
816
867
  function literalHasLangTag(lit) {
817
868
  // True iff the literal is a quoted string literal with a language tag suffix,
818
869
  // e.g. "hello"@en or """hello"""@en.
@@ -4014,7 +4065,8 @@
4014
4065
  const keepVars = new Set();
4015
4066
  if (g.s instanceof GraphTerm) __builtinCollectVarsInTriples(g.s.triples, keepVars);
4016
4067
 
4017
- const goalVariants = __expandScopedVarPredicateGoals(Array.from(g.o.triples));
4068
+ const goalTriples = __prepareQuotedPatternTriples(goal.o.triples, subst, varGen);
4069
+ const goalVariants = __expandScopedVarPredicateGoals(goalTriples);
4018
4070
  const out = [];
4019
4071
  for (const variant of goalVariants) {
4020
4072
  const sols = proveGoals(
@@ -4096,7 +4148,7 @@
4096
4148
 
4097
4149
  const visited2 = [];
4098
4150
  const sols = proveGoals(
4099
- Array.from(g.o.triples),
4151
+ __prepareQuotedPatternTriples(goal.o.triples, subst, varGen),
4100
4152
  { ...subst },
4101
4153
  scopeFacts,
4102
4154
  scopeBackRules,
@@ -4190,15 +4242,12 @@
4190
4242
  }
4191
4243
 
4192
4244
  const visited2 = [];
4193
- const sols = proveGoals(
4194
- Array.from(clauseTerm.triples),
4195
- {},
4196
- scopeFacts,
4197
- scopeBackRules,
4198
- depth + 1,
4199
- visited2,
4200
- varGen,
4201
- );
4245
+ const rawClauseTerm = goal.s instanceof ListTerm ? goal.s.elems[1] : clauseTerm;
4246
+ const clauseGoals =
4247
+ rawClauseTerm instanceof GraphTerm
4248
+ ? __prepareQuotedPatternTriples(rawClauseTerm.triples, subst, varGen)
4249
+ : __prepareQuotedPatternTriples(clauseTerm.triples, subst, varGen);
4250
+ const sols = proveGoals(clauseGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
4202
4251
 
4203
4252
  const collected = sols.map((sBody) => applySubstTerm(valueTempl, sBody));
4204
4253
  const collectedList = new ListTerm(collected);
@@ -4251,28 +4300,23 @@
4251
4300
  scopeFacts = snap;
4252
4301
  }
4253
4302
 
4303
+ const rawWhereClause = goal.s instanceof ListTerm ? goal.s.elems[0] : whereClause;
4304
+ const rawThenClause = goal.s instanceof ListTerm ? goal.s.elems[1] : thenClause;
4305
+ const whereGoals =
4306
+ rawWhereClause instanceof GraphTerm
4307
+ ? __prepareQuotedPatternTriples(rawWhereClause.triples, subst, varGen)
4308
+ : __prepareQuotedPatternTriples(whereClause.triples, subst, varGen);
4309
+ const thenGoals =
4310
+ rawThenClause instanceof GraphTerm
4311
+ ? __prepareQuotedPatternTriples(rawThenClause.triples, subst, varGen)
4312
+ : __prepareQuotedPatternTriples(thenClause.triples, subst, varGen);
4313
+
4254
4314
  const visited1 = [];
4255
- const sols1 = proveGoals(
4256
- Array.from(whereClause.triples),
4257
- {},
4258
- scopeFacts,
4259
- scopeBackRules,
4260
- depth + 1,
4261
- visited1,
4262
- varGen,
4263
- );
4315
+ const sols1 = proveGoals(whereGoals, {}, scopeFacts, scopeBackRules, depth + 1, visited1, varGen);
4264
4316
 
4265
4317
  for (const s1 of sols1) {
4266
4318
  const visited2 = [];
4267
- const sols2 = proveGoals(
4268
- Array.from(thenClause.triples),
4269
- s1,
4270
- scopeFacts,
4271
- scopeBackRules,
4272
- depth + 1,
4273
- visited2,
4274
- varGen,
4275
- );
4319
+ const sols2 = proveGoals(thenGoals, s1, scopeFacts, scopeBackRules, depth + 1, visited2, varGen);
4276
4320
  if (!sols2.length) return [];
4277
4321
  }
4278
4322
  return [outSubst];
@@ -12735,14 +12779,26 @@ ${triples.map((tr) => ` ${tripleToN3(tr, prefixes)}`).join('\n')}
12735
12779
  return new Var(name);
12736
12780
  }
12737
12781
 
12782
+ function copyQuotedTerm(t) {
12783
+ // Quoted formulas are data terms with their own local blank scope.
12784
+ // Copy them structurally so later in-place rewrites cannot mutate shared AST,
12785
+ // but do not lift their blank nodes into rule-body variables.
12786
+ if (t instanceof ListTerm) return new ListTerm(t.elems.map(copyQuotedTerm));
12787
+ if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(copyQuotedTerm), t.tailVar);
12788
+ if (t instanceof GraphTerm) {
12789
+ const triples = t.triples.map(
12790
+ (tr) => new Triple(copyQuotedTerm(tr.s), copyQuotedTerm(tr.p), copyQuotedTerm(tr.o)),
12791
+ );
12792
+ return new GraphTerm(triples);
12793
+ }
12794
+ return t;
12795
+ }
12796
+
12738
12797
  function convertTerm(t) {
12739
12798
  if (t instanceof Blank) return blankToVar(t.label);
12740
12799
  if (t instanceof ListTerm) return new ListTerm(t.elems.map(convertTerm));
12741
12800
  if (t instanceof OpenListTerm) return new OpenListTerm(t.prefix.map(convertTerm), t.tailVar);
12742
- if (t instanceof GraphTerm) {
12743
- const triples = t.triples.map((tr) => new Triple(convertTerm(tr.s), convertTerm(tr.p), convertTerm(tr.o)));
12744
- return new GraphTerm(triples);
12745
- }
12801
+ if (t instanceof GraphTerm) return copyQuotedTerm(t);
12746
12802
  return t;
12747
12803
  }
12748
12804
 
@@ -6,7 +6,7 @@ An Arcling case sits alongside the declarative N3 cases in `examples/`. Its purp
6
6
 
7
7
  In one line:
8
8
 
9
- > `examples/arcling/` presents ARC cases in mathematical English with reference ECMAScript realizations and JSON test vectors.
9
+ > `examples/arcling/` presents ARC cases in mathematical English with reference Go realizations and JSON test vectors.
10
10
 
11
11
  ## Insight Economy context
12
12
 
@@ -27,7 +27,7 @@ Eyeling already has a strong way to present a case in declarative N3. Arcling ad
27
27
  Each Arcling case gives you:
28
28
 
29
29
  - a **normative statement** in mathematical English,
30
- - a **reference realization** in ECMAScript,
30
+ - a **reference realization** in Go,
31
31
  - a **concrete instance** in JSON,
32
32
  - and an **expected result** for comparison and regression testing.
33
33
 
@@ -65,7 +65,7 @@ Typical uses:
65
65
  - cases with a stable logical core and a small executable shell,
66
66
  - cases that benefit from conformance-style testing.
67
67
 
68
- ## The five files
68
+ ## The four files
69
69
 
70
70
  Each Arcling case should contain these files.
71
71
 
@@ -75,7 +75,7 @@ The normative case description.
75
75
 
76
76
  This file should use **mathematical English**. It should define the vocabulary, the inputs, the derived predicates, the decision rule, the governance rule, the checks, and the output contract.
77
77
 
78
- The spec should be written so that a careful reader can understand the case without reading the ECMAScript source first.
78
+ The spec should be written so that a careful reader can understand the case without reading the Go source first.
79
79
 
80
80
  ### 2. `name.data.json`
81
81
 
@@ -83,40 +83,36 @@ The concrete instance data.
83
83
 
84
84
  This file contains the facts for the case: entities, thresholds, observed values, policies, timestamps, candidate actions, and any other case inputs.
85
85
 
86
- ### 3. `name.model.mjs`
86
+ ### 3. `name.model.go`
87
87
 
88
- The reference ECMAScript realization.
88
+ The reference Go realization.
89
89
 
90
90
  This file should implement the case directly and clearly. A good pattern is to map named clauses in the spec to named functions in the model.
91
91
 
92
92
  For example:
93
93
 
94
- - `clauseR1_exportWeakness`
95
- - `clauseS2_recommendedPackage`
96
- - `clauseG1_authorizedUse`
97
- - `clauseM2_payloadHash`
94
+ - `clauseR1ExportWeakness`
95
+ - `clauseS2RecommendedPackage`
96
+ - `clauseG1AuthorizedUse`
97
+ - `clauseM2PayloadHash`
98
98
 
99
99
  The model is not the normative source. It is the **reference realization** of the normative source.
100
100
 
101
+ Input validation is part of the reference model. A malformed instance should fail before evaluation rather than requiring a separate schema artifact.
102
+
101
103
  ### 4. `name.expected.json`
102
104
 
103
105
  The expected derived result.
104
106
 
105
107
  This file is the conformance vector for the case. It should capture the main derived predicates, the selected answer, the visible checks, and any stable integrity values needed for regression testing.
106
108
 
107
- ### 5. `name.instance.schema.json`
108
-
109
- The instance schema.
110
-
111
- This file defines the required structure of the input JSON. It should be strict enough to catch malformed case instances before evaluation.
112
-
113
109
  ## How to read an Arcling case
114
110
 
115
111
  A good reading order is:
116
112
 
117
113
  1. start with `name.spec.md`,
118
114
  2. inspect `name.data.json`,
119
- 3. run `name.model.mjs`,
115
+ 3. run `go run name.model.go --json`,
120
116
  4. compare the result with `name.expected.json`,
121
117
  5. then relate the case back to its N3 counterpart.
122
118
 
@@ -127,7 +123,7 @@ That order keeps the meaning visible before the operational details.
127
123
  A useful mental model is:
128
124
 
129
125
  - `examples/` shows ARC cases in **declarative Eyeling form**,
130
- - `examples/arcling/` shows the same kind of cases in **mathematical-English specification plus reference ECMAScript form**.
126
+ - `examples/arcling/` shows the same kind of cases in **mathematical-English specification plus reference Go form**.
131
127
 
132
128
  So the two collections are complementary:
133
129
 
@@ -144,7 +140,7 @@ The spec should say what the case means. It should not merely paraphrase the cod
144
140
 
145
141
  ### 2. Keep the code direct
146
142
 
147
- The ECMAScript model should say what it does and do what it says. Avoid unnecessary framework machinery.
143
+ The Go model should say what it does and do what it says. Avoid unnecessary framework machinery.
148
144
 
149
145
  ### 3. Keep the data separate
150
146
 
@@ -163,7 +159,7 @@ If a case is called `delfour`, `medior`, or `flandor` in `examples/`, the Arclin
163
159
  1. Start from a strong ARC-style N3 example.
164
160
  2. Write a mathematical-English specification of the case.
165
161
  3. Move the concrete instance into JSON.
166
- 4. Implement a small ECMAScript reference model.
162
+ 4. Implement a small Go reference model.
167
163
  5. Capture the expected result in JSON.
168
164
  6. Keep the visible output in Answer / Reason Why / Check shape.
169
165
  7. Link the Arcling case to its N3 counterpart.
@@ -186,4 +182,4 @@ It exists to make a case simultaneously:
186
182
 
187
183
  ## In one line
188
184
 
189
- `examples/arcling/` presents ARC cases in mathematical English with reference ECMAScript realizations, JSON instances, and expected results, alongside declarative Eyeling examples.
185
+ `examples/arcling/` presents ARC cases in mathematical English with reference Go realizations, JSON instances, and expected results, alongside declarative Eyeling examples.