---
description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs + vitest.config.mjs і гейтом покриття/мутаційного тестування (концерн coverage; конфіг cargo-mutants — концерн cargo_mutants_config правила rust у @7n/rules-lang-rust).
version: '3.0'
globs: "**/{.n-rules.json,package.json,stryker.config.mjs,vitest.config.mjs,vitest.config.js},**/*.test.mjs,**/*.vue,**/.storybook/**"
alwaysApply: false
---

Правило **test** керує розміщенням тестових файлів, безпекою ізоляції тестів (заборона `process.chdir`, відносних шляхів у FS, ручного store/restore console), налаштуванням Vitest/Stryker baseline і **гейтом покриття/мутаційного тестування** (концерн `coverage`). Конфігурація cargo-mutants для Rust — концерн `cargo_mutants_config` правила `rust` (@7n/rules-lang-rust).

## Покриття + мутаційне тестування (концерн coverage)

Покриття вимірюється як звичайний lint-концерн (spec 2026-07-22, влиття `@7n/test` у `@7n/rules`) — пакета `@7n/test` і файлу `COVERAGE.md` більше немає. Мовну специфіку постачають coverage-провайдери плагінів (слот `coverage.provider@1`): `@7n/rules-lang-js` — vitest + Stryker (vitest-runner, `coverageAnalysis: 'perTest'`) + окремий Storybook-вимір; провайдери rust/python — у відповідних плагінах.

- **Делта-lint** (`npx @7n/rules lint`): легкий per-file вимір покриття рядків лише змінених файлів, без мутаційного тестування; файл нижче порогу → порушення.
- **`npx @7n/rules lint test --no-fix`** — повний вимір (coverage + мутаційне тестування по всіх workspaces) лише з гейтом: нижче порогу → ненульовий exit. **Канонічний CI-крок.**
- **`npx @7n/rules lint test`** — те саме + fix-шлях: LLM генерує тести на непокриті файли й survived-мутанти (fix-worker концерну, ladder ядра).

Пороги (дефолт 80/80 %) конфігуруються у `.n-rules.json`:

```json
{ "coverage": { "coverageThreshold": 80, "mutationThreshold": 80 } }
```

У `package.json` (корінь) має бути `scripts.coverage` із викликом `npx @7n/rules lint test --no-fix`.

### Multi-workspace iteration

У monorepo провайдер ітерує усі workspaces з власним `package.json` і агрегує метрики lcov + Stryker у єдиний рядок області (`JS`, `Vue (Storybook)`). Workspace без тестів пропускається без помилки.

- [package.json.contains.json](./package_json/template/package.json.contains.json)

## Канон Storybook для Vue-компонентних бібліотек (хвиля 1; концерни storybook-*)

