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 +6 -4
- package/dist/browser/eyeling.browser.js +207 -34
- package/examples/arcling/README.md +13 -1
- package/examples/arcling/delfour/delfour.spec.md +3 -3
- package/examples/arcling/flandor/flandor.spec.md +3 -3
- package/examples/arcling/medior/medior.data.json +97 -0
- package/examples/arcling/medior/medior.expected.json +100 -0
- package/examples/arcling/medior/medior.instance.schema.json +275 -0
- package/examples/arcling/medior/medior.model.mjs +344 -0
- package/examples/arcling/medior/medior.spec.md +158 -0
- package/examples/medior.n3 +421 -0
- package/examples/output/medior.n3 +32 -0
- package/examples/output/string-builtins-tests.n3 +5 -0
- package/examples/string-builtins-tests.n3 +40 -1
- package/eyeling-builtins.ttl +1 -1
- package/eyeling.js +207 -34
- package/lib/builtins.js +207 -34
- package/package.json +1 -1
- package/test/arcling.test.js +8 -4
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
|
|
1571
|
+
A small `printf`/`sprintf` subset:
|
|
1572
1572
|
|
|
1573
|
-
- Supports
|
|
1574
|
-
-
|
|
1575
|
-
-
|
|
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
|
-
//
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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 === '
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
4205
|
-
//
|
|
4206
|
-
//
|
|
4207
|
-
//
|
|
4208
|
-
// bound
|
|
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
|
|
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
|
-
##
|
|
7
|
+
## Insight Economy context
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
7
|
+
## Insight Economy context
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
+
}
|