eyeling 1.21.9 → 1.22.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.
- package/HANDBOOK.md +45 -0
- package/dist/browser/eyeling.browser.js +89 -33
- package/examples/arcling/README.md +17 -21
- package/examples/arcling/delfour/delfour.model.go +546 -0
- package/examples/arcling/delfour/delfour.spec.md +1 -2
- package/examples/arcling/flandor/flandor.model.go +630 -0
- package/examples/arcling/flandor/flandor.spec.md +1 -2
- package/examples/arcling/medior/medior.model.go +626 -0
- package/examples/arcling/medior/medior.spec.md +1 -2
- package/eyeling.js +88 -33
- package/lib/builtins.js +72 -29
- package/lib/rules.js +16 -4
- package/package.json +1 -1
- package/test/api.test.js +117 -0
- package/test/arcling.test.js +34 -16
- package/examples/arcling/delfour/delfour.instance.schema.json +0 -201
- package/examples/arcling/delfour/delfour.model.mjs +0 -273
- package/examples/arcling/flandor/flandor.instance.schema.json +0 -285
- package/examples/arcling/flandor/flandor.model.mjs +0 -303
- package/examples/arcling/medior/medior.instance.schema.json +0 -275
- package/examples/arcling/medior/medior.model.mjs +0 -344
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://example.org/schema/delfour.instance.schema.json",
|
|
4
|
-
"title": "Arcling Delfour instance",
|
|
5
|
-
"type": "object",
|
|
6
|
-
"additionalProperties": false,
|
|
7
|
-
"required": [
|
|
8
|
-
"$schema",
|
|
9
|
-
"caseName",
|
|
10
|
-
"retailer",
|
|
11
|
-
"question",
|
|
12
|
-
"timestamps",
|
|
13
|
-
"evaluationContext",
|
|
14
|
-
"thresholds",
|
|
15
|
-
"householdProfile",
|
|
16
|
-
"catalog",
|
|
17
|
-
"scan",
|
|
18
|
-
"insightPolicy",
|
|
19
|
-
"integrity"
|
|
20
|
-
],
|
|
21
|
-
"properties": {
|
|
22
|
-
"$schema": {
|
|
23
|
-
"type": "string"
|
|
24
|
-
},
|
|
25
|
-
"caseName": {
|
|
26
|
-
"type": "string",
|
|
27
|
-
"minLength": 1
|
|
28
|
-
},
|
|
29
|
-
"retailer": {
|
|
30
|
-
"type": "string",
|
|
31
|
-
"minLength": 1
|
|
32
|
-
},
|
|
33
|
-
"question": {
|
|
34
|
-
"type": "string",
|
|
35
|
-
"minLength": 1
|
|
36
|
-
},
|
|
37
|
-
"timestamps": {
|
|
38
|
-
"type": "object",
|
|
39
|
-
"additionalProperties": false,
|
|
40
|
-
"required": ["createdAt", "expiresAt", "authorizedAt", "dutyPerformedAt"],
|
|
41
|
-
"properties": {
|
|
42
|
-
"createdAt": {
|
|
43
|
-
"type": "string",
|
|
44
|
-
"format": "date-time"
|
|
45
|
-
},
|
|
46
|
-
"expiresAt": {
|
|
47
|
-
"type": "string",
|
|
48
|
-
"format": "date-time"
|
|
49
|
-
},
|
|
50
|
-
"authorizedAt": {
|
|
51
|
-
"type": "string",
|
|
52
|
-
"format": "date-time"
|
|
53
|
-
},
|
|
54
|
-
"dutyPerformedAt": {
|
|
55
|
-
"type": "string",
|
|
56
|
-
"format": "date-time"
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
"evaluationContext": {
|
|
61
|
-
"type": "object",
|
|
62
|
-
"additionalProperties": false,
|
|
63
|
-
"required": ["scopeDevice", "scopeEvent", "purpose", "prohibitedReusePurpose", "requestAction"],
|
|
64
|
-
"properties": {
|
|
65
|
-
"scopeDevice": {
|
|
66
|
-
"type": "string",
|
|
67
|
-
"minLength": 1
|
|
68
|
-
},
|
|
69
|
-
"scopeEvent": {
|
|
70
|
-
"type": "string",
|
|
71
|
-
"minLength": 1
|
|
72
|
-
},
|
|
73
|
-
"purpose": {
|
|
74
|
-
"type": "string",
|
|
75
|
-
"minLength": 1
|
|
76
|
-
},
|
|
77
|
-
"prohibitedReusePurpose": {
|
|
78
|
-
"type": "string",
|
|
79
|
-
"minLength": 1
|
|
80
|
-
},
|
|
81
|
-
"requestAction": {
|
|
82
|
-
"type": "string",
|
|
83
|
-
"minLength": 1
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
"thresholds": {
|
|
88
|
-
"type": "object",
|
|
89
|
-
"additionalProperties": false,
|
|
90
|
-
"required": ["sugarPerServingGAtLeast"],
|
|
91
|
-
"properties": {
|
|
92
|
-
"sugarPerServingGAtLeast": {
|
|
93
|
-
"type": "number"
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
"householdProfile": {
|
|
98
|
-
"type": "object",
|
|
99
|
-
"additionalProperties": false,
|
|
100
|
-
"required": ["condition"],
|
|
101
|
-
"properties": {
|
|
102
|
-
"condition": {
|
|
103
|
-
"type": "string",
|
|
104
|
-
"minLength": 1
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
"catalog": {
|
|
109
|
-
"type": "array",
|
|
110
|
-
"minItems": 1,
|
|
111
|
-
"items": {
|
|
112
|
-
"type": "object",
|
|
113
|
-
"additionalProperties": false,
|
|
114
|
-
"required": ["id", "name", "sugarTenths", "sugarPerServing"],
|
|
115
|
-
"properties": {
|
|
116
|
-
"id": {
|
|
117
|
-
"type": "string",
|
|
118
|
-
"minLength": 1
|
|
119
|
-
},
|
|
120
|
-
"name": {
|
|
121
|
-
"type": "string",
|
|
122
|
-
"minLength": 1
|
|
123
|
-
},
|
|
124
|
-
"sugarTenths": {
|
|
125
|
-
"type": "integer",
|
|
126
|
-
"minimum": 0
|
|
127
|
-
},
|
|
128
|
-
"sugarPerServing": {
|
|
129
|
-
"type": "number",
|
|
130
|
-
"minimum": 0
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
"scan": {
|
|
136
|
-
"type": "object",
|
|
137
|
-
"additionalProperties": false,
|
|
138
|
-
"required": ["scannedProductId"],
|
|
139
|
-
"properties": {
|
|
140
|
-
"scannedProductId": {
|
|
141
|
-
"type": "string",
|
|
142
|
-
"minLength": 1
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
"insightPolicy": {
|
|
147
|
-
"type": "object",
|
|
148
|
-
"additionalProperties": false,
|
|
149
|
-
"required": ["id", "metric", "type", "suggestionPolicy", "policyType", "policyProfile"],
|
|
150
|
-
"properties": {
|
|
151
|
-
"id": {
|
|
152
|
-
"type": "string",
|
|
153
|
-
"minLength": 1
|
|
154
|
-
},
|
|
155
|
-
"metric": {
|
|
156
|
-
"type": "string",
|
|
157
|
-
"minLength": 1
|
|
158
|
-
},
|
|
159
|
-
"type": {
|
|
160
|
-
"type": "string",
|
|
161
|
-
"minLength": 1
|
|
162
|
-
},
|
|
163
|
-
"suggestionPolicy": {
|
|
164
|
-
"type": "string",
|
|
165
|
-
"minLength": 1
|
|
166
|
-
},
|
|
167
|
-
"policyType": {
|
|
168
|
-
"type": "string",
|
|
169
|
-
"minLength": 1
|
|
170
|
-
},
|
|
171
|
-
"policyProfile": {
|
|
172
|
-
"type": "string",
|
|
173
|
-
"minLength": 1
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
"integrity": {
|
|
178
|
-
"type": "object",
|
|
179
|
-
"additionalProperties": false,
|
|
180
|
-
"required": ["hashAlgorithm", "macAlgorithm", "secret", "verificationMode"],
|
|
181
|
-
"properties": {
|
|
182
|
-
"hashAlgorithm": {
|
|
183
|
-
"type": "string",
|
|
184
|
-
"const": "SHA-256"
|
|
185
|
-
},
|
|
186
|
-
"macAlgorithm": {
|
|
187
|
-
"type": "string",
|
|
188
|
-
"const": "HMAC-SHA-256"
|
|
189
|
-
},
|
|
190
|
-
"secret": {
|
|
191
|
-
"type": "string",
|
|
192
|
-
"minLength": 1
|
|
193
|
-
},
|
|
194
|
-
"verificationMode": {
|
|
195
|
-
"type": "string",
|
|
196
|
-
"minLength": 1
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
import { webcrypto } from 'node:crypto';
|
|
6
|
-
|
|
7
|
-
const subtle = webcrypto.subtle;
|
|
8
|
-
const encoder = new TextEncoder();
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
|
|
12
|
-
function assert(condition, message) {
|
|
13
|
-
if (!condition) throw new Error(message);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function sha256Hex(text) {
|
|
17
|
-
const digest = await subtle.digest('SHA-256', encoder.encode(text));
|
|
18
|
-
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function hmacSha256Hex(secret, text) {
|
|
22
|
-
const key = await subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
23
|
-
const signature = await subtle.sign('HMAC', key, encoder.encode(text));
|
|
24
|
-
return Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function validateInstance(data) {
|
|
28
|
-
assert(typeof data?.caseName === 'string' && data.caseName.length > 0, 'caseName is required');
|
|
29
|
-
assert(typeof data?.retailer === 'string' && data.retailer.length > 0, 'retailer is required');
|
|
30
|
-
assert(Array.isArray(data?.catalog) && data.catalog.length > 0, 'catalog is required');
|
|
31
|
-
assert(typeof data?.scan?.scannedProductId === 'string', 'scan.scannedProductId is required');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function productById(data, id) {
|
|
35
|
-
return data.catalog.find((product) => product.id === id) ?? null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function clauseR1_needsLowSugar(data) {
|
|
39
|
-
return data.householdProfile.condition === 'Diabetes';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function clauseR2_highSugarScanned(data, scannedProduct) {
|
|
43
|
-
return scannedProduct.sugarPerServing >= data.thresholds.sugarPerServingGAtLeast;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function clauseR3_lowerSugarCandidates(data, scannedProduct) {
|
|
47
|
-
return data.catalog
|
|
48
|
-
.filter((product) => product.sugarTenths < scannedProduct.sugarTenths)
|
|
49
|
-
.sort((a, b) => a.sugarTenths - b.sugarTenths);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function clauseR4_recommendedAlternative(data, scannedProduct) {
|
|
53
|
-
const candidates = clauseR3_lowerSugarCandidates(data, scannedProduct);
|
|
54
|
-
return {
|
|
55
|
-
candidates,
|
|
56
|
-
recommended: candidates[0] ?? null,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function clauseR5_alternativeLowersSugar(scannedProduct, recommended) {
|
|
61
|
-
return Boolean(recommended) && recommended.sugarTenths < scannedProduct.sugarTenths;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function deriveInsight(data) {
|
|
65
|
-
return {
|
|
66
|
-
createdAt: data.timestamps.createdAt,
|
|
67
|
-
expiresAt: data.timestamps.expiresAt,
|
|
68
|
-
id: data.insightPolicy.id,
|
|
69
|
-
metric: data.insightPolicy.metric,
|
|
70
|
-
retailer: data.retailer,
|
|
71
|
-
scopeDevice: data.evaluationContext.scopeDevice,
|
|
72
|
-
scopeEvent: data.evaluationContext.scopeEvent,
|
|
73
|
-
suggestionPolicy: data.insightPolicy.suggestionPolicy,
|
|
74
|
-
threshold: data.thresholds.sugarPerServingGAtLeast,
|
|
75
|
-
type: data.insightPolicy.type,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function derivePolicy(data) {
|
|
80
|
-
return {
|
|
81
|
-
duty: {
|
|
82
|
-
action: 'odrl:delete',
|
|
83
|
-
constraint: {
|
|
84
|
-
leftOperand: 'odrl:dateTime',
|
|
85
|
-
operator: 'odrl:eq',
|
|
86
|
-
rightOperand: data.timestamps.expiresAt,
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
permission: {
|
|
90
|
-
action: data.evaluationContext.requestAction,
|
|
91
|
-
constraint: {
|
|
92
|
-
leftOperand: 'odrl:purpose',
|
|
93
|
-
operator: 'odrl:eq',
|
|
94
|
-
rightOperand: data.evaluationContext.purpose,
|
|
95
|
-
},
|
|
96
|
-
target: data.insightPolicy.id,
|
|
97
|
-
},
|
|
98
|
-
profile: data.insightPolicy.policyProfile,
|
|
99
|
-
prohibition: {
|
|
100
|
-
action: 'odrl:distribute',
|
|
101
|
-
constraint: {
|
|
102
|
-
leftOperand: 'odrl:purpose',
|
|
103
|
-
operator: 'odrl:eq',
|
|
104
|
-
rightOperand: data.evaluationContext.prohibitedReusePurpose,
|
|
105
|
-
},
|
|
106
|
-
target: data.insightPolicy.id,
|
|
107
|
-
},
|
|
108
|
-
type: data.insightPolicy.policyType,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function clauseG1_authorizedUse(data) {
|
|
113
|
-
return (
|
|
114
|
-
data.evaluationContext.requestAction === 'odrl:use' &&
|
|
115
|
-
data.evaluationContext.purpose === 'shopping_assist' &&
|
|
116
|
-
Date.parse(data.timestamps.authorizedAt) <= Date.parse(data.timestamps.expiresAt)
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function clauseG2_marketingProhibited(policy) {
|
|
121
|
-
return (
|
|
122
|
-
policy.prohibition?.action === 'odrl:distribute' && policy.prohibition?.constraint?.rightOperand === 'marketing'
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function clauseG3_dutyTimely(data) {
|
|
127
|
-
return Date.parse(data.timestamps.dutyPerformedAt) <= Date.parse(data.timestamps.expiresAt);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function clauseM1_canonicalEnvelope(data) {
|
|
131
|
-
const insight = deriveInsight(data);
|
|
132
|
-
const policy = derivePolicy(data);
|
|
133
|
-
const envelope = { insight, policy };
|
|
134
|
-
const canonicalEnvelope = `{"insight":{"createdAt":"${insight.createdAt}","expiresAt":"${insight.expiresAt}","id":"${insight.id}","metric":"${insight.metric}","retailer":"${insight.retailer}","scopeDevice":"${insight.scopeDevice}","scopeEvent":"${insight.scopeEvent}","suggestionPolicy":"${insight.suggestionPolicy}","threshold":10.0,"type":"${insight.type}"},"policy":{"duty":{"action":"${policy.duty.action}","constraint":{"leftOperand":"${policy.duty.constraint.leftOperand}","operator":"${policy.duty.constraint.operator}","rightOperand":"${policy.duty.constraint.rightOperand}"}},"permission":{"action":"${policy.permission.action}","constraint":{"leftOperand":"${policy.permission.constraint.leftOperand}","operator":"${policy.permission.constraint.operator}","rightOperand":"${policy.permission.constraint.rightOperand}"},"target":"${policy.permission.target}"},"profile":"${policy.profile}","prohibition":{"action":"${policy.prohibition.action}","constraint":{"leftOperand":"${policy.prohibition.constraint.leftOperand}","operator":"${policy.prohibition.constraint.operator}","rightOperand":"${policy.prohibition.constraint.rightOperand}"},"target":"${policy.prohibition.target}"},"type":"${policy.type}"}}`;
|
|
135
|
-
return { envelope, canonicalEnvelope };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export async function clauseM2_payloadHash(data) {
|
|
139
|
-
const { envelope, canonicalEnvelope } = clauseM1_canonicalEnvelope(data);
|
|
140
|
-
const payloadHashSHA256 = await sha256Hex(canonicalEnvelope);
|
|
141
|
-
return { envelope, canonicalEnvelope, payloadHashSHA256 };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function clauseM3_hmac(data) {
|
|
145
|
-
const { envelope, canonicalEnvelope, payloadHashSHA256 } = await clauseM2_payloadHash(data);
|
|
146
|
-
const envelopeHmacSHA256 = await hmacSha256Hex(data.integrity.secret, canonicalEnvelope);
|
|
147
|
-
return { envelope, canonicalEnvelope, payloadHashSHA256, envelopeHmacSHA256 };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function clauseM4_minimizationRespected(insight) {
|
|
151
|
-
return !JSON.stringify(insight)
|
|
152
|
-
.toLowerCase()
|
|
153
|
-
.match(/diabetes|medical/);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function clauseM5_scopeComplete(insight) {
|
|
157
|
-
return Boolean(insight.scopeDevice && insight.scopeEvent && insight.expiresAt);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export async function evaluate(data) {
|
|
161
|
-
validateInstance(data);
|
|
162
|
-
|
|
163
|
-
const scannedProduct = productById(data, data.scan.scannedProductId);
|
|
164
|
-
assert(scannedProduct, `scanned product not found: ${data.scan.scannedProductId}`);
|
|
165
|
-
|
|
166
|
-
const needsLowSugar = clauseR1_needsLowSugar(data);
|
|
167
|
-
const highSugarScanned = clauseR2_highSugarScanned(data, scannedProduct);
|
|
168
|
-
const { candidates, recommended } = clauseR4_recommendedAlternative(data, scannedProduct);
|
|
169
|
-
const alternativeLowersSugar = clauseR5_alternativeLowersSugar(scannedProduct, recommended);
|
|
170
|
-
|
|
171
|
-
const insight = deriveInsight(data);
|
|
172
|
-
const policy = derivePolicy(data);
|
|
173
|
-
const { canonicalEnvelope, payloadHashSHA256, envelopeHmacSHA256 } = await clauseM3_hmac(data);
|
|
174
|
-
const minimizationRespected = clauseM4_minimizationRespected(insight);
|
|
175
|
-
const scopeComplete = clauseM5_scopeComplete(insight);
|
|
176
|
-
const authorizationAllowed = clauseG1_authorizedUse(data);
|
|
177
|
-
const dutyTimingConsistent = clauseG3_dutyTimely(data);
|
|
178
|
-
const marketingProhibited = clauseG2_marketingProhibited(policy);
|
|
179
|
-
|
|
180
|
-
const checks = {
|
|
181
|
-
signatureVerifies:
|
|
182
|
-
data.integrity.verificationMode === 'trustedPrecomputedInput' &&
|
|
183
|
-
envelopeHmacSHA256 === (await hmacSha256Hex(data.integrity.secret, canonicalEnvelope)),
|
|
184
|
-
payloadHashMatches: payloadHashSHA256 === (await sha256Hex(canonicalEnvelope)),
|
|
185
|
-
minimizationRespected,
|
|
186
|
-
scopeComplete,
|
|
187
|
-
authorizationAllowed,
|
|
188
|
-
highSugarBanner: highSugarScanned,
|
|
189
|
-
alternativeLowersSugar,
|
|
190
|
-
dutyTimingConsistent,
|
|
191
|
-
marketingProhibited,
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const answerSentence =
|
|
195
|
-
'The scanner is allowed to use a neutral shopping insight and recommends Low-Sugar Tea Biscuits instead of Classic Tea Biscuits.';
|
|
196
|
-
const reasonWhy = [
|
|
197
|
-
'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.',
|
|
198
|
-
`scanned product : ${scannedProduct.name}`,
|
|
199
|
-
`suggested alternative: ${recommended?.name ?? 'none'}`,
|
|
200
|
-
`payload SHA-256 : ${payloadHashSHA256}`,
|
|
201
|
-
`HMAC-SHA256 : ${envelopeHmacSHA256}`,
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
const arcLines = [
|
|
205
|
-
'=== Answer ===',
|
|
206
|
-
answerSentence,
|
|
207
|
-
'',
|
|
208
|
-
'=== Reason Why ===',
|
|
209
|
-
...reasonWhy,
|
|
210
|
-
'',
|
|
211
|
-
'=== Check ===',
|
|
212
|
-
`signature verifies : ${checks.signatureVerifies ? 'yes' : 'no'}`,
|
|
213
|
-
`payload hash matches : ${checks.payloadHashMatches ? 'yes' : 'no'}`,
|
|
214
|
-
`minimization strips sensitive terms: ${checks.minimizationRespected ? 'yes' : 'no'}`,
|
|
215
|
-
`scope complete : ${checks.scopeComplete ? 'yes' : 'no'}`,
|
|
216
|
-
`authorization allowed : ${checks.authorizationAllowed ? 'yes' : 'no'}`,
|
|
217
|
-
`high-sugar banner : ${checks.highSugarBanner ? 'yes' : 'no'}`,
|
|
218
|
-
`alternative lowers sugar : ${checks.alternativeLowersSugar ? 'yes' : 'no'}`,
|
|
219
|
-
`duty timing consistent : ${checks.dutyTimingConsistent ? 'yes' : 'no'}`,
|
|
220
|
-
`marketing prohibited : ${checks.marketingProhibited ? 'yes' : 'no'}`,
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
caseName: data.caseName,
|
|
225
|
-
derived: {
|
|
226
|
-
needsLowSugar,
|
|
227
|
-
highSugarScanned,
|
|
228
|
-
lowerSugarCandidateIds: candidates.map((product) => product.id),
|
|
229
|
-
recommendedAlternativeId: recommended?.id ?? null,
|
|
230
|
-
recommendedAlternativeName: recommended?.name ?? null,
|
|
231
|
-
alternativeLowersSugar,
|
|
232
|
-
},
|
|
233
|
-
envelope: { insight, policy },
|
|
234
|
-
integrity: {
|
|
235
|
-
canonicalEnvelope,
|
|
236
|
-
payloadHashSHA256,
|
|
237
|
-
envelopeHmacSHA256,
|
|
238
|
-
verificationMode: data.integrity.verificationMode,
|
|
239
|
-
},
|
|
240
|
-
answer: {
|
|
241
|
-
sentence: answerSentence,
|
|
242
|
-
scannedProduct: scannedProduct.name,
|
|
243
|
-
suggestedAlternative: recommended?.name ?? null,
|
|
244
|
-
payloadHashSHA256,
|
|
245
|
-
envelopeHmacSHA256,
|
|
246
|
-
},
|
|
247
|
-
reasonWhy,
|
|
248
|
-
checks,
|
|
249
|
-
allChecksPass: Object.values(checks).every(Boolean),
|
|
250
|
-
arcText: arcLines.join('\n'),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async function main() {
|
|
255
|
-
const inputPath = resolve(process.argv[2] ?? resolve(__dirname, 'delfour.data.json'));
|
|
256
|
-
const data = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
257
|
-
const result = await evaluate(data);
|
|
258
|
-
|
|
259
|
-
if (process.argv.includes('--json')) {
|
|
260
|
-
console.log(JSON.stringify(result, null, 2));
|
|
261
|
-
} else {
|
|
262
|
-
console.log(result.arcText);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!result.allChecksPass) process.exitCode = 1;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
269
|
-
main().catch((error) => {
|
|
270
|
-
console.error(error.message);
|
|
271
|
-
process.exit(1);
|
|
272
|
-
});
|
|
273
|
-
}
|