Джерело рішення: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`. Storybook впроваджувався вручну двічі в різних nitra-репо в одній сесії — обидва рази з тими самими невидимими заздалегідь проблемами (конфлікт `@vitejs/plugin-vue`/`quasar()`, недоступні внутрішні Quasar-іконки без `iconSet`+`iconMapFn`, ручне мокання мережі) і одним реальним merge-конфліктом між двома паралельними ручними реалізаціями тієї самої фічі. Це правило робить канонічний скафолд стандартом, а не одноразовим рішенням кожного агента.

**Rollout:** із влиттям у правило `test` (spec 2026-07-22 absorb-7n-test) storybook-концерни їдуть разом із завжди-активним правилом `test`; поза скоупом (не Vue-компонентна бібліотека) вони no-op через детекцію скоупу. Хвиля 1 покриває детекцію скоупу, канонічний скафолд `.storybook/`, vitest/Stryker-конфіг (Кластер 5), гігієну сторонніх залежностей (Кластер 6), рецепти мокання (docs-only, Кластер 3) і `--adopt`-режим rollout-у для вже впроваджених вручну пакетів (Кластер 8); **LLM-генерація args для stories — хвиля 2**, свідомо відкладена (ADR: ризик одночасного org-wide CI-блоку на необкатаному generation pipeline, на відміну від production-proven doc-files).

Усі concern-и хвилі 1 (`storybook-scope`/`storybook-scaffold`/`storybook-vitest-config`/`storybook-hygiene`) мають `lint`-поверхню в `concern.json` і виконуються під `npx @7n/rules lint test` — `scope`/`scaffold`/`vitest-config` раніше декларували лише `check: true` без `lint`-блоку і тому мовчки не підхоплювались unified lint-рушієм (`run-detectors.mjs` виконує лише concern-и з явним `lint` чи `policy` блоком); виправлено разом із введенням `adopt`-режиму.

## Скоуп

Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати.

**Наявність `vite.config.{js,ts,mjs}` пакета — НЕ умова скоупу** (хвиля 1.4, фікс за результатами rollout-у на `tauri-components/npm`). До хвилі 1.4 тут була функція `hasStandardBuild` — вона мовчки виключала зі скоупу пакети без власного `vite.config.*` ("skip нестандартного build", ADR Кластер 1). На практиці це виключало легітимні source-only Vue-бібліотеки (peerDependencies.vue + достатньо `.vue`-файлів, просто без окремого Vite-білду) — а канонічний скафолд для них працює без жодних змін: `viteConfigPath` у `.storybook/main.js` завжди вказує на власний `empty-vite.config.js` скафолда (не на `vite.config.*` пакета), а `viteFinal` викликає `loadConfigFromFile`, який толерує відсутній конфіг пакета (повертає `null`, `viteFinal` мерджить порожній список плагінів). Емпірично перевірено на `tauri-components/npm`: `storybook build` + повний прогін тестів у реальному chromium — без жодного `vite.config.*` у пакеті. `hasStandardBuild` прибрано з `storybook-scope/main.mjs` разом з усіма згадками; для пакета, де канонічний скафолд дійсно не підходить (справжня екзотика білду — не сам факт відсутності Vite-конфіга), власник додає його у `storybook.optOut` вручну — окрема автоматична детекція "нестандартного build" дублювала б наявний opt-out і давала б хибні спрацювання.

Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`), хвиля 2a. Детекція реалізована в `storybook-scope/main.mjs`, викликається лише за явного прапорця `storybook.detectApps: true` у `.n-rules.json` консюмера — деталі й асиметрія скафолда нижче, розділ «Хвиля 2a: app-проєкти».

**Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.

Логіка детекції (поріг, opt-out, app-проєкти) — у `storybook-scope/main.mjs`, не дублюється тут.

`storybook-vitest-config`-концерн (Кластер 5, нижче) генерує baseline `vitest.config.mjs`/`vitest.stryker.config.mjs` з `import viteConfig from './<vite.config.*>'` — для source-only пакета без `vite.config.*` це зламало б import неіснуючого файлу; `storybook-vitest-config/fix-storybook-vitest-config.mjs#applyViteConfigImport` підставляє замість import-у порожній локальний `const viteConfig = {}` (еквівалент для `mergeConfig`), решта baseline-структури не змінюється.

## Канонічний скафолд

Для кожного пакета в скоупі обов'язкові: `.storybook/main.js`, `.storybook/preview.js`, `package.json#scripts.storybook`. Канонічні шаблони — `storybook-scaffold/template/`.

Фіксовані рішення (деталі — ADR, Кластер 2):

