enigma-cli 1.33.7 → 1.34.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.
@@ -7,6 +7,6 @@
7
7
  "minimalCode"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.33.7",
10
+ "cliVersion": "1.34.0",
11
11
  "sha": "3f0dcc28341bb0407860534f7ce9314cfc91b5e673f8f3d13b89d61851ed75f6"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
6
6
  "updated": "2026-06-16T11:24:30+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "f742a2be3f328b9ea1ff9a35a449177c2cbec35ad16e46f7054b7a873a2ab017"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, modern TypeScript project configuration (module resolution, strict flags, @/* path alias), API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "652637f818b4ce4f4d5d83fa68fa57fe7842e32fb179523958c5496af54bb938"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports incl. namespace imports for wide module surfaces, path-alias specifiers and no file extensions, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "3beb0383a9cb0331ef8a579308a2517d24d57a8ba33751ad17d16d0a4f66e344"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "b35f0bb4a29f2346d9a1b00a4bfee9557a9b6a44116d9e6e59abcd945466dbcb"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Core engineering execution policy and harness orchestration (highest-authority rules), including the generalization rule that treats a named example as a class to sweep and fix.",
6
6
  "updated": "2026-08-02T04:28:51+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "95091678d4e72e3a9b1ef3ad0514e503a2a631f0d134bf772b4a1b3c66881995"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: engine selection (PostgreSQL by default, SQLite only for local-first/embedded stores), ORM selection (Prisma in TypeScript/JavaScript), query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "f4ccf51286027c97c8402f3bac6c894e05a84cba2de014463cb3a83be17537d4"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "968876bcac9ce6e5a20be05c89ac97191b112c9e08679a8dc506d150c375fec4"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
6
  "updated": "2026-06-01T00:45:28+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Transactional email: React Email templates instead of hand-written HTML tables, server-side rendering, one send module behind the provider SDK, plain-text alternatives, idempotent background sending, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).",
6
6
  "updated": "2026-07-30T19:29:19+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "c9724fdbcdbeab99573be3fd44d4cdd97c2a394d99f3c4395f118f17356b00ed"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one.",
6
6
  "updated": "2026-07-29T01:18:36+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
9
9
  }
@@ -288,6 +288,19 @@ Where this lives and when it runs:
288
288
 
289
289
  ---
290
290
 
291
+ ## A Textarea Is Bounded At Both Ends
292
+
293
+ A textarea is the only input the user can resize, so it is the only one whose size can break a layout after the page has rendered. Give every textarea a floor and a ceiling; the `fe-textarea-size-bounds` guardrail blocks a new one that has neither.
294
+
295
+ - Floor: `rows` (or a `min-height`) so it never collapses to a line and a half. A bare `<textarea>` defaults to about two rows and can be dragged smaller than the text it holds.
296
+ - Ceiling: a `max-height`, so dragging the handle cannot push the rest of the page off the screen. Past the ceiling the textarea scrolls its own content; the page does not grow.
297
+ - Prefer `resize: vertical` over free resize: horizontal dragging escapes the column and breaks a form grid that everything else respects. `resize: none` is acceptable only when the size is genuinely fixed, and it removes an affordance the user expects, so do not reach for it first.
298
+ - An autosizing textarea (`field-sizing: content`, a `scrollHeight` assignment, an autosize package) MUST carry the ceiling: growing with content is exactly the case where a long paste otherwise pushes the submit button out of reach.
299
+ - Put the bounds on the shared Textarea component or one base rule, not on each usage. A per-usage fix is one component away from being forgotten, and the rule is cleared by any bound in the file precisely so the base fix is the one that scales.
300
+ - The exceptions, and they are real: a surface that owns its viewport (a full-page editor, a code or log pane sized to its container) and any case the design deliberately calls otherwise. Mark that line `enigma:allow-unbounded-textarea` so the intent is recorded rather than re-litigated.
301
+
302
+ ---
303
+
291
304
  ## Sign-In, Sign-Up and Recovery Screens
292
305
 
293
306
  Auth is the first screen a user meets and the one most often shipped half-built. Treat the four screens as one flow: sign in, sign up, forgot password, set a new password. The server-side rules (token lifetime, rate limits, what an answer may reveal) are owned by security-policy; what follows is the UI half.
