prae-cli 0.1.1 → 0.2.0

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.
@@ -0,0 +1,929 @@
1
+ //#region src/console/logo.ts
2
+ /**
3
+ * The mark, inlined.
4
+ *
5
+ * A P drawn as a door: the product is the moment permission is granted,
6
+ * and the glyph is the gate it comes through. Inlined as a data URI rather
7
+ * than linked because these pages are read offline and mailed around as
8
+ * evidence — a page that fetches an image renders wrong on the machine that
9
+ * matters.
10
+ */
11
+ const LOGO = "data:image/png;base64,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";
12
+
13
+ //#endregion
14
+ //#region ../tokens/src/palette.ts
15
+ /**
16
+ * The two grounds, as declaration blocks.
17
+ *
18
+ * The console renders twice from one design: `prae console --out` inlines its
19
+ * stylesheet into standalone documents, and the web app links a real one. Both
20
+ * need these values, so they are defined here once and the test in this
21
+ * package asserts the stylesheet agrees — the same guard the rest of the
22
+ * tokens already have, for the same reason.
23
+ *
24
+ * Names are unprefixed because these are consumed as a surface's own palette
25
+ * rather than alongside another product's. `tokens.css` carries the prefixed
26
+ * `--prae-*` set for anyone embedding PRAE inside their own page.
27
+ */
28
+ /** §06 — Surface B, the ledger, on paper. Printable and high-contrast. */
29
+ const PAPER = ` --signal: oklch(0.45 0.16 140);
30
+ --signal-bright: oklch(0.5 0.16 140);
31
+ --signal-edge: oklch(0.5 0.13 140);
32
+ --signal-text: oklch(0.42 0.14 140);
33
+ --signal-on: oklch(0.985 0.003 100);
34
+ --hold: oklch(0.55 0.15 55);
35
+ --veto: oklch(0.52 0.19 25);
36
+ --veto-text: oklch(0.5 0.19 25);
37
+ --veto-edge: oklch(0.62 0.17 25);
38
+
39
+ --ground: oklch(0.975 0.003 100);
40
+ --raised: oklch(0.99 0.002 100);
41
+ --sunk: oklch(0.955 0.004 100);
42
+ --active: oklch(0.955 0.006 130);
43
+ --line: oklch(0.88 0.004 100);
44
+ --line-soft: oklch(0.92 0.004 100);
45
+ --line-faint: oklch(0.93 0.004 100);
46
+ --line-card: oklch(0.86 0.005 100);
47
+ --line-strong: oklch(0.8 0.006 100);
48
+ --edge: oklch(0.32 0.01 255);
49
+ --ink: oklch(0.22 0.01 255);
50
+ --ink-row: oklch(0.24 0.01 255);
51
+ --secondary: oklch(0.4 0.01 255);
52
+ --muted: oklch(0.5 0.01 255);
53
+ --muted-soft: oklch(0.55 0.01 255);
54
+ --dim: oklch(0.45 0.01 255);
55
+ --seal: oklch(0.6 0.01 255);
56
+
57
+ --mark: none;`;
58
+ /** §02 — the graphite ramp and the neon signal it is drawn for. */
59
+ const FENCE = ` --signal: oklch(0.86 0.21 133);
60
+ --signal-bright: oklch(0.95 0.15 133);
61
+ --signal-edge: oklch(0.4 0.06 133);
62
+ --signal-text: oklch(0.86 0.21 133);
63
+ --signal-on: oklch(0.155 0.008 255);
64
+ --hold: oklch(0.83 0.16 78);
65
+ --veto: oklch(0.66 0.2 25);
66
+ --veto-text: oklch(0.75 0.16 25);
67
+ --veto-edge: oklch(0.45 0.12 25);
68
+
69
+ --ground: oklch(0.155 0.008 255);
70
+ --raised: oklch(0.185 0.009 255);
71
+ --sunk: oklch(0.135 0.008 255);
72
+ --active: oklch(0.215 0.022 133);
73
+ --line: oklch(0.28 0.01 255);
74
+ --line-soft: oklch(0.26 0.01 255);
75
+ --line-faint: oklch(0.235 0.01 255);
76
+ --line-card: oklch(0.3 0.01 255);
77
+ --line-strong: oklch(0.32 0.01 255);
78
+ --edge: oklch(0.72 0.01 255);
79
+ --ink: oklch(0.96 0.004 255);
80
+ --ink-row: oklch(0.9 0.006 255);
81
+ --secondary: oklch(0.72 0.01 255);
82
+ --muted: oklch(0.6 0.012 255);
83
+ /* Lands where paper's does, at AA on its own ground: 0.55 measures 4.03
84
+ against graphite, and this token labels pending and absent states that
85
+ still have to be read. */
86
+ --muted-soft: oklch(0.575 0.012 255);
87
+ --dim: oklch(0.62 0.012 255);
88
+ --seal: oklch(0.58 0.012 255);
89
+
90
+ /* The mark is a solid black glyph, so it disappears on the graphite ground.
91
+ Inverting it yields the white it would have been drawn as. */
92
+ --mark: invert(1);`;
93
+
94
+ //#endregion
95
+ //#region ../tokens/src/views.ts
96
+ /**
97
+ * The console's view styles, as one definition.
98
+ *
99
+ * Header, stat band, ledger grid, rule and mapping tables, the awaiting-human
100
+ * panel, and the limits block — everything that makes a PRAE view look like a
101
+ * PRAE view rather than a generic table.
102
+ *
103
+ * Both renderers need these. `prae console --out` inlines them into standalone
104
+ * documents; the web app injects them into a global style tag. Exported as a
105
+ * string rather than a stylesheet so there is exactly one copy: a `.css` file
106
+ * plus a mirrored constant would be two, and two copies of a stylesheet drift
107
+ * the same way two copies of a palette do.
108
+ *
109
+ * What is *not* here is layout — the rail, the scroll containment, the routing.
110
+ * The app has a persistent frame and the exporter produces separate documents,
111
+ * and that is the one thing the two are genuinely allowed to disagree about.
112
+ */
113
+ const VIEWS_CSS = `/* ── header ───────────────────────────────────────────────────────────── */
114
+ .head {
115
+ display: flex; align-items: flex-end; justify-content: space-between;
116
+ gap: 24px; padding: 26px 30px 22px; border-bottom: 1px solid var(--line);
117
+ }
118
+ .head h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.03em; margin: 0; }
119
+ .head .meta {
120
+ font-family: var(--proof); font-size: 11.5px; color: var(--muted); margin-top: 6px;
121
+ }
122
+ .actions { display: flex; align-items: center; gap: 10px; }
123
+ .chain {
124
+ display: flex; align-items: center; gap: 8px; padding: 7px 12px;
125
+ border: 1px solid var(--signal-edge); border-radius: 999px;
126
+ font-family: var(--proof); font-size: 10.5px; letter-spacing: 0.12em;
127
+ color: var(--signal-text); white-space: nowrap;
128
+ }
129
+ .chain i { width: 6px; height: 6px; background: var(--signal-bright); border-radius: 999px; }
130
+ .chain.broken { border-color: var(--veto-edge); color: var(--veto-text); }
131
+ .chain.broken i { background: var(--veto); }
132
+ .btn {
133
+ padding: 9px 16px; border: 1px solid var(--line-strong); font-size: 13.5px;
134
+ border-radius: 2px; background: none; color: inherit; font-family: inherit;
135
+ white-space: nowrap;
136
+ }
137
+
138
+ /* ── stat band ────────────────────────────────────────────────────────── */
139
+ .stats { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--line); }
140
+ .stat { padding: 20px 24px; border-right: 1px solid var(--line-soft); }
141
+ .stat:first-child { padding-left: 30px; }
142
+ .stat:last-child { border-right: none; }
143
+ .stat .v {
144
+ font-size: 28px; font-weight: 700; letter-spacing: -0.03em;
145
+ font-variant-numeric: tabular-nums;
146
+ }
147
+ .stat .v.veto { color: var(--veto); }
148
+ .stat .v.hold { color: var(--hold); }
149
+ .stat .k {
150
+ font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
151
+ color: var(--muted); margin-top: 4px;
152
+ }
153
+
154
+ /* ── ledger table ─────────────────────────────────────────────────────── */
155
+ .grid {
156
+ display: grid;
157
+ grid-template-columns: 96px 1fr 150px 130px 104px 120px;
158
+ gap: 16px;
159
+ }
160
+ .thead {
161
+ padding: 12px 30px; font-family: var(--proof); font-size: 10px;
162
+ letter-spacing: 0.14em; color: var(--muted); border-bottom: 1px solid var(--line);
163
+ }
164
+ .row {
165
+ padding: 14px 30px; font-family: var(--proof); font-size: 12px;
166
+ border-bottom: 1px solid var(--line-faint); align-items: center;
167
+ }
168
+ .row .seq { color: var(--muted-soft); font-variant-numeric: tabular-nums; }
169
+ .row .call { color: var(--ink-row); overflow-wrap: anywhere; }
170
+ .row .actor, .row .clause { color: var(--dim); }
171
+ .row .seal { color: var(--seal); }
172
+ .row .verdict { letter-spacing: 0.1em; }
173
+ .v-allow { color: var(--signal-text); }
174
+ .v-deny { color: var(--veto-text); }
175
+ .v-ask { color: var(--hold); }
176
+ .v-correct { color: var(--muted); }
177
+ .empty { padding: 30px; font-family: var(--proof); font-size: 12px; color: var(--muted); }
178
+
179
+ /* ── awaiting-human panel ─────────────────────────────────────────────── */
180
+ .await { padding: 26px 30px 30px; background: var(--sunk); border-top: 1px solid var(--line); }
181
+ .await .k {
182
+ font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
183
+ color: var(--muted); margin-bottom: 16px;
184
+ }
185
+ .card {
186
+ display: flex; gap: 24px; align-items: flex-start; background: var(--raised);
187
+ border: 1px solid var(--line-card); padding: 22px 24px;
188
+ }
189
+ .card h2 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
190
+ .card .detail { font-family: var(--proof); font-size: 12px; color: var(--muted); line-height: 1.8; }
191
+ .decide { display: flex; flex-direction: column; gap: 8px; width: 200px; flex: none; }
192
+ .approve {
193
+ padding: 11px 18px; background: var(--signal); color: var(--signal-on);
194
+ font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
195
+ border: none; font-family: inherit;
196
+ }
197
+ .deny {
198
+ padding: 11px 18px; border: 1px solid var(--veto-edge); color: var(--veto-text);
199
+ font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
200
+ background: none; font-family: inherit;
201
+ }
202
+ /*
203
+ * A control that cannot act must not look like one that can. These render so
204
+ * the reader knows where the decision lives, and read as inert so they never
205
+ * reach for it — the harness settles approvals, and a button that looks live
206
+ * here would be an invitation to a second audit trail.
207
+ *
208
+ * The fill is dropped rather than faded. Fading a filled button works against
209
+ * paper and fails against graphite, where a dimmed green block still reads as
210
+ * a live primary action; an outline reads the same on either ground.
211
+ */
212
+ .approve[disabled], .deny[disabled] {
213
+ background: none; border: 1px solid var(--line-strong);
214
+ color: var(--muted-soft); font-weight: 500; cursor: not-allowed;
215
+ }
216
+ .recorded {
217
+ font-family: var(--proof); font-size: 10px; color: var(--muted-soft);
218
+ text-align: center; letter-spacing: 0.08em;
219
+ }
220
+
221
+ /* ── limits ───────────────────────────────────────────────────────────── */
222
+ .limits { padding: 22px 30px 28px; border-top: 1px solid var(--line); }
223
+ .limits .k {
224
+ font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
225
+ color: var(--muted); margin-bottom: 10px;
226
+ }
227
+ .limits ul { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; color: var(--secondary); }
228
+ .limits code { font-family: var(--proof); font-size: 12px; }
229
+
230
+ /* ── rule / mapping tables, shared by the packs and compliance views ──── */
231
+ .rules { display: grid; grid-template-columns: 110px 1fr 96px; gap: 16px; }
232
+ .map { display: grid; grid-template-columns: 210px 1fr 96px; gap: 16px; }
233
+ .sect { padding: 22px 30px 8px; }
234
+ .sect h2 {
235
+ font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 4px;
236
+ }
237
+ .sect p {
238
+ margin: 0 0 4px; font-size: 14px; line-height: 1.6; color: var(--secondary);
239
+ max-width: 76ch;
240
+ }
241
+ .caps { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 30px 22px; }
242
+ .cap {
243
+ font-family: var(--proof); font-size: 12px; padding: 6px 10px;
244
+ border: 1px solid var(--signal-edge); color: var(--signal-text);
245
+ border-radius: 2px;
246
+ }
247
+ .cap.off {
248
+ border-color: var(--line-strong); color: var(--muted-soft);
249
+ text-decoration: line-through;
250
+ }
251
+ .effect { letter-spacing: 0.1em; font-size: 11px; }
252
+ .e-deny { color: var(--veto-text); }
253
+ .e-ask { color: var(--hold); }
254
+ .e-warn { color: var(--muted); }
255
+ .e-correct { color: var(--signal-text); }
256
+ .covered { color: var(--signal-text); }
257
+ .partial { color: var(--hold); }
258
+ .absent { color: var(--muted-soft); }
259
+
260
+ button { cursor: pointer; }
261
+ a:focus-visible, button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
262
+ `;
263
+
264
+ //#endregion
265
+ //#region ../tokens/src/index.ts
266
+ /** The Google Fonts stylesheet both surfaces link. */
267
+ const FONT_HREF = "https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap";
268
+ /**
269
+ * Verdict chip labels. Uppercase, mono, and never rendered without a clause
270
+ * reference within 8px — §04 is explicit that a chip alone is not allowed.
271
+ */
272
+ const VERDICT_LABEL = {
273
+ allow: "ALLOWED",
274
+ ask: "HELD",
275
+ deny: "DENIED",
276
+ correct: "CORRECTED"
277
+ };
278
+
279
+ //#endregion
280
+ //#region src/console/shell.ts
281
+ function esc(value) {
282
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
283
+ }
284
+ /** The stylesheet both console views share. Tokens first, then the surface. */
285
+ /** The stylesheet both console views share. Tokens first, then the surface. */
286
+ const SHEET = `
287
+ :root {
288
+ ${PAPER}
289
+ --claim: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
290
+ --proof: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
291
+ --ease: cubic-bezier(0.2, 0, 0, 1);
292
+ --surface-motion: 180ms;
293
+ }
294
+
295
+ /* An explicit choice, remembered. */
296
+ :root[data-theme='dark'] {
297
+ ${FENCE}
298
+ }
299
+
300
+ /* No choice made: follow the machine, unless the reader has said light. */
301
+ @media (prefers-color-scheme: dark) {
302
+ :root:not([data-theme='light']) {
303
+ ${FENCE}
304
+ }
305
+ }
306
+
307
+ /*
308
+ * The ground moves at the surface budget, and only while it is moving.
309
+ *
310
+ * Arming this permanently would put a 180ms fade on every hover and every
311
+ * verdict that renders, so the class goes on for one toggle and comes off.
312
+ * §01's 0ms rule is about a decision animating in; nothing is deciding here,
313
+ * the page is repainting, and a verdict that snapped while the page behind it
314
+ * faded would read as a glitch.
315
+ */
316
+ html.theming, html.theming * {
317
+ transition:
318
+ background-color var(--surface-motion) var(--ease),
319
+ border-color var(--surface-motion) var(--ease),
320
+ color var(--surface-motion) var(--ease),
321
+ fill var(--surface-motion) var(--ease) !important;
322
+ }
323
+
324
+ * { box-sizing: border-box; }
325
+
326
+ /*
327
+ * An application, not a document: the shell fills the viewport and only the
328
+ * content column scrolls, so the rail and the header stay where a reader left
329
+ * them. Print undoes all of it further down — the same page still has to
330
+ * flatten into evidence.
331
+ */
332
+ html, body { height: 100%; }
333
+ body {
334
+ margin: 0;
335
+ background: var(--ground);
336
+ color: var(--ink);
337
+ font-family: var(--claim);
338
+ -webkit-font-smoothing: antialiased;
339
+ overflow: hidden;
340
+ }
341
+
342
+ .wrap { height: 100%; }
343
+ .scroll { height: 100%; }
344
+
345
+ .console {
346
+ background: var(--ground);
347
+ display: grid;
348
+ grid-template-columns: 232px 1fr;
349
+ height: 100%;
350
+ min-height: 0;
351
+ }
352
+
353
+ /* The one column that moves. */
354
+ .console > main {
355
+ overflow-y: auto;
356
+ overflow-x: auto;
357
+ min-height: 0;
358
+ scroll-behavior: smooth;
359
+ }
360
+
361
+ /* ── rail ─────────────────────────────────────────────────────────────── */
362
+ .rail {
363
+ border-right: 1px solid var(--line); padding: 22px 0;
364
+ overflow-y: auto; min-height: 0;
365
+ display: flex; flex-direction: column;
366
+ }
367
+ .rail .compliance { margin-top: auto; }
368
+
369
+ /*
370
+ * Two positions, named, rather than a switch with a state you have to infer.
371
+ * Hidden until the head script has run: without scripting it would be a
372
+ * control that does nothing.
373
+ */
374
+ .ground { display: none; margin: auto 20px 14px; }
375
+ html.js .ground { display: grid; grid-template-columns: 1fr 1fr; }
376
+ html.js .rail .compliance { margin-top: 0; }
377
+ .ground button {
378
+ font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
379
+ padding: 6px 0; background: none; color: var(--muted);
380
+ border: 1px solid var(--line); border-radius: 0;
381
+ transition: color 120ms var(--ease), border-color 120ms var(--ease);
382
+ }
383
+ .ground button:first-child { border-radius: 2px 0 0 2px; border-right: none; }
384
+ .ground button:last-child { border-radius: 0 2px 2px 0; }
385
+ .ground button:hover { color: var(--secondary); }
386
+ :root:not([data-theme='dark']) .ground [data-ground='paper'],
387
+ :root[data-theme='dark'] .ground [data-ground='fence'] {
388
+ color: var(--ink); border-color: var(--line-strong); background: var(--active);
389
+ }
390
+ .brand { display: flex; align-items: center; gap: 10px; padding: 0 20px 22px; }
391
+ /* The canvas sizes the mark at 26px; the glyph is square and needs no frame. */
392
+ .mark { width: 26px; height: 26px; display: block; filter: var(--mark); }
393
+ .brand b {
394
+ font-weight: 700; font-size: 16px; letter-spacing: 0.04em;
395
+ }
396
+ .eyebrow {
397
+ font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
398
+ color: var(--muted-soft); padding: 0 20px 10px;
399
+ }
400
+ .nav { display: grid; }
401
+ .nav a, .nav span {
402
+ padding: 10px 20px; font-size: 14.5px; color: var(--secondary);
403
+ text-decoration: none; display: flex; justify-content: space-between;
404
+ align-items: center;
405
+ }
406
+ .nav a { transition: background var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)),
407
+ color var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)); }
408
+ .nav a:hover { background: var(--sunk); }
409
+
410
+ /*
411
+ * The surface transition, spent where the language says to spend it. Verdicts
412
+ * are deliberately excluded: a decision appears, it never animates in.
413
+ */
414
+ main > * { animation: surface 180ms cubic-bezier(0.2, 0, 0, 1) both; }
415
+ @keyframes surface {
416
+ from { opacity: 0; transform: translateY(4px); }
417
+ to { opacity: 1; transform: none; }
418
+ }
419
+ main.leaving { opacity: 0; transition: opacity 120ms cubic-bezier(0.2, 0, 0, 1); }
420
+ @media (prefers-reduced-motion: reduce) {
421
+ main > *, main.leaving { animation: none; transition: none; }
422
+ .console > main { scroll-behavior: auto; }
423
+ :root { --surface-motion: 0ms; }
424
+ }
425
+ .nav .pending { color: var(--muted-soft); }
426
+ .nav .badge.soon {
427
+ font-size: 9px; letter-spacing: 0.14em; color: var(--muted-soft);
428
+ border: 1px solid var(--line); border-radius: 999px; padding: 1px 6px;
429
+ }
430
+ .nav a.on {
431
+ font-weight: 600; color: var(--ink);
432
+ border-left: 2px solid var(--signal-bright);
433
+ background: var(--active); margin-left: -1px;
434
+ }
435
+ .nav .badge { font-family: var(--proof); font-size: 11px; color: var(--hold); }
436
+ .compliance {
437
+ margin: 22px 20px 0; padding-top: 18px; border-top: 1px solid var(--line-soft);
438
+ font-family: var(--proof); font-size: 10.5px; line-height: 1.9; color: var(--muted);
439
+ }
440
+
441
+ ${VIEWS_CSS}
442
+
443
+ /* Print is a first-class state, not an afterthought. */
444
+ @media print {
445
+ @page { margin: 12mm; }
446
+ /* Evidence prints on paper. The fence has no printed form. */
447
+ :root, :root[data-theme='dark'] {
448
+ ${PAPER}
449
+ }
450
+ html.theming, html.theming * { transition: none !important; }
451
+ html, body { height: auto; overflow: visible; }
452
+ body { padding: 0; background: #fff; }
453
+ .wrap, .scroll { height: auto; }
454
+ .console > main { overflow: visible; height: auto; }
455
+ main > * { animation: none; }
456
+ .console {
457
+ border: none; grid-template-columns: 1fr; min-width: 0;
458
+ height: auto; display: block;
459
+ }
460
+ .rail, .actions, .decide, .ground { display: none; }
461
+ .thead { display: grid; }
462
+ thead { display: table-header-group; }
463
+ .row { break-inside: avoid; }
464
+ /* A chip is colour; a printed page may not be. Keep the word. */
465
+ .chain { border-color: currentColor !important; }
466
+ .stats { break-inside: avoid; }
467
+ }
468
+ `;
469
+ /**
470
+ * Resolve the ground before the first paint.
471
+ *
472
+ * This runs in `<head>`, synchronously, on purpose: read the choice after the
473
+ * body has painted and the reader watches the page flash the wrong palette on
474
+ * every navigation. It also marks the document as scripted, which is what
475
+ * reveals the toggle — a control that cannot work should not be offered.
476
+ */
477
+ const GROUND_SCRIPT = `
478
+ (() => {
479
+ const root = document.documentElement;
480
+ root.classList.add('js');
481
+ let choice = null;
482
+ try { choice = localStorage.getItem('prae-ground'); } catch (e) {}
483
+ if (choice !== 'dark' && choice !== 'light') {
484
+ choice = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
485
+ }
486
+ root.dataset.theme = choice;
487
+ })();
488
+ `;
489
+ /**
490
+ * Client-side navigation, as progressive enhancement.
491
+ *
492
+ * Every view is still a complete document that opens, prints and mails on its
493
+ * own — that requirement has not moved. This only intercepts a click when the
494
+ * page is being served, swaps the main column, and leaves the rail alone, so
495
+ * the frame stays put and the surface transition the language budgets actually
496
+ * has somewhere to happen. With scripting off, the links are ordinary links.
497
+ */
498
+ const NAV_SCRIPT = `
499
+ (() => {
500
+ const main = () => document.querySelector('main');
501
+ const rail = () => document.querySelector('.rail');
502
+ if (!main() || !window.fetch) return;
503
+
504
+ const swap = async (href, push) => {
505
+ const target = main();
506
+ target.classList.add('leaving');
507
+ try {
508
+ const res = await fetch(href, { headers: { accept: 'text/html' } });
509
+ if (!res.ok) throw new Error(String(res.status));
510
+ const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
511
+ const next = doc.querySelector('main');
512
+ const nextRail = doc.querySelector('.rail');
513
+ if (!next) throw new Error('no main');
514
+
515
+ // Collect the view's scripts BEFORE moving it: replaceWith detaches the
516
+ // element from the parsed document, so querying that document afterwards
517
+ // finds nothing and the view arrives inert. A parser-created script never
518
+ // executes on insertion either, which is why each is re-created below.
519
+ const scripts = [...next.querySelectorAll('script')].map((s) => s.textContent);
520
+ for (const s of next.querySelectorAll('script')) s.remove();
521
+
522
+ target.replaceWith(next);
523
+ // Only the rail's active marker changes; the element itself stays, so
524
+ // nothing in it flickers or loses scroll position.
525
+ if (nextRail) rail().innerHTML = nextRail.innerHTML;
526
+ if (doc.title) document.title = doc.title;
527
+ if (push) history.pushState({}, '', href);
528
+ next.scrollTop = 0;
529
+ wire();
530
+ for (const code of scripts) {
531
+ const el = document.createElement('script');
532
+ el.textContent = code;
533
+ document.body.appendChild(el);
534
+ }
535
+ } catch {
536
+ // A failed swap falls back to a real navigation rather than stranding
537
+ // the reader on a half-updated page.
538
+ location.href = href;
539
+ }
540
+ };
541
+
542
+ const wire = () => {
543
+ for (const a of document.querySelectorAll('.nav a[href$=".html"]')) {
544
+ a.addEventListener('click', (e) => {
545
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
546
+ e.preventDefault();
547
+ swap(a.getAttribute('href'), true);
548
+ });
549
+ }
550
+ };
551
+
552
+ // Delegated, because a client-side swap replaces the rail's markup and a
553
+ // handler bound to the old buttons would go with it.
554
+ let settle;
555
+ document.addEventListener('click', (e) => {
556
+ const button = e.target.closest('[data-ground]');
557
+ if (!button) return;
558
+ const next = button.dataset.ground === 'fence' ? 'dark' : 'light';
559
+ const root = document.documentElement;
560
+ if (root.dataset.theme === next) return;
561
+
562
+ // Arm the transition, let the current palette paint once, then move. Doing
563
+ // both in one frame gives the browser no 'from' state and it jumps.
564
+ root.classList.add('theming');
565
+ void root.offsetHeight;
566
+ root.dataset.theme = next;
567
+ try { localStorage.setItem('prae-ground', next); } catch (err) {}
568
+ clearTimeout(settle);
569
+ settle = setTimeout(() => root.classList.remove('theming'), 240);
570
+ });
571
+
572
+ addEventListener('popstate', () => swap(location.pathname, false));
573
+ wire();
574
+ })();
575
+ `;
576
+ /** Wrap a view in the console shell: rail, then the view's own header and body. */
577
+ function renderShell(page) {
578
+ return `<!doctype html>
579
+ <html lang="en" data-prae="ledger"><head>
580
+ <meta charset="utf-8">
581
+ <meta name="viewport" content="width=device-width, initial-scale=1">
582
+ <title>${esc(page.title)}</title>
583
+ <link rel="preconnect" href="https://fonts.googleapis.com">
584
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
585
+ <link href="${FONT_HREF}" rel="stylesheet">
586
+ <script>${GROUND_SCRIPT}<\/script>
587
+ <style>${SHEET}</style>
588
+ </head><body>
589
+ <div class="wrap"><div class="scroll">
590
+ <div class="console">
591
+ <nav class="rail">
592
+ <div class="brand"><img class="mark" src="${LOGO}" alt=""><b>PRAE</b></div>
593
+ <div class="eyebrow">CONTROL PLANE</div>
594
+ ${page.nav}
595
+ <div class="ground" role="group" aria-label="Ground">
596
+ <button type="button" data-ground="paper">PAPER</button>
597
+ <button type="button" data-ground="fence">FENCE</button>
598
+ </div>
599
+ <div class="compliance">EU AI ACT art. 12<br>NIST AI 600-1<br>SOC 2 CC7.2</div>
600
+ </nav>
601
+ <main>
602
+ ${page.head}
603
+ ${page.body}
604
+ </main>
605
+ </div>
606
+ </div></div>
607
+ <script>${NAV_SCRIPT}<\/script>
608
+ </body></html>
609
+ `;
610
+ }
611
+ function renderNav(items, active) {
612
+ return `<div class="nav">\n${items.map((item) => {
613
+ const classes = [item.label === active ? "on" : "", item.pending === true ? "pending" : ""].filter(Boolean).join(" ");
614
+ const attr = classes === "" ? "" : ` class="${classes}"`;
615
+ const badge = item.pending === true ? "<span class=\"badge soon\">SOON</span>" : item.badge === void 0 || item.badge === 0 ? "" : `<span class="badge">${item.badge}</span>`;
616
+ const href = item.pending === true ? "" : ` href="${esc(item.href ?? "#")}"`;
617
+ const tag = item.pending === true ? "span" : "a";
618
+ return ` <${tag}${href}${attr}>${esc(item.label)}${badge}</${tag}>`;
619
+ }).join("\n")}\n </div>`;
620
+ }
621
+
622
+ //#endregion
623
+ //#region ../controls/src/index.ts
624
+ /**
625
+ * What each control asks for, and what this profile actually produces.
626
+ *
627
+ * Framed as evidence toward a control, never as compliance with it. Nothing
628
+ * here certifies anything: an auditor decides whether the evidence satisfies
629
+ * the obligation, and a tool that told them the answer would be selling the
630
+ * one thing it cannot supply.
631
+ */
632
+ const CONTROLS = [
633
+ {
634
+ id: "EU AI Act art. 12",
635
+ asks: "Automatic recording of events over the lifetime of the system, to a degree that enables tracing.",
636
+ evidence: (data) => ({
637
+ state: data.entries.length > 0 ? "covered" : "partial",
638
+ note: data.entries.length > 0 ? `${data.entries.length} decisions recorded, each with the clause that produced it and a hash linking it to the one before.` : "Recording is configured but nothing has been logged yet."
639
+ })
640
+ },
641
+ {
642
+ id: "NIST AI 600-1 · GV-1.2",
643
+ asks: "Characteristics of trustworthy AI are integrated into organisational policies.",
644
+ evidence: (data) => ({
645
+ state: data.pack === void 0 ? "absent" : "covered",
646
+ note: data.pack === void 0 ? "No pack supplied to this view." : `${data.pack.rules.length} rules are declared as data and applied to every tool call, rather than living in a runbook.`
647
+ })
648
+ },
649
+ {
650
+ id: "NIST AI 600-1 · MS-2.6",
651
+ asks: "AI system is evaluated regularly for safety risks, with results documented.",
652
+ evidence: () => ({
653
+ state: "partial",
654
+ note: "Decisions are documented per call. Periodic evaluation of the policy itself is an operator process this tool does not perform."
655
+ })
656
+ },
657
+ {
658
+ id: "SOC 2 · CC7.2",
659
+ asks: "The entity monitors system components for anomalies indicative of malicious acts.",
660
+ evidence: (data) => ({
661
+ state: data.entries.some((e) => e.verdict === "deny") ? "covered" : "partial",
662
+ note: `Refusals and held calls are recorded with their clause. ${data.entries.some((e) => e.verdict === "deny") ? "This record contains refusals." : "This record contains none yet, so the monitoring is unproven in practice."}`
663
+ })
664
+ },
665
+ {
666
+ id: "SOC 2 · CC6.1",
667
+ asks: "Logical access to protected information assets is restricted to authorised users.",
668
+ evidence: () => ({
669
+ state: "partial",
670
+ note: "Capabilities are restricted per agent. Authorisation of the human is not: approvals record what was decided, never who decided it."
671
+ })
672
+ }
673
+ ];
674
+
675
+ //#endregion
676
+ //#region src/console/views.ts
677
+ /** One rail for every view, so the shape of the product stays consistent. */
678
+ function navFor(data) {
679
+ return [
680
+ {
681
+ label: "Overview",
682
+ href: "overview.html"
683
+ },
684
+ {
685
+ label: "Policy packs",
686
+ href: "packs.html"
687
+ },
688
+ {
689
+ label: "Instructions",
690
+ href: "instructions.html"
691
+ },
692
+ {
693
+ label: "Playground",
694
+ href: "playground.html"
695
+ },
696
+ {
697
+ label: "Approvals",
698
+ href: "approvals.html",
699
+ badge: data.entries.filter((e) => e.verdict === "ask").length
700
+ },
701
+ {
702
+ label: "Audit ledger",
703
+ href: "ledger.html"
704
+ },
705
+ {
706
+ label: "Fleet",
707
+ pending: true
708
+ },
709
+ {
710
+ label: "Compliance map",
711
+ href: "compliance.html"
712
+ }
713
+ ];
714
+ }
715
+ function chainPill(ok) {
716
+ return `<div class="chain${ok ? "" : " broken"}"><i></i>${ok ? "CHAIN INTACT" : "CHAIN BROKEN"}</div>`;
717
+ }
718
+ function header(title, meta, ok) {
719
+ return ` <header class="head">
720
+ <div><h1>${esc(title)}</h1><div class="meta">${esc(meta)}</div></div>
721
+ <div class="actions">${chainPill(ok)}
722
+ <button type="button" class="btn" onclick="window.print()">Export evidence</button>
723
+ </div>
724
+ </header>`;
725
+ }
726
+ function renderOverview(data) {
727
+ const counts = {};
728
+ for (const entry of data.entries) counts[entry.verdict] = (counts[entry.verdict] ?? 0) + 1;
729
+ const held = counts.ask ?? 0;
730
+ const pack = data.pack;
731
+ const enforcing = pack?.rules.some((r) => r.effect.kind === "deny") ?? false;
732
+ const body = `${` <section class="stats">
733
+ <div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
734
+ <div class="stat"><div class="v veto">${counts.deny ?? 0}</div><div class="k">VETOED</div></div>
735
+ <div class="stat"><div class="v hold">${held}</div><div class="k">AWAITING HUMAN</div></div>
736
+ <div class="stat"><div class="v">${pack === void 0 ? "—" : esc(pack.rules.length)}</div><div class="k">RULES IN FORCE</div></div>
737
+ </section>`}
738
+ <section class="sect">
739
+ <h2>Posture</h2>
740
+ <p>${pack === void 0 ? "No pack was supplied to this view, so nothing here describes a policy in force." : enforcing ? `<strong>${esc(pack.pack)} ${esc(pack.version)}</strong> is enforcing: some calls are refused outright.` : `<strong>${esc(pack.pack)} ${esc(pack.version)}</strong> is observing: it records what an enforcing pack would refuse, and denies nothing.`}</p>
741
+ <p>${data.verification.ok ? "The decision chain verifies: no row has been removed, edited, or reordered since it was written." : "The decision chain does <strong>not</strong> verify. This record is not continuous, and the Audit ledger names where it breaks."}</p>
742
+ </section>
743
+ <section class="sect" style="padding-bottom:26px">
744
+ <h2>What is not covered</h2>
745
+ <p>This gate decides tool calls. It does not sandbox plugin code — a
746
+ plugin's <code>apply()</code> runs with full privileges before any tool
747
+ call exists — and it does not secure the harness itself. Approvals record
748
+ what was decided, never who decided it.</p>
749
+ </section>`;
750
+ return renderShell({
751
+ title: `Overview — ${pack?.pack ?? "PRAE"}`,
752
+ nav: renderNav(navFor(data), "Overview"),
753
+ head: header("Overview", `${data.entries.length} decisions · ${pack === void 0 ? "no pack" : `${pack.pack} ${pack.version}`}`, data.verification.ok),
754
+ body
755
+ });
756
+ }
757
+ function matcherOf(rule) {
758
+ const when = rule.when;
759
+ const parts = [];
760
+ const list = (v) => v === void 0 ? void 0 : Array.isArray(v) ? v.join(", ") : String(v);
761
+ const cap = list(when.capability);
762
+ if (cap !== void 0) parts.push(`capability ${cap}`);
763
+ const tool = list(when.tool);
764
+ if (tool !== void 0) parts.push(`tool ${tool}`);
765
+ const plugin = list(when.plugin);
766
+ if (plugin !== void 0) parts.push(`plugin ${plugin}`);
767
+ if (when.path?.outside !== void 0) parts.push(`path outside ${when.path.outside.join(", ")}`);
768
+ if (when.path?.inside !== void 0) parts.push(`path inside ${when.path.inside.join(", ")}`);
769
+ if (when.host?.notIn !== void 0) parts.push(`host not in ${when.host.notIn.join(", ")}`);
770
+ if (when.host?.in !== void 0) parts.push(`host in ${when.host.in.join(", ")}`);
771
+ if (when.count !== void 0) parts.push(`more than ${when.count.above}`);
772
+ for (const arg of when.arg ?? []) {
773
+ const bits = [
774
+ arg.above === void 0 ? "" : `above ${arg.above}`,
775
+ arg.below === void 0 ? "" : `below ${arg.below}`,
776
+ arg.equals === void 0 ? "" : `= ${String(arg.equals)}`
777
+ ].filter(Boolean);
778
+ parts.push(`${arg.name} ${bits.join(" ")}`.trim());
779
+ }
780
+ return parts.length === 0 ? "every call" : parts.join(" · ");
781
+ }
782
+ function renderPacks(data) {
783
+ const pack = data.pack;
784
+ if (pack === void 0) return renderShell({
785
+ title: "Policy packs",
786
+ nav: renderNav(navFor(data), "Policy packs"),
787
+ head: header("Policy packs", "no pack supplied", data.verification.ok),
788
+ body: " <div class=\"empty\">Pass --pack to render the policy in force.</div>"
789
+ });
790
+ const baseline = pack.baseline?.capabilities;
791
+ const caps = baseline === void 0 ? `<p style="padding:0 30px 22px;font-size:14px;color:var(--secondary);max-width:76ch">This pack sets no capability ceiling. It governs through its rules alone, and withholds nothing an author declares.</p>` : `<div class="caps">${baseline.map((c) => `<span class="cap">${esc(c)}</span>`).join("")}</div>`;
792
+ const rows = pack.rules.map((rule) => ` <div class="grid rules row">
793
+ <div class="seq">${esc(rule.id)}</div>
794
+ <div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span></div>
795
+ <div class="effect e-${esc(rule.effect.kind)}">${esc(rule.effect.kind.toUpperCase())}</div>
796
+ </div>`).join("\n");
797
+ const body = ` <section class="sect">
798
+ <h2>${esc(pack.pack)} <span style="font-family:var(--proof);font-size:14px;color:var(--muted)">${esc(pack.version)}</span></h2>
799
+ <p>The operator baseline is intersected with whatever a plugin's own
800
+ manifest declares, and the operator can only ever narrow. A pack cannot
801
+ grant a capability an author never asked for.</p>
802
+ </section>
803
+ ${caps}
804
+ <div class="grid rules thead">
805
+ <div>CLAUSE</div><div>RULE</div><div>EFFECT</div>
806
+ </div>
807
+ ${rows}
808
+ <section class="sect" style="padding-bottom:26px">
809
+ <h2>Ordering</h2>
810
+ <p>When several rules match one call the strongest effect wins —
811
+ deny over ask over correct over warn — and every match stays in the
812
+ trace. That is what makes a pack safe to extend: appending a rule can
813
+ raise a decision's strength and never lower it.</p>
814
+ </section>`;
815
+ return renderShell({
816
+ title: `Policy packs — ${pack.pack}`,
817
+ nav: renderNav(navFor(data), "Policy packs"),
818
+ head: header("Policy packs", `${pack.pack} ${pack.version} · ${pack.rules.length} rules`, data.verification.ok),
819
+ body
820
+ });
821
+ }
822
+ function renderCompliance(data) {
823
+ const body = ` <section class="sect">
824
+ <h2>Evidence, not compliance</h2>
825
+ <p>Each row states what a control asks for and what this profile actually
826
+ produces. Nothing here certifies anything — whether the evidence satisfies
827
+ the obligation is an auditor's judgement, and a tool that answered it for
828
+ them would be selling the one thing it cannot supply.</p>
829
+ </section>
830
+ <div class="grid map thead">
831
+ <div>CONTROL</div><div>ASKS FOR · WHAT THIS PROFILE PRODUCES</div><div>STATE</div>
832
+ </div>
833
+ ${CONTROLS.map((control) => {
834
+ const found = control.evidence(data);
835
+ const label = found.state === "covered" ? "EVIDENCE" : found.state === "partial" ? "PARTIAL" : "NONE";
836
+ return ` <div class="grid map row">
837
+ <div class="call">${esc(control.id)}</div>
838
+ <div class="actor">${esc(control.asks)}<br><span style="color:var(--ink-row)">${esc(found.note)}</span></div>
839
+ <div class="effect ${esc(found.state)}">${label}</div>
840
+ </div>`;
841
+ }).join("\n")}
842
+ <section class="sect" style="padding-bottom:26px">
843
+ <h2>The gap that recurs</h2>
844
+ <p>Three of these rows stop at <em>partial</em> for the same reason: the
845
+ harness records what was decided and never who decided it. Attributable
846
+ approval needs an identity anchor outside this machine, which is a
847
+ control-plane concern this build does not claim.</p>
848
+ </section>`;
849
+ return renderShell({
850
+ title: "Compliance map",
851
+ nav: renderNav(navFor(data), "Compliance map"),
852
+ head: header("Compliance map", `${CONTROLS.length} controls · evidence from this profile`, data.verification.ok),
853
+ body
854
+ });
855
+ }
856
+ /**
857
+ * Every call the policy held, and what became of it.
858
+ *
859
+ * This is a queue you read, not one you act on. The gate returns `ask` into the
860
+ * harness's own approval seam and the harness settles it there; a second set of
861
+ * buttons here would mean two places to approve from and two audit trails to
862
+ * reconcile. So the view shows what is waiting and what was decided, and says
863
+ * plainly where the decision happens.
864
+ */
865
+ function renderApprovals(data) {
866
+ const held = data.entries.filter((e) => e.verdict === "ask");
867
+ const settled = data.entries.filter((e) => e.rule === "approval");
868
+ const now = Date.parse(data.generatedAt);
869
+ const waited = (iso) => {
870
+ const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
871
+ if (!Number.isFinite(s)) return "";
872
+ const h = Math.floor(s / 3600);
873
+ const m = Math.floor(s % 3600 / 60);
874
+ return h > 0 ? `${h}h ${m}m` : `${m}m`;
875
+ };
876
+ const queue = held.length === 0 ? " <div class=\"empty\">Nothing is waiting on a human.</div>" : held.map((e) => ` <section class="await" style="border-top:none">
877
+ <div class="card">
878
+ <div style="flex:1">
879
+ <h2>${esc(e.tool)}${e.subject === void 0 ? "" : ` on ${esc(e.subject)}`}</h2>
880
+ <div class="detail">
881
+ ${esc(e.rule ?? "held")} · ${esc(e.session ?? "local session")} · waiting ${esc(waited(e.at))}<br>
882
+ ${esc(e.reason ?? "")}
883
+ </div>
884
+ </div>
885
+ <div class="decide">
886
+ <button type="button" class="approve" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Approve</button>
887
+ <button type="button" class="deny" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Deny</button>
888
+ <div class="recorded">SETTLED IN THE HARNESS</div>
889
+ </div>
890
+ </div>
891
+ </section>`).join("\n");
892
+ const history = settled.length === 0 ? "" : ` <div class="grid map thead">
893
+ <div>WHEN</div><div>CALL · WHY</div><div>OUTCOME</div>
894
+ </div>
895
+ ${settled.map((e) => ` <div class="grid map row">
896
+ <div class="seq">${esc(e.at.replace("T", " ").replace(/\.\d+Z$/, "Z"))}</div>
897
+ <div class="call">${esc(e.tool)}${e.subject === void 0 ? "" : ` ${esc(e.subject)}`}<br><span style="color:var(--muted)">${esc(e.reason ?? "")}${e.actor === void 0 ? "" : ` · ${esc(e.actor)}`}</span></div>
898
+ <div class="effect ${e.verdict === "allow" ? "covered" : "absent"}">${e.verdict === "allow" ? "APPROVED" : "REFUSED"}</div>
899
+ </div>`).join("\n")}`;
900
+ const body = ` <section class="stats">
901
+ <div class="stat"><div class="v hold">${held.length}</div><div class="k">AWAITING HUMAN</div></div>
902
+ <div class="stat"><div class="v">${settled.length}</div><div class="k">SETTLED</div></div>
903
+ <div class="stat"><div class="v">${settled.filter((e) => e.actor !== void 0).length}</div><div class="k">WITH AN ACTOR</div></div>
904
+ <div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
905
+ </section>
906
+ <section class="sect">
907
+ <h2>Waiting</h2>
908
+ <p>A held call is one a clause would not decide alone. The harness owns
909
+ the prompt and settles it; this is the queue, not a second place to
910
+ approve from.</p>
911
+ </section>
912
+ ${queue}
913
+ <section class="sect">
914
+ <h2>Settled</h2>
915
+ <p>Where an entry names an actor, that name is asserted by configuration.
916
+ The harness records what was decided and never who decided it, so an
917
+ attributable approval needs an identity anchor this build does not have.</p>
918
+ </section>
919
+ ${history}`;
920
+ return renderShell({
921
+ title: "Approvals",
922
+ nav: renderNav(navFor(data), "Approvals"),
923
+ head: header("Approvals", `${held.length} waiting · ${settled.length} settled`, data.verification.ok),
924
+ body
925
+ });
926
+ }
927
+
928
+ //#endregion
929
+ export { renderPacks as a, renderShell as c, renderOverview as i, VERDICT_LABEL as l, renderApprovals as n, esc as o, renderCompliance as r, renderNav as s, navFor as t };