eyeling 1.16.3 → 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 (45) hide show
  1. package/README.md +0 -1
  2. package/package.json +2 -3
  3. package/arctifacts/README.md +0 -59
  4. package/arctifacts/ackermann.html +0 -678
  5. package/arctifacts/auroracare.html +0 -1297
  6. package/arctifacts/bike-trip.html +0 -752
  7. package/arctifacts/binomial-theorem.html +0 -631
  8. package/arctifacts/bmi.html +0 -511
  9. package/arctifacts/building-performance.html +0 -750
  10. package/arctifacts/clinical-care.html +0 -726
  11. package/arctifacts/collatz.html +0 -403
  12. package/arctifacts/complex.html +0 -321
  13. package/arctifacts/control-system.html +0 -482
  14. package/arctifacts/delfour.html +0 -849
  15. package/arctifacts/earthquake-epicenter.html +0 -982
  16. package/arctifacts/eco-route.html +0 -662
  17. package/arctifacts/euclid-infinitude.html +0 -564
  18. package/arctifacts/euler-identity.html +0 -667
  19. package/arctifacts/exoplanet-transit.html +0 -1000
  20. package/arctifacts/faltings-theorem.html +0 -1046
  21. package/arctifacts/fibonacci.html +0 -299
  22. package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
  23. package/arctifacts/godel-numbering.html +0 -743
  24. package/arctifacts/gps-bike.html +0 -759
  25. package/arctifacts/gps-clinical-bench.html +0 -792
  26. package/arctifacts/graph-french.html +0 -449
  27. package/arctifacts/grass-molecular.html +0 -592
  28. package/arctifacts/group-theory.html +0 -740
  29. package/arctifacts/health-info.html +0 -833
  30. package/arctifacts/kaprekar-constant.html +0 -576
  31. package/arctifacts/lee.html +0 -805
  32. package/arctifacts/linked-lists.html +0 -502
  33. package/arctifacts/lldm.html +0 -612
  34. package/arctifacts/matrix-multiplication.html +0 -502
  35. package/arctifacts/matrix.html +0 -651
  36. package/arctifacts/newton-raphson.html +0 -944
  37. package/arctifacts/peano-factorial.html +0 -456
  38. package/arctifacts/pi.html +0 -363
  39. package/arctifacts/polynomial.html +0 -646
  40. package/arctifacts/prime.html +0 -366
  41. package/arctifacts/pythagorean-theorem.html +0 -468
  42. package/arctifacts/rest-path.html +0 -469
  43. package/arctifacts/roots-of-unity.html +0 -363
  44. package/arctifacts/turing.html +0 -409
  45. package/arctifacts/wind-turbines.html +0 -726
@@ -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>