prae-cli 0.1.1 → 0.1.2
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 +2 -2
- package/lib/bin.js +2 -1
- package/lib/{cli-DGVWMDT0.js → cli-B6X4uAj5.js} +121 -928
- package/lib/index.js +2 -1
- package/lib/instructions-ijuj4IP1.js +133 -0
- package/lib/src-C5neqxZc.js +274 -0
- package/lib/src-Dd2_8m2g.js +153 -0
- package/lib/views-BU0sOciR.js +929 -0
- package/package.json +6 -4
|
@@ -1,3 +1,4 @@
|
|
|
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";
|
|
1
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
@@ -757,930 +758,6 @@ var Ledger = class Ledger {
|
|
|
757
758
|
}
|
|
758
759
|
};
|
|
759
760
|
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
761
|
//#endregion
|
|
1685
762
|
//#region src/console/ledger.ts
|
|
1686
763
|
function ago(iso, now) {
|
|
@@ -1998,7 +1075,7 @@ function praeRow(options) {
|
|
|
1998
1075
|
"# Holds every tool call on this profile to the pack below.",
|
|
1999
1076
|
"- insert:",
|
|
2000
1077
|
" - id: prae",
|
|
2001
|
-
" name:
|
|
1078
|
+
" name: prae-gate",
|
|
2002
1079
|
" config:",
|
|
2003
1080
|
" strict: true"
|
|
2004
1081
|
];
|
|
@@ -2013,7 +1090,7 @@ async function planWrite(profile, row$1) {
|
|
|
2013
1090
|
const after = composePatchFile(before, row$1, OWNER);
|
|
2014
1091
|
const findings = await preflightOps(profile.dir, [{ insert: [{
|
|
2015
1092
|
id: "prae",
|
|
2016
|
-
name: "
|
|
1093
|
+
name: "prae-gate"
|
|
2017
1094
|
}] }]);
|
|
2018
1095
|
const blocking = findings.some((finding) => finding.level === "blocking");
|
|
2019
1096
|
return {
|
|
@@ -2136,8 +1213,18 @@ const USAGE = `prae — operator policy for agent harnesses
|
|
|
2136
1213
|
Render the decision record as a printable evidence report.
|
|
2137
1214
|
|
|
2138
1215
|
prae console <ledger.jsonl> --out <dir> [--pack <file>] [--serve [port]]
|
|
1216
|
+
[--instructions <dir>]
|
|
2139
1217
|
Write the control plane: overview, policy packs, playground, ledger,
|
|
2140
|
-
compliance map. The playground needs --pack
|
|
1218
|
+
compliance map. The playground needs --pack; --instructions adds the
|
|
1219
|
+
migration review for the files at that directory.
|
|
1220
|
+
|
|
1221
|
+
prae instructions [<dir>] [--json]
|
|
1222
|
+
Read every AGENTS.md, CLAUDE.md and .cursorrules an agent would be
|
|
1223
|
+
given at <dir>, and list what they say. Nothing is interpreted.
|
|
1224
|
+
|
|
1225
|
+
prae propose [<dir>] [--as warn|ask] [--out <pack.json>]
|
|
1226
|
+
Read the instruction files at <dir> and propose the rules they imply.
|
|
1227
|
+
Proposals only: an existing pack is never modified.
|
|
2141
1228
|
|
|
2142
1229
|
prae init [--pack <file>] [--profile <dir>] [--ledger <file>]
|
|
2143
1230
|
[--approver <name>] [--apply]
|
|
@@ -2271,6 +1358,90 @@ async function report(path, out) {
|
|
|
2271
1358
|
* and a governance tool that opens a port to show you its own audit trail has
|
|
2272
1359
|
* added an attack surface to solve a rendering problem.
|
|
2273
1360
|
*/
|
|
1361
|
+
/**
|
|
1362
|
+
* Show what the agent is actually told, before anyone proposes a rule from it.
|
|
1363
|
+
*
|
|
1364
|
+
* The first half of "point PRAE at your existing AGENTS.md": read the same
|
|
1365
|
+
* files the harness would, in the same order, and print them as statements
|
|
1366
|
+
* with the line each came from. Nothing here decides what any of them mean —
|
|
1367
|
+
* that step is a proposal a human approves, and it starts from this list.
|
|
1368
|
+
*/
|
|
1369
|
+
async function instructions(from, asJson) {
|
|
1370
|
+
const { ingest } = await import("./src-C5neqxZc.js");
|
|
1371
|
+
const result = ingest(from);
|
|
1372
|
+
if (asJson) {
|
|
1373
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (result.files.length === 0) {
|
|
1377
|
+
console.log(`No instruction files found at or above ${from}.`);
|
|
1378
|
+
console.log("Looked for AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules up to the project root.");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
console.log(`instructions ${result.hash.slice(0, 16)}`);
|
|
1382
|
+
console.log(`${result.files.length} file${result.files.length === 1 ? "" : "s"} · ${result.statements.length} statements\n`);
|
|
1383
|
+
for (const file of result.files) {
|
|
1384
|
+
console.log(`${file.path} ${file.hash.slice(0, 12)}`);
|
|
1385
|
+
for (const section of file.sections) {
|
|
1386
|
+
if (section.statements.length === 0) continue;
|
|
1387
|
+
const where = section.headingPath.slice(-2).join(" › ");
|
|
1388
|
+
if (where !== "") console.log(` ${where}`);
|
|
1389
|
+
for (const statement of section.statements) console.log(` ${String(statement.line).padStart(4)} ${statement.text}`);
|
|
1390
|
+
}
|
|
1391
|
+
console.log("");
|
|
1392
|
+
}
|
|
1393
|
+
console.log("Nothing above is interpreted. Turning a statement into a rule is a");
|
|
1394
|
+
console.log("proposal you review — see `prae check` for what a pack enforces.");
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Propose rules from instruction files, and never apply them.
|
|
1398
|
+
*
|
|
1399
|
+
* The second half of the migration, and the half that has to stay honest.
|
|
1400
|
+
* Reading a sentence as a gate is a judgement; this prints the judgement next
|
|
1401
|
+
* to the sentence it came from and stops. An existing pack is never touched —
|
|
1402
|
+
* `--out` writes a *new* file, so approving a proposal is a diff someone makes
|
|
1403
|
+
* on purpose rather than something that happened to their policy.
|
|
1404
|
+
*/
|
|
1405
|
+
async function propose(from, options) {
|
|
1406
|
+
const [{ ingest }, { extract }] = await Promise.all([import("./src-C5neqxZc.js"), import("./src-Dd2_8m2g.js")]);
|
|
1407
|
+
const read = ingest(from);
|
|
1408
|
+
if (read.files.length === 0) {
|
|
1409
|
+
console.log(`No instruction files found at or above ${from}.`);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
const { proposals, skipped } = extract(read.statements, options.as === void 0 ? {} : { as: options.as });
|
|
1413
|
+
console.log(`${read.files.length} file${read.files.length === 1 ? "" : "s"} · ${read.statements.length} statements · ${proposals.length} proposed\n`);
|
|
1414
|
+
for (const proposal of proposals) {
|
|
1415
|
+
const rule = proposal.rule;
|
|
1416
|
+
console.log(`${rule.id} ${rule.effect.kind.toUpperCase()} ${rule.title ?? ""}`);
|
|
1417
|
+
console.log(` from ${proposal.from.path}:${proposal.from.line}`);
|
|
1418
|
+
console.log(` "${proposal.from.text}"`);
|
|
1419
|
+
console.log(` read ${proposal.why} (${proposal.confidence} confidence)`);
|
|
1420
|
+
console.log(` when ${JSON.stringify(rule.when)}`);
|
|
1421
|
+
console.log("");
|
|
1422
|
+
}
|
|
1423
|
+
console.log(`${skipped.length} statement${skipped.length === 1 ? "" : "s"} proposed nothing and remain context.`);
|
|
1424
|
+
if (proposals.length === 0) {
|
|
1425
|
+
console.log("\nThat is a normal result, not a failure. Serve them as");
|
|
1426
|
+
console.log("context instead: `context: <file>` in the plugin config.");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
if (options.out === void 0) {
|
|
1430
|
+
console.log("\nNothing was written. Re-run with --out <file> to save these");
|
|
1431
|
+
console.log("for review, then merge what you agree with into your pack.");
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const pack = {
|
|
1435
|
+
prae: "0.1",
|
|
1436
|
+
pack: "proposed",
|
|
1437
|
+
version: "0.0.0",
|
|
1438
|
+
rules: proposals.map((proposal) => proposal.rule)
|
|
1439
|
+
};
|
|
1440
|
+
await writeFile(options.out, `${JSON.stringify(pack, null, 2)}\n`);
|
|
1441
|
+
console.log(`\n${options.out} · ${proposals.length} rules, unreviewed`);
|
|
1442
|
+
console.log("Read every one against the sentence above it before it enforces");
|
|
1443
|
+
console.log("anything. `prae check` summarises what it would do.");
|
|
1444
|
+
}
|
|
2274
1445
|
async function consoleOut(path, argv) {
|
|
2275
1446
|
const out = flag(argv, "out");
|
|
2276
1447
|
if (out === void 0) fail("console needs --out <dir>");
|
|
@@ -2288,6 +1459,17 @@ async function consoleOut(path, argv) {
|
|
|
2288
1459
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2289
1460
|
...packPath === void 0 ? {} : { pack: await loadPack(packPath) }
|
|
2290
1461
|
};
|
|
1462
|
+
let instructionsView;
|
|
1463
|
+
const instructionsDir = flag(argv, "instructions");
|
|
1464
|
+
if (instructionsDir !== void 0) {
|
|
1465
|
+
const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
|
|
1466
|
+
import("./src-C5neqxZc.js"),
|
|
1467
|
+
import("./src-Dd2_8m2g.js"),
|
|
1468
|
+
import("./instructions-ijuj4IP1.js")
|
|
1469
|
+
]);
|
|
1470
|
+
const read = ingest(instructionsDir);
|
|
1471
|
+
instructionsView = renderInstructions(data, read, extract(read.statements));
|
|
1472
|
+
}
|
|
2291
1473
|
await mkdir(out, { recursive: true });
|
|
2292
1474
|
const pages = [
|
|
2293
1475
|
["overview.html", renderOverview(data)],
|
|
@@ -2298,6 +1480,7 @@ async function consoleOut(path, argv) {
|
|
|
2298
1480
|
...data.pack === void 0 ? {} : { pack: data.pack }
|
|
2299
1481
|
})],
|
|
2300
1482
|
["approvals.html", renderApprovals(data)],
|
|
1483
|
+
...instructionsView === void 0 ? [] : [["instructions.html", instructionsView]],
|
|
2301
1484
|
["compliance.html", renderCompliance(data)],
|
|
2302
1485
|
...packSource === void 0 ? [] : [["playground.html", renderPlayground(data, packSource)]]
|
|
2303
1486
|
];
|
|
@@ -2345,7 +1528,7 @@ async function init(argv) {
|
|
|
2345
1528
|
}
|
|
2346
1529
|
if (plan.blocking) {
|
|
2347
1530
|
console.error("\nNot writing. A row naming a package the profile cannot resolve stops it");
|
|
2348
|
-
console.error("from booting entirely — install
|
|
1531
|
+
console.error("from booting entirely — install prae-gate into the profile first.");
|
|
2349
1532
|
process.exit(1);
|
|
2350
1533
|
}
|
|
2351
1534
|
if (!argv.includes("--apply")) {
|
|
@@ -2391,6 +1574,16 @@ async function main(argv) {
|
|
|
2391
1574
|
if (path === void 0) fail("console needs a ledger file");
|
|
2392
1575
|
return consoleOut(path, rest);
|
|
2393
1576
|
}
|
|
1577
|
+
case "instructions": return instructions(rest[0] ?? process.cwd(), rest.includes("--json"));
|
|
1578
|
+
case "propose": {
|
|
1579
|
+
const dir = rest[0]?.startsWith("--") ? process.cwd() : rest[0] ?? process.cwd();
|
|
1580
|
+
const as = flag(rest, "as");
|
|
1581
|
+
if (as !== void 0 && as !== "warn" && as !== "ask") fail("--as takes warn or ask");
|
|
1582
|
+
return propose(dir, {
|
|
1583
|
+
...as === void 0 ? {} : { as },
|
|
1584
|
+
...flag(rest, "out") === void 0 ? {} : { out: flag(rest, "out") }
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
2394
1587
|
case "init": return init(rest);
|
|
2395
1588
|
case "--help":
|
|
2396
1589
|
case "-h":
|