enigma-cli 1.34.2 → 1.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/assets/commands/gate.md +65 -3
  2. package/assets/memory/AGENTS.md +3 -2
  3. package/assets/memory/CLAUDE.md +3 -2
  4. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  5. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  6. package/assets/skills/backend-policy/skill.json +1 -1
  7. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  8. package/assets/skills/code-review-policy/skill.json +1 -1
  9. package/assets/skills/core-engineering-policy/skill.json +1 -1
  10. package/assets/skills/database-expert/SKILL.md +11 -0
  11. package/assets/skills/database-expert/skill.json +3 -3
  12. package/assets/skills/debugging-policy/skill.json +1 -1
  13. package/assets/skills/dependency-policy/skill.json +1 -1
  14. package/assets/skills/email-policy/skill.json +1 -1
  15. package/assets/skills/frontend-design/skill.json +1 -1
  16. package/assets/skills/frontend-policy/SKILL.md +21 -1
  17. package/assets/skills/frontend-policy/skill.json +4 -4
  18. package/assets/skills/git-policy/SKILL.md +10 -0
  19. package/assets/skills/git-policy/skill.json +3 -3
  20. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  21. package/assets/skills/security-policy/skill.json +1 -1
  22. package/assets/skills/skill-creator/skill.json +1 -1
  23. package/assets/skills/task-completion-policy/skill.json +1 -1
  24. package/assets/skills/technical-writing-policy/skill.json +1 -1
  25. package/assets/skills/testing-policy/skill.json +1 -1
  26. package/assets/skills/validation-policy/skill.json +1 -1
  27. package/bin/checksums.json +4 -4
  28. package/dist/guardrails.js +336 -20
  29. package/package.json +1 -1
@@ -87,6 +87,53 @@ tradeoffs, constraints ruled in or out, and anything explicitly asked for that
87
87
  might look surprising in the diff. A few sentences is normal - the review step
88
88
  uses `--intent` to tell a deliberate choice from a mistake.
89
89
 
90
+ ## What a run costs, and what you may skip
91
+
92
+ Two scales, and they do not agree. Quote the one that matches the diff in front
93
+ of you.
94
+
95
+ **The floor**, measured over a 12-line diff with `commands.test` and
96
+ `commands.lint` configured and `--skip push,pr,ci`:
97
+
98
+ | Pass | Cost |
99
+ | --- | --- |
100
+ | review, initial | ~1 min |
101
+ | review, one fix round | 3.3-5.5 min |
102
+ | test | ~3.5 min |
103
+ | document | 1-1.8 min |
104
+ | lint, rebase, intent | under 2 s with commands configured |
105
+
106
+ **A real run**, measured over 72 runs of this repo: review 46.8% of gate time
107
+ (avg 15.6 min, p90 37.3 min, 1.57 rounds per review), ci 32.2%, test 14%,
108
+ document and lint 6.5% combined. Median run 22.8 min, p90 59.1 min. Review is the
109
+ single most expensive pass at that scale, not the cheapest, and ci is a third of
110
+ the clock while not being an agent pass at all - so do not quote a duration from
111
+ the floor table for a diff larger than the one it was measured on.
112
+
113
+ What follows from that:
114
+
115
+ - **One fix round, not three.** Every `--action fix` is two full passes, one to
116
+ apply and one to re-review. Select every finding you intend to fix in a single
117
+ `--findings` list instead of responding once per finding.
118
+ - **Propose `--skip test,document` on a throwaway diff** (a typo, a comment, a
119
+ version bump) and let the user decide. Skipping them means no test evidence was
120
+ gathered and no docs were checked, so it is their call - unless they already
121
+ asked for speed, in which case skip and say so. Either way, always report which
122
+ steps were skipped, so the user knows what was and was not validated.
123
+ - **Never skip `review`.** It is the only step that justifies the gate.
124
+ - **`test` costs a full pass even when `commands.test` is set.** The step runs its
125
+ evidence agent when the test command is empty *or* the run supplied intent, and
126
+ `--intent` is mandatory, so a configured command makes the step thorough, not
127
+ cheap. `--skip test` is the only way to not pay it.
128
+ - **Batch the work, not the runs.** The floor above is paid per run whatever the
129
+ diff size, so gating one coherent unit of work beats gating each small commit
130
+ that composes it.
131
+
132
+ Steps also take their model from `agent_step_args_override` in
133
+ `~/.enigma/gate/config.yaml`, keyed per step. If the user complains about run
134
+ duration, that is the lever to point at: the mechanical steps can run on a smaller
135
+ model while review keeps the strong one.
136
+
90
137
  ## Validate and decide
91
138
 
92
139
  1. Start the run. It blocks until the first decision point or the end:
@@ -107,6 +154,15 @@ uses `--intent` to tell a deliberate choice from a mistake.
107
154
  - `ask-user` - a judgment call only the user can make. STOP and escalate it
108
155
  (see below) before responding.
109
156
 
157
+ The gate object also carries `fix_policy`, the user's standing answer to "who
158
+ decides?". Follow it - the `help` line spells out what it means for the gate in
159
+ front of you, and it is the user's setting, not a suggestion:
160
+ - `assisted` (default) - anything mechanical was already settled before this
161
+ gate reached you, so what is left is the judgment call. Escalate it.
162
+ - `ask` - the user wants every finding put to them, `auto-fix` ones included.
163
+ Relay them and let the user choose instead of authorizing anything yourself.
164
+ - `auto` - the user asked you to settle everything. Respond without checking back.
165
+
110
166
  Choose one response:
