enigma-cli 1.63.6 → 1.64.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/assets/memory/AGENTS.md +1 -1
  2. package/assets/memory/CLAUDE.md +1 -1
  3. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  4. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  5. package/assets/skills/backend-policy/SKILL.md +14 -1
  6. package/assets/skills/backend-policy/skill.json +5 -5
  7. package/assets/skills/ci-policy/skill.json +1 -1
  8. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  9. package/assets/skills/code-review-policy/skill.json +1 -1
  10. package/assets/skills/core-engineering-policy/skill.json +1 -1
  11. package/assets/skills/database-expert/skill.json +1 -1
  12. package/assets/skills/debugging-policy/skill.json +1 -1
  13. package/assets/skills/dependency-policy/skill.json +1 -1
  14. package/assets/skills/email-policy/skill.json +1 -1
  15. package/assets/skills/frontend-design/skill.json +1 -1
  16. package/assets/skills/frontend-policy/SKILL.md +4 -1
  17. package/assets/skills/frontend-policy/skill.json +4 -4
  18. package/assets/skills/git-policy/skill.json +1 -1
  19. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  20. package/assets/skills/security-policy/SKILL.md +2 -0
  21. package/assets/skills/security-policy/skill.json +4 -4
  22. package/assets/skills/skill-creator/skill.json +1 -1
  23. package/assets/skills/task-completion-policy/skill.json +1 -1
  24. package/assets/skills/technical-writing-policy/skill.json +1 -1
  25. package/assets/skills/testing-policy/SKILL.md +1 -0
  26. package/assets/skills/testing-policy/skill.json +4 -4
  27. package/assets/skills/validation-policy/skill.json +1 -1
  28. package/bin/checksums.json +4 -4
  29. package/dist/guard.js +0 -0
  30. package/dist/guardrails.js +194 -24
  31. package/dist/post-edit.js +241 -68
  32. package/package.json +1 -1
