prae-cli 0.9.2 → 0.10.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.
- package/lib/bin.js +2 -6
- package/lib/{checkpoint-C_q_Rpa5.js → checkpoint-BqJ8xwky.js} +1 -1
- package/lib/cli-BcAJlB9B.js +8 -0
- package/lib/{cli-B_B6-bUZ.js → cli-DPldZ2ND.js} +259 -526
- package/lib/docs/pilot-claude-code.md +191 -0
- package/lib/{evaluate-BqLxhYiP.js → evaluate-DhoGQw0Z.js} +23 -1
- package/lib/frameworks-BgZHGKiH.js +118 -0
- package/lib/hook-Gll2spTP.js +695 -0
- package/lib/index.js +6 -4
- package/lib/init-DRd-IRR6.js +266 -0
- package/lib/{instructions-ya0LVnZy.js → instructions-DihPiiC2.js} +2 -1
- package/lib/{mcp-BCXbpqu4.js → mcp-D521EoP6.js} +2 -2
- package/lib/pack-h5n43S0F.js +448 -0
- package/lib/playground.js +4 -0
- package/lib/{receipt-check-DQK_gVPc.js → receipt-check-CUuEjGHL.js} +1 -1
- package/lib/{src-LA7NdLKb.js → src-Cy-Acrgj.js} +104 -25
- package/lib/{src-CwBEFhww.js → src-DMoYsdfo.js} +1 -1
- package/lib/src-wG36kT6K.js +3 -0
- package/lib/{test-run-CCDgJ0Dp.js → test-run-C8dHjIE2.js} +1 -1
- package/lib/views-CUcixte0.js +1396 -0
- package/package.json +4 -4
- package/lib/src-7X6dO7gC.js +0 -3
- package/lib/views-D4UEfbzt.js +0 -1505
- /package/lib/{ocsf-DhVGbBUw.js → ocsf-DF9cMiQJ.js} +0 -0
- /package/lib/{src-CBGZVvt7.js → src-BlmsTGdY.js} +0 -0
- /package/lib/{src-B25SHWcz.js → src-BuaOiL2v.js} +0 -0
- /package/lib/{src-d4c_n6wd.js → src-CDYyZyeU.js} +0 -0
package/lib/views-D4UEfbzt.js
DELETED
|
@@ -1,1505 +0,0 @@
|
|
|
1
|
-
//#region ../kernel/src/frameworks.ts
|
|
2
|
-
const FRAMEWORKS = {
|
|
3
|
-
"owasp-llm-top-10-2026": {
|
|
4
|
-
name: "OWASP Top 10 for LLM Applications — 2026",
|
|
5
|
-
short: "OWASP LLM 2026",
|
|
6
|
-
controls: {
|
|
7
|
-
LLM01: {
|
|
8
|
-
name: "Prompt Injection",
|
|
9
|
-
summary: "Instructions in a prompt or in retrieved content alter what the model does."
|
|
10
|
-
},
|
|
11
|
-
LLM02: {
|
|
12
|
-
name: "Sensitive Information Disclosure",
|
|
13
|
-
summary: "Credentials, personal data or proprietary content reach the model or its output."
|
|
14
|
-
},
|
|
15
|
-
LLM03: {
|
|
16
|
-
name: "Excessive Agency",
|
|
17
|
-
summary: "The model can take actions, or holds permissions, beyond what its task needs."
|
|
18
|
-
},
|
|
19
|
-
LLM04: {
|
|
20
|
-
name: "Supply Chain",
|
|
21
|
-
summary: "A compromised model, dataset, plugin or dependency enters the application."
|
|
22
|
-
},
|
|
23
|
-
LLM05: {
|
|
24
|
-
name: "Data and Model Poisoning",
|
|
25
|
-
summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
|
|
26
|
-
},
|
|
27
|
-
LLM06: {
|
|
28
|
-
name: "Unbounded Consumption",
|
|
29
|
-
summary: "The application permits resource use without a limit, exhausting budget or service."
|
|
30
|
-
},
|
|
31
|
-
LLM07: {
|
|
32
|
-
name: "Misinformation",
|
|
33
|
-
summary: "The model produces false or misleading content that is acted on as if true."
|
|
34
|
-
},
|
|
35
|
-
LLM08: {
|
|
36
|
-
name: "Hidden Context Exposure",
|
|
37
|
-
summary: "System prompts, hidden instructions or internal context are revealed."
|
|
38
|
-
},
|
|
39
|
-
LLM09: {
|
|
40
|
-
name: "Vector and Embedding Weaknesses",
|
|
41
|
-
summary: "Retrieval stores are manipulated or leak across tenants and permissions."
|
|
42
|
-
},
|
|
43
|
-
LLM10: {
|
|
44
|
-
name: "Improper Output Handling",
|
|
45
|
-
summary: "Model output is passed to downstream systems without validation."
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"owasp-agentic-top-10-2026": {
|
|
50
|
-
name: "OWASP Top 10 for Agentic Applications — 2026",
|
|
51
|
-
short: "OWASP Agentic 2026",
|
|
52
|
-
controls: {
|
|
53
|
-
ASI01: {
|
|
54
|
-
name: "Agent Goal Hijack",
|
|
55
|
-
summary: "Untrusted input redirects what the agent is trying to do, turning its objectives against its owner."
|
|
56
|
-
},
|
|
57
|
-
ASI02: {
|
|
58
|
-
name: "Tool Misuse and Exploitation",
|
|
59
|
-
summary: "A legitimate tool the agent already holds is turned to an attacker's ends."
|
|
60
|
-
},
|
|
61
|
-
ASI03: {
|
|
62
|
-
name: "Identity and Privilege Abuse",
|
|
63
|
-
summary: "The agent's own authority, credentials or delegated access is inherited or escalated."
|
|
64
|
-
},
|
|
65
|
-
ASI04: {
|
|
66
|
-
name: "Agentic Supply Chain",
|
|
67
|
-
summary: "A poisoned tool, model, plugin, skill or dependency is trusted before the agent runs."
|
|
68
|
-
},
|
|
69
|
-
ASI05: {
|
|
70
|
-
name: "Unexpected Code Execution",
|
|
71
|
-
summary: "The agent's output reaches something that runs it: a shell, an interpreter, a parser."
|
|
72
|
-
},
|
|
73
|
-
ASI06: {
|
|
74
|
-
name: "Memory and Context Poisoning",
|
|
75
|
-
summary: "Corrupted memory or retrieved context persists across sessions and steers future reasoning."
|
|
76
|
-
},
|
|
77
|
-
ASI07: {
|
|
78
|
-
name: "Insecure Inter-Agent Communication",
|
|
79
|
-
summary: "One agent trusts another's messages by default, so a compromised agent can steer its peers."
|
|
80
|
-
},
|
|
81
|
-
ASI08: {
|
|
82
|
-
name: "Cascading Failures",
|
|
83
|
-
summary: "A single fault propagates across agents, tools and workflows into system-wide impact."
|
|
84
|
-
},
|
|
85
|
-
ASI09: {
|
|
86
|
-
name: "Human-Agent Trust Exploitation",
|
|
87
|
-
summary: "Persuasive or misleading agent output moves a person to an unsafe action or approval."
|
|
88
|
-
},
|
|
89
|
-
ASI10: {
|
|
90
|
-
name: "Rogue Agents",
|
|
91
|
-
summary: "A compromised, misaligned or drifting agent keeps operating in unintended ways."
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Every framework control a rule cites, resolved to names.
|
|
98
|
-
*
|
|
99
|
-
* A pack that parsed cites only ids the registry knows, so the lookups here
|
|
100
|
-
* cannot miss; the fallbacks exist for a `Rule` built by hand in code.
|
|
101
|
-
*/
|
|
102
|
-
function cite(rule) {
|
|
103
|
-
const out = [];
|
|
104
|
-
for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
|
|
105
|
-
const known = FRAMEWORKS[framework];
|
|
106
|
-
for (const control of controls) out.push({
|
|
107
|
-
framework,
|
|
108
|
-
frameworkName: known?.name ?? framework,
|
|
109
|
-
frameworkShort: known?.short ?? framework,
|
|
110
|
-
control,
|
|
111
|
-
controlName: known?.controls[control]?.name ?? control
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region ../tokens/src/mark.ts
|
|
119
|
-
/**
|
|
120
|
-
* The mark — Guardians — as code.
|
|
121
|
-
*
|
|
122
|
-
* Four corner brackets around a core that none of them touch: the Guardians
|
|
123
|
-
* PRAE assembles around an agent, and the agent, green because what is
|
|
124
|
-
* inside has been verified. The console, the site and the CLI draw it from
|
|
125
|
-
* here, so there is one geometry and no copies to drift. `brand/` holds the
|
|
126
|
-
* same shapes as files, for places that need a file.
|
|
127
|
-
*
|
|
128
|
-
* Grid 32u. Arms 11u long, 5u thick. Core 8u, centred, 5u clear. Radius 0.
|
|
129
|
-
*/
|
|
130
|
-
const MARK = {
|
|
131
|
-
viewBox: "0 0 32 32",
|
|
132
|
-
brackets: "M2 2H13V7H7V13H2ZM19 2H30V13H25V7H19ZM2 19H7V25H13V30H2ZM25 19H30V30H19V25H25Z",
|
|
133
|
-
corners: [
|
|
134
|
-
{
|
|
135
|
-
d: "M2 2H13V7H7V13H2Z",
|
|
136
|
-
dx: -14,
|
|
137
|
-
dy: -14
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
d: "M19 2H30V13H25V7H19Z",
|
|
141
|
-
dx: 14,
|
|
142
|
-
dy: -14
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
d: "M25 19H30V30H19V25H25Z",
|
|
146
|
-
dx: 14,
|
|
147
|
-
dy: 14
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
d: "M2 19H7V25H13V30H2Z",
|
|
151
|
-
dx: -14,
|
|
152
|
-
dy: 14
|
|
153
|
-
}
|
|
154
|
-
],
|
|
155
|
-
core: {
|
|
156
|
-
x: 12,
|
|
157
|
-
y: 12,
|
|
158
|
-
width: 8,
|
|
159
|
-
height: 8
|
|
160
|
-
},
|
|
161
|
-
colour: {
|
|
162
|
-
graphite: "#0E1114",
|
|
163
|
-
ink: "#FFFFFF",
|
|
164
|
-
signal: "#98ED49",
|
|
165
|
-
signalOnLight: "#0C6800"
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
/** The inner drawing: brackets and, unless absent, the core. */
|
|
169
|
-
function markShapes({ surface = "graphite", core = "verified", mono = false } = {}) {
|
|
170
|
-
const ink = surface === "graphite" ? MARK.colour.ink : MARK.colour.graphite;
|
|
171
|
-
const green = surface === "graphite" ? MARK.colour.signal : MARK.colour.signalOnLight;
|
|
172
|
-
const coreFill = mono || core === "unverified" ? ink : green;
|
|
173
|
-
const { x, y, width, height } = MARK.core;
|
|
174
|
-
const rect = core === "absent" ? "" : `<rect class="prae-core" x="${x}" y="${y}" width="${width}" height="${height}" fill="${coreFill}"/>`;
|
|
175
|
-
return `${MARK.corners.map((c, i) => `<path class="prae-corner" style="--i:${i};--dx:${c.dx};--dy:${c.dy}" d="${c.d}" fill="${ink}"/>`).join("")}${rect}`;
|
|
176
|
-
}
|
|
177
|
-
/** A complete SVG document of the mark. */
|
|
178
|
-
function markSvg(options = {}) {
|
|
179
|
-
const size = options.size ?? 32;
|
|
180
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${MARK.viewBox}" width="${size}" height="${size}">${markShapes(options)}</svg>`;
|
|
181
|
-
}
|
|
182
|
-
/** The mark as a data URI, for an `<img>` that must not fetch anything. */
|
|
183
|
-
function markDataUri(options = {}) {
|
|
184
|
-
return `data:image/svg+xml;utf8,${encodeURIComponent(markSvg(options))}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
//#endregion
|
|
188
|
-
//#region ../tokens/src/palette.ts
|
|
189
|
-
/**
|
|
190
|
-
* The two grounds, as declaration blocks.
|
|
191
|
-
*
|
|
192
|
-
* The console renders twice from one design: `prae console --out` inlines its
|
|
193
|
-
* stylesheet into standalone documents, and the web app links a real one. Both
|
|
194
|
-
* need these values, so they are defined here once and the test in this
|
|
195
|
-
* package asserts the stylesheet agrees — the same guard the rest of the
|
|
196
|
-
* tokens already have, for the same reason.
|
|
197
|
-
*
|
|
198
|
-
* Names are unprefixed because these are consumed as a surface's own palette
|
|
199
|
-
* rather than alongside another product's. `tokens.css` carries the prefixed
|
|
200
|
-
* `--prae-*` set for anyone embedding PRAE inside their own page.
|
|
201
|
-
*/
|
|
202
|
-
/** §06 — Surface B, the ledger, on paper. Printable and high-contrast. */
|
|
203
|
-
const PAPER = ` --signal: oklch(0.45 0.16 140);
|
|
204
|
-
--signal-bright: oklch(0.5 0.16 140);
|
|
205
|
-
--signal-edge: oklch(0.5 0.13 140);
|
|
206
|
-
--signal-text: oklch(0.42 0.14 140);
|
|
207
|
-
--signal-on: oklch(0.985 0.003 100);
|
|
208
|
-
--hold: oklch(0.55 0.15 55);
|
|
209
|
-
--veto: oklch(0.52 0.19 25);
|
|
210
|
-
--veto-text: oklch(0.5 0.19 25);
|
|
211
|
-
--veto-edge: oklch(0.62 0.17 25);
|
|
212
|
-
|
|
213
|
-
--ground: oklch(0.975 0.003 100);
|
|
214
|
-
--raised: oklch(0.99 0.002 100);
|
|
215
|
-
--sunk: oklch(0.955 0.004 100);
|
|
216
|
-
--active: oklch(0.955 0.006 130);
|
|
217
|
-
--line: oklch(0.88 0.004 100);
|
|
218
|
-
--line-soft: oklch(0.92 0.004 100);
|
|
219
|
-
--line-faint: oklch(0.93 0.004 100);
|
|
220
|
-
--line-card: oklch(0.86 0.005 100);
|
|
221
|
-
--line-strong: oklch(0.8 0.006 100);
|
|
222
|
-
--edge: oklch(0.32 0.01 255);
|
|
223
|
-
--ink: oklch(0.22 0.01 255);
|
|
224
|
-
--ink-row: oklch(0.24 0.01 255);
|
|
225
|
-
--secondary: oklch(0.4 0.01 255);
|
|
226
|
-
--muted: oklch(0.5 0.01 255);
|
|
227
|
-
--muted-soft: oklch(0.55 0.01 255);
|
|
228
|
-
--dim: oklch(0.45 0.01 255);
|
|
229
|
-
--seal: oklch(0.6 0.01 255);
|
|
230
|
-
|
|
231
|
-
--mark: none;`;
|
|
232
|
-
/** §02 — the graphite ramp and the neon signal it is drawn for. */
|
|
233
|
-
const FENCE = ` --signal: oklch(0.86 0.21 133);
|
|
234
|
-
--signal-bright: oklch(0.95 0.15 133);
|
|
235
|
-
--signal-edge: oklch(0.4 0.06 133);
|
|
236
|
-
--signal-text: oklch(0.86 0.21 133);
|
|
237
|
-
--signal-on: oklch(0.155 0.008 255);
|
|
238
|
-
--hold: oklch(0.83 0.16 78);
|
|
239
|
-
--veto: oklch(0.66 0.2 25);
|
|
240
|
-
--veto-text: oklch(0.75 0.16 25);
|
|
241
|
-
--veto-edge: oklch(0.45 0.12 25);
|
|
242
|
-
|
|
243
|
-
--ground: oklch(0.155 0.008 255);
|
|
244
|
-
--raised: oklch(0.185 0.009 255);
|
|
245
|
-
--sunk: oklch(0.135 0.008 255);
|
|
246
|
-
--active: oklch(0.215 0.022 133);
|
|
247
|
-
--line: oklch(0.28 0.01 255);
|
|
248
|
-
--line-soft: oklch(0.26 0.01 255);
|
|
249
|
-
--line-faint: oklch(0.235 0.01 255);
|
|
250
|
-
--line-card: oklch(0.3 0.01 255);
|
|
251
|
-
--line-strong: oklch(0.32 0.01 255);
|
|
252
|
-
--edge: oklch(0.72 0.01 255);
|
|
253
|
-
--ink: oklch(0.96 0.004 255);
|
|
254
|
-
--ink-row: oklch(0.9 0.006 255);
|
|
255
|
-
--secondary: oklch(0.72 0.01 255);
|
|
256
|
-
--muted: oklch(0.6 0.012 255);
|
|
257
|
-
/* Lands where paper's does, at AA on its own ground: 0.55 measures 4.03
|
|
258
|
-
against graphite, and this token labels pending and absent states that
|
|
259
|
-
still have to be read. */
|
|
260
|
-
--muted-soft: oklch(0.575 0.012 255);
|
|
261
|
-
--dim: oklch(0.62 0.012 255);
|
|
262
|
-
--seal: oklch(0.58 0.012 255);
|
|
263
|
-
|
|
264
|
-
/* The mark is a solid black glyph, so it disappears on the graphite ground.
|
|
265
|
-
Inverting it yields the white it would have been drawn as. */
|
|
266
|
-
--mark: invert(1);`;
|
|
267
|
-
|
|
268
|
-
//#endregion
|
|
269
|
-
//#region ../tokens/src/views.ts
|
|
270
|
-
/**
|
|
271
|
-
* The console's view styles, as one definition.
|
|
272
|
-
*
|
|
273
|
-
* Header, stat band, ledger grid, rule and mapping tables, the awaiting-human
|
|
274
|
-
* panel, and the limits block — everything that makes a PRAE view look like a
|
|
275
|
-
* PRAE view rather than a generic table.
|
|
276
|
-
*
|
|
277
|
-
* Both renderers need these. `prae console --out` inlines them into standalone
|
|
278
|
-
* documents; the web app injects them into a global style tag. Exported as a
|
|
279
|
-
* string rather than a stylesheet so there is exactly one copy: a `.css` file
|
|
280
|
-
* plus a mirrored constant would be two, and two copies of a stylesheet drift
|
|
281
|
-
* the same way two copies of a palette do.
|
|
282
|
-
*
|
|
283
|
-
* What is *not* here is layout — the rail, the scroll containment, the routing.
|
|
284
|
-
* The app has a persistent frame and the exporter produces separate documents,
|
|
285
|
-
* and that is the one thing the two are genuinely allowed to disagree about.
|
|
286
|
-
*/
|
|
287
|
-
const VIEWS_CSS = `/* ── header ───────────────────────────────────────────────────────────── */
|
|
288
|
-
.head {
|
|
289
|
-
display: flex; align-items: flex-end; justify-content: space-between;
|
|
290
|
-
gap: 24px; padding: 26px 30px 22px; border-bottom: 1px solid var(--line);
|
|
291
|
-
}
|
|
292
|
-
.head h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.03em; margin: 0; }
|
|
293
|
-
.head .meta {
|
|
294
|
-
font-family: var(--proof); font-size: 11.5px; color: var(--muted); margin-top: 6px;
|
|
295
|
-
}
|
|
296
|
-
.actions { display: flex; align-items: center; gap: 10px; }
|
|
297
|
-
.chain {
|
|
298
|
-
display: flex; align-items: center; gap: 8px; padding: 7px 12px;
|
|
299
|
-
border: 1px solid var(--signal-edge); border-radius: 999px;
|
|
300
|
-
font-family: var(--proof); font-size: 10.5px; letter-spacing: 0.12em;
|
|
301
|
-
color: var(--signal-text); white-space: nowrap;
|
|
302
|
-
}
|
|
303
|
-
.chain i { width: 6px; height: 6px; background: var(--signal-bright); border-radius: 999px; }
|
|
304
|
-
.chain.broken { border-color: var(--veto-edge); color: var(--veto-text); }
|
|
305
|
-
.chain.broken i { background: var(--veto); }
|
|
306
|
-
.btn {
|
|
307
|
-
padding: 9px 16px; border: 1px solid var(--line-strong); font-size: 13.5px;
|
|
308
|
-
border-radius: 2px; background: none; color: inherit; font-family: inherit;
|
|
309
|
-
white-space: nowrap;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/* ── stat band ────────────────────────────────────────────────────────── */
|
|
313
|
-
.stats { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--line); }
|
|
314
|
-
.stat { padding: 20px 24px; border-right: 1px solid var(--line-soft); }
|
|
315
|
-
.stat:first-child { padding-left: 30px; }
|
|
316
|
-
.stat:last-child { border-right: none; }
|
|
317
|
-
.stat .v {
|
|
318
|
-
font-size: 28px; font-weight: 700; letter-spacing: -0.03em;
|
|
319
|
-
font-variant-numeric: tabular-nums;
|
|
320
|
-
}
|
|
321
|
-
.stat .v.veto { color: var(--veto); }
|
|
322
|
-
.stat .v.hold { color: var(--hold); }
|
|
323
|
-
.stat .k {
|
|
324
|
-
font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
|
|
325
|
-
color: var(--muted); margin-top: 4px;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/* ── ledger table ─────────────────────────────────────────────────────── */
|
|
329
|
-
.grid {
|
|
330
|
-
display: grid;
|
|
331
|
-
grid-template-columns: 96px 1fr 150px 130px 104px 120px;
|
|
332
|
-
gap: 16px;
|
|
333
|
-
}
|
|
334
|
-
.thead {
|
|
335
|
-
padding: 12px 30px; font-family: var(--proof); font-size: 10px;
|
|
336
|
-
letter-spacing: 0.14em; color: var(--muted); border-bottom: 1px solid var(--line);
|
|
337
|
-
}
|
|
338
|
-
.row {
|
|
339
|
-
padding: 14px 30px; font-family: var(--proof); font-size: 12px;
|
|
340
|
-
border-bottom: 1px solid var(--line-faint); align-items: center;
|
|
341
|
-
}
|
|
342
|
-
.row .seq { color: var(--muted-soft); font-variant-numeric: tabular-nums; }
|
|
343
|
-
.row .call { color: var(--ink-row); overflow-wrap: anywhere; }
|
|
344
|
-
.row .actor, .row .clause { color: var(--dim); }
|
|
345
|
-
.row .seal { color: var(--seal); }
|
|
346
|
-
.row .verdict { letter-spacing: 0.1em; }
|
|
347
|
-
.v-allow { color: var(--signal-text); }
|
|
348
|
-
.v-deny { color: var(--veto-text); }
|
|
349
|
-
.v-ask { color: var(--hold); }
|
|
350
|
-
.v-correct { color: var(--muted); }
|
|
351
|
-
.empty { padding: 30px; font-family: var(--proof); font-size: 12px; color: var(--muted); }
|
|
352
|
-
|
|
353
|
-
/* ── awaiting-human panel ─────────────────────────────────────────────── */
|
|
354
|
-
.await { padding: 26px 30px 30px; background: var(--sunk); border-top: 1px solid var(--line); }
|
|
355
|
-
.await .k {
|
|
356
|
-
font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
|
|
357
|
-
color: var(--muted); margin-bottom: 16px;
|
|
358
|
-
}
|
|
359
|
-
.card {
|
|
360
|
-
display: flex; gap: 24px; align-items: flex-start; background: var(--raised);
|
|
361
|
-
border: 1px solid var(--line-card); padding: 22px 24px;
|
|
362
|
-
}
|
|
363
|
-
.card h2 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
|
|
364
|
-
.card .detail { font-family: var(--proof); font-size: 12px; color: var(--muted); line-height: 1.8; }
|
|
365
|
-
.decide { display: flex; flex-direction: column; gap: 8px; width: 200px; flex: none; }
|
|
366
|
-
.approve {
|
|
367
|
-
padding: 11px 18px; background: var(--signal); color: var(--signal-on);
|
|
368
|
-
font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
|
|
369
|
-
border: none; font-family: inherit;
|
|
370
|
-
}
|
|
371
|
-
.deny {
|
|
372
|
-
padding: 11px 18px; border: 1px solid var(--veto-edge); color: var(--veto-text);
|
|
373
|
-
font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
|
|
374
|
-
background: none; font-family: inherit;
|
|
375
|
-
}
|
|
376
|
-
/*
|
|
377
|
-
* A control that cannot act must not look like one that can. These render so
|
|
378
|
-
* the reader knows where the decision lives, and read as inert so they never
|
|
379
|
-
* reach for it — the harness settles approvals, and a button that looks live
|
|
380
|
-
* here would be an invitation to a second audit trail.
|
|
381
|
-
*
|
|
382
|
-
* The fill is dropped rather than faded. Fading a filled button works against
|
|
383
|
-
* paper and fails against graphite, where a dimmed green block still reads as
|
|
384
|
-
* a live primary action; an outline reads the same on either ground.
|
|
385
|
-
*/
|
|
386
|
-
.approve[disabled], .deny[disabled] {
|
|
387
|
-
background: none; border: 1px solid var(--line-strong);
|
|
388
|
-
color: var(--muted-soft); font-weight: 500; cursor: not-allowed;
|
|
389
|
-
}
|
|
390
|
-
.recorded {
|
|
391
|
-
font-family: var(--proof); font-size: 10px; color: var(--muted-soft);
|
|
392
|
-
text-align: center; letter-spacing: 0.08em;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/* ── limits ───────────────────────────────────────────────────────────── */
|
|
396
|
-
.limits { padding: 22px 30px 28px; border-top: 1px solid var(--line); }
|
|
397
|
-
.limits .k {
|
|
398
|
-
font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
|
|
399
|
-
color: var(--muted); margin-bottom: 10px;
|
|
400
|
-
}
|
|
401
|
-
.limits ul { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; color: var(--secondary); }
|
|
402
|
-
.limits code { font-family: var(--proof); font-size: 12px; }
|
|
403
|
-
|
|
404
|
-
/* ── rule / mapping tables, shared by the packs and compliance views ──── */
|
|
405
|
-
.rules { display: grid; grid-template-columns: 110px 1fr 96px; gap: 16px; }
|
|
406
|
-
.map { display: grid; grid-template-columns: 210px 1fr 96px; gap: 16px; }
|
|
407
|
-
.sect { padding: 22px 30px 8px; }
|
|
408
|
-
.sect h2 {
|
|
409
|
-
font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 4px;
|
|
410
|
-
}
|
|
411
|
-
.sect p {
|
|
412
|
-
margin: 0 0 4px; font-size: 14px; line-height: 1.6; color: var(--secondary);
|
|
413
|
-
max-width: 76ch;
|
|
414
|
-
}
|
|
415
|
-
.caps { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 30px 22px; }
|
|
416
|
-
.cap {
|
|
417
|
-
font-family: var(--proof); font-size: 12px; padding: 6px 10px;
|
|
418
|
-
border: 1px solid var(--signal-edge); color: var(--signal-text);
|
|
419
|
-
border-radius: 2px;
|
|
420
|
-
}
|
|
421
|
-
.cap.off {
|
|
422
|
-
border-color: var(--line-strong); color: var(--muted-soft);
|
|
423
|
-
text-decoration: line-through;
|
|
424
|
-
}
|
|
425
|
-
.effect { letter-spacing: 0.1em; font-size: 11px; }
|
|
426
|
-
.e-deny { color: var(--veto-text); }
|
|
427
|
-
.e-ask { color: var(--hold); }
|
|
428
|
-
.e-warn { color: var(--muted); }
|
|
429
|
-
.e-correct { color: var(--signal-text); }
|
|
430
|
-
.covered { color: var(--signal-text); }
|
|
431
|
-
.partial { color: var(--hold); }
|
|
432
|
-
.absent { color: var(--muted-soft); }
|
|
433
|
-
|
|
434
|
-
button { cursor: pointer; }
|
|
435
|
-
a:focus-visible, button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
|
|
436
|
-
`;
|
|
437
|
-
|
|
438
|
-
//#endregion
|
|
439
|
-
//#region ../tokens/src/index.ts
|
|
440
|
-
/** The Google Fonts stylesheet both surfaces link. */
|
|
441
|
-
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";
|
|
442
|
-
/**
|
|
443
|
-
* Verdict chip labels. Uppercase, mono, and never rendered without a clause
|
|
444
|
-
* reference within 8px — §04 is explicit that a chip alone is not allowed.
|
|
445
|
-
*/
|
|
446
|
-
const VERDICT_LABEL = {
|
|
447
|
-
allow: "ALLOWED",
|
|
448
|
-
ask: "HELD",
|
|
449
|
-
deny: "DENIED",
|
|
450
|
-
correct: "CORRECTED"
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
//#endregion
|
|
454
|
-
//#region src/console/logo.ts
|
|
455
|
-
const LOGO = markDataUri({ size: 26 });
|
|
456
|
-
|
|
457
|
-
//#endregion
|
|
458
|
-
//#region src/console/shell.ts
|
|
459
|
-
function esc(value) {
|
|
460
|
-
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
461
|
-
}
|
|
462
|
-
/** The stylesheet both console views share. Tokens first, then the surface. */
|
|
463
|
-
/** The stylesheet both console views share. Tokens first, then the surface. */
|
|
464
|
-
const SHEET = `
|
|
465
|
-
:root {
|
|
466
|
-
${PAPER}
|
|
467
|
-
--claim: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
|
|
468
|
-
--proof: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
|
469
|
-
--ease: cubic-bezier(0.2, 0, 0, 1);
|
|
470
|
-
--surface-motion: 180ms;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/* An explicit choice, remembered. */
|
|
474
|
-
:root[data-theme='dark'] {
|
|
475
|
-
${FENCE}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/* No choice made: follow the machine, unless the reader has said light. */
|
|
479
|
-
@media (prefers-color-scheme: dark) {
|
|
480
|
-
:root:not([data-theme='light']) {
|
|
481
|
-
${FENCE}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/*
|
|
486
|
-
* The ground moves at the surface budget, and only while it is moving.
|
|
487
|
-
*
|
|
488
|
-
* Arming this permanently would put a 180ms fade on every hover and every
|
|
489
|
-
* verdict that renders, so the class goes on for one toggle and comes off.
|
|
490
|
-
* §01's 0ms rule is about a decision animating in; nothing is deciding here,
|
|
491
|
-
* the page is repainting, and a verdict that snapped while the page behind it
|
|
492
|
-
* faded would read as a glitch.
|
|
493
|
-
*/
|
|
494
|
-
html.theming, html.theming * {
|
|
495
|
-
transition:
|
|
496
|
-
background-color var(--surface-motion) var(--ease),
|
|
497
|
-
border-color var(--surface-motion) var(--ease),
|
|
498
|
-
color var(--surface-motion) var(--ease),
|
|
499
|
-
fill var(--surface-motion) var(--ease) !important;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
* { box-sizing: border-box; }
|
|
503
|
-
|
|
504
|
-
/*
|
|
505
|
-
* An application, not a document: the shell fills the viewport and only the
|
|
506
|
-
* content column scrolls, so the rail and the header stay where a reader left
|
|
507
|
-
* them. Print undoes all of it further down — the same page still has to
|
|
508
|
-
* flatten into evidence.
|
|
509
|
-
*/
|
|
510
|
-
html, body { height: 100%; }
|
|
511
|
-
body {
|
|
512
|
-
margin: 0;
|
|
513
|
-
background: var(--ground);
|
|
514
|
-
color: var(--ink);
|
|
515
|
-
font-family: var(--claim);
|
|
516
|
-
-webkit-font-smoothing: antialiased;
|
|
517
|
-
overflow: hidden;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
.wrap { height: 100%; }
|
|
521
|
-
.scroll { height: 100%; }
|
|
522
|
-
|
|
523
|
-
.console {
|
|
524
|
-
background: var(--ground);
|
|
525
|
-
display: grid;
|
|
526
|
-
grid-template-columns: 232px 1fr;
|
|
527
|
-
height: 100%;
|
|
528
|
-
min-height: 0;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/* The one column that moves. */
|
|
532
|
-
.console > main {
|
|
533
|
-
overflow-y: auto;
|
|
534
|
-
overflow-x: auto;
|
|
535
|
-
min-height: 0;
|
|
536
|
-
scroll-behavior: smooth;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/* ── rail ─────────────────────────────────────────────────────────────── */
|
|
540
|
-
.rail {
|
|
541
|
-
border-right: 1px solid var(--line); padding: 22px 0;
|
|
542
|
-
overflow-y: auto; min-height: 0;
|
|
543
|
-
display: flex; flex-direction: column;
|
|
544
|
-
}
|
|
545
|
-
.rail .compliance { margin-top: auto; }
|
|
546
|
-
|
|
547
|
-
/*
|
|
548
|
-
* Two positions, named, rather than a switch with a state you have to infer.
|
|
549
|
-
* Hidden until the head script has run: without scripting it would be a
|
|
550
|
-
* control that does nothing.
|
|
551
|
-
*/
|
|
552
|
-
.ground { display: none; margin: auto 20px 14px; }
|
|
553
|
-
html.js .ground { display: grid; grid-template-columns: 1fr 1fr; }
|
|
554
|
-
html.js .rail .compliance { margin-top: 0; }
|
|
555
|
-
.ground button {
|
|
556
|
-
font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
|
|
557
|
-
padding: 6px 0; background: none; color: var(--muted);
|
|
558
|
-
border: 1px solid var(--line); border-radius: 0;
|
|
559
|
-
transition: color 120ms var(--ease), border-color 120ms var(--ease);
|
|
560
|
-
}
|
|
561
|
-
.ground button:first-child { border-radius: 2px 0 0 2px; border-right: none; }
|
|
562
|
-
.ground button:last-child { border-radius: 0 2px 2px 0; }
|
|
563
|
-
.ground button:hover { color: var(--secondary); }
|
|
564
|
-
:root:not([data-theme='dark']) .ground [data-ground='paper'],
|
|
565
|
-
:root[data-theme='dark'] .ground [data-ground='fence'] {
|
|
566
|
-
color: var(--ink); border-color: var(--line-strong); background: var(--active);
|
|
567
|
-
}
|
|
568
|
-
.brand { display: flex; align-items: center; gap: 10px; padding: 0 20px 22px; }
|
|
569
|
-
/* The canvas sizes the mark at 26px; the glyph is square and needs no frame. */
|
|
570
|
-
.mark { width: 26px; height: 26px; display: block; filter: var(--mark); }
|
|
571
|
-
.brand b {
|
|
572
|
-
font-weight: 700; font-size: 16px; letter-spacing: 0.04em;
|
|
573
|
-
}
|
|
574
|
-
.eyebrow {
|
|
575
|
-
font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
|
|
576
|
-
color: var(--muted-soft); padding: 0 20px 10px;
|
|
577
|
-
}
|
|
578
|
-
.nav { display: grid; }
|
|
579
|
-
.nav a, .nav span {
|
|
580
|
-
padding: 10px 20px; font-size: 14.5px; color: var(--secondary);
|
|
581
|
-
text-decoration: none; display: flex; justify-content: space-between;
|
|
582
|
-
align-items: center;
|
|
583
|
-
}
|
|
584
|
-
.nav a { transition: background var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)),
|
|
585
|
-
color var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)); }
|
|
586
|
-
.nav a:hover { background: var(--sunk); }
|
|
587
|
-
|
|
588
|
-
/*
|
|
589
|
-
* The surface transition, spent where the language says to spend it. Verdicts
|
|
590
|
-
* are deliberately excluded: a decision appears, it never animates in.
|
|
591
|
-
*/
|
|
592
|
-
main > * { animation: surface 180ms cubic-bezier(0.2, 0, 0, 1) both; }
|
|
593
|
-
@keyframes surface {
|
|
594
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
595
|
-
to { opacity: 1; transform: none; }
|
|
596
|
-
}
|
|
597
|
-
main.leaving { opacity: 0; transition: opacity 120ms cubic-bezier(0.2, 0, 0, 1); }
|
|
598
|
-
@media (prefers-reduced-motion: reduce) {
|
|
599
|
-
main > *, main.leaving { animation: none; transition: none; }
|
|
600
|
-
.console > main { scroll-behavior: auto; }
|
|
601
|
-
:root { --surface-motion: 0ms; }
|
|
602
|
-
}
|
|
603
|
-
.nav .pending { color: var(--muted-soft); }
|
|
604
|
-
.nav .badge.soon {
|
|
605
|
-
font-size: 9px; letter-spacing: 0.14em; color: var(--muted-soft);
|
|
606
|
-
border: 1px solid var(--line); border-radius: 999px; padding: 1px 6px;
|
|
607
|
-
}
|
|
608
|
-
.nav a.on {
|
|
609
|
-
font-weight: 600; color: var(--ink);
|
|
610
|
-
border-left: 2px solid var(--signal-bright);
|
|
611
|
-
background: var(--active); margin-left: -1px;
|
|
612
|
-
}
|
|
613
|
-
.nav .badge { font-family: var(--proof); font-size: 11px; color: var(--hold); }
|
|
614
|
-
.compliance {
|
|
615
|
-
margin: 22px 20px 0; padding-top: 18px; border-top: 1px solid var(--line-soft);
|
|
616
|
-
font-family: var(--proof); font-size: 10.5px; line-height: 1.9; color: var(--muted);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
${VIEWS_CSS}
|
|
620
|
-
|
|
621
|
-
/* Print is a first-class state, not an afterthought. */
|
|
622
|
-
@media print {
|
|
623
|
-
@page { margin: 12mm; }
|
|
624
|
-
/* Evidence prints on paper. The fence has no printed form. */
|
|
625
|
-
:root, :root[data-theme='dark'] {
|
|
626
|
-
${PAPER}
|
|
627
|
-
}
|
|
628
|
-
html.theming, html.theming * { transition: none !important; }
|
|
629
|
-
html, body { height: auto; overflow: visible; }
|
|
630
|
-
body { padding: 0; background: #fff; }
|
|
631
|
-
.wrap, .scroll { height: auto; }
|
|
632
|
-
.console > main { overflow: visible; height: auto; }
|
|
633
|
-
main > * { animation: none; }
|
|
634
|
-
.console {
|
|
635
|
-
border: none; grid-template-columns: 1fr; min-width: 0;
|
|
636
|
-
height: auto; display: block;
|
|
637
|
-
}
|
|
638
|
-
.rail, .actions, .decide, .ground { display: none; }
|
|
639
|
-
.thead { display: grid; }
|
|
640
|
-
thead { display: table-header-group; }
|
|
641
|
-
.row { break-inside: avoid; }
|
|
642
|
-
/* A chip is colour; a printed page may not be. Keep the word. */
|
|
643
|
-
.chain { border-color: currentColor !important; }
|
|
644
|
-
.stats { break-inside: avoid; }
|
|
645
|
-
}
|
|
646
|
-
`;
|
|
647
|
-
/**
|
|
648
|
-
* Resolve the ground before the first paint.
|
|
649
|
-
*
|
|
650
|
-
* This runs in `<head>`, synchronously, on purpose: read the choice after the
|
|
651
|
-
* body has painted and the reader watches the page flash the wrong palette on
|
|
652
|
-
* every navigation. It also marks the document as scripted, which is what
|
|
653
|
-
* reveals the toggle — a control that cannot work should not be offered.
|
|
654
|
-
*/
|
|
655
|
-
const GROUND_SCRIPT = `
|
|
656
|
-
(() => {
|
|
657
|
-
const root = document.documentElement;
|
|
658
|
-
root.classList.add('js');
|
|
659
|
-
let choice = null;
|
|
660
|
-
try { choice = localStorage.getItem('prae-ground'); } catch (e) {}
|
|
661
|
-
if (choice !== 'dark' && choice !== 'light') {
|
|
662
|
-
choice = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
663
|
-
}
|
|
664
|
-
root.dataset.theme = choice;
|
|
665
|
-
})();
|
|
666
|
-
`;
|
|
667
|
-
/**
|
|
668
|
-
* Client-side navigation, as progressive enhancement.
|
|
669
|
-
*
|
|
670
|
-
* Every view is still a complete document that opens, prints and mails on its
|
|
671
|
-
* own — that requirement has not moved. This only intercepts a click when the
|
|
672
|
-
* page is being served, swaps the main column, and leaves the rail alone, so
|
|
673
|
-
* the frame stays put and the surface transition the language budgets actually
|
|
674
|
-
* has somewhere to happen. With scripting off, the links are ordinary links.
|
|
675
|
-
*/
|
|
676
|
-
const NAV_SCRIPT = `
|
|
677
|
-
(() => {
|
|
678
|
-
const main = () => document.querySelector('main');
|
|
679
|
-
const rail = () => document.querySelector('.rail');
|
|
680
|
-
if (!main() || !window.fetch) return;
|
|
681
|
-
|
|
682
|
-
const swap = async (href, push) => {
|
|
683
|
-
const target = main();
|
|
684
|
-
target.classList.add('leaving');
|
|
685
|
-
try {
|
|
686
|
-
const res = await fetch(href, { headers: { accept: 'text/html' } });
|
|
687
|
-
if (!res.ok) throw new Error(String(res.status));
|
|
688
|
-
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
|
|
689
|
-
const next = doc.querySelector('main');
|
|
690
|
-
const nextRail = doc.querySelector('.rail');
|
|
691
|
-
if (!next) throw new Error('no main');
|
|
692
|
-
|
|
693
|
-
// Collect the view's scripts BEFORE moving it: replaceWith detaches the
|
|
694
|
-
// element from the parsed document, so querying that document afterwards
|
|
695
|
-
// finds nothing and the view arrives inert. A parser-created script never
|
|
696
|
-
// executes on insertion either, which is why each is re-created below.
|
|
697
|
-
const scripts = [...next.querySelectorAll('script')].map((s) => s.textContent);
|
|
698
|
-
for (const s of next.querySelectorAll('script')) s.remove();
|
|
699
|
-
|
|
700
|
-
target.replaceWith(next);
|
|
701
|
-
// Only the rail's active marker changes; the element itself stays, so
|
|
702
|
-
// nothing in it flickers or loses scroll position.
|
|
703
|
-
if (nextRail) rail().innerHTML = nextRail.innerHTML;
|
|
704
|
-
if (doc.title) document.title = doc.title;
|
|
705
|
-
if (push) history.pushState({}, '', href);
|
|
706
|
-
next.scrollTop = 0;
|
|
707
|
-
wire();
|
|
708
|
-
for (const code of scripts) {
|
|
709
|
-
const el = document.createElement('script');
|
|
710
|
-
el.textContent = code;
|
|
711
|
-
document.body.appendChild(el);
|
|
712
|
-
}
|
|
713
|
-
} catch {
|
|
714
|
-
// A failed swap falls back to a real navigation rather than stranding
|
|
715
|
-
// the reader on a half-updated page.
|
|
716
|
-
location.href = href;
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
|
|
720
|
-
const wire = () => {
|
|
721
|
-
for (const a of document.querySelectorAll('.nav a[href$=".html"]')) {
|
|
722
|
-
a.addEventListener('click', (e) => {
|
|
723
|
-
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
|
724
|
-
e.preventDefault();
|
|
725
|
-
swap(a.getAttribute('href'), true);
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
// Delegated, because a client-side swap replaces the rail's markup and a
|
|
731
|
-
// handler bound to the old buttons would go with it.
|
|
732
|
-
let settle;
|
|
733
|
-
document.addEventListener('click', (e) => {
|
|
734
|
-
const button = e.target.closest('[data-ground]');
|
|
735
|
-
if (!button) return;
|
|
736
|
-
const next = button.dataset.ground === 'fence' ? 'dark' : 'light';
|
|
737
|
-
const root = document.documentElement;
|
|
738
|
-
if (root.dataset.theme === next) return;
|
|
739
|
-
|
|
740
|
-
// Arm the transition, let the current palette paint once, then move. Doing
|
|
741
|
-
// both in one frame gives the browser no 'from' state and it jumps.
|
|
742
|
-
root.classList.add('theming');
|
|
743
|
-
void root.offsetHeight;
|
|
744
|
-
root.dataset.theme = next;
|
|
745
|
-
try { localStorage.setItem('prae-ground', next); } catch (err) {}
|
|
746
|
-
clearTimeout(settle);
|
|
747
|
-
settle = setTimeout(() => root.classList.remove('theming'), 240);
|
|
748
|
-
});
|
|
749
|
-
|
|
750
|
-
addEventListener('popstate', () => swap(location.pathname, false));
|
|
751
|
-
wire();
|
|
752
|
-
})();
|
|
753
|
-
`;
|
|
754
|
-
/** Wrap a view in the console shell: rail, then the view's own header and body. */
|
|
755
|
-
function renderShell(page) {
|
|
756
|
-
return `<!doctype html>
|
|
757
|
-
<html lang="en" data-prae="ledger"><head>
|
|
758
|
-
<meta charset="utf-8">
|
|
759
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
760
|
-
<title>${esc(page.title)}</title>
|
|
761
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
762
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
763
|
-
<link href="${FONT_HREF}" rel="stylesheet">
|
|
764
|
-
<script>${GROUND_SCRIPT}<\/script>
|
|
765
|
-
<style>${SHEET}</style>
|
|
766
|
-
</head><body>
|
|
767
|
-
<div class="wrap"><div class="scroll">
|
|
768
|
-
<div class="console">
|
|
769
|
-
<nav class="rail">
|
|
770
|
-
<div class="brand"><img class="mark" src="${LOGO}" alt=""><b>PRAE</b></div>
|
|
771
|
-
<div class="eyebrow">CONTROL PLANE</div>
|
|
772
|
-
${page.nav}
|
|
773
|
-
<div class="ground" role="group" aria-label="Ground">
|
|
774
|
-
<button type="button" data-ground="paper">PAPER</button>
|
|
775
|
-
<button type="button" data-ground="fence">FENCE</button>
|
|
776
|
-
</div>
|
|
777
|
-
<div class="compliance">EU AI ACT art. 12<br>NIST AI 600-1<br>SOC 2 CC7.2</div>
|
|
778
|
-
</nav>
|
|
779
|
-
<main>
|
|
780
|
-
${page.head}
|
|
781
|
-
${page.body}
|
|
782
|
-
</main>
|
|
783
|
-
</div>
|
|
784
|
-
</div></div>
|
|
785
|
-
<script>${NAV_SCRIPT}<\/script>
|
|
786
|
-
</body></html>
|
|
787
|
-
`;
|
|
788
|
-
}
|
|
789
|
-
function renderNav(items, active) {
|
|
790
|
-
return `<div class="nav">\n${items.map((item) => {
|
|
791
|
-
const classes = [item.label === active ? "on" : "", item.pending === true ? "pending" : ""].filter(Boolean).join(" ");
|
|
792
|
-
const attr = classes === "" ? "" : ` class="${classes}"`;
|
|
793
|
-
const badge = item.pending === true ? "<span class=\"badge soon\">SOON</span>" : item.badge === void 0 || item.badge === 0 ? "" : `<span class="badge">${item.badge}</span>`;
|
|
794
|
-
const href = item.pending === true ? "" : ` href="${esc(item.href ?? "#")}"`;
|
|
795
|
-
const tag = item.pending === true ? "span" : "a";
|
|
796
|
-
return ` <${tag}${href}${attr}>${esc(item.label)}${badge}</${tag}>`;
|
|
797
|
-
}).join("\n")}\n </div>`;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
//#endregion
|
|
801
|
-
//#region ../controls/src/index.ts
|
|
802
|
-
const EU_AI_ACT = {
|
|
803
|
-
id: "eu-ai-act-2024",
|
|
804
|
-
name: "EU AI Act — Regulation (EU) 2024/1689"
|
|
805
|
-
};
|
|
806
|
-
const NIST_600_1 = {
|
|
807
|
-
id: "nist-ai-600-1-2024",
|
|
808
|
-
name: "NIST AI 600-1 (2024) — Generative AI profile"
|
|
809
|
-
};
|
|
810
|
-
const SOC_2 = {
|
|
811
|
-
id: "soc2-tsc-2017",
|
|
812
|
-
name: "SOC 2 — Trust Services Criteria (2017)"
|
|
813
|
-
};
|
|
814
|
-
/**
|
|
815
|
-
* What each control asks for, and what this profile actually produces.
|
|
816
|
-
*
|
|
817
|
-
* Framed as evidence toward a control, never as compliance with it. Nothing
|
|
818
|
-
* here certifies anything: an auditor decides whether the evidence satisfies
|
|
819
|
-
* the obligation, and a tool that told them the answer would be selling the
|
|
820
|
-
* one thing it cannot supply.
|
|
821
|
-
*/
|
|
822
|
-
const isEvent = (e) => e.tool.startsWith("prae:") || e.tool === "system-prompt";
|
|
823
|
-
const decisionsOf = (data) => data.entries.filter((e) => !isEvent(e));
|
|
824
|
-
const allRules = (data) => (data.pack?.rules ?? []).map((r) => r.id);
|
|
825
|
-
/** Rows under the given rules. */
|
|
826
|
-
const under = (data, rules) => {
|
|
827
|
-
const ids = new Set(rules);
|
|
828
|
-
return {
|
|
829
|
-
rules,
|
|
830
|
-
rows: data.entries.filter((e) => e.rule !== void 0 && ids.has(e.rule))
|
|
831
|
-
};
|
|
832
|
-
};
|
|
833
|
-
/** Refusals and holds, with the rules that produced them. */
|
|
834
|
-
function heldAndRefused(data) {
|
|
835
|
-
const rows = decisionsOf(data).filter((e) => e.verdict === "deny" || e.verdict === "ask");
|
|
836
|
-
return {
|
|
837
|
-
rules: [...new Set(rows.flatMap((e) => e.rule ? [e.rule] : []))],
|
|
838
|
-
rows
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
const OBLIGATIONS = [
|
|
842
|
-
{
|
|
843
|
-
id: "EU AI Act art. 12",
|
|
844
|
-
code: "Art. 12",
|
|
845
|
-
name: "Record-keeping",
|
|
846
|
-
standard: EU_AI_ACT,
|
|
847
|
-
trace: (data) => ({
|
|
848
|
-
rules: [],
|
|
849
|
-
rows: data.entries
|
|
850
|
-
}),
|
|
851
|
-
asks: "Automatic recording of events over the lifetime of the system, to a degree that enables tracing.",
|
|
852
|
-
evidence: (data) => ({
|
|
853
|
-
state: data.entries.length > 0 ? "covered" : "partial",
|
|
854
|
-
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."
|
|
855
|
-
})
|
|
856
|
-
},
|
|
857
|
-
{
|
|
858
|
-
id: "NIST AI 600-1 · GV-1.2",
|
|
859
|
-
name: "Trustworthy AI in policy",
|
|
860
|
-
standard: NIST_600_1,
|
|
861
|
-
trace: (data) => ({
|
|
862
|
-
rules: allRules(data),
|
|
863
|
-
rows: []
|
|
864
|
-
}),
|
|
865
|
-
asks: "Characteristics of trustworthy AI are integrated into organisational policies.",
|
|
866
|
-
evidence: (data) => ({
|
|
867
|
-
state: data.pack === void 0 ? "absent" : "covered",
|
|
868
|
-
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.`
|
|
869
|
-
})
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
id: "NIST AI 600-1 · MS-2.6",
|
|
873
|
-
name: "Regular safety evaluation",
|
|
874
|
-
standard: NIST_600_1,
|
|
875
|
-
asks: "AI system is evaluated regularly for safety risks, with results documented.",
|
|
876
|
-
evidence: () => ({
|
|
877
|
-
state: "partial",
|
|
878
|
-
note: "Decisions are documented per call. Periodic evaluation of the policy itself is an operator process this tool does not perform."
|
|
879
|
-
})
|
|
880
|
-
},
|
|
881
|
-
{
|
|
882
|
-
id: "SOC 2 · CC7.2",
|
|
883
|
-
name: "Monitoring for anomalies",
|
|
884
|
-
standard: SOC_2,
|
|
885
|
-
trace: (data) => heldAndRefused(data),
|
|
886
|
-
asks: "The entity monitors system components for anomalies indicative of malicious acts.",
|
|
887
|
-
evidence: (data) => ({
|
|
888
|
-
state: data.entries.some((e) => e.verdict === "deny") ? "covered" : "partial",
|
|
889
|
-
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."}`
|
|
890
|
-
})
|
|
891
|
-
},
|
|
892
|
-
{
|
|
893
|
-
id: "SOC 2 · CC6.1",
|
|
894
|
-
name: "Logical access restriction",
|
|
895
|
-
standard: SOC_2,
|
|
896
|
-
trace: (data) => ({
|
|
897
|
-
rules: allRules(data),
|
|
898
|
-
rows: []
|
|
899
|
-
}),
|
|
900
|
-
asks: "Logical access to protected information assets is restricted to authorised users.",
|
|
901
|
-
evidence: () => ({
|
|
902
|
-
state: "partial",
|
|
903
|
-
note: "Capabilities are restricted per agent. Authorisation of the human is not: approvals record what was decided, never who decided it."
|
|
904
|
-
})
|
|
905
|
-
}
|
|
906
|
-
];
|
|
907
|
-
const OWASP = "owasp-llm-top-10-2026";
|
|
908
|
-
const AGENTIC = "owasp-agentic-top-10-2026";
|
|
909
|
-
/** Rules in the pack that cite one control of one framework. */
|
|
910
|
-
function citing(pack, framework, control) {
|
|
911
|
-
return (pack?.rules ?? []).filter((rule) => (rule.frameworks?.[framework] ?? []).includes(control));
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Why a runtime execution gate is not the control for a risk no rule cites.
|
|
915
|
-
*
|
|
916
|
-
* Said per risk rather than as one generic line, because "out of scope" with
|
|
917
|
-
* no reason reads as an excuse. Each of these is a boundary a reader can
|
|
918
|
-
* check: the risk lives somewhere a tool-call gate does not stand.
|
|
919
|
-
*/
|
|
920
|
-
const NOT_THIS_BOUNDARY = {
|
|
921
|
-
LLM04: "Where a model or dependency came from is settled before an agent runs; the gate sees the call, not the provenance.",
|
|
922
|
-
LLM05: "Training and fine-tuning data sit outside the execution boundary this gate stands on.",
|
|
923
|
-
LLM07: "Whether an answer is true is not a property of a tool call.",
|
|
924
|
-
LLM08: "The assembled system prompt is sealed into the ledger as evidence of what the model was told; nothing here stops the model repeating it.",
|
|
925
|
-
LLM09: "Retrieval stores are upstream of the call the gate judges.",
|
|
926
|
-
LLM10: "What downstream code does with model output is that code's boundary, not this one's.",
|
|
927
|
-
ASI04: "Where a tool, skill or plugin came from is settled before the agent runs. A rule on the skill tool (integrity, source) is how a pack refuses to trust one at load time; none in this pack does.",
|
|
928
|
-
ASI06: "Memory and retrieval stores are upstream of the call the gate judges. What the gate can do is seal what the agent was told (PRAE-ASM, PRAE-CTX) so a poisoned context is evidenced, never silent; this record holds no such seal.",
|
|
929
|
-
ASI07: "A message between agents is not a tool call; when it becomes one, it is judged like any other agent's call. The channel itself is outside this boundary.",
|
|
930
|
-
ASI09: "Whether a person was misled is not a property of a tool call. An ask verdict puts a human in the path and seals the outcome; nothing here judges the persuasion."
|
|
931
|
-
};
|
|
932
|
-
/**
|
|
933
|
-
* A risk this gate answers by *not* answering it: nothing here detects an
|
|
934
|
-
* injected instruction or a hijacked goal, and saying otherwise would be the
|
|
935
|
-
* claim a red team disproves first. What the gate does is bound what the
|
|
936
|
-
* subverted agent can execute — the cited controls of another risk doing
|
|
937
|
-
* their job. Said in those words, every time.
|
|
938
|
-
*/
|
|
939
|
-
const BOUNDED_BY = {
|
|
940
|
-
LLM01: {
|
|
941
|
-
framework: OWASP,
|
|
942
|
-
control: "LLM03"
|
|
943
|
-
},
|
|
944
|
-
ASI01: {
|
|
945
|
-
framework: AGENTIC,
|
|
946
|
-
control: "ASI02"
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
function bounded(data, risk, by) {
|
|
950
|
-
const bounding = citing(data.pack, by.framework, by.control);
|
|
951
|
-
const what = risk === "ASI01" ? "a hijacked goal" : "injected instructions";
|
|
952
|
-
const who = risk === "ASI01" ? "a hijacked agent" : "an injected instruction";
|
|
953
|
-
return {
|
|
954
|
-
state: "partial",
|
|
955
|
-
note: bounding.length === 0 ? `Not mitigated: this gate does not detect ${what}, and no ${by.control} control in this pack bounds what ${who} can execute.` : `Not mitigated: this gate does not detect ${what}. Bounded: ${bounding.length} ${by.control} control${bounding.length === 1 ? "" : "s"} (${bounding.map((r) => r.id).join(", ")}) decide what ${who} can execute.`
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
/**
|
|
959
|
-
* Risks whose evidence is in the record rather than in a citing rule.
|
|
960
|
-
*
|
|
961
|
-
* ASI06: a poisoned context cannot be prevented here, but every change to what
|
|
962
|
-
* the agent was told is sealed, so it is evidenced. ASI10: a drifting agent's
|
|
963
|
-
* actions are judged like any other's; what the record shows is whether the
|
|
964
|
-
* gate refused any of them.
|
|
965
|
-
*/
|
|
966
|
-
const FROM_THE_RECORD = {
|
|
967
|
-
ASI06: (data) => {
|
|
968
|
-
const seals = data.entries.filter((e) => e.rule === "PRAE-ASM" || e.rule === "PRAE-CTX");
|
|
969
|
-
return seals.length === 0 ? void 0 : {
|
|
970
|
-
state: "partial",
|
|
971
|
-
note: `Evidenced, not prevented: ${seals.length} seal${seals.length === 1 ? "" : "s"} of what the agent was told (PRAE-ASM / PRAE-CTX) in this record; a poisoned context changes the hash and is answerable afterwards.`
|
|
972
|
-
};
|
|
973
|
-
},
|
|
974
|
-
ASI10: (data) => {
|
|
975
|
-
const refused = data.entries.filter((e) => e.verdict === "deny").length;
|
|
976
|
-
const armed = [
|
|
977
|
-
"ASI02",
|
|
978
|
-
"ASI03",
|
|
979
|
-
"ASI08"
|
|
980
|
-
].flatMap((c) => citing(data.pack, AGENTIC, c));
|
|
981
|
-
if (armed.length === 0) return void 0;
|
|
982
|
-
return {
|
|
983
|
-
state: refused > 0 ? "covered" : "partial",
|
|
984
|
-
note: refused > 0 ? `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call, and ${refused} refusal${refused === 1 ? "" : "s"} in this record show them holding.` : `A drifting agent's actions are judged like any other's: ${armed.length} controls under ASI02/03/08 apply to every call; none has refused one in this record yet. Detecting drift itself is derived from the record, not a rule.`
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
};
|
|
988
|
-
function frameworkRow(frameworkId, control) {
|
|
989
|
-
const framework = FRAMEWORKS[frameworkId];
|
|
990
|
-
const risk = framework?.controls[control];
|
|
991
|
-
const name = risk?.name ?? control;
|
|
992
|
-
return {
|
|
993
|
-
id: `${framework?.short ?? frameworkId} · ${control}`,
|
|
994
|
-
asks: `${name}. ${risk?.summary ?? ""}`.trim(),
|
|
995
|
-
trace: (data) => {
|
|
996
|
-
const by = BOUNDED_BY[control];
|
|
997
|
-
if (by !== void 0) return under(data, citing(data.pack, by.framework, by.control).map((r) => r.id));
|
|
998
|
-
const rules = citing(data.pack, frameworkId, control).map((r) => r.id);
|
|
999
|
-
if (rules.length > 0) return under(data, rules);
|
|
1000
|
-
if (control === "ASI06") return {
|
|
1001
|
-
rules: [],
|
|
1002
|
-
rows: data.entries.filter((e) => e.rule === "PRAE-ASM" || e.rule === "PRAE-CTX")
|
|
1003
|
-
};
|
|
1004
|
-
if (control === "ASI10") return {
|
|
1005
|
-
rules: [
|
|
1006
|
-
"ASI02",
|
|
1007
|
-
"ASI03",
|
|
1008
|
-
"ASI08"
|
|
1009
|
-
].flatMap((c) => citing(data.pack, AGENTIC, c)).map((r) => r.id),
|
|
1010
|
-
rows: decisionsOf(data).filter((e) => e.verdict === "deny")
|
|
1011
|
-
};
|
|
1012
|
-
return {
|
|
1013
|
-
rules: [],
|
|
1014
|
-
rows: []
|
|
1015
|
-
};
|
|
1016
|
-
},
|
|
1017
|
-
evidence: (data) => {
|
|
1018
|
-
if (data.pack === void 0) return {
|
|
1019
|
-
state: "absent",
|
|
1020
|
-
note: "No pack supplied to this view."
|
|
1021
|
-
};
|
|
1022
|
-
const by = BOUNDED_BY[control];
|
|
1023
|
-
if (by !== void 0) return bounded(data, control, by);
|
|
1024
|
-
const rules = citing(data.pack, frameworkId, control);
|
|
1025
|
-
if (rules.length === 0) {
|
|
1026
|
-
const derived = FROM_THE_RECORD[control]?.(data);
|
|
1027
|
-
if (derived !== void 0) return derived;
|
|
1028
|
-
return {
|
|
1029
|
-
state: "absent",
|
|
1030
|
-
note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
const ids = new Set(rules.map((rule) => rule.id));
|
|
1034
|
-
const decisions = data.entries.filter((entry) => entry.rule !== void 0 && ids.has(entry.rule));
|
|
1035
|
-
const listed = rules.map((rule) => rule.id).join(", ");
|
|
1036
|
-
return decisions.length > 0 ? {
|
|
1037
|
-
state: "covered",
|
|
1038
|
-
note: `${rules.length} control${rules.length === 1 ? "" : "s"} mapped (${listed}) · ${decisions.length} sealed decision${decisions.length === 1 ? "" : "s"} under them in this record.`
|
|
1039
|
-
} : {
|
|
1040
|
-
state: "partial",
|
|
1041
|
-
note: `${rules.length} control${rules.length === 1 ? "" : "s"} mapped (${listed}); none has decided a call in this record yet, so the mapping is declared, not exercised.`
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
const rowsOf = (frameworkId) => Object.keys(FRAMEWORKS[frameworkId]?.controls ?? {}).map((control) => frameworkRow(frameworkId, control));
|
|
1047
|
-
const ISO_42001 = {
|
|
1048
|
-
id: "iso-iec-42001-2023",
|
|
1049
|
-
name: "ISO/IEC 42001:2023 — AI management system, Annex A"
|
|
1050
|
-
};
|
|
1051
|
-
const ISO_27001 = {
|
|
1052
|
-
id: "iso-iec-27001-2022",
|
|
1053
|
-
name: "ISO/IEC 27001:2022 — Annex A"
|
|
1054
|
-
};
|
|
1055
|
-
/** Rules in the pack whose `when` names a capability matching `test`. */
|
|
1056
|
-
function rulesOn(pack, test) {
|
|
1057
|
-
return (pack?.rules ?? []).filter((rule) => {
|
|
1058
|
-
const cap = rule.when.capability;
|
|
1059
|
-
return (cap === void 0 ? [] : typeof cap === "string" ? [cap] : cap).some(test);
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1062
|
-
/** The record itself: recorded, chained, and how much of it is signed. */
|
|
1063
|
-
function recorded(data) {
|
|
1064
|
-
const n = data.entries.length;
|
|
1065
|
-
if (n === 0) return {
|
|
1066
|
-
state: "partial",
|
|
1067
|
-
note: "Recording is configured but nothing has been logged yet."
|
|
1068
|
-
};
|
|
1069
|
-
const signed = data.entries.filter((e) => e.sig !== void 0).length;
|
|
1070
|
-
if (!data.verification.ok) return {
|
|
1071
|
-
state: "partial",
|
|
1072
|
-
note: `${n} rows recorded, but the chain does not verify: the log is present and its integrity is not.`
|
|
1073
|
-
};
|
|
1074
|
-
return {
|
|
1075
|
-
state: "covered",
|
|
1076
|
-
note: `${n} rows recorded, each hash-linked to the one before, chain intact${signed > 0 ? `, ${signed} of ${n} signed` : ", unsigned"} — an edited, removed or reordered row breaks it.`
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
/** Refusals and holds, recorded with their clause. */
|
|
1080
|
-
function monitored(data) {
|
|
1081
|
-
const refused = decisionsOf(data).filter((e) => e.verdict === "deny").length;
|
|
1082
|
-
const held = decisionsOf(data).filter((e) => e.verdict === "ask").length;
|
|
1083
|
-
return refused + held > 0 ? {
|
|
1084
|
-
state: "covered",
|
|
1085
|
-
note: `Every call is judged as it happens; this record holds ${refused} refusal${refused === 1 ? "" : "s"} and ${held} hold${held === 1 ? "" : "s"} for a human, each with the clause that produced it.`
|
|
1086
|
-
} : {
|
|
1087
|
-
state: "partial",
|
|
1088
|
-
note: "Every call is judged as it happens, but this record contains no refusal or hold yet, so the monitoring is unproven in practice."
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
const ISO_CONTROLS = [
|
|
1092
|
-
{
|
|
1093
|
-
id: "ISO/IEC 42001 · A.2.2",
|
|
1094
|
-
name: "AI policy",
|
|
1095
|
-
standard: ISO_42001,
|
|
1096
|
-
trace: (data) => ({
|
|
1097
|
-
rules: allRules(data),
|
|
1098
|
-
rows: []
|
|
1099
|
-
}),
|
|
1100
|
-
asks: "The organisation has a documented policy for developing or using AI systems.",
|
|
1101
|
-
evidence: (data) => data.pack === void 0 ? {
|
|
1102
|
-
state: "absent",
|
|
1103
|
-
note: "No pack supplied to this view."
|
|
1104
|
-
} : {
|
|
1105
|
-
state: "partial",
|
|
1106
|
-
note: `The executable part of an AI policy: ${data.pack.rules.length} rules declared as data and applied to every tool call. The policy document and its approval are the organisation's.`
|
|
1107
|
-
}
|
|
1108
|
-
},
|
|
1109
|
-
{
|
|
1110
|
-
id: "ISO/IEC 42001 · A.6.2.6",
|
|
1111
|
-
name: "AI system operation and monitoring",
|
|
1112
|
-
standard: ISO_42001,
|
|
1113
|
-
trace: heldAndRefused,
|
|
1114
|
-
asks: "The AI system is monitored while it operates, and what is found is acted on.",
|
|
1115
|
-
evidence: monitored
|
|
1116
|
-
},
|
|
1117
|
-
{
|
|
1118
|
-
id: "ISO/IEC 42001 · A.6.2.8",
|
|
1119
|
-
name: "Recording of event logs",
|
|
1120
|
-
standard: ISO_42001,
|
|
1121
|
-
trace: (data) => ({
|
|
1122
|
-
rules: [],
|
|
1123
|
-
rows: data.entries
|
|
1124
|
-
}),
|
|
1125
|
-
asks: "Event logs are kept while the AI system is in use.",
|
|
1126
|
-
evidence: recorded
|
|
1127
|
-
},
|
|
1128
|
-
{
|
|
1129
|
-
id: "ISO/IEC 42001 · A.9.4",
|
|
1130
|
-
name: "Intended use of the AI system",
|
|
1131
|
-
standard: ISO_42001,
|
|
1132
|
-
trace: (data) => {
|
|
1133
|
-
const rows = decisionsOf(data).filter((e) => e.verdict === "deny");
|
|
1134
|
-
return {
|
|
1135
|
-
rules: [...new Set(rows.flatMap((e) => e.rule ? [e.rule] : []))],
|
|
1136
|
-
rows
|
|
1137
|
-
};
|
|
1138
|
-
},
|
|
1139
|
-
asks: "The AI system is used only for its intended uses.",
|
|
1140
|
-
evidence: (data) => {
|
|
1141
|
-
if (data.pack === void 0) return {
|
|
1142
|
-
state: "absent",
|
|
1143
|
-
note: "No pack supplied to this view."
|
|
1144
|
-
};
|
|
1145
|
-
const refused = decisionsOf(data).filter((e) => e.verdict === "deny").length;
|
|
1146
|
-
return refused > 0 ? {
|
|
1147
|
-
state: "covered",
|
|
1148
|
-
note: `Intended use is enforced as scope: ${refused} action${refused === 1 ? "" : "s"} outside it refused before execution in this record.`
|
|
1149
|
-
} : {
|
|
1150
|
-
state: "partial",
|
|
1151
|
-
note: "Intended use is enforced as scope by the pack; nothing outside it has been attempted in this record yet."
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
},
|
|
1155
|
-
{
|
|
1156
|
-
id: "ISO/IEC 27001 · A.8.3",
|
|
1157
|
-
name: "Information access restriction",
|
|
1158
|
-
standard: ISO_27001,
|
|
1159
|
-
trace: (data) => ({
|
|
1160
|
-
rules: rulesOn(data.pack, (c) => c.startsWith("fs:")).map((r) => r.id),
|
|
1161
|
-
rows: decisionsOf(data).filter((e) => e.verdict === "deny" && e.capability?.startsWith("fs:"))
|
|
1162
|
-
}),
|
|
1163
|
-
asks: "Access to information is restricted according to the access-control policy.",
|
|
1164
|
-
evidence: (data) => {
|
|
1165
|
-
const rules = rulesOn(data.pack, (c) => c.startsWith("fs:"));
|
|
1166
|
-
if (rules.length === 0) return {
|
|
1167
|
-
state: "absent",
|
|
1168
|
-
note: "No rule in this pack restricts file access."
|
|
1169
|
-
};
|
|
1170
|
-
const refused = decisionsOf(data).filter((e) => e.verdict === "deny" && e.capability?.startsWith("fs:")).length;
|
|
1171
|
-
return refused > 0 ? {
|
|
1172
|
-
state: "covered",
|
|
1173
|
-
note: `${rules.length} file-access rules restrict what the agent may read and write; ${refused} access${refused === 1 ? "" : "es"} refused in this record. Restriction is per agent, not per human.`
|
|
1174
|
-
} : {
|
|
1175
|
-
state: "partial",
|
|
1176
|
-
note: `${rules.length} file-access rules restrict what the agent may read and write; none has refused an access in this record yet.`
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
},
|
|
1180
|
-
{
|
|
1181
|
-
id: "ISO/IEC 27001 · A.8.12",
|
|
1182
|
-
name: "Data leakage prevention",
|
|
1183
|
-
standard: ISO_27001,
|
|
1184
|
-
trace: (data) => ({
|
|
1185
|
-
rules: [...new Set([...rulesOn(data.pack, (c) => c === "net:fetch").map((r) => r.id), ...(data.pack?.rules ?? []).filter((r) => r.when.secrets !== void 0).map((r) => r.id)])],
|
|
1186
|
-
rows: decisionsOf(data).filter((e) => e.verdict === "deny" && (e.capability === "net:fetch" || e.secrets !== void 0 || e.phase === "output"))
|
|
1187
|
-
}),
|
|
1188
|
-
asks: "Leakage of sensitive information is prevented where it is processed or sent.",
|
|
1189
|
-
evidence: (data) => {
|
|
1190
|
-
const rules = [...rulesOn(data.pack, (c) => c === "net:fetch"), ...(data.pack?.rules ?? []).filter((r) => r.when.secrets !== void 0)];
|
|
1191
|
-
if (rules.length === 0) return {
|
|
1192
|
-
state: "absent",
|
|
1193
|
-
note: "No rule in this pack governs egress or credentials."
|
|
1194
|
-
};
|
|
1195
|
-
const stopped = decisionsOf(data).filter((e) => e.verdict === "deny" && (e.capability === "net:fetch" || e.secrets !== void 0 || e.phase === "output")).length;
|
|
1196
|
-
return stopped > 0 ? {
|
|
1197
|
-
state: "covered",
|
|
1198
|
-
note: `${new Set(rules.map((r) => r.id)).size} egress and credential rules; ${stopped} send${stopped === 1 ? "" : "s"} or credential read${stopped === 1 ? "" : "s"} stopped in this record before the data left.`
|
|
1199
|
-
} : {
|
|
1200
|
-
state: "partial",
|
|
1201
|
-
note: `${new Set(rules.map((r) => r.id)).size} egress and credential rules in force; none has stopped anything in this record yet.`
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
},
|
|
1205
|
-
{
|
|
1206
|
-
id: "ISO/IEC 27001 · A.8.15",
|
|
1207
|
-
name: "Logging",
|
|
1208
|
-
standard: ISO_27001,
|
|
1209
|
-
trace: (data) => ({
|
|
1210
|
-
rules: [],
|
|
1211
|
-
rows: data.entries
|
|
1212
|
-
}),
|
|
1213
|
-
asks: "Activities, exceptions and other relevant events are logged, and the logs are protected.",
|
|
1214
|
-
evidence: recorded
|
|
1215
|
-
},
|
|
1216
|
-
{
|
|
1217
|
-
id: "ISO/IEC 27001 · A.8.16",
|
|
1218
|
-
name: "Monitoring activities",
|
|
1219
|
-
standard: ISO_27001,
|
|
1220
|
-
trace: heldAndRefused,
|
|
1221
|
-
asks: "Systems are monitored for anomalous behaviour, and potential incidents are evaluated.",
|
|
1222
|
-
evidence: monitored
|
|
1223
|
-
}
|
|
1224
|
-
];
|
|
1225
|
-
/**
|
|
1226
|
-
* The obligations first, then the risks framework by framework. Two kinds of
|
|
1227
|
-
* row in one table on purpose: an auditor reads down one list and finds the
|
|
1228
|
-
* audit obligations the ledger evidences and the application risks the pack's
|
|
1229
|
-
* rules address, each stated in its framework's own words.
|
|
1230
|
-
*/
|
|
1231
|
-
const CONTROLS = [
|
|
1232
|
-
...OBLIGATIONS,
|
|
1233
|
-
...rowsOf(OWASP),
|
|
1234
|
-
...rowsOf(AGENTIC),
|
|
1235
|
-
...ISO_CONTROLS
|
|
1236
|
-
];
|
|
1237
|
-
|
|
1238
|
-
//#endregion
|
|
1239
|
-
//#region src/console/views.ts
|
|
1240
|
-
/** One rail for every view, so the shape of the product stays consistent. */
|
|
1241
|
-
function navFor(data) {
|
|
1242
|
-
return [
|
|
1243
|
-
{
|
|
1244
|
-
label: "Overview",
|
|
1245
|
-
href: "overview.html"
|
|
1246
|
-
},
|
|
1247
|
-
{
|
|
1248
|
-
label: "Policy packs",
|
|
1249
|
-
href: "packs.html"
|
|
1250
|
-
},
|
|
1251
|
-
{
|
|
1252
|
-
label: "Instructions",
|
|
1253
|
-
href: "instructions.html"
|
|
1254
|
-
},
|
|
1255
|
-
{
|
|
1256
|
-
label: "Playground",
|
|
1257
|
-
href: "playground.html"
|
|
1258
|
-
},
|
|
1259
|
-
{
|
|
1260
|
-
label: "Approvals",
|
|
1261
|
-
href: "approvals.html",
|
|
1262
|
-
badge: data.entries.filter((e) => e.verdict === "ask").length
|
|
1263
|
-
},
|
|
1264
|
-
{
|
|
1265
|
-
label: "Audit ledger",
|
|
1266
|
-
href: "ledger.html"
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
label: "Fleet",
|
|
1270
|
-
pending: true
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
label: "Compliance map",
|
|
1274
|
-
href: "compliance.html"
|
|
1275
|
-
}
|
|
1276
|
-
];
|
|
1277
|
-
}
|
|
1278
|
-
function chainPill(ok) {
|
|
1279
|
-
return `<div class="chain${ok ? "" : " broken"}"><i></i>${ok ? "CHAIN INTACT" : "CHAIN BROKEN"}</div>`;
|
|
1280
|
-
}
|
|
1281
|
-
function header(title, meta, ok) {
|
|
1282
|
-
return ` <header class="head">
|
|
1283
|
-
<div><h1>${esc(title)}</h1><div class="meta">${esc(meta)}</div></div>
|
|
1284
|
-
<div class="actions">${chainPill(ok)}
|
|
1285
|
-
<button type="button" class="btn" onclick="window.print()">Export evidence</button>
|
|
1286
|
-
</div>
|
|
1287
|
-
</header>`;
|
|
1288
|
-
}
|
|
1289
|
-
function renderOverview(data) {
|
|
1290
|
-
const counts = {};
|
|
1291
|
-
for (const entry of data.entries) counts[entry.verdict] = (counts[entry.verdict] ?? 0) + 1;
|
|
1292
|
-
const held = counts.ask ?? 0;
|
|
1293
|
-
const pack = data.pack;
|
|
1294
|
-
const enforcing = pack?.rules.some((r) => r.effect.kind === "deny") ?? false;
|
|
1295
|
-
const body = `${` <section class="stats">
|
|
1296
|
-
<div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
|
|
1297
|
-
<div class="stat"><div class="v veto">${counts.deny ?? 0}</div><div class="k">VETOED</div></div>
|
|
1298
|
-
<div class="stat"><div class="v hold">${held}</div><div class="k">AWAITING HUMAN</div></div>
|
|
1299
|
-
<div class="stat"><div class="v">${pack === void 0 ? "—" : esc(pack.rules.length)}</div><div class="k">RULES IN FORCE</div></div>
|
|
1300
|
-
</section>`}
|
|
1301
|
-
<section class="sect">
|
|
1302
|
-
<h2>Posture</h2>
|
|
1303
|
-
<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>
|
|
1304
|
-
<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>
|
|
1305
|
-
</section>
|
|
1306
|
-
<section class="sect" style="padding-bottom:26px">
|
|
1307
|
-
<h2>What is not covered</h2>
|
|
1308
|
-
<p>This gate decides tool calls. It does not sandbox plugin code — a
|
|
1309
|
-
plugin's <code>apply()</code> runs with full privileges before any tool
|
|
1310
|
-
call exists — and it does not secure the harness itself. Approvals record
|
|
1311
|
-
what was decided, never who decided it.</p>
|
|
1312
|
-
</section>`;
|
|
1313
|
-
return renderShell({
|
|
1314
|
-
title: `Overview — ${pack?.pack ?? "PRAE"}`,
|
|
1315
|
-
nav: renderNav(navFor(data), "Overview"),
|
|
1316
|
-
head: header("Overview", `${data.entries.length} decisions · ${pack === void 0 ? "no pack" : `${pack.pack} ${pack.version}`}`, data.verification.ok),
|
|
1317
|
-
body
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
function matcherOf(rule) {
|
|
1321
|
-
const when = rule.when;
|
|
1322
|
-
const parts = [];
|
|
1323
|
-
const list = (v) => v === void 0 ? void 0 : Array.isArray(v) ? v.join(", ") : String(v);
|
|
1324
|
-
const cap = list(when.capability);
|
|
1325
|
-
if (cap !== void 0) parts.push(`capability ${cap}`);
|
|
1326
|
-
const tool = list(when.tool);
|
|
1327
|
-
if (tool !== void 0) parts.push(`tool ${tool}`);
|
|
1328
|
-
const plugin = list(when.plugin);
|
|
1329
|
-
if (plugin !== void 0) parts.push(`plugin ${plugin}`);
|
|
1330
|
-
if (when.path?.outside !== void 0) parts.push(`path outside ${when.path.outside.join(", ")}`);
|
|
1331
|
-
if (when.path?.inside !== void 0) parts.push(`path inside ${when.path.inside.join(", ")}`);
|
|
1332
|
-
if (when.host?.notIn !== void 0) parts.push(`host not in ${when.host.notIn.join(", ")}`);
|
|
1333
|
-
if (when.host?.in !== void 0) parts.push(`host in ${when.host.in.join(", ")}`);
|
|
1334
|
-
if (when.count !== void 0) parts.push(`more than ${when.count.above}`);
|
|
1335
|
-
for (const arg of when.arg ?? []) {
|
|
1336
|
-
const bits = [
|
|
1337
|
-
arg.above === void 0 ? "" : `above ${arg.above}`,
|
|
1338
|
-
arg.below === void 0 ? "" : `below ${arg.below}`,
|
|
1339
|
-
arg.equals === void 0 ? "" : `= ${String(arg.equals)}`
|
|
1340
|
-
].filter(Boolean);
|
|
1341
|
-
parts.push(`${arg.name} ${bits.join(" ")}`.trim());
|
|
1342
|
-
}
|
|
1343
|
-
return parts.length === 0 ? "every call" : parts.join(" · ");
|
|
1344
|
-
}
|
|
1345
|
-
function renderPacks(data) {
|
|
1346
|
-
const pack = data.pack;
|
|
1347
|
-
if (pack === void 0) return renderShell({
|
|
1348
|
-
title: "Policy packs",
|
|
1349
|
-
nav: renderNav(navFor(data), "Policy packs"),
|
|
1350
|
-
head: header("Policy packs", "no pack supplied", data.verification.ok),
|
|
1351
|
-
body: " <div class=\"empty\">Pass --pack to render the policy in force.</div>"
|
|
1352
|
-
});
|
|
1353
|
-
const baseline = pack.baseline?.capabilities;
|
|
1354
|
-
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>`;
|
|
1355
|
-
const cited = /* @__PURE__ */ new Map();
|
|
1356
|
-
for (const rule of pack.rules) for (const c of cite(rule)) cited.set(c.framework, c.frameworkName);
|
|
1357
|
-
const frameworks = cited.size === 0 ? "" : `<div class="caps">${[...cited.values()].map((name) => `<span class="cap">${esc(name)}</span>`).join("")}</div>`;
|
|
1358
|
-
const rows = pack.rules.map((rule) => {
|
|
1359
|
-
const tags = cite(rule).map((c) => `<br><span style="color:var(--secondary)">${esc(c.frameworkShort)} · ${esc(c.control)} ${esc(c.controlName)}</span>`).join("");
|
|
1360
|
-
return ` <div class="grid rules row">
|
|
1361
|
-
<div class="seq">${esc(rule.id)}</div>
|
|
1362
|
-
<div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span>${tags}</div>
|
|
1363
|
-
<div class="effect e-${esc(rule.effect.kind)}">${esc(rule.effect.kind.toUpperCase())}</div>
|
|
1364
|
-
</div>`;
|
|
1365
|
-
}).join("\n");
|
|
1366
|
-
const body = ` <section class="sect">
|
|
1367
|
-
<h2>${esc(pack.pack)} <span style="font-family:var(--proof);font-size:14px;color:var(--muted)">${esc(pack.version)}</span></h2>
|
|
1368
|
-
<p>The operator baseline is intersected with whatever a plugin's own
|
|
1369
|
-
manifest declares, and the operator can only ever narrow. A pack cannot
|
|
1370
|
-
grant a capability an author never asked for.</p>
|
|
1371
|
-
</section>
|
|
1372
|
-
${caps}
|
|
1373
|
-
${frameworks}
|
|
1374
|
-
<div class="grid rules thead">
|
|
1375
|
-
<div>CLAUSE</div><div>RULE</div><div>EFFECT</div>
|
|
1376
|
-
</div>
|
|
1377
|
-
${rows}
|
|
1378
|
-
<section class="sect" style="padding-bottom:26px">
|
|
1379
|
-
<h2>Ordering</h2>
|
|
1380
|
-
<p>When several rules match one call the strongest effect wins —
|
|
1381
|
-
deny over ask over correct over warn — and every match stays in the
|
|
1382
|
-
trace. That is what makes a pack safe to extend: appending a rule can
|
|
1383
|
-
raise a decision's strength and never lower it.</p>
|
|
1384
|
-
</section>`;
|
|
1385
|
-
return renderShell({
|
|
1386
|
-
title: `Policy packs — ${pack.pack}`,
|
|
1387
|
-
nav: renderNav(navFor(data), "Policy packs"),
|
|
1388
|
-
head: header("Policy packs", `${pack.pack} ${pack.version} · ${pack.rules.length} rules`, data.verification.ok),
|
|
1389
|
-
body
|
|
1390
|
-
});
|
|
1391
|
-
}
|
|
1392
|
-
function renderCompliance(data) {
|
|
1393
|
-
const body = ` <section class="sect">
|
|
1394
|
-
<h2>Evidence, not compliance</h2>
|
|
1395
|
-
<p>Each row states what a control asks for and what this profile actually
|
|
1396
|
-
produces. Nothing here certifies anything — whether the evidence satisfies
|
|
1397
|
-
the obligation is an auditor's judgement, and a tool that answered it for
|
|
1398
|
-
them would be selling the one thing it cannot supply.</p>
|
|
1399
|
-
</section>
|
|
1400
|
-
<div class="grid map thead">
|
|
1401
|
-
<div>CONTROL</div><div>ASKS FOR · WHAT THIS PROFILE PRODUCES</div><div>STATE</div>
|
|
1402
|
-
</div>
|
|
1403
|
-
${CONTROLS.map((control) => {
|
|
1404
|
-
const found = control.evidence(data);
|
|
1405
|
-
const label = found.state === "covered" ? "EVIDENCE" : found.state === "partial" ? "PARTIAL" : "NONE";
|
|
1406
|
-
return ` <div class="grid map row">
|
|
1407
|
-
<div class="call">${esc(control.id)}</div>
|
|
1408
|
-
<div class="actor">${esc(control.asks)}<br><span style="color:var(--ink-row)">${esc(found.note)}</span></div>
|
|
1409
|
-
<div class="effect ${esc(found.state)}">${label}</div>
|
|
1410
|
-
</div>`;
|
|
1411
|
-
}).join("\n")}
|
|
1412
|
-
<section class="sect" style="padding-bottom:26px">
|
|
1413
|
-
<h2>Reading the states</h2>
|
|
1414
|
-
<p>For the audit obligations, several rows stop at <em>partial</em> for
|
|
1415
|
-
one reason: the harness records what was decided and never who decided
|
|
1416
|
-
it. Attributable approval needs an identity anchor outside this machine,
|
|
1417
|
-
which is a control-plane concern this build does not claim.</p>
|
|
1418
|
-
<p>For the OWASP risks, <em>evidence</em> means rules in this pack cite
|
|
1419
|
-
the risk and have decided calls in this record; <em>partial</em> means
|
|
1420
|
-
they cite it and have not yet, or — for prompt injection — that the gate
|
|
1421
|
-
bounds the risk without claiming to detect it; <em>none</em> means no
|
|
1422
|
-
rule cites it, with the reason a tool-call gate is not where that risk
|
|
1423
|
-
is controlled.</p>
|
|
1424
|
-
</section>`;
|
|
1425
|
-
return renderShell({
|
|
1426
|
-
title: "Compliance map",
|
|
1427
|
-
nav: renderNav(navFor(data), "Compliance map"),
|
|
1428
|
-
head: header("Compliance map", `${CONTROLS.length} controls · evidence from this profile`, data.verification.ok),
|
|
1429
|
-
body
|
|
1430
|
-
});
|
|
1431
|
-
}
|
|
1432
|
-
/**
|
|
1433
|
-
* Every call the policy held, and what became of it.
|
|
1434
|
-
*
|
|
1435
|
-
* This is a queue you read, not one you act on. The gate returns `ask` into the
|
|
1436
|
-
* harness's own approval seam and the harness settles it there; a second set of
|
|
1437
|
-
* buttons here would mean two places to approve from and two audit trails to
|
|
1438
|
-
* reconcile. So the view shows what is waiting and what was decided, and says
|
|
1439
|
-
* plainly where the decision happens.
|
|
1440
|
-
*/
|
|
1441
|
-
function renderApprovals(data) {
|
|
1442
|
-
const held = data.entries.filter((e) => e.verdict === "ask");
|
|
1443
|
-
const settled = data.entries.filter((e) => e.rule === "approval");
|
|
1444
|
-
const now = Date.parse(data.generatedAt);
|
|
1445
|
-
const waited = (iso) => {
|
|
1446
|
-
const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
|
|
1447
|
-
if (!Number.isFinite(s)) return "";
|
|
1448
|
-
const h = Math.floor(s / 3600);
|
|
1449
|
-
const m = Math.floor(s % 3600 / 60);
|
|
1450
|
-
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
1451
|
-
};
|
|
1452
|
-
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">
|
|
1453
|
-
<div class="card">
|
|
1454
|
-
<div style="flex:1">
|
|
1455
|
-
<h2>${esc(e.tool)}${e.subject === void 0 ? "" : ` on ${esc(e.subject)}`}</h2>
|
|
1456
|
-
<div class="detail">
|
|
1457
|
-
${esc(e.rule ?? "held")} · ${esc(e.session ?? "local session")} · waiting ${esc(waited(e.at))}<br>
|
|
1458
|
-
${esc(e.reason ?? "")}
|
|
1459
|
-
</div>
|
|
1460
|
-
</div>
|
|
1461
|
-
<div class="decide">
|
|
1462
|
-
<button type="button" class="approve" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Approve</button>
|
|
1463
|
-
<button type="button" class="deny" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Deny</button>
|
|
1464
|
-
<div class="recorded">SETTLED IN THE HARNESS</div>
|
|
1465
|
-
</div>
|
|
1466
|
-
</div>
|
|
1467
|
-
</section>`).join("\n");
|
|
1468
|
-
const history = settled.length === 0 ? "" : ` <div class="grid map thead">
|
|
1469
|
-
<div>WHEN</div><div>CALL · WHY</div><div>OUTCOME</div>
|
|
1470
|
-
</div>
|
|
1471
|
-
${settled.map((e) => ` <div class="grid map row">
|
|
1472
|
-
<div class="seq">${esc(e.at.replace("T", " ").replace(/\.\d+Z$/, "Z"))}</div>
|
|
1473
|
-
<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>
|
|
1474
|
-
<div class="effect ${e.verdict === "allow" ? "covered" : "absent"}">${e.verdict === "allow" ? "APPROVED" : "REFUSED"}</div>
|
|
1475
|
-
</div>`).join("\n")}`;
|
|
1476
|
-
const body = ` <section class="stats">
|
|
1477
|
-
<div class="stat"><div class="v hold">${held.length}</div><div class="k">AWAITING HUMAN</div></div>
|
|
1478
|
-
<div class="stat"><div class="v">${settled.length}</div><div class="k">SETTLED</div></div>
|
|
1479
|
-
<div class="stat"><div class="v">${settled.filter((e) => e.actor !== void 0).length}</div><div class="k">WITH AN ACTOR</div></div>
|
|
1480
|
-
<div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
|
|
1481
|
-
</section>
|
|
1482
|
-
<section class="sect">
|
|
1483
|
-
<h2>Waiting</h2>
|
|
1484
|
-
<p>A held call is one a clause would not decide alone. The harness owns
|
|
1485
|
-
the prompt and settles it; this is the queue, not a second place to
|
|
1486
|
-
approve from.</p>
|
|
1487
|
-
</section>
|
|
1488
|
-
${queue}
|
|
1489
|
-
<section class="sect">
|
|
1490
|
-
<h2>Settled</h2>
|
|
1491
|
-
<p>Where an entry names an actor, that name is asserted by configuration.
|
|
1492
|
-
The harness records what was decided and never who decided it, so an
|
|
1493
|
-
attributable approval needs an identity anchor this build does not have.</p>
|
|
1494
|
-
</section>
|
|
1495
|
-
${history}`;
|
|
1496
|
-
return renderShell({
|
|
1497
|
-
title: "Approvals",
|
|
1498
|
-
nav: renderNav(navFor(data), "Approvals"),
|
|
1499
|
-
head: header("Approvals", `${held.length} waiting · ${settled.length} settled`, data.verification.ok),
|
|
1500
|
-
body
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
//#endregion
|
|
1505
|
-
export { renderPacks as a, renderNav as c, FRAMEWORKS as d, cite as f, renderOverview as i, renderShell as l, renderApprovals as n, CONTROLS as o, renderCompliance as r, esc as s, navFor as t, VERDICT_LABEL as u };
|