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.
- package/HANDBOOK.md +4 -0
- package/README.md +0 -1
- package/examples/ershov-mixed-computation.n3 +106 -0
- package/examples/output/ershov-mixed-computation.n3 +15 -0
- package/eyeling.js +510 -263
- package/lib/cli.js +22 -12
- package/lib/engine.js +488 -251
- package/package.json +2 -3
- package/arctifacts/README.md +0 -59
- package/arctifacts/ackermann.html +0 -678
- package/arctifacts/auroracare.html +0 -1297
- package/arctifacts/bike-trip.html +0 -752
- package/arctifacts/binomial-theorem.html +0 -631
- package/arctifacts/bmi.html +0 -511
- package/arctifacts/building-performance.html +0 -750
- package/arctifacts/clinical-care.html +0 -726
- package/arctifacts/collatz.html +0 -403
- package/arctifacts/complex.html +0 -321
- package/arctifacts/control-system.html +0 -482
- package/arctifacts/delfour.html +0 -849
- package/arctifacts/earthquake-epicenter.html +0 -982
- package/arctifacts/eco-route.html +0 -662
- package/arctifacts/euclid-infinitude.html +0 -564
- package/arctifacts/euler-identity.html +0 -667
- package/arctifacts/exoplanet-transit.html +0 -1000
- package/arctifacts/faltings-theorem.html +0 -1046
- package/arctifacts/fibonacci.html +0 -299
- package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
- package/arctifacts/godel-numbering.html +0 -743
- package/arctifacts/gps-bike.html +0 -759
- package/arctifacts/gps-clinical-bench.html +0 -792
- package/arctifacts/graph-french.html +0 -449
- package/arctifacts/grass-molecular.html +0 -592
- package/arctifacts/group-theory.html +0 -740
- package/arctifacts/health-info.html +0 -833
- package/arctifacts/kaprekar-constant.html +0 -576
- package/arctifacts/lee.html +0 -805
- package/arctifacts/linked-lists.html +0 -502
- package/arctifacts/lldm.html +0 -612
- package/arctifacts/matrix-multiplication.html +0 -502
- package/arctifacts/matrix.html +0 -651
- package/arctifacts/newton-raphson.html +0 -944
- package/arctifacts/peano-factorial.html +0 -456
- package/arctifacts/pi.html +0 -363
- package/arctifacts/polynomial.html +0 -646
- package/arctifacts/prime.html +0 -366
- package/arctifacts/pythagorean-theorem.html +0 -468
- package/arctifacts/rest-path.html +0 -469
- package/arctifacts/roots-of-unity.html +0 -363
- package/arctifacts/turing.html +0 -409
- package/arctifacts/wind-turbines.html +0 -726
package/arctifacts/delfour.html
DELETED
|
@@ -1,849 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Delfour Insight Economy — Phone ↔ Scanner</title>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
-
|
|
8
|
-
<style>
|
|
9
|
-
:root {
|
|
10
|
-
--bg: #f5f5f7;
|
|
11
|
-
--card-bg: #ffffff;
|
|
12
|
-
--accent: #2563eb;
|
|
13
|
-
--accent-soft: rgba(37, 99, 235, 0.12);
|
|
14
|
-
--danger: #dc2626;
|
|
15
|
-
--ok: #16a34a;
|
|
16
|
-
--text-main: #111827;
|
|
17
|
-
--text-muted: #6b7280;
|
|
18
|
-
--radius-xl: 18px;
|
|
19
|
-
--shadow-sm: 0 4px 12px rgba(15, 23, 42, 0.06);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
* {
|
|
23
|
-
box-sizing: border-box;
|
|
24
|
-
margin: 0;
|
|
25
|
-
padding: 0;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
body {
|
|
29
|
-
font-family:
|
|
30
|
-
system-ui,
|
|
31
|
-
-apple-system,
|
|
32
|
-
BlinkMacSystemFont,
|
|
33
|
-
'SF Pro Text',
|
|
34
|
-
sans-serif;
|
|
35
|
-
background: var(--bg);
|
|
36
|
-
color: var(--text-main);
|
|
37
|
-
line-height: 1.5;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.app {
|
|
41
|
-
max-width: 780px;
|
|
42
|
-
margin: 0 auto;
|
|
43
|
-
padding: 1.2rem 1rem 2.4rem;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
header.top-header {
|
|
47
|
-
margin-bottom: 1.3rem;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
header.top-header h1 {
|
|
51
|
-
font-size: 1.45rem;
|
|
52
|
-
font-weight: 700;
|
|
53
|
-
margin-bottom: 0.3rem;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
header.top-header p {
|
|
57
|
-
font-size: 0.9rem;
|
|
58
|
-
color: var(--text-muted);
|
|
59
|
-
margin-bottom: 0.6rem;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.see-also {
|
|
63
|
-
font-size: 0.82rem;
|
|
64
|
-
color: var(--text-muted);
|
|
65
|
-
margin-top: 0.2rem;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.status-pill {
|
|
69
|
-
display: inline-flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
gap: 0.4rem;
|
|
72
|
-
font-size: 0.78rem;
|
|
73
|
-
text-transform: uppercase;
|
|
74
|
-
letter-spacing: 0.08em;
|
|
75
|
-
padding: 0.2rem 0.7rem;
|
|
76
|
-
border-radius: 999px;
|
|
77
|
-
background: #e5e7eb;
|
|
78
|
-
color: #374151;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.status-dot {
|
|
82
|
-
width: 0.55rem;
|
|
83
|
-
height: 0.55rem;
|
|
84
|
-
border-radius: 999px;
|
|
85
|
-
background: #9ca3af;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.status-pill.ready .status-dot {
|
|
89
|
-
background: var(--accent);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.status-pill.error .status-dot {
|
|
93
|
-
background: var(--danger);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.section {
|
|
97
|
-
margin-bottom: 1.2rem;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.section-title {
|
|
101
|
-
font-size: 1.05rem;
|
|
102
|
-
font-weight: 600;
|
|
103
|
-
margin-bottom: 0.2rem;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.section-subtitle {
|
|
107
|
-
font-size: 0.86rem;
|
|
108
|
-
color: var(--text-muted);
|
|
109
|
-
margin-bottom: 0.6rem;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.card {
|
|
113
|
-
background: var(--card-bg);
|
|
114
|
-
border-radius: var(--radius-xl);
|
|
115
|
-
box-shadow: var(--shadow-sm);
|
|
116
|
-
padding: 0.9rem 0.9rem 0.85rem;
|
|
117
|
-
margin-bottom: 0.8rem;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.card-header {
|
|
121
|
-
display: flex;
|
|
122
|
-
justify-content: space-between;
|
|
123
|
-
align-items: center;
|
|
124
|
-
gap: 0.6rem;
|
|
125
|
-
margin-bottom: 0.35rem;
|
|
126
|
-
flex-wrap: wrap;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.card-title {
|
|
130
|
-
font-weight: 600;
|
|
131
|
-
font-size: 0.98rem;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.chip {
|
|
135
|
-
font-size: 0.72rem;
|
|
136
|
-
font-weight: 600;
|
|
137
|
-
text-transform: uppercase;
|
|
138
|
-
letter-spacing: 0.06em;
|
|
139
|
-
padding: 0.15rem 0.55rem;
|
|
140
|
-
border-radius: 999px;
|
|
141
|
-
background: #eef2ff;
|
|
142
|
-
color: #3730a3;
|
|
143
|
-
white-space: nowrap;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.card-body {
|
|
147
|
-
font-size: 0.9rem;
|
|
148
|
-
color: var(--text-muted);
|
|
149
|
-
margin-bottom: 0.6rem;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
button {
|
|
153
|
-
-webkit-tap-highlight-color: transparent;
|
|
154
|
-
width: 100%;
|
|
155
|
-
border: none;
|
|
156
|
-
outline: none;
|
|
157
|
-
border-radius: 999px;
|
|
158
|
-
padding: 0.6rem 0.9rem;
|
|
159
|
-
font-size: 0.95rem;
|
|
160
|
-
font-weight: 600;
|
|
161
|
-
background: var(--accent);
|
|
162
|
-
color: #fff;
|
|
163
|
-
box-shadow: 0 4px 10px rgba(37, 99, 235, 0.35);
|
|
164
|
-
display: flex;
|
|
165
|
-
align-items: center;
|
|
166
|
-
justify-content: center;
|
|
167
|
-
gap: 0.4rem;
|
|
168
|
-
cursor: pointer;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
button span.btn-icon {
|
|
172
|
-
font-size: 1rem;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
button:active {
|
|
176
|
-
transform: scale(0.98);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
button[disabled] {
|
|
180
|
-
opacity: 0.6;
|
|
181
|
-
box-shadow: none;
|
|
182
|
-
cursor: default;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
.status-line {
|
|
186
|
-
font-size: 0.8rem;
|
|
187
|
-
color: var(--text-muted);
|
|
188
|
-
margin-top: 0.4rem;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
.status-line.ok {
|
|
192
|
-
color: var(--ok);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
.status-line.err {
|
|
196
|
-
color: var(--danger);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.files-list {
|
|
200
|
-
margin-top: 0.5rem;
|
|
201
|
-
border-radius: 14px;
|
|
202
|
-
background: #f9fafb;
|
|
203
|
-
padding: 0.5rem 0.55rem;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.file-item {
|
|
207
|
-
padding: 0.3rem 0.25rem;
|
|
208
|
-
border-bottom: 1px solid #e5e7eb;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.file-item:last-child {
|
|
212
|
-
border-bottom: none;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.file-name {
|
|
216
|
-
font-size: 0.84rem;
|
|
217
|
-
font-weight: 600;
|
|
218
|
-
margin-bottom: 0.15rem;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.file-tag {
|
|
222
|
-
display: inline-block;
|
|
223
|
-
font-size: 0.72rem;
|
|
224
|
-
padding: 0.1rem 0.45rem;
|
|
225
|
-
border-radius: 999px;
|
|
226
|
-
background: #e5e7eb;
|
|
227
|
-
color: #374151;
|
|
228
|
-
margin-left: 0.25rem;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.file-content {
|
|
232
|
-
font-size: 0.78rem;
|
|
233
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
234
|
-
white-space: pre-wrap;
|
|
235
|
-
word-break: break-word;
|
|
236
|
-
color: #4b5563;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.file-content.json {
|
|
240
|
-
background: #f3f4f6;
|
|
241
|
-
border-radius: 0.4rem;
|
|
242
|
-
padding: 0.35rem 0.4rem;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
.hint {
|
|
246
|
-
font-size: 0.8rem;
|
|
247
|
-
color: var(--text-muted);
|
|
248
|
-
margin-top: 0.4rem;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
code {
|
|
252
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
253
|
-
font-size: 0.85em;
|
|
254
|
-
background: #e5e7eb;
|
|
255
|
-
padding: 0.08rem 0.25rem;
|
|
256
|
-
border-radius: 0.25rem;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
@media (min-width: 640px) {
|
|
260
|
-
header.top-header h1 {
|
|
261
|
-
font-size: 1.6rem;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
</style>
|
|
265
|
-
</head>
|
|
266
|
-
<body>
|
|
267
|
-
<main class="app">
|
|
268
|
-
<header class="top-header">
|
|
269
|
-
<h1>Delfour Insight Economy — Phone ↔ Scanner</h1>
|
|
270
|
-
<p>
|
|
271
|
-
Fully client-side demo: direct <strong>embedded JavaScript</strong> port.
|
|
272
|
-
<br />
|
|
273
|
-
the phone logic becomes an in-page phone step that writes a neutral Insight + Policy, and the scanner logic
|
|
274
|
-
becomes an in-page scanner step that consumes it.
|
|
275
|
-
</p>
|
|
276
|
-
<p class="see-also">
|
|
277
|
-
See also:
|
|
278
|
-
<a
|
|
279
|
-
href="https://ruben.verborgh.org/blog/2025/08/12/inside-the-insight-economy/"
|
|
280
|
-
target="_blank"
|
|
281
|
-
rel="noopener noreferrer">
|
|
282
|
-
Inside the Insight Economy
|
|
283
|
-
</a>
|
|
284
|
-
</p>
|
|
285
|
-
<div id="runtime-status" class="status-pill ready">
|
|
286
|
-
<span class="status-dot"></span>
|
|
287
|
-
<span class="status-text">Embedded JavaScript ready</span>
|
|
288
|
-
</div>
|
|
289
|
-
</header>
|
|
290
|
-
|
|
291
|
-
<section class="section">
|
|
292
|
-
<h2 class="section-title">Phone → Insight + Policy</h2>
|
|
293
|
-
<p class="section-subtitle">
|
|
294
|
-
Shopper’s phone turns a sensitive profile into a neutral, scoped, expiring Insight.
|
|
295
|
-
</p>
|
|
296
|
-
|
|
297
|
-
<article class="card">
|
|
298
|
-
<div class="card-header">
|
|
299
|
-
<div class="card-title">Phone (shopper)</div>
|
|
300
|
-
<div class="chip">Embedded phone logic</div>
|
|
301
|
-
</div>
|
|
302
|
-
<p class="card-body">
|
|
303
|
-
Runs the browser-side phone logic and writes an <code>envelope.json</code>, signature, and a private
|
|
304
|
-
<code>reason.txt</code> into the demo bus session.
|
|
305
|
-
</p>
|
|
306
|
-
<button id="run-phone-btn" type="button">
|
|
307
|
-
<span class="btn-icon">📱</span>
|
|
308
|
-
<span class="btn-label">Run phone step</span>
|
|
309
|
-
</button>
|
|
310
|
-
<div id="phone-status" class="status-line"></div>
|
|
311
|
-
</article>
|
|
312
|
-
</section>
|
|
313
|
-
|
|
314
|
-
<section class="section">
|
|
315
|
-
<h2 class="section-title">Scanner → Apply Insight</h2>
|
|
316
|
-
<p class="section-subtitle">
|
|
317
|
-
Retailer self-scanner reads the same bus session and derives local artifacts (banner, audit, checks… matching
|
|
318
|
-
the original scanner flow).
|
|
319
|
-
</p>
|
|
320
|
-
|
|
321
|
-
<article class="card">
|
|
322
|
-
<div class="card-header">
|
|
323
|
-
<div class="card-title">Scanner (retailer)</div>
|
|
324
|
-
<div class="chip">Embedded scanner logic</div>
|
|
325
|
-
</div>
|
|
326
|
-
<p class="card-body">
|
|
327
|
-
Runs the browser-side scanner logic, reading from the same bus session and writing its own files into the
|
|
328
|
-
demo bus.
|
|
329
|
-
</p>
|
|
330
|
-
<button id="run-scanner-btn" type="button" disabled>
|
|
331
|
-
<span class="btn-icon">🛒</span>
|
|
332
|
-
<span class="btn-label">Run scanner step</span>
|
|
333
|
-
</button>
|
|
334
|
-
<div id="scanner-status" class="status-line"></div>
|
|
335
|
-
</article>
|
|
336
|
-
</section>
|
|
337
|
-
|
|
338
|
-
<section class="section">
|
|
339
|
-
<h2 class="section-title">Bus session: <code>/cases/bus/delfour</code></h2>
|
|
340
|
-
<p class="section-subtitle">Files currently on the bus, after the last phone/scanner run.</p>
|
|
341
|
-
|
|
342
|
-
<article class="card">
|
|
343
|
-
<div class="card-header">
|
|
344
|
-
<div class="card-title">Bus contents</div>
|
|
345
|
-
<div class="chip">persistent browser storage</div>
|
|
346
|
-
</div>
|
|
347
|
-
<div id="bus-files" class="files-list">
|
|
348
|
-
<div class="file-item">
|
|
349
|
-
<div class="file-name">No files yet</div>
|
|
350
|
-
<div class="file-content">Run the phone step to generate an Insight + Policy, then the scanner.</div>
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
<p class="hint">
|
|
354
|
-
Hint: this view is generic on purpose — whatever the phone and scanner steps write into the bus session will
|
|
355
|
-
show up here.
|
|
356
|
-
</p>
|
|
357
|
-
</article>
|
|
358
|
-
</section>
|
|
359
|
-
</main>
|
|
360
|
-
|
|
361
|
-
<script>
|
|
362
|
-
const runtimeStatusEl = document.getElementById('runtime-status');
|
|
363
|
-
const runtimeStatusTextEl = runtimeStatusEl.querySelector('.status-text');
|
|
364
|
-
const runPhoneBtn = document.getElementById('run-phone-btn');
|
|
365
|
-
const runScannerBtn = document.getElementById('run-scanner-btn');
|
|
366
|
-
const phoneStatusEl = document.getElementById('phone-status');
|
|
367
|
-
const scannerStatusEl = document.getElementById('scanner-status');
|
|
368
|
-
const busFilesEl = document.getElementById('bus-files');
|
|
369
|
-
|
|
370
|
-
const BUS_STORAGE_KEY = 'follows-from:delfour:bus';
|
|
371
|
-
const DEMO_SHARED_SECRET = 'neutral-insight-demo-shared-secret';
|
|
372
|
-
const PHONE_CREATED_AT = '2025-10-05T20:33:48.907163+00:00';
|
|
373
|
-
const PHONE_EXPIRES_AT = '2025-10-05T22:33:48.907185+00:00';
|
|
374
|
-
const SCANNER_AUTH_AT = '2025-10-05T20:35:48.907163+00:00';
|
|
375
|
-
const SCANNER_DUTY_AT = '2025-10-05T20:37:48.907163+00:00';
|
|
376
|
-
|
|
377
|
-
const CATALOG = [
|
|
378
|
-
{ id: 'prod:BIS_001', name: 'Classic Tea Biscuits', sku: 'BIS-001', sugar: 12.0, price: 2.1 },
|
|
379
|
-
{ id: 'prod:BIS_101', name: 'Low-Sugar Tea Biscuits', sku: 'BIS-101', sugar: 3.0, price: 2.6 },
|
|
380
|
-
{ id: 'prod:CHOC_050', name: 'Milk Chocolate Bar', sku: 'CHOC-050', sugar: 15.0, price: 1.8 },
|
|
381
|
-
{ id: 'prod:CHOC_150', name: '85% Dark Chocolate', sku: 'CHOC-150', sugar: 6.0, price: 2.2 },
|
|
382
|
-
];
|
|
383
|
-
|
|
384
|
-
const REQUEST = {
|
|
385
|
-
type: 'req:Request',
|
|
386
|
-
action: 'odrl:use',
|
|
387
|
-
constraint: {
|
|
388
|
-
leftOperand: 'odrl:purpose',
|
|
389
|
-
rightOperand: 'shopping_assist',
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
let isBusy = false;
|
|
394
|
-
|
|
395
|
-
function setRuntimeStatus(kind, text) {
|
|
396
|
-
runtimeStatusEl.classList.remove('ready', 'error');
|
|
397
|
-
if (kind === 'ready') runtimeStatusEl.classList.add('ready');
|
|
398
|
-
if (kind === 'error') runtimeStatusEl.classList.add('error');
|
|
399
|
-
runtimeStatusTextEl.textContent = text;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function setInlineStatus(el, text, type) {
|
|
403
|
-
el.textContent = text;
|
|
404
|
-
el.classList.remove('ok', 'err');
|
|
405
|
-
if (type === 'ok') el.classList.add('ok');
|
|
406
|
-
if (type === 'err') el.classList.add('err');
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function loadBusState() {
|
|
410
|
-
try {
|
|
411
|
-
return JSON.parse(localStorage.getItem(BUS_STORAGE_KEY) || '{}');
|
|
412
|
-
} catch {
|
|
413
|
-
return {};
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function saveBusState(state) {
|
|
418
|
-
localStorage.setItem(BUS_STORAGE_KEY, JSON.stringify(state));
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function writeBusFile(name, content) {
|
|
422
|
-
const state = loadBusState();
|
|
423
|
-
state[name] = content;
|
|
424
|
-
saveBusState(state);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function readBusFile(name) {
|
|
428
|
-
return loadBusState()[name] || null;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function hasPhoneArtifacts() {
|
|
432
|
-
const state = loadBusState();
|
|
433
|
-
return Boolean(state['envelope.json'] && state['envelope.sig.json']);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function updateButtonState() {
|
|
437
|
-
if (isBusy) return;
|
|
438
|
-
runPhoneBtn.disabled = false;
|
|
439
|
-
runScannerBtn.disabled = !hasPhoneArtifacts();
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function formatNumber(value) {
|
|
443
|
-
return Number.isInteger(value) ? value.toFixed(1) : String(value);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function canonicalJson(value) {
|
|
447
|
-
if (value === null) return 'null';
|
|
448
|
-
if (Array.isArray(value)) {
|
|
449
|
-
return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
|
|
450
|
-
}
|
|
451
|
-
if (typeof value === 'object') {
|
|
452
|
-
const keys = Object.keys(value).sort();
|
|
453
|
-
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
454
|
-
}
|
|
455
|
-
if (typeof value === 'string') return JSON.stringify(value);
|
|
456
|
-
if (typeof value === 'number') return formatNumber(value);
|
|
457
|
-
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
458
|
-
return 'null';
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
function prettyJson(value, indent = 2, level = 0) {
|
|
462
|
-
const pad = ' '.repeat(indent * level);
|
|
463
|
-
const innerPad = ' '.repeat(indent * (level + 1));
|
|
464
|
-
|
|
465
|
-
if (value === null) return 'null';
|
|
466
|
-
if (Array.isArray(value)) {
|
|
467
|
-
if (!value.length) return '[]';
|
|
468
|
-
return `[
|
|
469
|
-
${value.map((item) => `${innerPad}${prettyJson(item, indent, level + 1)}`).join(',\n')}
|
|
470
|
-
${pad}]`;
|
|
471
|
-
}
|
|
472
|
-
if (typeof value === 'object') {
|
|
473
|
-
const keys = Object.keys(value);
|
|
474
|
-
if (!keys.length) return '{}';
|
|
475
|
-
return `{
|
|
476
|
-
${keys.map((key) => `${innerPad}${JSON.stringify(key)}: ${prettyJson(value[key], indent, level + 1)}`).join(',\n')}
|
|
477
|
-
${pad}}`;
|
|
478
|
-
}
|
|
479
|
-
if (typeof value === 'string') return JSON.stringify(value);
|
|
480
|
-
if (typeof value === 'number') return formatNumber(value);
|
|
481
|
-
if (typeof value === 'boolean') return value ? 'true' : 'false';
|
|
482
|
-
return 'null';
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function hexFromBuffer(buffer) {
|
|
486
|
-
return Array.from(new Uint8Array(buffer))
|
|
487
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
488
|
-
.join('');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function sha256Hex(text) {
|
|
492
|
-
const bytes = new TextEncoder().encode(text);
|
|
493
|
-
const hash = await crypto.subtle.digest('SHA-256', bytes);
|
|
494
|
-
return hexFromBuffer(hash);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
async function hmacSha256Hex(secret, text) {
|
|
498
|
-
const encoder = new TextEncoder();
|
|
499
|
-
const key = await crypto.subtle.importKey(
|
|
500
|
-
'raw',
|
|
501
|
-
encoder.encode(secret),
|
|
502
|
-
{ name: 'HMAC', hash: 'SHA-256' },
|
|
503
|
-
false,
|
|
504
|
-
['sign'],
|
|
505
|
-
);
|
|
506
|
-
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(text));
|
|
507
|
-
return hexFromBuffer(signature);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function desensitize(profile) {
|
|
511
|
-
const conditions = new Set((profile.householdConditions || []).map(String));
|
|
512
|
-
const needsLowSugar = conditions.has('Diabetes');
|
|
513
|
-
return { 'need:needsLowSugar': Boolean(needsLowSugar) };
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
function deriveInsight(need, retailer, device, event, ttlHours, sessionId) {
|
|
517
|
-
const threshold = need['need:needsLowSugar'] ? 10.0 : 10.0;
|
|
518
|
-
return {
|
|
519
|
-
id: `https://example.org/insight/${sessionId}`,
|
|
520
|
-
type: 'ins:Insight',
|
|
521
|
-
metric: 'sugar_g_per_serving',
|
|
522
|
-
threshold: threshold,
|
|
523
|
-
suggestionPolicy: 'lower_metric_first_higher_price_ok',
|
|
524
|
-
scopeDevice: device,
|
|
525
|
-
scopeEvent: event,
|
|
526
|
-
retailer: retailer,
|
|
527
|
-
createdAt: PHONE_CREATED_AT,
|
|
528
|
-
expiresAt: PHONE_EXPIRES_AT,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function policyFromInsight(insight) {
|
|
533
|
-
const insId = insight.id;
|
|
534
|
-
return {
|
|
535
|
-
type: 'odrl:Policy',
|
|
536
|
-
profile: 'Delfour-Insight-Policy',
|
|
537
|
-
permission: {
|
|
538
|
-
action: 'odrl:use',
|
|
539
|
-
target: insId,
|
|
540
|
-
constraint: {
|
|
541
|
-
leftOperand: 'odrl:purpose',
|
|
542
|
-
operator: 'odrl:eq',
|
|
543
|
-
rightOperand: 'shopping_assist',
|
|
544
|
-
},
|
|
545
|
-
},
|
|
546
|
-
prohibition: {
|
|
547
|
-
action: 'odrl:distribute',
|
|
548
|
-
target: insId,
|
|
549
|
-
constraint: {
|
|
550
|
-
leftOperand: 'odrl:purpose',
|
|
551
|
-
operator: 'odrl:eq',
|
|
552
|
-
rightOperand: 'marketing',
|
|
553
|
-
},
|
|
554
|
-
},
|
|
555
|
-
duty: {
|
|
556
|
-
action: 'odrl:delete',
|
|
557
|
-
constraint: {
|
|
558
|
-
leftOperand: 'odrl:dateTime',
|
|
559
|
-
operator: 'odrl:eq',
|
|
560
|
-
rightOperand: insight.expiresAt,
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function signEnvelope(envelope) {
|
|
567
|
-
const canonical = canonicalJson(envelope);
|
|
568
|
-
const digest = await sha256Hex(canonical);
|
|
569
|
-
const mac = await hmacSha256Hex(DEMO_SHARED_SECRET, canonical);
|
|
570
|
-
return {
|
|
571
|
-
alg: 'HMAC-SHA256',
|
|
572
|
-
keyid: 'demo-shared-secret',
|
|
573
|
-
created: PHONE_CREATED_AT,
|
|
574
|
-
payloadHashSHA256: digest,
|
|
575
|
-
signatureHMAC: mac,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
async function verifySignature(envelope, signature) {
|
|
580
|
-
const canonical = canonicalJson(envelope);
|
|
581
|
-
const digest = await sha256Hex(canonical);
|
|
582
|
-
if (String(signature.payloadHashSHA256 || '').toLowerCase() !== digest.toLowerCase()) {
|
|
583
|
-
return false;
|
|
584
|
-
}
|
|
585
|
-
const expected = await hmacSha256Hex(DEMO_SHARED_SECRET, canonical);
|
|
586
|
-
return expected === signature.signatureHMAC;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function authorize(envelope) {
|
|
590
|
-
const insight = envelope.insight;
|
|
591
|
-
const policy = envelope.policy;
|
|
592
|
-
const now = SCANNER_AUTH_AT;
|
|
593
|
-
const expired = now > insight.expiresAt;
|
|
594
|
-
const permission = policy.permission || {};
|
|
595
|
-
const matches =
|
|
596
|
-
REQUEST.action === permission.action &&
|
|
597
|
-
permission.target === insight.id &&
|
|
598
|
-
(permission.constraint || {}).leftOperand === 'odrl:purpose' &&
|
|
599
|
-
(permission.constraint || {}).rightOperand === REQUEST.constraint.rightOperand;
|
|
600
|
-
|
|
601
|
-
if (expired || !matches) {
|
|
602
|
-
return [
|
|
603
|
-
false,
|
|
604
|
-
{
|
|
605
|
-
type: 'act:Decision',
|
|
606
|
-
at: now,
|
|
607
|
-
request: REQUEST,
|
|
608
|
-
outcome: 'Blocked',
|
|
609
|
-
reason: expired ? 'expired' : 'policy_mismatch',
|
|
610
|
-
},
|
|
611
|
-
];
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
return [
|
|
615
|
-
true,
|
|
616
|
-
{
|
|
617
|
-
type: 'act:Decision',
|
|
618
|
-
at: now,
|
|
619
|
-
request: REQUEST,
|
|
620
|
-
outcome: 'Allowed',
|
|
621
|
-
target: insight.id,
|
|
622
|
-
},
|
|
623
|
-
];
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function simulateScanAndSuggest(insight) {
|
|
627
|
-
const scanned = CATALOG.find((product) => product.id === 'prod:BIS_001');
|
|
628
|
-
const threshold = Number(insight.threshold);
|
|
629
|
-
let note = null;
|
|
630
|
-
let alternativeName = null;
|
|
631
|
-
|
|
632
|
-
if (scanned.sugar >= threshold) {
|
|
633
|
-
note = 'High sugar';
|
|
634
|
-
const candidates = CATALOG.filter((product) => product.sugar < scanned.sugar).sort(
|
|
635
|
-
(a, b) => a.sugar - b.sugar,
|
|
636
|
-
);
|
|
637
|
-
if (candidates.length) {
|
|
638
|
-
alternativeName = candidates[0].name;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return {
|
|
643
|
-
headline: note ? 'Track sugar per serving while you scan' : 'Scan complete',
|
|
644
|
-
product_name: scanned.name,
|
|
645
|
-
note: note,
|
|
646
|
-
suggested_alternative: alternativeName,
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function computeChecks(envelope, banner, audit) {
|
|
651
|
-
const insight = envelope.insight;
|
|
652
|
-
const insightText = canonicalJson(insight).toLowerCase();
|
|
653
|
-
const hasDecision = audit.some((entry) => entry.type === 'act:Decision');
|
|
654
|
-
return [
|
|
655
|
-
['insight_nonempty', Boolean(insight)],
|
|
656
|
-
['minimization_no_sensitive_terms', !insightText.includes('diabetes') && !insightText.includes('medical')],
|
|
657
|
-
['scope_has_device_event_expiry', ['scopeDevice', 'scopeEvent', 'expiresAt'].every((key) => key in insight)],
|
|
658
|
-
['runtime_present', Boolean(banner && banner.headline)],
|
|
659
|
-
['behavior_suggests_on_high_sugar', banner.note === 'High sugar'],
|
|
660
|
-
['audit_has_decision', hasDecision],
|
|
661
|
-
];
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function detectFileTag(name) {
|
|
665
|
-
const lower = name.toLowerCase();
|
|
666
|
-
if (lower.includes('envelope') && lower.endsWith('.json')) return 'envelope';
|
|
667
|
-
if (lower.includes('sig') && lower.endsWith('.json')) return 'signature';
|
|
668
|
-
if (lower.includes('banner') && lower.endsWith('.json')) return 'banner';
|
|
669
|
-
if (lower.includes('audit')) return 'audit';
|
|
670
|
-
if (lower.includes('check')) return 'checks';
|
|
671
|
-
if (lower.includes('reason')) return 'reason';
|
|
672
|
-
if (lower.endsWith('.json')) return 'json';
|
|
673
|
-
if (lower.endsWith('.ttl')) return 'ttl';
|
|
674
|
-
return '';
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function prettySnippet(name, content) {
|
|
678
|
-
const maxChars = 800;
|
|
679
|
-
let snippet = content || '';
|
|
680
|
-
if (snippet.length > maxChars) {
|
|
681
|
-
snippet = `${snippet.slice(0, maxChars)}\n…`;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const looksJson =
|
|
685
|
-
name.toLowerCase().endsWith('.json') || snippet.trim().startsWith('{') || snippet.trim().startsWith('[');
|
|
686
|
-
|
|
687
|
-
return { text: snippet, isJson: looksJson };
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function refreshBusView() {
|
|
691
|
-
const files = loadBusState();
|
|
692
|
-
const entries = Object.entries(files).sort(([a], [b]) => a.localeCompare(b));
|
|
693
|
-
|
|
694
|
-
if (!entries.length) {
|
|
695
|
-
busFilesEl.innerHTML = `
|
|
696
|
-
<div class="file-item">
|
|
697
|
-
<div class="file-name">No files yet</div>
|
|
698
|
-
<div class="file-content">
|
|
699
|
-
Run the phone step to generate an Insight + Policy, then the scanner.
|
|
700
|
-
</div>
|
|
701
|
-
</div>`;
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
busFilesEl.innerHTML = '';
|
|
706
|
-
for (const [name, content] of entries) {
|
|
707
|
-
const item = document.createElement('div');
|
|
708
|
-
item.className = 'file-item';
|
|
709
|
-
|
|
710
|
-
const nameEl = document.createElement('div');
|
|
711
|
-
nameEl.className = 'file-name';
|
|
712
|
-
nameEl.textContent = name;
|
|
713
|
-
|
|
714
|
-
const tag = detectFileTag(name);
|
|
715
|
-
if (tag) {
|
|
716
|
-
const tagEl = document.createElement('span');
|
|
717
|
-
tagEl.className = 'file-tag';
|
|
718
|
-
tagEl.textContent = tag;
|
|
719
|
-
nameEl.appendChild(tagEl);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const body = document.createElement('div');
|
|
723
|
-
const { text, isJson } = prettySnippet(name, content);
|
|
724
|
-
body.className = `file-content${isJson ? ' json' : ''}`;
|
|
725
|
-
body.textContent = text;
|
|
726
|
-
|
|
727
|
-
item.appendChild(nameEl);
|
|
728
|
-
item.appendChild(body);
|
|
729
|
-
busFilesEl.appendChild(item);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
async function runPhone() {
|
|
734
|
-
if (isBusy) return;
|
|
735
|
-
isBusy = true;
|
|
736
|
-
runPhoneBtn.disabled = true;
|
|
737
|
-
runScannerBtn.disabled = true;
|
|
738
|
-
setInlineStatus(phoneStatusEl, '', null);
|
|
739
|
-
setRuntimeStatus('ready', 'Running phone step…');
|
|
740
|
-
|
|
741
|
-
const originalLabel = runPhoneBtn.innerHTML;
|
|
742
|
-
runPhoneBtn.innerHTML = '<span class="btn-icon">⏳</span><span class="btn-label">Running…</span>';
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
const profile = { householdConditions: ['Diabetes'] };
|
|
746
|
-
const need = desensitize(profile);
|
|
747
|
-
const insight = deriveInsight(need, 'Delfour', 'self-scanner', 'pick_up_scanner', 2.0, 'delfour');
|
|
748
|
-
const policy = policyFromInsight(insight);
|
|
749
|
-
const envelope = { insight, policy };
|
|
750
|
-
const signature = await signEnvelope(envelope);
|
|
751
|
-
const reason =
|
|
752
|
-
[
|
|
753
|
-
'Household requires low-sugar guidance (diabetes in POD).',
|
|
754
|
-
"A neutral Insight is scoped to device 'self-scanner', event 'pick_up_scanner', retailer 'Delfour', and expires soon; the policy confines use to shopping assistance.",
|
|
755
|
-
].join(' ') + '\n';
|
|
756
|
-
|
|
757
|
-
writeBusFile('envelope.json', `${prettyJson(envelope)}\n`);
|
|
758
|
-
writeBusFile('envelope.sig.json', `${prettyJson(signature)}\n`);
|
|
759
|
-
writeBusFile('reason.txt', reason);
|
|
760
|
-
|
|
761
|
-
setInlineStatus(phoneStatusEl, 'Phone step completed — envelope + signature + reason written.', 'ok');
|
|
762
|
-
setRuntimeStatus('ready', 'Embedded JavaScript ready');
|
|
763
|
-
refreshBusView();
|
|
764
|
-
} catch (err) {
|
|
765
|
-
console.error('Phone step error', err);
|
|
766
|
-
setInlineStatus(phoneStatusEl, 'Error running phone step (check console).', 'err');
|
|
767
|
-
setRuntimeStatus('error', 'Phone step failed');
|
|
768
|
-
} finally {
|
|
769
|
-
runPhoneBtn.innerHTML = originalLabel;
|
|
770
|
-
isBusy = false;
|
|
771
|
-
updateButtonState();
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
async function runScanner() {
|
|
776
|
-
if (isBusy) return;
|
|
777
|
-
isBusy = true;
|
|
778
|
-
runPhoneBtn.disabled = true;
|
|
779
|
-
runScannerBtn.disabled = true;
|
|
780
|
-
setInlineStatus(scannerStatusEl, '', null);
|
|
781
|
-
setRuntimeStatus('ready', 'Running scanner step…');
|
|
782
|
-
|
|
783
|
-
const originalLabel = runScannerBtn.innerHTML;
|
|
784
|
-
runScannerBtn.innerHTML = '<span class="btn-icon">⏳</span><span class="btn-label">Running…</span>';
|
|
785
|
-
|
|
786
|
-
try {
|
|
787
|
-
const envelopeText = readBusFile('envelope.json');
|
|
788
|
-
const signatureText = readBusFile('envelope.sig.json');
|
|
789
|
-
if (!envelopeText || !signatureText) {
|
|
790
|
-
throw new Error('Missing envelope/signature in bus session');
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const envelope = JSON.parse(envelopeText);
|
|
794
|
-
const signature = JSON.parse(signatureText);
|
|
795
|
-
|
|
796
|
-
const verified = await verifySignature(envelope, signature);
|
|
797
|
-
if (!verified) {
|
|
798
|
-
throw new Error('Signature verification failed');
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const [allowed, decision] = authorize(envelope);
|
|
802
|
-
const audit = [decision];
|
|
803
|
-
const banner = allowed
|
|
804
|
-
? simulateScanAndSuggest(envelope.insight)
|
|
805
|
-
: {
|
|
806
|
-
headline: 'Policy blocked action',
|
|
807
|
-
product_name: null,
|
|
808
|
-
note: 'Expired or prohibited',
|
|
809
|
-
suggested_alternative: null,
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
if (SCANNER_DUTY_AT > envelope.insight.expiresAt) {
|
|
813
|
-
audit.push({
|
|
814
|
-
type: 'act:Duty',
|
|
815
|
-
at: SCANNER_DUTY_AT,
|
|
816
|
-
duty: 'delete_due',
|
|
817
|
-
target: envelope.insight.id,
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const checks = computeChecks(envelope, banner, audit);
|
|
822
|
-
writeBusFile('audit.json', `${prettyJson(audit)}\n`);
|
|
823
|
-
writeBusFile('banner.json', `${prettyJson(banner)}\n`);
|
|
824
|
-
writeBusFile('checks.json', `${prettyJson(checks)}\n`);
|
|
825
|
-
|
|
826
|
-
setInlineStatus(scannerStatusEl, 'Scanner step completed — bus folder updated.', 'ok');
|
|
827
|
-
setRuntimeStatus('ready', 'Embedded JavaScript ready');
|
|
828
|
-
refreshBusView();
|
|
829
|
-
} catch (err) {
|
|
830
|
-
console.error('Scanner step error', err);
|
|
831
|
-
setInlineStatus(scannerStatusEl, 'Error running scanner step (check console).', 'err');
|
|
832
|
-
setRuntimeStatus('error', 'Scanner step failed');
|
|
833
|
-
} finally {
|
|
834
|
-
runScannerBtn.innerHTML = originalLabel;
|
|
835
|
-
isBusy = false;
|
|
836
|
-
updateButtonState();
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
841
|
-
setRuntimeStatus('ready', 'Embedded JavaScript ready');
|
|
842
|
-
refreshBusView();
|
|
843
|
-
updateButtonState();
|
|
844
|
-
runPhoneBtn.addEventListener('click', runPhone);
|
|
845
|
-
runScannerBtn.addEventListener('click', runScanner);
|
|
846
|
-
});
|
|
847
|
-
</script>
|
|
848
|
-
</body>
|
|
849
|
-
</html>
|