@7n/rules-lang-js 0.9.0 → 0.10.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 (51) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +1 -1
  3. package/rules/bun/package_json/package_json.mdc +3 -1
  4. package/rules/bun/package_json/package_json.rego +30 -1
  5. package/rules/npm-module/npm_package_json/npm_package_json.mdc +18 -3
  6. package/rules/npm-module/npm_package_json/npm_package_json.rego +42 -5
  7. package/rules/storybook/adopt/docs/index.md +9 -0
  8. package/rules/storybook/adopt/docs/main.md +46 -0
  9. package/rules/storybook/adopt/main.mjs +379 -0
  10. package/rules/storybook/hygiene/concern.json +4 -0
  11. package/rules/storybook/hygiene/docs/index.md +9 -0
  12. package/rules/storybook/hygiene/docs/main.md +45 -0
  13. package/rules/storybook/hygiene/main.mjs +254 -0
  14. package/rules/storybook/main.json +1 -0
  15. package/rules/storybook/main.mdc +60 -0
  16. package/rules/storybook/mocking/concern.json +3 -0
  17. package/rules/storybook/mocking/mocking.mdc +108 -0
  18. package/rules/storybook/scaffold/concern.json +5 -0
  19. package/rules/storybook/scaffold/docs/fix-scaffold.md +29 -0
  20. package/rules/storybook/scaffold/docs/index.md +10 -0
  21. package/rules/storybook/scaffold/docs/main.md +36 -0
  22. package/rules/storybook/scaffold/fix-scaffold.mjs +150 -0
  23. package/rules/storybook/scaffold/main.mjs +164 -0
  24. package/rules/storybook/scaffold/template/docs/index.md +10 -0
  25. package/rules/storybook/scaffold/template/docs/main.md +30 -0
  26. package/rules/storybook/scaffold/template/docs/preview.md +46 -0
  27. package/rules/storybook/scaffold/template/main.js +45 -0
  28. package/rules/storybook/scaffold/template/mocks/docs/gql-sse.md +36 -0
  29. package/rules/storybook/scaffold/template/mocks/docs/index.md +9 -0
  30. package/rules/storybook/scaffold/template/mocks/gql-sse.js +25 -0
  31. package/rules/storybook/scaffold/template/preview.js +47 -0
  32. package/rules/storybook/scope/concern.json +4 -0
  33. package/rules/storybook/scope/docs/index.md +9 -0
  34. package/rules/storybook/scope/docs/main.md +62 -0
  35. package/rules/storybook/scope/main.mjs +202 -0
  36. package/rules/storybook/vitest-config/concern.json +8 -0
  37. package/rules/storybook/vitest-config/docs/fix-vitest-config.md +38 -0
  38. package/rules/storybook/vitest-config/docs/index.md +10 -0
  39. package/rules/storybook/vitest-config/docs/main.md +72 -0
  40. package/rules/storybook/vitest-config/fix-vitest-config.mjs +340 -0
  41. package/rules/storybook/vitest-config/main.mjs +358 -0
  42. package/rules/storybook/vitest-config/template/docs/index.md +12 -0
  43. package/rules/storybook/vitest-config/template/docs/storybook-project-entry.md +34 -0
  44. package/rules/storybook/vitest-config/template/docs/unit-project-entry.md +30 -0
  45. package/rules/storybook/vitest-config/template/docs/vitest.config.baseline.md +29 -0
  46. package/rules/storybook/vitest-config/template/docs/vitest.stryker.config.baseline.md +31 -0
  47. package/rules/storybook/vitest-config/template/storybook-project-entry.js +22 -0
  48. package/rules/storybook/vitest-config/template/unit-project-entry.js +5 -0
  49. package/rules/storybook/vitest-config/template/vitest.config.baseline.mjs +37 -0
  50. package/rules/storybook/vitest-config/template/vitest.stryker.config.baseline.mjs +19 -0
  51. package/rules/storybook/vitest-config/vitest-config.mdc +33 -0
