@nitra/cursor 5.2.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/lib/docs/models.md +24 -17
- package/package.json +1 -1
- package/rules/changelog/js/docs/consistency.md +36 -383
- package/rules/feedback/docs/fix.md +21 -131
- package/rules/ga/docs/fix.md +14 -12
- package/rules/ga/js/docs/lint.md +12 -9
- package/rules/ga/js/docs/workflows.md +20 -19
- package/rules/graphql/js/docs/tooling.md +18 -253
- package/rules/hasura/docs/fix.md +18 -111
- package/skills/doc-files/js/docgen-extract-anchors.mjs +28 -2
- package/skills/doc-files/js/docgen-extract.mjs +24 -1
- package/skills/doc-files/js/docgen-gen.mjs +54 -9
- package/skills/doc-files/js/docgen-prompts.mjs +28 -16
- package/skills/doc-files/js/units-js.mjs +139 -0
- package/skills/doc-files/js/units.mjs +19 -0
package/rules/hasura/docs/fix.md
CHANGED
|
@@ -1,120 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
docgen:
|
|
3
|
+
source: npm/rules/hasura/fix.mjs
|
|
4
|
+
crc: 12fc1644
|
|
5
|
+
score: 100
|
|
6
|
+
---
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Файл `npm/rules/hasura/fix.mjs` — тонкий entry-point для правила з ідентифікатором `hasura` у складі CLI-пакета `@nitra/cursor`. Виконує дві ролі одночасно:
|
|
6
|
-
|
|
7
|
-
1. **Library mode** — експортує функцію `run(ctx)`, яка викликається з оркестратора CLI (наприклад, з `runRuleCli` або з агрегаторів типу `n-fix`), що дозволяє запускати правило в межах загального прогону всіх правил без породження окремого процесу.
|
|
8
|
-
2. **Standalone mode** — якщо файл виконується безпосередньо (через `bun rules/<id>/fix.mjs` або `node rules/<id>/fix.mjs`), він виступає самостійним CLI-входом, повністю еквівалентним `npx @nitra/cursor fix hasura`: підвантажує config, застосовує whitelist, друкує summary та повертає процесний exit-code.
|
|
9
|
-
|
|
10
|
-
Уся фактична логіка правила (послідовність кроків `applies → JS-concerns → policy → mdc-refs`) винесена в спільну реалізацію `runStandardRule` із бібліотечного шару `scripts/lib/`. Файл-обгортка несе лише прив'язку правила до власної директорії (через `import.meta.dirname`) та dispatch між двома режимами запуску.
|
|
11
|
-
|
|
12
|
-
Відповідає конвенції дворольового `fix.mjs`, ухваленій для всіх «стандартних» правил у `npm/rules/*/`, тож має мінімальну поверхню коду й не містить специфіки `hasura`: тип правила, скоупи, перевірки та поліcі описуються деінде (`meta.json`, `hasura.mdc`, `js/`, `policy/`).
|
|
13
|
-
|
|
14
|
-
## Експорти / API
|
|
15
|
-
|
|
16
|
-
| Експорт | Тип | Призначення |
|
|
17
|
-
| ------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
18
|
-
| `run` | `(ctx?: RuleContext) => Promise<number>` | Library-mode прогін правила в спільному CLI orchestration. Повертає `0` при відсутності порушень або `1` при наявності порушень. |
|
|
19
|
-
|
|
20
|
-
Файл не має `default`-експорту й не експортує жодних інших символів.
|
|
21
|
-
|
|
22
|
-
Поведінка при безпосередньому запуску файлу як скрипта — побічний ефект (top-level `await runRuleCli(...)` + `process.exit(...)`) і не є частиною програмного API.
|
|
23
|
-
|
|
24
|
-
## Функції
|
|
25
|
-
|
|
26
|
-
### `run(ctx)`
|
|
27
|
-
|
|
28
|
-
Сигнатура: `export function run(ctx)`.
|
|
29
|
-
|
|
30
|
-
- **Параметри:**
|
|
31
|
-
- `ctx` (`RuleContext`, опційний) — контекст прогону, що передається з оркестратора. Тип імпортовано (через JSDoc-tag `@param`) з `../../scripts/lib/run-standard-rule.mjs`. Очікувані поля контексту (за конвенцією `runStandardRule`) — спільні для всіх правил, серед них зокрема `walkCache` (кеш обходу файлової системи, щоб не повторювати walk між кількома правилами в одному прогоні). Якщо `ctx` не передано (`undefined`), `runStandardRule` сам ініціалізує мінімально необхідний контекст.
|
|
32
|
-
- **Повертає:** `Promise<number>` — exit-code правила:
|
|
33
|
-
- `0` — застосовні файли пройшли всі етапи (`applies → JS-concerns → policy → mdc-refs`) без виявлених порушень.
|
|
34
|
-
- `1` — знайдено принаймні одне порушення (або зафіксована помилка валідації, що інтерпретується як failure).
|
|
35
|
-
- **Side effects:**
|
|
36
|
-
- Делегує всю роботу `runStandardRule(import.meta.dirname, ctx)`, який, своєю чергою, читає файли правила (`meta.json`, `*.mdc`, `js/*.mjs`, `policy/*.rego`) із директорії, обходить цільові файли проєкту, друкує діагностичні повідомлення у stdout/stderr та повертає підсумкове число порушень. Сам `run` стану не модифікує.
|
|
37
|
-
- Прив'язує правило до своєї директорії саме через `import.meta.dirname`, тож при переміщенні файлу `fix.mjs` правило автоматично «переїде» разом із його артефактами (без хардкоду шляхів).
|
|
38
|
-
|
|
39
|
-
### Безіменна процедура top-level (standalone-блок)
|
|
8
|
+
# fix.mjs
|
|
40
9
|
|
|
41
|
-
|
|
42
|
-
if (isRunAsCli(import.meta.url)) {
|
|
43
|
-
process.exit(await runRuleCli(import.meta.dirname))
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
- **Тип:** виконується один раз під час завантаження модуля.
|
|
48
|
-
- **Параметри:** немає (CLI-аргументи зчитуються всередині `runRuleCli` через `process.argv`).
|
|
49
|
-
- **Повертає:** нічого (виконує `process.exit`).
|
|
50
|
-
- **Side effects:**
|
|
51
|
-
- `isRunAsCli(import.meta.url)` визначає, чи модуль завантажено як прямий entry-point (а не як `import`).
|
|
52
|
-
- У випадку прямого запуску викликає `runRuleCli(import.meta.dirname)`, який повертає числовий exit-code, і негайно завершує процес із цим кодом через `process.exit`.
|
|
53
|
-
- Через `process.exit` подальші асинхронні задачі (мікротаски, відкриті ресурси) можуть бути обірвані — це свідомо: standalone entry-point повинен повертати чіткий код для CI/IDE-інтеграцій.
|
|
54
|
-
- **Локальні правила лінту:**
|
|
55
|
-
- Поряд із викликом стоїть прагма `// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE`, оскільки за загальним правилом `process.exit` заборонений, але для CLI-входів дозволений.
|
|
56
|
-
|
|
57
|
-
## Залежності
|
|
58
|
-
|
|
59
|
-
### Внутрішні (відносні імпорти)
|
|
60
|
-
|
|
61
|
-
- `../../scripts/lib/run-rule-cli.mjs`
|
|
62
|
-
- `isRunAsCli(metaUrl: string): boolean` — детектор «чи запущено модуль як скрипт». Зазвичай порівнює `import.meta.url` із URL виконуваного скрипта (`process.argv[1]`), щоб розрізнити `import` та прямий запуск.
|
|
63
|
-
- `runRuleCli(ruleDir: string): Promise<number>` — повноцінний CLI-runner: парсить аргументи, читає глобальний config (whitelist, severity, опції), запускає правило з директорії `ruleDir` і друкує summary. Повертає exit-code.
|
|
64
|
-
- `../../scripts/lib/run-standard-rule.mjs`
|
|
65
|
-
- `runStandardRule(ruleDir: string, ctx?: RuleContext): Promise<number>` — стандартна послідовність кроків для «звичайного» правила: `applies → JS-concerns → policy → mdc-refs`. Це публічне ядро правил-обгорток на кшталт `fix.mjs`.
|
|
66
|
-
- `RuleContext` (тип) — імпортовано лише в JSDoc для типізації параметра `run`.
|
|
67
|
-
|
|
68
|
-
### Зовнішні
|
|
69
|
-
|
|
70
|
-
Прямих залежностей від npm-пакетів файл не має. Усі сторонні бібліотеки, які можуть знадобитися (наприклад, walker, парсер `.mdc`, OPA-обгортки тощо), використовуються транзитивно через `run-standard-rule.mjs` та `run-rule-cli.mjs`.
|
|
71
|
-
|
|
72
|
-
### Залежність від файлів-побратимів у директорії правила
|
|
73
|
-
|
|
74
|
-
Хоча `fix.mjs` сам нічого з них не читає, `runStandardRule(import.meta.dirname)` спирається на сусідні артефакти у тій же директорії правила `hasura/`:
|
|
75
|
-
|
|
76
|
-
- `meta.json` — метадані правила (id, scope, опції тощо);
|
|
77
|
-
- `hasura.mdc` — людинозрозумілий опис правила у форматі Cursor `.mdc`;
|
|
78
|
-
- `js/` — JS-перевірки правила (`check-*.mjs`, fix-helpers);
|
|
79
|
-
- `policy/` — Rego-полісі для OPA-кроку.
|
|
80
|
-
|
|
81
|
-
Тож структурно файл `fix.mjs` має сенс лише як частина повної папки правила.
|
|
82
|
-
|
|
83
|
-
## Потік виконання / Використання
|
|
84
|
-
|
|
85
|
-
### Сценарій A — Library mode (з оркестратора)
|
|
10
|
+
## Огляд
|
|
86
11
|
|
|
87
|
-
|
|
88
|
-
2. Викликає `await run(ctx)`, передаючи контекст із попередньо побудованим `walkCache` та іншими опціями.
|
|
89
|
-
3. `run` синхронно повертає результат виклику `runStandardRule(import.meta.dirname, ctx)`.
|
|
90
|
-
4. `runStandardRule`:
|
|
91
|
-
- читає `meta.json` із директорії правила;
|
|
92
|
-
- проганяє послідовність `applies` (фільтр цільових файлів) → JS-concerns (динамічно підвантажені `js/check-*.mjs`) → policy (OPA з `policy/*.rego`) → mdc-refs (валідація `.mdc`-посилань);
|
|
93
|
-
- агрегує знахідки й повертає кількість порушень як exit-code (`0`/`1`).
|
|
94
|
-
5. Оркестратор підсумовує exit-коди всіх правил.
|
|
12
|
+
Цей файл ініціює застосування правил. Він запускає процес, що включає перевірку JS-занепокоєнь та політик. При самостійному запуску скрипт завантажує конфігурацію та вайтлістинг, а також надає звіт про виконання.
|
|
95
13
|
|
|
96
|
-
|
|
14
|
+
## Поведінка
|
|
97
15
|
|
|
98
|
-
1.
|
|
99
|
-
2.
|
|
100
|
-
3. Виконується `await runRuleCli(import.meta.dirname)`:
|
|
101
|
-
- парсинг CLI-аргументів (`--whitelist`, `--severity`, прапори `-q`/`-v` тощо — деталі в `run-rule-cli.mjs`);
|
|
102
|
-
- підвантаження конфіг-файлу пакета;
|
|
103
|
-
- застосування whitelist;
|
|
104
|
-
- виклик внутрішнього еквівалента `runStandardRule` для цієї ж директорії;
|
|
105
|
-
- друк summary-таблиці результатів.
|
|
106
|
-
4. `process.exit(<exit-code>)` миттєво завершує процес зі значенням, яке повернув `runRuleCli`.
|
|
16
|
+
1. Викликати функцію `run` для виконання правила. Це ініціює процес застосування правил, включаючи перевірку JS-занепокоєнь, політик та посилань MDC.
|
|
17
|
+
2. Якщо скрипт виконується як окрема утиліта (standalone), викликати механізм оркестрації для виконання правила. Це забезпечує повну функціональність, включаючи завантаження конфігурації, вайтлістинг та підсумок.
|
|
107
18
|
|
|
108
|
-
|
|
19
|
+
## Публічний API
|
|
109
20
|
|
|
110
|
-
|
|
111
|
-
- **Локально:** `npx @nitra/cursor fix hasura` запускає той самий entry-point через диспетчер CLI пакета (а не напряму).
|
|
112
|
-
- **Інтеграція в IDE:** редактор може імпортувати `run` і викликати його в окремому процесі для отримання структурованого результату по правилу `hasura` (наприклад, через виклик `bun -e "import('...fix.mjs').then(m => m.run())"`).
|
|
113
|
-
- **Агрегатори скілів** (наприклад, `n-fix`): паралельно або послідовно імпортують `run` із усіх правил `npm/rules/*/fix.mjs`, передають спільний `ctx` із `walkCache`, щоб уникнути повторного обходу файлової системи.
|
|
21
|
+
run — виконує послідовність дій: застосовує правила, обробляє JavaScript-занепокоєння, перевіряє політику та посилання MDC.
|
|
114
22
|
|
|
115
|
-
|
|
23
|
+
## Гарантії поведінки
|
|
116
24
|
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
- Файл нейтральний щодо помилок: `runStandardRule` та `runRuleCli` самі вирішують, чи виносити exception у консоль і конвертувати її в exit-code; `fix.mjs` не обгортає виклики в `try/catch`.
|
|
25
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
26
|
+
- Кешує результати в межах одного прогону.
|
|
27
|
+
- Не звертається до мережі.
|
|
@@ -15,9 +15,15 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
|
|
18
|
+
// Після обрізання template-частини URL має лишитися host (R10).
|
|
19
|
+
const STATIC_URL_RE = /^https?:\/\/[^/${]+/
|
|
18
20
|
const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]+)\2/g
|
|
19
21
|
const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
|
|
20
|
-
|
|
22
|
+
// Повне ім'я json-конфіга (з опційним провідним дотом). Lookbehind `(?<![\w.])`
|
|
23
|
+
// не дає почати матч усередині складеного імені — інакше `settings.local.json`
|
|
24
|
+
// дало б хибний анкор `.local.json`, а `capacitor.config.json` → `.config.json`,
|
|
25
|
+
// і модель, маючи їх у «обов'язкових анкорах», писала б неіснуючий файл як факт.
|
|
26
|
+
const CONFIG_REF_RE = /(?<![\w.])(\.?[a-z][\w.-]*\.json)\b/gi
|
|
21
27
|
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
22
28
|
const CODE_BLOCK_RE = /```[a-z]{0,12}\n([\s\S]*?)\n[ \t]{0,8}\*?[ \t]{0,8}```/g
|
|
23
29
|
|
|
@@ -50,7 +56,16 @@ function uniq(arr) {
|
|
|
50
56
|
* }} категоризовані анкори файлу
|
|
51
57
|
*/
|
|
52
58
|
export function extractAnchors(src) {
|
|
53
|
-
|
|
59
|
+
// R10: template-literal URL (`https://h/${expr}/x`) — обрізаємо на `${`, лишаючи
|
|
60
|
+
// статичний префікс. Інакше анкор тягне у доку сміття типу `…/${encodeURIComponent(name`.
|
|
61
|
+
const urls = uniq(
|
|
62
|
+
Array.from(src.matchAll(URL_RE), m => m[0])
|
|
63
|
+
.map(u => {
|
|
64
|
+
const i = u.indexOf('${')
|
|
65
|
+
return i === -1 ? u : u.slice(0, i)
|
|
66
|
+
})
|
|
67
|
+
.filter(u => STATIC_URL_RE.test(u))
|
|
68
|
+
)
|
|
54
69
|
|
|
55
70
|
const magicStrings = []
|
|
56
71
|
const seenNames = new Set()
|
|
@@ -73,6 +88,17 @@ export function extractAnchors(src) {
|
|
|
73
88
|
return { urls, magicStrings, errorMarkers, configRefs, examples }
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Плоский список анкор-токенів, які мають дослівно зʼявитися в документі (R5):
|
|
93
|
+
* URLs, імена констант-рядків, маркери `(rule.mdc)`, конфіги. Приклади й
|
|
94
|
+
* code-блоки опускаються — їх багаторядковість не звіряється підрядком.
|
|
95
|
+
* @param {ReturnType<typeof extractAnchors>} a анкори файлу
|
|
96
|
+
* @returns {string[]} токени для перевірки покриття/валідності
|
|
97
|
+
*/
|
|
98
|
+
export function anchorTokens(a) {
|
|
99
|
+
return [...a.urls, ...a.magicStrings.map(s => s.name), ...a.errorMarkers.map(m => `(${m})`), ...a.configRefs]
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
/**
|
|
77
103
|
* Форматує анкори у компактний текст для system-промпта.
|
|
78
104
|
* Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
|
|
@@ -30,6 +30,9 @@ const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,
|
|
|
30
30
|
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
31
31
|
const PRECEDING_JSDOC_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
32
32
|
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
33
|
+
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
|
|
34
|
+
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
|
|
35
|
+
const TOP_FN_DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class)\s+(\w+)/gm
|
|
33
36
|
const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
|
|
34
37
|
const NODE_PREFIX_RE = /^node:/
|
|
35
38
|
const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
|
|
@@ -41,7 +44,10 @@ const CATCH_RE = /catch\s*\(/
|
|
|
41
44
|
const TRY_RE = /\btry\s*\{/
|
|
42
45
|
const FALSY_RETURN_RE = /return\s+(false|null|''|"")/
|
|
43
46
|
const NETWORK_RE = /\bfetch\(|https?\.|axios|got\(/
|
|
44
|
-
|
|
47
|
+
// Кеш — лише за ІМЕНОВАНИМ маркером (`cache`/`Cache`/`memoize`), не за будь-яким
|
|
48
|
+
// `new Map()`: акумулятор (напр. `byPath = new Map()`) — не кеш, а хибна гарантія
|
|
49
|
+
// «Кешує результати» гірша за пропуск (фабрикація > мовчання).
|
|
50
|
+
const CACHE_RE = /cache|memoi[sz]e/i
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Прибирає `/** */`-обрамлення й `*`-префікси, повертає чистий текст рядками.
|
|
@@ -168,6 +174,22 @@ function extractInternalSymbols(src) {
|
|
|
168
174
|
return [...out]
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
|
|
179
|
+
* Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
|
|
180
|
+
* Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
|
|
181
|
+
* @param {string} src вміст файлу
|
|
182
|
+
* @returns {Array<string>} список імен неекспортованих функцій/класів
|
|
183
|
+
*/
|
|
184
|
+
function extractLocalSymbols(src) {
|
|
185
|
+
const exported = new Set(Array.from(src.matchAll(EXPORT_DECL_RE), m => m[2]))
|
|
186
|
+
const out = new Set()
|
|
187
|
+
for (const m of src.matchAll(TOP_FN_DECL_RE)) {
|
|
188
|
+
if (!exported.has(m[1])) out.add(m[1])
|
|
189
|
+
}
|
|
190
|
+
return [...out]
|
|
191
|
+
}
|
|
192
|
+
|
|
171
193
|
/**
|
|
172
194
|
* Поведінкові маркери — евристики регулярками.
|
|
173
195
|
* @param {string} src вміст файлу
|
|
@@ -207,6 +229,7 @@ export function extractFacts(src, relPath) {
|
|
|
207
229
|
exports: extractExports(src),
|
|
208
230
|
imports: extractImports(src),
|
|
209
231
|
internalSymbols: extractInternalSymbols(src),
|
|
232
|
+
localSymbols: extractLocalSymbols(src),
|
|
210
233
|
markers: extractMarkers(src)
|
|
211
234
|
}
|
|
212
235
|
}
|
|
@@ -7,11 +7,12 @@ import { DEFAULT_OMLX_MODEL } from '../../../lib/omlx.mjs'
|
|
|
7
7
|
import { callLlm } from '../../../lib/llm.mjs'
|
|
8
8
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
9
9
|
import { extractFacts } from './docgen-extract.mjs'
|
|
10
|
-
import { extractAnchors } from './docgen-extract-anchors.mjs'
|
|
10
|
+
import { extractAnchors, anchorTokens } from './docgen-extract-anchors.mjs'
|
|
11
11
|
import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
12
12
|
import {
|
|
13
13
|
oneShotMessages,
|
|
14
14
|
sectionMessages,
|
|
15
|
+
overviewMessages,
|
|
15
16
|
criticMessages,
|
|
16
17
|
refineMessages,
|
|
17
18
|
guaranteesFromMarkers
|
|
@@ -25,6 +26,15 @@ const SECTION_KEY_CLEAN_RE = /[^а-яіїєґa-z0-9]/gi
|
|
|
25
26
|
const CACHE_MENTION_RE = /кеш/i
|
|
26
27
|
const CACHE_NEGATION_RE = /(?:не|без)\s+(?:\S+\s+)?кеш|немає\s+кеш/i
|
|
27
28
|
const CRITIC_NONE_RE = /^\s*NONE\s*$/i
|
|
29
|
+
// R4: абстрактні «нічого-не-кажучі» формули, які обходять exact-blocklist і дають score=100
|
|
30
|
+
const GENERIC_RE =
|
|
31
|
+
/відповідност\S*\s+(?:даних\s+)?(?:визначеному\s+)?контракту|валідаці\S*\s+даних|перевірк\S*\s+(?:відповідності\s+)?даних|обробк\S*\s+даних|застосову\S*\s+логіку|інспекту\S*\s+та\s+збира\S*\s+дан/i
|
|
32
|
+
// R7: часті русизми/суржик (курований безпечний список — без false-positive на нормальній мові).
|
|
33
|
+
// Без \b: кирилиця не є ASCII-`\w`, тож межі слова в JS-regex не спрацьовують — терміни специфічні.
|
|
34
|
+
const SURZHIK_RE =
|
|
35
|
+
/пропуская|являється|в залежності|по замовчуванню|на протязі|відповідаюч|слідуюч|наступним разом|приймати участь|у відповідності/i
|
|
36
|
+
const ANCHOR_MISS_PENALTY = 5
|
|
37
|
+
const ANCHOR_MISS_CAP = 20
|
|
28
38
|
|
|
29
39
|
/**
|
|
30
40
|
* Прибирає код-фенс-обгортку (потрійні бектіки) й випадковий провідний
|
|
@@ -86,18 +96,26 @@ function hasName(text, sym) {
|
|
|
86
96
|
* Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
|
|
87
97
|
* @param {string} md зібраний документ
|
|
88
98
|
* @param {object} facts факт-лист про файл
|
|
99
|
+
* @param {{ anchors?: object|null, src?: string }} [ctx] анкори й джерело для R5
|
|
89
100
|
* @returns {{ score: number, issues: string[] }} оцінка 0–100 і коди проблем
|
|
90
101
|
*/
|
|
91
|
-
function scoreDoc(md, facts) {
|
|
102
|
+
export function scoreDoc(md, facts, { anchors = null, src = '' } = {}) {
|
|
92
103
|
const s = parseSections(md)
|
|
93
104
|
let score = 100
|
|
94
105
|
const issues = []
|
|
106
|
+
const overview = s['огляд'] ?? ''
|
|
95
107
|
|
|
96
108
|
if (!s['огляд']) {
|
|
97
109
|
score -= 25
|
|
98
110
|
issues.push('no-overview')
|
|
99
111
|
}
|
|
100
112
|
|
|
113
|
+
// R4: generic-Огляд (парафрази, які обходять exact-blocklist) — як майже-відсутній.
|
|
114
|
+
if (GENERIC_RE.test(overview)) {
|
|
115
|
+
score -= 35
|
|
116
|
+
issues.push('generic-overview')
|
|
117
|
+
}
|
|
118
|
+
|
|
101
119
|
const behavior = s['поведінка'] ?? ''
|
|
102
120
|
if (behavior.length < 60) {
|
|
103
121
|
score -= 20
|
|
@@ -113,14 +131,35 @@ function scoreDoc(md, facts) {
|
|
|
113
131
|
issues.push('cache-hallucination')
|
|
114
132
|
}
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
// R6: службові (неекспортовані) функції не мають фігурувати як публічні
|
|
135
|
+
const api = s['публічнийapi'] ?? ''
|
|
136
|
+
for (const sym of [...(facts.internalSymbols ?? []), ...(facts.localSymbols ?? [])]) {
|
|
137
|
+
const inDoc = hasName(guarantees, sym) || hasName(overview, sym) || hasName(behavior, sym) || hasName(api, sym)
|
|
118
138
|
if (inDoc) {
|
|
119
139
|
score -= 10
|
|
120
140
|
issues.push(`internal-name:${sym}`)
|
|
121
141
|
}
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
// R5: кожен валідний анкор (дослівний підрядок src) має зʼявитися в документі
|
|
145
|
+
if (anchors && src) {
|
|
146
|
+
let missPenalty = 0
|
|
147
|
+
for (const tok of anchorTokens(anchors)) {
|
|
148
|
+
if (!src.includes(tok)) continue // валідність: фейковий анкор не вимагаємо
|
|
149
|
+
if (!md.includes(tok) && missPenalty < ANCHOR_MISS_CAP) {
|
|
150
|
+
missPenalty += ANCHOR_MISS_PENALTY
|
|
151
|
+
issues.push(`anchor-miss:${tok}`)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
score -= missPenalty
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// R7: суржик/русизми
|
|
158
|
+
if (SURZHIK_RE.test(md)) {
|
|
159
|
+
score -= 10
|
|
160
|
+
issues.push('surzhik')
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
return { score: Math.max(0, score), issues }
|
|
125
164
|
}
|
|
126
165
|
|
|
@@ -205,15 +244,21 @@ function orchestratedDoc(facts, src, model, timeoutMs, { anchors = null, tempera
|
|
|
205
244
|
const anc = anchors ?? extractAnchors(src)
|
|
206
245
|
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
207
246
|
sections.guarantees = guaranteesFromMarkers(facts)
|
|
247
|
+
// Спершу Поведінка (+API) — секції з фактажем
|
|
208
248
|
for (const s of sectionMessages(facts, src, anc)) {
|
|
209
|
-
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
|
|
210
249
|
let draft = stripSignatures(stripSection(callLlm(s.messages, model, { timeoutMs, temperature })))
|
|
211
|
-
// E2
|
|
212
|
-
if (s.key === '
|
|
250
|
+
// E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
|
|
251
|
+
if (s.key === 'api' && apiNeedsRefine(facts)) {
|
|
213
252
|
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
|
|
214
253
|
}
|
|
215
254
|
sections[s.key] = draft
|
|
216
255
|
}
|
|
256
|
+
// R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
|
|
257
|
+
let overview = stripSignatures(
|
|
258
|
+
stripSection(callLlm(overviewMessages(facts, sections.behavior ?? '', anc), model, { timeoutMs, temperature }))
|
|
259
|
+
)
|
|
260
|
+
overview = critiqueRefineSection('overview', overview, facts, anc, model, timeoutMs)
|
|
261
|
+
sections.overview = overview
|
|
217
262
|
return { md: assemble(basename(facts.relPath), sections) }
|
|
218
263
|
}
|
|
219
264
|
|
|
@@ -253,13 +298,13 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
|
|
|
253
298
|
}
|
|
254
299
|
|
|
255
300
|
// Stage 2.5: детермінований скоринг (0 токенів)
|
|
256
|
-
let { score, issues } = scoreDoc(r.md, facts)
|
|
301
|
+
let { score, issues } = scoreDoc(r.md, facts, { anchors, src })
|
|
257
302
|
|
|
258
303
|
// E4: best-of-2 — один retry з вищою температурою, det-вибір кращого
|
|
259
304
|
if (score < threshold && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
|
|
260
305
|
try {
|
|
261
306
|
const r2 = orchestratedDoc(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
|
|
262
|
-
const s2 = scoreDoc(r2.md, facts)
|
|
307
|
+
const s2 = scoreDoc(r2.md, facts, { anchors, src })
|
|
263
308
|
if (s2.score > score) {
|
|
264
309
|
r = r2
|
|
265
310
|
score = s2.score
|
|
@@ -52,31 +52,26 @@ const msgs = (system, user) => [
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
|
|
55
|
-
* Код потрапляє лише в `behavior`;
|
|
55
|
+
* Код потрапляє лише в `behavior`; «Огляд» генерується окремо ОСТАННІМ
|
|
56
|
+
* (`overviewMessages`) з уже написаної Поведінки — тут його немає.
|
|
56
57
|
* @param {object} facts факт-лист про файл
|
|
57
58
|
* @param {string} src вміст файлу
|
|
58
59
|
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
|
|
59
|
-
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
|
|
60
|
+
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (behavior[, api])
|
|
60
61
|
*/
|
|
61
62
|
export function sectionMessages(facts, src, anchors = null) {
|
|
62
63
|
const factsTxt = factsSummary(facts)
|
|
63
64
|
const anch = anchorsBlock(anchors)
|
|
64
65
|
const multi = (facts.exports?.length || 0) > 1
|
|
65
66
|
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
key: 'overview',
|
|
69
|
-
numPredict: 220,
|
|
70
|
-
messages: msgs(
|
|
71
|
-
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
72
|
-
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Поведінка — ЄДИНА секція, якій потрібен код
|
|
67
|
+
// R6: Поведінка описує РІВНО експортовані імена, не службові помічники
|
|
68
|
+
const exportNames = (facts.exports ?? []).map(e => e.name)
|
|
77
69
|
const behaviorTask = multi
|
|
78
70
|
? 'для кожної публічної функції — один короткий пункт «що вона робить»'
|
|
79
71
|
: 'нумерований алгоритм у бізнес-термінах'
|
|
72
|
+
const onlyExports = exportNames.length
|
|
73
|
+
? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
|
|
74
|
+
: ''
|
|
80
75
|
const noInternal = facts.internalSymbols?.length
|
|
81
76
|
? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
|
|
82
77
|
: ''
|
|
@@ -85,12 +80,12 @@ export function sectionMessages(facts, src, anchors = null) {
|
|
|
85
80
|
numPredict: 500,
|
|
86
81
|
messages: msgs(
|
|
87
82
|
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
88
|
-
`Напиши вміст секції «Поведінка»: ${behaviorTask}
|
|
83
|
+
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
89
84
|
)
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
// API — лише список експортів (без коду)
|
|
93
|
-
if (!multi && !facts.exports?.some(e => e.desc)) return [
|
|
88
|
+
if (!multi && !facts.exports?.some(e => e.desc)) return [behavior]
|
|
94
89
|
const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
|
|
95
90
|
const api = {
|
|
96
91
|
key: 'api',
|
|
@@ -100,7 +95,24 @@ export function sectionMessages(facts, src, anchors = null) {
|
|
|
100
95
|
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
|
|
101
96
|
)
|
|
102
97
|
}
|
|
103
|
-
return [
|
|
98
|
+
return [behavior, api]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* R3 — «Огляд» ОСТАННІМ: узагальнення вже написаної Поведінки, а не здогад із
|
|
103
|
+
* голого факт-листа. Лікує generic/хибний Огляд на складних файлах.
|
|
104
|
+
* @param {object} facts факт-лист про файл
|
|
105
|
+
* @param {string} behaviorText готовий текст секції «Поведінка»
|
|
106
|
+
* @param {object|null} [anchors] анкори файлу
|
|
107
|
+
* @returns {Array<{role:string,content:string}>} messages-масив для Огляду
|
|
108
|
+
*/
|
|
109
|
+
export function overviewMessages(facts, behaviorText, anchors = null) {
|
|
110
|
+
const factsTxt = factsSummary(facts)
|
|
111
|
+
const anch = anchorsBlock(anchors)
|
|
112
|
+
return msgs(
|
|
113
|
+
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
114
|
+
`На основі вже написаної секції «Поведінка» (нижче) напиши «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Узагальнюй САМЕ описану поведінку, не додавай нових фактів. Без заголовка, без переліку функцій. Заборонені абстрактні формули без конкретики («перевірка/валідація/обробка даних», «відповідність контракту», «застосовує логіку») — пиши, ЩО саме і за яким контрактом.\n\nПОВЕДІНКА:\n${behaviorText}`
|
|
115
|
+
)
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/**
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** @see ./docs/units-js.md */
|
|
2
|
+
|
|
3
|
+
import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
4
|
+
|
|
5
|
+
// JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
|
|
6
|
+
const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
7
|
+
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
8
|
+
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
9
|
+
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Очищає JSDoc від обрамлення `/** */` і `*`-префіксів.
|
|
13
|
+
* @param {string} raw сирий блок або порожній рядок
|
|
14
|
+
* @returns {string} текст опису без тегів-обрамлення
|
|
15
|
+
*/
|
|
16
|
+
function cleanDoc(raw) {
|
|
17
|
+
if (!raw) return ''
|
|
18
|
+
return raw
|
|
19
|
+
.replace(JSDOC_OPEN_RE, '')
|
|
20
|
+
.replace(JSDOC_CLOSE_RE, '')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
23
|
+
.join('\n')
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
|
|
29
|
+
* @param {string} src вміст файлу
|
|
30
|
+
* @param {number} start зміщення початку декларації
|
|
31
|
+
* @returns {string} очищений опис
|
|
32
|
+
*/
|
|
33
|
+
function precedingDoc(src, start) {
|
|
34
|
+
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
|
|
35
|
+
return cleanDoc(m ? m[0] : '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Імʼя функції, що викликається (проста Identifier або `obj.method`).
|
|
40
|
+
* @param {Record<string, unknown>} node CallExpression
|
|
41
|
+
* @returns {string|null} імʼя callee або null
|
|
42
|
+
*/
|
|
43
|
+
function calleeName(node) {
|
|
44
|
+
const c = node.callee
|
|
45
|
+
if (!c || typeof c !== 'object') return null
|
|
46
|
+
if (c.type === 'Identifier') return c.name
|
|
47
|
+
if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
|
|
53
|
+
* call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
|
|
54
|
+
* @param {unknown} node AST-вузол юніта
|
|
55
|
+
* @returns {Set<string>} імена викликів
|
|
56
|
+
*/
|
|
57
|
+
function collectCalls(node) {
|
|
58
|
+
const names = new Set()
|
|
59
|
+
walkAstWithAncestors(node, [], n => {
|
|
60
|
+
if (n.type === 'CallExpression') {
|
|
61
|
+
const name = calleeName(n)
|
|
62
|
+
if (name) names.add(name)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
return names
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Будує юніт із декларації, додає у `units`. Розпізнає function/class та
|
|
70
|
+
* const-функції (`const x = () => {}` / `function expression`).
|
|
71
|
+
* @param {Record<string, unknown>} decl декларація (function/class/variable)
|
|
72
|
+
* @param {boolean} exported чи експортується
|
|
73
|
+
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
|
|
74
|
+
* @param {string} src вміст файлу
|
|
75
|
+
* @param {Array<object>} units акумулятор
|
|
76
|
+
* @returns {void}
|
|
77
|
+
*/
|
|
78
|
+
function pushUnits(decl, exported, docStart, src, units) {
|
|
79
|
+
if (!decl || typeof decl !== 'object') return
|
|
80
|
+
const doc = precedingDoc(src, docStart)
|
|
81
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
82
|
+
const name = decl.id?.name
|
|
83
|
+
if (!name) return
|
|
84
|
+
units.push({
|
|
85
|
+
name,
|
|
86
|
+
kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
|
|
87
|
+
exported,
|
|
88
|
+
span: { start: decl.start, end: decl.end },
|
|
89
|
+
body: src.slice(decl.start, decl.end),
|
|
90
|
+
calls: collectCalls(decl),
|
|
91
|
+
doc
|
|
92
|
+
})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (decl.type === 'VariableDeclaration') {
|
|
96
|
+
for (const d of decl.declarations ?? []) {
|
|
97
|
+
const init = d.init
|
|
98
|
+
const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
|
99
|
+
if (!isFn || d.id?.type !== 'Identifier') continue
|
|
100
|
+
units.push({
|
|
101
|
+
name: d.id.name,
|
|
102
|
+
kind: 'const',
|
|
103
|
+
exported,
|
|
104
|
+
span: { start: init.start, end: init.end },
|
|
105
|
+
body: src.slice(init.start, init.end),
|
|
106
|
+
calls: collectCalls(init),
|
|
107
|
+
doc
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
|
|
115
|
+
* прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
|
|
116
|
+
* @param {string} src вміст файлу
|
|
117
|
+
* @param {string} [relPath] шлях (для вибору мови oxc)
|
|
118
|
+
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
|
|
119
|
+
*/
|
|
120
|
+
export function extractUnitsJs(src, relPath = 'scan.ts') {
|
|
121
|
+
const program = parseProgramOrNull(src, relPath)
|
|
122
|
+
if (!program || !Array.isArray(program.body)) return null
|
|
123
|
+
|
|
124
|
+
const units = []
|
|
125
|
+
for (const node of program.body) {
|
|
126
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
127
|
+
pushUnits(node.declaration, true, node.start, src, units)
|
|
128
|
+
} else if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
|
|
129
|
+
pushUnits(node.declaration, true, node.start, src, units)
|
|
130
|
+
} else {
|
|
131
|
+
pushUnits(node, false, node.start, src, units)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
|
|
136
|
+
const names = new Set(units.map(u => u.name))
|
|
137
|
+
for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
|
|
138
|
+
return units
|
|
139
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @see ./docs/units.md */
|
|
2
|
+
|
|
3
|
+
import { extractUnitsJs } from './units-js.mjs'
|
|
4
|
+
|
|
5
|
+
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Мовно-агностичний фасад юніт-шару (Інкремент 1). Диспатчить за розширенням:
|
|
9
|
+
* js/mjs/ts → oxc; vue/py — додаються наступними кроками (поки `null` → виклик
|
|
10
|
+
* відкочується на whole-file шлях, як і раніше).
|
|
11
|
+
* @param {string} src вміст файлу
|
|
12
|
+
* @param {string} relPath шлях файлу
|
|
13
|
+
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
|
|
14
|
+
*/
|
|
15
|
+
export function extractUnits(src, relPath) {
|
|
16
|
+
const ext = (relPath.split('.').pop() || '').toLowerCase()
|
|
17
|
+
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
|
|
18
|
+
return null
|
|
19
|
+
}
|