eyeling 1.16.4 → 1.17.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.
@@ -1,9 +1,21 @@
1
- # ==============
2
- # Control System
3
- # ==============
1
+ # =============================================================================
2
+ # Control System — ARC-style
3
+ #
4
+ # This example shows how a small control system turns sensor readings into two
5
+ # actuator commands. One command anticipates a known disturbance before it hits
6
+ # the system. The other compares the measured output with the target and then
7
+ # corrects the remaining gap. The ARC presentation makes the result easy to
8
+ # inspect by giving an Answer, a clear Reason Why, and independent Checks.
9
+ # =============================================================================
4
10
 
11
+ @prefix log: <http://www.w3.org/2000/10/swap/log#>.
5
12
  @prefix math: <http://www.w3.org/2000/10/swap/math#>.
6
- @prefix : <https://eyereasoner.github.io/eye/reasoning/cs#>.
13
+ @prefix string: <http://www.w3.org/2000/10/swap/string#>.
14
+ @prefix : <https://example.org/control-system#>.
15
+
16
+ # -----
17
+ # Facts
18
+ # -----
7
19
 
8
20
  # input
9
21
  :input1 :measurement1 (6 11).
@@ -23,55 +35,218 @@
23
35
  :output2 :measurement4 24.
24
36
  :output2 :target2 29.
25
37
 
