eyeleng 1.0.10 → 1.0.12

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/README.md CHANGED
@@ -76,6 +76,8 @@ then the body matches with `?parent = :alice` and `?child = :bob`, and the head
76
76
 
77
77
  Negation is handled by stratified evaluation: rules are grouped into dependency layers, and recursion through negation is rejected so the result stays deterministic.
78
78
 
79
+ Recursive rules that create new terms through `SET`/`BIND` or head blank nodes run in relaxed mode by default. Relaxed mode can derive useful finite closures, but termination is not guaranteed; use `--max-iterations` as a safety valve. `--strict` rejects these recursive term-generating cycles at analysis time. Head blank nodes are deterministically skolemized per rule and solution mapping, so the same firing reuses the same witness node instead of creating a fresh one each pass.
80
+
79
81
  ## Language surface
80
82
 
81
83
  SRL supports the practical rule features used by the SHACL 1.2 Rules tests and examples:
@@ -90,7 +92,7 @@ DATA {
90
92
 
91
93
  RULE { ?x :grade ?grade } WHERE {
92
94
  ?x :score ?score .
93
- FILTER(?score >= 5) .
95
+ FILTER(?score >= 5)
94
96
  BIND(concat("pass-", str(?score)) AS ?grade)
95
97
  }
96
98
 
@@ -197,19 +199,24 @@ Common commands:
197
199
  Important options:
198
200
 
199
201
  ```text
200
- --all print input and inferred triples
201
- --check parse and analyze only
202
- --strict warnings become fatal
203
- --deps print dependency edges and layers
204
- --trace show rule firings
205
- --stats show iteration and rule counts
206
- --json structured output
207
- --query run a raw body pattern over the closure
208
- --query-file FILE read a query body from a file
209
- --max-iterations N recursive-layer fixpoint safety guard
210
- --no-imports parse imports but do not load them
211
- --rdf-messages parse input as an RDF Message Log
212
- --stream-messages replay RDF Message Log envelopes
202
+ --all print the full closure, including input facts
203
+ --json print JSON instead of compact triples/bindings
204
+ --trace print derivation trace to stderr, or include it in JSON
205
+ --stats print iteration and triple counts to stderr
206
+ --check parse and analyze only; do not run rules
207
+ --strict treat static warnings as errors, including recursive term generation
208
+ --deps print rule dependency edges during --check
209
+ --query TEXT run a raw SRL body pattern over the closure
210
+ --query-file FILE read a raw SRL body pattern from a file
211
+ --max-iterations N stop after N fixpoint iterations within a recursive layer
212
+ --no-imports parse IMPORTS/owl:imports but do not load imported rule sets
213
+ --rdf-messages parse input as an RDF Message Log
214
+ --stream-messages replay RDF Message Log envelopes
215
+ --include-message-facts include payload facts while parsing RDF Message Logs
216
+ --syntax MODE use srl, rdf, or auto syntax detection (default auto)
217
+ --ruleset TERM in RDF syntax, run only the selected srl:RuleSet
218
+ --version print version
219
+ -h, --help print help
213
220
  ```
214
221
 
215
222
  ## Public API
@@ -4048,8 +4048,8 @@
4048
4048
  "src/engine.js": function (require, module, exports) {
4049
4049
  'use strict';
4050
4050
 
4051
- const { TripleStore, bindingKey, instantiateTriple } = require('./store.js');
4052
- const { tripleKey, termEquals } = require('./term.js');
4051
+ const { TripleStore, bindingKey } = require('./store.js');
4052
+ const { tripleKey, termKey, termEquals, blankNode, tripleTerm } = require('./term.js');
4053
4053
  const { evalExpression, booleanValue, asTerm } = require('./builtins.js');
4054
4054
  const { analyze } = require('./analyze.js');
4055
4055
 
@@ -4069,7 +4069,7 @@
4069
4069
  runOnce: !!rule.runOnce,
4070
4070
  }));
4071
4071
 
4072
- const analysis = options.analysis || analyze(program);
4072
+ const analysis = options.analysis || analyze(program, options);
4073
4073
  if (analysis.errors && analysis.errors.length > 0 && !options.ignoreAnalysisErrors) {
4074
4074
  throw new Error(`Analysis failed: ${analysis.errors.map((error) => error.message).join('; ')}`);
4075
4075
  }
@@ -4080,6 +4080,9 @@
4080
4080
  layerIndexes,