111
167
  ```sh
112
168
  enigma gate axi respond --action approve
@@ -140,13 +196,19 @@ acknowledge those and list each fix.
140
196
 
141
197
  A finding marked `ask-user` is the user's decision, not yours. Relay it verbatim
142
198
  (its `id`, `file`, full `description`) - do not paraphrase or pre-judge - ask how
143
- to proceed, then translate their answer into the matching `respond` call. The one
144
- exception is `--yes` (below): standing consent to drive every gate unattended.
199
+ to proceed, then translate their answer into the matching `respond` call. The
200
+ exceptions are `fix_policy: auto` and `--yes` (below), both of which are standing
201
+ consent to drive every gate unattended.
145
202
 
146
203
  If you have clear consent to drive the whole run automatically, pass `--yes` to
147
204
  `axi run` or `axi respond`: it treats every actionable finding (auto-fix and
148
205
  ask-user alike) as consent to fix, accepts the resulting fix review, and approves
149
- no-op-only gates. Only use it when the user asked you to drive without checking back.
206
+ no-op-only gates. Only use it when the user asked you to drive without checking
207
+ back - it overrides `fix_policy` for that run.
208
+
209
+ The user sets `fix_policy` themselves, in the dashboard's Quality gate view or as
210
+ `fix_policy` in `~/.enigma/gate/config.yaml`. If they ask you to stop checking back
211
+ (or to start), point them there rather than passing `--yes` from then on.
150
212
 
151
213
  ## Inspecting state
152
214
 
@@ -28,6 +28,7 @@
28
28
  - No emojis in responses, code, or docs. Use ASCII punctuation: "-" not the long dash, "->" not the arrow. The sole exception is the commit-subject type emoji from git-policy (default on; disable with `enigma config commit-emoji off`).
29
29
  - Treat all external input as untrusted; never expose secrets or hardcode credentials.
30
30
  - When removing leaked or sensitive data the user asked to delete, the remediation commit/PR/branch must NOT name the leaked values or flag the security motive (that signposts where to look and re-leaks the values permanently) - use a neutral, mundane message and the `🔒 security` type is forbidden for it; offer history-rewrite vs. discreet-removal first. This carve-out is ONLY for that case; every other commit stays normal and descriptive (see git-policy).
31
+ - A value you cannot source is not a value you may write. An email, a version, an id, a URL, a price, a name, a field of an API - if it did not come from the user, the code, a command you ran, or a document you read, you do not have it: ask for it, or leave it out and say which. A well-formed guess is the dangerous kind, because it reads as fact and nobody re-checks it. Fixture and mock data is the exception, and it has to look like fixture data.
31
32
  - Reuse existing code before writing new code; do not duplicate logic.
32
33
  - End files with exactly one trailing newline and no trailing whitespace.
33
34
  - When editing existing code, match its established style instead of imposing a different one.
@@ -39,7 +40,7 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
39
40
  - Validate EVERY external input (request body, query, params, event payload, form field, CLI arg, webhook/message) against an explicit schema before use - Zod (TS/JS), Pydantic (Python), the language's equivalent elsewhere. Never consume an unvalidated shape or leave it open-ended. When the input is a tagged/event union, validate the discriminant AND that specific variant's body, with the expected fields typed.
40
41
  - Normalize before validating, on the client AND the server, from one shared normalizer: trim every string, lowercase the email, capitalize each word of a person's name, canonicalize a link or handle to one stored form. A check that cannot fail is not validation - never patch the value into validity and then check the patched value.
41
42
  - Frontend forms: validate in real time against the same schema, on EVERY field that has a rule and not only the ones with a famous format, and use optimistic UI with rollback on failure for user-facing mutations.
42
- - Never block the first paint on data: ship the HTML shell, then request the data. Everything that does not depend on the response renders now (nav, headings, table chrome, filters, anything already cached) and only the region genuinely waiting gets a skeleton shaped like its content - never a full-page loader, and never a page that renders nothing until the fetch resolves. The rules are frontend-policy's Instant First Paint.
43
+ - Never block the first paint on data: ship the HTML shell, then request the data. Everything that does not depend on the response renders now (nav, headings, table chrome, filters, anything already cached) and only the region genuinely waiting gets a skeleton shaped like its content - never a full-page loader, and never a page that renders nothing until the fetch resolves. A skeleton over the WHOLE view is the same defect: a loading guard that returns from the component blanks its headings, tabs and filters too, and an awaited query in a server route blocks the navigation instead. The rules are frontend-policy's Instant First Paint.
43
44
  - Cache reads on the client (localStorage/sessionStorage, or the data layer's cache) with a short TTL (~30s or more) to avoid redundant queries and survive rate limits; invalidate on write.
44
45
  - Build reusable, composable components instead of duplicating UI - e.g. a single Input that renders a show/hide toggle when the type is password. Reuse before writing new.
45
46
  - Never use the browser's native `alert`/`confirm`/`prompt` - use a dialog/modal component that matches the page design.
@@ -92,7 +93,7 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
92
93
  - Those are the only exits and each has to be NAMED. Asking is not one of them: the gate being on is already the decision, so "the gate has not run, tell me if you want me to launch it" ends a turn on a question with one answer. Ending unvalidated and silent is worse. `enigma verify` denies the stop both ways - on a message that reports the gate as skipped, and on commits no run ever saw.
93
94
  - On the default branch the pipeline opens no PR and its push lands directly on that branch. That is the intended behavior there; report it as the outcome instead of waiting for a PR link.
94
95
  - If the repo is not initialized yet (`enigma gate axi` says so), run `enigma gate init` once yourself, then proceed - an uninitialized repo is a setup step you can perform, never a reason to stop or ask. If `enigma gate` itself misbehaves, `enigma gate doctor` reports why.
95
- - Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`): authorize `auto-fix` and `no-op` findings on your own judgment, but STOP and escalate every `ask-user` finding verbatim. Do NOT pass `--yes` and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
96
+ - Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`). A gate it hands back carries `fix_policy`, the user's standing answer to who decides - follow it: under `assisted` (the default) escalate the `ask-user` findings verbatim, under `ask` put every finding to the user before authorizing anything, under `auto` settle them all yourself. Do NOT pass `--yes` (the setting is how the user grants that) and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
96
97
  - While a run is active never edit code to fix a finding; the pipeline owns the fixes (respond with `--action fix`). The full command reference lives in the gate skill / `/gate` command.
97
98
  <!-- enigma:gate:end -->
98
99
 
@@ -28,6 +28,7 @@
28
28
  - No emojis in responses, code, or docs. Use ASCII punctuation: "-" not the long dash, "->" not the arrow. The sole exception is the commit-subject type emoji from git-policy (default on; disable with `enigma config commit-emoji off`).
29
29
  - Treat all external input as untrusted; never expose secrets or hardcode credentials.
30
30
  - When removing leaked or sensitive data the user asked to delete, the remediation commit/PR/branch must NOT name the leaked values or flag the security motive (that signposts where to look and re-leaks the values permanently) - use a neutral, mundane message and the `🔒 security` type is forbidden for it; offer history-rewrite vs. discreet-removal first. This carve-out is ONLY for that case; every other commit stays normal and descriptive (see git-policy).
31
+ - A value you cannot source is not a value you may write. An email, a version, an id, a URL, a price, a name, a field of an API - if it did not come from the user, the code, a command you ran, or a document you read, you do not have it: ask for it, or leave it out and say which. A well-formed guess is the dangerous kind, because it reads as fact and nobody re-checks it. Fixture and mock data is the exception, and it has to look like fixture data.
31
32
  - Reuse existing code before writing new code; do not duplicate logic.
32
33
  - End files with exactly one trailing newline and no trailing whitespace.
33
34
  - When editing existing code, match its established style instead of imposing a different one.
@@ -39,7 +40,7 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
39
40
  - Validate EVERY external input (request body, query, params, event payload, form field, CLI arg, webhook/message) against an explicit schema before use - Zod (TS/JS), Pydantic (Python), the language's equivalent elsewhere. Never consume an unvalidated shape or leave it open-ended. When the input is a tagged/event union, validate the discriminant AND that specific variant's body, with the expected fields typed.
40
41
  - Normalize before validating, on the client AND the server, from one shared normalizer: trim every string, lowercase the email, capitalize each word of a person's name, canonicalize a link or handle to one stored form. A check that cannot fail is not validation - never patch the value into validity and then check the patched value.
41
42
  - Frontend forms: validate in real time against the same schema, on EVERY field that has a rule and not only the ones with a famous format, and use optimistic UI with rollback on failure for user-facing mutations.
42
- - Never block the first paint on data: ship the HTML shell, then request the data. Everything that does not depend on the response renders now (nav, headings, table chrome, filters, anything already cached) and only the region genuinely waiting gets a skeleton shaped like its content - never a full-page loader, and never a page that renders nothing until the fetch resolves. The rules are frontend-policy's Instant First Paint.
43
+ - Never block the first paint on data: ship the HTML shell, then request the data. Everything that does not depend on the response renders now (nav, headings, table chrome, filters, anything already cached) and only the region genuinely waiting gets a skeleton shaped like its content - never a full-page loader, and never a page that renders nothing until the fetch resolves. A skeleton over the WHOLE view is the same defect: a loading guard that returns from the component blanks its headings, tabs and filters too, and an awaited query in a server route blocks the navigation instead. The rules are frontend-policy's Instant First Paint.
43
44
  - Cache reads on the client (localStorage/sessionStorage, or the data layer's cache) with a short TTL (~30s or more) to avoid redundant queries and survive rate limits; invalidate on write.
44
45
  - Build reusable, composable components instead of duplicating UI - e.g. a single Input that renders a show/hide toggle when the type is password. Reuse before writing new.
45
46
  - Never use the browser's native `alert`/`confirm`/`prompt` - use a dialog/modal component that matches the page design.
@@ -92,7 +93,7 @@ Non-negotiable, language-agnostic defaults - apply them by default without being
92
93
  - Those are the only exits and each has to be NAMED. Asking is not one of them: the gate being on is already the decision, so "the gate has not run, tell me if you want me to launch it" ends a turn on a question with one answer. Ending unvalidated and silent is worse. `enigma verify` denies the stop both ways - on a message that reports the gate as skipped, and on commits no run ever saw.
93
94
  - On the default branch the pipeline opens no PR and its push lands directly on that branch. That is the intended behavior there; report it as the outcome instead of waiting for a PR link.
94
95
  - If the repo is not initialized yet (`enigma gate axi` says so), run `enigma gate init` once yourself, then proceed - an uninitialized repo is a setup step you can perform, never a reason to stop or ask. If `enigma gate` itself misbehaves, `enigma gate doctor` reports why.
95
- - Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`): authorize `auto-fix` and `no-op` findings on your own judgment, but STOP and escalate every `ask-user` finding verbatim. Do NOT pass `--yes` and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
96
+ - Drive it through the `/gate` workflow (`enigma gate axi run --intent "<what the user set out to accomplish>"`). A gate it hands back carries `fix_policy`, the user's standing answer to who decides - follow it: under `assisted` (the default) escalate the `ask-user` findings verbatim, under `ask` put every finding to the user before authorizing anything, under `auto` settle them all yourself. Do NOT pass `--yes` (the setting is how the user grants that) and do NOT merge the PR yourself - on `checks-passed`, leave the PR ready and ask the user to review and merge it.
96
97
  - While a run is active never edit code to fix a finding; the pipeline owns the fixes (respond with `--action fix`). The full command reference lives in the gate skill / `/gate` command.