@@ -425,10 +438,15 @@ Never render an unbounded or large dataset in one shot (no fetch-everything then
425
438
 
426
439
  ## Optimistic UI & Rollback
427
440
 
428
- - Use optimistic UI updates when the operation is safe and likely to succeed.
429
- - Always implement rollback handling for failed operations.
430
- - Reconcile optimistic state with the server response; never leave the UI in a divergent state.
431
- - Keep the optimistic update and any client cache consistent with each other.
441
+ For a reversible user action, optimistic is the DEFAULT and not an enhancement to add later. The order is: apply the change to local state, then send the request, then reconcile - never request, await, and only then touch the UI.
442
+
443
+ - The defect has a name and a shape: a handler that awaits the mutation and updates state afterwards (`await fetch(...)` then `setItems(prev => prev.filter(...))`, or `await api.patch(...)` then a refetch). The user pays the whole round trip for an action whose outcome was never in doubt. The `fe-server-first-mutation` guardrail catches the clearest form of this on the lines a change adds; the rest is on you, because the rule cannot see a handler that hides the wait behind a parent refetch (`await onChanged()`, `await load()`, `router.refresh()`) - that is the same defect and it is the most common one.
444
+ - Which actions: toggle a flag, remove a row, rename, reorder, mark read, add a tag, favourite, vote - anything the client can compute the result of. Optimistic is wrong when only the server knows the outcome: a payment or charge, a server-assigned identifier the UI must display, a uniqueness check, a long job whose result IS the response, anything irreversible.
445
+ - Rollback is part of the feature, not a follow-up. Capture the previous value BEFORE mutating (`const previous = items`), restore it on failure, and say what failed - a silent revert reads as a UI bug and is worse than the wait. Do not roll back a value the user has since edited: reconcile against the server's response rather than blindly restoring.
446
+ - Reconcile optimistic state with the server response; never leave the UI in a divergent state, and keep the optimistic update and any client cache consistent with each other (invalidate or patch the cache, do not leave a stale list beside a fresh one).
447
+ - Where a delete is reversible, the optimistic update plus an Undo affordance replaces the confirmation prompt entirely (see Destructive & Irreversible Actions).
448
+ - Guard against the double-apply: an in-flight mutation that the user triggers again must not stack two optimistic updates, and a re-render must not re-apply one.
449
+ - With a data layer that owns this (TanStack Query `onMutate` + `onError` rollback + `onSettled` invalidate, SWR `optimisticData` + `rollbackOnError`, React `useOptimistic`), use it instead of hand-rolling local state - it already solves the cache reconciliation.
432
450
  - Surface failures to the user clearly without exposing internal error details (see validation-policy).
433
451
 
434
452
  ---
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "frontend-policy",
3
- "version": "1.25.0",
3
+ "version": "1.27.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, no-op detection (skip any operation whose result equals current state, not just form saves; dirty means different from the loaded snapshot, not touched), instant first paint (render the shell, load data async, skeletons), perceived performance (prefetch on intent, debounce/throttle, cancel stale requests, avoid waterfalls, lazy widgets), large-list rendering (infinite scroll is the preferred default, pagination the deliberate exception when the design or the user calls for it, virtualization, skeletons, progressive loading), data views that ship their own affordances by default (a log, expense, transaction or history table is not done when the rows render: fuse.js search, the filters its column kinds imply, a date range, sort, filter state in the URL, and an export of the filtered set), every reference to an entity being a way into it (a name, id, project or path in a row links to that record, reveals it in a hover card, or at minimum copies and filters by it, never inert text; machine codes get human labels and raw payloads are never dumped into a cell), optimistic UI with rollback, visual restraint (one card level, spacing before borders, one elevation scale), icon actions (repeated row/card actions are icon-only buttons with aria-label plus title, not text labels), responsive/adaptive layout (fluid units, breakpoints, no overlap/overflow, viewport meta, touch targets), form fields that declare their keyboard and casing (autocapitalize/autocomplete/inputmode per field kind, set once in the shared Input, normalized on blur), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), auth screens (breached-password feedback, strength meter, cookie consent before login/register), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
6
- "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.33.7",
8
- "sha": "6c04c2bf01f5d46d75f032fa93f5ec92244f327fb6f266de46ed448ce50a0a99"
6
+ "updated": "2026-08-02T22:52:54+02:00",
7
+ "cliVersion": "1.34.0",
8
+ "sha": "6c4b973c41ca7c12e1839a4fba76f26ede4e04a57567392687ca61e7f6687d9b"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
6
  "updated": "2026-07-16T22:44:02+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "logoColorPolicy"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.33.7",
10
+ "cliVersion": "1.34.0",
11
11
  "sha": "09cdbefd98625b02a7d03685e5deed128238ff8454a83fe22279610fe3ef8ddf"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), credential flows with breach-checked passwords (Have I Been Pwned), OWASP Top 10, transport/crypto baseline, cookies and consent, secure logging, and agent/MCP/tool-use safety.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "3201b6d41437626eb7bf45648c0d7f8c419dc4de87fbd9d4ca83c5ae3b6b6edf"
