Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@nitra/cursor

Package Overview
Dependencies
Maintainers
1
Versions
401
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nitra/cursor - npm Package Compare versions

Comparing version
5.2.0
to
5.2.1
+139
skills/doc-files/js/units-js.mjs
/** @see ./docs/units-js.md */
import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
// JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
const JSDOC_CLOSE_RE = /\*\/\s*$/
const STAR_PREFIX_RE = /^\s*\*?\s?/
/**
* Очищає JSDoc від обрамлення `/** *​/` і `*`-префіксів.
* @param {string} raw сирий блок або порожній рядок
* @returns {string} текст опису без тегів-обрамлення
*/
function cleanDoc(raw) {
if (!raw) return ''
return raw
.replace(JSDOC_OPEN_RE, '')
.replace(JSDOC_CLOSE_RE, '')
.split('\n')
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
.join('\n')
.trim()
}
/**
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
* @param {string} src вміст файлу
* @param {number} start зміщення початку декларації
* @returns {string} очищений опис
*/
function precedingDoc(src, start) {
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
return cleanDoc(m ? m[0] : '')
}
/**
* Імʼя функції, що викликається (проста Identifier або `obj.method`).
* @param {Record<string, unknown>} node CallExpression
* @returns {string|null} імʼя callee або null
*/
function calleeName(node) {
const c = node.callee
if (!c || typeof c !== 'object') return null
if (c.type === 'Identifier') return c.name
if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
return null
}
/**
* Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
* call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
* @param {unknown} node AST-вузол юніта
* @returns {Set<string>} імена викликів
*/
function collectCalls(node) {
const names = new Set()
walkAstWithAncestors(node, [], n => {
if (n.type === 'CallExpression') {
const name = calleeName(n)
if (name) names.add(name)
}
})
return names
}
/**
* Будує юніт із декларації, додає у `units`. Розпізнає function/class та
* const-функції (`const x = () => {}` / `function expression`).
* @param {Record<string, unknown>} decl декларація (function/class/variable)
* @param {boolean} exported чи експортується
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
* @param {string} src вміст файлу
* @param {Array<object>} units акумулятор
* @returns {void}
*/
function pushUnits(decl, exported, docStart, src, units) {
if (!decl || typeof decl !== 'object') return
const doc = precedingDoc(src, docStart)
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
const name = decl.id?.name
if (!name) return
units.push({
name,
kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
exported,
span: { start: decl.start, end: decl.end },
body: src.slice(decl.start, decl.end),
calls: collectCalls(decl),
doc
})
return
}
if (decl.type === 'VariableDeclaration') {
for (const d of decl.declarations ?? []) {
const init = d.init
const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
if (!isFn || d.id?.type !== 'Identifier') continue
units.push({
name: d.id.name,
kind: 'const',
exported,
span: { start: init.start, end: init.end },
body: src.slice(init.start, init.end),
calls: collectCalls(init),
doc
})
}
}
}
/**
* Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
* прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
* @param {string} src вміст файлу
* @param {string} [relPath] шлях (для вибору мови oxc)
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
*/
export function extractUnitsJs(src, relPath = 'scan.ts') {
const program = parseProgramOrNull(src, relPath)
if (!program || !Array.isArray(program.body)) return null
const units = []
for (const node of program.body) {
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
pushUnits(node.declaration, true, node.start, src, units)
} else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
pushUnits(node.declaration, true, node.start, src, units)
} else {
pushUnits(node, false, node.start, src, units)
}
}
// Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
const names = new Set(units.map(u => u.name))
for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
return units
}
/** @see ./docs/units.md */
import { extractUnitsJs } from './units-js.mjs'
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
/**
* Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
* js/mjs/ts → oxc; vue/py — додаються наступними кроками (поки `null` → виклик
* відкочується на whole-file шлях, як і раніше).
* @param {string} src вміст файлу
* @param {string} relPath шлях файлу
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
*/
export function extractUnits(src, relPath) {
const ext = (relPath.split('.').pop() || '').toLowerCase()
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
return null
}
+24
-17

@@ -0,1 +1,8 @@

---
docgen:
source: npm/lib/models.mjs
crc: feb82992
score: 100
---
# models.mjs

@@ -5,25 +12,25 @@

Файл визначає ієрархічну класифікацію моделей для системи pi. Класифікація встановлює зв'язок між локальними та хмарними провайдерами. Функція resolveModel забезпечує маршрутизацію вибору моделі залежно від заданого рівня доступності.
Файл визначає глобальну класифікацію моделей для системи pi, встановлюючи конфігураційні моделі для локального та хмарного інференсу через змінні середовища (наприклад, `N_LOCAL_MIN_MODEL`). Значення моделі мають формат "provider/model-id".
Система надає механізм каскадного вибору моделі через функцію `resolveModel`. Цей механізм послідовно перевіряє локальні тири (`LOCAL_MIN` $\rightarrow$ `LOCAL_AVG` $\rightarrow$ `LOCAL_MAX`), а потім хмарні тири, якщо попередні не визначені. Це забезпечує прозору роботу, навіть якщо локальні моделі відсутні. Прямі константи (наприклад, `LOCAL_MIN`) залишені для випадків, що вимагають явного контролю над вибором моделі.
## Поведінка
LOCAL_MIN встановлює мінімальний локальний провайдер
LOCAL_AVG встановлює середній локальний провайдер
LOCAL_MAX встановлює максимальний локальний провайдер
CLOUD_MIN встановлює мінімальний хмарний провайдер
CLOUD_AVG встановлює середній хмарний провайдер
CLOUD_MAX встановлює максимальний хмарний провайдер
resolveModel повертає перший непорожній model-id з каскадного перевірки локальних та хмарних провайдерів
resolveModel приймає тир min avg або max
resolveModel повертає model-id або порожній рядок якщо жоден тир не задано
LOCAL_MIN повертає модель для швидкого локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
LOCAL_AVG повертає модель для середнього локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
LOCAL_MAX повертає модель для максимального локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
CLOUD_MIN повертає модель для мінімального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
CLOUD_AVG повертає модель для середнього хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
CLOUD_MAX повертає модель для максимального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
resolveModel повертає перший непорожній model-id для запитаного тиру, каскадно перевіряючи локальні тири, а потім хмарний еквівалент, або порожній рядок, якщо жоден тир не задано.
## Публічний API
LOCAL_MIN — Виконує швидкий локальний inference.
LOCAL_AVG — Виконує середній локальний inference.
LOCAL_MAX — Виконує максимальний локальний inference.
CLOUD_MIN — Виконує мінімальний хмарний inference.
CLOUD_AVG — Виконує середній хмарний inference.
CLOUD_MAX — Виконує максимальний хмарний inference.
resolveModel — Повертає перший непорожній model-id для запиту, перевіряючи спочатку локальні, а потім хмарні варіанти.
LOCAL_MIN — Швидке виконання моделі на локальному пристрої.
LOCAL_AVG — Середнє за продуктивністю виконання моделі на локальному пристрої.
LOCAL_MAX — Найпотужніше виконання моделі на локальному пристрої.
CLOUD_MIN — Найменш ресурсомістке виконання моделі в хмарі.
CLOUD_AVG — Середній рівень продуктивності виконання моделі в хмарі.
CLOUD_MAX — Найпотужніше виконання моделі в хмарі.
resolveModel — Знаходить і повертає ідентифікатор моделі, починаючи з локальних варіантів, а потім переходячи до хмарних відповідників.

@@ -30,0 +37,0 @@ ## Гарантії поведінки