- **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
- **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
- **`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` — деталі й обґрунтування коментарем у `storybook-scaffold/template/main.js`.
- **`core.builder.options.viteConfigPath` на `.storybook/empty-vite.config.js` — ОБОВ'ЯЗКОВИЙ, не опційний.** Емпірично підтверджено (лише через `storybook build`, `dev --smoke-test` не ловить): без явного `viteConfigPath` `@storybook/builder-vite` сам через `loadConfigFromFile` знаходить `../vite.config.js` пакета ЩЕ ДО виклику `viteFinal` і домерджує його НЕФІЛЬТРОВАНІ плагіни в `storybookConfig` — фільтр у `viteFinal` тоді лише ДОДАЄ ще один `@vitejs/plugin-vue`, а не прибирає вже змерджений builder-vite дублікат (подвійна SFC-трансформація, `storybook build` падає на кожному `.vue`). `empty-vite.config.js` — канонічний порожній `defineConfig({})`-стенд-ін, генерується скафолдом поряд з `main.js`; окрема секція перевірки/adopt-діагностики (не частина `MAIN_JS_MARKERS` — main.js може бути канонічним, а сусідній файл видалено окремо).
- **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
- **`preview.js`**: повний `Quasar`-install (не тільки окремі компоненти) + `iconSet`+`iconMapFn`-комбо — без цієї пари внутрішні Quasar-компоненти (напр. стрілка `QSelect`) не резолвлять вбудовані іконки поза full CLI build. Обидва `iconSet` — підшляховий default-імпорт (`quasar/icon-set/svg-material-icons`, `quasar/icon-set/material-icons`), **НЕ** named-import `iconSet` напряму з пакета `quasar` — quasar 2.18.x не має такого runtime-binding (лише компонент `IconSet` з великої літери), `@quasar/vite-plugin`-transform падає на такому специфікаторі. `msw-storybook-addon` ініціалізується з `onUnhandledRequest`-фільтром: **same-origin GET мовчки пропускається** (Vite HMR/asset-шум), решта — попередження (не білд-помилка — навмисно м'яко для хвилі 1). Мережевий мок підключається через `loaders: [mswLoader]`, **не** `decorators: [mswDecorator]` — `mswDecorator` deprecated у `msw-storybook-addon` 2.x (буде видалений у наступному релізі), `mswLoader` виконується per-story до рендеру.
- **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
- **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `storybook-scaffold/main.mjs`, не дублюється тут).

