@7n/rules 1.43.1 → 1.44.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.
- package/CHANGELOG.md +15 -0
- package/docs/vitest.config.md +14 -19
- package/package.json +1 -1
- package/rules/abie/lib/docs/index.md +0 -2
- package/rules/abie/lib/http-route.mjs +1 -0
- package/rules/abie/lib/yaml.mjs +2 -0
- package/rules/ci4/marksman_config/docs/index.md +10 -0
- package/rules/ci4/marksman_config/main.mjs +2 -0
- package/rules/doc-files/docgen-prompts/docs/index.md +9 -0
- package/rules/doc-files/docgen-prompts/main.mjs +5 -0
- package/rules/hasura/internal_urls/docs/index.md +0 -2
- package/rules/hasura/internal_urls/docs/main.md +27 -15
- package/rules/hasura/internal_urls/main.mjs +1 -0
- package/rules/image-avif/avif_generation/docs/index.md +10 -0
- package/rules/image-avif/avif_generation/main.mjs +2 -0
- package/rules/rego/vscode_settings/docs/fix-vscode_settings.md +12 -10
- package/rules/rego/vscode_settings/fix-vscode_settings.mjs +5 -0
- package/rules/tauri/cargo_mutants_config/docs/index.md +0 -2
- package/rules/tauri/cargo_mutants_config/docs/main.md +38 -12
- package/rules/tauri/cargo_mutants_config/main.mjs +5 -1
- package/rules/tauri/core_test_isolation/docs/main.md +19 -16
- package/rules/tauri/core_test_isolation/main.mjs +3 -1
- package/rules/tauri/linux_deps/docs/main.md +25 -18
- package/rules/tauri/linux_deps/main.mjs +2 -1
- package/rules/tauri/release/docs/main.md +20 -12
- package/rules/tauri/release/main.mjs +2 -0
- package/rules/tauri/updater/docs/main.md +31 -14
- package/rules/tauri/updater/main.mjs +4 -0
- package/rules/test/coverage/concern.json +9 -0
- package/rules/test/coverage/fix-worker.mjs +111 -0
- package/rules/test/coverage/lib/classify/apply.mjs +67 -0
- package/rules/test/coverage/lib/classify/cache.mjs +77 -0
- package/rules/test/coverage/lib/classify/docs/apply.md +28 -0
- package/rules/test/coverage/lib/classify/docs/cache.md +34 -0
- package/rules/test/coverage/lib/classify/docs/index.md +37 -0
- package/rules/test/coverage/lib/classify/docs/prompt.md +30 -0
- package/rules/test/coverage/lib/classify/docs/verdict-schema.md +30 -0
- package/rules/test/coverage/lib/classify/index.mjs +140 -0
- package/rules/test/coverage/lib/classify/prompt.mjs +136 -0
- package/rules/test/coverage/lib/classify/verdict-schema.mjs +163 -0
- package/rules/test/coverage/lib/llm.mjs +100 -0
- package/rules/test/coverage/main.mjs +161 -0
- package/rules/test/main.json +1 -0
- package/rules/test/main.mdc +140 -0
- package/rules/test/package_json/concern.json +11 -0
- package/rules/test/package_json/package_json.mdc +18 -0
- package/rules/test/package_json/package_json.rego +25 -0
- package/rules/test/package_json/template/package.json.contains.json +6 -0
- package/rules/text/oxfmtrc/fix-oxfmtrc.mjs +5 -0
- package/rules/text/vscode_settings/fix-vscode_settings.mjs +5 -0
- package/rules/worktree/vscode_settings/docs/fix-vscode_settings.md +10 -7
- package/rules/worktree/vscode_settings/fix-vscode_settings.mjs +5 -0
- package/rules/worktree/zed_settings/fix-zed_settings.mjs +5 -0
- package/schemas/n-rules.json +18 -1
- package/scripts/lib/adr/docs/index.md +0 -2
- package/scripts/lib/adr/docs/normalize-pipeline.md +44 -28
- package/scripts/lib/adr/normalize-pipeline.mjs +11 -0
- package/scripts/lib/docs/inline-template-links.md +17 -8
- package/scripts/lib/docs/plugin-api.md +1 -1
- package/scripts/lib/inline-template-links.mjs +5 -0
- package/scripts/lib/lint-surface/docs/run-detectors.md +25 -22
- package/scripts/lib/lint-surface/docs/tier-sampling-experiment.md +30 -14
- package/scripts/lib/lint-surface/run-detectors.mjs +1 -1
- package/scripts/lib/lint-surface/tier-sampling-experiment.mjs +1 -0
- package/scripts/lib/plugin-api.mjs +46 -0
- package/scripts/utils/docs/walkDir.md +26 -15
- package/scripts/utils/docs/worktree-fingerprint.md +16 -15
- package/scripts/utils/walkDir.mjs +9 -5
- package/scripts/utils/worktree-fingerprint.mjs +5 -0
- package/skills/storybook/SKILL.md +4 -4
- package/skills/taze/js/migration-cache.mjs +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/** @see ./docs/main.md */
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
8
|
+
import { assertCoverageProvider } from '../../../scripts/lib/plugin-api.mjs'
|
|
9
|
+
import { readNRulesConfigLite } from '../../../scripts/lib/read-n-rules-config-lite.mjs'
|
|
10
|
+
import { getHandlers } from '../../../scripts/lib/resolve-plugins.mjs'
|
|
11
|
+
import { applyVerdicts } from './lib/classify/apply.mjs'
|
|
12
|
+
import { classify } from './lib/classify/index.mjs'
|
|
13
|
+
|
|
14
|
+
/** Дефолтний поріг line coverage, % (успадковано з `@7n/test` COVERAGE_THRESHOLD). */
|
|
15
|
+
const DEFAULT_COVERAGE_THRESHOLD = 80
|
|
16
|
+
/** Дефолтний поріг mutation score, % (рішення spec 2026-07-22, п. 3 підтверджених judgment calls). */
|
|
17
|
+
const DEFAULT_MUTATION_THRESHOLD = 80
|
|
18
|
+
/**
|
|
19
|
+
* Дефолт confidence-порогу LLM-класифікації allowed-gaps: 1.1 = rollout-mode
|
|
20
|
+
* (confidence ∈ [0,1], жоден мутант не виключається) — успадковано з `@7n/test`.
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_CLASSIFY_THRESHOLD = 1.1
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Читає пороги з `.n-rules.json#coverage` (top-level обʼєкт — `rules` у схемі
|
|
26
|
+
* є масивом id, тож per-rule конфіг там неможливий; зафіксоване відхилення
|
|
27
|
+
* від спеки absorb-7n-test п. 2.7 dev-design).
|
|
28
|
+
* @param {string} cwd корінь проєкту
|
|
29
|
+
* @returns {Promise<{coverage: number, mutation: number, classify: number}>} пороги (coverage/mutation — %, classify — confidence [0..1] або 1.1 = вимкнено)
|
|
30
|
+
*/
|
|
31
|
+
export async function readThresholds(cwd) {
|
|
32
|
+
const defaults = {
|
|
33
|
+
coverage: DEFAULT_COVERAGE_THRESHOLD,
|
|
34
|
+
mutation: DEFAULT_MUTATION_THRESHOLD,
|
|
35
|
+
classify: DEFAULT_CLASSIFY_THRESHOLD
|
|
36
|
+
}
|
|
37
|
+
const configPath = join(cwd, '.n-rules.json')
|
|
38
|
+
if (!existsSync(configPath)) return defaults
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(await readFile(configPath, 'utf8'))
|
|
41
|
+
const c = parsed?.coverage
|
|
42
|
+
return {
|
|
43
|
+
coverage: typeof c?.coverageThreshold === 'number' ? c.coverageThreshold : defaults.coverage,
|
|
44
|
+
mutation: typeof c?.mutationThreshold === 'number' ? c.mutationThreshold : defaults.mutation,
|
|
45
|
+
classify: typeof c?.classifyConfidenceThreshold === 'number' ? c.classifyConfidenceThreshold : defaults.classify
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
return defaults
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Резолвить активні coverage-провайдери мовних плагінів (порт `coverage`
|
|
54
|
+
* plugin-api, реєстрація через `contributes.handlers.coverage`). Обовʼязкова
|
|
55
|
+
* частина контракту — detect/collect/collectPerFile (assert); fix-hooks
|
|
56
|
+
* (generateTests/generateStories/fixSurvived/fixFailingTests) опційні —
|
|
57
|
+
* `fix-worker.mjs` перевіряє їх через `typeof`.
|
|
58
|
+
* @param {string} cwd корінь проєкту
|
|
59
|
+
* @returns {Promise<Array<import('../../../scripts/lib/plugin-api.mjs').CoverageProvider>>} валідні провайдери
|
|
60
|
+
*/
|
|
61
|
+
export async function resolveProviders(cwd) {
|
|
62
|
+
const config = await readNRulesConfigLite(cwd)
|
|
63
|
+
const providers = []
|
|
64
|
+
for (const handler of getHandlers(cwd, config, 'coverage')) {
|
|
65
|
+
const mod = await import(pathToFileURL(handler.modulePath).href)
|
|
66
|
+
providers.push(assertCoverageProvider(mod.default, handler.pluginName))
|
|
67
|
+
}
|
|
68
|
+
return providers
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Відсоток `covered/total`; `null` коли вимірювати нічого (total 0).
|
|
73
|
+
* @param {number} covered покрито
|
|
74
|
+
* @param {number} total всього
|
|
75
|
+
* @returns {number|null} відсоток або null
|
|
76
|
+
*/
|
|
77
|
+
function pct(covered, total) {
|
|
78
|
+
return total === 0 ? null : (covered / total) * 100
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Гейт покриття/мутаційного тестування (spec 2026-07-22 absorb-7n-test).
|
|
83
|
+
*
|
|
84
|
+
* Делта (`ctx.files`): легкий per-file line coverage змінених файлів через
|
|
85
|
+
* `provider.collectPerFile` — БЕЗ мутаційки; порушення = файл нижче порогу.
|
|
86
|
+
* Full/`lint test` (`ctx.files === undefined`): повний вимір
|
|
87
|
+
* `provider.collect` (coverage + мутаційка + Storybook-вимір); порушення =
|
|
88
|
+
* область нижче порогу line coverage або mutation score (survived-мутанти
|
|
89
|
+
* йдуть у `data` порушення — вхід fix-worker-а).
|
|
90
|
+
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
|
|
91
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} порушення гейта
|
|
92
|
+
*/
|
|
93
|
+
export async function lint(ctx) {
|
|
94
|
+
const reporter = createViolationReporter(ctx)
|
|
95
|
+
const { fail } = reporter
|
|
96
|
+
const cwd = ctx.cwd
|
|
97
|
+
const thresholds = await readThresholds(cwd)
|
|
98
|
+
const providers = await resolveProviders(cwd)
|
|
99
|
+
|
|
100
|
+
for (const provider of providers) {
|
|
101
|
+
if (ctx.files) {
|
|
102
|
+
const rows = await provider.collectPerFile(cwd, { files: ctx.files })
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
if (row.pct >= thresholds.coverage) continue
|
|
105
|
+
fail(
|
|
106
|
+
`${row.file}: line coverage ${row.pct.toFixed(1)}% < порогу ${thresholds.coverage}% ` +
|
|
107
|
+
`(${row.linesCovered}/${row.linesFound} рядків${row.reason ? `; ${row.reason}` : ''}) — ` +
|
|
108
|
+
'додай unit-тести (test.mdc) або запусти `npx @7n/rules lint test`',
|
|
109
|
+
{ reason: 'coverage-below-threshold', file: row.file, data: { pct: row.pct, threshold: thresholds.coverage } }
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!(await provider.detect(cwd))) continue
|
|
116
|
+
let rows = await provider.collect(cwd, {})
|
|
117
|
+
|
|
118
|
+
// LLM-класифікація survived-мутантів (allowed gaps): verdict-и
|
|
119
|
+
// equivalent/defensive/glue/wrapper з confidence ≥ порогу виключаються зі
|
|
120
|
+
// знаменника score. Дефолтний поріг 1.1 = вимкнено (rollout-mode) — тоді
|
|
121
|
+
// й LLM не викликається. Провал класифікації не валить вимір.
|
|
122
|
+
const hasSurvived = rows.some(r => (r.survived ?? []).length > 0)
|
|
123
|
+
if (hasSurvived && thresholds.classify <= 1) {
|
|
124
|
+
try {
|
|
125
|
+
const verdicts = await classify(
|
|
126
|
+
rows.flatMap(r => r.survived ?? []),
|
|
127
|
+
cwd
|
|
128
|
+
)
|
|
129
|
+
rows = applyVerdicts(rows, verdicts, thresholds.classify).rows
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`⚠ coverage classify недоступний (${String(error.message ?? error).slice(0, 120)}) — гейт без allowed-gaps`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const row of rows) {
|
|
138
|
+
const linePct = pct(row.coverage.lines.covered, row.coverage.lines.total)
|
|
139
|
+
if (linePct !== null && linePct < thresholds.coverage) {
|
|
140
|
+
fail(
|
|
141
|
+
`${row.area}: line coverage ${linePct.toFixed(1)}% < порогу ${thresholds.coverage}% ` +
|
|
142
|
+
`(${row.coverage.lines.covered}/${row.coverage.lines.total} рядків)`,
|
|
143
|
+
{ reason: 'coverage-below-threshold', data: { area: row.area, pct: linePct, threshold: thresholds.coverage } }
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
const mutationPct = pct(row.mutation.caught, row.mutation.total)
|
|
147
|
+
if (mutationPct !== null && mutationPct < thresholds.mutation) {
|
|
148
|
+
fail(
|
|
149
|
+
`${row.area}: mutation score ${mutationPct.toFixed(1)}% < порогу ${thresholds.mutation}% ` +
|
|
150
|
+
`(вбито ${row.mutation.caught}/${row.mutation.total}; вцілілі мутанти у data.survived)`,
|
|
151
|
+
{
|
|
152
|
+
reason: 'mutation-below-threshold',
|
|
153
|
+
data: { area: row.area, pct: mutationPct, threshold: thresholds.mutation, survived: row.survived }
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return reporter.result()
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "auto": "завжди" }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs + vitest.config.mjs і гейтом покриття/мутаційного тестування (концерн coverage; конфіг cargo-mutants — концерн cargo_mutants_config правила rust у @7n/rules-lang-rust).
|
|
3
|
+
version: '3.0'
|
|
4
|
+
globs: "**/{.n-rules.json,package.json,stryker.config.mjs,vitest.config.mjs,vitest.config.js},**/*.test.mjs,**/*.vue,**/.storybook/**"
|
|
5
|
+
alwaysApply: false
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Правило **test** керує розміщенням тестових файлів, безпекою ізоляції тестів (заборона `process.chdir`, відносних шляхів у FS, ручного store/restore console), налаштуванням Vitest/Stryker baseline і **гейтом покриття/мутаційного тестування** (концерн `coverage`). Конфігурація cargo-mutants для Rust — концерн `cargo_mutants_config` правила `rust` (@7n/rules-lang-rust).
|
|
9
|
+
|
|
10
|
+
## Покриття + мутаційне тестування (концерн coverage)
|
|
11
|
+
|
|
12
|
+
Покриття вимірюється як звичайний lint-концерн (spec 2026-07-22, влиття `@7n/test` у `@7n/rules`) — пакета `@7n/test` і файлу `COVERAGE.md` більше немає. Мовну специфіку постачають coverage-провайдери плагінів (`contributes.handlers.coverage`): `@7n/rules-lang-js` — vitest + Stryker (vitest-runner, `coverageAnalysis: 'perTest'`) + окремий Storybook-вимір; провайдери rust/python — у відповідних плагінах.
|
|
13
|
+
|
|
14
|
+
- **Делта-lint** (`npx @7n/rules lint`): легкий per-file вимір покриття рядків лише змінених файлів, без мутаційного тестування; файл нижче порогу → порушення.
|
|
15
|
+
- **`npx @7n/rules lint test --no-fix`** — повний вимір (coverage + мутаційне тестування по всіх workspaces) лише з гейтом: нижче порогу → ненульовий exit. **Канонічний CI-крок.**
|
|
16
|
+
- **`npx @7n/rules lint test`** — те саме + fix-шлях: LLM генерує тести на непокриті файли й survived-мутанти (fix-worker концерну, ladder ядра).
|
|
17
|
+
|
|
18
|
+
Пороги (дефолт 80/80 %) конфігуруються у `.n-rules.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{ "coverage": { "coverageThreshold": 80, "mutationThreshold": 80 } }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
У `package.json` (корінь) має бути `scripts.coverage` із викликом `npx @7n/rules lint test --no-fix`.
|
|
25
|
+
|
|
26
|
+
### Multi-workspace iteration
|
|
27
|
+
|
|
28
|
+
У monorepo провайдер ітерує усі workspaces з власним `package.json` і агрегує метрики lcov + Stryker у єдиний рядок області (`JS`, `Vue (Storybook)`). Workspace без тестів пропускається без помилки.
|
|
29
|
+
|
|
30
|
+
- [package.json.contains.json](./package_json/template/package.json.contains.json)
|
|
31
|
+
|
|
32
|
+
## Канон Storybook для Vue-компонентних бібліотек (хвиля 1; концерни storybook-*)
|
|
33
|
+
|
|
34
|
+
Джерело рішення: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`. Storybook впроваджувався вручну двічі в різних nitra-репо в одній сесії — обидва рази з тими самими невидимими заздалегідь проблемами (конфлікт `@vitejs/plugin-vue`/`quasar()`, недоступні внутрішні Quasar-іконки без `iconSet`+`iconMapFn`, ручне мокання мережі) і одним реальним merge-конфліктом між двома паралельними ручними реалізаціями тієї самої фічі. Це правило робить канонічний скафолд стандартом, а не одноразовим рішенням кожного агента.
|
|
35
|
+
|
|
36
|
+
**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).
|
|
37
|
+
|
|
38
|
+
Усі 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`-режиму.
|
|
39
|
+
|
|
40
|
+
## Скоуп
|
|
41
|
+
|
|
42
|
+
Тільки Vue-компонентні бібліотеки — пакет із `vue` у `peerDependencies` (маркер `isVueComponentLibraryPkg`, той самий що й у `vue.mdc`, не дублюється) і не менше **3** `.vue`-файлів. Поріг відсікає пакети з одним-двома допоміжними компонентами, для яких повний Storybook-скафолд — зайві накладні витрати.
|
|
43
|
+
|
|
44
|
+
**Наявність `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 і давала б хибні спрацювання.
|
|
45
|
+
|
|
46
|
+
Опційно (вимкнено за замовчуванням) — app-проєкти (`vue` у `dependencies`, не бібліотека, + `src/pages/`), хвиля 2a. Детекція реалізована в `storybook-scope/main.mjs`, викликається лише за явного прапорця `storybook.detectApps: true` у `.n-rules.json` консюмера — деталі й асиметрія скафолда нижче, розділ «Хвиля 2a: app-проєкти».
|
|
47
|
+
|
|
48
|
+
**Opt-out:** окремий пакет можна виключити зі скоупу через `.n-rules.json` → `storybook.optOut: string[]` (root dir пакета, той самий формат що й у виводі workspace-роутингу — `.` для кореня, `packages/ui` тощо). Виняток — намір, не помилка: якщо в `optOut` вказано неіснуючий workspace-пакет, `scope`-концерн репортує це як застаріле налаштування.
|
|
49
|
+
|
|
50
|
+
Логіка детекції (поріг, opt-out, app-проєкти) — у `storybook-scope/main.mjs`, не дублюється тут.
|
|
51
|
+
|
|
52
|
+
`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-структури не змінюється.
|
|
53
|
+
|
|
54
|
+
## Канонічний скафолд
|
|
55
|
+
|
|
56
|
+
Для кожного пакета в скоупі обов'язкові: `.storybook/main.js`, `.storybook/preview.js`, `package.json#scripts.storybook`. Канонічні шаблони — `storybook-scaffold/template/`.
|
|
57
|
+
|
|
58
|
+
Фіксовані рішення (деталі — ADR, Кластер 2):
|
|
59
|
+
|
|
60
|
+
- **Порядок Vite-плагінів фіксований**: `@vitejs/plugin-vue` **перед** `quasar()` — інакше Quasar-плагін не бачить SFC, уже скомпільований `plugin-vue`.
|
|
61
|
+
- **Layout-детекція**: `src/components/` присутній → stories-glob звужується до нього; пласка структура (`src/` без `components/`) — ширший glob по всьому `src/`.
|
|
62
|
+
- **`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`.
|
|
63
|
+
- **`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 може бути канонічним, а сусідній файл видалено окремо).
|
|
64
|
+
- **`staticDirs`** покриває `.storybook/public` — статичний asset для msw service worker (`preview.js`).
|
|
65
|
+
- **`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 до рендеру.
|
|
66
|
+
- **`.storybook/mocks/gql-sse.js`**: єдиний канонічний хелпер `sseSubscription` для MSW-мокання Apollo-підписок через wire-протокол `graphql-sse` (`event: next\ndata: …`, distinct-connection mode) — переносити цю логіку в кожен пакет окремо заборонено, є одне джерело істини.
|
|
67
|
+
- **`package.json#scripts.storybook`** — уніфікований скрипт, однаковий для всіх пакетів у скоупі (значення — `storybook-scaffold/main.mjs`, не дублюється тут).
|
|
68
|
+
|
|
69
|
+
Перевірка присутності й ключових маркерів канону — `storybook-scaffold/main.mjs`; детерміноване відтворення відсутніх файлів із `storybook-scaffold/template/` — `storybook-scaffold/fix-storybook-scaffold.mjs` (fixability: `config` — канонічна форма одна, LLM у ланцюжку фіксу не потрібен).
|
|
70
|
+
|
|
71
|
+
### knip-виключення для `.storybook/`-артефактів
|
|
72
|
+
|
|
73
|
+
Кожен 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/`):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"ignore": ["**/.storybook/empty-vite.config.js", "**/.storybook/mocks/apollo.js", "**/.storybook/mocks/gql-sse.js"]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
(домердж у наявний масив `ignore`, не заміна). Звірено з реальним `knip.json` пілотного консюмера.
|
|
82
|
+
|
|
83
|
+
Симетрична ручна правка для `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`).
|
|
84
|
+
|
|
85
|
+
## Vitest-конфіг і Stryker-ізоляція (Кластер 5)
|
|
86
|
+
|
|
87
|
+
Канонічний `test.projects` (`unit`+`storybook`, browser-mode лише chromium) і ізольований `vitest.stryker.config` (той самий unit-набір, без browser-mode — `@stryker-mutator/vitest-runner` крашиться на browser-mode `projects`) — `storybook-vitest-config/main.mjs` (перевірка, AST через `oxc-parser`) і `storybook-vitest-config/fix-storybook-vitest-config.mjs` (точкові insert-only правки наявного конфіга, fixability: `config`). Деталі канону, чому саме chromium і межі автофіксу — `storybook-vitest-config/storybook-vitest-config.mdc`, не дублюється тут.
|
|
88
|
+
|
|
89
|
+
### CI: Playwright-кеш і швидкий PR-прогін (Кластер 5, CI-частина)
|
|
90
|
+
|
|
91
|
+
`storybook-ci/main.mjs` + `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.
|
|
92
|
+
|
|
93
|
+
`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`).
|
|
94
|
+
|
|
95
|
+
Nightly-only `@7n/test coverage` (mutation testing) — свідомо поза цим concern-ом: ADR розділяє швидкий PR-шлях (`--project=storybook`, цей concern) і nightly mutation-прогін, який лишається окремою інфраструктурою (`test/stryker_config`) і не дублюється тут.
|
|
96
|
+
|
|
97
|
+
## Гігієна сторонніх залежностей (Кластер 6)
|
|
98
|
+
|
|
99
|
+
`storybook-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`.
|
|
100
|
+
|
|
101
|
+
**Свідомо лише `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`).
|
|
102
|
+
|
|
103
|
+
## Мокання (Кластер 3, docs-only)
|
|
104
|
+
|
|
105
|
+
Рецепти router/`@nitra/tfm`/Apollo-GraphQL(MSW)/Pinia/сторінкових stories — `storybook-mocking/storybook-mocking.mdc`. Свідомо без механічної перевірки (`concern.json` без `check`/`policy`/`lint`-блоку, як і решта чисто-документаційних concern-ів репозиторію) — кожен пакет мокає свій набір залежностей по-своєму, детермінований чек дав би або хибні спрацювання, або нульове покриття.
|
|
106
|
+
|
|
107
|
+
## Хвиля 2a: app-проєкти
|
|
108
|
+
|
|
109
|
+
Джерело рішення: розділ «Розширення (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.
|
|
110
|
+
|
|
111
|
+
**Скоуп-прапорець:** 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-проєкти.
|
|
112
|
+
|
|
113
|
+
**Асиметрія скафолда (свідома дзеркальність із бібліотекою):** app-канонічні `.storybook/main.js`/`preview.js` (`storybook-scaffold/template/app-main.js`/`app-preview.js`, маркери `APP_MAIN_JS_MARKERS`/`APP_PREVIEW_JS_MARKERS`) — **протилежний** підхід до `viteConfigPath`:
|
|
114
|
+
|
|
115
|
+
- **Бібліотека (хвиля 1):** `viteConfigPath` на порожній `empty-vite.config.js` — `builder-vite` НЕ бачить `vite.config.js` пакета, `viteFinal` домерджує ВІДФІЛЬТРОВАНІ плагіни (свої `vue()`/`quasar()`-інстанси у фіксованому порядку).
|
|
116
|
+
- **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`.
|
|
117
|
+
- Тому app-скафолд **не має** `.storybook/empty-vite.config.js`-секції — `storybook-scaffold/main.mjs#checkAppScaffold` її не перевіряє, `storybook-adopt/main.mjs#diagnosePackage` не діагностує для `type: 'app'`.
|
|
118
|
+
|
|
119
|
+
`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-файл).
|
|
120
|
+
|
|
121
|
+
**Smoke-покриття (`storybook-page-coverage`-концерн, рівень `warn`):** кожен `.vue` під `src/pages/` app-пакета має мати хоча б один `*.stories.js` у тому самому каталозі (не обов'язково той самий basename — реальний кейс `gt`: `task/[id].vue` + `task/task-detail.stories.js`). М'який сигнал (не гейт) — хвиля 2a свідомо не блокує CI на відсутність story.
|
|
122
|
+
|
|
123
|
+
**Stryker-рішення (прийняте, відкрите питання ADR закрито консервативно):** page-stories з vitest `storybook`-проєкту виключаються зі Stryker-скоупу — mutation testing по сторінках з живими Apollo-підписками дорогий і флейкі. Механізм — той самий, що й для бібліотек: ізольований `vitest.stryker.config.*` (`storybook-vitest-config`-концерн) взагалі не містить `projects`/browser-mode, тож page-stories фізично не входять у Stryker-прогін незалежно від типу пакета. Рішення відкрите до перегляду, якщо з'явиться дешевший спосіб мутувати сторінки без флейкі SSE-таймінгів.
|
|
124
|
+
|
|
125
|
+
**Vitest-конфіг для app-пакета:** та сама генерична augment-логіка `storybook-vitest-config`-концерна (дописує `test.projects` до наявного `vitest.config.js`, якщо він уже є — реальний кейс `gt`), з ДВОМА нюансами, залежними від типу пакета:
|
|
126
|
+
|
|
127
|
+
- **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)`).
|
|
128
|
+
- **Плагіни 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 (`storybook-vitest-config/main.mjs#collectStorybookMarkerHints`) і adopt-діагностика (`storybook-adopt/main.mjs#collectAdoptMarkerHints`) для `type: 'app'` так само вимагають ці три маркери в наявному `storybook`-проєкті (не лише при генерації).
|
|
129
|
+
|
|
130
|
+
**`.storybook/vitest.setup.js`** (canon-фікс, було відсутнє в шаблонах): той самий стандартний `@storybook/addon-vitest`-boilerplate (`setProjectAnnotations`/`beforeAll`) для ОБОХ типів пакета — `storybook-scaffold`-концерн (`VITEST_SETUP_JS_MARKERS`, `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-тестів.
|
|
131
|
+
|
|
132
|
+
**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-конфіг.
|
|
133
|
+
|
|
134
|
+
## Adopt-режим і скіл `n-storybook` (Кластер 8)
|
|
135
|
+
|
|
136
|
+
Скіл `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` скіла.
|
|
137
|
+
|
|
138
|
+
## Що свідомо поза хвилею 1 і 2a
|
|
139
|
+
|
|
140
|
+
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) — окремий трек, не в обсязі цього правила.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
|
|
3
|
+
"fixability": "config",
|
|
4
|
+
"policy": {
|
|
5
|
+
"files": {
|
|
6
|
+
"single": "package.json",
|
|
7
|
+
"required": true
|
|
8
|
+
},
|
|
9
|
+
"missingMessage": "package.json не існує — створи зі scripts.coverage (test.mdc)"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Обовʼязкові скрипти в package.json
|
|
2
|
+
|
|
3
|
+
Rego-пакет: `test.package_json`
|
|
4
|
+
|
|
5
|
+
Цільовий файл: `package.json` кожного workspace.
|
|
6
|
+
|
|
7
|
+
Перевіряє substring-відповідність значень у `scripts`:
|
|
8
|
+
|
|
9
|
+
| Поле | Має містити |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `scripts.coverage` | `@7n/test coverage` |
|
|
12
|
+
| `scripts.test` | `vitest` і `--bun` (напр. `"bun run --bun vitest run"`) |
|
|
13
|
+
|
|
14
|
+
Substring-семантика: команди-обгортки (наприклад `bun run pre-coverage && npx @7n/test coverage`, `bun run pre-test && bun run --bun vitest run`) дозволені — головне, щоб потрібні рядки були присутні.
|
|
15
|
+
|
|
16
|
+
**Чому `--bun` для `scripts.test`:** без Bun-рушія (`bun run vitest` без `--bun`, чи vitest під Node) `import { SQL } from 'bun'` та інші Bun-нативні built-in модулі не резолвуються у forked test-процесах — консьюмери змушені тримати exclude-списки для тестів, що їх торкаються. Під `bun run --bun vitest run` forked pool-процеси успадковують Bun-рушій і резолюція працює без жодних exclude.
|
|
17
|
+
|
|
18
|
+
Канон підтягується через `--data`: [package.json.contains.json](./template/package.json.contains.json)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Перевірка `package.json` для правила test (test.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "contains": ... } }
|
|
4
|
+
# Структура --data сформована з template/package.json.contains.json.
|
|
5
|
+
# Перевіряємо substring-вимоги до scripts.coverage і scripts.test:
|
|
6
|
+
# рядки мають містити відповідно "@7n/test coverage" і "vitest" + "--bun".
|
|
7
|
+
package test.package_json
|
|
8
|
+
|
|
9
|
+
import rego.v1
|
|
10
|
+
|
|
11
|
+
deny contains msg if {
|
|
12
|
+
some script_name, needles in data.template.contains.scripts
|
|
13
|
+
actual := object.get(object.get(input, "scripts", {}), script_name, "")
|
|
14
|
+
some needle in needles
|
|
15
|
+
not contains(actual, needle)
|
|
16
|
+
msg := sprintf("package.json: scripts.%s має містити %q (test.mdc)%s", [script_name, needle, explain_suffix(needle)])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# --bun пояснюється окремо: без нього forked vitest pool-процеси не успадковують
|
|
20
|
+
# Bun-рушій, і Bun-нативні built-in модулі (напр. `import { SQL } from 'bun'`)
|
|
21
|
+
# не резолвуються у forked test-процесах.
|
|
22
|
+
explain_suffix(needle) := msg if {
|
|
23
|
+
needle == "--bun"
|
|
24
|
+
msg := " — без --bun forked vitest pool-процеси не успадковують Bun-рушій, тож Bun-нативні built-in модулі (напр. import { SQL } from 'bun') не резолвуються у forked test-процесах"
|
|
25
|
+
} else := ""
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-fix концерну text/oxfmtrc: доводить `.oxfmtrc.json` до канону deep-merge-ом
|
|
3
|
+
* шаблону правила, зберігаючи наявні локальні ключі конфігу.
|
|
4
|
+
*/
|
|
1
5
|
import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
|
|
2
6
|
|
|
7
|
+
/** Fix-патерни концерну: один шаблонний deep-merge у `.oxfmtrc.json`. */
|
|
3
8
|
export const patterns = [createTemplateFixPattern({ id: 'text-oxfmtrc-template', targetPath: '.oxfmtrc.json' })]
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-fix концерну text/vscode_settings: доводить `.vscode/settings.json` до канону
|
|
3
|
+
* deep-merge-ом шаблону правила, не зачіпаючи локальні налаштування користувача.
|
|
4
|
+
*/
|
|
1
5
|
import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
|
|
2
6
|
|
|
7
|
+
/** Fix-патерни концерну: один шаблонний deep-merge у `.vscode/settings.json`. */
|
|
3
8
|
export const patterns = [
|
|
4
9
|
createTemplateFixPattern({ id: 'text-vscode_settings-template', targetPath: '.vscode/settings.json' })
|
|
5
10
|
]
|
|
@@ -3,26 +3,29 @@ type: JS Module
|
|
|
3
3
|
title: fix-vscode_settings.mjs
|
|
4
4
|
resource: npm/rules/worktree/vscode_settings/fix-vscode_settings.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: e39aa982
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
8
|
tier: cloud-avg
|
|
9
9
|
score: 100
|
|
10
|
-
issues: judge:inaccurate:0.97
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
## Огляд
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Файл описує fix-патерн для робочого дерева, пов’язаний із приведенням `.vscode/settings.json` до командного канону через `deep-merge` із шаблоном правила. Він потрібен, щоб канонічні налаштування могли накладатися поверх наявної конфігурації без стирання локальних параметрів розробника.
|
|
16
|
+
|
|
17
|
+
`patterns` експортує масив із шаблонним fix-патерном для цього сценарію.
|
|
17
18
|
|
|
18
19
|
## Поведінка
|
|
19
20
|
|
|
20
|
-
1. `patterns`
|
|
21
|
+
1. `patterns` містить fix-патерн для робочого дерева, пов’язаний із приведенням settings.json до командного канону.
|
|
22
|
+
2. Патерн описує додавання або оновлення потрібних налаштувань із шаблону правила через безпечне злиття.
|
|
23
|
+
3. Наявні локальні налаштування мають зберігатися під час застосування відповідного fix-патерна, щоб автоматичне виправлення не стирало індивідуальні параметри розробника.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## Публічний API
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
- patterns — масив fix-патернів концерну: один шаблонний deep-merge у `.vscode/settings.json`.
|
|
25
28
|
|
|
26
29
|
## Гарантії поведінки
|
|
27
30
|
|
|
28
|
-
-
|
|
31
|
+
- Власних операцій запису (ФС/БД) у файлі немає; файл лише експортує опис fix-патерна.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-fix концерну worktree/vscode_settings: доводить `.vscode/settings.json` до
|
|
3
|
+
* канону deep-merge-ом шаблону правила, не зачіпаючи локальні налаштування.
|
|
4
|
+
*/
|
|
1
5
|
import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
|
|
2
6
|
|
|
7
|
+
/** Fix-патерни концерну: один шаблонний deep-merge у `.vscode/settings.json`. */
|
|
3
8
|
export const patterns = [
|
|
4
9
|
createTemplateFixPattern({ id: 'worktree-vscode_settings-template', targetPath: '.vscode/settings.json' })
|
|
5
10
|
]
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T0-fix концерну worktree/zed_settings: доводить `.zed/settings.json` до канону
|
|
3
|
+
* deep-merge-ом шаблону правила, не зачіпаючи локальні налаштування користувача.
|
|
4
|
+
*/
|
|
1
5
|
import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
|
|
2
6
|
|
|
7
|
+
/** Fix-патерни концерну: один шаблонний deep-merge у `.zed/settings.json`. */
|
|
3
8
|
export const patterns = [
|
|
4
9
|
createTemplateFixPattern({ id: 'worktree-zed_settings-template', targetPath: '.zed/settings.json' })
|
|
5
10
|
]
|
package/schemas/n-rules.json
CHANGED
|
@@ -68,10 +68,27 @@
|
|
|
68
68
|
"description": "Чи синхронізувати `.claude/settings.json` (hooks + permissions, merge зі збереженням користувацьких полів) і slash-команди checks. За замовчуванням true.",
|
|
69
69
|
"default": true
|
|
70
70
|
},
|
|
71
|
+
"coverage": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"description": "Пороги гейта покриття/мутаційного тестування (концерн coverage правила test). Відсутнє поле — дефолти 80/80 %.",
|
|
74
|
+
"additionalProperties": false,
|
|
75
|
+
"properties": {
|
|
76
|
+
"coverageThreshold": {
|
|
77
|
+
"type": "number",
|
|
78
|
+
"description": "Мінімальний line coverage у відсотках (делта-гейт per-file і повний вимір).",
|
|
79
|
+
"default": 80
|
|
80
|
+
},
|
|
81
|
+
"mutationThreshold": {
|
|
82
|
+
"type": "number",
|
|
83
|
+
"description": "Мінімальний mutation score у відсотках (лише повний вимір: lint --full / lint test).",
|
|
84
|
+
"default": 80
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
71
88
|
"storybook": {
|
|
72
89
|
"type": "object",
|
|
73
90
|
"additionalProperties": false,
|
|
74
|
-
"description": "Конфігурація канону Storybook (
|
|
91
|
+
"description": "Конфігурація канону Storybook (storybook-* концерни правила test, test.mdc).",
|
|
75
92
|
"properties": {
|
|
76
93
|
"detectApps": {
|
|
77
94
|
"type": "boolean",
|