eyeling 1.22.1 → 1.22.3
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/examples/arcling/README.md +37 -167
- package/examples/calidor.n3 +500 -0
- package/examples/output/calidor.n3 +29 -0
- package/package.json +2 -3
- package/examples/arcling/delfour/delfour.data.json +0 -67
- package/examples/arcling/delfour/delfour.expected.json +0 -88
- package/examples/arcling/delfour/delfour.model.go +0 -564
- package/examples/arcling/delfour/delfour.spec.md +0 -117
- package/examples/arcling/flandor/flandor.data.json +0 -106
- package/examples/arcling/flandor/flandor.expected.json +0 -98
- package/examples/arcling/flandor/flandor.model.go +0 -655
- package/examples/arcling/flandor/flandor.spec.md +0 -155
- package/examples/arcling/medior/medior.data.json +0 -96
- package/examples/arcling/medior/medior.expected.json +0 -100
- package/examples/arcling/medior/medior.model.go +0 -652
- package/examples/arcling/medior/medior.spec.md +0 -157
- package/test/arcling.test.js +0 -191
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"caseName": "Delfour",
|
|
3
|
-
"derived": {
|
|
4
|
-
"needsLowSugar": true,
|
|
5
|
-
"highSugarScanned": true,
|
|
6
|
-
"lowerSugarCandidateIds": ["prod:BIS_101", "prod:CHOC_150"],
|
|
7
|
-
"recommendedAlternativeId": "prod:BIS_101",
|
|
8
|
-
"recommendedAlternativeName": "Low-Sugar Tea Biscuits",
|
|
9
|
-
"alternativeLowersSugar": true
|
|
10
|
-
},
|
|
11
|
-
"envelope": {
|
|
12
|
-
"insight": {
|
|
13
|
-
"createdAt": "2025-10-05T20:33:48.907163+00:00",
|
|
14
|
-
"expiresAt": "2025-10-05T22:33:48.907185+00:00",
|
|
15
|
-
"id": "https://example.org/insight/delfour",
|
|
16
|
-
"metric": "sugar_g_per_serving",
|
|
17
|
-
"retailer": "Delfour",
|
|
18
|
-
"scopeDevice": "self-scanner",
|
|
19
|
-
"scopeEvent": "pick_up_scanner",
|
|
20
|
-
"suggestionPolicy": "lower_metric_first_higher_price_ok",
|
|
21
|
-
"threshold": 10,
|
|
22
|
-
"type": "ins:Insight"
|
|
23
|
-
},
|
|
24
|
-
"policy": {
|
|
25
|
-
"duty": {
|
|
26
|
-
"action": "odrl:delete",
|
|
27
|
-
"constraint": {
|
|
28
|
-
"leftOperand": "odrl:dateTime",
|
|
29
|
-
"operator": "odrl:eq",
|
|
30
|
-
"rightOperand": "2025-10-05T22:33:48.907185+00:00"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"permission": {
|
|
34
|
-
"action": "odrl:use",
|
|
35
|
-
"constraint": {
|
|
36
|
-
"leftOperand": "odrl:purpose",
|
|
37
|
-
"operator": "odrl:eq",
|
|
38
|
-
"rightOperand": "shopping_assist"
|
|
39
|
-
},
|
|
40
|
-
"target": "https://example.org/insight/delfour"
|
|
41
|
-
},
|
|
42
|
-
"profile": "Delfour-Insight-Policy",
|
|
43
|
-
"prohibition": {
|
|
44
|
-
"action": "odrl:distribute",
|
|
45
|
-
"constraint": {
|
|
46
|
-
"leftOperand": "odrl:purpose",
|
|
47
|
-
"operator": "odrl:eq",
|
|
48
|
-
"rightOperand": "marketing"
|
|
49
|
-
},
|
|
50
|
-
"target": "https://example.org/insight/delfour"
|
|
51
|
-
},
|
|
52
|
-
"type": "odrl:Policy"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
"integrity": {
|
|
56
|
-
"canonicalEnvelope": "{\"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\"}}",
|
|
57
|
-
"payloadHashSHA256": "e1ad69852c98ca7697a164dbc6f0ca28f873508a6676865dba37b81faa66ebcb",
|
|
58
|
-
"envelopeHmacSHA256": "518a84185e2975928c6c935dae6e251a071766078c6e9e70d6f583a1147728db",
|
|
59
|
-
"verificationMode": "trustedPrecomputedInput"
|
|
60
|
-
},
|
|
61
|
-
"answer": {
|
|
62
|
-
"sentence": "The scanner is allowed to use a neutral shopping insight and recommends Low-Sugar Tea Biscuits instead of Classic Tea Biscuits.",
|
|
63
|
-
"scannedProduct": "Classic Tea Biscuits",
|
|
64
|
-
"suggestedAlternative": "Low-Sugar Tea Biscuits",
|
|
65
|
-
"payloadHashSHA256": "e1ad69852c98ca7697a164dbc6f0ca28f873508a6676865dba37b81faa66ebcb",
|
|
66
|
-
"envelopeHmacSHA256": "518a84185e2975928c6c935dae6e251a071766078c6e9e70d6f583a1147728db"
|
|
67
|
-
},
|
|
68
|
-
"reasonWhy": [
|
|
69
|
-
"The phone desensitizes a diabetes-related household condition into a scoped low-sugar need, wraps it in an expiring Insight+Policy envelope, and signs it.",
|
|
70
|
-
"scanned product : Classic Tea Biscuits",
|
|
71
|
-
"suggested alternative: Low-Sugar Tea Biscuits",
|
|
72
|
-
"payload SHA-256 : e1ad69852c98ca7697a164dbc6f0ca28f873508a6676865dba37b81faa66ebcb",
|
|
73
|
-
"HMAC-SHA256 : 518a84185e2975928c6c935dae6e251a071766078c6e9e70d6f583a1147728db"
|
|
74
|
-
],
|
|
75
|
-
"checks": {
|
|
76
|
-
"signatureVerifies": true,
|
|
77
|
-
"payloadHashMatches": true,
|
|
78
|
-
"minimizationRespected": true,
|
|
79
|
-
"scopeComplete": true,
|
|
80
|
-
"authorizationAllowed": true,
|
|
81
|
-
"highSugarBanner": true,
|
|
82
|
-
"alternativeLowersSugar": true,
|
|
83
|
-
"dutyTimingConsistent": true,
|
|
84
|
-
"marketingProhibited": true
|
|
85
|
-
},
|
|
86
|
-
"allChecksPass": true,
|
|
87
|
-
"arcText": "=== Answer ===\nThe scanner is allowed to use a neutral shopping insight and recommends Low-Sugar Tea Biscuits instead of Classic Tea Biscuits.\n\n=== Reason Why ===\nThe phone desensitizes a diabetes-related household condition into a scoped low-sugar need, wraps it in an expiring Insight+Policy envelope, and signs it.\nscanned product : Classic Tea Biscuits\nsuggested alternative: Low-Sugar Tea Biscuits\npayload SHA-256 : e1ad69852c98ca7697a164dbc6f0ca28f873508a6676865dba37b81faa66ebcb\nHMAC-SHA256 : 518a84185e2975928c6c935dae6e251a071766078c6e9e70d6f583a1147728db\n\n=== Check ===\nsignature verifies : yes\npayload hash matches : yes\nminimization strips sensitive terms: yes\nscope complete : yes\nauthorization allowed : yes\nhigh-sugar banner : yes\nalternative lowers sugar : yes\nduty timing consistent : yes\nmarketing prohibited : yes"
|
|
88
|
-
}
|
|
@@ -1,564 +0,0 @@
|
|
|
1
|
-
package main
|
|
2
|
-
|
|
3
|
-
// Delfour is a reference Arcling model written as a small CLI program.
|
|
4
|
-
// It reads delfour.data.json, derives the neutral shopping insight,
|
|
5
|
-
// computes the canonical envelope/hash/HMAC values, and emits either
|
|
6
|
-
// ARC text or a JSON result object.
|
|
7
|
-
|
|
8
|
-
import (
|
|
9
|
-
"crypto/hmac"
|
|
10
|
-
"crypto/sha256"
|
|
11
|
-
"encoding/hex"
|
|
12
|
-
"encoding/json"
|
|
13
|
-
"errors"
|
|
14
|
-
"fmt"
|
|
15
|
-
"os"
|
|
16
|
-
"sort"
|
|
17
|
-
"strings"
|
|
18
|
-
"time"
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
// Data mirrors the input instance shape from delfour.data.json.
|
|
22
|
-
type Data struct {
|
|
23
|
-
CaseName string `json:"caseName"`
|
|
24
|
-
Retailer string `json:"retailer"`
|
|
25
|
-
Question string `json:"question"`
|
|
26
|
-
Timestamps Timestamps `json:"timestamps"`
|
|
27
|
-
EvaluationContext EvaluationContext `json:"evaluationContext"`
|
|
28
|
-
Thresholds Thresholds `json:"thresholds"`
|
|
29
|
-
HouseholdProfile HouseholdProfile `json:"householdProfile"`
|
|
30
|
-
Catalog []Product `json:"catalog"`
|
|
31
|
-
Scan Scan `json:"scan"`
|
|
32
|
-
InsightPolicy InsightPolicy `json:"insightPolicy"`
|
|
33
|
-
Integrity Integrity `json:"integrity"`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type Timestamps struct {
|
|
37
|
-
CreatedAt string `json:"createdAt"`
|
|
38
|
-
ExpiresAt string `json:"expiresAt"`
|
|
39
|
-
AuthorizedAt string `json:"authorizedAt"`
|
|
40
|
-
DutyPerformedAt string `json:"dutyPerformedAt"`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type EvaluationContext struct {
|
|
44
|
-
ScopeDevice string `json:"scopeDevice"`
|
|
45
|
-
ScopeEvent string `json:"scopeEvent"`
|
|
46
|
-
Purpose string `json:"purpose"`
|
|
47
|
-
ProhibitedReusePurpose string `json:"prohibitedReusePurpose"`
|
|
48
|
-
RequestAction string `json:"requestAction"`
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type Thresholds struct {
|
|
52
|
-
SugarPerServingGAtLeast float64 `json:"sugarPerServingGAtLeast"`
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type HouseholdProfile struct {
|
|
56
|
-
Condition string `json:"condition"`
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type Product struct {
|
|
60
|
-
ID string `json:"id"`
|
|
61
|
-
Name string `json:"name"`
|
|
62
|
-
SugarTenths int `json:"sugarTenths"`
|
|
63
|
-
SugarPerServing float64 `json:"sugarPerServing"`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
type Scan struct {
|
|
67
|
-
ScannedProductID string `json:"scannedProductId"`
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type InsightPolicy struct {
|
|
71
|
-
ID string `json:"id"`
|
|
72
|
-
Metric string `json:"metric"`
|
|
73
|
-
Type string `json:"type"`
|
|
74
|
-
SuggestionPolicy string `json:"suggestionPolicy"`
|
|
75
|
-
PolicyType string `json:"policyType"`
|
|
76
|
-
PolicyProfile string `json:"policyProfile"`
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type Integrity struct {
|
|
80
|
-
HashAlgorithm string `json:"hashAlgorithm"`
|
|
81
|
-
MacAlgorithm string `json:"macAlgorithm"`
|
|
82
|
-
Secret string `json:"secret"`
|
|
83
|
-
VerificationMode string `json:"verificationMode"`
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Insight is the minimized payload shared with the retailer.
|
|
87
|
-
type Insight struct {
|
|
88
|
-
CreatedAt string `json:"createdAt"`
|
|
89
|
-
ExpiresAt string `json:"expiresAt"`
|
|
90
|
-
ID string `json:"id"`
|
|
91
|
-
Metric string `json:"metric"`
|
|
92
|
-
Retailer string `json:"retailer"`
|
|
93
|
-
ScopeDevice string `json:"scopeDevice"`
|
|
94
|
-
ScopeEvent string `json:"scopeEvent"`
|
|
95
|
-
SuggestionPolicy string `json:"suggestionPolicy"`
|
|
96
|
-
Threshold float64 `json:"threshold"`
|
|
97
|
-
Type string `json:"type"`
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
type Constraint struct {
|
|
101
|
-
LeftOperand string `json:"leftOperand"`
|
|
102
|
-
Operator string `json:"operator"`
|
|
103
|
-
RightOperand string `json:"rightOperand"`
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
type Duty struct {
|
|
107
|
-
Action string `json:"action"`
|
|
108
|
-
Constraint Constraint `json:"constraint"`
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
type Permission struct {
|
|
112
|
-
Action string `json:"action"`
|
|
113
|
-
Constraint Constraint `json:"constraint"`
|
|
114
|
-
Target string `json:"target"`
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
type Prohibition struct {
|
|
118
|
-
Action string `json:"action"`
|
|
119
|
-
Constraint Constraint `json:"constraint"`
|
|
120
|
-
Target string `json:"target"`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
type Policy struct {
|
|
124
|
-
Duty Duty `json:"duty"`
|
|
125
|
-
Permission Permission `json:"permission"`
|
|
126
|
-
Profile string `json:"profile"`
|
|
127
|
-
Prohibition Prohibition `json:"prohibition"`
|
|
128
|
-
Type string `json:"type"`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
type Envelope struct {
|
|
132
|
-
Insight Insight `json:"insight"`
|
|
133
|
-
Policy Policy `json:"policy"`
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
type Derived struct {
|
|
137
|
-
NeedsLowSugar bool `json:"needsLowSugar"`
|
|
138
|
-
HighSugarScanned bool `json:"highSugarScanned"`
|
|
139
|
-
LowerSugarCandidateIDs []string `json:"lowerSugarCandidateIds"`
|
|
140
|
-
RecommendedAlternativeID *string `json:"recommendedAlternativeId"`
|
|
141
|
-
RecommendedAlternativeName *string `json:"recommendedAlternativeName"`
|
|
142
|
-
AlternativeLowersSugar bool `json:"alternativeLowersSugar"`
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
type IntegrityResult struct {
|
|
146
|
-
CanonicalEnvelope string `json:"canonicalEnvelope"`
|
|
147
|
-
PayloadHashSHA256 string `json:"payloadHashSHA256"`
|
|
148
|
-
EnvelopeHmacSHA256 string `json:"envelopeHmacSHA256"`
|
|
149
|
-
VerificationMode string `json:"verificationMode"`
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
type Answer struct {
|
|
153
|
-
Sentence string `json:"sentence"`
|
|
154
|
-
ScannedProduct string `json:"scannedProduct"`
|
|
155
|
-
SuggestedAlternative *string `json:"suggestedAlternative"`
|
|
156
|
-
PayloadHashSHA256 string `json:"payloadHashSHA256"`
|
|
157
|
-
EnvelopeHmacSHA256 string `json:"envelopeHmacSHA256"`
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
type Checks struct {
|
|
161
|
-
SignatureVerifies bool `json:"signatureVerifies"`
|
|
162
|
-
PayloadHashMatches bool `json:"payloadHashMatches"`
|
|
163
|
-
MinimizationRespected bool `json:"minimizationRespected"`
|
|
164
|
-
ScopeComplete bool `json:"scopeComplete"`
|
|
165
|
-
AuthorizationAllowed bool `json:"authorizationAllowed"`
|
|
166
|
-
HighSugarBanner bool `json:"highSugarBanner"`
|
|
167
|
-
AlternativeLowersSugar bool `json:"alternativeLowersSugar"`
|
|
168
|
-
DutyTimingConsistent bool `json:"dutyTimingConsistent"`
|
|
169
|
-
MarketingProhibited bool `json:"marketingProhibited"`
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
type Result struct {
|
|
173
|
-
CaseName string `json:"caseName"`
|
|
174
|
-
Derived Derived `json:"derived"`
|
|
175
|
-
Envelope Envelope `json:"envelope"`
|
|
176
|
-
Integrity IntegrityResult `json:"integrity"`
|
|
177
|
-
Answer Answer `json:"answer"`
|
|
178
|
-
ReasonWhy []string `json:"reasonWhy"`
|
|
179
|
-
Checks Checks `json:"checks"`
|
|
180
|
-
AllChecksPass bool `json:"allChecksPass"`
|
|
181
|
-
ArcText string `json:"arcText"`
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
func must(condition bool, message string) error {
|
|
185
|
-
if !condition {
|
|
186
|
-
return errors.New(message)
|
|
187
|
-
}
|
|
188
|
-
return nil
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
func readJSON(path string) (Data, error) {
|
|
192
|
-
var data Data
|
|
193
|
-
b, err := os.ReadFile(path)
|
|
194
|
-
if err != nil {
|
|
195
|
-
return data, err
|
|
196
|
-
}
|
|
197
|
-
err = json.Unmarshal(b, &data)
|
|
198
|
-
return data, err
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// validate performs the structural checks that used to live in JSON Schema.
|
|
202
|
-
func validate(data Data) error {
|
|
203
|
-
if err := must(data.CaseName != "", "caseName is required"); err != nil {
|
|
204
|
-
return err
|
|
205
|
-
}
|
|
206
|
-
if err := must(data.Retailer != "", "retailer is required"); err != nil {
|
|
207
|
-
return err
|
|
208
|
-
}
|
|
209
|
-
if err := must(len(data.Catalog) > 0, "catalog is required"); err != nil {
|
|
210
|
-
return err
|
|
211
|
-
}
|
|
212
|
-
if err := must(data.Scan.ScannedProductID != "", "scan.scannedProductId is required"); err != nil {
|
|
213
|
-
return err
|
|
214
|
-
}
|
|
215
|
-
if err := must(data.Timestamps.CreatedAt != "", "timestamps.createdAt is required"); err != nil {
|
|
216
|
-
return err
|
|
217
|
-
}
|
|
218
|
-
if err := must(data.Timestamps.ExpiresAt != "", "timestamps.expiresAt is required"); err != nil {
|
|
219
|
-
return err
|
|
220
|
-
}
|
|
221
|
-
if err := must(data.Timestamps.AuthorizedAt != "", "timestamps.authorizedAt is required"); err != nil {
|
|
222
|
-
return err
|
|
223
|
-
}
|
|
224
|
-
if err := must(data.Timestamps.DutyPerformedAt != "", "timestamps.dutyPerformedAt is required"); err != nil {
|
|
225
|
-
return err
|
|
226
|
-
}
|
|
227
|
-
if err := must(data.HouseholdProfile.Condition != "", "householdProfile.condition is required"); err != nil {
|
|
228
|
-
return err
|
|
229
|
-
}
|
|
230
|
-
if err := must(data.EvaluationContext.Purpose != "", "evaluationContext.purpose is required"); err != nil {
|
|
231
|
-
return err
|
|
232
|
-
}
|
|
233
|
-
if err := must(data.EvaluationContext.RequestAction != "", "evaluationContext.requestAction is required"); err != nil {
|
|
234
|
-
return err
|
|
235
|
-
}
|
|
236
|
-
if err := must(data.InsightPolicy.ID != "", "insightPolicy.id is required"); err != nil {
|
|
237
|
-
return err
|
|
238
|
-
}
|
|
239
|
-
if err := must(data.Integrity.Secret != "", "integrity.secret is required"); err != nil {
|
|
240
|
-
return err
|
|
241
|
-
}
|
|
242
|
-
return nil
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// parseTime accepts RFC3339Nano timestamps from the case instance.
|
|
246
|
-
func parseTime(s string) time.Time {
|
|
247
|
-
t, err := time.Parse(time.RFC3339Nano, s)
|
|
248
|
-
if err != nil {
|
|
249
|
-
panic(err)
|
|
250
|
-
}
|
|
251
|
-
return t
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// findProduct resolves the scanned or recommended product by its catalog id.
|
|
255
|
-
func findProduct(data Data, id string) *Product {
|
|
256
|
-
for i := range data.Catalog {
|
|
257
|
-
if data.Catalog[i].ID == id {
|
|
258
|
-
return &data.Catalog[i]
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
return nil
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// deriveInsight strips the household condition down to the neutral shopping insight.
|
|
265
|
-
func deriveInsight(data Data) Insight {
|
|
266
|
-
return Insight{
|
|
267
|
-
CreatedAt: data.Timestamps.CreatedAt,
|
|
268
|
-
ExpiresAt: data.Timestamps.ExpiresAt,
|
|
269
|
-
ID: data.InsightPolicy.ID,
|
|
270
|
-
Metric: data.InsightPolicy.Metric,
|
|
271
|
-
Retailer: data.Retailer,
|
|
272
|
-
ScopeDevice: data.EvaluationContext.ScopeDevice,
|
|
273
|
-
ScopeEvent: data.EvaluationContext.ScopeEvent,
|
|
274
|
-
SuggestionPolicy: data.InsightPolicy.SuggestionPolicy,
|
|
275
|
-
Threshold: data.Thresholds.SugarPerServingGAtLeast,
|
|
276
|
-
Type: data.InsightPolicy.Type,
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// derivePolicy builds the companion ODRL-style policy used for governance checks.
|
|
281
|
-
func derivePolicy(data Data) Policy {
|
|
282
|
-
return Policy{
|
|
283
|
-
Duty: Duty{
|
|
284
|
-
Action: "odrl:delete",
|
|
285
|
-
Constraint: Constraint{
|
|
286
|
-
LeftOperand: "odrl:dateTime",
|
|
287
|
-
Operator: "odrl:eq",
|
|
288
|
-
RightOperand: data.Timestamps.ExpiresAt,
|
|
289
|
-
},
|
|
290
|
-
},
|
|
291
|
-
Permission: Permission{
|
|
292
|
-
Action: data.EvaluationContext.RequestAction,
|
|
293
|
-
Constraint: Constraint{
|
|
294
|
-
LeftOperand: "odrl:purpose",
|
|
295
|
-
Operator: "odrl:eq",
|
|
296
|
-
RightOperand: data.EvaluationContext.Purpose,
|
|
297
|
-
},
|
|
298
|
-
Target: data.InsightPolicy.ID,
|
|
299
|
-
},
|
|
300
|
-
Profile: data.InsightPolicy.PolicyProfile,
|
|
301
|
-
Prohibition: Prohibition{
|
|
302
|
-
Action: "odrl:distribute",
|
|
303
|
-
Constraint: Constraint{
|
|
304
|
-
LeftOperand: "odrl:purpose",
|
|
305
|
-
Operator: "odrl:eq",
|
|
306
|
-
RightOperand: data.EvaluationContext.ProhibitedReusePurpose,
|
|
307
|
-
},
|
|
308
|
-
Target: data.InsightPolicy.ID,
|
|
309
|
-
},
|
|
310
|
-
Type: data.InsightPolicy.PolicyType,
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// canonicalEnvelope returns the exact byte string used for the integrity vector.
|
|
315
|
-
// The field order and the lexical form of threshold (10.0) are intentional.
|
|
316
|
-
func canonicalEnvelope(insight Insight, policy Policy) string {
|
|
317
|
-
return fmt.Sprintf(
|
|
318
|
-
"{\"insight\":{\"createdAt\":\"%s\",\"expiresAt\":\"%s\",\"id\":\"%s\",\"metric\":\"%s\",\"retailer\":\"%s\",\"scopeDevice\":\"%s\",\"scopeEvent\":\"%s\",\"suggestionPolicy\":\"%s\",\"threshold\":10.0,\"type\":\"%s\"},\"policy\":{\"duty\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"}},\"permission\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"},\"target\":\"%s\"},\"profile\":\"%s\",\"prohibition\":{\"action\":\"%s\",\"constraint\":{\"leftOperand\":\"%s\",\"operator\":\"%s\",\"rightOperand\":\"%s\"},\"target\":\"%s\"},\"type\":\"%s\"}}",
|
|
319
|
-
insight.CreatedAt,
|
|
320
|
-
insight.ExpiresAt,
|
|
321
|
-
insight.ID,
|
|
322
|
-
insight.Metric,
|
|
323
|
-
insight.Retailer,
|
|
324
|
-
insight.ScopeDevice,
|
|
325
|
-
insight.ScopeEvent,
|
|
326
|
-
insight.SuggestionPolicy,
|
|
327
|
-
insight.Type,
|
|
328
|
-
policy.Duty.Action,
|
|
329
|
-
policy.Duty.Constraint.LeftOperand,
|
|
330
|
-
policy.Duty.Constraint.Operator,
|
|
331
|
-
policy.Duty.Constraint.RightOperand,
|
|
332
|
-
policy.Permission.Action,
|
|
333
|
-
policy.Permission.Constraint.LeftOperand,
|
|
334
|
-
policy.Permission.Constraint.Operator,
|
|
335
|
-
policy.Permission.Constraint.RightOperand,
|
|
336
|
-
policy.Permission.Target,
|
|
337
|
-
policy.Profile,
|
|
338
|
-
policy.Prohibition.Action,
|
|
339
|
-
policy.Prohibition.Constraint.LeftOperand,
|
|
340
|
-
policy.Prohibition.Constraint.Operator,
|
|
341
|
-
policy.Prohibition.Constraint.RightOperand,
|
|
342
|
-
policy.Prohibition.Target,
|
|
343
|
-
policy.Type,
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
func sha256Hex(s string) string {
|
|
348
|
-
sum := sha256.Sum256([]byte(s))
|
|
349
|
-
return hex.EncodeToString(sum[:])
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
func hmacSHA256Hex(secret, s string) string {
|
|
353
|
-
mac := hmac.New(sha256.New, []byte(secret))
|
|
354
|
-
_, _ = mac.Write([]byte(s))
|
|
355
|
-
return hex.EncodeToString(mac.Sum(nil))
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
func yesNo(v bool) string {
|
|
359
|
-
if v {
|
|
360
|
-
return "yes"
|
|
361
|
-
}
|
|
362
|
-
return "no"
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// evaluate runs the full Arcling pipeline: derive facts, select the recommendation,
|
|
366
|
-
// build the envelope, verify integrity values, and render the final report.
|
|
367
|
-
func evaluate(data Data) (Result, error) {
|
|
368
|
-
var result Result
|
|
369
|
-
if err := validate(data); err != nil {
|
|
370
|
-
return result, err
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
scanned := findProduct(data, data.Scan.ScannedProductID)
|
|
374
|
-
if scanned == nil {
|
|
375
|
-
return result, fmt.Errorf("scanned product not found: %s", data.Scan.ScannedProductID)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
needsLowSugar := data.HouseholdProfile.Condition == "Diabetes"
|
|
379
|
-
highSugarScanned := scanned.SugarPerServing >= data.Thresholds.SugarPerServingGAtLeast
|
|
380
|
-
|
|
381
|
-
var candidates []Product
|
|
382
|
-
for _, p := range data.Catalog {
|
|
383
|
-
if p.SugarTenths < scanned.SugarTenths {
|
|
384
|
-
candidates = append(candidates, p)
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
sort.Slice(candidates, func(i, j int) bool {
|
|
388
|
-
return candidates[i].SugarTenths < candidates[j].SugarTenths
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
var recommended *Product
|
|
392
|
-
if len(candidates) > 0 {
|
|
393
|
-
recommended = &candidates[0]
|
|
394
|
-
}
|
|
395
|
-
alternativeLowersSugar := recommended != nil && recommended.SugarTenths < scanned.SugarTenths
|
|
396
|
-
|
|
397
|
-
insight := deriveInsight(data)
|
|
398
|
-
policy := derivePolicy(data)
|
|
399
|
-
canonical := canonicalEnvelope(insight, policy)
|
|
400
|
-
payloadHash := sha256Hex(canonical)
|
|
401
|
-
envelopeHMAC := hmacSHA256Hex(data.Integrity.Secret, canonical)
|
|
402
|
-
|
|
403
|
-
lowerIDs := make([]string, 0, len(candidates))
|
|
404
|
-
for _, p := range candidates {
|
|
405
|
-
lowerIDs = append(lowerIDs, p.ID)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
insightBytes, _ := json.Marshal(insight)
|
|
409
|
-
insightText := strings.ToLower(string(insightBytes))
|
|
410
|
-
minimizationRespected := !strings.Contains(insightText, "diabetes") && !strings.Contains(insightText, "medical")
|
|
411
|
-
|
|
412
|
-
scopeComplete := insight.ScopeDevice != "" && insight.ScopeEvent != "" && insight.ExpiresAt != ""
|
|
413
|
-
authorizationAllowed := data.EvaluationContext.RequestAction == "odrl:use" &&
|
|
414
|
-
data.EvaluationContext.Purpose == "shopping_assist" &&
|
|
415
|
-
!parseTime(data.Timestamps.AuthorizedAt).After(parseTime(data.Timestamps.ExpiresAt))
|
|
416
|
-
dutyTimingConsistent := !parseTime(data.Timestamps.DutyPerformedAt).After(parseTime(data.Timestamps.ExpiresAt))
|
|
417
|
-
marketingProhibited := policy.Prohibition.Action == "odrl:distribute" &&
|
|
418
|
-
policy.Prohibition.Constraint.RightOperand == "marketing"
|
|
419
|
-
signatureVerifies := data.Integrity.VerificationMode == "trustedPrecomputedInput" &&
|
|
420
|
-
envelopeHMAC == hmacSHA256Hex(data.Integrity.Secret, canonical)
|
|
421
|
-
payloadHashMatches := payloadHash == sha256Hex(canonical)
|
|
422
|
-
|
|
423
|
-
checks := Checks{
|
|
424
|
-
SignatureVerifies: signatureVerifies,
|
|
425
|
-
PayloadHashMatches: payloadHashMatches,
|
|
426
|
-
MinimizationRespected: minimizationRespected,
|
|
427
|
-
ScopeComplete: scopeComplete,
|
|
428
|
-
AuthorizationAllowed: authorizationAllowed,
|
|
429
|
-
HighSugarBanner: highSugarScanned,
|
|
430
|
-
AlternativeLowersSugar: alternativeLowersSugar,
|
|
431
|
-
DutyTimingConsistent: dutyTimingConsistent,
|
|
432
|
-
MarketingProhibited: marketingProhibited,
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
var recommendedID *string
|
|
436
|
-
var recommendedName *string
|
|
437
|
-
recommendedText := "none"
|
|
438
|
-
sentence := fmt.Sprintf("The scanner is allowed to use a neutral shopping insight and recommends no alternative instead of %s.", scanned.Name)
|
|
439
|
-
|
|
440
|
-
if recommended != nil {
|
|
441
|
-
recommendedID = &recommended.ID
|
|
442
|
-
recommendedName = &recommended.Name
|
|
443
|
-
recommendedText = recommended.Name
|
|
444
|
-
sentence = fmt.Sprintf(
|
|
445
|
-
"The scanner is allowed to use a neutral shopping insight and recommends %s instead of %s.",
|
|
446
|
-
recommended.Name,
|
|
447
|
-
scanned.Name,
|
|
448
|
-
)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
reasonWhy := []string{
|
|
452
|
-
"The phone desensitizes a diabetes-related household condition into a scoped low-sugar need, wraps it in an expiring Insight+Policy envelope, and signs it.",
|
|
453
|
-
fmt.Sprintf("scanned product : %s", scanned.Name),
|
|
454
|
-
fmt.Sprintf("suggested alternative: %s", recommendedText),
|
|
455
|
-
fmt.Sprintf("payload SHA-256 : %s", payloadHash),
|
|
456
|
-
fmt.Sprintf("HMAC-SHA256 : %s", envelopeHMAC),
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
arcLines := []string{
|
|
460
|
-
"=== Answer ===",
|
|
461
|
-
sentence,
|
|
462
|
-
"",
|
|
463
|
-
"=== Reason Why ===",
|
|
464
|
-
}
|
|
465
|
-
arcLines = append(arcLines, reasonWhy...)
|
|
466
|
-
arcLines = append(
|
|
467
|
-
arcLines,
|
|
468
|
-
"",
|
|
469
|
-
"=== Check ===",
|
|
470
|
-
fmt.Sprintf("signature verifies : %s", yesNo(checks.SignatureVerifies)),
|
|
471
|
-
fmt.Sprintf("payload hash matches : %s", yesNo(checks.PayloadHashMatches)),
|
|
472
|
-
fmt.Sprintf("minimization strips sensitive terms: %s", yesNo(checks.MinimizationRespected)),
|
|
473
|
-
fmt.Sprintf("scope complete : %s", yesNo(checks.ScopeComplete)),
|
|
474
|
-
fmt.Sprintf("authorization allowed : %s", yesNo(checks.AuthorizationAllowed)),
|
|
475
|
-
fmt.Sprintf("high-sugar banner : %s", yesNo(checks.HighSugarBanner)),
|
|
476
|
-
fmt.Sprintf("alternative lowers sugar : %s", yesNo(checks.AlternativeLowersSugar)),
|
|
477
|
-
fmt.Sprintf("duty timing consistent : %s", yesNo(checks.DutyTimingConsistent)),
|
|
478
|
-
fmt.Sprintf("marketing prohibited : %s", yesNo(checks.MarketingProhibited)),
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
allChecksPass := checks.SignatureVerifies &&
|
|
482
|
-
checks.PayloadHashMatches &&
|
|
483
|
-
checks.MinimizationRespected &&
|
|
484
|
-
checks.ScopeComplete &&
|
|
485
|
-
checks.AuthorizationAllowed &&
|
|
486
|
-
checks.HighSugarBanner &&
|
|
487
|
-
checks.AlternativeLowersSugar &&
|
|
488
|
-
checks.DutyTimingConsistent &&
|
|
489
|
-
checks.MarketingProhibited
|
|
490
|
-
|
|
491
|
-
result = Result{
|
|
492
|
-
CaseName: data.CaseName,
|
|
493
|
-
Derived: Derived{
|
|
494
|
-
NeedsLowSugar: needsLowSugar,
|
|
495
|
-
HighSugarScanned: highSugarScanned,
|
|
496
|
-
LowerSugarCandidateIDs: lowerIDs,
|
|
497
|
-
RecommendedAlternativeID: recommendedID,
|
|
498
|
-
RecommendedAlternativeName: recommendedName,
|
|
499
|
-
AlternativeLowersSugar: alternativeLowersSugar,
|
|
500
|
-
},
|
|
501
|
-
Envelope: Envelope{
|
|
502
|
-
Insight: insight,
|
|
503
|
-
Policy: policy,
|
|
504
|
-
},
|
|
505
|
-
Integrity: IntegrityResult{
|
|
506
|
-
CanonicalEnvelope: canonical,
|
|
507
|
-
PayloadHashSHA256: payloadHash,
|
|
508
|
-
EnvelopeHmacSHA256: envelopeHMAC,
|
|
509
|
-
VerificationMode: data.Integrity.VerificationMode,
|
|
510
|
-
},
|
|
511
|
-
Answer: Answer{
|
|
512
|
-
Sentence: sentence,
|
|
513
|
-
ScannedProduct: scanned.Name,
|
|
514
|
-
SuggestedAlternative: recommendedName,
|
|
515
|
-
PayloadHashSHA256: payloadHash,
|
|
516
|
-
EnvelopeHmacSHA256: envelopeHMAC,
|
|
517
|
-
},
|
|
518
|
-
ReasonWhy: reasonWhy,
|
|
519
|
-
Checks: checks,
|
|
520
|
-
AllChecksPass: allChecksPass,
|
|
521
|
-
ArcText: strings.Join(arcLines, "\n"),
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return result, nil
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// main is a tiny CLI wrapper around evaluate. It defaults to delfour.data.json,
|
|
528
|
-
// prints ARC text, and switches to JSON output when --json is supplied.
|
|
529
|
-
func main() {
|
|
530
|
-
inputPath := "delfour.data.json"
|
|
531
|
-
jsonMode := false
|
|
532
|
-
|
|
533
|
-
for _, arg := range os.Args[1:] {
|
|
534
|
-
if arg == "--json" {
|
|
535
|
-
jsonMode = true
|
|
536
|
-
} else if !strings.HasPrefix(arg, "--") {
|
|
537
|
-
inputPath = arg
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
data, err := readJSON(inputPath)
|
|
542
|
-
if err != nil {
|
|
543
|
-
fmt.Fprintln(os.Stderr, err)
|
|
544
|
-
os.Exit(1)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
result, err := evaluate(data)
|
|
548
|
-
if err != nil {
|
|
549
|
-
fmt.Fprintln(os.Stderr, err)
|
|
550
|
-
os.Exit(1)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if jsonMode {
|
|
554
|
-
enc := json.NewEncoder(os.Stdout)
|
|
555
|
-
enc.SetIndent("", " ")
|
|
556
|
-
_ = enc.Encode(result)
|
|
557
|
-
} else {
|
|
558
|
-
fmt.Println(result.ArcText)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if !result.AllChecksPass {
|
|
562
|
-
os.Exit(1)
|
|
563
|
-
}
|
|
564
|
-
}
|