97
98
  <!-- enigma:gate:end -->
98
99
 
@@ -7,6 +7,6 @@
7
7
  "minimalCode"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.34.2",
10
+ "cliVersion": "1.35.1",
11
11
  "sha": "3f0dcc28341bb0407860534f7ce9314cfc91b5e673f8f3d13b89d61851ed75f6"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "On-demand over-engineering review - diff review, whole-repo audit, and enigma: debt-marker ledger (tags delete/stdlib/native/yagni/shrink, line/dep scoring); lists cuts, applies nothing.",
6
6
  "updated": "2026-06-16T11:24:30+02:00",
7
- "cliVersion": "1.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
8
8
  "sha": "95091678d4e72e3a9b1ef3ad0514e503a2a631f0d134bf772b4a1b3c66881995"
9
9
  }
@@ -106,6 +106,17 @@ Any denormalized or duplicated value MUST have:
106
106
  - Persist derived/aggregated values only when recomputation is expensive or required for performance/analytics.
107
107
  - When persisting derived data, prefer materialized views or summary tables over scattered duplicate columns.
108
108
 
109
+ ### A Pure Function Of A Key Needs No Column
110
+
111
+ The rules above are about a value computed from other columns. A stronger case is worth looking for first: a value that is a pure function of an id you already hold needs no column, no row, and no cache. Same id, same answer, in every process, forever.
112
+
113
+ - The shapes this covers: a human-readable label for an opaque id (a session, a run, an invite), an avatar colour or identicon, a bucket or shard assignment, a rollout cohort, a deterministic seed. Hash the id with a small stable function you own (FNV-1a, xxHash, or the leading bytes of a digest) and index into a fixed list.
114
+ - The function must be stable across processes and versions of the runtime: no clock, no locale, no random seed, and not a language built-in whose value is unspecified (JavaScript has no string hash at all; a JVM `hashCode` is specified for `String` but not for most other types). Write the few lines yourself rather than depending on a package for them.
115
+ - STATE THE TRADEOFF BEFORE CHOOSING, because it is not reversible in either direction. Derived means the mapping lives in the code, so editing the function or the list silently renames everything that already exists. Stored means the value is fixed forever, at the cost of a column, a write path and a backfill.
116
+ - The test is what the value is FOR. A convenience label the user reads and forgets is derived. An identity they will use to refer to the thing ("the Pegasus run", a share URL, anything they can search or that appears in an invoice) is data: store it on creation, seeded by the same function if you like.
117
+ - Do not derive across a boundary you do not own: a value another system persists, or one the user can rename, is data by definition.
118
+ - The same question applies before adding a row, not only a column: a join table that only ever expresses a rule ("everyone in this org is a member") is a query, and a settings row that only ever holds the defaults is an absence.
119
+
109
120
  ---
110
121
 
111
122
  ## Query & Index Optimization (Always)
@@ -3,7 +3,7 @@
3
3
  "version": "1.4.0",
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
- "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.34.2",
8
- "sha": "f4ccf51286027c97c8402f3bac6c894e05a84cba2de014463cb3a83be17537d4"
6
+ "updated": "2026-08-04T18:33:27+02:00",
7
+ "cliVersion": "1.35.1",
8
+ "sha": "26fddc14caf697e830448a2cc453ddc470fe83d098d50bd67b278effba63274e"
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
8
8
  "sha": "9e30ee7d8a1a1e8c6e7f4e043857cd01841c68a427752e45bc0cad9ec5cfa279"
9
9
  }
@@ -194,8 +194,10 @@ The check to run while building, not afterwards: go column by column and ask wha
194
194
  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.
195
195
 
196
196
  - Design for the extremes of each string, not the sample: the longest realistic value (an unbounded user-supplied name, a long identifier, a full path, a big formatted number) and the shortest (empty, one character). Translations run noticeably longer than English - roughly a third more for German - so a label that only just fits is already broken.
197
+ - **Ask both questions before laying anything out, not after the bug report.** (1) Is this string translated? If the project has i18n, every label is variable-length by definition, and the English sample is usually the shortest one it will ever be - so a fixed-width column, a `whitespace-nowrap` label or a button sized to its current text is already a defect in the other locales. (2) Is this value data? A name, email, title, description, path, URL, filename or anything else a user or an API supplies has no length limit worth trusting, and it is exactly the value that runs long in production and never in the sample. Those two answers decide the layout: what wraps, what clips, what gets `shrink-0`, and what needs a minimum width.
198
+ - Fixed copy you wrote yourself is the only text you may size a box around. Everything else gets room to grow, a place to wrap, or a clip with the full value kept reachable.
197
199
  - **A flex or grid item whose overflow is visible refuses to shrink below its content** (its automatic minimum size, `min-width: auto`). This causes most "text sticks out of its card" bugs, but not where people look for it: an element that truncates has already set `overflow: hidden`, which resolves that minimum to 0, so it shrinks by itself and does NOT need `min-width: 0`. The culprit is nearly always an **ancestor** - a flex or grid item with default overflow wrapping the truncating element. Put `min-width: 0` (Tailwind `min-w-0`) on those ancestors, and use `minmax(0, 1fr)` rather than a bare `1fr` for a grid track holding text. Confirm by measuring which box actually overflows rather than scattering `min-w-0` until it looks right.
198
- - Decide per string whether it wraps or truncates. Truncation needs `overflow: hidden` - `text-overflow: ellipsis` does nothing without it - and the full value must stay reachable via `title`, a tooltip, or an accessible label. Never truncate a value the user has no way to recover.
200
+ - Decide per string whether it wraps or truncates. Truncation needs `overflow: hidden` - `text-overflow: ellipsis` does nothing without it - and the full value must stay reachable via `title`, a tooltip, or an accessible label. Never truncate a value the user has no way to recover. Clipping and hiding are one keystroke apart: `<span className="truncate">{user.email}</span>` shows `alberto.rodriguez@lo...` and offers no way to ever see the rest. Add `title={user.email}` in the same edit, or reach for the design system's tooltip where it has one.
199
201
  - Long unbroken strings (URLs, tokens, hashes, ids, file paths) have no spaces to wrap at and will push their container wide. Use `overflow-wrap: anywhere` on those. Prefer it over `word-break: break-word`: it also lowers the element's min-content width, which is what actually stops the track being forced wider.
200
202
  - Content whose width changes as it updates (counters, timers, prices) reflows its row on every tick. Use tabular numerals (`font-variant-numeric: tabular-nums`) or reserve the space.
201
203
  - Absolutely positioned or overlaid text is where collisions happen, because it is outside normal flow and cannot push anything away. Constrain it with a `max-width` and check it at the narrowest breakpoint.
@@ -405,6 +407,24 @@ Render the page shell immediately; never block the first paint on data. A view t
405
407
  - For an instant first paint with REAL content, read the client cache first (per Client-Side Caching) and render it immediately, then revalidate in the background (stale-while-revalidate); fall back to skeletons only on a cold cache.
406
408
  - Keep empty and error states per region, so a single failed or slow widget shows its own inline state without blanking the whole page.
407
409
 
410
+ ### A whole-view skeleton is a full-page loader
411
+
412
+ A skeleton is not compliance by itself. `if (!data) return <PageSkeleton />` at the top of a component returns from the WHOLE component, so every heading, tab, filter and card frame it renders disappears too. Replacing a full-page spinner with a full-page skeleton changes the colour of the defect, not the defect.
413
+
414
+ - The guard belongs INSIDE the waiting region, not at the top of the view. Render the shell unconditionally and put the placeholder where the data goes: `{rows ? <Rows rows={rows}/> : <RowsSkeleton/>}` inside a table that already drew its header, filters and toolbar.
415
+ - The alternative is to push the request down: give the data-dependent region its own child component that owns the fetch, so the parent never has a reason to return early. A component whose entire output is built from the response may early-return, because there the component and the region are the same thing.
416
+ - Check what survives the guard before writing it: if the body below it draws a title, a tab bar, a button or any literal copy, that markup could have painted on the first tick and the guard is blanking it.
417
+ - The same applies to a detail view: the record's name from the route, breadcrumbs, the action buttons and the section headings render immediately, and only the fields wait.
418
+
419
+ ### The shell ships first, the data follows
420
+
421
+ Data that arrives inside the first HTML document is data the navigation waited for. In a server-rendered route (Next App Router, Remix, Nuxt), an awaited query in the route component blocks the whole transition: the router has nothing to commit until it resolves, so the previous page stays on screen and the app feels stuck on every link click.
422
+
423
+ - An async route component that awaits a query declares a streaming boundary: a `loading.tsx` for the segment, or `<Suspense>` around just the data-dependent part with the awaited call inside it. Then the shell commits at once and the data streams in.
424
+ - Where the view is interactive anyway, let the client component own the request and render from the cache first (see Client-Side Caching). The server route stays a thin shell that carries the chrome: title, nav, tabs and layout.
425
+ - Server-render the data only where it must be in the document: SEO-indexable content, the above-the-fold content of a public page, or a value the page is meaningless without. Those are worth the wait; a dashboard table is not.
426
+ - Never make it a waterfall: a server route that awaits, then hands props to a client component that fetches again on mount, pays both costs.
427
+
408
428
  ---
