eyeling 1.21.2 → 1.21.4
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 +65 -0
- package/README.md +0 -1
- package/dist/browser/eyeling.browser.js +12 -1
- package/examples/arc-bridge/README.md +208 -0
- package/examples/arc-bridge/delfour/delfour.data.json +68 -0
- package/examples/arc-bridge/delfour/delfour.expected.json +88 -0
- package/examples/arc-bridge/delfour/delfour.instance.schema.json +201 -0
- package/examples/arc-bridge/delfour/delfour.model.mjs +273 -0
- package/examples/arc-bridge/delfour/delfour.spec.md +118 -0
- package/examples/arc-bridge/flandor/flandor.data.json +107 -0
- package/examples/arc-bridge/flandor/flandor.expected.json +98 -0
- package/examples/arc-bridge/flandor/flandor.instance.schema.json +285 -0
- package/examples/arc-bridge/flandor/flandor.model.mjs +303 -0
- package/examples/arc-bridge/flandor/flandor.spec.md +156 -0
- package/examples/extra/flandor.js +349 -0
- package/examples/extra/output/flandor.txt +31 -0
- package/examples/flandor.n3 +425 -0
- package/examples/output/flandor.n3 +31 -0
- package/eyeling.js +12 -1
- package/lib/builtins.js +12 -1
- package/package.json +1 -2
- package/test/api.test.js +49 -0
- package/SEMANTICS.md +0 -41
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"caseName": "Flandor",
|
|
3
|
+
"derived": {
|
|
4
|
+
"exportWeakness": true,
|
|
5
|
+
"skillsStrain": true,
|
|
6
|
+
"gridStress": true,
|
|
7
|
+
"activeNeedCount": 3,
|
|
8
|
+
"needsRetoolingPulse": true,
|
|
9
|
+
"eligiblePackageIds": ["pkg:RET_FLEX_120"],
|
|
10
|
+
"recommendedPackageId": "pkg:RET_FLEX_120",
|
|
11
|
+
"recommendedPackageName": "Flandor Retooling Pulse"
|
|
12
|
+
},
|
|
13
|
+
"envelope": {
|
|
14
|
+
"insight": {
|
|
15
|
+
"createdAt": "2026-04-08T07:00:00+00:00",
|
|
16
|
+
"expiresAt": "2026-04-08T19:00:00+00:00",
|
|
17
|
+
"id": "https://example.org/insight/flandor",
|
|
18
|
+
"metric": "regional_retooling_priority",
|
|
19
|
+
"region": "Flanders",
|
|
20
|
+
"scopeDevice": "economic-resilience-board",
|
|
21
|
+
"scopeEvent": "budget-prep-window",
|
|
22
|
+
"suggestionPolicy": "lowest_cost_package_covering_all_active_needs",
|
|
23
|
+
"threshold": 3,
|
|
24
|
+
"type": "ins:Insight"
|
|
25
|
+
},
|
|
26
|
+
"policy": {
|
|
27
|
+
"duty": {
|
|
28
|
+
"action": "odrl:delete",
|
|
29
|
+
"constraint": {
|
|
30
|
+
"leftOperand": "odrl:dateTime",
|
|
31
|
+
"operator": "odrl:eq",
|
|
32
|
+
"rightOperand": "2026-04-08T19:00:00+00:00"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"permission": {
|
|
36
|
+
"action": "odrl:use",
|
|
37
|
+
"constraint": {
|
|
38
|
+
"leftOperand": "odrl:purpose",
|
|
39
|
+
"operator": "odrl:eq",
|
|
40
|
+
"rightOperand": "regional_stabilization"
|
|
41
|
+
},
|
|
42
|
+
"target": "https://example.org/insight/flandor"
|
|
43
|
+
},
|
|
44
|
+
"profile": "Flandor-Insight-Policy",
|
|
45
|
+
"prohibition": {
|
|
46
|
+
"action": "odrl:distribute",
|
|
47
|
+
"constraint": {
|
|
48
|
+
"leftOperand": "odrl:purpose",
|
|
49
|
+
"operator": "odrl:eq",
|
|
50
|
+
"rightOperand": "firm_surveillance"
|
|
51
|
+
},
|
|
52
|
+
"target": "https://example.org/insight/flandor"
|
|
53
|
+
},
|
|
54
|
+
"type": "odrl:Policy"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"integrity": {
|
|
58
|
+
"canonicalEnvelope": "{\"insight\":{\"createdAt\":\"2026-04-08T07:00:00+00:00\",\"expiresAt\":\"2026-04-08T19:00:00+00:00\",\"id\":\"https://example.org/insight/flandor\",\"metric\":\"regional_retooling_priority\",\"region\":\"Flanders\",\"scopeDevice\":\"economic-resilience-board\",\"scopeEvent\":\"budget-prep-window\",\"suggestionPolicy\":\"lowest_cost_package_covering_all_active_needs\",\"threshold\":3,\"type\":\"ins:Insight\"},\"policy\":{\"duty\":{\"action\":\"odrl:delete\",\"constraint\":{\"leftOperand\":\"odrl:dateTime\",\"operator\":\"odrl:eq\",\"rightOperand\":\"2026-04-08T19:00:00+00:00\"}},\"permission\":{\"action\":\"odrl:use\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"regional_stabilization\"},\"target\":\"https://example.org/insight/flandor\"},\"profile\":\"Flandor-Insight-Policy\",\"prohibition\":{\"action\":\"odrl:distribute\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"firm_surveillance\"},\"target\":\"https://example.org/insight/flandor\"},\"type\":\"odrl:Policy\"}}",
|
|
59
|
+
"payloadHashSHA256": "718f5b17d07ab6a95503bc04a1000ddb132409f600659c03d21def81914b780b",
|
|
60
|
+
"envelopeHmacSHA256": "955968ca99a191783bc00cba068128ccb9ff40a5e6114fda13a52c74ee27329e",
|
|
61
|
+
"verificationMode": "trustedPrecomputedInput"
|
|
62
|
+
},
|
|
63
|
+
"answer": {
|
|
64
|
+
"name": "Flandor",
|
|
65
|
+
"region": "Flanders",
|
|
66
|
+
"metric": "regional_retooling_priority",
|
|
67
|
+
"activeNeedCount": 3,
|
|
68
|
+
"threshold": 3,
|
|
69
|
+
"recommendedPackage": "Flandor Retooling Pulse",
|
|
70
|
+
"budgetCapMEUR": 140,
|
|
71
|
+
"packageCostMEUR": 120,
|
|
72
|
+
"payloadHashSHA256": "718f5b17d07ab6a95503bc04a1000ddb132409f600659c03d21def81914b780b",
|
|
73
|
+
"envelopeHmacSHA256": "955968ca99a191783bc00cba068128ccb9ff40a5e6114fda13a52c74ee27329e"
|
|
74
|
+
},
|
|
75
|
+
"reasonWhy": [
|
|
76
|
+
"ExportWeakness holds because at least one cluster has exportOrdersIndex < 90 (Antwerp chemicals=84, Ghent manufacturing=87).",
|
|
77
|
+
"SkillsStrain holds because the technical vacancy rate is 4.6% and the threshold is > 3.9%.",
|
|
78
|
+
"GridStress holds because congestion hours = 19 and the threshold is > 11.",
|
|
79
|
+
"The recommendation rule selects the least-cost package that covers every active need and remains within budget.",
|
|
80
|
+
"The selected package is \"Flandor Retooling Pulse\" with cost \u20ac120M, workerCoverage=1200, gridReliefMW=85.",
|
|
81
|
+
"Use is permitted only for purpose \"regional_stabilization\" and expires at 2026-04-08T19:00:00+00:00."
|
|
82
|
+
],
|
|
83
|
+
"checks": {
|
|
84
|
+
"payloadHashMatches": true,
|
|
85
|
+
"signatureVerifies": true,
|
|
86
|
+
"thresholdReached": true,
|
|
87
|
+
"scopeComplete": true,
|
|
88
|
+
"minimizationRespected": true,
|
|
89
|
+
"authorizationAllowed": true,
|
|
90
|
+
"dutyTimely": true,
|
|
91
|
+
"surveillanceReuseProhibited": true,
|
|
92
|
+
"packageWithinBudget": true,
|
|
93
|
+
"packageCoversAllActiveNeeds": true,
|
|
94
|
+
"lowestCostEligiblePackageChosen": true
|
|
95
|
+
},
|
|
96
|
+
"allChecksPass": true,
|
|
97
|
+
"arcText": "=== Answer ===\nName: Flandor\nRegion: Flanders\nMetric: regional_retooling_priority\nActive need count: 3/3\nRecommended package: Flandor Retooling Pulse\nBudget cap: \u20ac140M\nPackage cost: \u20ac120M\nPayload SHA-256: 718f5b17d07ab6a95503bc04a1000ddb132409f600659c03d21def81914b780b\nEnvelope HMAC-SHA-256: 955968ca99a191783bc00cba068128ccb9ff40a5e6114fda13a52c74ee27329e\n\n=== Reason Why ===\nExportWeakness holds because at least one cluster has exportOrdersIndex < 90 (Antwerp chemicals=84, Ghent manufacturing=87).\nSkillsStrain holds because the technical vacancy rate is 4.6% and the threshold is > 3.9%.\nGridStress holds because congestion hours = 19 and the threshold is > 11.\nThe recommendation rule selects the least-cost package that covers every active need and remains within budget.\nThe selected package is \"Flandor Retooling Pulse\" with cost \u20ac120M, workerCoverage=1200, gridReliefMW=85.\nUse is permitted only for purpose \"regional_stabilization\" and expires at 2026-04-08T19:00:00+00:00.\n\n=== Check ===\n- PASS: payloadHashMatches\n- PASS: signatureVerifies\n- PASS: thresholdReached\n- PASS: scopeComplete\n- PASS: minimizationRespected\n- PASS: authorizationAllowed\n- PASS: dutyTimely\n- PASS: surveillanceReuseProhibited\n- PASS: packageWithinBudget\n- PASS: packageCoversAllActiveNeeds\n- PASS: lowestCostEligiblePackageChosen"
|
|
98
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://example.org/schema/flandor.instance.schema.json",
|
|
4
|
+
"title": "ARC Bridge Flandor instance",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"$schema",
|
|
9
|
+
"caseName",
|
|
10
|
+
"region",
|
|
11
|
+
"question",
|
|
12
|
+
"timestamps",
|
|
13
|
+
"evaluationContext",
|
|
14
|
+
"thresholds",
|
|
15
|
+
"signals",
|
|
16
|
+
"budget",
|
|
17
|
+
"packages",
|
|
18
|
+
"insightPolicy",
|
|
19
|
+
"integrity"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"$schema": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"caseName": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"minLength": 1
|
|
28
|
+
},
|
|
29
|
+
"region": {
|
|
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"],
|
|
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
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"thresholds": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"required": [
|
|
87
|
+
"exportOrdersIndexBelow",
|
|
88
|
+
"technicalVacancyRatePctAbove",
|
|
89
|
+
"gridCongestionHoursAbove",
|
|
90
|
+
"activeNeedCountAtLeast"
|
|
91
|
+
],
|
|
92
|
+
"properties": {
|
|
93
|
+
"exportOrdersIndexBelow": {
|
|
94
|
+
"type": "number"
|
|
95
|
+
},
|
|
96
|
+
"technicalVacancyRatePctAbove": {
|
|
97
|
+
"type": "number"
|
|
98
|
+
},
|
|
99
|
+
"gridCongestionHoursAbove": {
|
|
100
|
+
"type": "number"
|
|
101
|
+
},
|
|
102
|
+
"activeNeedCountAtLeast": {
|
|
103
|
+
"type": "integer",
|
|
104
|
+
"minimum": 1
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"signals": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"required": ["clusters", "labourMarket", "grid"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"clusters": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"minItems": 1,
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"additionalProperties": false,
|
|
119
|
+
"required": ["id", "name", "exportOrdersIndex", "energyIntensity"],
|
|
120
|
+
"properties": {
|
|
121
|
+
"id": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"minLength": 1
|
|
124
|
+
},
|
|
125
|
+
"name": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"minLength": 1
|
|
128
|
+
},
|
|
129
|
+
"exportOrdersIndex": {
|
|
130
|
+
"type": "number"
|
|
131
|
+
},
|
|
132
|
+
"energyIntensity": {
|
|
133
|
+
"type": "number"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"labourMarket": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"additionalProperties": false,
|
|
141
|
+
"required": ["technicalVacancyRatePct"],
|
|
142
|
+
"properties": {
|
|
143
|
+
"technicalVacancyRatePct": {
|
|
144
|
+
"type": "number",
|
|
145
|
+
"minimum": 0
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"grid": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"additionalProperties": false,
|
|
152
|
+
"required": ["congestionHours", "renewableCurtailmentMWh"],
|
|
153
|
+
"properties": {
|
|
154
|
+
"congestionHours": {
|
|
155
|
+
"type": "number",
|
|
156
|
+
"minimum": 0
|
|
157
|
+
},
|
|
158
|
+
"renewableCurtailmentMWh": {
|
|
159
|
+
"type": "number",
|
|
160
|
+
"minimum": 0
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"budget": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"additionalProperties": false,
|
|
169
|
+
"required": ["windowName", "maxMEUR"],
|
|
170
|
+
"properties": {
|
|
171
|
+
"windowName": {
|
|
172
|
+
"type": "string",
|
|
173
|
+
"minLength": 1
|
|
174
|
+
},
|
|
175
|
+
"maxMEUR": {
|
|
176
|
+
"type": "number",
|
|
177
|
+
"exclusiveMinimum": 0
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"packages": {
|
|
182
|
+
"type": "array",
|
|
183
|
+
"minItems": 1,
|
|
184
|
+
"items": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"additionalProperties": false,
|
|
187
|
+
"required": [
|
|
188
|
+
"id",
|
|
189
|
+
"name",
|
|
190
|
+
"costMEUR",
|
|
191
|
+
"workerCoverage",
|
|
192
|
+
"gridReliefMW",
|
|
193
|
+
"coversExportWeakness",
|
|
194
|
+
"coversSkillsStrain",
|
|
195
|
+
"coversGridStress"
|
|
196
|
+
],
|
|
197
|
+
"properties": {
|
|
198
|
+
"id": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"minLength": 1
|
|
201
|
+
},
|
|
202
|
+
"name": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"minLength": 1
|
|
205
|
+
},
|
|
206
|
+
"costMEUR": {
|
|
207
|
+
"type": "number",
|
|
208
|
+
"exclusiveMinimum": 0
|
|
209
|
+
},
|
|
210
|
+
"workerCoverage": {
|
|
211
|
+
"type": "number",
|
|
212
|
+
"minimum": 0
|
|
213
|
+
},
|
|
214
|
+
"gridReliefMW": {
|
|
215
|
+
"type": "number",
|
|
216
|
+
"minimum": 0
|
|
217
|
+
},
|
|
218
|
+
"coversExportWeakness": {
|
|
219
|
+
"type": "boolean"
|
|
220
|
+
},
|
|
221
|
+
"coversSkillsStrain": {
|
|
222
|
+
"type": "boolean"
|
|
223
|
+
},
|
|
224
|
+
"coversGridStress": {
|
|
225
|
+
"type": "boolean"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
"insightPolicy": {
|
|
231
|
+
"type": "object",
|
|
232
|
+
"additionalProperties": false,
|
|
233
|
+
"required": ["id", "metric", "type", "suggestionPolicy", "policyType", "policyProfile"],
|
|
234
|
+
"properties": {
|
|
235
|
+
"id": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"format": "uri"
|
|
238
|
+
},
|
|
239
|
+
"metric": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"minLength": 1
|
|
242
|
+
},
|
|
243
|
+
"type": {
|
|
244
|
+
"type": "string",
|
|
245
|
+
"const": "ins:Insight"
|
|
246
|
+
},
|
|
247
|
+
"suggestionPolicy": {
|
|
248
|
+
"type": "string",
|
|
249
|
+
"minLength": 1
|
|
250
|
+
},
|
|
251
|
+
"policyType": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"const": "odrl:Policy"
|
|
254
|
+
},
|
|
255
|
+
"policyProfile": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"minLength": 1
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"integrity": {
|
|
262
|
+
"type": "object",
|
|
263
|
+
"additionalProperties": false,
|
|
264
|
+
"required": ["hashAlgorithm", "macAlgorithm", "secret", "verificationMode"],
|
|
265
|
+
"properties": {
|
|
266
|
+
"hashAlgorithm": {
|
|
267
|
+
"type": "string",
|
|
268
|
+
"const": "SHA-256"
|
|
269
|
+
},
|
|
270
|
+
"macAlgorithm": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"const": "HMAC-SHA-256"
|
|
273
|
+
},
|
|
274
|
+
"secret": {
|
|
275
|
+
"type": "string",
|
|
276
|
+
"minLength": 1
|
|
277
|
+
},
|
|
278
|
+
"verificationMode": {
|
|
279
|
+
"type": "string",
|
|
280
|
+
"const": "trustedPrecomputedInput"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
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 stableStringify(value) {
|
|
13
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
14
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
15
|
+
const keys = Object.keys(value).sort();
|
|
16
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assert(condition, message) {
|
|
20
|
+
if (!condition) throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function countTrue(values) {
|
|
24
|
+
return values.reduce((sum, value) => sum + (value ? 1 : 0), 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function sha256Hex(text) {
|
|
28
|
+
const digest = await subtle.digest('SHA-256', encoder.encode(text));
|
|
29
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function hmacSha256Hex(secret, text) {
|
|
33
|
+
const key = await subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
34
|
+
const signature = await subtle.sign('HMAC', key, encoder.encode(text));
|
|
35
|
+
return Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateInstance(data) {
|
|
39
|
+
assert(typeof data?.caseName === 'string' && data.caseName.length > 0, 'caseName is required');
|
|
40
|
+
assert(typeof data?.region === 'string' && data.region.length > 0, 'region is required');
|
|
41
|
+
assert(Array.isArray(data?.signals?.clusters) && data.signals.clusters.length > 0, 'signals.clusters is required');
|
|
42
|
+
assert(Array.isArray(data?.packages) && data.packages.length > 0, 'packages is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clauseR1_exportWeakness(data) {
|
|
46
|
+
return data.signals.clusters.some((cluster) => cluster.exportOrdersIndex < data.thresholds.exportOrdersIndexBelow);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clauseR2_skillsStrain(data) {
|
|
50
|
+
return data.signals.labourMarket.technicalVacancyRatePct > data.thresholds.technicalVacancyRatePctAbove;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clauseR3_gridStress(data) {
|
|
54
|
+
return data.signals.grid.congestionHours > data.thresholds.gridCongestionHoursAbove;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function clauseR4_activeNeedCount(state) {
|
|
58
|
+
return countTrue([state.exportWeakness, state.skillsStrain, state.gridStress]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function clauseR5_needsRetoolingPulse(data, state) {
|
|
62
|
+
return state.activeNeedCount >= data.thresholds.activeNeedCountAtLeast;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function deriveInsight(data) {
|
|
66
|
+
return {
|
|
67
|
+
createdAt: data.timestamps.createdAt,
|
|
68
|
+
expiresAt: data.timestamps.expiresAt,
|
|
69
|
+
id: data.insightPolicy.id,
|
|
70
|
+
metric: data.insightPolicy.metric,
|
|
71
|
+
region: data.region,
|
|
72
|
+
scopeDevice: data.evaluationContext.scopeDevice,
|
|
73
|
+
scopeEvent: data.evaluationContext.scopeEvent,
|
|
74
|
+
suggestionPolicy: data.insightPolicy.suggestionPolicy,
|
|
75
|
+
threshold: data.thresholds.activeNeedCountAtLeast,
|
|
76
|
+
type: data.insightPolicy.type,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function derivePolicy(data) {
|
|
81
|
+
return {
|
|
82
|
+
duty: {
|
|
83
|
+
action: 'odrl:delete',
|
|
84
|
+
constraint: {
|
|
85
|
+
leftOperand: 'odrl:dateTime',
|
|
86
|
+
operator: 'odrl:eq',
|
|
87
|
+
rightOperand: data.timestamps.expiresAt,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
permission: {
|
|
91
|
+
action: 'odrl:use',
|
|
92
|
+
constraint: {
|
|
93
|
+
leftOperand: 'odrl:purpose',
|
|
94
|
+
operator: 'odrl:eq',
|
|
95
|
+
rightOperand: data.evaluationContext.purpose,
|
|
96
|
+
},
|
|
97
|
+
target: data.insightPolicy.id,
|
|
98
|
+
},
|
|
99
|
+
profile: data.insightPolicy.policyProfile,
|
|
100
|
+
prohibition: {
|
|
101
|
+
action: 'odrl:distribute',
|
|
102
|
+
constraint: {
|
|
103
|
+
leftOperand: 'odrl:purpose',
|
|
104
|
+
operator: 'odrl:eq',
|
|
105
|
+
rightOperand: data.evaluationContext.prohibitedReusePurpose,
|
|
106
|
+
},
|
|
107
|
+
target: data.insightPolicy.id,
|
|
108
|
+
},
|
|
109
|
+
type: data.insightPolicy.policyType,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function packageCoversAllActiveNeeds(pkg, state) {
|
|
114
|
+
return (
|
|
115
|
+
(!state.exportWeakness || pkg.coversExportWeakness) &&
|
|
116
|
+
(!state.skillsStrain || pkg.coversSkillsStrain) &&
|
|
117
|
+
(!state.gridStress || pkg.coversGridStress)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function clauseS1_eligiblePackages(data, state) {
|
|
122
|
+
return data.packages
|
|
123
|
+
.filter((pkg) => pkg.costMEUR <= data.budget.maxMEUR)
|
|
124
|
+
.filter((pkg) => packageCoversAllActiveNeeds(pkg, state))
|
|
125
|
+
.sort((a, b) => a.costMEUR - b.costMEUR);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function clauseS2_recommendedPackage(data, state) {
|
|
129
|
+
const eligible = clauseS1_eligiblePackages(data, state);
|
|
130
|
+
return {
|
|
131
|
+
eligible,
|
|
132
|
+
recommended: eligible[0] ?? null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function clauseG1_authorizedUse(data) {
|
|
137
|
+
return (
|
|
138
|
+
data.evaluationContext.purpose === 'regional_stabilization' &&
|
|
139
|
+
Date.parse(data.timestamps.authorizedAt) <= Date.parse(data.timestamps.expiresAt)
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function clauseG2_surveillanceReuseProhibited(data) {
|
|
144
|
+
return data.evaluationContext.prohibitedReusePurpose === 'firm_surveillance';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function clauseG3_dutyTimely(data) {
|
|
148
|
+
return Date.parse(data.timestamps.dutyPerformedAt) <= Date.parse(data.timestamps.expiresAt);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function clauseM1_canonicalEnvelope(data) {
|
|
152
|
+
const envelope = { insight: deriveInsight(data), policy: derivePolicy(data) };
|
|
153
|
+
return { envelope, canonicalEnvelope: stableStringify(envelope) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function clauseM2_payloadHash(data) {
|
|
157
|
+
const { envelope, canonicalEnvelope } = clauseM1_canonicalEnvelope(data);
|
|
158
|
+
const payloadHashSHA256 = await sha256Hex(canonicalEnvelope);
|
|
159
|
+
return { envelope, canonicalEnvelope, payloadHashSHA256 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function clauseM3_hmac(data) {
|
|
163
|
+
const { envelope, canonicalEnvelope, payloadHashSHA256 } = await clauseM2_payloadHash(data);
|
|
164
|
+
const envelopeHmacSHA256 = await hmacSha256Hex(data.integrity.secret, canonicalEnvelope);
|
|
165
|
+
return { envelope, canonicalEnvelope, payloadHashSHA256, envelopeHmacSHA256 };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function clauseM4_minimizationRespected(insight) {
|
|
169
|
+
return !JSON.stringify(insight)
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.match(/salary|payroll|invoice|medical|firmname/);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function clauseM5_scopeComplete(insight) {
|
|
175
|
+
return Boolean(insight.scopeDevice && insight.scopeEvent && insight.expiresAt);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function evaluate(data) {
|
|
179
|
+
validateInstance(data);
|
|
180
|
+
|
|
181
|
+
const exportWeakness = clauseR1_exportWeakness(data);
|
|
182
|
+
const skillsStrain = clauseR2_skillsStrain(data);
|
|
183
|
+
const gridStress = clauseR3_gridStress(data);
|
|
184
|
+
const activeNeedCount = clauseR4_activeNeedCount({ exportWeakness, skillsStrain, gridStress });
|
|
185
|
+
const needsRetoolingPulse = clauseR5_needsRetoolingPulse(data, { activeNeedCount });
|
|
186
|
+
|
|
187
|
+
const insight = deriveInsight(data);
|
|
188
|
+
const policy = derivePolicy(data);
|
|
189
|
+
const { canonicalEnvelope, payloadHashSHA256, envelopeHmacSHA256 } = await clauseM3_hmac(data);
|
|
190
|
+
const { eligible, recommended } = clauseS2_recommendedPackage(data, { exportWeakness, skillsStrain, gridStress });
|
|
191
|
+
const authorizedUse = clauseG1_authorizedUse(data);
|
|
192
|
+
const surveillanceReuseProhibited = clauseG2_surveillanceReuseProhibited(data);
|
|
193
|
+
const dutyTimely = clauseG3_dutyTimely(data);
|
|
194
|
+
const minimizationRespected = clauseM4_minimizationRespected(insight);
|
|
195
|
+
const scopeComplete = clauseM5_scopeComplete(insight);
|
|
196
|
+
|
|
197
|
+
const checks = {
|
|
198
|
+
payloadHashMatches: payloadHashSHA256 === (await sha256Hex(canonicalEnvelope)),
|
|
199
|
+
signatureVerifies:
|
|
200
|
+
data.integrity.verificationMode === 'trustedPrecomputedInput' &&
|
|
201
|
+
envelopeHmacSHA256 === (await hmacSha256Hex(data.integrity.secret, canonicalEnvelope)),
|
|
202
|
+
thresholdReached: needsRetoolingPulse,
|
|
203
|
+
scopeComplete,
|
|
204
|
+
minimizationRespected,
|
|
205
|
+
authorizationAllowed: authorizedUse,
|
|
206
|
+
dutyTimely,
|
|
207
|
+
surveillanceReuseProhibited,
|
|
208
|
+
packageWithinBudget: Boolean(recommended) && recommended.costMEUR <= data.budget.maxMEUR,
|
|
209
|
+
packageCoversAllActiveNeeds:
|
|
210
|
+
Boolean(recommended) && packageCoversAllActiveNeeds(recommended, { exportWeakness, skillsStrain, gridStress }),
|
|
211
|
+
lowestCostEligiblePackageChosen: Boolean(recommended) && recommended.id === (eligible[0]?.id ?? null),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const reasonWhy = [
|
|
215
|
+
`ExportWeakness holds because at least one cluster has exportOrdersIndex < ${data.thresholds.exportOrdersIndexBelow} (${data.signals.clusters.map((c) => `${c.name}=${c.exportOrdersIndex}`).join(', ')}).`,
|
|
216
|
+
`SkillsStrain holds because the technical vacancy rate is ${data.signals.labourMarket.technicalVacancyRatePct}% and the threshold is > ${data.thresholds.technicalVacancyRatePctAbove}%.`,
|
|
217
|
+
`GridStress holds because congestion hours = ${data.signals.grid.congestionHours} and the threshold is > ${data.thresholds.gridCongestionHoursAbove}.`,
|
|
218
|
+
'The recommendation rule selects the least-cost package that covers every active need and remains within budget.',
|
|
219
|
+
recommended
|
|
220
|
+
? `The selected package is "${recommended.name}" with cost €${recommended.costMEUR}M, workerCoverage=${recommended.workerCoverage}, gridReliefMW=${recommended.gridReliefMW}.`
|
|
221
|
+
: 'No eligible package exists within budget.',
|
|
222
|
+
`Use is permitted only for purpose "${data.evaluationContext.purpose}" and expires at ${data.timestamps.expiresAt}.`,
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
const answer = {
|
|
226
|
+
name: data.caseName,
|
|
227
|
+
region: data.region,
|
|
228
|
+
metric: data.insightPolicy.metric,
|
|
229
|
+
activeNeedCount,
|
|
230
|
+
threshold: data.thresholds.activeNeedCountAtLeast,
|
|
231
|
+
recommendedPackage: recommended?.name ?? null,
|
|
232
|
+
budgetCapMEUR: data.budget.maxMEUR,
|
|
233
|
+
packageCostMEUR: recommended?.costMEUR ?? null,
|
|
234
|
+
payloadHashSHA256,
|
|
235
|
+
envelopeHmacSHA256,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const arcLines = [
|
|
239
|
+
'=== Answer ===',
|
|
240
|
+
`Name: ${answer.name}`,
|
|
241
|
+
`Region: ${answer.region}`,
|
|
242
|
+
`Metric: ${answer.metric}`,
|
|
243
|
+
`Active need count: ${answer.activeNeedCount}/${answer.threshold}`,
|
|
244
|
+
`Recommended package: ${answer.recommendedPackage}`,
|
|
245
|
+
`Budget cap: €${answer.budgetCapMEUR}M`,
|
|
246
|
+
`Package cost: €${answer.packageCostMEUR}M`,
|
|
247
|
+
`Payload SHA-256: ${answer.payloadHashSHA256}`,
|
|
248
|
+
`Envelope HMAC-SHA-256: ${answer.envelopeHmacSHA256}`,
|
|
249
|
+
'',
|
|
250
|
+
'=== Reason Why ===',
|
|
251
|
+
...reasonWhy,
|
|
252
|
+
'',
|
|
253
|
+
'=== Check ===',
|
|
254
|
+
...Object.entries(checks).map(([name, ok]) => `- ${ok ? 'PASS' : 'FAIL'}: ${name}`),
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
caseName: data.caseName,
|
|
259
|
+
derived: {
|
|
260
|
+
exportWeakness,
|
|
261
|
+
skillsStrain,
|
|
262
|
+
gridStress,
|
|
263
|
+
activeNeedCount,
|
|
264
|
+
needsRetoolingPulse,
|
|
265
|
+
eligiblePackageIds: eligible.map((pkg) => pkg.id),
|
|
266
|
+
recommendedPackageId: recommended?.id ?? null,
|
|
267
|
+
recommendedPackageName: recommended?.name ?? null,
|
|
268
|
+
},
|
|
269
|
+
envelope: { insight, policy },
|
|
270
|
+
integrity: {
|
|
271
|
+
canonicalEnvelope,
|
|
272
|
+
payloadHashSHA256,
|
|
273
|
+
envelopeHmacSHA256,
|
|
274
|
+
verificationMode: data.integrity.verificationMode,
|
|
275
|
+
},
|
|
276
|
+
answer,
|
|
277
|
+
reasonWhy,
|
|
278
|
+
checks,
|
|
279
|
+
allChecksPass: Object.values(checks).every(Boolean),
|
|
280
|
+
arcText: arcLines.join('\n'),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function main() {
|
|
285
|
+
const inputPath = resolve(process.argv[2] ?? resolve(__dirname, 'flandor.data.json'));
|
|
286
|
+
const data = JSON.parse(await readFile(inputPath, 'utf8'));
|
|
287
|
+
const result = await evaluate(data);
|
|
288
|
+
|
|
289
|
+
if (process.argv.includes('--json')) {
|
|
290
|
+
console.log(JSON.stringify(result, null, 2));
|
|
291
|
+
} else {
|
|
292
|
+
console.log(result.arcText);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!result.allChecksPass) process.exitCode = 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
299
|
+
main().catch((error) => {
|
|
300
|
+
console.error(error.message);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
}
|