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,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>