4081
4081
  analysis.dependency ? analysis.dependency.edges : [],
4082
4082
  );
4083
+ const relaxedRecursiveRunOnce = options.relaxedRecursion === false
4084
+ ? new Set()
4085
+ : recursiveTermGenerationRuleIndexes(analysis);
4083
4086
  const baseContext = {
4084
4087
  ...evalOptions,
4085
4088
  maxIterations,
@@ -4095,8 +4098,8 @@
4095
4098
 
4096
4099
  for (let layerIndex = 0; layerIndex < layerIndexes.length; layerIndex += 1) {
4097
4100
  const layer = layerIndexes[layerIndex];
4098
- const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce);
4099
- const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce);
4101
+ const ordinary = layer.filter((ruleIndex) => !program.rules[ruleIndex].runOnce || relaxedRecursiveRunOnce.has(ruleIndex));
4102
+ const runOnce = layer.filter((ruleIndex) => program.rules[ruleIndex].runOnce && !relaxedRecursiveRunOnce.has(ruleIndex));
4100
4103
 
4101
4104
  if (runOnce.length > 0) {
4102
4105
  iterations += 1;
@@ -4196,6 +4199,7 @@
4196
4199
  let added = 0;
4197
4200
  const dedupeBindings = rule.body.some((clause) => clause.type === 'path');
4198
4201
  const seenBindings = dedupeBindings ? new Set() : null;
4202
+ const headBlankLabels = collectHeadBlankLabels(rule.head);
4199
4203
 
4200
4204
  const bodyBindings = rule.body.length === 1 && rule.body[0].type === 'triple'
4201
4205
  ? store.match(rule.body[0].triple, {})
@@ -4210,8 +4214,10 @@
4210
4214
  applications += 1;
4211
4215
  context.perRule[ruleIndex].applications += 1;
4212
4216
 
4217
+ const headBlankMap = headBlankLabels.size > 0 ? new Map() : null;
4218
+ const skolemKey = headBlankMap ? skolemizationKey(ruleIndex, binding) : null;
4213
4219
  for (const head of rule.head) {
4214
- const triple = instantiateTriple(head, binding);
4220
+ const triple = instantiateHeadTriple(head, binding, headBlankLabels, headBlankMap, skolemKey);
4215
4221
  if (!triple) continue;
4216
4222
  if (store.add(triple)) {
4217
4223
  added += 1;
@@ -4233,6 +4239,94 @@
4233
4239
  return { applications, added };
4234
4240
  }
4235
4241
 
4242
+ function recursiveTermGenerationRuleIndexes(analysis) {
4243
+ const out = new Set();
4244
+ if (!analysis || !analysis.dependency || !analysis.diagnostics) return out;
4245
+ const byName = new Map((analysis.dependency.rules || []).map((rule) => [rule.name, rule.index]));
4246
+ for (const diagnostic of analysis.diagnostics) {
4247
+ if (diagnostic.code !== 'recursive-assignment-rule') continue;
4248
+ if (byName.has(diagnostic.rule)) out.add(byName.get(diagnostic.rule));
4249
+ }
4250
+ return out;
4251
+ }
4252
+
4253
+ function instantiateHeadTriple(pattern, binding, headBlankLabels, headBlankMap, skolemKey) {
4254
+ const s = instantiateHeadTerm(pattern.s, binding, headBlankLabels, headBlankMap, skolemKey);
4255
+ const p = instantiateHeadTerm(pattern.p, binding, headBlankLabels, headBlankMap, skolemKey);
4256
+ const o = instantiateHeadTerm(pattern.o, binding, headBlankLabels, headBlankMap, skolemKey);
4257
+ if (!s || !p || !o) return null;
4258
+ if (p.type !== 'iri') return null;
4259
+ return { s, p, o };
4260
+ }
4261
+
4262
+ function instantiateHeadTerm(term, binding, headBlankLabels, headBlankMap, skolemKey) {
4263
+ if (term.type === 'var') return binding[term.value] || null;
4264
+ if (term.type === 'blank' && headBlankLabels.has(term.value)) {
4265
+ let label = headBlankMap.get(term.value);
4266
+ if (!label) {
4267
+ label = `sk_${deterministicSkolemIdFromKey(`${skolemKey}|${term.value}`).replace(/-/g, '_')}`;
4268
+ headBlankMap.set(term.value, label);
4269
+ }
4270
+ return blankNode(label);
4271
+ }
4272
+ if (term.type === 'triple') {
4273
+ const s = instantiateHeadTerm(term.s, binding, headBlankLabels, headBlankMap, skolemKey);
4274
+ const p = instantiateHeadTerm(term.p, binding, headBlankLabels, headBlankMap, skolemKey);
4275
+ const o = instantiateHeadTerm(term.o, binding, headBlankLabels, headBlankMap, skolemKey);
4276
+ if (!s || !p || !o) return null;
4277
+ return tripleTerm(s, p, o);
4278
+ }
4279
+ return term;
4280
+ }
4281
+
4282
+ function collectHeadBlankLabels(head) {
4283
+ const labels = new Set();
4284
+ for (const triple of head || []) {
4285
+ collectBlankLabelsFromTerm(triple.s, labels);
4286
+ collectBlankLabelsFromTerm(triple.p, labels);
4287
+ collectBlankLabelsFromTerm(triple.o, labels);
4288
+ }
4289
+ return labels;
4290
+ }
4291
+
4292
+ function collectBlankLabelsFromTerm(term, labels) {
4293
+ if (!term) return;
4294
+ if (term.type === 'blank') {
4295
+ labels.add(term.value);
4296
+ return;
4297
+ }
4298
+ if (term.type === 'triple') {
4299
+ collectBlankLabelsFromTerm(term.s, labels);
4300
+ collectBlankLabelsFromTerm(term.p, labels);
4301
+ collectBlankLabelsFromTerm(term.o, labels);
4302
+ }
4303
+ }
4304
+
4305
+ function skolemizationKey(ruleIndex, binding) {
4306
+ let out = `rule:${ruleIndex}`;
4307
+ for (const name of Object.keys(binding).sort()) {
4308
+ const value = binding[name];
4309
+ out += `|${name}=${value ? termKey(value) : 'unbound'}`;
4310
+ }
4311
+ return out;
4312
+ }
4313
+
4314
+ function deterministicSkolemIdFromKey(key) {
4315
+ let h1 = 0x811c9dc5;
4316
+ let h2 = 0x811c9dc5;
4317
+ let h3 = 0x811c9dc5;
4318
+ let h4 = 0x811c9dc5;
4319
+ for (let i = 0; i < key.length; i += 1) {
4320
+ const c = key.charCodeAt(i);
4321
+ h1 ^= c; h1 = Math.imul(h1, 0x01000193) >>> 0;
4322
+ h2 ^= c + 1; h2 = Math.imul(h2, 0x01000193) >>> 0;
4323
+ h3 ^= c + 2; h3 = Math.imul(h3, 0x01000193) >>> 0;
4324
+ h4 ^= c + 3; h4 = Math.imul(h4, 0x01000193) >>> 0;
4325
+ }
4326
+ return [h1, h2, h3, h4].map((h) => h.toString(16).padStart(8, '0')).join('');
4327
+ }
4328
+
4329
+
4236
4330
  function evaluateBody(clauses, store, initialBinding = {}, options = {}) {
4237
4331
  const bindings = [];
4238
4332
  const seen = new Set();
@@ -4589,8 +4683,8 @@
4589
4683
  function analyze(program, options = {}) {
4590
4684
  const diagnostics = [];
4591
4685
  const dependency = dependencyGraph(program, options);
4592
- const hasRunOnceRules = program.rules.some((rule) => rule.runOnce);
4593
- const recursiveIndexes = hasRunOnceRules ? recursiveRuleIndexes(dependency) : new Set();
4686
+ const hasTermGeneratingRules = dependency.rules.some((rule) => rule.createsTerms);
4687
+ const recursiveIndexes = hasTermGeneratingRules ? recursiveRuleIndexes(dependency) : new Set();
4594
4688
 
4595
4689
  program.rules.forEach((rule, index) => {
4596
4690
  const name = ruleName(rule, index);
@@ -4622,12 +4716,13 @@
4622
4716
 
4623
4717
  diagnostics.push(...sequentialWellFormednessDiagnostics(rule.body, name, program.prefixes || {}));
4624
4718
 
4625
- if (rule.runOnce && recursiveIndexes.has(index)) {
4719
+ const depRule = dependency.rules[index] || {};
4720
+ if (depRule.createsTerms && recursiveIndexes.has(index)) {
4626
4721
  diagnostics.push({
4627
4722
  code: 'recursive-assignment-rule',
4628
4723
  severity: 'warning',
4629
4724
  rule: name,
4630
- message: `${displayRuleName(name, program.prefixes || {})} is a run-once rule in a recursive dependency cycle`,
4725
+ message: `${displayRuleName(name, program.prefixes || {})} creates terms in a recursive dependency cycle; relaxed mode allows this but termination is not guaranteed (use --strict to reject it)`,
4631
4726
  });
4632
4727
  }
4633
4728
 
@@ -4674,34 +4769,47 @@
4674
4769
  negativePredicates: new Set(negativePatterns.flatMap((triple) => predicateIRIs(triple))),
4675
4770
  runOnce: !!rule.runOnce,
4676
4771
  hasAssignment: ruleHasAssignment(rule, options),
4772
+ hasTermGeneratingAssignment: ruleHasTermGeneratingAssignment(rule, options),
4677
4773
  headHasBlankNode: ruleHeadHasBlankNode(rule),
4774
+ createsTerms: ruleCreatesTerms(rule, options),
4678
4775
  };
4679
4776
  });
4680
4777
 
4681
4778
  const edgeMap = new Map();
4682
- function addEdge(from, to, negative, predicate) {
4779
+ function addEdge(from, to, kind, predicate) {
4683
4780
  const label = predicate || '*';
4684
4781
  const key = `${from.index}->${to.index}:${label}`;
4782
+ const negated = kind === 'negated';
4783
+ const termGeneration = kind === 'term-generation';
4685
4784
  const existing = edgeMap.get(key);
4686
4785
  if (existing) {
4687
- existing.negative = existing.negative || negative;
4786
+ existing.negated = existing.negated || negated;
4787
+ existing.termGeneration = existing.termGeneration || termGeneration;
4788
+ existing.negative = existing.negated || existing.termGeneration;
4688
4789
  return;
4689
4790
  }
4690
- edgeMap.set(key, { from: from.index, to: to.index, negative, predicate });
4791
+ edgeMap.set(key, {
4792
+ from: from.index,
4793
+ to: to.index,
4794
+ negative: negated || termGeneration,
4795
+ negated,
4796
+ termGeneration,
4797
+ predicate,
4798
+ });
4691
4799
  }
4692
4800
 
4693
4801
  const headIndex = buildHeadTemplateIndex(rules);
4694
4802
 
4695
4803
  for (const from of rules) {
4696
- const forceClosed = from.hasAssignment || from.headHasBlankNode;
4804
+ const forceClosed = from.createsTerms;
4697
4805
  for (const pattern of from.positivePatterns) {
4698
4806
  for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
4699
- if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed, dependencyPredicateLabel(pattern));
4807
+ if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], forceClosed ? 'term-generation' : 'positive', dependencyPredicateLabel(pattern));
4700
4808
  }
4701
4809
  }
4702
4810
  for (const pattern of from.negativePatterns) {
4703
4811
  for (const candidate of candidateHeadTemplates(headIndex, pattern)) {
4704
- if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], true, dependencyPredicateLabel(pattern));
4812
+ if (canPossiblyGenerate(candidate.template, pattern)) addEdge(from, rules[candidate.ruleIndex], 'negated', dependencyPredicateLabel(pattern));
4705
4813
  }
4706
4814
  }
