@7n/rules-lang-js 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +10 -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 +15 -9
  5. package/rules/storybook/adopt/docs/main.md +1 -2
  6. package/rules/storybook/adopt/main.mjs +5 -2
  7. package/rules/storybook/ci/concern.json +15 -0
  8. package/rules/storybook/ci/docs/fix-ci.md +39 -0
  9. package/rules/storybook/ci/docs/index.md +10 -0
  10. package/rules/storybook/ci/docs/main.md +50 -0
  11. package/rules/storybook/ci/fix-ci.mjs +105 -0
  12. package/rules/storybook/ci/main.mjs +112 -0
  13. package/rules/storybook/ci/template/lint-storybook.yml.snippet.yml +41 -0
  14. package/rules/storybook/ci/template/setup-playwright-chromium.action.yml +34 -0
  15. package/rules/storybook/main.mdc +7 -1
  16. package/rules/storybook/mocking/mocking.mdc +24 -0
  17. package/rules/storybook/scaffold/docs/main.md +1 -1
  18. package/rules/storybook/scaffold/main.mjs +37 -5
  19. package/rules/storybook/scaffold/template/docs/main.md +1 -2
  20. package/rules/storybook/scaffold/template/main.js +53 -6
  21. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +1 -2
  22. package/rules/storybook/vitest-config/docs/main.md +1 -2
  23. package/rules/storybook/vitest-config/fix-vitest-config.mjs +22 -6
  24. package/rules/storybook/vitest-config/main.mjs +33 -2
  25. package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +1 -2
  26. package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +1 -2
  27. package/rules/storybook/vitest-config/template/storybook-project-entry.js +6 -2
  28. package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +2 -1
  29. package/rules/storybook/vitest-config/vitest-config.mdc +7 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0] - 2026-07-21
4
+
5
+ ### Added
6
+
7
+ - storybook: новий concern `ci` (ADR Кластер 5, CI-частина) — канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і `.github/workflows/lint-storybook.yml` (швидкий PR-прогін `vitest --project=storybook`), гейтований `requires.capability: ci:github`
8
+
9
+ ### Fixed
10
+
11
+ - storybook: viteFinal-фільтр стійкий до VueMacros-стека (Promise/масив-резолв, сімейний фільтр vite:*/vue-macros), vitest@^4 provider-factory (@vitest/browser-playwright) замість застарілого рядка `'playwright'`, flat-root layout у detectStoriesGlob (components без src/), точковий alias-мок одного модуля в mocking.mdc, і STORIES_RE false positive на `storybookTest({ configDir })` без явного include — усе за результатами пілота adopt-діагностики на nitra/components. Заодно governance package_json.rego: allowlist доповнено `@vitest/browser-playwright`.
12
+
3
13
  ## [0.10.0] - 2026-07-21
4
14
 