{
"name": "@nitra/cursor",
"version": "5.2.0",
"version": "5.2.1",
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",

@@ -5,0 +5,0 @@ "keywords": [

@@ -0,1 +1,8 @@

---
docgen:
source: npm/rules/changelog/js/consistency.mjs
crc: eaf98d6d
score: 100
---
# consistency.mjs

@@ -5,384 +12,30 @@

Модуль `consistency.mjs` реалізує перевірку правила `n-changelog` для монорепозиторіїв із кількома воркспейсами (npm та Python). Його завдання — гарантувати, що **будь-яка реліз-релевантна зміна у воркспейсі супроводжується change-файлом** (`<ws>/.changes/*.md`), а поле `version` у маніфесті воркспейсу не зміщене ручним bump-ом поза CI.
Перевіряє версіонування проектів у монорепозиторії, порівнюючи версії, зазначені в маніфестах, з даними, отриманими з мережі з https://pypi.org/pypi/. Аналізує відповідність версій встановленим правилам, визначеним у конфігурації res.json. Визначає, чи відповідають версії формату, а також перевіряє наявність змін, порівнюючи їх із даними, описаними у (n-changelog.mdc). При невдачі перевірки повертає false/null, не кидаючи винятків.
Ключові інваріанти, що їх стверджує перевірка:
## Поведінка
- `version` не повинен дрейфувати відносно бази (опублікованої у реєстрі версії або версії в git-базі гілки). Будь-який ручний bump — **fail**, навіть якщо присутній change-файл.
- Bump `version` і генерацію `CHANGELOG.md` виконує **виключно** `n-cursor release` у CI на гілці `main`.
- Релевантні зміни без change-файлу — **fail**; зміни лише в інверсних шляхах (`docs/`, `doc/`, `.cursor/`, `.claude/`) — змінами не вважаються.
- npm-пакети, що публікують `CHANGELOG.md` разом із пакетом, повинні мати рядок `"CHANGELOG.md"` у масиві `files` маніфесту, але це перевіряється лише за наявності pending change-файлів.
1. Ініціалізує репортер для збору результатів перевірки.
2. Визначає робочий каталог та стан autofix-режиму.
3. Зчитує всі кореневі каталоги проектів у монорепо.
4. Класифікує кожен проект як публікувальний (registryPublishable) або локальний.
5. Для кожного публікувального проекту виконує перевірку:
а. Зчитує маніфест проекту.
б. Якщо у маніфесті відсутнє ім'я або поле version, фіксує помилку.
в. Якщо є change-файл(и) у .changes/, фіксує успіх, оскільки bump зробить CI (n-changelog.mdc), і перевіряє наявність "CHANGELOG.md" у файлах проекту.
г. Якщо немає change-файлу, визначає точку порівняння на основі поточної гілки.
д. Порівнює версію в маніфесті з опублікованою версією (за допомогою npm view або запиту до https://pypi.org/pypi/).
е. Якщо версія в маніфесті випереджає опубліковану, фіксує помилку, оскільки ручний bump поза CI заборонений.
ж. Якщо версія в маніфесті позаду опублікованої, фіксує успіх, оскільки це відставання локального репозиторію від реєстру.
з. Якщо версії збігаються, перевіряє змінений код відносно точки порівняння. Якщо зміни є, але немає change-файлу, викликає механізм фіксації або фіксує помилку, надаючи підказку для використання команди `npx @nitra/cursor change`.
6. Для кожного локального проекту виконує перевірку:
а. Зчитує маніфест проекту.
б. Визначає точку порівняння на основі поточної гілки.
в. Перевіряє, чи є релевантні зміни у проекті відносно точки порівняння.
г. Якщо зміни є, але немає change-файлу, викликає механізм фіксації або фіксує помилку, надаючи підказку для використання команди `npx @nitra/cursor change`.
7. Повертає кінцевий код виходу, що відображає результат перевірки.
Передбачено дві моделі визначення бази на рівні воркспейсу:
## Гарантії поведінки
1. **registry-published** — npm-пакети з `name` і `files`, не `private`; Python-проєкти зі статичною `project.version` і `project.name`. База — версія, опублікована в npm-реєстрі або PyPI.
2. **local-only** — приватні npm без `files`, Python без імені/версії для реєстру. База визначається через git:
- feature-гілка → `merge-base` з `dev`, інакше з `main`;
- гілка `main` → diff від `origin/main` (або `HEAD~1` без remote);
- гілка `dev` → перевірка пропускається (крім незакомічених registry-published).
Усі виклики `git` і зовнішні HTTP/CLI — через `execFile` / `fetch`, без shell-інтерполяції (безпека, виключає command injection).
## Експорти / API
| Експорт | Тип | Призначення |
| -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
| `check(opts?)` | `async function` | Єдина публічна точка входу. Запускає весь цикл перевірок для всіх воркспейсів монорепо і повертає exit-код. |
Сигнатура `check`:
```js
export async function check(opts = {}): Promise<number>
```
`opts`:
- `opts.getPublishedVersion?: (name: string, kind?: 'npm' | 'python') => Promise<string | null>` — перевизначення стандартного резолвера опублікованої версії (для юніт-тестів, оффлайн-режимів).
- `opts.cwd?: string` — корінь репозиторію; за замовчуванням `process.cwd()`.
- `opts.autofix?: boolean` — autofix-режим. За замовчуванням береться з env `N_CURSOR_CHANGELOG_AUTOFIX === '1'` (виставляє лише крок `npm-changelog` у `hk.pkl` для pre-commit). Коли увімкнено, замість `fail` на відсутній change-файл правило створює його через `writeChange()` з дефолтами (`bump=patch`, `section=Changed`, `message` = subject останнього коміту, fallback — назва гілки чи `оновлення`) і ставить у git-індекс (`git add`), тож коміт не падає. **Жодної мережі:** у autofix-режимі published-перевірка пропускає реєстровий резолв (`npm view` / PyPI fetch) і drift-перевірку version vs опублікована — лишається лише наявність change-файлу та git-diff. Ручний bump `version` у хуці не ловиться; його далі ловить CI та ручний `fix changelog` без env. Поза хуком режим вимкнено — поведінка лишається fail-on-missing з повною drift-перевіркою, щоб CI не плодив артефактів.
Повертає **exit-код** (0 — pass, ≠ 0 — fail), отриманий від `createCheckReporter()`.
## Функції
### `gitOrNull(args, cwd)`
- **Сигнатура:** `async (args: string[], cwd: string) => Promise<string | null>`
- **Параметри:** `args` — аргументи `git`; `cwd` — робочий каталог процесу.
- **Повертає:** `stdout` команди або `null` при будь-якій помилці.
- **Side effects:** виконує дочірній процес `git` через `execFile`.
Тиха обгортка над `git`, що ковтає виключення — використовується скрізь, де відсутність гілки/ref/маніфесту є штатним кейсом.
### `isInsideGitRepo(cwd)`
- **Сигнатура:** `async (cwd: string) => Promise<boolean>`
- **Повертає:** `true`, якщо `cwd` всередині git working tree.
- **Side effects:** запит `git rev-parse --is-inside-work-tree`.
### `currentBranchName(cwd)`
- **Сигнатура:** `async (cwd: string) => Promise<string | null>`
- **Повертає:** ім'я поточної гілки (`git rev-parse --abbrev-ref HEAD`) або `null`.
### `baseRefLabel(ref)`
- **Сигнатура:** `(ref: string) => string`
- **Параметри:** `ref` — git-ref.
- **Повертає:** човничок без префіксу `origin/` (наприклад, `origin/main` → `main`); інакше повертає `ref` без змін.
- **Side effects:** немає.
### `isGitAncestor(ancestor, descendant, cwd)`
- **Сигнатура:** `async (ancestor: string, descendant: string, cwd: string) => Promise<boolean>`
- **Повертає:** `true`, якщо `ancestor` є предком `descendant` (через `git merge-base --is-ancestor`).
- **Зауваження:** `git merge-base --is-ancestor` повертає exit-код, тому всередині використовується `gitOrNull`, який ловить ненульовий exit і повертає `null` — у такому випадку результат функції `false`.
### `resolveBranchRef(branchName, cwd)`
- **Сигнатура:** `async (branchName: string, cwd: string) => Promise<string | null>`
- **Поведінка:** для `branchName` пробує спочатку локальний ref, потім `origin/<branchName>`; повертає перший, що верифікується через `git rev-parse --verify --quiet`.
### `isChangelogIgnoredPath(relPath)`
- **Сигнатура:** `(relPath: string) => boolean`
- **Поведінка:** нормалізує шлях до posix (заміна `\` на `/`, обрізання провідного `./`), повертає `true`, якщо починається з одного з префіксів `CHANGELOG_IGNORE_PATH_PREFIXES`.
### `isPathGitIgnored(relPath, cwd)`
- **Сигнатура:** `async (relPath: string, cwd: string) => Promise<boolean>`
- **Поведінка:** виконує `git check-ignore -q -- <relPath>`. Exit-код 0 → ignored (повертає `true`); будь-яка помилка → `false`.
- **Side effects:** дочірній процес `git`.
### `resolveMergeBase(baseRef, cwd)`
- **Сигнатура:** `async (baseRef: string, cwd: string) => Promise<string | null>`
- **Повертає:** SHA `git merge-base baseRef HEAD` або `null`.
### `resolveChangelogComparisonPoint(branch, cwd)`
- **Сигнатура:** `async (branch: string | null, cwd: string) => Promise<{ ref: string, label: string } | null>`
- **Логіка:**
- якщо `branch === 'dev'` → `null` (local-only пропускається);
- якщо `branch === 'main'`:
- якщо `origin/main` верифіковано і `origin/main === HEAD` або `origin/main` — предок `HEAD` → `{ ref: 'origin/main', label: 'main' }`;
- інакше `HEAD~1` → `{ ref: <sha>, label: 'main~1' }`;
- якщо ні те, ні те — `null`.
- feature-гілки: ітерує по `FEATURE_BASE_BRANCH_CANDIDATES` (`['dev', 'main']`); перший, для якого вдається резолвити ref **і** обчислити merge-base, дає `{ ref: <merge-base SHA>, label: baseRefLabel(...) }`.
- **Повертає:** опис точки порівняння (`ref` для `git diff`/`git show`, `label` для повідомлень) або `null`.
### `pathspecForWorkspace(ws, subWorkspaces)`
- **Сигнатура:** `(ws: string, subWorkspaces: string[]) => string[]`
- **Поведінка:**
- для `ws !== '.'` → `[\`${ws}/\`]`;
- для `ws === '.'` (корінь монорепо) → `['.', ':(exclude)<sub>/' для кожного підворкспейсу]`, щоб залишити лише файли кореня без вкладених воркспейсів.
- **Повертає:** масив pathspec-ів для передачі в `git diff -- <pathspec>`.
### `splitNulPaths(nulSeparated)`
- **Сигнатура:** `(nulSeparated: string | null) => string[]`
- **Поведінка:** ділить вхідний рядок по `\0`, відкидає порожні елементи.
- **Чому `-z`:** без прапорця git застосовує `core.quotePath` і повертає не-ASCII імена (наприклад, кирилицю) у C-quoted формі (`"docs/\320\262..."`), що ламає префіксне порівняння для `CHANGELOG_IGNORE_PATH_PREFIXES`.
### `listChangedPathsAgainstBase(baseRef, pathspec, cwd)`
- **Сигнатура:** `async (baseRef: string, pathspec: string[], cwd: string) => Promise<string[]>`
- **Поведінка:** об'єднує два джерела через `Set`:
- `git diff --name-only -z <baseRef> -- <pathspec>` — закомічені/staged зміни;
- `git ls-files --others --exclude-standard -z -- <pathspec>` — нові untracked-файли.
- **Повертає:** дедуплікований масив відносних шляхів.
### `workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces, cwd)`
- **Сигнатура:** `async (baseRef: string, ws: string, subWorkspaces: string[], cwd: string) => Promise<boolean>`
- **Поведінка:** обчислює pathspec для `ws`, отримує всі змінені шляхи, ітерує по них:
- інверсія (`docs/`, `.cursor/`, ...) → пропустити;
- `git check-ignore` → пропустити;
- інакше — повернути `true`.
- **Повертає:** `true`, якщо є хоч один шлях, що вважається релевантною змінною.
### `readBaseVersion(baseRef, manifest, cwd)`
- **Сигнатура:** `async (baseRef: string, manifest: PackageManifest, cwd: string) => Promise<string | null>`
- **Поведінка:** виконує `git show <baseRef>:<wsPath>`, де `wsPath` — шлях до маніфесту відносно репозиторію; парсить:
- `npm` → `JSON.parse(...).version` (`null` при помилці парсу);
- `python` → `parsePyprojectFields(out).version`.
- **Повертає:** версію з маніфесту на `baseRef` або `null`.
### `defaultGetPublishedNpmVersion(name)`
- **Сигнатура:** `async (name: string) => Promise<string | null>`
- **Поведінка:** `npm view <name> version` із таймаутом `REGISTRY_TIMEOUT_MS` (10 с). Trim і повернення; пуста відповідь / помилка → `null`.
- **Side effects:** дочірній процес `npm`, мережа.
### `defaultGetPublishedPyPiVersion(name)`
- **Сигнатура:** `async (name: string) => Promise<string | null>`
- **Поведінка:** `fetch('https://pypi.org/pypi/<encodedName>/json', { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) })`; читає `data.info.version`. Будь-яка помилка / `!res.ok` → `null`.
- **Side effects:** мережа.
### `resolvePublishedVersion(manifest, getPublishedVersion)`
- **Сигнатура:** `(manifest: PackageManifest, getPublishedVersion) => Promise<string | null>`
- **Поведінка:** якщо в маніфесті немає `name` → `Promise.resolve(null)`; інакше делегує до `getPublishedVersion(name, kind)`.
### `defaultGetPublishedVersion(name, kind = 'npm')`
- **Сигнатура:** `(name: string, kind?: 'npm' | 'python') => Promise<string | null>`
- **Поведінка:** диспетчер за `kind` (Python → PyPI, інакше — npm).
### `createDefaultGetPublishedVersion()`
- **Сигнатура:** `() => (name, kind?) => Promise<string | null>`
- **Поведінка:** фабрика, що повертає `defaultGetPublishedVersion`. Використовується як дефолт у `check` для зручної підміни в тестах.
### `checkNpmFilesArrayContainsChangelog(manifest, pass, fail)`
- **Сигнатура:** `(manifest: PackageManifest, pass: (msg)=>void, fail: (msg)=>void) => void`
- **Поведінка:**
- якщо `kind !== 'npm'` або `npmFiles` відсутній — рання терміновка;
- `pass`, якщо `npmFiles` містить `'CHANGELOG.md'`;
- інакше `fail` з рекомендацією додати рядок.
### `workspaceLabel(manifest)`
- **Сигнатура:** `(manifest: PackageManifest) => string`
- **Повертає:** `'<root>'` для `ws === '.'`, інакше `manifest.ws`.
### `missingChangeFileMessage(label, mf)`
- **Сигнатура:** `(label: string, mf: string) => string`
- **Повертає:** уніфікований текст для `fail` про відсутній change-файл, включно з інструкцією для `npx @nitra/cursor change`.
### `hasPendingChangeFiles(ws, cwd)`
- **Сигнатура:** `async (ws: string, cwd: string) => Promise<boolean>`
- **Поведінка:** `(await readChangeFiles(ws, cwd)).length > 0`.
### `checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, subWorkspaces, pass, fail, cwd)`
- **Сигнатура:** `async (...) => Promise<void>`
- **Параметр `_Vcurrent`:** ігнорується (залишений для сумісності сигнатури; bump робить CI).
- **Поведінка:**
1. Якщо `hasPendingChangeFiles` → `pass` про change-файл(и) + перевірка `CHANGELOG.md` у `files` npm-маніфесту. Вихід.
2. Якщо не в git-репі — вихід без перевірок.
3. Беремо `currentBranchName`:
- `branch === 'dev'`: лише перевірка наявності релевантних змін відносно `HEAD` (staged/working tree). Є — `fail` `missingChangeFileMessage`; нема — мовчазний вихід.
- інакше: резолвимо `comparison`; якщо `comparison` + є зміни відносно `comparison.ref` → `fail`.
- на `main` додатково перевіряємо ще й `HEAD` (working/staged) — `fail`, якщо є зміни.
### `checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail, cwd)`
- **Сигнатура:** `async (...) => Promise<void>`
- **Поведінка:**
1. `manifest.version` відсутній → `fail` («у маніфесті відсутнє поле version»). Вихід.
2. `manifest.name` відсутній → `fail` («відсутнє ім'я пакета»). Вихід.
3. `Vpublished = resolvePublishedVersion(...)`; якщо `null` → `pass` («опублікована версія недоступна, перевірку пропущено»). Вихід.
4. Якщо `Vpublished !== Vcurrent` → `fail` про drift (ручний bump заборонено — навіть із change-файлом). Вихід.
5. Інакше `pass` про збіг із реєстром і виклик `checkPublishedWorkspacePendingGitChanges`.
### `checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail, cwd)`
- **Сигнатура:** `async (...) => Promise<void>`
- **Поведінка** (виконується для воркспейсів, де `workspaceHasRelevantChangesAgainstBase` дала `true`):
1. `Vbase = readBaseVersion(comparisonRef, manifest, cwd)`.
2. Якщо `Vbase && Vcurrent && Vbase !== Vcurrent` → `fail` про drift (`Vbase → Vcurrent`). Вихід.
3. Якщо `hasPendingChangeFiles` → `pass`. Вихід.
4. Інакше `fail` `missingChangeFileMessage`.
- Drift-перевірка йде **перед** перевіркою наявності change-файлу: симетрія з registry-published-шляхом (ручний bump заборонено навіть із change-файлом).
### `runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail, cwd)`
- **Сигнатура:** `async (localOnly: PackageManifest[], subWorkspaces: string[], pass, fail, cwd) => Promise<void>`
- **Поведінка:**
1. Якщо `localOnly` пустий → ранній вихід.
2. Не git-репозиторій → `pass` про пропуск.
3. `branch === 'dev'` → `pass` про пропуск.
4. `comparison` не знайдено (немає `dev`/`main`/`origin/*`) → `pass` про пропуск.
5. Для кожного `manifest` із `localOnly`: пропустити, якщо немає релевантних змін відносно `comparison.ref`; інакше виставити `checkedAny = true` і викликати `checkLocalOnlyChangedWorkspace`.
6. Якщо жоден воркспейс не змінено — `pass` («local-only воркспейси без змін відносно `<label>`»).
### `check(opts)`
- **Сигнатура:** `async (opts?: { getPublishedVersion?, cwd? }) => Promise<number>`
- **Покрокове виконання:**
1. Створюється `reporter = createCheckReporter()`; беруться його `pass` і `fail`.
2. `getPublishedVersion` — з `opts` або `createDefaultGetPublishedVersion()`.
3. `cwd` — з `opts` або `process.cwd()`.
4. `workspaces = await getMonorepoProjectRootDirs(cwd)`; `subWorkspaces = workspaces.filter(w => w !== '.')`.
5. `isMonorepoRoot = subWorkspaces.length > 0` — корінь монорепо вважається glue/конфіг/tooling.
6. Розділяємо воркспейси на `published` та `localOnly`:
- корінь `.` за наявності підпакетів → одразу `pass` про пропуск, не читаємо маніфест;
- `readPackageManifest(ws, cwd)` → якщо `null`, ws пропускається;
- `manifest.registryPublishable === true` → у `published`, інакше — у `localOnly`.
7. Послідовно перевіряємо всі `published` через `checkPublishedWorkspace`.
8. `runLocalOnlyChecks(localOnly, ...)`.
9. Повертаємо `reporter.getExitCode()`.
## Залежності
### Стандартна бібліотека Node.js
- `node:child_process` → `execFile` — запуск `git`, `npm` без shell-інтерполяції.
- `node:util` → `promisify` — обгортка `execFileAsync = promisify(execFile)`.
- Глобальні: `fetch`, `AbortSignal.timeout` — для PyPI (Node ≥ 18).
### Внутрішні модулі
- `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — створює пару `{ pass, fail }` і обчислює `getExitCode()`.
- `../lib/package-manifest.mjs`:
- `getMonorepoProjectRootDirs(cwd)` — список воркспейсів (включно з `.`);
- `manifestFilePath(ws, manifest)` — шлях до маніфесту в повідомленнях;
- `parsePyprojectFields(text)` — отримання `{ name, version }` із `pyproject.toml`;
- `readPackageManifest(ws, cwd)` — нормалізований опис воркспейсу (тип `PackageManifest`).
- `../../release/lib/change-file.mjs` → `readChangeFiles(ws, cwd)` — список pending change-файлів у `<ws>/.changes/`.
### Зовнішні системи / процеси
- `git` (CLI) — `rev-parse`, `merge-base`, `diff`, `ls-files`, `show`, `check-ignore`.
- `npm` (CLI) — `npm view <name> version` для registry-published npm-пакетів.
- PyPI HTTP API — `https://pypi.org/pypi/<name>/json` для Python-пакетів.
### Константи модуля
- `FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])` — порядок пошуку бази для feature-гілок.
- `LOCAL_ONLY_SKIP_BRANCH = 'dev'` — гілка, де local-only перевірка не активна.
- `CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/', '.cursor/', '.claude/'])` — інверсні префікси (зміни в них не релевантні).
- `REGISTRY_TIMEOUT_MS = 10_000` — таймаут для `npm view` / PyPI fetch.
- `LEADING_DOTSLASH_RE = /^\.\//` — для нормалізації шляхів у `isChangelogIgnoredPath`.
## Потік виконання / Використання
Типовий виклик (із CLI/скрипту):
```js
import { check } from './consistency.mjs'
const exitCode = await check()
process.exit(exitCode)
```
Із кастомним резолвером опублікованої версії (наприклад, у тестах):
```js
import { check } from './consistency.mjs'
const exitCode = await check({
cwd: '/tmp/sandbox-repo',
async getPublishedVersion(name, kind) {
if (name === '@scope/pkg-a') return '1.0.0'
return null
}
})
```
### Високорівневий потік `check`
```
check(opts)
├─ createCheckReporter() → { pass, fail, getExitCode }
├─ getMonorepoProjectRootDirs(cwd) → workspaces
├─ subWorkspaces = workspaces \ ['.']
├─ isMonorepoRoot = subWorkspaces.length > 0
├─ For each ws:
│ ├─ ws === '.' && isMonorepoRoot → pass (root skipped) ; continue
│ ├─ manifest = readPackageManifest(ws, cwd)
│ ├─ !manifest → continue
│ └─ manifest.registryPublishable ? published.push : localOnly.push
├─ For each published manifest:
│ └─ checkPublishedWorkspace(...)
│ ├─ no version → fail
│ ├─ no name → fail
│ ├─ Vpublished == null → pass (skipped)
│ ├─ drift → fail
│ └─ checkPublishedWorkspacePendingGitChanges(...)
│ ├─ hasPendingChangeFiles → pass + checkNpmFilesArrayContainsChangelog
│ ├─ branch dev → fail iff relevant changes vs HEAD
│ ├─ comparison ref + relevant changes → fail
│ └─ main + relevant changes vs HEAD → fail
├─ runLocalOnlyChecks(localOnly, ...)
│ ├─ not git → pass (skipped)
│ ├─ branch dev → pass (skipped)
│ ├─ no comparison → pass (skipped)
│ └─ for each localOnly with relevant changes:
│ └─ checkLocalOnlyChangedWorkspace(...)
│ ├─ Vbase != Vcurrent → fail (drift)
│ ├─ hasPendingChangeFiles → pass
│ └─ else fail (missing change file)
└─ return reporter.getExitCode()
```
### Контракти / гарантії
- **Безпека:** жодних викликів `exec` / `spawn` із інтерполяцією рядків — лише `execFile` із масивом аргументів.
- **Idempotency:** функція виконує лише читання (git/fs/network); не змінює нічого на диску.
- **Деградація:** мережеві / репо-помилки — м'які (повертають `null`); їх результат — `pass` про пропуск, а не `fail`. Виняток: реальні відмінності, які можна спостерігати локально (drift, відсутність change-файлу), завжди дають `fail`.
- **Симетрія шляхів:** registry-published і local-only обидва ставлять drift-перевірку **перед** перевіркою change-файлу, тому ручний bump поза CI стабільно falsies перевірку незалежно від моделі.
### Точки розширення
- `opts.getPublishedVersion` — підміна джерела опублікованих версій (стаб для офлайн-тестів або проксі-реєстру).
- `opts.cwd` — переключення активного репозиторію без `process.chdir`.
## Rebuild Test
Контрольний перелік для відтворення/верифікації поведінки:
1. **Експорт API** — модуль експортує єдину `async function check(opts?)`, що повертає `Promise<number>`.
2. **Дефолти** — `opts.cwd` за замовчуванням `process.cwd()`; `opts.getPublishedVersion` за замовчуванням `defaultGetPublishedVersion` (npm-view для `kind === 'npm'`, PyPI fetch для `kind === 'python'`).
3. **Корінь монорепо** — для `ws === '.'` за наявності підворкспейсів виставляється `pass` про пропуск без читання маніфесту.
4. **Класифікація** — `manifest.registryPublishable === true` → `published`; інакше → `localOnly`. Воркспейси без читабельного маніфесту мовчки пропускаються.
5. **Drift > change-файл** — для обох моделей перевірка drift `version` спрацьовує **раніше** за перевірку наявності change-файлу і `fail` має пріоритет.
6. **Гілка `dev`** — `runLocalOnlyChecks` повністю пропускає local-only (`pass`); registry-published у `checkPublishedWorkspacePendingGitChanges` на `dev` перевіряє лише робоче дерево/staged відносно `HEAD`.
7. **Гілка `main`** — точка порівняння: `origin/main`, якщо це предок `HEAD` або збігається; інакше `HEAD~1`; також додаткова перевірка `HEAD` (working/staged), щоб виявити незакомічені зміни.
8. **Feature-гілка** — точка порівняння визначається ітерацією по `['dev', 'main']`, береться merge-base першої доступної бази; `label` приводиться до короткої форми (`origin/main` → `main`).
9. **Інверсні шляхи** — `docs/`, `doc/`, `.cursor/`, `.claude/` (із normalize `\` → `/` і обрізанням `./`) не вважаються релевантними змінами.
10. **`git -z`** — у `git diff --name-only` та `git ls-files --others` обов'язково використовується `-z`, інакше не-ASCII імена потраплять у C-quoted формі й ламатимуть префіксне порівняння.
11. **Untracked + tracked** — `listChangedPathsAgainstBase` об'єднує `git diff` (відносно `baseRef`) і `git ls-files --others --exclude-standard`, дедуплікація через `Set`.
12. **gitignored** — кожен кандидат додатково перевіряється через `git check-ignore -q --`; ігноровані пропускаються.
13. **`checkNpmFilesArrayContainsChangelog`** — викликається лише в гілці «pending change-файли є» для registry-published; для не-npm або відсутнього `npmFiles` — раннє return без `pass`/`fail`.
14. **Мовчазний skip** — недоступність опублікованої версії (мережа/реєстр) даює `pass` про пропуск, а не `fail`.
15. **`workspaceLabel`** — `'<root>'` для `.`, інакше шлях ws.
16. **`missingChangeFileMessage`** — текст fail містить шлях до маніфесту, інструкцію `npx @nitra/cursor change --bump … --section … --message …` і нагадування «bump зробить CI на main (n-changelog.mdc)».
17. **Послідовність публічних перевірок** — спершу всі `published` (у порядку, повернутому з `getMonorepoProjectRootDirs`), потім `runLocalOnlyChecks`.
18. **Exit-код** — повертається з `reporter.getExitCode()` (агрегує всі `pass`/`fail`).
- Read-only: файл не виконує операцій запису у файлову систему.
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
- За невдалої перевірки повертає `false`/`null` замість винятку.

@@ -1,140 +0,30 @@

# `npm/rules/feedback/fix.mjs`
---
docgen:
source: npm/rules/feedback/fix.mjs
crc: 12fc1644
score: 100
---
# fix.mjs
## Огляд
Файл є точкою входу (entry-point) для правила `feedback` у CLI-наборі `@nitra/cursor`. Він реалізує дві паралельні ролі того ж самого модуля:
Модуль виконує задану перевірку, ініціалізуючи її з локального файлу конфігурації. При запуску як окрема програма, він завантажує конфігурацію, перевіряє білий список та надає звіт про результати виконання. Результат виконання правила повертається як код виходу.
1. **Library mode** — експортує функцію `run(ctx)`, яку викликають інші частини оркестратора (наприклад, агрегований `fix`/`lint`-прогін, де всі правила запускаються послідовно з шарінгом кешу обходу файлів `walkCache`).
2. **Standalone mode** — якщо файл стартує безпосередньо як CLI-скрипт (`bun rules/feedback/fix.mjs`), він виконується як повний аналог команди `npx @nitra/cursor fix feedback` із завантаженням конфігу, застосуванням whitelist та підсумком.
## Поведінка
Сам файл логіки правила не містить — він є тонким адаптером, що делегує роботу до спільного раннера `runStandardRule`. Стандартний потік правила, який запускається через цей адаптер: `applies → JS-concerns → policy → mdc-refs` (тобто перевіряється застосовність до проєкту, далі — JS-специфічні перевірки, далі — policy-шар, наприкінці — синхронізація посилань у відповідному `.mdc`-файлі).
1. Викликається функція `run` для виконання правила.
2. Виконання правила відбувається шляхом ініціалізації стандартного правила з директорії цього файлу.
3. Якщо код виконується як окрема утиліта (standalone), ініціюється повний запуск правила.
4. Запуск правила як окремої утиліти включає завантаження конфігурації, перевірку білого списку та підведення підсумків.
5. Результат цього запуску повертається як код виходу.
Модуль використовує `import.meta.dirname`, тому він повністю прив'язаний до місця свого розташування: каталог правила (`npm/rules/feedback/`) автоматично стає коренем, у якому раннер шукає `meta.json`, `feedback.mdc` та інші артефакти.
## Публічний API
## Експорти / API
run — виконує послідовність перевірок: застосовує правила, аналізує JS-занепокоєння, порівнює з політикою та перевіряє посилання MDC.
| Експорт | Тип | Призначення |
| ------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `run` | `function(ctx?): Promise<number>` | Library-точка входу правила. Викликається оркестратором; повертає exit-code (0 — OK, 1 — порушення). |
## Гарантії поведінки
Side-експортів немає. Default-експорту немає.
Поведінка при прямому запуску файлу як CLI (через `process.exit(await runRuleCli(...))`) — це не експорт, а top-level side-effect, активний лише коли `isRunAsCli(import.meta.url)` повертає `true`.
## Функції
### `run(ctx)`
```js
export function run(ctx)
```
- **Призначення:** запустити правило `feedback` у library-режимі. Делегує всю роботу до `runStandardRule`, передаючи каталог поточного файлу як ідентифікатор правила.
- **Параметри:**
- `ctx` — `RuleContext | undefined`. Опціональний контекст прогону. Тип імпортується через JSDoc-посилання `import('../../scripts/lib/run-standard-rule.mjs').RuleContext`. Зокрема, контекст несе спільні структури між правилами (наприклад, `walkCache` — кеш обходу файлової системи, щоб не сканувати дерево кілька разів у разі прогону кількох правил поспіль). Якщо `ctx` не переданий, `runStandardRule` створює власний внутрішній контекст.
- **Повертає:** `Promise<number>` — exit-code:
- `0` — правило застосовне і порушень не знайдено, або правило не застосовне до поточного проєкту (`applies` повернув `false`);
- `1` — знайдені порушення (policy / JS-concerns / mdc-refs).
- **Side effects:** жодних прямих у цій функції. Опосередковано через `runStandardRule` можливі: читання файлів проєкту, читання `meta.json` і `feedback.mdc`, лог у stdout/stderr, мутація переданого `walkCache`.
- **Винятки:** функція сама не кидає; будь-яка помилка приходить як rejected promise від `runStandardRule`.
### Top-level standalone-блок
```js
if (isRunAsCli(import.meta.url)) {
process.exit(await runRuleCli(import.meta.dirname))
}
```
- **Призначення:** перетворити цей же модуль на CLI-скрипт. Якщо файл був запущений напряму (а не імпортований), виконується повний CLI-цикл правила — еквівалент `npx @nitra/cursor fix feedback`.
- **Виклик:** `runRuleCli(import.meta.dirname)` отримує абсолютний шлях до каталогу правила; всередині раннера це задає, який саме `meta.json`/`*.mdc` і `applies/policy/...`-функції підтягуються.
- **Повертає / завершення:** результат `runRuleCli` (число) йде в `process.exit(...)`, тобто процес одразу завершується з відповідним exit-code. Це навмисно: standalone entry-point має повернути код виходу для CI/IDE-інтеграцій.
- **Спеціальні маркери:**
- `// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit` — свідома відмова від загальної заборони `process.exit` саме тут, бо це standalone-точка входу.
- **Side effects:** завершує процес Node/Bun.
## Залежності
Файл імпортує два внутрішні модулі (relative-шляхи, без зовнішніх npm-пакетів):
| Імпорт | З файлу | Що використовується | Роль |
| ----------------- | ----------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isRunAsCli` | `../../scripts/lib/run-rule-cli.mjs` | Функція-предикат | Визначає, чи цей `.mjs`-файл був запущений напряму як CLI (через `import.meta.url`), а не імпортований як модуль. |
| `runRuleCli` | `../../scripts/lib/run-rule-cli.mjs` | Async-функція | Виконує повний CLI-цикл правила: завантаження конфігу, whitelist-фільтрацію, прогін стандартного раннера, форматування підсумку, повернення exit-code. |
| `runStandardRule` | `../../scripts/lib/run-standard-rule.mjs` | Async-функція + JSDoc-тип `RuleContext` | Стандартний раннер для правил: `applies → JS-concerns → policy → mdc-refs`. Шукає артефакти у переданому каталозі. |
Глобали з рантайму:
- `import.meta.dirname` — абсолютний шлях каталогу поточного `.mjs`-файлу (Bun / Node ≥ 20.11). Використовується як «корінь правила».
- `import.meta.url` — file-URL модуля; передається в `isRunAsCli` для надійного порівняння з `process.argv[1]`.
- `process.exit` — викликається лише у standalone-гілці.
Зовнішні пакети не імпортуються.
## Потік виконання / Використання
### Сценарій A — імпорт як library
```js
import { run } from './npm/rules/feedback/fix.mjs'
const code = await run(/* ctx */)
if (code !== 0) {
// правило знайшло порушення
}
```
Послідовність всередині:
1. Викликається `run(ctx)`.
2. `run` повертає `runStandardRule(import.meta.dirname, ctx)`.
3. Усередині `runStandardRule`:
- перевіряє `applies` (чи правило застосовне до проєкту);
- проганяє JS-concerns (lint/structure);
- проганяє policy-перевірки;
- звіряє `mdc-refs` (узгодженість посилань між кодом і `feedback.mdc`).
4. Повертається `0` або `1`.
Top-level `if (isRunAsCli(...))` у цьому випадку не спрацьовує — `isRunAsCli(import.meta.url)` повертає `false`, бо стартова точка процесу — інший файл.
### Сценарій B — прямий запуск як CLI
```bash
bun npm/rules/feedback/fix.mjs
# або еквівалент:
npx @nitra/cursor fix feedback
```
Послідовність:
1. Node/Bun завантажує модуль; виконуються `import`-и.
2. Перевіряється `isRunAsCli(import.meta.url)` → `true`.
3. Викликається `await runRuleCli(import.meta.dirname)`. Цей раннер:
- завантажує загальний конфіг проєкту;
- застосовує whitelist (якщо в конфізі обмежений набір правил);
- всередині все одно проганяє ту ж саму `runStandardRule`-логіку;
- друкує форматований summary;
- повертає exit-code.
4. `process.exit(code)` миттєво завершує процес із отриманим кодом, який потім читає CI/IDE.
### Чому дві ролі в одному файлі
Конвенція в `npm/rules/<id>/fix.mjs` дозволяє:
- оркестратору проганяти всі правила одним процесом, шарячи `walkCache` (швидко, для batch-режимів);
- розробнику запустити **одне** правило точково з шелла без обгорток і отримати CI-сумісний exit-code.
Жодного дублювання логіки немає — обидві гілки в кінцевому рахунку викликають один і той самий `runStandardRule`, просто standalone-гілка додає шар CLI-обгортки (`runRuleCli`).
## Rebuild Test
Чи можна за цим документом без доступу до файлу відтворити модуль еквівалентної поведінки? Так:
- ім'я експорту: `run`, сигнатура `(ctx) => Promise<number>`;
- тіло: `return runStandardRule(import.meta.dirname, ctx)`;
- top-level `if`: `isRunAsCli(import.meta.url)` → `process.exit(await runRuleCli(import.meta.dirname))`;
- два імпорти з точно вказаних шляхів (`../../scripts/lib/run-rule-cli.mjs` для `isRunAsCli`/`runRuleCli`, `../../scripts/lib/run-standard-rule.mjs` для `runStandardRule`);
- ESLint-disable коментар над `process.exit` для `n/no-process-exit` і `unicorn/no-process-exit`;
- JSDoc-блок над `run` із посиланням на тип `RuleContext` через `import('...')`-нотацію.
Усі ці елементи описані вище — реконструкція еквівалентного файлу можлива.
- Read-only: файл не виконує операцій запису у файлову систему.
- Кешує результати в межах одного прогону.
- Не звертається до мережі.

@@ -0,1 +1,8 @@

---
docgen:
source: npm/rules/ga/fix.mjs
crc: 12fc1644
score: 100
---
# fix.mjs

@@ -5,22 +12,17 @@

Цей файл запускає процес перевірки наявності та стану необхідних системних ресурсів. Він забезпечує швидку оцінку готовності системи до подальшої роботи, використовуючи кешовані дані для прискорення процесу. Результати перевірки використовуються для прийняття рішень щодо подальших дій.
Модуль відповідає за запуск механізму виконання стандартного правила. Він шукає та застосовує правила, використовуючи поточний каталог як джерело. При запуску як окрема програма, він ініціює виконання команди, необхідної для застосування цього правила.
## Поведінка
1. Ініціалізує контекст прогону.
2. Викликає стандартне правило, використовуючи контекст.
3. Якщо код виконується як окремий CLI-скрипт, то:
1. Викликає оркестрацію правил.
2. Повертає код завершення процесу.
1. Викликає механізм виконання стандартного правила, використовуючи поточний каталог як джерело правил.
2. Якщо скрипт виконується як окрема програма, запускає оркестрацію командного рядка для виконання правила.
## Публічний API
- run — Запускає стандартне правило, враховуючи контекст та перетворюючи його на JS-коду, політику та посилання на MDC.
- Library mode — Викликається через CLI-оркестрацію за допомогою імпорту та функції `run`.
run — виконує послідовність дій: застосовує правила, обробляє JS-занепокоєння, перевіряє політику та посилання MDC.
## Гарантії поведінки
- Запускає процес.
- Кеш зберігає результати попередніх прогонів.
- Результат залежить від попередніх прогонів.
- Немає взаємодії з мережею.
- Read-only: файл не виконує операцій запису у файлову систему.
- Кешує результати в межах одного прогону.
- Не звертається до мережі.

@@ -0,1 +1,8 @@

---
docgen:
source: npm/rules/ga/js/lint.mjs
crc: 428bf482
score: 100
---
# lint.mjs

@@ -5,16 +12,12 @@

Файл `lint` делегує перевірку коду інструменту, який запускається з командного рядка. Він використовується для автоматизованої перевірки коду на відповідність певним правилам. Це забезпечує консистентність та якість коду в проєкті.
Делегує застосування правил зовнішньому інструменту командного рядка. Параметр `files` ігнорується, оскільки режим перевірки на рівні окремих файлів відсутній. Публічна функція `lint` повертає код виходу інструменту, який визначає успішність або невдачу перевірки.
## Поведінка
1. Запускає наявний CLI для перевірки правил.
2. Проводить аналіз всього репозиторію, ігноруючи вказаний список файлів.
3. Повертає код завершення процесу.
4. Не використовує кеш результатів.
5. Не здійснює взаємодії з мережею.
1. Викликає зовнішній інструмент для перевірки коду.
2. Повертає код виходу цього інструменту.
## Гарантії поведінки
- Делегує правила до існуючого CLI.
- Не враховує `files` в режимі per-file.
- Не має кешування.
- Read-only: файл не виконує операцій запису у файлову систему.
- Не звертається до мережі.

@@ -0,1 +1,8 @@

---
docgen:
source: npm/rules/ga/js/workflows.mjs
crc: a9296cf1
score: 100
---
# workflows.mjs

@@ -5,29 +12,23 @@

Файл перевіряє GitHub Actions workflows на відповідність певним правилам та стандартам. Він забезпечує консистентність та якість workflow, виявляючи потенційні проблеми, такі як відсутність clean/lint workflow або використання непідтримуваних інструментів. Це частина автоматизованого процесу забезпечення дотримання найкращих практик при розробці GitHub Actions.
Модуль виявляє наявність інструменту `shellcheck` та підтверджує відповідність конфігурації проєкту правилам ga.mdc. Він перевіряє відповідність workflow-файлів Rego-полісі, наявність обов'язкових компонентів та коректність шляхів тригерів GitHub Actions. Модуль свідомо пропускає шляхи `.github` та `.git` під час перевірки.
## Поведінка
- `checkShellcheckInstalled`: Перевіряє наявність бінарника `shellcheck` в системному PATH.
- `checkGaWorkflowFiles`: Перевіряє наявність workflow-файлів з розширенням `.yml` та відсутність інших розширень.
- `runAllGaRego`: Запускає Rego-перевірки для всіх workflow-файлів, використовуючи `conftest` для аналізу.
- `check`: Координує всі перевірки, включаючи Rego-аналіз, перевірку workflow-структури та перевірку наявність файлів.
checkShellcheckInstalled перевіряє наявність бінарника `shellcheck` у системному шляху, щоб забезпечити узгодженість локальних та CI-перевірок (ga.mdc).
check виконує комплексну валідацію конфігурації проєкту щодо правил ga.mdc, включаючи структурну перевірку workflow-файлів за допомогою Rego-полісі, перевірку наявності обов'язкових компонентів, валідацію шляхів тригерів GitHub Actions, пошук залишків MegaLinter та перевірку наявності `shellcheck` (ga.mdc). При цьому ігнорує директорії `.github` та `.git`.
## Публічний API
- checkShellcheckInstalled — Перевіряє наявність `shellcheck` у системі та зупиняє workflow, якщо його немає.
- check — Перевіряє відповідність проєкту правилам валідації.
- runAllGaRego — Запускає Rego-перевірку правил, як перший етап валідації.
- lint-ga — Запускає перевірку `bun lint-ga` з використанням `actionlint` та `zizmor`.
checkShellcheckInstalled — визначає, чи встановлено `shellcheck` у системному шляху. Це дозволяє `actionlint` запускати перевірки скриптів лише тоді, коли інструмент доступний локально.
check — порівнює структуру проєкту з вимогами, визначеними у правилах (ga.mdc).
Plan B-патерн — виконує первинну перевірку структури робочого процесу за допомогою Rego-полісі. Потім виконує перевірки на рівні JavaScript, включаючи пошук файлів та конфігурацій. Команда `bun run lint-ga` повторно викликає цю ж перевірку, використовуючи зовнішні інструменти.
## Гарантії поведінки
- Гарантується наявність файлів `package.json`, `.vscode/*` та `.github/zizmor.yml`.
- Гарантується, що workflow використовує `actions/checkout@v6` перед локальними операціями.
- Гарантується, що workflow використовує composite action `action.yml` з `npx @nitra/cursor`.
- Гарантується, що workflow містить `clean-ga-workflows.yml`, `clean-merged-branch.yml`, `lint-ga.yml` та `git-ai.yml`.
- Гарантується, що workflow використовує `concurrency` для паралельного виконання.
- Гарантується, що workflow не містить `oven-sh/setup-bun`, `actions/cache`, `bun install` у `uses` або `run`.
- Гарантується, що workflow не використовує shell-продовження `\` у `run`.
- Гарантується, що workflow використовує `shellcheck` локально.
- Гарантується, що workflow перевіряє наявність файлів за допомогою `git ls-files :(glob)` та `on.*.paths`.
- Гарантується, що workflow перевіряє наявність файлів, що залишилися від MegaLinter.
- Read-only: файл не виконує операцій запису у файлову систему.
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
- За невдалої перевірки повертає `false`/`null` замість винятку.
- Свідомо пропускає шляхи: `.github`, `.git`.
- Не звертається до мережі.

@@ -1,264 +0,29 @@

# tooling.mjs
## Огляд
Модуль `npm/rules/graphql/js/tooling.mjs` — це **check-скрипт правила `graphql.mdc`**, який перевіряє, що репозиторій налаштований для роботи з GraphQL за наявності у коді tagged template literals `gql\`...\``.
Логіка перевірки **умовна**:
1. Скрипт рекурсивно обходить дерево проєкту з кореня (`process.cwd()` за замовчуванням) і збирає файли-кандидати (`.vue`, `.js`, `.ts`, `.jsx`, `.tsx` тощо) — пропускаючи службові артефакти типу `.d.ts`, `auto-imports.d.ts` тощо.
2. Для кожного кандидата виконує AST-сканування (oxc-parser; для `.vue` — після витягування блоку `<script>`) у пошуках `gql` tagged template literal.
3. Якщо **жодного збігу не знайдено** — перевірка завершується успішно й нічого більше не вимагає.
4. Якщо `gql\`…\`` **знайдено хоча б в одному файлі** — модуль вимагає:
- наявність файлу `.graphqlrc.yml` у корені репозиторію (GraphQL Config);
- відповідність `.vscode/extensions.json` rego-пакету `graphql.vscode_extensions` (тобто рекомендацію розширення VS Code `graphql.vscode-graphql`).
Модуль є частиною інфраструктури `n-cursor` для перевірок правил `.mdc` і дотримується контракту check-скрипта: повертає exit code `0` при успіху й `1` при порушенні.
## Експорти / API
| Експорт | Тип | Призначення |
| ----------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------- |
| `GRAPHQL_RC_FILENAME` | `string` (const, `.graphqlrc.yml`) | Очікувана назва файлу GraphQL Config у корені проєкту (з `graphql.mdc`). |
| `REQUIRED_GRAPHQL_VSCODE_EXTENSION` | `string` (const, `graphql.vscode-graphql`) | Ідентифікатор обов'язкового розширення VS Code, яке має бути в `recommendations`. |
| `check(cwd?)` | `async function` | Основна точка входу — виконує всю перевірку правила `graphql.mdc` і повертає exit code. |
Внутрішні (не експортовані) хелпери:
- `collectScanCandidates(root, ignorePaths)` — збір абсолютних шляхів файлів для сканування.
- `collectGqlHits(root, candidates)` — фільтрація кандидатів за наявністю `gql` tagged template.
- `checkExtensionsRecommendation(pass, fail, cwd)` — делегування перевірки `.vscode/extensions.json` rego-пакету `graphql.vscode_extensions` через `conftest`.
## Функції
### `collectScanCandidates(root, ignorePaths)`
**Сигнатура:**
```js
async function collectScanCandidates(root: string, ignorePaths: string[]): Promise<string[]>
```
**Параметри:**
- `root` — абсолютний шлях до кореня репозиторію, з якого починається обхід.
- `ignorePaths` — масив абсолютних шляхів каталогів, які повністю виключаються з обходу (формується через `loadCursorIgnorePaths`).
**Що робить:**
- Викликає `walkDir(root, visitor, ignorePaths)` (рекурсивний обхід файлової системи).
- Для кожного відвіданого файлу обчислює відносний шлях від `root`, нормалізує роздільники (Windows `\` → POSIX `/`).
- Застосовує два фільтри:
- `shouldSkipFileForGqlScan(rel)` — пропуск службових файлів (`.d.ts`, `auto-imports.d.ts` тощо).
- `isGqlScanSourceFile(rel)` — допуск лише відповідних розширень (Vue/JS/TS-сімейство).
- Накопичує **абсолютні** шляхи прийнятих файлів у масив `candidates`.
**Повертає:** `Promise<string[]>` — список абсолютних шляхів файлів-кандидатів.
**Side effects:** читає метадані файлової системи (через `walkDir`); записів не робить.
---
### `collectGqlHits(root, candidates)`
**Сигнатура:**
```js
async function collectGqlHits(root: string, candidates: string[]): Promise<string[]>
```
**Параметри:**
- `root` — абсолютний шлях до кореня (для обчислення відносних шляхів у результаті).
- `candidates` — список абсолютних шляхів файлів, отриманих від `collectScanCandidates`.
**Що робить:**
- Послідовно (`for-of` + `await`) для кожного абсолютного шляху:
- обчислює відносний шлях і нормалізує роздільники до `/`;
- читає вміст файлу через `readFile(absPath, 'utf8')`;
- викликає `sourceFileHasGqlTaggedTemplate(content, rel)` — парсер AST oxc, що враховує особливості `.vue` (витягування `<script>`);
- якщо результат `true` — додає **відносний** шлях до результуючого масиву `hits`.
**Повертає:** `Promise<string[]>` — відносні шляхи файлів, у яких знайдено хоча б одне `gql` tagged template.
**Side effects:** читання вмісту файлів з диска. Запис відсутній.
docgen:
source: npm/rules/graphql/js/tooling.mjs
crc: eb6b4713
score: 100
---
### `checkExtensionsRecommendation(pass, fail, cwd)`
# tooling.mjs
**Сигнатура:**
## Огляд
```js
function checkExtensionsRecommendation(
pass: (msg: string) => void,
fail: (msg: string) => void,
cwd: string
): void
```
Визначає ім'я файлу конфігурації (`GRAPHQL_RC_FILENAME` = ".graphqlrc.yml") та обов'язкове розширення VS Code (`REQUIRED_GRAPHQL_VSCODE_EXTENSION` = "graphql.vscode-graphql"). Перевіряє наявність конфігурації в `extensions.json` (конфігурація, на яку спирається код) та підтверджує необхідність розширення для роботи з GraphQL (graphql.mdc).
**Параметри:**
## Поведінка
- `pass` — функція-репортер «успішно» (отримана з `createCheckReporter()`).
- `fail` — функція-репортер «порушення».
- `cwd` — абсолютний корінь репозиторію.
GRAPHQL_RC_FILENAME: Повертає ім'я файлу конфігурації GraphQL.
REQUIRED_GRAPHQL_VSCODE_EXTENSION: Повертає ім'я розширення VS Code, необхідне для GraphQL.
check: Перевіряє наявність `gql` tagged template у джерелах. Якщо знайдено, вимагає наявності `.graphqlrc.yml` та валідує `.vscode/extensions.json` щодо `graphql.vscode-graphql` (graphql.mdc).
**Що робить:**
## Публічний API
1. Формує відносний (`.vscode/extensions.json`) і абсолютний шляхи до файлу VS Code-конфігу.
2. Якщо файл **не існує** (`existsSync`) — викликає `fail(...)` з повідомленням про те, що треба створити файл і додати `graphql.vscode-graphql` у `recommendations`, посилаючись на `graphql.mdc`. Виходить.
3. Інакше викликає `runConftestBatch({ policyDirRel: 'graphql/vscode_extensions', namespace: 'graphql.vscode_extensions', files: [pathAbs] })` — делегує перевірку rego-пакету conftest-у.
4. Якщо `violations.length === 0` — викликає `pass(...)` з повідомленням про відповідність. Інакше — для кожного порушення `v` викликає `fail(v.message)`.
GRAPHQL_RC_FILENAME — вказує на файл конфігурації GraphQL у корені проєкту (graphql.mdc).
REQUIRED_GRAPHQL_VSCODE_EXTENSION — вимагає встановлення розширення `graphql.vscode-graphql` для роботи з graphql.mdc.
check — перевіряє наявність файлу `.graphqlrc.yml` та розширення `graphql.vscode-graphql` при використанні тегів GraphQL.
**Повертає:** нічого (`void`). Результати фіксуються через `pass` / `fail`.
## Гарантії поведінки
**Side effects:**
- читання `.vscode/extensions.json` через зовнішній conftest-процес;
- виклик `conftest` (binary) усередині `runConftestBatch`;
- зміна стану внутрішнього reporter-у (накопичення успіхів/порушень).
**Виклик умовний:** виконується лише після того, як основна функція `check` виявила `gql` у дереві.
---
### `check(cwd = process.cwd())`
**Сигнатура:**
```js
export async function check(cwd?: string): Promise<number>
```
**Параметри:**
- `cwd` — _необов'язковий_ абсолютний шлях до кореня репозиторію. За замовчуванням `process.cwd()`.
**Що робить (потік):**
1. Створює репортер `createCheckReporter()` і деструктурує з нього `pass`, `fail`.
2. Завантажує список ігнорованих шляхів через `loadCursorIgnorePaths(root)`.
3. Викликає `collectScanCandidates(root, ignorePaths)` — отримує список файлів-кандидатів.
4. Викликає `collectGqlHits(root, candidates)` — отримує файли з `gql`.
5. **Розгалуження:**
- Якщо `hits.length === 0` — викликає `pass(...)` з повідомленням «немає `gql\`…\``у джерелах, переглянуто N файлів —`.graphqlrc.yml`не вимагається» і **повертає**`reporter.getExitCode()`(зазвичай`0`).
- Інакше викликає `pass(...)` зі звітом про N файлів (з перших 5 з суфіксом `…` якщо більше).
6. Перевіряє наявність `GRAPHQL_RC_FILENAME` (`.graphqlrc.yml`) у корені через `existsSync`:
- Існує → `pass('.graphqlrc.yml існує')`.
- Не існує → `fail(...)` з вимогою додати GraphQL Config (з посиланням на `graphql.mdc`).
7. Викликає `checkExtensionsRecommendation(pass, fail, root)` для перевірки `.vscode/extensions.json`.
8. Повертає `reporter.getExitCode()`: `0` — усі перевірки пройдені, `1` — є хоча б одне порушення.
**Повертає:** `Promise<number>` — exit code (`0` — OK, `1` — порушення).
**Side effects:**
- читання файлової системи (метадані + вміст файлів кандидатів);
- читання `.cursor/...` ignore-конфігу;
- читання `.vscode/extensions.json` (опосередковано через conftest);
- запуск процесу `conftest` через `runConftestBatch`;
- мутація стану внутрішнього reporter-у (накопичення pass/fail-повідомлень).
## Залежності
### Node.js builtins
| Модуль | Що використовується | Призначення |
| ------------------ | ------------------- | ---------------------------------------------------------------------------- |
| `node:fs` | `existsSync` | Синхронна перевірка наявності `.graphqlrc.yml` та `.vscode/extensions.json`. |
| `node:fs/promises` | `readFile` | Асинхронне читання вмісту файлів-кандидатів. |
| `node:path` | `join`, `relative` | Збирання абсолютних шляхів і обчислення відносних шляхів від кореня. |
### Внутрішні модулі репозиторію
- `../../../scripts/lib/check-reporter.mjs` — `createCheckReporter`: фабрика репортера зі стандартним інтерфейсом `pass` / `fail` / `getExitCode()`.
- `../lib/graphql-gql-scan.mjs`:
- `isGqlScanSourceFile(rel)` — предикат «це source-файл, який потенційно містить `gql`» (за розширенням);
- `shouldSkipFileForGqlScan(rel)` — предикат «цей файл слід ігнорувати при скануванні» (`.d.ts`, `auto-imports.d.ts` тощо);
- `sourceFileHasGqlTaggedTemplate(content, rel)` — AST-перевірка на наявність `gql\`...\``(через oxc-parser; для`.vue`— після витягування`<script>`).
- `../../../scripts/lib/load-cursor-config.mjs` — `loadCursorIgnorePaths(root)`: повертає абсолютні шляхи каталогів, повністю виключених з обходу (узгоджено з іншими check-скриптами).
- `../../../scripts/lib/run-conftest-batch.mjs` — `runConftestBatch({ policyDirRel, namespace, files })`: запускає `conftest` з rego-пакетом і повертає масив `violations` (об'єкти з `.message`).
- `../../../scripts/utils/walkDir.mjs` — `walkDir(root, visitor, ignorePaths)`: рекурсивний обхід файлової системи з підтримкою списку ігнорування.
### Зовнішні артефакти (не імпортуються, але потрібні в runtime)
- **`conftest`** як CLI-binary (запускається з `runConftestBatch`).
- **Rego-політика** в `graphql/vscode_extensions` з namespace `graphql.vscode_extensions` (перевіряє `.vscode/extensions.json`).
- **`.cursor/ignore`-конфіг** у корені (читається `loadCursorIgnorePaths`).
- **`graphql.mdc`** — правило, яке цей check реалізує.
## Потік виконання / Використання
### Запуск як check
Модуль використовується з єдиною експортованою функцією `check`:
```js
import { check } from 'npm/rules/graphql/js/tooling.mjs'
const exitCode = await check() // або await check('/absolute/path/to/repo')
process.exit(exitCode)
```
Типово викликається диспетчером check-скриптів інфраструктури `n-cursor` (наприклад, із `npm/scripts/...`), який отримує exit code й агрегує результати по всіх правилах `.mdc`.
### Послідовність дій усередині `check`
```
process.cwd() (або переданий cwd)
createCheckReporter() ──► { pass, fail, getExitCode }
loadCursorIgnorePaths(root) ──► ignorePaths
collectScanCandidates(root, ignorePaths)
│ walkDir(root, visitor, ignorePaths)
│ filter: !shouldSkipFileForGqlScan && isGqlScanSourceFile
candidates: string[]
collectGqlHits(root, candidates)
│ for each: readFile + sourceFileHasGqlTaggedTemplate (oxc-parser AST)
hits: string[]
├── hits.length === 0 ──► pass("немає gql у джерелах") ──► return 0
└── hits.length > 0
├── pass("Знайдено gql у N файлі(ах): ...")
├── existsSync(.graphqlrc.yml)
│ ├── true ──► pass(".graphqlrc.yml існує")
│ └── false ──► fail("Відсутній .graphqlrc.yml ...")
├── checkExtensionsRecommendation(pass, fail, root)
│ ├── !existsSync(.vscode/extensions.json) ──► fail(...)
│ └── runConftestBatch(graphql/vscode_extensions)
│ ├── violations.length === 0 ──► pass(...)
│ └── for v of violations: fail(v.message)
└── return reporter.getExitCode() (0 або 1)
```
### Семантика повідомлень
- `pass` — інформативне повідомлення про успішну перевірку (логнеться як OK, не впливає на exit code).
- `fail` — повідомлення про порушення; навіть одне `fail` робить `getExitCode()` рівним `1`.
### Чому перевірка умовна
`graphql.mdc` вимагає `.graphqlrc.yml` і VS Code-розширення `graphql.vscode-graphql` **лише** для проєктів, де реально використовуються `gql` tagged template literals. У монорепо це дозволяє не «спамити» правилом workspace-и, що не торкаються GraphQL — check спочатку доводить релевантність (`hits.length > 0`), і лише потім вимагає інфраструктуру.
### Обмеження та особливості
- Шляхи в `hits` та в попередженнях завжди **відносні** до `root`, з POSIX-роздільниками `/` (навіть на Windows).
- Перші 5 файлів-збігів показуються у повідомленні `pass`; решта приховується за суфіксом `…`.
- Якщо `gql` знайдено, але `.vscode/extensions.json` відсутній — це порушення, незалежне від rego-перевірки (тобто `fail` без виклику conftest).
- `collectGqlHits` читає файли **послідовно** (без `Promise.all`) — це навмисно, щоб не перевантажувати I/O при великих репозиторіях.
- Виявлення `gql` — на рівні AST (oxc-parser), а не регулярних виразів; рядкові збіги типу `// gql\`...\`` у коментарі не дають false-positive.
- Read-only: файл не виконує операцій запису у файлову систему.
- Не звертається до мережі.

@@ -1,120 +0,27 @@

# fix.mjs — entry-point правила `hasura`
---
docgen:
source: npm/rules/hasura/fix.mjs
crc: 12fc1644
score: 100
---
# fix.mjs
## Огляд
Файл `npm/rules/hasura/fix.mjs` — тонкий entry-point для правила з ідентифікатором `hasura` у складі CLI-пакета `@nitra/cursor`. Виконує дві ролі одночасно:
Цей файл ініціює застосування правил. Він запускає процес, що включає перевірку JS-занепокоєнь та політик. При самостійному запуску скрипт завантажує конфігурацію та вайтлістинг, а також надає звіт про виконання.
1. **Library mode** — експортує функцію `run(ctx)`, яка викликається з оркестратора CLI (наприклад, з `runRuleCli` або з агрегаторів типу `n-fix`), що дозволяє запускати правило в межах загального прогону всіх правил без породження окремого процесу.
2. **Standalone mode** — якщо файл виконується безпосередньо (через `bun rules/<id>/fix.mjs` або `node rules/<id>/fix.mjs`), він виступає самостійним CLI-входом, повністю еквівалентним `npx @nitra/cursor fix hasura`: підвантажує config, застосовує whitelist, друкує summary та повертає процесний exit-code.
## Поведінка
Уся фактична логіка правила (послідовність кроків `applies → JS-concerns → policy → mdc-refs`) винесена в спільну реалізацію `runStandardRule` із бібліотечного шару `scripts/lib/`. Файл-обгортка несе лише прив'язку правила до власної директорії (через `import.meta.dirname`) та dispatch між двома режимами запуску.
1. Викликати функцію `run` для виконання правила. Це ініціює процес застосування правил, включаючи перевірку JS-занепокоєнь, політик та посилань MDC.
2. Якщо скрипт виконується як окрема утиліта (standalone), викликати механізм оркестрації для виконання правила. Це забезпечує повну функціональність, включаючи завантаження конфігурації, вайтлістинг та підсумок.
Відповідає конвенції двороль­ового `fix.mjs`, ухваленій для всіх «стандартних» правил у `npm/rules/*/`, тож має мінімальну поверхню коду й не містить специфіки `hasura`: тип правила, скоупи, перевірки та поліcі описуються деінде (`meta.json`, `hasura.mdc`, `js/`, `policy/`).
## Публічний API
## Експорти / API
run — виконує послідовність дій: застосовує правила, обробляє JavaScript-занепокоєння, перевіряє політику та посилання MDC.
| Експорт | Тип | Призначення |
| ------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `run` | `(ctx?: RuleContext) => Promise<number>` | Library-mode прогін правила в спільному CLI orchestration. Повертає `0` при відсутності порушень або `1` при наявності порушень. |
## Гарантії поведінки
Файл не має `default`-експорту й не експортує жодних інших символів.
Поведінка при безпосередньому запуску файлу як скрипта — побічний ефект (top-level `await runRuleCli(...)` + `process.exit(...)`) і не є частиною програмного API.
## Функції
### `run(ctx)`
Сигнатура: `export function run(ctx)`.
- **Параметри:**
- `ctx` (`RuleContext`, опційний) — контекст прогону, що передається з оркестратора. Тип імпортовано (через JSDoc-tag `@param`) з `../../scripts/lib/run-standard-rule.mjs`. Очікувані поля контексту (за конвенцією `runStandardRule`) — спільні для всіх правил, серед них зокрема `walkCache` (кеш обходу файлової системи, щоб не повторювати walk між кількома правилами в одному прогоні). Якщо `ctx` не передано (`undefined`), `runStandardRule` сам ініціалізує мінімально необхідний контекст.
- **Повертає:** `Promise<number>` — exit-code правила:
- `0` — застосовні файли пройшли всі етапи (`applies → JS-concerns → policy → mdc-refs`) без виявлених порушень.
- `1` — знайдено принаймні одне порушення (або зафіксована помилка валідації, що інтерпретується як failure).
- **Side effects:**
- Делегує всю роботу `runStandardRule(import.meta.dirname, ctx)`, який, своєю чергою, читає файли правила (`meta.json`, `*.mdc`, `js/*.mjs`, `policy/*.rego`) із директорії, обходить цільові файли проєкту, друкує діагностичні повідомлення у stdout/stderr та повертає підсумкове число порушень. Сам `run` стану не модифікує.
- Прив'язує правило до своєї директорії саме через `import.meta.dirname`, тож при переміщенні файлу `fix.mjs` правило автоматично «переїде» разом із його артефактами (без хардкоду шляхів).
### Безіменна процедура top-level (standalone-блок)
```js
if (isRunAsCli(import.meta.url)) {
process.exit(await runRuleCli(import.meta.dirname))
}
```
- **Тип:** виконується один раз під час завантаження модуля.
- **Параметри:** немає (CLI-аргументи зчитуються всередині `runRuleCli` через `process.argv`).
- **Повертає:** нічого (виконує `process.exit`).
- **Side effects:**
- `isRunAsCli(import.meta.url)` визначає, чи модуль завантажено як прямий entry-point (а не як `import`).
- У випадку прямого запуску викликає `runRuleCli(import.meta.dirname)`, який повертає числовий exit-code, і негайно завершує процес із цим кодом через `process.exit`.
- Через `process.exit` подальші асинхронні задачі (мікротаски, відкриті ресурси) можуть бути обірвані — це свідомо: standalone entry-point повинен повертати чіткий код для CI/IDE-інтеграцій.
- **Локальні правила лінту:**
- Поряд із викликом стоїть прагма `// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE`, оскільки за загальним правилом `process.exit` заборонений, але для CLI-входів дозволений.
## Залежності
### Внутрішні (відносні імпорти)
- `../../scripts/lib/run-rule-cli.mjs`
- `isRunAsCli(metaUrl: string): boolean` — детектор «чи запущено модуль як скрипт». Зазвичай порівнює `import.meta.url` із URL виконуваного скрипта (`process.argv[1]`), щоб розрізнити `import` та прямий запуск.
- `runRuleCli(ruleDir: string): Promise<number>` — повноцінний CLI-runner: парсить аргументи, читає глобальний config (whitelist, severity, опції), запускає правило з директорії `ruleDir` і друкує summary. Повертає exit-code.
- `../../scripts/lib/run-standard-rule.mjs`
- `runStandardRule(ruleDir: string, ctx?: RuleContext): Promise<number>` — стандартна послідовність кроків для «звичайного» правила: `applies → JS-concerns → policy → mdc-refs`. Це публічне ядро правил-обгорток на кшталт `fix.mjs`.
- `RuleContext` (тип) — імпортовано лише в JSDoc для типізації параметра `run`.
### Зовнішні
Прямих залежностей від npm-пакетів файл не має. Усі сторонні бібліотеки, які можуть знадобитися (наприклад, walker, парсер `.mdc`, OPA-обгортки тощо), використовуються транзитивно через `run-standard-rule.mjs` та `run-rule-cli.mjs`.
### Залежність від файлів-побратимів у директорії правила
Хоча `fix.mjs` сам нічого з них не читає, `runStandardRule(import.meta.dirname)` спирається на сусідні артефакти у тій же директорії правила `hasura/`:
- `meta.json` — метадані правила (id, scope, опції тощо);
- `hasura.mdc` — людинозрозумілий опис правила у форматі Cursor `.mdc`;
- `js/` — JS-перевірки правила (`check-*.mjs`, fix-helpers);
- `policy/` — Rego-полісі для OPA-кроку.
Тож структурно файл `fix.mjs` має сенс лише як частина повної папки правила.
## Потік виконання / Використання
### Сценарій A — Library mode (з оркестратора)
1. Оркестратор (`n-cursor` CLI, агрегатор скілів, тести) робить `import { run } from '<path>/npm/rules/hasura/fix.mjs'`.
2. Викликає `await run(ctx)`, передаючи контекст із попередньо побудованим `walkCache` та іншими опціями.
3. `run` синхронно повертає результат виклику `runStandardRule(import.meta.dirname, ctx)`.
4. `runStandardRule`:
- читає `meta.json` із директорії правила;
- проганяє послідовність `applies` (фільтр цільових файлів) → JS-concerns (динамічно підвантажені `js/check-*.mjs`) → policy (OPA з `policy/*.rego`) → mdc-refs (валідація `.mdc`-посилань);
- агрегує знахідки й повертає кількість порушень як exit-code (`0`/`1`).
5. Оркестратор підсумовує exit-коди всіх правил.
### Сценарій B — Standalone CLI
1. Користувач/CI виконує `bun npm/rules/hasura/fix.mjs [args...]`.
2. На завантаженні модуля Node/Bun обчислює `isRunAsCli(import.meta.url)` — для прямого запуску результат `true`.
3. Виконується `await runRuleCli(import.meta.dirname)`:
- парсинг CLI-аргументів (`--whitelist`, `--severity`, прапори `-q`/`-v` тощо — деталі в `run-rule-cli.mjs`);
- підвантаження конфіг-файлу пакета;
- застосування whitelist;
- виклик внутрішнього еквівалента `runStandardRule` для цієї ж директорії;
- друк summary-таблиці результатів.
4. `process.exit(<exit-code>)` миттєво завершує процес зі значенням, яке повернув `runRuleCli`.
### Типові випадки використання
- **CI:** `bun npm/rules/hasura/fix.mjs` як standalone-крок у пайплайні; ненульовий exit-code зриває build.
- **Локально:** `npx @nitra/cursor fix hasura` запускає той самий entry-point через диспетчер CLI пакета (а не напряму).
- **Інтеграція в IDE:** редактор може імпортувати `run` і викликати його в окремому процесі для отримання структурованого результату по правилу `hasura` (наприклад, через виклик `bun -e "import('...fix.mjs').then(m => m.run())"`).
- **Агрегатори скілів** (наприклад, `n-fix`): паралельно або послідовно імпортують `run` із усіх правил `npm/rules/*/fix.mjs`, передають спільний `ctx` із `walkCache`, щоб уникнути повторного обходу файлової системи.
### Технічні зауваження щодо реалізації
- `import.meta.dirname` (Node ≥ 20.11 / Bun) повертає абсолютний шлях до директорії модуля без потреби в `fileURLToPath(import.meta.url)`. Це поточний рекомендований спосіб локалізації власної директорії в ESM-модулях.
- Прапор `await` у `process.exit(await runRuleCli(...))` працює завдяки top-level await у ESM — додаткової `main()`-обгортки не потрібно.
- Свідомо обираний `process.exit` (а не «м'який» вихід через `process.exitCode = ...`) гарантує детермінований код повернення для CI/IDE навіть якщо хтось у фоновому таску щось «недописав».
- Файл нейтральний щодо помилок: `runStandardRule` та `runRuleCli` самі вирішують, чи виносити exception у консоль і конвертувати її в exit-code; `fix.mjs` не обгортає виклики в `try/catch`.
- Read-only: файл не виконує операцій запису у файлову систему.
- Кешує результати в межах одного прогону.
- Не звертається до мережі.

@@ -18,5 +18,11 @@ /**

const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
// Після обрізання template-частини URL має лишитися host (R10).
const STATIC_URL_RE = /^https?:\/\/[^/${]+/
const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]+)\2/g
const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
// Повне ім'я json-конфіга (з опційним провідним дотом). Lookbehind `(?<![\w.])`
// не дає почати матч усередині складеного імені — інакше `settings.local.json`
// дало б хибний анкор `.local.json`, а `capacitor.config.json` → `.config.json`,
// і модель, маючи їх у «обов'язкових анкорах», писала б неіснуючий файл як факт.
const CONFIG_REF_RE = /(?<![\w.])(\.?[a-z][\w.-]*\.json)\b/gi
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//

@@ -54,3 +60,12 @@ const CODE_BLOCK_RE = /```[a-z]{0,12}\n([\s\S]*?)\n[ \t]{0,8}\*?[ \t]{0,8}```/g

export function extractAnchors(src) {
const urls = uniq(Array.from(src.matchAll(URL_RE), m => m[0]))
// R10: template-literal URL (`https://h/${expr}/x`) — обрізаємо на `${`, лишаючи
// статичний префікс. Інакше анкор тягне у доку сміття типу `…/${encodeURIComponent(name`.
const urls = uniq(
Array.from(src.matchAll(URL_RE), m => m[0])
.map(u => {
const i = u.indexOf('${')
return i === -1 ? u : u.slice(0, i)
})
.filter(u => STATIC_URL_RE.test(u))
)

@@ -79,2 +94,13 @@ const magicStrings = []

/**
* Плоский список анкор-токенів, які мають дослівно зʼявитися в документі (R5):
* URLs, імена констант-рядків, маркери `(rule.mdc)`, конфіги. Приклади й
* code-блоки опускаються — їх багаторядковість не звіряється підрядком.
* @param {ReturnType<typeof extractAnchors>} a анкори файлу
* @returns {string[]} токени для перевірки покриття/валідності
*/
export function anchorTokens(a) {
return [...a.urls, ...a.magicStrings.map(s => s.name), ...a.errorMarkers.map(m => `(${m})`), ...a.configRefs]
}
/**
* Форматує анкори у компактний текст для system-промпта.

@@ -81,0 +107,0 @@ * Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про

@@ -33,2 +33,5 @@ /** @see ./docs/docgen-extract.md */

const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
const TOP_FN_DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class)\s+(\w+)/gm
const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm

@@ -45,3 +48,6 @@ const NODE_PREFIX_RE = /^node:/

const NETWORK_RE = /\bfetch\(|https?\.|axios|got\(/
const CACHE_RE = /new Map\(\)|Cache|cache/
// Кеш — лише за ІМЕНОВАНИМ маркером (`cache`/`Cache`/`memoize`), не за будь-яким
// `new Map()`: акумулятор (напр. `byPath = new Map()`) — не кеш, а хибна гарантія
// «Кешує результати» гірша за пропуск (фабрикація > мовчання).
const CACHE_RE = /cache|memoi[sz]e/i

@@ -174,2 +180,18 @@ /**

/**
* Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
* Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
* Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
* @param {string} src вміст файлу
* @returns {Array<string>} список імен неекспортованих функцій/класів
*/
function extractLocalSymbols(src) {
const exported = new Set(Array.from(src.matchAll(EXPORT_DECL_RE), m => m[2]))
const out = new Set()
for (const m of src.matchAll(TOP_FN_DECL_RE)) {
if (!exported.has(m[1])) out.add(m[1])
}
return [...out]
}
/**
* Поведінкові маркери — евристики регулярками.

@@ -213,2 +235,3 @@ * @param {string} src вміст файлу

internalSymbols: extractInternalSymbols(src),
localSymbols: extractLocalSymbols(src),
markers: extractMarkers(src)

@@ -215,0 +238,0 @@ }

@@ -10,3 +10,3 @@ /** @see ./docs/docgen-gen.md */

import { extractFacts } from './docgen-extract.mjs'
import { extractAnchors } from './docgen-extract-anchors.mjs'
import { extractAnchors, anchorTokens } from './docgen-extract-anchors.mjs'
import { QUALITY_THRESHOLD } from './docgen-crc.mjs'

@@ -16,2 +16,3 @@ import {

sectionMessages,
overviewMessages,
criticMessages,

@@ -30,2 +31,11 @@ refineMessages,

const CRITIC_NONE_RE = /^\s*NONE\s*$/i
// R4: абстрактні «нічого-не-кажучі» формули, які обходять exact-blocklist і дають score=100
const GENERIC_RE =
/відповідност\S*\s+(?:даних\s+)?(?:визначеному\s+)?контракту|валідаці\S*\s+даних|перевірк\S*\s+(?:відповідності\s+)?даних|обробк\S*\s+даних|застосову\S*\s+логіку|інспекту\S*\s+та\s+збира\S*\s+дан/i
// R7: часті русизми/суржик (курований безпечний список — без false-positive на нормальній мові).
// Без \b: кирилиця не є ASCII-`\w`, тож межі слова в JS-regex не спрацьовують — терміни специфічні.
const SURZHIK_RE =
/пропуская|являється|в залежності|по замовчуванню|на протязі|відповідаюч|слідуюч|наступним разом|приймати участь|у відповідності/i
const ANCHOR_MISS_PENALTY = 5
const ANCHOR_MISS_CAP = 20

@@ -92,8 +102,10 @@ /**

* @param {object} facts факт-лист про файл
* @param {{ anchors?: object|null, src?: string }} [ctx] анкори й джерело для R5
* @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
*/
function scoreDoc(md, facts) {
export function scoreDoc(md, facts, { anchors = null, src = '' } = {}) {
const s = parseSections(md)
let score = 100
const issues = []
const overview = s['огляд'] ?? ''

@@ -105,2 +117,8 @@ if (!s['огляд']) {

// R4: generic-Огляд (парафрази, які обходять exact-blocklist) — як майже-відсутній.
if (GENERIC_RE.test(overview)) {
score -= 35
issues.push('generic-overview')
}
const behavior = s['поведінка'] ?? ''

@@ -121,4 +139,6 @@ if (behavior.length < 60) {

for (const sym of facts.internalSymbols ?? []) {
const inDoc = hasName(guarantees, sym) || hasName(s['огляд'] ?? '', sym) || hasName(s['поведінка'] ?? '', sym)
// R6: службові (неекспортовані) функції не мають фігурувати як публічні
const api = s['публічнийapi'] ?? ''
for (const sym of [...(facts.internalSymbols ?? []), ...(facts.localSymbols ?? [])]) {
const inDoc = hasName(guarantees, sym) || hasName(overview, sym) || hasName(behavior, sym) || hasName(api, sym)
if (inDoc) {

@@ -130,2 +150,21 @@ score -= 10

// R5: кожен валідний анкор (дослівний підрядок src) має зʼявитися в документі
if (anchors && src) {
let missPenalty = 0
for (const tok of anchorTokens(anchors)) {
if (!src.includes(tok)) continue // валідність: фейковий анкор не вимагаємо
if (!md.includes(tok) && missPenalty < ANCHOR_MISS_CAP) {
missPenalty += ANCHOR_MISS_PENALTY
issues.push(`anchor-miss:${tok}`)
}
}
score -= missPenalty
}
// R7: суржик/русизми
if (SURZHIK_RE.test(md)) {
score -= 10
issues.push('surzhik')
}
return { score: Math.max(0, score), issues }

@@ -215,7 +254,7 @@ }

sections.guarantees = guaranteesFromMarkers(facts)
// Спершу Поведінка (+API) — секції з фактажем
for (const s of sectionMessages(facts, src, anc)) {
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
// E2 + E3: critique→refine лише для секцій, де мала модель зриває на generic
if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
// E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
if (s.key === 'api' && apiNeedsRefine(facts)) {
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)

@@ -225,2 +264,8 @@ }

}
// R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
let overview = stripSignatures(
stripSection(callLlm(overviewMessages(facts, sections.behavior ?? '', anc), model, { timeoutMs, temperature }))
)
overview = critiqueRefineSection('overview', overview, facts, anc, model, timeoutMs)
sections.overview = overview
return { md: assemble(basename(facts.relPath), sections) }

@@ -265,3 +310,3 @@ }

// Stage 2.5: детермінований скоринг (0 токенів)
let { score, issues } = scoreDoc(r.md, facts)
let { score, issues } = scoreDoc(r.md, facts, { anchors, src })

@@ -272,3 +317,3 @@ // E4: best-of-2 — один retry з вищою температурою, det-вибір кращого

const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
const s2 = scoreDoc(r2.md, facts)
const s2 = scoreDoc(r2.md, facts, { anchors, src })
if (s2.score > score) {

@@ -275,0 +320,0 @@ r = r2

@@ -55,7 +55,8 @@ /** @see ./docs/docgen-prompts.md */

* Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
* Код потрапляє лише в `behavior`; решта секцій — на факт-листі.
* Код потрапляє лише в `behavior`; «Огляд» генерується окремо ОСТАННІМ
* (`overviewMessages`) з уже написаної Поведінки — тут його немає.
* @param {object} facts факт-лист про файл
* @param {string} src вміст файлу
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (behavior[, api])
*/

@@ -67,16 +68,10 @@ export function sectionMessages(facts, src, anchors = null) {

// Огляд — лише факти (без коду)
const overview = {
key: 'overview',
numPredict: 220,
messages: msgs(
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
)
}
// Поведінка — ЄДИНА секція, якій потрібен код
// R6: Поведінка описує РІВНО експортовані імена, не службові помічники
const exportNames = (facts.exports ?? []).map(e => e.name)
const behaviorTask = multi
? 'для кожної публічної функції — один короткий пункт «що вона робить»'
: 'нумерований алгоритм у бізнес-термінах'
const onlyExports = exportNames.length
? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
: ''
const noInternal = facts.internalSymbols?.length

@@ -90,3 +85,3 @@ ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`

`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
`Напиши вміст секції «Поведінка»: ${behaviorTask}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
)

@@ -96,3 +91,3 @@ }

// API — лише список експортів (без коду)
if (!multi && !facts.exports?.some(e => e.desc)) return [overview, behavior]
if (!multi && !facts.exports?.some(e => e.desc)) return [behavior]
const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')

@@ -107,6 +102,23 @@ const api = {

}
return [overview, behavior, api]
return [behavior, api]
}
/**
* R3 — «Огляд» ОСТАННІМ: узагальнення вже написаної Поведінки, а не здогад із
* голого факт-листа. Лікує generic/хибний Огляд на складних файлах.
* @param {object} facts факт-лист про файл
* @param {string} behaviorText готовий текст секції «Поведінка»
* @param {object|null} [anchors] анкори файлу
* @returns {Array<{role:string,content:string}>} messages-масив для Огляду
*/
export function overviewMessages(facts, behaviorText, anchors = null) {
const factsTxt = factsSummary(facts)
const anch = anchorsBlock(anchors)
return msgs(
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
`На основі вже написаної секції «Поведінка» (нижче) напиши «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Узагальнюй САМЕ описану поведінку, не додавай нових фактів. Без заголовка, без переліку функцій. Заборонені абстрактні формули без конкретики («перевірка/валідація/обробка даних», «відповідність контракту», «застосовує логіку») — пиши, ЩО саме і за яким контрактом.\n\nПОВЕДІНКА:\n${behaviorText}`
)
}
/**
* E2-step 1 — критик. Перевіряє чорнетку секції на конкретні дефекти.

@@ -113,0 +125,0 @@ * Повертає messages для LLM-запиту: вихід має бути СПИСКОМ issues або словом NONE.

Sorry, the diff of this file is too big to display