4707
4815
  }
@@ -4717,7 +4825,7 @@
4717
4825
  const unstratifiedCycles = [];
4718
4826
  const seen = new Set();
4719
4827
  for (const edge of edges) {
4720
- if (!edge.negative) continue;
4828
+ if (!edge.negated) continue;
4721
4829
  if (edge.from === edge.to && rules[edge.from].runOnce && !rules[edge.from].headHasBlankNode) continue;
4722
4830
  if (componentOf.get(edge.from) !== componentOf.get(edge.to)) continue;
4723
4831
  const component = components[componentOf.get(edge.from)];
@@ -4740,7 +4848,10 @@
4740
4848
  positivePredicates: Array.from(rule.positivePredicates),
4741
4849
  negativePredicates: Array.from(rule.negativePredicates),
4742
4850
  runOnce: rule.runOnce,
4851
+ hasAssignment: rule.hasAssignment,
4852
+ hasTermGeneratingAssignment: rule.hasTermGeneratingAssignment,
4743
4853
  headHasBlankNode: rule.headHasBlankNode,
4854
+ createsTerms: rule.createsTerms,
4744
4855
  })),
4745
4856
  edges,
4746
4857
  components: components.map((component) => component.map((ruleIndex) => rules[ruleIndex].name)),
@@ -4910,7 +5021,14 @@
4910
5021
  function recursiveRuleIndexes(dependency) {
4911
5022
  const out = new Set();
4912
5023
  const ruleByName = new Map(dependency.rules.map((rule) => [rule.name, rule]));
4913
- const ruleByIndex = new Map(dependency.rules.map((rule) => [rule.index, rule]));
5024
+ const componentOf = new Map();
5025
+
5026
+ dependency.components.forEach((component, componentIndex) => {
5027
+ for (const name of component) {
5028
+ const rule = ruleByName.get(name);
5029
+ if (rule) componentOf.set(rule.index, componentIndex);
5030
+ }
5031
+ });
4914
5032
 
4915
5033
  for (const component of dependency.components) {
4916
5034
  if (component.length <= 1) continue;
@@ -4921,9 +5039,9 @@
4921
5039
  }
4922
5040
 
4923
5041
  for (const edge of dependency.edges) {
4924
- const rule = ruleByIndex.get(edge.from);
4925
- if (edge.from === edge.to && edge.negative && rule && rule.runOnce && !rule.headHasBlankNode) continue;
4926
- out.add(edge.from);
5042
+ const rule = (dependency.rules || []).find((candidate) => candidate.index === edge.from);
5043
+ if (edge.from === edge.to && edge.negated && rule && rule.runOnce && !rule.headHasBlankNode) continue;
5044
+ if (edge.from === edge.to || componentOf.get(edge.from) === componentOf.get(edge.to)) out.add(edge.from);
4927
5045
  }
4928
5046
  return out;
4929
5047
  }