9
9
  }
@@ -26,7 +26,7 @@
26
26
  tr:nth-child(even) td { background: #faf9f5; }
27
27
  tr:hover td { background: #f3f1ea; }
28
28
  .section-header td { background: #e8e6dc; font-family: 'Poppins', sans-serif; font-weight: 500; font-size: 0.8rem; color: #141413; text-transform: uppercase; letter-spacing: 0.05em; }
29
- .query-input { width: 100%; padding: 0.4rem; border: 1px solid #e8e6dc; border-radius: 4px; font-size: 0.875rem; font-family: 'Lora', Georgia, serif; resize: vertical; min-height: 60px; }
29
+ .query-input { width: 100%; padding: 0.4rem; border: 1px solid #e8e6dc; border-radius: 4px; font-size: 0.875rem; font-family: 'Lora', Georgia, serif; resize: vertical; min-height: 60px; max-height: 40vh; }
30
30
  .query-input:focus { outline: none; border-color: #d97757; box-shadow: 0 0 0 2px rgba(217,119,87,0.15); }
31
31
  .toggle { position: relative; display: inline-block; width: 44px; height: 24px; }
32
32
  .toggle input { opacity: 0; width: 0; height: 0; }
@@ -239,6 +239,7 @@
239
239
  .feedback-textarea {
240
240
  width: 100%;
241
241
  min-height: 100px;
242
+ max-height: 50vh;
242
243
  padding: 0.75rem;
243
244
  border: 1px solid var(--border);
244
245
  border-radius: 4px;
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Create new skills, modify and improve existing skills, and measure skill performance with evals and benchmarks.",
6
6
  "updated": "2026-08-02T19:58:42+02:00",
7
- "cliVersion": "1.33.7",
8
- "sha": "34fc27f140ad425eff742d319b3179f861f70edc966a2cd96d1cd6b91717b766"
7
+ "cliVersion": "1.34.0",
8
+ "sha": "a49f6166f57d734e0f13487332da37903e925f32257447856af0fbd6a5f1dc84"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Exhaustive completion discipline for long/multi-item tasks - inventory, coverage ledger, verified done.",
6
6
  "updated": "2026-08-02T04:15:47+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "4c2197954135dd3375f32d839238b82a95c0e93e976ddf5a9cbc4a0cf0146e97"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Concise, realistic technical copy - UI microcopy, descriptions, hints, empty/error states, and README/doc prose that informs without over-explaining or restating the obvious, and never uses a typographic dash.",
6
6
  "updated": "2026-07-28T20:30:23+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "a4b792103eb1f9dad93b9d70ea79dc18fe9cbbc318facf5adb47ae5907d842f9"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, regression-first bug fixing, and test-suite organization (layout by type/domain, mirrored paths, file naming, fixture/helper placement).",
6
6
  "updated": "2026-06-16T17:11:49+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
9
9
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, normalization before validation (shared normalizers: trim, lowercase email, capitalize names, canonicalize links and handles), schema consistency, and safe client-facing error handling.",
6
6
  "updated": "2026-08-01T17:44:16+02:00",
7
- "cliVersion": "1.33.7",
7
+ "cliVersion": "1.34.0",
8
8
  "sha": "225e1e26f4a49fac70714b1bebc104ea42143203cc8fcc9e3330b29f05b6e025"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "527b4fd317dd95669889d9e474a1f0dbd926f3dfe69865eaff8f6254fe971256",
3
- "enigma-linux-arm64": "ed239f141d8628719494ca4563db886a880dbdf3f0f2456bfe0efe77c3918dc6",
4
- "enigma-linux-x64": "24950b825b7427d9651cf42c85ed29667fbf8309135c7b5bc1fe4811c5f91f99",
5
- "enigma-win32-x64.exe": "332cc818638fdb0fb94a651af2cfe49da3b1bde40bd151f42624108f4fb374e2"
2
+ "enigma-darwin-arm64": "1b0fdfa47b7dce341360391ffa8e3d78f061779312cb3ac7d8a42f92016faf18",
3
+ "enigma-linux-arm64": "17b52af9adcbea9bed91cbaae68658aaffa561bfc60b95dd159b481074e23faa",
4
+ "enigma-linux-x64": "702089b4919a13b954b60aa38b96ae5f10091ea13511cf56b135fb589d2d9857",
5
+ "enigma-win32-x64.exe": "ff51dfc714136f661ad798180a2b31c87f24dffe56714ce4b782fe22d6eb1ec4"
6
6
  }
@@ -5,7 +5,7 @@ import { homedir } from "os";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execFileSync } from "child_process";
7
7
  import { dirname, join, resolve, sep } from "path";
8
- import { readFileSync, writeFileSync, statSync, existsSync } from "fs";
8
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync, statSync, existsSync } from "fs";
9
9
  var COMMENT_LINE = /^\s*(\/\/|#|\*|--|<!--|\{?\/\*)/;
10
10
  var BUILTIN_RULES = [
11
11
  {
@@ -428,6 +428,39 @@ var BUILTIN_RULES = [
428
428
  severity: "block",
429
429
  skill: "frontend-policy"
430
430
  },
431
+ {
432
+ id: "fe-server-first-mutation",
433
+ label: "Optimistic update for a reversible mutation",
434
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte"],
435
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
436
+ scope: "file",
437
+ // DIFF stage, and that is the whole reason this rule can exist. Measured over 2762 UI files
438
+ // of real product repositories, 116 of the 140 files that mutate anything hold no optimistic
439
+ // update at all - a legacy backlog an edit-stage rule would fire on forever, which is how a
440
+ // gate teaches people to ignore it. Against the lines a turn ADDED there is no backlog: it
441
+ // fires only on a mutation the agent just wrote.
442
+ stage: "diff",
443
+ fileCheck: "fe-server-first-mutation",
444
+ message: "This mutation waits for the server before it touches the UI: the row is only dropped (or the flag only flipped) after the request resolves, so the interface freezes for the whole round trip on an action that cannot really fail in an interesting way. Apply the change to local state FIRST, then send the request, and on failure restore the value you saved and say what failed - a silent revert is worse than the wait. Where the delete is reversible, prefer acting immediately with an Undo affordance over a confirmation prompt. Mark the line `enigma:allow-server-first` when the server's response is genuinely required before the UI may change (a payment, a server-assigned identifier, an irreversible action) (frontend-policy).",
445
+ severity: "block",
446
+ skill: "frontend-policy"
447
+ },
448
+ {
449
+ id: "fe-textarea-size-bounds",
450
+ label: "Textarea declares a minimum and a maximum size",
451
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.html", "*.htm"],
452
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
453
+ scope: "file",
454
+ // DIFF stage, for the reason the measurement gave: 17 corpus textarea sites carry no upper
455
+ // bound, so an edit-stage rule would report a project's existing forms on every unrelated
456
+ // edit to the same file. Against the lines a change adds there is no backlog, and the rule
457
+ // can be as strict as the convention actually is.
458
+ stage: "diff",
459
+ fileCheck: "fe-textarea-size-bounds",
460
+ message: "A textarea is the only input the user can resize, so it is the only one that can break a layout after it has rendered. Give it both bounds: `rows` (or a min-height) so it never collapses below a usable size, and a max-height so dragging it - or letting it grow with its content - cannot push the page apart. Prefer `resize: vertical` so the column width survives, and put the bounds on the shared Textarea component rather than on each usage. Mark the line `enigma:allow-unbounded-textarea` when the surface owns its viewport (a full-page editor, a code surface) or the design calls for something else (frontend-policy).",
461
+ severity: "block",
462
+ skill: "frontend-policy"
463
+ },
431
464
  {
432
465
  id: "fe-ai-elements-chat",
433
466
  label: "AI chat UI via AI Elements",
@@ -1196,6 +1229,8 @@ var PROJECT_CHECKS = {
1196
1229
  };
1197
1230
  var FILE_CHECKS = {
1198
1231
  "proc-windows-hide": (content) => missingWindowsHide(content),
1232
+ "fe-server-first-mutation": (content) => serverFirstMutation(content),
1233
+ "fe-textarea-size-bounds": (content) => textareaSizeBounds(content),
1199
1234
  "ts-import-extension": (content, file) => extensionImports(content, file),
1200
1235
  "ts-alias-deep-relative": (content, file) => deepRelativeImports(content, file),
1201
1236
  "ts-alias-paths": (content, file) => missingPathAlias(content, file)
@@ -1269,6 +1304,94 @@ function missingWindowsHide(content) {
1269
1304
  }
1270
1305
  return out;
1271
1306
  }
1307
+ var MUTATING_REQUEST = /method:\s*["'`](POST|PUT|PATCH|DELETE)|\b(?:axios|api|\$fetch|http|client)\.(?:post|put|patch|delete)\s*\(/i;
1308
+ var ENTITY_WRITE = /\bset[A-Z]\w*\s*\(\s*(?:\(?\w+\)?\s*=>\s*)?[\w.]*\.(?:filter|map|slice|concat)\s*\(|\bset[A-Z]\w*\s*\(\s*!/;
1309
+ var OPTIMISTIC_SIGNAL = /useOptimistic|onMutate|optimisticData|setQueryData|rollbackOnError|\brollback\s*[(:]|\brevert\s*[(:]|previous[A-Z_]/;
1310
+ var ALLOW_SERVER_FIRST = /enigma:allow-server-first/;
1311
+ var RESULT_BINDING = /(?:const|let|var)\s+(\w+)\s*=\s*(?:await\s+)?/;
1312
+ var BLOCK_LOOKBACK = 120;
1313
+ var BLOCK_LOOKAHEAD = 200;
1314
+ function enclosingBlock(lines, index) {
1315
+ let depth = 0;
1316
+ let start = index;
1317
+ let foundStart = false;
1318
+ for (let i = index; i >= 0 && index - i < BLOCK_LOOKBACK && !foundStart; i--) {
1319
+ const line = lines[i];
1320
+ for (let c = line.length - 1; c >= 0; c--) {
1321
+ if (line[c] === "}") depth++;
1322
+ else if (line[c] === "{") {
1323
+ if (depth === 0) {
1324
+ start = i;
1325
+ foundStart = true;
1326
+ break;
1327
+ }
1328
+ depth--;
1329
+ }
1330
+ }
1331
+ }
1332
+ depth = 0;
1333
+ let end = index;
1334
+ let foundEnd = false;
1335
+ for (let i = start; i < lines.length && i - start < BLOCK_LOOKAHEAD && !foundEnd; i++) {
1336
+ for (const ch of lines[i]) {
1337
+ if (ch === "{") depth++;
1338
+ else if (ch === "}") {
1339
+ depth--;
1340
+ if (depth === 0) {
1341
+ end = i;
1342
+ foundEnd = true;
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+ }
1348
+ return { start, end: Math.max(end, index) };
1349
+ }
1350
+ function serverFirstMutation(content) {
1351
+ if (OPTIMISTIC_SIGNAL.test(content)) return [];
1352
+ const lines = content.split("\n");
1353
+ const out = [];
1354
+ for (let i = 0; i < lines.length; i++) {
1355
+ const line = lines[i];
1356
+ if (COMMENT_LINE.test(line) || !MUTATING_REQUEST.test(line)) continue;
1357
+ const { start, end } = enclosingBlock(lines, i);
1358
+ if (lines.slice(start, end + 1).some((l) => ALLOW_SERVER_FIRST.test(l))) continue;
1359
+ if (lines.slice(start, i).some((l) => ENTITY_WRITE.test(l))) continue;
1360
+ let binding = "";
1361
+ for (let b = i; b >= Math.max(0, i - 4) && !binding; b--) binding = RESULT_BINDING.exec(lines[b])?.[1] ?? "";
1362
+ const uses = binding ? new RegExp(`\\b${binding}\\b`) : null;
1363
+ const write = lines.slice(i + 1, end + 1).find((l) => {
1364
+ if (COMMENT_LINE.test(l)) return false;
1365
+ const at = ENTITY_WRITE.exec(l);
1366
+ if (!at) return false;
1367
+ return !uses?.test(l.slice(at.index));
1368
+ });
1369
+ if (write) out.push({ line: i + 1, detail: `the UI is only updated after the request resolves: ${write.trim().slice(0, 80)}` });
1370
+ }
1371
+ return out;
1372
+ }
1373
+ var TEXTAREA = /<textarea\b/;
1374
+ var TEXTAREA_LOWER = /\brows\s*=|\brows:\s*\d|min-h-|min-height|minHeight|\bh-\[|\bh-\d|height\s*:\s*\d/;
1375
+ var TEXTAREA_UPPER = /max-h-|max-height|maxHeight/;
1376
+ var TEXTAREA_FIXED = /resize-none|resize\s*:\s*none/;
1377
+ var TEXTAREA_AUTOSIZE = /field-?sizing|scrollHeight|autosize|auto-size|TextareaAutosize|textarea-autosize/i;
1378
+ function textareaSizeBounds(content) {
1379
+ const lower = TEXTAREA_LOWER.test(content);
1380
+ const upper = TEXTAREA_UPPER.test(content);
1381
+ const fixed = TEXTAREA_FIXED.test(content) && !TEXTAREA_AUTOSIZE.test(content);
1382
+ if (lower && (upper || fixed)) return [];
1383
+ const out = [];
1384
+ const lines = content.split("\n");
1385
+ for (let i = 0; i < lines.length; i++) {
1386
+ const line = lines[i];
1387
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line) || !TEXTAREA.test(line)) continue;
1388
+ const missing = [];
1389
+ if (!lower) missing.push("no minimum size: no rows, min-height or fixed height");
1390
+ if (!upper && !fixed) missing.push("no maximum size: no max-height, and it can be dragged or grows with its content");
1391
+ if (missing.length) out.push({ line: i + 1, detail: missing.join("; ") });
1392
+ }
1393
+ return out;
1394
+ }
1272
1395
  var SPECIFIER = /^[ \t]*(?:import|export)\b[^;]*?\bfrom\s*["']([^"']+)["']|^[ \t]*import\s*["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']|\brequire\(\s*["']([^"']+)["']/gm;
1273
1396
  var MODULE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
1274
1397
  var JS_EXT = /\.(js|jsx|mjs|cjs)$/i;
@@ -1382,6 +1505,16 @@ function globToRegExp(glob, ignoreCase = false) {
1382
1505
  const body = esc.replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/ /g, ".*").replace(/\?/g, "[^/]");
1383
1506
  return new RegExp(glob.includes("/") ? `^${body}$` : `(^|/)${body}$`, ignoreCase ? "i" : "");
1384
1507
  }
1508
+ var globCache = /* @__PURE__ */ new Map();
1509
+ function globRe(glob, ignoreCase = false) {
1510
+ const key = `${ignoreCase ? "i" : "s"}:${glob}`;
1511
+ let re = globCache.get(key);
1512
+ if (!re) {
1513
+ re = globToRegExp(glob, ignoreCase);
1514
+ globCache.set(key, re);
1515
+ }
1516
+ return re;
1517
+ }
1385
1518
  function guardrailsConfigPath() {
1386
1519
  return process.env.ENIGMA_GUARDRAILS_CONFIG || join(homedir(), ".enigma-guardrails.json");
1387
1520
  }
@@ -1421,12 +1554,13 @@ function findProjectRoot(file) {
1421
1554
  }
1422
1555
  return null;
1423
1556
  }
1424
- function checkFile(file, content, projectRoot) {
1557
+ function checkFile(file, content, projectRoot, stage = "edit", rules = loadRules()) {
1425
1558
  const norm = file.replace(/\\/g, "/");
1426
1559
  const out = [];
1427
- for (const rule of loadRules()) {
1428
- if (!rule.files.some((g) => globToRegExp(g, rule.ignoreFileCase).test(norm))) continue;
1429
- if (rule.excludeFiles?.some((g) => globToRegExp(g, rule.ignoreFileCase).test(norm))) continue;
1560
+ for (const rule of rules) {
1561
+ if (stage === "edit" && rule.stage === "diff") continue;
1562
+ if (!rule.files.some((g) => globRe(g, rule.ignoreFileCase).test(norm))) continue;
1563
+ if (rule.excludeFiles?.some((g) => globRe(g, rule.ignoreFileCase).test(norm))) continue;
1430
1564
  const base = { ruleId: rule.id, severity: rule.severity, file: norm, message: rule.message, skill: rule.skill };
1431
1565
  if (rule.scope === "file" && rule.maxBytes) {
1432
1566
  const bytes = Buffer.byteLength(content, "utf8");
@@ -1473,7 +1607,7 @@ function formatFindings(findings) {
1473
1607
  return `${tag} ${f.file}${loc} (${f.ruleId})${skill}: ${f.message}`;
1474
1608
  }).join("\n");
1475
1609
  }
1476
- function checkPath(file) {
1610
+ function checkPath(file, stage = "edit") {
1477
1611
  let content;
1478
1612
  try {
1479
1613
  content = readFileSync(file, "utf8");
@@ -1481,7 +1615,7 @@ function checkPath(file) {
1481
1615
  return [];
1482
1616
  }
1483
1617
  if (content.includes("\0")) return [];
1484
- return checkFile(file, content, findProjectRoot(file));
1618
+ return checkFile(file, content, findProjectRoot(file), stage);
1485
1619
  }
1486
1620
  function runGuardrailsHook(payload) {
1487
1621
  let file;
@@ -1496,9 +1630,12 @@ function runGuardrailsHook(payload) {
1496
1630
  if (fixed.length) process.stdout.write(`enigma guardrails (fixed)
1497
1631
  ${fixed.map((f) => `${f.file}:${f.line} (${f.ruleId})`).join("\n")}
1498
1632
  `);
1633
+ recordFindings(fixed, "fixed");
1499
1634
  if (!findings.length) return 0;
1500
1635
  const warns = findings.filter((f) => f.severity === "warn");
1501
1636
  const blocks = findings.filter((f) => f.severity === "block");
1637
+ recordFindings(warns, "warned");
1638
+ recordFindings(blocks, "blocked");
1502
1639
  if (warns.length) process.stdout.write(`enigma guardrails (suggestions)
1503
1640
  ${formatFindings(warns)}
1504
1641
  `);
@@ -1511,6 +1648,97 @@ Fix the above before continuing.
1511
1648
  }
1512
1649
  return 0;
1513
1650
  }
1651
+ var LEDGER_MAX_BYTES = 512 * 1024;
1652
+ var LEDGER_KEEP = 2e3;
1653
+ function ledgerPath() {
1654
+ return process.env.ENIGMA_GUARDRAILS_LOG || join(homedir(), ".enigma", "guardrail-log.jsonl");
1655
+ }
1656
+ function ledgerKey(rule, outcome, file, line) {
1657
+ return JSON.stringify([rule, outcome, file, line ?? null]);
1658
+ }
1659
+ function recordedToday(day) {
1660
+ const seen = /* @__PURE__ */ new Set();
1661
+ eachLedgerEntry(1, (e) => {
1662
+ if (e.at.slice(0, 10) === day) seen.add(ledgerKey(e.rule, e.outcome, e.file, e.line));
1663
+ });
1664
+ return seen;
1665
+ }
1666
+ function recordFindings(findings, outcome, stage = "edit") {
1667
+ if (!findings.length) return;
1668
+ const at = (/* @__PURE__ */ new Date()).toISOString();
1669
+ const seen = stage === "diff" || outcome !== "blocked" ? recordedToday(at.slice(0, 10)) : null;
1670
+ const rows = [];
1671
+ for (const f of findings) {
1672
+ if (seen) {
1673
+ const key = ledgerKey(f.ruleId, outcome, f.file, f.line);
1674
+ if (seen.has(key)) continue;
1675
+ seen.add(key);
1676
+ }
1677
+ rows.push(JSON.stringify({ at, rule: f.ruleId, severity: f.severity, outcome, stage, file: f.file, line: f.line }));
1678
+ }
1679
+ if (!rows.length) return;
1680
+ const path = ledgerPath();
1681
+ try {
1682
+ mkdirSync(dirname(path), { recursive: true });
1683
+ let size = 0;
1684
+ try {
1685
+ size = statSync(path).size;
1686
+ } catch {
1687
+ }
1688
+ if (size > LEDGER_MAX_BYTES) {
1689
+ const kept = readFileSync(path, "utf8").split("\n").filter(Boolean).slice(-LEDGER_KEEP);
1690
+ writeFileSync(path, `${kept.join("\n")}
1691
+ `);
1692
+ }
1693
+ appendFileSync(path, `${rows.join("\n")}
1694
+ `);
1695
+ } catch {
1696
+ }
1697
+ }
1698
+ function eachLedgerEntry(sinceDays, visit) {
1699
+ let text;
1700
+ try {
1701
+ text = readFileSync(ledgerPath(), "utf8");
1702
+ } catch {
1703
+ return;
1704
+ }
1705
+ const cutoff = sinceDays > 0 ? Date.now() - sinceDays * 864e5 : 0;
1706
+ for (const line of text.split("\n")) {
1707
+ if (!line.trim()) continue;
1708
+ try {
1709
+ const entry = JSON.parse(line);
1710
+ if (typeof entry?.rule !== "string") continue;
1711
+ if (cutoff && Date.parse(entry.at) < cutoff) continue;
1712
+ visit(entry);
1713
+ } catch {
1714
+ }
1715
+ }
1716
+ }
1717
+ function readLedger(sinceDays = 0) {
1718
+ const out = [];
1719
+ eachLedgerEntry(sinceDays, (entry) => out.push(entry));
1720
+ return out;
1721
+ }
1722
+ function countLedger(sinceDays = 0) {
1723
+ let count = 0;
1724
+ eachLedgerEntry(sinceDays, () => {
1725
+ count++;
1726
+ });
1727
+ return count;
1728
+ }
1729
+ function summarizeLedger(entries) {
1730
+ const by = /* @__PURE__ */ new Map();
1731
+ for (const e of entries) {
1732
+ const row = by.get(e.rule) || { rule: e.rule, total: 0, blocked: 0, warned: 0, fixed: 0, last: e.at };
1733
+ row.total++;
1734
+ if (e.outcome === "blocked") row.blocked++;
1735
+ else if (e.outcome === "warned") row.warned++;
1736
+ else row.fixed++;
1737
+ if (e.at > row.last) row.last = e.at;
1738
+ by.set(e.rule, row);
1739
+ }
1740
+ return [...by.values()].sort((a, b) => b.total - a.total || a.rule.localeCompare(b.rule));
1741
+ }
1514
1742
  function gitFiles(all) {
1515
1743
  const out = execFileSync("git", all ? ["ls-files"] : ["diff", "--cached", "--name-only", "--diff-filter=ACM"], { encoding: "utf8", windowsHide: true });
1516
1744
  return out.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -1570,6 +1798,7 @@ export {
1570
1798
  applyFixes,
1571
1799
  checkFile,
1572
1800
  checkPath,
1801
+ countLedger,
1573
1802
  deepRelativeImports,
1574
1803
  extensionImports,
1575
1804
  findProjectRoot,
@@ -1577,8 +1806,13 @@ export {
1577
1806
  loadRules,
1578
1807
  missingPathAlias,
1579
1808
  missingWindowsHide,
1809
+ readLedger,
1810
+ recordFindings,
1580
1811
  runGuardrailsHook,
1581
1812
  runGuardrailsScan,
1582
1813
  runGuardrailsScanCli,
1814
+ serverFirstMutation,
1815
+ summarizeLedger,
1816
+ textareaSizeBounds,
1583
1817
  wideNamedImports
1584
1818
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.33.7",
3
+ "version": "1.34.0",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {