@7n/rules-lang-js 0.10.0 → 0.12.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 (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +1 -1
  3. package/rules/bun/package_json/package_json.mdc +2 -2
  4. package/rules/bun/package_json/package_json.rego +26 -10
  5. package/rules/npm-module/npm_package_json/npm_package_json.mdc +1 -1
  6. package/rules/npm-module/npm_package_json/npm_package_json.rego +14 -5
  7. package/rules/storybook/adopt/docs/main.md +1 -2
  8. package/rules/storybook/adopt/main.mjs +37 -5
  9. package/rules/storybook/ci/concern.json +15 -0
  10. package/rules/storybook/ci/docs/fix-ci.md +39 -0
  11. package/rules/storybook/ci/docs/index.md +10 -0
  12. package/rules/storybook/ci/docs/main.md +50 -0
  13. package/rules/storybook/ci/fix-ci.mjs +105 -0
  14. package/rules/storybook/ci/main.mjs +112 -0
  15. package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +41 -0
  16. package/rules/storybook/ci/template/setup-playwright-chromium.action.yml +34 -0
  17. package/rules/storybook/hygiene/concern.json +1 -1
  18. package/rules/storybook/main.json +1 -1
  19. package/rules/storybook/main.mdc +24 -3
  20. package/rules/storybook/mocking/mocking.mdc +24 -0
  21. package/rules/storybook/scaffold/concern.json +1 -1
  22. package/rules/storybook/scaffold/docs/fix-scaffold.md +1 -1
  23. package/rules/storybook/scaffold/docs/main.md +1 -1
  24. package/rules/storybook/scaffold/fix-scaffold.mjs +50 -0
  25. package/rules/storybook/scaffold/main.mjs +68 -6
  26. package/rules/storybook/scaffold/template/docs/empty-vite.config.md +28 -0
  27. package/rules/storybook/scaffold/template/docs/index.md +5 -4
  28. package/rules/storybook/scaffold/template/docs/main.md +1 -2
  29. package/rules/storybook/scaffold/template/docs/preview.md +1 -2
  30. package/rules/storybook/scaffold/template/empty-vite.config.js +18 -0
  31. package/rules/storybook/scaffold/template/main.js +76 -6
  32. package/rules/storybook/scaffold/template/preview.js +13 -5
  33. package/rules/storybook/vitest-config/concern.json +1 -1
  34. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +1 -2
  35. package/rules/storybook/vitest-config/docs/main.md +1 -2
  36. package/rules/storybook/vitest-config/fix-vitest-config.mjs +22 -6
  37. package/rules/storybook/vitest-config/main.mjs +33 -2
  38. package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +1 -2
  39. package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +1 -2
  40. package/rules/storybook/vitest-config/template/storybook-project-entry.js +6 -2
  41. package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +2 -1
  42. package/rules/storybook/vitest-config/vitest-config.mdc +7 -3
@@ -3,10 +3,15 @@
3
3
  * канон-storybook-для-vue-компонентних-бібліотек). Згенеровано правилом `storybook` —
4
4
  * `npx @7n/rules fix storybook` відтворює цей файл, якщо його видалено чи зламано канон.
5
5
  */
6
- import { setup } from '@storybook/vue3'
7
- import { Dialog, iconSet as quasarBuiltinIconSet, Notify, Quasar } from 'quasar'
6
+ import { setup } from '@storybook/vue3-vite'
7
+ import { Dialog, Notify, Quasar } from 'quasar'
8
8
  import iconSet from 'quasar/icon-set/svg-material-icons'
9
- import { initialize, mswDecorator } from 'msw-storybook-addon'
9
+ // Імпорт через підшлях пакета НЕ named-import `iconSet` напряму з `quasar`: quasar
10
+ // 2.18.x не має цього імені як runtime-binding (лише компонент `IconSet` з великої
11
+ // літери), і `@quasar/vite-plugin`-transform падає з "Unknown import from Quasar" на
12
+ // такому специфікаторі. Підшлях (як і кастомний `iconSet` вище) обходить transform повністю.
13
+ import quasarBuiltinIconSet from 'quasar/icon-set/material-icons'
14
+ import { initialize, mswLoader } from 'msw-storybook-addon'
10
15
 
