@nitra/cursor 3.22.0 → 3.23.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 (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,241 @@
1
+ # avif_generation.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль реалізує перевірку правила `image-avif.mdc`: генерацію AVIF-двійників растрових зображень і ув'язування цих двійників із посиланнями у `.vue`- та `.html`-файлах монорепо. Експортує функцію `check`, яку викликає CLI `n-cursor` для команди `check image-avif` / `fix image-avif`.
6
+
7
+ Загальний сценарій роботи `check`:
8
+
9
+ 1. **Pre-scan** — шукає у `.vue`/`.html` хоча б одне raster-посилання (через `import x from '...png'` або `<img src="...png" />`), яке потенційно треба переписати на AVIF-двійник. Пакети з opt-out `"@nitra/minify-image": { "disable-avif": true }` у `package.json` пропускаються. Якщо жодного raster-посилання не знайдено — модуль одразу повертає успіх і не запускає ні `npx`, ні rewrite, ні cleanup-пасс.
10
+ 2. **AVIF-генерація** — викликає `npx @nitra/minify-image --src=. --write --avif`, який створює AVIF-двійники поряд з оригіналами.
11
+ 3. **Rewrite-пасс** — для кожного workspace-пакета (без opt-out) переписує raster-посилання у `.vue`/`.html` на `<...>.avif`, якщо двійник реально існує на диску. Якщо двійника немає (наприклад, оригіналу теж нема) — фейлить конкретне посилання.
12
+ 4. **Cleanup-пасс** — видаляє AVIF-сироти (`<...>.avif`, на які не лишилось жодного посилання у `.vue`/`.html` репозиторію), реалізуючи умову «AVIF лишається лише там, де заміна вдалася».
13
+
14
+ Модуль свідомо не дублює перевірки скрипта `lint-image` (заборона `--avif` у ньому) — це залишається у правилі `image-compress`. Правило `image-avif` самостійне й вмикається лише там, де AVIF підтримується (адмінки), а не у публічних сайтах.
15
+
16
+ ## Експорти / API
17
+
18
+ | Експорт | Тип | Призначення |
19
+ | ------------- | ---------------- | ----------------------------------------------------------------------------------- |
20
+ | `check(cwd?)` | `async function` | Точка входу перевірки `image-avif`; повертає exit-код (`0` — OK, `1` — є проблеми). |
21
+
22
+ Інші функції у модулі (`packageHasAvifDisabled`, `resolveImageCandidates`, `checkVueAvifImportsInPackage`, `checkVueAvifImports`, `hasAnyVueRasterReference`, `runAvifGeneration`, `cleanupOrphanAvifs`) — внутрішні, не експортуються.
23
+
24
+ ## Константи
25
+
26
+ | Константа | Значення / форма | Призначення |
27
+ | -------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
28
+ | `MINIFY_PACKAGE_NAME` | `'@nitra/minify-image'` | Імʼя CLI-пакета, який генерує AVIF (викликається через `npx`). |
29
+ | `PKG_CONFIG_FIELD` | `'@nitra/minify-image'` | Поле у `package.json` для конфігу `@nitra/minify-image` (`disable-avif: true` тощо). |
30
+ | `CLEANUP_EXTRA_IGNORE_DIR_NAMES` | `Set` з `'build'`, `'android'`, `'ios'`, `'.output'`, `'.nuxt'`, `'.cache'` | Імена каталогів, які cleanup-пасс ігнорує додатково до стандартних (`node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`, які вже скіпає `walkDir`). Це артефакти збірки / нативних платформ — AVIF всередині є продуктом попереднього `bun run build` / Capacitor sync. |
31
+ | `VUE_RASTER_IMPORT_RE` | `/import\s+\w[\w$]\*\s+from\s+['"]([^'"\n]+\.(?:png | jpe?g | gif))['"]/giu` | Регексп для `import name from '...ext'` у `.vue`/`.html`; група 1 — повний шлях до зображення. |
32
+ | `VUE_RASTER_STATIC_SRC_RE` | `/(?<![:-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png | jpe?g | gif))['"]/giu` | Регексп для статичних `<img src="...png" />` у шаблоні `.vue`. Lookbehind `(?<![:\-_.])` виключає `:src="..."` (реактивний JS-вираз), `data-src="..."` і `obj.src=...`. |
33
+ | `VUE_AVIF_REF_RE` | `/['"]([^'"\s]+\.(?:png | jpe?g | gif)\.avif)['"]/giu` | Регексп для готових AVIF-посилань у `.vue`/`.html`. Використовується тільки для збору множини «живих» AVIF — щоб після rewrite знати, які `<...>.avif` ще на щось посилаються. |
34
+
35
+ ## Типи
36
+
37
+ ### `RewriteStats`
38
+
39
+ ```text
40
+ {
41
+ rewrittenRefs: number // скільки конкретних посилань переписано на .avif
42
+ rewrittenFiles: number // у скількох .vue/.html файлах хоч одне посилання змінилося
43
+ failedRefs: number // скільки конкретних посилань не вдалося переписати (.avif не існував)
44
+ }
45
+ ```
46
+
47
+ Аґреговані лічильники по проходу `check image-avif`, мутуються `checkVueAvifImportsInPackage`, потрапляють у фінальний `pass`-меседж.
48
+
49
+ ## Функції
50
+
51
+ ### `packageHasAvifDisabled(pkg)`
52
+
53
+ - **Сигнатура:** `(pkg: Record<string, unknown>) => boolean`
54
+ - **Параметри:**
55
+ - `pkg` — розібраний обʼєкт `package.json` пакета.
56
+ - **Повертає:** `true`, якщо у `package.json` встановлено `"@nitra/minify-image": { "disable-avif": true }`, інакше `false`.
57
+ - **Side effects:** немає.
58
+
59
+ ### `resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs)`
60
+
61
+ - **Сигнатура:** `(importPath: string, sourceAbsPath: string, packageRootAbs: string | null) => string[]`
62
+ - **Параметри:**
63
+ - `importPath` — шлях з `import x from '...'` або `src="..."`.
64
+ - `sourceAbsPath` — абсолютний шлях файла-джерела (`.vue`/`.html`).
65
+ - `packageRootAbs` — абсолютний корінь workspace-пакета, у якому лежить джерело (для резолвера `/path` як `<root>/public<path>`); `null`, якщо невідомо.
66
+ - **Повертає:** впорядкований список абсолютних шляхів-кандидатів, по яких caller перевіряє існування `<candidate>` або `<candidate>.avif`.
67
+ - **Правила резолва:**
68
+ - `./x.png`, `../x.png` — відносно файла-джерела.
69
+ - `/x.png` (Vite/Quasar-конвенція) — спочатку `<packageRoot>/public/x.png`, потім `<packageRoot>/x.png`, нарешті `<cwd>/x.png` як legacy fallback.
70
+ - голий шлях з принаймні одним `/` (наприклад `assets/img.png`) — relative-to-source, плюс `<packageRoot>/public/<path>` як другий кандидат.
71
+ - bare-шлях без `/` (наприклад `foo`) — ймовірно alias-resolver Vite/Webpack; повертається порожній список → caller просто пропускає посилання, не звітує fail.
72
+ - **Side effects:** немає; шляхи будуються через `path.join`, файли не зачіпаються.
73
+
74
+ ### `checkVueAvifImportsInPackage(packageRoot, otherRootsAbs, ignorePaths, usedAvifAbs, stats, fail, cwd)`
75
+
76
+ - **Сигнатура:** `async (packageRoot: string, otherRootsAbs: string[], ignorePaths: string[], usedAvifAbs: Set<string>, stats: RewriteStats, fail: (msg: string) => void, cwd: string) => Promise<void>`
77
+ - **Параметри:**
78
+ - `packageRoot` — відносний шлях до кореня workspace-пакета (`'.'` або `'demo'`, тощо).
79
+ - `otherRootsAbs` — абсолютні шляхи інших workspace-коренів; їхні піддерева пропускаються, щоб не сканувати один файл двічі.
80
+ - `ignorePaths` — абсолютні шляхи каталогів, повністю виключених з обходу (зі `.cursorignore` тощо).
81
+ - `usedAvifAbs` — мутабельна множина абсолютних шляхів `.avif`, що мають хоч одне посилання у `.vue`/`.html`; функція доповнює її.
82
+ - `stats` — глобальні лічильники `RewriteStats`, мутуються тут.
83
+ - `fail` — callback для звіту про помилку.
84
+ - `cwd` — корінь репозиторію.
85
+ - **Повертає:** `Promise<void>`, який резолвиться по завершенню обробки пакета.
86
+ - **Side effects:**
87
+ - Читає всі `.vue`/`.html` файли пакета через `walkDir`.
88
+ - Перезаписує файли, у яких хоч одне посилання вдалось переписати (write-then-fail: запис відбувається ОДРАЗУ після обробки одного файла; провал на наступному файлі не відкочує вже записані зміни попередніх).
89
+ - Мутує `usedAvifAbs` і `stats`.
90
+ - Викликає `fail(msg)` для кожного raster-посилання, для якого AVIF-двійника немає на диску.
91
+ - **Логіка:**
92
+ - Збирає `targetFiles` — лише `.vue`/`.html` у `absRoot`, що не належать іншим workspace-кореням.
93
+ - Для кожного файла застосовує `processMatches` із двома регекспами: `VUE_RASTER_IMPORT_RE` і `VUE_RASTER_STATIC_SRC_RE`. У `replaceAll` для кожного матчу резолвить кандидатів через `resolveImageCandidates`; якщо `existsSync(c + '.avif')` знаходить двійник — переписує посилання на `<importPath>.avif`, інкрементує `rewrittenRefs`, додає до `usedAvifAbs`; якщо ні — інкрементує `failedRefs` і викликає `fail`. Bare-alias (порожній список кандидатів) — пропускається без fail.
94
+ - Окремо проходить `VUE_AVIF_REF_RE` по оновленому контенту й додає до `usedAvifAbs` усі AVIF-кандидати, які існують на диску (це треба, щоб cleanup-пасс не видалив AVIF, на який є посилання поза rewrite-патернами).
95
+ - Якщо контент змінився — записує файл і інкрементує `rewrittenFiles`.
96
+
97
+ ### `checkVueAvifImports(ignorePaths, usedAvifAbs, stats, pass, fail, cwd)`
98
+
99
+ - **Сигнатура:** `async (ignorePaths: string[], usedAvifAbs: Set<string>, stats: RewriteStats, pass: (msg: string) => void, fail: (msg: string) => void, cwd: string) => Promise<string[]>`
100
+ - **Параметри:**
101
+ - `ignorePaths` — абсолютні шляхи каталогів, повністю виключених з обходу.
102
+ - `usedAvifAbs` — мутабельна множина абсолютних шляхів `.avif`, що мають живі посилання (заповнюється у викликаних функціях).
103
+ - `stats` — глобальні лічильники `RewriteStats`, мутуються нижче.
104
+ - `pass` — callback при успішній перевірці пакета.
105
+ - `fail` — callback при помилці.
106
+ - `cwd` — корінь репозиторію.
107
+ - **Повертає:** `Promise<string[]>` — абсолютні шляхи коренів пакетів з активним opt-out (`disable-avif: true`).
108
+ - **Side effects:**
109
+ - Читає кожен `package.json` workspace-пакета.
110
+ - Для пакетів з opt-out — викликає `pass` з повідомленням про вимикач і додає корінь до `optedOutAbs`.
111
+ - Для решти — викликає `checkVueAvifImportsInPackage`, який може писати у файли.
112
+ - **Призначення `optedOutAbs`:** AVIF всередині opt-out пакета НЕ можна вважати сиротою лише на підставі відсутності посилань у його `.vue`/`.html` (ми взагалі не сканували його шаблони) — інакше cleanup помилково затирав би AVIF, що використовуються через alias / runtime-обчислений шлях / зовнішні посилання.
113
+
114
+ ### `hasAnyVueRasterReference(ignorePaths, cwd)`
115
+
116
+ - **Сигнатура:** `async (ignorePaths: string[], cwd: string) => Promise<boolean>`
117
+ - **Параметри:**
118
+ - `ignorePaths` — абсолютні шляхи каталогів, виключених з обходу.
119
+ - `cwd` — корінь репозиторію.
120
+ - **Повертає:** `true`, якщо у `.vue`/`.html` пакетів без opt-out знайдено принаймні одне raster-посилання (`VUE_RASTER_IMPORT_RE` або `VUE_RASTER_STATIC_SRC_RE`); `false` — інакше.
121
+ - **Side effects:** немає (тільки читання файлів).
122
+ - **Призначення:** дешевий pre-scan, що дозволяє пропустити дорогий `npx @nitra/minify-image --avif` і rewrite/cleanup у проєктах, де AVIF не вживається.
123
+ - **Нюанс:** перед кожним `test`/`replaceAll` функція скидає `lastIndex` регекспа на `0`, оскільки регекспи з прапором `g` тримають стан між викликами.
124
+
125
+ ### `runAvifGeneration(cwd)`
126
+
127
+ - **Сигнатура:** `(cwd: string) => void`
128
+ - **Параметри:**
129
+ - `cwd` — корінь репозиторію, у якому запускається `npx`.
130
+ - **Повертає:** `void`.
131
+ - **Side effects:**
132
+ - Викликає `spawnSync(npxPath, ['@nitra/minify-image', '--src=.', '--write', '--avif'], { stdio: 'inherit', cwd, env })`, який генерує AVIF-двійники.
133
+ - Логує попередження (`console.log`) при відсутності `npx` у PATH, помилці спавна або ненульовому коді виходу — без падіння перевірки.
134
+ - **Best-effort семантика:** якщо мережа/кеш недоступні чи бінарника нема — лог-варн без винятку; перевірка vue/html все одно виявить файли, для яких не вистачає `.avif`.
135
+ - **Опт-аут запуску:** якщо `process.env.NITRA_CURSOR_NO_AVIF_RUN === '1'` — функція no-op (потрібно для тестів та ізольованих середовищ).
136
+ - **Resolver `npx`:** `resolveCmd('npx')` повертає повний шлях; якщо `null` — функція друкує попередження і виходить.
137
+
138
+ ### `cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths, cwd)`
139
+
140
+ - **Сигнатура:** `async (usedAvifAbs: Set<string>, optedOutAbs: string[], ignorePaths: string[], cwd: string) => Promise<number>`
141
+ - **Параметри:**
142
+ - `usedAvifAbs` — абсолютні шляхи `.avif`, що мають живі посилання (їх не чіпаємо).
143
+ - `optedOutAbs` — абсолютні шляхи коренів opt-out пакетів; AVIF під ними не вважаємо сиротами.
144
+ - `ignorePaths` — абсолютні шляхи каталогів, виключених з обходу.
145
+ - `cwd` — корінь репозиторію.
146
+ - **Повертає:** `Promise<number>` — кількість видалених сиріт.
147
+ - **Side effects:** видаляє `.avif` файли через `unlink`.
148
+ - **Фільтр кандидатів:**
149
+ - файл закінчується на `.avif`;
150
+ - не присутній у `usedAvifAbs`;
151
+ - не лежить під жодним з `optedOutAbs`;
152
+ - жоден сегмент шляху не входить у `CLEANUP_EXTRA_IGNORE_DIR_NAMES` (`build`, `android`, `ios`, `.output`, `.nuxt`, `.cache`).
153
+ - **Ідемпотентність:** opt-out гарантує, що повторний `check image-avif` не починає циклічно видаляти AVIF в пакетах, що вимкнули правило (наприклад, мобільний бандл).
154
+
155
+ ### `check(cwd = process.cwd())` — експортована точка входу
156
+
157
+ - **Сигнатура:** `async (cwd?: string) => Promise<number>`
158
+ - **Параметри:**
159
+ - `cwd` — корінь репозиторію; за замовчуванням `process.cwd()`.
160
+ - **Повертає:** `Promise<number>` — exit-код (`0` — OK, `1` — є проблеми), отриманий з `reporter.getExitCode()`.
161
+ - **Side effects:**
162
+ - Створює `createCheckReporter()` для агрегації pass/fail-меседжів.
163
+ - Завантажує `ignorePaths` через `loadCursorIgnorePaths(cwd)`.
164
+ - Якщо `hasAnyVueRasterReference` повернула `false` — викликає `pass(...)` з відповідним повідомленням і одразу повертає exit-код (без AVIF-генерації, rewrite, cleanup).
165
+ - Інакше: викликає `runAvifGeneration(cwd)`, потім `checkVueAvifImports(...)` (rewrite + збір usedAvifAbs + список optedOutAbs), потім `cleanupOrphanAvifs(...)`, фіксує підсумкове `pass(...)` з кількістю переписаних посилань, файлів, видалених сиріт і фейлів. Фейли всередині rewrite-пасу йдуть через `fail(...)` і впливають на exit-код.
166
+
167
+ ## Залежності
168
+
169
+ ### Node.js builtins
170
+
171
+ - `node:fs` — `existsSync` (синхронна перевірка наявності файла, бо викликається у тісному `replaceAll`-циклі).
172
+ - `node:fs/promises` — `readFile`, `writeFile`, `unlink`.
173
+ - `node:path` — `join`, `relative`.
174
+ - `node:child_process` — `spawnSync` для запуску `npx @nitra/minify-image`.
175
+ - `node:process` — `env` (для опт-ауту `NITRA_CURSOR_NO_AVIF_RUN=1`); також глобально використовується `process.cwd()` у `resolveImageCandidates` і дефолтному параметрі `check`.
176
+
177
+ ### Внутрішні модулі (n-cursor)
178
+
179
+ - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — фабрика обʼєкта зі `pass`/`fail`/`getExitCode`.
180
+ - `../../../scripts/lib/load-cursor-config.mjs` → `loadCursorIgnorePaths` — завантажує абсолютні шляхи каталогів, які треба ігнорувати при обході.
181
+ - `../../../scripts/utils/resolve-cmd.mjs` → `resolveCmd` — резолвить абсолютний шлях до CLI-бінарника у `PATH`.
182
+ - `../../../scripts/utils/walkDir.mjs` → `walkDir` — рекурсивний обхід директорії з вбудованим скіпом `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next` та користувацьким callback.
183
+ - `../../../scripts/lib/workspaces.mjs` → `getMonorepoPackageRootDirs` — повертає відносні шляхи коренів workspace-пакетів монорепо.
184
+
185
+ ### Зовнішні CLI
186
+
187
+ - `npx` — резолвиться через `resolveCmd`; запускає `@nitra/minify-image` через `npx`.
188
+ - `@nitra/minify-image` — окремий npm-пакет, який і генерує AVIF-двійники (`--src=. --write --avif`).
189
+
190
+ ## Потік виконання / Використання
191
+
192
+ ### Виклик з CLI
193
+
194
+ Модуль викликається з реєстру правил `n-cursor` як check-функція правила `image-avif`. Очікувана крапка входу:
195
+
196
+ ```js
197
+ import { check } from './npm/rules/image-avif/js/avif_generation.mjs'
198
+ const exitCode = await check(process.cwd())
199
+ process.exit(exitCode)
200
+ ```
201
+
202
+ ### Послідовність кроків у `check(cwd)`
203
+
204
+ 1. Створюється reporter (`createCheckReporter`).
205
+ 2. Завантажується `ignorePaths` через `loadCursorIgnorePaths(cwd)`.
206
+ 3. **Pre-scan**: `hasAnyVueRasterReference(ignorePaths, cwd)` обходить пакети без opt-out, шукає raster-посилання. Якщо нема — `pass(...)` і ранній вихід.
207
+ 4. **AVIF-генерація**: `runAvifGeneration(cwd)` — викликає `npx @nitra/minify-image --src=. --write --avif`, який створює AVIF-двійники. Опціонально вимикається через `NITRA_CURSOR_NO_AVIF_RUN=1`.
208
+ 5. **Rewrite-пасс**: `checkVueAvifImports(...)` обходить кожен workspace-пакет:
209
+ - Якщо `package.json` має `disable-avif: true` — `pass(...)` і додає корінь у `optedOutAbs`.
210
+ - Інакше — `checkVueAvifImportsInPackage(...)` обробляє кожен `.vue`/`.html` у пакеті: переписує raster-посилання на `.avif` (якщо двійник існує), фейлить ті, для яких двійника нема, збирає `usedAvifAbs`.
211
+ 6. **Cleanup-пасс**: `cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths, cwd)` видаляє `.avif`, на які не лишилось живих посилань, з врахуванням opt-out і списку артефактів збірки.
212
+ 7. Фінальний `pass(...)` з підсумком: скільки посилань переписано, у скількох файлах, скільки сиріт видалено, скільки фейлів rewrite.
213
+ 8. Повертається `reporter.getExitCode()` — `1`, якщо був хоч один `fail`, інакше `0`.
214
+
215
+ ### Опт-аут на рівні пакета
216
+
217
+ Щоб вимкнути AVIF-перевірку у конкретному workspace-пакеті, у його `package.json` додається:
218
+
219
+ ```json
220
+ {
221
+ "@nitra/minify-image": {
222
+ "disable-avif": true
223
+ }
224
+ }
225
+ ```
226
+
227
+ Наслідки:
228
+
229
+ - pre-scan ігнорує цей пакет (його raster-посилання не провокують запуск `npx --avif`);
230
+ - rewrite-пасс не сканує і не змінює його `.vue`/`.html`;
231
+ - cleanup-пасс не видаляє `.avif` під його коренем (бо ми не зібрали `usedAvifAbs` для нього).
232
+
233
+ ### Опт-аут запуску `npx`
234
+
235
+ Змінна середовища `NITRA_CURSOR_NO_AVIF_RUN=1` повністю вимикає виклик `npx @nitra/minify-image --avif`. Pre-scan, rewrite і cleanup при цьому працюють як зазвичай — потрібно для юніт-тестів і ізольованих CI-середовищ.
236
+
237
+ ### Семантика помилок
238
+
239
+ - **Бракує `.avif`-двійника** — fail на конкретний `.vue`/`.html`-файл і конкретний `importPath` з підказкою про `npx @nitra/cursor fix image-avif` та локальний opt-out.
240
+ - **`npx` недоступний / падає** — лише warn у `console.log`, без переривання перевірки; fail прийде пізніше від rewrite-пасу, якщо `.avif` так і не з'явилися.
241
+ - **Bare alias** (`'foo'` без `/`) — резолвера нема, посилання пропускається без fail.
@@ -0,0 +1,150 @@
1
+ # fix.mjs — entry-point правила `image-compress`
2
+
3
+ ## Огляд
4
+
5
+ Файл `npm/rules/image-compress/fix.mjs` — це уніфікована точка входу (entry-point) для правила `image-compress` із набору правил `@nitra/cursor`. Він виконує дві ролі одночасно:
6
+
7
+ 1. **Library mode** — експортує функцію `run(ctx)`, яку викликає CLI-оркестратор `@nitra/cursor` (через динамічний `import` модуля та виклик `run(ctx)`) разом з іншими правилами в межах одного загального прогону (з конфіг-завантаженням, whitelist та підсумковим звітом).
8
+ 2. **Standalone mode** — якщо файл запущено напряму через `bun rules/image-compress/fix.mjs` (тобто є точкою входу процесу), він самостійно піднімає повний CLI-оркестратор для цього єдиного правила і завершує процес кодом виходу, придатним для CI/IDE.
9
+
10
+ Уся логіка правила (виявлення `applies`, перевірка JS-concerns, policy, валідація mdc-refs) делегується у `runStandardRule` зі спільної бібліотеки `scripts/lib/run-standard-rule.mjs`. Тобто сам `fix.mjs` правила `image-compress` не містить власної бізнес-логіки — він лише прив’язує спільний “стандартний” пайплайн правил до конкретної директорії цього правила через `import.meta.dirname`.
11
+
12
+ Правило `image-compress` належить до родини стандартних правил `@nitra/cursor`, де кожне правило живе у власній теці `npm/rules/<id>/` і має однаковий каркас: `fix.mjs` (цей файл), `check-*.mjs` (детектори порушень), `*.mdc` (людинозрозумілий опис правила) та інші артефакти, які `runStandardRule` автоматично знаходить за домовленостями про шляхи.
13
+
14
+ ## Експорти / API
15
+
16
+ Модуль має один іменований експорт.
17
+
18
+ ### `run(ctx?) → Promise<number>`
19
+
20
+ Function, що повертає `Promise<number>` — exit-code прогону правила:
21
+
22
+ - `0` — правило відпрацювало без порушень (OK).
23
+ - `1` — знайдено порушення політики/перевірок правила.
24
+
25
+ Параметри:
26
+
27
+ - `ctx` (необов’язковий) — об’єкт `RuleContext` з `scripts/lib/run-standard-rule.mjs`. Використовується для спільного стану між правилами в межах одного прогону, зокрема для кешу обходу файлової системи (`walkCache`) та інших оркестраційних метаданих. Якщо не передано — `runStandardRule` сам ініціалізує внутрішній контекст.
28
+
29
+ Призначення: викликається CLI-оркестратором `@nitra/cursor` як library API. Зовнішній код виконує `import { run } from '<абсолютний шлях>/npm/rules/image-compress/fix.mjs'` і потім `await run(ctx)`. Це дозволяє запускати багато правил в одному процесі з єдиним конфіг-завантаженням і єдиним звітом.
30
+
31
+ Модуль не має `export default`, не експортує жодних інших символів, констант чи типів.
32
+
33
+ ## Функції
34
+
35
+ ### `run(ctx)`
36
+
37
+ **Сигнатура:**
38
+
39
+ ```js
40
+ export function run(ctx)
41
+ ```
42
+
43
+ **JSDoc-тип (як у вихідному файлі):**
44
+
45
+ - `@param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext} [ctx]` — контекст прогону (наприклад, `walkCache`); параметр опціональний.
46
+ - `@returns {Promise<number>}` — `0` означає OK, `1` — порушення.
47
+
48
+ **Параметри:**
49
+
50
+ - `ctx?: RuleContext` — необов’язковий контекст із зовнішнього оркестратора; пробрасується далі без модифікації.
51
+
52
+ **Що робить:**
53
+
54
+ 1. Звертається до `import.meta.dirname` — це абсолютний шлях до директорії, у якій лежить сам `fix.mjs` (тобто до `npm/rules/image-compress/`).
55
+ 2. Викликає `runStandardRule(import.meta.dirname, ctx)` — стандартну реалізацію пайплайну правила: applies → JS-concerns → policy → mdc-refs.
56
+ 3. Повертає `Promise<number>`, який вирішить exit-code від `runStandardRule`.
57
+
58
+ **Повертає:** `Promise<number>` — exit-code пайплайна (`0` або `1`).
59
+
60
+ **Side effects:** прямих сайд-ефектів у самій функції немає; усі сайд-ефекти (читання файлів проєкту, виведення в `stdout`/`stderr`, формування підсумків звіту тощо) інкапсульовані всередині `runStandardRule`. Сама `run` лише делегує виклик.
61
+
62
+ ### Standalone-блок (не функція, а top-level код)
63
+
64
+ **Сигнатура:**
65
+
66
+ ```js
67
+ if (isRunAsCli(import.meta.url)) {
68
+ process.exit(await runRuleCli(import.meta.dirname))
69
+ }
70
+ ```
71
+
72
+ **Що робить:**
73
+
74
+ 1. `isRunAsCli(import.meta.url)` повертає `true`, якщо поточний модуль завантажено як точку входу процесу (а не як імпортовану бібліотеку). Це стандартний для Node/Bun спосіб розпізнати, що файл стартував напряму.
75
+ 2. Якщо так — викликає `runRuleCli(import.meta.dirname)`, який реалізує повний CLI-оркестратор саме для цього правила: завантаження конфігу, обчислення whitelist, виконання правила, друк підсумків. Він повертає `Promise<number>` — exit-code.
76
+ 3. `await` чекає виконання promise. Це — top-level `await`, що дозволено в ES-модулях.
77
+ 4. `process.exit(<code>)` передає отриманий exit-code операційній системі / CI / IDE. ESLint-правила `n/no-process-exit` і `unicorn/no-process-exit` тут свідомо вимкнено коментарем, оскільки standalone entry-point правомірно завершує процес явним кодом.
78
+
79
+ **Параметри / повертає:** немає (це side-effect блок верхнього рівня модуля).
80
+
81
+ **Side effects:**
82
+
83
+ - Якщо файл імпортовано як бібліотеку — гілка не виконується, бо `isRunAsCli` поверне `false`.
84
+ - Якщо файл запущено напряму — повний CLI-оркестратор виконує власні I/O (читання конфігу, обхід проєкту, друк звіту) і потім завершує процес через `process.exit`.
85
+
86
+ ## Залежності
87
+
88
+ ### Внутрішні модулі проєкту
89
+
90
+ - `../../scripts/lib/run-rule-cli.mjs` — постачає:
91
+ - `isRunAsCli(metaUrl)` — детектор, чи модуль є entry-point процесу (за `import.meta.url`).
92
+ - `runRuleCli(ruleDirname)` — standalone CLI-оркестратор для одного правила: конфіг + whitelist + summary + exit-code. Аналогічний за поведінкою до `npx @nitra/cursor fix <id>`.
93
+ - `../../scripts/lib/run-standard-rule.mjs` — постачає:
94
+ - `runStandardRule(ruleDirname, ctx?)` — стандартний пайплайн правила: applies → JS-concerns → policy → mdc-refs.
95
+ - Тип `RuleContext` (через JSDoc-`import('...')`), який описує спільний контекст прогону (зокрема `walkCache`).
96
+
97
+ Шляхи відносні: `../../scripts/lib/...` з директорії `npm/rules/image-compress/` веде до `npm/scripts/lib/...`.
98
+
99
+ ### Платформенні API
100
+
101
+ - `import.meta.dirname` — стандартна властивість ES-модулів у сучасних рантаймах (Node.js та Bun), містить абсолютну директорію поточного модуля.
102
+ - `import.meta.url` — стандартна властивість ES-модулів; URL-форма шляху поточного модуля; використовується для розпізнавання запуску як CLI.
103
+ - `process.exit(code)` — глобальний Node/Bun API завершення процесу з кодом виходу.
104
+ - Top-level `await` — підтримується в ESM.
105
+
106
+ ### Зовнішні npm-пакети
107
+
108
+ Прямих імпортів зовнішніх npm-пакетів у файлі немає. Уся залежність на зовнішнє API проходить транзитивно через `run-rule-cli.mjs` та `run-standard-rule.mjs`.
109
+
110
+ ### ESLint-директиви
111
+
112
+ У файлі присутній один inline-disable: `n/no-process-exit, unicorn/no-process-exit` навколо `process.exit(...)`. Він локалізований лише до одного рядка і обґрунтований у коментарі: standalone entry-point має повертати exit-code для CI/IDE.
113
+
114
+ ## Потік виконання / Використання
115
+
116
+ ### Сценарій A: library-режим (через CLI `@nitra/cursor`)
117
+
118
+ 1. CLI `@nitra/cursor` (наприклад, `npx @nitra/cursor fix` без аргументу-id) збирає список правил, серед яких є `image-compress`.
119
+ 2. Оркестратор робить `import` модуля `npm/rules/image-compress/fix.mjs` і отримує іменований експорт `run`.
120
+ 3. Викликає `await run(ctx)`, передаючи спільний `RuleContext` (з `walkCache` тощо).
121
+ 4. `run` делегує виконання в `runStandardRule(import.meta.dirname, ctx)`, який виконує стандартний пайплайн правила (applies → JS-concerns → policy → mdc-refs) для теки `npm/rules/image-compress/`.
122
+ 5. `run` повертає `Promise<number>`; оркестратор агрегує exit-коди всіх правил у загальний підсумок.
123
+
124
+ У цьому сценарії гілка `if (isRunAsCli(...))` НЕ виконується — `process.exit` не викликається, оскільки модуль завантажено як бібліотеку, а не як entry-point процесу.
125
+
126
+ ### Сценарій B: standalone-режим (`bun rules/image-compress/fix.mjs`)
127
+
128
+ 1. Користувач/CI запускає файл напряму: `bun rules/image-compress/fix.mjs` (або еквівалентна команда рантайму).
129
+ 2. Виконується top-level код модуля, у тому числі гілка `if (isRunAsCli(import.meta.url))`.
130
+ 3. Оскільки модуль — entry-point процесу, `isRunAsCli` повертає `true`.
131
+ 4. Викликається `await runRuleCli(import.meta.dirname)`, який повністю відтворює оркестрацію `@nitra/cursor fix <id>` для саме цього правила: завантажує конфіг, обчислює whitelist, прогонить правило, друкує summary, повертає exit-code.
132
+ 5. `process.exit(<code>)` завершує процес із цим кодом — придатним для CI-пайплайнів та IDE-інтеграцій.
133
+
134
+ Експорт `run` у цьому сценарії існує, але не викликається з самого файлу — він залишається доступним для зовнішніх імпортів.
135
+
136
+ ### Як файл вписаний у репозиторій
137
+
138
+ - Тека `npm/rules/image-compress/` — це “контейнер” правила: разом із `fix.mjs` тут (за конвенцією родини стандартних правил) лежать `check-*.mjs` детектори, `.mdc`-опис та інші артефакти, які `runStandardRule` зчитує за відомими шаблонами імен.
139
+ - Усі стандартні правила пакета `@nitra/cursor` мають однаковий каркас `fix.mjs` із цим самим патерном (експорт `run` + standalone-блок). Тобто цей файл — фактично шаблонний адаптер між конкретним правилом і спільною бібліотекою прогону.
140
+
141
+ ### Rebuild Test (відтворюваність документації)
142
+
143
+ За цією документацією без перегляду початкового коду можна відновити поведінку файлу:
144
+
145
+ - Експортує лише функцію `run(ctx?)` з делегуванням у `runStandardRule(import.meta.dirname, ctx)` і поверненням `Promise<number>` (`0|1`).
146
+ - Standalone-гілка: `if (isRunAsCli(import.meta.url)) process.exit(await runRuleCli(import.meta.dirname))` — з inline-disable для `n/no-process-exit, unicorn/no-process-exit`.
147
+ - Імпортує `isRunAsCli` та `runRuleCli` зі `../../scripts/lib/run-rule-cli.mjs`, і `runStandardRule` зі `../../scripts/lib/run-standard-rule.mjs`.
148
+ - Жодних інших експортів, констант чи побічних дій верхнього рівня.
149
+
150
+ Цього достатньо, щоб точно відтворити структуру та контракт оригінального файлу.
@@ -0,0 +1,191 @@
1
+ # `package_setup.mjs` — перевірка вимог правила `image-compress.mdc`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `package_setup.mjs` реалізує `check`-функцію для правила `image-compress.mdc`. Правило вимагає, щоб у проєкті була налаштована оптимізація raster/SVG-зображень через CLI `@nitra/minify-image` ≥ 3.2.0 (запускається локально через `npx`).
6
+
7
+ У цьому файлі лишилися лише **FS / cross-file**-перевірки, які важко або незручно виразити декларативно у Rego:
8
+
9
+ - наявність `package.json` у корені репозиторію;
10
+ - `.n-minify-image.tsv` (committed source of truth split-cache 3.2.0 з полями sha1 / originalSize / size) **не** перебуває у `.gitignore` — файл має жити в git;
11
+ - застарілий `.minify-image-cache.tsv` (4-колонковий кеш з `@nitra/minify-image` < 3.2) видалений як з диска, так і з `.gitignore`.
12
+
13
+ Структурні вимоги до `package.json` (наявність `scripts.lint-image` з правильним викликом `npx @nitra/minify-image --src=. --write` без `--avif`, включення `bun run lint-image` в агрегований `lint`, відсутність `@nitra/minify-image` у `dependencies`/`devDependencies`) перевіряються Rego-політикою у `npm/rules/image-compress/policy/package_json/` і автофіксяться через `npx @nitra/cursor fix`.
14
+
15
+ CI-workflow для image-лінту правилом **не** вимагається — оптимізація відбувається лише локально перед комітом.
16
+
17
+ ## Експорти / API
18
+
19
+ | Експорт | Тип | Призначення |
20
+ | ------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
21
+ | `check(cwd?)` | `async function` | Публічна точка входу. Виконує перевірки правила `image-compress.mdc` для вказаного робочого каталогу й повертає exit-код (`0` — OK, `1` — є помилки). |
22
+
23
+ Модуль використовує ES Modules (`.mjs`), іменований експорт `check`. Default-експорту немає.
24
+
25
+ ## Функції
26
+
27
+ ### `readGitignoreLines(cwd)`
28
+
29
+ Приватна допоміжна функція для читання змістовних рядків `.gitignore`.
30
+
31
+ - **Сигнатура:** `async function readGitignoreLines(cwd: string): Promise<string[] | null>`
32
+ - **Параметри:**
33
+ - `cwd` — абсолютний шлях до кореня репозиторію, де очікується `.gitignore`.
34
+ - **Повертає:**
35
+ - `null`, якщо файл `.gitignore` відсутній;
36
+ - масив рядків (`string[]`), кожен з яких уже `trim`-нутий, не порожній і не починається з `#`.
37
+ - **Side effects:** один синхронний `existsSync` і один асинхронний `readFile` (UTF-8) у файловій системі. Не пише нічого.
38
+ - **Поведінкові деталі:** коментарі визначаються лише за префіксом `#` на початку trim-нутого рядка; in-line коментарі (після `#` всередині рядка) не вирізаються.
39
+
40
+ ### `checkHashCacheNotIgnored(pass, fail, cwd)`
41
+
42
+ Перевіряє, що файл `.n-minify-image.tsv` **не** перерахований у `.gitignore`. Файл є закомічуваним source of truth для split-cache 3.2.0 (зберігає sha1 + originalSize + size для slow-path і метрики lifetime savings), тому має потрапляти в git.
43
+
44
+ - **Сигнатура:** `async function checkHashCacheNotIgnored(pass: (msg: string) => void, fail: (msg: string) => void, cwd: string): Promise<void>`
45
+ - **Параметри:**
46
+ - `pass` — callback, що викликається при успіху з людиночитаним повідомленням;
47
+ - `fail` — callback, що викликається при провалі з повідомленням і вказівкою на дію;
48
+ - `cwd` — корінь репозиторію.
49
+ - **Повертає:** `Promise<void>`. Реєстрація результату здійснюється через `pass`/`fail` (зовнішній reporter).
50
+ - **Side effects:** одне читання `.gitignore` через `readGitignoreLines`.
51
+ - **Логіка:**
52
+ - якщо `.gitignore` є й містить точний рядок `.n-minify-image.tsv` → `fail` з вимогою прибрати рядок;
53
+ - інакше (файл відсутній або рядка немає) → `pass`.
54
+ - **Важливо:** сам факт існування `.n-minify-image.tsv` не вимагається. На новому проєкті без обробки зображень файла ще немає — і це нормально.
55
+
56
+ ### `checkLegacyCacheRemoved(pass, fail, cwd)`
57
+
58
+ Перевіряє, що застарілий 4-колонковий кеш `.minify-image-cache.tsv` (з `@nitra/minify-image` < 3.2) повністю видалений з проєкту.
59
+
60
+ - **Сигнатура:** `async function checkLegacyCacheRemoved(pass: (msg: string) => void, fail: (msg: string) => void, cwd: string): Promise<void>`
61
+ - **Параметри:**
62
+ - `pass` — callback для успішного звіту;
63
+ - `fail` — callback для помилки;
64
+ - `cwd` — корінь репозиторію.
65
+ - **Повертає:** `Promise<void>`.
66
+ - **Side effects:** один `existsSync` на файл у корені; за умови, що файла нема, додатково читає `.gitignore`.
67
+ - **Логіка (виконується послідовно з `early return`):**
68
+ 1. Якщо `<cwd>/.minify-image-cache.tsv` **існує на диску** → `fail` з готовим bash-snippet для повного видалення (`git rm --cached … && rm -f …`) і нагадуванням прибрати рядок з `.gitignore`. Подальші перевірки в цій функції пропускаються.
69
+ 2. Інакше читає `.gitignore`; якщо там є точний рядок `.minify-image-cache.tsv` → `fail` з вимогою прибрати застарілий ігнор.
70
+ 3. Інакше → `pass` («міграція на split-cache завершена»).
71
+
72
+ ### `check(cwd?)`
73
+
74
+ Експортована публічна функція — точка входу для агрегатора правил `@nitra/cursor`.
75
+
76
+ - **Сигнатура:** `export async function check(cwd: string = process.cwd()): Promise<number>`
77
+ - **Параметри:**
78
+ - `cwd` — необов’язковий шлях до кореня репозиторію; за замовчуванням `process.cwd()`.
79
+ - **Повертає:** exit-код як `number` (`0` — все ок; `1` — є хоча б один `fail`). Значення формує `reporter.getExitCode()` з `createCheckReporter`.
80
+ - **Side effects:**
81
+ - друк звіту в stdout/stderr через reporter (формат залежить від `createCheckReporter`);
82
+ - читання файлів `package.json`, `.gitignore`, `.minify-image-cache.tsv` у `cwd`. Жодного запису на диск.
83
+ - **Поведінкові деталі:**
84
+ 1. Створює локальний `reporter` через `createCheckReporter()` і дістає з нього `pass`/`fail`.
85
+ 2. **Guard:** якщо в корені нема `package.json` — друкує `fail` із підказкою додати файл і **негайно повертає** `reporter.getExitCode()` (≈ `1`). Інші перевірки в цій ітерації не виконуються.
86
+ 3. Якщо `package.json` є — `pass` з нагадуванням, що структуру `scripts` перевіряє Rego через `npx @nitra/cursor fix → image_compress.package_json`.
87
+ 4. Виконує `checkHashCacheNotIgnored` (await).
88
+ 5. Виконує `checkLegacyCacheRemoved` (await).
89
+ 6. Повертає `reporter.getExitCode()`.
90
+
91
+ ## Залежності
92
+
93
+ ### Зовнішні (Node.js core)
94
+
95
+ - `node:fs`
96
+ - `existsSync` — синхронна перевірка існування `package.json`, `.gitignore`, застарілого кешу;
97
+ - `node:fs/promises`
98
+ - `readFile` — асинхронне читання `.gitignore` у кодуванні UTF-8;
99
+ - `node:path`
100
+ - `join` — кросплатформена побудова шляхів від кореня репозиторію.
101
+
102
+ ### Внутрішні
103
+
104
+ - `../../../scripts/lib/check-reporter.mjs`
105
+ - `createCheckReporter()` — створює об’єкт із методами `pass(msg)`, `fail(msg)` і `getExitCode()`. Інкапсулює формат друку результатів і агрегацію статусу.
106
+
107
+ ### Файли проєкту, з якими взаємодіє
108
+
109
+ - `<cwd>/package.json` — лише перевірка існування;
110
+ - `<cwd>/.gitignore` — читання й пошук рядків `.n-minify-image.tsv`, `.minify-image-cache.tsv`;
111
+ - `<cwd>/.minify-image-cache.tsv` — лише перевірка існування (legacy-файл, який має бути відсутній);
112
+ - `<cwd>/.n-minify-image.tsv` — **не** перевіряється напряму на диску, лише на присутність у `.gitignore`.
113
+
114
+ ### Зовнішня політика
115
+
116
+ - `npm/rules/image-compress/policy/package_json/` — Rego-правила, які перевіряють і автофіксять структуру `package.json` (script `lint-image`, агрегований `lint`, відсутність залежності `@nitra/minify-image`).
117
+
118
+ ## Потік виконання / Використання
119
+
120
+ ### Як викликається
121
+
122
+ `check(cwd)` запускається оркестратором `@nitra/cursor` як одна з перевірок правила `image-compress.mdc`. У типовому сценарії команда `npx @nitra/cursor check` або `npx @nitra/cursor fix` обходить активні правила, для кожного імпортує його `check` і збирає звіт.
123
+
124
+ Можливий також прямий ESM-імпорт:
125
+
126
+ ```js
127
+ import { check } from '@nitra/cursor/npm/rules/image-compress/js/package_setup.mjs'
128
+
129
+ const exitCode = await check(process.cwd())
130
+ process.exit(exitCode)
131
+ ```
132
+
133
+ ### Послідовність кроків усередині `check`
134
+
135
+ 1. Створюється `reporter`, з нього дістаються `pass`/`fail`.
136
+ 2. Перевірка наявності `package.json` (guard з early return при відсутності).
137
+ 3. `await checkHashCacheNotIgnored(pass, fail, cwd)` — гарантує, що split-cache не виключений із git.
138
+ 4. `await checkLegacyCacheRemoved(pass, fail, cwd)` — гарантує, що legacy-кеш не висить ні на диску, ні в `.gitignore`.
139
+ 5. Повертається `reporter.getExitCode()`.
140
+
141
+ ### Типові сценарії результату
142
+
143
+ - **Свіжий проєкт без зображень.** `package.json` є, `.gitignore` не містить ні `.n-minify-image.tsv`, ні `.minify-image-cache.tsv`, файлів на диску немає → три `pass`, exit-код `0`.
144
+ - **Проєкт з обробленими зображеннями (3.2+).** На диску є закомічений `.n-minify-image.tsv`, у `.gitignore` його немає, legacy-файла немає → exit-код `0`.
145
+ - **Незавершена міграція з 3.1 → 3.2.** На диску ще лежить `.minify-image-cache.tsv` → `fail` з готовим bash-snippet для очищення; exit-код `1`.
146
+ - **Помилково додано split-cache в `.gitignore`.** `.gitignore` містить `.n-minify-image.tsv` → `fail` з вимогою прибрати рядок; exit-код `1`.
147
+ - **Немає `package.json`.** Перша ж перевірка повертає `fail` і виконання обривається на guard-у; жодних інших перевірок не запускається; exit-код `1`.
148
+
149
+ ### Розподіл відповідальності з Rego
150
+
151
+ | Перевірка | Де реалізована |
152
+ | ----------------------------------------------------------- | ----------------------------- |
153
+ | Наявність `package.json` | `package_setup.mjs` |
154
+ | `.n-minify-image.tsv` не в `.gitignore` | `package_setup.mjs` |
155
+ | `.minify-image-cache.tsv` відсутній (диск + `.gitignore`) | `package_setup.mjs` |
156
+ | `scripts.lint-image` коректний (без `--avif`) | Rego (`policy/package_json/`) |
157
+ | `bun run lint-image` в агрегованому `lint` | Rego (`policy/package_json/`) |
158
+ | `@nitra/minify-image` не в `dependencies`/`devDependencies` | Rego (`policy/package_json/`) |
159
+
160
+ ## Rebuild Test
161
+
162
+ Якщо файл `package_setup.mjs` втрачено, відтворіть його за такою специфікацією:
163
+
164
+ 1. **Розташування:** `npm/rules/image-compress/js/package_setup.mjs` (ESM, розширення `.mjs`).
165
+ 2. **Імпорти:**
166
+ - `existsSync` з `node:fs`;
167
+ - `readFile` з `node:fs/promises`;
168
+ - `join` з `node:path`;
169
+ - `createCheckReporter` з `../../../scripts/lib/check-reporter.mjs`.
170
+ 3. **Константи модульного рівня:**
171
+ - `HASH_CACHE_FILENAME = '.n-minify-image.tsv'`;
172
+ - `LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'`.
173
+ 4. **Приватна `readGitignoreLines(cwd)`:** будує шлях `<cwd>/.gitignore`; якщо файла нема — `return null`; інакше читає UTF-8, розбиває по `\n`, `trim` кожного рядка, фільтрує порожні й ті, що починаються з `#`; повертає масив.
174
+ 5. **Приватна `checkHashCacheNotIgnored(pass, fail, cwd)`:** читає рядки `.gitignore`; якщо вони є й містять `HASH_CACHE_FILENAME` — `fail` (з підказкою прибрати рядок), інакше `pass` («не в .gitignore — має бути в git»).
175
+ 6. **Приватна `checkLegacyCacheRemoved(pass, fail, cwd)`:**
176
+ - якщо `<cwd>/<LEGACY_CACHE_FILENAME>` існує на диску — `fail` з готовим bash-snippet (`git rm --cached … 2>/dev/null || true && rm -f …`) і нагадуванням про `.gitignore`, потім `return`;
177
+ - інакше читає `.gitignore`; якщо містить `LEGACY_CACHE_FILENAME` — `fail` (прибрати застарілий ігнор), потім `return`;
178
+ - інакше `pass` («міграція на split-cache завершена»).
179
+ 7. **Експортована `check(cwd = process.cwd())`:**
180
+ - `const reporter = createCheckReporter()`; деструктурує `{ pass, fail }`;
181
+ - якщо `<cwd>/package.json` не існує — `fail` («додай — image-compress.mdc») і одразу `return reporter.getExitCode()`;
182
+ - інакше `pass` («package.json є; структуру перевіряє npx @nitra/cursor fix → image_compress.package_json»);
183
+ - `await checkHashCacheNotIgnored(pass, fail, cwd)`;
184
+ - `await checkLegacyCacheRemoved(pass, fail, cwd)`;
185
+ - `return reporter.getExitCode()`.
186
+ 8. **Інваріанти:**
187
+ - модуль не пише на диск нічого;
188
+ - `pass`/`fail` приймають лише `string` і викликаються рівно один раз на гілку логіки;
189
+ - функція `check` завжди повертає `number`, навіть на guard-шляху;
190
+ - усі шляхи будуються через `join(cwd, ...)`, ніяких рядкових конкатенацій.
191
+ 9. **Текст повідомлень** містить посилання на правило `image-compress.mdc` і термін `split-cache 3.2.0`, щоб користувач у звіті бачив джерело вимоги.