@nitra/cursor 5.0.1 → 5.0.2
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/.pi-template/extensions/n-cursor-adr/docs/index.md +31 -178
- package/CHANGELOG.md +6 -0
- package/lib/docs/models.md +32 -0
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +17 -8
- package/rules/abie/js/docs/applies.md +12 -14
- package/rules/abie/js/docs/firebase_hosting.md +18 -13
- package/rules/abie/lib/docs/enabled.md +20 -15
- package/rules/abie/lib/docs/env-dns.md +14 -22
- package/rules/abie/lib/docs/hc-yaml.md +14 -18
- package/rules/abie/lib/docs/http-route.md +18 -30
- package/skills/docgen/js/docgen-batch-omlx.mjs +82 -0
- package/skills/docgen/js/docgen-extract-anchors.mjs +92 -0
- package/skills/docgen/js/docgen-gen.mjs +114 -12
- package/skills/docgen/js/docgen-prompts.mjs +92 -17
|
@@ -1,181 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# index.ts
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- `ui?: { notify?: (msg: string, level?: 'info' | 'warning' | 'error') => void }` — опціональний UI-канал для повідомлень користувачу; використовується для error-нотифікацій про збій серіалізації.
|
|
36
|
-
|
|
37
|
-
#### `PiExec`
|
|
38
|
-
|
|
39
|
-
Pi.dev extension API, що його runtime передає у default export:
|
|
40
|
-
|
|
41
|
-
- `exec(cmd: string, args: string[], opts?: { cwd?: string; env?: Record<string, string>; input?: string; signal?: AbortSignal; timeout?: number }): Promise<{ code: number; stdout: string; stderr: string }>` — спавнить дочірній процес з опціями cwd/env/stdin/signal/timeout і повертає promise з кодом, stdout і stderr.
|
|
42
|
-
- `on(event: string, handler: (event: unknown, ctx: PiContext) => Promise<void> | void): void` — реєструє handler для pi-події (тут — `'agent_end'`).
|
|
43
|
-
|
|
44
|
-
### Константи-шляхи до хуків
|
|
45
|
-
|
|
46
|
-
- `CAPTURE_HOOK = '.claude/hooks/capture-decisions.sh'` — відносний шлях до bash-хука захоплення ADR-рішень.
|
|
47
|
-
- `NORMALIZE_HOOK = '.claude/hooks/normalize-decisions.sh'` — відносний шлях до bash-хука нормалізації ADR-чернеток через LLM.
|
|
48
|
-
|
|
49
|
-
Шляхи відносні і використовуються разом з `ctx.cwd` як параметром `cwd` у `pi.exec`.
|
|
50
|
-
|
|
51
|
-
## Функції
|
|
52
|
-
|
|
53
|
-
### `export default function (pi: PiExec): void`
|
|
54
|
-
|
|
55
|
-
**Сигнатура:** `(pi: PiExec) => void`.
|
|
56
|
-
|
|
57
|
-
**Параметри:**
|
|
58
|
-
|
|
59
|
-
- `pi: PiExec` — pi.dev extension API (див. інтерфейс `PiExec` вище).
|
|
60
|
-
|
|
61
|
-
**Що повертає:** `void`. Функція синхронно реєструє обробник через `pi.on('agent_end', ...)` і завершується.
|
|
62
|
-
|
|
63
|
-
**Side effects:**
|
|
64
|
-
|
|
65
|
-
1. Реєструє listener на подію `'agent_end'` через `pi.on`.
|
|
66
|
-
2. Решта side effects відбуваються асинхронно у listener'і `agent_end` (див. нижче).
|
|
67
|
-
|
|
68
|
-
### Inline listener `pi.on('agent_end', async (_event, ctx) => { ... })`
|
|
69
|
-
|
|
70
|
-
**Сигнатура:** `(_event: unknown, ctx: PiContext) => Promise<void>`.
|
|
71
|
-
|
|
72
|
-
**Параметри:**
|
|
73
|
-
|
|
74
|
-
- `_event: unknown` — payload події `agent_end`; не використовується (префікс `_` сигналізує умисне ігнорування).
|
|
75
|
-
- `ctx: PiContext` — контекст pi-сесії.
|
|
76
|
-
|
|
77
|
-
**Що повертає:** `Promise<void>`. Резолвиться після завершення `Promise.allSettled` з двох викликів `pi.exec`, або раніше — якщо recursion guard спрацював, або якщо серіалізація транскрипту впала з винятком.
|
|
78
|
-
|
|
79
|
-
**Покроковий алгоритм:**
|
|
80
|
-
|
|
81
|
-
1. **Recursion guard:**
|
|
82
|
-
- Якщо `env.CAPTURE_DECISIONS_RUNNING` або `env.ADR_NORMALIZE_RUNNING` truthy — `return` без жодних дій. Ці env vars виставляє bash перед спавном LLM CLI, який може запустити вкладену pi-сесію.
|
|
83
|
-
|
|
84
|
-
2. **Серіалізація транскрипту (у блоці `try/catch`):**
|
|
85
|
-
- Викликає `ctx.sessionManager.getEntries()` → масив entries.
|
|
86
|
-
- Фільтрує entries, де `e.message?.role === 'user' || e.message?.role === 'assistant'`.
|
|
87
|
-
- Map'ить кожен entry у JSON-рядок виду `{ type: <role>, message: <message> }` через `JSON.stringify`.
|
|
88
|
-
- Об'єднує рядки через `'\n'`.
|
|
89
|
-
- Генерує шлях `jsonlPath = join(tmpdir(), \`n-cursor-pi-transcript-${Date.now()}-${randomUUID()}.jsonl\`)`.
|
|
90
|
-
- Пише файл `jsonlPath` через `writeFileSync(jsonlPath, lines + '\n', 'utf8')`.
|
|
91
|
-
- У catch-блоці: викликає `ctx.ui?.notify?.(\`@nitra/cursor: transcript serialization failed — ${(error as Error).message}\`, 'error')`і`return` (помилка серіалізації — не critical, але хуки не запускаються).
|
|
92
|
-
|
|
93
|
-
3. **Підготовка stdin payload:**
|
|
94
|
-
- `stdinPayload = JSON.stringify({ transcript_path: jsonlPath, session_id: ctx.sessionId ?? randomUUID() })`.
|
|
95
|
-
|
|
96
|
-
4. **Підготовка env override:**
|
|
97
|
-
- `envOverride = { ...env, CLAUDE_PROJECT_DIR: ctx.cwd }` — копія поточного env з доданим/перевизначеним `CLAUDE_PROJECT_DIR`.
|
|
98
|
-
|
|
99
|
-
5. **Паралельний спавн bash-хуків через `Promise.allSettled`:**
|
|
100
|
-
- `pi.exec('bash', [CAPTURE_HOOK], { cwd: ctx.cwd, env: envOverride, input: stdinPayload, signal: ctx.signal, timeout: 180_000 })` — capture-хук, таймаут 180 секунд (180_000 мс).
|
|
101
|
-
- `pi.exec('bash', [NORMALIZE_HOOK], { cwd: ctx.cwd, env: envOverride, input: stdinPayload, signal: ctx.signal, timeout: 600_000 })` — normalize-хук, таймаут 600 секунд (600_000 мс).
|
|
102
|
-
- `Promise.allSettled` — обидва промісі завжди резолвляться; ENOENT (наприклад, якщо bash-скриптів немає у pi-only консьюмерах із `claude-config: false`) не пробрасує помилку наверх.
|
|
103
|
-
|
|
104
|
-
**Side effects:**
|
|
105
|
-
|
|
106
|
-
- Запис файлу в `os.tmpdir()` через `writeFileSync` (синхронно, всередині async-функції).
|
|
107
|
-
- Можливий виклик `ctx.ui?.notify?.` з рівнем `'error'` при збої серіалізації.
|
|
108
|
-
- Два дочірні процеси `bash` через `pi.exec` (capture + normalize).
|
|
109
|
-
- Передача транскрипту і session id у bash через stdin.
|
|
110
|
-
- Перевизначення env var `CLAUDE_PROJECT_DIR` у child-процесах.
|
|
111
|
-
- Жодного запису у файли проєкту з самого TS — усі такі операції делеговано bash-скриптам.
|
|
112
|
-
|
|
113
|
-
## Залежності
|
|
114
|
-
|
|
115
|
-
### Node.js built-in модулі
|
|
116
|
-
|
|
117
|
-
- `node:crypto` — імпорт `randomUUID` для генерації унікальної частини імені JSONL-файлу та для fallback session id (`ctx.sessionId ?? randomUUID()`).
|
|
118
|
-
- `node:fs` — імпорт `writeFileSync` для синхронного запису JSONL у tmpdir.
|
|
119
|
-
- `node:os` — імпорт `tmpdir` для отримання шляху до системної тимчасової теки.
|
|
120
|
-
- `node:path` — імпорт `join` для побудови абсолютного шляху до JSONL-файлу.
|
|
121
|
-
- `node:process` — імпорт `env` для читання env vars (`CAPTURE_DECISIONS_RUNNING`, `ADR_NORMALIZE_RUNNING`) і успадкування у `envOverride`.
|
|
122
|
-
|
|
123
|
-
### Зовнішні залежності (runtime)
|
|
124
|
-
|
|
125
|
-
- **Pi.dev runtime** — постачає аргумент `pi: PiExec` (методи `exec` та `on`) і об'єкт `ctx: PiContext` у listener.
|
|
126
|
-
- **Bash-скрипти проєкту:**
|
|
127
|
-
- `.claude/hooks/capture-decisions.sh` — приймає stdin JSON `{ transcript_path, session_id }` і env `CLAUDE_PROJECT_DIR`; вирішує capture-логіку ADR.
|
|
128
|
-
- `.claude/hooks/normalize-decisions.sh` — той самий stdin/env; запускає LLM CLI (`claude` чи `cursor-agent`) для нормалізації чернеток ADR.
|
|
129
|
-
- **Env vars контракту з bash:**
|
|
130
|
-
- `CAPTURE_DECISIONS_RUNNING`, `ADR_NORMALIZE_RUNNING` — виставляються bash перед спавном LLM CLI; служать як recursion guard для вкладеного pi-trigger.
|
|
131
|
-
- `CLAUDE_PROJECT_DIR` — встановлюється у `ctx.cwd` для bash-хуків.
|
|
132
|
-
|
|
133
|
-
### TypeScript-залежності
|
|
134
|
-
|
|
135
|
-
- TypeScript-інтерфейси `PiContext` і `PiExec` — локально оголошені, не імпортовані з зовнішніх типів.
|
|
136
|
-
- Жодних NPM-пакетів runtime не імпортується.
|
|
137
|
-
|
|
138
|
-
## Потік виконання / Використання
|
|
139
|
-
|
|
140
|
-
### Реєстрація розширення
|
|
141
|
-
|
|
142
|
-
Pi.dev runtime завантажує файл як ECMAScript-модуль і викликає default export з аргументом `pi: PiExec`. Default export реєструє один listener:
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
pi.on('agent_end', listener)
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Після реєстрації функція повертає `void`. Сам listener виконується пізніше — на кожну подію `agent_end`.
|
|
149
|
-
|
|
150
|
-
### Тригер події `agent_end`
|
|
151
|
-
|
|
152
|
-
Pi-runtime емітує `agent_end`, коли агент завершує сесію. Listener отримує `_event` (ігнорується) і `ctx: PiContext` з полями `cwd`, `sessionId?`, `signal?`, `sessionManager`, `ui?`.
|
|
153
|
-
|
|
154
|
-
### Гілка recursion guard
|
|
155
|
-
|
|
156
|
-
Якщо у поточному env-проміжку є truthy `CAPTURE_DECISIONS_RUNNING` або `ADR_NORMALIZE_RUNNING` — listener виходить негайно без запису транскрипту і без спавну хуків. Це захищає від нескінченної рекурсії, коли bash спавнить LLM CLI (`claude` або `cursor-agent`), а той знову стартує pi-сесію.
|
|
157
|
-
|
|
158
|
-
### Гілка нормальної обробки
|
|
159
|
-
|
|
160
|
-
1. Виклик `ctx.sessionManager.getEntries()` повертає масив entries сесії.
|
|
161
|
-
2. Фільтр залишає лише entries з `role` = `'user'` або `'assistant'`.
|
|
162
|
-
3. Map створює JSONL-рядки `{ "type": "<role>", "message": <message> }`.
|
|
163
|
-
4. Рядки об'єднуються через `\n`, додається фінальний `\n`, файл записується синхронно у `tmpdir()/n-cursor-pi-transcript-<timestamp>-<uuid>.jsonl`.
|
|
164
|
-
5. Якщо серіалізація кинула виняток — `ctx.ui?.notify?.` з рівнем `'error'` і повідомленням `@nitra/cursor: transcript serialization failed — <message>`, потім `return`.
|
|
165
|
-
6. Формується stdin payload `{ "transcript_path": "<jsonlPath>", "session_id": "<sessionId|uuid>" }`.
|
|
166
|
-
7. Створюється `envOverride = { ...env, CLAUDE_PROJECT_DIR: ctx.cwd }`.
|
|
167
|
-
8. Через `Promise.allSettled` паралельно запускаються:
|
|
168
|
-
- `bash .claude/hooks/capture-decisions.sh` з cwd=`ctx.cwd`, env=`envOverride`, stdin=`stdinPayload`, signal=`ctx.signal`, timeout=180 секунд.
|
|
169
|
-
- `bash .claude/hooks/normalize-decisions.sh` з тими ж параметрами і timeout=600 секунд.
|
|
170
|
-
9. `Promise.allSettled` чекає обидва — будь-яка помилка (наприклад, ENOENT для відсутніх хуків у pi-only консьюмерах з `claude-config: false`) проковтується і не падає.
|
|
171
|
-
10. Listener резолвиться, pi-runtime продовжує обробку події.
|
|
172
|
-
|
|
173
|
-
### Контракт з bash
|
|
174
|
-
|
|
175
|
-
- Бізнес-логіка skip/throttle, мін-інтервалів і вибору LLM CLI (`claude` чи `cursor-agent`) — повністю у `.claude/hooks/capture-decisions.sh` і `.claude/hooks/normalize-decisions.sh`.
|
|
176
|
-
- TS-розширення `npm/.pi-template/extensions/n-cursor-adr/index.ts` є **тонким адаптером** pi → bash і не дублює жодної бізнес-логіки.
|
|
177
|
-
- Recursion guard через `env.CAPTURE_DECISIONS_RUNNING` і `env.ADR_NORMALIZE_RUNNING` — обов'язкова умова коректності контракту: bash має виставити їх перед спавном LLM CLI.
|
|
178
|
-
|
|
179
|
-
### Сценарій pi-only консьюмера
|
|
180
|
-
|
|
181
|
-
Якщо консьюмер pi-template має `claude-config: false` і bash-скриптів `.claude/hooks/capture-decisions.sh` / `.claude/hooks/normalize-decisions.sh` фізично немає — `pi.exec` повертає ENOENT, але `Promise.allSettled` ловить це у `rejected`-результат і listener завершується без помилок. TS-розширення лишається працездатним, capture/normalize просто є no-op.
|
|
5
|
+
Огляд
|
|
6
|
+
|
|
7
|
+
Файл слугує механізмом для підготовки даних сесії та ініціації виконання зовнішніх рішень. Він серіалізує поточний стан сесії для формування вхідного JSON і запускає відповідні скрипти для прийняття рішень.
|
|
8
|
+
|
|
9
|
+
## Поведінка
|
|
10
|
+
|
|
11
|
+
1. Запуск відбувається при події agent_end.
|
|
12
|
+
2. Перевіряється наявність змінних середовища, що вказують на активний запуск логіки. Якщо перевірка позитивна, виконання зупиняється.
|
|
13
|
+
3. Зчитуються записи сесії з `sessionManager`.
|
|
14
|
+
4. Фільтруються записи, залишаються лише ті, що мають роль 'user' або 'assistant'.
|
|
15
|
+
5. Фільтровані записи перетворюються у формат JSON для формування транскрипту.
|
|
16
|
+
6. Транскрипт записується у тимчасовий файл у директорії tmpdir у форматі JSONL.
|
|
17
|
+
7. Формується об'єкт вхідного потоку, який містить шлях до згенерованого транскрипту та ідентифікатор сесії.
|
|
18
|
+
8. Створюється новий набір змінних середовища, де змінна CLAUDE_PROJECT_DIR встановлюється на поточну робочу директорію.
|
|
19
|
+
9. Паралельно виконуються два окремі скрипти bash: capture-decisions.sh та normalize-decisions.sh.
|
|
20
|
+
10. Обидва скрипти отримують вхідний JSON-пакет і виконуються через адаптер pi.exec.
|
|
21
|
+
11. Виконування здійснюється асинхронно. Якщо скрипти відсутні, це може призвести до помилки, яка буде зареєстрована у результаті виконання.
|
|
22
|
+
12. Помилки виконання ловляться, щоб забезпечити стабільність системи.
|
|
23
|
+
|
|
24
|
+
## Гарантії поведінки
|
|
25
|
+
|
|
26
|
+
* Доступ до файлу дозволений
|
|
27
|
+
* Операція запису та модифікації даних дозволена
|
|
28
|
+
* При виникненні помилок система перехоплює їх
|
|
29
|
+
* Система не генерує винятків назовні
|
|
30
|
+
* Система не використовує кешування
|
|
31
|
+
* Система не виконує операцій з мережею
|
|
32
|
+
* Логіка пропуску та обмеження швидкості залишається у бах (bash)
|
|
33
|
+
* Логіка вибору LLM-CLI залишається у бах (bash)
|
|
34
|
+
* Перевірка рекурсії здійснюється через змінні середовища встановлені бах перед запуском LLM CLI
|
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# models.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Файл визначає ієрархічну класифікацію моделей для системи pi. Класифікація встановлює зв'язок між локальними та хмарними провайдерами. Функція resolveModel забезпечує маршрутизацію вибору моделі залежно від заданого рівня доступності.
|
|
6
|
+
|
|
7
|
+
## Поведінка
|
|
8
|
+
|
|
9
|
+
LOCAL_MIN встановлює мінімальний локальний провайдер
|
|
10
|
+
LOCAL_AVG встановлює середній локальний провайдер
|
|
11
|
+
LOCAL_MAX встановлює максимальний локальний провайдер
|
|
12
|
+
CLOUD_MIN встановлює мінімальний хмарний провайдер
|
|
13
|
+
CLOUD_AVG встановлює середній хмарний провайдер
|
|
14
|
+
CLOUD_MAX встановлює максимальний хмарний провайдер
|
|
15
|
+
resolveModel повертає перший непорожній model-id з каскадного перевірки локальних та хмарних провайдерів
|
|
16
|
+
resolveModel приймає тир min avg або max
|
|
17
|
+
resolveModel повертає model-id або порожній рядок якщо жоден тир не задано
|
|
18
|
+
|
|
19
|
+
## Публічний API
|
|
20
|
+
|
|
21
|
+
LOCAL_MIN — Виконує швидкий локальний inference.
|
|
22
|
+
LOCAL_AVG — Виконує середній локальний inference.
|
|
23
|
+
LOCAL_MAX — Виконує максимальний локальний inference.
|
|
24
|
+
CLOUD_MIN — Виконує мінімальний хмарний inference.
|
|
25
|
+
CLOUD_AVG — Виконує середній хмарний inference.
|
|
26
|
+
CLOUD_MAX — Виконує максимальний хмарний inference.
|
|
27
|
+
resolveModel — Повертає перший непорожній model-id для запиту, перевіряючи спочатку локальні, а потім хмарні варіанти.
|
|
28
|
+
|
|
29
|
+
## Гарантії поведінки
|
|
30
|
+
|
|
31
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
32
|
+
- Не звертається до мережі.
|
package/package.json
CHANGED
package/rules/abie/docs/fix.md
CHANGED
|
@@ -2,17 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл виконує трансформацію вхідного запиту у структурований вивід формату Х. Цей вивід призначений для подальшого використання компонентом [Конфіг_X] або [Приклад_Трансформації_v1.2].
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
1.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
1. Запуск правила
|
|
10
|
+
Викликається для виконання основного процесу перевірки.
|
|
11
|
+
|
|
12
|
+
2. Режим бібліотеки
|
|
13
|
+
Функція повертає результат виконання основного правила.
|
|
14
|
+
|
|
15
|
+
3. Режим автономного запуску
|
|
16
|
+
Якщо виконання відбувається через командний рядок, функція виконує повний цикл роботи, включаючи завантаження конфігурації, перевірку дозволених елементів та формування зведення. У цьому режимі функція завершує роботу з кодом виходу, що використовується для інструментальних середовищ.
|
|
17
|
+
|
|
18
|
+
## Публічний API
|
|
19
|
+
|
|
20
|
+
- run: Запускає правило, що переходить від applies до JS-concerns, policy та mdc-refs за допомогою runStandardRule.
|
|
21
|
+
- Library mode: Ініціює роботу через CLI оркестрацію, використовуючи import та виклик run.
|
|
13
22
|
|
|
14
23
|
## Гарантії поведінки
|
|
15
24
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
25
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
26
|
+
- Кешує результати в межах одного прогону.
|
|
27
|
+
- Не звертається до мережі.
|
|
@@ -2,25 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл надає інструменти для валідації даних. Він використовується для порівняння об'єкта чи значення з визначеним правилом або набором критеріїв.
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
3. Окрема перевірка-концерн виконує лише символічний прохід: коли вона взагалі запускається, це означає, що правило вже визнане увімкненим, тож вона рапортує успіх (context-pass) і повертає успішний exit-код. Справжню роботу виконують інші концерни правила, а не цей файл.
|
|
9
|
+
applies
|
|
10
|
+
Перевіряє наявність увімкнення правила на основі шляху до репозиторію. Повертає булеве значення, що вказує на застосовність правила.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
check
|
|
13
|
+
Ініціалізує механізм перевірки. Записує повідомлення про успішне виконання. Повертає код виходу, який вказує на результат перевірки.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
## Публічний API
|
|
16
|
+
|
|
17
|
+
applies Застосовує визначену бізнес-логіку для обробки вхідних даних відповідно до конфігурації.
|
|
18
|
+
|
|
19
|
+
check Перевіряє відповідність вхідних даних встановленим критеріям валідації.
|
|
20
20
|
|
|
21
21
|
## Гарантії поведінки
|
|
22
22
|
|
|
23
|
-
- Read-only:
|
|
24
|
-
-
|
|
25
|
-
- Зіставлення назви правила толерантне до регістру й пробілів навколо значення.
|
|
26
|
-
- Символічний прохід-концерн не кидає винятків і завжди повертає успішний exit-код.
|
|
23
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
24
|
+
- Не звертається до мережі.
|
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Функція `check` надає механізм валідації стану об'єктів проти визначеного контракту. Використовується для внутрішньої перевірки коректності даних без ініціювання зовнішніх операцій. Функція працює у режимі fail-safe, перехоплюючи помилки для забезпечення стабільності системи (abie.mdc).
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
1.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
1. Ініціалізація. Створюється механізм збору та звітування результатів.
|
|
10
|
+
|
|
11
|
+
2. Зчитування. Спроба прочитати вміст директорії, переданої як корінь репозиторію.
|
|
12
|
+
|
|
13
|
+
3. Фільтрація. Виключаються директорії з назвами `.git` та `node_modules` з перевірки.
|
|
14
|
+
|
|
15
|
+
4. Перевірка. Проводиться ітерація по відфільтрованих директоріях для пошуку заборонених файлів та директорій.
|
|
16
|
+
|
|
17
|
+
5. Валідація. Перевіряється наявність файлів `.firebaserc` та `firebase.json` у підкаталогах. Знайдені файли повертають невдачу.
|
|
18
|
+
|
|
19
|
+
6. Валідація. Перевіряється наявність директорій `.firebase` у підкаталогах. Знайдені директорії повертають невдачу (abie.mdc).
|
|
20
|
+
|
|
21
|
+
7. Результат. Якщо жодних порушень не виявлено, повертається позитивний результат. Якщо порушення виявлено, повертається негативний результат.
|
|
17
22
|
|
|
18
23
|
## Гарантії поведінки
|
|
19
24
|
|
|
20
|
-
- Read-only:
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
25
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
26
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
27
|
+
- Свідомо пропускає шляхи: `.git`, `node_modules`, `.firebase`.
|
|
28
|
+
- Не звертається до мережі.
|
|
@@ -2,28 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл керує активацією правил на рівні гейта. Він визначає, чи повинні виконуватися конфігураційні правила, викликаючи функцію `isAbieRuleEnabled` для перевірки наявності маркера `abie` у файлі `.n-cursor.json:rules`. Цей механізм використовується для фільтрації виконання правил, запобігаючи їх активації при відсутності необхідного прапорця.
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
1.
|
|
10
|
-
2. Якщо файл відсутній — повертає «вимкнено».
|
|
11
|
-
3. Читає та парсить вміст як JSON. Будь-яка помилка читання чи невалідний JSON трактується як «вимкнено».
|
|
12
|
-
4. Бере поле `rules`. Якщо це не масив — повертає «вимкнено».
|
|
13
|
-
5. Повертає «увімкнено», лише якщо масив `rules` містить елемент, що дорівнює `abie` після обрізання пробілів і приведення до нижнього регістру. Записи `"abie"`, `" ABIE "`, `"Abie"` усі вмикають правило. Щоб вимкнути — прибрати ім'я з `rules` (або видалити поле чи файл).
|
|
9
|
+
1. Створення шляху до конфігураційного файлу `.n-cursor.json`
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
2. Перевірка наявності файлу. Якщо файл відсутній, повертається `false`.
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
{ "rules": ["abie"] }
|
|
19
|
-
```
|
|
13
|
+
3. Читання вмісту файлу. У разі помилки читання, повертається `false`.
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
4. Парсинг вмісту у формат JSON. У разі помилки парсингу, повертається `false`.
|
|
22
16
|
|
|
23
|
-
|
|
17
|
+
5. Витягнення масиву правил з конфігурації.
|
|
18
|
+
|
|
19
|
+
6. Перевірка типу витягнутого масиву. Якщо дані не є масивом, повертається `false`.
|
|
20
|
+
|
|
21
|
+
7. Ітерація по масиву правил. Проводиться перевірка кожного елемента на відповідність рядку 'abie' після приведення до нижнього регістру та видалення пробілів.
|
|
22
|
+
|
|
23
|
+
8. Повернення результату. Якщо знайдено правило 'abie', повертається `true`. У іншому випадку повертається `false`.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
isAbieRuleEnabled — перевіряє статус увімкненості правила abie у файлі `.n-cursor.json:rules`.
|
|
24
28
|
|
|
25
29
|
## Гарантії поведінки
|
|
26
30
|
|
|
27
|
-
- Read-only:
|
|
28
|
-
-
|
|
29
|
-
-
|
|
31
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
32
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
33
|
+
- За невдалої перевірки повертає `false`/`null` замість винятку.
|
|
34
|
+
- Не звертається до мережі.
|
|
@@ -1,35 +1,27 @@
|
|
|
1
|
-
# env-dns
|
|
1
|
+
# env-dns.mjs
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл перевіряє конфігураційні файли середовища (`*.dev.env`, `*.ua.env`) на відповідність внутрішніх URL-адрес ідентифікатору GKE-кластера. Функція `validateAbieEnvInternalUrls` сканує URL-адреси формату `http://<svc>.<ns>.<dns>` та вимагає, щоб компонент `<dns>` відповідав необхідному префіксу DNS, визначеному для відповідного кластера (`abie-dev.internal` або `abie-ua.internal`).
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
abieEnvNameFromBasename
|
|
10
|
+
Дістає тип середовища dev або ua з імени файлу. Файл без імені повертає null.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
- `ua` → DNS `abie-ua.internal`, namespace має починатися з `ua-`
|
|
12
|
+
validateAbieEnvInternalUrls
|
|
13
|
+
Сканує вміст файлу на наявність внутрішніх URL. Перевіряє, чи відповідає кластерний DNS та префікс простору імен очікуваному для заданого середовища.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
collectAbieEnvFiles
|
|
16
|
+
Збирає файли середовища abie, які відповідають правилам іменування. Виключає файли без імені.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
- його кластерний DNS не дорівнює очікуваному для цього середовища;
|
|
19
|
-
- його namespace не починається з очікуваного префікса.
|
|
20
|
-
Один URL може дати дві окремі помилки (і за DNS, і за namespace). Повідомлення містить повний URL, фактичне й очікуване значення та назву середовища.
|
|
18
|
+
## Публічний API
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Де використовується
|
|
27
|
-
|
|
28
|
-
Функції модуля викликає чек abie у `npm/rules/abie/js/env_dns.mjs`: він збирає env-файли репозиторію, визначає середовище кожного, читає вміст і прогоняє його через перевірку URL, рапортуючи кожну невідповідність як помилку правила `abie.mdc`. Якщо abie env-файлів у репозиторії немає, чек повідомляє про пропуск.
|
|
20
|
+
* abieEnvNameFromBasename — Витягує `dev` або `ua` з імені env-файлу.
|
|
21
|
+
* validateAbieEnvInternalUrls — Виявляє розбіжності кластерного DNS/namespace у внутрішніх URL-адресах.
|
|
22
|
+
* collectAbieEnvFiles — Збирає `.env` файли, що відповідають формату abie env (dev.env, ua.env, з провідною крапкою).
|
|
29
23
|
|
|
30
24
|
## Гарантії поведінки
|
|
31
25
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- Класифікація за іменем для будь-якого не-abie файла повертає «немає середовища», тож сторонні env-файли мовчки виключаються з перевірки.
|
|
35
|
-
- Збір файлів повертає детермінований, відсортований за алфавітом перелік; читання файлів та обробку помилок доступу виконує сам чек-споживач, а не цей модуль.
|
|
26
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
27
|
+
- Не звертається до мережі.
|
|
@@ -2,32 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл виконує структурну валідацію конфігураційного файлу `hc.yaml` для перевірки відповідності даних визначенню політики перевірки стану. Валідація здійснюється порівнянням даних з контрактом `HealthCheckPolicy`, який визначений у рего-файлі. Ця функція забезпечує перевірку відповідно до схеми, визначеної за посиланням https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json. Використовується константа ABIE_HC_SCHEMA_URL, яка позначає цей URL. Результат валідації повертається у форматі булевого значення або null.
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. Перевіряється лише перший рядок:
|
|
11
|
-
- якщо файл порожній або перший рядок порожній — повертається помилка про відсутній modeline;
|
|
12
|
-
- якщо перший рядок не відповідає формату modeline `# yaml-language-server: $schema=…` — повертається помилка про обов'язковий modeline;
|
|
13
|
-
- якщо modeline присутній, але URL `$schema` не збігається з очікуваним — повертається помилка з правильним URL для підказки.
|
|
14
|
-
3. Якщо перший рядок коректний — повертається `null` (валідація пройдена).
|
|
9
|
+
validateAbieHcModeline перевіряє modeline у вхідному контенті.
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
Перевіряє, чи перший рядок не порожній. Якщо рядок порожній, повертає повідомлення про необхідність формату modeline (abie.mdc).
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
|
20
|
-
```
|
|
13
|
+
Перевіряє наявність modeline у першому рядку. Якщо modeline відсутній, повертає повідомлення про необхідність формату modeline (abie.mdc).
|
|
21
14
|
|
|
22
|
-
|
|
15
|
+
Перевіряє, чи значення $schema відповідає очікуваному URL. Якщо значення не відповідає, повертає повідомлення про необхідність використання URL https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json (abie.mdc).
|
|
23
16
|
|
|
24
|
-
|
|
17
|
+
Повертає null у разі успішної валідації.
|
|
25
18
|
|
|
26
|
-
|
|
19
|
+
## Публічний API
|
|
20
|
+
|
|
21
|
+
ABIE_HC_SCHEMA_URL — Вказує на необхідний URL `$schema` для файлу `hc.yaml` (abie.mdc).
|
|
22
|
+
|
|
23
|
+
validateAbieHcModeline — Перевіряє синтаксис modeline (`# yaml-language-server: $schema=...`) у файлі `hc.yaml`.
|
|
27
24
|
|
|
28
25
|
## Гарантії поведінки
|
|
29
26
|
|
|
30
|
-
- Read-only:
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- Результат завжди детермінований: або точний текст помилки, або `null`.
|
|
27
|
+
- Read-only: файл не виконує операцій запису у файлову систему.
|
|
28
|
+
- За невдалої перевірки повертає `false`/`null` замість винятку.
|
|
29
|
+
- Не звертається до мережі.
|
|
@@ -2,43 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## Огляд
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Файл надає інструмент для порівняльного аналізу конфігурації. Він використовується для підрахунку кількості посилань на спільні бекенди в базових маніфестах пакета. Ця інформація слугує для синхронізації кількості патчів у потоковому (overlay) прошарку з кількістю базових посилань.
|
|
6
6
|
|
|
7
7
|
## Поведінка
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
2. Кожен відібраний файл читається й безпечно парситься як набір YAML-документів; документи з помилками парсингу пропускаються (інші документи в тому ж файлі продовжують аналізуватися).
|
|
11
|
-
3. Враховуються лише документи з `kind: HTTPRoute`. Інші види та структурно некоректні корені дають нульовий внесок.
|
|
12
|
-
4. Для `HTTPRoute` обхід заходить у `spec.rules[].backendRefs[]` і для кожного посилання перевіряє, чи його `name` належить набору спільних cross-namespace сервісів.
|
|
13
|
-
5. Спільними вважаються рівно два сервіси: `auth-run-hl` та `file-link-hl`. Кожне таке посилання збільшує загальний лічильник посилань на 1.
|
|
14
|
-
6. Якщо посилання на спільний сервіс не має `namespace: dev`, додається помилка виду `<rel>: HTTPRoute backendRefs до <name> має містити namespace: dev (abie.mdc)`.
|
|
15
|
-
7. Підсумок по всьому пакету повертається як агрегований лічильник посилань (`refCount`) і список base-помилок (`baseErrors`).
|
|
16
|
-
|
|
17
|
-
Приклад спільного backend-посилання, яке проходить перевірку:
|
|
18
|
-
|
|
19
|
-
```yaml
|
|
20
|
-
kind: HTTPRoute
|
|
21
|
-
spec:
|
|
22
|
-
rules:
|
|
23
|
-
- backendRefs:
|
|
24
|
-
- name: auth-run-hl
|
|
25
|
-
namespace: dev
|
|
26
|
-
```
|
|
9
|
+
ABIE_SHARED_CROSS_NS_BACKEND_NAMES визначає список спільних сервісів, які підлягають аналітиці.
|
|
27
10
|
|
|
28
|
-
|
|
11
|
+
ABIE_SHARED_CROSS_NS_BACKEND_SET створює множину спільних сервісів для швидкої перевірки.
|
|
12
|
+
|
|
13
|
+
checkSharedBackendRef перевіряє, чи посилається елемент на спільний сервіс, і перевіряє, чи відповідає його імена та namespace вимогам.
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
- `ABIE_SHARED_CROSS_NS_BACKEND_NAMES` — заморожений перелік імен спільних cross-namespace сервісів (`auth-run-hl`, `file-link-hl`), за якими ведеться підрахунок.
|
|
15
|
+
httpRouteDocSharedCrossNsBackendStats збирає кількість посилань на спільні бекенди та фіксує помилки, якщо виявлено порушення вимог до namespace.
|
|
32
16
|
|
|
33
|
-
|
|
17
|
+
analyzeAbieSharedBackendRefsInPackageK8s збирає статистику щодо посилань на спільні бекенди та помилки щодо namespace з базових YAML-документів пакета, виключаючи оверлей `ua`.
|
|
18
|
+
|
|
19
|
+
## Публічний API
|
|
34
20
|
|
|
35
|
-
|
|
21
|
+
- ABIE_SHARED_CROSS_NS_BACKEND_NAMES — Ідентифікація назв бекендів, спільних між різними просторами імен.
|
|
22
|
+
- analyzeAbieSharedBackendRefsInPackageK8s — Аналізує YAML-файли пакета, збираючи кількість спільних посилань на бекенди та виявляючи базові помилки.
|
|
36
23
|
|
|
37
24
|
## Гарантії поведінки
|
|
38
25
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
* Функція повертає підрахунок `backendRefs` для спільних сервісів.
|
|
27
|
+
* Підрахунок здійснюється у base-маніфестах пакета поза overlay `ua`.
|
|
28
|
+
* Використовується `ua_http_route_concern` для синхронізації кількості patch-ів namespace у overlay із кількістю base-reference.
|
|
29
|
+
* Функція є read-only.
|
|
30
|
+
* Функція не виконує операцій з мережею.
|
|
31
|
+
* Функція не використовує кешування.
|
|
32
|
+
* Функція не змінює стан системи.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Тимчасовий A/B-batch: docgen Tier 1 через omlx (gemma-4-e2b 4bit на MLX)
|
|
3
|
+
* замість pi/ollama. Перезаписує всі docs/<stem>.md для файлів з sym<4,
|
|
4
|
+
* НЕ ескалює в cloud. Призначення — порівняння якості omlx vs попередньої версії.
|
|
5
|
+
*
|
|
6
|
+
* Запуск: node npm/skills/docgen/js/docgen-batch-omlx.mjs [--limit N] [--from N]
|
|
7
|
+
* --limit N — обробити перші N файлів зі списку sym<4
|
|
8
|
+
* --from N — почати з індексу N (для дозапуску)
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { execSync } from 'node:child_process'
|
|
14
|
+
import { env } from 'node:process'
|
|
15
|
+
import { generateDoc } from './docgen-gen.mjs'
|
|
16
|
+
import { extractFacts } from './docgen-extract.mjs'
|
|
17
|
+
|
|
18
|
+
const ROOT = resolve(fileURLToPath(import.meta.url), '../../../../..')
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2)
|
|
21
|
+
const limitIdx = args.indexOf('--limit')
|
|
22
|
+
const limit = limitIdx !== -1 ? Number(args[limitIdx + 1]) : Infinity
|
|
23
|
+
const fromIdx = args.indexOf('--from')
|
|
24
|
+
const from = fromIdx !== -1 ? Number(args[fromIdx + 1]) : 0
|
|
25
|
+
|
|
26
|
+
env.N_CURSOR_DOCGEN_BACKEND = 'omlx'
|
|
27
|
+
|
|
28
|
+
const scanOut = execSync('node npm/bin/n-cursor.js docgen scan', { cwd: ROOT, encoding: 'utf8' })
|
|
29
|
+
const all = JSON.parse(scanOut)
|
|
30
|
+
|
|
31
|
+
const local = []
|
|
32
|
+
for (const f of all) {
|
|
33
|
+
try {
|
|
34
|
+
const src = readFileSync(join(ROOT, f.sourcePath), 'utf8')
|
|
35
|
+
const facts = extractFacts(src, join(ROOT, f.sourcePath))
|
|
36
|
+
const sym = (facts.internalSymbols ?? []).length
|
|
37
|
+
if (sym < 4) local.push({ ...f, sym })
|
|
38
|
+
} catch {
|
|
39
|
+
/* пропускаємо нечитані */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const slice = local.slice(from, from + limit)
|
|
44
|
+
console.log(`📋 Файлів sym<4 у проєкті: ${local.length}; обробляємо: ${slice.length} (from=${from}, limit=${limit === Infinity ? 'усе' : limit})`)
|
|
45
|
+
console.log(`🤖 Бекенд: omlx → ${env.N_CURSOR_DOCGEN_OMLX_URL ?? 'http://127.0.0.1:8000/v1/chat/completions'}`)
|
|
46
|
+
|
|
47
|
+
const stats = { ok: 0, err: 0, totalMs: 0, scores: [], errors: [] }
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < slice.length; i++) {
|
|
50
|
+
const f = slice[i]
|
|
51
|
+
const t0 = Date.now()
|
|
52
|
+
const pct = Math.round(((i + 1) / slice.length) * 100)
|
|
53
|
+
process.stdout.write(` [${i + 1}/${slice.length} ${pct}%] sym=${f.sym} ${f.sourcePath} ... `)
|
|
54
|
+
try {
|
|
55
|
+
const result = await generateDoc(join(ROOT, f.sourcePath), {
|
|
56
|
+
symThreshold: 999, // не уходити в cloud за sym
|
|
57
|
+
cloudModel: null // повністю вимкнути cloud-fallback навіть при low det-score
|
|
58
|
+
})
|
|
59
|
+
const docAbs = join(ROOT, f.docPath)
|
|
60
|
+
mkdirSync(dirname(docAbs), { recursive: true })
|
|
61
|
+
writeFileSync(docAbs, result.md)
|
|
62
|
+
const ms = Date.now() - t0
|
|
63
|
+
stats.ok++
|
|
64
|
+
stats.totalMs += ms
|
|
65
|
+
stats.scores.push(result.score ?? 0)
|
|
66
|
+
process.stdout.write(`✓ ${Math.round(ms / 1000)}s score=${result.score ?? '?'} tier=${result.tier}\n`)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
stats.err++
|
|
69
|
+
stats.errors.push({ path: f.sourcePath, msg: error.message })
|
|
70
|
+
process.stdout.write(`✗ ${error.message}\n`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const avgScore = stats.scores.length ? Math.round(stats.scores.reduce((a, b) => a + b, 0) / stats.scores.length) : 0
|
|
75
|
+
console.log(`\n${'─'.repeat(60)}`)
|
|
76
|
+
console.log(`✓ OK: ${stats.ok} ✗ Err: ${stats.err}`)
|
|
77
|
+
console.log(` Сумарний час: ${Math.round(stats.totalMs / 1000)}s; середній на файл: ${stats.ok ? Math.round(stats.totalMs / stats.ok / 1000) : 0}s`)
|
|
78
|
+
console.log(` Середній det-score: ${avgScore}`)
|
|
79
|
+
if (stats.errors.length) {
|
|
80
|
+
console.log('Помилки:')
|
|
81
|
+
for (const e of stats.errors) console.log(` - ${e.path}: ${e.msg}`)
|
|
82
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1 (Fact-anchoring): детермінований витяг «анкорів» — конкретних фрагментів
|
|
3
|
+
* з коду, які LLM зобовʼязана згадати в документації, щоб не зісковзнути на
|
|
4
|
+
* generic-фрази.
|
|
5
|
+
*
|
|
6
|
+
* Категорії анкорів:
|
|
7
|
+
* - urls : усі https?://… у вихідному коді
|
|
8
|
+
* - magicStrings : export const X = '…' з непорожнім value (≤120 символів)
|
|
9
|
+
* - errorMarkers : суфікси повідомлень про помилки виду `(rule.mdc)`
|
|
10
|
+
* - configRefs : посилання на .json-конфіги проєкту (.n-cursor.json, …)
|
|
11
|
+
* - examples : ```…```-блоки у file-header JSDoc (першому коментарі файла)
|
|
12
|
+
*
|
|
13
|
+
* Всі регулярки — на сирому src без AST: дешево, безпечно, без false-positive
|
|
14
|
+
* критичної ваги (надмір — менша проблема, ніж пропуск).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
|
|
18
|
+
const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]+)\2/g
|
|
19
|
+
const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
|
|
20
|
+
const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
|
|
21
|
+
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
22
|
+
const CODE_BLOCK_RE = /```[a-z]*\n([\s\S]*?)\n\s*\*?\s*```/g
|
|
23
|
+
|
|
24
|
+
/** Dedup масив, зберігаючи порядок появи. */
|
|
25
|
+
function uniq(arr) {
|
|
26
|
+
const seen = new Set()
|
|
27
|
+
const out = []
|
|
28
|
+
for (const x of arr) {
|
|
29
|
+
if (!seen.has(x)) {
|
|
30
|
+
seen.add(x)
|
|
31
|
+
out.push(x)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Витягує анкори з вихідного коду файла.
|
|
39
|
+
* @param {string} src
|
|
40
|
+
* @returns {{
|
|
41
|
+
* urls: string[],
|
|
42
|
+
* magicStrings: Array<{name:string, value:string}>,
|
|
43
|
+
* errorMarkers: string[],
|
|
44
|
+
* configRefs: string[],
|
|
45
|
+
* examples: string[]
|
|
46
|
+
* }}
|
|
47
|
+
*/
|
|
48
|
+
export function extractAnchors(src) {
|
|
49
|
+
const urls = uniq([...src.matchAll(URL_RE)].map(m => m[0]))
|
|
50
|
+
|
|
51
|
+
const magicStrings = []
|
|
52
|
+
const seenNames = new Set()
|
|
53
|
+
for (const m of src.matchAll(EXPORT_CONST_RE)) {
|
|
54
|
+
const name = m[1]
|
|
55
|
+
const value = m[3]
|
|
56
|
+
if (!seenNames.has(name) && value.length <= 120) {
|
|
57
|
+
seenNames.add(name)
|
|
58
|
+
magicStrings.push({ name, value })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const errorMarkers = uniq([...src.matchAll(ERROR_MARKER_RE)].map(m => m[1]))
|
|
63
|
+
const configRefs = uniq([...src.matchAll(CONFIG_REF_RE)].map(m => m[1]))
|
|
64
|
+
|
|
65
|
+
// Витягуємо code-block приклади тільки з file-header — там автор зазвичай показує контракт.
|
|
66
|
+
const headerMatch = src.match(FILE_HEADER_RE)
|
|
67
|
+
const examples = headerMatch ? uniq([...headerMatch[1].matchAll(CODE_BLOCK_RE)].map(m => m[1].trim())) : []
|
|
68
|
+
|
|
69
|
+
return { urls, magicStrings, errorMarkers, configRefs, examples }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Форматує анкори у компактний текст для system-промпта.
|
|
74
|
+
* Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
|
|
75
|
+
* анкори не додається, щоб не вводити LLM в оману «обовʼязковими» полями).
|
|
76
|
+
* @param {ReturnType<typeof extractAnchors>} a
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function anchorsToPrompt(a) {
|
|
80
|
+
const blocks = []
|
|
81
|
+
if (a.urls.length) blocks.push(`URLs (згадай у тексті): ${a.urls.join(', ')}`)
|
|
82
|
+
if (a.magicStrings.length) {
|
|
83
|
+
blocks.push(
|
|
84
|
+
`Експортовані константи-рядки (наведи назву і призначення): ${a.magicStrings.map(s => `${s.name}=${JSON.stringify(s.value)}`).join('; ')}`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
if (a.errorMarkers.length) blocks.push(`Маркери повідомлень (згадай у Поведінці): ${a.errorMarkers.map(m => `(${m})`).join(', ')}`)
|
|
88
|
+
if (a.configRefs.length) blocks.push(`Конфіги, на які спирається код: ${a.configRefs.join(', ')}`)
|
|
89
|
+
if (a.examples.length) blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${a.examples.map(e => '```\n' + e + '\n```').join('\n')}`)
|
|
90
|
+
if (!blocks.length) return ''
|
|
91
|
+
return `АНКОРИ ДО ОБОВ'ЯЗКОВОГО ВКЛЮЧЕННЯ:\n${blocks.join('\n')}`
|
|
92
|
+
}
|
|
@@ -5,7 +5,8 @@ import { spawnSync } from 'node:child_process'
|
|
|
5
5
|
import { env } from 'node:process'
|
|
6
6
|
import { resolveModel } from '../../../lib/models.mjs'
|
|
7
7
|
import { extractFacts } from './docgen-extract.mjs'
|
|
8
|
-
import {
|
|
8
|
+
import { extractAnchors } from './docgen-extract-anchors.mjs'
|
|
9
|
+
import { oneShotMessages, sectionMessages, criticMessages, refineMessages, guaranteesFromMarkers } from './docgen-prompts.mjs'
|
|
9
10
|
|
|
10
11
|
const QUALITY_THRESHOLD = 70
|
|
11
12
|
|
|
@@ -89,8 +90,62 @@ function scoreDoc(md, facts) {
|
|
|
89
90
|
return { score: Math.max(0, score), issues }
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
93
|
+
/**
|
|
94
|
+
* omlx-бекенд: справжні OpenAI-сумісні messages (system+user збереженi).
|
|
95
|
+
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`.
|
|
96
|
+
* URL: `N_CURSOR_DOCGEN_OMLX_URL` або http://127.0.0.1:8000/v1/chat/completions.
|
|
97
|
+
* Модель: переданий `model`, потім `N_CURSOR_DOCGEN_OMLX_MODEL`, потім дефолт.
|
|
98
|
+
*/
|
|
99
|
+
function callOmlxMessages(messages, model, timeoutMs, temperature = 0.2) {
|
|
100
|
+
const url = env.N_CURSOR_DOCGEN_OMLX_URL ?? 'http://127.0.0.1:8000/v1/chat/completions'
|
|
101
|
+
const m = model || env.N_CURSOR_DOCGEN_OMLX_MODEL || 'mlx-community--gemma-4-e2b-it-4bit'
|
|
102
|
+
const body = JSON.stringify({
|
|
103
|
+
model: m,
|
|
104
|
+
messages,
|
|
105
|
+
max_tokens: 4096,
|
|
106
|
+
temperature
|
|
107
|
+
})
|
|
108
|
+
// Ретраїмо лише transient curl-помилки (18 = transfer closed, 56 = recv failure, 52 = empty reply).
|
|
109
|
+
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
|
|
110
|
+
let lastErr
|
|
111
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
112
|
+
const r = spawnSync(
|
|
113
|
+
'curl',
|
|
114
|
+
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
|
|
115
|
+
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
|
|
116
|
+
)
|
|
117
|
+
if (r.error) {
|
|
118
|
+
lastErr = new Error(`omlx curl error: ${r.error.message}`)
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
if (r.status !== 0) {
|
|
122
|
+
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
|
|
123
|
+
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
|
|
127
|
+
}
|
|
128
|
+
let j
|
|
129
|
+
try { j = JSON.parse(r.stdout) } catch { throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`) }
|
|
130
|
+
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
|
|
131
|
+
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
|
|
132
|
+
if (!content) {
|
|
133
|
+
const finish = j.choices?.[0]?.finish_reason
|
|
134
|
+
throw new Error(`omlx empty content (finish=${finish})`)
|
|
135
|
+
}
|
|
136
|
+
return content
|
|
137
|
+
}
|
|
138
|
+
throw lastErr ?? new Error('omlx unknown failure')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Універсальний виклик LLM за повним messages-масивом.
|
|
143
|
+
* - omlx: шле messages напряму (system збережено)
|
|
144
|
+
* - pi: конкатенує message.content (pi приймає лише plain prompt)
|
|
145
|
+
*/
|
|
146
|
+
function callLlm(messages, model, timeoutMs, temperature = 0.2) {
|
|
147
|
+
if (env.N_CURSOR_DOCGEN_BACKEND === 'omlx') return callOmlxMessages(messages, model, timeoutMs, temperature)
|
|
148
|
+
const prompt = messages.map(m => m.content).join('\n\n')
|
|
94
149
|
const modelArgs = model ? ['--model', model] : []
|
|
95
150
|
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
|
|
96
151
|
encoding: 'utf8',
|
|
@@ -101,9 +156,30 @@ function callPi(prompt, model, timeoutMs) {
|
|
|
101
156
|
return r.stdout?.trim() ?? ''
|
|
102
157
|
}
|
|
103
158
|
|
|
104
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* E2 — один цикл critique→refine на секцію.
|
|
161
|
+
* Повертає або уточнену чорнетку, або оригінал якщо критик повідомив NONE.
|
|
162
|
+
*/
|
|
163
|
+
function critiqueRefineSection(sectionKey, draft, facts, anchors, model, timeoutMs) {
|
|
164
|
+
const critique = callLlm(criticMessages(sectionKey, draft, facts, anchors), model, timeoutMs).trim()
|
|
165
|
+
if (!critique || /^\s*NONE\s*$/i.test(critique) || critique.length < 12) return draft
|
|
166
|
+
const refined = callLlm(refineMessages(sectionKey, draft, critique, facts, anchors), model, timeoutMs).trim()
|
|
167
|
+
return stripSignatures(stripSection(refined)) || draft
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
|
|
172
|
+
* (саме там модель схильна писати «застосовує логіку до файлу»).
|
|
173
|
+
*/
|
|
174
|
+
function apiNeedsRefine(facts) {
|
|
175
|
+
const exps = facts.exports ?? []
|
|
176
|
+
if (exps.length <= 1) return false
|
|
177
|
+
return exps.every(e => !e.desc)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** One-shot: один виклик LLM на весь документ. */
|
|
105
181
|
function piOneShot(facts, src, model, timeoutMs = 120_000) {
|
|
106
|
-
const text =
|
|
182
|
+
const text = callLlm(oneShotMessages(facts, src), model, timeoutMs)
|
|
107
183
|
let md = stripSignatures(stripSection(text))
|
|
108
184
|
if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
|
|
109
185
|
return { md: md + '\n', genTok: 0 }
|
|
@@ -129,12 +205,19 @@ function assemble(stem, sections) {
|
|
|
129
205
|
* Orchestrated: N окремих pi-викликів, по одному на секцію.
|
|
130
206
|
* Код потрапляє лише в `behavior`; решта секцій — на мінімальному факт-листі.
|
|
131
207
|
*/
|
|
132
|
-
function piOrchestrated(facts, src, model, timeoutMs) {
|
|
208
|
+
function piOrchestrated(facts, src, model, timeoutMs, { anchors = null, temperature = 0.2 } = {}) {
|
|
133
209
|
const sections = {}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
210
|
+
const anc = anchors ?? extractAnchors(src)
|
|
211
|
+
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
212
|
+
sections.guarantees = guaranteesFromMarkers(facts)
|
|
213
|
+
for (const s of sectionMessages(facts, src, anc)) {
|
|
214
|
+
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
|
|
215
|
+
let draft = stripSignatures(stripSection(callLlm(s.messages, model, timeoutMs, temperature)))
|
|
216
|
+
// E2 + E3: critique→refine лише для секцій, де gemma-4 зриває на generic
|
|
217
|
+
if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
|
|
218
|
+
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
|
|
219
|
+
}
|
|
220
|
+
sections[s.key] = draft
|
|
138
221
|
}
|
|
139
222
|
return { md: assemble(basename(facts.relPath), sections), genTok: 0 }
|
|
140
223
|
}
|
|
@@ -181,10 +264,11 @@ export async function generateDoc(
|
|
|
181
264
|
// Tier 1: pi orchestrated (секція за секцією), timeout на секцію = LOCAL_TIMEOUT_MS
|
|
182
265
|
// facts.unsupported → one-shot (структура файлу нестандартна)
|
|
183
266
|
let r
|
|
267
|
+
const anchors = facts.unsupported ? null : extractAnchors(src)
|
|
184
268
|
try {
|
|
185
269
|
r = facts.unsupported
|
|
186
270
|
? piOneShot(facts, src, model, LOCAL_TIMEOUT_MS)
|
|
187
|
-
: piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS)
|
|
271
|
+
: piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS, { anchors })
|
|
188
272
|
} catch (error) {
|
|
189
273
|
if (cloudModel) {
|
|
190
274
|
const r2 = piOneShot(facts, src, cloudModel)
|
|
@@ -194,7 +278,25 @@ export async function generateDoc(
|
|
|
194
278
|
}
|
|
195
279
|
|
|
196
280
|
// Stage 2.5: детермінований скоринг (0 токенів) — gate перед Tier 2
|
|
197
|
-
|
|
281
|
+
let { score: detScore, issues: detIssues } = scoreDoc(r.md, facts)
|
|
282
|
+
|
|
283
|
+
// E4: best-of-N. Якщо score нижчий за threshold і немає cloud-fallback — спроба
|
|
284
|
+
// ще раз з вищою температурою, керуємо через env (повторні прогони коштовні).
|
|
285
|
+
if (detScore < threshold && !cloudModel && !facts.unsupported && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
|
|
286
|
+
try {
|
|
287
|
+
const r2 = piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
|
|
288
|
+
const s2 = scoreDoc(r2.md, facts)
|
|
289
|
+
if (s2.score > detScore) {
|
|
290
|
+
r = r2
|
|
291
|
+
detScore = s2.score
|
|
292
|
+
detIssues = [...s2.issues, 'best-of-2:retry-won']
|
|
293
|
+
} else {
|
|
294
|
+
detIssues = [...detIssues, 'best-of-2:retry-lost']
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
detIssues = [...detIssues, `best-of-2:retry-error: ${error.message}`]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
198
300
|
|
|
199
301
|
if (detScore < threshold && cloudModel) {
|
|
200
302
|
const r2 = piOneShot(facts, src, cloudModel)
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
/** @see ./docs/docgen-prompts.md */
|
|
2
2
|
|
|
3
|
+
import { anchorsToPrompt } from './docgen-extract-anchors.mjs'
|
|
4
|
+
|
|
3
5
|
export const STYLE = [
|
|
4
6
|
'Ти технічний письменник. Пишеш лаконічну ПОВЕДІНКОВУ документацію до коду українською, чистим Markdown.',
|
|
5
7
|
'Пиши ЩО і НАВІЩО, не ЯК. Без вступів і висновків. Не обгортай у ```-блок.',
|
|
6
8
|
'Заборонено: сигнатури, типи, параметри функцій; перелік stdlib-модулів; опис regex чи внутрішніх приватних імен.'
|
|
7
9
|
].join(' ')
|
|
8
10
|
|
|
11
|
+
/** Окремий блок інструкцій з анкорами — підставляється коли вони є. */
|
|
12
|
+
function anchorsBlock(anchors) {
|
|
13
|
+
if (!anchors) return ''
|
|
14
|
+
const txt = anchorsToPrompt(anchors)
|
|
15
|
+
return txt ? `\n\n${txt}` : ''
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* Короткий людиночитний витяг фактів (без коду).
|
|
11
20
|
* @param {object} facts факт-лист про файл
|
|
@@ -38,8 +47,9 @@ const msgs = (system, user) => [
|
|
|
38
47
|
* @param {string} src вміст файлу
|
|
39
48
|
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів
|
|
40
49
|
*/
|
|
41
|
-
export function sectionMessages(facts, src) {
|
|
50
|
+
export function sectionMessages(facts, src, anchors = null) {
|
|
42
51
|
const factsTxt = factsSummary(facts)
|
|
52
|
+
const anch = anchorsBlock(anchors)
|
|
43
53
|
const multi = (facts.exports?.length || 0) > 1
|
|
44
54
|
const out = []
|
|
45
55
|
|
|
@@ -48,8 +58,8 @@ export function sectionMessages(facts, src) {
|
|
|
48
58
|
key: 'overview',
|
|
49
59
|
numPredict: 220,
|
|
50
60
|
messages: msgs(
|
|
51
|
-
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
52
|
-
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій.'
|
|
61
|
+
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
62
|
+
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
|
|
53
63
|
)
|
|
54
64
|
})
|
|
55
65
|
|
|
@@ -58,8 +68,8 @@ export function sectionMessages(facts, src) {
|
|
|
58
68
|
key: 'behavior',
|
|
59
69
|
numPredict: 500,
|
|
60
70
|
messages: msgs(
|
|
61
|
-
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
62
|
-
`Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без
|
|
71
|
+
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
|
|
72
|
+
`Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
63
73
|
)
|
|
64
74
|
})
|
|
65
75
|
|
|
@@ -70,25 +80,90 @@ export function sectionMessages(facts, src) {
|
|
|
70
80
|
key: 'api',
|
|
71
81
|
numPredict: 320,
|
|
72
82
|
messages: msgs(
|
|
73
|
-
STYLE
|
|
74
|
-
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез
|
|
83
|
+
`${STYLE}${anch}`,
|
|
84
|
+
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
|
|
75
85
|
)
|
|
76
86
|
})
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
// Гарантії — лише markers (без коду)
|
|
80
|
-
out.push({
|
|
81
|
-
key: 'guarantees',
|
|
82
|
-
numPredict: 300,
|
|
83
|
-
messages: msgs(
|
|
84
|
-
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
|
|
85
|
-
'Напиши вміст секції «Гарантії поведінки» як маркери-інваріанти СУВОРО на основі ВІДОМИХ ФАКТІВ (read-only, fail-safe, пропуски). Згадуй кеш ЛИШЕ якщо у фактах прямо є «Кешує». Без сигнатур у дужках і без імен внутрішніх структур/Map-ів/кешів. Не вигадуй гарантій, яких немає у фактах. Без заголовка.'
|
|
86
|
-
)
|
|
87
|
-
})
|
|
88
|
-
|
|
89
89
|
return out
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* E2-step 1 — критик. Перевіряє чорнетку секції на конкретні дефекти.
|
|
94
|
+
* Повертає messages для LLM-запиту: вихід має бути СПИСКОМ issues або словом NONE.
|
|
95
|
+
* @param {'overview'|'behavior'|'api'} sectionKey
|
|
96
|
+
* @param {string} draft вже згенерована чорнетка секції
|
|
97
|
+
* @param {object} facts факт-лист
|
|
98
|
+
* @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
|
|
99
|
+
* @returns {Array<{role:string,content:string}>}
|
|
100
|
+
*/
|
|
101
|
+
export function criticMessages(sectionKey, draft, facts, anchors) {
|
|
102
|
+
const anch = anchorsBlock(anchors)
|
|
103
|
+
const criteria = [
|
|
104
|
+
'generic-фрази без конкретики («забезпечує перевірку», «виконує валідацію», «застосовує логіку»)',
|
|
105
|
+
'пропущені обов\'язкові АНКОРИ з контексту (URLs, magic-string constants, error-маркери, конфіги, code-приклади)',
|
|
106
|
+
'граматичні помилки українською («перед їх застосування», «моделіне», англіцизми як «applys», «moduleline»)',
|
|
107
|
+
'h1/h2/h3 підзаголовки всередині секції — їх не повинно бути',
|
|
108
|
+
'дослівна копія JSDoc-сигнатури або параметрів у дужках',
|
|
109
|
+
'вигадані факти, відсутні у ВІДОМИХ ФАКТАХ і АНКОРАХ'
|
|
110
|
+
].join('\n - ')
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
role: 'system',
|
|
114
|
+
content: `Ти жорсткий редактор технічної документації українською. Знаходиш конкретні дефекти у чорнетці. ВІДОМІ ФАКТИ:\n${factsSummary(facts)}${anch}`
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
role: 'user',
|
|
118
|
+
content: `Перевір цю чорнетку секції «${sectionKey}» за критеріями:\n - ${criteria}\n\nЧЕРНЕТКА:\n${draft}\n\nВідповідь — короткий нумерований список знайдених issues (1-5 пунктів). Якщо дефектів немає — поверни одне слово: NONE.`
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* E2-step 2 — refine. Переписує чорнетку, виправляючи перелічені issues.
|
|
125
|
+
* @param {'overview'|'behavior'|'api'} sectionKey
|
|
126
|
+
* @param {string} draft
|
|
127
|
+
* @param {string} issues список issues від critic
|
|
128
|
+
* @param {object} facts
|
|
129
|
+
* @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
|
|
130
|
+
* @returns {Array<{role:string,content:string}>}
|
|
131
|
+
*/
|
|
132
|
+
export function refineMessages(sectionKey, draft, issues, facts, anchors) {
|
|
133
|
+
const anch = anchorsBlock(anchors)
|
|
134
|
+
return [
|
|
135
|
+
{
|
|
136
|
+
role: 'system',
|
|
137
|
+
content: `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsSummary(facts)}${anch}`
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
role: 'user',
|
|
141
|
+
content: `Перепиши чорнетку секції «${sectionKey}», прибравши перелічені issues. Збережи мову (українська) і формат (без додаткових ## підзаголовків, без обгортки \`\`\`). Якщо issues вимагають включення АНКОРІВ — додай їх дослівно.\n\nЧЕРНЕТКА:\n${draft}\n\nISSUES ВІД РЕДАКТОРА:\n${issues}\n\nПоверни ЛИШЕ оновлений текст секції без преамбули.`
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* E3 — детермінований шаблон секції «Гарантії поведінки» з facts.markers.
|
|
148
|
+
* НЕ використовує LLM: 0 запитів, 0 галюцинацій, 0 generic-фраз.
|
|
149
|
+
* @param {object} facts
|
|
150
|
+
* @returns {string} текст секції (без `## Гарантії` — це додає assemble())
|
|
151
|
+
*/
|
|
152
|
+
export function guaranteesFromMarkers(facts) {
|
|
153
|
+
const m = facts.markers || {}
|
|
154
|
+
const lines = []
|
|
155
|
+
if (m.readOnly) lines.push('- Read-only: файл не виконує операцій запису у файлову систему.')
|
|
156
|
+
if (m.catchesErrors) lines.push('- Перехоплює помилки і не пропускає винятків назовні (fail-safe).')
|
|
157
|
+
if (m.returnsFalsyOnFail) lines.push('- За невдалої перевірки повертає `false`/`null` замість винятку.')
|
|
158
|
+
if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
|
|
159
|
+
if (m.skips?.length) {
|
|
160
|
+
lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
|
|
161
|
+
}
|
|
162
|
+
if (!m.network) lines.push('- Не звертається до мережі.')
|
|
163
|
+
if (!lines.length) return '- Поведінка детермінована: результат залежить лише від вхідних даних.'
|
|
164
|
+
return lines.join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
92
167
|
/**
|
|
93
168
|
* One-shot messages (база для порівняння).
|
|
94
169
|
* @param {object} facts факт-лист про файл
|