@nitra/cursor 11.2.0 → 11.4.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/docs/n-cursor.md +1 -1
- package/bin/n-cursor.js +14 -4
- package/package.json +1 -1
- package/rules/doc-files/js/lint.mjs +4 -3
- package/rules/lint/js/orchestrate.mjs +10 -2
- package/rules/text/js/lint.mjs +3 -2
- package/rules/text/lint/cspell-fix.mjs +3 -2
- package/rules/text/lint/lint.mjs +7 -5
- package/rules/text/meta.json +1 -1
- package/scripts/auto-rules.mjs +42 -5
- package/scripts/docs/auto-rules.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [11.4.0] - 2026-06-15
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Додано логіку відсіювання неактуальних правил/скілів за `availableRules`/`availableSkills
|
|
8
|
+
|
|
9
|
+
## [11.3.0] - 2026-06-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- lint: opt-in `meta.json: llmFix:true` тепер реально дротується (раніше прапор був декоративний — opportunistic LLM-fix біг просто на `!readOnly`). `runLint` читає `llmFix` з meta правила й передає в `lint(files, cwd, { readOnly, llmFix })`; правило без прапора лишається detect-only. Це й забезпечує safety-тріаж зі спеки (логічні лінтери не вмикають LLM-fix випадково). doc-files і text позначені `llmFix:true`; cspell-класифікація гейтиться через `llmFix` (проведено `runLintTextCli`/`runLintTextSteps`/`runCspellText`), standalone `lint-text` передає `llmFix:true`. Принагідно: justified `no-unsanitized/method`-disable на package-internal динамічний import у `runLint` (pre-existing).
|
|
14
|
+
|
|
3
15
|
## [11.2.0] - 2026-06-15
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/bin/docs/n-cursor.md
CHANGED
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
#### Вкладена normalizeConfigWithAutoRules(parsedConfig)
|
|
174
174
|
|
|
175
175
|
- **Сигнатура:** `async function normalizeConfigWithAutoRules(parsedConfig: Record<string, unknown>): Promise<Record<string, unknown>>`
|
|
176
|
-
- **Призначення:** перевіряє типи полів, обчислює `auto-detected rules` (`detectAutoRules`), будує ефективний список правил (поточні + auto, мінус `disable-rules`), за яким `detectAutoSkills` визначає скіли. Далі `mergeConfigWithAutoDetected` зливає
|
|
176
|
+
- **Призначення:** перевіряє типи полів, обчислює `auto-detected rules` (`detectAutoRules`), будує ефективний список правил (поточні + auto, мінус `disable-rules`), за яким `detectAutoSkills` визначає скіли. Далі `mergeConfigWithAutoDetected` зливає дані (передаючи `availableRules`/`availableSkills` із каталогів пакета, щоб відсіяти з `rules`/`skills` неактуальні id, яких уже немає у пакеті — прибрані логуються через `🧹`), після чого `$schema` вирівнюється до `CONFIG_SCHEMA_URL`, додаються `disable-rules`/`disable-skills` (якщо непорожні), результат проходить через `sortConfigIdArrays`.
|
|
177
177
|
|
|
178
178
|
### logRuleMigrationsIfAny(parsedConfig)
|
|
179
179
|
|
package/bin/n-cursor.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
|
|
26
26
|
* `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
|
|
27
27
|
* `npx \@nitra/cursor lint-doc-files` — детермінований детектор застарілості файлових док (`stale`: `missing`|`crc-mismatch`); правило doc-files (ignore-glob у `npm/rules/doc-files/js/docgen-ignore.mjs`; тека `docs/` поряд із джерелом). Режими: повний (exit 1), `--json` (exit 0), `--missing-only`, `--hook`/`--git` (hook-протокол, exit 2), `--degraded`
|
|
28
|
-
* `npx \@nitra/cursor fix-doc-files` — JS-оркестрована генерація файлових док (роутинг local/cloud) зі штампом CRC (`--limit`/`--from`/`--overwrite
|
|
28
|
+
* `npx \@nitra/cursor fix-doc-files` — JS-оркестрована генерація файлових док (роутинг local/cloud) зі штампом CRC (`--limit`/`--from`/`--overwrite`); `--stamp` — детерміноване перештампування CRC без LLM
|
|
29
29
|
* `npx \@nitra/cursor doc-aggregate modules` — JSON-лістинг логічних модулів (межі за `package.json`) для Tier 2 скілу doc-aggregate
|
|
30
30
|
* `npx \@nitra/cursor skill list` — скіли пакета без синку в проєкт
|
|
31
31
|
* `npx \@nitra/cursor skill taze` — промпт на stdout
|
|
@@ -315,9 +315,18 @@ async function readConfig(paths = {}) {
|
|
|
315
315
|
const merged = mergeConfigWithAutoDetected({
|
|
316
316
|
config: parsedConfig,
|
|
317
317
|
detectedRules: autoDetectedRules.rules,
|
|
318
|
-
detectedSkills: autoDetectedSkills.skills
|
|
318
|
+
detectedSkills: autoDetectedSkills.skills,
|
|
319
|
+
availableRules,
|
|
320
|
+
availableSkills
|
|
319
321
|
})
|
|
320
322
|
|
|
323
|
+
if (merged.pruned) {
|
|
324
|
+
const parts = []
|
|
325
|
+
if (merged.pruned.rules.length > 0) parts.push(`rules: ${merged.pruned.rules.join(', ')}`)
|
|
326
|
+
if (merged.pruned.skills.length > 0) parts.push(`skills: ${merged.pruned.skills.join(', ')}`)
|
|
327
|
+
console.log(`🧹 Прибрано з ${CONFIG_FILE} неактуальні (немає у пакеті) — ${parts.join('; ')}\n`)
|
|
328
|
+
}
|
|
329
|
+
|
|
321
330
|
const rest = Object.fromEntries(Object.entries(parsedConfig).filter(([k]) => k !== '$schema'))
|
|
322
331
|
const normalized = {
|
|
323
332
|
$schema: CONFIG_SCHEMA_URL,
|
|
@@ -1568,7 +1577,8 @@ try {
|
|
|
1568
1577
|
case 'lint-text': {
|
|
1569
1578
|
// Канонічний lint-text: cspell → shellcheck → dotenv → markdownlint → v8r (text.mdc).
|
|
1570
1579
|
// `--read-only` (CI): без авто-фіксу (markdownlint/shellcheck/dotenv) — нуль мутацій.
|
|
1571
|
-
|
|
1580
|
+
// `llmFix:true` — text llmFix-capable, тож standalone lint-text робить omlx-класифікацію cspell.
|
|
1581
|
+
process.exitCode = await runLintTextCli({ readOnly: args.includes('--read-only'), llmFix: true })
|
|
1572
1582
|
|
|
1573
1583
|
break
|
|
1574
1584
|
}
|
|
@@ -1649,7 +1659,7 @@ try {
|
|
|
1649
1659
|
}
|
|
1650
1660
|
case 'fix-doc-files': {
|
|
1651
1661
|
// n-cursor fix-doc-files — local-only генерація файлових док (omlx) + CRC-штамп
|
|
1652
|
-
// (--limit/--from/--overwrite
|
|
1662
|
+
// (--limit/--from/--overwrite); --stamp — детерміноване
|
|
1653
1663
|
// перештампування source+crc без LLM. У CI не запускається (потрібна локальна модель).
|
|
1654
1664
|
if (args.includes('--stamp')) {
|
|
1655
1665
|
const { runDocFilesStampCli } = await import('../rules/doc-files/js/docgen-files-batch.mjs')
|
package/package.json
CHANGED
|
@@ -98,13 +98,14 @@ function collectStale(files, cwd) {
|
|
|
98
98
|
* Крок агрегатора lint для doc-files (opportunistic LLM-fix tier).
|
|
99
99
|
* @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
|
|
100
100
|
* @param {string} [cwd] корінь репо
|
|
101
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly: лише детект (CI/hook)
|
|
101
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly: лише детект (CI/hook);
|
|
102
|
+
* llmFix: opt-in opportunistic-генерація (з `meta.json: llmFix:true`) — без нього detect-only
|
|
102
103
|
* @returns {Promise<number>} 0 — доки свіжі; 1 — є застарілі (детект, fix пропущено чи помилка генерації)
|
|
103
104
|
*/
|
|
104
|
-
export async function lint(files, cwd = process.cwd(), { readOnly = false } = {}) {
|
|
105
|
+
export async function lint(files, cwd = process.cwd(), { readOnly = false, llmFix = false } = {}) {
|
|
105
106
|
const stale = collectStale(files, cwd)
|
|
106
107
|
if (stale.length === 0) return 0
|
|
107
|
-
if (readOnly) return reportStale(stale)
|
|
108
|
+
if (readOnly || !llmFix) return reportStale(stale)
|
|
108
109
|
|
|
109
110
|
// fix-by-default: opportunistic-генерація через спільне ядро (preflight omlx →
|
|
110
111
|
// батч із circuit-breaker'ом). omlx недоступний → runGenerationBatch друкує причину
|
|
@@ -108,15 +108,23 @@ export async function runLint(opts = {}) {
|
|
|
108
108
|
return 0
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const metaById = readAllMeta(rulesDir)
|
|
112
|
+
const ids = selectLintRules(metaById, full)
|
|
112
113
|
for (const id of ids) {
|
|
113
114
|
const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
|
|
114
115
|
if (!existsSync(lintPath)) {
|
|
115
116
|
log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
|
|
116
117
|
continue
|
|
117
118
|
}
|
|
119
|
+
// lintPath = join(rulesDir, id, …) — суто package-internal (rulesDir пакета + id зі
|
|
120
|
+
// selectLintRules за власним meta), не зовнішній вхід → ін'єкції немає.
|
|
121
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
118
122
|
const mod = await import(lintPath)
|
|
119
|
-
|
|
123
|
+
// `llmFix` (opt-in opportunistic LLM-fix, спека 2026-06-15): лише правила з
|
|
124
|
+
// `meta.json: llmFix:true` отримують fix-сходинку; решта — detect-only. Це й
|
|
125
|
+
// забезпечує safety-тріаж (логічні лінтери не вмикають LLM-fix випадково).
|
|
126
|
+
const llmFix = metaById[id]?.llmFix === true
|
|
127
|
+
const code = await mod.lint(changed, cwd, { readOnly, llmFix })
|
|
120
128
|
if (code !== 0) return code
|
|
121
129
|
}
|
|
122
130
|
|
package/rules/text/js/lint.mjs
CHANGED
|
@@ -6,9 +6,10 @@ import { runLintTextCli } from '../lint/lint.mjs'
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
|
|
8
8
|
* @param {string} [_cwd] корінь (ігнорується — CLI працює від process.cwd())
|
|
9
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій)
|
|
9
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій);
|
|
10
|
+
* llmFix → opt-in omlx-класифікація cspell (з `meta.json: llmFix:true`)
|
|
10
11
|
* @returns {Promise<number>} exit code
|
|
11
12
|
*/
|
|
12
13
|
export function lint(_files, _cwd, opts = {}) {
|
|
13
|
-
return runLintTextCli({ readOnly: opts.readOnly === true })
|
|
14
|
+
return runLintTextCli({ readOnly: opts.readOnly === true, llmFix: opts.llmFix === true })
|
|
14
15
|
}
|
|
@@ -113,9 +113,10 @@ export function appendWordsToDict(cwd, words) {
|
|
|
113
113
|
* cspell-крок lint-text: класифікація → словник (нова схема).
|
|
114
114
|
* @param {string} [cwd] корінь
|
|
115
115
|
* @param {boolean} [readOnly] true → лише детект (нуль мутацій)
|
|
116
|
+
* @param {boolean} [llmFix] opt-in omlx-класифікація (з `meta.json: llmFix:true`); без нього — лише детект
|
|
116
117
|
* @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
|
|
117
118
|
*/
|
|
118
|
-
export function runCspellText(cwd = process.cwd(), readOnly = false) {
|
|
119
|
+
export function runCspellText(cwd = process.cwd(), readOnly = false, llmFix = false) {
|
|
119
120
|
const bin = resolveCmd('npx')
|
|
120
121
|
if (!bin) {
|
|
121
122
|
process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
|
|
@@ -124,7 +125,7 @@ export function runCspellText(cwd = process.cwd(), readOnly = false) {
|
|
|
124
125
|
|
|
125
126
|
const first = detectCspell(cwd, bin)
|
|
126
127
|
if (first.code === 0) return 0
|
|
127
|
-
if (readOnly) {
|
|
128
|
+
if (readOnly || !llmFix) {
|
|
128
129
|
process.stdout.write(first.out)
|
|
129
130
|
return first.code
|
|
130
131
|
}
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -97,9 +97,10 @@ function preflight(dep) {
|
|
|
97
97
|
/**
|
|
98
98
|
* Внутрішні кроки `lint-text` без локу.
|
|
99
99
|
* @param {boolean} [readOnly] true → лише детект без авто-фіксу (нуль мутацій — CI/pre-commit)
|
|
100
|
+
* @param {boolean} [llmFix] opt-in omlx-класифікація cspell (інші кроки фіксяться детерміновано за readOnly)
|
|
100
101
|
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
101
102
|
*/
|
|
102
|
-
function runLintTextSteps(readOnly = false) {
|
|
103
|
+
function runLintTextSteps(readOnly = false, llmFix = false) {
|
|
103
104
|
// Auto-install: throws on failure → propagates as exit 1 from runStandardLint
|
|
104
105
|
ensureTool('shellcheck')
|
|
105
106
|
ensureTool('dotenv-linter')
|
|
@@ -107,8 +108,8 @@ function runLintTextSteps(readOnly = false) {
|
|
|
107
108
|
// patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight.
|
|
108
109
|
if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1
|
|
109
110
|
|
|
110
|
-
console.log(`\n▶ cspell (${readOnly
|
|
111
|
-
const cspellCode = runCspellText(process.cwd(), readOnly)
|
|
111
|
+
console.log(`\n▶ cspell (${!readOnly && llmFix ? 'omlx-класифікація + словник + перевірка' : 'перевірка'})`)
|
|
112
|
+
const cspellCode = runCspellText(process.cwd(), readOnly, llmFix)
|
|
112
113
|
if (cspellCode !== 0) return cspellCode
|
|
113
114
|
|
|
114
115
|
console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`)
|
|
@@ -129,8 +130,9 @@ function runLintTextSteps(readOnly = false) {
|
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
132
|
* Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
|
|
132
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без
|
|
133
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу;
|
|
134
|
+
* llmFix → omlx-класифікація cspell (opt-in із `meta.json: llmFix:true`)
|
|
133
135
|
* @returns {Promise<number>} код виходу
|
|
134
136
|
*/
|
|
135
137
|
export const runLintTextCli = (opts = {}) =>
|
|
136
|
-
runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true))
|
|
138
|
+
runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true, opts.llmFix === true))
|
package/rules/text/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": "завжди", "lint": "per-file" }
|
|
1
|
+
{ "auto": "завжди", "lint": "per-file", "llmFix": true }
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -392,14 +392,45 @@ export async function detectAutoRules({
|
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
/**
|
|
395
|
-
*
|
|
395
|
+
* Розділяє список id на доступні в пакеті й застарілі (відсутні).
|
|
396
|
+
* Без `available` нічого не прибирає — усе вважається доступним.
|
|
397
|
+
* @param {string[]} ids перелік id (rules або skills)
|
|
398
|
+
* @param {string[] | undefined} available id, що реально є у каталозі пакета
|
|
399
|
+
* @returns {{ kept: string[], pruned: string[] }} відфільтровані й прибрані id
|
|
400
|
+
*/
|
|
401
|
+
function partitionByAvailability(ids, available) {
|
|
402
|
+
if (!available) return { kept: ids, pruned: [] }
|
|
403
|
+
const availableSet = new Set(available)
|
|
404
|
+
const kept = []
|
|
405
|
+
const pruned = []
|
|
406
|
+
for (const id of ids) {
|
|
407
|
+
if (availableSet.has(id)) kept.push(id)
|
|
408
|
+
else pruned.push(id)
|
|
409
|
+
}
|
|
410
|
+
return { kept, pruned }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Доповнює конфіг автодетектом (лише додає; існуючі вручну задані елементи не прибирає),
|
|
415
|
+
* а за наявності `availableRules`/`availableSkills` ще й прибирає з `rules`/`skills`
|
|
416
|
+
* неактуальні id, яких уже немає у пакеті (наприклад, правило чи скіл видалено з нової
|
|
417
|
+
* версії \@nitra/cursor) — інакше sync щоразу падав би на завантаженні відсутнього
|
|
418
|
+
* `rules/<id>.mdc` чи `skills/<id>/`. Прибрані id повертаються у полі `pruned` (для логу).
|
|
396
419
|
* @param {object} params параметри оновлення
|
|
397
420
|
* @param {{ rules: unknown, skills?: unknown, ['disable-rules']?: unknown, ['disable-skills']?: unknown }} params.config розпарсений `.n-cursor.json`
|
|
398
421
|
* @param {string[]} params.detectedRules правила, визначені автодетектом
|
|
399
422
|
* @param {string[]} params.detectedSkills skills, визначені автодетектом
|
|
400
|
-
* @
|
|
423
|
+
* @param {string[]} [params.availableRules] id правил, наявних у каталозі `rules/` пакета (для відсіву неактуальних)
|
|
424
|
+
* @param {string[]} [params.availableSkills] id skills, наявних у каталозі `skills/` пакета (для відсіву неактуальних)
|
|
425
|
+
* @returns {{ rules: string[], skills: string[], pruned?: { rules: string[], skills: string[] } } & Record<string, unknown>} новий нормалізований конфіг
|
|
401
426
|
*/
|
|
402
|
-
export function mergeConfigWithAutoDetected({
|
|
427
|
+
export function mergeConfigWithAutoDetected({
|
|
428
|
+
config,
|
|
429
|
+
detectedRules,
|
|
430
|
+
detectedSkills,
|
|
431
|
+
availableRules,
|
|
432
|
+
availableSkills
|
|
433
|
+
}) {
|
|
403
434
|
const existingRules = migrateRuleIds(normalizeIdList(config.rules))
|
|
404
435
|
const existingSkills = normalizeIdList(config.skills)
|
|
405
436
|
const disableRules = migrateRuleIds(normalizeIdList(config['disable-rules']))
|
|
@@ -419,13 +450,19 @@ export function mergeConfigWithAutoDetected({ config, detectedRules, detectedSki
|
|
|
419
450
|
}
|
|
420
451
|
}
|
|
421
452
|
|
|
422
|
-
|
|
423
|
-
const
|
|
453
|
+
const { kept: keptRules, pruned: prunedRules } = partitionByAvailability(rules, availableRules)
|
|
454
|
+
const { kept: keptSkills, pruned: prunedSkills } = partitionByAvailability(skills, availableSkills)
|
|
455
|
+
|
|
456
|
+
/** @type {{ rules: string[], skills: string[], pruned?: { rules: string[], skills: string[] } } & Record<string, unknown>} */
|
|
457
|
+
const normalized = { rules: keptRules, skills: keptSkills }
|
|
424
458
|
if (disableRules.length > 0) {
|
|
425
459
|
normalized['disable-rules'] = disableRules
|
|
426
460
|
}
|
|
427
461
|
if (disableSkills.length > 0) {
|
|
428
462
|
normalized['disable-skills'] = disableSkills
|
|
429
463
|
}
|
|
464
|
+
if (prunedRules.length > 0 || prunedSkills.length > 0) {
|
|
465
|
+
normalized.pruned = { rules: prunedRules, skills: prunedSkills }
|
|
466
|
+
}
|
|
430
467
|
return normalized
|
|
431
468
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
docgen:
|
|
3
3
|
source: npm/scripts/auto-rules.mjs
|
|
4
|
-
crc:
|
|
4
|
+
crc: d50b922f
|
|
5
5
|
score: 90
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -29,7 +29,7 @@ detectAutoRules
|
|
|
29
29
|
Визначає активні правила на основі spec, перевіряючи їх проти згенерованих фактів.
|
|
30
30
|
|
|
31
31
|
mergeConfigWithAutoDetected
|
|
32
|
-
Доповнює конфігурацію, додаючи визначені автоправила та налаштування, з урахуванням legacy-ID.
|
|
32
|
+
Доповнює конфігурацію, додаючи визначені автоправила та налаштування, з урахуванням legacy-ID; за наявності `availableRules`/`availableSkills` ще й відсіює з `rules`/`skills` неактуальні id, яких немає у пакеті (повертає їх у полі `pruned`).
|
|
33
33
|
|
|
34
34
|
## Публічний API
|
|
35
35
|
|