eyeling 1.21.7 → 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);
@@ -10,14 +10,15 @@ In one line:
10
10
 
11
11
  ## Insight Economy context
12
12
 
13
- The `delfour` 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 both 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”.
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
14
 
15
- In this directory, the two cases show that pattern at two scales:
15
+ In this directory, the three cases show that pattern across three settings:
16
16
 
17
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.
18
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.
19
20
 
20
- That pairing is intentional: `delfour` is the micro case and `flandor` is the macro case. Both are easier to understand if read next to their declarative Eyeling counterparts: `examples/delfour.n3` and `examples/flandor.n3`.
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`.
21
22
 
22
23
  ## Why this directory exists
23
24
 
@@ -155,7 +156,7 @@ A check should add confidence. It should not only restate the answer.
155
156
 
156
157
  ### 5. Keep names aligned
157
158
 
158
- 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.
159
160
 
160
161
  ## Suggested workflow for a new case
161
162
 
@@ -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
+ }