prae-cli 0.6.0 → 0.6.1

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/README.md CHANGED
@@ -90,6 +90,14 @@ next to the result rather than in a footnote: a removed, edited, or reordered
90
90
  row breaks the chain; a rewrite of the whole file by someone holding this code
91
91
  does not.
92
92
 
93
+ ### `prae report <ledger.jsonl> [--out <file.html>] [--pack <pack.json>]`
94
+
95
+ Render the decision record as a printable evidence report. With `--pack`,
96
+ each clause also names the framework risk the rule cites — OWASP Top 10 for
97
+ LLM Applications 2026 today — so a reviewer reads risk → control → decision →
98
+ seal in one row. Without the pack the clause id stands alone rather than a
99
+ name guessed from it.
100
+
93
101
  ### `prae console <ledger.jsonl> --out <dir> [--pack <file>]`
94
102
 
95
103
  Write the control plane as linked pages: Overview, Policy packs, Playground,
package/lib/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import "./evaluate-CkYySl7T.js";
3
- import { t as main } from "./cli-Cr0U-PFD.js";
4
- import "./src-CXU0_W1P.js";
5
- import "./views-DalPkBZZ.js";
3
+ import "./views-CAD9738P.js";
4
+ import { t as main } from "./cli-BUgU-LpN.js";
5
+ import "./src-Ciggx9ja.js";
6
6
 
7
7
  //#region src/bin.ts
8
8
  main(process.argv.slice(2)).catch((error) => {
@@ -1,6 +1,6 @@
1
1
  import { t as evaluate } from "./evaluate-CkYySl7T.js";
2
- import { m as seal, t as Ledger } from "./src-CXU0_W1P.js";
3
- import { a as renderPacks, c as renderShell, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor } from "./views-DalPkBZZ.js";
2
+ import { a as renderPacks, c as renderShell, d as cite, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor, u as FRAMEWORKS } from "./views-CAD9738P.js";
3
+ import { m as seal, t as Ledger } from "./src-Ciggx9ja.js";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
@@ -302,7 +302,8 @@ function readRule(check$1, path, value) {
302
302
  "id",
303
303
  "title",
304
304
  "when",
305
- "effect"
305
+ "effect",
306
+ "frameworks"
306
307
  ]);
307
308
  const id = check$1.text(`${path}.id`, value.id);
308
309
  const effect = readEffect(check$1, `${path}.effect`, value.effect);
@@ -318,8 +319,40 @@ function readRule(check$1, path, value) {
318
319
  const title = check$1.text(`${path}.title`, value.title);
319
320
  if (title !== void 0) rule.title = title;
320
321
  }
322
+ if (value.frameworks !== void 0) {
323
+ const frameworks = readFrameworks(check$1, `${path}.frameworks`, value.frameworks);
324
+ if (frameworks !== void 0) rule.frameworks = frameworks;
325
+ }
321
326
  return rule;
322
327
  }
328
+ /**
329
+ * A citation names a framework edition and controls the registry knows. A
330
+ * typo here would be a rule that claims a mapping no view can render, so it
331
+ * is a pack error rather than an ignored key — the same line the rest of the
332
+ * parser takes.
333
+ */
334
+ function readFrameworks(check$1, path, value) {
335
+ if (!isObject(value)) {
336
+ check$1.fail(path, "expected an object keyed by framework id");
337
+ return;
338
+ }
339
+ const out = {};
340
+ for (const [framework, controls] of Object.entries(value)) {
341
+ const known = FRAMEWORKS[framework];
342
+ if (known === void 0) {
343
+ check$1.fail(`${path}.${framework}`, `unknown framework; expected one of ${Object.keys(FRAMEWORKS).map((k) => `\`${k}\``).join(", ")}`);
344
+ continue;
345
+ }
346
+ if (!Array.isArray(controls) || !controls.every((c) => typeof c === "string")) {
347
+ check$1.fail(`${path}.${framework}`, "expected an array of control ids");
348
+ continue;
349
+ }
350
+ const ids = controls;
351
+ for (const control of ids) if (known.controls[control] === void 0) check$1.fail(`${path}.${framework}`, `unknown control \`${control}\` in ${known.name}`);
352
+ if (ids.length > 0) out[framework] = [...new Set(ids)];
353
+ }
354
+ return out;
355
+ }
323
356
  /** Parse and validate a pack. Never throws; every problem lands in `errors`. */
