prae-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,149 @@
1
+ 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-BU0sOciR.js";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
3
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
4
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
3
5
  import { createHash } from "node:crypto";
4
- import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
6
  import { homedir } from "node:os";
6
7
  import { composePatchFile, preflightOps, revisionOf, takeSnapshot } from "dsh-overlay-check";
7
8
  import { createServer } from "node:http";
8
9
 
10
+ //#region ../kernel/src/command.ts
11
+ /** Wrappers whose first operand is the program actually being run. */
12
+ const WRAPPERS = new Set([
13
+ "sudo",
14
+ "command",
15
+ "env",
16
+ "nohup",
17
+ "time",
18
+ "xargs"
19
+ ]);
20
+ /** Wrapper options that consume the next token, so it is not the program. */
21
+ const OPTION_WITH_VALUE = new Set([
22
+ "-u",
23
+ "-g",
24
+ "-p",
25
+ "-h",
26
+ "-C",
27
+ "-r",
28
+ "-t",
29
+ "-U",
30
+ "-I",
31
+ "-n",
32
+ "-P",
33
+ "-L",
34
+ "-s"
35
+ ]);
36
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
37
+ /**
38
+ * Split a line into pipeline segments of words, one scan, quote-aware.
39
+ *
40
+ * Splitting on `|` before parsing quotes is the classic mistake — it turns
41
+ * `echo "a | b"` into two segments — so quoting and separators are read
42
+ * together.
43
+ */
44
+ function scan(command) {
45
+ const parts = [];
46
+ let tokens = [];
47
+ let current = "";
48
+ let quote;
49
+ const endToken = () => {
50
+ if (current !== "") tokens.push(current);
51
+ current = "";
52
+ };
53
+ const endSegment = () => {
54
+ endToken();
55
+ if (tokens.length > 0) parts.push(tokens);
56
+ tokens = [];
57
+ };
58
+ for (let at = 0; at < command.length; at += 1) {
59
+ const ch = command[at];
60
+ if (quote !== void 0) {
61
+ if (ch === quote) quote = void 0;
62
+ else current += ch;
63
+ continue;
64
+ }
65
+ if (ch === "'" || ch === "\"") {
66
+ quote = ch;
67
+ continue;
68
+ }
69
+ if (ch === ";") {
70
+ endSegment();
71
+ continue;
72
+ }
73
+ if (ch === "|" || ch === "&") {
74
+ endSegment();
75
+ if (command[at + 1] === ch) at += 1;
76
+ continue;
77
+ }
78
+ if (/\s/.test(ch)) {
79
+ endToken();
80
+ continue;
81
+ }
82
+ current += ch;
83
+ }
84
+ endSegment();
85
+ return parts;
86
+ }
87
+ /** Decompose a command line into pipeline segments. */
88
+ function segments(command) {
89
+ const found = [];
90
+ for (const tokens of scan(command)) {
91
+ let at = 0;
92
+ while (at < tokens.length) {
93
+ const token = tokens[at];
94
+ if (ASSIGNMENT.test(token)) {
95
+ at += 1;
96
+ continue;
97
+ }
98
+ if (WRAPPERS.has(token)) {
99
+ at += 1;
100
+ while (at < tokens.length) {
101
+ const option = tokens[at];
102
+ if (!option.startsWith("-")) break;
103
+ at += OPTION_WITH_VALUE.has(option) ? 2 : 1;
104
+ }
105
+ continue;
106
+ }
107
+ break;
108
+ }
109
+ const name = tokens[at];
110
+ if (name === void 0) continue;
111
+ found.push({
112
+ name: (name.split("/").pop() ?? name).toLowerCase(),
113
+ tokens: tokens.slice(at + 1)
114
+ });
115
+ }
116
+ return found;
117
+ }
118
+ /** `git*` matches `git`, `github`; bare strings match exactly. */
119
+ function matches$1(pattern, value) {
120
+ if (pattern.endsWith("*")) return value.startsWith(pattern.slice(0, -1));
121
+ return value === pattern;
122
+ }
123
+ const list = (v) => v === void 0 ? [] : typeof v === "string" ? [v] : v;
124
+ /**
125
+ * Judge one command line against one matcher. Every present field must hold.
126
+ */
127
+ function commandMatches(match, command) {
128
+ const parts = segments(command);
129
+ if (parts.length === 0) return false;
130
+ const names = list(match.name);
131
+ const args = list(match.token);
132
+ const pipes = list(match.pipedTo);
133
+ if (names.length > 0) {
134
+ const hit = parts.filter((segment) => names.some((pattern) => matches$1(pattern, segment.name)));
135
+ if (hit.length === 0) return false;
136
+ if (args.length > 0 && !hit.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
137
+ } else if (args.length > 0) {
138
+ if (!parts.some((segment) => args.every((wanted) => segment.tokens.some((token) => matches$1(wanted, token))))) return false;
139
+ }
140
+ if (pipes.length > 0) {
141
+ if (!parts.slice(1).some((segment) => pipes.some((pattern) => matches$1(pattern, segment.name)))) return false;
142
+ }
143
+ return true;
144
+ }
145
+
146
+ //#endregion
9
147
  //#region ../kernel/src/path.ts
10
148
  /**
11
149
  * Path normalization and glob containment, with no filesystem access.
@@ -97,6 +235,8 @@ function matchSegments(path, pattern) {
97
235
  function within(path, glob) {
98
236
  const target = normalize$1(path);
99
237
  if (target.escaped) return false;
238
+ const trimmed = glob.trim();
239
+ if (!(trimmed === "**" || trimmed.startsWith("**/")) && target.absolute !== trimmed.startsWith("/")) return false;
100
240
  const pattern = normalize$1(glob);
101
241
  if (matchSegments(target.segments, pattern.segments)) return true;
102
242
  return matchSegments(target.segments, [...pattern.segments, "**"]);
@@ -128,8 +268,8 @@ function hostOf(url) {
128
268
  * an operator writing the suffix form means the organisation, not a rule that
129
269
  * covers every subdomain except the one they first thought of.
130
270
  */
131
- function hostIn(host, list) {
132
- return list.some((entry) => {
271
+ function hostIn(host, list$1) {
272
+ return list$1.some((entry) => {
133
273
  const want = entry.toLowerCase();
134
274
  if (want.startsWith(".")) return host === want.slice(1) || host.endsWith(want);
135
275
  return host === want;
@@ -182,6 +322,11 @@ function matches(when, facts) {
182
322
  if (!listed(facts.capability, when.capability)) return false;
183
323
  if (!listed(facts.plugin, when.plugin)) return false;
184
324
  if (!listed(facts.agent, when.agent)) return false;
325
+ if (when.command !== void 0) {
326
+ const raw = facts.args.command ?? facts.args.cmd ?? facts.args.script;
327
+ if (typeof raw !== "string") return false;
328
+ if (!commandMatches(when.command, raw)) return false;
329
+ }
185
330
  if (when.path !== void 0) {
186
331
  const raw = facts.args[when.path.arg ?? "path"];
187
332
  if (typeof raw !== "string") return false;
@@ -331,6 +476,32 @@ var Check = class {
331
476
  return value;
332
477
  }
333
478
  };
479
+ function readCommandMatch(check$1, path, value) {
480
+ if (!isObject(value)) {
481
+ check$1.fail(path, "expected an object");
482
+ return;
483
+ }
484
+ check$1.keys(path, value, [
485
+ "name",
486
+ "token",
487
+ "pipedTo"
488
+ ]);
489
+ if (value.name === void 0 && value.token === void 0 && value.pipedTo === void 0) check$1.fail(path, "expected `name`, `token` or `pipedTo`; a command matcher with none matches nothing");
490
+ const out = {};
491
+ if (value.token !== void 0) {
492
+ const tokens = check$1.names(`${path}.token`, value.token);
493
+ if (tokens !== void 0) out.token = tokens;
494
+ }
495
+ if (value.name !== void 0) {
496
+ const names = check$1.names(`${path}.name`, value.name);
497
+ if (names !== void 0) out.name = names;
498
+ }
499
+ if (value.pipedTo !== void 0) {
500
+ const pipes = check$1.names(`${path}.pipedTo`, value.pipedTo);
501
+ if (pipes !== void 0) out.pipedTo = pipes;
502
+ }
503
+ return out;
504
+ }
334
505
  function readPathMatch(check$1, path, value) {
335
506
  if (!isObject(value)) {
336
507
  check$1.fail(path, "expected an object");
@@ -374,12 +545,12 @@ function readHostMatch(check$1, path, value) {
374
545
  if (arg !== void 0) out.arg = arg;
375
546
  }
376
547
  if (value.in !== void 0) {
377
- const list = check$1.globs(`${path}.in`, value.in);
378
- if (list !== void 0) out.in = list;
548
+ const list$1 = check$1.globs(`${path}.in`, value.in);
549
+ if (list$1 !== void 0) out.in = list$1;
379
550
  }
380
551
  if (value.notIn !== void 0) {
381
- const list = check$1.globs(`${path}.notIn`, value.notIn);
382
- if (list !== void 0) out.notIn = list;
552
+ const list$1 = check$1.globs(`${path}.notIn`, value.notIn);
553
+ if (list$1 !== void 0) out.notIn = list$1;
383
554
  }
384
555
  return out;
385
556
  }
@@ -424,6 +595,7 @@ function readMatch(check$1, path, value) {
424
595
  "capability",
425
596
  "plugin",
426
597
  "agent",
598
+ "command",
427
599
  "path",
428
600
  "host",
429
601
  "arg",
@@ -439,6 +611,10 @@ function readMatch(check$1, path, value) {
439
611
  const names = check$1.names(`${path}.${key}`, value[key]);
440
612
  if (names !== void 0) out[key] = names;
441
613
  }
614
+ if (value.command !== void 0) {
615
+ const match = readCommandMatch(check$1, `${path}.command`, value.command);
616
+ if (match !== void 0) out.command = match;
617
+ }
442
618
  if (value.path !== void 0) {
443
619
  const match = readPathMatch(check$1, `${path}.path`, value.path);
444
620
  if (match !== void 0) out.path = match;
@@ -449,12 +625,12 @@ function readMatch(check$1, path, value) {
449
625
  }
450
626
  if (value.arg !== void 0) if (!Array.isArray(value.arg)) check$1.fail(`${path}.arg`, "expected an array of argument matchers");
451
627
  else {
452
- const list = [];
628
+ const list$1 = [];
453
629
  value.arg.forEach((entry, index) => {
454
630
  const one = readArgMatch(check$1, `${path}.arg[${index}]`, entry);
455
- if (one !== void 0) list.push(one);
631
+ if (one !== void 0) list$1.push(one);
456
632
  });
457
- out.arg = list;
633
+ out.arg = list$1;
458
634
  }
459
635
  if (value.count !== void 0) if (!isObject(value.count)) check$1.fail(`${path}.count`, "expected an object");
460
636
  else {
@@ -757,930 +933,6 @@ var Ledger = class Ledger {
757
933
  }
758
934
  };
759
935
 
760
- //#endregion
761
- //#region src/console/logo.ts
762
- /**
763
- * The mark, inlined.
764
- *
765
- * A P drawn as a door: the product is the moment permission is granted,
766
- * and the glyph is the gate it comes through. Inlined as a data URI rather
767
- * than linked because these pages are read offline and mailed around as
768
- * evidence — a page that fetches an image renders wrong on the machine that
769
- * matters.
770
- */
771
- const LOGO = "data:image/png;base64,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";
772
-
773
- //#endregion
774
- //#region ../tokens/src/palette.ts
775
- /**
776
- * The two grounds, as declaration blocks.
777
- *
778
- * The console renders twice from one design: `prae console --out` inlines its
779
- * stylesheet into standalone documents, and the web app links a real one. Both
780
- * need these values, so they are defined here once and the test in this
781
- * package asserts the stylesheet agrees — the same guard the rest of the
782
- * tokens already have, for the same reason.
783
- *
784
- * Names are unprefixed because these are consumed as a surface's own palette
785
- * rather than alongside another product's. `tokens.css` carries the prefixed
786
- * `--prae-*` set for anyone embedding PRAE inside their own page.
787
- */
788
- /** §06 — Surface B, the ledger, on paper. Printable and high-contrast. */
789
- const PAPER = ` --signal: oklch(0.45 0.16 140);
790
- --signal-bright: oklch(0.5 0.16 140);
791
- --signal-edge: oklch(0.5 0.13 140);
792
- --signal-text: oklch(0.42 0.14 140);
793
- --signal-on: oklch(0.985 0.003 100);
794
- --hold: oklch(0.55 0.15 55);
795
- --veto: oklch(0.52 0.19 25);
796
- --veto-text: oklch(0.5 0.19 25);
797
- --veto-edge: oklch(0.62 0.17 25);
798
-
799
- --ground: oklch(0.975 0.003 100);
800
- --raised: oklch(0.99 0.002 100);
801
- --sunk: oklch(0.955 0.004 100);
802
- --active: oklch(0.955 0.006 130);
803
- --line: oklch(0.88 0.004 100);
804
- --line-soft: oklch(0.92 0.004 100);
805
- --line-faint: oklch(0.93 0.004 100);
806
- --line-card: oklch(0.86 0.005 100);
807
- --line-strong: oklch(0.8 0.006 100);
808
- --edge: oklch(0.32 0.01 255);
809
- --ink: oklch(0.22 0.01 255);
810
- --ink-row: oklch(0.24 0.01 255);
811
- --secondary: oklch(0.4 0.01 255);
812
- --muted: oklch(0.5 0.01 255);
813
- --muted-soft: oklch(0.55 0.01 255);
814
- --dim: oklch(0.45 0.01 255);
815
- --seal: oklch(0.6 0.01 255);
816
-
817
- --mark: none;`;
818
- /** §02 — the graphite ramp and the neon signal it is drawn for. */
819
- const FENCE = ` --signal: oklch(0.86 0.21 133);
820
- --signal-bright: oklch(0.95 0.15 133);
821
- --signal-edge: oklch(0.4 0.06 133);
822
- --signal-text: oklch(0.86 0.21 133);
823
- --signal-on: oklch(0.155 0.008 255);
824
- --hold: oklch(0.83 0.16 78);
825
- --veto: oklch(0.66 0.2 25);
826
- --veto-text: oklch(0.75 0.16 25);
827
- --veto-edge: oklch(0.45 0.12 25);
828
-
829
- --ground: oklch(0.155 0.008 255);
830
- --raised: oklch(0.185 0.009 255);
831
- --sunk: oklch(0.135 0.008 255);
832
- --active: oklch(0.215 0.022 133);
833
- --line: oklch(0.28 0.01 255);
834
- --line-soft: oklch(0.26 0.01 255);
835
- --line-faint: oklch(0.235 0.01 255);
836
- --line-card: oklch(0.3 0.01 255);
837
- --line-strong: oklch(0.32 0.01 255);
838
- --edge: oklch(0.72 0.01 255);
839
- --ink: oklch(0.96 0.004 255);
840
- --ink-row: oklch(0.9 0.006 255);
841
- --secondary: oklch(0.72 0.01 255);
842
- --muted: oklch(0.6 0.012 255);
843
- /* Lands where paper's does, at AA on its own ground: 0.55 measures 4.03
844
- against graphite, and this token labels pending and absent states that
845
- still have to be read. */
846
- --muted-soft: oklch(0.575 0.012 255);
847
- --dim: oklch(0.62 0.012 255);
848
- --seal: oklch(0.58 0.012 255);
849
-
850
- /* The mark is a solid black glyph, so it disappears on the graphite ground.
851
- Inverting it yields the white it would have been drawn as. */
852
- --mark: invert(1);`;
853
-
854
- //#endregion
855
- //#region ../tokens/src/views.ts
856
- /**
857
- * The console's view styles, as one definition.
858
- *
859
- * Header, stat band, ledger grid, rule and mapping tables, the awaiting-human
860
- * panel, and the limits block — everything that makes a PRAE view look like a
861
- * PRAE view rather than a generic table.
862
- *
863
- * Both renderers need these. `prae console --out` inlines them into standalone
864
- * documents; the web app injects them into a global style tag. Exported as a
865
- * string rather than a stylesheet so there is exactly one copy: a `.css` file
866
- * plus a mirrored constant would be two, and two copies of a stylesheet drift
867
- * the same way two copies of a palette do.
868
- *
869
- * What is *not* here is layout — the rail, the scroll containment, the routing.
870
- * The app has a persistent frame and the exporter produces separate documents,
871
- * and that is the one thing the two are genuinely allowed to disagree about.
872
- */
873
- const VIEWS_CSS = `/* ── header ───────────────────────────────────────────────────────────── */
874
- .head {
875
- display: flex; align-items: flex-end; justify-content: space-between;
876
- gap: 24px; padding: 26px 30px 22px; border-bottom: 1px solid var(--line);
877
- }
878
- .head h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.03em; margin: 0; }
879
- .head .meta {
880
- font-family: var(--proof); font-size: 11.5px; color: var(--muted); margin-top: 6px;
881
- }
882
- .actions { display: flex; align-items: center; gap: 10px; }
883
- .chain {
884
- display: flex; align-items: center; gap: 8px; padding: 7px 12px;
885
- border: 1px solid var(--signal-edge); border-radius: 999px;
886
- font-family: var(--proof); font-size: 10.5px; letter-spacing: 0.12em;
887
- color: var(--signal-text); white-space: nowrap;
888
- }
889
- .chain i { width: 6px; height: 6px; background: var(--signal-bright); border-radius: 999px; }
890
- .chain.broken { border-color: var(--veto-edge); color: var(--veto-text); }
891
- .chain.broken i { background: var(--veto); }
892
- .btn {
893
- padding: 9px 16px; border: 1px solid var(--line-strong); font-size: 13.5px;
894
- border-radius: 2px; background: none; color: inherit; font-family: inherit;
895
- white-space: nowrap;
896
- }
897
-
898
- /* ── stat band ────────────────────────────────────────────────────────── */
899
- .stats { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--line); }
900
- .stat { padding: 20px 24px; border-right: 1px solid var(--line-soft); }
901
- .stat:first-child { padding-left: 30px; }
902
- .stat:last-child { border-right: none; }
903
- .stat .v {
904
- font-size: 28px; font-weight: 700; letter-spacing: -0.03em;
905
- font-variant-numeric: tabular-nums;
906
- }
907
- .stat .v.veto { color: var(--veto); }
908
- .stat .v.hold { color: var(--hold); }
909
- .stat .k {
910
- font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
911
- color: var(--muted); margin-top: 4px;
912
- }
913
-
914
- /* ── ledger table ─────────────────────────────────────────────────────── */
915
- .grid {
916
- display: grid;
917
- grid-template-columns: 96px 1fr 150px 130px 104px 120px;
918
- gap: 16px;
919
- }
920
- .thead {
921
- padding: 12px 30px; font-family: var(--proof); font-size: 10px;
922
- letter-spacing: 0.14em; color: var(--muted); border-bottom: 1px solid var(--line);
923
- }
924
- .row {
925
- padding: 14px 30px; font-family: var(--proof); font-size: 12px;
926
- border-bottom: 1px solid var(--line-faint); align-items: center;
927
- }
928
- .row .seq { color: var(--muted-soft); font-variant-numeric: tabular-nums; }
929
- .row .call { color: var(--ink-row); overflow-wrap: anywhere; }
930
- .row .actor, .row .clause { color: var(--dim); }
931
- .row .seal { color: var(--seal); }
932
- .row .verdict { letter-spacing: 0.1em; }
933
- .v-allow { color: var(--signal-text); }
934
- .v-deny { color: var(--veto-text); }
935
- .v-ask { color: var(--hold); }
936
- .v-correct { color: var(--muted); }
937
- .empty { padding: 30px; font-family: var(--proof); font-size: 12px; color: var(--muted); }
938
-
939
- /* ── awaiting-human panel ─────────────────────────────────────────────── */
940
- .await { padding: 26px 30px 30px; background: var(--sunk); border-top: 1px solid var(--line); }
941
- .await .k {
942
- font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
943
- color: var(--muted); margin-bottom: 16px;
944
- }
945
- .card {
946
- display: flex; gap: 24px; align-items: flex-start; background: var(--raised);
947
- border: 1px solid var(--line-card); padding: 22px 24px;
948
- }
949
- .card h2 { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 8px; }
950
- .card .detail { font-family: var(--proof); font-size: 12px; color: var(--muted); line-height: 1.8; }
951
- .decide { display: flex; flex-direction: column; gap: 8px; width: 200px; flex: none; }
952
- .approve {
953
- padding: 11px 18px; background: var(--signal); color: var(--signal-on);
954
- font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
955
- border: none; font-family: inherit;
956
- }
957
- .deny {
958
- padding: 11px 18px; border: 1px solid var(--veto-edge); color: var(--veto-text);
959
- font-weight: 600; font-size: 13.5px; text-align: center; border-radius: 2px;
960
- background: none; font-family: inherit;
961
- }
962
- /*
963
- * A control that cannot act must not look like one that can. These render so
964
- * the reader knows where the decision lives, and read as inert so they never
965
- * reach for it — the harness settles approvals, and a button that looks live
966
- * here would be an invitation to a second audit trail.
967
- *
968
- * The fill is dropped rather than faded. Fading a filled button works against
969
- * paper and fails against graphite, where a dimmed green block still reads as
970
- * a live primary action; an outline reads the same on either ground.
971
- */
972
- .approve[disabled], .deny[disabled] {
973
- background: none; border: 1px solid var(--line-strong);
974
- color: var(--muted-soft); font-weight: 500; cursor: not-allowed;
975
- }
976
- .recorded {
977
- font-family: var(--proof); font-size: 10px; color: var(--muted-soft);
978
- text-align: center; letter-spacing: 0.08em;
979
- }
980
-
981
- /* ── limits ───────────────────────────────────────────────────────────── */
982
- .limits { padding: 22px 30px 28px; border-top: 1px solid var(--line); }
983
- .limits .k {
984
- font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
985
- color: var(--muted); margin-bottom: 10px;
986
- }
987
- .limits ul { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; color: var(--secondary); }
988
- .limits code { font-family: var(--proof); font-size: 12px; }
989
-
990
- /* ── rule / mapping tables, shared by the packs and compliance views ──── */
991
- .rules { display: grid; grid-template-columns: 110px 1fr 96px; gap: 16px; }
992
- .map { display: grid; grid-template-columns: 210px 1fr 96px; gap: 16px; }
993
- .sect { padding: 22px 30px 8px; }
994
- .sect h2 {
995
- font-size: 20px; font-weight: 600; letter-spacing: -0.02em; margin: 0 0 4px;
996
- }
997
- .sect p {
998
- margin: 0 0 4px; font-size: 14px; line-height: 1.6; color: var(--secondary);
999
- max-width: 76ch;
1000
- }
1001
- .caps { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 30px 22px; }
1002
- .cap {
1003
- font-family: var(--proof); font-size: 12px; padding: 6px 10px;
1004
- border: 1px solid var(--signal-edge); color: var(--signal-text);
1005
- border-radius: 2px;
1006
- }
1007
- .cap.off {
1008
- border-color: var(--line-strong); color: var(--muted-soft);
1009
- text-decoration: line-through;
1010
- }
1011
- .effect { letter-spacing: 0.1em; font-size: 11px; }
1012
- .e-deny { color: var(--veto-text); }
1013
- .e-ask { color: var(--hold); }
1014
- .e-warn { color: var(--muted); }
1015
- .e-correct { color: var(--signal-text); }
1016
- .covered { color: var(--signal-text); }
1017
- .partial { color: var(--hold); }
1018
- .absent { color: var(--muted-soft); }
1019
-
1020
- button { cursor: pointer; }
1021
- a:focus-visible, button:focus-visible { outline: 2px solid var(--signal); outline-offset: 2px; }
1022
- `;
1023
-
1024
- //#endregion
1025
- //#region ../tokens/src/index.ts
1026
- /** The Google Fonts stylesheet both surfaces link. */
1027
- 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";
1028
- /**
1029
- * Verdict chip labels. Uppercase, mono, and never rendered without a clause
1030
- * reference within 8px — §04 is explicit that a chip alone is not allowed.
1031
- */
1032
- const VERDICT_LABEL$1 = {
1033
- allow: "ALLOWED",
1034
- ask: "HELD",
1035
- deny: "DENIED",
1036
- correct: "CORRECTED"
1037
- };
1038
-
1039
- //#endregion
1040
- //#region src/console/shell.ts
1041
- function esc(value) {
1042
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1043
- }
1044
- /** The stylesheet both console views share. Tokens first, then the surface. */
1045
- /** The stylesheet both console views share. Tokens first, then the surface. */
1046
- const SHEET = `
1047
- :root {
1048
- ${PAPER}
1049
- --claim: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
1050
- --proof: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
1051
- --ease: cubic-bezier(0.2, 0, 0, 1);
1052
- --surface-motion: 180ms;
1053
- }
1054
-
1055
- /* An explicit choice, remembered. */
1056
- :root[data-theme='dark'] {
1057
- ${FENCE}
1058
- }
1059
-
1060
- /* No choice made: follow the machine, unless the reader has said light. */
1061
- @media (prefers-color-scheme: dark) {
1062
- :root:not([data-theme='light']) {
1063
- ${FENCE}
1064
- }
1065
- }
1066
-
1067
- /*
1068
- * The ground moves at the surface budget, and only while it is moving.
1069
- *
1070
- * Arming this permanently would put a 180ms fade on every hover and every
1071
- * verdict that renders, so the class goes on for one toggle and comes off.
1072
- * §01's 0ms rule is about a decision animating in; nothing is deciding here,
1073
- * the page is repainting, and a verdict that snapped while the page behind it
1074
- * faded would read as a glitch.
1075
- */
1076
- html.theming, html.theming * {
1077
- transition:
1078
- background-color var(--surface-motion) var(--ease),
1079
- border-color var(--surface-motion) var(--ease),
1080
- color var(--surface-motion) var(--ease),
1081
- fill var(--surface-motion) var(--ease) !important;
1082
- }
1083
-
1084
- * { box-sizing: border-box; }
1085
-
1086
- /*
1087
- * An application, not a document: the shell fills the viewport and only the
1088
- * content column scrolls, so the rail and the header stay where a reader left
1089
- * them. Print undoes all of it further down — the same page still has to
1090
- * flatten into evidence.
1091
- */
1092
- html, body { height: 100%; }
1093
- body {
1094
- margin: 0;
1095
- background: var(--ground);
1096
- color: var(--ink);
1097
- font-family: var(--claim);
1098
- -webkit-font-smoothing: antialiased;
1099
- overflow: hidden;
1100
- }
1101
-
1102
- .wrap { height: 100%; }
1103
- .scroll { height: 100%; }
1104
-
1105
- .console {
1106
- background: var(--ground);
1107
- display: grid;
1108
- grid-template-columns: 232px 1fr;
1109
- height: 100%;
1110
- min-height: 0;
1111
- }
1112
-
1113
- /* The one column that moves. */
1114
- .console > main {
1115
- overflow-y: auto;
1116
- overflow-x: auto;
1117
- min-height: 0;
1118
- scroll-behavior: smooth;
1119
- }
1120
-
1121
- /* ── rail ─────────────────────────────────────────────────────────────── */
1122
- .rail {
1123
- border-right: 1px solid var(--line); padding: 22px 0;
1124
- overflow-y: auto; min-height: 0;
1125
- display: flex; flex-direction: column;
1126
- }
1127
- .rail .compliance { margin-top: auto; }
1128
-
1129
- /*
1130
- * Two positions, named, rather than a switch with a state you have to infer.
1131
- * Hidden until the head script has run: without scripting it would be a
1132
- * control that does nothing.
1133
- */
1134
- .ground { display: none; margin: auto 20px 14px; }
1135
- html.js .ground { display: grid; grid-template-columns: 1fr 1fr; }
1136
- html.js .rail .compliance { margin-top: 0; }
1137
- .ground button {
1138
- font-family: var(--proof); font-size: 10px; letter-spacing: 0.14em;
1139
- padding: 6px 0; background: none; color: var(--muted);
1140
- border: 1px solid var(--line); border-radius: 0;
1141
- transition: color 120ms var(--ease), border-color 120ms var(--ease);
1142
- }
1143
- .ground button:first-child { border-radius: 2px 0 0 2px; border-right: none; }
1144
- .ground button:last-child { border-radius: 0 2px 2px 0; }
1145
- .ground button:hover { color: var(--secondary); }
1146
- :root:not([data-theme='dark']) .ground [data-ground='paper'],
1147
- :root[data-theme='dark'] .ground [data-ground='fence'] {
1148
- color: var(--ink); border-color: var(--line-strong); background: var(--active);
1149
- }
1150
- .brand { display: flex; align-items: center; gap: 10px; padding: 0 20px 22px; }
1151
- /* The canvas sizes the mark at 26px; the glyph is square and needs no frame. */
1152
- .mark { width: 26px; height: 26px; display: block; filter: var(--mark); }
1153
- .brand b {
1154
- font-weight: 700; font-size: 16px; letter-spacing: 0.04em;
1155
- }
1156
- .eyebrow {
1157
- font-family: var(--proof); font-size: 10px; letter-spacing: 0.16em;
1158
- color: var(--muted-soft); padding: 0 20px 10px;
1159
- }
1160
- .nav { display: grid; }
1161
- .nav a, .nav span {
1162
- padding: 10px 20px; font-size: 14.5px; color: var(--secondary);
1163
- text-decoration: none; display: flex; justify-content: space-between;
1164
- align-items: center;
1165
- }
1166
- .nav a { transition: background var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)),
1167
- color var(--t-state, 120ms) var(--ease, cubic-bezier(.2,0,0,1)); }
1168
- .nav a:hover { background: var(--sunk); }
1169
-
1170
- /*
1171
- * The surface transition, spent where the language says to spend it. Verdicts
1172
- * are deliberately excluded: a decision appears, it never animates in.
1173
- */
1174
- main > * { animation: surface 180ms cubic-bezier(0.2, 0, 0, 1) both; }
1175
- @keyframes surface {
1176
- from { opacity: 0; transform: translateY(4px); }
1177
- to { opacity: 1; transform: none; }
1178
- }
1179
- main.leaving { opacity: 0; transition: opacity 120ms cubic-bezier(0.2, 0, 0, 1); }
1180
- @media (prefers-reduced-motion: reduce) {
1181
- main > *, main.leaving { animation: none; transition: none; }
1182
- .console > main { scroll-behavior: auto; }
1183
- :root { --surface-motion: 0ms; }
1184
- }
1185
- .nav .pending { color: var(--muted-soft); }
1186
- .nav .badge.soon {
1187
- font-size: 9px; letter-spacing: 0.14em; color: var(--muted-soft);
1188
- border: 1px solid var(--line); border-radius: 999px; padding: 1px 6px;
1189
- }
1190
- .nav a.on {
1191
- font-weight: 600; color: var(--ink);
1192
- border-left: 2px solid var(--signal-bright);
1193
- background: var(--active); margin-left: -1px;
1194
- }
1195
- .nav .badge { font-family: var(--proof); font-size: 11px; color: var(--hold); }
1196
- .compliance {
1197
- margin: 22px 20px 0; padding-top: 18px; border-top: 1px solid var(--line-soft);
1198
- font-family: var(--proof); font-size: 10.5px; line-height: 1.9; color: var(--muted);
1199
- }
1200
-
1201
- ${VIEWS_CSS}
1202
-
1203
- /* Print is a first-class state, not an afterthought. */
1204
- @media print {
1205
- @page { margin: 12mm; }
1206
- /* Evidence prints on paper. The fence has no printed form. */
1207
- :root, :root[data-theme='dark'] {
1208
- ${PAPER}
1209
- }
1210
- html.theming, html.theming * { transition: none !important; }
1211
- html, body { height: auto; overflow: visible; }
1212
- body { padding: 0; background: #fff; }
1213
- .wrap, .scroll { height: auto; }
1214
- .console > main { overflow: visible; height: auto; }
1215
- main > * { animation: none; }
1216
- .console {
1217
- border: none; grid-template-columns: 1fr; min-width: 0;
1218
- height: auto; display: block;
1219
- }
1220
- .rail, .actions, .decide, .ground { display: none; }
1221
- .thead { display: grid; }
1222
- thead { display: table-header-group; }
1223
- .row { break-inside: avoid; }
1224
- /* A chip is colour; a printed page may not be. Keep the word. */
1225
- .chain { border-color: currentColor !important; }
1226
- .stats { break-inside: avoid; }
1227
- }
1228
- `;
1229
- /**
1230
- * Resolve the ground before the first paint.
1231
- *
1232
- * This runs in `<head>`, synchronously, on purpose: read the choice after the
1233
- * body has painted and the reader watches the page flash the wrong palette on
1234
- * every navigation. It also marks the document as scripted, which is what
1235
- * reveals the toggle — a control that cannot work should not be offered.
1236
- */
1237
- const GROUND_SCRIPT = `
1238
- (() => {
1239
- const root = document.documentElement;
1240
- root.classList.add('js');
1241
- let choice = null;
1242
- try { choice = localStorage.getItem('prae-ground'); } catch (e) {}
1243
- if (choice !== 'dark' && choice !== 'light') {
1244
- choice = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
1245
- }
1246
- root.dataset.theme = choice;
1247
- })();
1248
- `;
1249
- /**
1250
- * Client-side navigation, as progressive enhancement.
1251
- *
1252
- * Every view is still a complete document that opens, prints and mails on its
1253
- * own — that requirement has not moved. This only intercepts a click when the
1254
- * page is being served, swaps the main column, and leaves the rail alone, so
1255
- * the frame stays put and the surface transition the language budgets actually
1256
- * has somewhere to happen. With scripting off, the links are ordinary links.
1257
- */
1258
- const NAV_SCRIPT = `
1259
- (() => {
1260
- const main = () => document.querySelector('main');
1261
- const rail = () => document.querySelector('.rail');
1262
- if (!main() || !window.fetch) return;
1263
-
1264
- const swap = async (href, push) => {
1265
- const target = main();
1266
- target.classList.add('leaving');
1267
- try {
1268
- const res = await fetch(href, { headers: { accept: 'text/html' } });
1269
- if (!res.ok) throw new Error(String(res.status));
1270
- const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
1271
- const next = doc.querySelector('main');
1272
- const nextRail = doc.querySelector('.rail');
1273
- if (!next) throw new Error('no main');
1274
-
1275
- // Collect the view's scripts BEFORE moving it: replaceWith detaches the
1276
- // element from the parsed document, so querying that document afterwards
1277
- // finds nothing and the view arrives inert. A parser-created script never
1278
- // executes on insertion either, which is why each is re-created below.
1279
- const scripts = [...next.querySelectorAll('script')].map((s) => s.textContent);
1280
- for (const s of next.querySelectorAll('script')) s.remove();
1281
-
1282
- target.replaceWith(next);
1283
- // Only the rail's active marker changes; the element itself stays, so
1284
- // nothing in it flickers or loses scroll position.
1285
- if (nextRail) rail().innerHTML = nextRail.innerHTML;
1286
- if (doc.title) document.title = doc.title;
1287
- if (push) history.pushState({}, '', href);
1288
- next.scrollTop = 0;
1289
- wire();
1290
- for (const code of scripts) {
1291
- const el = document.createElement('script');
1292
- el.textContent = code;
1293
- document.body.appendChild(el);
1294
- }
1295
- } catch {
1296
- // A failed swap falls back to a real navigation rather than stranding
1297
- // the reader on a half-updated page.
1298
- location.href = href;
1299
- }
1300
- };
1301
-
1302
- const wire = () => {
1303
- for (const a of document.querySelectorAll('.nav a[href$=".html"]')) {
1304
- a.addEventListener('click', (e) => {
1305
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
1306
- e.preventDefault();
1307
- swap(a.getAttribute('href'), true);
1308
- });
1309
- }
1310
- };
1311
-
1312
- // Delegated, because a client-side swap replaces the rail's markup and a
1313
- // handler bound to the old buttons would go with it.
1314
- let settle;
1315
- document.addEventListener('click', (e) => {
1316
- const button = e.target.closest('[data-ground]');
1317
- if (!button) return;
1318
- const next = button.dataset.ground === 'fence' ? 'dark' : 'light';
1319
- const root = document.documentElement;
1320
- if (root.dataset.theme === next) return;
1321
-
1322
- // Arm the transition, let the current palette paint once, then move. Doing
1323
- // both in one frame gives the browser no 'from' state and it jumps.
1324
- root.classList.add('theming');
1325
- void root.offsetHeight;
1326
- root.dataset.theme = next;
1327
- try { localStorage.setItem('prae-ground', next); } catch (err) {}
1328
- clearTimeout(settle);
1329
- settle = setTimeout(() => root.classList.remove('theming'), 240);
1330
- });
1331
-
1332
- addEventListener('popstate', () => swap(location.pathname, false));
1333
- wire();
1334
- })();
1335
- `;
1336
- /** Wrap a view in the console shell: rail, then the view's own header and body. */
1337
- function renderShell(page) {
1338
- return `<!doctype html>
1339
- <html lang="en" data-prae="ledger"><head>
1340
- <meta charset="utf-8">
1341
- <meta name="viewport" content="width=device-width, initial-scale=1">
1342
- <title>${esc(page.title)}</title>
1343
- <link rel="preconnect" href="https://fonts.googleapis.com">
1344
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1345
- <link href="${FONT_HREF}" rel="stylesheet">
1346
- <script>${GROUND_SCRIPT}<\/script>
1347
- <style>${SHEET}</style>
1348
- </head><body>
1349
- <div class="wrap"><div class="scroll">
1350
- <div class="console">
1351
- <nav class="rail">
1352
- <div class="brand"><img class="mark" src="${LOGO}" alt=""><b>PRAE</b></div>
1353
- <div class="eyebrow">CONTROL PLANE</div>
1354
- ${page.nav}
1355
- <div class="ground" role="group" aria-label="Ground">
1356
- <button type="button" data-ground="paper">PAPER</button>
1357
- <button type="button" data-ground="fence">FENCE</button>
1358
- </div>
1359
- <div class="compliance">EU AI ACT art. 12<br>NIST AI 600-1<br>SOC 2 CC7.2</div>
1360
- </nav>
1361
- <main>
1362
- ${page.head}
1363
- ${page.body}
1364
- </main>
1365
- </div>
1366
- </div></div>
1367
- <script>${NAV_SCRIPT}<\/script>
1368
- </body></html>
1369
- `;
1370
- }
1371
- function renderNav(items, active) {
1372
- return `<div class="nav">\n${items.map((item) => {
1373
- const classes = [item.label === active ? "on" : "", item.pending === true ? "pending" : ""].filter(Boolean).join(" ");
1374
- const attr = classes === "" ? "" : ` class="${classes}"`;
1375
- const badge = item.pending === true ? "<span class=\"badge soon\">SOON</span>" : item.badge === void 0 || item.badge === 0 ? "" : `<span class="badge">${item.badge}</span>`;
1376
- const href = item.pending === true ? "" : ` href="${esc(item.href ?? "#")}"`;
1377
- const tag = item.pending === true ? "span" : "a";
1378
- return ` <${tag}${href}${attr}>${esc(item.label)}${badge}</${tag}>`;
1379
- }).join("\n")}\n </div>`;
1380
- }
1381
-
1382
- //#endregion
1383
- //#region ../controls/src/index.ts
1384
- /**
1385
- * What each control asks for, and what this profile actually produces.
1386
- *
1387
- * Framed as evidence toward a control, never as compliance with it. Nothing
1388
- * here certifies anything: an auditor decides whether the evidence satisfies
1389
- * the obligation, and a tool that told them the answer would be selling the
1390
- * one thing it cannot supply.
1391
- */
1392
- const CONTROLS = [
1393
- {
1394
- id: "EU AI Act art. 12",
1395
- asks: "Automatic recording of events over the lifetime of the system, to a degree that enables tracing.",
1396
- evidence: (data) => ({
1397
- state: data.entries.length > 0 ? "covered" : "partial",
1398
- 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."
1399
- })
1400
- },
1401
- {
1402
- id: "NIST AI 600-1 · GV-1.2",
1403
- asks: "Characteristics of trustworthy AI are integrated into organisational policies.",
1404
- evidence: (data) => ({
1405
- state: data.pack === void 0 ? "absent" : "covered",
1406
- 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.`
1407
- })
1408
- },
1409
- {
1410
- id: "NIST AI 600-1 · MS-2.6",
1411
- asks: "AI system is evaluated regularly for safety risks, with results documented.",
1412
- evidence: () => ({
1413
- state: "partial",
1414
- note: "Decisions are documented per call. Periodic evaluation of the policy itself is an operator process this tool does not perform."
1415
- })
1416
- },
1417
- {
1418
- id: "SOC 2 · CC7.2",
1419
- asks: "The entity monitors system components for anomalies indicative of malicious acts.",
1420
- evidence: (data) => ({
1421
- state: data.entries.some((e) => e.verdict === "deny") ? "covered" : "partial",
1422
- 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."}`
1423
- })
1424
- },
1425
- {
1426
- id: "SOC 2 · CC6.1",
1427
- asks: "Logical access to protected information assets is restricted to authorised users.",
1428
- evidence: () => ({
1429
- state: "partial",
1430
- note: "Capabilities are restricted per agent. Authorisation of the human is not: approvals record what was decided, never who decided it."
1431
- })
1432
- }
1433
- ];
1434
-
1435
- //#endregion
1436
- //#region src/console/views.ts
1437
- /** One rail for every view, so the shape of the product stays consistent. */
1438
- function navFor(data) {
1439
- return [
1440
- {
1441
- label: "Overview",
1442
- href: "overview.html"
1443
- },
1444
- {
1445
- label: "Policy packs",
1446
- href: "packs.html"
1447
- },
1448
- {
1449
- label: "Playground",
1450
- href: "playground.html"
1451
- },
1452
- {
1453
- label: "Approvals",
1454
- href: "approvals.html",
1455
- badge: data.entries.filter((e) => e.verdict === "ask").length
1456
- },
1457
- {
1458
- label: "Audit ledger",
1459
- href: "ledger.html"
1460
- },
1461
- {
1462
- label: "Fleet",
1463
- pending: true
1464
- },
1465
- {
1466
- label: "Compliance map",
1467
- href: "compliance.html"
1468
- }
1469
- ];
1470
- }
1471
- function chainPill(ok) {
1472
- return `<div class="chain${ok ? "" : " broken"}"><i></i>${ok ? "CHAIN INTACT" : "CHAIN BROKEN"}</div>`;
1473
- }
1474
- function header(title, meta, ok) {
1475
- return ` <header class="head">
1476
- <div><h1>${esc(title)}</h1><div class="meta">${esc(meta)}</div></div>
1477
- <div class="actions">${chainPill(ok)}
1478
- <button type="button" class="btn" onclick="window.print()">Export evidence</button>
1479
- </div>
1480
- </header>`;
1481
- }
1482
- function renderOverview(data) {
1483
- const counts = {};
1484
- for (const entry of data.entries) counts[entry.verdict] = (counts[entry.verdict] ?? 0) + 1;
1485
- const held = counts.ask ?? 0;
1486
- const pack = data.pack;
1487
- const enforcing = pack?.rules.some((r) => r.effect.kind === "deny") ?? false;
1488
- const body = `${` <section class="stats">
1489
- <div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
1490
- <div class="stat"><div class="v veto">${counts.deny ?? 0}</div><div class="k">VETOED</div></div>
1491
- <div class="stat"><div class="v hold">${held}</div><div class="k">AWAITING HUMAN</div></div>
1492
- <div class="stat"><div class="v">${pack === void 0 ? "—" : esc(pack.rules.length)}</div><div class="k">RULES IN FORCE</div></div>
1493
- </section>`}
1494
- <section class="sect">
1495
- <h2>Posture</h2>
1496
- <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>
1497
- <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>
1498
- </section>
1499
- <section class="sect" style="padding-bottom:26px">
1500
- <h2>What is not covered</h2>
1501
- <p>This gate decides tool calls. It does not sandbox plugin code — a
1502
- plugin's <code>apply()</code> runs with full privileges before any tool
1503
- call exists — and it does not secure the harness itself. Approvals record
1504
- what was decided, never who decided it.</p>
1505
- </section>`;
1506
- return renderShell({
1507
- title: `Overview — ${pack?.pack ?? "PRAE"}`,
1508
- nav: renderNav(navFor(data), "Overview"),
1509
- head: header("Overview", `${data.entries.length} decisions · ${pack === void 0 ? "no pack" : `${pack.pack} ${pack.version}`}`, data.verification.ok),
1510
- body
1511
- });
1512
- }
1513
- function matcherOf(rule) {
1514
- const when = rule.when;
1515
- const parts = [];
1516
- const list = (v) => v === void 0 ? void 0 : Array.isArray(v) ? v.join(", ") : String(v);
1517
- const cap = list(when.capability);
1518
- if (cap !== void 0) parts.push(`capability ${cap}`);
1519
- const tool = list(when.tool);
1520
- if (tool !== void 0) parts.push(`tool ${tool}`);
1521
- const plugin = list(when.plugin);
1522
- if (plugin !== void 0) parts.push(`plugin ${plugin}`);
1523
- if (when.path?.outside !== void 0) parts.push(`path outside ${when.path.outside.join(", ")}`);
1524
- if (when.path?.inside !== void 0) parts.push(`path inside ${when.path.inside.join(", ")}`);
1525
- if (when.host?.notIn !== void 0) parts.push(`host not in ${when.host.notIn.join(", ")}`);
1526
- if (when.host?.in !== void 0) parts.push(`host in ${when.host.in.join(", ")}`);
1527
- if (when.count !== void 0) parts.push(`more than ${when.count.above}`);
1528
- for (const arg of when.arg ?? []) {
1529
- const bits = [
1530
- arg.above === void 0 ? "" : `above ${arg.above}`,
1531
- arg.below === void 0 ? "" : `below ${arg.below}`,
1532
- arg.equals === void 0 ? "" : `= ${String(arg.equals)}`
1533
- ].filter(Boolean);
1534
- parts.push(`${arg.name} ${bits.join(" ")}`.trim());
1535
- }
1536
- return parts.length === 0 ? "every call" : parts.join(" · ");
1537
- }
1538
- function renderPacks(data) {
1539
- const pack = data.pack;
1540
- if (pack === void 0) return renderShell({
1541
- title: "Policy packs",
1542
- nav: renderNav(navFor(data), "Policy packs"),
1543
- head: header("Policy packs", "no pack supplied", data.verification.ok),
1544
- body: " <div class=\"empty\">Pass --pack to render the policy in force.</div>"
1545
- });
1546
- const baseline = pack.baseline?.capabilities;
1547
- 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>`;
1548
- const rows = pack.rules.map((rule) => ` <div class="grid rules row">
1549
- <div class="seq">${esc(rule.id)}</div>
1550
- <div class="call">${esc(rule.title ?? rule.effect.because)}<br><span style="color:var(--muted)">${esc(matcherOf(rule))}</span></div>
1551
- <div class="effect e-${esc(rule.effect.kind)}">${esc(rule.effect.kind.toUpperCase())}</div>
1552
- </div>`).join("\n");
1553
- const body = ` <section class="sect">
1554
- <h2>${esc(pack.pack)} <span style="font-family:var(--proof);font-size:14px;color:var(--muted)">${esc(pack.version)}</span></h2>
1555
- <p>The operator baseline is intersected with whatever a plugin's own
1556
- manifest declares, and the operator can only ever narrow. A pack cannot
1557
- grant a capability an author never asked for.</p>
1558
- </section>
1559
- ${caps}
1560
- <div class="grid rules thead">
1561
- <div>CLAUSE</div><div>RULE</div><div>EFFECT</div>
1562
- </div>
1563
- ${rows}
1564
- <section class="sect" style="padding-bottom:26px">
1565
- <h2>Ordering</h2>
1566
- <p>When several rules match one call the strongest effect wins —
1567
- deny over ask over correct over warn — and every match stays in the
1568
- trace. That is what makes a pack safe to extend: appending a rule can
1569
- raise a decision's strength and never lower it.</p>
1570
- </section>`;
1571
- return renderShell({
1572
- title: `Policy packs — ${pack.pack}`,
1573
- nav: renderNav(navFor(data), "Policy packs"),
1574
- head: header("Policy packs", `${pack.pack} ${pack.version} · ${pack.rules.length} rules`, data.verification.ok),
1575
- body
1576
- });
1577
- }
1578
- function renderCompliance(data) {
1579
- const body = ` <section class="sect">
1580
- <h2>Evidence, not compliance</h2>
1581
- <p>Each row states what a control asks for and what this profile actually
1582
- produces. Nothing here certifies anything — whether the evidence satisfies
1583
- the obligation is an auditor's judgement, and a tool that answered it for
1584
- them would be selling the one thing it cannot supply.</p>
1585
- </section>
1586
- <div class="grid map thead">
1587
- <div>CONTROL</div><div>ASKS FOR · WHAT THIS PROFILE PRODUCES</div><div>STATE</div>
1588
- </div>
1589
- ${CONTROLS.map((control) => {
1590
- const found = control.evidence(data);
1591
- const label = found.state === "covered" ? "EVIDENCE" : found.state === "partial" ? "PARTIAL" : "NONE";
1592
- return ` <div class="grid map row">
1593
- <div class="call">${esc(control.id)}</div>
1594
- <div class="actor">${esc(control.asks)}<br><span style="color:var(--ink-row)">${esc(found.note)}</span></div>
1595
- <div class="effect ${esc(found.state)}">${label}</div>
1596
- </div>`;
1597
- }).join("\n")}
1598
- <section class="sect" style="padding-bottom:26px">
1599
- <h2>The gap that recurs</h2>
1600
- <p>Three of these rows stop at <em>partial</em> for the same reason: the
1601
- harness records what was decided and never who decided it. Attributable
1602
- approval needs an identity anchor outside this machine, which is a
1603
- control-plane concern this build does not claim.</p>
1604
- </section>`;
1605
- return renderShell({
1606
- title: "Compliance map",
1607
- nav: renderNav(navFor(data), "Compliance map"),
1608
- head: header("Compliance map", `${CONTROLS.length} controls · evidence from this profile`, data.verification.ok),
1609
- body
1610
- });
1611
- }
1612
- /**
1613
- * Every call the policy held, and what became of it.
1614
- *
1615
- * This is a queue you read, not one you act on. The gate returns `ask` into the
1616
- * harness's own approval seam and the harness settles it there; a second set of
1617
- * buttons here would mean two places to approve from and two audit trails to
1618
- * reconcile. So the view shows what is waiting and what was decided, and says
1619
- * plainly where the decision happens.
1620
- */
1621
- function renderApprovals(data) {
1622
- const held = data.entries.filter((e) => e.verdict === "ask");
1623
- const settled = data.entries.filter((e) => e.rule === "approval");
1624
- const now = Date.parse(data.generatedAt);
1625
- const waited = (iso) => {
1626
- const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1e3));
1627
- if (!Number.isFinite(s)) return "";
1628
- const h = Math.floor(s / 3600);
1629
- const m = Math.floor(s % 3600 / 60);
1630
- return h > 0 ? `${h}h ${m}m` : `${m}m`;
1631
- };
1632
- 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">
1633
- <div class="card">
1634
- <div style="flex:1">
1635
- <h2>${esc(e.tool)}${e.subject === void 0 ? "" : ` on ${esc(e.subject)}`}</h2>
1636
- <div class="detail">
1637
- ${esc(e.rule ?? "held")} · ${esc(e.session ?? "local session")} · waiting ${esc(waited(e.at))}<br>
1638
- ${esc(e.reason ?? "")}
1639
- </div>
1640
- </div>
1641
- <div class="decide">
1642
- <button type="button" class="approve" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Approve</button>
1643
- <button type="button" class="deny" disabled title="The harness settles approvals; a second prompt would mean two audit trails">Deny</button>
1644
- <div class="recorded">SETTLED IN THE HARNESS</div>
1645
- </div>
1646
- </div>
1647
- </section>`).join("\n");
1648
- const history = settled.length === 0 ? "" : ` <div class="grid map thead">
1649
- <div>WHEN</div><div>CALL · WHY</div><div>OUTCOME</div>
1650
- </div>
1651
- ${settled.map((e) => ` <div class="grid map row">
1652
- <div class="seq">${esc(e.at.replace("T", " ").replace(/\.\d+Z$/, "Z"))}</div>
1653
- <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>
1654
- <div class="effect ${e.verdict === "allow" ? "covered" : "absent"}">${e.verdict === "allow" ? "APPROVED" : "REFUSED"}</div>
1655
- </div>`).join("\n")}`;
1656
- const body = ` <section class="stats">
1657
- <div class="stat"><div class="v hold">${held.length}</div><div class="k">AWAITING HUMAN</div></div>
1658
- <div class="stat"><div class="v">${settled.length}</div><div class="k">SETTLED</div></div>
1659
- <div class="stat"><div class="v">${settled.filter((e) => e.actor !== void 0).length}</div><div class="k">WITH AN ACTOR</div></div>
1660
- <div class="stat"><div class="v">${data.entries.length}</div><div class="k">DECISIONS</div></div>
1661
- </section>
1662
- <section class="sect">
1663
- <h2>Waiting</h2>
1664
- <p>A held call is one a clause would not decide alone. The harness owns
1665
- the prompt and settles it; this is the queue, not a second place to
1666
- approve from.</p>
1667
- </section>
1668
- ${queue}
1669
- <section class="sect">
1670
- <h2>Settled</h2>
1671
- <p>Where an entry names an actor, that name is asserted by configuration.
1672
- The harness records what was decided and never who decided it, so an
1673
- attributable approval needs an identity anchor this build does not have.</p>
1674
- </section>
1675
- ${history}`;
1676
- return renderShell({
1677
- title: "Approvals",
1678
- nav: renderNav(navFor(data), "Approvals"),
1679
- head: header("Approvals", `${held.length} waiting · ${settled.length} settled`, data.verification.ok),
1680
- body
1681
- });
1682
- }
1683
-
1684
936
  //#endregion
1685
937
  //#region src/console/ledger.ts
1686
938
  function ago(iso, now) {
@@ -1998,7 +1250,7 @@ function praeRow(options) {
1998
1250
  "# Holds every tool call on this profile to the pack below.",
1999
1251
  "- insert:",
2000
1252
  " - id: prae",
2001
- " name: dsh-prae",
1253
+ " name: prae-gate",
2002
1254
  " config:",
2003
1255
  " strict: true"
2004
1256
  ];
@@ -2013,7 +1265,7 @@ async function planWrite(profile, row$1) {
2013
1265
  const after = composePatchFile(before, row$1, OWNER);
2014
1266
  const findings = await preflightOps(profile.dir, [{ insert: [{
2015
1267
  id: "prae",
2016
- name: "dsh-prae"
1268
+ name: "prae-gate"
2017
1269
  }] }]);
2018
1270
  const blocking = findings.some((finding) => finding.level === "blocking");
2019
1271
  return {
@@ -2136,10 +1388,21 @@ const USAGE = `prae — operator policy for agent harnesses
2136
1388
  Render the decision record as a printable evidence report.
2137
1389
 
2138
1390
  prae console <ledger.jsonl> --out <dir> [--pack <file>] [--serve [port]]
1391
+ [--instructions <dir>]
2139
1392
  Write the control plane: overview, policy packs, playground, ledger,
2140
- compliance map. The playground needs --pack.
1393
+ compliance map. The playground needs --pack; --instructions adds the
1394
+ migration review for the files at that directory.
1395
+
1396
+ prae instructions [<dir>] [--json]
1397
+ Read every AGENTS.md, CLAUDE.md and .cursorrules an agent would be
1398
+ given at <dir>, and list what they say. Nothing is interpreted.
1399
+
1400
+ prae propose [<dir>] [--as warn|ask] [--out <pack.json>]
1401
+ Read the instruction files at <dir> and propose the rules they imply.
1402
+ Proposals only: an existing pack is never modified.
2141
1403
 
2142
1404
  prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
1405
+ (no --pack mounts the built-in baseline of 8 high-risk rules)
2143
1406
  [--approver <name>] [--apply]
2144
1407
  Compose the overlay row that mounts PRAE with a pack. Prints the
2145
1408
  diff; writes only with --apply.
@@ -2271,6 +1534,90 @@ async function report(path, out) {
2271
1534
  * and a governance tool that opens a port to show you its own audit trail has
2272
1535
  * added an attack surface to solve a rendering problem.
2273
1536
  */
1537
+ /**
1538
+ * Show what the agent is actually told, before anyone proposes a rule from it.
1539
+ *
1540
+ * The first half of "point PRAE at your existing AGENTS.md": read the same
1541
+ * files the harness would, in the same order, and print them as statements
1542
+ * with the line each came from. Nothing here decides what any of them mean —
1543
+ * that step is a proposal a human approves, and it starts from this list.
1544
+ */
1545
+ async function instructions(from, asJson) {
1546
+ const { ingest } = await import("./src-BrZXeEez.js");
1547
+ const result = ingest(from);
1548
+ if (asJson) {
1549
+ console.log(JSON.stringify(result, null, 2));
1550
+ return;
1551
+ }
1552
+ if (result.files.length === 0) {
1553
+ console.log(`No instruction files found at or above ${from}.`);
1554
+ console.log("Looked for AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules up to the project root.");
1555
+ return;
1556
+ }
1557
+ console.log(`instructions ${result.hash.slice(0, 16)}`);
1558
+ console.log(`${result.files.length} file${result.files.length === 1 ? "" : "s"} · ${result.statements.length} statements\n`);
1559
+ for (const file of result.files) {
1560
+ console.log(`${file.path} ${file.hash.slice(0, 12)}`);
1561
+ for (const section of file.sections) {
1562
+ if (section.statements.length === 0) continue;
1563
+ const where = section.headingPath.slice(-2).join(" › ");
1564
+ if (where !== "") console.log(` ${where}`);
1565
+ for (const statement of section.statements) console.log(` ${String(statement.line).padStart(4)} ${statement.text}`);
1566
+ }
1567
+ console.log("");
1568
+ }
1569
+ console.log("Nothing above is interpreted. Turning a statement into a rule is a");
1570
+ console.log("proposal you review — see `prae check` for what a pack enforces.");
1571
+ }
1572
+ /**
1573
+ * Propose rules from instruction files, and never apply them.
1574
+ *
1575
+ * The second half of the migration, and the half that has to stay honest.
1576
+ * Reading a sentence as a gate is a judgement; this prints the judgement next
1577
+ * to the sentence it came from and stops. An existing pack is never touched —
1578
+ * `--out` writes a *new* file, so approving a proposal is a diff someone makes
1579
+ * on purpose rather than something that happened to their policy.
1580
+ */
1581
+ async function propose(from, options) {
1582
+ const [{ ingest }, { extract }] = await Promise.all([import("./src-BrZXeEez.js"), import("./src-Dd2_8m2g.js")]);
1583
+ const read = ingest(from);
1584
+ if (read.files.length === 0) {
1585
+ console.log(`No instruction files found at or above ${from}.`);
1586
+ return;
1587
+ }
1588
+ const { proposals, skipped } = extract(read.statements, options.as === void 0 ? {} : { as: options.as });
1589
+ console.log(`${read.files.length} file${read.files.length === 1 ? "" : "s"} · ${read.statements.length} statements · ${proposals.length} proposed\n`);
1590
+ for (const proposal of proposals) {
1591
+ const rule = proposal.rule;
1592
+ console.log(`${rule.id} ${rule.effect.kind.toUpperCase()} ${rule.title ?? ""}`);
1593
+ console.log(` from ${proposal.from.path}:${proposal.from.line}`);
1594
+ console.log(` "${proposal.from.text}"`);
1595
+ console.log(` read ${proposal.why} (${proposal.confidence} confidence)`);
1596
+ console.log(` when ${JSON.stringify(rule.when)}`);
1597
+ console.log("");
1598
+ }
1599
+ console.log(`${skipped.length} statement${skipped.length === 1 ? "" : "s"} proposed nothing and remain context.`);
1600
+ if (proposals.length === 0) {
1601
+ console.log("\nThat is a normal result, not a failure. Serve them as");
1602
+ console.log("context instead: `context: <file>` in the plugin config.");
1603
+ return;
1604
+ }
1605
+ if (options.out === void 0) {
1606
+ console.log("\nNothing was written. Re-run with --out <file> to save these");
1607
+ console.log("for review, then merge what you agree with into your pack.");
1608
+ return;
1609
+ }
1610
+ const pack = {
1611
+ prae: "0.1",
1612
+ pack: "proposed",
1613
+ version: "0.0.0",
1614
+ rules: proposals.map((proposal) => proposal.rule)
1615
+ };
1616
+ await writeFile(options.out, `${JSON.stringify(pack, null, 2)}\n`);
1617
+ console.log(`\n${options.out} · ${proposals.length} rules, unreviewed`);
1618
+ console.log("Read every one against the sentence above it before it enforces");
1619
+ console.log("anything. `prae check` summarises what it would do.");
1620
+ }
2274
1621
  async function consoleOut(path, argv) {
2275
1622
  const out = flag(argv, "out");
2276
1623
  if (out === void 0) fail("console needs --out <dir>");
@@ -2288,6 +1635,17 @@ async function consoleOut(path, argv) {
2288
1635
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2289
1636
  ...packPath === void 0 ? {} : { pack: await loadPack(packPath) }
2290
1637
  };
1638
+ let instructionsView;
1639
+ const instructionsDir = flag(argv, "instructions");
1640
+ if (instructionsDir !== void 0) {
1641
+ const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1642
+ import("./src-BrZXeEez.js"),
1643
+ import("./src-Dd2_8m2g.js"),
1644
+ import("./instructions-ijuj4IP1.js")
1645
+ ]);
1646
+ const read = ingest(instructionsDir);
1647
+ instructionsView = renderInstructions(data, read, extract(read.statements));
1648
+ }
2291
1649
  await mkdir(out, { recursive: true });
2292
1650
  const pages = [
2293
1651
  ["overview.html", renderOverview(data)],
@@ -2298,6 +1656,7 @@ async function consoleOut(path, argv) {
2298
1656
  ...data.pack === void 0 ? {} : { pack: data.pack }
2299
1657
  })],
2300
1658
  ["approvals.html", renderApprovals(data)],
1659
+ ...instructionsView === void 0 ? [] : [["instructions.html", instructionsView]],
2301
1660
  ["compliance.html", renderCompliance(data)],
2302
1661
  ...packSource === void 0 ? [] : [["playground.html", renderPlayground(data, packSource)]]
2303
1662
  ];
@@ -2316,8 +1675,13 @@ async function consoleOut(path, argv) {
2316
1675
  console.log("loopback only, read-only. ctrl-c to stop.");
2317
1676
  }
2318
1677
  async function init(argv) {
2319
- const packPath = flag(argv, "pack");
2320
- if (packPath === void 0) fail("init needs --pack <file>");
1678
+ const bundledBaseline = [join(import.meta.dirname, "prae-baseline.prae.json"), join(import.meta.dirname, "..", "..", "..", "examples", "prae-baseline.prae.json")].find((candidate) => existsSync(candidate));
1679
+ const packPath = flag(argv, "pack") ?? bundledBaseline;
1680
+ if (packPath === void 0) fail("init needs --pack <file>; no bundled baseline found");
1681
+ if (flag(argv, "pack") === void 0) {
1682
+ console.log("No --pack given; mounting the built-in baseline (8 rules).");
1683
+ console.log(`See it with: prae check ${packPath}\n`);
1684
+ }
2321
1685
  const pack = await loadPack(packPath);
2322
1686
  const source = await readFile(packPath, "utf8");
2323
1687
  const profileDir = flag(argv, "profile");
@@ -2345,7 +1709,7 @@ async function init(argv) {
2345
1709
  }
2346
1710
  if (plan.blocking) {
2347
1711
  console.error("\nNot writing. A row naming a package the profile cannot resolve stops it");
2348
- console.error("from booting entirely — install dsh-prae into the profile first.");
1712
+ console.error("from booting entirely — install prae-gate into the profile first.");
2349
1713
  process.exit(1);
2350
1714
  }
2351
1715
  if (!argv.includes("--apply")) {
@@ -2391,6 +1755,16 @@ async function main(argv) {
2391
1755
  if (path === void 0) fail("console needs a ledger file");
2392
1756
  return consoleOut(path, rest);
2393
1757
  }
1758
+ case "instructions": return instructions(rest[0] ?? process.cwd(), rest.includes("--json"));
1759
+ case "propose": {
1760
+ const dir = rest[0]?.startsWith("--") ? process.cwd() : rest[0] ?? process.cwd();
1761
+ const as = flag(rest, "as");
1762
+ if (as !== void 0 && as !== "warn" && as !== "ask") fail("--as takes warn or ask");
1763
+ return propose(dir, {
1764
+ ...as === void 0 ? {} : { as },
1765
+ ...flag(rest, "out") === void 0 ? {} : { out: flag(rest, "out") }
1766
+ });
1767
+ }
2394
1768
  case "init": return init(rest);
2395
1769
  case "--help":
2396
1770
  case "-h":