@nitra/cursor 3.22.0 → 3.23.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 (228) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +31 -3
  3. package/bin/docs/n-cursor.md +636 -0
  4. package/bin/docs/rename-yaml-extensions.md +207 -0
  5. package/bin/n-cursor.js +30 -3
  6. package/package.json +1 -1
  7. package/rules/abie/docs/fix.md +18 -0
  8. package/rules/abie/js/docs/applies.md +26 -0
  9. package/rules/abie/js/docs/env_dns.md +32 -0
  10. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  11. package/rules/abie/js/docs/hc_pairing.md +35 -0
  12. package/rules/abie/js/docs/ua_http_route.md +28 -0
  13. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  14. package/rules/abie/lib/docs/enabled.md +29 -0
  15. package/rules/abie/lib/docs/env-dns.md +35 -0
  16. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  17. package/rules/abie/lib/docs/http-route.md +44 -0
  18. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  19. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  20. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  21. package/rules/abie/lib/docs/yaml.md +29 -0
  22. package/rules/adr/docs/fix.md +148 -0
  23. package/rules/adr/js/docs/hooks.md +259 -0
  24. package/rules/bun/docs/fix.md +156 -0
  25. package/rules/bun/js/docs/layout.md +393 -0
  26. package/rules/capacitor/docs/fix.md +121 -0
  27. package/rules/capacitor/js/docs/platforms.md +295 -0
  28. package/rules/changelog/changelog.mdc +2 -2
  29. package/rules/changelog/docs/fix.md +174 -0
  30. package/rules/changelog/js/consistency.mjs +114 -13
  31. package/rules/changelog/js/docs/consistency.md +387 -0
  32. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  33. package/rules/ci4/docs/fix.md +179 -0
  34. package/rules/ci4/js/docs/marksman_config.md +128 -0
  35. package/rules/docker/docker.mdc +8 -3
  36. package/rules/docker/docs/fix.md +171 -0
  37. package/rules/docker/js/docs/lint.md +258 -0
  38. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  39. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  40. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  41. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  42. package/rules/docker/lint/docs/lint.md +193 -0
  43. package/rules/efes/docs/fix.md +203 -0
  44. package/rules/feedback/docs/fix.md +140 -0
  45. package/rules/flow/docs/fix.md +152 -0
  46. package/rules/ga/docs/fix.md +158 -0
  47. package/rules/ga/js/docs/lint.md +100 -0
  48. package/rules/ga/js/docs/workflows.md +217 -0
  49. package/rules/ga/lint/docs/lint.md +209 -0
  50. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  51. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  52. package/rules/graphql/docs/fix.md +126 -0
  53. package/rules/graphql/js/docs/tooling.md +264 -0
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  55. package/rules/hasura/docs/fix.md +120 -0
  56. package/rules/hasura/hasura.mdc +14 -0
  57. package/rules/hasura/js/docs/internal_urls.md +326 -0
  58. package/rules/image-avif/docs/fix.md +132 -0
  59. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  60. package/rules/image-compress/docs/fix.md +150 -0
  61. package/rules/image-compress/js/docs/package_setup.md +191 -0
  62. package/rules/js-bun-db/docs/fix.md +148 -0
  63. package/rules/js-bun-db/js/docs/safety.md +231 -0
  64. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  65. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  66. package/rules/js-bun-redis/docs/fix.md +123 -0
  67. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  68. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  69. package/rules/js-lint/docs/fix.md +117 -0
  70. package/rules/js-lint/js/docs/lint.md +250 -0
  71. package/rules/js-lint/js/docs/tooling.md +348 -0
  72. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  73. package/rules/js-lint-ci/docs/fix.md +154 -0
  74. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  75. package/rules/js-mssql/docs/fix.md +128 -0
  76. package/rules/js-mssql/js/docs/deps.md +263 -0
  77. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  78. package/rules/js-run/docs/fix.md +144 -0
  79. package/rules/js-run/js/docs/runtime.md +388 -0
  80. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  81. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  82. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  83. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  84. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  85. package/rules/k8s/docs/fix.md +129 -0
  86. package/rules/k8s/js/docs/manifests.md +344 -0
  87. package/rules/k8s/js/manifests.mjs +6 -2
  88. package/rules/k8s/k8s.mdc +4 -2
  89. package/rules/k8s/lint/docs/lint.md +411 -0
  90. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  91. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  92. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  93. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  94. package/rules/npm-module/docs/fix.md +98 -0
  95. package/rules/npm-module/js/docs/package_structure.md +274 -0
  96. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  97. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  98. package/rules/php/docs/fix.md +107 -0
  99. package/rules/php/js/docs/tooling.md +152 -0
  100. package/rules/php/lint/docs/lint.md +215 -0
  101. package/rules/python/docs/fix.md +163 -0
  102. package/rules/python/js/docs/applies.md +108 -0
  103. package/rules/python/js/docs/tooling.md +153 -0
  104. package/rules/python/lint/docs/lint.md +322 -0
  105. package/rules/rego/docs/fix.md +121 -0
  106. package/rules/rego/js/docs/applies.md +174 -0
  107. package/rules/rego/js/docs/lint.md +118 -0
  108. package/rules/rego/lint/docs/lint.md +204 -0
  109. package/rules/release/docs/change.md +185 -0
  110. package/rules/release/docs/fix.md +119 -0
  111. package/rules/release/docs/release.md +222 -0
  112. package/rules/release/lib/docs/aggregate.md +246 -0
  113. package/rules/release/lib/docs/change-file.md +200 -0
  114. package/rules/release/lib/docs/fallback.md +203 -0
  115. package/rules/rust/docs/fix.md +129 -0
  116. package/rules/rust/js/docs/applies.md +140 -0
  117. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  118. package/rules/security/docs/fix.md +86 -0
  119. package/rules/security/js/docs/lint.md +171 -0
  120. package/rules/security/js/docs/sample_secret.md +190 -0
  121. package/rules/security/js/docs/trufflehog.md +137 -0
  122. package/rules/security/js/lint.mjs +9 -1
  123. package/rules/style-lint/docs/fix.md +155 -0
  124. package/rules/style-lint/js/docs/lint.md +184 -0
  125. package/rules/style-lint/js/docs/tooling.md +194 -0
  126. package/rules/tauri/docs/fix.md +158 -0
  127. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  128. package/rules/tauri/js/docs/tooling.md +228 -0
  129. package/rules/test/coverage/coverage.mjs +15 -3
  130. package/rules/test/docs/fix.md +132 -0
  131. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  134. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  135. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  136. package/rules/test/js/docs/location.md +136 -0
  137. package/rules/test/js/docs/no-process-chdir.md +160 -0
  138. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  139. package/rules/test/js/docs/stryker_config.md +152 -0
  140. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  141. package/rules/text/docs/fix.md +118 -0
  142. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  143. package/rules/text/js/docs/formatting.md +256 -0
  144. package/rules/text/js/docs/lint.md +122 -0
  145. package/rules/text/lint/docs/lint.md +220 -0
  146. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  147. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  148. package/rules/text/lint/docs/run-v8r.md +197 -0
  149. package/rules/vue/docs/fix.md +127 -0
  150. package/rules/vue/js/docs/packages.md +335 -0
  151. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  152. package/rules/worktree/docs/fix.md +161 -0
  153. package/schemas/rule-meta.json +5 -1
  154. package/scripts/auto-rules.mjs +7 -4
  155. package/scripts/coverage-classify/docs/apply.md +202 -0
  156. package/scripts/coverage-classify/docs/cache.md +203 -0
  157. package/scripts/coverage-classify/docs/index.md +218 -0
  158. package/scripts/coverage-classify/docs/prompt.md +132 -0
  159. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  160. package/scripts/coverage-fix-extract.mjs +122 -0
  161. package/scripts/coverage-fix.mjs +1 -1
  162. package/scripts/dispatcher/docs/graph.md +346 -0
  163. package/scripts/dispatcher/docs/index.md +236 -0
  164. package/scripts/dispatcher/docs/trace.md +296 -0
  165. package/scripts/dispatcher/index.mjs +1 -1
  166. package/scripts/dispatcher/lib/active.mjs +4 -8
  167. package/scripts/dispatcher/lib/commands.mjs +7 -11
  168. package/scripts/dispatcher/lib/docs/active.md +348 -0
  169. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  170. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  171. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  172. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  173. package/scripts/dispatcher/lib/docs/events.md +182 -0
  174. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  175. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  176. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  177. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  178. package/scripts/dispatcher/lib/docs/level.md +335 -0
  179. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  180. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  181. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  182. package/scripts/dispatcher/lib/docs/review.md +255 -0
  183. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  184. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  185. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  186. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  187. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  188. package/scripts/dispatcher/lib/executor.mjs +6 -1
  189. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  190. package/scripts/dispatcher/lib/level.mjs +29 -3
  191. package/scripts/dispatcher/lib/review.mjs +1 -1
  192. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  193. package/scripts/docs/auto-rules.md +376 -0
  194. package/scripts/docs/auto-skills.md +173 -0
  195. package/scripts/docs/build-agents-commands.md +183 -0
  196. package/scripts/docs/cli-entry.md +153 -0
  197. package/scripts/docs/coverage-fix.md +177 -0
  198. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  199. package/scripts/lib/changed-files.mjs +4 -1
  200. package/scripts/lib/docs/changed-files.md +149 -0
  201. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  202. package/scripts/lib/docs/check-reporter.md +175 -0
  203. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  204. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  205. package/scripts/lib/docs/ensure-tool.md +254 -0
  206. package/scripts/lib/docs/generated-markdown.md +275 -0
  207. package/scripts/lib/docs/gha-workflow.md +326 -0
  208. package/scripts/lib/docs/inline-template-links.md +303 -0
  209. package/scripts/lib/docs/list-rule-ids.md +156 -0
  210. package/scripts/lib/docs/load-cursor-config.md +147 -0
  211. package/scripts/lib/docs/mirror-parity.md +167 -0
  212. package/scripts/lib/worktree.mjs +26 -0
  213. package/scripts/worktree-cli.mjs +12 -2
  214. package/skills/coverage-fix/SKILL.md +34 -45
  215. package/skills/docgen/SKILL.md +44 -23
  216. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  217. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  218. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  219. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  220. package/skills/docgen/js/docgen-scan.mjs +37 -21
  221. package/skills/llm-patch/SKILL.md +23 -2
  222. package/skills/start-check/SKILL.md +26 -53
  223. package/skills/start-check/js/check.mjs +211 -0
  224. package/skills/taze/SKILL.md +9 -3
  225. package/skills/taze/js/diff.mjs +154 -0
  226. package/types/bin/n-cursor.d.ts +1 -1
  227. package/skills/fix-tests/SKILL.md +0 -119
  228. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,247 @@
1
+ # docker-mirror.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `docker-mirror.mjs` — це частина правила `n-docker` (тека `npm/rules/docker/lib/`) монорепо `n-cursor`. Він реалізує чисту (без I/O, без побічних ефектів) бібліотеку функцій для статичного аналізу директив `FROM` у файлах `Dockerfile` / `Containerfile` та визначає, чи звертається образ до Docker Hub без використання GCR-дзеркала `mirror.gcr.io`.
6
+
7
+ Призначення:
8
+
9
+ - Перевірити, що базові образи з обмеженого списку популярних публічних репозиторіїв (`oven/bun`, `library/alpine`, `library/nginx`, `library/node`, `nginxinc/nginx-unprivileged`) тягнуться не напряму з Docker Hub, а через дзеркало `mirror.gcr.io` (зменшує ризики rate limiting Docker Hub та підвищує доступність CI/CD).
10
+ - Виявити порушення й сформувати людиночитане повідомлення з номером рядка та рекомендованим референсом образу.
11
+
12
+ Модуль свідомо ігнорує приватні реєстри (наприклад, `gcr.io/foo/bar`, `registry.example.com:5000/app`, IP-адреси, `localhost`). Канонічні заміни:
13
+
14
+ - `oven/bun` → `mirror.gcr.io/oven/bun`
15
+ - `library/alpine` → `mirror.gcr.io/library/alpine`
16
+ - `library/nginx` → `mirror.gcr.io/library/nginx`
17
+ - `library/node` → `mirror.gcr.io/library/node`
18
+ - `nginxinc/nginx-unprivileged` → `mirror.gcr.io/nginxinc/nginx-unprivileged`
19
+
20
+ Модуль є чистим: жодних звернень до файлової системи, мережі, глобального стану. Усі експортовані функції детерміновані й безпечні для повторного виклику.
21
+
22
+ ## Експорти / API
23
+
24
+ Файл є ES-модулем (`.mjs`) і експортує чотири іменовані функції; стандартного експорту немає.
25
+
26
+ | Експорт | Тип | Призначення |
27
+ | --------------------------- | ----------------------------------------- | ------------------------------------------------------------------------ |
28
+ | `getFromImageToken` | `(line: string) => string \| null` | Витягує токен образу з рядка `FROM` Dockerfile. |
29
+ | `isDockerHubStyleImageRef` | `(imageToken: string) => boolean` | Повертає `true`, якщо посилання схоже на Docker Hub. |
30
+ | `normalizeHubRepoPath` | `(imageToken: string) => string` | Нормалізує шлях репозиторію (без тега/digest) у канонічну форму. |
31
+ | `getRequiredMirrorGcrImage` | `(imageToken: string) => string \| null` | Повертає рекомендований `mirror.gcr.io/...`-референс, якщо потрібен. |
32
+ | `getMirrorGcrHint` | `(fileContent: string) => string \| null` | Сканує вміст Dockerfile та повертає підказку про порушення (або `null`). |
33
+
34
+ Внутрішні (не експортовані) допоміжні елементи:
35
+
36
+ - Константи-регулярки: `FROM_LINE_RE`, `TOKEN_RE`, `MIRROR_GCR_RE`, `IP_LIKE_RE`, `HOST_PORT_RE`, `DOCKER_IO_PREFIX_RE`, `NEWLINE_SPLIT_RE`.
37
+ - Функція `stripFromImageQuotes(t)` — знімає зовнішні одинарні/подвійні лапки з токена.
38
+ - `HUB_REPOS_REQUIRING_MIRROR` — `Set` із п'яти канонічних шляхів репозиторіїв, що підлягають дзеркалу.
39
+ - `EXPECTED_MIRROR` — `Record<string, string>` із зіставленням канонічний шлях → рекомендований `mirror.gcr.io/...` префікс.
40
+
41
+ ## Функції
42
+
43
+ ### `stripFromImageQuotes(t)`
44
+
45
+ Внутрішня (не експортована) утиліта.
46
+
47
+ - **Сигнатура:** `(t: string) => string`
48
+ - **Параметри:**
49
+ - `t` — токен образу, можливо обгорнутий парою `"…"` або `'…'`.
50
+ - **Повертає:** ту саму рядкову форму без зовнішньої пари лапок, якщо довжина ≥ 2 і перший символ — `"` або `'`. Інакше — рядок без змін.
51
+ - **Side effects:** немає (чиста функція).
52
+ - **Особливості:** не валідовує парність лапок (просто зрізає перший і останній символи). Передбачає, що вхід уже виокремлено токенайзером, який сам парує лапки (`TOKEN_RE`).
53
+
54
+ ### `getFromImageToken(line)`
55
+
56
+ Експортована.
57
+
58
+ - **Сигнатура:** `(line: string) => string | null`
59
+ - **Параметри:**
60
+ - `line` — один рядок із Dockerfile.
61
+ - **Повертає:**
62
+ - Токен образу (без зовнішніх лапок), напр. `node:20-alpine`, `mirror.gcr.io/oven/bun:1.2`, `gcr.io/distroless/static@sha256:…`.
63
+ - `null`, якщо рядок не директива `FROM` або токен не вдалося виокремити (порожньо, лише прапорці тощо).
64
+ - **Алгоритм:**
65
+ 1. Зрізає inline-коментар: `line.split('#')[0].trim()`.
66
+ 2. Перевіряє, що результат починається з `FROM ` (case-insensitive) через `FROM_LINE_RE`.
67
+ 3. Розбиває залишок на токени за `TOKEN_RE` — регулярка зберігає вміст у лапках як один токен.
68
+ 4. Циклом проходить токени, ігноруючи прапорці:
69
+ - `--platform=…` — один токен, пропустити.
70
+ - `--platform` без `=` — пропустити 2 токени (прапорець + значення), або 1, якщо значення відсутнє.
71
+ - `--` або `AS` (case-insensitive) — припиняє пошук (повертає `null`, якщо токен ще не знайдено).
72
+ - Інший `--key=value` — пропустити 1 токен.
73
+ - Інший `--key` — пропустити 1 токен (припускає, що це boolean-прапорець без значення; це може бути неточно для прапорців із наступним значенням, окрім `--platform`).
74
+ - Перший не-прапорцевий токен — це образ; повертає `stripFromImageQuotes(token)`.
75
+ - **Side effects:** немає.
76
+ - **Особливості:**
77
+ - Не валідовує синтаксис Dockerfile повністю: підтримує найрозповсюдженіші форми `FROM`.
78
+ - Не розпізнає неіменованих прапорців із наступним значенням (`--foo bar`) — `bar` буде сприйнято як токен образу. Це компроміс заради простоти; для канонічного Dockerfile `--platform` — єдиний релевантний випадок.
79
+ - Залишок після образу (наприклад, `AS base`) не повертається.
80
+
81
+ ### `isDockerHubStyleImageRef(imageToken)`
82
+
83
+ Експортована.
84
+
85
+ - **Сигнатура:** `(imageToken: string) => boolean`
86
+ - **Параметри:**
87
+ - `imageToken` — ref образу (як повертає `getFromImageToken`).
88
+ - **Повертає:** `true`, якщо посилання виглядає як pull із Docker Hub; інакше `false`.
89
+ - **Алгоритм:**
90
+ 1. Порожнє/falsy значення → `false`.
91
+ 2. Починається з `mirror.gcr.io/` (за `MIRROR_GCR_RE`) → `false` (це вже дзеркало, не Hub).
92
+ 3. Зрізає `@digest` (`imageToken.split('@')[0]`).
93
+ 4. Якщо немає `/` — це коротке ім'я (`node:20`, `alpine`) → `true`.
94
+ 5. Виокремлює перший сегмент `first = noDigest.split('/')[0]`.
95
+ 6. `first === 'docker.io'` або `first === 'index.docker.io'` → `true`.
96
+ 7. Якщо `first` містить крапку (`.`) — це FQDN чужого реєстру (`gcr.io`, `ghcr.io`, `registry.example.com`) → `false`.
97
+ 8. `first === 'localhost'` або відповідає IP-патерну `^\d+\.\d+` → `false` (приватний/локальний реєстр).
98
+ 9. Містить `:` й відповідає `^\S+:\d+$` (host:port) → `false`.
99
+ 10. Інакше → `true` (типова форма `user/repo` або `org/image:tag`, яку Docker за замовчуванням резолвить через Docker Hub).
100
+ - **Side effects:** немає.
101
+ - **Особливості:** правило "крапка у першому сегменті = чужий реєстр" — Docker-канонічне евристичне правило (Docker CLI використовує його для розрізнення `docker.io/library/foo` від коротких посилань).
102
+
103
+ ### `normalizeHubRepoPath(imageToken)`
104
+
105
+ Експортована.
106
+
107
+ - **Сигнатура:** `(imageToken: string) => string`
108
+ - **Параметри:**
109
+ - `imageToken` — ref образу (передбачається, що це Hub-style, як після `isDockerHubStyleImageRef`).
110
+ - **Повертає:** канонічний шлях репозиторію (lower-case, без тега, без digest, без префіксу `docker.io/`):
111
+ - `node` → `library/node`
112
+ - `node:20-alpine` → `library/node`
113
+ - `docker.io/oven/bun:1.2` → `oven/bun`
114
+ - `index.docker.io/library/alpine` → `library/alpine`
115
+ - `oven/bun@sha256:…` → `oven/bun`
116
+ - **Алгоритм:**
117
+ 1. Зрізає `@digest`, переводить у lower-case.
118
+ 2. Зрізає префікс `docker.io/` або `index.docker.io/` (через `DOCKER_IO_PREFIX_RE`).
119
+ 3. Якщо немає `/` — це коротке ім'я: повертає `library/<name>` (де `<name>` — частина до `:`).
120
+ 4. Інакше шукає останній `/` і останній `:`. Якщо `:` йде після `/`, це тег — зрізає від нього й до кінця.
121
+ - **Side effects:** немає.
122
+ - **Особливості:** уважно обробляє port-у-host (`host:5000/foo`) для випадків, коли функцію викликають на не-Hub посиланнях — порт перед `/` зберігається, тільки тег після останнього `/` зрізається. Однак функція передбачена для Hub-посилань і не повинна викликатися на FQDN.
123
+
124
+ ### `getRequiredMirrorGcrImage(imageToken)`
125
+
126
+ Експортована.
127
+
128
+ - **Сигнатура:** `(imageToken: string) => string | null`
129
+ - **Параметри:**
130
+ - `imageToken` — ref після `FROM` (як повертає `getFromImageToken`).
131
+ - **Повертає:**
132
+ - Рекомендований `mirror.gcr.io/...` шлях (без тега й digest), якщо образ Hub-style і входить до `HUB_REPOS_REQUIRING_MIRROR`.
133
+ - `null`, якщо: токен порожній; вже використовує `mirror.gcr.io/...`; не Hub-style; нормалізований шлях не входить до списку.
134
+ - **Алгоритм:**
135
+ 1. `!imageToken` → `null`.
136
+ 2. `MIRROR_GCR_RE.test(imageToken)` → `null` (вже на дзеркалі).
137
+ 3. `!isDockerHubStyleImageRef(imageToken)` → `null` (приватний реєстр).
138
+ 4. `norm = normalizeHubRepoPath(imageToken)`.
139
+ 5. Якщо `!HUB_REPOS_REQUIRING_MIRROR.has(norm)` → `null` (інший Hub-образ, правило не застосовується).
140
+ 6. Повертає `EXPECTED_MIRROR[norm]`.
141
+ - **Side effects:** немає.
142
+
143
+ ### `getMirrorGcrHint(fileContent)`
144
+
145
+ Експортована.
146
+
147
+ - **Сигнатура:** `(fileContent: string) => string | null`
148
+ - **Параметри:**
149
+ - `fileContent` — повний вміст Dockerfile/Containerfile (рядки розділені `\n` або `\r\n`).
150
+ - **Повертає:**
151
+ - Рядок виду `рядок <N>: FROM має тягнути mirror.gcr.io/<repo> (замість <orig-token>)` — для першого знайденого порушення.
152
+ - `null`, якщо порушень немає.
153
+ - **Алгоритм:**
154
+ 1. Ділить контент на рядки за `NEWLINE_SPLIT_RE` (`/\r?\n/`).
155
+ 2. Ітерує з індексом (`.entries()`); номер рядка у повідомленні — `n + 1` (1-based).
156
+ 3. Для кожного рядка: `image = getFromImageToken(line)`; `expected = getRequiredMirrorGcrImage(image)`.
157
+ 4. Перший рядок, де `expected` істинний (string), формує повідомлення і функція повертається.
158
+ - **Side effects:** немає.
159
+ - **Особливості:**
160
+ - Зупиняється на першому порушенні (fail-fast). Якщо в одному Dockerfile є кілька `FROM` із порушеннями — повідомлено буде лише про перший.
161
+ - Текст повідомлення українською (узгоджується з мовою спец-документів проєкту).
162
+
163
+ ## Залежності
164
+
165
+ - **Зовнішні модулі:** немає (`import`/`require` відсутні).
166
+ - **Внутрішні модулі:** немає; модуль самодостатній.
167
+ - **Глобальні API:** виключно стандартний JavaScript — `RegExp`, `String.prototype` (`split`, `trim`, `match`, `slice`, `toLowerCase`, `replace`, `includes`, `startsWith`, `lastIndexOf`), `Set`, `Array.prototype.entries`.
168
+ - **Runtime:** ESM, працює в Node.js та Bun. Розширення `.mjs` обов'язкове за правилом `n-bun`/`n-js-run`.
169
+ - **JSDoc типи:** використовуються `@type`, `@param`, `@returns` із касти `/** @type {const} */` для іммутабельних літералів — допомагають TypeScript/JSDoc-перевірці й ESLint.
170
+
171
+ ## Потік виконання / Використання
172
+
173
+ ### Типовий сценарій інтеграції
174
+
175
+ Модуль використовується перевіркою правила `n-docker` (вочевидь, файл `check-*.mjs` у `npm/rules/docker/`), яка:
176
+
177
+ 1. Знаходить усі `Dockerfile` / `Containerfile` у воркспейсі.
178
+ 2. Для кожного зчитує вміст і передає в `getMirrorGcrHint(content)`.
179
+ 3. Якщо результат — рядок, репортує його як порушення лінтера/правила.
180
+
181
+ ### Програмний приклад
182
+
183
+ ```js
184
+ import { getMirrorGcrHint } from './docker-mirror.mjs'
185
+
186
+ const dockerfile = `
187
+ # syntax=docker/dockerfile:1
188
+ FROM --platform=linux/amd64 node:20-alpine AS builder
189
+ RUN bun install
190
+ FROM mirror.gcr.io/library/nginx:1.27
191
+ COPY --from=builder /app /usr/share/nginx/html
192
+ `
193
+
194
+ const hint = getMirrorGcrHint(dockerfile)
195
+ if (hint) {
196
+ console.error(hint)
197
+ // → рядок 3: FROM має тягнути mirror.gcr.io/library/node (замість node:20-alpine)
198
+ }
199
+ ```
200
+
201
+ ### Гранулярне використання
202
+
203
+ ```js
204
+ import {
205
+ getFromImageToken,
206
+ isDockerHubStyleImageRef,
207
+ normalizeHubRepoPath,
208
+ getRequiredMirrorGcrImage
209
+ } from './docker-mirror.mjs'
210
+
211
+ const token = getFromImageToken('FROM --platform=$BUILDPLATFORM oven/bun:1.2 AS bun')
212
+ // → 'oven/bun:1.2'
213
+
214
+ isDockerHubStyleImageRef(token) // → true
215
+ normalizeHubRepoPath(token) // → 'oven/bun'
216
+ getRequiredMirrorGcrImage(token) // → 'mirror.gcr.io/oven/bun'
217
+
218
+ getRequiredMirrorGcrImage('gcr.io/foo/bar') // → null (чужий реєстр)
219
+ getRequiredMirrorGcrImage('mirror.gcr.io/library/node') // → null (вже на дзеркалі)
220
+ getRequiredMirrorGcrImage('redis:7') // → null (не в списку)
221
+ ```
222
+
223
+ ### Покриті випадки
224
+
225
+ - `FROM node` — коротке ім'я, рекомендує `mirror.gcr.io/library/node`.
226
+ - `FROM library/alpine:3.19` — Hub з явним `library/`, рекомендує `mirror.gcr.io/library/alpine`.
227
+ - `FROM docker.io/oven/bun:1.2` — Hub з явним хостом, рекомендує `mirror.gcr.io/oven/bun`.
228
+ - `FROM --platform=linux/arm64 nginx AS base` — пропускає прапорці й `AS`.
229
+ - `FROM "node:20"` — знімає лапки.
230
+ - `FROM nginxinc/nginx-unprivileged:1.27` — Hub-style з користувацьким наміспейсом, рекомендує `mirror.gcr.io/nginxinc/nginx-unprivileged`.
231
+
232
+ ### Випадки, що не вважаються порушенням
233
+
234
+ - `FROM gcr.io/distroless/static` — крапка в першому сегменті → чужий реєстр.
235
+ - `FROM localhost:5000/myapp` — `localhost` + порт → приватний реєстр.
236
+ - `FROM 10.0.0.1/internal/app` — IP-патерн → приватний реєстр.
237
+ - `FROM mirror.gcr.io/library/node:20` — вже використовує дзеркало.
238
+ - `FROM redis:7` — Hub, але не в списку контрольованих репозиторіїв.
239
+ - `FROM scratch` — `scratch` нормалізується до `library/scratch`, але його немає в `HUB_REPOS_REQUIRING_MIRROR`.
240
+
241
+ ### Обмеження й нюанси
242
+
243
+ - Перевірка зупиняється на першому порушенні в файлі. Якщо потрібно зібрати всі порушення — функцію треба інтегрувати на рівні викликача (наприклад, проходом по `fileContent.split('\n')` із власною агрегацією).
244
+ - Багаторядкові `FROM` із продовженням рядка через `\` не підтримуються (Dockerfile-специфіка така, що `FROM` зазвичай одно-рядковий, але формально це можливо).
245
+ - Інші прапорці з пробільним значенням (наприклад, гіпотетичне `--foo bar`) можуть бути неточно розпарсені — `bar` буде сприйнято як токен образу. Для канонічних Dockerfile цей випадок не реалістичний.
246
+ - Heredoc-форма Dockerfile (`FROM <<EOF`) явно не підтримується.
247
+ - Регістр літерала `FROM` нечутливий (`FROM_LINE_RE` має флаг `i`); літерал `AS` теж нечутливий (порівняння через `.toUpperCase()`); імена реєстрів і репозиторіїв нормалізуються через `.toLowerCase()`.
@@ -0,0 +1,170 @@
1
+ # docker-native-addon.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `docker-native-addon.mjs` — це dep-специфічний чек-модуль правила `docker` для виявлення антипатерну, коли проєкт залежить від нативного `.node`-аддона з динамічним завантаженням біндингу (через динамічний `require`), і одночасно намагається бути запакованим через `bun build --compile` у Dockerfile.
6
+
7
+ Контекст проблеми:
8
+
9
+ - Деякі npm-пакети (передусім `sharp`, а також пакети у scope `@img/*`, `argon2`) містять нативний `.node`-аддон, який вантажиться у рантаймі через **динамічний** виклик `require`, наприклад `require(\`@img/sharp-${platform}/sharp.node\`)`. Ім'я модуля формується з підстановки змінних.
10
+ - Компілятор `bun build --compile` виконує статичний трейсинг імпортів і **не** бачить такі динамічні `require`, тож **не вшиває** відповідний нативний біндинг у standalone-бінарник.
11
+ - Результат — рантайм-помилка на кшталт `Could not load the "sharp" module using the linuxmusl-arm64 runtime`. Підтверджено реальними docker-збірками (`bun 1.3.14`, `sharp 0.34.5`) і відтворюється також на `darwin-arm64`, тобто проблема не пов'язана з різницею musl/glibc.
12
+ - Установка системного `apk add vips` **не** лікує проблему: вона дає системний `libvips`, але самого файлу `sharp.node` усе одно бракує.
13
+
14
+ Канонічне рішення для таких проєктів (за правилом `docker.mdc`, секція «компіляція»): **не** компілювати в standalone-бінарник, а shipити `node_modules` як є й запускати застосунок через `bun <entry>` на базі образу `mirror.gcr.io/oven/bun:alpine`. Це визнаний виняток із загального правила «лише `alpine`/`scratch` у фінальному stage» — тут потрібен саме bun-рантайм.
15
+
16
+ Це окрема гілка від генеричного compile-правила (`getBunCompileHint` у `../js/lint.mjs`): для проєктів **без** нативних аддонів канон лишається — standalone-бінарник на `alpine`.
17
+
18
+ Сусідній модуль `./docker-mirror.mjs` слугує взірцем структури dep-специфічного чек-модуля.
19
+
20
+ ## Експорти / API
21
+
22
+ Модуль експортує два константи (іменовані експорти) і три функції (іменовані експорти):
23
+
24
+ | Експорт | Тип | Призначення |
25
+ | -------------------------------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
26
+ | `NATIVE_ADDON_PACKAGES` | `readonly ['sharp', 'argon2']` | Точні імена відомих нативних аддонів. |
27
+ | `NATIVE_ADDON_SCOPES` | `readonly ['@img/']` | Scope-префікси, чиї пакети трактуються як нативні аддони. |
28
+ | `isNativeAddonPackage(name)` | `(name: string) => boolean` | Чи ім'я npm-пакета є нативним аддоном (точно або за scope-префіксом). |
29
+ | `getNativeAddonDeps(dependencies)` | `(dependencies: unknown) => string[]` | Відсортовані імена нативних аддонів, знайдених у `package.json#dependencies`. |
30
+ | `getNativeAddonNoCompileHint(fileContent, nativeAddons)` | `(fileContent: string, nativeAddons: string[]) => string \| null` | Текст hint-помилки, якщо у Dockerfile є `bun build --compile` при наявності нативного аддона. |
31
+
32
+ Default-експорт відсутній.
33
+
34
+ ## Функції
35
+
36
+ ### `isNativeAddonPackage(name)`
37
+
38
+ Сигнатура:
39
+
40
+ ```js
41
+ isNativeAddonPackage(name: string): boolean
42
+ ```
43
+
44
+ Параметри:
45
+
46
+ - `name` — ім'я npm-пакета (рядок). Очікується очищене ім'я ключа з `package.json#dependencies`, наприклад `"sharp"` або `"@img/sharp-linuxmusl-arm64"`.
47
+
48
+ Повертає:
49
+
50
+ - `true`, якщо `name` присутній у списку `NATIVE_ADDON_PACKAGES` (точне співпадіння), **або** починається з будь-якого з префіксів `NATIVE_ADDON_SCOPES` (наприклад, `@img/`).
51
+ - `false` в усіх інших випадках.
52
+
53
+ Алгоритм:
54
+
55
+ 1. Якщо `NATIVE_ADDON_PACKAGES.includes(name)` — повернути `true`. Аргумент `name` кастомним каста `/** @type {never} */` сатисфіє `readonly`-літерал-тип константи.
56
+ 2. Інакше повернути результат `NATIVE_ADDON_SCOPES.some(scope => name.startsWith(scope))`.
57
+
58
+ Side effects: відсутні. Чиста функція.
59
+
60
+ ### `getNativeAddonDeps(dependencies)`
61
+
62
+ Сигнатура:
63
+
64
+ ```js
65
+ getNativeAddonDeps(dependencies: unknown): string[]
66
+ ```
67
+
68
+ Параметри:
69
+
70
+ - `dependencies` — значення поля `dependencies` з `package.json`. Тип навмисно `unknown` — функція сама перевіряє форму вхідних даних.
71
+
72
+ Повертає:
73
+
74
+ - Відсортований за локалізованим порядком (`String#localeCompare`) масив ключів `dependencies`, відфільтрованих через `isNativeAddonPackage`.
75
+ - Порожній масив `[]`, якщо `dependencies` має невалідну форму (falsy, не об'єкт, або масив), або якщо серед ключів немає нативних аддонів.
76
+
77
+ Алгоритм:
78
+
79
+ 1. Якщо `!dependencies` (null/undefined/інше falsy), або `typeof dependencies !== 'object'`, або `Array.isArray(dependencies)` — повернути `[]`.
80
+ 2. Зібрати ключі через `Object.keys(dependencies)`.
81
+ 3. Відфільтрувати через `isNativeAddonPackage`.
82
+ 4. Повернути копію, відсортовану через `.toSorted((a, b) => a.localeCompare(b))` (іммутабельне сортування, не мутує вихідний масив ключів).
83
+
84
+ Side effects: відсутні. Чиста функція; виклик `Object.keys` не мутує вхід.
85
+
86
+ ### `getNativeAddonNoCompileHint(fileContent, nativeAddons)`
87
+
88
+ Сигнатура:
89
+
90
+ ```js
91
+ getNativeAddonNoCompileHint(fileContent: string, nativeAddons: string[]): string | null
92
+ ```
93
+
94
+ Параметри:
95
+
96
+ - `fileContent` — повний вміст Dockerfile або Containerfile як рядок.
97
+ - `nativeAddons` — масив імен нативних аддонів, попередньо знайдених у `dependencies` (результат `getNativeAddonDeps`).
98
+
99
+ Повертає:
100
+
101
+ - Рядок з повідомленням-підказкою про порушення, якщо тригер спрацював.
102
+ - `null`, якщо порушень не виявлено.
103
+
104
+ Тригер (умови, що мають виконатись усі одночасно):
105
+
106
+ 1. `nativeAddons` — масив (`Array.isArray`) і непорожній.
107
+ 2. У `fileContent` знайдено патерн `bun build --compile` за регуляркою `BUN_BUILD_COMPILE_RE` = `/\bbun\s+build\b[^\n]*\s--compile\b/iu` (нечутлива до регістру, юнікод-режим; шукає `bun build ... --compile` у межах одного рядка, бо `[^\n]*` забороняє перехід).
108
+
109
+ Якщо тригер не спрацював — повертається `null`.
110
+
111
+ Формування повідомлення (коли тригер спрацював):
112
+
113
+ 1. Створюється масив `problems` з одним обов'язковим повідомленням про антипатерн `bun build --compile` + нативний аддон. Список аддонів вставляється через `nativeAddons.join(', ')`. Повідомлення містить інструкцію канонічного фіксу: прибрати compile-крок, ship-нути `node_modules` і запускати через `bun <entry>` на базі `mirror.gcr.io/oven/bun:alpine` (з посиланням на `docker.mdc`). Entry-файл рекомендується брати з `--outfile`-таргета, `package.json#main` або `scripts.start`; якщо однозначно визначити неможливо — лишити TODO-маркер, **не вгадувати**.
114
+ 2. Якщо у `fileContent` додатково присутній патерн `apk add ... vips` (регулярка `APK_ADD_VIPS_RE` = `/\bapk\s+add\b[^\n]*\bvips\b/iu`) — додається друге повідомлення про те, що цей `apk add vips` зайвий: системний `libvips` не лікує брак `sharp.node`, його треба видалити разом із compile-кроком.
115
+ 3. Усі повідомлення з'єднуються через роздільник `'\n - '` (новий рядок + 5 пробілів + `- `), щоб формат був придатний для рендеру в pretty-output лінтера як вкладений список.
116
+
117
+ Side effects: відсутні. Чиста функція; жодних звернень до файлової системи, мережі чи глобального стану.
118
+
119
+ ## Залежності
120
+
121
+ Зовнішні залежності модуля відсутні: ні npm-пакетів, ні Node-core модулів (`fs`/`path`/тощо) не імпортується. Файл — самодостатній.
122
+
123
+ Внутрішні константи модуля (приватні, не експортуються):
124
+
125
+ - `BUN_BUILD_COMPILE_RE` — `/\bbun\s+build\b[^\n]*\s--compile\b/iu`. Виявляє наявність флагу `--compile` біля `bun build` у межах одного рядка Dockerfile.
126
+ - `APK_ADD_VIPS_RE` — `/\bapk\s+add\b[^\n]*\bvips\b/iu`. Виявляє пакет `vips` серед аргументів `apk add` у межах одного рядка.
127
+
128
+ Зв'язки з іншими модулями репозиторію:
129
+
130
+ - Логічно є частиною правила `docker` (`npm/rules/docker/...`). Викликається з check-обгортки правила, яка читає Dockerfile/`package.json` і запускає `getNativeAddonDeps` + `getNativeAddonNoCompileHint`.
131
+ - Тематично пов'язаний із `../js/lint.mjs#getBunCompileHint` — там обробляється канон **без** нативних аддонів. Це окрема гілка з різним кінцевим артефактом (standalone-бінарник vs `bun <entry>`).
132
+ - За структурою копіює `./docker-mirror.mjs` — той самий клас dep-специфічного чек-модуля.
133
+
134
+ ## Потік виконання / Використання
135
+
136
+ Типовий сценарій інтеграції модуля у правило `docker`:
137
+
138
+ 1. Чекер правила читає `package.json` проєкту й бере поле `dependencies` (як `unknown`).
139
+ 2. Викликає `getNativeAddonDeps(dependencies)` — отримує відсортований список нативних аддонів (можливо порожній).
140
+ 3. Якщо список порожній — гілка перевірки завершується мовчки, нативно-аддонного антипатерну тут немає.
141
+ 4. Якщо список непорожній — чекер читає вміст Dockerfile/`Containerfile` як рядок і викликає `getNativeAddonNoCompileHint(fileContent, nativeAddons)`.
142
+ 5. Якщо результат — рядок, чекер додає його як помилку (зі схемою «hint») і пропонує канонічний фікс (відмова від `bun build --compile`; за наявності зайвого `apk add vips` — і його видалення).
143
+ 6. Якщо результат — `null`, чекер вважає Dockerfile сумісним з нативним аддоном.
144
+
145
+ Приклад використання (псевдокод):
146
+
147
+ ```js
148
+ import { getNativeAddonDeps, getNativeAddonNoCompileHint } from './docker-native-addon.mjs'
149
+
150
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'))
151
+ const nativeAddons = getNativeAddonDeps(pkg.dependencies)
152
+ if (nativeAddons.length > 0) {
153
+ const dockerfile = await readFile('Dockerfile', 'utf8')
154
+ const hint = getNativeAddonNoCompileHint(dockerfile, nativeAddons)
155
+ if (hint) reportError(hint)
156
+ }
157
+ ```
158
+
159
+ Властивості та інваріанти, на які покладаються виклики:
160
+
161
+ - `getNativeAddonDeps` толерує будь-який вхід (`unknown`) і не кидає помилку.
162
+ - `getNativeAddonNoCompileHint` толерує невалідний `nativeAddons` (через `Array.isArray`-перевірку) — повертає `null`, замість того щоб впасти.
163
+ - Усі функції — чисті, без I/O, тож їх легко юніт-тестувати: достатньо передати рядкові константи й перевірити форму результату.
164
+ - Регулярки навмисно прості (одна-рядкові, через `[^\n]*`), щоб уникати false-positive на багаторядкових heredoc/RUN-блоках; ціна — потрібно, щоб `bun build --compile` і `apk add vips` були записані в межах одного рядка (стандартна практика Dockerfile).
165
+
166
+ Розширення модуля:
167
+
168
+ - Додати новий точний пакет — внести його у `NATIVE_ADDON_PACKAGES`.
169
+ - Додати новий scope, увесь вміст якого треба вважати нативним аддоном, — внести префікс у `NATIVE_ADDON_SCOPES` (з кінцевим `/`).
170
+ - Додаткові підказки про супутні зайві системні залежності (за аналогією з `apk add vips`) додавайте через нові регулярки + умовний `problems.push(...)`.