eyeling 1.12.13 → 1.12.15

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,870 @@
1
+ # =====================================================================================
2
+ # Notation3 (N3) + ODRL Risk Assessment + Mitigation Demo
3
+ #
4
+ # Purpose
5
+ # - Represent an agreement as an ODRL policy graph (odrl:Policy with
6
+ # odrl:permission / odrl:prohibition / odrl:duty / odrl:constraint).
7
+ # - Detect potentially unfair or risky terms using N3 rules (antecedent => consequent).
8
+ # - Infer risk triples (:Risk instances) with:
9
+ # * :score / :severity
10
+ # * :aboutClause / :issue
11
+ # * :explanation (human-readable justification)
12
+ # - Attach mitigations (:Mitigation instances) with:
13
+ # * :fixText (human-readable fix)
14
+ # * :suggestAdd { ... } (quoted N3 graph showing suggested additions)
15
+ # - Rank risks highest->lowest and print an explainable report via log:outputString.
16
+ # - Print a mitigation plan (highest risk first).
17
+ #
18
+ # Data Shape
19
+ # :Agreement
20
+ # :policyGraph { ...ODRL triples... } # quoted formula (N3 graph term)
21
+ #
22
+ # Rules
23
+ # - Use log:includes / log:notIncludes to pattern-match inside the quoted policyGraph.
24
+ # - Generate stable IRIs with log:skolem.
25
+ # - Compute scores using math:* builtins; format strings with string:format.
26
+ #
27
+ # Ranking / Reporting
28
+ # - Collect (score risk) or (score risk fixText) tuples with log:collectAllIn.
29
+ # - Sort with list:sort, reverse with list:reverse, iterate with list:iterate.
30
+ # - Emit lines with log:outputString (run Eyeling in strings-output mode).
31
+ #
32
+ # Notes
33
+ # - Heuristic compliance/risk checker for demo purposes (not legal advice).
34
+ # - Extend by adding more unfair-term rules, more needs, and more mitigations.
35
+ #
36
+ # References
37
+ # - N3 specification: https://w3c.github.io/N3/spec/
38
+ # - Eyeling builtins handbook: https://eyereasoner.github.io/eyeling/HANDBOOK#ch11
39
+ # - ODRL model/vocabulary: https://www.w3.org/TR/odrl-model/
40
+ # =====================================================================================
41
+
42
+ @prefix : <https://example.org/odrl-mitigation-demo#>.
43
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/> .
44
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
45
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
46
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
47
+ @prefix list: <http://www.w3.org/2000/10/swap/list#>.
48
+ @prefix math: <http://www.w3.org/2000/10/swap/math#>.
49
+ @prefix string:<http://www.w3.org/2000/10/swap/string#>.
50
+
51
+ # -------------------------------------------------------------
52
+ # 1) Consumer profile (needs) — tweak these to drive assessment
53
+ # -------------------------------------------------------------
54
+
55
+ :ConsumerCarol a :ConsumerProfile;
56
+ :label "Carol (example consumer)";
57
+ :hasNeed :Need_ChangeOnlyWithPriorNotice,
58
+ :Need_ReminderBeforeAutoRenew,
59
+ :Need_MinLiabilityCap,
60
+ :Need_DataPortability,
61
+ :Need_DataNotRemoved,
62
+ :Need_RefundRequired,
63
+ :Need_NoTrackingWithoutOptIn.
64
+
65
+ :Need_ChangeOnlyWithPriorNotice a :Need;
66
+ :label "Agreement may change only with prior notice";
67
+ :importance 15;
68
+ :minNoticeDays 14.
69
+
70
+ :Need_ReminderBeforeAutoRenew a :Need;
71
+ :label "Auto-renew only with prior reminder";
72
+ :importance 10;
73
+ :minReminderDays 7.
74
+
75
+ :Need_MinLiabilityCap a :Need;
76
+ :label "Liability cap must not be too low";
77
+ :importance 8;
78
+ :minLiabilityCapEuro 200.
79
+
80
+ :Need_DataPortability a :Need;
81
+ :label "Must be able to export my data";
82
+ :importance 8.
83
+
84
+ :Need_DataNotRemoved a :Need;
85
+ :label "Provider must not remove my data";
86
+ :importance 12.
87
+
88
+ :Need_RefundRequired a :Need;
89
+ :label "Fees should be refundable (at least within a cooling-off window)";
90
+ :importance 9;
91
+ :minCoolingOffDays 14.
92
+
93
+ :Need_NoTrackingWithoutOptIn a :Need;
94
+ :label "No tracking unless I opt in";
95
+ :importance 9.
96
+
97
+ # -------------------------------------------------------------
98
+ # 2) Agreement model in ODRL (quoted formula as the “document”)
99
+ # -------------------------------------------------------------
100
+
101
+ :Agreement3 a :Agreement;
102
+ :label "Example Platform Agreement (with fixes)";
103
+ :policyGraph {
104
+ :Policy3 a odrl:Policy;
105
+ odrl:permission :PermChangeTerms,
106
+ :PermAutoRenew,
107
+ :PermChargeFee,
108
+ :PermTrackUser,
109
+ :PermDeleteUserData;
110
+ odrl:prohibition :ProhibitExportData.
111
+
112
+ # Clause D1: change terms WITH notice duty, but notice is only 3 days (too short vs need 14)
113
+ :PermChangeTerms a odrl:Permission;
114
+ odrl:action :changeTerms;
115
+ odrl:target :AgreementText;
116
+ odrl:duty :DutyNotify;
117
+ :clause :ClauseD1.
118
+
119
+ :DutyNotify a odrl:Duty;
120
+ odrl:action :notifyPriorNotice;
121
+ odrl:constraint :C_noticeDays.
122
+
123
+ :C_noticeDays a odrl:Constraint;
124
+ odrl:leftOperand :noticeDays;
125
+ odrl:operator odrl:gteq;
126
+ odrl:rightOperand 3.
127
+
128
+ :ClauseD1 a :Clause;
129
+ :clauseId "D1";
130
+ :text "We may change these terms with notice. Notice may be as short as 3 days.".
131
+
132
+ # Clause D2: auto-renewal, but NO reminder duty
133
+ :PermAutoRenew a odrl:Permission;
134
+ odrl:action :autoRenewSubscription;
135
+ odrl:target :Subscription;
136
+ :clause :ClauseD2.
137
+
138
+ :ClauseD2 a :Clause;
139
+ :clauseId "D2";
140
+ :text "Your subscription renews automatically unless you cancel.".
141
+
142
+ # Clause D3: fees are non-refundable
143
+ :PermChargeFee a odrl:Permission;
144
+ odrl:action :chargeFee;
145
+ odrl:target :SubscriptionFee;
146
+ odrl:constraint :C_nonRefundable;
147
+ :clause :ClauseD3.
148
+
149
+ :C_nonRefundable a odrl:Constraint;
150
+ odrl:leftOperand :refundAllowed;
151
+ odrl:operator odrl:eq;
152
+ odrl:rightOperand false.
153
+
154
+ :ClauseD3 a :Clause;
155
+ :clauseId "D3";
156
+ :text "All fees are non-refundable.".
157
+
158
+ # Clause D4: provider may track user, without opt-in
159
+ :PermTrackUser a odrl:Permission;
160
+ odrl:action :trackUser;
161
+ odrl:target :User;
162
+ :clause :ClauseD4.
163
+
164
+ :ClauseD4 a :Clause;
165
+ :clauseId "D4";
166
+ :text "We may track your activity to improve services.".
167
+
168
+ # Clause D5: prohibits exporting data (no portability)
169
+ :ProhibitExportData a odrl:Prohibition;
170
+ odrl:action :exportData;
171
+ odrl:target :UserData;
172
+ :clause :ClauseD5.
173
+
174
+ :ClauseD5 a :Clause;
175
+ :clauseId "D5";
176
+ :text "You may not export or download your data from the service.".
177
+
178
+ # Clause D6: provider may delete user data
179
+ :PermDeleteUserData a odrl:Permission;
180
+ odrl:action :deleteUserData;
181
+ odrl:target :UserData;
182
+ :clause :ClauseD6.
183
+
184
+ :ClauseD6 a :Clause;
185
+ :clauseId "D6";
186
+ :text "We may delete your data at our discretion.".
187
+
188
+ # Clause D7: very low liability cap (20 EUR)
189
+ :PermAutoRenew odrl:constraint :C_liabilityCap.
190
+ :C_liabilityCap a odrl:Constraint;
191
+ odrl:leftOperand :liabilityCapEuro;
192
+ odrl:operator odrl:eq;
193
+ odrl:rightOperand 20.
194
+ }.
195
+
196
+ # ------------------------------------------------------------------
197
+ # 3) Risk inference rules (unfair-term heuristics + needs-aware)
198
+ #
199
+ # Output model:
200
+ # ?risk a :Risk ;
201
+ # :aboutAgreement, :forProfile, :aboutClause, :issue, :rawScore,
202
+ # :score, :severity, :explanation, ...
203
+ # ------------------------------------------------------------------
204
+
205
+ # Helper: deterministic risk IRIs
206
+ # (groundTerm) log:skolem ?iri
207
+
208
+ # ---------------------------------------------------------
209
+ # R1 — Notice period too short for changes to the agreement
210
+ # ---------------------------------------------------------
211
+ {
212
+ ?profile a :ConsumerProfile; :hasNeed :Need_ChangeOnlyWithPriorNotice.
213
+ :Need_ChangeOnlyWithPriorNotice :importance ?w; :minNoticeDays ?minDays.
214
+
215
+ ?agreement a :Agreement; :policyGraph ?G.
216
+ ?G log:includes {
217
+ ?P a odrl:Policy.
218
+ ?P odrl:permission ?perm.
219
+ ?perm odrl:action :changeTerms.
220
+ ?perm :clause ?clause.
221
+ ?clause :clauseId ?cid; :text ?txt.
222
+
223
+ ?perm odrl:duty ?d.
224
+ ?d odrl:action :notifyPriorNotice.
225
+ ?d odrl:constraint ?c.
226
+ ?c odrl:leftOperand :noticeDays;
227
+ odrl:operator odrl:gteq;
228
+ odrl:rightOperand ?days.
229
+ }.
230
+
231
+ ?days math:lessThan ?minDays.
232
+
233
+ (70 ?w) math:sum ?raw.
234
+ ( ?agreement ?profile :NoticeTooShort ?clause ) log:skolem ?risk.
235
+
236
+ ( "This clause is risky because the notice period (%s days) is below the consumer requirement (%s days). Clause %s: %s"
237
+ ?days ?minDays ?cid ?txt
238
+ ) string:format ?why.
239
+ }
240
+ =>
241
+ {
242
+ ?risk a :Risk;
243
+ :aboutAgreement ?agreement;
244
+ :forProfile ?profile;
245
+ :aboutClause ?clause;
246
+ :clauseId ?cid;
247
+ :issue :NoticeTooShort;
248
+ :baseScore 70;
249
+ :needWeight ?w;
250
+ :rawScore ?raw;
251
+ :title "Notice period too short";
252
+ :explanation ?why;
253
+ :violatesNeed :Need_ChangeOnlyWithPriorNotice.
254
+ }.
255
+
256
+ # ---------------------------------------
257
+ # R2 — Auto-renewal without reminder duty
258
+ # ---------------------------------------
259
+ {
260
+ ?profile a :ConsumerProfile; :hasNeed :Need_ReminderBeforeAutoRenew.
261
+ :Need_ReminderBeforeAutoRenew :importance ?w; :minReminderDays ?minDays.
262
+
263
+ ?agreement a :Agreement; :policyGraph ?G.
264
+ ?G log:includes {
265
+ ?P a odrl:Policy.
266
+ ?P odrl:permission ?perm.
267
+ ?perm odrl:action :autoRenewSubscription.
268
+ ?perm :clause ?clause.
269
+ ?clause :clauseId ?cid; :text ?txt.
270
+ }.
271
+
272
+ ?G log:notIncludes {
273
+ ?perm odrl:duty ?d.
274
+ ?d odrl:action :sendRenewalReminder.
275
+ }.
276
+
277
+ (75 ?w) math:sum ?raw.
278
+ ( ?agreement ?profile :AutoRenewNoReminder ?clause ) log:skolem ?risk.
279
+
280
+ ( "This clause is risky because it allows auto-renewal without a reminder. Consumer needs at least %s days reminder. Clause %s: %s"
281
+ ?minDays ?cid ?txt
282
+ ) string:format ?why.
283
+ }
284
+ =>
285
+ {
286
+ ?risk a :Risk;
287
+ :aboutAgreement ?agreement;
288
+ :forProfile ?profile;
289
+ :aboutClause ?clause;
290
+ :clauseId ?cid;
291
+ :issue :AutoRenewNoReminder;
292
+ :baseScore 75;
293
+ :needWeight ?w;
294
+ :rawScore ?raw;
295
+ :title "Auto-renewal without reminder";
296
+ :explanation ?why;
297
+ :violatesNeed :Need_ReminderBeforeAutoRenew.
298
+ }.
299
+
300
+ # ------------------------
301
+ # R3 — Non-refundable fees
302
+ # ------------------------
303
+ {
304
+ ?profile a :ConsumerProfile; :hasNeed :Need_RefundRequired.
305
+ :Need_RefundRequired :importance ?w; :minCoolingOffDays ?minDays.
306
+
307
+ ?agreement a :Agreement; :policyGraph ?G.
308
+ ?G log:includes {
309
+ ?P a odrl:Policy.
310
+ ?P odrl:permission ?perm.
311
+ ?perm odrl:action :chargeFee.
312
+ ?perm odrl:constraint ?c.
313
+ ?c odrl:leftOperand :refundAllowed;
314
+ odrl:operator odrl:eq;
315
+ odrl:rightOperand false.
316
+ ?perm :clause ?clause.
317
+ ?clause :clauseId ?cid; :text ?txt.
318
+ }.
319
+
320
+ (70 ?w) math:sum ?raw.
321
+ ( ?agreement ?profile :NonRefundableFee ?clause ) log:skolem ?risk.
322
+
323
+ ( "This clause is risky because it declares fees non-refundable, conflicting with a refund/cooling-off expectation (>= %s days). Clause %s: %s"
324
+ ?minDays ?cid ?txt
325
+ ) string:format ?why.
326
+ }
327
+ =>
328
+ {
329
+ ?risk a :Risk;
330
+ :aboutAgreement ?agreement;
331
+ :forProfile ?profile;
332
+ :aboutClause ?clause;
333
+ :clauseId ?cid;
334
+ :issue :NonRefundableFee;
335
+ :baseScore 70;
336
+ :needWeight ?w;
337
+ :rawScore ?raw;
338
+ :title "Non-refundable fees";
339
+ :explanation ?why;
340
+ :violatesNeed :Need_RefundRequired.
341
+ }.
342
+
343
+ # ------------------------------------
344
+ # R4 — Tracking without opt-in consent
345
+ # ------------------------------------
346
+ {
347
+ ?profile a :ConsumerProfile; :hasNeed :Need_NoTrackingWithoutOptIn.
348
+ :Need_NoTrackingWithoutOptIn :importance ?w.
349
+
350
+ ?agreement a :Agreement; :policyGraph ?G.
351
+ ?G log:includes {
352
+ ?P a odrl:Policy.
353
+ ?P odrl:permission ?perm.
354
+ ?perm odrl:action :trackUser.
355
+ ?perm :clause ?clause.
356
+ ?clause :clauseId ?cid; :text ?txt.
357
+ }.
358
+
359
+ ?G log:notIncludes {
360
+ ?perm odrl:constraint ?c.
361
+ ?c odrl:leftOperand :optInConsent;
362
+ odrl:operator odrl:eq;
363
+ odrl:rightOperand true.
364
+ }.
365
+
366
+ (80 ?w) math:sum ?raw.
367
+ ( ?agreement ?profile :TrackingNoOptIn ?clause ) log:skolem ?risk.
368
+
369
+ ( "This clause is risky because it permits tracking without explicit opt-in consent. Clause %s: %s"
370
+ ?cid ?txt
371
+ ) string:format ?why.
372
+ }
373
+ =>
374
+ {
375
+ ?risk a :Risk;
376
+ :aboutAgreement ?agreement;
377
+ :forProfile ?profile;
378
+ :aboutClause ?clause;
379
+ :clauseId ?cid;
380
+ :issue :TrackingNoOptIn;
381
+ :baseScore 80;
382
+ :needWeight ?w;
383
+ :rawScore ?raw;
384
+ :title "Tracking without opt-in";
385
+ :explanation ?why;
386
+ :violatesNeed :Need_NoTrackingWithoutOptIn.
387
+ }.
388
+
389
+ # ------------------------------------------------
390
+ # R5 — Prohibition of data export (no portability)
391
+ # ------------------------------------------------
392
+ {
393
+ ?profile a :ConsumerProfile; :hasNeed :Need_DataPortability.
394
+ :Need_DataPortability :importance ?w.
395
+
396
+ ?agreement a :Agreement; :policyGraph ?G.
397
+ ?G log:includes {
398
+ ?P a odrl:Policy.
399
+ ?P odrl:prohibition ?proh.
400
+ ?proh odrl:action :exportData.
401
+ ?proh :clause ?clause.
402
+ ?clause :clauseId ?cid; :text ?txt.
403
+ }.
404
+
405
+ (85 ?w) math:sum ?raw.
406
+ ( ?agreement ?profile :NoDataExport ?clause ) log:skolem ?risk.
407
+
408
+ ( "This clause is risky because it prohibits exporting data, undermining portability. Clause %s: %s"
409
+ ?cid ?txt
410
+ ) string:format ?why.
411
+ }
412
+ =>
413
+ {
414
+ ?risk a :Risk;
415
+ :aboutAgreement ?agreement;
416
+ :forProfile ?profile;
417
+ :aboutClause ?clause;
418
+ :clauseId ?cid;
419
+ :issue :NoDataExport;
420
+ :baseScore 85;
421
+ :needWeight ?w;
422
+ :rawScore ?raw;
423
+ :title "No data export / portability";
424
+ :explanation ?why;
425
+ :violatesNeed :Need_DataPortability.
426
+ }.
427
+
428
+ # ----------------------------------
429
+ # R6 — Provider may delete user data
430
+ # ----------------------------------
431
+ {
432
+ ?profile a :ConsumerProfile; :hasNeed :Need_DataNotRemoved.
433
+ :Need_DataNotRemoved :importance ?w.
434
+
435
+ ?agreement a :Agreement; :policyGraph ?G.
436
+ ?G log:includes {
437
+ ?P a odrl:Policy.
438
+ ?P odrl:permission ?perm.
439
+ ?perm odrl:action :deleteUserData.
440
+ ?perm :clause ?clause.
441
+ ?clause :clauseId ?cid; :text ?txt.
442
+ }.
443
+
444
+ (90 ?w) math:sum ?raw.
445
+ ( ?agreement ?profile :ProviderMayDeleteData ?clause ) log:skolem ?risk.
446
+
447
+ ( "This clause is risky because it allows the provider to delete the consumer’s data. Clause %s: %s"
448
+ ?cid ?txt
449
+ ) string:format ?why.
450
+ }
451
+ =>
452
+ {
453
+ ?risk a :Risk;
454
+ :aboutAgreement ?agreement;
455
+ :forProfile ?profile;
456
+ :aboutClause ?clause;
457
+ :clauseId ?cid;
458
+ :issue :ProviderMayDeleteData;
459
+ :baseScore 90;
460
+ :needWeight ?w;
461
+ :rawScore ?raw;
462
+ :title "Provider can delete user data";
463
+ :explanation ?why;
464
+ :violatesNeed :Need_DataNotRemoved.
465
+ }.
466
+
467
+ # --------------------------
468
+ # R7 — Liability cap too low
469
+ # --------------------------
470
+ {
471
+ ?profile a :ConsumerProfile; :hasNeed :Need_MinLiabilityCap.
472
+ :Need_MinLiabilityCap :importance ?w; :minLiabilityCapEuro ?minCap.
473
+
474
+ ?agreement a :Agreement; :policyGraph ?G.
475
+ ?G log:includes {
476
+ ?P a odrl:Policy.
477
+ ?P odrl:permission ?perm.
478
+ ?perm odrl:action :autoRenewSubscription.
479
+ ?perm odrl:constraint ?c.
480
+ ?c odrl:leftOperand :liabilityCapEuro;
481
+ odrl:operator odrl:eq;
482
+ odrl:rightOperand ?cap.
483
+ ?perm :clause ?clause.
484
+ ?clause :clauseId ?cid; :text ?txt.
485
+ }.
486
+
487
+ ?cap math:lessThan ?minCap.
488
+
489
+ (65 ?w) math:sum ?raw.
490
+ ( ?agreement ?profile :LiabilityCapTooLow ?clause ) log:skolem ?risk.
491
+
492
+ ( "This clause is risky because the liability cap (%s EUR) is below the consumer minimum (%s EUR). Clause %s: %s"
493
+ ?cap ?minCap ?cid ?txt
494
+ ) string:format ?why.
495
+ }
496
+ =>
497
+ {
498
+ ?risk a :Risk;
499
+ :aboutAgreement ?agreement;
500
+ :forProfile ?profile;
501
+ :aboutClause ?clause;
502
+ :clauseId ?cid;
503
+ :issue :LiabilityCapTooLow;
504
+ :baseScore 65;
505
+ :needWeight ?w;
506
+ :rawScore ?raw;
507
+ :title "Liability cap too low";
508
+ :explanation ?why;
509
+ :violatesNeed :Need_MinLiabilityCap.
510
+ }.
511
+
512
+ # --------------------------------------------------------
513
+ # 4) Mitigation inference rules (attach recommended fixes)
514
+ # --------------------------------------------------------
515
+
516
+ # Fix for :NoticeTooShort — raise notice days to meet requirement
517
+ {
518
+ ?risk a :Risk;
519
+ :issue :NoticeTooShort;
520
+ :aboutAgreement ?agreement;
521
+ :forProfile ?profile.
522
+
523
+ :Need_ChangeOnlyWithPriorNotice :minNoticeDays ?minDays.
524
+
525
+ ?agreement :policyGraph ?G.
526
+ ?G log:includes {
527
+ ?perm odrl:action :changeTerms.
528
+ }.
529
+
530
+ ( ?risk :Mitigation ) log:skolem ?fix.
531
+
532
+ ( "Suggested fix: ensure prior-notice duty specifies noticeDays >= %s."
533
+ ?minDays
534
+ ) string:format ?fixText.
535
+ }
536
+ =>
537
+ {
538
+ ?risk :hasMitigation ?fix.
539
+ ?fix a :Mitigation;
540
+ :forRisk ?risk;
541
+ :fixText ?fixText;
542
+ :suggestAdd {
543
+ ?perm odrl:duty [
544
+ a odrl:Duty;
545
+ odrl:action :notifyPriorNotice;
546
+ odrl:constraint [
547
+ a odrl:Constraint;
548
+ odrl:leftOperand :noticeDays;
549
+ odrl:operator odrl:gteq;
550
+ odrl:rightOperand ?minDays
551
+ ]
552
+ ].
553
+ }.
554
+ }.
555
+
556
+ # Fix for :AutoRenewNoReminder — add renewal reminder duty
557
+ {
558
+ ?risk a :Risk;
559
+ :issue :AutoRenewNoReminder;
560
+ :aboutAgreement ?agreement;
561
+ :forProfile ?profile.
562
+
563
+ :Need_ReminderBeforeAutoRenew :minReminderDays ?minDays.
564
+
565
+ ?agreement :policyGraph ?G.
566
+ ?G log:includes { ?perm odrl:action :autoRenewSubscription. }.
567
+
568
+ ( ?risk :Mitigation ) log:skolem ?fix.
569
+
570
+ ( "Suggested fix: add a reminder duty for auto-renewal with reminderDays >= %s."
571
+ ?minDays
572
+ ) string:format ?fixText.
573
+ }
574
+ =>
575
+ {
576
+ ?risk :hasMitigation ?fix.
577
+ ?fix a :Mitigation;
578
+ :forRisk ?risk;
579
+ :fixText ?fixText;
580
+ :suggestAdd {
581
+ ?perm odrl:duty [
582
+ a odrl:Duty;
583
+ odrl:action :sendRenewalReminder;
584
+ odrl:constraint [
585
+ a odrl:Constraint;
586
+ odrl:leftOperand :reminderDays;
587
+ odrl:operator odrl:gteq;
588
+ odrl:rightOperand ?minDays
589
+ ]
590
+ ].
591
+ }.
592
+ }.
593
+
594
+ # Fix for :NonRefundableFee — allow refund (or add cooling-off window)
595
+ {
596
+ ?risk a :Risk;
597
+ :issue :NonRefundableFee;
598
+ :aboutAgreement ?agreement;
599
+ :forProfile ?profile.
600
+
601
+ :Need_RefundRequired :minCoolingOffDays ?minDays.
602
+
603
+ ?agreement :policyGraph ?G.
604
+ ?G log:includes { ?perm odrl:action :chargeFee. }.
605
+
606
+ ( ?risk :Mitigation ) log:skolem ?fix.
607
+
608
+ ( "Suggested fix: allow refunds (e.g., refundAllowed=true) or define a cooling-off period >= %s days."
609
+ ?minDays
610
+ ) string:format ?fixText.
611
+ }
612
+ =>
613
+ {
614
+ ?risk :hasMitigation ?fix.
615
+ ?fix a :Mitigation;
616
+ :forRisk ?risk;
617
+ :fixText ?fixText;
618
+ :suggestAdd {
619
+ ?perm odrl:constraint [
620
+ a odrl:Constraint;
621
+ odrl:leftOperand :refundAllowed;
622
+ odrl:operator odrl:eq;
623
+ odrl:rightOperand true
624
+ ].
625
+ ?perm odrl:constraint [
626
+ a odrl:Constraint;
627
+ odrl:leftOperand :coolingOffDays;
628
+ odrl:operator odrl:gteq;
629
+ odrl:rightOperand ?minDays
630
+ ].
631
+ }.
632
+ }.
633
+
634
+ # Fix for :TrackingNoOptIn — require explicit opt-in consent constraint
635
+ {
636
+ ?risk a :Risk;
637
+ :issue :TrackingNoOptIn;
638
+ :aboutAgreement ?agreement;
639
+ :forProfile ?profile.
640
+
641
+ ?agreement :policyGraph ?G.
642
+ ?G log:includes { ?perm odrl:action :trackUser. }.
643
+
644
+ ( ?risk :Mitigation ) log:skolem ?fix.
645
+
646
+ "Suggested fix: require opt-in consent for tracking (optInConsent=true)." log:equalTo ?fixText.
647
+ }
648
+ =>
649
+ {
650
+ ?risk :hasMitigation ?fix.
651
+ ?fix a :Mitigation;
652
+ :forRisk ?risk;
653
+ :fixText ?fixText;
654
+ :suggestAdd {
655
+ ?perm odrl:constraint [
656
+ a odrl:Constraint;
657
+ odrl:leftOperand :optInConsent;
658
+ odrl:operator odrl:eq;
659
+ odrl:rightOperand true
660
+ ].
661
+ }.
662
+ }.
663
+
664
+ # Fix for :NoDataExport — permit data export / portability
665
+ {
666
+ ?risk a :Risk;
667
+ :issue :NoDataExport;
668
+ :aboutAgreement ?agreement;
669
+ :forProfile ?profile.
670
+
671
+ ?agreement :policyGraph ?G.
672
+
673
+ ( ?risk :Mitigation ) log:skolem ?fix.
674
+
675
+ "Suggested fix: add a permission to export/download user data (data portability)." log:equalTo ?fixText.
676
+ }
677
+ =>
678
+ {
679
+ ?risk :hasMitigation ?fix.
680
+ ?fix a :Mitigation;
681
+ :forRisk ?risk;
682
+ :fixText ?fixText;
683
+ :suggestAdd {
684
+ :Policy3 odrl:permission [
685
+ a odrl:Permission;
686
+ odrl:action :exportData;
687
+ odrl:target :UserData
688
+ ].
689
+ }.
690
+ }.
691
+
692
+ # Fix for :ProviderMayDeleteData — prohibit deletion or constrain to lawful/consumer-requested cases
693
+ {
694
+ ?risk a :Risk;
695
+ :issue :ProviderMayDeleteData;
696
+ :aboutAgreement ?agreement;
697
+ :forProfile ?profile.
698
+
699
+ ?agreement :policyGraph ?G.
700
+ ?G log:includes { ?perm odrl:action :deleteUserData. }.
701
+
702
+ ( ?risk :Mitigation ) log:skolem ?fix.
703
+
704
+ "Suggested fix: remove provider discretion to delete data; allow deletion only on consumer request or legal obligation." log:equalTo ?fixText.
705
+ }
706
+ =>
707
+ {
708
+ ?risk :hasMitigation ?fix.
709
+ ?fix a :Mitigation;
710
+ :forRisk ?risk;
711
+ :fixText ?fixText;
712
+ :suggestAdd {
713
+ ?perm odrl:constraint [
714
+ a odrl:Constraint;
715
+ odrl:leftOperand :deletionGround;
716
+ odrl:operator odrl:isAnyOf;
717
+ odrl:rightOperand ( :consumerRequest :legalObligation )
718
+ ].
719
+ }.
720
+ }.
721
+
722
+ # Fix for :LiabilityCapTooLow — raise cap to meet minimum
723
+ {
724
+ ?risk a :Risk;
725
+ :issue :LiabilityCapTooLow;
726
+ :aboutAgreement ?agreement;
727
+ :forProfile ?profile.
728
+
729
+ :Need_MinLiabilityCap :minLiabilityCapEuro ?minCap.
730
+
731
+ ?agreement :policyGraph ?G.
732
+ ?G log:includes { ?perm odrl:action :autoRenewSubscription. }.
733
+
734
+ ( ?risk :Mitigation ) log:skolem ?fix.
735
+
736
+ ( "Suggested fix: raise liabilityCapEuro so it is >= %s EUR (or remove the cap where inappropriate)."
737
+ ?minCap
738
+ ) string:format ?fixText.
739
+ }
740
+ =>
741
+ {
742
+ ?risk :hasMitigation ?fix.
743
+ ?fix a :Mitigation;
744
+ :forRisk ?risk;
745
+ :fixText ?fixText;
746
+ :suggestAdd {
747
+ ?perm odrl:constraint [
748
+ a odrl:Constraint;
749
+ odrl:leftOperand :liabilityCapEuro;
750
+ odrl:operator odrl:gteq;
751
+ odrl:rightOperand ?minCap
752
+ ].
753
+ }.
754
+ }.
755
+
756
+ # ---------------------------------------------------
757
+ # 5) Normalize score (clamp at 100) + severity labels
758
+ # ---------------------------------------------------
759
+
760
+ { ?r a :Risk; :rawScore ?raw. ?raw math:greaterThan 100. } => { ?r :score 100. }.
761
+ { ?r a :Risk; :rawScore ?raw. 100 math:greaterThan ?raw. } => { ?r :score ?raw. }.
762
+ { ?r a :Risk; :rawScore ?raw. ?raw math:equalTo 100. } => { ?r :score 100. }.
763
+
764
+ { ?r a :Risk; :score ?s. ?s math:notLessThan 80. } => { ?r :severity :High. }.
765
+ { ?r a :Risk; :score ?s. ?s math:lessThan 80. ?s math:notLessThan 50. } => { ?r :severity :Medium. }.
766
+ { ?r a :Risk; :score ?s. ?s math:lessThan 50. } => { ?r :severity :Low. }.
767
+
768
+ # ----------------------------------------------------------------------------
769
+ # 6) Ranked risk list (highest->lowest) + explainable report + mitigation plan
770
+ # ----------------------------------------------------------------------------
771
+
772
+ # Header line
773
+ {
774
+ ?agreement a :Agreement; :label ?alabel.
775
+ ?profile a :ConsumerProfile; :label ?plabel.
776
+ ( "\n=== Risk report for %s (profile: %s) ===\n" ?alabel ?plabel ) string:format ?hdr.
777
+ }
778
+ =>
779
+ {
780
+ ( ?agreement ?profile 0 ) log:outputString ?hdr.
781
+ }.
782
+
783
+ # Produce ranked risk lines
784
+ {
785
+ ?agreement a :Agreement.
786
+ ?profile a :ConsumerProfile.
787
+
788
+ ( ( ?score ?risk )
789
+ { ?risk a :Risk;
790
+ :aboutAgreement ?agreement;
791
+ :forProfile ?profile;
792
+ :score ?score. }
793
+ ?pairs
794
+ ) log:collectAllIn 1.
795
+
796
+ ?pairs list:sort ?sortedAsc.
797
+ ?sortedAsc list:reverse ?sortedDesc.
798
+
799
+ ?sortedDesc list:iterate ( ?i ?pair ).
800
+ ( ?i 1 ) math:sum ?rank.
801
+
802
+ ( ?pair 0 ) list:memberAt ?score.
803
+ ( ?pair 1 ) list:memberAt ?risk.
804
+
805
+ ?risk :severity ?sev;
806
+ :clauseId ?cid;
807
+ :title ?title;
808
+ :explanation ?why.
809
+
810
+ ( "%s) score=%s (%s), clause %s — %s. %s\n"
811
+ ?rank ?score ?sev ?cid ?title ?why
812
+ ) string:format ?line.
813
+ }
814
+ =>
815
+ {
816
+ ( ?agreement ?profile ?rank ) log:outputString ?line.
817
+ }.
818
+
819
+ # Mitigation header
820
+ {
821
+ ?agreement a :Agreement.
822
+ ?profile a :ConsumerProfile.
823
+ "\n--- Suggested mitigations (highest risk first) ---\n" log:equalTo ?hdr2.
824
+ }
825
+ =>
826
+ {
827
+ ( ?agreement ?profile 999 ) log:outputString ?hdr2.
828
+ }.
829
+
830
+ # Produce ranked mitigation lines (aligned with risk ranking by score)
831
+ {
832
+ ?agreement a :Agreement.
833
+ ?profile a :ConsumerProfile.
834
+
835
+ ( ( ?score ?risk ?fixText )
836
+ {
837
+ ?risk a :Risk;
838
+ :aboutAgreement ?agreement;
839
+ :forProfile ?profile;
840
+ :score ?score;
841
+ :clauseId ?cid;
842
+ :title ?title;
843
+ :hasMitigation ?fix.
844
+ ?fix :fixText ?fixText.
845
+ }
846
+ ?triples
847
+ ) log:collectAllIn 1.
848
+
849
+ ?triples list:sort ?sortedAsc.
850
+ ?sortedAsc list:reverse ?sortedDesc.
851
+
852
+ ?sortedDesc list:iterate ( ?i ?t ).
853
+ ( ?i 1 ) math:sum ?rank.
854
+
855
+ ( ?t 0 ) list:memberAt ?score.
856
+ ( ?t 1 ) list:memberAt ?risk.
857
+ ( ?t 2 ) list:memberAt ?fixText.
858
+
859
+ ?risk :clauseId ?cid;
860
+ :title ?title.
861
+
862
+ ( "%s) clause %s — %s (score=%s). %s\n"
863
+ ?rank ?cid ?title ?score ?fixText
864
+ ) string:format ?line.
865
+ }
866
+ =>
867
+ {
868
+ ( ?agreement ?profile (1000 ?rank) ) log:outputString ?line.
869
+ }.
870
+