@@ -5196,7 +5314,26 @@
5196
5314
  }
5197
5315
 
5198
5316
  function ruleHasAssignment(rule, options = {}) {
5199
- return !!options.shacl12Conformance && (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5317
+ return (rule.body || []).some((clause) => clause.type === 'set' || clause.type === 'bind');
5318
+ }
5319
+
5320
+ function ruleHasTermGeneratingAssignment(rule, options = {}) {
5321
+ return (rule.body || []).some((clause) => (clause.type === 'set' || clause.type === 'bind') && assignmentMayCreateNewTerm(clause.expr));
5322
+ }
5323
+
5324
+ function ruleCreatesTerms(rule, options = {}) {
5325
+ return ruleHeadHasBlankNode(rule) || ruleHasTermGeneratingAssignment(rule, options);
5326
+ }
5327
+
5328
+ function assignmentMayCreateNewTerm(expr) {
5329
+ // Simple aliases and constants are not term generators: they select from a
5330
+ // fixed term or from already-bound terms. Expressions that compute values
5331
+ // through operators or functions may create an unbounded sequence when used
5332
+ // recursively, e.g. SET(?v1 := ?v + 1), so they remain part of the strict
5333
+ // recursive-new-terms check.
5334
+ if (!expr) return false;
5335
+ if (expr.type === 'var' || expr.type === 'term' || expr.type === 'literal') return false;
5336
+ return true;
5200
5337
  }
5201
5338
 
5202
5339
  function ruleHeadHasBlankNode(rule) {
@@ -7,6 +7,6 @@ DATA {
7
7
 
8
8
  RULE { ?x :grade ?grade } WHERE {
9
9
  ?x :score ?score .
10
- FILTER(?score >= 5) .
10
+ FILTER(?score >= 5)
11
11
  SET(?grade := concat("pass-", str(?score)))
12
12
  }
@@ -15,6 +15,6 @@ RULE {
15
15
  } WHERE {
16
16
  ?person :name ?name ;
17
17
  :age ?age .
18
- FILTER(datatype(?age) = xsd:integer && ?age >= 18 && lang(?name) = "en") .
18
+ FILTER(datatype(?age) = xsd:integer && ?age >= 18 && lang(?name) = "en")
19
19
  SET(?slug := REPLACE(LCASE(STR(?name)), " ", "-"))
20
20
  }
@@ -1,3 +1,3 @@
1
- _:bsh a :BritishShortHair .
2
- _:cat a :Cat .
1
+ _:sk_319b8a2f709c2e2189803dc77b3d3e85 a :BritishShortHair .
2
+ _:sk_bb32b39908778273d11a8e49e72b9547 a :Cat .
3
3
  :test :is true .