324
357
  function parsePack(source) {
325
358
  let raw;
@@ -404,14 +437,16 @@ function ago(iso, now) {
404
437
  const s = seconds % 60;
405
438
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
406
439
  }
407
- function row(entry) {
440
+ function row(entry, pack) {
408
441
  const call = entry.subject === void 0 ? entry.tool : `${entry.tool} ${entry.subject}`;
409
442
  const actor = entry.actor ?? entry.agent ?? "—";
443
+ const rule = pack?.rules.find((r) => r.id === entry.rule);
444
+ const cited = rule === void 0 ? "" : cite(rule).map((c) => `<br><span style="color:var(--secondary)">${esc(c.control)} ${esc(c.controlName)}</span>`).join("");
410
445
  return ` <div class="grid row">
411
446
  <div class="seq">${entry.seq}</div>
412
447
  <div class="call">${esc(call)}</div>
413
448
  <div class="actor">${esc(actor)}</div>
414
- <div class="clause">${esc(entry.rule ?? "—")}</div>
449
+ <div class="clause">${esc(entry.rule ?? "—")}${cited}</div>
415
450
  <div class="verdict v-${esc(entry.verdict)}">${esc(VERDICT_LABEL$1[entry.verdict] ?? entry.verdict)}</div>
416
451
  <div class="seal">${esc(seal(entry))}</div>
417
452
  </div>`;
@@ -452,7 +487,7 @@ function renderLedgerView(entries, verification, options) {
452
487
  const table = ` <div class="grid thead">
453
488
  <div>SEQ</div><div>CALL</div><div>ACTOR</div><div>CLAUSE</div><div>VERDICT</div><div>SEAL</div>
454
489
  </div>
455
- ${entries.length === 0 ? " <div class=\"empty\">No decisions recorded yet.</div>" : [...entries].reverse().map(row).join("\n")}`;
490
+ ${entries.length === 0 ? " <div class=\"empty\">No decisions recorded yet.</div>" : [...entries].reverse().map((entry) => row(entry, options.pack)).join("\n")}`;
456
491
  const first = held.at(-1);
457
492
  const awaiting = first === void 0 ? "" : ` <section class="await">
458
493
  <div class="k">AWAITING HUMAN · 1 OF ${held.length}</div>
@@ -874,8 +909,11 @@ const USAGE = `prae — operator policy for agent harnesses
874
909
  Check one receipt with nothing but the file: recompute its hash, verify
875
910
  its signature, and print the key fingerprint to compare against your own.
876
911
 
877
- prae report <ledger.jsonl> [--out <file.html>]
878
- Render the decision record as a printable evidence report.
912
+ prae report <ledger.jsonl> [--out <file.html>] [--pack <pack.json>]
913
+ Render the decision record as a printable evidence report. With
914
+ --pack, each clause also names the framework risk it addresses
915
+ (OWASP LLM Top 10 2026), so a reviewer can trace risk → control →
916
+ decision → seal.
879
917
 
880
918
  prae console <ledger.jsonl> --out <dir> [--pack <file>] [--serve [port]]
881
919
  [--instructions <dir>]
@@ -925,6 +963,7 @@ async function check(path) {
925
963
  for (const rule of pack.rules) {
926
964
  const label = rule.title ?? rule.effect.because;
927
965
  console.log(` ${rule.id.padEnd(10)} ${rule.effect.kind.toUpperCase().padEnd(8)} ${label}`);
966
+ for (const c of cite(rule)) console.log(` ${"".padEnd(10)} ${"".padEnd(8)} ↳ ${c.frameworkShort} · ${c.control} ${c.controlName}`);
928
967
  }
929
968
  }
930
969
  /**
@@ -982,7 +1021,7 @@ async function mcp(root, packPath, ledgerPath) {
982
1021
  const chosen = packPath ?? bundled;
983
1022
  if (chosen === void 0) return fail("mcp needs --pack <file>; no bundled secret-catcher found");
984
1023
  const pack = await loadPack(chosen);
985
- const { Ledger: Ledger$1 } = await import("./src-Kubj3MTX.js");
1024
+ const { Ledger: Ledger$1 } = await import("./src-C9L6NBvu.js");
986
1025
  const ledger = ledgerPath === void 0 ? void 0 : Ledger$1.open(ledgerPath);
987
1026
  console.error(`prae mcp: serving ${resolve$1(root)} under ${pack.pack} ${pack.version}` + (ledgerPath === void 0 ? " (no ledger)" : ` · ledger ${ledgerPath}`));
988
1027
  await serveMcp({
@@ -1034,7 +1073,7 @@ async function explain(path, tool, pairs) {
1034
1073
  * a key that arrived beside it is checked against nothing.
1035
1074
  */
1036
1075
  async function keygen(dir) {
1037
- const { generateKeypair, fingerprint } = await import("./src-Kubj3MTX.js");
1076
+ const { generateKeypair, fingerprint } = await import("./src-C9L6NBvu.js");
1038
1077
  const { writeFileSync: writeFileSync$1 } = await import("node:fs");
1039
1078
  const keys = generateKeypair();
1040
1079
  const priv = join(dir, "prae-signing.pem");
@@ -1050,7 +1089,7 @@ async function keygen(dir) {
1050
1089
  console.log("private key with `signingKey` to sign every row.");
1051
1090
  }
1052
1091
  async function receipt(path, seq, keyPath, out) {
1053
- const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-Kubj3MTX.js");
1092
+ const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-C9L6NBvu.js");
1054
1093
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1055
1094
  const { createPublicKey } = await import("node:crypto");
1056
1095
  const privateKey = readFileSync$1(keyPath, "utf8");
@@ -1080,7 +1119,7 @@ async function receipt(path, seq, keyPath, out) {
1080
1119
  * file and sets the exit code, so scripts can branch on it.
1081
1120
  */
1082
1121
  async function verifyReceiptCmd(path) {
1083
- const { checkReceipt } = await import("./receipt-check-Dql0uqxw.js");
1122
+ const { checkReceipt } = await import("./receipt-check-DrDE3Wwi.js");
1084
1123
  let parsed;
1085
1124
  try {
1086
1125
  parsed = JSON.parse(await readFile(path, "utf8"));
@@ -1151,7 +1190,7 @@ async function verify(path, keyPath) {
1151
1190
  const last = entries.at(-1);
1152
1191
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1153
1192
  if (keyPath !== void 0) {
1154
- const { verifySignature, fingerprint } = await import("./src-Kubj3MTX.js");
1193
+ const { verifySignature, fingerprint } = await import("./src-C9L6NBvu.js");
1155
1194
  const { readFileSync: readFileSync$1 } = await import("node:fs");
1156
1195
  const publicKey = readFileSync$1(keyPath, "utf8");
1157
1196
  let signed = 0;
@@ -1190,18 +1229,20 @@ function flag(argv, name) {
1190
1229
  * has to go and check separately. A report that looks authoritative while
1191
1230
  * quietly sitting on a broken chain is worse than no report.
1192
1231
  */
1193
- async function report(path, out) {
1232
+ async function report(path, out, packPath) {
1194
1233
  let ledger;
1195
1234
  try {
1196
1235
  ledger = Ledger.open(path);
1197
1236
  } catch (error) {
1198
1237
  return fail(error instanceof Error ? error.message : String(error));
1199
1238
  }
1239
+ const pack = packPath === void 0 ? void 0 : await loadPack(packPath);
1200
1240
  const entries = ledger.read();
1201
1241
  const verification = ledger.verify();
1202
1242
  const html = renderLedgerView(entries, verification, {
1203
1243
  ledgerPath: path,
1204
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1244
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1245
+ ...pack === void 0 ? {} : { pack }
1205
1246
  });
1206
1247
  if (out === void 0) {
1207
1248
  process.stdout.write(html);
@@ -1325,7 +1366,7 @@ async function consoleOut(path, argv) {
1325
1366
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1326
1367
  import("./src-CraUX64d.js"),
1327
1368
  import("./src-CQ0UmFUK.js"),
1328
- import("./instructions-D4VOn2lk.js")
1369
+ import("./instructions-oGCdVxHM.js")
1329
1370
  ]);
1330
1371
  const read = ingest(instructionsDir);
1331
1372
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1460,7 +1501,7 @@ async function main(argv) {
1460
1501
  case "report": {
1461
1502
  const path = rest[0];
1462
1503
  if (path === void 0) fail("report needs a ledger file");
1463
- return report(path, flag(rest, "out"));
1504
+ return report(path, flag(rest, "out"), flag(rest, "pack"));
1464
1505
  }
1465
1506
  case "console": {
1466
1507
  const path = rest[0];
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./evaluate-CkYySl7T.js";
2
- import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-Cr0U-PFD.js";
3
- import "./src-CXU0_W1P.js";
4
- import "./views-DalPkBZZ.js";
2
+ import "./views-CAD9738P.js";
3
+ import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-BUgU-LpN.js";
4
+ import "./src-Ciggx9ja.js";
5
5
 
6
6
  export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
@@ -1,4 +1,4 @@
1
- import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-DalPkBZZ.js";
1
+ import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-CAD9738P.js";
2
2
 
3
3
  //#region src/console/instructions.ts
4
4
  const STYLE = `
package/lib/playground.js CHANGED
@@ -466,6 +466,55 @@
466
466
  };
467
467
  }
468
468
 
469
+ //#endregion
470
+ //#region ../kernel/src/frameworks.ts
471
+ const FRAMEWORKS = { "owasp-llm-top-10-2026": {
472
+ name: "OWASP Top 10 for LLM Applications — 2026",
473
+ short: "OWASP LLM 2026",
474
+ controls: {
475
+ LLM01: {
476
+ name: "Prompt Injection",
477
+ summary: "Instructions in a prompt or in retrieved content alter what the model does."
478
+ },
479
+ LLM02: {
480
+ name: "Sensitive Information Disclosure",
481
+ summary: "Credentials, personal data or proprietary content reach the model or its output."
482
+ },
483
+ LLM03: {
484
+ name: "Excessive Agency",
485
+ summary: "The model can take actions, or holds permissions, beyond what its task needs."
486
+ },
487
+ LLM04: {
488
+ name: "Supply Chain",
489
+ summary: "A compromised model, dataset, plugin or dependency enters the application."
490
+ },
491
+ LLM05: {
492
+ name: "Data and Model Poisoning",
493
+ summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
494
+ },
495
+ LLM06: {
496
+ name: "Unbounded Consumption",
497
+ summary: "The application permits resource use without a limit, exhausting budget or service."
498
+ },
499
+ LLM07: {
500
+ name: "Misinformation",
501
+ summary: "The model produces false or misleading content that is acted on as if true."
502
+ },
503
+ LLM08: {
504
+ name: "Hidden Context Exposure",
505
+ summary: "System prompts, hidden instructions or internal context are revealed."
506
+ },
507
+ LLM09: {
508
+ name: "Vector and Embedding Weaknesses",
509
+ summary: "Retrieval stores are manipulated or leak across tenants and permissions."
510
+ },
511
+ LLM10: {
512
+ name: "Improper Output Handling",
513
+ summary: "Model output is passed to downstream systems without validation."
514
+ }
515
+ }
516
+ } };
517
+
469
518
  //#endregion
470
519
  //#region ../kernel/src/pack.ts
471
520
  function isObject(value) {
@@ -761,7 +810,8 @@
761
810
  "id",
762
811
  "title",
763
812
  "when",
764
- "effect"
813
+ "effect",
814
+ "frameworks"
765
815
  ]);
766
816
  const id = check.text(`${path}.id`, value.id);
767
817
  const effect = readEffect(check, `${path}.effect`, value.effect);
@@ -777,8 +827,40 @@
777
827
  const title = check.text(`${path}.title`, value.title);
778
828
  if (title !== void 0) rule.title = title;
779
829
  }