409
429
 
410
430
  ## Large Lists & Progressive Loading
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "frontend-policy",
3
- "version": "1.27.0",
3
+ "version": "1.29.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-03T00:16:54+02:00",
7
- "cliVersion": "1.34.2",
8
- "sha": "6c4b973c41ca7c12e1839a4fba76f26ede4e04a57567392687ca61e7f6687d9b"
6
+ "updated": "2026-08-04T14:11:26+02:00",
7
+ "cliVersion": "1.35.1",
8
+ "sha": "e5fbf094be2e7a00e40d02fd7611b7333b1249d7ac951029b8944e359135aa21"
9
9
  }
@@ -55,6 +55,16 @@ description: Commit, branch, and pull request standards - conventional commits,
55
55
  - The commit/PR must read as if the user authored it; no trace of the assistant in author fields, trailers, or footers.
56
56
  - Enforcement note: in Claude Code this is also disabled deterministically via settings.json (`attribution.commit` / `attribution.pr` set to "", and `includeCoAuthoredBy: false`); the enigma installer sets this automatically. The rule here still applies on every runtime, settings or not.
57
57
 
58
+ ### An Attribution Trailer Carries An Identity You Were Given
59
+
60
+ When the user asks for a human co-author, the trailer is `Co-authored-by: Name <email>` - and the email is a fact about a real person, so it is never inferred, completed, or guessed from a username. Writing a plausible address is worse than writing none: it is wrong in a way that looks right, it goes into history permanently, and it silently credits nobody or the wrong person.
61
+
62
+ - A username is not an email. `FJRG2007` tells you nothing about the mailbox, and neither `name@gmail.com` nor `team.name@gmail.com` nor a company domain is a deduction - each is an invention that happens to be well-formed.
63
+ - Get it from a source, in this order: what the user told you; the repository's own history for that person (`git log --all --author="<name>" --format="%an <%ae>" | sort -u`, or `.mailmap`) - `--all`, because a co-author who only ever committed on another branch is invisible to the HEAD-only default; or GitHub, where `gh api users/<login> --jq '.id, .login, .email'` returns their numeric id and their public email when they have set one.
64
+ - The GitHub no-reply address is only constructible from that id, and its form depends on the account: `ID+LOGIN@users.noreply.github.com` for accounts created after 18 July 2017, and `LOGIN@users.noreply.github.com` for older accounts that enabled email privacy before that date. So it is a value you look up, not one you assemble from a username.
65
+ - For the co-author to be credited, the address must be one attached to their account. If you cannot source it, ask - one question costs a turn, a wrong trailer costs a rewrite of history.
66
+ - The same rule covers every identity a commit or PR can carry: reviewers, `Reported-by`/`Signed-off-by` trailers, an issue or PR number, a CVE id, a release version. If you did not read it somewhere, you do not write it.
67
+
58
68
  ---
59
69
 
60
70
  ## Commit Strategy
@@ -3,7 +3,7 @@
3
3
  "version": "1.5.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
- "updated": "2026-07-16T22:44:02+02:00",
7
- "cliVersion": "1.34.2",
8
- "sha": "e6dfbc33884000d9d25841bd9c5a84d6558ffd374882cb7b34451eb2cebc2161"
6
+ "updated": "2026-08-04T20:04:42+02:00",
7
+ "cliVersion": "1.35.1",
8
+ "sha": "f55f4ed5474d5c6e7fd0804d88ac38fb3c9c3cd863519645d1415c02f4416db7"
9
9
  }
@@ -7,6 +7,6 @@
7
7
  "logoColorPolicy"
8
8
  ],
9
9
  "updated": "2026-07-17T00:17:02+02:00",
10
- "cliVersion": "1.34.2",
10
+ "cliVersion": "1.35.1",
11
11
  "sha": "09cdbefd98625b02a7d03685e5deed128238ff8454a83fe22279610fe3ef8ddf"
12
12
  }
@@ -4,6 +4,6 @@
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), credential flows with breach-checked passwords (Have I Been Pwned), OWASP Top 10, transport/crypto baseline, cookies and consent, secure logging, and agent/MCP/tool-use safety.",
6
6
  "updated": "2026-08-02T19:59:01+02:00",
7
- "cliVersion": "1.34.2",
7
+ "cliVersion": "1.35.1",
8
8
  "sha": "3201b6d41437626eb7bf45648c0d7f8c419dc4de87fbd9d4ca83c5ae3b6b6edf"
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-08-03T00:16:54+02:00",
7
- "cliVersion": "1.34.2",
7
+ "cliVersion": "1.35.1",
8
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
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.34.2",
7
+ "cliVersion": "1.35.1",
8
8
  "sha": "225e1e26f4a49fac70714b1bebc104ea42143203cc8fcc9e3330b29f05b6e025"
9
9
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "enigma-darwin-arm64": "df777b37221a01749abc93d3297d93f50dea8b2a4166a9a895828c00904fb1c5",
3
- "enigma-linux-arm64": "e66581e34ce9cf58685724446cbf4b47c39558096c99875b8e86fb26dfe9c16b",
4
- "enigma-linux-x64": "42f11362bf6eba1ae14ddb5713318c6e400b044b7864eacaf0eced5b9db0e059",
5
- "enigma-win32-x64.exe": "0e6f118aac8d92f3fe6383d94808444be521044e1059731cac16f0cea16ead9b"
2
+ "enigma-darwin-arm64": "f54b09af03c95df904ae57543d4ffb0b9d3d3165a561de4a81e2fcb9271f152d",
3
+ "enigma-linux-arm64": "712f04659e37d77d2593e0c5a66135a690b76fbd37f3b48582063742a367c69f",
4
+ "enigma-linux-x64": "98afcea630f221f10e1eb9850e6df64036a3375a28bacd8276ec3b6559bd8b0a",
5
+ "enigma-win32-x64.exe": "0f52ef8da10adf303756785bc9f30c6470662840c21e0f27110bebc3159f4726"
6
6
  }