@@ -1,11 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/guardrails.ts
4
- import { homedir } from "os";
4
+ import { homedir as homedir2 } from "os";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execFileSync } from "child_process";
7
- import { dirname, join, resolve, sep } from "path";
8
- import { appendFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync, statSync, existsSync } from "fs";
7
+
8
+ // src/private-terms.ts
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import { readFileSync } from "fs";
12
+ function readPrivateTerms() {
13
+ try {
14
+ const raw = JSON.parse(readFileSync(join(process.env.ENIGMA_CONFIG_HOME || homedir(), ".enigma-guard.json"), "utf8"));
15
+ if (!Array.isArray(raw.privateTerms)) return [];
16
+ return raw.privateTerms.filter((t) => typeof t === "string" && t.trim().length >= 3);
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+ function privateTermMatcher(terms) {
22
+ const anyCase = (ch) => {
23
+ const lo = ch.toLowerCase(), up = ch.toUpperCase();
24
+ return lo === up ? ch.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&") : `[${lo}${up}]`;
25
+ };
26
+ const parts = terms.map((t) => t.trim().split(/[\s_-]+/).filter(Boolean).map((w) => [...w].map(anyCase).join("")).join("[\\s_-]*")).filter(Boolean);
27
+ if (!parts.length) return null;
28
+ return new RegExp(`(?:(?<![\\p{L}\\p{N}])|(?<=\\p{Ll})(?=\\p{Lu}))(?:${parts.join("|")})(?!\\p{Ll})`, "u");
29
+ }
30
+ function hasPrivateTerm(line, re) {
31
+ return re.test(line.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, " "));
32
+ }
33
+ function privateTermLines(text, re) {
34
+ const out = [];
35
+ text.split("\n").forEach((line, i) => {
36
+ if (hasPrivateTerm(line, re)) out.push(i + 1);
37
+ });
38
+ return out;
39
+ }
40
+
41
+ // src/guardrails.ts
42
+ import { dirname, join as join2, resolve, sep } from "path";
43
+ import { appendFileSync, mkdirSync, readFileSync as readFileSync2, readdirSync, writeFileSync, statSync, existsSync } from "fs";
9
44
  var COMMENT_LINE = /^\s*(\/\/|#|\*|--|<!--|\{?\/\*)/;
10
45
  var SKELETON_GUARD_SRC = "\\bif\\s*\\(\\s*(isLoading|isPending|isFetching|loading|pending)\\s*\\)\\s*return\\s+(null\\b|<\\s*\\w*(Spinner|Loader|Loading|CircularProgress)\\b)";
11
46
  var SKELETON_SIGNAL_SRC = "skeleton|animate-pulse|shimmer|Suspense|ContentLoader|content-loader|<\\s*Placeholder";
@@ -1410,6 +1445,35 @@ var BUILTIN_RULES = [
1410
1445
  severity: "block",
1411
1446
  skill: "frontend-policy"
1412
1447
  },
1448
+ {
1449
+ id: "fe-raw-img-over-component",
1450
+ label: "A project with an image component uses it",
1451
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro"],
1452
+ excludeFiles: [
1453
+ "*.test.*",
1454
+ "*.spec.*",
1455
+ "**/tests/**",
1456
+ "**/__tests__/**",
1457
+ "**/fixtures/**",
1458
+ "*.stories.*",
1459
+ "**/dist/**",
1460
+ "**/build/**",
1461
+ "**/node_modules/**",
1462
+ "**/vendor/**",
1463
+ "**/emails/**",
1464
+ "dist/**",
1465
+ "build/**",
1466
+ "node_modules/**",
1467
+ "vendor/**",
1468
+ "emails/**"
1469
+ ],
1470
+ scope: "file",
1471
+ stage: "diff",
1472
+ fileCheck: "fe-raw-img-over-component",
1473
+ message: "A raw <img> in a project that already has an image component. Every content image goes through ONE component, so loading, sizing, alt text, lazy decoding and the lightbox stay the same everywhere and a fix lands once; an <img> per page is a second, drifting copy of it. Use the component named here. Mark the line `enigma:allow-raw-img` where the element must stay raw - an email template, a tracking pixel, or markup the component cannot render (frontend-policy).",
1474
+ severity: "block",
1475
+ skill: "frontend-policy"
1476
+ },
1413
1477
  {
1414
1478
  id: "fe-toast-hand-rolled",
1415
1479
  label: "Toasts come from the primitive",
@@ -2220,6 +2284,35 @@ var BUILTIN_RULES = [
2220
2284
  severity: "block",
2221
2285
  skill: "security-policy"
2222
2286
  },
2287
+ {
2288
+ id: "sec-private-term",
2289
+ label: "Real people's data stays out of the repository",
2290
+ files: ["**"],
2291
+ excludeFiles: [
2292
+ "**/node_modules/**",
2293
+ "**/dist/**",
2294
+ "**/build/**",
2295
+ "**/vendor/**",
2296
+ "**/.next/**",
2297
+ "node_modules/**",
2298
+ "dist/**",
2299
+ "build/**",
2300
+ "vendor/**",
2301
+ "*.lock",
2302
+ "*.lockb",
2303
+ "package-lock.json",
2304
+ "*.min.js",
2305
+ "*.map"
2306
+ ],
2307
+ scope: "file",
2308
+ // DIFF stage like sec-operator-env-leak: it fires on the line being added, the moment
2309
+ // before the value reaches the history for good, and never on an old file it did not write.
2310
+ stage: "diff",
2311
+ fileCheck: "sec-private-term",
2312
+ message: "This line carries one of your private terms - the real name, handle, server or community name of you or of the people you serve (the list in ~/.enigma-guard.json, `enigma config guard-private-terms`). Real data never goes into a tracked file, and tests, fixtures, examples and docs use ANONYMOUS data: invent the values (`Player One`, `alice@example.test`, `Test Server`) and, when a real log or payload shape matters, reproduce the shape with invented values instead of pasting the capture. Being shown a real value to work with is permission to use it, never to publish it (security-policy, testing-policy).",
2313
+ severity: "block",
2314
+ skill: "security-policy"
2315
+ },
2223
2316
  // The injection class. Every one but sec-unsafe-deserialization is DIFF stage for the reason the
2224
2317
  // stage exists: each has a measured backlog in real repositories (the counts are in guardrails.md),
2225
2318
  // so at the edit stage it would block unrelated edits to a file that already carried the shape.
@@ -2689,7 +2782,7 @@ var PROJECT_CHECKS = {
2689
2782
  "ts-relational-no-prisma": (root) => {
2690
2783
  const pkg = readPkgDeps(root);
2691
2784
  if (!pkg) return false;
2692
- const hasTs = "typescript" in pkg || existsSync(join(root, "tsconfig.json"));
2785
+ const hasTs = "typescript" in pkg || existsSync(join2(root, "tsconfig.json"));
2693
2786
  if (!hasTs) return false;
2694
2787
  const relational = ["typeorm", "sequelize", "knex", "drizzle-orm", "pg", "mysql", "mysql2", "better-sqlite3", "@mikro-orm/core"];
2695
2788
  if (!relational.some((d) => d in pkg)) return false;
@@ -2726,6 +2819,7 @@ var FILE_CHECKS = {
2726
2819
  "ts-alias-deep-relative": (content, file) => deepRelativeImports(content, file),
2727
2820
  "ts-alias-paths": (content, file) => missingPathAlias(content, file),
2728
2821
  "sec-operator-env-leak": (content) => operatorHomePathLeak(content),
2822
+ "sec-private-term": (content) => privateTermLeak(content),
2729
2823
  "fe-new-password-affordance-on-signin": (content) => newPasswordAffordanceOnSignIn(content),
2730
2824
  "fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
2731
2825
  "fe-selection-hand-rolled": (content) => handRolledSelection(content),
@@ -2735,6 +2829,7 @@ var FILE_CHECKS = {
2735
2829
  "sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
2736
2830
  "fe-select-hand-rolled": (content) => handRolledSelect(content),
2737
2831
  "fe-native-select-over-primitive": (content) => nativeSelectOverPrimitive(content),
2832
+ "fe-raw-img-over-component": (content, file) => rawImgOverComponent(content, file),
2738
2833
  "fe-toast-hand-rolled": (content) => handRolledToast(content),
2739
2834
  "fe-palette-hand-rolled": (content) => handRolledPalette(content),
2740
2835
  "sec-sql-built-from-values": (content, file) => sqlBuiltFromValues(content, file),
@@ -2796,7 +2891,7 @@ function applyFixes(file, findings, stage = "edit") {
2796
2891
  if (!fixable.length) return { fixed: [], remaining: findings };
2797
2892
  let content;
2798
2893
  try {
2799
- content = readFileSync(file, "utf8");
2894
+ content = readFileSync2(file, "utf8");
2800
2895
  } catch {
2801
2896
  return { fixed: [], remaining: findings };
2802
2897
  }
@@ -3203,6 +3298,70 @@ function nativeSelectOverPrimitive(content) {
3203
3298
  }
3204
3299
  return [];
3205
3300
  }
3301
+ var RAW_IMG = /<img\b/;
3302
+ var ALLOW_RAW_IMG = /enigma:allow-raw-img/;
3303
+ var DECORATIVE_IMG = /alt=(?:""|''|\{["']{2}\})|\.svg\b|\b(?:logo|icon|flag|favicon|sprite)/i;
3304
+ var IMAGE_COMPONENT_FILE = /(?:^|\/)(?:image|img|picture)\.(?:tsx|jsx|vue|svelte|astro)$/i;
3305
+ var DEFINES_IMAGE = /\b(?:function|const|class)\s+(?:Image|Img|Picture)\b/;
3306
+ var LOCAL_IMAGE_COMPONENTS = ["src/components", "components", "src/components/ui", "components/ui", "src/lib/components", "app/components"];
3307
+ var imageComponentCache = /* @__PURE__ */ new Map();
3308
+ function projectImageComponent(root) {
3309
+ const cached = imageComponentCache.get(root);
3310
+ if (cached !== void 0) return cached;
3311
+ let deps = {};
3312
+ try {
3313
+ deps = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8")).dependencies ?? {};
3314
+ } catch {
3315
+ }
3316
+ let found = null;
3317
+ if ("@enigmax/primitives" in deps) found = "`Image` from @enigmax/primitives (`enigma add image`)";
3318
+ else if ("next" in deps) found = "`Image` from next/image";
3319
+ else if ("astro" in deps) found = "`Image` from astro:assets";
3320
+ else if ("@nuxt/image" in deps) found = "`NuxtImg` from @nuxt/image";
3321
+ else {
3322
+ for (const dir of LOCAL_IMAGE_COMPONENTS) {
3323
+ let names = [];
3324
+ try {
3325
+ names = readdirSync(join2(root, dir));
3326
+ } catch {
3327
+ continue;
3328
+ }
3329
+ const hit = names.find((n) => /^(?:image|img|picture)\.(?:tsx|jsx|vue|svelte|astro)$/i.test(n));
3330
+ if (hit) {
3331
+ found = `the project's own \`${dir}/${hit}\``;
3332
+ break;
3333
+ }
3334
+ }
3335
+ }
3336
+ imageComponentCache.set(root, found);
3337
+ return found;
3338
+ }
3339
+ function imgElementAt(lines, start) {
3340
+ const parts = [];
3341
+ for (let i = start; i < lines.length && i < start + 30; i++) {
3342
+ const text = i === start ? lines[i].slice(lines[i].search(RAW_IMG)) : lines[i];
3343
+ parts.push(text);
3344
+ if (/(?<!=)>/.test(text)) break;
3345
+ }
3346
+ return parts.join("\n");
3347
+ }
3348
+ function rawImgOverComponent(content, file) {
3349
+ const norm = file.replace(/\\/g, "/");
3350
+ if (!RAW_IMG.test(content) || IMAGE_COMPONENT_FILE.test(norm) || DEFINES_IMAGE.test(content)) return [];
3351
+ const root = findProjectRoot(file);
3352
+ const component = root ? projectImageComponent(root) : null;
3353
+ if (!component) return [];
3354
+ const lines = content.split("\n");
3355
+ const hits = [];
3356
+ for (let i = 0; i < lines.length; i++) {
3357
+ const line = lines[i];
3358
+ if (!RAW_IMG.test(line) || COMMENT_LINE.test(line) || allowedAt(lines, i, ALLOW_RAW_IMG)) continue;
3359
+ const element = imgElementAt(lines, i);
3360
+ if (DECORATIVE_IMG.test(element) || ALLOW_RAW_IMG.test(element)) continue;
3361
+ hits.push({ line: i + 1, detail: `this project already has ${component}` });
3362
+ }
3363
+ return hits;
3364
+ }
3206
3365
  var TOAST_QUEUE = /\bset(?:Toasts?|Snackbars?)\b|\btoasts\.(?:map|filter|slice)\b|\btoastQueue\b/;
3207
3366
  var TOAST_TIMER = /setTimeout\(/;
3208
3367
  var TOAST_MITIGATED = /@enigmax\/primitives|createNotifications|useNotifications|<Toaster|ToastT\b|ToasterProps\b|["']sonner["']|react-hot-toast|react-toastify|notistack|@radix-ui|@mui\/material|@chakra-ui|@mantine|antd|enigma:allow-hand-rolled-toast/;
@@ -3307,11 +3466,11 @@ function nearestTsconfig(file) {
3307
3466
  return cached;
3308
3467
  }
3309
3468
  seen.push(dir);
3310
- const candidate = join(dir, "tsconfig.json");
3469
+ const candidate = join2(dir, "tsconfig.json");
3311
3470
  if (existsSync(candidate)) {
3312
3471
  let found = null;
3313
3472
  try {
3314
- found = { dir, text: readFileSync(candidate, "utf8") };
3473
+ found = { dir, text: readFileSync2(candidate, "utf8") };
3315
3474
  } catch {
3316
3475
  found = null;
3317
3476
  }
@@ -3374,7 +3533,7 @@ function deepRelativeImports(content, file) {
3374
3533
  function missingPathAlias(content, file) {
3375
3534
  if (/["'](?:paths|extends)["']\s*:/.test(content)) return [];
3376
3535
  const dir = dirname(resolve(file));
3377
- const src = ["src", "app", "lib"].find((d) => existsSync(join(dir, d)));
3536
+ const src = ["src", "app", "lib"].find((d) => existsSync(join2(dir, d)));
3378
3537
  if (!src) return [];
3379
3538
  const anchor = content.split("\n").findIndex((l) => /["']compilerOptions["']/.test(l));
3380
3539
  return [{ line: anchor === -1 ? 1 : anchor + 1, detail: `no alias for ./${src}` }];
@@ -3383,7 +3542,7 @@ var NAMED_IMPORT = /^import[ \t]+(?:[\w$]+[ \t]*,[ \t]*)?(?:type[ \t]+)?\{([^}]*
3383
3542
  var GENERIC_ACCOUNTS = /* @__PURE__ */ new Set(["runner", "root", "ubuntu", "debian", "vagrant", "node", "user", "users", "developer", "ec2-user", "codespace", "gitpod", "jenkins", "circleci", "travis", "docker", "app"]);
3384
3543
  function operatorHome() {
3385
3544
  const env = process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME;
3386
- return env?.trim() || homedir();
3545
+ return env?.trim() || homedir2();
3387
3546
  }
3388
3547
  function operatorHomePathLeak(content) {
3389
3548
  const re = operatorHomeRegex("i");
@@ -3395,6 +3554,15 @@ function operatorHomePathLeak(content) {
3395
3554
  }
3396
3555
  return out;
3397
3556
  }
3557
+ var privateTermCache;
3558
+ function privateTermLeak(content) {
3559
+ const terms = readPrivateTerms();
3560
+ const key = terms.join("\n");
3561
+ if (privateTermCache?.key !== key) privateTermCache = { key, re: privateTermMatcher(terms) };
3562
+ const re = privateTermCache.re;
3563
+ if (re === null) return [];
3564
+ return privateTermLines(content, re).map((line) => ({ line, detail: "a private term from your list" }));
3565
+ }
3398
3566
  function redactOperatorHome(text) {
3399
3567
  const re = operatorHomeRegex("gi");
3400
3568
  return re === null ? text : text.replace(re, "~");
@@ -3798,7 +3966,7 @@ function wideNamedImports(content, max) {
3798
3966
  }
3799
3967
  function readPkgDeps(root) {
3800
3968
  try {
3801
- const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
3969
+ const pkg = JSON.parse(readFileSync2(join2(root, "package.json"), "utf8"));
3802
3970
  return { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies };
3803
3971
  } catch {
3804
3972
  return null;
@@ -3820,7 +3988,7 @@ function globRe(glob, ignoreCase = false) {
3820
3988
  return re;
3821
3989
  }
3822
3990
  function guardrailsConfigPath() {
3823
- return process.env.ENIGMA_GUARDRAILS_CONFIG || join(homedir(), ".enigma-guardrails.json");
3991
+ return process.env.ENIGMA_GUARDRAILS_CONFIG || join2(homedir2(), ".enigma-guardrails.json");
3824
3992
  }
3825
3993
  function isValidRule(r) {
3826
3994
  const x = r;
@@ -3834,7 +4002,7 @@ function loadRules() {
3834
4002
  let disabled = [];
3835
4003
  let custom = [];
3836
4004
  try {
3837
- const raw = JSON.parse(readFileSync(guardrailsConfigPath(), "utf8"));
4005
+ const raw = JSON.parse(readFileSync2(guardrailsConfigPath(), "utf8"));
3838
4006
  if (Array.isArray(raw.disabled)) disabled = raw.disabled.filter((s) => typeof s === "string");
3839
4007
  if (Array.isArray(raw.rules)) custom = raw.rules.filter(isValidRule);
3840
4008
  } catch {
@@ -3845,10 +4013,10 @@ function loadRules() {
3845
4013
  function findProjectRoot(file) {
3846
4014
  let dir = dirname(resolve(file));
3847
4015
  for (let i = 0; i < 40; i++) {
3848
- const isProj = existsSync(join(dir, "package.json")) || existsSync(join(dir, ".enigma.json"));
4016
+ const isProj = existsSync(join2(dir, "package.json")) || existsSync(join2(dir, ".enigma.json"));
3849
4017
  let hasGit = false;
3850
4018
  try {
3851
- hasGit = statSync(join(dir, ".git")).isDirectory();
4019
+ hasGit = statSync(join2(dir, ".git")).isDirectory();
3852
4020
  } catch {
3853
4021
  }
3854
4022
  if (isProj || hasGit) return dir;
@@ -3914,7 +4082,7 @@ function formatFindings(findings) {
3914
4082
  function checkPath(file, stage = "edit") {
3915
4083
  let content;
3916
4084
  try {
3917
- content = readFileSync(file, "utf8");
4085
+ content = readFileSync2(file, "utf8");
3918
4086
  } catch {
3919
4087
  return [];
3920
4088
  }
@@ -3953,7 +4121,7 @@ function repairableDiffFindings(file) {
3953
4121
  if (!changed) return [];
3954
4122
  let content;
3955
4123
  try {
3956
- content = readFileSync(file, "utf8");
4124
+ content = readFileSync2(file, "utf8");
3957
4125
  } catch {
3958
4126
  return [];
3959
4127
  }
@@ -3967,7 +4135,7 @@ function repairableDiffFindings(file) {
3967
4135
  function runGuardrailsHook(payload) {
3968
4136
  let file;
3969
4137
  try {
3970
- file = JSON.parse(payload ?? readFileSync(0, "utf8"))?.tool_input?.file_path;
4138
+ file = JSON.parse(payload ?? readFileSync2(0, "utf8"))?.tool_input?.file_path;
3971
4139
  } catch {
3972
4140
  }
3973
4141
  if (!file || typeof file !== "string") return 0;
@@ -4354,16 +4522,16 @@ function pathJoinedFromInput(content, file) {
4354
4522
  if (!python && /^\s*["']use client["']/m.test(content)) return [];
4355
4523
  if (PATH_CONTAINED.test(content)) return [];
4356
4524
  const input = python ? REQUEST_INPUT_PY : REQUEST_INPUT_JS;
4357
- const join2 = python ? PATH_JOIN_PY : PATH_JOIN_JS;
4525
+ const join3 = python ? PATH_JOIN_PY : PATH_JOIN_JS;
4358
4526
  const lines = content.split("\n");
4359
4527
  const names = namesFromInput(lines, input);
4360
4528
  const out = [];
4361
4529
  for (let i = 0; i < lines.length; i++) {
4362
4530
  const text = lines[i];
4363
4531
  if (auditSkipLine(text)) continue;
4364
- join2.lastIndex = 0;
4532
+ join3.lastIndex = 0;
4365
4533
  let m;
4366
- while (m = join2.exec(text)) {
4534
+ while (m = join3.exec(text)) {
4367
4535
  const parts = (m[1] ?? m[2] ?? "").split(",").slice(1).map((s) => s.trim());
4368
4536
  if (parts.some((p) => input.test(p) || names.has(p) || names.has(p.replace(/^`\$\{(\w+)\}.*`$/, "$1")))) {
4369
4537
  out.push({ line: i + 1, detail: text.trim().slice(0, 120) });
@@ -4730,7 +4898,7 @@ var LEDGER_MAX_BYTES = 512 * 1024;
4730
4898
  var LEDGER_KEEP = 2e3;
4731
4899
  var LEDGER_KEEP_REPLY = 200;
4732
4900
  function ledgerPath() {
4733
- return process.env.ENIGMA_GUARDRAILS_LOG || join(homedir(), ".enigma", "guardrail-log.jsonl");
4901
+ return process.env.ENIGMA_GUARDRAILS_LOG || join2(homedir2(), ".enigma", "guardrail-log.jsonl");
4734
4902
  }
4735
4903
  function ledgerKey(rule, outcome, file, line) {
4736
4904
  return JSON.stringify([rule, outcome, file, line ?? null]);
@@ -4766,7 +4934,7 @@ function recordFindings(findings, outcome, stage = "edit") {
4766
4934
  } catch {
4767
4935
  }
4768
4936
  if (size > LEDGER_MAX_BYTES) {
4769
- const kept = rotateLedger(readFileSync(path, "utf8"));
4937
+ const kept = rotateLedger(readFileSync2(path, "utf8"));
4770
4938
  writeFileSync(path, kept.length ? `${kept.join("\n")}
4771
4939
  ` : "");
4772
4940
  }
@@ -4801,7 +4969,7 @@ function rotateLedger(text) {
4801
4969
  function eachLedgerEntry(sinceDays, visit) {
4802
4970
  let text;
4803
4971
  try {
4804
- text = readFileSync(ledgerPath(), "utf8");
4972
+ text = readFileSync2(ledgerPath(), "utf8");
4805
4973
  } catch {
4806
4974
  return;
4807
4975
  }
@@ -4871,7 +5039,7 @@ function runGuardrailsScan(all) {
4871
5039
  for (const file of files) {
4872
5040
  let content;
4873
5041
  try {
4874
- content = readFileSync(file, "utf8");
5042
+ content = readFileSync2(file, "utf8");
4875
5043
  } catch {
4876
5044
  continue;
4877
5045
  }
@@ -4947,9 +5115,11 @@ export {
4947
5115
  pageAwaitWithoutBoundary,
4948
5116
  pathJoinedFromInput,
4949
5117
  poweredByBuzzword,
5118
+ privateTermLeak,
4950
5119
  purpleGradient,
4951
5120
  queryInLoop,
4952
5121
  radixContentWithoutPortal,
5122
+ rawImgOverComponent,
4953
5123
  readLedger,
4954
5124
  readReplyLedger,
4955
5125
  recordFindings,