830
+ if (value.frameworks !== void 0) {
831
+ const frameworks = readFrameworks(check, `${path}.frameworks`, value.frameworks);
832
+ if (frameworks !== void 0) rule.frameworks = frameworks;
833
+ }
780
834
  return rule;
781
835
  }
836
+ /**
837
+ * A citation names a framework edition and controls the registry knows. A
838
+ * typo here would be a rule that claims a mapping no view can render, so it
839
+ * is a pack error rather than an ignored key — the same line the rest of the
840
+ * parser takes.
841
+ */
842
+ function readFrameworks(check, path, value) {
843
+ if (!isObject(value)) {
844
+ check.fail(path, "expected an object keyed by framework id");
845
+ return;
846
+ }
847
+ const out = {};
848
+ for (const [framework, controls] of Object.entries(value)) {
849
+ const known = FRAMEWORKS[framework];
850
+ if (known === void 0) {
851
+ check.fail(`${path}.${framework}`, `unknown framework; expected one of ${Object.keys(FRAMEWORKS).map((k) => `\`${k}\``).join(", ")}`);
852
+ continue;
853
+ }
854
+ if (!Array.isArray(controls) || !controls.every((c) => typeof c === "string")) {
855
+ check.fail(`${path}.${framework}`, "expected an array of control ids");
856
+ continue;
857
+ }
858
+ const ids = controls;
859
+ for (const control of ids) if (known.controls[control] === void 0) check.fail(`${path}.${framework}`, `unknown control \`${control}\` in ${known.name}`);
860
+ if (ids.length > 0) out[framework] = [...new Set(ids)];
861
+ }
862
+ return out;
863
+ }
782
864
  /** Parse and validate a pack. Never throws; every problem lands in `errors`. */