5
15
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules-lang-js",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Плагін @7n/rules: JS/npm/bun-екосистема — lint-правила (js/bun/vue/js-run/npm-module/db), taze-провайдер (package.json, bunx taze) і doc-files-екстрактори (oxc AST)",
5
5
  "keywords": [
6
6
  "javascript",
@@ -9,8 +9,8 @@ Rego-пакет: `bun.package_json`
9
9
  Gate виносить два класи deny:
10
10
 
11
11
  1. **Заборонені top-level поля** — будь-яке поле з `package.json.deny.json` присутнє у файлі (навіть із порожнім значенням `{}`).
12
- 2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@vitest/browser`, `@stryker-mutator/vitest-runner`, `@stryker-mutator/core`, `@playwright/test`, `playwright`, `@storybook/addon-vitest`, `@7n/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
12
+ 2. **devDependencies не з білого списку** — дозволені лише `@nitra/*`/`@7n/*` та root-only тестові peer/tools (`vitest`, `@vitest/coverage-v8`, `@vitest/browser`, `@vitest/browser-playwright`, `@stryker-mutator/vitest-runner`, `@stryker-mutator/core`, `@playwright/test`, `playwright`, `@storybook/addon-vitest`, `@7n/test`). Будь-який інший пакет → deny. CLI-тули, які `n-rules lint` спавнить через `bunx` (oxlint, jscpd, v8r, github-actionlint тощо), у root не пінуються — вони приїжджають як `dependencies` пакета `@7n/rules` (npm-module.mdc).
13
13
 
14
- `@vitest/browser`/`playwright`/`@storybook/addon-vitest` — додані для named vitest project "storybook" (канон Storybook, кластер 5: browser-mode, лише chromium) — той самий root vitest.config, що й `unit`-проект; `@storybook/addon-vitest` постачає `storybookTest`-плагін для цього vitest-конфіга (канонічний template правила `storybook`). **Межа з `npm-module.mdc`**: Storybook-специфічні identity-пакети (`storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon`) у цей allowlist **не** додаються — вони живуть у `npm/package.json` консюмер-пакета (канон Storybook, кластер 7 Governance), бо `isStorybookRoot()` у `@7n/test` детектує Storybook-скоуп саме за тим файлом, не кореневим. `@storybook/addon-vitest` — виняток із цієї межі: він test-tooling (vitest-плагін), а не identity-маркер, тож root, як і решта vitest-peer'ів.
14
+ `@vitest/browser`/`@vitest/browser-playwright`/`playwright`/`@storybook/addon-vitest` — додані для named vitest project "storybook" (канон Storybook, кластер 5: browser-mode, лише chromium) — той самий root vitest.config, що й `unit`-проект; `@storybook/addon-vitest` постачає `storybookTest`-плагін для цього vitest-конфіга (канонічний template правила `storybook`). `@vitest/browser-playwright` — окремий npm-пакет: vitest@^4 прибрав рядкове API browser-провайдера (`provider: 'playwright'`), потрібна factory `import { playwright } from '@vitest/browser-playwright'` → `provider: playwright()` (звірено з реальним `bun.lock` пілотного консюмера — усі три `@vitest/*`/`playwright`-пакети встановлені поруч). **Межа з `npm-module.mdc`**: Storybook-специфічні identity-пакети (`storybook`, `@storybook/vue3-vite`, `@storybook/vue3`, `msw`, `msw-storybook-addon`) у цей allowlist **не** додаються — вони живуть у `npm/package.json` консюмер-пакета (канон Storybook, кластер 7 Governance), бо `isStorybookRoot()` у `@7n/test` детектує Storybook-скоуп саме за тим файлом, не кореневим. `@storybook/addon-vitest` — виняток із цієї межі: він test-tooling (vitest-плагін), а не identity-маркер, тож root, як і решта vitest-peer'ів.
15
15
 
16
16
  Перевірки, що потребують FS або cross-file контексту (наприклад наявність `yarn.lock`), лишаються у JS-шарі.
@@ -8,10 +8,11 @@
8
8
  # - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для `@7n/test coverage`
9
9
  # (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
10
10
  # `devDependencies` за `npm-module.mdc`)
11
- # - `@vitest/browser`/`playwright`/`@storybook/addon-vitest` (browser-mode provider +
12
- # `storybookTest`-плагін для named vitest project "storybook", лише chromium — канон
13
- # Storybook кластер 5) теж root-only test peers: той самий vitest.config, що й
14
- # `unit`-проект, живе в корені монорепо-споживача. Storybook-специфічні
11
+ # - `@vitest/browser`/`@vitest/browser-playwright`/`playwright`/`@storybook/addon-vitest`
12
+ # (browser-mode provider factory + `storybookTest`-плагін для named vitest project
13
+ # "storybook", лише chromium — канон Storybook кластер 5) теж root-only test peers:
14
+ # той самий vitest.config, що й `unit`-проект, живе в корені монорепо-споживача.
15
+ # Storybook-специфічні
15
16
  # identity-пакети (`storybook`, `@storybook/vue3*`, `msw*`) НЕ сюди — вони живуть у
16
17
  # `npm/package.json` (канон Storybook кластер 7, `npm-module.mdc`), бо
17
18
  # `isStorybookRoot()` @7n/test читає саме той файл, не кореневий package.json.
@@ -59,11 +60,15 @@ deny contains msg if {
59
60
  # @stryker-mutator/core — обов'язковий exact-pin peer vitest-runner@9+ (раніше тягнувся транзитивно)
60
61
  # @7n/test — оркестратор `coverage` (npx @7n/test coverage); devDependency, щоб npx резолвив
61
62
  # локально без мережевого fetch щоразу.
62
- # @vitest/browser + playwright — провайдер browser-mode для named vitest project
63
- # "storybook" (канон Storybook кластер 5: лише chromium, PR — швидкий
64
- # --project=storybook). `playwright` (не `@playwright/test`) сирий driver, який
65
- # @vitest/browser використовує як provider; `@playwright/test` лишається окремо для
66
- # змістовних E2E-сценаріїв (n-vue.mdc).
63
+ # @vitest/browser + @vitest/browser-playwright + playwright — провайдер browser-mode для
64
+ # named vitest project "storybook" (канон Storybook кластер 5: лише chromium, PR —
65
+ # швидкий --project=storybook). vitest@^4 прибрав рядкове API провайдера
66
+ # (`provider: 'playwright'`) `@vitest/browser-playwright` постачає factory
67
+ # `import { playwright } from '@vitest/browser-playwright'` → `provider: playwright()`,
68
+ # окремий npm-пакет, не входить у `@vitest/browser` (звірено з реальним bun.lock пілота —
69
+ # усі три пакети встановлені поруч). `playwright` (не `@playwright/test`) — сирий driver,
70
+ # який `@vitest/browser-playwright` використовує як provider; `@playwright/test` лишається
71
+ # окремо для змістовних E2E-сценаріїв (n-vue.mdc).
67
72
  # @storybook/addon-vitest — постачає `storybookTest` для vitest-плагіна в канонічному
68
73
  # vitest.config named-проекту "storybook" (той самий канон Storybook кластер 5); версія
69
74
  # з лінійки Storybook 9.x (узгоджена з `storybook`@9.1.10, запіненим у
@@ -73,6 +78,7 @@ allowed_root_test_deps := {
73
78
  "vitest",
74
79
  "@vitest/coverage-v8",
75
80
  "@vitest/browser",
81
+ "@vitest/browser-playwright",
76
82
  "@stryker-mutator/vitest-runner",
77
83
  "@stryker-mutator/core",
78
84
  "@playwright/test",
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/adopt/main.mjs
5
5
  docgen:
6
- crc: 640409c7
6
+ crc: c8e6e421
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 90
10
9
  issues: anchor-miss:(mocking.mdc),anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -29,9 +29,10 @@ import {
29
29
  classifyProjects,
30
30
  findProperty,
31
31
  findTestObject,
32
+ hasStoriesMarker,
32
33
  parseModule,
34
+ PROVIDER_FACTORY_RE,
33
35
  resolveVitestConfigPath,
34
- STORIES_RE,
35
36
  strykerConfigPathFor
36
37
  } from '../vitest-config/main.mjs'
37
38
 
@@ -167,7 +168,9 @@ function diagnoseVitestProjectsSection(entry) {
167
168
  const missingHints = []
168
169
  if (!CHROMIUM_RE.test(storybookSlice)) missingHints.push('chromium-інстанс')
169
170
  if (!BROWSER_KEY_RE.test(storybookSlice)) missingHints.push('browser-mode')
170
- if (!STORIES_RE.test(storybookSlice)) missingHints.push('stories-glob')
171
+ if (!hasStoriesMarker(storybookSlice)) missingHints.push('stories-джерело (include або storybookTest({ configDir }))')
172
+ if (!PROVIDER_FACTORY_RE.test(storybookSlice))
173
+ missingHints.push('provider-factory (playwright() з @vitest/browser-playwright)')
171
174
  if (missingHints.length > 0) {
172
175
  return { name, file, status: STATUS.DIFFER, detail: `storybook-project без: ${missingHints.join(', ')}` }
173
176
  }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "requires": { "capability": "ci:github" },
5
+ "lint": {
6
+ "scope": "full",
7
+ "glob": [
8
+ "package.json",
9
+ "**/*.vue",
10
+ ".storybook/**",
11
+ ".github/actions/setup-playwright-chromium/action.yml",
12
+ ".github/workflows/lint-storybook.yml"
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,39 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-ci.mjs
4
+ resource: plugins/lang-js/rules/storybook/ci/fix-ci.mjs
5
+ docgen:
6
+ crc: 877b9feb
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 90
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Порожній шаблон `storybook/ci`, з якого відтворюються репо-рівневі CI-артефакти: `setup-playwright-chromium` як verbatim-копія composite action для Playwright Chromium і `.github/workflows/lint-storybook.yml` із матрицею `strategy.matrix.package`, згенерованою з фактичного списку пакетів через `collectInScopeVuePackages`. Публічні anchors: `TEMPLATE_DIR`, `renderPlaywrightAction`, `renderPackageDirsYaml`, `renderStorybookWorkflow`, `patterns`.
16
+
17
+ ## Поведінка
18
+
19
+ - **TEMPLATE_DIR** — вказує на каталог `template/` цього concern-а як джерело канонічних CI-шаблонів.
20
+ - **renderPlaywrightAction** — читає канонічний composite action `setup-playwright-chromium` із шаблону без змін.
21
+ - **renderPackageDirsYaml** — перетворює список коренів пакетів у YAML-рядки для матриці Storybook CI.
22
+ - **renderStorybookWorkflow** — збирає канонічний `.github/workflows/lint-storybook.yml`, підставляючи в нього матрицю пакетів зі скоупу репозиторію.
23
+ - **patterns** — описує T0-правила автофіксу для відновлення відсутніх CI-файлів цього concern-а.
24
+
25
+ ## Публічний API
26
+
27
+ - TEMPLATE_DIR — Каталог `template/` цього concern-а.
28
+ - renderPlaywrightAction — Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
29
+ - renderPackageDirsYaml — Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
30
+ самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
31
+ `strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
32
+ `working-directory` для GitHub Actions.
33
+ - renderStorybookWorkflow — Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
34
+ матрицею пакетів у скоупі.
35
+ - patterns — повертає набір шаблонів, за якими далі шукають або зіставляють потрібні значення.
36
+
37
+ ## Гарантії поведінки
38
+
39
+ - (специфічних машинно-виведених гарантій немає)
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/lang-js/rules/storybook/ci
4
+ resource: plugins/lang-js/rules/storybook/ci/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ----------------------- | --------- |
9
+ | [fix-ci.mjs](fix-ci.md) | JS Module |
10
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,50 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/storybook/ci/main.mjs
5
+ docgen:
6
+ crc: 65df5791
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 85
10
+ issues: internal-name:collectInScopeVuePackages,anchor-miss:(storybook.mdc),judge:inaccurate:0.96
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл фіксує канонічні репо-шляхи й маркери для storybook CI, щоб у Vue component library пакетах у scope швидко знаходити правильні `.github/actions/setup-playwright-chromium/action.yml` і `.github/workflows/lint-storybook.yml` та зіставляти їх із повідомленнями в `storybook.mdc`. Експортовані константи-рядки: PLAYWRIGHT_ACTION_REL=".github/actions/setup-playwright-chromium/action.yml" — канонічний шлях до Playwright action; STORYBOOK_WORKFLOW_REL=".github/workflows/lint-storybook.yml" — канонічний шлях до workflow linting. Маркери повідомлень: PLAYWRIGHT_ACTION_MARKERS і STORYBOOK_WORKFLOW_MARKERS, які прив’язують перевірки до повідомлень у `storybook.mdc`. Це потрібно, щоб підтримувати єдиний storybook-канон без зайвого обходу `.github` і `.git`, із кешуванням результату в межах одного прогону.
17
+
18
+ ## Поведінка
19
+
20
+ - PLAYWRIGHT_ACTION_REL — задає репо-рівневий шлях до канонічного composite action `.github/actions/setup-playwright-chromium/action.yml`, який перевіряється як частина storybook CI.
21
+ - STORYBOOK_WORKFLOW_REL — задає репо-рівневий шлях до канонічного workflow `.github/workflows/lint-storybook.yml`, що відповідає за швидкий storybook-лінт.
22
+ - PLAYWRIGHT_ACTION_MARKERS — описує канонічні ознаки composite action для storybook CI, щоб гарантувати кеш Playwright і встановлення лише chromium; у повідомленнях використовується прив’язка до `storybook.mdc`.
23
+ - STORYBOOK_WORKFLOW_MARKERS — описує канонічні ознаки workflow для storybook CI, щоб гарантувати запуск швидкого `vitest --project=storybook`; у повідомленнях використовується прив’язка до `storybook.mdc`.
24
+ - lint — перевіряє наявність storybook CI-канону лише для репозиторіїв із Vue component library пакетами в scope; якщо таких пакетів немає, завершується без зауважень. Свідомо не зачіпає `.github` і `.git` як окремі області обходу; кешує результат у межах одного прогону.
25
+
26
+ ## Публічний API
27
+
28
+ - PLAYWRIGHT_ACTION_REL — Repo-relative шлях канонічного composite action (не per-package — один на репозиторій).
29
+ - STORYBOOK_WORKFLOW_REL — Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`.
30
+ - PLAYWRIGHT_ACTION_MARKERS — Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
31
+ `ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
32
+ Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
33
+ - STORYBOOK_WORKFLOW_MARKERS — Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
34
+ setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
35
+ `@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
36
+ - lint — Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
37
+ одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
38
+ канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
39
+ chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
40
+ `vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
41
+ у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
42
+
43
+ Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
44
+ Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
45
+ лишається окремою інфраструктурою `test/stryker_config`.
46
+
47
+ ## Гарантії поведінки
48
+
49
+ - Кешує результати в межах одного прогону.
50
+ - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * T0-autofix для concern-а `storybook/ci` (ADR Кластер 5, CI-частина): відтворює канонічний
3
+ * composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише chromium) і
4
+ * канонічний `.github/workflows/lint-storybook.yml` з `template/` цього concern-а.
5
+ *
6
+ * Обидва файли — репо-рівневі (не per-package): composite action — verbatim-копія; workflow —
7
+ * матриця `strategy.matrix.package` генерується з фактичного списку пакетів у скоупі
8
+ * (`collectInScopeVuePackages`), щоб CI реально покривав усі Storybook-пакети репозиторію.
9
+ */
10
+ import { mkdirSync, writeFileSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { readFile } from 'node:fs/promises'
14
+
15
+ import { collectInScopeVuePackages } from '../scope/main.mjs'
16
+
17
+ /** Каталог `template/` цього concern-а. */
18
+ export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), 'template')
19
+
20
+ /** Токен у `lint-storybook.yml.snippet.yml`, що заміщається списком пакетів матриці. */
21
+ const PACKAGE_DIRS_TOKEN = '__STORYBOOK_CI_PACKAGE_DIRS__'
22
+
23
+ /**
24
+ * Вміст канонічного composite action `setup-playwright-chromium` — verbatim з template.
25
+ * @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
26
+ * @returns {Promise<string>} вміст `action.yml`
27
+ */
28
+ export function renderPlaywrightAction(templateDir = TEMPLATE_DIR) {
29
+ return readFile(join(templateDir, 'setup-playwright-chromium.action.yml'), 'utf8')
30
+ }
31
+
32
+ /**
33
+ * Рендерить YAML-фрагмент `matrix.package` списку — по одному `- <rootDir>` на рядок, з тим
34
+ * самим відступом, що й токен-рядок у шаблоні (10 пробілів — рівень елемента списку під
35
+ * `strategy.matrix.package:`). `rootDir === '.'` (корінь монорепо) лишається `.` — валідний
36
+ * `working-directory` для GitHub Actions.
37
+ * @param {string[]} rootDirs відносні (posix) корені пакетів у скоупі
38
+ * @returns {string} YAML-рядки (без завершального переносу)
39
+ */
40
+ export function renderPackageDirsYaml(rootDirs) {
41
+ return rootDirs.map(dir => ` - ${dir}`).join('\n')
42
+ }
43
+
44
+ /**
45
+ * Вміст канонічного `.github/workflows/lint-storybook.yml` — template з підставленою
46
+ * матрицею пакетів у скоупі.
47
+ * @param {string[]} rootDirs відносні корені пакетів у скоупі (`collectInScopeVuePackages`)
48
+ * @param {string} [templateDir] каталог template/ (за замовчуванням — цього concern-а)
49
+ * @returns {Promise<string>} вміст `lint-storybook.yml`
50
+ */
51
+ export async function renderStorybookWorkflow(rootDirs, templateDir = TEMPLATE_DIR) {
52
+ const raw = await readFile(join(templateDir, 'lint-storybook.yml.snippet.yml'), 'utf8')
53
+ const tokenLineRe = new RegExp(`^[ \\t]*${PACKAGE_DIRS_TOKEN}[ \\t]*$`, 'mu')
54
+ // Replacer-функція (не рядок) — уникає unicorn/no-unsafe-string-replacement:
55
+ // рядок-заміна мав би трактувати `$`-послідовності як спецсимволи `String#replace`.
56
+ return raw.replace(tokenLineRe, () => renderPackageDirsYaml(rootDirs))
57
+ }
58
+
59
+ /**
60
+ * Записує файл, створюючи батьківські каталоги й реєструючи запис для rollback (той самий
61
+ * патерн, що й `writeScaffoldFile` у `scaffold/fix-scaffold.mjs`).
62
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').FixContext} ctx fix-контекст рунга
63
+ * @param {string} absPath абсолютний шлях цільового файлу
64
+ * @param {string} content вміст файлу
65
+ * @returns {void}
66
+ */
67
+ function writeCiFile(ctx, absPath, content) {
68
+ mkdirSync(dirname(absPath), { recursive: true })
69
+ ctx.recordWrite?.(absPath)
70
+ writeFileSync(absPath, content, 'utf8')
71
+ }
72
+
73
+ /** @type {import('@7n/rules/scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
74
+ export const patterns = [
75
+ {
76
+ id: 'storybook-ci-playwright-action',
77
+ test: violations => violations.some(v => v.reason === 'missing-playwright-action' && v.file),
78
+ apply: async (violations, ctx) => {
79
+ const target = violations.find(v => v.reason === 'missing-playwright-action' && v.file)
80
+ if (!target || !ctx.concernDir) return { touchedFiles: [] }
81
+
82
+ const content = await renderPlaywrightAction(join(ctx.concernDir, 'template'))
83
+ const abs = join(ctx.cwd, target.file)
84
+ writeCiFile(ctx, abs, content)
85
+ return { touchedFiles: [abs], message: `${target.file}: створено composite action setup-playwright-chromium` }
86
+ }
87
+ },
88
+ {
89
+ id: 'storybook-ci-workflow',
90
+ test: violations => violations.some(v => v.reason === 'missing-storybook-workflow' && v.file),
91
+ apply: async (violations, ctx) => {
92
+ const target = violations.find(v => v.reason === 'missing-storybook-workflow' && v.file)
93
+ if (!target || !ctx.concernDir) return { touchedFiles: [] }
94
+
95
+ const pkgs = await collectInScopeVuePackages(ctx.cwd)
96
+ const rootDirs = pkgs.map(p => p.rootDir)
97
+ if (rootDirs.length === 0) return { touchedFiles: [] }
98
+
99
+ const content = await renderStorybookWorkflow(rootDirs, join(ctx.concernDir, 'template'))
100
+ const abs = join(ctx.cwd, target.file)
101
+ writeCiFile(ctx, abs, content)
102
+ return { touchedFiles: [abs], message: `${target.file}: створено (${rootDirs.length} пакет(ів) у матриці)` }
103
+ }
104
+ }
105
+ ]
@@ -0,0 +1,112 @@
1
+ /** @see ./docs/main.md */
2
+ import { existsSync } from 'node:fs'
3
+ import { readFile } from 'node:fs/promises'
4
+ import { join } from 'node:path'
5
+
6
+ import { createViolationReporter } from '@7n/rules/scripts/lib/lint-surface/violation-reporter.mjs'
7
+ import { collectInScopeVuePackages } from '../scope/main.mjs'
8
+ import { missingMarkers } from '../scaffold/main.mjs'
9
+
10
+ /** Repo-relative шлях канонічного composite action (не per-package — один на репозиторій). */
11
+ export const PLAYWRIGHT_ACTION_REL = '.github/actions/setup-playwright-chromium/action.yml'
12
+
13
+ /** Repo-relative шлях канонічного workflow, що запускає `vitest --project=storybook`. */
14
+ export const STORYBOOK_WORKFLOW_REL = '.github/workflows/lint-storybook.yml'
15
+
16
+ /**
17
+ * Маркери канону composite action `setup-playwright-chromium` (ADR Кластер 5): кеш
18
+ * `ms-playwright` через `actions/cache`, ключ від версії playwright, install лише chromium.
19
+ * Текстовий пошук — той самий підхід, що й `MAIN_JS_MARKERS`/`PREVIEW_JS_MARKERS` у `scaffold`.
20
+ */
21
+ export const PLAYWRIGHT_ACTION_MARKERS = [
22
+ { token: 'ms-playwright', hint: 'кеш каталогу ms-playwright' },
23
+ { token: 'actions/cache@', hint: 'actions/cache для Playwright-браузерів' },
24
+ { token: 'playwright install chromium', hint: 'install лише chromium (не всі браузери)' }
25
+ ]
26
+
27
+ /**
28
+ * Маркери канону `.github/workflows/lint-storybook.yml`: композитний Playwright-кеш ПІСЛЯ
29
+ * setup-bun-deps, і швидкий `vitest --project=storybook` (ADR Кластер 5 — nightly-only
30
+ * `@7n/test coverage`/mutation-testing на PR не запускається, лише цей швидкий шлях).
31
+ */
32
+ export const STORYBOOK_WORKFLOW_MARKERS = [
33
+ { token: './.github/actions/setup-bun-deps', hint: 'setup-bun-deps перед Playwright-кроком' },
34
+ { token: './.github/actions/setup-playwright-chromium', hint: 'композитний Playwright-кеш' },
35
+ { token: '--project=storybook', hint: 'швидкий прогін лише storybook-проєкту (не повний coverage)' }
36
+ ]
37
+
38
+ /**
39
+ * Перевіряє один репо-рівневий канонічний файл (composite action чи workflow): відсутність →
40
+ * `missingReason`-порушення з посиланням на `npx \@7n/rules fix storybook`; присутність без
41
+ * якогось маркера → `markerReason`-порушення на конкретний маркер. Той самий патерн, що й
42
+ * `checkCanonFile` у `scaffold/main.mjs`, але без per-package `rootDir` — файл один на репо.
43
+ * @param {string} cwd абсолютний корінь репозиторію
44
+ * @param {string} relFile posix-relative шлях файлу від кореня репозиторію
45
+ * @param {{ token: string, hint: string }[]} markers канонічні маркери файлу
46
+ * @param {string} missingReason reason для порушення "файл відсутній"
47
+ * @param {string} markerReason reason для порушення "маркер відсутній"
48
+ * @param {ReturnType<typeof createViolationReporter>} reporter reporter поточного лінту
49
+ * @returns {Promise<void>}
50
+ */
51
+ async function checkRepoCanonFile(cwd, relFile, markers, missingReason, markerReason, reporter) {
52
+ const abs = join(cwd, relFile)
53
+ if (existsSync(abs)) {
54
+ const content = await readFile(abs, 'utf8')
55
+ for (const m of missingMarkers(content, markers)) {
56
+ reporter.fail(`${relFile} не відповідає канону — бракує: ${m.hint} (storybook.mdc, ADR Кластер 5)`, {
57
+ reason: markerReason,
58
+ file: relFile
59
+ })
60
+ }
61
+ return
62
+ }
63
+ reporter.fail(
64
+ `Відсутній ${relFile} — канонічний Playwright-кеш для vitest storybook-проєкту: npx @7n/rules fix storybook (storybook.mdc, ADR Кластер 5)`,
65
+ { reason: missingReason, file: relFile }
66
+ )
67
+ }
68
+
69
+ /**
70
+ * Detector concern-а `storybook/ci` (ADR Кластер 5, CI-частина): для репозиторіїв з бодай
71
+ * одним Vue component library пакетом у скоупі Storybook (`collectInScopeVuePackages`) —
72
+ * канонічний composite action `setup-playwright-chromium` (кеш Playwright-браузерів, лише
73
+ * chromium) і канонічний `.github/workflows/lint-storybook.yml`, що запускає швидкий
74
+ * `vitest --project=storybook` на PR. Гейтований `requires.capability: ci:github` — спить
75
+ * у репозиторіях без плагіна `@7n/rules-ci-github` (немає `.github/workflows`).
76
+ *
77
+ * Nightly-only `@7n/test coverage` (mutation testing) — поза обсягом цього concern-а: ADR
78
+ * Кластер 5 явно розділяє швидкий PR-шлях (цей concern) і nightly mutation-прогін, який
79
+ * лишається окремою інфраструктурою `test/stryker_config`.
80
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
81
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
82
+ */
83
+ export async function lint(ctx) {
84
+ const reporter = createViolationReporter(ctx)
85
+ const cwd = ctx.cwd
86
+
87
+ const pkgs = await collectInScopeVuePackages(cwd)
88
+ if (pkgs.length === 0) {
89
+ reporter.pass('storybook/ci: немає Vue component library пакетів у скоупі (storybook.mdc)')
90
+ return reporter.result()
91
+ }
92
+
93
+ await checkRepoCanonFile(
94
+ cwd,
95
+ PLAYWRIGHT_ACTION_REL,
96
+ PLAYWRIGHT_ACTION_MARKERS,
97
+ 'missing-playwright-action',
98
+ 'playwright-action-marker-missing',
99
+ reporter
100
+ )
101
+
102
+ await checkRepoCanonFile(
103
+ cwd,
104
+ STORYBOOK_WORKFLOW_REL,
105
+ STORYBOOK_WORKFLOW_MARKERS,
106
+ 'missing-storybook-workflow',
107
+ 'storybook-workflow-marker-missing',
108
+ reporter
109
+ )
110
+
111
+ return reporter.result()
112
+ }
@@ -0,0 +1,41 @@
1
+ name: Lint Storybook
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ paths:
9
+ - '**/*.stories.@(js|ts)'
10
+ - '.storybook/**'
11
+ pull_request:
12
+ branches:
13
+ - dev
14
+ - main
15
+
16
+ concurrency:
17
+ group: ${{ github.ref }}-${{ github.workflow }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ storybook-test:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ package:
29
+ __STORYBOOK_CI_PACKAGE_DIRS__
30
+ steps:
31
+ - uses: actions/checkout@v6
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - uses: ./.github/actions/setup-bun-deps
36
+
37
+ - uses: ./.github/actions/setup-playwright-chromium
38
+
39
+ - name: Storybook vitest project (${{ matrix.package }})
40
+ working-directory: ${{ matrix.package }}
41
+ run: bunx vitest run --project=storybook
@@ -0,0 +1,34 @@
1
+ # yaml-language-server: $schema=https://json.schemastore.org/github-action.json
2
+
3
+ name: Setup Playwright Chromium
4
+ description: >-
5
+ Кеш Playwright-браузерів (лише chromium, storybook.mdc ADR Кластер 5) для vitest
6
+ browser-mode storybook-проєкту. Викликати ПІСЛЯ ./.github/actions/setup-bun-deps
7
+ (потрібен node_modules/bun.lock для визначення версії).
8
+
9
+ runs:
10
+ using: composite
11
+ steps:
12
+ - name: Resolve Playwright version (bun.lock)
13
+ id: playwright-version
14
+ shell: bash
15
+ run: |
16
+ VERSION=$(grep -oE '"playwright(-core)?@[0-9]+\.[0-9]+\.[0-9]+"' bun.lock | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || true)
17
+ if [ -z "$VERSION" ]; then
18
+ VERSION=$(node -p "require('playwright-core/package.json').version" 2>/dev/null || echo unknown)
19
+ fi
20
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
21
+
22
+ - name: Cache Playwright browsers (chromium)
23
+ id: playwright-cache
24
+ uses: actions/cache@v5
25
+ with:
26
+ path: |
27
+ ~/.cache/ms-playwright
28
+ ~/Library/Caches/ms-playwright
29
+ key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
30
+
31
+ - name: Install Playwright chromium (лише chromium, storybook.mdc)
32
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
33
+ shell: bash
34
+ run: bunx playwright install chromium --with-deps
@@ -31,7 +31,7 @@ alwaysApply: false
31
31
 
32
32
  - **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
33
33
  - **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
34
- - **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** `vite-plugin-pages`/`vite-plugin-vue-layouts` — файлова маршрутизація належить додатку-споживачу, не ізольованому рендеру компонента.
34
+ - **`viteFinal`** зчитує `vite.config` самого пакета (той самий, що й для звичайного білду) і мерджить його плагіни в конфіг Storybook, **знімаючи** роутинг-плагіни файлової маршрутизації додатка-споживача (`vite-plugin-pages`, `unplugin-vue-router`, `vite-plugin-vue-layouts`, `vite-plugin-vue-layouts-next`)вони не мають сенсу в ізольованому рендері компонента. Плагіни `plugins`-масиву можуть бути `Promise`/вкладеними масивами (`VueMacros({ plugins: { vue: Vue() } })` — реальний стек пілотного консюмера повертає `Promise`, що резолвиться в масив плагінів) — фільтр спершу resolve/flatten-ить їх, інакше порівняння імені з `Promise`, що ще не резолвився, мовчки пропускає дублікат `@vitejs/plugin-vue`-трансформу (подвійна SFC-трансформація). Vue-трансформери фільтруються за сімейством імені (`vite:vue`-префікс або підрядок `vue-macros`), не лише буквальним `@vitejs/plugin-vue` — деталі й обґрунтування коментарем у `scaffold/template/main.js`.
35
35
  - **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
36
36
  - **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1).
37
37
  - **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
@@ -43,6 +43,12 @@ alwaysApply: false
43
43
 
44
44
  Канонічний `test.projects` (`unit`+`storybook`, browser-mode лише chromium) і ізольований `vitest.stryker.config` (той самий unit-набір, без browser-mode — `@stryker-mutator/vitest-runner` крашиться на browser-mode `projects`) — `vitest-config/main.mjs` (перевірка, AST через `oxc-parser`) і `vitest-config/fix-vitest-config.mjs` (точкові insert-only правки наявного конфіга, fixability: `config`). Деталі канону, чому саме chromium і межі автофіксу — `vitest-config/vitest-config.mdc`, не дублюється тут.
45
45
 
46
+ ### CI: Playwright-кеш і швидкий PR-прогін (Кластер 5, CI-частина)
47
+
48
+ `ci/main.mjs` + `ci/fix-ci.mjs` (fixability: `config`, `requires.capability: ci:github` — спить у репозиторіях без плагіна `@7n/rules-ci-github`): для кожного репозиторію з бодай одним пакетом у скоупі — канонічний composite action `.github/actions/setup-playwright-chromium/action.yml` (кеш `~/.cache/ms-playwright`/`~/Library/Caches/ms-playwright` за версією playwright з `bun.lock`, install **лише** `chromium --with-deps` при cache miss) і `.github/workflows/lint-storybook.yml` (матриця `strategy.matrix.package` — фактичні пакети у скоупі, `checkout` → `setup-bun-deps` → `setup-playwright-chromium` → `vitest run --project=storybook`). Композитний action і workflow — репо-рівневі файли (не per-package), перевірка й автофікс не per-package.
49
+
50
+ Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
51
+
46
52
  ## Гігієна сторонніх залежностей (Кластер 6)
47
53
 
48
54
  `hygiene/main.mjs`: (1) undeclared third-party imports у `.vue`-файлах пакета — import стороннього пакета, якого немає в `dependencies`/`peerDependencies` (реальний кейс ADR — зламаний default-export `@vuepic/vue-datepicker` v14, silent breakage без цієї перевірки); (2) наявність `src/css/quasar.variables.{scss,sass}` без відповідного `quasar({ sassVariables: true })` у `.storybook/main.js` — глобальні Quasar SCSS-змінні пакета інакше не резолвляться в ізольованому Storybook-рендері. Docs-only детальний виклад не потрібен — концерн самодостатній, без окремого `.mdc`.
@@ -73,6 +73,30 @@ export const Realtime = {
73
73
  }
74
74
  ```
75
75
 
76
+ ### Точковий alias-мок одного модуля
77
+
78
+ **Скоуп цього рецепта — компонентні бібліотеки, НЕ сторінки.** Рішення «виключно MSW» вище (розділ «Apollo/GraphQL — виключно MSW») стосується сторінок (розширення ADR, `2026-07-20`) — там повний data-flow (query+mutation+subscription, кілька викликів на сторінку), і мок-хендлери мають бути переносними в майбутні vitest/playwright-сценарії. Реальний кейс компонентної бібліотеки (пілот `components/npm`) — інший: один компонент статично імпортує модуль із side-effect на рівні build-у (напр. `@nitra/vite-boot/apollo` — конструює Apollo-клієнт із `VITE_HASURA_HOST`/WS-link env-змінних, яких немає в Storybook), і компонент має 1-2 точкові виклики цього модуля, не повний GraphQL data-flow. Розгортати MSW-стек (`msw-storybook-addon`, worker, handlers) заради одного статичного імпорту — зайві накладні витрати, надто коли `msw` навіть не встановлений у пакеті.
79
+
80
+ Рецепт — build-time підміна імпорту на рівні Vite-resolve у `viteFinal` `.storybook/main.js` (не мокає мережу, підміняє сам модуль):
81
+
82
+ ```js
83
+ async viteFinal(config) {
84
+ const { mergeConfig } = await import('vite')
85
+ return mergeConfig(config, {
86
+ resolve: {
87
+ alias: {
88
+ // NImages.vue статично імпортує apolloClient/gql (не provide/inject) — реальний
89
+ // модуль конструює Apollo-клієнт з env-змінних, яких немає в Storybook.
90
+ // Підміна на рівні Vite-resolve замість зміни коду компонента.
91
+ '@nitra/vite-boot/apollo': join(dirName, 'mocks/apollo.js')
92
+ }
93
+ }
94
+ })
95
+ }
96
+ ```
97
+
98
+ **Критерій вибору:** компонент з 1-2 точковими викликами стороннього модуля (side-effect boot/client-конструктор, не повний query/mutation/subscription-flow) → alias-мок одного модуля. Сторінки, підписки чи повний GraphQL data-flow (кілька query/mutation, стан, error/loading-сценарії) → MSW (розділ вище) — не супереч рішенню розширення ADR, а розмежування скоупу: alias — для компонентних бібліотек із мінімальним мережевим слідом, MSW — для сторінок і повнішого data-flow.
99
+
76
100
  ### Pinia
77
101
 
78
102
  Справжня `createPinia()` у page-декораторі — **без** `pinia-plugin-persistedstate` (`persist: true` у сторі стає no-op, читання/запис localStorage не потрібні в ізольованому рендері). Наповнення початкового стану — через `parameters.pinia.initialState`, не хардкод у декораторі:
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: plugins/lang-js/rules/storybook/scaffold/main.mjs
5
5
  docgen:
6
- crc: c06a9e15
6
+ crc: 2cad0405
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -1,5 +1,5 @@
1
1
  /** @see ./docs/main.md */
2
- import { existsSync } from 'node:fs'
2
+ import { existsSync, readdirSync } from 'node:fs'
3
3
  import { readFile } from 'node:fs/promises'
4
4
  import { join } from 'node:path'
5
5
 
@@ -17,7 +17,16 @@ export const MAIN_JS_MARKERS = [
17
17
  { token: '@storybook/vue3-vite', hint: 'framework @storybook/vue3-vite' },
18
18
  { token: 'viteFinal', hint: 'viteFinal-override vite.config пакета' },
19
19
  { token: "'vite-plugin-pages'", hint: 'фільтр vite-plugin-pages у viteFinal' },
20
- { token: "'vite-plugin-vue-layouts'", hint: 'фільтр vite-plugin-vue-layouts у viteFinal' }
20
+ { token: "'vite-plugin-vue-layouts'", hint: 'фільтр vite-plugin-vue-layouts у viteFinal' },
21
+ { token: "'vite-plugin-vue-layouts-next'", hint: 'фільтр vite-plugin-vue-layouts-next у viteFinal' },
22
+ {
23
+ token: 'isVueTransformFamily',
24
+ hint: 'сімейний фільтр vue-трансформерів (vite:vue/vue-macros) — стійкість до VueMacros-стека'
25
+ },
26
+ {
27
+ token: 'resolvePluginEntry',
28
+ hint: 'resolve/flatten Promise/масиву плагінів перед фільтрацією (VueMacros повертає Promise)'
29
+ }
21
30
  ]
22
31
 
23
32
  /** Маркери канону `.storybook/preview.js`. Експортовано — переюз у `adopt/main.mjs`. */
@@ -30,13 +39,36 @@ export const PREVIEW_JS_MARKERS = [
30
39
  ]
31
40
 
32
41
  /**
33
- * Layout-детекція для stories-glob (ADR Кластер 2): `src/components/` присутній glob
34
- * звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по `src/`.
35
- * Шлях відносний до `.storybook/` (де лежить сам `main.js`), тому з префіксом `../`.
42
+ * Чи має корінь пакета плоскі `.vue`-файли (flat-root layout `NDialog.vue`,
43
+ * `NDialog.stories.js` лежать прямо в КОРЕНІ пакета, `src/` майже порожній чи
44
+ * відсутній). Реальний кейс пілотного консюмера (components/npm) component
45
+ * library без `src/components/`, детекція за самою наявністю `src/` дала б 0
46
+ * знайдених історій (тихий регрес adopt-діагностики). Перевірка нерекурсивна —
47
+ * дивиться лише файли безпосередньо в `absPkgDir`.
48
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
49
+ * @returns {boolean} true — у корені пакета є хоча б один `.vue`-файл
50
+ */
51
+ function hasFlatRootVueFiles(absPkgDir) {
52
+ let entries
53
+ try {
54
+ entries = readdirSync(absPkgDir, { withFileTypes: true })
55
+ } catch {
56
+ return false
57
+ }
58
+ return entries.some(e => e.isFile() && e.name.endsWith('.vue'))
59
+ }
60
+
61
+ /**
62
+ * Layout-детекція для stories-glob (ADR Кластер 2, розширено пілотом на flat-root):
63
+ * `.vue`-файли прямо в корені пакета (без `src/`) → flat-root glob по корені;
64
+ * інакше `src/components/` присутній → glob звужується до нього; інакше — ширший
65
+ * glob по всьому `src/`. Шлях відносний до `.storybook/` (де лежить сам `main.js`),
66
+ * тому з префіксом `../`.
36
67
  * @param {string} absPkgDir абсолютний шлях кореня пакета
37
68
  * @returns {string} glob для `stories` у `.storybook/main.js`
38
69
  */
39
70
  export function detectStoriesGlob(absPkgDir) {
71
+ if (hasFlatRootVueFiles(absPkgDir)) return '../*.stories.@(js|ts)'
40
72
  return existsSync(join(absPkgDir, 'src/components'))
41
73
  ? '../src/components/**/*.stories.@(js|ts)'
42
74
  : '../src/**/*.stories.@(js|ts)'
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.js
4
4
  resource: plugins/lang-js/rules/storybook/scaffold/template/main.js
5
5
  docgen:
6
- crc: 5b21e6e2
6
+ crc: 74cf3397
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.96
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -12,8 +12,53 @@ import { quasar, transformAssetUrls } from '@quasar/vite-plugin'
12
12
  const OWN_PLUGINS = [vue({ template: { transformAssetUrls } }), quasar({ sassVariables: true })]
13
13
 
14
14
  // Плагіни файлової маршрутизації додатка-споживача — не мають сенсу в ізольованому
15
- // рендері одного компонента, знімаються при обʼєднанні з vite.config пакета.
16
- const INCOMPATIBLE_PLUGIN_NAMES = new Set(['vite-plugin-pages', 'vite-plugin-vue-layouts'])
15
+ // рендері одного компонента, знімаються при обʼєднанні з vite.config пакета. Обидві
16
+ // версії layouts-пакета (старий і `-next`-форк) та pages-роутери — реальні консюмери
17
+ // мігрують поступово, старий і новий пакет можуть співіснувати в різних vite.config.
18
+ const INCOMPATIBLE_PLUGIN_NAMES = new Set([
19
+ 'vite-plugin-pages',
20
+ 'unplugin-vue-router',
21
+ 'vite-plugin-vue-layouts',
22
+ 'vite-plugin-vue-layouts-next'
23
+ ])
24
+
25
+ /**
26
+ * Плагін належить до сімейства Vue SFC-трансформерів, які ДУБЛЮЮТЬ `OWN_PLUGINS`'
27
+ * `vue()`. Реальний стек консюмерів (components/npm/vite.config.js) обгортає
28
+ * `@vitejs/plugin-vue` через `VueMacros({ plugins: { vue: Vue() } })` — після
29
+ * резолву цей виклик повертає МАСИВ плагінів: сам `vite:vue` (той самий transform,
30
+ * що й `OWN_PLUGINS`' `vue()` — дублювання дає ПОДВІЙНУ SFC-трансформацію) плюс
31
+ * службові `vue-macros-*` (devtools/exclude-dep-optimize). Плагіни macro-синтаксису
32
+ * (`unplugin-vue-define-props`/`define-emit`/`reactivity-transform` тощо) — НЕ
33
+ * фільтруються: вони не дублюються `OWN_PLUGINS` і потрібні для macro-фіч пакета;
34
+ * Vite впорядковує їх через власний `enforce: 'pre'|'post'` незалежно від позиції
35
+ * в підсумковому масиві `plugins`, тож порядок відносно `OWN_PLUGINS` тут не важливий.
36
+ * @param {string | undefined} name ім'я плагіна
37
+ * @returns {boolean} true — плагін дублює `OWN_PLUGINS`' `vue()`
38
+ */
39
+ function isVueTransformFamily(name) {
40
+ return typeof name === 'string' && (name.startsWith('vite:vue') || name.includes('vue-macros'))
41
+ }
42
+
43
+ /**
44
+ * Резолвить один запис `config.plugins` у плаский масив реальних плагінів. Vite
45
+ * офіційно підтримує `Plugin | Promise<Plugin> | (Plugin | Promise<Plugin>)[]`
46
+ * (довільна вкладеність) — `VueMacros(...)` сам повертає `Promise`, що резолвиться
47
+ * в масив плагінів. `loadConfigFromFile` читає файл конфіга як є й НЕ виконує це
48
+ * resolve/flatten (це робить лише повний `resolveConfig` пізніше у власному циклі
49
+ * Vite) — без ручного resolve/flatten тут фільтр порівнював би ім'я з
50
+ * `Promise`-об'єктом, що ще не резолвився (`undefined`), і пропускав би дублікат далі.
51
+ * @param {unknown} entry один елемент/Promise/масив із `config.plugins`
52
+ * @returns {Promise<object[]>} плаский масив плагінів після resolve
53
+ */
54
+ async function resolvePluginEntry(entry) {
55
+ const resolved = await entry
56
+ if (Array.isArray(resolved)) {
57
+ const nested = await Promise.all(resolved.map(resolvePluginEntry))
58
+ return nested.flat(Infinity)
59
+ }
60
+ return resolved ? [resolved] : []
61
+ }
17
62
 
18
63
  /** @type {import('@storybook/vue3-vite').StorybookConfig} */
19
64
  const config = {
@@ -26,13 +71,15 @@ const config = {
26
71
  staticDirs: ['./public'],
27
72
  async viteFinal(storybookConfig) {
28
73
  const loaded = await loadConfigFromFile({ command: 'serve', mode: 'development' }, undefined, process.cwd())
29
- const userPlugins = (loaded?.config?.plugins ?? [])
30
- .flat()
74
+ const rawPlugins = loaded?.config?.plugins ?? []
75
+ const resolvedNested = await Promise.all(rawPlugins.map(resolvePluginEntry))
76
+ const userPlugins = resolvedNested
77
+ .flat(Infinity)
31
78
  .filter(Boolean)
32
79
  .filter(p => !INCOMPATIBLE_PLUGIN_NAMES.has(p.name))
33
80
  // vue()/quasar() пакета замінюємо власними екземплярами у фіксованому порядку вище —
34
- // не дублюємо; решта плагінів пакета (auto-import, svg-loader тощо) лишається.
35
- .filter(p => p.name !== 'vite:vue' && p.name !== 'quasar')
81
+ // не дублюємо; решта плагінів пакета (auto-import, VueMacros macro-sugar тощо) лишається.
82
+ .filter(p => !isVueTransformFamily(p.name) && p.name !== 'quasar')
36
83
 
37
84
  return mergeConfig(storybookConfig, {
38
85
  resolve: loaded?.config?.resolve,
@@ -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`).