Перевірка присутності й ключових маркерів канону — wasm-плагін `crates/plugin-lang-js` (детектор `detect_storybook_scaffold`; JS-детектор `storybook-scaffold/main.mjs#lint` видалено, зняття подвійної реалізації кластера `test/*`); детерміноване відтворення відсутніх файлів із `storybook-scaffold/template/` — `storybook-scaffold/fix-storybook-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен). `storybook-scaffold/main.mjs` лишається — маркер-набори (`MAIN_JS_MARKERS` тощо) і `detectStoriesGlob` переюзають фіксер і `storybook-adopt/main.mjs`.

### knip-виключення для `.storybook/`-артефактів

Кожен adopt цього канону наступає на ту саму knip-проблему: `empty-vite.config.js` (динамічний шлях через `join(dirName, …)` у `main.js` — knip не резолвить `join`-конструйовані шляхи), `mocks/apollo.js` (точковий alias-мок, `mocking.mdc` — не кожен пакет його використовує) і `mocks/gql-sse.js` (helper, статично імпортується лише зі story-файлів, яких у щойно заскафолженому пакеті може ще не бути) регулярно фолсяться knip-ом як orphan-files. Канонічний фікс — **docs-only** сніпет, не автофікс: механізм `js/check` copy-once канону `knip.json` (`n-js.mdc`) свідомо **не ревалідує** вміст після першого створення (консюмер вільно кастомізує), тож програмний patch наявного `knip.json` консюмера конфліктував би з цим дизайн-рішенням і вимагав би нового merge-механізму замість наявного copy-if-missing. Додай вручну в `knip.json` консюмера (root або поряд з першим `.storybook/`):

```json
{
  "ignore": ["**/.storybook/empty-vite.config.js", "**/.storybook/mocks/apollo.js", "**/.storybook/mocks/gql-sse.js"]
}
```

(домердж у наявний масив `ignore`, не заміна). Звірено з реальним `knip.json` пілотного консюмера.

Симетрична ручна правка для `oxfmt`: `.storybook/public/mockServiceWorker.js` (згенерований пакетом `msw` файл, `npx msw init` — той самий service worker, на який вказує `staticDirs` у `main.js`) слід додати в `.oxfmtrc.json` консюмера в `ignorePatterns: ["**/mockServiceWorker.js"]` — реформатування псує pristine-стан цього файлу (той самий принцип, що й `**/adr/**`-виняток `oxfmt`/`cspell`, `docs/adr/adr-виключення-oxfmt-cspell.md`).

## Vitest-конфіг і Stryker-ізоляція (Кластер 5)

Канонічний `test.projects` (`unit`+`storybook`, browser-mode лише chromium) і ізольований `vitest.stryker.config` (той самий unit-набір, без browser-mode — `@stryker-mutator/vitest-runner` крашиться на browser-mode `projects`) — перевірка (AST через `oxc_parser`) тепер у wasm-плагіні `crates/plugin-lang-js` (детектор `detect_storybook_vitest_config`; JS-детектор `storybook-vitest-config/main.mjs#lint` видалено) і `storybook-vitest-config/fix-storybook-vitest-config.mjs` (точкові insert-only правки наявного конфіга, fixability: `config`, переюзає AST-примітиви `storybook-vitest-config/main.mjs`, який лишається). Деталі канону, чому саме chromium і межі автофіксу — `storybook-vitest-config/storybook-vitest-config.mdc`, не дублюється тут.

### CI: Playwright-кеш і швидкий PR-прогін (Кластер 5, CI-частина)

Перевірка — wasm-плагін `crates/plugin-lang-js` (детектор `detect_storybook_ci`; JS-детектор `storybook-ci/main.mjs` видалено повністю, зняття подвійної реалізації кластера `test/*`) + `storybook-ci/fix-storybook-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.

`workflow.on.push.paths` — **`**/.storybook/**`** (не `.storybook/**` без префіксу): пакет у скоупі майже завжди лежить не в корені репозиторію (`npm/.storybook/**` тощо), а GitHub Actions `paths`-глоб без `**/`-префіксу анкорить збіг до кореня репо й мовчки не спрацьовує на push у вкладений пакет. Той самий анкоринг-баг був і в усіх `concern.json#lint.glob`/`main.json#auto.glob`-патернах цього правила (`package.json`, `.storybook/**`, `vitest.config.*` без префіксу) — виправлено разом, звірено емпірично на пілотному консюмері (`components/.github/workflows/lint-storybook.yml`).

Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.

## Гігієна сторонніх залежностей (Кластер 6)