26
- # forward rules
27
- {
28
- :input1 :measurement10 ?M1.
29
- :input2 :measurement2 true.
30
- :disturbance1 :measurement3 ?D1.
31
- (?M1 19.6) math:product ?C1. # proportial part
32
- (10 ?C2) math:exponentiation ?D1. # compensation part
33
- (?C1 ?C2) math:difference ?C. # simple feedforward control
38
+ # -------------
39
+ # control logic
40
+ # -------------
41
+
42
+ { :input1 :measurement10 ?M1.
43
+ :input2 :measurement2 true.
44
+ :disturbance1 :measurement3 ?D1.
45
+ (?M1 19.6) math:product ?C1.
46
+ (10 ?C2) math:exponentiation ?D1.
47
+ (?C1 ?C2) math:difference ?C.
34
48
  }
35
- =>
36
- {
37
- :actuator1 :control1 ?C.
38
- }.
49
+ =>
50
+ { :actuator1 :control1 ?C. }.
39
51
 
40
- {
41
- :input3 :measurement3 ?M3.
42
- :state3 :observation3 ?P3.
43
- :output2 :measurement4 ?M4.
44
- :output2 :target2 ?T2.
45
- (?T2 ?M4) math:difference ?E. # error
46
- (?P3 ?M4) math:difference ?D. # differential error
47
- (5.8 ?E) math:product ?C1. # proportial part
48
- (7.3 ?E) math:quotient ?N. # nonlinear factor
49
- (?N ?D) math:product ?C2. # nonlinear differential part
50
- (?C1 ?C2) math:sum ?C. # PND feedback control
52
+ { :input3 :measurement3 ?M3.
53
+ :state3 :observation3 ?P3.
54
+ :output2 :measurement4 ?M4.
55
+ :output2 :target2 ?T2.
56
+ (?T2 ?M4) math:difference ?E.
57
+ (?P3 ?M4) math:difference ?D.
58
+ (5.8 ?E) math:product ?C1.
59
+ (7.3 ?E) math:quotient ?N.
60
+ (?N ?D) math:product ?C2.
61
+ (?C1 ?C2) math:sum ?C.
51
62
  }
52
- =>
53
- {
54
- :actuator2 :control1 ?C.
63
+ =>
64
+ { :actuator2 :control1 ?C. }.
65
+
66
+ { ?I :measurement10 ?M. }
67
+ <=
68
+ { ?I :measurement1 (?M1 ?M2).
69
+ ?M1 math:lessThan ?M2.
70
+ (?M2 ?M1) math:difference ?M3.
71
+ (?M3 0.5) math:exponentiation ?M.
55
72
  }.
56
73
 
57
- # backward rules
58
- {
59
- ?I :measurement10 ?M.
60
- }
61
- <=
62
- {
63
- ?I :measurement1 (?M1 ?M2).
64
- ?M1 math:lessThan ?M2.
65
- (?M2 ?M1) math:difference ?M3.
66
- (?M3 0.5) math:exponentiation ?M.
74
+ { ?I :measurement10 ?M1. }
75
+ <=
76
+ { ?I :measurement1 (?M1 ?M2).
77
+ ?M1 math:notLessThan ?M2.
67
78
  }.
68
79
 
69
- {
70
- ?I :measurement10 ?M1.
80
+ # -------------------------------------------------------------------
81
+ # Explanation layer: compute an explicit why-story from the raw facts
82
+ # -------------------------------------------------------------------
83
+
84
+ { :input1 :measurement1 (?Low ?High).
85
+ ?Low math:lessThan ?High.
86
+ (?High ?Low) math:difference ?Gap.
87
+ (?Gap 0.5) math:exponentiation ?Norm.
71
88
  }
72
- <=
73
- {
74
- ?I :measurement1 (?M1 ?M2).
75
- ?M1 math:notLessThan ?M2.
76
- }.
89
+ =>
90
+ { :why :sensorTrend "rising";
91
+ :sensorLow ?Low;
92
+ :sensorHigh ?High;
93
+ :sensorGap ?Gap;
94
+ :normalizedMeasurement ?Norm. }.
95
+
96
+ { :why :normalizedMeasurement ?Norm.
97
+ (?Norm 19.6) math:product ?Prop.
98
+ :disturbance1 :measurement3 ?Dist.
99
+ (10 ?Comp) math:exponentiation ?Dist.
100
+ (?Prop ?Comp) math:difference ?Cmd.
101
+ }
102
+ =>
103
+ { :why :feedforwardProportional ?Prop;
104
+ :disturbanceMagnitude ?Dist;
105
+ :disturbanceCompensation ?Comp;
106
+ :feedforwardCommand ?Cmd. }.
107
+
108
+ { :state3 :observation3 ?Observed.
109
+ :output2 :measurement4 ?Measured.
110
+ :output2 :target2 ?Target.
111
+ (?Target ?Measured) math:difference ?Err.
112
+ (?Observed ?Measured) math:difference ?DiffErr.
113
+ (5.8 ?Err) math:product ?Prop.
114
+ (7.3 ?Err) math:quotient ?Factor.
115
+ (?Factor ?DiffErr) math:product ?DiffPart.
116
+ (?Prop ?DiffPart) math:sum ?Cmd.
117
+ }
118
+ =>
119
+ { :why :observedState ?Observed;
120
+ :measuredOutput ?Measured;
121
+ :targetOutput ?Target;
122
+ :trackingError ?Err;
123
+ :differentialError ?DiffErr;
124
+ :feedbackProportional ?Prop;
125
+ :feedbackFactor ?Factor;
126
+ :feedbackDifferential ?DiffPart;
127
+ :feedbackCommand ?Cmd. }.
128
+
129
+ { :actuator1 :control1 ?A1.
130
+ :actuator2 :control1 ?A2. }
131
+ =>
132
+ { :decision :answer "Send both actuator commands now.";
133
+ :actuator1Command ?A1;
134
+ :actuator2Command ?A2. }.
135
+
136
+ # ----------
137
+ # ARC report
138
+ # ----------
139
+
140
+ { :decision :answer ?Answer;
141
+ :actuator1Command ?A1;
142
+ :actuator2Command ?A2.
143
+ :why :sensorTrend ?Trend;
144
+ :sensorLow ?Low;
145
+ :sensorHigh ?High;
146
+ :sensorGap ?Gap;
147
+ :normalizedMeasurement ?Norm;
148
+ :feedforwardProportional ?FFProp;
149
+ :disturbanceMagnitude ?Dist;
150
+ :disturbanceCompensation ?FFComp;
151
+ :trackingError ?Err;
152
+ :differentialError ?DiffErr;
153
+ :feedbackProportional ?FBProp;
154
+ :feedbackFactor ?FBFactor;
155
+ :feedbackDifferential ?FBDiff.
156
+ (
157
+ "Control System — ARC explanation of two control signals\n\n"
158
+ "Answer\n"
159
+ ?Answer "\n"
160
+ "Actuator 1 command: " ?A1 "\n"
161
+ "Actuator 2 command: " ?A2 "\n\n"
162
+ "Reason Why\n"
163
+ "The first sensor pair is " ?Low " and " ?High ", so the reading is " ?Trend
164
+ " and the controller normalizes the gap " ?Gap " into " ?Norm ". "
165
+ "That normalized value creates a feedforward term of " ?FFProp
166
+ ", while the known disturbance " ?Dist " contributes a compensation term of " ?FFComp
167
+ ". Subtracting that compensation gives actuator 1 the command " ?A1 ". "
168
+ "For actuator 2, the target is " ?Err " units above the measured output, so the tracking error is positive. "
169
+ "The observed state is " ?DiffErr " relative units below the measured output, so the differential correction is negative. "
170
+ "That yields a proportional feedback part of " ?FBProp ", a nonlinear factor of " ?FBFactor
171
+ ", and a differential contribution of " ?FBDiff ". Together they produce actuator 2 command " ?A2 ".\n\n"
172
+ "Check\n"
173
+ "C1 OK - the first sensor pair is rising, so the normalization uses the rising-branch rule.\n"
174
+ "C2 OK - the normalized measurement is positive and smaller than the raw gap, which is consistent with a square-root normalization.\n"
175
+ "C3 OK - actuator 1 is lower than its proportional feedforward term because disturbance compensation is subtracted.\n"
176
+ "C4 OK - the target is above the measured output, so the tracking error is positive.\n"
177
+ "C5 OK - the observed state is below the measured output, so the differential error is negative.\n"
178
+ "C6 OK - actuator 2 is lower than its pure proportional term because the differential part reduces it.\n"
179
+ "C7 OK - actuator 1 matches an independently reconstructed feedforward calculation.\n"
180
+ "C8 OK - actuator 2 matches an independently reconstructed feedback calculation.\n"
181
+ "C9 OK - both actuator commands stay positive.\n"
182
+ ) string:concatenation ?Block.
183
+ }
184
+ =>
185
+ { :report log:outputString ?Block. }.
186
+
187
+ # ---------------------------------------------------------------------------
188
+ # Independent checks: validate the shape of the reasoning, not just constants
189
+ # ---------------------------------------------------------------------------
190
+
191
+ # The first sensor pair must be rising for this explanation path.
192
+ { :input1 :measurement1 (?Low ?High).
193
+ ?Low math:notLessThan ?High. }
194
+ => false.
195
+
196
+ # Square-root normalization should stay positive and smaller than the raw gap.
197
+ { :why :normalizedMeasurement ?Norm.
198
+ ?Norm math:notGreaterThan 0. }
199
+ => false.
200
+
201
+ { :why :normalizedMeasurement ?Norm;
202
+ :sensorGap ?Gap.
203
+ ?Norm math:notLessThan ?Gap. }
204
+ => false.
205
+
206
+ # Feedforward compensation must reduce the raw proportional response.
207
+ { :actuator1 :control1 ?A1.
208
+ :why :feedforwardProportional ?Prop.
209
+ ?A1 math:notLessThan ?Prop. }
210
+ => false.
211
+
212
+ # The tracking target must exceed the current measurement in this case.
213
+ { :output2 :target2 ?Target.
214
+ :output2 :measurement4 ?Measured.
215
+ ?Target math:notGreaterThan ?Measured. }
216
+ => false.
217
+
218
+ # The observed state must be below the measured output, making the differential error negative.
219
+ { :state3 :observation3 ?Observed.
220
+ :output2 :measurement4 ?Measured.
221
+ ?Observed math:notLessThan ?Measured. }
222
+ => false.
223
+
224
+ { :why :feedbackDifferential ?Diff.
225
+ ?Diff math:notLessThan 0. }
226
+ => false.
227
+
228
+ # The differential part must reduce the pure proportional feedback command.
229
+ { :actuator2 :control1 ?A2.
230
+ :why :feedbackProportional ?Prop.
231
+ ?A2 math:notLessThan ?Prop. }
232
+ => false.
233
+
234
+ # Independent reconstructions must agree with the derived actuator commands.
235
+ { :actuator1 :control1 ?A1.
236
+ :why :feedforwardCommand ?Rebuilt.
237
+ ?A1 math:notEqualTo ?Rebuilt. }
238
+ => false.
239
+
240
+ { :actuator2 :control1 ?A2.
241
+ :why :feedbackCommand ?Rebuilt.
242
+ ?A2 math:notEqualTo ?Rebuilt. }
243
+ => false.
244
+
245
+ # The resulting commands should stay positive.
246
+ { :actuator1 :control1 ?A1.
247
+ ?A1 math:notGreaterThan 0. }
248
+ => false.
77
249
 
250
+ { :actuator2 :control1 ?A2.
251
+ ?A2 math:notGreaterThan 0. }
252
+ => false.
@@ -0,0 +1,409 @@
1
+ # ==============================================================================
2
+ # Delfour — Ruben Verborgh's "Inside the Insight Economy" case.
3
+ # See https://ruben.verborgh.org/blog/2025/08/12/inside-the-insight-economy/
4
+ #
5
+ # This example shows how a person can share a useful shopping hint without
6
+ # exposing sensitive health details. A phone turns a private condition into a
7
+ # neutral, limited insight such as “prefer lower-sugar products”, attaches clear
8
+ # usage rules and an expiry time, and sends it to a store scanner. The scanner
9
+ # may use that insight to suggest a better product, but not for unrelated
10
+ # purposes such as marketing.
11
+ # ==============================================================================
12
+
13
+ @prefix : <https://example.org/delfour#> .
14
+ @prefix arc: <https://josd.github.io/arc/terms#> .
15
+ @prefix ins: <https://example.org/insight#> .
16
+ @prefix odrl: <http://www.w3.org/ns/odrl/2/> .
17
+ @prefix log: <http://www.w3.org/2000/10/swap/log#> .
18
+ @prefix math: <http://www.w3.org/2000/10/swap/math#> .
19
+ @prefix string: <http://www.w3.org/2000/10/swap/string#> .
20
+ @prefix crypto: <http://www.w3.org/2000/10/swap/crypto#> .
21
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
22
+
23
+ # -----
24
+ # Facts
25
+ # -----
26
+
27
+ :case
28
+ a arc:Case ;
29
+ :caseName "delfour" ;
30
+ arc:question "Is the Delfour self-scanner allowed to use a neutral shopping insight for shopping assistance, and if so what lower-sugar alternative should it suggest?" ;
31
+ :expectedFilesWritten 6 ;
32
+ :requestPurpose "shopping_assist" ;
33
+ :requestAction odrl:use ;
34
+ :phoneCreatedAt "2025-10-05T20:33:48.907163+00:00"^^xsd:dateTime ;
35
+ :phoneExpiresAt "2025-10-05T22:33:48.907185+00:00"^^xsd:dateTime ;
36
+ :scannerAuthAt "2025-10-05T20:35:48.907163+00:00"^^xsd:dateTime ;
37
+ :scannerDutyAt "2025-10-05T20:37:48.907163+00:00"^^xsd:dateTime ;
38
+ :filesWritten 6 ;
39
+ :auditEntries 1 .
40
+
41
+ :catalog a :Catalog .
42
+
43
+ :prod_BIS_001
44
+ a :Product ;
45
+ :productId "prod:BIS_001" ;
46
+ :productName "Classic Tea Biscuits" ;
47
+ :sugarTenths 120 ;
48
+ :sugarPerServing 12.0 .
49
+
50
+ :prod_BIS_101
51
+ a :Product ;
52
+ :productId "prod:BIS_101" ;
53
+ :productName "Low-Sugar Tea Biscuits" ;
54
+ :sugarTenths 30 ;
55
+ :sugarPerServing 3.0 .
56
+
57
+ :prod_CHOC_050
58
+ a :Product ;
59
+ :productId "prod:CHOC_050" ;
60
+ :productName "Milk Chocolate Bar" ;
61
+ :sugarTenths 150 ;
62
+ :sugarPerServing 15.0 .
63
+
64
+ :prod_CHOC_150
65
+ a :Product ;
66
+ :productId "prod:CHOC_150" ;
67
+ :productName "85% Dark Chocolate" ;
68
+ :sugarTenths 60 ;
69
+ :sugarPerServing 6.0 .
70
+
71
+ :householdProfile :condition "Diabetes" .
72
+ :scan :scannedProduct :prod_BIS_001 .
73
+
74
+ <https://example.org/insight/delfour>
75
+ a ins:Insight ;
76
+ :metric "sugar_g_per_serving" ;
77
+ :thresholdTenths 100 ;
78
+ :thresholdDisplay "10.0" ;
79
+ :thresholdG 10.0 ;
80
+ :suggestionPolicy "lower_metric_first_higher_price_ok" ;
81
+ :scopeDevice "self-scanner" ;
82
+ :scopeEvent "pick_up_scanner" ;
83
+ :retailer "Delfour" ;
84
+ :createdAt "2025-10-05T20:33:48.907163+00:00"^^xsd:dateTime ;
85
+ :expiresAt "2025-10-05T22:33:48.907185+00:00"^^xsd:dateTime ;
86
+ :serializedLowercase "{\"createdat\":\"2025-10-05t20:33:48.907163+00:00\",\"expiresat\":\"2025-10-05t22:33:48.907185+00:00\",\"id\":\"https://example.org/insight/delfour\",\"metric\":\"sugar_g_per_serving\",\"retailer\":\"delfour\",\"scopedevice\":\"self-scanner\",\"scopeevent\":\"pick_up_scanner\",\"suggestionpolicy\":\"lower_metric_first_higher_price_ok\",\"threshold\":10.0,\"type\":\"ins:insight\"}" .
87
+
88
+ :policy
89
+ a odrl:Policy ;
90
+ :profile "Delfour-Insight-Policy" ;
91
+ odrl:permission [
92
+ odrl:action odrl:use ;
93
+ odrl:target <https://example.org/insight/delfour> ;
94
+ odrl:constraint [
95
+ odrl:leftOperand odrl:purpose ;
96
+ odrl:operator odrl:eq ;
97
+ odrl:rightOperand "shopping_assist"
98
+ ]
99
+ ] ;
100
+ odrl:prohibition [
101
+ odrl:action odrl:distribute ;
102
+ odrl:target <https://example.org/insight/delfour> ;
103
+ odrl:constraint [
104
+ odrl:leftOperand odrl:purpose ;
105
+ odrl:operator odrl:eq ;
106
+ odrl:rightOperand "marketing"
107
+ ]
108
+ ] ;
109
+ odrl:duty [
110
+ odrl:action odrl:delete ;
111
+ odrl:constraint [
112
+ odrl:leftOperand odrl:dateTime ;
113
+ odrl:operator odrl:eq ;
114
+ odrl:rightOperand "2025-10-05T22:33:48.907185+00:00"^^xsd:dateTime
115
+ ]
116
+ ] .
117
+
118
+ :envelope
119
+ :insight <https://example.org/insight/delfour> ;
120
+ :policy :policy ;
121
+ :canonicalJson "{\"insight\":{\"createdAt\":\"2025-10-05T20:33:48.907163+00:00\",\"expiresAt\":\"2025-10-05T22:33:48.907185+00:00\",\"id\":\"https://example.org/insight/delfour\",\"metric\":\"sugar_g_per_serving\",\"retailer\":\"Delfour\",\"scopeDevice\":\"self-scanner\",\"scopeEvent\":\"pick_up_scanner\",\"suggestionPolicy\":\"lower_metric_first_higher_price_ok\",\"threshold\":10.0,\"type\":\"ins:Insight\"},\"policy\":{\"duty\":{\"action\":\"odrl:delete\",\"constraint\":{\"leftOperand\":\"odrl:dateTime\",\"operator\":\"odrl:eq\",\"rightOperand\":\"2025-10-05T22:33:48.907185+00:00\"}},\"permission\":{\"action\":\"odrl:use\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"shopping_assist\"},\"target\":\"https://example.org/insight/delfour\"},\"profile\":\"Delfour-Insight-Policy\",\"prohibition\":{\"action\":\"odrl:distribute\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"marketing\"},\"target\":\"https://example.org/insight/delfour\"},\"type\":\"odrl:Policy\"}}" .
122
+
123
+ :signature
124
+ :alg "HMAC-SHA256" ;
125
+ :keyid "demo-shared-secret" ;
126
+ :created "2025-10-05T20:33:48.907163+00:00"^^xsd:dateTime ;
127
+ :payloadHashSHA256 "34ad35638dfd7c67d031eeca8abb235ec24280740f863f3f31cd9d7b6517f098" ;
128
+ :signatureHMAC "b21d0072d90112a9f820aced0286889f4b6ef92b145e6fdef1011f3bfa4608c2" ;
129
+ :hmacVerificationMode :trustedPrecomputedInput .
130
+
131
+ :reasonText
132
+ :value "Household requires low-sugar guidance (diabetes in POD). A neutral Insight is scoped to device 'self-scanner', event 'pick_up_scanner', retailer 'Delfour', and expires soon; the policy confines use to shopping assistance.\n" .
133
+
134
+ # -----
135
+ # Logic
136
+ # -----
137
+
138
+ # desensitize(&["Diabetes"]) -> true
139
+ { :householdProfile :condition "Diabetes" . }
140
+ => { :case :needsLowSugar true . } .
141
+
142
+ # derive_insight(...)
143
+ { :case :needsLowSugar true . }
144
+ => { <https://example.org/insight/delfour> :derivedFromNeed "low_sugar" . } .
145
+
146
+ # payload_hash_matches
147
+ {
148
+ :envelope :canonicalJson ?json .
149
+ ?json crypto:sha256 ?digest .
150
+ :signature :payloadHashSHA256 ?digest .
151
+ } => {
152
+ :check :payloadHashMatches true .
153
+ } .
154
+
155
+ # signature_verified
156
+ {
157
+ :signature :hmacVerificationMode :trustedPrecomputedInput .
158
+ } => {
159
+ :check :signatureVerifies true .
160
+ } .
161
+
162
+ # minimization_no_sensitive_terms
163
+ {
164
+ <https://example.org/insight/delfour> :serializedLowercase ?s .
165
+ ?s string:notMatches "diabetes|medical" .
166
+ } => {
167
+ :check :minimizationStripsSensitiveTerms true .
168
+ } .
169
+
170
+ # scope_complete
171
+ {
172
+ <https://example.org/insight/delfour> :scopeDevice ?device ;
173
+ :scopeEvent ?event ;
174
+ :expiresAt ?expiry .
175
+ } => {
176
+ :check :scopeComplete true .
177
+ } .
178
+
179
+ # authorization_allowed
180
+ {
181
+ :policy odrl:permission [
182
+ odrl:action odrl:use ;
183
+ odrl:target <https://example.org/insight/delfour> ;
184
+ odrl:constraint [
185
+ odrl:leftOperand odrl:purpose ;
186
+ odrl:operator odrl:eq ;
187
+ odrl:rightOperand "shopping_assist"
188
+ ]
189
+ ] .
190
+ :case :scannerAuthAt ?authAt .
191
+ <https://example.org/insight/delfour> :expiresAt ?expiresAt .
192
+ ?authAt math:notGreaterThan ?expiresAt .
193
+ } => {
194
+ :decision
195
+ :at "2025-10-05T20:35:48.907163+00:00"^^xsd:dateTime ;
196
+ :outcome "Allowed" ;
197
+ :target <https://example.org/insight/delfour> .
198
+ :check :authorizationAllowed true .
199
+ } .
200
+
201
+ # banner if scanned product meets/exceeds threshold
202
+ {
203
+ :decision :outcome "Allowed" .
204
+ :scan :scannedProduct ?product .
205
+ ?product :sugarPerServing ?sugar .
206
+ <https://example.org/insight/delfour> :thresholdG ?threshold .
207
+ ?sugar math:notLessThan ?threshold .
208
+ } => {
209
+ :banner
210
+ :headline "Track sugar per serving while you scan" ;
211
+ :note "High sugar" .
212
+ :check :bannerFlagsHighSugar true .
213
+ } .
214
+
215
+ # choose the lower-sugar alternative with the smallest sugar score
216
+ {
217
+ :scan :scannedProduct ?scanned .
218
+ ?scanned :sugarTenths ?scannedSugar .
219
+ ?candidate a :Product ;
220
+ :sugarTenths ?candidateSugar .
221
+ ?scannedSugar math:greaterThan ?candidateSugar .
222
+ 1 log:notIncludes {
223
+ ?other a :Product ;
224
+ :sugarTenths ?otherSugar .
225
+ ?scannedSugar math:greaterThan ?otherSugar .
226
+ ?otherSugar math:lessThan ?candidateSugar .
227
+ } .
228
+ } => {
229
+ :case :suggestedAlternative ?candidate .
230
+ } .
231
+
232
+ {
233
+ :banner :note "High sugar" .
234
+ :case :suggestedAlternative ?alt .
235
+ ?alt :productName ?altName .
236
+ } => {
237
+ :banner :suggestedAlternative ?altName .
238
+ } .
239
+
240
+ {
241
+ :scan :scannedProduct ?scanned .
242
+ ?scanned :sugarTenths ?scannedSugar .
243
+ :case :suggestedAlternative ?alt .
244
+ ?alt :sugarTenths ?altSugar .
245
+ ?scannedSugar math:greaterThan ?altSugar .
246
+ } => {
247
+ :check :alternativeIsLowerSugar true .
248
+ } .
249
+
250
+ # duty_timing_consistent
251
+ {
252
+ :case :scannerDutyAt ?dutyAt .
253
+ <https://example.org/insight/delfour> :expiresAt ?expiresAt .
254
+ ?dutyAt math:notGreaterThan ?expiresAt .
255
+ } => {
256
+ :check :dutyTimingConsistent true .
257
+ } .
258
+
259
+ # marketing_prohibited
260
+ {
261
+ :policy odrl:prohibition [
262
+ odrl:action odrl:distribute ;
263
+ odrl:constraint [
264
+ odrl:rightOperand "marketing"
265
+ ]
266
+ ] .
267
+ } => {
268
+ :check :marketingProhibited true .
269
+ } .
270
+
271
+ # file count check mirrors EXPECTED_FILES_WRITTEN
272
+ {
273
+ :case :filesWritten 6 .
274
+ } => {
275
+ :check :filesWrittenExpected true .
276
+ } .
277
+
278
+ {
279
+ :check :signatureVerifies true ;
280
+ :payloadHashMatches true ;
281
+ :minimizationStripsSensitiveTerms true ;
282
+ :scopeComplete true ;
283
+ :authorizationAllowed true ;
284
+ :bannerFlagsHighSugar true ;
285
+ :alternativeIsLowerSugar true ;
286
+ :dutyTimingConsistent true ;
287
+ :marketingProhibited true ;
288
+ :filesWrittenExpected true .
289
+ } => {
290
+ :result :allChecksPass true .
291
+ } .
292
+
293
+ # -------------------------------------
294
+ # Hard checks (Eyeling inference fuses)
295
+ # -------------------------------------
296
+
297
+ {
298
+ :case :filesWritten ?n .
299
+ ?n math:notEqualTo 6 .
300
+ } => false .
301
+
302
+ {
303
+ :case :scannerAuthAt ?authAt .
304
+ <https://example.org/insight/delfour> :expiresAt ?expiresAt .
305
+ ?authAt math:greaterThan ?expiresAt .
306
+ } => false .
307
+
308
+ {
309
+ :scan :scannedProduct ?scanned .
310
+ ?scanned :sugarTenths ?scannedSugar .
311
+ :case :suggestedAlternative ?alt .
312
+ ?alt :sugarTenths ?altSugar .
313
+ ?altSugar math:notLessThan ?scannedSugar .
314
+ } => false .
315
+
316
+ {
317
+ :policy odrl:prohibition [
318
+ odrl:action ?action
319
+ ] .
320
+ ?action log:notEqualTo odrl:distribute .
321
+ } => false .
322
+
323
+ {
324
+ :envelope :canonicalJson ?json .
325
+ ?json crypto:sha256 ?actual .
326
+ :signature :payloadHashSHA256 ?expected .
327
+ ?actual log:notEqualTo ?expected .
328
+ } => false .
329
+
330
+ # --------------------------------------
331
+ # ARC rendering through log:outputString
332
+ # --------------------------------------
333
+
334
+ :out01 log:outputString "=== Answer ===\n" .
335
+ :out02 log:outputString "The scanner is allowed to use a neutral shopping insight and recommends Low-Sugar Tea Biscuits instead of Classic Tea Biscuits.\n" .
336
+ :out03 log:outputString "case : delfour\n" .
337
+ :out04 log:outputString "decision : Allowed\n" .
338
+ :out05 log:outputString "scanned product : Classic Tea Biscuits\n" .
339
+
340
+ {
341
+ :case :suggestedAlternative ?alt .
342
+ ?alt :productName ?altName .
343
+ ("suggested alternative: %s\n" ?altName) string:format ?line .
344
+ } => {
345
+ :out06 log:outputString ?line .
346
+ } .
347
+
348
+ :out07 log:outputString "\n=== Reason Why ===\n" .
349
+ :out08 log:outputString "The phone desensitizes a diabetes-related household condition into a scoped low-sugar need, wraps it in an expiring Insight + Policy envelope, signs it, and the scanner consumes that envelope for shopping assistance.\n" .
350
+ :out09 log:outputString "metric : sugar_g_per_serving\n" .
351
+
352
+ {
353
+ <https://example.org/insight/delfour> :thresholdDisplay ?threshold .
354
+ ("threshold : %s\n" ?threshold) string:format ?line .
355
+ } => {
356
+ :out10 log:outputString ?line .
357
+ } .
358
+
359
+ :out11 log:outputString "scope : self-scanner @ pick_up_scanner\n" .
360
+ :out12 log:outputString "retailer : Delfour\n" .
361
+
362
+ {
363
+ :signature :alg ?alg .
364
+ ("signature alg : %s\n" ?alg) string:format ?line .
365
+ } => {
366
+ :out13 log:outputString ?line .
367
+ } .
368
+
369
+ {
370
+ :banner :headline ?headline .
371
+ ("banner headline : %s\n" ?headline) string:format ?line .
372
+ } => {
373
+ :out14 log:outputString ?line .
374
+ } .
375
+
376
+ :out15 log:outputString "expires at : 2025-10-05T22:33:48.907185+00:00\n" .
377
+
378
+ {
379
+ :reasonText :value ?reason .
380
+ ("reason.txt : %s" ?reason) string:format ?line .
381
+ } => {
382
+ :out16 log:outputString ?line .
383
+ } .
384
+
385
+ {
386
+ :case :auditEntries ?n .
387
+ ("audit entries : %s\n" ?n) string:format ?line .
388
+ } => {
389
+ :out17 log:outputString ?line .
390
+ } .
391
+
392
+ {
393
+ :case :filesWritten ?n .
394
+ ("bus files written : %s\n" ?n) string:format ?line .
395
+ } => {
396
+ :out18 log:outputString ?line .
397
+ } .
398
+
399
+ :out19 log:outputString "\n=== Check ===\n" .
400
+
401
+ { :check :signatureVerifies true . } => { :out20 log:outputString "signature verifies : yes\n" . } .
402
+ { :check :payloadHashMatches true . } => { :out21 log:outputString "payload hash matches : yes\n" . } .
403
+ { :check :minimizationStripsSensitiveTerms true . } => { :out22 log:outputString "minimization strips sensitive terms: yes\n" . } .
404
+ { :check :scopeComplete true . } => { :out23 log:outputString "scope complete : yes\n" . } .
405
+ { :check :authorizationAllowed true . } => { :out24 log:outputString "authorization allowed : yes\n" . } .
406
+ { :check :bannerFlagsHighSugar true . } => { :out25 log:outputString "high-sugar banner : yes\n" . } .
407
+ { :check :alternativeIsLowerSugar true . } => { :out26 log:outputString "alternative lowers sugar : yes\n" . } .
408
+ { :check :dutyTimingConsistent true . } => { :out27 log:outputString "duty timing consistent : yes\n" . } .
409
+ { :check :marketingProhibited true . } => { :out28 log:outputString "marketing prohibited : yes\n" . } .