eyeling 1.22.0 → 1.22.2
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 +5 -4
- package/examples/arcling/calidor/calidor.data.json +79 -0
- package/examples/arcling/calidor/calidor.expected.json +94 -0
- package/examples/arcling/calidor/calidor.model.go +612 -0
- package/examples/arcling/calidor/calidor.spec.md +166 -0
- package/examples/arcling/delfour/delfour.data.json +0 -1
- package/examples/arcling/delfour/delfour.model.go +18 -0
- package/examples/arcling/flandor/flandor.data.json +0 -1
- package/examples/arcling/flandor/flandor.model.go +25 -0
- package/examples/arcling/medior/medior.data.json +0 -1
- package/examples/arcling/medior/medior.model.go +26 -0
- package/examples/calidor.n3 +500 -0
- package/examples/output/calidor.n3 +29 -0
- package/package.json +1 -1
- package/test/arcling.test.js +50 -15
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
// Calidor is an Arcling case about municipal heatwave support.
|
|
4
|
+
// Raw household heat, vulnerability, and prepaid-energy details stay local.
|
|
5
|
+
// The shareable output is a narrow, expiring support insight plus policy.
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"crypto/hmac"
|
|
9
|
+
"crypto/sha256"
|
|
10
|
+
"encoding/hex"
|
|
11
|
+
"encoding/json"
|
|
12
|
+
"errors"
|
|
13
|
+
"fmt"
|
|
14
|
+
"os"
|
|
15
|
+
"sort"
|
|
16
|
+
"strings"
|
|
17
|
+
"time"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
type Data struct {
|
|
21
|
+
CaseName string `json:"caseName"`
|
|
22
|
+
Municipality string `json:"municipality"`
|
|
23
|
+
Question string `json:"question"`
|
|
24
|
+
Timestamps Timestamps `json:"timestamps"`
|
|
25
|
+
EvaluationContext EvaluationContext `json:"evaluationContext"`
|
|
26
|
+
Thresholds Thresholds `json:"thresholds"`
|
|
27
|
+
LocalProfile LocalProfile `json:"localProfile"`
|
|
28
|
+
HeatStatus HeatStatus `json:"heatStatus"`
|
|
29
|
+
EnergyProfile EnergyProfile `json:"energyProfile"`
|
|
30
|
+
SupportCatalog []SupportPackage `json:"supportCatalog"`
|
|
31
|
+
Budget Budget `json:"budget"`
|
|
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
|
+
AlertLevelAtLeast int `json:"alertLevelAtLeast"`
|
|
53
|
+
IndoorTempCAtLeast float64 `json:"indoorTempCAtLeast"`
|
|
54
|
+
HoursAtOrAboveThresholdAtLeast int `json:"hoursAtOrAboveThresholdAtLeast"`
|
|
55
|
+
EnergyCreditEurAtMost float64 `json:"energyCreditEurAtMost"`
|
|
56
|
+
MinimumActiveNeedCount int `json:"minimumActiveNeedCount"`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type LocalProfile struct {
|
|
60
|
+
VulnerabilityFlags []string `json:"vulnerabilityFlags"`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type HeatStatus struct {
|
|
64
|
+
CurrentAlertLevel int `json:"currentAlertLevel"`
|
|
65
|
+
CurrentIndoorTempC float64 `json:"currentIndoorTempC"`
|
|
66
|
+
HoursAtOrAboveThreshold int `json:"hoursAtOrAboveThreshold"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type EnergyProfile struct {
|
|
70
|
+
RemainingPrepaidCreditEur float64 `json:"remainingPrepaidCreditEur"`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type SupportPackage struct {
|
|
74
|
+
ID string `json:"id"`
|
|
75
|
+
Name string `json:"name"`
|
|
76
|
+
CostEur int `json:"costEur"`
|
|
77
|
+
Capabilities []string `json:"capabilities"`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type Budget struct {
|
|
81
|
+
MaxPackageCostEur int `json:"maxPackageCostEur"`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type InsightPolicy struct {
|
|
85
|
+
ID string `json:"id"`
|
|
86
|
+
Metric string `json:"metric"`
|
|
87
|
+
Type string `json:"type"`
|
|
88
|
+
SupportPolicy string `json:"supportPolicy"`
|
|
89
|
+
PolicyType string `json:"policyType"`
|
|
90
|
+
PolicyProfile string `json:"policyProfile"`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Integrity struct {
|
|
94
|
+
HashAlgorithm string `json:"hashAlgorithm"`
|
|
95
|
+
MacAlgorithm string `json:"macAlgorithm"`
|
|
96
|
+
Secret string `json:"secret"`
|
|
97
|
+
VerificationMode string `json:"verificationMode"`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Insight is the minimized claim that leaves the household boundary.
|
|
101
|
+
type Insight struct {
|
|
102
|
+
CreatedAt string `json:"createdAt"`
|
|
103
|
+
ExpiresAt string `json:"expiresAt"`
|
|
104
|
+
ID string `json:"id"`
|
|
105
|
+
Metric string `json:"metric"`
|
|
106
|
+
Municipality string `json:"municipality"`
|
|
107
|
+
ScopeDevice string `json:"scopeDevice"`
|
|
108
|
+
ScopeEvent string `json:"scopeEvent"`
|
|
109
|
+
SupportPolicy string `json:"supportPolicy"`
|
|
110
|
+
Threshold float64 `json:"threshold"`
|
|
111
|
+
Type string `json:"type"`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
type Constraint struct {
|
|
115
|
+
LeftOperand string `json:"leftOperand"`
|
|
116
|
+
Operator string `json:"operator"`
|
|
117
|
+
RightOperand string `json:"rightOperand"`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
type Duty struct {
|
|
121
|
+
Action string `json:"action"`
|
|
122
|
+
Constraint Constraint `json:"constraint"`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type Permission struct {
|
|
126
|
+
Action string `json:"action"`
|
|
127
|
+
Constraint Constraint `json:"constraint"`
|
|
128
|
+
Target string `json:"target"`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type Prohibition struct {
|
|
132
|
+
Action string `json:"action"`
|
|
133
|
+
Constraint Constraint `json:"constraint"`
|
|
134
|
+
Target string `json:"target"`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
type Policy struct {
|
|
138
|
+
Duty Duty `json:"duty"`
|
|
139
|
+
Permission Permission `json:"permission"`
|
|
140
|
+
Profile string `json:"profile"`
|
|
141
|
+
Prohibition Prohibition `json:"prohibition"`
|
|
142
|
+
Type string `json:"type"`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type Envelope struct {
|
|
146
|
+
Insight Insight `json:"insight"`
|
|
147
|
+
Policy Policy `json:"policy"`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type IntegrityResult struct {
|
|
151
|
+
CanonicalEnvelope string `json:"canonicalEnvelope"`
|
|
152
|
+
PayloadHashSHA256 string `json:"payloadHashSHA256"`
|
|
153
|
+
EnvelopeHmacSHA256 string `json:"envelopeHmacSHA256"`
|
|
154
|
+
VerificationMode string `json:"verificationMode"`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type Answer struct {
|
|
158
|
+
Sentence string `json:"sentence"`
|
|
159
|
+
RecommendedPackage string `json:"recommendedPackage"`
|
|
160
|
+
RequiredCapabilities []string `json:"requiredCapabilities"`
|
|
161
|
+
PayloadHashSHA256 string `json:"payloadHashSHA256"`
|
|
162
|
+
EnvelopeHmacSHA256 string `json:"envelopeHmacSHA256"`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type Derived struct {
|
|
166
|
+
HeatAlertActive bool `json:"heatAlertActive"`
|
|
167
|
+
UnsafeIndoorHeat bool `json:"unsafeIndoorHeat"`
|
|
168
|
+
VulnerabilityPresent bool `json:"vulnerabilityPresent"`
|
|
169
|
+
EnergyConstraint bool `json:"energyConstraint"`
|
|
170
|
+
ActiveNeedCount int `json:"activeNeedCount"`
|
|
171
|
+
PriorityCoolingSupportNeeded bool `json:"priorityCoolingSupportNeeded"`
|
|
172
|
+
RequiredCapabilities []string `json:"requiredCapabilities"`
|
|
173
|
+
EligiblePackageIDs []string `json:"eligiblePackageIds"`
|
|
174
|
+
RecommendedPackageID string `json:"recommendedPackageId"`
|
|
175
|
+
RecommendedPackageName string `json:"recommendedPackageName"`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
type Checks struct {
|
|
179
|
+
SignatureVerifies bool `json:"signatureVerifies"`
|
|
180
|
+
PayloadHashMatches bool `json:"payloadHashMatches"`
|
|
181
|
+
MinimizationRespected bool `json:"minimizationRespected"`
|
|
182
|
+
ScopeComplete bool `json:"scopeComplete"`
|
|
183
|
+
AuthorizationAllowed bool `json:"authorizationAllowed"`
|
|
184
|
+
HeatAlertActive bool `json:"heatAlertActive"`
|
|
185
|
+
UnsafeIndoorHeat bool `json:"unsafeIndoorHeat"`
|
|
186
|
+
PriorityCoolingSupportNeeded bool `json:"priorityCoolingSupportNeeded"`
|
|
187
|
+
RecommendedPackageEligible bool `json:"recommendedPackageEligible"`
|
|
188
|
+
DutyTimingConsistent bool `json:"dutyTimingConsistent"`
|
|
189
|
+
TenantScreeningProhibited bool `json:"tenantScreeningProhibited"`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
type Result struct {
|
|
193
|
+
CaseName string `json:"caseName"`
|
|
194
|
+
Derived Derived `json:"derived"`
|
|
195
|
+
Envelope Envelope `json:"envelope"`
|
|
196
|
+
Integrity IntegrityResult `json:"integrity"`
|
|
197
|
+
Answer Answer `json:"answer"`
|
|
198
|
+
ReasonWhy []string `json:"reasonWhy"`
|
|
199
|
+
Checks Checks `json:"checks"`
|
|
200
|
+
AllChecksPass bool `json:"allChecksPass"`
|
|
201
|
+
ArcText string `json:"arcText"`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func must(condition bool, message string) error {
|
|
205
|
+
if !condition {
|
|
206
|
+
return errors.New(message)
|
|
207
|
+
}
|
|
208
|
+
return nil
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func readJSON(path string) (Data, error) {
|
|
212
|
+
var data Data
|
|
213
|
+
b, err := os.ReadFile(path)
|
|
214
|
+
if err != nil {
|
|
215
|
+
return data, err
|
|
216
|
+
}
|
|
217
|
+
err = json.Unmarshal(b, &data)
|
|
218
|
+
return data, err
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func parseTime(s string) time.Time {
|
|
222
|
+
t, err := time.Parse(time.RFC3339, s)
|
|
223
|
+
if err != nil {
|
|
224
|
+
panic(err)
|
|
225
|
+
}
|
|
226
|
+
return t
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The 4-file layout moves basic validation into the model itself.
|
|
230
|
+
func validate(data Data) error {
|
|
231
|
+
if err := must(data.CaseName != "", "caseName is required"); err != nil {
|
|
232
|
+
return err
|
|
233
|
+
}
|
|
234
|
+
if err := must(data.Municipality != "", "municipality is required"); err != nil {
|
|
235
|
+
return err
|
|
236
|
+
}
|
|
237
|
+
if err := must(len(data.SupportCatalog) > 0, "supportCatalog is required"); err != nil {
|
|
238
|
+
return err
|
|
239
|
+
}
|
|
240
|
+
if err := must(data.EvaluationContext.Purpose != "", "evaluationContext.purpose is required"); err != nil {
|
|
241
|
+
return err
|
|
242
|
+
}
|
|
243
|
+
if err := must(data.EvaluationContext.RequestAction != "", "evaluationContext.requestAction is required"); err != nil {
|
|
244
|
+
return err
|
|
245
|
+
}
|
|
246
|
+
if err := must(data.InsightPolicy.ID != "", "insightPolicy.id is required"); err != nil {
|
|
247
|
+
return err
|
|
248
|
+
}
|
|
249
|
+
if err := must(data.Integrity.Secret != "", "integrity.secret is required"); err != nil {
|
|
250
|
+
return err
|
|
251
|
+
}
|
|
252
|
+
return nil
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
func containsAll(have []string, required []string) bool {
|
|
256
|
+
set := map[string]bool{}
|
|
257
|
+
for _, item := range have {
|
|
258
|
+
set[item] = true
|
|
259
|
+
}
|
|
260
|
+
for _, item := range required {
|
|
261
|
+
if !set[item] {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
func deriveInsight(data Data) Insight {
|
|
269
|
+
return Insight{
|
|
270
|
+
CreatedAt: data.Timestamps.CreatedAt,
|
|
271
|
+
ExpiresAt: data.Timestamps.ExpiresAt,
|
|
272
|
+
ID: data.InsightPolicy.ID,
|
|
273
|
+
Metric: data.InsightPolicy.Metric,
|
|
274
|
+
Municipality: data.Municipality,
|
|
275
|
+
ScopeDevice: data.EvaluationContext.ScopeDevice,
|
|
276
|
+
ScopeEvent: data.EvaluationContext.ScopeEvent,
|
|
277
|
+
SupportPolicy: data.InsightPolicy.SupportPolicy,
|
|
278
|
+
Threshold: float64(data.Thresholds.MinimumActiveNeedCount),
|
|
279
|
+
Type: data.InsightPolicy.Type,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
func derivePolicy(data Data) Policy {
|
|
284
|
+
return Policy{
|
|
285
|
+
Duty: Duty{
|
|
286
|
+
Action: "odrl:delete",
|
|
287
|
+
Constraint: Constraint{
|
|
288
|
+
LeftOperand: "odrl:dateTime",
|
|
289
|
+
Operator: "odrl:eq",
|
|
290
|
+
RightOperand: data.Timestamps.ExpiresAt,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
Permission: Permission{
|
|
294
|
+
Action: data.EvaluationContext.RequestAction,
|
|
295
|
+
Constraint: Constraint{
|
|
296
|
+
LeftOperand: "odrl:purpose",
|
|
297
|
+
Operator: "odrl:eq",
|
|
298
|
+
RightOperand: data.EvaluationContext.Purpose,
|
|
299
|
+
},
|
|
300
|
+
Target: data.InsightPolicy.ID,
|
|
301
|
+
},
|
|
302
|
+
Profile: data.InsightPolicy.PolicyProfile,
|
|
303
|
+
Prohibition: Prohibition{
|
|
304
|
+
Action: "odrl:distribute",
|
|
305
|
+
Constraint: Constraint{
|
|
306
|
+
LeftOperand: "odrl:purpose",
|
|
307
|
+
Operator: "odrl:eq",
|
|
308
|
+
RightOperand: data.EvaluationContext.ProhibitedReusePurpose,
|
|
309
|
+
},
|
|
310
|
+
Target: data.InsightPolicy.ID,
|
|
311
|
+
},
|
|
312
|
+
Type: data.InsightPolicy.PolicyType,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Canonical serialization is explicit because the integrity vector depends
|
|
317
|
+
// on these exact bytes, including threshold written as 3.0.
|
|
318
|
+
func canonicalEnvelope(insight Insight, policy Policy) string {
|
|
319
|
+
return fmt.Sprintf(
|
|
320
|
+
"{\"insight\":{\"createdAt\":\"%s\",\"expiresAt\":\"%s\",\"id\":\"%s\",\"metric\":\"%s\",\"municipality\":\"%s\",\"scopeDevice\":\"%s\",\"scopeEvent\":\"%s\",\"supportPolicy\":\"%s\",\"threshold\":3.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\"}}",
|
|
321
|
+
insight.CreatedAt,
|
|
322
|
+
insight.ExpiresAt,
|
|
323
|
+
insight.ID,
|
|
324
|
+
insight.Metric,
|
|
325
|
+
insight.Municipality,
|
|
326
|
+
insight.ScopeDevice,
|
|
327
|
+
insight.ScopeEvent,
|
|
328
|
+
insight.SupportPolicy,
|
|
329
|
+
insight.Type,
|
|
330
|
+
policy.Duty.Action,
|
|
331
|
+
policy.Duty.Constraint.LeftOperand,
|
|
332
|
+
policy.Duty.Constraint.Operator,
|
|
333
|
+
policy.Duty.Constraint.RightOperand,
|
|
334
|
+
policy.Permission.Action,
|
|
335
|
+
policy.Permission.Constraint.LeftOperand,
|
|
336
|
+
policy.Permission.Constraint.Operator,
|
|
337
|
+
policy.Permission.Constraint.RightOperand,
|
|
338
|
+
policy.Permission.Target,
|
|
339
|
+
policy.Profile,
|
|
340
|
+
policy.Prohibition.Action,
|
|
341
|
+
policy.Prohibition.Constraint.LeftOperand,
|
|
342
|
+
policy.Prohibition.Constraint.Operator,
|
|
343
|
+
policy.Prohibition.Constraint.RightOperand,
|
|
344
|
+
policy.Prohibition.Target,
|
|
345
|
+
policy.Type,
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func sha256Hex(s string) string {
|
|
350
|
+
sum := sha256.Sum256([]byte(s))
|
|
351
|
+
return hex.EncodeToString(sum[:])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func hmacSHA256Hex(secret, s string) string {
|
|
355
|
+
mac := hmac.New(sha256.New, []byte(secret))
|
|
356
|
+
_, _ = mac.Write([]byte(s))
|
|
357
|
+
return hex.EncodeToString(mac.Sum(nil))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
func yesNo(v bool) string {
|
|
361
|
+
if v {
|
|
362
|
+
return "yes"
|
|
363
|
+
}
|
|
364
|
+
return "no"
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// evaluate implements the normative case logic:
|
|
368
|
+
// derive need signals, choose the lowest-cost eligible package,
|
|
369
|
+
// construct the envelope, then compute integrity and governance checks.
|
|
370
|
+
func evaluate(data Data) (Result, error) {
|
|
371
|
+
var result Result
|
|
372
|
+
|
|
373
|
+
if err := validate(data); err != nil {
|
|
374
|
+
return result, err
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
heatAlertActive := data.HeatStatus.CurrentAlertLevel >= data.Thresholds.AlertLevelAtLeast
|
|
378
|
+
unsafeIndoorHeat := data.HeatStatus.CurrentIndoorTempC >= data.Thresholds.IndoorTempCAtLeast &&
|
|
379
|
+
data.HeatStatus.HoursAtOrAboveThreshold >= data.Thresholds.HoursAtOrAboveThresholdAtLeast
|
|
380
|
+
vulnerabilityPresent := len(data.LocalProfile.VulnerabilityFlags) > 0
|
|
381
|
+
energyConstraint := data.EnergyProfile.RemainingPrepaidCreditEur <= data.Thresholds.EnergyCreditEurAtMost
|
|
382
|
+
|
|
383
|
+
activeNeedCount := 0
|
|
384
|
+
for _, v := range []bool{heatAlertActive, unsafeIndoorHeat, vulnerabilityPresent, energyConstraint} {
|
|
385
|
+
if v {
|
|
386
|
+
activeNeedCount++
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
priorityCoolingSupportNeeded := activeNeedCount >= data.Thresholds.MinimumActiveNeedCount
|
|
391
|
+
|
|
392
|
+
requiredSet := map[string]bool{}
|
|
393
|
+
if heatAlertActive && unsafeIndoorHeat {
|
|
394
|
+
requiredSet["cooling_kit"] = true
|
|
395
|
+
}
|
|
396
|
+
if vulnerabilityPresent {
|
|
397
|
+
requiredSet["welfare_check"] = true
|
|
398
|
+
requiredSet["transport"] = true
|
|
399
|
+
}
|
|
400
|
+
if energyConstraint {
|
|
401
|
+
requiredSet["bill_credit"] = true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
requiredCapabilities := make([]string, 0, len(requiredSet))
|
|
405
|
+
for capability := range requiredSet {
|
|
406
|
+
requiredCapabilities = append(requiredCapabilities, capability)
|
|
407
|
+
}
|
|
408
|
+
sort.Strings(requiredCapabilities)
|
|
409
|
+
|
|
410
|
+
eligiblePackages := []SupportPackage{}
|
|
411
|
+
for _, pkg := range data.SupportCatalog {
|
|
412
|
+
if pkg.CostEur <= data.Budget.MaxPackageCostEur && containsAll(pkg.Capabilities, requiredCapabilities) {
|
|
413
|
+
eligiblePackages = append(eligiblePackages, pkg)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
sort.Slice(eligiblePackages, func(i, j int) bool {
|
|
417
|
+
if eligiblePackages[i].CostEur != eligiblePackages[j].CostEur {
|
|
418
|
+
return eligiblePackages[i].CostEur < eligiblePackages[j].CostEur
|
|
419
|
+
}
|
|
420
|
+
return eligiblePackages[i].ID < eligiblePackages[j].ID
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
if len(eligiblePackages) == 0 {
|
|
424
|
+
return result, errors.New("no eligible package found")
|
|
425
|
+
}
|
|
426
|
+
recommended := eligiblePackages[0]
|
|
427
|
+
|
|
428
|
+
insight := deriveInsight(data)
|
|
429
|
+
policy := derivePolicy(data)
|
|
430
|
+
canonical := canonicalEnvelope(insight, policy)
|
|
431
|
+
payloadHash := sha256Hex(canonical)
|
|
432
|
+
envelopeHMAC := hmacSHA256Hex(data.Integrity.Secret, canonical)
|
|
433
|
+
|
|
434
|
+
insightBytes, _ := json.Marshal(insight)
|
|
435
|
+
insightText := strings.ToLower(string(insightBytes))
|
|
436
|
+
minimizationRespected := !strings.Contains(insightText, "heat_sensitive_condition") &&
|
|
437
|
+
!strings.Contains(insightText, "mobility_limitation") &&
|
|
438
|
+
!strings.Contains(insightText, "credit") &&
|
|
439
|
+
!strings.Contains(insightText, "meter_trace")
|
|
440
|
+
|
|
441
|
+
scopeComplete := insight.ScopeDevice != "" && insight.ScopeEvent != "" && insight.ExpiresAt != ""
|
|
442
|
+
|
|
443
|
+
authorizationAllowed := data.EvaluationContext.RequestAction == "odrl:use" &&
|
|
444
|
+
data.EvaluationContext.Purpose == "heatwave_response" &&
|
|
445
|
+
!parseTime(data.Timestamps.AuthorizedAt).After(parseTime(data.Timestamps.ExpiresAt))
|
|
446
|
+
|
|
447
|
+
recommendedPackageEligible := containsAll(recommended.Capabilities, requiredCapabilities) &&
|
|
448
|
+
recommended.CostEur <= data.Budget.MaxPackageCostEur
|
|
449
|
+
|
|
450
|
+
dutyTimingConsistent := !parseTime(data.Timestamps.DutyPerformedAt).After(parseTime(data.Timestamps.ExpiresAt))
|
|
451
|
+
|
|
452
|
+
tenantScreeningProhibited := policy.Prohibition.Action == "odrl:distribute" &&
|
|
453
|
+
policy.Prohibition.Constraint.RightOperand == "tenant_screening"
|
|
454
|
+
|
|
455
|
+
signatureVerifies := data.Integrity.VerificationMode == "trustedPrecomputedInput" &&
|
|
456
|
+
envelopeHMAC == hmacSHA256Hex(data.Integrity.Secret, canonical)
|
|
457
|
+
|
|
458
|
+
payloadHashMatches := payloadHash == sha256Hex(canonical)
|
|
459
|
+
|
|
460
|
+
eligiblePackageIDs := make([]string, 0, len(eligiblePackages))
|
|
461
|
+
for _, pkg := range eligiblePackages {
|
|
462
|
+
eligiblePackageIDs = append(eligiblePackageIDs, pkg.ID)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
checks := Checks{
|
|
466
|
+
SignatureVerifies: signatureVerifies,
|
|
467
|
+
PayloadHashMatches: payloadHashMatches,
|
|
468
|
+
MinimizationRespected: minimizationRespected,
|
|
469
|
+
ScopeComplete: scopeComplete,
|
|
470
|
+
AuthorizationAllowed: authorizationAllowed,
|
|
471
|
+
HeatAlertActive: heatAlertActive,
|
|
472
|
+
UnsafeIndoorHeat: unsafeIndoorHeat,
|
|
473
|
+
PriorityCoolingSupportNeeded: priorityCoolingSupportNeeded,
|
|
474
|
+
RecommendedPackageEligible: recommendedPackageEligible,
|
|
475
|
+
DutyTimingConsistent: dutyTimingConsistent,
|
|
476
|
+
TenantScreeningProhibited: tenantScreeningProhibited,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
answerSentence := fmt.Sprintf(
|
|
480
|
+
"The city is allowed to use a narrow heatwave-response insight and recommends %s for this household.",
|
|
481
|
+
recommended.Name,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
reasonWhy := []string{
|
|
485
|
+
"The household gateway converts local heat, vulnerability, and prepaid-energy stress into an expiring priority-support insight rather than sharing raw household traces or sensitive details.",
|
|
486
|
+
fmt.Sprintf("recommended package: %s", recommended.Name),
|
|
487
|
+
fmt.Sprintf("required capabilities: %s", strings.Join(requiredCapabilities, ", ")),
|
|
488
|
+
fmt.Sprintf("payload SHA-256 : %s", payloadHash),
|
|
489
|
+
fmt.Sprintf("HMAC-SHA256 : %s", envelopeHMAC),
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
arcLines := []string{
|
|
493
|
+
"=== Answer ===",
|
|
494
|
+
answerSentence,
|
|
495
|
+
"",
|
|
496
|
+
"=== Reason Why ===",
|
|
497
|
+
}
|
|
498
|
+
arcLines = append(arcLines, reasonWhy...)
|
|
499
|
+
arcLines = append(arcLines,
|
|
500
|
+
"",
|
|
501
|
+
"=== Check ===",
|
|
502
|
+
fmt.Sprintf("signature verifies : %s", yesNo(checks.SignatureVerifies)),
|
|
503
|
+
fmt.Sprintf("payload hash matches : %s", yesNo(checks.PayloadHashMatches)),
|
|
504
|
+
fmt.Sprintf("minimization strips sensitive terms: %s", yesNo(checks.MinimizationRespected)),
|
|
505
|
+
fmt.Sprintf("scope complete : %s", yesNo(checks.ScopeComplete)),
|
|
506
|
+
fmt.Sprintf("authorization allowed : %s", yesNo(checks.AuthorizationAllowed)),
|
|
507
|
+
fmt.Sprintf("heat-alert active : %s", yesNo(checks.HeatAlertActive)),
|
|
508
|
+
fmt.Sprintf("unsafe indoor heat : %s", yesNo(checks.UnsafeIndoorHeat)),
|
|
509
|
+
fmt.Sprintf("priority cooling support needed : %s", yesNo(checks.PriorityCoolingSupportNeeded)),
|
|
510
|
+
fmt.Sprintf("recommended package eligible : %s", yesNo(checks.RecommendedPackageEligible)),
|
|
511
|
+
fmt.Sprintf("duty timing consistent : %s", yesNo(checks.DutyTimingConsistent)),
|
|
512
|
+
fmt.Sprintf("tenant screening prohibited : %s", yesNo(checks.TenantScreeningProhibited)),
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
allChecksPass := true
|
|
516
|
+
for _, v := range []bool{
|
|
517
|
+
checks.SignatureVerifies,
|
|
518
|
+
checks.PayloadHashMatches,
|
|
519
|
+
checks.MinimizationRespected,
|
|
520
|
+
checks.ScopeComplete,
|
|
521
|
+
checks.AuthorizationAllowed,
|
|
522
|
+
checks.HeatAlertActive,
|
|
523
|
+
checks.UnsafeIndoorHeat,
|
|
524
|
+
checks.PriorityCoolingSupportNeeded,
|
|
525
|
+
checks.RecommendedPackageEligible,
|
|
526
|
+
checks.DutyTimingConsistent,
|
|
527
|
+
checks.TenantScreeningProhibited,
|
|
528
|
+
} {
|
|
529
|
+
if !v {
|
|
530
|
+
allChecksPass = false
|
|
531
|
+
break
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
result = Result{
|
|
536
|
+
CaseName: data.CaseName,
|
|
537
|
+
Derived: Derived{
|
|
538
|
+
HeatAlertActive: heatAlertActive,
|
|
539
|
+
UnsafeIndoorHeat: unsafeIndoorHeat,
|
|
540
|
+
VulnerabilityPresent: vulnerabilityPresent,
|
|
541
|
+
EnergyConstraint: energyConstraint,
|
|
542
|
+
ActiveNeedCount: activeNeedCount,
|
|
543
|
+
PriorityCoolingSupportNeeded: priorityCoolingSupportNeeded,
|
|
544
|
+
RequiredCapabilities: requiredCapabilities,
|
|
545
|
+
EligiblePackageIDs: eligiblePackageIDs,
|
|
546
|
+
RecommendedPackageID: recommended.ID,
|
|
547
|
+
RecommendedPackageName: recommended.Name,
|
|
548
|
+
},
|
|
549
|
+
Envelope: Envelope{
|
|
550
|
+
Insight: insight,
|
|
551
|
+
Policy: policy,
|
|
552
|
+
},
|
|
553
|
+
Integrity: IntegrityResult{
|
|
554
|
+
CanonicalEnvelope: canonical,
|
|
555
|
+
PayloadHashSHA256: payloadHash,
|
|
556
|
+
EnvelopeHmacSHA256: envelopeHMAC,
|
|
557
|
+
VerificationMode: data.Integrity.VerificationMode,
|
|
558
|
+
},
|
|
559
|
+
Answer: Answer{
|
|
560
|
+
Sentence: answerSentence,
|
|
561
|
+
RecommendedPackage: recommended.Name,
|
|
562
|
+
RequiredCapabilities: requiredCapabilities,
|
|
563
|
+
PayloadHashSHA256: payloadHash,
|
|
564
|
+
EnvelopeHmacSHA256: envelopeHMAC,
|
|
565
|
+
},
|
|
566
|
+
ReasonWhy: reasonWhy,
|
|
567
|
+
Checks: checks,
|
|
568
|
+
AllChecksPass: allChecksPass,
|
|
569
|
+
ArcText: strings.Join(arcLines, "\n"),
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return result, nil
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// main keeps the CLI behavior aligned with the other Arcling Go models:
|
|
576
|
+
// default to the local data file, print ARC text by default, or JSON with --json.
|
|
577
|
+
func main() {
|
|
578
|
+
inputPath := "calidor.data.json"
|
|
579
|
+
jsonMode := false
|
|
580
|
+
|
|
581
|
+
for _, arg := range os.Args[1:] {
|
|
582
|
+
if arg == "--json" {
|
|
583
|
+
jsonMode = true
|
|
584
|
+
} else if !strings.HasPrefix(arg, "--") {
|
|
585
|
+
inputPath = arg
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
data, err := readJSON(inputPath)
|
|
590
|
+
if err != nil {
|
|
591
|
+
fmt.Fprintln(os.Stderr, err)
|
|
592
|
+
os.Exit(1)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
result, err := evaluate(data)
|
|
596
|
+
if err != nil {
|
|
597
|
+
fmt.Fprintln(os.Stderr, err)
|
|
598
|
+
os.Exit(1)
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if jsonMode {
|
|
602
|
+
enc := json.NewEncoder(os.Stdout)
|
|
603
|
+
enc.SetIndent("", " ")
|
|
604
|
+
_ = enc.Encode(result)
|
|
605
|
+
} else {
|
|
606
|
+
fmt.Println(result.ArcText)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if !result.AllChecksPass {
|
|
610
|
+
os.Exit(1)
|
|
611
|
+
}
|
|
612
|
+
}
|