enigma-cli 1.43.0 → 1.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/assets/registry/primitives/registry.json +498 -8
  2. package/assets/registry/utils/registry.json +4 -2
  3. package/assets/skills/anti-overengineering-policy/skill.json +1 -1
  4. package/assets/skills/anti-overengineering-review/skill.json +1 -1
  5. package/assets/skills/backend-policy/skill.json +1 -1
  6. package/assets/skills/ciphera-style-policy/skill.json +1 -1
  7. package/assets/skills/code-review-policy/skill.json +1 -1
  8. package/assets/skills/core-engineering-policy/skill.json +1 -1
  9. package/assets/skills/database-expert/skill.json +1 -1
  10. package/assets/skills/debugging-policy/skill.json +1 -1
  11. package/assets/skills/dependency-policy/skill.json +1 -1
  12. package/assets/skills/email-policy/skill.json +1 -1
  13. package/assets/skills/frontend-design/skill.json +1 -1
  14. package/assets/skills/frontend-policy/SKILL.md +21 -1
  15. package/assets/skills/frontend-policy/skill.json +2 -2
  16. package/assets/skills/git-policy/skill.json +1 -1
  17. package/assets/skills/logo-sourcing-policy/skill.json +1 -1
  18. package/assets/skills/security-policy/skill.json +1 -1
  19. package/assets/skills/skill-creator/skill.json +1 -1
  20. package/assets/skills/task-completion-policy/skill.json +1 -1
  21. package/assets/skills/technical-writing-policy/skill.json +1 -1
  22. package/assets/skills/testing-policy/skill.json +1 -1
  23. package/assets/skills/validation-policy/skill.json +1 -1
  24. package/bin/checksums.json +4 -4
  25. package/bin/enigma.mjs +34 -0
  26. package/dist/codegraph-hook.js +2483 -0
  27. package/dist/guardrails.js +130 -0
  28. package/dist/post-edit.js +131 -1
  29. package/package.json +1 -1
@@ -177,6 +177,42 @@ var BUILTIN_RULES = [
177
177
  severity: "warn",
178
178
  skill: "frontend-policy"
179
179
  },