@@ -0,0 +1,202 @@
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 { loadCursorIgnorePaths } from '@7n/rules/scripts/lib/load-cursor-config.mjs'
8
+ import { walkDir } from '@7n/rules/scripts/utils/walkDir.mjs'
9
+ import { getMonorepoPackageRootDirs } from '@7n/rules/scripts/lib/workspaces.mjs'
10
+ import { isVueComponentLibraryPkg } from '../../vue/packages/main.mjs'
11
+
12
+ const CONFIG_FILE = '.n-rules.json'
13
+ const LEGACY_CONFIG_FILE = '.n-cursor.json'
14
+ const VITE_CONFIG_FILES = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
15
+
16
+ /** Поріг кількості `.vue`-файлів для скоупу канону Storybook (ADR Кластер 1). */
17
+ export const VUE_FILE_THRESHOLD = 3
18
+
19
+ /**
20
+ * @typedef {object} InScopePackage
21
+ * @property {string} rootDir відносний (posix) корінь пакета, `.` для кореня монорепо
22
+ * @property {string} absDir абсолютний шлях кореня пакета
23
+ * @property {Record<string, unknown>} pkg розпарсений `package.json` пакета
24
+ * @property {number} vueFileCount кількість знайдених `.vue`-файлів
25
+ */
26
+
27
+ /**
28
+ * Читає `storybook.optOut` з `.n-rules.json` (fallback — legacy `.n-cursor.json`). Толерантно до
29
+ * відсутнього файлу/поля/невалідного JSON — повертає порожній масив (open-by-default, як
30
+ * `read-n-rules-config-lite.mjs`). Значення — root dir пакетів (`.` для кореня, `packages/ui` тощо),
31
+ * той самий формат, що повертає `getMonorepoPackageRootDirs`.
32
+ * @param {string} cwd абсолютний корінь репозиторію
33
+ * @returns {Promise<string[]>} перелік opt-out root dir-ів
34
+ */
35
+ export async function readStorybookOptOut(cwd) {
36
+ let file = join(cwd, CONFIG_FILE)
37
+ if (!existsSync(file)) file = join(cwd, LEGACY_CONFIG_FILE)
38
+ if (!existsSync(file)) return []
39
+ let raw
40
+ try {
41
+ raw = JSON.parse(await readFile(file, 'utf8'))
42
+ } catch {
43
+ return []
44
+ }
45
+ const list = raw?.storybook?.optOut
46
+ if (!Array.isArray(list)) return []
47
+ return list.filter(v => typeof v === 'string' && v.trim().length > 0)
48
+ }
49
+
50
+ /**
51
+ * Читає прапорець хвилі 2 `storybook.detectApps` з `.n-rules.json`. За замовчуванням `false` —
52
+ * детекція app-проєктів (`vue` у dependencies + `src/pages/`) лишається відкритим питанням ADR
53
+ * і не впливає на скоуп, доки консюмер-репо не увімкне прапорець явно.
54
+ * @param {string} cwd абсолютний корінь репозиторію
55
+ * @returns {Promise<boolean>} true, якщо app-проєкти теж треба зібрати у скоуп
56
+ */
57
+ export async function readDetectAppsFlag(cwd) {
58
+ let file = join(cwd, CONFIG_FILE)
59
+ if (!existsSync(file)) file = join(cwd, LEGACY_CONFIG_FILE)
60
+ if (!existsSync(file)) return false
61
+ let raw
62
+ try {
63
+ raw = JSON.parse(await readFile(file, 'utf8'))
64
+ } catch {
65
+ return false
66
+ }
67
+ return raw?.storybook?.detectApps === true
68
+ }
69
+
70
+ /**
71
+ * Рахує `.vue`-файли в дереві пакета (поважає `.gitignore` й `ignore` з `.n-rules.json` через
72
+ * `walkDir`/`ignorePaths` — той самий обхід, що й `vue/packages`).
73
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
74
+ * @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
75
+ * @returns {Promise<number>} кількість `.vue`-файлів
76
+ */
77
+ export async function countVueFiles(absPkgDir, ignorePaths) {
78
+ let count = 0
79
+ await walkDir(
80
+ absPkgDir,
81
+ absPath => {
82
+ if (absPath.endsWith('.vue')) count += 1
83
+ },
84
+ ignorePaths
85
+ )
86
+ return count
87
+ }
88
+
89
+ /**
90
+ * Чи має пакет "стандартний" build — розпізнаваний `vite.config.{js,ts,mjs}` у корені пакета.
91
+ * Канонічний `.storybook/main.js` спирається саме на цей файл (`viteFinal` мерджить його
92
+ * плагіни) — без нього автоматичний скафолд неможливий, і пакет пропускається мовчки
93
+ * (ADR Кластер 1: "skip пакетів із нестандартним build").
94
+ * @param {string} absPkgDir абсолютний шлях кореня пакета
95
+ * @returns {boolean} true, якщо знайдено відомий `vite.config.*`
96
+ */
97
+ export function hasStandardBuild(absPkgDir) {
98
+ return VITE_CONFIG_FILES.some(f => existsSync(join(absPkgDir, f)))
99
+ }
100
+
101
+ /**
102
+ * Чи є пакет app-проєктом (не бібліотекою) для хвилі 2: `vue` у `dependencies` (не лише
103
+ * `peerDependencies`) і не бібліотека компонентів. Реалізовано зараз (щоб не переписувати
104
+ * модуль пізніше), але результат впливає на скоуп лише за прапорця `storybook.detectApps`.
105
+ * @param {{ dependencies?: Record<string, unknown>, peerDependencies?: Record<string, unknown> }} pkg розпарсений package.json
106
+ * @returns {boolean} true — app-проєкт (кандидат хвилі 2)
107
+ */
108
+ export function isVueAppPkg(pkg) {
109
+ return Boolean(pkg?.dependencies?.vue) && !isVueComponentLibraryPkg(pkg)
110
+ }
111
+
112
+ /**
113
+ * Перевіряє один workspace-корінь на відповідність предикату скоупу (бібліотека чи app) і,
114
+ * за успіху, повертає його `InScopePackage`-запис.
115
+ * @param {string} rootDir відносний корінь пакета
116
+ * @param {string} cwd абсолютний корінь репозиторію
117
+ * @param {(pkg: Record<string, unknown>) => boolean} matches предикат скоупу (бібліотека/app)
118
+ * @param {string[]} ignorePaths абсолютні шляхи, повністю виключені з обходу
119
+ * @returns {Promise<InScopePackage|null>} запис пакета або `null`, якщо поза скоупом
120
+ */
121
+ async function evaluateCandidate(rootDir, cwd, matches, ignorePaths) {
122
+ const absDir = rootDir === '.' ? cwd : join(cwd, rootDir)
123
+ const pkgPath = join(absDir, 'package.json')
124
+ if (!existsSync(pkgPath)) return null
125
+ let pkg
126
+ try {
127
+ pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
128
+ } catch {
129
+ return null
130
+ }
131
+ if (!matches(pkg)) return null
132
+ if (!hasStandardBuild(absDir)) return null
133
+ const vueFileCount = await countVueFiles(absDir, ignorePaths)
134
+ if (vueFileCount < VUE_FILE_THRESHOLD) return null
135
+ return { rootDir, absDir, pkg, vueFileCount }
136
+ }
137
+
138
+ /**
139
+ * Збирає workspace-пакети у скоупі канону Storybook хвилі 1: Vue-компонентна бібліотека
140
+ * (`vue` у `peerDependencies`, маркер `isVueComponentLibraryPkg` — той самий, що й `vue.mdc`)
141
+ * з не менше {@link VUE_FILE_THRESHOLD} `.vue`-файлами, без `storybook.optOut`, зі
142
+ * стандартним build (`vite.config.*`). Хвиля 2 (app-проєкти) додається лише за явного
143
+ * прапорця `storybook.detectApps` у `.n-rules.json`.
144
+ * @param {string} cwd абсолютний корінь репозиторію
145
+ * @returns {Promise<InScopePackage[]>} пакети у скоупі
146
+ */
147
+ export async function collectInScopeVuePackages(cwd) {
148
+ const roots = await getMonorepoPackageRootDirs(cwd)
149
+ const ignorePaths = await loadCursorIgnorePaths(cwd)
150
+ const optOut = new Set(await readStorybookOptOut(cwd))
151
+ const candidateRoots = roots.filter(r => !optOut.has(r))
152
+
153
+ /** @type {InScopePackage[]} */
154
+ const result = []
155
+ for (const rootDir of candidateRoots) {
156
+ const found = await evaluateCandidate(rootDir, cwd, isVueComponentLibraryPkg, ignorePaths)
157
+ if (found) result.push(found)
158
+ }
159
+
160
+ if (await readDetectAppsFlag(cwd)) {
161
+ for (const rootDir of candidateRoots) {
162
+ if (result.some(r => r.rootDir === rootDir)) continue
163
+ const absDir = rootDir === '.' ? cwd : join(cwd, rootDir)
164
+ if (!existsSync(join(absDir, 'src/pages'))) continue
165
+ const found = await evaluateCandidate(rootDir, cwd, isVueAppPkg, ignorePaths)
166
+ if (found) result.push(found)
167
+ }
168
+ }
169
+
170
+ return result
171
+ }
172
+
173
+ /**
174
+ * Self-check конфігурації: `.n-rules.json` → `storybook.optOut` не має посилатись на
175
+ * неіснуючі workspace-пакети (застаріле налаштування — пакет перейменували/видалили, а
176
+ * opt-out лишився). Сама детекція скоупу (поріг, build, app-проєкти) — pure-функції вище,
177
+ * покриті тестами напряму; тут лише конфіг-гігієна.
178
+ * @param {import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
179
+ * @returns {Promise<import('@7n/rules/scripts/lib/lint-surface/types.mjs').LintResult>} результат лінту
180
+ */
181
+ export async function lint(ctx) {
182
+ const reporter = createViolationReporter(ctx)
183
+ const cwd = ctx.cwd
184
+
185
+ const optOut = await readStorybookOptOut(cwd)
186
+ if (optOut.length === 0) {
187
+ reporter.pass('storybook: storybook.optOut порожній або не заданий')
188
+ return reporter.result()
189
+ }
190
+
191
+ const roots = new Set(await getMonorepoPackageRootDirs(cwd))
192
+ for (const rootDir of optOut) {
193
+ if (!roots.has(rootDir)) {
194
+ reporter.fail(
195
+ `.n-rules.json storybook.optOut містить '${rootDir}' — такого workspace-пакета немає (застаріле opt-out, storybook.mdc)`,
196
+ 'stale-opt-out'
197
+ )
198
+ }
199
+ }
200
+
201
+ return reporter.result()
202
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "fixability": "config",
4
+ "lint": {
5
+ "scope": "full",
6
+ "glob": ["package.json", "**/*.vue", ".storybook/**", "vitest.config.*", "vitest.stryker.config.*"]
7
+ }
8
+ }
@@ -0,0 +1,38 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix-vitest-config.mjs
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/fix-vitest-config.mjs
5
+ docgen:
6
+ crc: b3961bc2
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 100
10
+ issues: judge:inaccurate:0.98
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Файл детерміновано приводить `vitest.config.mjs` у Vue-компонентних пакетах із Storybook до канону `test.projects`: окремі проєкти `unit` і `storybook`, а browser-mode обмежує лише `chromium`. Для Stryker він генерує ізольований `vitest.stryker.config.*` у межах того ж пакета, згідно з підходом з `ADR Кластер 5`.
17
+
18
+ Для вже наявного `vitest.config.mjs` зміни вносяться точковими `string-splice`-ами лише на вставку, щоб не переписувати решту форматування й коментарів; після цього конфіг повторно парситься, і зміни відкочуються, якщо результат невалідний. Логіка побудови покладена на `buildFreshVitestConfig`, `buildStrykerConfig` і `patterns`, а читання й аналіз виконуються через `oxc-parser`. Детектор `main.mjs` працює read-only, тоді як запис зосереджений у `fix-stryker_config.mjs`.
19
+
20
+ ## Поведінка
21
+
22
+ - `buildFreshVitestConfig` — створює новий `vitest.config.mjs` для пакета з канонічним `test.projects` для `unit` і `storybook`, підставляє шлях до локального Vite-конфіга та сторінковий glob для Storybook.
23
+ - `buildStrykerConfig` — генерує ізольований `vitest.stryker.config.*` на основі canonical baseline-шаблону для того самого пакета, підставляючи шлях до локального Vite-конфіга.
24
+ - `patterns` — описує autofix-сценарій, який реагує на відсутній або неканонічний vitest-конфіг, доповнює наявний файл без переписування зайвого форматування та створює `vitest.stryker.config.*` у відповідному package root.
25
+
26
+ ## Публічний API
27
+
28
+ - buildFreshVitestConfig — Генерує повністю новий `vitest.config.mjs` (unit+storybook projects) для
29
+ пакета без жодного наявного vitest-конфіга. Експортовано — переюз у
30
+ `adopt/main.mjs` (генерація лише для повністю відсутніх файлів).
31
+ - buildStrykerConfig — Генерує ізольований `vitest.stryker.config.*` (той самий basename/ext що
32
+ й основний vitest-конфіг пакета) з canonical baseline-шаблону. Експортовано —
33
+ переюз у `adopt/main.mjs`.
34
+ - patterns — повертає набір шаблонів, які використовуються для зіставлення й обробки відповідних випадків у коді
35
+
36
+ ## Гарантії поведінки
37
+
38
+ - (специфічних машинно-виведених гарантій немає)
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: Directory Index
3
+ title: plugins/lang-js/rules/storybook/vitest-config
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | --------------------------------------------- | --------- |
9
+ | [fix-vitest-config.mjs](fix-vitest-config.md) | JS Module |
10
+ | [main.mjs](main.md) | JS Module |
@@ -0,0 +1,72 @@
1
+ ---
2
+ type: JS Module
3
+ title: main.mjs
4
+ resource: plugins/lang-js/rules/storybook/vitest-config/main.mjs
5
+ docgen:
6
+ crc: f5db98b8
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 85
10
+ issues: internal-name:detectStoriesGlob,anchor-miss:(vitest-config.mdc),judge:inaccurate:0.99
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Описує поведінку, за якою визначаються канонічні `vitest.config.*`, пов’язаний Stryker-конфіг, а також наявність і придатність `unit` та `storybook`-проєктів у пакеті. Для цього використовуються експортовані константи-рядки `REASON_VITEST_CONFIG_MISSING="vitest-config-missing"`, `REASON_STRYKER_CONFIG_MISSING="stryker-config-missing"`, `REASON_CONFIG_UNRESOLVABLE="vitest-config-unresolvable"`, `REASON_PROJECTS_DYNAMIC="projects-dynamic"`, `REASON_UNIT_PROJECT_MISSING="unit-project-missing"`, `REASON_STORYBOOK_PROJECT_MISSING="storybook-project-missing"`, `REASON_STORYBOOK_PROJECT_MARKER_MISSING="storybook-project-marker-missing"`, а повідомлення про такі стани передаються через маркери `vitest-config.mdc`.
17
+
18
+ ## Поведінка
19
+
20
+ - `VITEST_CONFIG_NAMES` — задає канонічний пріоритет імен `vitest.config.*` для пошуку конфіга.
21
+ - `STORYBOOK_TEST_IMPORT` — містить обов’язковий import для резолву `storybookTest`.
22
+ - `REASON_VITEST_CONFIG_MISSING` — причина, коли в пакеті немає `vitest.config.*`.
23
+ - `REASON_STRYKER_CONFIG_MISSING` — причина, коли відсутній ізольований `vitest.stryker.config.*`.
24
+ - `REASON_CONFIG_UNRESOLVABLE` — причина, коли vitest-конфіг не вдається розпарсити або знайти в ньому `test`-блок.
25
+ - `REASON_PROJECTS_DYNAMIC` — причина, коли `test.projects` не є статичним масивом.
26
+ - `REASON_UNIT_PROJECT_MISSING` — причина, коли в `test.projects` немає `unit`.
27
+ - `REASON_STORYBOOK_PROJECT_MISSING` — причина, коли в `test.projects` немає `storybook`.
28
+ - `REASON_STORYBOOK_PROJECT_MARKER_MISSING` — причина, коли `storybook`-проєкт не має канонічних маркерів.
29
+ - `CHROMIUM_RE` — маркує наявність chromium-інстансу в `storybook`-проєкті.
30
+ - `BROWSER_KEY_RE` — маркує browser-mode в `storybook`-проєкті.
31
+ - `STORIES_RE` — маркує stories-glob у `storybook`-проєкті.
32
+ - `WHITESPACE_ONLY_RE` — визначає рядок, що складається лише з whitespace.
33
+ - `LEADING_COMMA_RE` — визначає фрагмент, що починається з коми з можливим whitespace.
34
+ - `resolveVitestConfigPath` — знаходить наявний `vitest.config.*` у корені пакета або повертає `null`.
35
+ - `resolveViteConfigName` — повертає ім’я доступного `vite.config.*` для пакета, а якщо його немає — `vite.config.js`.
36
+ - `strykerConfigPathFor` — будує шлях до поруч розташованого `vitest.stryker.config.*` з тим самим розширенням.
37
+ - `storiesGlobForVitestConfig` — повертає stories-glob у вигляді, придатному для vitest-конфіга в корені пакета.
38
+ - `parseModule` — парсить JS/TS модуль і повертає AST з діагностикою.
39
+ - `findTestObject` — знаходить у дереві AST `test`-об’єкт незалежно від обгорток конфігурації.
40
+ - `findProperty` — знаходить у об’єкті property за іменем.
41
+ - `classifyProjects` — визначає, чи є в `test.projects` `unit`, і витягає зріз `storybook`-запису для перевірки маркерів.
42
+ - `lint` — перевіряє всі пакети у скоупі на канонічний vitest-конфіг для `unit+storybook` і наявність ізольованого `vitest.stryker.config.*`, повідомляючи про порушення через маркери `vitest-config.mdc`.
43
+
44
+ ## Публічний API
45
+
46
+ - STORYBOOK_TEST_IMPORT — Import, який має бути присутній у файлі, щоб `storybookTest(...)` резолвився.
47
+ - resolveVitestConfigPath — Резолвить абсолютний шлях наявного `vitest.config.*` пакета (перший знайдений
48
+ за пріоритетом {@link VITEST_CONFIG_NAMES}), або `null` якщо жодного немає.
49
+ - resolveViteConfigName — Резолвить ім'я `vite.config.*` пакета для import-шляху в baseline-шаблонах
50
+ (той самий файл, що й `viteFinal` у `.storybook/main.js`). Пакет у скоупі
51
+ гарантовано має один із них (`scope/main.mjs#hasStandardBuild`).
52
+ - strykerConfigPathFor — Шлях до ізольованого `vitest.stryker.config.*` — той самий каталог і
53
+ розширення, що й основний vitest-конфіг пакета.
54
+ - storiesGlobForVitestConfig — Stories-glob для vitest-конфіга пакета (на відміну від `detectStoriesGlob`
55
+ scaffold-концерна — той повертає шлях відносно `.storybook/`, тут vitest-конфіг
56
+ лежить у корені пакета, тож префікс `../` треба зняти).
57
+ - parseModule — Парсить JS/TS файл через oxc-parser з обраним lang за розширенням.
58
+ - findTestObject — Шукає перший `ObjectExpression` у дереві AST, що має property `test` зі
59
+ значенням-`ObjectExpression` — незалежно від того, чи огорнутий він у
60
+ `defineConfig(...)`/`mergeConfig(...)`, чи це простий об'єкт (стійко до
61
+ варіацій testing.mdc-канону).
62
+ - findProperty — Шукає property `name` у `ObjectExpression`.
63
+ - classifyProjects — Класифікує елементи масиву `test.projects`: чи є `unit`-проєкт, і
64
+ source-зріз елемента `storybook`-проєкту (для marker-перевірки).
65
+ - lint — Перевіряє канонічний vitest-конфіг (unit+storybook projects) і наявність
66
+ ізольованого `vitest.stryker.config` для всіх пакетів у скоупі Storybook.
67
+ Надішли, будь ласка, файл або вміст коду, до якого треба написати документацію.
68
+
69
+ ## Гарантії поведінки
70
+
71
+ - Read-only: не виконує операцій запису (ФС/БД).
72
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).