783
865
  function parsePack(source) {
784
866
  let raw;
@@ -1,4 +1,4 @@
1
- import { c as verifyReceipt, i as fingerprint } from "./src-CXU0_W1P.js";
1
+ import { c as verifyReceipt, i as fingerprint } from "./src-Ciggx9ja.js";
2
2
 
3
3
  //#region src/receipt-check.ts
4
4
  /** Shape we require before we will call it a receipt at all. */
@@ -17,11 +17,16 @@
17
17
  "./**/*.sample"
18
18
  ]
19
19
  },
20
- "secrets": { "minConfidence": 0.8 }
20
+ "secrets": {
21
+ "minConfidence": 0.8
22
+ }
21
23
  },
22
24
  "effect": {
23
25
  "kind": "deny",
24
26
  "because": "the requested file contains a protected credential; the contents were not returned"
27
+ },
28
+ "frameworks": {
29
+ "owasp-llm-top-10-2026": ["LLM02"]
25
30
  }
26
31
  },
27
32
  {
@@ -29,17 +34,27 @@
29
34
  "title": "Private keys are refused everywhere",
30
35
  "when": {
31
36
  "capability": "fs:read",
32
- "secrets": { "types": ["private-key"] }
37
+ "secrets": {
38
+ "types": ["private-key"]
39
+ }
33
40
  },
34
41
  "effect": {
35
42
  "kind": "deny",
36
43
  "because": "the requested file contains a private key; the contents were not returned"
44
+ },
45
+ "frameworks": {
46
+ "owasp-llm-top-10-2026": ["LLM02"]
37
47
  }
38
48
  },
39
49
  {
40
50
  "id": "SEC-003",
41
51
  "title": "Anything credential-shaped is at least recorded",
42
- "when": { "capability": "fs:read", "secrets": { "minConfidence": 0.5 } },
52
+ "when": {
53
+ "capability": "fs:read",
54
+ "secrets": {
55
+ "minConfidence": 0.5
56
+ }
57
+ },
43
58
  "effect": {
44
59
  "kind": "warn",
45
60
  "because": "a possible credential was noticed in this file; where no stronger rule fired, the read proceeded and this row is the record"
@@ -1,3 +1,3 @@
1
- import { a as generateKeypair, c as verifyReceipt, d as canonical, f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, o as makeReceipt, p as hashEntry, r as verifyEntries, s as signEntry, t as Ledger, u as GENESIS } from "./src-CXU0_W1P.js";
1
+ import { a as generateKeypair, c as verifyReceipt, d as canonical, f as chain, i as fingerprint, l as verifySignature, m as seal, n as readEntries, o as makeReceipt, p as hashEntry, r as verifyEntries, s as signEntry, t as Ledger, u as GENESIS } from "./src-Ciggx9ja.js";
2
2
 
3
3
  export { GENESIS, Ledger, canonical, chain, fingerprint, generateKeypair, hashEntry, makeReceipt, readEntries, seal, signEntry, verifyEntries, verifyReceipt, verifySignature };
@@ -1,3 +1,72 @@
1
+ //#region ../kernel/src/frameworks.ts
2
+ const FRAMEWORKS = { "owasp-llm-top-10-2026": {
3
+ name: "OWASP Top 10 for LLM Applications — 2026",
4
+ short: "OWASP LLM 2026",
5
+ controls: {
6
+ LLM01: {
7
+ name: "Prompt Injection",
8
+ summary: "Instructions in a prompt or in retrieved content alter what the model does."
9
+ },
10
+ LLM02: {
11
+ name: "Sensitive Information Disclosure",
12
+ summary: "Credentials, personal data or proprietary content reach the model or its output."
13
+ },
14
+ LLM03: {
15
+ name: "Excessive Agency",
16
+ summary: "The model can take actions, or holds permissions, beyond what its task needs."
17
+ },
18
+ LLM04: {
19
+ name: "Supply Chain",
20
+ summary: "A compromised model, dataset, plugin or dependency enters the application."
21
+ },
22
+ LLM05: {
23
+ name: "Data and Model Poisoning",
24
+ summary: "Training, fine-tuning or embedding data is manipulated to change behaviour."
25
+ },
26
+ LLM06: {
27
+ name: "Unbounded Consumption",
28
+ summary: "The application permits resource use without a limit, exhausting budget or service."
29
+ },
30
+ LLM07: {
31
+ name: "Misinformation",
32
+ summary: "The model produces false or misleading content that is acted on as if true."
33
+ },
34
+ LLM08: {
35
+ name: "Hidden Context Exposure",
36
+ summary: "System prompts, hidden instructions or internal context are revealed."
37
+ },
38
+ LLM09: {
39
+ name: "Vector and Embedding Weaknesses",
40
+ summary: "Retrieval stores are manipulated or leak across tenants and permissions."
41
+ },
42
+ LLM10: {
43
+ name: "Improper Output Handling",
44
+ summary: "Model output is passed to downstream systems without validation."
45
+ }
46
+ }
47
+ } };
48
+ /**
49
+ * Every framework control a rule cites, resolved to names.
50
+ *
51
+ * A pack that parsed cites only ids the registry knows, so the lookups here
52
+ * cannot miss; the fallbacks exist for a `Rule` built by hand in code.
53
+ */
54
+ function cite(rule) {
55
+ const out = [];
56
+ for (const [framework, controls] of Object.entries(rule.frameworks ?? {})) {
57
+ const known = FRAMEWORKS[framework];
58
+ for (const control of controls) out.push({
59
+ framework,
60
+ frameworkName: known?.name ?? framework,
61
+ frameworkShort: known?.short ?? framework,
62
+ control,
63
+ controlName: known?.controls[control]?.name ?? control
64
+ });
65
+ }
66
+ return out;
67
+ }
68
+
69
+ //#endregion
1
70
  //#region src/console/logo.ts
2
71
  /**
3
72
  * The mark, inlined.
@@ -629,7 +698,7 @@ function renderNav(items, active) {
629
698
  * the obligation, and a tool that told them the answer would be selling the
630
699
  * one thing it cannot supply.
631
700
  */
632
- const CONTROLS = [
701
+ const OBLIGATIONS = [
633
702
  {
634
703
  id: "EU AI Act art. 12",
635
704
  asks: "Automatic recording of events over the lifetime of the system, to a degree that enables tracing.",
@@ -671,6 +740,71 @@ const CONTROLS = [
671
740
  })
672
741
  }
673
742
  ];
743
+ const OWASP = "owasp-llm-top-10-2026";
744
+ /** Rules in the pack that cite one control of one framework. */
745
+ function citing(pack, framework, control) {
746
+ return (pack?.rules ?? []).filter((rule) => (rule.frameworks?.[framework] ?? []).includes(control));
747
+ }
748
+ /**
749
+ * Why a runtime execution gate is not the control for a risk no rule cites.
750
+ *
751
+ * Said per risk rather than as one generic line, because "out of scope" with
752
+ * no reason reads as an excuse. Each of these is a boundary a reader can
753
+ * check: the risk lives somewhere a tool-call gate does not stand.
754
+ */
755
+ const NOT_THIS_BOUNDARY = {
756
+ LLM04: "Where a model or dependency came from is settled before an agent runs; the gate sees the call, not the provenance.",
757
+ LLM05: "Training and fine-tuning data sit outside the execution boundary this gate stands on.",
758
+ LLM07: "Whether an answer is true is not a property of a tool call.",
759
+ 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.",
760
+ LLM09: "Retrieval stores are upstream of the call the gate judges.",
761
+ LLM10: "What downstream code does with model output is that code's boundary, not this one's."
762
+ };
763
+ function owaspRow(control) {
764
+ const framework = FRAMEWORKS[OWASP];
765
+ const risk = framework?.controls[control];
766
+ const name = risk?.name ?? control;
767
+ return {
768
+ id: `${framework?.short ?? OWASP} · ${control}`,
769
+ asks: `${name}. ${risk?.summary ?? ""}`.trim(),
770
+ evidence: (data) => {
771
+ if (data.pack === void 0) return {
772
+ state: "absent",
773
+ note: "No pack supplied to this view."
774
+ };
775
+ if (control === "LLM01") {
776
+ const bounding = citing(data.pack, OWASP, "LLM03");
777
+ return {
778
+ state: "partial",
779
+ note: bounding.length === 0 ? "Not mitigated: this gate does not detect injected instructions, and no LLM03 control in this pack bounds what one can execute." : `Not mitigated: this gate does not detect injected instructions. Bounded: ${bounding.length} LLM03 control${bounding.length === 1 ? "" : "s"} (${bounding.map((r) => r.id).join(", ")}) decide what an injected instruction can execute.`
780
+ };
781
+ }
782
+ const rules = citing(data.pack, OWASP, control);
783
+ if (rules.length === 0) return {
784
+ state: "absent",
785
+ note: NOT_THIS_BOUNDARY[control] ?? "No rule in this pack cites this risk."
786
+ };
787
+ const ids = new Set(rules.map((rule) => rule.id));
788
+ const decisions = data.entries.filter((entry) => entry.rule !== void 0 && ids.has(entry.rule));
789
+ const listed = rules.map((rule) => rule.id).join(", ");
790
+ return decisions.length > 0 ? {
791
+ state: "covered",
792
+ note: `${rules.length} control${rules.length === 1 ? "" : "s"} mapped (${listed}) · ${decisions.length} sealed decision${decisions.length === 1 ? "" : "s"} under them in this record.`
793
+ } : {
794
+ state: "partial",
795
+ 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.`
796
+ };
797
+ }
798
+ };
799
+ }
800
+ const OWASP_ROWS = Object.keys(FRAMEWORKS[OWASP]?.controls ?? {}).map(owaspRow);
801
+ /**
802
+ * The obligations first, then the OWASP risks. Two kinds of row in one table
803
+ * on purpose: an auditor reads down one list and finds the audit obligations
804
+ * the ledger evidences and the application risks the pack's rules address,
805
+ * each stated in its framework's own words.
806
+ */
807
+ const CONTROLS = [...OBLIGATIONS, ...OWASP_ROWS];
674
808
 
675
809
  //#endregion
676
810
  //#region src/console/views.ts
@@ -789,11 +923,17 @@ function renderPacks(data) {
789
923
  });
790
924
  const baseline = pack.baseline?.capabilities;
791
925
  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">
926
+ const cited = /* @__PURE__ */ new Map();
927
+ for (const rule of pack.rules) for (const c of cite(rule)) cited.set(c.framework, c.frameworkName);
928
+ const frameworks = cited.size === 0 ? "" : `<div class="caps">${[...cited.values()].map((name) => `<span class="cap">${esc(name)}</span>`).join("")}</div>`;
929
+ const rows = pack.rules.map((rule) => {
930
+ const tags = cite(rule).map((c) => `<br><span style="color:var(--secondary)">${esc(c.frameworkShort)} · ${esc(c.control)} ${esc(c.controlName)}</span>`).join("");
931
+ return ` <div class="grid rules row">
793
932
  <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>
933
+ <div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span>${tags}</div>
795
934
  <div class="effect e-${esc(rule.effect.kind)}">${esc(rule.effect.kind.toUpperCase())}</div>
796
- </div>`).join("\n");
935
+ </div>`;
936
+ }).join("\n");
797
937
  const body = ` <section class="sect">
798
938
  <h2>${esc(pack.pack)} <span style="font-family:var(--proof);font-size:14px;color:var(--muted)">${esc(pack.version)}</span></h2>
799
939
  <p>The operator baseline is intersected with whatever a plugin's own
@@ -801,6 +941,7 @@ function renderPacks(data) {
801
941
  grant a capability an author never asked for.</p>
802
942
  </section>
803
943
  ${caps}
944
+ ${frameworks}
804
945
  <div class="grid rules thead">
805
946
  <div>CLAUSE</div><div>RULE</div><div>EFFECT</div>
806
947
  </div>
@@ -840,11 +981,17 @@ ${CONTROLS.map((control) => {
840
981
  </div>`;
841
982
  }).join("\n")}
842
983
  <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>
984
+ <h2>Reading the states</h2>
985
+ <p>For the audit obligations, several rows stop at <em>partial</em> for
986
+ one reason: the harness records what was decided and never who decided
987
+ it. Attributable approval needs an identity anchor outside this machine,
988
+ which is a control-plane concern this build does not claim.</p>
989
+ <p>For the OWASP risks, <em>evidence</em> means rules in this pack cite
990
+ the risk and have decided calls in this record; <em>partial</em> means
991
+ they cite it and have not yet, or — for prompt injection — that the gate
992
+ bounds the risk without claiming to detect it; <em>none</em> means no
993
+ rule cites it, with the reason a tool-call gate is not where that risk
994
+ is controlled.</p>
848
995
  </section>`;
849
996
  return renderShell({
850
997
  title: "Compliance map",
@@ -926,4 +1073,4 @@ ${history}`;
926
1073
  }
927
1074
 
928
1075
  //#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 };
1076
+ export { renderPacks as a, renderShell as c, cite as d, renderOverview as i, VERDICT_LABEL as l, renderApprovals as n, esc as o, renderCompliance as r, renderNav as s, navFor as t, FRAMEWORKS as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prae-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Operator policy for agent harnesses: validate packs, explain a decision, test a pack against security invariants, verify a decision log, and provision a governed DSH profile.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -41,9 +41,9 @@
41
41
  "typescript": "^5.9.2",
42
42
  "vitest": "^3.2.4",
43
43
  "@prae/controls": "0.1.0",
44
+ "@prae/extract": "0.1.0",
44
45
  "@prae/inspect": "0.1.0",
45
46
  "@prae/instructions": "0.1.0",
46
- "@prae/extract": "0.1.0",
47
47
  "@prae/kernel": "0.1.0",
48
48
  "@prae/ledger": "0.1.0",
49
49
  "@prae/tokens": "0.1.0"
File without changes