eyeling 1.16.2 → 1.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/HANDBOOK.md +4 -0
  2. package/README.md +0 -1
  3. package/examples/ershov-mixed-computation.n3 +106 -0
  4. package/examples/output/ershov-mixed-computation.n3 +15 -0
  5. package/eyeling.js +510 -263
  6. package/lib/cli.js +22 -12
  7. package/lib/engine.js +488 -251
  8. package/package.json +2 -3
  9. package/arctifacts/README.md +0 -59
  10. package/arctifacts/ackermann.html +0 -678
  11. package/arctifacts/auroracare.html +0 -1297
  12. package/arctifacts/bike-trip.html +0 -752
  13. package/arctifacts/binomial-theorem.html +0 -631
  14. package/arctifacts/bmi.html +0 -511
  15. package/arctifacts/building-performance.html +0 -750
  16. package/arctifacts/clinical-care.html +0 -726
  17. package/arctifacts/collatz.html +0 -403
  18. package/arctifacts/complex.html +0 -321
  19. package/arctifacts/control-system.html +0 -482
  20. package/arctifacts/delfour.html +0 -849
  21. package/arctifacts/earthquake-epicenter.html +0 -982
  22. package/arctifacts/eco-route.html +0 -662
  23. package/arctifacts/euclid-infinitude.html +0 -564
  24. package/arctifacts/euler-identity.html +0 -667
  25. package/arctifacts/exoplanet-transit.html +0 -1000
  26. package/arctifacts/faltings-theorem.html +0 -1046
  27. package/arctifacts/fibonacci.html +0 -299
  28. package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
  29. package/arctifacts/godel-numbering.html +0 -743
  30. package/arctifacts/gps-bike.html +0 -759
  31. package/arctifacts/gps-clinical-bench.html +0 -792
  32. package/arctifacts/graph-french.html +0 -449
  33. package/arctifacts/grass-molecular.html +0 -592
  34. package/arctifacts/group-theory.html +0 -740
  35. package/arctifacts/health-info.html +0 -833
  36. package/arctifacts/kaprekar-constant.html +0 -576
  37. package/arctifacts/lee.html +0 -805
  38. package/arctifacts/linked-lists.html +0 -502
  39. package/arctifacts/lldm.html +0 -612
  40. package/arctifacts/matrix-multiplication.html +0 -502
  41. package/arctifacts/matrix.html +0 -651
  42. package/arctifacts/newton-raphson.html +0 -944
  43. package/arctifacts/peano-factorial.html +0 -456
  44. package/arctifacts/pi.html +0 -363
  45. package/arctifacts/polynomial.html +0 -646
  46. package/arctifacts/prime.html +0 -366
  47. package/arctifacts/pythagorean-theorem.html +0 -468
  48. package/arctifacts/rest-path.html +0 -469
  49. package/arctifacts/roots-of-unity.html +0 -363
  50. package/arctifacts/turing.html +0 -409
  51. 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>