eyeling 1.16.3 → 1.16.5

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.
Files changed (61) hide show
  1. package/HANDBOOK.md +153 -0
  2. package/README.md +0 -1
  3. package/examples/auroracare.n3 +528 -0
  4. package/examples/control-system.n3 +223 -47
  5. package/examples/delfour.n3 +409 -0
  6. package/examples/gps.n3 +144 -53
  7. package/examples/ill-formed-literals.n3 +195 -0
  8. package/examples/output/auroracare.n3 +118 -0
  9. package/examples/output/control-system.n3 +24 -1
  10. package/examples/output/delfour.n3 +40 -0
  11. package/examples/output/gps.n3 +14 -2
  12. package/examples/output/ill-formed-literals.n3 +27 -0
  13. package/examples/output/parcellocker.n3 +15 -0
  14. package/examples/parcellocker.n3 +164 -0
  15. package/eyeling.js +16 -1
  16. package/lib/engine.js +14 -1
  17. package/lib/prelude.js +2 -0
  18. package/package.json +2 -3
  19. package/arctifacts/README.md +0 -59
  20. package/arctifacts/ackermann.html +0 -678
  21. package/arctifacts/auroracare.html +0 -1297
  22. package/arctifacts/bike-trip.html +0 -752
  23. package/arctifacts/binomial-theorem.html +0 -631
  24. package/arctifacts/bmi.html +0 -511
  25. package/arctifacts/building-performance.html +0 -750
  26. package/arctifacts/clinical-care.html +0 -726
  27. package/arctifacts/collatz.html +0 -403
  28. package/arctifacts/complex.html +0 -321
  29. package/arctifacts/control-system.html +0 -482
  30. package/arctifacts/delfour.html +0 -849
  31. package/arctifacts/earthquake-epicenter.html +0 -982
  32. package/arctifacts/eco-route.html +0 -662
  33. package/arctifacts/euclid-infinitude.html +0 -564
  34. package/arctifacts/euler-identity.html +0 -667
  35. package/arctifacts/exoplanet-transit.html +0 -1000
  36. package/arctifacts/faltings-theorem.html +0 -1046
  37. package/arctifacts/fibonacci.html +0 -299
  38. package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
  39. package/arctifacts/godel-numbering.html +0 -743
  40. package/arctifacts/gps-bike.html +0 -759
  41. package/arctifacts/gps-clinical-bench.html +0 -792
  42. package/arctifacts/graph-french.html +0 -449
  43. package/arctifacts/grass-molecular.html +0 -592
  44. package/arctifacts/group-theory.html +0 -740
  45. package/arctifacts/health-info.html +0 -833
  46. package/arctifacts/kaprekar-constant.html +0 -576
  47. package/arctifacts/lee.html +0 -805
  48. package/arctifacts/linked-lists.html +0 -502
  49. package/arctifacts/lldm.html +0 -612
  50. package/arctifacts/matrix-multiplication.html +0 -502
  51. package/arctifacts/matrix.html +0 -651
  52. package/arctifacts/newton-raphson.html +0 -944
  53. package/arctifacts/peano-factorial.html +0 -456
  54. package/arctifacts/pi.html +0 -363
  55. package/arctifacts/polynomial.html +0 -646
  56. package/arctifacts/prime.html +0 -366
  57. package/arctifacts/pythagorean-theorem.html +0 -468
  58. package/arctifacts/rest-path.html +0 -469
  59. package/arctifacts/roots-of-unity.html +0 -363
  60. package/arctifacts/turing.html +0 -409
  61. package/arctifacts/wind-turbines.html +0 -726