180
+ {
181
+ id: "fe-scroll-chaining",
182
+ label: "A scroll region contains its own scroll",
183
+ files: ["*.css", "*.scss", "*.less", "*.tsx", "*.jsx", "*.astro", "*.vue", "*.svelte"],
184
+ excludeFiles: [
185
+ "*.test.*",
186
+ "*.spec.*",
187
+ "*.stories.*",
188
+ "*.min.js",
189
+ "*.min.css",
190
+ "**/tests/**",
191
+ "tests/**",
192
+ "**/__tests__/**",
193
+ "__tests__/**",
194
+ "**/dist/**",
195
+ "dist/**",
196
+ "**/build/**",
197
+ "build/**",
198
+ "**/node_modules/**",
199
+ "node_modules/**",
200
+ "**/vendor/**",
201
+ "vendor/**"
202
+ ],
203
+ scope: "file",
204
+ stage: "diff",
205
+ // A VERTICAL scroll container, in CSS or in Tailwind. `overflow-x` is left alone: a wide
206
+ // table scrolling sideways chains into nothing anybody notices, and including it doubled
207
+ // the rule's noise for no defect.
208
+ pattern: "overflow(-y)?\\s*:\\s*(auto|scroll)|\\boverflow-y-(auto|scroll)\\b",
209
+ // Any mention anywhere in the file, in either spelling, and the file has clearly thought
210
+ // about it - one declaration usually covers the pane the rule is about.
211
+ absent: "overscroll-behavior|\\boverscroll-(y-)?(contain|none|auto)\\b|enigma:allow-scroll-chaining",
212
+ message: "This file makes something scroll but never mentions overscroll-behavior, so its scroll CHAINS: the wheel reaches the end of this box and keeps going into whatever is behind it. In a two-pane view - a list beside the record, a tree beside the editor, a thread beside the conversation - that is the reading pane dragging the list along with it, which is the single most-missed part of a multi-pane layout because it only shows up once content is long enough to reach an end. Add `overscroll-behavior: contain` (Tailwind `overscroll-contain`) to the scroll container, and check the other two halves while you are there: the shell is the viewport (`height: 100dvh; overflow: hidden`) and each pane carries `min-height: 0`, or the pane grows instead of scrolling and the page scrolls in its place. Deliberate - a page-level scroller, or an effect that must reach the browser - mark it `enigma:allow-scroll-chaining` (frontend-policy).",
213
+ severity: "warn",
214
+ skill: "frontend-policy"
215
+ },
180
216
  {
181
217
  id: "fe-pointer-capture-drag",
182
218
  label: "Drag bound to window, not setPointerCapture",
@@ -1063,6 +1099,60 @@ var BUILTIN_RULES = [
1063
1099
  severity: "block",
1064
1100
  skill: "frontend-policy"
1065
1101
  },
1102
+ {
1103
+ id: "fe-lightbox-hand-rolled",
1104
+ label: "Image viewers come from the primitive",
1105
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
1106
+ excludeFiles: [
1107
+ "*.test.*",
1108
+ "*.spec.*",
1109
+ "**/tests/**",
1110
+ "**/__tests__/**",
1111
+ "**/fixtures/**",
1112
+ "*.min.js",
1113
+ "**/dist/**",
1114
+ "**/build/**",
1115
+ "**/node_modules/**",
1116
+ "**/vendor/**",
1117
+ "dist/**",
1118
+ "build/**",
1119
+ "node_modules/**",
1120
+ "vendor/**"
1121
+ ],
1122
+ scope: "file",
1123
+ stage: "diff",
1124
+ fileCheck: "fe-lightbox-hand-rolled",
1125
+ message: "A lightbox written by hand: a picture in a dialog, a wheel that scales it, and a zoom kept as state. The tail is long and always the same. A wheel listener is PASSIVE by default, so every notch zooms the image and scrolls the article behind the backdrop with it. Scaling around the frame's centre slides whatever was under the pointer away from it, so zooming into a face means hunting for it again. Nothing bounds the pan, so a drag strands the picture off screen with no way back but closing. The dialog is left in the page rather than portalled, and the first ancestor with `overflow: hidden` clips it. Escape does not close it, the body still scrolls under it, and focus never returns to the image it was opened from. `enigma add image` (@enigmax/primitives) is `<Image>`: a press enlarges it and the wheel zooms around the cursor by default, with a drag to pan, a pinch on touch and the keyboard driving both - and the arrows, the strip of previews, a download menu and a discard action off until asked for. The viewer is its own chunk, fetched on intent, so a page of images nobody enlarges downloads none of it. Mark the line `enigma:allow-hand-rolled-lightbox` when the viewer genuinely has to be your own (frontend-policy).",
1126
+ severity: "block",
1127
+ skill: "frontend-policy"
1128
+ },
1129
+ {
1130
+ id: "fe-video-player-hand-rolled",
1131
+ label: "Video controls come from the primitive",
1132
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
1133
+ excludeFiles: [
1134
+ "*.test.*",
1135
+ "*.spec.*",
1136
+ "**/tests/**",
1137
+ "**/__tests__/**",
1138
+ "**/fixtures/**",
1139
+ "*.min.js",
1140
+ "**/dist/**",
1141
+ "**/build/**",
1142
+ "**/node_modules/**",
1143
+ "**/vendor/**",
1144
+ "dist/**",
1145
+ "build/**",
1146
+ "node_modules/**",
1147
+ "vendor/**"
1148
+ ],
1149
+ scope: "file",
1150
+ stage: "diff",
1151
+ fileCheck: "fe-video-player-hand-rolled",
1152
+ message: "Controls written by hand over a `<video>`. What follows is the same every time: `play()` returns a promise that REJECTS - no gesture yet, an autoplay policy, no source - and the unhandled rejection leaves the button saying pause over a video that never started; the state is kept in the component instead of read back from the element's own events, so anything else that plays or pauses it (the media keys, picture in picture, the operating system) desynchronises the bar; the scrubber is a div with a click handler rather than a slider a keyboard can drive or a screen reader can read; the buffered ranges are ignored, or read as `buffered.end(0)`, which reports a range nobody is watching; the clock changes width as it passes ten minutes and shoves every control along with it; the shortcuts fire while somebody is typing in a comment box; and fullscreen calls `requestFullscreen` on a container, which iOS Safari does not have at all. `enigma add video` (@enigmax/primitives) is `<Video>`, shaped after Plyr: the same control set and the same shortcuts, the element as the source of truth, and a theme restyled through `data-*` attributes. Mark the line `enigma:allow-hand-rolled-video-player` when the player genuinely has to be your own (frontend-policy).",
1153
+ severity: "block",
1154
+ skill: "frontend-policy"
1155
+ },
1066
1156
  {
1067
1157
  id: "sec-2fa-reauth-prompt",
1068
1158
  label: "A credential just proven is not asked for again",
@@ -1971,6 +2061,8 @@ var FILE_CHECKS = {
1971
2061
  "fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
1972
2062
  "fe-selection-hand-rolled": (content) => handRolledSelection(content),
1973
2063
  "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content),
2064
+ "fe-lightbox-hand-rolled": (content) => handRolledLightbox(content),
2065
+ "fe-video-player-hand-rolled": (content) => handRolledVideoPlayer(content),
1974
2066
  "sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
1975
2067
  "fe-select-hand-rolled": (content) => handRolledSelect(content),
1976
2068
  "fe-toast-hand-rolled": (content) => handRolledToast(content),
@@ -2280,6 +2372,42 @@ function handRolledColorPicker(content) {
2280
2372
  }
2281
2373
  return [];
2282
2374
  }
2375
+ var LIGHTBOX_IMAGE = /<img\b|<Image\b|createElement\(\s*["'`]img["'`]/;
2376
+ var LIGHTBOX_WHEEL = /onWheel|@wheel|on:wheel|addEventListener\(\s*["'`]wheel["'`]/;
2377
+ var LIGHTBOX_ZOOM = /\bset(?:Scale|Zoom)\b|\b(?:scale|zoom)\s*[:=]\s*[0-9(]|scale\(/;
2378
+ var LIGHTBOX_DIALOG = /role=["']dialog["']|aria-modal|lightbox|createPortal|["']Escape["']|backdrop/i;
2379
+ var LIGHTBOX_MITIGATED = /@enigmax\/primitives|data-enigma-image|ImageViewer\b|yet-another-react-lightbox|react-image-lightbox|photoswipe|medium-zoom|viewerjs|fancybox|lightgallery|react-medium-image-zoom|enigma:allow-hand-rolled-lightbox/i;
2380
+ function handRolledLightbox(content) {
2381
+ if (LIGHTBOX_MITIGATED.test(content)) return [];
2382
+ if (!LIGHTBOX_IMAGE.test(content) || !LIGHTBOX_WHEEL.test(content)) return [];
2383
+ if (!LIGHTBOX_ZOOM.test(content) || !LIGHTBOX_DIALOG.test(content)) return [];
2384
+ const lines = content.split("\n");
2385
+ for (let i = 0; i < lines.length; i++) {
2386
+ const line = lines[i];
2387
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2388
+ if (!LIGHTBOX_WHEEL.test(line)) continue;
2389
+ return [{ line: i + 1, detail: "a picture in a dialog, a wheel that scales it, and the zoom it keeps" }];
2390
+ }
2391
+ return [];
2392
+ }
2393
+ var VIDEO_ELEMENT = /<video\b|createElement\(\s*["'`]video["'`]|HTMLVideoElement/;
2394
+ var VIDEO_DRIVEN = /\.play\(\)|\.pause\(\)|\bplaybackRate\b|requestPictureInPicture/;
2395
+ var VIDEO_TIMELINE = /\bcurrentTime\b|timeupdate|\.buffered\b/i;
2396
+ var VIDEO_CONTROLS = /aria-label=["'](?:Play|Pause|Mute|Seek|Fullscreen)|onClick=\{[^}]*(?:play|pause)|requestFullscreen|toggleFullscreen|isPlaying|setPlaying/i;
2397
+ var VIDEO_MITIGATED = /@enigmax\/primitives|data-enigma-video|["'`]plyr|from\s+["'`]plyr|video\.js|videojs|react-player|vidstack|media-chrome|@vime|shaka-player|enigma:allow-hand-rolled-video-player/i;
2398
+ function handRolledVideoPlayer(content) {
2399
+ if (VIDEO_MITIGATED.test(content)) return [];
2400
+ if (!VIDEO_ELEMENT.test(content) || !VIDEO_DRIVEN.test(content)) return [];
2401
+ if (!VIDEO_TIMELINE.test(content) || !VIDEO_CONTROLS.test(content)) return [];
2402
+ const lines = content.split("\n");
2403
+ for (let i = 0; i < lines.length; i++) {
2404
+ const line = lines[i];
2405
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
2406
+ if (!VIDEO_DRIVEN.test(line)) continue;
2407
+ return [{ line: i + 1, detail: "a video element, its playback driven by code, and a bar of controls over it" }];
2408
+ }
2409
+ return [];
2410
+ }
2283
2411
  var TWOFA_SUBJECT = /2fa|two[-_ ]?factor|totp|mfa|authenticator|otpauth/i;
2284
2412
  var TWOFA_ENROLL = /enroll?(?!ed)|set[-_ ]?up|activate(?!d)|enable(?!d)|provision(?!ed)/i;
2285
2413
  var TWOFA_PASSWORD = /type=["']password["']|\bpassword\s*[:=]|\bpassword\b\s*[,)]|currentPassword|current_password/i;
@@ -2930,10 +3058,12 @@ export {
2930
3058
  formatFindings,
2931
3059
  handRolledColorPicker,
2932
3060
  handRolledContextMenu,
3061
+ handRolledLightbox,
2933
3062
  handRolledPalette,
2934
3063
  handRolledSelect,
2935
3064
  handRolledSelection,
2936
3065
  handRolledToast,
3066
+ handRolledVideoPlayer,
2937
3067
  loadRules,
2938
3068
  missingPathAlias,
2939
3069
  missingWindowsHide,
package/dist/post-edit.js CHANGED
@@ -569,10 +569,12 @@ __export(guardrails_exports, {
569
569
  formatFindings: () => formatFindings,
570
570
  handRolledColorPicker: () => handRolledColorPicker,
571
571
  handRolledContextMenu: () => handRolledContextMenu,
572
+ handRolledLightbox: () => handRolledLightbox,
572
573
  handRolledPalette: () => handRolledPalette,
573
574
  handRolledSelect: () => handRolledSelect,
574
575
  handRolledSelection: () => handRolledSelection,
575
576
  handRolledToast: () => handRolledToast,
577
+ handRolledVideoPlayer: () => handRolledVideoPlayer,
576
578
  loadRules: () => loadRules,
577
579
  missingPathAlias: () => missingPathAlias,
578
580
  missingWindowsHide: () => missingWindowsHide,
@@ -845,6 +847,32 @@ function handRolledColorPicker(content) {
845
847
  }
846
848
  return [];
847
849
  }
850
+ function handRolledLightbox(content) {
851
+ if (LIGHTBOX_MITIGATED.test(content)) return [];
852
+ if (!LIGHTBOX_IMAGE.test(content) || !LIGHTBOX_WHEEL.test(content)) return [];
853
+ if (!LIGHTBOX_ZOOM.test(content) || !LIGHTBOX_DIALOG.test(content)) return [];
854
+ const lines = content.split("\n");
855
+ for (let i = 0; i < lines.length; i++) {
856
+ const line = lines[i];
857
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
858
+ if (!LIGHTBOX_WHEEL.test(line)) continue;
859
+ return [{ line: i + 1, detail: "a picture in a dialog, a wheel that scales it, and the zoom it keeps" }];
860
+ }
861
+ return [];
862
+ }
863
+ function handRolledVideoPlayer(content) {
864
+ if (VIDEO_MITIGATED.test(content)) return [];
865
+ if (!VIDEO_ELEMENT.test(content) || !VIDEO_DRIVEN.test(content)) return [];
866
+ if (!VIDEO_TIMELINE.test(content) || !VIDEO_CONTROLS.test(content)) return [];
867
+ const lines = content.split("\n");
868
+ for (let i = 0; i < lines.length; i++) {
869
+ const line = lines[i];
870
+ if (COMMENT_LINE.test(line) || /enigma:/.test(line)) continue;
871
+ if (!VIDEO_DRIVEN.test(line)) continue;
872
+ return [{ line: i + 1, detail: "a video element, its playback driven by code, and a bar of controls over it" }];
873
+ }
874
+ return [];
875
+ }
848
876
  function twoFactorPasswordPrompt(content) {
849
877
  if (TWOFA_MITIGATED.test(content)) return [];
850
878
  if (!TWOFA_SUBJECT.test(content) || !TWOFA_ENROLL.test(content)) return [];
@@ -1434,7 +1462,7 @@ enigma-guardrails: BLOCKED - ${r.blocks.length} convention violation(s):`);
1434
1462
  console.log(`enigma-guardrails: ${r.count} ${all ? "tracked" : "staged"} file(s) checked, no blocking violations.`);
1435
1463
  return 0;
1436
1464
  }
1437
- var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, TWOFA_SUBJECT, TWOFA_ENROLL, TWOFA_PASSWORD, TWOFA_MITIGATED, TWOFA_WINDOW, LISTBOX_ROLE, LISTBOX_OPEN, LISTBOX_OPTIONS, LISTBOX_MITIGATED, TOAST_QUEUE, TOAST_TIMER, TOAST_MITIGATED, PALETTE_SHORTCUT, PALETTE_MODIFIER, PALETTE_OPEN, PALETTE_FILTER, PALETTE_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1465
+ var COMMENT_LINE, SKELETON_GUARD_SRC, SKELETON_SIGNAL_SRC, BUILTIN_RULES, PROJECT_CHECKS, CURRENT_PASSWORD, NEW_PASSWORD, NEW_PASSWORD_PROP, NOT_A_JSX_PROP, FILE_CHECKS, FIXERS, SPAWNERS, MUTATING_REQUEST, ENTITY_WRITE, OPTIMISTIC_SIGNAL, ALLOW_SERVER_FIRST, RESULT_BINDING, BLOCK_LOOKBACK, BLOCK_LOOKAHEAD, TEXTAREA, TEXTAREA_LOWER, TEXTAREA_UPPER, TEXTAREA_FIXED, TEXTAREA_AUTOSIZE, VIEW_GUARD, LOADING_NAME, LOADING_VALUE, VIEW_GUARD_LOADING, VIEW_CHROME_HEADING, VIEW_CHROME_TEXT, VIEW_BODY_LOOKAHEAD, ALLOW_VIEW_SKELETON, SKELETON_GUARD, SKELETON_SIGNAL, REMOTE_COLLECTION_BINDINGS, LIST_BOUNDED, ALLOW_UNBOUNDED_LIST, MENU_HANDLER, MENU_POINT, MENU_STATE, MENU_MITIGATED, SELECT_RANGE_KEY, SELECT_ADD_KEY, SELECT_STATE, SELECT_MITIGATED, COLOR_MATHS, COLOR_DRAG, COLOR_MEASURE, COLOR_STATE, COLOR_MITIGATED, LIGHTBOX_IMAGE, LIGHTBOX_WHEEL, LIGHTBOX_ZOOM, LIGHTBOX_DIALOG, LIGHTBOX_MITIGATED, VIDEO_ELEMENT, VIDEO_DRIVEN, VIDEO_TIMELINE, VIDEO_CONTROLS, VIDEO_MITIGATED, TWOFA_SUBJECT, TWOFA_ENROLL, TWOFA_PASSWORD, TWOFA_MITIGATED, TWOFA_WINDOW, LISTBOX_ROLE, LISTBOX_OPEN, LISTBOX_OPTIONS, LISTBOX_MITIGATED, TOAST_QUEUE, TOAST_TIMER, TOAST_MITIGATED, PALETTE_SHORTCUT, PALETTE_MODIFIER, PALETTE_OPEN, PALETTE_FILTER, PALETTE_MITIGATED, CLIP_ONE_LINE, DYNAMIC_CHILD, VALUE_REACHABLE, WRAPPER_LOOKBACK, ALLOW_CLIPPED_VALUE, CLIPPED_SIMPLE_VALUE, NOT_TEXT_BINDING, ASYNC_ROUTE, SERVER_DATA_AWAIT, STREAM_BOUNDARY, ALLOW_BLOCKING_PAGE, LOADING_BOUNDARY_FILE, SPECIFIER, MODULE_EXT, JS_EXT, tsconfigCache, NAMED_IMPORT, GENERIC_ACCOUNTS, INTERNAL_MODULE, globCache, DIFF_HUNK, LEDGER_MAX_BYTES, LEDGER_KEEP, LEDGER_KEEP_REPLY, grEntry, isGrEntry;
1438
1466
  var init_guardrails = __esm({
1439
1467
  "src/guardrails.ts"() {
1440
1468
  "use strict";
@@ -1609,6 +1637,42 @@ var init_guardrails = __esm({
1609
1637
  severity: "warn",
1610
1638
  skill: "frontend-policy"
1611
1639
  },
1640
+ {
1641
+ id: "fe-scroll-chaining",
1642
+ label: "A scroll region contains its own scroll",
1643
+ files: ["*.css", "*.scss", "*.less", "*.tsx", "*.jsx", "*.astro", "*.vue", "*.svelte"],
1644
+ excludeFiles: [
1645
+ "*.test.*",
1646
+ "*.spec.*",
1647
+ "*.stories.*",
1648
+ "*.min.js",
1649
+ "*.min.css",
1650
+ "**/tests/**",
1651
+ "tests/**",
1652
+ "**/__tests__/**",
1653
+ "__tests__/**",
1654
+ "**/dist/**",
1655
+ "dist/**",
1656
+ "**/build/**",
1657
+ "build/**",
1658
+ "**/node_modules/**",
1659
+ "node_modules/**",
1660
+ "**/vendor/**",
1661
+ "vendor/**"
1662
+ ],
1663
+ scope: "file",
1664
+ stage: "diff",
1665
+ // A VERTICAL scroll container, in CSS or in Tailwind. `overflow-x` is left alone: a wide
1666
+ // table scrolling sideways chains into nothing anybody notices, and including it doubled
1667
+ // the rule's noise for no defect.
1668
+ pattern: "overflow(-y)?\\s*:\\s*(auto|scroll)|\\boverflow-y-(auto|scroll)\\b",
1669
+ // Any mention anywhere in the file, in either spelling, and the file has clearly thought
1670
+ // about it - one declaration usually covers the pane the rule is about.
1671
+ absent: "overscroll-behavior|\\boverscroll-(y-)?(contain|none|auto)\\b|enigma:allow-scroll-chaining",
1672
+ message: "This file makes something scroll but never mentions overscroll-behavior, so its scroll CHAINS: the wheel reaches the end of this box and keeps going into whatever is behind it. In a two-pane view - a list beside the record, a tree beside the editor, a thread beside the conversation - that is the reading pane dragging the list along with it, which is the single most-missed part of a multi-pane layout because it only shows up once content is long enough to reach an end. Add `overscroll-behavior: contain` (Tailwind `overscroll-contain`) to the scroll container, and check the other two halves while you are there: the shell is the viewport (`height: 100dvh; overflow: hidden`) and each pane carries `min-height: 0`, or the pane grows instead of scrolling and the page scrolls in its place. Deliberate - a page-level scroller, or an effect that must reach the browser - mark it `enigma:allow-scroll-chaining` (frontend-policy).",
1673
+ severity: "warn",
1674
+ skill: "frontend-policy"
1675
+ },
1612
1676
  {
1613
1677
  id: "fe-pointer-capture-drag",
1614
1678
  label: "Drag bound to window, not setPointerCapture",
@@ -2495,6 +2559,60 @@ var init_guardrails = __esm({
2495
2559
  severity: "block",
2496
2560
  skill: "frontend-policy"
2497
2561
  },
2562
+ {
2563
+ id: "fe-lightbox-hand-rolled",
2564
+ label: "Image viewers come from the primitive",
2565
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
2566
+ excludeFiles: [
2567
+ "*.test.*",
2568
+ "*.spec.*",
2569
+ "**/tests/**",
2570
+ "**/__tests__/**",
2571
+ "**/fixtures/**",
2572
+ "*.min.js",
2573
+ "**/dist/**",
2574
+ "**/build/**",
2575
+ "**/node_modules/**",
2576
+ "**/vendor/**",
2577
+ "dist/**",
2578
+ "build/**",
2579
+ "node_modules/**",
2580
+ "vendor/**"
2581
+ ],
2582
+ scope: "file",
2583
+ stage: "diff",
2584
+ fileCheck: "fe-lightbox-hand-rolled",
2585
+ message: "A lightbox written by hand: a picture in a dialog, a wheel that scales it, and a zoom kept as state. The tail is long and always the same. A wheel listener is PASSIVE by default, so every notch zooms the image and scrolls the article behind the backdrop with it. Scaling around the frame's centre slides whatever was under the pointer away from it, so zooming into a face means hunting for it again. Nothing bounds the pan, so a drag strands the picture off screen with no way back but closing. The dialog is left in the page rather than portalled, and the first ancestor with `overflow: hidden` clips it. Escape does not close it, the body still scrolls under it, and focus never returns to the image it was opened from. `enigma add image` (@enigmax/primitives) is `<Image>`: a press enlarges it and the wheel zooms around the cursor by default, with a drag to pan, a pinch on touch and the keyboard driving both - and the arrows, the strip of previews, a download menu and a discard action off until asked for. The viewer is its own chunk, fetched on intent, so a page of images nobody enlarges downloads none of it. Mark the line `enigma:allow-hand-rolled-lightbox` when the viewer genuinely has to be your own (frontend-policy).",
2586
+ severity: "block",
2587
+ skill: "frontend-policy"
2588
+ },
2589
+ {
2590
+ id: "fe-video-player-hand-rolled",
2591
+ label: "Video controls come from the primitive",
2592
+ files: ["*.tsx", "*.jsx", "*.vue", "*.svelte", "*.astro", "*.ts", "*.js"],
2593
+ excludeFiles: [
2594
+ "*.test.*",
2595
+ "*.spec.*",
2596
+ "**/tests/**",
2597
+ "**/__tests__/**",
2598
+ "**/fixtures/**",
2599
+ "*.min.js",
2600
+ "**/dist/**",
2601
+ "**/build/**",
2602
+ "**/node_modules/**",
2603
+ "**/vendor/**",
2604
+ "dist/**",
2605
+ "build/**",
2606
+ "node_modules/**",
2607
+ "vendor/**"
2608
+ ],
2609
+ scope: "file",
2610
+ stage: "diff",
2611
+ fileCheck: "fe-video-player-hand-rolled",
2612
+ message: "Controls written by hand over a `<video>`. What follows is the same every time: `play()` returns a promise that REJECTS - no gesture yet, an autoplay policy, no source - and the unhandled rejection leaves the button saying pause over a video that never started; the state is kept in the component instead of read back from the element's own events, so anything else that plays or pauses it (the media keys, picture in picture, the operating system) desynchronises the bar; the scrubber is a div with a click handler rather than a slider a keyboard can drive or a screen reader can read; the buffered ranges are ignored, or read as `buffered.end(0)`, which reports a range nobody is watching; the clock changes width as it passes ten minutes and shoves every control along with it; the shortcuts fire while somebody is typing in a comment box; and fullscreen calls `requestFullscreen` on a container, which iOS Safari does not have at all. `enigma add video` (@enigmax/primitives) is `<Video>`, shaped after Plyr: the same control set and the same shortcuts, the element as the source of truth, and a theme restyled through `data-*` attributes. Mark the line `enigma:allow-hand-rolled-video-player` when the player genuinely has to be your own (frontend-policy).",
2613
+ severity: "block",
2614
+ skill: "frontend-policy"
2615
+ },
2498
2616
  {
2499
2617
  id: "sec-2fa-reauth-prompt",
2500
2618
  label: "A credential just proven is not asked for again",
@@ -3391,6 +3509,8 @@ var init_guardrails = __esm({
3391
3509
  "fe-context-menu-hand-rolled": (content) => handRolledContextMenu(content),
3392
3510
  "fe-selection-hand-rolled": (content) => handRolledSelection(content),
3393
3511
  "fe-color-picker-hand-rolled": (content) => handRolledColorPicker(content),
3512
+ "fe-lightbox-hand-rolled": (content) => handRolledLightbox(content),
3513
+ "fe-video-player-hand-rolled": (content) => handRolledVideoPlayer(content),
3394
3514
  "sec-2fa-reauth-prompt": (content) => twoFactorPasswordPrompt(content),
3395
3515
  "fe-select-hand-rolled": (content) => handRolledSelect(content),
3396
3516
  "fe-toast-hand-rolled": (content) => handRolledToast(content),
@@ -3467,6 +3587,16 @@ var init_guardrails = __esm({
3467
3587
  COLOR_MEASURE = /getBoundingClientRect/;
3468
3588
  COLOR_STATE = /\bset(?:Color|Colour|Hue|Saturation|Lightness|Brightness|Alpha|Opacity)\b|\b(?:hue|saturation)\s*[:=]/i;
3469
3589
  COLOR_MITIGATED = /@enigmax\/primitives|type=["']color["']|react-colorful|react-color|@uiw\/react-color|vue-color|ngx-color|@radix-ui|enigma:allow-hand-rolled-color-picker/;
3590
+ LIGHTBOX_IMAGE = /<img\b|<Image\b|createElement\(\s*["'`]img["'`]/;
3591
+ LIGHTBOX_WHEEL = /onWheel|@wheel|on:wheel|addEventListener\(\s*["'`]wheel["'`]/;
3592
+ LIGHTBOX_ZOOM = /\bset(?:Scale|Zoom)\b|\b(?:scale|zoom)\s*[:=]\s*[0-9(]|scale\(/;
3593
+ LIGHTBOX_DIALOG = /role=["']dialog["']|aria-modal|lightbox|createPortal|["']Escape["']|backdrop/i;
3594
+ LIGHTBOX_MITIGATED = /@enigmax\/primitives|data-enigma-image|ImageViewer\b|yet-another-react-lightbox|react-image-lightbox|photoswipe|medium-zoom|viewerjs|fancybox|lightgallery|react-medium-image-zoom|enigma:allow-hand-rolled-lightbox/i;
3595
+ VIDEO_ELEMENT = /<video\b|createElement\(\s*["'`]video["'`]|HTMLVideoElement/;
3596
+ VIDEO_DRIVEN = /\.play\(\)|\.pause\(\)|\bplaybackRate\b|requestPictureInPicture/;
3597
+ VIDEO_TIMELINE = /\bcurrentTime\b|timeupdate|\.buffered\b/i;
3598
+ VIDEO_CONTROLS = /aria-label=["'](?:Play|Pause|Mute|Seek|Fullscreen)|onClick=\{[^}]*(?:play|pause)|requestFullscreen|toggleFullscreen|isPlaying|setPlaying/i;
3599
+ VIDEO_MITIGATED = /@enigmax\/primitives|data-enigma-video|["'`]plyr|from\s+["'`]plyr|video\.js|videojs|react-player|vidstack|media-chrome|@vime|shaka-player|enigma:allow-hand-rolled-video-player/i;
3470
3600
  TWOFA_SUBJECT = /2fa|two[-_ ]?factor|totp|mfa|authenticator|otpauth/i;
3471
3601
  TWOFA_ENROLL = /enroll?(?!ed)|set[-_ ]?up|activate(?!d)|enable(?!d)|provision(?!ed)/i;
3472
3602
  TWOFA_PASSWORD = /type=["']password["']|\bpassword\s*[:=]|\bpassword\b\s*[,)]|currentPassword|current_password/i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.43.0",
3
+ "version": "1.45.0",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {