@nitra/cursor 3.21.1 → 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 (231) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +37 -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/js/lint-findings.mjs +110 -0
  74. package/rules/js-lint/js/lint.mjs +86 -15
  75. package/rules/js-lint-ci/docs/fix.md +154 -0
  76. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  77. package/rules/js-mssql/docs/fix.md +128 -0
  78. package/rules/js-mssql/js/docs/deps.md +263 -0
  79. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  80. package/rules/js-run/docs/fix.md +144 -0
  81. package/rules/js-run/js/docs/runtime.md +388 -0
  82. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  83. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  84. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  85. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  86. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  87. package/rules/k8s/docs/fix.md +129 -0
  88. package/rules/k8s/js/docs/manifests.md +344 -0
  89. package/rules/k8s/js/manifests.mjs +6 -2
  90. package/rules/k8s/k8s.mdc +4 -2
  91. package/rules/k8s/lint/docs/lint.md +411 -0
  92. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  93. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  94. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  95. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  96. package/rules/npm-module/docs/fix.md +98 -0
  97. package/rules/npm-module/js/docs/package_structure.md +274 -0
  98. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  99. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  100. package/rules/php/docs/fix.md +107 -0
  101. package/rules/php/js/docs/tooling.md +152 -0
  102. package/rules/php/lint/docs/lint.md +215 -0
  103. package/rules/python/docs/fix.md +163 -0
  104. package/rules/python/js/docs/applies.md +108 -0
  105. package/rules/python/js/docs/tooling.md +153 -0
  106. package/rules/python/lint/docs/lint.md +322 -0
  107. package/rules/rego/docs/fix.md +121 -0
  108. package/rules/rego/js/docs/applies.md +174 -0
  109. package/rules/rego/js/docs/lint.md +118 -0
  110. package/rules/rego/lint/docs/lint.md +204 -0
  111. package/rules/release/docs/change.md +185 -0
  112. package/rules/release/docs/fix.md +119 -0
  113. package/rules/release/docs/release.md +222 -0
  114. package/rules/release/lib/docs/aggregate.md +246 -0
  115. package/rules/release/lib/docs/change-file.md +200 -0
  116. package/rules/release/lib/docs/fallback.md +203 -0
  117. package/rules/rust/docs/fix.md +129 -0
  118. package/rules/rust/js/docs/applies.md +140 -0
  119. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  120. package/rules/security/docs/fix.md +86 -0
  121. package/rules/security/js/docs/lint.md +171 -0
  122. package/rules/security/js/docs/sample_secret.md +190 -0
  123. package/rules/security/js/docs/trufflehog.md +137 -0
  124. package/rules/security/js/lint.mjs +9 -1
  125. package/rules/style-lint/docs/fix.md +155 -0
  126. package/rules/style-lint/js/docs/lint.md +184 -0
  127. package/rules/style-lint/js/docs/tooling.md +194 -0
  128. package/rules/tauri/docs/fix.md +158 -0
  129. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  130. package/rules/tauri/js/docs/tooling.md +228 -0
  131. package/rules/test/coverage/coverage.mjs +15 -3
  132. package/rules/test/docs/fix.md +132 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  135. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  136. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  137. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  138. package/rules/test/js/docs/location.md +136 -0
  139. package/rules/test/js/docs/no-process-chdir.md +160 -0
  140. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  141. package/rules/test/js/docs/stryker_config.md +152 -0
  142. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  143. package/rules/text/docs/fix.md +118 -0
  144. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  145. package/rules/text/js/docs/formatting.md +256 -0
  146. package/rules/text/js/docs/lint.md +122 -0
  147. package/rules/text/lint/docs/lint.md +220 -0
  148. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  149. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  150. package/rules/text/lint/docs/run-v8r.md +197 -0
  151. package/rules/vue/docs/fix.md +127 -0
  152. package/rules/vue/js/docs/packages.md +335 -0
  153. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  154. package/rules/worktree/docs/fix.md +161 -0
  155. package/schemas/rule-meta.json +5 -1
  156. package/scripts/auto-rules.mjs +7 -4
  157. package/scripts/coverage-classify/docs/apply.md +202 -0
  158. package/scripts/coverage-classify/docs/cache.md +203 -0
  159. package/scripts/coverage-classify/docs/index.md +218 -0
  160. package/scripts/coverage-classify/docs/prompt.md +132 -0
  161. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  162. package/scripts/coverage-fix-extract.mjs +122 -0
  163. package/scripts/coverage-fix.mjs +1 -1
  164. package/scripts/dispatcher/docs/graph.md +346 -0
  165. package/scripts/dispatcher/docs/index.md +236 -0
  166. package/scripts/dispatcher/docs/trace.md +296 -0
  167. package/scripts/dispatcher/index.mjs +1 -1
  168. package/scripts/dispatcher/lib/active.mjs +4 -8
  169. package/scripts/dispatcher/lib/commands.mjs +7 -11
  170. package/scripts/dispatcher/lib/docs/active.md +348 -0
  171. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  172. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  173. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  174. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  175. package/scripts/dispatcher/lib/docs/events.md +182 -0
  176. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  177. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  178. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  179. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  180. package/scripts/dispatcher/lib/docs/level.md +335 -0
  181. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  182. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  183. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  184. package/scripts/dispatcher/lib/docs/review.md +255 -0
  185. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  186. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  187. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  188. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  189. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  190. package/scripts/dispatcher/lib/executor.mjs +6 -1
  191. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  192. package/scripts/dispatcher/lib/level.mjs +29 -3
  193. package/scripts/dispatcher/lib/review.mjs +1 -1
  194. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  195. package/scripts/docs/auto-rules.md +376 -0
  196. package/scripts/docs/auto-skills.md +173 -0
  197. package/scripts/docs/build-agents-commands.md +183 -0
  198. package/scripts/docs/cli-entry.md +153 -0
  199. package/scripts/docs/coverage-fix.md +177 -0
  200. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  201. package/scripts/lib/changed-files.mjs +4 -1
  202. package/scripts/lib/diff-added-lines.mjs +85 -0
  203. package/scripts/lib/docs/changed-files.md +149 -0
  204. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  205. package/scripts/lib/docs/check-reporter.md +175 -0
  206. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  207. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  208. package/scripts/lib/docs/ensure-tool.md +254 -0
  209. package/scripts/lib/docs/generated-markdown.md +275 -0
  210. package/scripts/lib/docs/gha-workflow.md +326 -0
  211. package/scripts/lib/docs/inline-template-links.md +303 -0
  212. package/scripts/lib/docs/list-rule-ids.md +156 -0
  213. package/scripts/lib/docs/load-cursor-config.md +147 -0
  214. package/scripts/lib/docs/mirror-parity.md +167 -0
  215. package/scripts/lib/worktree.mjs +26 -0
  216. package/scripts/worktree-cli.mjs +12 -2
  217. package/skills/coverage-fix/SKILL.md +34 -45
  218. package/skills/docgen/SKILL.md +44 -23
  219. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  220. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  221. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  222. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  223. package/skills/docgen/js/docgen-scan.mjs +37 -21
  224. package/skills/llm-patch/SKILL.md +23 -2
  225. package/skills/start-check/SKILL.md +26 -53
  226. package/skills/start-check/js/check.mjs +211 -0
  227. package/skills/taze/SKILL.md +9 -3
  228. package/skills/taze/js/diff.mjs +154 -0
  229. package/types/bin/n-cursor.d.ts +1 -1
  230. package/skills/fix-tests/SKILL.md +0 -119
  231. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,219 @@
1
+ # docker-nginx-user.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `docker-nginx-user.mjs` — це чек-модуль для статичного аналізу Dockerfile (Containerfile), що спеціалізується на фінальному (runtime) `FROM`-stage на базі офіційного образу `nginxinc/nginx-unprivileged`. Належить до набору правил `npm/rules/docker/lib/` і викликається з агрегованого Docker-lint-набору.
6
+
7
+ Файл вирішує дві суміжні задачі для nginx-unprivileged-stage:
8
+
9
+ 1. **Заборона зайвого `USER`.** Базовий образ `nginxinc/nginx-unprivileged` уже декларує `USER 101` (uid=101) та `EXPOSE 8080`. Будь-яка явна інструкція `USER …` у фінальному stage — це порушення канону:
10
+ - `USER root` (`USER 0`) перезатирає успадкований `USER 101`. Якщо потім non-root не повернути, фінальний образ лишається root, а Kubernetes-pod із `securityContext.runAsNonRoot: true` падає у стан `CreateContainerConfigError`.
11
+ - kubelet перевіряє non-root лише за **числовим** UID, а не за іменем користувача (`nginx`). Тому повернення `USER 101` чи `USER nginx` наприкінці stage саме по собі — симптом зайвого попереднього `USER root`.
12
+ - Безпечний канон — взагалі не виходити з-під дефолтного uid=101: ні `USER root`, ні switch-back. Тому **будь-який** явний `USER`-токен у nginx-unprivileged-stage прапорцюється як зайвий, з диференційованим повідомленням залежно від токена.
13
+
14
+ 2. **Заборона `COPY`/`ADD` без `--chown`.** Без явного `--chown` файли копіюються власником `root:root` і не читаються процесом nginx, який працює від uid=101. Канонічна форма — `COPY --chown=nginx:nginx …` / `ADD --chown=nginx:nginx …`.
15
+
16
+ Тригер модуля — **лише** фінальний (останній у файлі) `FROM`, який базується на `nginxinc/nginx-unprivileged` (з урахуванням можливих префіксів дзеркала на кшталт `mirror.gcr.io/…` чи `docker.io/…` та будь-якого тега/digest). Build-stage-и не перевіряються — там root і інструменти-помічники є нормою.
17
+
18
+ Цей чек — це окрема гілка від генеричного non-root-правила для alpine-бекендів (де канон, навпаки, `addgroup`+`adduser`+`USER app`; див. `getNonRootRuntimeHint` у `../js/lint.mjs`).
19
+
20
+ Шаблон структури base-image-специфічного чек-модуля — сусідній `./docker-mirror.mjs`.
21
+
22
+ ## Експорти / API
23
+
24
+ Модуль експортує дві публічні функції (ESM-named exports):
25
+
26
+ | Експорт | Тип | Призначення |
27
+ | ------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `isNginxUnprivilegedImage(image)` | `(string) => boolean` | Перевірити, чи токен `FROM`-образу вказує на `nginxinc/nginx-unprivileged` (з будь-яким тегом і необов'язковим префіксом дзеркала). |
29
+ | `getNginxUnprivilegedUserHint(fileContent)` | `(string) => (string \| null)` | Перевірити вміст Dockerfile і повернути готове повідомлення помилки про порушення в nginx-unprivileged-stage, або `null`, якщо порушень немає, або це не nginx-unprivileged-stage, або у файлі взагалі немає `FROM`. |
30
+
31
+ Внутрішні (не експортуються): `getFinalStage`, `normalizeUserToken`, регулярні вирази `NEWLINE_RE`, `USER_LINE_RE`, `COPY_ADD_RE`, `CHOWN_FLAG_RE`, `NGINX_UNPRIVILEGED_REPO_RE`.
32
+
33
+ JSDoc-тип, що використовується внутрішньо:
34
+
35
+ ```
36
+ @typedef {{ image: string, lines: Array<{ lineNo: number, text: string }> }} FinalStage
37
+ ```
38
+
39
+ ## Функції
40
+
41
+ ### `isNginxUnprivilegedImage(image)`
42
+
43
+ **Сигнатура:** `export function isNginxUnprivilegedImage(image: string): boolean`
44
+
45
+ **Параметри:**
46
+
47
+ - `image` (`string`) — токен образу, який ідентифікує посилання після інструкції `FROM` у Dockerfile. Допускається `undefined`/`null`/порожній рядок: функція захищена від цього через `(image || '').trim()`.
48
+
49
+ **Повертає:** `boolean` — `true`, якщо `image` (після тримінгу) збігається з регулярним виразом `NGINX_UNPRIVILEGED_REPO_RE`, тобто містить шлях `nginxinc/nginx-unprivileged`, який або стоїть на початку рядка, або після `/` (це покриває префікси дзеркал на зразок `mirror.gcr.io/nginxinc/nginx-unprivileged` чи `docker.io/nginxinc/nginx-unprivileged`), і за яким іде `:` (тег), `@` (digest) або кінець рядка. Інакше `false`.
50
+
51
+ **Side effects:** немає. Чиста, ідемпотентна функція.
52
+
53
+ **Деталі реалізації:** регулярний вираз нечутливий до регістру (прапор `iu`):
54
+
55
+ ```
56
+ /(?:^|\/)nginxinc\/nginx-unprivileged(?::|@|$)/iu
57
+ ```
58
+
59
+ ### `getNginxUnprivilegedUserHint(fileContent)`
60
+
61
+ **Сигнатура:** `export function getNginxUnprivilegedUserHint(fileContent: string): string | null`
62
+
63
+ **Параметри:**
64
+
65
+ - `fileContent` (`string`) — повний текстовий вміст Dockerfile або Containerfile (з можливими `\r\n` чи `\n` як роздільниками рядків).
66
+
67
+ **Повертає:** `string | null`.
68
+
69
+ - `null`, якщо:
70
+ - у файлі немає жодного `FROM` (`getFinalStage` повертає `null`);
71
+ - фінальний stage не базується на `nginxinc/nginx-unprivileged`;
72
+ - у фінальному stage не знайдено жодного порушення.
73
+ - Інакше — рядок із зібраними повідомленнями про порушення. Кожне повідомлення на окремому рядку, склеєне роздільником `'\n - '` (символ нового рядка + п'ять пробілів + дефіс із пробілом). Такий роздільник розрахований на форматування у bullet-list у звітах лінтера.
74
+
75
+ **Кроки алгоритму:**
76
+
77
+ 1. Виділити фінальний stage функцією `getFinalStage(fileContent)`. Якщо `null` — повернути `null`.
78
+ 2. Перевірити `stage.image` функцією `isNginxUnprivilegedImage`. Якщо не nginx-unprivileged — повернути `null`.
79
+ 3. Для кожного рядка stage (включно з самим `FROM` — на цьому рядку `USER`/`COPY` неможливі, але цикл лишається загальним для уніфікації):
80
+ - Спробувати зматчити `USER_LINE_RE`. Якщо є збіг — захопити перший аргумент `USER`, нормалізувати через `normalizeUserToken` і додати одне з трьох повідомлень:
81
+ - якщо токен — `root` або `0`: підказка прибрати `USER root`/`USER 0`, бо без switch-back образ лишиться root і k8s `runAsNonRoot` впаде;
82
+ - якщо токен — `101` або `nginx`: підказка прибрати зайвий switch-back — база вже працює від uid=101, повернення USER — симптом зайвого `USER root`;
83
+ - інакше: підказка прибрати будь-який інший явний `USER` — окремий `USER` не потрібен.
84
+ - Після обробки `USER` зробити `continue` (рядок із `USER` уже не може бути одночасно `COPY`/`ADD`).
85
+ - Якщо рядок не `USER`, зматчити `COPY_ADD_RE`. Якщо це `COPY` чи `ADD` **і** в рядку немає прапорця `--chown=` (перевірка `CHOWN_FLAG_RE`) — додати повідомлення з підказкою додати `--chown=nginx:nginx`. У повідомленні інструкція приводиться до верхнього регістру через `.toUpperCase()`.
86
+ 4. Якщо масив `problems` порожній — повернути `null`, інакше — `problems.join('\n - ')`.
87
+
88
+ **Side effects:** немає. Не читає файлову систему, не виконує I/O, не модифікує аргументи.
89
+
90
+ ### `getFinalStage(fileContent)` (внутрішня)
91
+
92
+ **Сигнатура:** `function getFinalStage(fileContent: string): FinalStage | null`
93
+
94
+ **Параметри:**
95
+
96
+ - `fileContent` (`string`) — вміст Dockerfile/Containerfile.
97
+
98
+ **Повертає:** об'єкт `{ image, lines }` або `null`, якщо у файлі немає `FROM`.
99
+
100
+ - `image` — токен образу останнього `FROM` (значення, повернене `getFromImageToken` із `./docker-mirror.mjs`);
101
+ - `lines` — масив об'єктів `{ lineNo, text }`, де `lineNo` — людиночитабельний номер рядка (1-based), `text` — текст рядка. Масив містить усі рядки **від рядка останнього `FROM` і до кінця файла**.
102
+
103
+ **Алгоритм:**
104
+
105
+ 1. Розбити `fileContent` на рядки регулярним виразом `NEWLINE_RE` (`/\r?\n/`).
106
+ 2. Пройти всі рядки, обчислити `getFromImageToken(line)`. Якщо результат істинний — оновити `lastFrom = { image, idx }`. Це гарантує, що `lastFrom` зрештою вкаже на **останній** `FROM` у файлі (фінальний stage).
107
+ 3. Якщо `lastFrom === null` — повернути `null`.
108
+ 4. Інакше — вирізати з `lines` хвіст починаючи з `lastFrom.idx` і змапити в `{ lineNo: lastFrom.idx + i + 1, text }`. Зверніть увагу: `lineNo` обчислюється як зсув від `lastFrom.idx` плюс зміщення в хвості плюс 1 — це коректно конвертує 0-based індекс у людиночитабельний 1-based номер рядка вихідного файлу.
109
+
110
+ **Side effects:** немає.
111
+
112
+ ### `normalizeUserToken(token)` (внутрішня)
113
+
114
+ **Сигнатура:** `function normalizeUserToken(token: string): string`
115
+
116
+ **Параметри:**
117
+
118
+ - `token` (`string`) — захоплена група після ключового слова `USER` (один токен — або UID, або ім'я користувача, без пробілів і коментарів).
119
+
120
+ **Повертає:** нормалізований рядок:
121
+
122
+ 1. Видаляються подвійні лапки (`"`) через `replaceAll('"', '')`.
123
+ 2. Видаляються одинарні лапки (`'`) через `replaceAll("'", '')`.
124
+ 3. Тримаються пробіли по краях (`.trim()`).
125
+ 4. Все приводиться до нижнього регістру (`.toLowerCase()`).
126
+
127
+ **Призначення:** дозволяє уніфіковано порівнювати токен із константами `'root'`, `'0'`, `'101'`, `'nginx'` незалежно від того, чи був він написаний як `Root`, `"nginx"`, `'101'` тощо.
128
+
129
+ **Side effects:** немає.
130
+
131
+ ## Константи та регулярні вирази
132
+
133
+ | Ім'я | Значення | Призначення |
134
+ | ---------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
135
+ | `NEWLINE_RE` | `/\r?\n/` | Розбиття рядка на лінії з підтримкою CRLF. |
136
+ | `USER_LINE_RE` | `/^\s*USER\s+([^\s#]+)/iu` | Розпізнавання інструкції `USER` на початку рядка (допускається ведучий whitespace), захоплює перший токен-аргумент до пробілу чи `#`. Прапори: case-insensitive, Unicode. |
137
+ | `COPY_ADD_RE` | `/^\s*(COPY\|ADD)\b(.*)$/iu` | Розпізнавання інструкцій `COPY` чи `ADD` на початку рядка. Перша група захоплює саме ім'я інструкції, друга — решту рядка (для діагностики не використовується, окрім потенційних розширень). |
138
+ | `CHOWN_FLAG_RE` | `/(?:^\|\s)--chown=/iu` | Виявлення прапорця `--chown=` у рядку `COPY`/`ADD`. Перевіряє, що прапор стоїть на початку або після whitespace, щоб не сплутати з підрядком в імені файлу. |
139
+ | `NGINX_UNPRIVILEGED_REPO_RE` | `/(?:^\|\/)nginxinc\/nginx-unprivileged(?::\|@\|$)/iu` | Точне розпізнавання репозиторію `nginxinc/nginx-unprivileged` — або на початку токена, або після `/` (для дзеркал на зразок `mirror.gcr.io/…`), з допустимим суфіксом `:tag`, `@digest` чи кінцем рядка. |
140
+
141
+ ## Залежності
142
+
143
+ ### Імпорти модуля
144
+
145
+ - `getFromImageToken` із `./docker-mirror.mjs` — повертає токен образу з рядка `FROM …` (або `null`/`undefined` для не-`FROM`-рядків). Використовується для пошуку **останнього** `FROM` у файлі. Стиль реалізації цього модуля прямо успадковано від `docker-mirror.mjs` (він названий шаблоном для base-image-специфічних чеків).
146
+
147
+ ### Зовнішні залежності
148
+
149
+ - Жодних npm-пакетів. Тільки стандартні можливості JavaScript (`String.prototype.split`, `String.prototype.match`, `String.prototype.replaceAll`, `Array.prototype.entries`, `Array.prototype.slice`, `Array.prototype.map`, регулярні вирази, шаблонні рядки).
150
+
151
+ ### Сторонні припущення / контекст
152
+
153
+ - Модуль не виконує файлового I/O — він приймає вже прочитаний рядок із вмістом Dockerfile.
154
+ - Очікується, що `getFromImageToken` повертає очищений токен (без коментарів, без `AS <alias>`), або falsy-значення для рядків без `FROM`.
155
+ - Модуль розрахований на каноні **дзеркал**: `mirror.gcr.io/nginxinc/nginx-unprivileged:<tag>`, `docker.io/nginxinc/nginx-unprivileged:<tag>`, чисте `nginxinc/nginx-unprivileged:<tag>` тощо. Локальні префікси на зразок `localhost:5000/nginxinc/nginx-unprivileged` також спрацюють — головне, щоб `nginxinc/nginx-unprivileged` стояло після `/`.
156
+
157
+ ## Потік виконання / Використання
158
+
159
+ Типовий сценарій вбудовування — у Docker-lint-набір, що ітерує всі Dockerfile у репозиторії й агрегує `null`/`string`-повідомлення кількох чек-функцій:
160
+
161
+ ```
162
+ import { readFile } from 'node:fs/promises'
163
+ import { getNginxUnprivilegedUserHint } from './docker-nginx-user.mjs'
164
+
165
+ const content = await readFile('Dockerfile', 'utf8')
166
+ const hint = getNginxUnprivilegedUserHint(content)
167
+ if (hint) {
168
+ console.error(`Dockerfile:\n - ${hint}`)
169
+ process.exitCode = 1
170
+ }
171
+ ```
172
+
173
+ Послідовність роботи `getNginxUnprivilegedUserHint`:
174
+
175
+ 1. Викликає `getFinalStage(fileContent)` — розбиває файл на рядки, шукає **останній** `FROM`. Якщо `FROM` немає — `null`.
176
+ 2. Викликає `isNginxUnprivilegedImage(stage.image)` — швидкий regex-фільтр базового образу. Якщо це не `nginxinc/nginx-unprivileged` — `null` (тобто чек ігнорує сторонні образи й залишає їх іншим правилам).
177
+ 3. Ітерує рядки фінального stage. Кожен рядок класифікується одним із трьох сценаріїв:
178
+ - **`USER …`** — порушення завжди (для трьох категорій токенів — різні повідомлення):
179
+ - `root`/`0` — попередження про root-escape;
180
+ - `101`/`nginx` — попередження про зайвий switch-back;
181
+ - інше — попередження про зайвий явний `USER`.
182
+ - **`COPY …` / `ADD …` без `--chown=`** — порушення, потрібно додати `--chown=nginx:nginx`.
183
+ - решта (`RUN`, `ENV`, `EXPOSE`, `ENTRYPOINT`, `CMD`, коментарі, порожні рядки тощо) — ігнорується.
184
+ 4. Якщо порушень не знайдено — `null`. Інакше всі повідомлення склеюються в один рядок із роздільником `'\n - '` (відформатовано для bullet-list).
185
+
186
+ **Приклади поведінки:**
187
+
188
+ - Dockerfile без `FROM` → `null`.
189
+ - Dockerfile, де **останній** `FROM` — `node:20-alpine`, навіть якщо раніше був `nginxinc/nginx-unprivileged` як build-stage → `null` (чек дивиться лише на фінальний stage).
190
+ - `FROM mirror.gcr.io/nginxinc/nginx-unprivileged:1.27` без `USER` і з `COPY --chown=nginx:nginx ./dist /usr/share/nginx/html` → `null`.
191
+ - `FROM nginxinc/nginx-unprivileged:1.27` + `USER root` + `RUN apk add …` + `USER 101` + `COPY ./dist /usr/share/nginx/html` → повідомлення з трьома bullet-пунктами: про `USER root`, про switch-back `USER 101`, про `COPY` без `--chown`.
192
+
193
+ ## Rebuild Test
194
+
195
+ Після гіпотетичного перепису модуля з нуля для перевірки еквівалентної поведінки використовуйте такий набір кейсів (мінімум, що відтворює всі гілки):
196
+
197
+ 1. **Файл без `FROM`** (`''`, `'# comment\n'`) → `getNginxUnprivilegedUserHint` повертає `null`.
198
+ 2. **Фінальний `FROM` — інший образ** (`'FROM node:20-alpine\n'`) → `null`.
199
+ 3. **Фінальний `FROM` — `nginxinc/nginx-unprivileged` без зайвих інструкцій** (`'FROM nginxinc/nginx-unprivileged:1.27\nCOPY --chown=nginx:nginx ./dist /usr/share/nginx/html\n'`) → `null`.
200
+ 4. **`USER root` усередині nginx-stage** → одне повідомлення з підстрокою `прибери \`USER root\`` і коректним номером рядка.
201
+ 5. **`USER 0`** → повідомлення з підстрокою `прибери \`USER 0\``.
202
+ 6. **`USER 101`** → повідомлення з підстрокою про зайвий switch-back і `uid=101`.
203
+ 7. **`USER nginx`** (у тому числі `USER "nginx"`, `USER 'NGINX'`) → той самий тип повідомлення (нормалізація лапок і регістру).
204
+ 8. **`USER app`** (інше ім'я) → повідомлення з підстрокою про зайвий явний `USER`.
205
+ 9. **`COPY ./a ./b`** у nginx-stage → повідомлення `додай \`--chown=nginx:nginx\` до \`COPY\``.
206
+ 10. **`ADD ./a ./b`** у nginx-stage → повідомлення з `\`ADD\``(верхній регістр, навіть якщо в Dockerfile було`add`).
207
+ 11. **`COPY --chown=nginx:nginx ./a ./b`** → не прапорцюється.
208
+ 12. **Кілька порушень одночасно** (USER root + USER 101 + COPY без chown) → рядок із трьома пунктами, склеєними `'\n - '`.
209
+ 13. **Префікс дзеркала**: `FROM mirror.gcr.io/nginxinc/nginx-unprivileged:1.27` → активує перевірку. `FROM docker.io/nginxinc/nginx-unprivileged@sha256:…` → також активує.
210
+ 14. **Регістр і пробіли**: рядок ` user ROOT ` (нижній регістр інструкції, лідуючі пробіли, верхній регістр аргументу) → ловиться як `USER root`.
211
+ 15. **Build-stage із `nginxinc/nginx-unprivileged`, але фінальний — інший** → `null` (чек ігнорує не-фінальні stage).
212
+ 16. **`isNginxUnprivilegedImage` окремо**:
213
+ - `'nginxinc/nginx-unprivileged:1.27'` → `true`;
214
+ - `'mirror.gcr.io/nginxinc/nginx-unprivileged@sha256:abc'` → `true`;
215
+ - `'nginx:1.27'` → `false`;
216
+ - `''`/`null`/`undefined` → `false` (без винятків).
217
+ 17. **Номер рядка**: для файлу з порожніми/коментованими рядками перед фінальним `FROM`, повідомлення повинне посилатися на 1-based номер рядка вихідного файла, а не на зміщення всередині stage.
218
+
219
+ Якщо повний набір кейсів проходить — поведінка переписаного модуля еквівалентна оригіналу.
@@ -0,0 +1,193 @@
1
+ # lint.mjs — реалізація підкоманди `lint-docker`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `npm/rules/docker/lint/lint.mjs` реалізує підкоманду `lint-docker` інструмента `n-cursor`. Її задача — знайти у дереві репозиторію канонічні Dockerfile-и та пропустити їх через `hadolint`, фіксуючи результат у форматі звіту `check-reporter` і виставляючи числовий код виходу.
6
+
7
+ Особливості, що визначають поведінку:
8
+
9
+ - Перевіряються **лише** файли з іменем, що рівне `Dockerfile` (регістр не важливий) та файли, ім’я яких закінчується на суфікс `.dockerfile` (наприклад, `app.Dockerfile`, `worker.dockerfile`). Файли виду `Dockerfile.dev`, `Containerfile` й інші варіанти, які бере `check-docker`, тут навмисно **не** обробляються.
10
+ - Обхід дерева виконується тим самим `walkDir`, що й у `check-docker`, з тими самими пропусками каталогів (на основі `.cursorignore`/конфігу Cursor).
11
+ - Виклик `hadolint` йде через `lintDockerfileWithHadolint` із `../lib/docker-hadolint.mjs` — інструмент шукається у `PATH` або запускається в Docker (`docker run`).
12
+ - Серіалізація важкої CLI-команди (тільки один паралельний прогін на репозиторій і дедуплікація за станом git-дерева) виконана через `runStandardLint` — це канонічний патерн для `lint-*` команд згідно з `.cursor/rules/scripts.mdc`, секція «Серіалізація важких CLI-команд». Прямий `withLock` тут не використовується.
13
+ - Запуск файла напряму через Node (`node lint.mjs`) працює як CLI: модуль ставить `process.exitCode` у код, повернений `runLintDocker`.
14
+
15
+ ## Експорти / API
16
+
17
+ | Експорт | Вид | Призначення |
18
+ | ------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------- |
19
+ | `isLintDockerfileName` | `function` | Перевіряє basename файла: чи входить він до набору `lint-docker`. |
20
+ | `findLintDockerfilePaths` | `async function` | Збирає відсортований список абсолютних шляхів придатних для `lint-docker` файлів. |
21
+ | `runLintDocker` | `() => Promise<number>` | Публічна CLI-форма команди `lint-docker`: серіалізований прогон з кешуванням за станом дерева. |
22
+
23
+ Внутрішня (не експортована) функція:
24
+
25
+ - `runLintDockerSteps()` — самі кроки lint-у без обгортки локу й дедупу. Викликається `runStandardLint`.
26
+
27
+ CLI-режим (`if (isRunAsCli(import.meta.url))`) при прямому запуску файла ставить `process.exitCode = await runLintDocker()`.
28
+
29
+ ## Функції
30
+
31
+ ### `isLintDockerfileName(name)`
32
+
33
+ ```js
34
+ export function isLintDockerfileName(name): boolean
35
+ ```
36
+
37
+ - **Параметри:**
38
+ - `name: string` — basename шляху (тобто лише ім’я файла, без каталогів).
39
+ - **Повертає:** `boolean` — `true`, якщо файл підходить під набір `lint-docker`, інакше `false`.
40
+ - **Логіка:**
41
+ 1. Зводить ім’я до нижнього регістру (`n`).
42
+ 2. Якщо `n === 'dockerfile'` — повертає `true`.
43
+ 3. Інакше повертає `true`, лише якщо `n` закінчується на `.dockerfile` **і** довжина `n` строго більша за довжину суфікса `.dockerfile`. Завдяки цій додатковій умові саме `.dockerfile` (без префікса) — не валідний кейс.
44
+ - **Side effects:** немає.
45
+ - **Приклади:**
46
+ - `isLintDockerfileName('Dockerfile')` → `true`
47
+ - `isLintDockerfileName('dockerfile')` → `true`
48
+ - `isLintDockerfileName('app.Dockerfile')` → `true`
49
+ - `isLintDockerfileName('worker.dockerfile')` → `true`
50
+ - `isLintDockerfileName('.dockerfile')` → `false`
51
+ - `isLintDockerfileName('Dockerfile.dev')` → `false`
52
+ - `isLintDockerfileName('Containerfile')` → `false`
53
+
54
+ ### `findLintDockerfilePaths(root, ignorePaths = [])`
55
+
56
+ ```js
57
+ export async function findLintDockerfilePaths(root, ignorePaths = []): Promise<string[]>
58
+ ```
59
+
60
+ - **Параметри:**
61
+ - `root: string` — корінь обходу (зазвичай корінь репозиторію).
62
+ - `ignorePaths?: string[]` — абсолютні шляхи каталогів, повністю виключених з обходу (за замовчуванням пустий масив).
63
+ - **Повертає:** `Promise<string[]>` — масив абсолютних шляхів придатних файлів, відсортований за `String.prototype.localeCompare` через `Array.prototype.toSorted` (не мутує проміжний масив).
64
+ - **Логіка:**
65
+ 1. Створює локальний акумулятор `out`.
66
+ 2. Викликає `walkDir(root, visit, ignorePaths)`, де `visit(p)` додає `p` до `out`, якщо `isLintDockerfileName(basename(p))` істинне.
67
+ 3. Повертає `out.toSorted((a, b) => a.localeCompare(b))`.
68
+ - **Side effects:** виконує асинхронний обхід файлової системи через `walkDir`. Не пише нічого на диск і нічого не виводить у stdout/stderr.
69
+
70
+ ### `runLintDockerSteps()` _(internal)_
71
+
72
+ ```js
73
+ async function runLintDockerSteps(): Promise<number>
74
+ ```
75
+
76
+ - **Параметри:** немає.
77
+ - **Повертає:** `Promise<number>` — `reporter.getExitCode()`: `0`, якщо помилок не зафіксовано, `1`, якщо хоча б один `fail`.
78
+ - **Логіка:**
79
+ 1. Створює репортер: `const reporter = createCheckReporter(); const { pass, fail } = reporter`.
80
+ 2. Бере `root = process.cwd()`.
81
+ 3. Завантажує `ignorePaths` через `await loadCursorIgnorePaths(root)`.
82
+ 4. Шукає кандидатів: `files = await findLintDockerfilePaths(root, ignorePaths)`.
83
+ 5. Якщо `files.length === 0` — викликає `pass('lint-docker: немає Dockerfile / *.Dockerfile — hadolint пропущено')` і повертає `reporter.getExitCode()`.
84
+ 6. Інакше повідомляє `pass(\`lint-docker: файлів для hadolint: ${files.length}\`)`.
85
+ 7. Для кожного абсолютного шляху `abs`:
86
+ - Обчислює відносний шлях `rel = posixRel(root, abs) || basename(abs)` — якщо `posixRel` повернула порожній рядок, береться лише basename.
87
+ - Викликає `const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)` (синхронний виклик, як видно з відсутності `await`).
88
+ - Об’єднує `stdout + stderr`, тримує (`trim()`) у `tail`.
89
+ - Якщо `ok` — `pass(\`${rel} (${via})\`)`.
90
+ - Якщо ні — формує `detail = tail ? \`:\n${tail}\` : ''` і викликає `fail(\`${rel} (${via})${detail}\`)`.
91
+ 8. Повертає `reporter.getExitCode()`.
92
+ - **Side effects:**
93
+ - Читає поточну робочу директорію (`process.cwd()`).
94
+ - Читає файлову систему репозиторію через `walkDir`/`loadCursorIgnorePaths`.
95
+ - Запускає зовнішній процес `hadolint` (через `PATH` або через `docker run`) для кожного знайденого Dockerfile.
96
+ - Пише повідомлення у консоль/звіт через `pass`/`fail` репортера.
97
+
98
+ ### `runLintDocker`
99
+
100
+ ```js
101
+ export const runLintDocker = (): Promise<number> =>
102
+ runStandardLint(import.meta.dirname, runLintDockerSteps)
103
+ ```
104
+
105
+ - **Параметри:** немає.
106
+ - **Повертає:** `Promise<number>` — фінальний код виходу команди.
107
+ - **Логіка:** делегує виконання у `runStandardLint`, передаючи:
108
+ - `import.meta.dirname` — каталог цього модуля (`npm/rules/docker/lint`); `runStandardLint` використовує його як ідентифікатор/корінь для серіалізації та для дедуплікації запусків за станом git-дерева;
109
+ - `runLintDockerSteps` — асинхронну функцію з фактичними кроками lint-у.
110
+ - **Side effects:** усі побічні ефекти `runStandardLint` (взяття локу `lint-docker`, перевірка попереднього стану git, можливе пропускання запуску при ідентичному стані тощо) + усі ефекти `runLintDockerSteps`.
111
+
112
+ ### CLI-ентрі
113
+
114
+ ```js
115
+ if (isRunAsCli(import.meta.url)) {
116
+ process.exitCode = await runLintDocker()
117
+ }
118
+ ```
119
+
120
+ - **Поведінка:** при прямому запуску модуля як Node-скрипта (`node npm/rules/docker/lint/lint.mjs` або у вигляді ESM bin) встановлює `process.exitCode` у значення, повернене `runLintDocker()`. Імпорт як модуля з іншого файла не активує цю гілку.
121
+ - **Side effects:** мутує `process.exitCode`.
122
+
123
+ ## Залежності
124
+
125
+ ### Зовнішні (Node API)
126
+
127
+ - `node:path` — використовується лише `basename` для виділення імені файла зі шляху.
128
+
129
+ ### Внутрішні модулі репозиторію
130
+
131
+ | Імпорт | Що з нього береться | Роль у цьому модулі |
132
+ | --------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------- |
133
+ | `../../../scripts/cli-entry.mjs` | `isRunAsCli` | Визначення, чи модуль запущено як CLI напряму. |
134
+ | `../lib/docker-hadolint.mjs` | `lintDockerfileWithHadolint`, `posixRel` | Власне виклик `hadolint` (через PATH або `docker run`) і обчислення POSIX-відносного шляху. |
135
+ | `../../../scripts/lib/check-reporter.mjs` | `createCheckReporter` | Створення репортера з методами `pass`/`fail` і фінальним `getExitCode`. |
136
+ | `../../../scripts/lib/load-cursor-config.mjs` | `loadCursorIgnorePaths` | Завантаження списку каталогів, які потрібно ігнорувати під час обходу. |
137
+ | `../../../scripts/utils/walkDir.mjs` | `walkDir` | Асинхронний обхід дерева файлів з підтримкою списку ігнорувань. |
138
+ | `../../../scripts/lib/run-standard-lint.mjs` | `runStandardLint` | Канонічна обгортка `lint-*`-команд: лок, дедуп за станом git-дерева, уніфікований запуск. |
139
+
140
+ ### Зовнішні CLI/інструменти, що викликаються опосередковано
141
+
142
+ - `hadolint` — або з `PATH`, або через `docker run` (визначається в `lintDockerfileWithHadolint`).
143
+ - Опосередковано — `git` (для обчислення стану дерева всередині `runStandardLint`), `docker` (якщо `hadolint` запускається в контейнері).
144
+
145
+ ## Потік виконання / Використання
146
+
147
+ ### Послідовність дій при `n-cursor lint-docker`
148
+
149
+ 1. `bin/n-cursor.js` диспатчить підкоманду `lint-docker` на `runLintDocker` із цього модуля.
150
+ 2. `runLintDocker` → `runStandardLint(import.meta.dirname, runLintDockerSteps)`:
151
+ - бере серіалізаційний лок на ім’я `lint-docker`;
152
+ - перевіряє стан git-дерева; якщо стан збігається з попереднім успішним прогоном — крок може бути пропущено (дедуп);
153
+ - інакше викликає `runLintDockerSteps`.
154
+ 3. `runLintDockerSteps`:
155
+ - читає `cwd`;
156
+ - завантажує `ignorePaths`;
157
+ - обходить дерево і збирає Dockerfile-кандидатів (тільки `Dockerfile` і `*.Dockerfile`/`*.dockerfile`);
158
+ - якщо нічого не знайдено — фіксує `pass` про пропуск і виходить;
159
+ - інакше повідомляє кількість файлів і по кожному викликає `lintDockerfileWithHadolint`;
160
+ - кожен результат маркується як `pass` або `fail` (з прикладеним хвостом `stdout`+`stderr`).
161
+ 4. Підсумковий `reporter.getExitCode()` повертається у `runStandardLint`, а той — у `runLintDocker`.
162
+ 5. При прямому запуску файла Node — код виходу пишеться у `process.exitCode`.
163
+
164
+ ### Як це використовується ззовні
165
+
166
+ - **CLI:** `bun run n-cursor lint-docker` (або відповідний bin-скрипт) — основний сценарій.
167
+ - **Програмно з інших скриптів:**
168
+ ```js
169
+ import { runLintDocker } from 'npm/rules/docker/lint/lint.mjs'
170
+ const code = await runLintDocker() // 0 — OK, 1 — є зауваження/помилки
171
+ ```
172
+ - **Тести/допоміжний код:**
173
+
174
+ ```js
175
+ import { isLintDockerfileName, findLintDockerfilePaths } from 'npm/rules/docker/lint/lint.mjs'
176
+
177
+ isLintDockerfileName('Dockerfile') // true
178
+ isLintDockerfileName('app.Dockerfile') // true
179
+ isLintDockerfileName('Dockerfile.dev') // false
180
+
181
+ const files = await findLintDockerfilePaths(process.cwd(), [])
182
+ ```
183
+
184
+ ### Контракт коду виходу
185
+
186
+ - `0` — або не знайдено Dockerfile-ів, або всі знайдені пройшли `hadolint`.
187
+ - `1` — хоча б один файл не пройшов `hadolint` (або зафіксована інша помилка через `fail`).
188
+
189
+ ### Відмінності від `check-docker`
190
+
191
+ - `check-docker` працює з ширшим набором імен (зокрема `Dockerfile.*`, `Containerfile` тощо).
192
+ - `lint-docker` свідомо звужений до канонічного `Dockerfile` та суфікса `.dockerfile`, що відповідає правилу `docker.mdc` для самого hadolint-кроку.
193
+ - Серіалізація `lint-docker` — через `runStandardLint`, а не через прямий `withLock`, як того вимагає `scripts.mdc`.