Перевірка — wasm-плагін `crates/plugin-lang-js` (детектор `detect_storybook_hygiene`; JS-детектор `storybook-hygiene/main.mjs` видалено повністю, зняття подвійної реалізації кластера `test/*`): (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`.

**Свідомо лише `type: 'library'`** (хвиля 2a, фікс за результатами живого пілота gt): обидві перевірки писались і перевірялись лише на бібліотечному кейсі й дають хибні спрацювання на app-пакетах — Vite `resolve.alias`-специфікатори (`components`, `src`, `boot` тощо, типова Quasar CLI-конвенція) у `.vue`-сторінках хибно розпізнаються як undeclared third-party пакет, а канонічний app-`main.js` (розділ «Хвиля 2a» нижче) СВІДОМО ніколи не викликає `quasar()` (маркер `sassVariables` там не з'явиться, навіть якщо SCSS-змінні пакета коректно підключені через власний `vite.config.js`).

## Мокання (Кластер 3, docs-only)

Рецепти router/`@nitra/tfm`/Apollo-GraphQL(MSW)/Pinia/сторінкових stories — `storybook-mocking/storybook-mocking.mdc`. Свідомо без механічної перевірки (`concern.json` без `check`/`policy`/`lint`-блоку, як і решта чисто-документаційних concern-ів репозиторію) — кожен пакет мокає свій набір залежностей по-своєму, детермінований чек дав би або хибні спрацювання, або нульове покриття.

## Хвиля 2a: app-проєкти

Джерело рішення: розділ «Розширення (2026-07-20): сторінки — route.params + Apollo subscription + Pinia» ADR, прототип-verified на `gt` (`src/pages/task/[id].vue`). Друга хвиля rollout-у — canonical-скафолд і smoke-покриття для app-проєктів (сторінки з `route.params` + Apollo-підпискою + Pinia), opt-in і свідомо м'який (warn), на відміну від обов'язкового гейта бібліотек хвилі 1.

**Скоуп-прапорець:** app-проєкти детектуються за `vue` у `dependencies` (не `peerDependencies`, не бібліотека) + наявний `src/pages/`, але потрапляють у скоуп **лише** за явного `storybook.detectApps: true` у `.n-rules.json` консюмера — глобального увімкнення нема (`storybook-scope/main.mjs#isVueAppPkg`/`readDetectAppsFlag`). `collectInScopeVuePackages` повертає поле `type: 'library'|'app'` на кожен запис — downstream-concern-и (`scaffold`, `vitest-config`, `adopt`) розгалужують перевірку за ним. **Без порога {@link VUE_FILE_THRESHOLD}**: на відміну від бібліотек хвилі 1 (≥3 `.vue`), app-проєкт потрапляє у скоуп навіть з однією сторінкою — сторінкове покриття смоук-рівня свідомо м'яке, поріг відсікав би легітимні малі app-проєкти.

**Асиметрія скафолда (свідома дзеркальність із бібліотекою):** app-канонічні `.storybook/main.js`/`preview.js` (`storybook-scaffold/template/app-main.js`/`app-preview.js`, маркери `APP_MAIN_JS_MARKERS`/`APP_PREVIEW_JS_MARKERS`) — **протилежний** підхід до `viteConfigPath`:

- **Бібліотека (хвиля 1):** `viteConfigPath` на порожній `empty-vite.config.js` — `builder-vite` НЕ бачить `vite.config.js` пакета, `viteFinal` домерджує ВІДФІЛЬТРОВАНІ плагіни (свої `vue()`/`quasar()`-інстанси у фіксованому порядку).
- **App-проєкт (хвиля 2a):** **немає** `viteConfigPath`-обходу взагалі — `@storybook/builder-vite` сам підхоплює ПОВНИЙ `vite.config.js` app-проєкту (`VueMacros`/`$ref`, `unplugin-auto-import`, `quasar()` — усі лишаються як є, без власних інстансів у `viteFinal`, бо сторінки app-проєкту використовують build-time макроси). `viteFinal` знімає ЛИШЕ справжні layout/router-генератори консюмера (`unplugin-vue-router`, `vite-plugin-vue-layouts`/`-next`) — story імпортує сторінку напряму, маршрут будує `pageLoader`. **`vite-plugin-pages` СВІДОМО НЕ знімається** (фікс за результатами живого пілота на `gt`, було багом ранньої версії канону): прототипні сторінки з custom-блоком `<route lang="yaml">` (типова конвенція `vite-plugin-pages` для per-page layout/meta) без самого плагіна лишаються без обробника цього блоку — `@vitejs/plugin-vue` генерує `import … from '<файл>?vue&type=route&…&lang.yaml'`, який ніхто не обробляє далі, і `storybook build` падає з `MISSING_EXPORT` для ВСЬОГО пакета (не лише сторінки в story), незалежно від того, чи є на неї story. `vite-plugin-pages` сам по собі — no-op для stories (генерує `virtual:generated-pages`, який ніхто не імпортує з `.storybook/preview.js` чи story-файлів), лишається активним і мовчки обробляє `<route>`-блоки для docgen-проходу Storybook по `src/pages/`. Деталі — коментар `storybook-scaffold/template/app-main.js`.
- Тому app-скафолд **не має** `.storybook/empty-vite.config.js`-секції — wasm-детектор `detect_storybook_scaffold` (`crates/plugin-lang-js`, app-гілка) її не перевіряє, `storybook-adopt/main.mjs#diagnosePackage` не діагностує для `type: 'app'`.

`app-preview.js` — canonical `pageLoader` (per-story `router`/`pinia` за `parameters.route`/`parameters.pinia` story-meta, `router.replace(url)` + `await router.isReady()` до mount, `createPinia()` БЕЗ `pinia-plugin-persistedstate` + сідінг з `parameters.pinia.initialState`) і явна реєстрація `QLayout`/`QPageContainer` (Quasar SFC-transform не працює в runtime-темплейтах декораторів, `q-page` кидає без layout-предка) — той самий `msw-storybook-addon`/`onUnhandledRequest`-фільтр, що й бібліотечний preview. `.storybook/mocks/gql-sse.js` — **реюз** того самого канонічного `sseSubscription`-helper-а бібліотек (`storybook-scaffold/template/mocks/gql-sse.js`), не окремий app-варіант. Story-патерн (page-декоратор QLayout-wrapper, фікстури в `.storybook/fixtures/<page>.js`, `parameters.msw`, smoke + Loading/Error/Realtime) — `storybook-mocking/storybook-mocking.mdc`, розділ «Патерн story для сторінки» (узгоджено з прототипом — router/pinia будує канонічний `pageLoader`, не story-файл).

**Smoke-покриття (`storybook-page-coverage`-концерн, рівень `warn`):** кожен `.vue` під `src/pages/` app-пакета має мати хоча б один `*.stories.js` у тому самому каталозі (не обов'язково той самий basename — реальний кейс `gt`: `task/[id].vue` + `task/task-detail.stories.js`). М'який сигнал (не гейт) — хвиля 2a свідомо не блокує CI на відсутність story.

**Stryker-рішення (прийняте, відкрите питання ADR закрито консервативно):** page-stories з vitest `storybook`-проєкту виключаються зі Stryker-скоупу — mutation testing по сторінках з живими Apollo-підписками дорогий і флейкі. Механізм — той самий, що й для бібліотек: ізольований `vitest.stryker.config.*` (`storybook-vitest-config`-концерн) взагалі не містить `projects`/browser-mode, тож page-stories фізично не входять у Stryker-прогін незалежно від типу пакета. Рішення відкрите до перегляду, якщо з'явиться дешевший спосіб мутувати сторінки без флейкі SSE-таймінгів.

**Vitest-конфіг для app-пакета:** та сама генерична augment-логіка `storybook-vitest-config`-концерна (дописує `test.projects` до наявного `vitest.config.js`, якщо він уже є — реальний кейс `gt`), з ДВОМА нюансами, залежними від типу пакета:

- **Stories-glob** для нового `storybook`-запису — фіксований `APP_STORIES_GLOB` (`src/**/*.stories.@(js|ts)`), не бібліотечна layout-детекція `detectStoriesGlob` (`src/components/` vs `src/`) — інакше app-проєкт з ОБОМА `src/components/` (переюзані презентаційні компоненти) і `src/pages/` отримав би glob, звужений лише до `components/`, і мовчки загубив би page-stories з vitest-прогону (`storiesGlobForVitestConfig(absPkgDir, type)`).
- **Плагіни storybook-запису** (фікс за результатами живого пілота gt, було багом ранньої версії канону): app-варіант — `app-storybook-project-entry.js`/`vitest.config.app.baseline.mjs` (`storybook-vitest-config/template/`, вибір за `storybookEntryTemplateName(type)`/`vitestConfigBaselineName(type)` у `fix-storybook-vitest-config.mjs`) — отримує ВЛАСНІ `quasar({ sassVariables: true })`/`AutoImport({ imports: [...] })`/`Pages()`-плагіни (плюс власні import-и, `ensureStorybookEntryImports(src, type)`), а не голий `extends: true` бібліотечного `storybook-project-entry.js`. Причина: батьківський `baseVite` (unit-проєкт, canon `test.mdc`) свідомо СТРИПАЄ ці плагіни для юніт-ізоляції (`vite:quasar`/`unplugin-auto-import`/`vite-plugin-pages` — `STRIPPED_PREFIXES`), а сторінкові stories app-проєкту реально їх потребують (SCSS sass-змінні, auto-import глобали типу `gql`/`useSubscription`, обробник `<route>`-блоку). Lint (wasm-детектор `collect_storybook_marker_hints` у `crates/plugin-lang-js`; JS-еквівалент `storybook-vitest-config/main.mjs#collectStorybookMarkerHints` видалено разом із рештою lint-поверхні кластера `test/*`) і adopt-діагностика (`storybook-adopt/main.mjs#collectAdoptMarkerHints`) для `type: 'app'` так само вимагають ці три маркери в наявному `storybook`-проєкті (не лише при генерації).

**`.storybook/vitest.setup.js`** (canon-фікс, було відсутнє в шаблонах): той самий стандартний `@storybook/addon-vitest`-boilerplate (`setProjectAnnotations`/`beforeAll`) для ОБОХ типів пакета — `storybook-scaffold`-концерн (маркери `setProjectAnnotations`/`beforeAll` — wasm-каноні `VITEST_SETUP_JS_MARKERS`-const у `crates/plugin-lang-js`, JS-сторона їх більше не тримає окремо після видалення lint-поверхні; `template/vitest.setup.js`, `storybook-scaffold-vitest-setup-js`-T0-патерн, `storybook-adopt/main.mjs#diagnoseVitestSetupJsSection`) перевіряє й відтворює його як частину спільного скафолда — без цього файлу `vitest run --project=storybook` не підключає анотації `.storybook/preview.js` (decorators/loaders/parameters) до browser-тестів.

**Adopt-режим (пілот `gt`):** посекційна діагностика для app-пакетів — `app-main.js`/`app-preview.js` (app-маркери, без `empty-vite.config.js`), `mocks/gql-sse.js` (реюз), `vitest.setup.js` (той самий файл, що й у бібліотек), нова секція `.storybook/fixtures/` (наявність каталогу з бодай одним файлом — вміст app-специфічний, `--fix-missing` її НЕ генерує), `package.json#scripts.storybook`, vitest `test.projects` (для `type: 'app'` — і власні quasar()/AutoImport()/Pages()-маркери, не лише chromium/browser/stories/provider-factory)/Stryker-конфіг.

## Adopt-режим і скіл `n-storybook` (Кластер 8)

Скіл `npm/skills/storybook/` (`.cursor/skills/n-storybook/` після синку) — тонка обгортка запуску: звичайний режим — `npx @7n/rules lint test`; `--adopt` — окремий діагностичний JS-модуль `storybook-adopt/main.mjs` для пакетів, де ВЖЕ є ручний `.storybook/`, що не збігається з каноном. Adopt діагностує diff по секціях проти `template/` (main.js/preview.js/mocks/gql-sse.js/package.json#scripts.storybook/vitest test.projects/vitest.stryker.config — плюс `empty-vite.config.js`/`.storybook/fixtures/` розгалужено за типом пакета, хвиля 2a) — статус `match`/`differ`/`missing` на секцію, **без сліпого перезапису** розбіжних файлів; автофікс (`--fix-missing`) генерує лише секції зі статусом `missing`. Circuit breaker: збій діагностики одного пакета деградує до `status: 'broken'` для нього, решта пакетів прогону обробляються далі. Викликається напряму (`bun node_modules/@7n/rules-lang-js/rules/test/storybook-adopt/main.mjs`), без окремого CLI-прапорця в ядрі `n-rules.js` — деталі й приклади звіту в `SKILL.md` скіла.

## Що свідомо поза хвилею 1 і 2a

LLM-генерація `args` для stories з `defineProps`/`defineEmits`/slots (Кластер 4 ADR) — окрема хвиля (номерована як «хвиля 2» в ADR, не плутати з app-скафолдом «хвиля 2a» вище), свідомо відкладена. Governance-винятки в `n-npm-module.mdc`/`n-bun.mdc` (Кластер 7 — Storybook-devDeps у `npm/package.json`, canonical version pin, review-гейт лише для нових stories) — окремий трек, не в обсязі цього правила.