@@ -5,8 +5,10 @@ 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 { appendFileSync, mkdirSync, readFileSync, writeFileSync, statSync, existsSync } from "fs";
8
+ import { appendFileSync, mkdirSync, readFileSync, readdirSync, writeFileSync, statSync, existsSync } from "fs";
9
9
  var COMMENT_LINE = /^\s*(\/\/|#|\*|--|<!--|\{?\/\*)/;
10
+ var SKELETON_GUARD_SRC = "\\bif\\s*\\(\\s*(isLoading|isPending|isFetching|loading|pending)\\s*\\)\\s*return\\s+(null\\b|<\\s*\\w*(Spinner|Loader|Loading|CircularProgress)\\b)";
11
+ var SKELETON_SIGNAL_SRC = "skeleton|animate-pulse|shimmer|Suspense|ContentLoader|content-loader|<\\s*Placeholder";
10
12
  var BUILTIN_RULES = [
11
13
  {
12
14
  id: "db-uuid-pk",
@@ -143,15 +145,45 @@ var BUILTIN_RULES = [
143
145
  id: "fe-password-input",
144
146
  label: "Reusable password input (show/hide)",
145
147
  files: ["*.tsx", "*.jsx"],
148
+ excludeFiles: [
149
+ "*.test.*",
150
+ "*.spec.*",
151
+ "*.stories.*",
152
+ "*.min.js",
153
+ "**/tests/**",
154
+ "tests/**",
155
+ "**/__tests__/**",
156
+ "__tests__/**",
157
+ "**/stories/**",
158
+ "stories/**",
159
+ "**/dist/**",
160
+ "dist/**",
161
+ "**/build/**",
162
+ "build/**",
163
+ "**/_build/**",
164
+ "_build/**",
165
+ "**/node_modules/**",
166
+ "node_modules/**",
167
+ "**/vendor/**",
168
+ "vendor/**"
169
+ ],
146
170
  scope: "file",
147
171
  // A raw lowercase <input type="password"> (not a component) with no show/hide toggle in the
148
172
  // file. flags:"" = case-sensitive so a capitalized <Input> component is NOT matched; a
149
173
  // literal type="password" only, so a dynamic type={visible?...} toggle is not matched either.
150
- pattern: `<input\\b[^>]*type=["']password["']`,
174
+ pattern: `^(?!.*enigma:).*<input\\b[^>]*type=["']password["']`,
151
175
  flags: "",
152
- absent: "showPassword|setShowPassword|togglePassword|revealPassword|passwordVisible|isPasswordVisible|showPw|hidePassword",
153
- message: 'Raw <input type="password">: use the shared reusable Input component (which renders a show/hide toggle for passwords) instead of a bare input, or add the toggle (frontend-policy).',
154
- severity: "warn",
176
+ absent: "showPassword|setShowPassword|togglePassword|revealPassword|passwordVisible|isPasswordVisible|showPw|hidePassword|enigma:allow-raw-password-input",
177
+ message: 'Raw <input type="password">: use the shared reusable Input component (which renders a show/hide toggle for passwords) instead of a bare input, or add the toggle (frontend-policy). Mark the line `enigma:` or add `enigma:allow-raw-password-input` to the file where the field is deliberately bare.',
178
+ // BLOCK for the same reason as fe-ellipsis-without-overflow, and it is the same class of
179
+ // routing failure: a warn is never fed back by the hook and never denies the stop at turn
180
+ // end, while "one Input that renders a show/hide toggle for a password" is an always-on
181
+ // kernel convention the model is expected to apply, not advice it may weigh. It is the
182
+ // reverse of the two rules that stay warn - the criterion is the backlog, and this rule
183
+ // has none: measured over 39316 files of the whole local
184
+ // corpus, 75 candidate lines and 0 findings - every real password field already carries a
185
+ // toggle, so it fires on a bare one an agent writes and on nothing else.
186
+ severity: "block",
155
187
  skill: "frontend-policy"
156
188
  },
157
189
  {
@@ -413,12 +445,15 @@ var BUILTIN_RULES = [
413
445
  // returns null or a bare spinner (blank screen until the fetch resolves) - the "page doesn't
414
446
  // render until it has data" tell. The return group matches ONLY null / a
415
447
  // *Spinner|*Loader|*Loading|CircularProgress element, so `return <Skeleton/>` is NOT matched
416
- // (that is the correct pattern). `absent` skips the file when ANY placeholder/skeleton signal
448
+ // here - NOT because it is correct (a whole-component return blanks the view either way) but
449
+ // because that shape needs the body read to tell a blanked view from a loader whose whole
450
+ // output is the awaited data, which is fe-view-blanked-while-loading's job at the diff stage.
451
+ // `absent` skips the file when ANY placeholder/skeleton signal
417
452
  // is present (skeleton, animate-pulse, shimmer, Suspense, a content-loader lib, <Placeholder>)
418
453
  // - the component already renders a placeholder somewhere. Kept to the terse one-line guard for
419
454
  // precision (a multi-line block is not matched: precision > recall).
420
- pattern: "\\bif\\s*\\(\\s*(isLoading|isPending|isFetching|loading|pending)\\s*\\)\\s*return\\s+(null\\b|<\\s*\\w*(Spinner|Loader|Loading|CircularProgress)\\b)",
421
- absent: "skeleton|animate-pulse|shimmer|Suspense|ContentLoader|content-loader|<\\s*Placeholder",
455
+ pattern: SKELETON_GUARD_SRC,
456
+ absent: SKELETON_SIGNAL_SRC,
422
457
  message: "Component returns nothing (or only a spinner) while data loads, so the whole page stays blank until the fetch resolves. Render the shell on first paint - nav, headings, card frames, table chrome, filters, and any value you already hold - and skeleton ONLY the region whose data is missing, shaped like the real content with its space reserved so nothing shifts when it lands. A region that does not depend on this request is not loading and must render now (frontend-policy).",
423
458
  // BLOCK, changed from warn: this is the rule for the defect users keep reporting (a page
424
459
  // that renders nothing until its data arrives), and as a warn it exited 0 - printed to
@@ -461,6 +496,52 @@ var BUILTIN_RULES = [
461
496
  severity: "block",
462
497
  skill: "frontend-policy"
463
498
  },
499
+ {
500
+ id: "fe-view-blanked-while-loading",
501
+ label: "Only the waiting region is a placeholder, not the whole view",
502
+ files: ["*.tsx", "*.jsx"],
503
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
504
+ scope: "file",
505
+ // The hole fe-skeleton-loading left open, and the reason the defect kept shipping after that
506
+ // rule went to BLOCK: it matches only `return null` / `return <Spinner/>` and deliberately
507
+ // treats `return <Skeleton/>` as the correct pattern. It is not - a whole-component early
508
+ // return swaps the ENTIRE view for placeholders, so a page whose headings, tabs, filters and
509
+ // card frames never depended on the response still renders as a grey page. The old rule's
510
+ // `absent` makes it worse: any skeleton anywhere in the file clears it, so drawing a
511
+ // full-view skeleton is the one way to satisfy the gate while committing the defect.
512
+ // DIFF stage: 13 of these lived in the corpus BEFORE the condition set was widened - a
513
+ // standing backlog an edit-stage rule would re-report on every unrelated edit to the same
514
+ // file. Current figure, after the widening and the fe-skeleton-loading suppression: 10
515
+ // findings over 7199 files, still 0 false positives.
516
+ stage: "diff",
517
+ fileCheck: "fe-view-blanked-while-loading",
518
+ message: "This loading guard returns from the whole component, so the entire view becomes placeholders until the request resolves - a full-page loader with rounded corners. The elements listed below do not depend on the response and must paint on the first tick: headings, tab bars, filters, search, buttons, table and card chrome, and any value already in hand (a name from the route, a count from the cache). Move the guard INSIDE the region that is actually waiting - render the shell, and skeleton only the rows, the chart or the tiles - or hand the region to a child component that owns its own request. Mark the line `enigma:allow-view-skeleton` when the component genuinely renders nothing but the awaited data (frontend-policy).",
519
+ severity: "block",
520
+ skill: "frontend-policy"
521
+ },
522
+ {
523
+ id: "fe-page-await-no-boundary",
524
+ label: "Server route streams its shell before its data",
525
+ files: ["page.tsx", "page.jsx"],
526
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
527
+ scope: "file",
528
+ // The other half of the same report ("the data arrives inside the first HTML, which slows the
529
+ // navigation down"). An awaited query in an async route component holds the WHOLE navigation:
530
+ // the router has nothing to commit until it resolves, so the previous page stays on screen and
531
+ // the app feels stuck. This was rejected as a rule once, when it was framed as "do not fetch on
532
+ // the server" - a judgement call a regex cannot make, since a static page awaiting build-time
533
+ // content is correct. Framed as "an async route that awaits a query declares a streaming
534
+ // boundary" it is mechanical, and the fix is never wrong: a `loading.tsx` in the segment (or any
535
+ // ancestor) or a <Suspense> around the data region both let the shell paint immediately.
536
+ // MEASURED over 1127 route files / 159 async route components: 11 candidates, 7 already covered
537
+ // by a loading.tsx up their segment chain, 4 route files - reported at 5 anchors, since every
538
+ // awaited query gets its own.
539
+ stage: "diff",
540
+ fileCheck: "fe-page-await-no-boundary",
541
+ message: "This route awaits its data before it returns anything, so the navigation blocks for the whole query: the router holds the old page on screen until the query resolves, and the data ships inside the first HTML instead of the shell shipping first. Give the segment a streaming boundary - add a `loading.tsx` beside this page (the shell paints at once and this subtree streams in), or wrap only the data-dependent region in <Suspense> with a skeleton fallback and keep the awaited call inside it. Where the view is interactive anyway, let the client component own the request and render from the cache first. Mark the line `enigma:allow-blocking-page` when the page must not commit until the data is known (frontend-policy).",
542
+ severity: "block",
543
+ skill: "frontend-policy"
544
+ },
464
545
  {
465
546
  id: "fe-ai-elements-chat",
466
547
  label: "AI chat UI via AI Elements",
@@ -501,6 +582,13 @@ var BUILTIN_RULES = [
501
582
  pattern: "<head[\\s>]",
502
583
  absent: "viewport|mso-|<!--\\[if\\s|-webkit-text-size-adjust|<mj-",
503
584
  message: 'HTML document with a <head> but no responsive viewport meta. Add <meta name="viewport" content="width=device-width, initial-scale=1"> so the page is responsive on mobile instead of rendering at desktop width (frontend-policy).',
585
+ // STAYS warn, and it was measured rather than assumed when its two siblings were flipped
586
+ // to block: 571 candidate lines over 39316 files produce 145 findings - generated API
587
+ // docs, framework error templates, sample apps and one-off report pages that are all genuine
588
+ // matches and none of them anyone's current work. That is a legacy backlog, so a block
589
+ // would fire on every unrelated edit to those files, which is exactly the cost the
590
+ // ellipsis and password rules do NOT carry (0 findings each). Backlog, not precision and
591
+ // not how bad the defect is, is what decides this severity.
504
592
  severity: "warn",
505
593
  skill: "frontend-policy"
506
594
  },
@@ -525,24 +613,80 @@ var BUILTIN_RULES = [
525
613
  // the rule only ever flagged correct code. The real defect is an ANCESTOR flex/grid item with
526
614
  // visible overflow wrapping the truncating element, which spans two elements and so has no
527
615
  // single-line signature. It stays in frontend-policy as guidance rather than becoming a rule
528
- // that cries wolf.
616
+ // that cries wolf. Three neighbours of it were measured over 9072 UI files and rejected for the
617
+ // same reason - whether a value is BOUNDED is not in the line: `whitespace-nowrap` on a dynamic
618
+ // value with no clipping (91 hits, and the majority are correct - dates, amounts, durations and
619
+ // short type labels are exactly what nowrap is for), a fixed-width box holding an unclipped
620
+ // value (207, same), and a translated string inside a hard box (10 candidates, about half of
621
+ // them `sr-only` spans or an input placeholder where visual width does not apply). What IS
622
+ // gateable is the CLIPPING half, below: the element that clips is the element that hides the
623
+ // value, so it alone decides whether the user can still read it.
529
624
  {
530
625
  id: "fe-ellipsis-without-overflow",
531
626
  label: "Ellipsis needs overflow hidden",
532
627
  files: ["*.css", "*.scss", "*.sass", "*.less", "*.styl", "*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.html"],
533
- excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "**/build/**", "*.min.css"],
628
+ excludeFiles: [
629
+ "*.test.*",
630
+ "*.spec.*",
631
+ "*.min.css",
632
+ "**/tests/**",
633
+ "tests/**",
634
+ "**/__tests__/**",
635
+ "__tests__/**",
636
+ "**/dist/**",
637
+ "dist/**",
638
+ "**/build/**",
639
+ "build/**",
640
+ "**/.next/**",
641
+ ".next/**",
642
+ "**/node_modules/**",
643
+ "node_modules/**"
644
+ ],
534
645
  scope: "file",
535
646
  // text-overflow only applies to a box that actually overflows, so ellipsis without an
536
647
  // overflow value does nothing at all - the text just spills. The absent set covers the
537
648
  // CSS declarations and the Tailwind utilities that provide it; note it cannot simply be
538
649
  // "overflow", which would match the text-overflow property on this very line.
539
- pattern: "text-overflow\\s*:\\s*ellipsis",
650
+ pattern: "^(?!.*enigma:).*text-overflow\\s*:\\s*ellipsis",
540
651
  // Only things that actually PROVIDE the missing overflow value. Tailwind's `truncate`
541
652
  // does (it sets overflow-hidden); `text-ellipsis` does not - it is the ellipsis
542
653
  // declaration itself, so listing it would suppress the very case being flagged.
543
- absent: "overflow(?:-x|-y)?\\s*:\\s*(?:hidden|clip|auto|scroll)|overflow-hidden|overflow-clip|\\btruncate\\b",
544
- message: "text-overflow: ellipsis has no effect without an overflow value other than visible - the text overflows instead of being clipped. Add overflow: hidden (with white-space: nowrap for a single line), and keep the full value reachable via title or a tooltip (frontend-policy).",
545
- severity: "warn",
654
+ absent: "overflow(?:-x|-y)?\\s*:\\s*(?:hidden|clip|auto|scroll)|overflow-hidden|overflow-clip|\\btruncate\\b|enigma:allow-inert-ellipsis",
655
+ message: "text-overflow: ellipsis has no effect without an overflow value other than visible - the text overflows instead of being clipped. Add overflow: hidden (with white-space: nowrap for a single line), and keep the full value reachable via title or a tooltip (frontend-policy). Mark the line `enigma:` or add `enigma:allow-inert-ellipsis` to the file when the overflow value is set from another file (a shared utility class, an inherited base rule).",
656
+ // BLOCK, and the reason is the fe-skeleton-loading lesson rather than the severity of the
657
+ // defect: a warn exits 0, so runGuardrailsHook prints it where the model never reads it,
658
+ // and the only channel that can carry a warning - the turn-end sweep - writes it into the
659
+ // message only when a blocking finding fires in the same sweep, and never decides the exit
660
+ // code itself. So the one gate for a defect the model keeps writing could report it but
661
+ // never require the fix. Affordable here, and
662
+ // this is the half that had to be measured, because there is no legacy backlog: measured
663
+ // with this rule over 10253 UI files of the whole local corpus (this repo, apps, and every
664
+ // sibling product repo on the machine), 116 candidate lines in 40 files carry the
665
+ // declaration and NOT ONE of them is missing its overflow value. So it is a scaffolding
666
+ // guard like db-sqlite-app-datastore and sec-password-identity-match: it fires when an
667
+ // agent writes a new ellipsis, never on a project's existing stylesheets.
668
+ severity: "block",
669
+ skill: "frontend-policy"
670
+ },
671
+ {
672
+ id: "fe-truncated-value-unreachable",
673
+ label: "A clipped value keeps its full text reachable",
674
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.html", "*.htm"],
675
+ excludeFiles: ["*.test.*", "*.spec.*", "**/tests/**", "**/__tests__/**", "**/dist/**", "dist/**", "**/build/**", "build/**", "**/.next/**", ".next/**", "**/node_modules/**"],
676
+ scope: "file",
677
+ // The half of variable-length text that HAS a signature. text-overflow.md records why the
678
+ // layout half does not - the defect spans an ancestor and its child, and a rule written for
679
+ // it flagged correct code and was deleted. Clipping is different: the element that clips is
680
+ // the element that hides the value, so the question "can the user still read it?" is answered
681
+ // by that element and its immediate wrapper, and nothing else.
682
+ // DIFF stage, and not optional: hiding the value is what a clipped dynamic value does by
683
+ // default - the great majority of the measured corpus's clipped values carry their text
684
+ // nowhere - so an edit-stage rule would report a project's rows forever. The figures are in
685
+ // text-overflow.md and are deliberately not repeated here.
686
+ stage: "diff",
687
+ fileCheck: "fe-truncated-value-unreachable",
688
+ message: 'This clips a value the user cannot recover: the text is ellipsised and the full string appears nowhere. A name, email, path or title is exactly the value someone needs in full, and the sample used while building is always short enough to hide the problem. Where the design system has a tooltip, wrap the element in it - that is the better answer. Otherwise give the clipping element a `title` attribute carrying the same value, written in this file\'s own binding syntax (`title={value}` in JSX, `:title="value"` in Vue, `title={value}` in Svelte, `title="..."` in plain HTML) - and where the value must be readable at a glance rather than on hover, let it wrap instead of clipping. Mark the line `enigma:allow-clipped-value` when the full value is already shown elsewhere on the screen (frontend-policy).',
689
+ severity: "block",
546
690
  skill: "frontend-policy"
547
691
  },
548
692
  {
@@ -700,6 +844,13 @@ var BUILTIN_RULES = [
700
844
  // responses, so logging the real error is never flagged - only sending it out is.
701
845
  pattern: "\\.status\\(\\s*5\\d\\d\\s*\\)[^;]*\\b(err|error|e|ex|exception)\\.(message|stack)\\b|(res|reply|response)\\.(json|send|end)\\([^;]*\\b(err|error|e|ex|exception)\\.stack\\b",
702
846
  message: "Leaking an internal error to the client. Never send a caught exception's .message/.stack (or a raw ORM/DB error) in a 5xx response - it exposes your schema, ORM, and internals. Log it server-side (console.error / your logger) and return a generic message with a stable code (validation-policy, security-policy).",
847
+ // STAYS warn on the same measurement, and this is the uncomfortable one: 29 candidate
848
+ // lines produce 24 findings and every one is a real leak in a real backend, several of
849
+ // them in one product's route files. Severity here is not a judgement about how bad the
850
+ // defect is - it is whether the gate can block without firing on work nobody is doing
851
+ // today, and 24 pre-existing leaks in live route files means it cannot. The rule that
852
+ // would carry this is a DIFF-stage block (the fe-truncated-value-unreachable shape,
853
+ // which exists for exactly a defect that is the default in existing code).
703
854
  severity: "warn",
704
855
  skill: "validation-policy"
705
856
  },
@@ -713,7 +864,8 @@ var BUILTIN_RULES = [
713
864
  // Size has no regex form, hence maxBytes. 40 KB is ~10k tokens paid on EVERY session
714
865
  // in the project, relevant or not - a memory file that large is already broken, and
715
866
  // the fix (an index plus on-demand docs) is mechanical, so this blocks rather than
716
- // warns: a warn exits 0 and never reaches the model that keeps growing the file.
867
+ // warns: a warn exits 0 and reaches the model that keeps growing the file only when a
868
+ // blocking finding fires in the same turn-end sweep, so it is reported and never required.
717
869
  maxBytes: 4e4,
718
870
  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`.",
719
871
  severity: "block"
@@ -942,7 +1094,7 @@ var BUILTIN_RULES = [
942
1094
  scope: "file",
943
1095
  // A call spanning several lines has no line-regex form, hence a coded check (see
944
1096
  // missingWindowsHide for the three shapes it deliberately leaves alone). BLOCK for the
945
- // ui-no-em-dash reason: a warn exits 0 and never reaches the model, the symptom is
1097
+ // ui-no-em-dash reason: a warn exits 0 and is reported but never required, the symptom is
946
1098
  // invisible to whoever writes the code on macOS or Linux, and the fix is one key.
947
1099
  fileCheck: "proc-windows-hide",
948
1100
  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.",
@@ -1231,6 +1383,9 @@ var FILE_CHECKS = {
1231
1383
  "proc-windows-hide": (content) => missingWindowsHide(content),
1232
1384
  "fe-server-first-mutation": (content) => serverFirstMutation(content),
1233
1385
  "fe-textarea-size-bounds": (content) => textareaSizeBounds(content),
1386
+ "fe-view-blanked-while-loading": (content) => viewBlankedWhileLoading(content),
1387
+ "fe-truncated-value-unreachable": (content) => truncatedValueUnreachable(content),
1388
+ "fe-page-await-no-boundary": (content, file) => pageAwaitWithoutBoundary(content, file),
1234
1389
  "ts-import-extension": (content, file) => extensionImports(content, file),
1235
1390
  "ts-alias-deep-relative": (content, file) => deepRelativeImports(content, file),
1236
1391
  "ts-alias-paths": (content, file) => missingPathAlias(content, file)
@@ -1242,9 +1397,21 @@ var FIXERS = {
1242
1397
  if (!tags || tags.length !== 1) return null;
1243
1398
  const attr = /\.[jt]sx$/i.test(file) ? 'autoCapitalize="words"' : 'autocapitalize="words"';
1244
1399
  return line.replace(/<input\b/i, `<input ${attr}`);
1400
+ },
1401
+ "fe-truncated-value-unreachable": (line, file) => {
1402
+ if (!/\.[jt]sx$/i.test(file)) return null;
1403
+ if (/title\s*=/.test(line)) return null;
1404
+ const tags = line.match(/<[a-z][a-z0-9]*\b/g);
1405
+ if (!tags || tags.length !== 1) return null;
1406
+ const match = CLIPPED_SIMPLE_VALUE.exec(line);
1407
+ if (!match) return null;
1408
+ const [, tag, attrs, expr] = match;
1409
+ if (!CLIP_ONE_LINE.test(attrs) || NOT_TEXT_BINDING.test(expr)) return null;
1410
+ const replacement = `<${tag}${attrs} title={${expr}}>`;
1411
+ return line.replace(`<${tag}${attrs}>`, () => replacement);
1245
1412
  }
1246
1413
  };
1247
- function applyFixes(file, findings) {
1414
+ function applyFixes(file, findings, stage = "edit") {
1248
1415
  const fixable = findings.filter((f) => f.line && FIXERS[f.ruleId]);
1249
1416
  if (!fixable.length) return { fixed: [], remaining: findings };
1250
1417
  let content;
@@ -1270,7 +1437,11 @@ function applyFixes(file, findings) {
1270
1437
  } catch {
1271
1438
  return { fixed: [], remaining: findings };
1272
1439
  }
1273
- return { fixed, remaining: checkPath(file) };
1440
+ try {
1441
+ return { fixed, remaining: checkPath(file, stage) };
1442
+ } catch {
1443
+ return { fixed, remaining: [] };
1444
+ }
1274
1445
  }
1275
1446
  var SPAWNERS = /* @__PURE__ */ new Set(["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"]);
1276
1447
  function spawnerBindings(content) {
@@ -1392,6 +1563,103 @@ function textareaSizeBounds(content) {
1392
1563
  }
1393
1564
  return out;
1394
1565
  }
1566
+ var VIEW_GUARD = /\bif\s*\(\s*(!?\s*[\w.]{1,30}(?:\s*(?:===|!==|==|!=)\s*[\w."']{1,20})?)\s*\)\s*return\s+<\s*\w*(Skeleton|Placeholder|Shimmer|Loading|Loader|Spinner)\b/;
1567
+ var LOADING_NAME = "loading|pending|fetching|data|resource|result|state|profile|items|rows|list|summary|overview|response";
1568
+ var LOADING_VALUE = "loading|pending|fetching";
1569
+ var VIEW_GUARD_LOADING = new RegExp(`^(?:!?(?:\\w{1,30}\\.)*(?:is)?(?:${LOADING_NAME})|(?:\\w{1,30}\\.)*\\w{1,30}(?:===|!==|==|!=)["']?(?:${LOADING_VALUE})["']?)$`, "i");
1570
+ var VIEW_CHROME_HEADING = /<\s*(h1|h2|h3|CardTitle|DialogTitle|PageHeader|SheetTitle|SectionTitle)\b/;
1571
+ var VIEW_CHROME_TEXT = />\s*[A-Z][A-Za-z0-9 ,.'&:%/()-]{2,60}\s*</;
1572
+ var VIEW_BODY_LOOKAHEAD = 400;
1573
+ var ALLOW_VIEW_SKELETON = /enigma:allow-view-skeleton/;
1574
+ function markedNearby(lines, index, marker) {
1575
+ const { start, end } = enclosingBlock(lines, index);
1576
+ return lines.slice(Math.max(0, Math.min(start, index - 1)), end + 1).some((line) => marker.test(line));
1577
+ }
1578
+ var SKELETON_GUARD = new RegExp(SKELETON_GUARD_SRC);
1579
+ var SKELETON_SIGNAL = new RegExp(SKELETON_SIGNAL_SRC, "i");
1580
+ function viewBlankedWhileLoading(content) {
1581
+ const lines = content.split("\n");
1582
+ const owned = !SKELETON_SIGNAL.test(content);
1583
+ const out = [];
1584
+ for (let i = 0; i < lines.length; i++) {
1585
+ const line = lines[i];
1586
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
1587
+ const match = VIEW_GUARD.exec(line);
1588
+ if (!match || !VIEW_GUARD_LOADING.test(match[1].replace(/\s+/g, ""))) continue;
1589
+ if (owned && SKELETON_GUARD.test(line)) continue;
1590
+ if (markedNearby(lines, i, ALLOW_VIEW_SKELETON)) continue;
1591
+ let headings = 0;
1592
+ let texts = 0;
1593
+ for (let j = i + 1; j < lines.length && j - i < VIEW_BODY_LOOKAHEAD; j++) {
1594
+ const body = lines[j];
1595
+ if (/^\}/.test(body)) break;
1596
+ if (COMMENT_LINE.test(body)) continue;
1597
+ if (VIEW_CHROME_HEADING.test(body)) headings++;
1598
+ if (VIEW_CHROME_TEXT.test(body)) texts++;
1599
+ }
1600
+ if (headings < 1 && texts < 2) continue;
1601
+ const drawn = [headings ? `${headings} heading/title element(s)` : "", texts ? `${texts} literal text node(s)` : ""].filter(Boolean).join(" and ");
1602
+ out.push({ line: i + 1, detail: `the component still draws ${drawn} below this guard, none of which needs the response` });
1603
+ }
1604
+ return out;
1605
+ }
1606
+ var CLIP_ONE_LINE = /\btruncate\b|\btext-ellipsis\b|text-overflow\s*:\s*ellipsis/;
1607
+ var DYNAMIC_CHILD = /(?<![=!<>-])>\s*\{(?![#/:@])/;
1608
+ var VALUE_REACHABLE = /\btitle\s*=|aria-label\s*=|<\s*Tooltip|TooltipTrigger|data-tooltip|hoverCard|HoverCard/i;
1609
+ var WRAPPER_LOOKBACK = 4;
1610
+ var ALLOW_CLIPPED_VALUE = /enigma:allow-clipped-value/;
1611
+ function truncatedValueUnreachable(content) {
1612
+ const lines = content.split("\n");
1613
+ const out = [];
1614
+ for (let i = 0; i < lines.length; i++) {
1615
+ const line = lines[i];
1616
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
1617
+ if (!CLIP_ONE_LINE.test(line) || !DYNAMIC_CHILD.test(line)) continue;
1618
+ if (VALUE_REACHABLE.test(line)) continue;
1619
+ if (lines.slice(Math.max(0, i - WRAPPER_LOOKBACK), i).some((l) => VALUE_REACHABLE.test(l))) continue;
1620
+ if (markedNearby(lines, i, ALLOW_CLIPPED_VALUE)) continue;
1621
+ out.push({ line: i + 1, detail: "the value is ellipsised here and its full text is carried by nothing on this element or its wrapper" });
1622
+ }
1623
+ return out;
1624
+ }
1625
+ var CLIPPED_SIMPLE_VALUE = /<([a-z][a-z0-9]*)\b([^<>]*)>\s*\{\s*([A-Za-z_$][\w$]*(?:\??\.[\w$]+)*)\s*\}\s*<\/\1>/;
1626
+ var NOT_TEXT_BINDING = /^(children|icon|node|element|content|component)$|\.(children|icon|node|element|content|component)$/i;
1627
+ var ASYNC_ROUTE = /export\s+default\s+async\s+function\b/;
1628
+ var SERVER_DATA_AWAIT = /await\s+(prisma|db|supabase|drizzle|knex|mongoose)\b|await\s+[\w.]{1,40}\.(findMany|findUnique|findFirst|aggregate|groupBy|count|createQueryBuilder)\s*\(/;
1629
+ var STREAM_BOUNDARY = /<\s*Suspense\b/;
1630
+ var ALLOW_BLOCKING_PAGE = /enigma:allow-blocking-page/;
1631
+ var LOADING_BOUNDARY_FILE = /^loading\.(jsx?|tsx)$/;
1632
+ function pageAwaitWithoutBoundary(content, file) {
1633
+ if (/^\s*["']use client["']/m.test(content)) return [];
1634
+ if (!ASYNC_ROUTE.test(content)) return [];
1635
+ const lines = content.split("\n");
1636
+ if (lines.some((l) => !COMMENT_LINE.test(l) && STREAM_BOUNDARY.test(l))) return [];
1637
+ const out = [];
1638
+ for (let i = 0; i < lines.length; i++) {
1639
+ const line = lines[i];
1640
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line) || !SERVER_DATA_AWAIT.test(line)) continue;
1641
+ if (markedNearby(lines, i, ALLOW_BLOCKING_PAGE)) continue;
1642
+ out.push({ line: i + 1, detail: "the route awaits this query before it returns any markup, and neither a loading.tsx in the segment chain nor a <Suspense> boundary lets the shell paint first" });
1643
+ }
1644
+ if (!out.length || nearestLoadingBoundary(file)) return [];
1645
+ return out;
1646
+ }
1647
+ function nearestLoadingBoundary(file) {
1648
+ const root = findProjectRoot(file);
1649
+ let dir = dirname(resolve(file));
1650
+ for (let i = 0; i < 20; i++) {
1651
+ try {
1652
+ if (readdirSync(dir).some((name) => LOADING_BOUNDARY_FILE.test(name))) return true;
1653
+ } catch {
1654
+ return false;
1655
+ }
1656
+ if (!root || dir === root || /[\\/](app|pages|src)$/.test(dir)) return false;
1657
+ const parent = dirname(dir);
1658
+ if (parent === dir) return false;
1659
+ dir = parent;
1660
+ }
1661
+ return false;
1662
+ }
1395
1663
  var SPECIFIER = /^[ \t]*(?:import|export)\b[^;]*?\bfrom\s*["']([^"']+)["']|^[ \t]*import\s*["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']|\brequire\(\s*["']([^"']+)["']/gm;
1396
1664
  var MODULE_EXT = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
1397
1665
  var JS_EXT = /\.(js|jsx|mjs|cjs)$/i;
@@ -1617,6 +1885,49 @@ function checkPath(file, stage = "edit") {
1617
1885
  if (content.includes("\0")) return [];
1618
1886
  return checkFile(file, content, findProjectRoot(file), stage);
1619
1887
  }
1888
+ function gitProbe(cwd, args) {
1889
+ try {
1890
+ return execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
1891
+ } catch {
1892
+ return null;
1893
+ }
1894
+ }
1895
+ var DIFF_HUNK = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
1896
+ function changedLineFilter(file) {
1897
+ const cwd = dirname(file);
1898
+ const status = gitProbe(cwd, ["-c", "core.quotepath=false", "status", "--porcelain", "--untracked-files=all", "--", file]);
1899
+ if (status === null) return null;
1900
+ if (/^\?\?/m.test(status)) return () => true;
1901
+ const diff = gitProbe(cwd, ["diff", "-U0", "HEAD", "--", file]);
1902
+ if (diff === null) return null;
1903
+ const changed = /* @__PURE__ */ new Set();
1904
+ for (const line of diff.split("\n")) {
1905
+ const hunk = DIFF_HUNK.exec(line);
1906
+ if (!hunk) continue;
1907
+ const start = Number(hunk[1]);
1908
+ const count = hunk[2] === void 0 ? 1 : Number(hunk[2]);
1909
+ for (let i = 0; i < count; i++) changed.add(start + i);
1910
+ }
1911
+ return (line) => changed.has(line);
1912
+ }
1913
+ function repairableDiffFindings(file) {
1914
+ const rules = loadRules().filter((r) => r.stage === "diff");
1915
+ if (!rules.length) return [];
1916
+ const changed = changedLineFilter(file);
1917
+ if (!changed) return [];
1918
+ let content;
1919
+ try {
1920
+ content = readFileSync(file, "utf8");
1921
+ } catch {
1922
+ return [];
1923
+ }
1924
+ if (content.includes("\0")) return [];
1925
+ try {
1926
+ return checkFile(file, content, findProjectRoot(file), "diff", rules).filter((f) => f.line && changed(f.line));
1927
+ } catch {
1928
+ return [];
1929
+ }
1930
+ }
1620
1931
  function runGuardrailsHook(payload) {
1621
1932
  let file;
1622
1933
  try {
@@ -1625,12 +1936,14 @@ function runGuardrailsHook(payload) {
1625
1936
  }
1626
1937
  if (!file || typeof file !== "string") return 0;
1627
1938
  const found = checkPath(file);
1628
- if (!found.length) return 0;
1629
- const { fixed, remaining: findings } = applyFixes(file, found);
1939
+ const repairable = [...found, ...repairableDiffFindings(file)];
1940
+ if (!repairable.length) return 0;
1941
+ const { fixed } = applyFixes(file, repairable, "diff");
1630
1942
  if (fixed.length) process.stdout.write(`enigma guardrails (fixed)
1631
1943
  ${fixed.map((f) => `${f.file}:${f.line} (${f.ruleId})`).join("\n")}
1632
1944
  `);
1633
1945
  recordFindings(fixed, "fixed");
1946
+ const findings = fixed.length ? checkPath(file) : found;
1634
1947
  if (!findings.length) return 0;
1635
1948
  const warns = findings.filter((f) => f.severity === "warn");
1636
1949
  const blocks = findings.filter((f) => f.severity === "block");
@@ -1806,6 +2119,7 @@ export {
1806
2119
  loadRules,
1807
2120
  missingPathAlias,
1808
2121
  missingWindowsHide,
2122
+ pageAwaitWithoutBoundary,
1809
2123
  readLedger,
1810
2124
  recordFindings,
1811
2125
  runGuardrailsHook,
@@ -1814,5 +2128,7 @@ export {
1814
2128
  serverFirstMutation,
1815
2129
  summarizeLedger,
1816
2130
  textareaSizeBounds,
2131
+ truncatedValueUnreachable,
2132
+ viewBlankedWhileLoading,
1817
2133
  wideNamedImports
1818
2134
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.34.2",
3
+ "version": "1.35.1",
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": {