eyeling 1.21.6 → 1.21.8

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 CHANGED
@@ -1568,15 +1568,17 @@ Casts each element to a string and concatenates.
1568
1568
 
1569
1569
  **Shape:** `( fmt a1 a2 ... ) string:format out`
1570
1570
 
1571
- A tiny `sprintf` subset:
1571
+ A small `printf`/`sprintf` subset:
1572
1572
 
1573
- - Supports only `%s` and `%%`.
1574
- - Any other specifier (`%d`, `%f`, …) causes the builtin to fail.
1575
- - Missing arguments are treated as empty strings.
1573
+ - Supports `%%`, `%s`, `%d`/`%i`/`%u`, `%f`/`%F`, `%e`/`%E`, `%g`/`%G`, and `%c`.
1574
+ - Supports width and precision, plus the `-` and `0` flags.
1575
+ - Unsupported flags/specifiers cause the builtin to fail.
1576
+ - Missing `%s` arguments are treated as empty strings.
1576
1577
  - The format string `fmt` itself must be string-castable.
1577
1578
  - Each `%s` argument may be any bound non-variable term:
1578
1579
  - string-castable terms (IRIs and literals) use their direct string value;
1579
1580
  - other bound terms (blank nodes, lists, quoted formulas, …) are rendered as N3.
1581
+ - Numeric directives require numerically parseable literals.
1580
1582
 
1581
1583
  ### Length and character utilities (Eyeling extensions)
1582
1584
 
@@ -983,34 +983,213 @@
983
983
  return stripQuotes(lex);
984
984
  }
985
985
 
