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.
- package/assets/skills/anti-overengineering-policy/skill.json +1 -1
- package/assets/skills/anti-overengineering-review/skill.json +1 -1
- package/assets/skills/backend-policy/skill.json +1 -1
- package/assets/skills/ciphera-style-policy/skill.json +1 -1
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/skill.json +1 -1
- package/assets/skills/database-expert/skill.json +1 -1
- package/assets/skills/debugging-policy/skill.json +1 -1
- package/assets/skills/dependency-policy/skill.json +1 -1
- package/assets/skills/email-policy/skill.json +1 -1
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +22 -4
- package/assets/skills/frontend-policy/skill.json +4 -4
- package/assets/skills/git-policy/skill.json +1 -1
- package/assets/skills/logo-sourcing-policy/skill.json +1 -1
- package/assets/skills/security-policy/skill.json +1 -1
- package/assets/skills/skill-creator/assets/eval_review.html +1 -1
- package/assets/skills/skill-creator/eval-viewer/viewer.html +1 -0
- package/assets/skills/skill-creator/skill.json +2 -2
- package/assets/skills/task-completion-policy/skill.json +1 -1
- package/assets/skills/technical-writing-policy/skill.json +1 -1
- package/assets/skills/testing-policy/skill.json +1 -1
- package/assets/skills/validation-policy/skill.json +1 -1
- package/bin/checksums.json +4 -4
- package/dist/guardrails.js +241 -7
- package/package.json +1 -1
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
-
|
|
431
|
-
-
|
|
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.
|
|
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-
|
|
7
|
-
"cliVersion": "1.
|
|
8
|
-
"sha": "
|
|
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.
|
|
7
|
+
"cliVersion": "1.34.0",
|
|
8
8
|
"sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
|
|
9
9
|
}
|
|
@@ -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.
|
|
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; }
|
|
@@ -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.
|
|
8
|
-
"sha": "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
7
|
+
"cliVersion": "1.34.0",
|
|
8
8
|
"sha": "225e1e26f4a49fac70714b1bebc104ea42143203cc8fcc9e3330b29f05b6e025"
|
|
9
9
|
}
|
package/bin/checksums.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"enigma-darwin-arm64": "
|
|
3
|
-
"enigma-linux-arm64": "
|
|
4
|
-
"enigma-linux-x64": "
|
|
5
|
-
"enigma-win32-x64.exe": "
|
|
2
|
+
"enigma-darwin-arm64": "1b0fdfa47b7dce341360391ffa8e3d78f061779312cb3ac7d8a42f92016faf18",
|
|
3
|
+
"enigma-linux-arm64": "17b52af9adcbea9bed91cbaae68658aaffa561bfc60b95dd159b481074e23faa",
|
|
4
|
+
"enigma-linux-x64": "702089b4919a13b954b60aa38b96ae5f10091ea13511cf56b135fb589d2d9857",
|
|
5
|
+
"enigma-win32-x64.exe": "ff51dfc714136f661ad798180a2b31c87f24dffe56714ce4b782fe22d6eb1ec4"
|
|
6
6
|
}
|
package/dist/guardrails.js
CHANGED
|
@@ -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
|
|
1428
|
-
if (
|
|
1429
|
-
if (rule.
|
|
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.
|
|
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": {
|