@nitra/cursor 3.23.1 → 3.25.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 +12 -0
- package/bin/n-cursor.js +36 -6
- package/package.json +1 -1
- package/rules/test/js/stryker_config.mjs +256 -2
- package/rules/test/test.mdc +2 -0
- package/skills/fix/SKILL.md +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.25.0] - 2026-06-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- fix: прапорець `n-cursor fix --json` друкує компактний {total, failed, rules:[{ruleId, ok, output}]} у stdout (per-rule захоплення замість stdio inherit; у json-режимі пропускається ensureHkInstall, щоб stdout лишався чистим JSON). Скіл n-fix більше не парсить термінальний вивід — працює лише з rules[].ok:false. Дефолтна поведінка без прапорця незмінна
|
|
8
|
+
|
|
9
|
+
## [3.24.0] - 2026-06-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- test/stryker_config: auto-augment існуючого stryker.config.mjs у Vue JS-roots — точкова вставка plugins/ignorers (vue-macros ignorer) string-splice-ом за AST-аналізом, зі збереженням полів і коментарів; idempotent, з graceful-skip для non-literal/динамічних default-export
|
|
14
|
+
|
|
3
15
|
## [3.23.1] - 2026-06-05
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -1230,11 +1230,17 @@ function logRemovedManagedItems(title, basePath, names) {
|
|
|
1230
1230
|
* На рівні `runFixCommand` локу нема: різні набори правил можуть прогресувати незалежно,
|
|
1231
1231
|
* а однакові правила серіалізуються в spawn'ах нижче.
|
|
1232
1232
|
* @param {string[]} requestedRules імена правил; порожній масив — discovery з `.cursor/rules/`
|
|
1233
|
+
* @param {{json?: boolean}} [opts] json — друкувати компактний JSON `{total, failed, rules:[{ruleId, ok, output}]}`
|
|
1234
|
+
* замість per-rule stdio + timing (для скілу n-fix: агент читає лише впалі правила, не парсить текст)
|
|
1233
1235
|
* @returns {Promise<void>}
|
|
1234
1236
|
*/
|
|
1235
|
-
async function runFixCommand(requestedRules) {
|
|
1237
|
+
async function runFixCommand(requestedRules, opts = {}) {
|
|
1238
|
+
const json = opts.json === true
|
|
1239
|
+
// json-режим — діагностичний read-only вивід для скілу: пропускаємо встановлення
|
|
1240
|
+
// git-hook (`ensureHkInstall` друкує «Installed hk hook…» у stdout і забруднив би
|
|
1241
|
+
// чистий JSON; сам pre-commit hook для діагностики не потрібен).
|
|
1236
1242
|
const hkBin = ensureTool('hk')
|
|
1237
|
-
ensureHkInstall(hkBin)
|
|
1243
|
+
if (!json) ensureHkInstall(hkBin)
|
|
1238
1244
|
ensureTool('conftest')
|
|
1239
1245
|
|
|
1240
1246
|
const available = await listRuleIds(BUNDLED_RULES_DIR)
|
|
@@ -1261,6 +1267,10 @@ async function runFixCommand(requestedRules) {
|
|
|
1261
1267
|
}
|
|
1262
1268
|
idsToRun = discoverCheckRulesFromCursorRules(available, mdcFiles)
|
|
1263
1269
|
if (idsToRun.length === 0) {
|
|
1270
|
+
if (json) {
|
|
1271
|
+
process.stdout.write(`${JSON.stringify({ total: 0, failed: 0, rules: [] })}\n`)
|
|
1272
|
+
return
|
|
1273
|
+
}
|
|
1264
1274
|
console.log(
|
|
1265
1275
|
`\n🔍 ${PACKAGE_NAME} fix — у ${RULES_DIR}/ немає правил з programmatic перевіркою ` +
|
|
1266
1276
|
`(відповідного fix.mjs у пакеті). Нічого не запущено.\n`
|
|
@@ -1272,16 +1282,29 @@ async function runFixCommand(requestedRules) {
|
|
|
1272
1282
|
let totalFailed = 0
|
|
1273
1283
|
/** @type {{ id: string, ms: number, ok: boolean }[]} */
|
|
1274
1284
|
const timings = []
|
|
1285
|
+
/** @type {{ ruleId: string, ok: boolean, output: string }[]} */
|
|
1286
|
+
const ruleResults = []
|
|
1275
1287
|
for (const id of idsToRun) {
|
|
1276
1288
|
const fixPath = join(BUNDLED_RULES_DIR, id, 'fix.mjs')
|
|
1277
1289
|
const startedAt = Date.now()
|
|
1278
|
-
|
|
1290
|
+
// json-режим: захоплюємо stdout/stderr правила у структуру (а не inherit у термінал),
|
|
1291
|
+
// щоб віддати агенту згруповано {ruleId, ok, output} і він читав лише впалі.
|
|
1292
|
+
const result = json
|
|
1293
|
+
? spawnSync('bun', [fixPath], { encoding: 'utf8' })
|
|
1294
|
+
: spawnSync('bun', [fixPath], { stdio: 'inherit' })
|
|
1279
1295
|
const ok = result.status === 0
|
|
1280
1296
|
timings.push({ id: `fix-${id}`, ms: Date.now() - startedAt, ok })
|
|
1297
|
+
if (json) {
|
|
1298
|
+
ruleResults.push({ ruleId: id, ok, output: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim() })
|
|
1299
|
+
}
|
|
1281
1300
|
if (!ok) totalFailed++
|
|
1282
1301
|
}
|
|
1283
1302
|
|
|
1284
|
-
|
|
1303
|
+
if (json) {
|
|
1304
|
+
process.stdout.write(`${JSON.stringify({ total: idsToRun.length, failed: totalFailed, rules: ruleResults })}\n`)
|
|
1305
|
+
} else {
|
|
1306
|
+
process.stdout.write(formatTimingSummary('Fix timing', timings))
|
|
1307
|
+
}
|
|
1285
1308
|
|
|
1286
1309
|
if (totalFailed > 0) {
|
|
1287
1310
|
throw new Error(`${totalFailed} з ${idsToRun.length} правил мають проблеми`)
|
|
@@ -1573,7 +1596,11 @@ try {
|
|
|
1573
1596
|
await ensureNitraCursorInRootDevDependencies(cwd())
|
|
1574
1597
|
switch (command) {
|
|
1575
1598
|
case 'fix': {
|
|
1576
|
-
|
|
1599
|
+
// --json: компактний {total, failed, rules:[{ruleId, ok, output}]} у stdout для скілу n-fix.
|
|
1600
|
+
await runFixCommand(
|
|
1601
|
+
args.filter(a => a !== '--json'),
|
|
1602
|
+
{ json: args.includes('--json') }
|
|
1603
|
+
)
|
|
1577
1604
|
|
|
1578
1605
|
break
|
|
1579
1606
|
}
|
|
@@ -1582,7 +1609,10 @@ try {
|
|
|
1582
1609
|
console.warn(
|
|
1583
1610
|
`⚠️ Команда \`check\` deprecated — використовуйте \`fix\` (\`npx ${PACKAGE_NAME} fix [<rule>...]\`)`
|
|
1584
1611
|
)
|
|
1585
|
-
await runFixCommand(
|
|
1612
|
+
await runFixCommand(
|
|
1613
|
+
args.filter(a => a !== '--json'),
|
|
1614
|
+
{ json: args.includes('--json') }
|
|
1615
|
+
)
|
|
1586
1616
|
|
|
1587
1617
|
break
|
|
1588
1618
|
}
|
package/package.json
CHANGED
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* `@vue/compiler-sfc` падає при компіляції SFC. Плагін копіюється у той самий
|
|
11
11
|
* jsRoot як `stryker-vue-macros-ignorer.mjs`.
|
|
12
12
|
*
|
|
13
|
+
* Augment (drift-hole): якщо у Vue-root `stryker.config.mjs` уже існував (проєкт
|
|
14
|
+
* мав non-vue config ще до 3.x Vue-підтримки), `ensureBaselineFile` його не
|
|
15
|
+
* перетирає — тож `augmentVueStrykerConfig` точково вставляє `plugins`/`ignorers`
|
|
16
|
+
* у наявний файл (string-splice за AST-аналізом), зберігаючи решту полів і
|
|
17
|
+
* коментарів. Idempotent: повторний прогон нічого не дублює.
|
|
18
|
+
*
|
|
13
19
|
* Self-gating: концерн silently skips коли `js-lint` не enabled — це навмисно,
|
|
14
20
|
* щоб не шуміти у single-language проєктах без JS coverage tooling.
|
|
15
21
|
*
|
|
@@ -17,10 +23,12 @@
|
|
|
17
23
|
* лишаються на Stryker defaults (`src/**\/*.{js,mjs,ts,jsx,tsx,cjs}`).
|
|
18
24
|
*/
|
|
19
25
|
import { existsSync } from 'node:fs'
|
|
20
|
-
import { copyFile, glob } from 'node:fs/promises'
|
|
26
|
+
import { copyFile, glob, readFile, writeFile } from 'node:fs/promises'
|
|
21
27
|
import { dirname, join, relative } from 'node:path'
|
|
22
28
|
import { fileURLToPath } from 'node:url'
|
|
23
29
|
|
|
30
|
+
import { parseSync } from 'oxc-parser'
|
|
31
|
+
|
|
24
32
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
25
33
|
import { readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
|
|
26
34
|
import { ensureGitignoreEntries } from '../../../scripts/utils/ensure-gitignore-entries.mjs'
|
|
@@ -33,6 +41,20 @@ const STRYKER_VUE_PLUGIN_PATH = join(HERE, 'data', 'stryker_config', 'stryker-vu
|
|
|
33
41
|
const STRYKER_VUE_PLUGIN_FILENAME = 'stryker-vue-macros-ignorer.mjs'
|
|
34
42
|
const VITEST_BASELINE_PATH = join(HERE, 'data', 'vitest_config', 'vitest.config.baseline.js')
|
|
35
43
|
|
|
44
|
+
// Канонічні entries, які vue-варіант baseline тримає у `plugins`/`ignorers`.
|
|
45
|
+
// Augment-крок (augmentVueStrykerConfig) дбає, щоб саме вони були присутні в
|
|
46
|
+
// уже-існуючому `stryker.config.mjs` Vue-root-а. Нову property пишемо у
|
|
47
|
+
// canonical-порядку; у наявний масив лише дописуємо відсутні entries в кінець
|
|
48
|
+
// (Stryker нечутливий до порядку plugins/ignorers).
|
|
49
|
+
const VITEST_RUNNER_PLUGIN = '@stryker-mutator/vitest-runner'
|
|
50
|
+
const VUE_MACROS_PLUGIN = './stryker-vue-macros-ignorer.mjs'
|
|
51
|
+
const VUE_MACROS_IGNORER = 'vue-macros'
|
|
52
|
+
|
|
53
|
+
// Module-scope (prefer-static-regex): рядок-відступ цілком whitespace; leading
|
|
54
|
+
// кома (можливо з whitespace) після останньої property об'єкта.
|
|
55
|
+
const INDENT_WS_RE = /^\s*$/u
|
|
56
|
+
const LEADING_COMMA_RE = /^\s*,/u
|
|
57
|
+
|
|
36
58
|
// Тест-артефакти для .gitignore (подвійний-зірочка-префікс — для monorepo workspaces):
|
|
37
59
|
// - `**/reports/stryker/` — увесь каталог Stryker-output-у (`tempDirName` backup'и,
|
|
38
60
|
// mutation.json, HTML/dashboard-репорти якщо користувач додасть інші reporter-и).
|
|
@@ -77,6 +99,229 @@ async function ensureBaselineFile(reporter, cwd, baselinePath, target, label) {
|
|
|
77
99
|
reporter.pass(`${label} створено з canonical baseline (${relative(cwd, target)}) (test.mdc)`)
|
|
78
100
|
}
|
|
79
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Огортає рядкове значення в single-quotes для вставки у JS-масив. Канонічні
|
|
104
|
+
* entries (`@stryker-mutator/...`, `vue-macros`, `./stryker-...`) не містять
|
|
105
|
+
* лапок, тож escaping не потрібен.
|
|
106
|
+
* @param {string} s рядкове значення
|
|
107
|
+
* @returns {string} `'<s>'`
|
|
108
|
+
*/
|
|
109
|
+
function quote(s) {
|
|
110
|
+
return `'${s}'`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Знаходить `export default { … }` як ObjectExpression. Повертає null, якщо
|
|
115
|
+
* default-export відсутній або не є object-literal (factory/функція/змінна) —
|
|
116
|
+
* augment у такому разі не чіпає файл.
|
|
117
|
+
* @param {{body: Array<{type: string, declaration?: {type: string}}>}} program oxc Program node
|
|
118
|
+
* @returns {object | null} ObjectExpression node або null
|
|
119
|
+
*/
|
|
120
|
+
function findDefaultExportObject(program) {
|
|
121
|
+
const exp = program.body.find(n => n.type === 'ExportDefaultDeclaration')
|
|
122
|
+
const decl = exp?.declaration
|
|
123
|
+
return decl && decl.type === 'ObjectExpression' ? decl : null
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Аналізує property `name` об'єкта: чи присутній, чи це чистий масив рядкових
|
|
128
|
+
* літералів і які значення вже містить. `dynamic: true` сигналить, що масив —
|
|
129
|
+
* computed (spread / non-string element / не ArrayExpression), і зливати його
|
|
130
|
+
* небезпечно.
|
|
131
|
+
* @param {object} obj ObjectExpression node
|
|
132
|
+
* @param {string} name ім'я property ('plugins' | 'ignorers')
|
|
133
|
+
* @returns {{prop: object|null, array: object|null, values: string[], dynamic: boolean}} стан property
|
|
134
|
+
*/
|
|
135
|
+
function analyzeArrayProperty(obj, name) {
|
|
136
|
+
const prop = obj.properties.find(
|
|
137
|
+
p => p.type === 'Property' && !p.computed && p.key && (p.key.name === name || p.key.value === name)
|
|
138
|
+
)
|
|
139
|
+
if (!prop) return { prop: null, array: null, values: [], dynamic: false }
|
|
140
|
+
const value = prop.value
|
|
141
|
+
if (!value || value.type !== 'ArrayExpression') return { prop, array: null, values: [], dynamic: true }
|
|
142
|
+
const values = []
|
|
143
|
+
for (const el of value.elements) {
|
|
144
|
+
if (!el || el.type !== 'Literal' || typeof el.value !== 'string') {
|
|
145
|
+
return { prop, array: value, values: [], dynamic: true }
|
|
146
|
+
}
|
|
147
|
+
values.push(el.value)
|
|
148
|
+
}
|
|
149
|
+
return { prop, array: value, values, dynamic: false }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Вставка відсутніх рядкових елементів у вже існуючий масив (append перед `]`).
|
|
154
|
+
* Порожній масив → елементи між `[` `]`; непорожній → `, '<item>'` після
|
|
155
|
+
* останнього елемента (trailing comma, якщо вже є, лишається валідною).
|
|
156
|
+
* @param {object} arr ArrayExpression node
|
|
157
|
+
* @param {string[]} values поточні значення масиву
|
|
158
|
+
* @param {string[]} missing значення, яких бракує (вже у потрібному порядку)
|
|
159
|
+
* @returns {{pos: number, text: string}} одна точкова вставка
|
|
160
|
+
*/
|
|
161
|
+
function arrayAppendEdit(arr, values, missing) {
|
|
162
|
+
if (values.length === 0) {
|
|
163
|
+
return { pos: arr.end - 1, text: missing.map(v => quote(v)).join(', ') }
|
|
164
|
+
}
|
|
165
|
+
const lastEl = arr.elements.at(-1)
|
|
166
|
+
return { pos: lastEl.end, text: missing.map(v => `, ${quote(v)}`).join('') }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Визначає відступ properties об'єкта за рядком останньої property (для нових
|
|
171
|
+
* рядків `plugins`/`ignorers`). Дефолт — 2 пробіли.
|
|
172
|
+
* @param {string} src вихідний текст конфіга
|
|
173
|
+
* @param {object} obj ObjectExpression node
|
|
174
|
+
* @returns {string} рядок-відступ (whitespace)
|
|
175
|
+
*/
|
|
176
|
+
function detectIndent(src, obj) {
|
|
177
|
+
const props = obj.properties
|
|
178
|
+
if (props.length > 0) {
|
|
179
|
+
const start = props.at(-1).start
|
|
180
|
+
const lineStart = src.lastIndexOf('\n', start - 1) + 1
|
|
181
|
+
const ws = src.slice(lineStart, start)
|
|
182
|
+
if (INDENT_WS_RE.test(ws)) return ws
|
|
183
|
+
}
|
|
184
|
+
return ' '
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Вставка нових properties (`plugins`/`ignorers`) у object-literal перед його
|
|
189
|
+
* закривальною `}`. Поважає trailing comma останньої property й коректно
|
|
190
|
+
* обробляє порожній об'єкт `{}`.
|
|
191
|
+
* @param {string} src вихідний текст конфіга
|
|
192
|
+
* @param {object} obj ObjectExpression node
|
|
193
|
+
* @param {string} indent відступ properties
|
|
194
|
+
* @param {string[]} lines рядки нових properties (без відступу й коми), напр. `plugins: [...]`
|
|
195
|
+
* @returns {{pos: number, text: string}} одна точкова вставка
|
|
196
|
+
*/
|
|
197
|
+
function newPropertyEdit(src, obj, indent, lines) {
|
|
198
|
+
const block = lines.join(`,\n${indent}`)
|
|
199
|
+
const props = obj.properties
|
|
200
|
+
if (props.length === 0) {
|
|
201
|
+
return { pos: obj.start + 1, text: `\n${indent}${block}\n` }
|
|
202
|
+
}
|
|
203
|
+
const lastProp = props.at(-1)
|
|
204
|
+
const tail = src.slice(lastProp.end, obj.end - 1)
|
|
205
|
+
const commaMatch = tail.match(LEADING_COMMA_RE)
|
|
206
|
+
if (commaMatch) {
|
|
207
|
+
return { pos: lastProp.end + commaMatch[0].length, text: `\n${indent}${block}` }
|
|
208
|
+
}
|
|
209
|
+
return { pos: lastProp.end, text: `,\n${indent}${block}` }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Застосовує точкові вставки до тексту. Сортує за спаданням `pos`, щоб ранні
|
|
214
|
+
* offsets лишались валідними після вставок справа.
|
|
215
|
+
* @param {string} src вихідний текст
|
|
216
|
+
* @param {Array<{pos: number, text: string}>} edits вставки
|
|
217
|
+
* @returns {string} новий текст
|
|
218
|
+
*/
|
|
219
|
+
function applyEdits(src, edits) {
|
|
220
|
+
let out = src
|
|
221
|
+
for (const e of edits.toSorted((a, b) => b.pos - a.pos)) {
|
|
222
|
+
out = out.slice(0, e.pos) + e.text + out.slice(e.pos)
|
|
223
|
+
}
|
|
224
|
+
return out
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Augment-крок для вже-існуючого `stryker.config.mjs` у Vue JS-root:
|
|
229
|
+
* реєструє локальний `vue-macros` ignorer-плагін (`plugins`/`ignorers`), якщо
|
|
230
|
+
* його ще немає. Закриває drift-hole для проєктів, які мали non-vue config ще
|
|
231
|
+
* до 3.x Vue-підтримки — `ensureBaselineFile` такий файл idempotent-skip-ить,
|
|
232
|
+
* тож baseline-секцій `plugins`/`ignorers` він мовчки не отримує, і Stryker
|
|
233
|
+
* падає у dry-run з `defineProps()` error.
|
|
234
|
+
*
|
|
235
|
+
* Стратегія: oxc-parser — лише для **аналізу** (де у source-тексті
|
|
236
|
+
* default-export object, які properties/offsets уже є). Зміни — точкові
|
|
237
|
+
* string-splice-и у вихідному тексті (insert items), щоб НЕ переписати
|
|
238
|
+
* форматування й коментарі користувача (oxc serializer їх не зберігає). Після
|
|
239
|
+
* splice — повторний parse: якщо результат не компілюється → відкат і fail.
|
|
240
|
+
* @param {ReturnType<typeof createCheckReporter>} reporter check-reporter
|
|
241
|
+
* @param {string} cwd корінь проєкту (для relative-шляхів у логах)
|
|
242
|
+
* @param {string} jsRoot абсолютний шлях до Vue workspace-каталогу
|
|
243
|
+
* @returns {Promise<void>}
|
|
244
|
+
*/
|
|
245
|
+
async function augmentVueStrykerConfig(reporter, cwd, jsRoot) {
|
|
246
|
+
const target = join(jsRoot, 'stryker.config.mjs')
|
|
247
|
+
const rel = relative(cwd, target)
|
|
248
|
+
const src = await readFile(target, 'utf8')
|
|
249
|
+
|
|
250
|
+
let result
|
|
251
|
+
try {
|
|
252
|
+
result = parseSync(target, src, { lang: 'js', sourceType: 'module' })
|
|
253
|
+
} catch (error) {
|
|
254
|
+
reporter.fail(`stryker.config.mjs не парситься (${rel}): ${error.message} — augment скіпнуто`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (result.errors?.length) {
|
|
258
|
+
const msg = result.errors[0]?.message ?? 'syntax error'
|
|
259
|
+
reporter.fail(`stryker.config.mjs має syntax error (${rel}): ${msg} — augment скіпнуто`)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const obj = findDefaultExportObject(result.program)
|
|
264
|
+
if (!obj) {
|
|
265
|
+
reporter.fail(
|
|
266
|
+
`stryker.config.mjs has non-literal default export (${rel}) — augment скіпнуто, ` +
|
|
267
|
+
'додай вручну plugins/ignorers згідно stryker.config.vue.baseline.mjs'
|
|
268
|
+
)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const plugins = analyzeArrayProperty(obj, 'plugins')
|
|
273
|
+
const ignorers = analyzeArrayProperty(obj, 'ignorers')
|
|
274
|
+
if (plugins.dynamic || ignorers.dynamic) {
|
|
275
|
+
reporter.fail(
|
|
276
|
+
`stryker.config.mjs: plugins/ignorers — динамічний вираз (spread/computed) (${rel}) — ` +
|
|
277
|
+
'augment скіпнуто, додай vue-macros ignorer вручну згідно stryker.config.vue.baseline.mjs'
|
|
278
|
+
)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const edits = []
|
|
283
|
+
const newPropLines = []
|
|
284
|
+
for (const [name, state, required] of [
|
|
285
|
+
['plugins', plugins, [VITEST_RUNNER_PLUGIN, VUE_MACROS_PLUGIN]],
|
|
286
|
+
['ignorers', ignorers, [VUE_MACROS_IGNORER]]
|
|
287
|
+
]) {
|
|
288
|
+
const missing = required.filter(v => !state.values.includes(v))
|
|
289
|
+
if (state.array) {
|
|
290
|
+
if (missing.length > 0) edits.push(arrayAppendEdit(state.array, state.values, missing))
|
|
291
|
+
} else {
|
|
292
|
+
newPropLines.push(`${name}: [${required.map(v => quote(v)).join(', ')}]`)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (newPropLines.length > 0) {
|
|
296
|
+
edits.push(newPropertyEdit(src, obj, detectIndent(src, obj), newPropLines))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (edits.length === 0) {
|
|
300
|
+
reporter.pass(`vue-macros ignorer уже зареєстровано (${rel})`)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const next = applyEdits(src, edits)
|
|
305
|
+
|
|
306
|
+
// Safety: результат має компілюватися. Якщо string-splice дав невалідний JS
|
|
307
|
+
// (errors або виняток парсера на патологічному вводі) — відкат (не пишемо) і
|
|
308
|
+
// fail, щоб користувач не лишився зі зламаним конфігом.
|
|
309
|
+
let recheck
|
|
310
|
+
try {
|
|
311
|
+
recheck = parseSync(target, next, { lang: 'js', sourceType: 'module' })
|
|
312
|
+
} catch (error) {
|
|
313
|
+
reporter.fail(`stryker.config.mjs: augment дав некоректний результат (${rel}): ${error.message} — відкат, додай вручну`)
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (recheck.errors?.length) {
|
|
317
|
+
reporter.fail(`stryker.config.mjs: augment дав некоректний результат (${rel}) — відкат, додай вручну`)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await writeFile(target, next, 'utf8')
|
|
322
|
+
reporter.pass(`vue-macros ignorer додано у stryker.config.mjs (${rel}) (test.mdc)`)
|
|
323
|
+
}
|
|
324
|
+
|
|
80
325
|
/**
|
|
81
326
|
* @param {string} [cwd] корінь проєкту (default: `process.cwd()` — CLI-сумісність)
|
|
82
327
|
* @returns {Promise<number>} 0 — OK або silently skipped, 1 — порушення
|
|
@@ -110,9 +355,18 @@ export async function check(cwd = process.cwd()) {
|
|
|
110
355
|
|
|
111
356
|
for (const jsRoot of jsRoots) {
|
|
112
357
|
const isVueRoot = await hasVueFiles(jsRoot)
|
|
358
|
+
const strykerTarget = join(jsRoot, 'stryker.config.mjs')
|
|
359
|
+
// Зчитуємо ДО ensureBaselineFile: чи файл уже існував. Якщо ні — baseline
|
|
360
|
+
// (vue-варіант для Vue-root) копіюється з уже-присутніми plugins/ignorers,
|
|
361
|
+
// augment не потрібен. Якщо існував — ensureBaselineFile idempotent-skip-ить,
|
|
362
|
+
// і саме тут augment закриває drift-hole.
|
|
363
|
+
const wasMissing = !existsSync(strykerTarget)
|
|
113
364
|
const strykerBaseline = isVueRoot ? STRYKER_VUE_BASELINE_PATH : STRYKER_BASELINE_PATH
|
|
114
|
-
await ensureBaselineFile(reporter, cwd, strykerBaseline,
|
|
365
|
+
await ensureBaselineFile(reporter, cwd, strykerBaseline, strykerTarget, 'stryker.config.mjs')
|
|
115
366
|
if (isVueRoot) {
|
|
367
|
+
if (!wasMissing) {
|
|
368
|
+
await augmentVueStrykerConfig(reporter, cwd, jsRoot)
|
|
369
|
+
}
|
|
116
370
|
await ensureBaselineFile(
|
|
117
371
|
reporter,
|
|
118
372
|
cwd,
|
package/rules/test/test.mdc
CHANGED
|
@@ -156,6 +156,8 @@ test.skipIf(env.STRYKER_MUTATOR_WORKER)('узгоджені з поточним
|
|
|
156
156
|
|
|
157
157
|
JS-root без `.vue` отримує дефолтний baseline без `plugins`/`ignorers` (backward-compatible). Обидва файли копіюються idempotent — наявний `stryker.config.mjs` / `stryker-vue-macros-ignorer.mjs` не перетирається.
|
|
158
158
|
|
|
159
|
+
**Augment існуючого config.** Якщо у Vue JS-root `stryker.config.mjs` **уже лежить** (наприклад, після апгрейду з версії без Vue-підтримки — `ensureBaselineFile` його idempotent-skip-ить і baseline-секцій `plugins`/`ignorers` він мовчки не отримує, а Stryker падає у dry-run з `defineProps()` error), концерн `stryker_config` точково вставляє у наявний файл `plugins: [..., './stryker-vue-macros-ignorer.mjs']` і `ignorers: ['vue-macros']`, зберігши решту полів і коментарів. Редагування — string-splice за AST-аналізом (oxc-parser — лише для пошуку offsets, не для re-serialize), тож форматування й коментарі не переписуються; idempotent — повторний `fix test` не дублює entries. Якщо `plugins`/`ignorers` уже частково є — добавляються лише відсутні entries. Якщо `export default` — **не** object-literal (factory/функція/змінна) або масиви динамічні (spread/computed), augment пропускається з вимогою додати плагін вручну згідно [`stryker.config.vue.baseline.mjs`](./js/data/stryker_config/stryker.config.vue.baseline.mjs).
|
|
160
|
+
|
|
159
161
|
### Vitest baseline та `package.json#scripts`
|
|
160
162
|
|
|
161
163
|
Поряд зі Stryker концерн `stryker_config` без дублювання копіює `vitest.config.js` (теж тільки якщо файлу немає). Canonical: [vitest.config.baseline.js](./js/data/vitest_config/vitest.config.baseline.js) — `environment: 'node'`, `coverage.provider: 'v8'` з lcov+text-summary репортами, `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']` (підхоплює обидві розкладки — тести у `tests/`-піддиректоріях і top-level integration suites у `<root>/tests/`).
|
package/skills/fix/SKILL.md
CHANGED
|
@@ -12,13 +12,13 @@ description: >-
|
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
|
-
1. **Діагностика** — запусти перевірку через retry-обгортку `n_cursor_npx` (визначена у worktree-preflight, крок 0.1: переживає транзитну CDN-гонку щойно опублікованої версії, а реальний `❌` від `fix` віддає одразу). За замовчуванням — лише правила з `.cursor/rules/*.mdc`, для яких у пакеті є programmatic check; повний набір — явні аргументи: `n_cursor_npx fix bun ga
|
|
15
|
+
1. **Діагностика** — запусти перевірку через retry-обгортку `n_cursor_npx` (визначена у worktree-preflight, крок 0.1: переживає транзитну CDN-гонку щойно опублікованої версії, а реальний `❌` від `fix` віддає одразу). Прапорець `--json` дає **структурований** результат у stdout, щоб не парсити термінальний текст. За замовчуванням — лише правила з `.cursor/rules/*.mdc`, для яких у пакеті є programmatic check; повний набір — явні аргументи: `n_cursor_npx fix bun ga --json`:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
n_cursor_npx fix
|
|
18
|
+
n_cursor_npx fix --json
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
2. **Аналіз** —
|
|
21
|
+
2. **Аналіз** — розбери JSON `{ total, failed, rules: [{ ruleId, ok, output }] }`. Працюй **лише** з елементами `ok:false`; їх `output` містить готові `❌`-повідомлення правила (не парси stdout вручну, не визначай правила з тексту). Якщо `failed === 0` — нічого виправляти.
|
|
22
22
|
|
|
23
23
|
3. **Виправлення** — для кожного `❌` відкрий відповідне правило з `.cursor/rules/` і виправ:
|
|
24
24
|
- Створи відсутні конфігураційні файли (`.cspell.json`, `.oxfmtrc.json`, `eslint.config.js`, тощо)
|
|
@@ -40,10 +40,10 @@ bun i
|
|
|
40
40
|
oxfmt .
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
6. **Верифікація** — перевір що все виправлено (та сама retry-обгортка `n_cursor_npx`)
|
|
43
|
+
6. **Верифікація** — перевір що все виправлено (та сама retry-обгортка `n_cursor_npx`); чекаєш `failed === 0`:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
n_cursor_npx fix
|
|
46
|
+
n_cursor_npx fix --json
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
7. **Результат** —
|
|
49
|
+
7. **Результат** — `failed` має стати `0` (усі правила `ok:true`). Якщо лишились `ok:false` — повтори кроки 3-6. Лінт-помилки від `bun run lint` тут **не виправляй** — вони на скіл `/n-lint`.
|