@@ -1,833 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Health Info Processing (FHIR + LOINC/SNOMED)</title>
7
- <style>
8
- :root {
9
- --bg: #f7f9fc;
10
- --card: #ffffff;
11
- --muted: #5b6b87;
12
- --text: #0f172a;
13
- --accent: #0ea5e9;
14
- --ok: #16a34a;
15
- --bad: #dc2626;
16
- --border: #e5e7eb;
17
- }
18
- html,
19
- body {
20
- height: 100%;
21
- background: var(--bg);
22
- color: var(--text);
23
- font:
24
- 15px/1.5 system-ui,
25
- -apple-system,
26
- Segoe UI,
27
- Roboto,
28
- Inter,
29
- Helvetica,
30
- Arial,
31
- sans-serif;
32
- }
33
- h1 {
34
- font-weight: 700;
35
- letter-spacing: 0.2px;
36
- margin: 18px 0 6px;
37
- }
38
- h2 {
39
- font-size: 13px;
40
- color: var(--muted);
41
- margin: 0 0 8px;
42
- letter-spacing: 0.2px;
43
- text-transform: uppercase;
44
- }
45
- .wrap {
46
- max-width: 980px;
47
- margin: 0 auto;
48
- padding: 24px;
49
- }
50
- .stack {
51
- display: grid;
52
- grid-template-columns: 1fr;
53
- gap: 14px;
54
- }
55
- .card {
56
- background: var(--card);
57
- border: 1px solid var(--border);
58
- border-radius: 14px;
59
- padding: 14px;
60
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.04);
61
- }
62
- .row {
63
- display: flex;
64
- gap: 10px;
65
- align-items: center;
66
- flex-wrap: wrap;
67
- }
68
- button {
69
- border: 0;
70
- background: linear-gradient(180deg, #7dd3fc, #38bdf8);
71
- color: #052436;
72
- padding: 10px 14px;
73
- border-radius: 12px;
74
- font-weight: 700;
75
- cursor: pointer;
76
- box-shadow: 0 3px 12px rgba(56, 189, 248, 0.35);
77
- }
78
- button.secondary {
79
- background: #f3f4f6;
80
- color: #111827;
81
- border: 1px solid var(--border);
82
- box-shadow: none;
83
- }
84
- .output {
85
- min-height: 0;
86
- white-space: pre-wrap;
87
- background: #fbfdff;
88
- border: 1px solid var(--border);
89
- border-radius: 10px;
90
- padding: 10px;
91
- overflow: auto;
92
- }
93
- .output.tall {
94
- min-height: 160px;
95
- }
96
- .muted {
97
- color: var(--muted);
98
- }
99
- .tiny {
100
- font-size: 12px;
101
- }
102
- .diag {
103
- font-size: 12px;
104
- color: #6b7280;
105
- }
106
- /* badges */
107
- .badge {
108
- display: inline-block;
109
- padding: 2px 8px;
110
- border-radius: 999px;
111
- font-size: 12px;
112
- font-weight: 700;
113
- letter-spacing: 0.3px;
114
- border: 1px solid var(--border);
115
- background: #f5f7fb;
116
- color: #334155;
117
- }
118
- /* textareas */
119
- textarea,
120
- pre,
121
- code,
122
- input,
123
- button {
124
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Courier New', monospace;
125
- }
126
- textarea {
127
- width: 100%;
128
- min-height: 0;
129
- height: auto;
130
- overflow: hidden;
131
- resize: none;
132
- border-radius: 10px;
133
- padding: 10px;
134
- border: 1px solid var(--border);
135
- background: #fbfdff;
136
- color: var(--text);
137
- box-sizing: border-box;
138
- white-space: pre-wrap;
139
- }
140
- </style>
141
- </head>
142
- <body>
143
- <div class="wrap">
144
- <h1>Health Info Processing — FHIR + LOINC/SNOMED</h1>
145
-
146
- <div class="stack">
147
- <!-- What this is? -->
148
- <div class="card" id="what">
149
- <h2>What this is?</h2>
150
- <p>
151
- One-file, pure JS + JSON demo that mimics FHIR-ish resources and coding. Edit the JSON below and click
152
- <b>Run ARC</b>:
153
- </p>
154
- <ul>
155
- <li>
156
- <b>Data</b> — FHIR-flavored resources: <code>Patient</code>, <code>Encounter</code>,
157
- <code>DocumentReference</code>, <code>Observation</code>, <code>Condition</code>, <code>Consent</code>;
158
- lab codes (LOINC), condition codes (SNOMED).
159
- </li>
160
- <li>
161
- <b>Policies</b> — declarative JSON rules: an array of objects with <code>if</code> atoms (allowing
162
- <code>"not": true</code>) ⇒ <code>then</code> atoms, plus an <code>explain</code> template.
163
- </li>
164
- <li>
165
- <b>Answer</b> — the <em>newly derived</em> facts only (keeps the strongest <code>priority</code> per
166
- patient).
167
- </li>
168
- <li><b>Reason Why</b> — mathematical-English derivations based on bound variables per rule firing.</li>
169
- <li>
170
- <b>Check</b> — JSON assertions evaluated against the full fact set (<span
171
- id="checksCountInline"
172
- class="badge"
173
- >…</span
174
- >
175
- items).
176
- </li>
177
- <li>
178
- <b>Health Info Summary</b> — per-patient strongest priority and tasks. Priority order: Blocked &gt; High
179
- &gt; Normal &gt; Low.
180
- </li>
181
- </ul>
182
- <p class="tiny muted">
183
- Engine: tiny forward-chainer with unification, subclassing (<code>subClass</code>), transitive
184
- <code>partOf</code>, observation→patient mapping, and negation-as-failure for <code>"not": true</code>.
185
- </p>
186
- </div>
187
-
188
- <div class="card">
189
- <h2>Data (JSON)</h2>
190
- <textarea id="dataTA" spellcheck="false">
191
- {
192
- "classes": [
193
- "Patient","Encounter","DocumentReference","Observation","Condition","Consent","Component",
194
- "ObservationClass","ClinicalIssue","PrivacyIssue","DataQualityIssue","WorkflowEvent",
195
- "ShareRequested",
196
- "Task","Priority","Blocked","High","Normal","Low"
197
- ],
198
-
199
- "subclassOf": [
200
- ["Encounter","Component"], ["DocumentReference","Component"],
201
- ["ObservationClass","Observation"],
202
- ["ShareRequested","WorkflowEvent"],
203
- ["PrivacyIssue","Observation"], ["DataQualityIssue","Observation"], ["ClinicalIssue","Observation"], ["WorkflowEvent","Observation"]
204
- ],
205
-
206
- "assets": {
207
- "P1": {"type":"Patient"},
208
- "Enc1":{"type":"Encounter","partOf":"P1"},
209
- "Note1":{"type":"DocumentReference","partOf":"Enc1"},
210
-
211
- "P2": {"type":"Patient"},
212
- "Enc2":{"type":"Encounter","partOf":"P2"},
213
- "Note2":{"type":"DocumentReference","partOf":"Enc2"}
214
- },
215
-
216
- "consents": [
217
- {"id":"C1","type":"Consent","partOf":"P1","scope":"treatment","status":"active"},
218
- {"id":"C3","type":"Consent","partOf":"P2","scope":"research","status":"active","permittedOrg":"OrgR1"}
219
- ],
220
-
221
- "conditions": [
222
- {"id":"CondDM2","type":"Condition","partOf":"P2","codeSystem":"unknown","code":"E11"}
223
- ],
224
-
225
- "observations": [
226
- {"id":"ObsPHI1","type":"PrivacyIssue","about":"Note1","codeSystem":"custom","code":"PHIInNote"},
227
- {"id":"SR1","type":"ShareRequested","about":"P1","sharePurpose":"treatment","shareOrg":"OrgH1"},
228
- {"id":"ObsHb1","type":"Observation","about":"Enc1","codeSystem":"loinc","code":"718-7","category":"laboratory","valueFlag":"criticalLow"},
229
- {"id":"ObsCovid1","type":"Observation","about":"Enc1","codeSystem":"loinc","code":"94531-1","category":"laboratory","valueFlag":"positive"},
230
-
231
- {"id":"SR2","type":"ShareRequested","about":"P2","sharePurpose":"research","shareOrg":"OrgR1"},
232
- {"id":"ObsUnknownLab2","type":"Observation","about":"Enc2","codeSystem":"unknown","code":"X123","category":"laboratory"},
233
- {"id":"ObsPHI2","type":"PrivacyIssue","about":"Note2","codeSystem":"custom","code":"PHIInNote"}
234
- ]
235
- }</textarea
236
- >
237
- </div>
238
-
239
- <div class="card">
240
- <h2>Policies (declarative JSON rules)</h2>
241
- <textarea id="policyTA" spellcheck="false">
242
- [
243
- { "id":"R1-Subclass",
244
- "if":[{"pred":"subClass","s":"?A","o":"?B"},{"pred":"isA","s":"?S","o":"?A"}],
245
- "then":[{"pred":"isA","s":"?S","o":"?B"}],
246
- "explain":"Since ?A ⊑ ?B and ?S ∈ ?A, infer ?S ∈ ?B."
247
- },
248
- { "id":"R2-PartOfTransitive",
249
- "if":[{"pred":"partOf","s":"?x","o":"?y"},{"pred":"partOf","s":"?y","o":"?z"}],
250
- "then":[{"pred":"partOf","s":"?x","o":"?z"}],
251
- "explain":"Because ?x is part of ?y and ?y is part of ?z, ?x is part of ?z."
252
- },
253
- { "id":"R3-ObsToPatient",
254
- "if":[{"pred":"about","s":"?obs","o":"?comp"},{"pred":"partOf","s":"?comp","o":"?P"}],
255
- "then":[{"pred":"aboutPatient","s":"?obs","o":"?P"}],
256
- "explain":"Observation ?obs concerns component ?comp of patient ?P; thus aboutPatient(?obs,?P)."
257
- },
258
-
259
- { "id":"R4-ConsentToPatient",
260
- "if":[{"pred":"isA","s":"?C","o":"Consent"},{"pred":"partOf","s":"?C","o":"?P"}],
261
- "then":[{"pred":"consentOfPatient","s":"?C","o":"?P"}],
262
- "explain":"Consent ?C belongs to patient ?P."
263
- },
264
-
265
- { "id":"R5-ActiveResearchConsent-Org",
266
- "if":[{"pred":"isA","s":"?C","o":"Consent"},{"pred":"consentOfPatient","s":"?C","o":"?P"},
267
- {"pred":"consentScope","s":"?C","o":"research"},{"pred":"consentStatus","s":"?C","o":"active"},
268
- {"pred":"consentPermittedOrg","s":"?C","o":"?Org"}],
269
- "then":[{"pred":"hasActiveResearchConsentFor","s":"?P","o":"?Org"}],
270
- "explain":"?P has an active research consent permitting org ?Org."
271
- },
272
- { "id":"R6-ActiveResearchConsent-AnyOrg",
273
- "if":[{"pred":"isA","s":"?C","o":"Consent"},{"pred":"consentOfPatient","s":"?C","o":"?P"},
274
- {"pred":"consentScope","s":"?C","o":"research"},{"pred":"consentStatus","s":"?C","o":"active"},
275
- {"not":true,"pred":"consentPermittedOrg","s":"?C","o":"?x"}],
276
- "then":[{"pred":"hasActiveResearchConsentFor","s":"?P","o":"ANY"}],
277
- "explain":"?P has an active research consent without org restriction."
278
- },
279
- { "id":"R7-ActiveTreatmentConsent",
280
- "if":[{"pred":"isA","s":"?C","o":"Consent"},{"pred":"consentOfPatient","s":"?C","o":"?P"},
281
- {"pred":"consentScope","s":"?C","o":"treatment"},{"pred":"consentStatus","s":"?C","o":"active"}],
282
- "then":[{"pred":"hasActiveTreatmentConsent","s":"?P","o":"yes"}],
283
- "explain":"?P has an active treatment consent."
284
- },
285
-
286
- { "id":"C-PHI-Handling",
287
- "if":[{"pred":"isA","s":"?obs","o":"PrivacyIssue"},{"pred":"aboutPatient","s":"?obs","o":"?P"}],
288
- "then":[{"pred":"requiresTask","s":"?P","o":"DeidentifyText"}],
289
- "explain":"PHI present for ?P requires de-identification."
290
- },
291
-
292
- { "id":"Share-Research-Allow-Org",
293
- "if":[{"pred":"isA","s":"?sr","o":"ShareRequested"},{"pred":"aboutPatient","s":"?sr","o":"?P"},
294
- {"pred":"sharePurpose","s":"?sr","o":"research"},{"pred":"shareOrg","s":"?sr","o":"?Org"},
295
- {"pred":"hasActiveResearchConsentFor","s":"?P","o":"?Org"}],
296
- "then":[{"pred":"requiresTask","s":"?P","o":"AllowResearchShare"},{"pred":"priority","s":"?P","o":"Normal"}],
297
- "explain":"Research share for ?P allowed via matching active consent to org ?Org."
298
- },
299
- { "id":"Share-Research-Allow-Any",
300
- "if":[{"pred":"isA","s":"?sr","o":"ShareRequested"},{"pred":"aboutPatient","s":"?sr","o":"?P"},
301
- {"pred":"sharePurpose","s":"?sr","o":"research"},{"pred":"shareOrg","s":"?sr","o":"?Org"},
302
- {"pred":"hasActiveResearchConsentFor","s":"?P","o":"ANY"}],
303
- "then":[{"pred":"requiresTask","s":"?P","o":"AllowResearchShare"},{"pred":"priority","s":"?P","o":"Normal"}],
304
- "explain":"Research share for ?P allowed by active consent without org restriction."
305
- },
306
- { "id":"Share-Research-Block",
307
- "if":[{"pred":"isA","s":"?sr","o":"ShareRequested"},{"pred":"aboutPatient","s":"?sr","o":"?P"},
308
- {"pred":"sharePurpose","s":"?sr","o":"research"},{"pred":"shareOrg","s":"?sr","o":"?Org"},
309
- {"not":true,"pred":"hasActiveResearchConsentFor","s":"?P","o":"?Org"},
310
- {"not":true,"pred":"hasActiveResearchConsentFor","s":"?P","o":"ANY"}],
311
- "then":[{"pred":"requiresTask","s":"?P","o":"ObtainResearchConsent"},{"pred":"priority","s":"?P","o":"Blocked"}],
312
- "explain":"Research share for ?P is blocked: no suitable active research consent."
313
- },
314
-
315
- { "id":"Share-Treatment-Allow",
316
- "if":[{"pred":"isA","s":"?sr","o":"ShareRequested"},{"pred":"aboutPatient","s":"?sr","o":"?P"},
317
- {"pred":"sharePurpose","s":"?sr","o":"treatment"},{"pred":"hasActiveTreatmentConsent","s":"?P","o":"yes"}],
318
- "then":[{"pred":"requiresTask","s":"?P","o":"AllowTreatmentShare"},{"pred":"priority","s":"?P","o":"Normal"}],
319
- "explain":"Treatment share for ?P allowed by active consent."
320
- },
321
- { "id":"Share-Treatment-Block",
322
- "if":[{"pred":"isA","s":"?sr","o":"ShareRequested"},{"pred":"aboutPatient","s":"?sr","o":"?P"},
323
- {"pred":"sharePurpose","s":"?sr","o":"treatment"},
324
- {"not":true,"pred":"hasActiveTreatmentConsent","s":"?P","o":"yes"}],
325
- "then":[{"pred":"requiresTask","s":"?P","o":"ObtainTreatmentConsent"},{"pred":"priority","s":"?P","o":"Blocked"}],
326
- "explain":"Treatment share for ?P is blocked: no active treatment consent."
327
- },
328
-
329
- { "id":"Map-UnknownLab→LOINC",
330
- "if":[{"pred":"isA","s":"?obs","o":"Observation"},{"pred":"aboutPatient","s":"?obs","o":"?P"},
331
- {"pred":"category","s":"?obs","o":"laboratory"},{"pred":"codeSystem","s":"?obs","o":"unknown"}],
332
- "then":[{"pred":"requiresTask","s":"?P","o":"MapToLOINC"}],
333
- "explain":"Lab observation for ?P has unknown coding; map to LOINC."
334
- },
335
- { "id":"Map-UnknownCondition→SNOMED",
336
- "if":[{"pred":"isA","s":"?cond","o":"Condition"},{"pred":"partOf","s":"?cond","o":"?P"},
337
- {"pred":"codeSystem","s":"?cond","o":"unknown"}],
338
- "then":[{"pred":"requiresTask","s":"?P","o":"MapToSNOMED"}],
339
- "explain":"Condition for ?P has unknown code; map to SNOMED CT."
340
- },
341
-
342
- { "id":"Clinical-COVIDPCR-Positive",
343
- "if":[{"pred":"isA","s":"?obs","o":"Observation"},{"pred":"aboutPatient","s":"?obs","o":"?P"},
344
- {"pred":"codeSystem","s":"?obs","o":"loinc"},{"pred":"code","s":"?obs","o":"94531-1"},
345
- {"pred":"valueFlag","s":"?obs","o":"positive"}],
346
- "then":[{"pred":"requiresTask","s":"?P","o":"IsolatePatient"},
347
- {"pred":"requiresTask","s":"?P","o":"NotifyPublicHealth"},
348
- {"pred":"priority","s":"?P","o":"High"}],
349
- "explain":"LOINC 94531-1 is positive for ?P → isolation, public health notification (High)."
350
- },
351
- { "id":"Clinical-Hb-CriticalLow",
352
- "if":[{"pred":"isA","s":"?obs","o":"Observation"},{"pred":"aboutPatient","s":"?obs","o":"?P"},
353
- {"pred":"codeSystem","s":"?obs","o":"loinc"},{"pred":"code","s":"?obs","o":"718-7"},
354
- {"pred":"valueFlag","s":"?obs","o":"criticalLow"}],
355
- "then":[{"pred":"requiresTask","s":"?P","o":"UrgentTransfusionConsult"},
356
- {"pred":"priority","s":"?P","o":"High"}],
357
- "explain":"LOINC 718-7 critical low for ?P → urgent transfusion consult (High)."
358
- },
359
-
360
- { "id":"P-Default-Normal",
361
- "if":[{"pred":"requiresTask","s":"?P","o":"?X"},
362
- {"not":true,"pred":"priority","s":"?P","o":"?Any"}],
363
- "then":[{"pred":"priority","s":"?P","o":"Normal"}],
364
- "explain":"If ?P has tasks but no priority, default to Normal."
365
- }
366
- ]</textarea
367
- >
368
- </div>
369
-
370
- <div class="card">
371
- <h2>Checks <span id="checksCount" class="badge">0</span></h2>
372
- <textarea id="checksTA" spellcheck="false">
373
- [
374
- {"name":"PHI in Note1 → about P1", "pattern":{"pred":"aboutPatient","s":"ObsPHI1","o":"P1"}},
375
- {"name":"SR1 (treatment) → about P1", "pattern":{"pred":"aboutPatient","s":"SR1","o":"P1"}},
376
- {"name":"P1 has active treatment consent", "pattern":{"pred":"hasActiveTreatmentConsent","s":"P1","o":"yes"}},
377
- {"name":"Allow treatment share for P1", "pattern":{"pred":"requiresTask","s":"P1","o":"AllowTreatmentShare"}},
378
- {"name":"P1 priority High (labs)", "pattern":{"pred":"priority","s":"P1","o":"High"}},
379
- {"name":"P1 needs UrgentTransfusionConsult", "pattern":{"pred":"requiresTask","s":"P1","o":"UrgentTransfusionConsult"}},
380
- {"name":"P1 needs Isolation (COVID+)", "pattern":{"pred":"requiresTask","s":"P1","o":"IsolatePatient"}},
381
- {"name":"P1 notify public health", "pattern":{"pred":"requiresTask","s":"P1","o":"NotifyPublicHealth"}},
382
- {"name":"P2 has active research consent for OrgR1","pattern":{"pred":"hasActiveResearchConsentFor","s":"P2","o":"OrgR1"}},
383
- {"name":"Allow research share for P2", "pattern":{"pred":"requiresTask","s":"P2","o":"AllowResearchShare"}},
384
- {"name":"P2 map unknown lab to LOINC", "pattern":{"pred":"requiresTask","s":"P2","o":"MapToLOINC"}},
385
- {"name":"P2 map unknown condition to SNOMED", "pattern":{"pred":"requiresTask","s":"P2","o":"MapToSNOMED"}},
386
- {"name":"P2 deidentify text", "pattern":{"pred":"requiresTask","s":"P2","o":"DeidentifyText"}},
387
- {"name":"P2 priority Normal", "pattern":{"pred":"priority","s":"P2","o":"Normal"}}
388
- ]</textarea
389
- >
390
- </div>
391
-
392
- <div class="card">
393
- <h2>Controls</h2>
394
- <div class="row">
395
- <button id="runBtn">▶ Run ARC</button>
396
- <button id="reasonBtn" class="secondary">Show Reason only</button>
397
- <span id="status" class="muted tiny" style="margin-left: auto"></span>
398
- </div>
399
- <div id="diag" class="diag"></div>
400
- </div>
401
-
402
- <div class="card">
403
- <h2>Answer (newly derived facts)</h2>
404
- <div id="answer" class="output tall">computing…</div>
405
- </div>
406
-
407
- <div class="card">
408
- <h2>Reason Why (mathematical English)</h2>
409
- <div id="reason" class="output tall">(click “Run ARC”)</div>
410
- </div>
411
-
412
- <div class="card">
413
- <h2>Check</h2>
414
- <div id="checks" class="output tall">computing…</div>
415
- </div>
416
-
417
- <div class="card" id="summaryCard">
418
- <h2>Health Info Summary</h2>
419
- <div id="summary" class="output">(run to populate)</div>
420
- </div>
421
- </div>
422
- </div>
423
-
424
- <script>
425
- const $ = (id) => document.getElementById(id);
426
- const els = {
427
- dataTA: $('dataTA'),
428
- policyTA: $('policyTA'),
429
- checksTA: $('checksTA'),
430
- runBtn: $('runBtn'),
431
- reasonBtn: $('reasonBtn'),
432
- status: $('status'),
433
- diag: $('diag'),
434
- answer: $('answer'),
435
- reason: $('reason'),
436
- checks: $('checks'),
437
- summary: $('summary'),
438
- checksCount: $('checksCount'),
439
- checksCountInline: $('checksCountInline'),
440
- };
441
-
442
- // Auto-grow textareas
443
- function autoResize(el) {
444
- el.style.height = 'auto';
445
- el.style.height = el.scrollHeight + 'px';
446
- }
447
- ['dataTA', 'policyTA', 'checksTA'].forEach((id) => {
448
- const el = $(id);
449
- el.addEventListener('input', () => autoResize(el));
450
- setTimeout(() => autoResize(el), 0);
451
- });
452
-
453
- // ---- KB helpers
454
- function key(f) {
455
- return `${f.pred}|${f.s}|${f.o}`;
456
- }
457
-
458
- function buildFactsFromData(spec) {
459
- const facts = [];
460
- // subclass axioms
461
- (spec.subclassOf || []).forEach(([a, b]) => facts.push({ pred: 'subClass', s: a, o: b, _base: true }));
462
- // core assets
463
- for (const [id, info] of Object.entries(spec.assets || {})) {
464
- if (info.type) facts.push({ pred: 'isA', s: id, o: info.type, _base: true });
465
- if (info.partOf) facts.push({ pred: 'partOf', s: id, o: info.partOf, _base: true });
466
- }
467
- // consents
468
- (spec.consents || []).forEach((c) => {
469
- facts.push({ pred: 'isA', s: c.id, o: c.type || 'Consent', _base: true });
470
- if (c.partOf) facts.push({ pred: 'partOf', s: c.id, o: c.partOf, _base: true });
471
- if (c.scope) facts.push({ pred: 'consentScope', s: c.id, o: c.scope, _base: true });
472
- if (c.status) facts.push({ pred: 'consentStatus', s: c.id, o: c.status, _base: true });
473
- if (c.permittedOrg) facts.push({ pred: 'consentPermittedOrg', s: c.id, o: c.permittedOrg, _base: true });
474
- });
475
- // conditions
476
- (spec.conditions || []).forEach((d) => {
477
- facts.push({ pred: 'isA', s: d.id, o: d.type || 'Condition', _base: true });
478
- if (d.partOf) facts.push({ pred: 'partOf', s: d.id, o: d.partOf, _base: true });
479
- if (d.codeSystem) facts.push({ pred: 'codeSystem', s: d.id, o: d.codeSystem, _base: true });
480
- if (d.code) facts.push({ pred: 'code', s: d.id, o: d.code, _base: true });
481
- });
482
- // observations/events
483
- (spec.observations || []).forEach((o) => {
484
- facts.push({ pred: 'isA', s: o.id, o: o.type || 'Observation', _base: true });
485
- if (o.about) facts.push({ pred: 'about', s: o.id, o: o.about, _base: true });
486
- if (spec.assets[o.about]?.type === 'Patient') {
487
- facts.push({ pred: 'aboutPatient', s: o.id, o: o.about, _base: true });
488
- }
489
- if (o.codeSystem) facts.push({ pred: 'codeSystem', s: o.id, o: o.codeSystem, _base: true });
490
- if (o.code) facts.push({ pred: 'code', s: o.id, o: o.code, _base: true });
491
- if (o.category) facts.push({ pred: 'category', s: o.id, o: o.category, _base: true });
492
- if (o.valueFlag) facts.push({ pred: 'valueFlag', s: o.id, o: o.valueFlag, _base: true });
493
- if (o.sharePurpose) facts.push({ pred: 'sharePurpose', s: o.id, o: o.sharePurpose, _base: true });
494
- if (o.shareOrg) facts.push({ pred: 'shareOrg', s: o.id, o: o.shareOrg, _base: true });
495
- });
496
- return facts;
497
- }
498
-
499
- function indexFacts(facts) {
500
- const byPred = new Map();
501
- for (const f of facts) {
502
- if (!byPred.has(f.pred)) byPred.set(f.pred, []);
503
- byPred.get(f.pred).push(f);
504
- }
505
- return { byPred };
506
- }
507
-
508
- function isVar(x) {
509
- return typeof x === 'string' && x.startsWith('?');
510
- }
511
-
512
- function unifyAtomWithFact(atom, f, env) {
513
- const out = { ...env };
514
- const slots = ['pred', 's', 'o'];
515
- for (const slot of slots) {
516
- const val = atom[slot];
517
- const factVal = f[slot] ?? (slot === 'pred' ? f.pred : slot === 's' ? f.s : f.o);
518
- if (isVar(val)) {
519
- if (out[val] !== undefined && out[val] !== factVal) return null;
520
- out[val] = factVal;
521
- } else {
522
- if (val !== factVal) return null;
523
- }
524
- }
525
- return out;
526
- }
527
-
528
- function matchPositives(rule, factsIdx, env = {}, i = 0) {
529
- if (i >= rule.if.length) return [env];
530
- const atom = rule.if[i];
531
- if (atom.not) return matchPositives(rule, factsIdx, env, i + 1);
532
- const candidates = factsIdx.byPred.get(atom.pred) || [];
533
- const out = [];
534
- for (const f of candidates) {
535
- const env2 = unifyAtomWithFact(atom, f, env);
536
- if (env2) out.push(...matchPositives(rule, factsIdx, env2, i + 1));
537
- }
538
- return out;
539
- }
540
-
541
- function negHolds(atom, factsIdx, env) {
542
- const candidates = factsIdx.byPred.get(atom.pred) || [];
543
- for (const f of candidates) {
544
- if (unifyAtomWithFact(atom, f, env)) return true;
545
- }
546
- return false;
547
- }
548
-
549
- function substitute(t, env) {
550
- const sub = (v) => (isVar(v) ? env[v] : v);
551
- return { pred: t.pred, s: sub(t.s), o: sub(t.o) };
552
- }
553
-
554
- // ---- Priority handling (Blocked > High > Normal > Low)
555
- const PRIORITY_RANK = { Blocked: 4, High: 3, Normal: 2, Low: 1 };
556
-
557
- function bestPriorities(facts) {
558
- const best = new Map();
559
- for (const f of facts) {
560
- if (f.pred !== 'priority') continue;
561
- const cur = best.get(f.s);
562
- if (!cur || (PRIORITY_RANK[f.o] || 0) > (PRIORITY_RANK[cur] || 0)) {
563
- best.set(f.s, f.o);
564
- }
565
- }
566
- return best;
567
- }
568
-
569
- function derive(data, policies) {
570
- let facts = buildFactsFromData(data);
571
- const baseSet = new Set(facts.map(key));
572
- const factSet = new Set(baseSet);
573
- const factsIdx = indexFacts(facts);
574
- const proofs = new Map();
575
-
576
- let changed = true,
577
- guard = 0;
578
- while (changed && guard++ < 200) {
579
- changed = false;
580
- for (const rule of policies) {
581
- const posBindings = matchPositives(rule, factsIdx, {}, 0);
582
- for (const env of posBindings) {
583
- const negs = rule.if.filter((a) => a.not);
584
- let ok = true,
585
- negUsed = [];
586
- for (const n of negs) {
587
- const nSub = substitute(n, env);
588
- if (negHolds(nSub, factsIdx, env)) {
589
- ok = false;
590
- break;
591
- }
592
- negUsed.push(nSub);
593
- }
594
- if (!ok) continue;
595
-
596
- for (const t of rule.then) {
597
- const concl = substitute(t, env);
598
- const k = key(concl);
599
- if (!factSet.has(k)) {
600
- facts.push({ ...concl, _base: false });
601
- factSet.add(k);
602
- changed = true;
603
- if (!factsIdx.byPred.has(concl.pred)) factsIdx.byPred.set(concl.pred, []);
604
- factsIdx.byPred.get(concl.pred).push({ ...concl, _base: false });
605
- const exp = buildExplanation(rule, env, negUsed);
606
- proofs.set(k, exp);
607
- }
608
- }
609
- }
610
- }
611
- }
612
-
613
- const derived = facts.filter((f) => !f._base);
614
-
615
- // Only keep strongest priority per patient in Answer/Reason
616
- const best = bestPriorities(facts);
617
- const derivedFiltered = derived.filter((f) => f.pred !== 'priority' || best.get(f.s) === f.o);
618
-
619
- const answerLines = formatFacts(derivedFiltered);
620
- const reasonLines = [];
621
- for (const f of derivedFiltered) {
622
- const k = key(f);
623
- const exp = proofs.get(k);
624
- if (exp) reasonLines.push(`• ${exp} ⇒ therefore ${prettyFact(f)}.`);
625
- else reasonLines.push(`• Derived ${prettyFact(f)}.`);
626
- }
627
-
628
- return { facts, derived, answerText: answerLines.join('\n'), reasonText: reasonLines.join('\n') };
629
- }
630
-
631
- function buildExplanation(rule, env, negUsed) {
632
- const fill = (s) => s.replace(/\?[A-Za-z0-9_]+/g, (m) => env[m] ?? m);
633
- let text = fill(rule.explain || `Applied ${rule.id}`);
634
- if (negUsed && negUsed.length) {
635
- const negBits = negUsed.map((n) => `no fact ${n.pred}(${n.s}, ${n.o})`).join(' and ');
636
- text += `, and ${negBits}`;
637
- }
638
- return text;
639
- }
640
-
641
- function prettyFact(f) {
642
- const tri = {
643
- isA: (s, o) => `${s} ∈ ${o}`,
644
- subClass: (s, o) => `${s} ⊑ ${o}`,
645
- partOf: (s, o) => `${s} partOf ${o}`,
646
- about: (s, o) => `${s} about ${o}`,
647
- aboutPatient: (s, o) => `${s} aboutPatient ${o}`,
648
- consentOfPatient: (s, o) => `${s} consentOfPatient ${o}`,
649
- consentScope: (s, o) => `${s} consentScope ${o}`,
650
- consentStatus: (s, o) => `${s} consentStatus ${o}`,
651
- consentPermittedOrg: (s, o) => `${s} consentPermittedOrg ${o}`,
652
- hasActiveResearchConsentFor: (s, o) => `${s} hasActiveResearchConsentFor ${o}`,
653
- hasActiveTreatmentConsent: (s, o) => `${s} hasActiveTreatmentConsent ${o}`,
654
- sharePurpose: (s, o) => `${s} sharePurpose ${o}`,
655
- shareOrg: (s, o) => `${s} shareOrg ${o}`,
656
- codeSystem: (s, o) => `${s} codeSystem ${o}`,
657
- code: (s, o) => `${s} code ${o}`,
658
- category: (s, o) => `${s} category ${o}`,
659
- valueFlag: (s, o) => `${s} valueFlag ${o}`,
660
- requiresTask: (s, o) => `${s} requires ${o}`,
661
- priority: (s, o) => `${s} priority ${o}`,
662
- };
663
- const fn = tri[f.pred] || ((s, o) => `${s} ${f.pred} ${o}`);
664
- return fn(f.s, f.o);
665
- }
666
-
667
- function formatFacts(facts) {
668
- const groups = {};
669
- for (const f of facts) {
670
- (groups[f.pred] ||= []).push(f);
671
- }
672
- const order = [
673
- 'isA',
674
- 'subClass',
675
- 'partOf',
676
- 'about',
677
- 'aboutPatient',
678
- 'consentOfPatient',
679
- 'consentScope',
680
- 'consentStatus',
681
- 'consentPermittedOrg',
682
- 'sharePurpose',
683
- 'shareOrg',
684
- 'codeSystem',
685
- 'code',
686
- 'category',
687
- 'valueFlag',
688
- 'hasActiveResearchConsentFor',
689
- 'hasActiveTreatmentConsent',
690
- 'requiresTask',
691
- 'priority',
692
- ];
693
- const lines = [];
694
- for (const pred of order) {
695
- if (!groups[pred]) continue;
696
- const sorted = groups[pred].slice().sort((a, b) => (a.s + a.o).localeCompare(b.s + b.o));
697
- for (const f of sorted) lines.push(prettyFact(f));
698
- }
699
- for (const pred of Object.keys(groups)) {
700
- if (order.includes(pred)) continue;
701
- const sorted = groups[pred].slice().sort((a, b) => (a.s + a.o).localeCompare(b.s + b.o));
702
- for (const f of sorted) lines.push(prettyFact(f));
703
- }
704
- return lines;
705
- }
706
-
707
- // Checks
708
- function runChecks(facts, checksSpec) {
709
- const factSet = new Set(facts.map(key));
710
- const out = [];
711
- for (let i = 0; i < checksSpec.length; i++) {
712
- const chk = checksSpec[i];
713
- const k = key(chk.pattern);
714
- out.push({ i: i + 1, name: chk.name, passed: factSet.has(k) });
715
- }
716
- return out;
717
- }
718
- function renderChecks(results) {
719
- if (!results.length) return '(no checks)';
720
- const lines = [];
721
- for (const r of results) {
722
- lines.push(`${r.passed ? '✅' : '❌'} ${String(r.i).padStart(2, ' ')} — ${r.name}`);
723
- }
724
- const passCt = results.filter((r) => r.passed).length;
725
- lines.push(`\nSummary: ${passCt}/${results.length} PASS`);
726
- return lines.join('\n');
727
- }
728
-
729
- // Summary
730
- function summarizePatients(facts) {
731
- const PRIORITY_RANK = { Blocked: 4, High: 3, Normal: 2, Low: 1 };
732
- const tasksByP = new Map();
733
- for (const f of facts) {
734
- if (f.pred === 'requiresTask') {
735
- if (!tasksByP.has(f.s)) tasksByP.set(f.s, new Set());
736
- tasksByP.get(f.s).add(f.o);
737
- }
738
- }
739
- // best priority
740
- const best = new Map();
741
- for (const f of facts) {
742
- if (f.pred !== 'priority') continue;
743
- const cur = best.get(f.s);
744
- if (!cur || (PRIORITY_RANK[f.o] || 0) > (PRIORITY_RANK[cur] || 0)) best.set(f.s, f.o);
745
- }
746
- const patients = new Set([...tasksByP.keys(), ...best.keys()]);
747
- if (!patients.size) return '(no derived tasks/priority)';
748
- const lines = [];
749
- for (const p of [...patients].sort()) {
750
- const pr = best.get(p) || 'Normal';
751
- const tasks = [...(tasksByP.get(p) || [])].sort().join(', ');
752
- lines.push(`${p}: priority ${pr}${tasks ? ' — tasks: ' + tasks : ''}`);
753
- }
754
- return lines.join('\n');
755
- }
756
-
757
- // Orchestration
758
- function parseJSON(text, label) {
759
- try {
760
- return JSON.parse(text);
761
- } catch (e) {
762
- throw new Error(`${label} JSON error: ${e.message}`);
763
- }
764
- }
765
- function toPolicies(spec) {
766
- return spec.map((r) => ({
767
- id: r.id || '(unnamed)',
768
- if: r.if || [],
769
- then: r.then || [],
770
- explain: r.explain || '',
771
- }));
772
- }
773
- function buildDataObj(spec) {
774
- return spec;
775
- }
776
-
777
- async function runARC() {
778
- els.status.textContent = 'Parsing JSON…';
779
- els.answer.textContent = els.reason.textContent = els.checks.textContent = 'computing…';
780
- els.summary.textContent = '(run to populate)';
781
- els.diag.textContent = '';
782
- try {
783
- const dataSpec = parseJSON(els.dataTA.value, 'Data');
784
- const polSpec = parseJSON(els.policyTA.value, 'Policies');
785
- const checksSpec = parseJSON(els.checksTA.value, 'Checks');
786
-
787
- // Update checks count badges
788
- els.checksCount.textContent = String(checksSpec.length);
789
- if (els.checksCountInline) els.checksCountInline.textContent = String(checksSpec.length);
790
-
791
- const data = buildDataObj(dataSpec);
792
- const policies = toPolicies(polSpec);
793
-
794
- els.status.textContent = 'Reasoning…';
795
- const { facts, derived, answerText, reasonText } = derive(data, policies);
796
-
797
- els.answer.textContent = answerText || '(no new derivations)';
798
- els.reason.textContent = reasonText || '(no explanations available)';
799
-
800
- els.status.textContent = 'Running checks…';
801
- const checkResults = runChecks(facts, checksSpec);
802
- els.checks.textContent = renderChecks(checkResults);
803
-
804
- els.summary.textContent = summarizePatients(facts);
805
-
806
- els.status.textContent = 'Done.';
807
- } catch (e) {
808
- console.error(e);
809
- els.status.textContent = 'Error';
810
- els.answer.textContent = '(failed)';
811
- els.reason.textContent = '(failed)';
812
- els.checks.textContent = '(failed)';
813
- els.summary.textContent = '(failed)';
814
- els.diag.textContent = e.message;
815
- }
816
- // re-auto-size textareas after run
817
- autoResize(els.dataTA);
818
- autoResize(els.policyTA);
819
- autoResize(els.checksTA);
820
- }
821
-
822
- function showReasonOnly() {
823
- runARC().then(() => {
824
- window.scrollTo({ top: $('reason').getBoundingClientRect().top + window.scrollY - 12, behavior: 'smooth' });
825
- });
826
- }
827
-
828
- els.runBtn.addEventListener('click', runARC);
829
- els.reasonBtn.addEventListener('click', showReasonOnly);
830
- window.addEventListener('DOMContentLoaded', runARC);
831
- </script>
832
- </body>
833
- </html>