986
- // Tiny subset of sprintf: supports only %s and %%.
987
- // Good enough for most N3 string:format use cases that just splice strings.
988
- function simpleStringFormat(fmt, args) {
989
- let out = '';
990
- let argIndex = 0;
986
+ // Small printf/sprintf subset for string:format.
987
+ // Supports: %% , %s, %d/%i/%u, %f/%F, %e/%E, %g/%G, %c and width/precision
988
+ // with - and 0 flags. Unsupported specifiers/flags fail the builtin.
989
+ function padLeft(str, width, ch) {
990
+ return width > str.length ? ch.repeat(width - str.length) + str : str;
991
+ }
992
+
993
+ function padRight(str, width, ch) {
994
+ return width > str.length ? str + ch.repeat(width - str.length) : str;
995
+ }
996
+
997
+ function applyPrintfWidth(str, width, left, ch) {
998
+ if (width == null || width <= str.length) return str;
999
+ return left ? padRight(str, width, ch) : padLeft(str, width, ch);
1000
+ }
1001
+
1002
+ function applyPrintfNumericWidth(str, width, left, zero) {
1003
+ if (width == null || width <= str.length) return str;
1004
+ if (left) return padRight(str, width, ' ');
1005
+ const padChar = zero ? '0' : ' ';
1006
+ if (padChar === '0' && (str.startsWith('-') || str.startsWith('+'))) {
1007
+ return str[0] + padLeft(str.slice(1), width - 1, '0');
1008
+ }
1009
+ return padLeft(str, width, padChar);
1010
+ }
991
1011
 
992
- for (let i = 0; i < fmt.length; i++) {
1012
+ function parsePrintfSpec(fmt, percentIndex) {
1013
+ let i = percentIndex + 1;
1014
+ if (i >= fmt.length) return null;
1015
+ if (fmt[i] === '%') return { conv: '%', next: i + 1 };
1016
+
1017
+ let left = false;
1018
+ let zero = false;
1019
+ while (i < fmt.length) {
993
1020
  const ch = fmt[i];
994
- if (ch === '%' && i + 1 < fmt.length) {
995
- const spec = fmt[i + 1];
1021
+ if (ch === '-') {
1022
+ left = true;
1023
+ i++;
1024
+ continue;
1025
+ }
1026
+ if (ch === '0') {
1027
+ zero = true;
1028
+ i++;
1029
+ continue;
1030
+ }
1031
+ break;
1032
+ }
996
1033
 
997
- if (spec === 's') {
998
- const arg = argIndex < args.length ? args[argIndex++] : '';
999
- out += arg;
1000
- i++;
1001
- continue;
1002
- }
1034
+ let width = null;
1035
+ let widthStr = '';
1036
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) widthStr += fmt[i++];
1037
+ if (widthStr) width = Number(widthStr);
1003
1038
 
1004
- if (spec === '%') {
1005
- out += '%';
1006
- i++;
1007
- continue;
1008
- }
1039
+ let precision = null;
1040
+ if (fmt[i] === '.') {
1041
+ i++;
1042
+ let p = '';
1043
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) p += fmt[i++];
1044
+ precision = p === '' ? 0 : Number(p);
1045
+ }
1046
+
1047
+ while (i < fmt.length && /[hlLztj]/.test(fmt[i])) i++;
1048
+ if (i >= fmt.length) return null;
1049
+
1050
+ const conv = fmt[i];
1051
+ if (!'sdiufFeEgGc'.includes(conv)) return null;
1052
+ return { left, zero, width, precision, conv, next: i + 1 };
1053
+ }
1009
1054
 
1010
- // Unsupported specifier (like %d, %f, …) ⇒ fail the builtin.
1055
+ function trimPrintfGeneralLex(str) {
1056
+ return str
1057
+ .replace(/(\.\d*?[1-9])0+(e|$)/i, '$1$2')
1058
+ .replace(/\.0+(e|$)/i, '$1')
1059
+ .replace(/\.(e|$)/i, '$1');
1060
+ }
1061
+
1062
+ function termToPrintfInteger(t) {
1063
+ const bi = parseIntLiteral(t);
1064
+ if (bi !== null) return bi;
1065
+ const info = parseNumericLiteralInfo(t);
1066
+ if (!info || info.kind !== 'number') return null;
1067
+ if (!Number.isFinite(info.value) || !Number.isInteger(info.value)) return null;
1068
+ return BigInt(info.value);
1069
+ }
1070
+
1071
+ function termToPrintfNumber(t) {
1072
+ const info = parseNumericLiteralInfo(t);
1073
+ if (!info) return null;
1074
+ if (info.kind === 'bigint') return Number(info.value);
1075
+ return info.value;
1076
+ }
1077
+
1078
+ function formatPrintfStringTerm(t, spec) {
1079
+ const value = t === undefined ? '' : termToFormatArgString(t);
1080
+ if (value === null) return null;
1081
+ const str = spec.precision == null ? value : value.slice(0, spec.precision);
1082
+ return applyPrintfWidth(str, spec.width, spec.left, ' ');
1083
+ }
1084
+
1085
+ function formatPrintfIntegerTerm(t, spec, unsigned) {
1086
+ if (t === undefined) return null;
1087
+ let value = termToPrintfInteger(t);
1088
+ if (value === null) return null;
1089
+ if (unsigned && value < 0n) return null;
1090
+ const negative = value < 0n;
1091
+ if (negative) value = -value;
1092
+ let digits = value.toString();
1093
+ if (spec.precision != null) digits = digits.padStart(spec.precision, '0');
1094
+ const out = (negative ? '-' : '') + digits;
1095
+ return applyPrintfNumericWidth(out, spec.width, spec.left, spec.zero && spec.precision == null);
1096
+ }
1097
+
1098
+ function formatPrintfFloatTerm(t, spec) {
1099
+ if (t === undefined) return null;
1100
+ const value = termToPrintfNumber(t);
1101
+ if (value === null || Number.isNaN(value)) return null;
1102
+ if (!Number.isFinite(value)) return applyPrintfNumericWidth(String(value), spec.width, spec.left, false);
1103
+
1104
+ let out;
1105
+ switch (spec.conv) {
1106
+ case 'f':
1107
+ case 'F': {
1108
+ const precision = spec.precision == null ? 6 : spec.precision;
1109
+ out = value.toFixed(precision);
1110
+ break;
1111
+ }
1112
+ case 'e':
1113
+ case 'E': {
1114
+ const precision = spec.precision == null ? 6 : spec.precision;
1115
+ out = value.toExponential(precision);
1116
+ break;
1117
+ }
1118
+ case 'g':
1119
+ case 'G': {
1120
+ const precision = spec.precision == null ? 6 : Math.max(spec.precision, 1);
1121
+ out = trimPrintfGeneralLex(value.toPrecision(precision));
1122
+ break;
1123
+ }
1124
+ default:
1011
1125
  return null;
1126
+ }
1127
+ if (spec.conv === 'E' || spec.conv === 'F' || spec.conv === 'G') out = out.toUpperCase();
1128
+ return applyPrintfNumericWidth(out, spec.width, spec.left, spec.zero);
1129
+ }
1130
+
1131
+ function formatPrintfCharTerm(t, spec) {
1132
+ if (t === undefined) return null;
1133
+ const value = termToPrintfInteger(t);
1134
+ if (value === null) return null;
1135
+ const codePoint = Number(value);
1136
+ if (!Number.isInteger(codePoint) || codePoint < 0 || !Number.isFinite(codePoint)) return null;
1137
+ try {
1138
+ return applyPrintfWidth(String.fromCodePoint(codePoint), spec.width, spec.left, ' ');
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+
1144
+ function simpleStringFormat(fmt, argTerms) {
1145
+ let out = '';
1146
+ let argIndex = 0;
1147
+
1148
+ for (let i = 0; i < fmt.length; ) {
1149
+ if (fmt[i] !== '%') {
1150
+ out += fmt[i++];
1151
+ continue;
1152
+ }
1153
+
1154
+ const spec = parsePrintfSpec(fmt, i);
1155
+ if (!spec) return null;
1156
+ i = spec.next;
1157
+
1158
+ if (spec.conv === '%') {
1159
+ out += '%';
1160
+ continue;
1161
+ }
1162
+
1163
+ const term = argIndex < argTerms.length ? argTerms[argIndex++] : undefined;
1164
+ let piece = null;
1165
+ switch (spec.conv) {
1166
+ case 's':
1167
+ piece = formatPrintfStringTerm(term, spec);
1168
+ break;
1169
+ case 'd':
1170
+ case 'i':
1171
+ piece = formatPrintfIntegerTerm(term, spec, false);
1172
+ break;
1173
+ case 'u':
1174
+ piece = formatPrintfIntegerTerm(term, spec, true);
1175
+ break;
1176
+ case 'f':
1177
+ case 'F':
1178
+ case 'e':
1179
+ case 'E':
1180
+ case 'g':
1181
+ case 'G':
1182
+ piece = formatPrintfFloatTerm(term, spec);
1183
+ break;
1184
+ case 'c':
1185
+ piece = formatPrintfCharTerm(term, spec);
1186
+ break;
1187
+ default:
1188
+ return null;
1012
1189
  }
1013
- out += ch;
1190
+
1191
+ if (piece === null) return null;
1192
+ out += piece;
1014
1193
  }
1015
1194
 
1016
1195
  return out;
@@ -4201,24 +4380,18 @@
4201
4380
  }
4202
4381
 
4203
4382
  // string:format
4204
- // (limited: only %s and %% are supported, anything else ⇒ builtin fails)
4205
- // The format string itself must be string-castable, but placeholder arguments
4206
- // are allowed to be any bound non-variable term. Plain strings/IRIs keep their
4207
- // direct string value; other terms fall back to N3 rendering so formatting a
4208
- // bound blank node, list, or quoted formula does not make the whole builtin fail.
4383
+ // Supports a small printf/sprintf subset: %% , %s, %d/%i/%u, %f/%F,
4384
+ // %e/%E, %g/%G, %c plus width/precision and - / 0 flags.
4385
+ // The format string itself must be string-castable. %s accepts any bound
4386
+ // non-variable term: plain strings/IRIs keep their direct string value;
4387
+ // other bound terms fall back to N3 rendering. Numeric directives require
4388
+ // numerically parseable literals and otherwise make the builtin fail.
4209
4389
  if (pv === STRING_NS + 'format') {
4210
4390
  if (!(g.s instanceof ListTerm) || g.s.elems.length < 1) return [];
4211
4391
  const fmtStr = termToJsString(g.s.elems[0]);
4212
4392
  if (fmtStr === null) return [];
4213
4393
 
4214
- const args = [];
4215
- for (let i = 1; i < g.s.elems.length; i++) {
4216
- const aStr = termToFormatArgString(g.s.elems[i]);
4217
- if (aStr === null) return [];
4218
- args.push(aStr);
4219
- }
4220
-
4221
- const formatted = simpleStringFormat(fmtStr, args);
4394
+ const formatted = simpleStringFormat(fmtStr, g.s.elems.slice(1));
4222
4395
  if (formatted === null) return []; // unsupported format specifier(s)
4223
4396
 
4224
4397
  const lit = makeStringLiteral(formatted);
@@ -8,6 +8,18 @@ In one line:
8
8
 
9
9
  > `examples/arcling/` presents ARC cases in mathematical English with reference ECMAScript realizations and JSON test vectors.
10
10
 
11
+ ## Insight Economy context
12
+
13
+ The `delfour`, `medior`, and `flandor` cases are concrete Arcling readings of Ruben Verborgh’s [Inside the Insight Economy](https://ruben.verborgh.org/blog/2025/08/12/inside-the-insight-economy/). The central move is the same in all three cases: what gets traded is not risky raw data, but a narrow, expiring, purpose-bound insight that is useful enough to trigger action. In Ruben’s phrasing, the goal is to “don’t exchange raw data” and to prefer “meaningful insights, not risky raw data”.
14
+
15
+ In this directory, the three cases show that pattern across three settings:
16
+
17
+ - **Delfour** keeps a household-level medical condition private and turns it into a neutral shopping insight such as “prefer lower-sugar products” for shopping assistance.
18
+ - **Medior** keeps laboratory, medication, and readmission evidence local and turns it into a minimal post-discharge coordination insight that can justify activating a continuity bundle.
19
+ - **Flandor** keeps exporter, labour-market, and grid evidence local and turns it into a regional macro-economic insight that justifies a temporary retooling response.
20
+
21
+ That progression is intentional: `delfour` is the micro case, `medior` is the care-coordination case, and `flandor` is the macro case. They are easiest to read next to their declarative Eyeling counterparts: `examples/delfour.n3`, `examples/medior.n3`, and `examples/flandor.n3`.
22
+
11
23
  ## Why this directory exists
12
24
 
13
25
  Eyeling already has a strong way to present a case in declarative N3. Arcling adds a second presentation layer for cases that benefit from a normative mathematical-English statement plus a compact reference model.
@@ -144,7 +156,7 @@ A check should add confidence. It should not only restate the answer.
144
156
 
145
157
  ### 5. Keep names aligned
146
158
 
147
- If a case is called `delfour` or `flandor` in `examples/`, the Arcling case should use the same base name.
159
+ If a case is called `delfour`, `medior`, or `flandor` in `examples/`, the Arcling case should use the same base name.
148
160
 
149
161
  ## Suggested workflow for a new case
150
162
 
@@ -4,11 +4,11 @@
4
4
 
5
5
  This document is the **normative specification** for the Delfour case. The file `delfour.model.mjs` is the **reference ECMAScript implementation** of these clauses. The file `delfour.data.json` is the **instance** evaluated in this bundle. The file `delfour.expected.json` is the **conformance vector** for that instance.
6
6
 
7
- ## Aha
7
+ ## Insight Economy context
8
8
 
9
- The scanner does not need the diagnosis. It only needs the right shopping conclusion.
9
+ This case is the household-scale reading of Ruben Verborgh’s [Inside the Insight Economy](https://ruben.verborgh.org/blog/2025/08/12/inside-the-insight-economy/). Its core claim is that a person can share a useful shopping hint without exposing sensitive health details. A phone turns a private condition into a neutral, limited insight such as "prefer lower-sugar products", attaches clear usage rules and an expiry time, and sends it to a store scanner.
10
10
 
11
- A household-level medical condition remains private on the phone. What crosses the store boundary is a narrow, signed, expiring shopping insight: prefer lower-sugar products while scanning. The scanner receives enough truth to help with the purchase, but not enough detail to infer or reuse the underlying condition for marketing.
11
+ The scanner may use that insight to suggest a better product, but not for unrelated purposes such as marketing. The scanner does not need the diagnosis. It only needs the right shopping conclusion.
12
12
 
13
13
  ## Conventions
14
14
 
@@ -4,11 +4,11 @@
4
4
 
5
5
  This document is the **normative specification** for the Flandor case. The file `flandor.model.mjs` is the **reference ECMAScript implementation** of these clauses. The file `flandor.data.json` is the **instance** evaluated in this bundle. The file `flandor.expected.json` is the **conformance vector** for that instance.
6
6
 
7
- ## Aha
7
+ ## Insight Economy context
8
8
 
9
- Nobody has to reveal their books for the region to coordinate.
9
+ This case is the macro-economic reading of Ruben Verborgh’s [Inside the Insight Economy](https://ruben.verborgh.org/blog/2025/08/12/inside-the-insight-economy/). Its core claim is that nobody has to reveal their books for the region to coordinate. Exporters, training actors, and grid operators each keep their sensitive data local. What crosses the policy boundary is not the underlying evidence, but a narrow, signed, expiring insight: Flanders presently faces enough combined pressure to justify a temporary retooling response.
10
10
 
11
- Firm-side, labour-side, and grid-side evidence remain local. What crosses the policy boundary is a narrow, signed, expiring conclusion: Flanders presently faces enough combined pressure to justify a temporary retooling response. The traded product is not raw data, but a permissioned conclusion.
11
+ The product being traded is therefore not raw data, and not even a general forecast, but a context-bound permissioned conclusion: a policy-grade insight for regional stabilization, with reuse for firm surveillance explicitly forbidden.
12
12
 
13
13
  ## Conventions
14
14
 
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "./medior.instance.schema.json",
3
+ "caseName": "Medior",
4
+ "region": "Flanders",
5
+ "question": "Is the discharge coordination team allowed to use a minimal continuity insight after hospital discharge, and if so which package should it activate?",
6
+ "timestamps": {
7
+ "createdAt": "2026-04-09T08:00:00+00:00",
8
+ "expiresAt": "2026-04-11T08:00:00+00:00",
9
+ "authorizedAt": "2026-04-09T09:15:00+00:00",
10
+ "dutyPerformedAt": "2026-04-10T19:30:00+00:00"
11
+ },
12
+ "evaluationContext": {
13
+ "scopeDevice": "discharge-coordination-team",
14
+ "scopeEvent": "48h-post-discharge-window",
15
+ "purpose": "care_coordination",
16
+ "prohibitedReusePurpose": "insurance_pricing"
17
+ },
18
+ "thresholds": {
19
+ "egfrBelow": 60,
20
+ "activeMedicationCountAtLeast": 8,
21
+ "admissionsLast180DaysAtLeast": 1,
22
+ "hoursSinceDischargeAtMost": 48,
23
+ "activeNeedCountAtLeast": 3
24
+ },
25
+ "signals": {
26
+ "lab": {
27
+ "egfr": 52
28
+ },
29
+ "medications": {
30
+ "activeMedicationCount": 9
31
+ },
32
+ "history": {
33
+ "admissionsLast180Days": 2
34
+ },
35
+ "discharge": {
36
+ "hoursSinceDischarge": 18
37
+ }
38
+ },
39
+ "budget": {
40
+ "windowName": "post-discharge continuity window",
41
+ "maxEUR": 5
42
+ },
43
+ "packages": [
44
+ {
45
+ "id": "pkg:CALL_001",
46
+ "name": "Nurse follow-up call",
47
+ "costEUR": 1,
48
+ "touches": 1,
49
+ "coversRenalSafetyConcern": false,
50
+ "coversPolypharmacyRisk": false,
51
+ "coversReadmissionHistory": false,
52
+ "coversRecentDischargeWindow": true
53
+ },
54
+ {
55
+ "id": "pkg:MEDREC_002",
56
+ "name": "Medication reconciliation bundle",
57
+ "costEUR": 2,
58
+ "touches": 2,
59
+ "coversRenalSafetyConcern": true,
60
+ "coversPolypharmacyRisk": true,
61
+ "coversReadmissionHistory": false,
62
+ "coversRecentDischargeWindow": false
63
+ },
64
+ {
65
+ "id": "pkg:MEDIOR_004",
66
+ "name": "Medior Continuity Pulse",
67
+ "costEUR": 4,
68
+ "touches": 3,
69
+ "coversRenalSafetyConcern": true,
70
+ "coversPolypharmacyRisk": true,
71
+ "coversReadmissionHistory": true,
72
+ "coversRecentDischargeWindow": true
73
+ },
74
+ {
75
+ "id": "pkg:TRANSITION_006",
76
+ "name": "Extended transition program",
77
+ "costEUR": 6,
78
+ "touches": 4,
79
+ "coversRenalSafetyConcern": true,
80
+ "coversPolypharmacyRisk": true,
81
+ "coversReadmissionHistory": true,
82
+ "coversRecentDischargeWindow": true
83
+ }
84
+ ],
85
+ "insightPolicy": {
86
+ "id": "https://example.org/insight/medior",
87
+ "metric": "post_discharge_coordination_priority",
88
+ "suggestionPolicy": "lowest_cost_package_covering_all_active_needs",
89
+ "type": "ins:Insight",
90
+ "policyProfile": "Medior-Insight-Policy",
91
+ "policyType": "odrl:Policy"
92
+ },
93
+ "integrity": {
94
+ "secret": "medior-demo-shared-secret",
95
+ "verificationMode": "trustedPrecomputedInput"
96
+ }
97
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "caseName": "Medior",
3
+ "derived": {
4
+ "renalSafetyConcern": true,
5
+ "polypharmacyRisk": true,
6
+ "readmissionHistory": true,
7
+ "recentDischargeWindow": true,
8
+ "activeNeedCount": 4,
9
+ "needsContinuityBundle": true,
10
+ "eligiblePackageIds": ["pkg:MEDIOR_004"],
11
+ "recommendedPackageId": "pkg:MEDIOR_004",
12
+ "recommendedPackageName": "Medior Continuity Pulse"
13
+ },
14
+ "envelope": {
15
+ "insight": {
16
+ "createdAt": "2026-04-09T08:00:00+00:00",
17
+ "expiresAt": "2026-04-11T08:00:00+00:00",
18
+ "id": "https://example.org/insight/medior",
19
+ "metric": "post_discharge_coordination_priority",
20
+ "region": "Flanders",
21
+ "scopeDevice": "discharge-coordination-team",
22
+ "scopeEvent": "48h-post-discharge-window",
23
+ "suggestionPolicy": "lowest_cost_package_covering_all_active_needs",
24
+ "threshold": 3,
25
+ "type": "ins:Insight"
26
+ },
27
+ "policy": {
28
+ "duty": {
29
+ "action": "odrl:delete",
30
+ "constraint": {
31
+ "leftOperand": "odrl:dateTime",
32
+ "operator": "odrl:eq",
33
+ "rightOperand": "2026-04-11T08:00:00+00:00"
34
+ }
35
+ },
36
+ "permission": {
37
+ "action": "odrl:use",
38
+ "constraint": {
39
+ "leftOperand": "odrl:purpose",
40
+ "operator": "odrl:eq",
41
+ "rightOperand": "care_coordination"
42
+ },
43
+ "target": "https://example.org/insight/medior"
44
+ },
45
+ "profile": "Medior-Insight-Policy",
46
+ "prohibition": {
47
+ "action": "odrl:distribute",
48
+ "constraint": {
49
+ "leftOperand": "odrl:purpose",
50
+ "operator": "odrl:eq",
51
+ "rightOperand": "insurance_pricing"
52
+ },
53
+ "target": "https://example.org/insight/medior"
54
+ },
55
+ "type": "odrl:Policy"
56
+ }
57
+ },
58
+ "integrity": {
59
+ "canonicalEnvelope": "{\"insight\":{\"createdAt\":\"2026-04-09T08:00:00+00:00\",\"expiresAt\":\"2026-04-11T08:00:00+00:00\",\"id\":\"https://example.org/insight/medior\",\"metric\":\"post_discharge_coordination_priority\",\"region\":\"Flanders\",\"scopeDevice\":\"discharge-coordination-team\",\"scopeEvent\":\"48h-post-discharge-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-11T08:00:00+00:00\"}},\"permission\":{\"action\":\"odrl:use\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"care_coordination\"},\"target\":\"https://example.org/insight/medior\"},\"profile\":\"Medior-Insight-Policy\",\"prohibition\":{\"action\":\"odrl:distribute\",\"constraint\":{\"leftOperand\":\"odrl:purpose\",\"operator\":\"odrl:eq\",\"rightOperand\":\"insurance_pricing\"},\"target\":\"https://example.org/insight/medior\"},\"type\":\"odrl:Policy\"}}",
60
+ "payloadHashSHA256": "b5fec8971d6c5e313a1387f08151f1c3203effce05d6455469c9f63305be05ae",
61
+ "envelopeHmacSHA256": "072f4c2774ce362e660649d145c9d784e5e95d63d24d4057b119a419c6ba34dc",
62
+ "verificationMode": "trustedPrecomputedInput"
63
+ },
64
+ "answer": {
65
+ "name": "Medior",
66
+ "region": "Flanders",
67
+ "metric": "post_discharge_coordination_priority",
68
+ "activeNeedCount": 4,
69
+ "threshold": 3,
70
+ "recommendedPackage": "Medior Continuity Pulse",
71
+ "budgetCapEUR": 5,
72
+ "packageCostEUR": 4,
73
+ "payloadHashSHA256": "b5fec8971d6c5e313a1387f08151f1c3203effce05d6455469c9f63305be05ae",
74
+ "envelopeHmacSHA256": "072f4c2774ce362e660649d145c9d784e5e95d63d24d4057b119a419c6ba34dc"
75
+ },
76
+ "reasonWhy": [
77
+ "RenalSafetyConcern holds because eGFR = 52 and the threshold is < 60.",
78
+ "PolypharmacyRisk holds because the active medication count is 9 and the threshold is ≥ 8.",
79
+ "ReadmissionHistory holds because admissionsLast180Days = 2 and the threshold is ≥ 1.",
80
+ "RecentDischargeWindow holds because hoursSinceDischarge = 18 and the threshold is ≤ 48.",
81
+ "The recommendation rule selects the least-cost package that covers every active need and remains within budget.",
82
+ "The selected package is \"Medior Continuity Pulse\" with cost €4, touches=3.",
83
+ "Use is permitted only for purpose \"care_coordination\" and expires at 2026-04-11T08:00:00+00:00."
84
+ ],
85
+ "checks": {
86
+ "payloadHashMatches": true,
87
+ "signatureVerifies": true,
88
+ "thresholdReached": true,
89
+ "scopeComplete": true,
90
+ "minimizationRespected": true,
91
+ "authorizationAllowed": true,
92
+ "dutyTimely": true,
93
+ "insurancePricingProhibited": true,
94
+ "packageWithinBudget": true,
95
+ "packageCoversAllActiveNeeds": true,
96
+ "lowestCostEligiblePackageChosen": true
97
+ },
98
+ "allChecksPass": true,
99
+ "arcText": "=== Answer ===\nName: Medior\nRegion: Flanders\nMetric: post_discharge_coordination_priority\nActive need count: 4/3\nRecommended package: Medior Continuity Pulse\nBudget cap: €5\nPackage cost: €4\nPayload SHA-256: b5fec8971d6c5e313a1387f08151f1c3203effce05d6455469c9f63305be05ae\nEnvelope HMAC-SHA-256: 072f4c2774ce362e660649d145c9d784e5e95d63d24d4057b119a419c6ba34dc\n\n=== Reason Why ===\nRenalSafetyConcern holds because eGFR = 52 and the threshold is < 60.\nPolypharmacyRisk holds because the active medication count is 9 and the threshold is ≥ 8.\nReadmissionHistory holds because admissionsLast180Days = 2 and the threshold is ≥ 1.\nRecentDischargeWindow holds because hoursSinceDischarge = 18 and the threshold is ≤ 48.\nThe recommendation rule selects the least-cost package that covers every active need and remains within budget.\nThe selected package is \"Medior Continuity Pulse\" with cost €4, touches=3.\nUse is permitted only for purpose \"care_coordination\" and expires at 2026-04-11T08: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: insurancePricingProhibited\n- PASS: packageWithinBudget\n- PASS: packageCoversAllActiveNeeds\n- PASS: lowestCostEligiblePackageChosen"
100
+ }