enigma-cli 1.32.0 → 1.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/memory/AGENTS.md +1 -0
- package/assets/memory/CLAUDE.md +1 -0
- 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.md +17 -1
- package/assets/skills/ciphera-style-policy/skill.json +5 -5
- package/assets/skills/code-review-policy/skill.json +1 -1
- package/assets/skills/core-engineering-policy/SKILL.md +2 -0
- package/assets/skills/core-engineering-policy/skill.json +4 -4
- 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.md +95 -0
- package/assets/skills/email-policy/skill.json +9 -0
- package/assets/skills/frontend-design/skill.json +1 -1
- package/assets/skills/frontend-policy/SKILL.md +24 -0
- package/assets/skills/frontend-policy/skill.json +3 -3
- 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/skill.json +1 -1
- 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/bin/enigma.mjs +1 -0
- package/dist/guard.js +0 -0
- package/dist/guardrails.js +143 -2
- package/dist/trim.js +212 -0
- package/package.json +3 -2
package/assets/memory/AGENTS.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- anti-overengineering-policy: writing or refactoring implementation code, or any "be lazy"/"simplify"/over-engineering request - the YAGNI ladder and minimal-code discipline.
|
|
15
15
|
- anti-overengineering-review: on-demand "what can we delete"/audit/over-engineering review or an enigma: debt-marker ledger - lists cuts, applies nothing.
|
|
16
16
|
- backend-policy, frontend-policy, database-expert, validation-policy: server, client, persistence, and input-validation work.
|
|
17
|
+
- email-policy: sending or templating email from the server - React Email instead of hand-written HTML, plain-text alternative, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).
|
|
17
18
|
- security-policy: secrets, auth, permissions, crypto, untrusted/tool output, and AI-agent/MCP/tool-use safety.
|
|
18
19
|
- dependency-policy: adding/upgrading/auditing dependencies, lockfiles, and supply-chain risk.
|
|
19
20
|
- testing-policy, code-review-policy, debugging-policy, git-policy: tests and test-suite layout (structured subfolders, never a flat tests/ dump), pre-delivery review, debugging, and commits/PRs.
|
package/assets/memory/CLAUDE.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- anti-overengineering-policy: writing or refactoring implementation code, or any "be lazy"/"simplify"/over-engineering request - the YAGNI ladder and minimal-code discipline.
|
|
15
15
|
- anti-overengineering-review: on-demand "what can we delete"/audit/over-engineering review or an enigma: debt-marker ledger - lists cuts, applies nothing.
|
|
16
16
|
- backend-policy, frontend-policy, database-expert, validation-policy: server, client, persistence, and input-validation work.
|
|
17
|
+
- email-policy: sending or templating email from the server - React Email instead of hand-written HTML, plain-text alternative, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).
|
|
17
18
|
- security-policy: secrets, auth, permissions, crypto, untrusted/tool output, and AI-agent/MCP/tool-use safety.
|
|
18
19
|
- dependency-policy: adding/upgrading/auditing dependencies, lockfiles, and supply-chain risk.
|
|
19
20
|
- testing-policy, code-review-policy, debugging-policy, git-policy: tests and test-suite layout (structured subfolders, never a flat tests/ dump), pre-delivery review, debugging, and commits/PRs.
|
|
@@ -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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
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, API and request optimization (batching, avoiding redundant calls, skipping no-op writes), server-side caching (Redis), and Zod boundary validation.",
|
|
6
6
|
"updated": "2026-07-23T04:41:39+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "d091e8708888d23ee14cdd6c0e71f386861f2252d95360895a31c73db090a689"
|
|
9
9
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ciphera-style-policy
|
|
3
|
-
description: Ciphera code style conventions - mandatory formatting and language idioms for source code (TypeScript-first, applies to every language) - American-English naming, double quotes, string interpolation, length-sorted imports, 4-space indentation, comment/JSDoc format, compact single-line blocks, and code-level anti-patterns (barrel files, external CDN/hosting dependencies). Use whenever writing, refactoring, or reviewing source code.
|
|
3
|
+
description: Ciphera code style conventions - mandatory formatting and language idioms for source code (TypeScript-first, applies to every language) - American-English naming, double quotes, string interpolation, length-sorted imports, one statement per module and a namespace import (`import * as ns`) instead of a long named list from a project module, 4-space indentation, comment/JSDoc format, compact single-line blocks, and code-level anti-patterns (barrel files, external CDN/hosting dependencies). Use whenever writing, refactoring, or reviewing source code.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Ciphera Code Style Policy
|
|
@@ -60,6 +60,22 @@ const path = `/path/to/${folderName}/file`;
|
|
|
60
60
|
## Imports
|
|
61
61
|
|
|
62
62
|
- Sort imports by line length, shortest first.
|
|
63
|
+
- One statement per module: never import the same module twice in a file (a value import plus a `type` import of the same module is still one statement, with `type` on the members).
|
|
64
|
+
- When you need many symbols from one of the project's own modules, import the module as a NAMESPACE instead of listing them. A named list is fine for a handful; past that the line stops being readable, and every new export widens it again. The namespace also makes each call site say where the symbol comes from.
|
|
65
|
+
- Name the namespace after the module. When that name is already a local variable in the file, pick a distinct one (`conf` for a config module whose values are held in `config` variables, `gateDb` for a `db` module) rather than shadowing it.
|
|
66
|
+
- This applies to modules the project owns (relative paths, path aliases). Standard-library and package imports stay named: their surface is fixed and the ecosystem reads them that way.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Bad: 13 symbols on one line, and it grows with every new export.
|
|
70
|
+
import { readConfig, readGlobalConfig, CONFIG_DEFAULTS, setEnigmaToggle, setEnigmaValue, OUTPUT_STYLES, DASHBOARD_MODES } from "./config";
|
|
71
|
+
|
|
72
|
+
// Good
|
|
73
|
+
import * as conf from "./config";
|
|
74
|
+
|
|
75
|
+
const { config } = conf.readConfig();
|
|
76
|
+
if (!conf.OUTPUT_STYLES.includes(style)) return;
|
|
77
|
+
```
|
|
78
|
+
|
|
63
79
|
- Do not import from external hostings or CDNs; depend on a package name, not a remote URL.
|
|
64
80
|
- For obscure libraries, vendor the needed code into the project utilities instead of adding a fragile dependency.
|
|
65
81
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ciphera-style-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
|
-
"description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
|
|
6
|
-
"updated": "2026-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
5
|
+
"description": "Ciphera code style conventions (formatting, naming, imports incl. namespace imports for wide module surfaces, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
|
|
6
|
+
"updated": "2026-07-30T15:48:04+02:00",
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
|
+
"sha": "05dc812da459071110d96ee41c5201b1ee230471d635352415e10ef8b220c267"
|
|
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-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
|
|
9
9
|
}
|
|
@@ -33,6 +33,7 @@ description: Highest-authority engineering rules - priority hierarchy, modular a
|
|
|
33
33
|
- Any dependency added/upgraded/removed/audited, or package manifest/lockfile change -> dependency-policy.
|
|
34
34
|
- Any UI, client state, data fetching, or client caching -> frontend-policy.
|
|
35
35
|
- Any API endpoint, service, controller, or server request flow -> backend-policy.
|
|
36
|
+
- Any email sent or templated from the server (verification, password reset, receipts, alerts, digests) or any email-provider integration -> email-policy.
|
|
36
37
|
- Any new or changed code that needs verification, or any test file created/moved/renamed or a test suite scaffolded/restructured -> testing-policy.
|
|
37
38
|
- Any source code written, refactored, or reviewed (formatting, naming, language idioms) -> ciphera-style-policy.
|
|
38
39
|
- Any implementation code being written or refactored, or any "be lazy"/"simplify"/over-engineering request -> anti-overengineering-policy.
|
|
@@ -83,6 +84,7 @@ This core policy owns orchestration, architecture, and the global rules. Each co
|
|
|
83
84
|
- validation-policy: strict frontend + backend schema validation (Zod), schema consistency, client-facing error handling.
|
|
84
85
|
- frontend-policy: frontend structure, reusable components, abstraction threshold, client-side caching, optimistic UI and rollback.
|
|
85
86
|
- backend-policy: API/service architecture, controller-service-repository layering, API/request optimization, server-side caching (Redis).
|
|
87
|
+
- email-policy: transactional and notification email - React Email templating instead of hand-written HTML, server-side rendering, a single send module behind the provider SDK, plain-text alternatives, idempotent background sending, link safety, and deliverability (SPF/DKIM/DMARC, bounce suppression, unsubscribe).
|
|
86
88
|
- testing-policy: test strategy, coverage gates, deterministic tests, test/regression-first discipline, and test-suite layout (structure by type and domain, mirrored source paths, file naming, fixture/helper/factory placement).
|
|
87
89
|
- code-review-policy: self-review before delivery, review dimensions, change-quality gates.
|
|
88
90
|
- debugging-policy: reproduce-isolate-fix methodology and root-cause discipline.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "core-engineering-policy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
|
|
6
|
-
"updated": "2026-07-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
6
|
+
"updated": "2026-07-30T19:29:19+02:00",
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
|
+
"sha": "6a881d8589926fa7f48058314fd26d7042fd2ac82f1c87a6c11ffb54d1fda22b"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
|
|
6
6
|
"updated": "2026-06-03T14:19:50+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "2883bcecb3202683ae6f81b073c3d6a9cec9c55029e011bdd06ba7ac3537297e"
|
|
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-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
|
|
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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
|
|
9
9
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email-policy
|
|
3
|
+
description: Transactional and notification email - build templates with React Email (react.email) instead of hand-written HTML tables or string concatenation, render them server-side, send through the provider SDK behind a single send module, and apply deliverability (SPF/DKIM/DMARC, bounce and complaint suppression, unsubscribe headers), plain-text alternatives, idempotent background sending, and link safety. Use when sending, templating, styling, previewing, or testing any email from a backend, API route, cron job, worker, or CLI - signup and verification, password reset, receipts, alerts, digests, or any provider integration (Resend, Postmark, SendGrid, AWS SES, Mailgun, Nodemailer/SMTP).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Email Policy (Transactional & Notification Mail)
|
|
7
|
+
|
|
8
|
+
## Activation Scope
|
|
9
|
+
|
|
10
|
+
- Apply whenever code sends an email or builds an email body: verification, password reset, invites, receipts, alerts, digests, notifications, or any email-provider integration.
|
|
11
|
+
- Owns templating, rendering, sending, and deliverability. Layering, retries, and queues stay with backend-policy; secrets, tokens, and link safety with security-policy; recipient and payload schemas with validation-policy; package choice with dependency-policy.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Never Hand-Write Email HTML
|
|
16
|
+
|
|
17
|
+
Email clients are roughly two decades behind browsers: the Outlook desktop engine renders with Word, Gmail rewrites and strips `<style>`, external CSS never loads, and flexbox/grid are unavailable. HTML assembled by hand or from template strings looks correct in a browser preview and breaks in the actual inbox - and string-concatenated user values in markup are an injection surface.
|
|
18
|
+
|
|
19
|
+
- Do NOT: build bodies with template literals or Handlebars-into-tables, copy a random table skeleton off a blog, or reuse an app React component as an email.
|
|
20
|
+
- Do NOT judge an email by a browser screenshot - a browser is the one client that renders everything.
|
|
21
|
+
- DO: use a compiler that targets email HTML. In JavaScript/TypeScript that is React Email.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## React Email (default for JS/TS)
|
|
26
|
+
|
|
27
|
+
https://react.email - MIT, maintained by the Resend team. React components that compile to email-safe HTML, tested across Gmail, Apple Mail, Outlook, Yahoo! Mail, HEY, and Superhuman.
|
|
28
|
+
|
|
29
|
+
### Packages
|
|
30
|
+
|
|
31
|
+
- `@react-email/components` - the component set (peer: React 18 or 19): `Html`, `Head`, `Body`, `Container`, `Section`, `Row`, `Column`, `Heading`, `Text`, `Link`, `Button`, `Img`, `Hr`, `Preview`, `Font`, `Markdown`, `CodeBlock`, `CodeInline`, `Tailwind`.
|
|
32
|
+
- `@react-email/render` - the runtime renderer: `render()`, `pretty()`, `toPlainText()`. This is the only piece production needs, so it belongs in `dependencies`.
|
|
33
|
+
- `react-email` - the local preview server (`email dev`, bin `email`) that also re-exports `render`. It pulls in esbuild, socket.io, tailwindcss, prismjs, and chokidar: keep it a **devDependency**. Do not add it to production dependencies just to import `render` (dependency-policy, anti-overengineering-policy).
|
|
34
|
+
|
|
35
|
+
### Render, then send
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { render } from "@react-email/render";
|
|
39
|
+
import { WelcomeEmail } from "@/emails/welcome";
|
|
40
|
+
|
|
41
|
+
const html = await render(<WelcomeEmail name={user.name} verifyUrl={url} />);
|
|
42
|
+
const text = await render(<WelcomeEmail name={user.name} verifyUrl={url} />, { plainText: true });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `render()` is asynchronous - await it. Options: `plainText`, `pretty`, `htmlToTextOptions`. `toPlainText(html)` converts an already-rendered body, and `data-skip-in-text="true"` drops an element from the text version only.
|
|
46
|
+
- Templates live in one folder (`emails/`), one file per template, props typed. They are ordinary React components, so the reuse rules in frontend-policy apply: one shared layout/branding wrapper composed by every template, never a copied header per email.
|
|
47
|
+
- Style with the `<Tailwind>` wrapper when the project already uses Tailwind, otherwise inline `style` props. Never link an external stylesheet or a CDN webfont - clients strip or block them (use `<Font>` with a fallback stack).
|
|
48
|
+
- Always include `<Preview>`; without it the inbox preview line shows whatever stray text comes first.
|
|
49
|
+
- Host images on an absolute HTTPS URL and always set `alt` - most clients block images by default, so an email whose meaning lives only in an image arrives empty.
|
|
50
|
+
- Preview with `email dev` (or a rendered fixture) before shipping.
|
|
51
|
+
|
|
52
|
+
### Boundaries
|
|
53
|
+
|
|
54
|
+
- Non-React backends: use MJML or the provider's own template system, under the same doctrine - never hand-rolled tables.
|
|
55
|
+
- Marketing and campaign email belongs in the marketing platform, not in application code. This policy covers transactional and notification mail.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Sending
|
|
60
|
+
|
|
61
|
+
- One module owns sending (e.g. `lib/email/send.ts`): it takes `{to, subject, template, props}`, renders HTML plus text, calls the provider, and returns a typed result. Feature code never imports the provider SDK directly - that keeps the provider swappable and concentrates logging, suppression, and idempotency in one place (backend-policy).
|
|
62
|
+
- Prefer the provider's official SDK or HTTP API over raw SMTP when one exists; React Email documents integrations for Resend, Postmark, SendGrid, AWS SES, Mailgun, MailerSend, Plunk, Scaleway, Azure Communication Email, and Nodemailer.
|
|
63
|
+
- API keys come from the environment or a secret store, server-side only - never in a client bundle, never committed (security-policy).
|
|
64
|
+
- Always send HTML **and** a plain-text alternative. Text-only clients, accessibility tooling, and spam filters all read it.
|
|
65
|
+
- Validate the recipient address and the template payload against a schema before rendering (validation-policy).
|
|
66
|
+
- Never block the user's response on the provider. Enqueue the send and return; a slow or failing mail provider must not fail the request. Where the project has no queue, send after the response with a timeout and log the failure instead of propagating it - unless the email IS the operation the user asked for.
|
|
67
|
+
- Make sends idempotent: derive a key from the event (user id + template + event id) so a retried job or a double-submitted request does not send twice, and pass the provider's idempotency key when it offers one. Retries are bounded with backoff (backend-policy).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Content & Link Safety
|
|
72
|
+
|
|
73
|
+
- Never put a password, a secret, an API key, a full session token, or sensitive personal data in an email body. Mail sits in plaintext in most mailboxes and is forwarded freely. Send a short-lived, single-use link instead, and expire it on use.
|
|
74
|
+
- Build action links from a server-side base URL constant, never from the request `Host` header - header poisoning turns a password-reset email into account takeover.
|
|
75
|
+
- Validate any redirect or callback target against an allowlist before embedding it.
|
|
76
|
+
- React escapes text by default; the remaining risks are attributes and raw HTML. Validate every `href`/`src` against an `http`/`https` scheme allowlist, and never feed unsanitized user input to `<Markdown>` or a dangerouslySetInnerHTML equivalent (security-policy).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Deliverability
|
|
81
|
+
|
|
82
|
+
- Send from a verified domain with SPF, DKIM, and DMARC configured. An unauthenticated domain lands in spam no matter how good the content is.
|
|
83
|
+
- Use a real, monitored `Reply-To`. Avoid `noreply@` for mail a human may reasonably answer.
|
|
84
|
+
- Consume the provider's bounce and complaint webhooks: suppress hard bounces and spam complaints permanently in your own store, and check that suppression list before every send. Ignoring it burns domain reputation for every other email you send.
|
|
85
|
+
- Any bulk or recurring email (digest, newsletter, activity summary) needs a working unsubscribe: `List-Unsubscribe` and `List-Unsubscribe-Post` headers plus a visible link, honored immediately and without a login. Genuine transactional mail (receipt, password reset, security alert) is exempt - do not use that exemption to smuggle marketing.
|
|
86
|
+
- Respect the provider's rate limits and daily quota; queue and pace bulk sends rather than bursting into a 429.
|
|
87
|
+
- Do not enable open/click tracking by default. It is a privacy decision with GDPR/RGPD consequences, not a sensible default.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Testing & Observability
|
|
92
|
+
|
|
93
|
+
- Test templates by rendering them, not by sending: assert on the rendered HTML and text (subject line, required links present, no unresolved placeholder, correct locale). Deterministic and offline (testing-policy).
|
|
94
|
+
- Never send real mail from tests or local development. Use the provider's sandbox or test key, or a local catcher (Mailpit, Mailhog, Ethereal).
|
|
95
|
+
- Log every attempt with template id, provider message id, and outcome - never the body, the token, or the link. Alert on bounce and complaint rates, not just on send errors.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "email-policy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"provider": "FJRG2007/enigma",
|
|
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
|
+
"updated": "2026-07-30T19:29:19+02:00",
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
|
+
"sha": "c9724fdbcdbeab99573be3fd44d4cdd97c2a394d99f3c4395f118f17356b00ed"
|
|
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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
|
|
9
9
|
}
|
|
@@ -123,6 +123,30 @@ Build every UI to adapt to the viewport; never assume a desktop width. A layout
|
|
|
123
123
|
|
|
124
124
|
---
|
|
125
125
|
|
|
126
|
+
## Persistent Chrome Stays Put (Sidebars, Nav, Panels)
|
|
127
|
+
|
|
128
|
+
Navigation and any other persistent chrome must stay where the user left it while the CONTENT scrolls. A sidebar that scrolls up and off the top with the page is one of the most common generated-layout defects, and it makes a long page unnavigable: the user has to scroll back up to reach any other link.
|
|
129
|
+
|
|
130
|
+
- Put the scroll on the content, not on the page. The reliable shape is a viewport-height shell whose sidebar and main are siblings, each owning its own overflow: `position: sticky` with a `top` offset (simplest - the sidebar stays in the grid and needs no width duplication), or a `position: fixed` sidebar with matching padding on the content. Tailwind's `sticky top-0` / `h-screen sticky` do the same thing.
|
|
131
|
+
- **Pinning alone is not enough, and this is the part that gets missed.** `position: sticky` only pins the element's TOP edge: as soon as the sidebar's own content is taller than the viewport, the rest keeps scrolling away with the page and the last entries can never be reached. A pinned sidebar therefore needs BOTH a height bounded by the viewport (`max-height: calc(100vh - <header height>)`, or `h-screen`) AND its own `overflow-y: auto`. A sidebar with a content-sized height (`height: max-content`, `fit-content`, or none at all) plus `overflow: visible` is the broken pattern, even though it looks correct until the list grows.
|
|
132
|
+
- Add `overscroll-behavior: contain` to the sidebar's scroll container so reaching its end does not start scrolling the page behind it.
|
|
133
|
+
- Undo it where the layout stacks. At the mobile breakpoint the sidebar usually returns to normal flow (`position: static`), and the viewport height cap has to be lifted with it (`max-height: none; overflow-y: visible`) or it turns into a small scrolling box inside the page.
|
|
134
|
+
- The same rule covers a sticky header, toolbar, filter rail or side panel: pinned, bounded, and independently scrollable. Keep pinned chrome shallow - it eats vertical space on small screens, so collapse it into a drawer or a top bar there rather than pinning it over half the viewport.
|
|
135
|
+
- Verify by scrolling to the BOTTOM of a long page and confirming the sidebar is still on screen with its last entry reachable. Mechanically: the sidebar's `getBoundingClientRect().top` stays at its offset as the page scrolls, and `scrollHeight <= clientHeight` holds for it, or it can scroll itself.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Links In Copy Are Links
|
|
140
|
+
|
|
141
|
+
When UI copy names a destination - a URL, a doc page, a dashboard, a settings screen, an external service - make it reachable from where it is written. Printing a bare URL as plain text in a hint, description, empty state or error message leaves the user to select and copy it by hand, which is exactly the work the interface exists to remove.
|
|
142
|
+
|
|
143
|
+
- A URL inside a sentence becomes an inline anchor on the words that name it, not a raw address dropped mid-paragraph: "Leave empty to use the [deployment default](https://example.com)" reads better than repeating the URL. Show the bare address only when the exact value is the information (a host to whitelist, an endpoint to paste elsewhere) - and then pair it with a copy button rather than expecting a manual selection.
|
|
144
|
+
- External links carry `target="_blank"` with `rel="noopener noreferrer"`, and say where they go when the destination is not obvious from the text. In-app destinations use the router, never a full page reload.
|
|
145
|
+
- Choose the affordance by weight, not by habit: an inline anchor for a reference inside a sentence, a button for the primary action of a panel or empty state ("Open dashboard"), an icon button where it repeats per row (Icon Actions above). A whole sentence is never a link - link the noun.
|
|
146
|
+
- The same applies to anything else with a natural affordance: a file path gets a copy button, an email gets `mailto:`, a referenced setting gets a link to that settings screen. If the copy tells the user to go somewhere, take them there.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
126
150
|
## Text That Does Not Fit (Variable-Length Content)
|
|
127
151
|
|
|
128
152
|
Every string is variable-length; the value on screen during development is one sample. Text escaping its card or colliding with a neighbour is the most common layout defect, it is invisible until the content changes, and it is the responsibility of whoever writes the layout - not something to be pointed out afterwards.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"version": "1.20.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/pagination, virtualization, skeletons, progressive loading), 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), variable-length text (min-width:0 in flex/grid, wrap vs truncate, long unbroken strings, worst-case content checks), and AI chat/agent interfaces via Vercel's AI Elements registry instead of hand-rolled message threads.",
|
|
6
|
-
"updated": "2026-07-
|
|
7
|
-
"cliVersion": "1.32.
|
|
8
|
-
"sha": "
|
|
6
|
+
"updated": "2026-07-30T19:29:05+02:00",
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
|
+
"sha": "a9750bd74505373aea8d9265515fc3b7f0fddf3e822d9e046248223e24ff96af"
|
|
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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
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), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
|
|
6
6
|
"updated": "2026-06-01T00:45:28+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
|
|
9
9
|
}
|
|
@@ -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-07-29T01:18:36+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "13d24c217bdb6fe83fe16835d8f5c3d397a9f3338f16876c61ef96e97f34c90a"
|
|
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-07-27T17:22:17+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "b9046c15fd636057a8e42e199d2eeb47297477f38a0a26a07a7b0bf71d61fcb4"
|
|
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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
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.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "3bdf591057b760f674fb2b1425f63acb426cda2c4f042e1a74c5a5d3807df664"
|
|
9
9
|
}
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
"provider": "FJRG2007/enigma",
|
|
5
5
|
"description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
|
|
6
6
|
"updated": "2026-07-22T01:41:06+02:00",
|
|
7
|
-
"cliVersion": "1.32.
|
|
7
|
+
"cliVersion": "1.32.2",
|
|
8
8
|
"sha": "d937df0052d1ec151728a28f6567744d9e7ca0a65d84b4d5c9a697f1e40d704f"
|
|
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": "8221580f5a8bb0a1b9400c88d52765573cde4185501310b8b6bbd6f257cb604b",
|
|
3
|
+
"enigma-linux-arm64": "492da75deb97e22217255d6477097450c23392cabd90e21c6ad5edf152b650d1",
|
|
4
|
+
"enigma-linux-x64": "2f60fc015b2933523b211a8df4cacef015be6c2141535f4fbc3ca99bdc341c4d",
|
|
5
|
+
"enigma-win32-x64.exe": "556cf44c2ce5fc7a55872078069f24a5fc30adea90259637f5e9e7baed43794a"
|
|
6
6
|
}
|
package/bin/enigma.mjs
CHANGED
|
@@ -66,6 +66,7 @@ const env = { ...process.env };
|
|
|
66
66
|
env.ENIGMA_ASSETS_DIR = join(pkgRoot, "assets");
|
|
67
67
|
env.ENIGMA_GUARD_PATH = join(pkgRoot, "dist", "guard.js");
|
|
68
68
|
env.ENIGMA_GUARDRAILS_PATH = join(pkgRoot, "dist", "guardrails.js");
|
|
69
|
+
env.ENIGMA_TRIM_PATH = join(pkgRoot, "dist", "trim.js");
|
|
69
70
|
try {
|
|
70
71
|
env.ENIGMA_VERSION = packageVersion();
|
|
71
72
|
} catch { /* version is best-effort */ }
|
package/dist/guard.js
CHANGED
|
Binary file
|
package/dist/guardrails.js
CHANGED
|
@@ -199,6 +199,20 @@ var BUILTIN_RULES = [
|
|
|
199
199
|
severity: "warn",
|
|
200
200
|
skill: "frontend-policy"
|
|
201
201
|
},
|
|
202
|
+
// NOTE: there is deliberately no "a pinned sidebar needs its own scroll" rule, and no
|
|
203
|
+
// "a URL in UI copy must be a link" rule, though both conventions were asked for. Neither
|
|
204
|
+
// has a file-local signature this engine can read.
|
|
205
|
+
// - The sidebar defect is a RELATION between three declarations (pinned + height bound +
|
|
206
|
+
// overflow) that real stylesheets spread over several lines of one block, which a
|
|
207
|
+
// line-based scan cannot correlate; the Tailwind one-line form would be the exception,
|
|
208
|
+
// and the corpus (this repo, apps/web, references/repos) holds exactly two real
|
|
209
|
+
// sidebars, neither of them Tailwind - zero measured true positives, which is the same
|
|
210
|
+
// evidence that got the no-op-save rule rejected. It is also not a defect until the
|
|
211
|
+
// sidebar's content outgrows the viewport, so a rule would flag correct short ones.
|
|
212
|
+
// - The link one cannot tell COPY from DATA: a URL in a string is far more often an API
|
|
213
|
+
// endpoint, a default, or a docs reference in a comment than a piece of UI text, and
|
|
214
|
+
// nothing in the string says which. Both live in frontend-policy instead (Persistent
|
|
215
|
+
// Chrome Stays Put, Links In Copy Are Links).
|
|
202
216
|
// NOTE: there is deliberately no "truncating flex item needs min-w-0" rule. It was written
|
|
203
217
|
// and then removed after measuring it in a browser: per CSS Flexbox 4.5 a flex item's
|
|
204
218
|
// automatic minimum size only applies while its computed overflow is visible, and Tailwind's
|
|
@@ -383,6 +397,72 @@ var BUILTIN_RULES = [
|
|
|
383
397
|
maxBytes: 4e4,
|
|
384
398
|
message: "This memory file loads into every session in the project, so its cost is paid on every task regardless of relevance. Keep it an INDEX: move each subsystem's detail into its own doc (docs/notes/<topic>.md) and leave one line here saying what the note covers and when to read it. Route new conventions by tier - a file-local syntactic signature becomes a guardrail rule, a domain-scoped rule belongs in the owning skill (loaded on demand), and only a truly universal rule stays in memory. Turn this off with `enigma guardrails disable ctx-memory-budget`.",
|
|
385
399
|
severity: "block"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "ts-import-namespace",
|
|
403
|
+
label: "Namespace import for a wide module surface",
|
|
404
|
+
files: ["*.ts", "*.tsx", "*.mts", "*.cts", "*.js", "*.mjs", "*.cjs", "*.jsx"],
|
|
405
|
+
// Two-form generated/vendored excludes (`**/x/**` misses a root-level dist/). Declaration
|
|
406
|
+
// files are excluded too: a .d.ts re-declares another module's surface, it has no call sites.
|
|
407
|
+
excludeFiles: [
|
|
408
|
+
"*.min.js",
|
|
409
|
+
"*.d.ts",
|
|
410
|
+
"**/dist/**",
|
|
411
|
+
"**/build/**",
|
|
412
|
+
"**/_build/**",
|
|
413
|
+
"**/node_modules/**",
|
|
414
|
+
"**/vendor/**",
|
|
415
|
+
"dist/**",
|
|
416
|
+
"build/**",
|
|
417
|
+
"_build/**",
|
|
418
|
+
"node_modules/**",
|
|
419
|
+
"vendor/**"
|
|
420
|
+
],
|
|
421
|
+
scope: "file",
|
|
422
|
+
// A count has no regex form, hence maxNamedImports. 9 is the budget: past that the import
|
|
423
|
+
// line stops being readable, and every new export of the module widens it again. Only the
|
|
424
|
+
// project's OWN modules are counted - a bare specifier (node builtin, npm package) is a
|
|
425
|
+
// fixed surface the ecosystem writes as named imports, so counting those would flag
|
|
426
|
+
// idiomatic code. BLOCK for the ui-no-em-dash reason: a warn exits 0 and never reaches
|
|
427
|
+
// the model, and the fix is mechanical.
|
|
428
|
+
maxNamedImports: 9,
|
|
429
|
+
message: 'Too many named bindings from one module. Import it as a namespace instead - `import * as <ns> from "<module>"`, then call `<ns>.thing()` - so the import stays one short line, each call site says where the symbol comes from, and a new export never widens the import again. The count sums every named import of that module in this file, so splitting the statement in two does not help; name the namespace for the module, and pick a distinct name when the natural one is already a local variable. Keep named imports for a handful of symbols. Mark a deliberate exception with an `enigma:` note on the import line (ciphera-style-policy).',
|
|
430
|
+
severity: "block",
|
|
431
|
+
skill: "ciphera-style-policy"
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
id: "proc-windows-hide",
|
|
435
|
+
label: "Spawned process must not pop a console window",
|
|
436
|
+
files: ["*.ts", "*.tsx", "*.mts", "*.cts", "*.js", "*.mjs", "*.cjs", "*.jsx"],
|
|
437
|
+
// Tests run in a terminal that already has a console, so a flashing window is not a
|
|
438
|
+
// defect there. Same two-form generated/vendored excludes as the rules above.
|
|
439
|
+
excludeFiles: [
|
|
440
|
+
"*.test.*",
|
|
441
|
+
"*.spec.*",
|
|
442
|
+
"**/tests/**",
|
|
443
|
+
"**/__tests__/**",
|
|
444
|
+
"**/fixtures/**",
|
|
445
|
+
"*.min.js",
|
|
446
|
+
"*.d.ts",
|
|
447
|
+
"**/dist/**",
|
|
448
|
+
"**/build/**",
|
|
449
|
+
"**/_build/**",
|
|
450
|
+
"**/node_modules/**",
|
|
451
|
+
"**/vendor/**",
|
|
452
|
+
"dist/**",
|
|
453
|
+
"build/**",
|
|
454
|
+
"_build/**",
|
|
455
|
+
"node_modules/**",
|
|
456
|
+
"vendor/**"
|
|
457
|
+
],
|
|
458
|
+
scope: "file",
|
|
459
|
+
// A call spanning several lines has no line-regex form, hence a coded check (see
|
|
460
|
+
// missingWindowsHide for the three shapes it deliberately leaves alone). BLOCK for the
|
|
461
|
+
// ui-no-em-dash reason: a warn exits 0 and never reaches the model, the symptom is
|
|
462
|
+
// invisible to whoever writes the code on macOS or Linux, and the fix is one key.
|
|
463
|
+
fileCheck: "proc-windows-hide",
|
|
464
|
+
message: "Process spawned without windowsHide. On Windows a console child started by a process that has no console of its own - a daemon, an editor hook, a detached background task - pops a real console window on screen and closes it again, which reads as something crashing. Add `windowsHide: true` to the options object; it is inert on macOS and Linux, and inert on Windows when the parent already has a console, so it is safe on every call that is not deliberately opening a terminal for the user. For one that IS (a login flow that must show a terminal), mark the call with an `enigma:` note.",
|
|
465
|
+
severity: "block"
|
|
386
466
|
}
|
|
387
467
|
];
|
|
388
468
|
var PROJECT_CHECKS = {
|
|
@@ -396,6 +476,55 @@ var PROJECT_CHECKS = {
|
|
|
396
476
|
return !("prisma" in pkg || "@prisma/client" in pkg);
|
|
397
477
|
}
|
|
398
478
|
};
|
|
479
|
+
var FILE_CHECKS = {
|
|
480
|
+
"proc-windows-hide": (content) => missingWindowsHide(content)
|
|
481
|
+
};
|
|
482
|
+
var SPAWNERS = /* @__PURE__ */ new Set(["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"]);
|
|
483
|
+
function spawnerBindings(content) {
|
|
484
|
+
const names = /* @__PURE__ */ new Set();
|
|
485
|
+
const stmt = /(?:import|(?:const|let|var))\s*\{([^}]*)\}\s*(?:from\s*|=\s*require\(\s*)["'](?:node:)?child_process["']/g;
|
|
486
|
+
for (const m of content.matchAll(stmt)) {
|
|
487
|
+
for (const part of m[1].split(",")) {
|
|
488
|
+
const [orig, alias] = part.trim().split(/\s+as\s+/).map((s) => s.trim());
|
|
489
|
+
if (orig && SPAWNERS.has(orig)) names.add(alias || orig);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return [...names];
|
|
493
|
+
}
|
|
494
|
+
function missingWindowsHide(content) {
|
|
495
|
+
const names = spawnerBindings(content);
|
|
496
|
+
if (names.length === 0) return [];
|
|
497
|
+
const out = [];
|
|
498
|
+
const call = new RegExp(`(?<![.\\w$])(${names.join("|")})\\s*\\(`, "g");
|
|
499
|
+
for (const m of content.matchAll(call)) {
|
|
500
|
+
let i = m.index + m[0].length;
|
|
501
|
+
for (let depth = 1; i < content.length && depth > 0; i++) {
|
|
502
|
+
if (content[i] === "(") depth++;
|
|
503
|
+
else if (content[i] === ")") depth--;
|
|
504
|
+
}
|
|
505
|
+
const eol = content.indexOf("\n", i);
|
|
506
|
+
const text = content.slice(m.index, eol === -1 ? content.length : eol);
|
|
507
|
+
if (/windowsHide|enigma:|"inherit"|'inherit'/.test(text)) continue;
|
|
508
|
+
if (!text.includes("{")) continue;
|
|
509
|
+
if (/\{[^{}]*\.\.\.[A-Za-z_$]/.test(text)) continue;
|
|
510
|
+
out.push({ line: content.slice(0, m.index).split("\n").length, detail: m[1] });
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
var NAMED_IMPORT = /^import[ \t]+(?:[\w$]+[ \t]*,[ \t]*)?(?:type[ \t]+)?\{([^}]*)\}[ \t]*from[ \t]*["']([^"']+)["'].*$/gm;
|
|
515
|
+
var INTERNAL_MODULE = /^\.|^#|^[@~]\//;
|
|
516
|
+
function wideNamedImports(content, max) {
|
|
517
|
+
const per = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const m of content.matchAll(NAMED_IMPORT)) {
|
|
519
|
+
const mod = m[2];
|
|
520
|
+
if (!INTERNAL_MODULE.test(mod)) continue;
|
|
521
|
+
const entry = per.get(mod) ?? { count: 0, line: content.slice(0, m.index).split("\n").length, allowed: false };
|
|
522
|
+
entry.count += m[1].split(",").filter((s) => s.trim()).length;
|
|
523
|
+
if (m[0].includes("enigma:")) entry.allowed = true;
|
|
524
|
+
per.set(mod, entry);
|
|
525
|
+
}
|
|
526
|
+
return [...per].filter(([, v]) => !v.allowed && v.count > max).map(([module, v]) => ({ line: v.line, module, count: v.count }));
|
|
527
|
+
}
|
|
399
528
|
function readPkgDeps(root) {
|
|
400
529
|
try {
|
|
401
530
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
@@ -458,6 +587,15 @@ function checkFile(file, content, projectRoot) {
|
|
|
458
587
|
if (rule.scope === "file" && rule.maxBytes) {
|
|
459
588
|
const bytes = Buffer.byteLength(content, "utf8");
|
|
460
589
|
if (bytes > rule.maxBytes) out.push({ ...base, message: `${rule.message} (${bytes} bytes, budget ${rule.maxBytes})` });
|
|
590
|
+
} else if (rule.scope === "file" && rule.maxNamedImports) {
|
|
591
|
+
for (const w of wideNamedImports(content, rule.maxNamedImports)) {
|
|
592
|
+
out.push({ ...base, line: w.line, message: `${rule.message} (${w.count} bindings from "${w.module}", budget ${rule.maxNamedImports})` });
|
|
593
|
+
}
|
|
594
|
+
} else if (rule.scope === "file" && rule.fileCheck) {
|
|
595
|
+
const check = FILE_CHECKS[rule.fileCheck];
|
|
596
|
+
for (const hit of check ? check(content) : []) {
|
|
597
|
+
out.push({ ...base, line: hit.line, message: `${rule.message} (${hit.detail})` });
|
|
598
|
+
}
|
|
461
599
|
} else if (rule.scope === "file" && rule.pattern) {
|
|
462
600
|
if (rule.absent) {
|
|
463
601
|
try {
|
|
@@ -525,7 +663,7 @@ Fix the above before continuing.
|
|
|
525
663
|
return 0;
|
|
526
664
|
}
|
|
527
665
|
function gitFiles(all) {
|
|
528
|
-
const out = execFileSync("git", all ? ["ls-files"] : ["diff", "--cached", "--name-only", "--diff-filter=ACM"], { encoding: "utf8" });
|
|
666
|
+
const out = execFileSync("git", all ? ["ls-files"] : ["diff", "--cached", "--name-only", "--diff-filter=ACM"], { encoding: "utf8", windowsHide: true });
|
|
529
667
|
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
530
668
|
}
|
|
531
669
|
function runGuardrailsScan(all) {
|
|
@@ -577,13 +715,16 @@ if (isGrEntry && fileURLToPath(import.meta.url) === grEntry) {
|
|
|
577
715
|
}
|
|
578
716
|
export {
|
|
579
717
|
BUILTIN_RULES,
|
|
718
|
+
FILE_CHECKS,
|
|
580
719
|
PROJECT_CHECKS,
|
|
581
720
|
checkFile,
|
|
582
721
|
checkPath,
|
|
583
722
|
findProjectRoot,
|
|
584
723
|
formatFindings,
|
|
585
724
|
loadRules,
|
|
725
|
+
missingWindowsHide,
|
|
586
726
|
runGuardrailsHook,
|
|
587
727
|
runGuardrailsScan,
|
|
588
|
-
runGuardrailsScanCli
|
|
728
|
+
runGuardrailsScanCli,
|
|
729
|
+
wideNamedImports
|
|
589
730
|
};
|
package/dist/trim.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/trim.ts
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { extname, join } from "path";
|
|
7
|
+
import { open, stat } from "fs/promises";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
9
|
+
var TAIL_BYTES = 4096;
|
|
10
|
+
var MAX_TAIL = 1024 * 1024;
|
|
11
|
+
var CONCURRENCY = 32;
|
|
12
|
+
var SKIP_EXT = /* @__PURE__ */ new Set([
|
|
13
|
+
".patch",
|
|
14
|
+
".diff",
|
|
15
|
+
".snap",
|
|
16
|
+
".png",
|
|
17
|
+
".jpg",
|
|
18
|
+
".jpeg",
|
|
19
|
+
".gif",
|
|
20
|
+
".webp",
|
|
21
|
+
".avif",
|
|
22
|
+
".ico",
|
|
23
|
+
".bmp",
|
|
24
|
+
".tiff",
|
|
25
|
+
".pdf",
|
|
26
|
+
".zip",
|
|
27
|
+
".gz",
|
|
28
|
+
".tgz",
|
|
29
|
+
".bz2",
|
|
30
|
+
".xz",
|
|
31
|
+
".7z",
|
|
32
|
+
".rar",
|
|
33
|
+
".jar",
|
|
34
|
+
".woff",
|
|
35
|
+
".woff2",
|
|
36
|
+
".ttf",
|
|
37
|
+
".otf",
|
|
38
|
+
".eot",
|
|
39
|
+
".mp3",
|
|
40
|
+
".mp4",
|
|
41
|
+
".wav",
|
|
42
|
+
".ogg",
|
|
43
|
+
".webm",
|
|
44
|
+
".mov",
|
|
45
|
+
".avi",
|
|
46
|
+
".exe",
|
|
47
|
+
".dll",
|
|
48
|
+
".so",
|
|
49
|
+
".dylib",
|
|
50
|
+
".bin",
|
|
51
|
+
".wasm",
|
|
52
|
+
".node",
|
|
53
|
+
".class",
|
|
54
|
+
".pyc",
|
|
55
|
+
".db",
|
|
56
|
+
".sqlite",
|
|
57
|
+
".sqlite3",
|
|
58
|
+
".lock"
|
|
59
|
+
]);
|
|
60
|
+
var SKIP_DIR = /(^|\/)(__snapshots__|fixtures|testdata|golden|node_modules|\.git|vendor|third_party|third-party|dist|build|coverage|\.next|\.venv)(\/|$)/;
|
|
61
|
+
function readIgnoreGlobs(root) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = JSON.parse(readFileSync(join(root, ".githooks", "enigma-trim.json"), "utf8"));
|
|
64
|
+
return Array.isArray(raw.ignore) ? raw.ignore.filter((g) => typeof g === "string") : [];
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function globToRegExp(glob) {
|
|
70
|
+
const esc = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
71
|
+
const body = esc.replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/ /g, ".*").replace(/\?/g, "[^/]");
|
|
72
|
+
return new RegExp(glob.includes("/") ? `^${body}$` : `(^|/)${body}$`);
|
|
73
|
+
}
|
|
74
|
+
var BLANK_LINE = /^[ \t\r]*$/;
|
|
75
|
+
function isSkipped(file) {
|
|
76
|
+
const norm = file.replace(/\\/g, "/");
|
|
77
|
+
return SKIP_DIR.test(norm) || SKIP_EXT.has(extname(norm).toLowerCase());
|
|
78
|
+
}
|
|
79
|
+
function trailingBlankBytes(tail, atStart) {
|
|
80
|
+
const lines = tail.split("\n");
|
|
81
|
+
if (lines.pop() !== "") return 0;
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
if (!BLANK_LINE.test(line)) return bytes;
|
|
86
|
+
bytes += line.length + 1;
|
|
87
|
+
}
|
|
88
|
+
return atStart ? 0 : -1;
|
|
89
|
+
}
|
|
90
|
+
async function trimFile(file) {
|
|
91
|
+
if (isSkipped(file)) return false;
|
|
92
|
+
let handle;
|
|
93
|
+
try {
|
|
94
|
+
const info = await stat(file);
|
|
95
|
+
if (!info.isFile() || info.size === 0) return false;
|
|
96
|
+
handle = await open(file, "r+");
|
|
97
|
+
let want = TAIL_BYTES;
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const length = Math.min(want, info.size);
|
|
100
|
+
const position = info.size - length;
|
|
101
|
+
const buf = Buffer.allocUnsafe(length);
|
|
102
|
+
const { bytesRead } = await handle.read(buf, 0, length, position);
|
|
103
|
+
const tail = buf.subarray(0, bytesRead);
|
|
104
|
+
if (tail.includes(0)) return false;
|
|
105
|
+
const cut = trailingBlankBytes(tail.toString("latin1"), position === 0);
|
|
106
|
+
if (cut === 0) return false;
|
|
107
|
+
if (cut > 0) {
|
|
108
|
+
await handle.truncate(info.size - cut);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (position === 0 || want >= MAX_TAIL) return false;
|
|
112
|
+
want *= 2;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
} finally {
|
|
117
|
+
await handle?.close().catch(() => {
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function trimAll(files, task = trimFile) {
|
|
122
|
+
const changed = [];
|
|
123
|
+
let next = 0;
|
|
124
|
+
const worker = async () => {
|
|
125
|
+
for (let i = next++; i < files.length; i = next++) {
|
|
126
|
+
if (await task(files[i])) changed.push(files[i]);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, files.length) }, worker));
|
|
130
|
+
return changed;
|
|
131
|
+
}
|
|
132
|
+
function gitLines(args) {
|
|
133
|
+
try {
|
|
134
|
+
const out = execFileSync("git", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, windowsHide: true });
|
|
135
|
+
return out.split("\0").map((s) => s.trim()).filter(Boolean);
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function repoRoot() {
|
|
141
|
+
try {
|
|
142
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true }).trim() || null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function stage(files) {
|
|
148
|
+
for (let i = 0; i < files.length; i += 500) {
|
|
149
|
+
try {
|
|
150
|
+
execFileSync("git", ["add", "--", ...files.slice(i, i + 500)], { stdio: "ignore", windowsHide: true });
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function runTrimScan(all) {
|
|
156
|
+
const root = repoRoot();
|
|
157
|
+
if (root === null) return { changed: [], unstaged: [], count: 0, notRepo: true };
|
|
158
|
+
const listed = all ? gitLines(["ls-files", "-z"]) : gitLines(["diff", "--cached", "--name-only", "--diff-filter=ACM", "-z"]);
|
|
159
|
+
const ignore = readIgnoreGlobs(root).map(globToRegExp);
|
|
160
|
+
const files = ignore.length ? listed.filter((f) => !ignore.some((re) => re.test(f))) : listed;
|
|
161
|
+
if (files.length === 0) return { changed: [], unstaged: [], count: 0 };
|
|
162
|
+
const dirty = all ? /* @__PURE__ */ new Set() : new Set(gitLines(["diff", "--name-only", "-z"]));
|
|
163
|
+
const changed = await trimAll(files);
|
|
164
|
+
const unstaged = changed.filter((f) => dirty.has(f));
|
|
165
|
+
const restage = changed.filter((f) => !dirty.has(f));
|
|
166
|
+
if (!all && restage.length) stage(restage);
|
|
167
|
+
return { changed, unstaged, count: files.length };
|
|
168
|
+
}
|
|
169
|
+
async function runTrimHook(payload) {
|
|
170
|
+
try {
|
|
171
|
+
const file = JSON.parse(payload || "{}")?.tool_input?.file_path;
|
|
172
|
+
if (typeof file === "string" && file) await trimFile(file);
|
|
173
|
+
} catch {
|
|
174
|
+
}
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
async function runTrimScanCli(all) {
|
|
178
|
+
const r = await runTrimScan(all);
|
|
179
|
+
if (r.notRepo) {
|
|
180
|
+
console.error("enigma-trim: not a git repository; nothing to do.");
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
if (r.changed.length === 0) {
|
|
184
|
+
console.log(`enigma-trim: ${r.count} ${all ? "tracked" : "staged"} file(s) checked, no trailing blank lines.`);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
const staged = r.changed.length - r.unstaged.length;
|
|
188
|
+
console.log(`enigma-trim: removed a trailing blank line from ${r.changed.length} file(s)${all ? "" : `, ${staged} re-staged`}.`);
|
|
189
|
+
for (const f of r.changed.slice(0, 20)) console.log(` - ${f}`);
|
|
190
|
+
if (r.changed.length > 20) console.log(` ... and ${r.changed.length - 20} more`);
|
|
191
|
+
if (r.unstaged.length) {
|
|
192
|
+
console.log(`
|
|
193
|
+
enigma-trim: ${r.unstaged.length} file(s) were fixed on disk but NOT re-staged, because they also`);
|
|
194
|
+
console.log("hold unstaged edits and `git add` would have pulled those into this commit:");
|
|
195
|
+
for (const f of r.unstaged) console.log(` ! ${f}`);
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
var trimEntry = process.argv[1] ?? "";
|
|
200
|
+
var isTrimEntry = /(^|[\\/])trim\.[mc]?[jt]s$/.test(trimEntry);
|
|
201
|
+
if (isTrimEntry && fileURLToPath(import.meta.url) === trimEntry) {
|
|
202
|
+
void runTrimScanCli(process.argv.includes("--all")).then((code) => process.exit(code));
|
|
203
|
+
}
|
|
204
|
+
export {
|
|
205
|
+
isSkipped,
|
|
206
|
+
readIgnoreGlobs,
|
|
207
|
+
runTrimHook,
|
|
208
|
+
runTrimScan,
|
|
209
|
+
runTrimScanCli,
|
|
210
|
+
trailingBlankBytes,
|
|
211
|
+
trimFile
|
|
212
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enigma-cli",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.2",
|
|
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": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"guardrails": "tsx src/guardrails.ts --all",
|
|
21
21
|
"verify": "npm run typecheck && npm run check && npm run guard && npm run guardrails",
|
|
22
22
|
"prepublishOnly": "npm run verify && npm run build",
|
|
23
|
-
"postinstall": "node bin/postinstall.mjs"
|
|
23
|
+
"postinstall": "node bin/postinstall.mjs",
|
|
24
|
+
"trim": "tsx src/trim.ts --all"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"bin",
|