eyeprolog 1.3.21 → 1.3.23

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.
@@ -0,0 +1,270 @@
1
+ # ODRL policy reasoning
2
+
3
+ ## One reasoner, many kinds of policy questions
4
+
5
+ A policy system rarely needs to answer only one question.
6
+
7
+ It may need to decide whether an action is allowed, detect contradictory rules,
8
+ compare a specific rule with a more general one, or admit that the available
9
+ rules do not determine a unique answer.
10
+
11
+ The `odrl-policy-reasoning.pl` example shows how those questions can live in
12
+ one executable logical model.
13
+
14
+ [Run the example in the EyeProlog playground](https://eyereasoner.github.io/eyeprolog/playground#example=odrl-policy-reasoning)
15
+
16
+ ---
17
+
18
+ # The problem is bigger than permit or deny
19
+
20
+ A realistic policy engine may be asked:
21
+
22
+ - **Enforcement:** may Alice print this report?
23
+ - **Conditions:** does the permission apply only for research use?
24
+ - **Duties:** is attribution required before distribution is allowed?
25
+ - **Conflicts:** what if one rule permits an action and another prohibits it?
26
+ - **Subsumption:** does a broad policy already cover a narrower policy?
27
+ - **Incomplete information:** can the rules justify a definite answer at all?
28
+
29
+ These are different questions, but they are questions about the same policy
30
+ model.
31
+
32
+ ---
33
+
34
+ # One small policy world
35
+
36
+ The example contains:
37
+
38
+ - parties such as `alice`, `bob`, `analysts`, and `anyone`;
39
+ - assets such as reports and datasets;
40
+ - actions such as `use`, `display`, `print`, `read`, and `aggregate`;
41
+ - permissions and prohibitions;
42
+ - constraints and duties;
43
+ - requests asking whether a concrete action may be performed.
44
+
45
+ For example, a policy can permit analysts to `use` reports while separately
46
+ prohibiting Alice from `print`ing one particular report.
47
+
48
+ That is enough to create a genuine policy conflict.
49
+
50
+ ---
51
+
52
+ # Actions are related, not isolated
53
+
54
+ Policy actions can stand in several useful relationships.
55
+
56
+ ```prolog
57
+ broader(use, present).
58
+ broader(present, display).
59
+ broader(use, print).
60
+
61
+ requires(aggregate, read).
62
+ ```
63
+
64
+ So a rule about `use` can also be relevant to a request to `display` or
65
+ `print`, while `aggregate` may depend on `read` even though neither action is a
66
+ specialization of the other.
67
+
68
+ The example can therefore answer six kinds of action-matching question:
69
+
70
+ `exact`, `broader`, `narrower`, `required`, `requiring`, and `no_match`.
71
+
72
+ ---
73
+
74
+ # Rules can be active, inactive, or irrelevant
75
+
76
+ A rule does not automatically apply just because its action looks similar to
77
+ the requested action.
78
+
79
+ The reasoner also checks the party, asset, constraints, and duties.
80
+
81
+ For example:
82
+
83
+ ```prolog
84
+ permission(context_policy, research_use, analysts, use, reports).
85
+ constraint(research_use, purpose, research).
86
+ ```
87
+
88
+ A research request can activate this permission. A commercial request reaches
89
+ the same rule but fails its constraint, so the rule is **inactive**. A request
90
+ for an unrelated action is simply **not applicable**.
91
+
92
+ This distinction matters: "the rule applies but its condition failed" is not
93
+ the same as "this rule has nothing to say about the request".
94
+
95
+ ---
96
+
97
+ # Enforcement combines the applicable rules
98
+
99
+ Once individual rules have been evaluated, the policy can answer the practical
100
+ question: what should the system do?
101
+
102
+ The example demonstrates:
103
+
104
+ - an active permission becoming `permit`;
105
+ - an active prohibition becoming `deny`;
106
+ - a failed duty or constraint becoming `deny` in the closed evaluator;
107
+ - an unmatched request being permitted by an `open` evaluator;
108
+ - the same unmatched request being denied by `closed` or `default` behaviour.
109
+
110
+ So enforcement is built from the same rule facts used by the other reasoning
111
+ questions rather than being a separate policy representation.
112
+
113
+ ---
114
+
115
+ # Conflicting rules need a policy decision
116
+
117
+ Suppose a request matches both a permission and a prohibition.
118
+
119
+ The example runs the same kind of conflict under all three ODRL conflict
120
+ strategies:
121
+
122
+ - `perm` — the permission wins;
123
+ - `prohibit` — the prohibition wins;
124
+ - `invalid` — the conflict invalidates the result.
125
+
126
+ This keeps **detecting a conflict** separate from **deciding what a policy does
127
+ with that conflict**.
128
+
129
+ That separation is useful because different policies may deliberately resolve
130
+ the same logical conflict in different ways.
131
+
132
+ ---
133
+
134
+ # Conflicts can be indirect
135
+
136
+ Two rules do not need to mention exactly the same action to interfere with one
137
+ another.
138
+
139
+ The example detects three forms:
140
+
141
+ 1. **Exact conflict** — permission and prohibition concern the same action.
142
+ 2. **Subsumption conflict** — a broad permission such as `use` overlaps a
143
+ narrower prohibition such as `print`.
144
+ 3. **Dependency conflict** — a permitted action such as `aggregate` requires
145
+ another action such as `read`, while `read` is prohibited.
146
+
147
+ This is why an action model is important: looking only for identical action
148
+ names would miss meaningful policy conflicts.
149
+
150
+ ---
151
+
152
+ # Subsumption asks "does this already cover that?"
153
+
154
+ Subsumption turns policy comparison into another executable query.
155
+
156
+ At the action level:
157
+
158
+ ```prolog
159
+ ?- action_subsumes(use, display).
160
+ true.
161
+ ```
162
+
163
+ At the rule level, the comparison also takes parties, assets, constraints, and
164
+ duties into account.
165
+
166
+ At the policy level, the reasoner asks whether every rule in a more specific
167
+ policy is covered by some rule in the more general policy.
168
+
169
+ This supports questions such as:
170
+
171
+ > Does policy A already express everything required by policy B?
172
+
173
+ The example contains both positive and negative cases for action, rule, and
174
+ whole-policy subsumption.
175
+
176
+ ---
177
+
178
+ # Sometimes "yes or no" is the wrong choice
179
+
180
+ Rule systems can contain recursion through negation.
181
+
182
+ In simplified form:
183
+
184
+ ```prolog
185
+ permission(X) :- tnot(prohibition(X)).
186
+ prohibition(X) :- tnot(permission(X)).
187
+ ```
188
+
189
+ For a request caught in this negative cycle, arbitrarily choosing one side
190
+ would invent information that the rules do not justify.
191
+
192
+ EyeProlog uses **Well-Founded Semantics (WFS)**, which has three truth states:
193
+
194
+ - **true** — the claim can be established;
195
+ - **false** — its negation can be established;
196
+ - **undefined** — neither side is justified because the reasoning is cyclic.
197
+
198
+ ---
199
+
200
+ # Undefined is useful information
201
+
202
+ The example deliberately asks three WFS questions:
203
+
204
+ ```text
205
+ clear_permission -> true
206
+ absent_permission -> false
207
+ negative_cycle -> undefined
208
+ ```
209
+
210
+ `undefined` is not an error and it is not a random third answer.
211
+
212
+ It tells the application that the policy knowledge itself does not determine a
213
+ stable true/false conclusion.
214
+
215
+ An enforcement layer can then choose how to handle that uncertainty — for
216
+ example by denying conservatively, asking for more information, or escalating
217
+ for review — without corrupting the logical result.
218
+
219
+ ---
220
+
221
+ # Why put these questions in one reasoner?
222
+
223
+ A mixed *reasoning* approach can still have a single declarative home.
224
+
225
+ The example uses different logical capabilities where they fit:
226
+
227
+ - hierarchy and transitive reasoning for action relationships;
228
+ - ordinary rules for applicability and enforcement;
229
+ - structural reasoning for conflict detection and subsumption;
230
+ - negation and WFS for recursive defaults and incomplete conclusions.
231
+
232
+ They are not forced into one algorithm. They coexist behind one language, one
233
+ knowledge model, and one query mechanism.
234
+
235
+ That is the sense in which EyeProlog can be **"a reasoner to run them all."**
236
+
237
+ ---
238
+
239
+ # What this example is — and is not
240
+
241
+ `odrl-policy-reasoning.pl` demonstrates the **reasoning layer** in plain
242
+ EyeProlog.
243
+
244
+ It is not intended to be a complete ODRL JSON-LD parser or a replacement for
245
+ the full ODRL information model. Other repository examples demonstrate RDF and
246
+ ODRL data handling.
247
+
248
+ Its purpose is narrower and practical: show that enforcement, conflict
249
+ analysis, subsumption, dependencies, constraints, duties, and three-valued WFS
250
+ reasoning can be queried coherently in one executable model.
251
+
252
+ ---
253
+
254
+ # Try it yourself
255
+
256
+ Run the example in the browser:
257
+
258
+ **https://eyereasoner.github.io/eyeprolog/playground#example=odrl-policy-reasoning**
259
+
260
+ Then change one fact at a time:
261
+
262
+ - fulfil or remove a duty;
263
+ - change `research` to `commercial`;
264
+ - add or remove an action hierarchy edge;
265
+ - introduce a new prohibition;
266
+ - change the policy conflict strategy;
267
+ - create or break a negative cycle.
268
+
269
+ Then observe which enforcement, conflict, subsumption, and WFS answers change
270
+ together.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.21",
6
+ "version": "1.3.23",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -1371,6 +1371,7 @@ function writeBuiltin(mode) {
1371
1371
  solver.io.writeUnit(stream, formatTermForWrite(goal.args[goal.arity - 1], env, {
1372
1372
  ...options,
1373
1373
  generateVariableNames: true,
1374
+ variableNameState: solver.writeVariableState,
1374
1375
  operators: solver.program.operators.values(),
1375
1376
  }));
1376
1377
  yield env;
@@ -1388,6 +1389,7 @@ function* writeTermBuiltin({ solver, goal, env }) {
1388
1389
  solver.io.writeUnit(stream, formatTermForWrite(goal.args[goal.arity - 2], env, {
1389
1390
  ...options,
1390
1391
  generateVariableNames: true,
1392
+ variableNameState: solver.writeVariableState,
1391
1393
  operators: solver.program.operators.values(),
1392
1394
  }));
1393
1395
  yield env;
package/src/solver.js CHANGED
@@ -116,6 +116,11 @@ export class Solver {
116
116
  }
117
117
  }
118
118
  this.io = options.io ?? new StreamManager(options.ioOptions);
119
+ // Keep generated write-variable names stable for the lifetime of one
120
+ // top-level query. Inner/meta-call solvers share this state so separate
121
+ // write/1, writeq/1, write_canonical/1, and write_term/2-3 calls can refer
122
+ // to the same logical variable by the same printed name.
123
+ this.writeVariableState = options.writeVariableState ?? { depth: 0, names: new Map(), next: 0 };
119
124
  this.solveStacks = [];
120
125
  this.active = [];
121
126
  this.cutEpoch = 0;
@@ -163,6 +168,7 @@ export class Solver {
163
168
  io: this.io,
164
169
  innerTableScopes: this.innerTableScopes,
165
170
  inferenceObservation: this.inferenceObservation,
171
+ writeVariableState: this.writeVariableState,
166
172
  skipListTailTabling: options.skipListTailTabling ?? this.skipListTailTabling,
167
173
  });
168
174
  if (options.tableScope != null) {
@@ -279,6 +285,13 @@ export class Solver {
279
285
  if (!Array.isArray(goals)) goals = [goals];
280
286
  env.setOccursCheckHandler(this.occursCheckHandler);
281
287
 
288
+ const writeVariableState = this.writeVariableState;
289
+ if (writeVariableState.depth === 0) {
290
+ writeVariableState.names.clear();
291
+ writeVariableState.next = 0;
292
+ }
293
+ writeVariableState.depth++;
294
+
282
295
  const savedActive = this.active;
283
296
  let registeredStack = null;
284
297
  try {
@@ -651,6 +664,11 @@ export class Solver {
651
664
  const stackIndex = this.solveStacks.indexOf(registeredStack);
652
665
  if (stackIndex >= 0) this.solveStacks.splice(stackIndex, 1);
653
666
  this.active = savedActive;
667
+ writeVariableState.depth = Math.max(0, writeVariableState.depth - 1);
668
+ if (writeVariableState.depth === 0) {
669
+ writeVariableState.names.clear();
670
+ writeVariableState.next = 0;
671
+ }
654
672
  }
655
673
  }
656
674
 
package/src/write.js CHANGED
@@ -180,13 +180,14 @@ function generatedVariableName(index) {
180
180
  return suffix === 0 ? `_${letter}` : `_${letter}${suffix}`;
181
181
  }
182
182
 
183
- function printableGeneratedVariableNames(term, env, explicit) {
183
+ function printableGeneratedVariableNames(term, env, explicit, state = null) {
184
184
  const names = new Map(explicit);
185
- const used = new Set(names.values());
185
+ const sharedNames = state?.names instanceof Map ? state.names : new Map();
186
+ const used = new Set([...sharedNames.values(), ...names.values()]);
186
187
  const seenVariables = new Set();
187
188
  const seenTerms = new Set();
188
189
  const stack = [term];
189
- let generated = 0;
190
+ let generated = Number.isSafeInteger(state?.next) ? state.next : 0;
190
191
 
191
192
  while (stack.length) {
192
193
  const current = deref(stack.pop(), env);
@@ -194,8 +195,14 @@ function printableGeneratedVariableNames(term, env, explicit) {
194
195
  if (seenVariables.has(current.name)) continue;
195
196
  seenVariables.add(current.name);
196
197
  if (names.has(current.name)) continue;
198
+ const shared = sharedNames.get(current.name);
199
+ if (shared != null) {
200
+ names.set(current.name, shared);
201
+ continue;
202
+ }
197
203
  let candidate;
198
204
  do candidate = generatedVariableName(generated++); while (used.has(candidate));
205
+ sharedNames.set(current.name, candidate);
199
206
  names.set(current.name, candidate);
200
207
  used.add(candidate);
201
208
  continue;
@@ -205,6 +212,7 @@ function printableGeneratedVariableNames(term, env, explicit) {
205
212
  for (let i = current.arity - 1; i >= 0; i--) stack.push(current.args[i]);
206
213
  }
207
214
 
215
+ if (state != null) state.next = generated;
208
216
  return names;
209
217
  }
210
218
 
@@ -317,7 +325,7 @@ export function formatTermForWrite(term, env = new Env(), options = {}) {
317
325
  numbervars: options.numbervars !== false,
318
326
  doubleQuotes: options.doubleQuotes,
319
327
  variableNames: options.generateVariableNames === true
320
- ? printableGeneratedVariableNames(term, env, explicitVariableNames)
328
+ ? printableGeneratedVariableNames(term, env, explicitVariableNames, options.variableNameState)
321
329
  : printableReadVariableNames(term, env, explicitVariableNames),
322
330
  compact: options.compact === true,
323
331
  operatorAtomsAsArgs: options.operatorAtomsAsArgs === true,
@@ -2989,6 +2989,20 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
2989
2989
  assertEqual(fresh.stdout, '_A\nemit.\n', 'fresh clause variable hides its internal suffix');
2990
2990
  },
2991
2991
  },
2992
+ {
2993
+ name: 'write predicates keep generated variable names stable across calls (issue #53 comment 5356861151)',
2994
+ run: () => {
2995
+ const result = run([
2996
+ "emit :- write_term(pair(A,B), []), write(' / '), write_term(user_output,B,[]), write(' / '), writeq(A), write(' / '), write_canonical(B), nl.",
2997
+ "again :- write_canonical(B+B), nl.",
2998
+ ].join('\n'), { goals: ['emit', 'again'] });
2999
+ assertEqual(
3000
+ result.stdout,
3001
+ 'pair(_A,_B) / _B / _A / _B\nemit.\n+(_A,_A)\nagain.\n',
3002
+ 'stable names across calls and reset at the next top-level query',
3003
+ );
3004
+ },
3005
+ },
2992
3006
  {
2993
3007
  name: 'write predicates and write_term options select distinct formats',
2994
3008
  run: () => {