@nitra/cursor 12.10.0 → 12.11.1
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 +9 -27
- package/package.json +1 -1
- package/rules/adr/js/docs/hooks.md +16 -26
- package/rules/adr/js/hooks.mjs +59 -0
- package/rules/changelog/js/consistency.mjs +54 -0
- package/rules/changelog/js/docs/consistency.md +21 -16
- package/rules/changelog/js/docs/index.md +2 -2
- package/rules/ga/policy/workflow_common/workflow_common.rego +15 -0
- package/rules/k8s/policy/lint_k8s_yml/lint_k8s_yml.rego +57 -0
- package/rules/k8s/policy/lint_k8s_yml/target.json +4 -0
- package/rules/k8s/policy/lint_k8s_yml/template/lint-k8s.yml.snippet.yml +43 -0
- package/rules/python/policy/lint_python_yml/lint_python_yml.rego +9 -0
- package/rules/python/policy/pyproject_toml/pyproject_toml.rego +15 -1
- package/rules/vue/js/docs/packages.md +12 -17
- package/rules/vue/js/packages.mjs +41 -1
- package/scripts/lib/fix/docs/index.md +10 -10
- package/scripts/lib/fix/docs/orchestrator.md +15 -45
- package/scripts/lib/fix/llm-fix-apply.mjs +34 -3
- package/scripts/lib/fix/llm-worker.mjs +24 -15
- package/scripts/lib/fix/orchestrator.mjs +0 -4
- package/scripts/lib/fix/t0.mjs +116 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.11.1] - 2026-06-25
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- ♻️ refactor(scripts): T0-autofix контракт fix-<concern>.mjs — нова конвенція в scripts.mdc
|
|
8
|
+
|
|
9
|
+
## [12.11.0] - 2026-06-25
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Додано перевірку на наявність `CHANGELOG.md` та його формату в workspace-ах
|
|
14
|
+
|
|
3
15
|
## [12.10.0] - 2026-06-24
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
import { spawnSync } from 'node:child_process'
|
|
65
65
|
import { existsSync } from 'node:fs'
|
|
66
66
|
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises'
|
|
67
|
-
import { basename, dirname, join } from 'node:path'
|
|
67
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
68
68
|
import { cwd, env } from 'node:process'
|
|
69
69
|
import { fileURLToPath } from 'node:url'
|
|
70
70
|
|
|
@@ -1457,7 +1457,7 @@ async function runSync() {
|
|
|
1457
1457
|
* `--root`-команди `doc-aggregate`/`rename-yaml-extensions`,
|
|
1458
1458
|
* sub-лінтери) гард не зачіпає.
|
|
1459
1459
|
*/
|
|
1460
|
-
const ROOT_GUARDED_COMMANDS = new Set([undefined, '', 'lint', '
|
|
1460
|
+
const ROOT_GUARDED_COMMANDS = new Set([undefined, '', 'lint', 'change', 'release'])
|
|
1461
1461
|
|
|
1462
1462
|
/**
|
|
1463
1463
|
* Короткий опис дії для тексту root-guard помилки за іменем команди.
|
|
@@ -1473,9 +1473,6 @@ function describeRootGuardedAction(cmd) {
|
|
|
1473
1473
|
case 'lint': {
|
|
1474
1474
|
return '`lint` за замовчуванням авто-fix лінтерів (oxfmt/eslint --fix/stylelint --fix) і конформності (--full) у поточному каталозі'
|
|
1475
1475
|
}
|
|
1476
|
-
case 'coverage': {
|
|
1477
|
-
return '`coverage` генерує COVERAGE.md і Stryker-артефакти в поточному каталозі'
|
|
1478
|
-
}
|
|
1479
1476
|
case 'change': {
|
|
1480
1477
|
return '`change` пише change-файл у .changes/ поточного каталогу'
|
|
1481
1478
|
}
|
|
@@ -1493,7 +1490,7 @@ const [command, ...args] = process.argv.slice(2)
|
|
|
1493
1490
|
|
|
1494
1491
|
try {
|
|
1495
1492
|
// Root-guard до перших мутацій: дефолтний sync скаффолдить .cursor/.claude/CLAUDE.md/
|
|
1496
|
-
// .n-cursor.json + bun install, а
|
|
1493
|
+
// .n-cursor.json + bun install, а lint/change/release переписують файли в CWD —
|
|
1497
1494
|
// усе це ключиться на cwd(). Запуск із піддиректорії git-репо (типово прямий
|
|
1498
1495
|
// `bun npm/bin/n-cursor.js` не з кореня) зачепив би не той каталог → STOP. Read-only та
|
|
1499
1496
|
// `--root`-команди (trace, graph, doc-aggregate, rename-yaml-extensions) не зачіпаємо.
|
|
@@ -1523,33 +1520,18 @@ try {
|
|
|
1523
1520
|
// Дві ортогональні осі: --full (scope: весь репо vs дельта vs origin) × --read-only (behavior).
|
|
1524
1521
|
// Позиційні (не-флаг) аргументи — фільтр правил конформності (напр. `lint changelog`):
|
|
1525
1522
|
// прогнати лише конформність цих правил, без лінтер-скану (мапить колишній `fix <rule>`).
|
|
1526
|
-
const
|
|
1523
|
+
const cwdIdx = args.indexOf('--cwd')
|
|
1524
|
+
const cwdArg = cwdIdx !== -1 ? resolve(args[cwdIdx + 1]) : undefined
|
|
1525
|
+
const rules = args.filter((a, i) => !a.startsWith('-') && i !== cwdIdx + 1)
|
|
1527
1526
|
process.exitCode = await runLint({
|
|
1528
1527
|
full: args.includes('--full'),
|
|
1529
1528
|
readOnly: args.includes('--read-only'),
|
|
1530
|
-
rules
|
|
1529
|
+
rules,
|
|
1530
|
+
cwd: cwdArg
|
|
1531
1531
|
})
|
|
1532
1532
|
|
|
1533
1533
|
break
|
|
1534
1534
|
}
|
|
1535
|
-
case 'coverage': {
|
|
1536
|
-
// n-cursor coverage — оркестратор покриття + мутаційного тестування з discovery
|
|
1537
|
-
// провайдерів через .n-cursor.json#rules (test.mdc). --changed звужує scope до
|
|
1538
|
-
// змінених від base файлів (flow-турнікет: лише vitest/Stryker по diff).
|
|
1539
|
-
const { runCoverageCli } = await import('../rules/test/coverage/coverage.mjs')
|
|
1540
|
-
process.exitCode = await runCoverageCli({ fix: args.includes('--fix'), changed: args.includes('--changed') })
|
|
1541
|
-
|
|
1542
|
-
break
|
|
1543
|
-
}
|
|
1544
|
-
case 'coverage-fix': {
|
|
1545
|
-
// n-cursor coverage-fix index|slice — read-only витяг вцілілих мутантів із
|
|
1546
|
-
// COVERAGE.md для скілу n-coverage-fix. Важкий парсинг несе скрипт (0 LLM-
|
|
1547
|
-
// токенів); агент отримує лише компактний index або зріз під один файл.
|
|
1548
|
-
const { runCoverageFixCli } = await import('../scripts/coverage-fix-extract.mjs')
|
|
1549
|
-
process.exitCode = await runCoverageFixCli(args)
|
|
1550
|
-
|
|
1551
|
-
break
|
|
1552
|
-
}
|
|
1553
1535
|
case 'analyze-escalation': {
|
|
1554
1536
|
// n-cursor analyze-escalation — читає весь escalation-лог (.n-cursor/fix-escalation.jsonl),
|
|
1555
1537
|
// чанкує й просить хмарну avg-модель запропонувати, як зменшити LLM-залежність fix-
|
|
@@ -1634,7 +1616,7 @@ try {
|
|
|
1634
1616
|
default: {
|
|
1635
1617
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1636
1618
|
console.error(
|
|
1637
|
-
` Очікується: (без аргументів) синхронізація правил, rename-yaml-extensions, hook, adr-normalize-local, lint (включно зі scope: lint ga|rego|k8s|docker|text),
|
|
1619
|
+
` Очікується: (без аргументів) синхронізація правил, rename-yaml-extensions, hook, adr-normalize-local, lint (включно зі scope: lint ga|rego|k8s|docker|text), analyze-escalation, taze, start-check, release, skill, trace, doc-aggregate`
|
|
1638
1620
|
)
|
|
1639
1621
|
process.exitCode = 1
|
|
1640
1622
|
}
|
package/package.json
CHANGED
|
@@ -3,40 +3,30 @@ type: JS Module
|
|
|
3
3
|
title: hooks.mjs
|
|
4
4
|
resource: npm/rules/adr/js/hooks.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 7195231e
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
7
8
|
score: 100
|
|
8
9
|
---
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
## Огляд
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
1. Перевірка канонічності скриптів
|
|
15
|
-
Проводиться перевірка наявності та відповідності логіки між скриптами, визначеними в артефактах, і їхніми канонічними версіями. У разі невідповідності повертається сигнал про необхідність повторного синхронізації.
|
|
16
|
-
|
|
17
|
-
2. Валідація налаштувань проєкту
|
|
18
|
-
Перевіряється наявність файлу `.claude/settings.json` для підтвердження коректності конфігурації відповідно до вимог (adr.mdc).
|
|
19
|
-
|
|
20
|
-
3. Перевірка конфігурації хуків Cursor
|
|
21
|
-
Перевіряється, чи містить конфігурацію Cursor необхідні стоп-хуки для кожного визначеного скрипта.
|
|
13
|
+
Модуль перевіряє стан проєкту, валідуючи конфігураційні файли (`settings.json`, `hooks.json`, `settings.local.json`) та перевіряючи наявність структур для документації (adr.mdc). Перевірка включає перевірку наявності необхідних елементів, при цьому шляхи `.git` свідомо ігноруються.
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
Перевіряється вміст файлу `.gitignore` на відповідність ігнорування кожного лог-файлу хука.
|
|
25
|
-
|
|
26
|
-
5. Перевірка покриття логів
|
|
27
|
-
Перевіряється, чи покриває конфігурація `.gitignore` всі необхідні лог-файли.
|
|
28
|
-
|
|
29
|
-
6. Перевірка доступності CLI
|
|
30
|
-
Перевіряється наявність бінарних файлів `claude` або `cursor-agent` у системному PATH. Якщо жодного не знайдено, це інформується як попередження, оскільки хук працюватиме у режимі мовчки.
|
|
31
|
-
|
|
32
|
-
## Публічний API
|
|
15
|
+
## Поведінка
|
|
33
16
|
|
|
34
|
-
|
|
17
|
+
1. Викликається `check` для запуску повного набору перевірок проєкту.
|
|
18
|
+
2. Перевіряється наявність та збіг конфігураційних скриптів хуків у проєкті з канонічними версіями.
|
|
19
|
+
3. Перевіряється наявність конфігурацій проєкту, зокрема `settings.json`.
|
|
20
|
+
4. Перевіряється, чи містить конфігурація хуків проєкту (`hooks.json`) необхідні маркери для зупинення (stop-hook) кожного артефакту хука.
|
|
21
|
+
5. Перевіряється, чи ігнорує файл `.gitignore` лог-файли кожного хука.
|
|
22
|
+
6. Перевіряється, чи ігнорує файл `.gitignore` файли стану та блокування, пов'язані з нормалізацією хуків.
|
|
23
|
+
7. Перевіряється наявність каталогу `docs/adr/` для зберігання ADR-ів.
|
|
24
|
+
8. Перевіряється доступність LLM CLI (`claude` або `cursor-agent`) у системному шляху.
|
|
25
|
+
9. Повертається код виходу, що відображає результати всіх перевірок.
|
|
35
26
|
|
|
36
27
|
## Гарантії поведінки
|
|
37
28
|
|
|
38
|
-
- Read-only:
|
|
29
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
39
30
|
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
40
|
-
- За
|
|
31
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
41
32
|
- Свідомо пропускає шляхи: `.git`.
|
|
42
|
-
- Не звертається до мережі.
|
package/rules/adr/js/hooks.mjs
CHANGED
|
@@ -55,6 +55,9 @@ function gitignoreLineCoversHookLog(line, logPath) {
|
|
|
55
55
|
if (line === '.claude/hooks/*.log' || line === '.claude/hooks/**/*.log') {
|
|
56
56
|
return true
|
|
57
57
|
}
|
|
58
|
+
if (line === '.claude/hooks/*' || line === '.claude/hooks/**') {
|
|
59
|
+
return true
|
|
60
|
+
}
|
|
58
61
|
if (line === '*.log' || line === '**/*.log') {
|
|
59
62
|
return true
|
|
60
63
|
}
|
|
@@ -264,6 +267,60 @@ function checkLlmCliAvailable(reporter) {
|
|
|
264
267
|
* @param {string} [cwd] корінь репозиторію
|
|
265
268
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
266
269
|
*/
|
|
270
|
+
/** Файли стану/блокування normalize-хука, які не мають потрапляти в git. */
|
|
271
|
+
const NORMALIZE_STATE_FILES = ['.normalize-state', '.normalize.lock']
|
|
272
|
+
const CLAUDE_HOOKS_REL = '.claude/hooks'
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Перевіряє рядок `.gitignore` на покриття конкретного state/lock файлу.
|
|
276
|
+
* @param {string} line нормалізований (trim) рядок
|
|
277
|
+
* @param {string} statePath відносний шлях файлу (наприклад `.claude/hooks/.normalize-state`)
|
|
278
|
+
* @returns {boolean} true — рядок покриває файл
|
|
279
|
+
*/
|
|
280
|
+
function gitignoreLineCoversStatePath(line, statePath) {
|
|
281
|
+
if (line === statePath) return true
|
|
282
|
+
// .claude/hooks/* або .claude/hooks/**
|
|
283
|
+
if (line === `${CLAUDE_HOOKS_REL}/*` || line === `${CLAUDE_HOOKS_REL}/**`) return true
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Перевіряє `.gitignore` на наявність рядків для файлів стану normalize-хука.
|
|
289
|
+
* @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter
|
|
290
|
+
* @param {string} cwd корінь репозиторію
|
|
291
|
+
* @returns {Promise<void>}
|
|
292
|
+
*/
|
|
293
|
+
async function checkGitignoreForStateFiles(reporter, cwd) {
|
|
294
|
+
const { pass, fail } = reporter
|
|
295
|
+
const gitignoreAbs = join(cwd, '.gitignore')
|
|
296
|
+
const content = existsSync(gitignoreAbs) ? await readFile(gitignoreAbs, 'utf8') : ''
|
|
297
|
+
const lines = content.split(EOL_RE).map(l => l.trim())
|
|
298
|
+
for (const file of NORMALIZE_STATE_FILES) {
|
|
299
|
+
const statePath = `${CLAUDE_HOOKS_REL}/${file}`
|
|
300
|
+
if (lines.some(l => gitignoreLineCoversStatePath(l, statePath))) {
|
|
301
|
+
pass(`.gitignore покриває ${statePath}`)
|
|
302
|
+
} else {
|
|
303
|
+
fail(`.gitignore не ігнорує \`${statePath}\` — додай рядок (adr.mdc)`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Перевіряє наявність каталогу `docs/adr/` — обов'язкового місця зберігання ADR-ів.
|
|
310
|
+
* @param {import('../../../scripts/lib/check-reporter.mjs').CheckReporter} reporter
|
|
311
|
+
* @param {string} cwd корінь репозиторію
|
|
312
|
+
* @returns {void}
|
|
313
|
+
*/
|
|
314
|
+
function checkDocsAdrDir(reporter, cwd) {
|
|
315
|
+
const { pass, fail } = reporter
|
|
316
|
+
const adrDir = join(cwd, 'docs', 'adr')
|
|
317
|
+
if (existsSync(adrDir)) {
|
|
318
|
+
pass('docs/adr/ існує (каталог ADR-ів)')
|
|
319
|
+
} else {
|
|
320
|
+
fail('docs/adr/ відсутній — створи каталог для ADR-ів (adr.mdc)')
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
267
324
|
export async function check(cwd = process.cwd()) {
|
|
268
325
|
const reporter = createCheckReporter()
|
|
269
326
|
for (const { scriptName } of HOOK_ARTIFACTS) {
|
|
@@ -272,6 +329,8 @@ export async function check(cwd = process.cwd()) {
|
|
|
272
329
|
checkProjectSettings(reporter, cwd)
|
|
273
330
|
await checkCursorHooks(reporter, cwd)
|
|
274
331
|
await checkGitignore(reporter, cwd)
|
|
332
|
+
await checkGitignoreForStateFiles(reporter, cwd)
|
|
333
|
+
checkDocsAdrDir(reporter, cwd)
|
|
275
334
|
checkLlmCliAvailable(reporter)
|
|
276
335
|
return reporter.getExitCode()
|
|
277
336
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** @see ./docs/consistency.md */
|
|
2
2
|
import { execFile } from 'node:child_process'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { readFile } from 'node:fs/promises'
|
|
3
5
|
import { join } from 'node:path'
|
|
4
6
|
import { promisify } from 'node:util'
|
|
5
7
|
|
|
@@ -379,6 +381,46 @@ function createDefaultGetPublishedVersion() {
|
|
|
379
381
|
* @param {(msg: string) => void} pass параметр
|
|
380
382
|
* @param {(msg: string) => void} fail параметр
|
|
381
383
|
*/
|
|
384
|
+
/**
|
|
385
|
+
* Перевіряє наявність `CHANGELOG.md` у воркспейсі.
|
|
386
|
+
* @param {string} ws відносний шлях воркспейсу від кореня репо
|
|
387
|
+
* @param {string} label мітка для повідомлень
|
|
388
|
+
* @param {string} cwd корінь репозиторію
|
|
389
|
+
* @param {(msg: string) => void} pass
|
|
390
|
+
* @param {(msg: string) => void} fail
|
|
391
|
+
* @returns {boolean} true — файл існує
|
|
392
|
+
*/
|
|
393
|
+
function checkChangelogFileExists(ws, label, cwd, pass, fail) {
|
|
394
|
+
const path = join(cwd, ws, 'CHANGELOG.md')
|
|
395
|
+
if (existsSync(path)) {
|
|
396
|
+
pass(`${label}: CHANGELOG.md існує`)
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
fail(`${label}: CHANGELOG.md відсутній — створи файл за форматом Keep a Changelog (n-changelog.mdc)`)
|
|
400
|
+
return false
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Перевіряє базовий формат `CHANGELOG.md`: наявність H1 `# Changelog`.
|
|
405
|
+
* Версійні секції `## [x.y.z]` не вимагаються для нових workspace-ів без релізів.
|
|
406
|
+
* @param {string} ws відносний шлях воркспейсу від кореня репо
|
|
407
|
+
* @param {string} label мітка для повідомлень
|
|
408
|
+
* @param {string} cwd корінь репозиторію
|
|
409
|
+
* @param {(msg: string) => void} pass
|
|
410
|
+
* @param {(msg: string) => void} fail
|
|
411
|
+
* @returns {Promise<void>}
|
|
412
|
+
*/
|
|
413
|
+
async function checkChangelogFormat(ws, label, cwd, pass, fail) {
|
|
414
|
+
const path = join(cwd, ws, 'CHANGELOG.md')
|
|
415
|
+
const content = await readFile(path, 'utf8')
|
|
416
|
+
const hasH1 = content.split('\n').some(l => l.trimEnd() === '# Changelog')
|
|
417
|
+
if (hasH1) {
|
|
418
|
+
pass(`${label}: CHANGELOG.md має рядок "# Changelog"`)
|
|
419
|
+
} else {
|
|
420
|
+
fail(`${label}: CHANGELOG.md не має рядка "# Changelog" — перший рядок має бути H1-заголовком (n-changelog.mdc)`)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
382
424
|
function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
|
|
383
425
|
if (manifest.kind !== 'npm' || !manifest.npmFiles) return
|
|
384
426
|
const pkgPath = manifestFilePath(manifest.ws, manifest)
|
|
@@ -542,6 +584,10 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, sub
|
|
|
542
584
|
async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, autofix, pass, fail, cwd) {
|
|
543
585
|
const label = workspaceLabel(manifest)
|
|
544
586
|
const mf = manifestFilePath(manifest.ws, manifest)
|
|
587
|
+
const changelogExists = checkChangelogFileExists(manifest.ws, label, cwd, pass, fail)
|
|
588
|
+
if (changelogExists) {
|
|
589
|
+
await checkChangelogFormat(manifest.ws, label, cwd, pass, fail)
|
|
590
|
+
}
|
|
545
591
|
const Vcurrent = manifest.version
|
|
546
592
|
if (!Vcurrent) {
|
|
547
593
|
fail(`${label}: у ${mf} відсутнє поле version (registry-published воркспейс)`)
|
|
@@ -635,6 +681,14 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
|
|
|
635
681
|
async function runLocalOnlyChecks(localOnly, subWorkspaces, autofix, pass, fail, cwd) {
|
|
636
682
|
if (localOnly.length === 0) return
|
|
637
683
|
|
|
684
|
+
for (const manifest of localOnly) {
|
|
685
|
+
const label = workspaceLabel(manifest)
|
|
686
|
+
const exists = checkChangelogFileExists(manifest.ws, label, cwd, pass, fail)
|
|
687
|
+
if (exists) {
|
|
688
|
+
await checkChangelogFormat(manifest.ws, label, cwd, pass, fail)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
638
692
|
if (!(await isInsideGitRepo(cwd))) {
|
|
639
693
|
pass('changelog: не git-репозиторій — local-only перевірку пропущено')
|
|
640
694
|
return
|
|
@@ -3,28 +3,33 @@ type: JS Module
|
|
|
3
3
|
title: consistency.mjs
|
|
4
4
|
resource: npm/rules/changelog/js/consistency.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: a9bebf31
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score:
|
|
8
|
+
score: 95
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Компонент виконує перевірку стану проєктів у монорепозиторії. Він звертається до мережі для порівняння версій з реєстром, зокрема https://pypi.org/pypi/, та аналізує зміни, посилаючись на (n-changelog.mdc). Код спирається на конфігурацію, визначену у res.json. При виявленні невідповідностей компонент перехоплює помилки (fail-safe) і повертає порожнє значення замість кидання винятків.
|
|
11
|
+
Модуль сканує монорепозиторій, ідентифікує проєкти та порівнює їхні версії з даними, отриманими з https://pypi.org/pypi/, для верифікації відповідності версій. Для проєктів, що не призначені для публікації, виконуються локальні перевірки на відповідність файлу `CHANGELOG.md` конфігурації, визначеній у res.json.
|
|
14
12
|
|
|
15
13
|
## Поведінка
|
|
16
14
|
|
|
17
|
-
1. `check` ініціалізує репортер
|
|
18
|
-
2. `check`
|
|
19
|
-
3. `check`
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
1. `check` ініціалізує репортер для збору результатів перевірки.
|
|
16
|
+
2. `check` визначає робочий каталог та режим автоматичного виправлення на основі змінних середовища.
|
|
17
|
+
3. `check` знаходить усі кореневі каталоги проєктів у монорепозиторії.
|
|
18
|
+
4. `check` класифікує знайдені проєкти на ті, що можуть бути опубліковані в реєстрі, та ті, що є локальними.
|
|
19
|
+
5. Для кожного проєкту, що може бути опублікованим, `check` перевіряє його відповідність вимогам:
|
|
20
|
+
а. Перевіряє наявність файлу `CHANGELOG.md`.
|
|
21
|
+
б. Перевіряє базовий формат `CHANGELOG.md` на наявність заголовка `# Changelog`.
|
|
22
|
+
в. Якщо проєкт має поле `version` у маніфесті, `check` порівнює його з опублікованою версією, отриманою через мережевий запит до https://pypi.org/pypi/ або `npm view`.
|
|
23
|
+
г. Якщо версія у проєкті випереджає опубліковану, `check` повідомляє про заборонений ручний bump.
|
|
24
|
+
д. Якщо версія у проєкті відстає від опублікованої, `check` повідомляє про відставання локальної копії від реєстру.
|
|
25
|
+
е. Якщо версії збігаються, `check` перевіряє, чи є незрелі зміни у проєкті відносно базового релізу.
|
|
26
|
+
ж. Якщо проєкт має незрелі зміни, `check` перевіряє наявність change-файлу. Якщо його немає, `check` або повідомляє про необхідність створити його (якщо не в режимі autofix), або автоматично створює його та додає до індексу.
|
|
27
|
+
6. `check` виконує локальні перевірки для проєктів, які не призначені для публікації:
|
|
28
|
+
а. Для кожного локального проєкту `check` перевіряє наявність файлу `CHANGELOG.md` та його формат.
|
|
29
|
+
б. `check` визначає точку порівняння (базу) на основі поточної гілки.
|
|
30
|
+
в. `check` перевіряє, чи є релевантні зміни у проєкті відносно цієї бази.
|
|
31
|
+
г. Якщо зміни є, `check` перевіряє наявність change-файлу. Якщо його немає, `check` або повідомляє про необхідність створити його (якщо не в режимі autofix), або автоматично створює його та додає до індексу.
|
|
32
|
+
7. `check` повертає кінцевий код завершення, що відображає результати всіх перевірок.
|
|
28
33
|
|
|
29
34
|
## Гарантії поведінки
|
|
30
35
|
|
|
@@ -127,6 +127,21 @@ deny contains msg if {
|
|
|
127
127
|
msg := sprintf(setup_bun_no_checkout_template, [job_id])
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
# ── deny: actions/checkout без persist-credentials: false ─────────────────
|
|
131
|
+
#
|
|
132
|
+
# `persist-credentials: false` запобігає кешуванню git-токена після checkout,
|
|
133
|
+
# що є обов'язковою вимогою безпеки (ga.mdc).
|
|
134
|
+
|
|
135
|
+
deny contains msg if {
|
|
136
|
+
some _job_id, job in jobs
|
|
137
|
+
some step in object.get(job, "steps", [])
|
|
138
|
+
uses := object.get(step, "uses", "")
|
|
139
|
+
startswith(uses, "actions/checkout@")
|
|
140
|
+
creds := object.get(object.get(step, "with", {}), "persist-credentials", true)
|
|
141
|
+
creds != false
|
|
142
|
+
msg := sprintf("jobs.%s: actions/checkout@v6 потребує `with: persist-credentials: false` (ga.mdc)", [_job_id])
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
# ── deny: concurrency блок ─────────────────────────────────────────────────
|
|
131
146
|
#
|
|
132
147
|
# Дублює окремі per-workflow перевірки для clean-ga-workflows / clean-merged-branch /
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Перевірка `.github/workflows/lint-k8s.yml` (k8s.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/lint-k8s.yml.snippet.yml.
|
|
5
|
+
# Перевіряємо (drift-safe — усе ведеться з template, без inline-літералів):
|
|
6
|
+
# - кожен `uses` з template: actions/checkout@v6, setup-bun-deps;
|
|
7
|
+
# - кожен `run` з template (як substring): install kubeconform, kubescape,
|
|
8
|
+
# n-cursor lint k8s --read-only.
|
|
9
|
+
# Універсальні workflow-перевірки (name, concurrency, branches,
|
|
10
|
+
# persist-credentials) — у `ga.workflow_common`.
|
|
11
|
+
package k8s.lint_k8s_yml
|
|
12
|
+
|
|
13
|
+
import rego.v1
|
|
14
|
+
|
|
15
|
+
# Усі `uses` з канону workflow (по всіх job'ах template).
|
|
16
|
+
expected_uses contains u if {
|
|
17
|
+
some job in data.template.snippet.jobs
|
|
18
|
+
some step in job.steps
|
|
19
|
+
u := object.get(step, "uses", "")
|
|
20
|
+
u != ""
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Усі `uses` з input workflow.
|
|
24
|
+
actual_uses contains u if {
|
|
25
|
+
some job in object.get(input, "jobs", {})
|
|
26
|
+
some step in object.get(job, "steps", [])
|
|
27
|
+
u := object.get(step, "uses", "")
|
|
28
|
+
u != ""
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Конкатенація всіх `run`-кроків з input workflow.
|
|
32
|
+
all_run_text := concat("\n", [run_text |
|
|
33
|
+
some job in object.get(input, "jobs", {})
|
|
34
|
+
some step in object.get(job, "steps", [])
|
|
35
|
+
run_text := step_run_to_text(step)
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
deny contains msg if {
|
|
39
|
+
some required_use in expected_uses
|
|
40
|
+
not required_use in actual_uses
|
|
41
|
+
msg := sprintf("lint-k8s.yml: відсутній step з `uses: %s` (k8s.mdc)", [required_use])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
deny contains msg if {
|
|
45
|
+
some job in data.template.snippet.jobs
|
|
46
|
+
some step in job.steps
|
|
47
|
+
expected_run := object.get(step, "run", "")
|
|
48
|
+
expected_run != ""
|
|
49
|
+
not contains(all_run_text, expected_run)
|
|
50
|
+
msg := sprintf("lint-k8s.yml: жоден крок run не містить %q (k8s.mdc)", [expected_run])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
step_run_to_text(step) := step.run if is_string(step.run)
|
|
54
|
+
|
|
55
|
+
else := concat("\n", [s | some s in step.run]) if is_array(step.run)
|
|
56
|
+
|
|
57
|
+
else := ""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Lint K8s
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- dev
|
|
7
|
+
- main
|
|
8
|
+
paths:
|
|
9
|
+
- '**/k8s/**/*.yaml'
|
|
10
|
+
|
|
11
|
+
pull_request:
|
|
12
|
+
branches:
|
|
13
|
+
- dev
|
|
14
|
+
- main
|
|
15
|
+
|
|
16
|
+
concurrency:
|
|
17
|
+
group: ${{ github.ref }}-${{ github.workflow }}
|
|
18
|
+
cancel-in-progress: true
|
|
19
|
+
|
|
20
|
+
jobs:
|
|
21
|
+
lint-k8s:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
|
|
30
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
31
|
+
|
|
32
|
+
- name: Install kubeconform
|
|
33
|
+
run: |
|
|
34
|
+
curl -sSL "https://github.com/yannh/kubeconform/releases/download/v0.7.0/kubeconform-linux-amd64.tar.gz" | tar xz
|
|
35
|
+
sudo mv kubeconform /usr/local/bin/
|
|
36
|
+
|
|
37
|
+
- name: Install kubescape
|
|
38
|
+
run: |
|
|
39
|
+
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash
|
|
40
|
+
echo "$HOME/.kubescape/bin" >> $GITHUB_PATH
|
|
41
|
+
|
|
42
|
+
- name: Lint K8s
|
|
43
|
+
run: n-cursor lint k8s --read-only
|
|
@@ -43,6 +43,15 @@ deny contains msg if {
|
|
|
43
43
|
msg := sprintf("lint-python.yml: відсутній step з `uses: %s` (python.mdc)", [required_use])
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
deny contains msg if {
|
|
47
|
+
some job in object.get(input, "jobs", {})
|
|
48
|
+
some step in object.get(job, "steps", [])
|
|
49
|
+
object.get(step, "uses", "") == "actions/checkout@v6"
|
|
50
|
+
creds := object.get(object.get(step, "with", {}), "persist-credentials", true)
|
|
51
|
+
creds != false
|
|
52
|
+
msg := "lint-python.yml: actions/checkout@v6 потребує `with: persist-credentials: false` (python.mdc)"
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
deny contains msg if {
|
|
47
56
|
some job in data.template.snippet.jobs
|
|
48
57
|
some step in job.steps
|
|
@@ -21,7 +21,7 @@ deny contains msg if {
|
|
|
21
21
|
msg := sprintf("pyproject.toml: [tool.%s] — %s", [key, reason])
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
# ── PEP 621: обовʼязкові [project]
|
|
24
|
+
# ── PEP 621: обовʼязкові [project].* ─────────────────────────────────────────
|
|
25
25
|
|
|
26
26
|
deny contains msg if {
|
|
27
27
|
not project_field_set("name")
|
|
@@ -33,7 +33,21 @@ deny contains msg if {
|
|
|
33
33
|
msg := "pyproject.toml: відсутній статичний [project].version (PEP 621, python.mdc)"
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
deny contains msg if {
|
|
37
|
+
not project_field_set("requires-python")
|
|
38
|
+
msg := "pyproject.toml: відсутній [project].requires-python (наприклад '>=3.12') (PEP 621, python.mdc)"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deny contains msg if {
|
|
42
|
+
not project_key_exists("dependencies")
|
|
43
|
+
msg := "pyproject.toml: відсутній [project].dependencies (навіть порожній список `[]`) (PEP 621, python.mdc)"
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
project_field_set(key) if {
|
|
37
47
|
value := object.get(object.get(input, "project", {}), key, "")
|
|
38
48
|
value != ""
|
|
39
49
|
}
|
|
50
|
+
|
|
51
|
+
project_key_exists(key) if {
|
|
52
|
+
key in object.keys(object.get(input, "project", {}))
|
|
53
|
+
}
|
|
@@ -3,33 +3,28 @@ type: JS Module
|
|
|
3
3
|
title: packages.mjs
|
|
4
4
|
resource: npm/rules/vue/js/packages.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
|
|
6
|
+
crc: 8589151d
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
+
score: 100
|
|
8
9
|
---
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
Перевіряє, чи є пакет бібліотекою компонентів Vue шляхом перевірки `peerDependencies`.
|
|
11
|
+
## Огляд
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
Перевіряє залежності та конфігурацію vite.config одного Vue-пакета.
|
|
13
|
+
Модуль визначає, чи є пакет бібліотекою компонентів Vue, виходячи з даних у `package.json`, `jsconfig.json`, `package-lock.json`, `extensions.json`. Він також перевіряє відповідність усіх пакетів, що залежать від `vue`, критеріям, описаним у `vue.mdc`, і повертає код виходу.
|
|
15
14
|
|
|
16
15
|
## Поведінка
|
|
17
16
|
|
|
18
|
-
isVueComponentLibraryPkg
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
check
|
|
22
|
-
Перевіряє залежності та vite.config одного Vue-пакета
|
|
17
|
+
isVueComponentLibraryPkg визначає, чи є пакет бібліотекою компонентів Vue, перевіряючи наявність `vue` у `peerDependencies` його `package.json`.
|
|
18
|
+
check перевіряє відповідність проєкту правилам vue.mdc для всіх пакетів, що містять `vue` у `dependencies`, і повертає код виходу. При цьому ігноруються шляхи `.git` та `node_modules`.
|
|
23
19
|
|
|
24
20
|
## Публічний API
|
|
25
21
|
|
|
26
|
-
isVueComponentLibraryPkg —
|
|
27
|
-
passFn —
|
|
28
|
-
check — перевіряє, чи
|
|
22
|
+
isVueComponentLibraryPkg — визначає, чи є пакет бібліотекою компонентів Vue, щоб IDE коректно обробляла файли `.vue` та `vite-env.d.ts`.
|
|
23
|
+
passFn — підтверджує наявність файлу `jsconfig.json` у вказаній директорії.
|
|
24
|
+
check — перевіряє, чи відповідає проєкт вимогам vue.mdc, а саме: чи є `vue` у залежностях кореневого та всіх workspace-пакетів.
|
|
29
25
|
|
|
30
26
|
## Гарантії поведінки
|
|
31
27
|
|
|
32
|
-
- Read-only:
|
|
33
|
-
-
|
|
28
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
29
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
34
30
|
- Свідомо пропускає шляхи: `.git`, `node_modules`.
|
|
35
|
-
- Не звертається до мережі.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @see ./docs/packages.md */
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
3
|
import { readFile } from 'node:fs/promises'
|
|
4
4
|
import { join, relative } from 'node:path'
|
|
5
5
|
|
|
@@ -500,6 +500,45 @@ async function checkVueVolarRecommendation(pass, fail, cwd) {
|
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
// Vitest-пакети мусять бути у кореневому devDependencies монорепо,
|
|
504
|
+
// бо npm-module правило забороняє devDeps у published Vue workspace.
|
|
505
|
+
const ROOT_VITEST_DEV_DEPS = ['vitest', '@vitest/coverage-v8', '@stryker-mutator/vitest-runner']
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Перевіряє, що кореневий `package.json` монорепо містить vitest-залежності
|
|
509
|
+
* у `devDependencies`. Викликається тільки коли є Vue-пакети у воркспейсі.
|
|
510
|
+
* @param {string} cwd корінь репозиторію
|
|
511
|
+
* @param {(msg: string) => void} pass pass callback
|
|
512
|
+
* @param {(msg: string) => void} fail fail callback
|
|
513
|
+
* @returns {void}
|
|
514
|
+
*/
|
|
515
|
+
function checkRootVitestDevDeps(cwd, pass, fail) {
|
|
516
|
+
const rootPkgPath = join(cwd, 'package.json')
|
|
517
|
+
if (!existsSync(rootPkgPath)) {
|
|
518
|
+
fail('vue: кореневий package.json не знайдено — неможливо перевірити vitest devDependencies')
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
let rootPkg
|
|
522
|
+
try {
|
|
523
|
+
rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf8'))
|
|
524
|
+
} catch {
|
|
525
|
+
fail('vue: кореневий package.json не вдалося розпарсити — неможливо перевірити vitest devDependencies')
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
const devDeps =
|
|
529
|
+
rootPkg.devDependencies && typeof rootPkg.devDependencies === 'object' ? Object.keys(rootPkg.devDependencies) : []
|
|
530
|
+
const missing = ROOT_VITEST_DEV_DEPS.filter(p => !devDeps.includes(p))
|
|
531
|
+
if (missing.length === 0) {
|
|
532
|
+
pass(`vue: кореневий devDependencies містить ${ROOT_VITEST_DEV_DEPS.join(', ')} (vue.mdc testing)`)
|
|
533
|
+
} else {
|
|
534
|
+
for (const pkg of missing) {
|
|
535
|
+
fail(
|
|
536
|
+
`vue: кореневий devDependencies не містить '${pkg}' — перенеси з Vue workspace у корінь монорепо (vue.mdc testing)`
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
503
542
|
/**
|
|
504
543
|
* Перевіряє відповідність проєкту правилам vue.mdc (корінь і всі workspace-пакети з `vue` у dependencies).
|
|
505
544
|
* @param {string} [cwd] корінь репозиторію
|
|
@@ -519,6 +558,7 @@ export async function check(cwd = process.cwd()) {
|
|
|
519
558
|
}
|
|
520
559
|
|
|
521
560
|
await checkVueVolarRecommendation(pass, fail, cwd)
|
|
561
|
+
checkRootVitestDevDeps(cwd, pass, fail)
|
|
522
562
|
|
|
523
563
|
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
524
564
|
for (const { rootDir, isComponentLibrary } of vueRoots) {
|
|
@@ -6,14 +6,14 @@ resource: npm/scripts/lib/fix/
|
|
|
6
6
|
|
|
7
7
|
# npm/scripts/lib/fix
|
|
8
8
|
|
|
9
|
-
| Файл
|
|
10
|
-
|
|
11
|
-
| [analyze-escalation.mjs](analyze-escalation.md)
|
|
12
|
-
| [escalation-log.mjs](escalation-log.md)
|
|
13
|
-
| [llm-fix-apply.mjs](llm-fix-apply.md)
|
|
14
|
-
| [llm-lint-fix.mjs](llm-lint-fix.md)
|
|
15
|
-
| [llm-worker.mjs](llm-worker.md)
|
|
16
|
-
| [orchestrator.mjs](orchestrator.md)
|
|
9
|
+
| Файл | Тип |
|
|
10
|
+
| ----------------------------------------------------- | --------- |
|
|
11
|
+
| [analyze-escalation.mjs](analyze-escalation.md) | JS Module |
|
|
12
|
+
| [escalation-log.mjs](escalation-log.md) | JS Module |
|
|
13
|
+
| [llm-fix-apply.mjs](llm-fix-apply.md) | JS Module |
|
|
14
|
+
| [llm-lint-fix.mjs](llm-lint-fix.md) | JS Module |
|
|
15
|
+
| [llm-worker.mjs](llm-worker.md) | JS Module |
|
|
16
|
+
| [orchestrator.mjs](orchestrator.md) | JS Module |
|
|
17
17
|
| [run-conformance-check.mjs](run-conformance-check.md) | JS Module |
|
|
18
|
-
| [t0.mjs](t0.md)
|
|
19
|
-
| [verbose-block.mjs](verbose-block.md)
|
|
18
|
+
| [t0.mjs](t0.md) | JS Module |
|
|
19
|
+
| [verbose-block.mjs](verbose-block.md) | JS Module |
|
|
@@ -3,62 +3,32 @@ type: JS Module
|
|
|
3
3
|
title: orchestrator.mjs
|
|
4
4
|
resource: npm/scripts/lib/fix/orchestrator.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
6
|
+
crc: c34d0630
|
|
7
|
+
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Огляд
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
### `buildLadder`
|
|
16
|
-
|
|
17
|
-
Будує масив рунгів ескалації з чотирьох тирів:
|
|
18
|
-
|
|
19
|
-
| Тир | Модель | Feedback | Timeout |
|
|
20
|
-
| ----------------- | ----------- | -------- | ------------------------- |
|
|
21
|
-
| `local-min` | `LOCAL_MIN` | ні | `LOCAL_TIMEOUT_MS` (45s) |
|
|
22
|
-
| `local-min-retry` | `LOCAL_MIN` | так | `LOCAL_TIMEOUT_MS` |
|
|
23
|
-
| `cloud-min` | `CLOUD_MIN` | так | `CLOUD_TIMEOUT_MS` (120s) |
|
|
24
|
-
| `cloud-avg` | `CLOUD_AVG` | так | `CLOUD_TIMEOUT_MS` |
|
|
25
|
-
|
|
26
|
-
Рунги з порожньою моделлю (`''`) відфільтровуються — драбина стискається до доступних тирів.
|
|
27
|
-
|
|
28
|
-
### `escalateRule`
|
|
13
|
+
Модуль відповідає за управління процесом вирішення порушень. Він будує послідовність тирів ескалації за допомогою `buildLadder`. Функція `parseOrchestratorArgs` визначає бюджет LLM та фільтр правил. Далі, `runOrchestratorCli` виконує процес виправлення правил, послідовно застосовуючи `escalateRule` по тирах до досягнення першого успішного вирішення.
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
1. Виклик `worker.runLlmWorker` (синхронно) з feedback від попереднього рунга.
|
|
33
|
-
2. Re-check через `check([ruleId], cwd)`.
|
|
34
|
-
3. Запис у escalation-лог (`logEscalation`).
|
|
35
|
-
4. Якщо re-check зелений → `{ resolved: true }`.
|
|
36
|
-
5. Якщо ні → `decideAfterFailure` визначає дію: `break` (no-key або cloud transport fail), `skip-model` (systemic omlx-помилка), або продовжити.
|
|
37
|
-
6. Avg-рунг пропускається, якщо `avgBudget <= 0` (з фіксацією у лог).
|
|
38
|
-
|
|
39
|
-
Після кожного рунга виводить verbose-блок (`printVerboseBlock`), якщо `N_CURSOR_FIX_VERBOSE !== 'off'`.
|
|
40
|
-
|
|
41
|
-
### `parseOrchestratorArgs`
|
|
15
|
+
## Поведінка
|
|
42
16
|
|
|
43
|
-
|
|
17
|
+
buildLadder будує послідовність тирів ескалації для вирішення порушень.
|
|
18
|
+
escalateRule проходить по послідовності тирів, намагаючись вирішити одне правило до першого успішного перевірки.
|
|
19
|
+
parseOrchestratorArgs парсить аргументи командного рядка для визначення максимального бюджету LLM та фільтра правил.
|
|
20
|
+
runOrchestratorCli виконує повний процес виправлення правил, використовуючи послідовність тирів та бюджет LLM.
|
|
44
21
|
|
|
45
|
-
|
|
22
|
+
## Публічний API
|
|
46
23
|
|
|
47
|
-
|
|
24
|
+
buildLadder — Створює послідовність моделей для ескалації, виходячи з доступних рівнів. Недоступні рівні ігноруються.
|
|
48
25
|
|
|
49
|
-
|
|
50
|
-
2. `runT0Step` — детермінований фікс без LLM; якщо після нього чисто, exit 0.
|
|
51
|
-
3. Для кожного правила, що лишилося — `escalateRule` з відстеженням `avgBudget`.
|
|
52
|
-
4. Фінальна перевірка → exit 0 якщо чисто, exit 1 якщо є невирішені.
|
|
26
|
+
escalateRule — Виконує один етап перевірки за драбиною ескалації. Для кожного кроку викликається модель, відбувається повторна перевірка правила, і результат фіксується в лозі. Процес може зупинитися достроково при певних умовах або після вичерпання ліміту.
|
|
53
27
|
|
|
54
|
-
|
|
28
|
+
parseOrchestratorArgs — Витягує максимальне значення для середнього бюджету з аргументів командного рядка та збирає список фільтрів правил.
|
|
55
29
|
|
|
56
|
-
|
|
57
|
-
- `escalateRule(rule, cwd, deps)` — проводить одне правило через драбину; `deps` дозволяє ін'єкцію worker/check/clock для тестів; повертає `{ resolved, avgUsed }`.
|
|
58
|
-
- `parseOrchestratorArgs(args)` — повертає `{ maxAvg, ruleFilter }`.
|
|
59
|
-
- `runOrchestratorCli(args, cwd)` — CLI-точка входу; повертає `Promise<0|1>`.
|
|
30
|
+
runOrchestratorCli — Запускає основний процес оркестрації, обробляючи аргументи та керуючи виконанням правил.
|
|
60
31
|
|
|
61
32
|
## Гарантії поведінки
|
|
62
33
|
|
|
63
|
-
-
|
|
64
|
-
- Не кидає винятків назовні: помилки LLM перехоплює worker і повертає як `res.error`.
|
|
34
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
|
|
4
4
|
* `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
|
|
5
5
|
*/
|
|
6
|
+
import { execSync } from 'node:child_process'
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
-
import { dirname, join } from 'node:path'
|
|
8
|
+
import { basename, dirname, join } from 'node:path'
|
|
8
9
|
|
|
9
10
|
const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
|
|
10
11
|
|
|
@@ -39,8 +40,30 @@ export function parseChangesResponse(text) {
|
|
|
39
40
|
return null
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Шукає файл за basename у дереві проєкту (fallback коли прямий шлях не існує).
|
|
45
|
+
* Повертає відносний шлях якщо знайдено рівно один матч, інакше `null` (ambiguous/not found).
|
|
46
|
+
* @param {string} name basename файлу
|
|
47
|
+
* @param {string} projectRoot абсолютний корінь
|
|
48
|
+
* @returns {string|null} відносний шлях або null
|
|
49
|
+
*/
|
|
50
|
+
function findByBasename(name, projectRoot) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = execSync(
|
|
53
|
+
`find . -maxdepth 7 -name '${name.replace(/'/g, "'\\''")}' -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.worktrees/*'`,
|
|
54
|
+
{ cwd: projectRoot, encoding: 'utf8', timeout: 3000 }
|
|
55
|
+
).trim()
|
|
56
|
+
const hits = raw.split('\n').filter(Boolean)
|
|
57
|
+
return hits.length === 1 ? hits[0].replace(/^\.\//, '') : null
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/**
|
|
43
64
|
* Читає існуючі файли за відносними шляхами у форму `{path, content}` (для prompt).
|
|
65
|
+
* Якщо файл не знайдений за прямим шляхом — намагається знайти за basename через `find`.
|
|
66
|
+
* Повертає resolved path (може відрізнятись від вхідного коли `find` знайшов реальне місце).
|
|
44
67
|
* @param {string[]} filePaths відносні шляхи від кореня
|
|
45
68
|
* @param {string} projectRoot абсолютний корінь
|
|
46
69
|
* @returns {Array<{path:string, content:string}>} наявні файли з вмістом
|
|
@@ -48,10 +71,18 @@ export function parseChangesResponse(text) {
|
|
|
48
71
|
export function readFilesForFix(filePaths, projectRoot) {
|
|
49
72
|
return filePaths
|
|
50
73
|
.map(p => {
|
|
51
|
-
|
|
74
|
+
let abs = join(projectRoot, p)
|
|
75
|
+
let resolvedPath = p
|
|
76
|
+
if (!existsSync(abs)) {
|
|
77
|
+
const found = findByBasename(basename(p), projectRoot)
|
|
78
|
+
if (found) {
|
|
79
|
+
resolvedPath = found
|
|
80
|
+
abs = join(projectRoot, found)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
52
83
|
if (!existsSync(abs)) return null
|
|
53
84
|
try {
|
|
54
|
-
return { path:
|
|
85
|
+
return { path: resolvedPath, content: readFileSync(abs, 'utf8') }
|
|
55
86
|
} catch {
|
|
56
87
|
return null
|
|
57
88
|
}
|
|
@@ -18,35 +18,44 @@ const DEFAULT_THINKING_BUDGET = Number(env.N_CURSOR_OMLX_THINKING_BUDGET ?? 4096
|
|
|
18
18
|
|
|
19
19
|
const API_KEY_RE = /api key/i
|
|
20
20
|
|
|
21
|
+
const FILE_EXTS = 'json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py'
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Витягує відносні шляхи файлів із violation output.
|
|
23
25
|
* Розуміє workspace-prefix: `[npm] skills/foo.mjs` → `npm/skills/foo.mjs`.
|
|
26
|
+
* Спочатку явно парсить рядки ❌ (найвищий сигнал — файл потребує фіксу),
|
|
27
|
+
* потім підхоплює решту файлів generic-regex (контекст для читання).
|
|
24
28
|
* @param {string} output violation output з fix check
|
|
25
29
|
* @returns {string[]} унікальні відносні шляхи (від кореня проєкту)
|
|
26
30
|
*/
|
|
27
|
-
function extractFilePaths(output) {
|
|
31
|
+
export function extractFilePaths(output) {
|
|
28
32
|
const seen = new Set()
|
|
29
33
|
const results = []
|
|
30
|
-
|
|
31
|
-
// Патерн з workspace: [npm] skills/foo.mjs або [demo] src/bar.ts
|
|
32
|
-
const wsRe = /\[([\w-]+)\]\s+([\w./][\w./-]*\.(?:json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py))(?::\d+)?/gm
|
|
33
|
-
for (const m of output.matchAll(wsRe)) {
|
|
34
|
-
const p = `${m[1]}/${m[2]}`
|
|
34
|
+
const add = p => {
|
|
35
35
|
if (!seen.has(p)) {
|
|
36
36
|
seen.add(p)
|
|
37
37
|
results.push(p)
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
// 1. Явні рядки ❌ — найвищий сигнал: саме ці файли потребують фіксу.
|
|
42
|
+
// Формати: `❌ [ws] path/file.ext:line — msg` та `❌ path/file.ext: msg`
|
|
43
|
+
// Роздільник після шляху: `:` (з пробілом або цифрою), `—` (em-dash), або кінець рядка.
|
|
44
|
+
const failSep = `(?::\\d+)?(?::\\s|[\\s—]|$)`
|
|
45
|
+
const failWsRe = new RegExp(`^\\s*❌\\s+\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
46
|
+
for (const m of output.matchAll(failWsRe)) add(`${m[1]}/${m[2]}`)
|
|
47
|
+
|
|
48
|
+
const failRe = new RegExp(`^\\s*❌\\s+(\\.?[\\w][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
|
|
49
|
+
for (const m of output.matchAll(failRe)) add(m[1])
|
|
50
|
+
|
|
51
|
+
// 2. Generic-regex: підхоплює файли з ✅-рядків та описів (контекст для читання).
|
|
52
|
+
// Workspace: [npm] skills/foo.mjs
|
|
53
|
+
const wsRe = new RegExp(`\\[([\\w-]+)\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
54
|
+
for (const m of output.matchAll(wsRe)) add(`${m[1]}/${m[2]}`)
|
|
55
|
+
|
|
56
|
+
// Без workspace: path/to/file.ext або ./file.ext
|
|
57
|
+
const re = new RegExp(`(?:^|\\s)(\\.?\\w[\\w./-]*\\.(?:${FILE_EXTS}))(?::\\d+)?`, 'gm')
|
|
58
|
+
for (const m of output.matchAll(re)) add(m[1])
|
|
50
59
|
|
|
51
60
|
return results
|
|
52
61
|
}
|
|
@@ -153,10 +153,6 @@ export async function escalateRule(rule, cwd, deps) {
|
|
|
153
153
|
log(` ⚡ ${rung.tier} (${rung.model || 'pi'}): ${rule.ruleId}${hint}`)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// DEBUG
|
|
157
|
-
console.error(
|
|
158
|
-
`[DBG] verbose check: VERBOSE=${env.N_CURSOR_FIX_VERBOSE} promptSummary=${JSON.stringify(res.promptSummary)} reasoning=${res.reasoning}`
|
|
159
|
-
)
|
|
160
156
|
if (env.N_CURSOR_FIX_VERBOSE !== 'off' && res.promptSummary) {
|
|
161
157
|
printVerboseBlock(rule.ruleId, res.promptSummary, res.reasoning ?? null, res.reasoningSource ?? null)
|
|
162
158
|
}
|
package/scripts/lib/fix/t0.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** @see ./docs/t0.md */
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { appendFileSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
|
|
6
6
|
import { runConformanceCheck } from './run-conformance-check.mjs'
|
|
@@ -116,6 +116,121 @@ const PATTERNS = [
|
|
|
116
116
|
}
|
|
117
117
|
return { ok: true, action: `створено change-файл (${CHANGE_BUMP}/${CHANGE_SECTION}): ${created.join(', ')}` }
|
|
118
118
|
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ── bun-bunfig-create ────────────────────────────────────────────────────────
|
|
122
|
+
// Violation: «Відсутній bunfig.toml — створи з [install] linker = "hoisted"»
|
|
123
|
+
// Fix: створити bunfig.toml з канонічним вмістом (bun.mdc)
|
|
124
|
+
{
|
|
125
|
+
id: 'bun-bunfig-create',
|
|
126
|
+
test: out => /Відсутній bunfig\.toml/.test(out),
|
|
127
|
+
apply: (_out, cwd) => {
|
|
128
|
+
const target = join(cwd, 'bunfig.toml')
|
|
129
|
+
if (existsSync(target)) return { ok: false, action: 'bunfig.toml вже існує' }
|
|
130
|
+
writeFileSync(target, '[install]\nlinker = "hoisted"\n', 'utf8')
|
|
131
|
+
return { ok: true, action: 'створено bunfig.toml' }
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── bun-yarn-dir-remove ──────────────────────────────────────────────────────
|
|
136
|
+
// Violation: «Знайдено директорію .yarn — видали її»
|
|
137
|
+
// Fix: рекурсивно видалити .yarn/
|
|
138
|
+
{
|
|
139
|
+
id: 'bun-yarn-dir-remove',
|
|
140
|
+
test: out => /Знайдено директорію \.yarn/.test(out),
|
|
141
|
+
apply: (_out, cwd) => {
|
|
142
|
+
const target = join(cwd, '.yarn')
|
|
143
|
+
if (!existsSync(target)) return { ok: false, action: '.yarn не знайдено' }
|
|
144
|
+
rmSync(target, { recursive: true, force: true })
|
|
145
|
+
return { ok: true, action: 'видалено .yarn/' }
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
// ── style-stylelintignore-create ─────────────────────────────────────────────
|
|
150
|
+
// Violation: «.stylelintignore не існує — створи з вмістом: dist/»
|
|
151
|
+
// Fix: створити .stylelintignore з рядком dist/
|
|
152
|
+
{
|
|
153
|
+
id: 'style-stylelintignore-create',
|
|
154
|
+
test: out => /\.stylelintignore не існує/.test(out),
|
|
155
|
+
apply: (_out, cwd) => {
|
|
156
|
+
writeFileSync(join(cwd, '.stylelintignore'), 'dist/\n', 'utf8')
|
|
157
|
+
return { ok: true, action: 'створено .stylelintignore' }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// ── style-stylelintignore-dist-add ───────────────────────────────────────────
|
|
162
|
+
// Violation: «.stylelintignore не містить рядка dist/»
|
|
163
|
+
// Fix: дописати dist/ до існуючого .stylelintignore
|
|
164
|
+
{
|
|
165
|
+
id: 'style-stylelintignore-dist-add',
|
|
166
|
+
test: out => /\.stylelintignore не містить рядка dist\//.test(out),
|
|
167
|
+
apply: (_out, cwd) => {
|
|
168
|
+
const target = join(cwd, '.stylelintignore')
|
|
169
|
+
appendFileSync(target, '\ndist/\n', 'utf8')
|
|
170
|
+
return { ok: true, action: 'додано dist/ до .stylelintignore' }
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// ── style-pkg-stylelint-add ──────────────────────────────────────────────────
|
|
175
|
+
// Violation: «Немає конфігу stylelint — додай "stylelint": {...} до package.json»
|
|
176
|
+
// Fix: додати поле stylelint до кореневого package.json
|
|
177
|
+
{
|
|
178
|
+
id: 'style-pkg-stylelint-add',
|
|
179
|
+
test: out => /Немає конфігу stylelint/.test(out),
|
|
180
|
+
apply: (_out, cwd) => {
|
|
181
|
+
const pkgPath = join(cwd, 'package.json')
|
|
182
|
+
if (!existsSync(pkgPath)) return { ok: false, action: 'package.json не знайдено' }
|
|
183
|
+
let pkg
|
|
184
|
+
try {
|
|
185
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
186
|
+
} catch {
|
|
187
|
+
return { ok: false, action: 'package.json: невалідний JSON' }
|
|
188
|
+
}
|
|
189
|
+
if (pkg.stylelint) return { ok: false, action: 'stylelint вже є в package.json' }
|
|
190
|
+
pkg.stylelint = { extends: '@nitra/stylelint-config' }
|
|
191
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8')
|
|
192
|
+
return { ok: true, action: 'додано stylelint до package.json' }
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// ── js-run-jsconfig-create ───────────────────────────────────────────────────
|
|
197
|
+
// Violation: «[packages/api] є каталог src/, але немає jsconfig.json»
|
|
198
|
+
// Fix: для кожного воркспейсу з violation створити канонічний jsconfig.json
|
|
199
|
+
// (NodeNext + include: src/**/*; шаблон: js-run/policy/jsconfig/template/)
|
|
200
|
+
{
|
|
201
|
+
id: 'js-run-jsconfig-create',
|
|
202
|
+
test: out => /є каталог src\/, але немає jsconfig\.json/.test(out),
|
|
203
|
+
apply: (out, cwd) => {
|
|
204
|
+
const RE = /\[([^\]]+)\] є каталог src\/, але немає jsconfig\.json/gu
|
|
205
|
+
const matches = [...out.matchAll(RE)]
|
|
206
|
+
if (matches.length === 0) return { ok: false, action: 'no match' }
|
|
207
|
+
const canonical =
|
|
208
|
+
JSON.stringify(
|
|
209
|
+
{
|
|
210
|
+
compilerOptions: {
|
|
211
|
+
lib: ['esnext'],
|
|
212
|
+
module: 'NodeNext',
|
|
213
|
+
moduleResolution: 'NodeNext',
|
|
214
|
+
target: 'esnext',
|
|
215
|
+
checkJs: false
|
|
216
|
+
},
|
|
217
|
+
include: ['src/**/*']
|
|
218
|
+
},
|
|
219
|
+
null,
|
|
220
|
+
2
|
|
221
|
+
) + '\n'
|
|
222
|
+
const created = []
|
|
223
|
+
for (const m of matches) {
|
|
224
|
+
const ws = m[1]
|
|
225
|
+
const target = join(cwd, ws, 'jsconfig.json')
|
|
226
|
+
if (!existsSync(target)) {
|
|
227
|
+
writeFileSync(target, canonical, 'utf8')
|
|
228
|
+
created.push(ws)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (created.length === 0) return { ok: false, action: 'jsconfig.json вже існує в усіх воркспейсах' }
|
|
232
|
+
return { ok: true, action: `створено jsconfig.json: ${created.join(', ')}` }
|
|
233
|
+
}
|
|
119
234
|
}
|
|
120
235
|
]
|
|
121
236
|
|