@nitra/cursor
Advanced tools
+1
-1
| { | ||
| "name": "@nitra/cursor", | ||
| "version": "5.4.0", | ||
| "version": "6.0.0", | ||
| "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -94,2 +94,15 @@ /** | ||
| /** | ||
| * Рядок таймінгу одного файлу: загальний час, час у LLM (і кількість викликів) | ||
| * та залишок — оркестрація (екстракт фактів, скоринг, парсинг, IO). Дає зрозуміти, | ||
| * скільки коштує сама модель проти JS-оркестрації. | ||
| * @param {{ ms: number, llmMs?: number, llmCalls?: number }} r результат generateDoc | ||
| * @returns {string} напр. `12.3s (llm 11.8s/7 calls, orch 0.5s)` | ||
| */ | ||
| function fmtTiming(r) { | ||
| const s = ms => `${(ms / 1000).toFixed(1)}s` | ||
| const llmMs = r.llmMs ?? 0 | ||
| return `${s(r.ms)} (llm ${s(llmMs)}/${r.llmCalls ?? 0} calls, orch ${s(r.ms - llmMs)})` | ||
| } | ||
| /** | ||
| * Генерує й штампує доку для одного файлу, оновлюючи лічильники й прогрес. | ||
@@ -118,5 +131,5 @@ * @param {object} file елемент scanForDocFiles | ||
| stats.degraded++ | ||
| process.stdout.write(`⚠ degraded score=${result.score} crc=${crc}\n`) | ||
| process.stdout.write(`⚠ degraded score=${result.score} crc=${crc} ${fmtTiming(result)}\n`) | ||
| } else { | ||
| process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc}\n`) | ||
| process.stdout.write(`✓ score=${result.score ?? '—'} crc=${crc} ${fmtTiming(result)}\n`) | ||
| } | ||
@@ -142,3 +155,3 @@ } catch (error) { | ||
| if (stats.degraded > 0) { | ||
| console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor doc-files gen --retry-degraded`) | ||
| console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor fix-doc-files --retry-degraded`) | ||
| } | ||
@@ -170,3 +183,3 @@ } | ||
| if (problem) { | ||
| console.error(`✗ doc-files gen: ${problem}`) | ||
| console.error(`✗ fix-doc-files: ${problem}`) | ||
| return 1 | ||
@@ -208,3 +221,3 @@ } | ||
| } | ||
| console.log(`✓ doc-files stamp: оновлено frontmatter у ${stamped} доці(ах).`) | ||
| console.log(`✓ fix-doc-files --stamp: оновлено frontmatter у ${stamped} доці(ах).`) | ||
| return 0 | ||
@@ -211,0 +224,0 @@ } |
@@ -7,3 +7,3 @@ /** @see ./docs/docgen-gen.md */ | ||
| import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs' | ||
| import { callLlm } from '../../../lib/llm.mjs' | ||
| import { callLlm as callLlmRaw } from '../../../lib/llm.mjs' | ||
| import { isRunAsCli } from '../../../scripts/cli-entry.mjs' | ||
@@ -23,2 +23,22 @@ import { docPathForSource } from './docgen-scan.mjs' | ||
| /** Облік LLM-викликів і часу в них у межах однієї генерації (скидається на старті generateDoc). */ | ||
| let llmMeter = { calls: 0, ms: 0 } | ||
| /** | ||
| * Обгортка callLlm з обліком: лічить кількість викликів і сумарний час у них. | ||
| * callLlm синхронний (spawnSync/curl), генерація одного файлу послідовна — лічильник без гонок. | ||
| * Усі виклики `callLlm(...)` у цьому модулі йдуть через неї автоматично (імпорт як callLlmRaw). | ||
| * @param {...any} args ті самі аргументи, що й у callLlm з lib/llm.mjs | ||
| * @returns {string} відповідь моделі | ||
| */ | ||
| function callLlm(...args) { | ||
| const started = Date.now() | ||
| try { | ||
| return callLlmRaw(...args) | ||
| } finally { | ||
| llmMeter.calls += 1 | ||
| llmMeter.ms += Date.now() - started | ||
| } | ||
| } | ||
| const FENCE_OPEN_RE = /^```[a-z]*\n?/ | ||
@@ -365,3 +385,3 @@ const FENCE_CLOSE_RE = /\n?```\s*$/ | ||
| * @param {{ model?: string, threshold?: number, existingMd?: string|null }} [opts] model-id, поріг degraded, наявна дока (для збереження захищеної секції) | ||
| * @returns {{ md: string, ms: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації | ||
| * @returns {{ md: string, ms: number, llmMs: number, llmCalls: number, score: number|null, issues: string[], degraded: boolean, model: string }} документ і метадані генерації (ms — увесь файл; llmMs/llmCalls — лише LLM; решта ms — оркестрація) | ||
| */ | ||
@@ -372,2 +392,3 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUALITY_THRESHOLD, existingMd = null } = {}) { | ||
| const t0 = Date.now() | ||
| llmMeter = { calls: 0, ms: 0 } | ||
@@ -383,3 +404,12 @@ // Варіант B: захищена секція «Призначення» з наявної доки — зберегти й подати як контекст | ||
| if (facts.unsupported) { | ||
| return { ...r, ms: Date.now() - t0, score: null, issues: [], degraded: false, model } | ||
| return { | ||
| ...r, | ||
| ms: Date.now() - t0, | ||
| llmMs: llmMeter.ms, | ||
| llmCalls: llmMeter.calls, | ||
| score: null, | ||
| issues: [], | ||
| degraded: false, | ||
| model | ||
| } | ||
| } | ||
@@ -407,3 +437,12 @@ | ||
| return { ...r, ms: Date.now() - t0, score, issues, degraded: score < threshold, model } | ||
| return { | ||
| ...r, | ||
| ms: Date.now() - t0, | ||
| llmMs: llmMeter.ms, | ||
| llmCalls: llmMeter.calls, | ||
| score, | ||
| issues, | ||
| degraded: score < threshold, | ||
| model | ||
| } | ||
| } | ||
@@ -425,4 +464,6 @@ | ||
| const issuesTxt = r.issues?.length ? ` issues=${r.issues.join(',')}` : '' | ||
| process.stderr.write(`[local ${r.model}] ${r.ms}ms / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n`) | ||
| process.stderr.write( | ||
| `[local ${r.model}] ${r.ms}ms (llm ${r.llmMs}ms/${r.llmCalls} calls, orch ${r.ms - r.llmMs}ms) / score=${r.score}${r.degraded ? ' DEGRADED' : ''}${issuesTxt}\n` | ||
| ) | ||
| process.stdout.write(r.md) | ||
| } |
@@ -232,3 +232,3 @@ /** @see ./docs/docgen-scan.md */ | ||
| console.log( | ||
| `⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ перегенеруй: npx @nitra/cursor doc-files gen --retry-degraded` | ||
| `⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ перегенеруй: npx @nitra/cursor fix-doc-files --retry-degraded` | ||
| ) | ||
@@ -289,3 +289,3 @@ return 0 | ||
| console.error( | ||
| `⚠ doc-files: застарілих док ${stale.length} (> ${gateMax}) — гейт не блокує. Запусти масовий прогін:\n npx @nitra/cursor doc-files gen` | ||
| `⚠ doc-files: застарілих док ${stale.length} (> ${gateMax}) — гейт не блокує. Запусти масовий прогін:\n npx @nitra/cursor fix-doc-files` | ||
| ) | ||
@@ -292,0 +292,0 @@ return 0 |
| --- | ||
| docgen: | ||
| source: npm/rules/doc-files/js/docgen-files-batch.mjs | ||
| crc: 5c9b8d72 | ||
| crc: 6f01f8b9 | ||
| score: 95 | ||
@@ -6,0 +6,0 @@ --- |
| --- | ||
| docgen: | ||
| source: npm/rules/doc-files/js/docgen-gen.mjs | ||
| crc: e2af04d6 | ||
| crc: 70215974 | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
| --- | ||
| docgen: | ||
| source: npm/rules/doc-files/js/docgen-scan.mjs | ||
| crc: 46f11827 | ||
| crc: dcc90d44 | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
@@ -1,1 +0,1 @@ | ||
| { "auto": "завжди", "lint": "quick" } | ||
| { "auto": "завжди", "lint": "per-file" } |
@@ -1,1 +0,1 @@ | ||
| { "auto": { "glob": ".github/workflows/**" }, "lint": "ci" } | ||
| { "auto": { "glob": ".github/workflows/**" }, "lint": "full" } |
@@ -1,1 +0,1 @@ | ||
| { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "ci" } | ||
| { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "full" } |
| --- | ||
| docgen: | ||
| source: npm/rules/js-lint/js/lint.mjs | ||
| crc: c90c15eb | ||
| crc: 1f38613e | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
@@ -57,10 +57,11 @@ /** @see ./docs/lint.md */ | ||
| /** | ||
| * Full-режим (ci): лінт усього проєкту зі стрімінгом і fail-fast (без класифікації). | ||
| * Full-режим (--full): лінт усього проєкту зі стрімінгом і fail-fast (без класифікації). | ||
| * @param {string} cwd корінь | ||
| * @param {boolean} readOnly true → без `--fix` (детект, нуль мутацій — CI) | ||
| * @returns {number} exit code | ||
| */ | ||
| function lintFullProject(cwd) { | ||
| const ox = runInherit(['oxlint', '--fix'], cwd) | ||
| function lintFullProject(cwd, readOnly) { | ||
| const ox = runInherit(readOnly ? ['oxlint'] : ['oxlint', '--fix'], cwd) | ||
| if (ox !== 0) return ox | ||
| return runInherit(['eslint', '--fix', '.'], cwd) | ||
| return runInherit(readOnly ? ['eslint', '.'] : ['eslint', '--fix', '.'], cwd) | ||
| } | ||
@@ -73,8 +74,12 @@ | ||
| * @param {string} cwd корінь | ||
| * @param {boolean} readOnly true → пропустити фікс-пас (детект, нуль мутацій) | ||
| * @returns {number} exit code (0 — чисто; 1 — лишились findings) | ||
| */ | ||
| function lintChangedClassified(js, cwd) { | ||
| function lintChangedClassified(js, cwd, readOnly) { | ||
| // Фікс-пас обох інструментів (послідовно; обидва — щоб репорт показав повну картину). | ||
| runFix(['oxlint', '--fix', ...js], cwd) | ||
| runFix(['eslint', '--fix', ...js], cwd) | ||
| // У read-only пропускаємо — лише детект без мутацій (CI / pre-commit). | ||
| if (!readOnly) { | ||
| runFix(['oxlint', '--fix', ...js], cwd) | ||
| runFix(['eslint', '--fix', ...js], cwd) | ||
| } | ||
@@ -104,14 +109,16 @@ // Репорт-пас по ФІНАЛЬНОМУ (пост-фікс) файлу — рядки findings і diff узгоджені. | ||
| /** | ||
| * Запускає oxlint+eslint з автофіксом. | ||
| * @param {string[] | undefined} files quick: лише ці файли; undefined: весь проєкт | ||
| * Запускає oxlint+eslint. За замовчуванням — з автофіксом; `opts.readOnly` — лише детект. | ||
| * @param {string[] | undefined} files per-file: лише ці файли; undefined: весь проєкт (--full) | ||
| * @param {string} [cwd] корінь репо | ||
| * @param {{ readOnly?: boolean }} [opts] readOnly → без `--fix` (нуль мутацій) | ||
| * @returns {Promise<number>} 0 — OK, ≠0 — порушення | ||
| */ | ||
| export function lint(files, cwd = process.cwd()) { | ||
| export function lint(files, cwd = process.cwd(), opts = {}) { | ||
| const readOnly = opts.readOnly === true | ||
| if (files === undefined) { | ||
| return Promise.resolve(lintFullProject(cwd)) | ||
| return Promise.resolve(lintFullProject(cwd, readOnly)) | ||
| } | ||
| const js = filterJsFiles(files) | ||
| if (js.length === 0) return Promise.resolve(0) | ||
| return Promise.resolve(lintChangedClassified(js, cwd)) | ||
| return Promise.resolve(lintChangedClassified(js, cwd, readOnly)) | ||
| } |
@@ -1,1 +0,1 @@ | ||
| { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "quick" } | ||
| { "auto": { "glob": ["**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] }, "lint": "per-file" } |
| --- | ||
| docgen: | ||
| source: npm/rules/npm-module/js/rule_meta.mjs | ||
| crc: fa29bd00 | ||
| crc: 8262678c | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
@@ -6,3 +6,3 @@ /** @see ./docs/rule_meta.md */ | ||
| import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs' | ||
| import { parseRuleAutoSpec, parseRuleLintPhase, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs' | ||
| import { parseRuleAutoSpec, parseRuleLintSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs' | ||
| import { RULE_PREDICATES } from '../../../scripts/lib/rule-predicates.mjs' | ||
@@ -41,4 +41,4 @@ | ||
| if (raw.lint === undefined) return true | ||
| if (parseRuleLintPhase(raw.lint) === null) { | ||
| reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "quick"|"ci")`) | ||
| if (parseRuleLintSpec(raw.lint) === null) { | ||
| reporter.fail(`rules/${id}: meta.json.lint нерозпізнане (очікується "per-file"|"full")`) | ||
| return false | ||
@@ -45,0 +45,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| { "auto": { "glob": "**/*.rego" }, "lint": "ci" } | ||
| { "auto": { "glob": "**/*.rego" }, "lint": "full" } |
@@ -1,1 +0,1 @@ | ||
| { "auto": "завжди", "lint": "ci" } | ||
| { "auto": "завжди", "lint": "per-file" } |
| --- | ||
| docgen: | ||
| source: npm/rules/style-lint/js/lint.mjs | ||
| crc: 94e067b3 | ||
| crc: 2013a66b | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
@@ -15,8 +15,9 @@ /** @see ./docs/lint.md */ | ||
| /** | ||
| * @param {string[] | undefined} files quick: ці файли; undefined: весь проєкт | ||
| * @param {string[] | undefined} files per-file: ці файли; undefined: весь проєкт (--full) | ||
| * @param {string} [cwd] корінь | ||
| * @param {{ readOnly?: boolean }} [opts] readOnly → без `--fix` (детект, нуль мутацій) | ||
| * @returns {Promise<number>} exit code | ||
| */ | ||
| export function lint(files, cwd = process.cwd()) { | ||
| const args = ['stylelint', '--fix'] | ||
| export function lint(files, cwd = process.cwd(), opts = {}) { | ||
| const args = opts.readOnly === true ? ['stylelint'] : ['stylelint', '--fix'] | ||
| if (files === undefined) { | ||
@@ -23,0 +24,0 @@ args.push('**/*.{css,scss,vue}') |
@@ -1,1 +0,1 @@ | ||
| { "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "quick" } | ||
| { "auto": { "glob": ["**/*.css", "**/*.vue"] }, "lint": "per-file" } |
| --- | ||
| docgen: | ||
| source: npm/rules/text/js/lint.mjs | ||
| crc: 4ee054a0 | ||
| crc: 49aab7ce | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
| /** | ||
| * Ci-крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується). | ||
| * Крок text: делегує у наявний CLI правила (per-file режиму немає — `files` ігнорується). | ||
| */ | ||
@@ -8,6 +8,8 @@ import { runLintTextCli } from '../lint/lint.mjs' | ||
| * @param {string[] | undefined} _files ігнорується (whole-repo аналіз) | ||
| * @param {string} [_cwd] корінь (ігнорується — CLI працює від process.cwd()) | ||
| * @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій) | ||
| * @returns {Promise<number>} exit code | ||
| */ | ||
| export function lint(_files) { | ||
| return runLintTextCli() | ||
| export function lint(_files, _cwd, opts = {}) { | ||
| return runLintTextCli({ readOnly: opts.readOnly === true }) | ||
| } |
| --- | ||
| docgen: | ||
| source: npm/rules/text/lint/lint.mjs | ||
| crc: 05f3f108 | ||
| crc: bdaef0f8 | ||
| --- | ||
@@ -6,0 +6,0 @@ |
| --- | ||
| docgen: | ||
| source: npm/rules/text/lint/run-dotenv-linter.mjs | ||
| crc: 8bb94af4 | ||
| crc: 4719ac66 | ||
| --- | ||
@@ -6,0 +6,0 @@ |
| --- | ||
| docgen: | ||
| source: npm/rules/text/lint/run-shellcheck.mjs | ||
| crc: e6fa8c23 | ||
| crc: 6b2daaa8 | ||
| --- | ||
@@ -6,0 +6,0 @@ |
@@ -98,5 +98,6 @@ /** | ||
| * Внутрішні кроки `lint-text` без локу. | ||
| * @param {boolean} [readOnly] true → лише детект без авто-фіксу (нуль мутацій — CI/pre-commit) | ||
| * @returns {number} 0 — все OK, інакше — код першого кроку, що впав | ||
| */ | ||
| function runLintTextSteps() { | ||
| function runLintTextSteps(readOnly = false) { | ||
| // Auto-install: throws on failure → propagates as exit 1 from runStandardLint | ||
@@ -106,4 +107,4 @@ ensureTool('shellcheck') | ||
| // patch is hint-only (system tool) | ||
| if (!preflight(PATCH_PREFLIGHT)) return 1 | ||
| // patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight. | ||
| if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1 | ||
@@ -113,11 +114,12 @@ const cspellCode = runLintStep('cspell', 'npx', ['cspell', '.']) | ||
| console.log('\n▶ shellcheck (авто-фікс + фінальна перевірка *.sh)') | ||
| const shellcheckCode = runShellcheckText() | ||
| console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`) | ||
| const shellcheckCode = runShellcheckText(process.cwd(), readOnly) | ||
| if (shellcheckCode !== 0) return shellcheckCode | ||
| console.log('\n▶ dotenv-linter (авто-фікс + фінальна перевірка .env*)') | ||
| const dotenvCode = runDotenvLinter() | ||
| console.log(`\n▶ dotenv-linter (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} .env*)`) | ||
| const dotenvCode = runDotenvLinter(process.cwd(), readOnly) | ||
| if (dotenvCode !== 0) return dotenvCode | ||
| const markdownlintCode = runLintStep('markdownlint', 'bunx', ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc']) | ||
| const mdArgs = readOnly ? ['markdownlint-cli2', '**/*.md', '**/*.mdc'] : ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc'] | ||
| const markdownlintCode = runLintStep('markdownlint', 'bunx', mdArgs) | ||
| if (markdownlintCode !== 0) return markdownlintCode | ||
@@ -131,4 +133,6 @@ | ||
| * Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева. | ||
| * @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу | ||
| * @returns {Promise<number>} код виходу | ||
| */ | ||
| export const runLintTextCli = () => runStandardLint(import.meta.dirname, () => runLintTextSteps()) | ||
| export const runLintTextCli = (opts = {}) => | ||
| runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true)) |
@@ -55,5 +55,6 @@ /** | ||
| * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`) | ||
| * @param {boolean} [readOnly] true → пропустити авто-фікс (`fix`), лише `check` (нуль мутацій) | ||
| * @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення | ||
| */ | ||
| export function runDotenvLinter(cwd = process.cwd()) { | ||
| export function runDotenvLinter(cwd = process.cwd(), readOnly = false) { | ||
| const root = resolve(cwd) | ||
@@ -67,11 +68,13 @@ const bin = resolveCmd('dotenv-linter') | ||
| const exclude = buildExcludeArgs() | ||
| const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], { | ||
| cwd: root, | ||
| encoding: 'utf8', | ||
| env: process.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'] | ||
| }) | ||
| if (fixRun.error) { | ||
| process.stderr.write(`${fixRun.error.message}\n`) | ||
| return 1 | ||
| if (!readOnly) { | ||
| const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], { | ||
| cwd: root, | ||
| encoding: 'utf8', | ||
| env: process.env, | ||
| stdio: ['ignore', 'pipe', 'pipe'] | ||
| }) | ||
| if (fixRun.error) { | ||
| process.stderr.write(`${fixRun.error.message}\n`) | ||
| return 1 | ||
| } | ||
| } | ||
@@ -78,0 +81,0 @@ |
@@ -99,5 +99,6 @@ /** | ||
| * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`) | ||
| * @param {boolean} [readOnly] true → пропустити авто-фікс (diff+patch), лише фінальна перевірка | ||
| * @returns {number} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck | ||
| */ | ||
| export function runShellcheckText(cwd = process.cwd()) { | ||
| export function runShellcheckText(cwd = process.cwd(), readOnly = false) { | ||
| const root = resolve(cwd) | ||
@@ -109,4 +110,5 @@ const shellcheck = resolveCmd('shellcheck') | ||
| } | ||
| const patchBin = resolveCmd('patch') | ||
| if (!patchBin) { | ||
| // patch потрібен лише для авто-фіксу (diff+patch); у read-only його відсутність не блокує детект. | ||
| const patchBin = readOnly ? null : resolveCmd('patch') | ||
| if (!readOnly && !patchBin) { | ||
| printPatchInstallHints() | ||
@@ -121,5 +123,7 @@ return 1 | ||
| for (const rel of files) { | ||
| const fixCode = autofixOneFile(shellcheck, patchBin, root, rel) | ||
| if (fixCode !== 0) return fixCode | ||
| if (!readOnly) { | ||
| for (const rel of files) { | ||
| const fixCode = autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel) | ||
| if (fixCode !== 0) return fixCode | ||
| } | ||
| } | ||
@@ -126,0 +130,0 @@ |
@@ -1,1 +0,1 @@ | ||
| { "auto": "завжди", "lint": "ci" } | ||
| { "auto": "завжди", "lint": "per-file" } |
| --- | ||
| docgen: | ||
| source: npm/scripts/lint-cli.mjs | ||
| crc: d4a7562d | ||
| crc: 9e0a12b9 | ||
| score: 100 | ||
@@ -6,0 +6,0 @@ --- |
| --- | ||
| docgen: | ||
| source: npm/scripts/lib/rule-meta.mjs | ||
| crc: 4475d5ff | ||
| crc: fa5ca866 | ||
| --- | ||
@@ -6,0 +6,0 @@ |
@@ -51,12 +51,16 @@ /** | ||
| /** Допустимі фази lint. */ | ||
| const LINT_PHASES = new Set(['quick', 'ci']) | ||
| /** Допустимі значення `meta.json.lint` (вісь scope: чи детектор дробиться на changed-set). */ | ||
| const LINT_SCOPES = new Set(['per-file', 'full']) | ||
| /** | ||
| * Нормалізує значення `meta.json.lint` у фазу lint. | ||
| * Нормалізує значення `meta.json.lint` у scope детектора. | ||
| * - `"per-file"` — детектор декомпозується на змінені файли (дельта vs origin); | ||
| * - `"full"` — нероздільно крос-файловий (лише `--full` / CI). | ||
| * Об'єктна форма `{scope, ci}` скасована: CI=`--read-only --full` ганяє все повністю, | ||
| * тож per-rule CI-override не потрібен (spec 2026-06-14-lint-rule-consolidation §3-А). | ||
| * @param {unknown} value значення поля `lint` | ||
| * @returns {'quick' | 'ci' | null} фаза або `null` (відсутнє/невалідне = не lint-крок) | ||
| * @returns {'per-file' | 'full' | null} scope або `null` (відсутнє/невалідне = не lint-крок) | ||
| */ | ||
| export function parseRuleLintPhase(value) { | ||
| return typeof value === 'string' && LINT_PHASES.has(value) ? /** @type {'quick'|'ci'} */ (value) : null | ||
| export function parseRuleLintSpec(value) { | ||
| return typeof value === 'string' && LINT_SCOPES.has(value) ? /** @type {'per-file'|'full'} */ (value) : null | ||
| } | ||
@@ -63,0 +67,0 @@ |
+28
-20
| /** | ||
| * Оркестратор `n-cursor lint` (quick) / `n-cursor lint-ci` (full). | ||
| * Оркестратор `n-cursor lint` — дві ортогональні осі (spec 2026-06-14-lint-rule-consolidation | ||
| * + компаньйон 2026-06-14-lint-orchestrator-fix-readonly-unification): | ||
| * - **scope** (`--full`): default = дельта vs origin (лише `per-file` правила); | ||
| * `--full` = весь репо (`per-file` ∪ `full` правила); | ||
| * - **behavior** (`--read-only`): default = fix; `--read-only` = лише детект без мутацій. | ||
| * | ||
| * Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`quick`|`ci`), | ||
| * послідовно (заборона паралельного eslint) викликає `rules/<id>/js/lint.mjs`: | ||
| * - quick: `lint(changedFiles)` — лише змінені файли (git diff HEAD + untracked); | ||
| * - ci: `lint(undefined)` — весь проєкт. | ||
| * Порядок правил — алфавітний; ci-набір = quick ∪ ci. Fail-fast: перший ненульовий код спиняє. | ||
| * Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`per-file`|`full`), | ||
| * викликає `rules/<id>/js/lint.mjs` → `lint(files, cwd, { readOnly })`: | ||
| * - default scope: `files` = змінені відносно origin (`collectChangedFilesSince`); | ||
| * - `--full`: `files = undefined` — весь проєкт. | ||
| * Порядок правил — алфавітний. Fail-fast: перший ненульовий код спиняє. | ||
| */ | ||
@@ -15,4 +19,4 @@ import { existsSync, readdirSync } from 'node:fs' | ||
| import { parseRuleLintPhase, readRuleMetaRaw } from './lib/rule-meta.mjs' | ||
| import { collectChangedFiles } from './lib/changed-files.mjs' | ||
| import { parseRuleLintSpec, readRuleMetaRaw } from './lib/rule-meta.mjs' | ||
| import { collectChangedFilesSince, resolveChangedBase } from './lib/changed-files.mjs' | ||
@@ -23,12 +27,12 @@ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url))) | ||
| /** | ||
| * Вибирає id правил для фази, алфавітно. | ||
| * Вибирає id правил для контексту, алфавітно. | ||
| * @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт | ||
| * @param {'quick'|'ci'} phase цільова фаза (quick → лише quick; ci → quick+ci) | ||
| * @param {boolean} full `false` → лише `per-file` правила; `true` → усі (`per-file` ∪ `full`) | ||
| * @returns {string[]} відсортовані id | ||
| */ | ||
| export function selectLintRules(metaById, phase) { | ||
| export function selectLintRules(metaById, full) { | ||
| const out = [] | ||
| for (const [id, raw] of Object.entries(metaById)) { | ||
| const p = parseRuleLintPhase(raw?.lint) | ||
| if (p === 'quick' || (phase === 'ci' && p === 'ci')) out.push(id) | ||
| const scope = parseRuleLintSpec(raw?.lint) | ||
| if (scope === 'per-file' || (full && scope === 'full')) out.push(id) | ||
| } | ||
@@ -57,7 +61,10 @@ return out.toSorted((a, b) => a.localeCompare(b)) | ||
| * Запускає lint-оркестрацію. | ||
| * @param {{ ci?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри | ||
| * @param {{ full?: boolean, readOnly?: boolean, cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри | ||
| * - `full` — весь репо (`true`) проти дельти vs origin (`false`, default); | ||
| * - `readOnly` — лише детект без мутацій (`true`) проти fix (`false`, default). | ||
| * @returns {Promise<number>} exit code | ||
| */ | ||
| export async function runLint(opts = {}) { | ||
| const ci = opts.ci === true | ||
| const full = opts.full === true | ||
| const readOnly = opts.readOnly === true | ||
| const cwd = opts.cwd ?? processCwd() | ||
@@ -67,9 +74,10 @@ const rulesDir = opts.rulesDir ?? RULES_DIR | ||
| const changed = ci ? undefined : collectChangedFiles(cwd) | ||
| if (!ci && changed.length === 0) { | ||
| log('\nℹ️ lint: немає змінених файлів — нічого перевіряти.\n') | ||
| // Default scope — дельта vs origin (merge-base main/origin/main); `--full` — весь репо. | ||
| const changed = full ? undefined : collectChangedFilesSince(resolveChangedBase(cwd), cwd) | ||
| if (!full && changed.length === 0) { | ||
| log('\nℹ️ lint: немає змінених файлів відносно origin — нічого перевіряти.\n') | ||
| return 0 | ||
| } | ||
| const ids = selectLintRules(readAllMeta(rulesDir), ci ? 'ci' : 'quick') | ||
| const ids = selectLintRules(readAllMeta(rulesDir), full) | ||
| for (const id of ids) { | ||
@@ -82,3 +90,3 @@ const lintPath = join(rulesDir, id, 'js', 'lint.mjs') | ||
| const mod = await import(lintPath) | ||
| const code = await mod.lint(changed, cwd) | ||
| const code = await mod.lint(changed, cwd, { readOnly }) | ||
| if (code !== 0) return code | ||
@@ -85,0 +93,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
4466949
0.16%35986
0.24%