11
16
  import 'quasar/dist/quasar.css'
12
17
 
@@ -36,9 +41,12 @@ setup(app => {
36
41
  })
37
42
  })
38
43
 
39
- /** @type {import('@storybook/vue3').Preview} */
44
+ /** @type {import('@storybook/vue3-vite').Preview} */
40
45
  const preview = {
41
- decorators: [mswDecorator],
46
+ // mswLoader (не mswDecorator — deprecated у msw-storybook-addon 2.x, буде видалений
47
+ // у наступному релізі): loaders виконуються per-story ДО рендеру й інтегруються з
48
+ // async story-рендерингом Storybook, на відміну від декоратора.
49
+ loaders: [mswLoader],
42
50
  parameters: {
43
51
  controls: { matchers: { color: /(background|color)$/iu, date: /Date$/u } }
44
52
  }
@@ -3,6 +3,6 @@
3
3
  "fixability": "config",
4
4
  "lint": {
5
5
  "scope": "full",
6
- "glob": ["package.json", "**/*.vue", ".storybook/**", "vitest.config.*", "vitest.stryker.config.*"]
6
+ "glob": ["**/package.json", "**/*.vue", "**/.storybook/**", "**/vitest.config.*", "**/vitest.stryker.config.*"]
7
7
  }
8
8
  }
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-vitest-config.mjs
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/fix-vitest-config.mjs
5
5
  docgen:
6
- crc: b3961bc2
6
+ crc: 35b10da8
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/main.mjs
5
5
  docgen:
6
- crc: f5db98b8
6
+ crc: 543716c9
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 85
10
9
  issues: internal-name:detectStoriesGlob,anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -21,6 +21,7 @@ import {
21
21
  findTestObject,
22
22
  LEADING_COMMA_RE,
23
23
  parseModule,
24
+ PLAYWRIGHT_PROVIDER_IMPORT,
24
25
  REASON_STORYBOOK_PROJECT_MISSING,
25
26
  REASON_STRYKER_CONFIG_MISSING,
26
27
  REASON_UNIT_PROJECT_MISSING,
@@ -145,14 +146,29 @@ function arrayInsertBeforeClose(arr, indent, entries) {
145
146
  }
146
147
 
147
148
  /**
148
- * Гарантує наявність import-у `storybookTest` на самому початку файлу — ESM
149
- * толерантний до порядку import-ів, тож prepend завжди синтаксично коректний
150
- * незалежно від наявних import-ів.
149
+ * Гарантує наявність import-у на самому початку файлу — ESM толерантний до
150
+ * порядку import-ів, тож prepend завжди синтаксично коректний незалежно від
151
+ * наявних import-ів.
151
152
  * @param {string} src вихідний текст
153
+ * @param {string} marker підрядок, наявність якого означає "import уже є"
154
+ * @param {string} importLine рядок import-у (з `\n` наприкінці), що вставляється, якщо `marker` відсутній
152
155
  * @returns {string} текст з гарантованим import-ом
153
156
  */
154
- function ensureStorybookTestImport(src) {
155
- return src.includes('@storybook/addon-vitest/vitest-plugin') ? src : `${STORYBOOK_TEST_IMPORT}${src}`
157
+ function ensureImport(src, marker, importLine) {
158
+ return src.includes(marker) ? src : `${importLine}${src}`
159
+ }
160
+
161
+ /**
162
+ * Гарантує наявність обох import-ів, потрібних новому storybook-запису
163
+ * `test.projects`: `storybookTest` (`@storybook/addon-vitest/vitest-plugin`) і
164
+ * `playwright`-provider factory (`@vitest/browser-playwright`, vitest@^4 —
165
+ * рядкове API `provider: 'playwright'` застаріле).
166
+ * @param {string} src вихідний текст
167
+ * @returns {string} текст з гарантованими import-ами
168
+ */
169
+ function ensureStorybookEntryImports(src) {
170
+ const withStorybookTest = ensureImport(src, '@storybook/addon-vitest/vitest-plugin', STORYBOOK_TEST_IMPORT)
171
+ return ensureImport(withStorybookTest, '@vitest/browser-playwright', PLAYWRIGHT_PROVIDER_IMPORT)
156
172
  }
157
173
 
158
174
  /**
@@ -224,7 +240,7 @@ async function computeVitestConfigAugment(absPkgDir, vitestConfigPath) {
224
240
  if (!plan) return null
225
241
 
226
242
  let next = applyEdits(src, [plan.edit])
227
- if (plan.needsImport) next = ensureStorybookTestImport(next)
243
+ if (plan.needsImport) next = ensureStorybookEntryImports(next)
228
244
 
229
245
  // Safety: результат має компілюватися; інакше не пишемо (fail-closed, як у test/stryker_config).
230
246
  let recheck
@@ -18,6 +18,9 @@ const VITE_CONFIG_NAMES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs'
18
18
  /** Import, який має бути присутній у файлі, щоб `storybookTest(...)` резолвився. */
19
19
  export const STORYBOOK_TEST_IMPORT = "import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'\n"
20
20
 
21
+ /** Import, який має бути присутній у файлі, щоб `playwright(...)`-factory (vitest@^4) резолвилась. */
22
+ export const PLAYWRIGHT_PROVIDER_IMPORT = "import { playwright } from '@vitest/browser-playwright'\n"
23
+
21
24
  // Стабільні reasons (namespace: ruleId/concernId/reason).
22
25
  export const REASON_VITEST_CONFIG_MISSING = 'vitest-config-missing'
23
26
  export const REASON_STRYKER_CONFIG_MISSING = 'stryker-config-missing'
@@ -29,12 +32,20 @@ export const REASON_STORYBOOK_PROJECT_MARKER_MISSING = 'storybook-project-marker
29
32
 
30
33
  // Маркери канонічного storybook-проєкту (текстовий пошук у зрізі джерела самого
31
34
  // елемента масиву — стійко до форматування/AST-варіацій, як і scaffold/main.mjs).
32
- // Три останні (chromium/browser/stories) експортовано — переюз у `adopt/main.mjs`.
35
+ // Експортовані — переюз у `adopt/main.mjs`.
33
36
  const UNIT_NAME_RE = /name\s*:\s*['"]unit['"]/u
34
37
  const STORYBOOK_NAME_RE = /name\s*:\s*['"]storybook['"]/u
35
38
  export const CHROMIUM_RE = /chromium/u
36
39
  export const BROWSER_KEY_RE = /\bbrowser\s*:/u
37
40
  export const STORIES_RE = /stories/iu
41
+ // `storybookTest({ configDir: ... })` без явного `include` — легітимний патерн:
42
+ // Storybook підхоплює stories-glob автоматично зі своєї `.storybook/main.js`-конфігурації
43
+ // (той самий stories-glob, що й `detectStoriesGlob`), явний include у vitest-конфізі не
44
+ // обов'язковий (реальний кейс components/npm/vitest.config.js — пілот adopt-діагностики).
45
+ export const STORYBOOK_TEST_CONFIG_DIR_RE = /storybookTest\([^)]*configDir/u
46
+ // vitest@^4: `browser.provider` — factory-виклик (`playwright()` з `@vitest/browser-playwright`),
47
+ // не рядок `'playwright'` (застаріле API попередніх мажорів).
48
+ export const PROVIDER_FACTORY_RE = /provider\s*:\s*playwright\s*\(/u
38
49
  const STORIES_GLOB_PREFIX_RE = /^\.\.\//u
39
50
  // Module-scope (prefer-static-regex): рядок-відступ цілком whitespace; leading
40
51
  // кома (можливо з whitespace) — спільні з fix-vitest-config.mjs патерном augment-у.
@@ -173,6 +184,22 @@ export function classifyProjects(src, arr) {
173
184
  return { hasUnit, storybookSlice }
174
185
  }
175
186
 
187
+ /**
188
+ * Чи присутній валідний маркер джерела stories у зрізі `storybook`-проєкту:
189
+ * явний stories-glob (`include: [...]`, {@link STORIES_RE}) АБО виклик
190
+ * `storybookTest({ configDir })` без явного `include` — Storybook підхоплює glob
191
+ * автоматично зі своєї конфігурації, явний include не обов'язковий (реальний
192
+ * кейс components/npm/vitest.config.js — пілот adopt-діагностики). Раніше гола
193
+ * вимога підрядка `stories` давала хибний позитив на цьому валідному патерні.
194
+ * Спільна логіка для `main.mjs` (lint) і `adopt/main.mjs` (diff-діагностика) —
195
+ * не дублювати комбінацію двох regex у двох місцях.
196
+ * @param {string} storybookSlice текстовий зріз елемента `storybook` у `test.projects`
197
+ * @returns {boolean} true — маркер джерела stories присутній (явно чи неявно)
198
+ */
199
+ export function hasStoriesMarker(storybookSlice) {
200
+ return STORIES_RE.test(storybookSlice) || STORYBOOK_TEST_CONFIG_DIR_RE.test(storybookSlice)
201
+ }
202
+
176
203
  /**
177
204
  * Будує спільний контекст перевірки одного пакета (label/relPrefix/шлях vitest-конфіга).
178
205
  * @param {import('../scope/main.mjs').InScopePackage} entry пакет у скоупі
@@ -272,7 +299,11 @@ async function checkVitestConfigContent(pkgCtx, reporter) {
272
299
  const missingHints = []
273
300
  if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
274
301
  if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
275
- if (!STORIES_RE.test(storybookSlice)) missingHints.push('stories-glob')
302
+ if (!hasStoriesMarker(storybookSlice))
303
+ missingHints.push('stories-джерело (include або storybookTest({ configDir }))')
304
+ if (!PROVIDER_FACTORY_RE.test(storybookSlice)) {
305
+ missingHints.push("provider-factory (vitest v4: import { playwright } from '@vitest/browser-playwright')")
306
+ }
276
307
  if (missingHints.length > 0) {
277
308
  reporter.fail(
278
309
  `[${label}] ${relVitestFile}: storybook-project без канонічних маркерів — бракує: ${missingHints.join(', ')} (vitest-config.mdc)`,
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: storybook-project-entry.js
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/template/storybook-project-entry.js
5
5
  docgen:
6
- crc: 5d9feafd
6
+ crc: 2646d55c
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: vitest.config.baseline.mjs
4
4
  resource: plugins/lang-js/rules/storybook/vitest-config/template/vitest.config.baseline.mjs
5
5
  docgen:
6
- crc: 1eca6e97
6
+ crc: 89da594d
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -1,9 +1,13 @@
1
1
  // Канонічний вигляд запису `storybook`-проєкту у `test.projects` (storybook.mdc,
2
2
  // vitest-config-концерн, ADR Кластер 5): browser-mode, лише chromium, stories-glob
3
3
  // (`__STORYBOOK_STORIES_GLOB__` — токен, `detectStoriesGlob` зі scaffold-концерна
4
- // без префіксу `../`). Сам файлвалідний модуль лише для JS-лінту репозиторію
4
+ // без префіксу `../`). `provider: playwright()`factory-виклик з
5
+ // `@vitest/browser-playwright`, не рядок `'playwright'`: vitest@^4 прибрав рядкове
6
+ // API провайдера, `@vitest/browser-playwright` — окремий пакет (не входить у
7
+ // `@vitest/browser`). Сам файл — валідний модуль лише для JS-лінту репозиторію
5
8
  // правил; `fix-vitest-config.mjs` зчитує лише вміст `export default {...}`.
6
9
  import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
10
+ import { playwright } from '@vitest/browser-playwright'
7
11
 
8
12
  export default {
9
13
  extends: true,
@@ -14,7 +18,7 @@ export default {
14
18
  browser: {
15
19
  enabled: true,
16
20
  headless: true,
17
- provider: 'playwright',
21
+ provider: playwright(),
18
22
  instances: [{ browser: 'chromium' }]
19
23
  },
20
24
  setupFiles: ['.storybook/vitest.setup.js']
@@ -1,5 +1,6 @@
1
1
  import { defineConfig, mergeConfig } from 'vitest/config'
2
2
  import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
3
+ import { playwright } from '@vitest/browser-playwright'
3
4
  import viteConfig from './__VITE_CONFIG_IMPORT__'
4
5
 
5
6
  // Канонічний vitest-конфіг Vue-компонентної бібліотеки у скоупі Storybook
@@ -25,7 +26,7 @@ export default mergeConfig(
25
26
  browser: {
26
27
  enabled: true,
27
28
  headless: true,
28
- provider: 'playwright',
29
+ provider: playwright(),
29
30
  instances: [{ browser: 'chromium' }]
30
31
  },
31
32
  setupFiles: ['.storybook/vitest.setup.js']
@@ -7,10 +7,14 @@
7
7
  Vue-компонентна бібліотека вже має `vitest.config.{mjs,js,ts}` за каноном `vue/testing.mdc` (`mergeConfig(viteConfig, defineConfig({ test: {...} }))` з `environment: 'happy-dom'`). Storybook-хвиля **дописує** до цього `test`-блоку named-project `unit`+`storybook` — існуючі поля (`include`, `environment`, `coverage`) лишаються без змін, `projects` — нова властивість поруч:
8
8
 
9
9
  - `{ extends: true, test: { name: 'unit' } }` — успадковує решту `test`-опцій пакета як є.
10
- - `storybook`-проєкт — **browser-mode, лише chromium** (`browser: { enabled: true, provider: 'playwright', instances: [{ browser: 'chromium' }] }`) і `include` на stories-glob пакета (та сама layout-детекція, що й `scaffold/main.mjs#detectStoriesGlob`, лише без префіксу `../` — vitest-конфіг лежить у корені пакета, а не в `.storybook/`).
10
+ - `storybook`-проєкт — **browser-mode, лише chromium** (`browser: { enabled: true, provider: playwright(), instances: [{ browser: 'chromium' }] }`) і `include` на stories-glob пакета (та сама layout-детекція, що й `scaffold/main.mjs#detectStoriesGlob`, лише без префіксу `../` — vitest-конфіг лежить у корені пакета, а не в `.storybook/`).
11
11
 
12
12
  Firefox/webkit свідомо не додаються — ADR і main.mdc хвилі 1 фіксують лише chromium; розширення на інші браузери — поза обсягом цього концерну.
13
13
 
14
+ **`provider` — factory-виклик, не рядок.** vitest@^4 прибрав рядкове API browser-провайдера: `provider: 'playwright'` більше не резолвиться, потрібен `import { playwright } from '@vitest/browser-playwright'` і `provider: playwright()` (окремий npm-пакет, не входить у `@vitest/browser`). Канон і автофікс генерують факторі-форму; рядкова форма (застаріле API попередніх мажорів) — `storybook-project-marker-missing`.
15
+
16
+ **Явний `include` не обов'язковий, якщо є `storybookTest({ configDir })`.** Реальний кейс пілота (components/npm/vitest.config.js): `storybook`-проєкт без явного `include` — Storybook підхоплює stories-glob автоматично зі своєї `.storybook/main.js`-конфігурації. Перевірка (`hasStoriesMarker`) визнає запис валідним, якщо є АБО явний stories-glob, АБО виклик `storybookTest({ configDir: ... })`.
17
+
14
18
  ### Чому окремий `vitest.stryker.config`
15
19
 
16
20
  `@stryker-mutator/vitest-runner` крашиться, коли конфіг, який він завантажує, містить browser-mode `projects` (сам факт присутності `storybook`-проєкту в основному vitest-конфізі ламає mutation-прогін, незалежно від того, чи Stryker фактично торкається storybook-файлів). Тому концерн генерує **сусідній** ізольований `vitest.stryker.config.<ext>` — той самий `mergeConfig(viteConfig, defineConfig({ test: {...} }))`, але **без** `projects`/browser-mode: лише unit-набір (`include`, `happy-dom`, `coverage`), на якому mutation-testing працює штатно.
@@ -19,7 +23,7 @@ Firefox/webkit свідомо не додаються — ADR і main.mdc хви
19
23
 
20
24
  ### Що перевіряє й що виправляє концерн
21
25
 
22
- Перевірка (`main.mjs`) — AST-аналіз (`oxc-parser`) наявного vitest-конфіга: знаходить `test`-блок (незалежно від того, чи він огорнутий у `defineConfig`/`mergeConfig`, чи це простий об'єкт), дивиться на `test.projects` і класифікує елементи за `name: 'unit'`/`name: 'storybook'`; для `storybook`-елемента перевіряє маркери `chromium`/`browser`/stories-glob текстовим пошуком. Стійка до форматування й порядку полів — не вимагає конкретного стилю запису, лише семантичну присутність маркерів.
26
+ Перевірка (`main.mjs`) — AST-аналіз (`oxc-parser`) наявного vitest-конфіга: знаходить `test`-блок (незалежно від того, чи він огорнутий у `defineConfig`/`mergeConfig`, чи це простий об'єкт), дивиться на `test.projects` і класифікує елементи за `name: 'unit'`/`name: 'storybook'`; для `storybook`-елемента перевіряє маркери `chromium`/`browser`/`provider`-factory/stories-джерело (явний glob або `storybookTest({ configDir })`) текстовим пошуком. Стійка до форматування й порядку полів — не вимагає конкретного стилю запису, лише семантичну присутність маркерів.
23
27
 
24
28
  Автофікс (`fix-vitest-config.mjs`, fixability `config`) виправляє й дописує **лише відсутнє**:
25
29
 
@@ -30,4 +34,4 @@ Firefox/webkit свідомо не додаються — ADR і main.mdc хви
30
34
 
31
35
  Як і в `test/stryker_config`, правка — точкові string-splice-и (insert-only) поверх існуючого тексту, а не переписаний AST-serializer, щоб не зачепити форматування й коментарі решти файлу; результат повторно парситься, і при невалідному виводі запис скасовується (fail-closed).
32
36
 
33
- **Свідомо без автофіксу:** `test.projects` — не статичний масив (spread/змінна), test-блок не вдалось знайти структурно, або `storybook`-запис уже присутній, але без якогось із маркерів (chromium/browser/stories-glob) — надійно виправити частковий вручну написаний запис програмно ризиковано, тож це лишається порушенням для ручного review (той самий компроміс, що й marker-порушення в `scaffold`).
37
+ **Свідомо без автофіксу:** `test.projects` — не статичний масив (spread/змінна), test-блок не вдалось знайти структурно, або `storybook`-запис уже присутній, але без якогось із маркерів (chromium/browser/provider-factory/stories-джерело) — надійно виправити частковий вручну написаний запис програмно ризиковано, тож це лишається порушенням для ручного review (той самий компроміс, що й marker-порушення в `scaffold`).