@7n/rules 1.48.1 → 1.49.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/n-rules-cli.mjs +2045 -0
  3. package/bin/n-rules.js +4 -2026
  4. package/package.json +1 -1
  5. package/rules/changelog/.changes/260724-1500.md +5 -0
  6. package/rules/doc-files/docgen-files-batch/docs/index.md +9 -0
  7. package/rules/doc-files/docgen-files-batch/docs/main.md +59 -18
  8. package/rules/doc-files/docgen-files-batch/main.mjs +280 -29
  9. package/rules/doc-files/docgen-gen/docs/index.md +9 -0
  10. package/rules/doc-files/docgen-gen/docs/main.md +40 -29
  11. package/rules/doc-files/docgen-gen/main.mjs +79 -25
  12. package/rules/test/coverage/fix-worker.mjs +9 -1
  13. package/rules/test/coverage/lib/classify/verdict-schema.mjs +3 -1
  14. package/scripts/docs/skills-cli.md +18 -24
  15. package/scripts/lib/acp-runner.mjs +1 -1
  16. package/scripts/lib/lint-surface/collateral-veto.mjs +78 -2
  17. package/scripts/lib/lint-surface/docs/collateral-veto.md +30 -16
  18. package/scripts/lib/lint-surface/docs/index.md +1 -0
  19. package/scripts/lib/lint-surface/docs/run-fix.md +8 -23
  20. package/scripts/lib/lint-surface/docs/snapshot.md +6 -17
  21. package/scripts/lib/lint-surface/docs/test-gate.md +29 -0
  22. package/scripts/lib/lint-surface/run-fix.mjs +209 -38
  23. package/scripts/lib/lint-surface/snapshot.mjs +6 -0
  24. package/scripts/lib/lint-surface/test-gate.mjs +87 -0
  25. package/scripts/skills-cli.mjs +60 -10
  26. package/scripts/utils/docs/glob-compat.md +20 -14
  27. package/scripts/utils/glob-compat.mjs +18 -3
  28. package/skills/git-reconcile/SKILL.md +58 -0
  29. package/skills/git-reconcile/js/docs/index.md +9 -0
  30. package/skills/git-reconcile/js/docs/orchestrate.md +33 -0
  31. package/skills/git-reconcile/js/orchestrate.mjs +776 -0
  32. package/skills/git-reconcile/main.json +1 -0
  33. package/skills/taze/js/docs/orchestrate.md +40 -18
  34. package/skills/taze/js/orchestrate.mjs +6 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.48.1",
3
+ "version": "1.49.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,5 @@
1
+ ---
2
+ bump: patch
3
+ section: Fixed
4
+ ---
5
+ changelog consistency: додано change-файл для змін у npm/rules/changelog
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/doc-files/docgen-files-batch
4
+ resource: npm/rules/doc-files/docgen-files-batch/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------- | --------- |
9
+ | [main.mjs](main.md) | JS Module |
@@ -3,36 +3,77 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-files-batch/main.mjs
5
5
  docgen:
6
- crc: 8b00ffa9
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 100
9
- issues: judge:inaccurate:0.99
6
+ crc: 5245bd1f
7
+ model: openai-codex/gpt-5.5
8
+ tier: cloud-avg
9
+ score: 80
10
+ issues: internal-name:generateOne,internal-name:runBatchPass,judge-refine:kept-original,judge:inaccurate:0.99
10
11
  judgeModel: openai-codex/gpt-5.4-mini
11
12
  ---
12
13
 
13
14
  ## Огляд
14
15
 
15
- Цей модуль керує повним циклом генерації документації. Він використовує функцію `selectTargets` для вибору цільових файлів та `purgeOrphanedDocs` для видалення посилань на відсутні файли. Для ініціалізації процесу застосовується `runDocFilesGenCli`, а пакетна генерація контролюється через `runGenerationBatch`. Після генерації метадані файлів оновлюються за допомогою `runDocFilesStampCli`, що додає хеш відповідного вихідного файлу. Усі операції виконуються з механізмом fail-safe, що перехоплює помилки та не кидає винятків назовні.
16
+ Файл керує життєвим циклом поведінкової документації для source-файлів: `selectTargets` визначає цілі, `runGenerationBatch` оновлює docs-файли, `purgeOrphanedDocs` прибирає сирітські документи, а `generateDirIndex` підтримує індекси директорій. `nativeBatchAvailable`, `runDocFilesGenCli` і `runDocFilesStampCli` забезпечують безпечний контур CLI/batch-прогонів із кешуванням у межах одного запуску.
17
+
18
+ Модуль звертається до мережі, але працює fail-safe: перехоплює помилки, не кидає винятків назовні і дає прогону завершитися контрольовано.
16
19
 
17
20
  ## Поведінка
18
21
 
19
- Поведінка
20
- selectTargets вибирає файли для генерації документації, які є застарілими або мають низьку якість, виключаючи ті, що вже мають потік (tier) `cloud-avg`. Рукописні доки (docPath існує без docgen-frontmatter, `foreign`) без `--overwrite` цілями не стають.
21
- purgeOrphanedDocs видаляє вказівники на файли документації, для яких не існує відповідного вихідного файлу, і оновлює індекси директорій.
22
- runDocFilesGenCli керує процесом генерації документації, видаляючи сирітських доків, визначаючи цілі та запускаючи пакетну генерацію; про пропущені рукописні (foreign) доки попереджає у stderr — тихого перезапису людського змісту немає, перезапис лише explicit `--overwrite`.
23
- runGenerationBatch виконує генерацію документації для визначеного набору файлів, керуючи логікою циркут-брейкера при системних збоях; опційний `deadlineAt` (м'який дедлайн fix-pipeline) зупиняє батч перед стартом наступного файлу штатно зроблене лишається на диску зі свіжими CRC, решту підбирає наступний прогін (перший файл стартує завжди). Той самий `deadlineAt` прокидається у `generateDoc`, тож per-call LLM-таймаути ріжуться під залишок бюджету і файл у процесі обривається на дедлайні transient-помилкою, а не живе батчем-зомбі поверх наступного rung-а.
24
- generateDirIndex (пере)генерує `index.md` директорії docs/ як OKF Directory Index без H1 у тілі — top-level заголовком лишається frontmatter `title:`, тож MD025/single-title чистий; чужий `index.md` (дока source-файлу чи людський зміст без OKF-типу) не перезаписується.
25
- runDocFilesStampCli оновлює метадані (frontmatter) наявних файлів документації, додаючи хеш вихідного файлу, без повторного запуску генерації.
22
+ runDocFilesGenCli запускає повний прогін документації: прибирає сирітські файли через purgeOrphanedDocs, обирає актуальні цілі через selectTargets і передає їх у runGenerationBatch. Результати записуються у відповідні docs-файли, а після проходу оновлюються директорійні індекси через generateDirIndex.
23
+
24
+ selectTargets підтримує збіжний режим роботи: за замовчуванням бере відсутні, застарілі або degraded-документи, але не ганяє безкінечно ті самі degraded-версії без зміни джерела. Режим перезапису переводить вибір у повну регенерацію всіх знайдених цілей.
25
+
26
+ runGenerationBatch є спільним ядром для CLI і автоматичних scoped-прогонів. Перед роботою перевіряє локальний LLM-backend, далі або використовує native batch-шлях після позитивного nativeBatchAvailable, або переходить у послідовний безпечний режим. Для локального provider-контуру очікується endpoint http://127.0.0.1:8000/v1/. Помилки класифікуються так, щоб незворотні пропуски не ламали весь прогін, інфраструктурні збої потрапляли у підсумкову статистику, а системні падіння могли зупинити batch fail-safe exit-кодом без винятків назовні.
27
+
28
+ nativeBatchAvailable кешує результат перевірки в межах прогону, щоб не повторювати однаковий тест доступності native-аддона. Якщо batch-недоступний або є м’який дедлайн, runGenerationBatch лишається на послідовному шляху, де частковий прогрес безпечно зберігається по файлах і наступний запуск продовжує за станом CRC.
29
+
30
+ purgeOrphanedDocs видаляє документацію без відповідного source-файлу, після чого підтримує docs-директорії у чистому стані: оновлює index.md або прибирає порожню директорію. generateDirIndex формує локальний огляд наявних документів у конкретній docs-директорії й не створює індекс там, де немає документів для переліку.
31
+
32
+ runDocFilesStampCli не звертається до LLM: він лише приводить наявні документи до актуального frontmatter-штампа source і CRC, зберігаючи вже наявні метадані моделі та якості. Це дає міграційний шлях для старих документів без повторної генерації змісту.
26
33
 
27
34
  ## Публічний API
28
35
 
29
- selectTargets — Визначає цілі для генерації документації: `default` — застарілі або погіршені доки, які ще не пройшли перерахунок CRC; `--overwrite` — всі доки. Погіршена дока отримує лише один повторний ретрай на версію джерела; після невдалого ретраю вона позначається як `retried: true` і більше не змінюється до оновлення джерела.
30
- purgeOrphanedDocs Видаляє доки, які втратили зв'язок із джерелом, та оновлює індекс.md. Якщо в каталозі docs залишається лише index.md або нічого, він очищається.
31
- runDocFilesGenCli Генерує документацію для доків, які є застарілими або відсутніми.
32
- runGenerationBatch Основний процес генерації: перевіряє локальний бекенд, послідовно генерує документацію для визначених цілей, застосовуючи захист від ланцюгових збоїв і м'який дедлайн (`deadlineAt`); також використовується для лінтінгу змінених файлів.
33
- generateDirIndex — (Пере)генерує Directory Index (`index.md`) у docs/-директорії; детекція власного індексу за frontmatter `type: Directory Index`, чужі index.md недоторкані.
34
- runDocFilesStampCli Автоматично додає або виправляє метадані (source + crc) у існуючих доках без залучення LLM. Цей процес використовується для міграції доків без CRC, зберігаючи при цьому інформацію про модель та якість з метаданих.
36
+ - selectTargets — Цілі генерації:
37
+ - default → застарілі (stale) АБО degraded-доки, які ще не доретраювали при цьому CRC;
38
+ - `--overwrite` усі.
39
+ Degraded-док отримує рівно ОДИН доретрай на версію джерела: після невдалого доретраю
40
+ (лишився degraded) штампується `retried: true` і його більше не чіпають до зміни джерела
41
+ (нова версія CRC-mismatch stale лічильник скидається). Конвеєр сходиться без прапора.
42
+ - nativeBatchAvailable — Чи доступний native-аддон `@7n/llm-lib` для 2b-batch (T8, рішення Р). Викликає
43
+ `submitBatchImpl` з порожнім `items` — це не робить жодного LLM-виклику
44
+ (Rust-крейт повертає порожній результат до резолву моделі), лише перевіряє,
45
+ що napi-аддон завантажується. Zero-native споживачі (аддон не зібраний/не
46
+ підтримувана платформа) отримують `false` і йдуть у послідовний фолбек.
47
+ - generateDirIndex — Генерує/оновлює `index.md` у директорії `docs/` — OKF Directory Index із таблицею
48
+ всіх наявних doc-файлів у цій директорії. Не зачіпає `index.md` при відсутності
49
+ інших doc-файлів.
50
+ - purgeOrphanedDocs — Видаляє сирітські доки (source-файл не існує) і оновлює/прибирає index.md.
51
+ Якщо після видалення в docs/-директорії лишились тільки index.md або нічого — очищує її.
52
+ - runDocFilesGenCli — `doc-files gen` — згенерувати документацію для застарілих/відсутніх док.
53
+ - runGenerationBatch — Спільне ядро генерації: preflight локального бекенда → послідовний прогін
54
+ `targets` через `generateOne` з circuit-breaker'ом (K systemic-збоїв підряд →
55
+ abort) → підсумковий звіт. Перевикористовують і батч-CLI (`runDocFilesGenCli`),
56
+ і opportunistic lint-крок doc-files (scoped-набір змінених файлів).
57
+
58
+ `deadlineAt` (epoch ms): м'який дедлайн fix-pipeline — перед стартом КОЖНОГО
59
+ наступного файлу (перший стартує завжди) батч звіряється з дедлайном і, коли час
60
+ вийшов, завершується штатно з частковим прогресом. Той самий дедлайн прокидається
61
+ у generateDoc: per-call LLM-таймаути ріжуться під залишок бюджету, тож і файл
62
+ У ПРОЦЕСІ обривається на дедлайні (transient-помилка), а не живе батчем-зомбі
63
+ поверх наступного rung-а. Зроблене записано по одному файлу (durable, свіжий
64
+ CRC) — наступний прогін підбирає решту за CRC.
65
+ T8 (2b-batch, рішення Р): коли доступний native-аддон `@7n/llm-lib` (`nativeBatchAvailable`)
66
+ і рунг БЕЗ `deadlineAt` (fix-pipeline рунги лишаються на послідовному шляху —
67
+ там дедлайн підтримується), увесь `targets` іде ОДНИМ `submitBatch` через
68
+ `runBatchPass` замість цього циклу по одному файлу. Zero-native споживачі
69
+ (аддон не зібраний/платформа не підтримується) автоматично лишаються на
70
+ послідовному шляху нижче — жодної відмінності в CLI/skill/hook-контракті.
71
+ - runDocFilesStampCli — `doc-files stamp` — детерміновано (пере)штампувати frontmatter `source`+`crc`
72
+ у НАЯВНИХ доках без виклику LLM. Для міграції док, які ще не мають CRC.
73
+ Поля `model`, `tier` та якості (`score`/`issues`/`judgeModel`) при цьому зберігаються
74
+ з наявного frontmatter.
35
75
 
36
76
  ## Гарантії поведінки
37
77
 
38
78
  - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
79
+ - Кешує результати в межах одного прогону.
@@ -11,12 +11,14 @@ import {
11
11
  } from 'node:fs'
12
12
  import { spawnSync } from 'node:child_process'
13
13
  import { basename, dirname, join, relative } from 'node:path'
14
+ import { env } from 'node:process'
14
15
 
15
16
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
16
17
  import { createProgressReporter } from '../../../scripts/lib/lint-surface/progress.mjs'
17
- import { generateDoc, DEFAULT_LOCAL_MODEL } from '../docgen-gen/main.mjs'
18
+ import { generateDoc, DEFAULT_LOCAL_MODEL, prepareBatchItem, finishBatchItem } from '../docgen-gen/main.mjs'
18
19
  import { crc32, stampDoc, readDocQuality, readDocModel, readDocTier, QUALITY_THRESHOLD } from '../docgen-crc/main.mjs'
19
20
  import { resolveRoot, scanForDocFiles, scanOrphanedDocs } from '../docgen-scan/main.mjs'
21
+ import { submitBatch as submitBatchNative } from '@7n/llm-lib/batch'
20
22
 
21
23
  /** Regex-класифікатори помилки генерації (module-scope, без ре-компіляції на виклик). */
22
24
  const ERR_PERMANENT_RE = /prompt too long|pre-send guard|too long/i
@@ -192,6 +194,219 @@ async function generateOne(file, root, progress, stats, { model, tier, emit, dea
192
194
  }
193
195
  }
194
196
 
197
+ /**
198
+ * Кеш перевірки доступності native-аддону (T8): один процес — один результат
199
+ * ЛИШЕ для дефолтного `submitBatchNative` (production-шлях) — перевірка триває
200
+ * на першому виклику (порожній `items` — 0 LLM-викликів, лише спроба
201
+ * завантажити napi-аддон). Інжектований `submitBatchImpl` (тести) кешем НЕ
202
+ * керується — інакше перший тест зафіксував би результат для всіх наступних.
203
+ * @type {boolean|null}
204
+ */
205
+ let nativeBatchAvailableCache = null
206
+
207
+ /**
208
+ * Чи доступний native-аддон `@7n/llm-lib` для 2b-batch (T8, рішення Р). Викликає
209
+ * `submitBatchImpl` з порожнім `items` — це не робить жодного LLM-виклику
210
+ * (Rust-крейт повертає порожній результат до резолву моделі), лише перевіряє,
211
+ * що napi-аддон завантажується. Zero-native споживачі (аддон не зібраний/не
212
+ * підтримувана платформа) отримують `false` і йдуть у послідовний фолбек.
213
+ * @param {(modelSpecOrTier: string, items: Array<object>) => Promise<Array<object>>} submitBatchImpl injectable submitBatch (тест/прод)
214
+ * @param {boolean} [useCache] кешувати результат (типово лише для дефолтного `submitBatchNative`)
215
+ * @returns {Promise<boolean>} true — можна йти batch-шляхом
216
+ */
217
+ export async function nativeBatchAvailable(submitBatchImpl, useCache = true) {
218
+ if (useCache && nativeBatchAvailableCache !== null) return nativeBatchAvailableCache
219
+ let result
220
+ try {
221
+ await submitBatchImpl('min', [])
222
+ result = true
223
+ } catch {
224
+ result = false
225
+ }
226
+ if (useCache) nativeBatchAvailableCache = result
227
+ return result
228
+ }
229
+
230
+ /**
231
+ * `localProviders` для Rust-крейта `llm_lib::local_cloud` (2b-batch, T8):
232
+ * інша половина конфігурації, ніж у sequential-шляху. `generateDoc`/`callLlm`
233
+ * ходять через `runOneShot` (pi ModelRegistry, свій резолв ендпоінта — читає
234
+ * pi-конфіг), а `submitBatch` — через Rust `LocalCloud`, якому явний
235
+ * `{ omlx: { baseUrl, apiKey } }` ОБОВʼЯЗКОВИЙ: без нього незнайомий provider-
236
+ * префікс (`"omlx/…"`) падає крізь cloud-гілку genai, яка бачить голу назву
237
+ * моделі без провайдера й типово вгадує адаптер Ollama (`localhost:11434`,
238
+ * жива помилка з бенчу T8) — тихий, неочевидний збій. Той самий baseUrl-
239
+ * конвент, що й `llm-lib/tests/batch.test.mjs` та `examples/batch_bench.rs`.
240
+ * Override — `opts.localProviders` (кастомний конфіг для нестандартного порту).
241
+ * @returns {{ omlx: { baseUrl: string, apiKey: string|null } }} дефолтна мапа локальних провайдерів
242
+ */
243
+ function defaultLocalProviders() {
244
+ return { omlx: { baseUrl: 'http://127.0.0.1:8000/v1/', apiKey: env.OMLX_API_KEY ?? null } }
245
+ }
246
+
247
+ /**
248
+ * T8 (2b-batch, рішення Р): один `submit` на ВЕСЬ набір `targets` замість
249
+ * послідовного циклу по одному файлу. Готує items через `prepareBatchItem`
250
+ * (pre-send guard тут же відсіює завеликі джерела — 0 LLM-викликів/0 items у
251
+ * batch-і), один `submitBatchImpl(...)`, потім постобробка кожного результату
252
+ * (`finishBatchItem`) і запис доки — той самий штамп/файл-запис, що й
253
+ * послідовний `generateOne`. Помилка ОДНОГО item-у (класифікується
254
+ * `classifyDocgenError`, як і в послідовному шляху) не валить решту —
255
+ * заноситься у `stats.err`/`stats.skipped`, batch триває.
256
+ *
257
+ * Обмеження v1: без circuit-breaker (concurrency в Rust-крейті — помилки не
258
+ * «підряд» у тому сенсі, що має сенс для fail-fast) і без `deadlineAt`
259
+ * (викликач цього шляху не проставляє дедлайн — гейт `!opts.deadlineAt` у
260
+ * `runGenerationBatch`; fix-pipeline рунги лишаються на послідовному шляху,
261
+ * де м'який дедлайн підтримується).
262
+ * @param {Array<object>} targets елементи scanForDocFiles
263
+ * @param {string} root абсолютний корінь
264
+ * @param {{ model?: string, tier?: string|null, localProviders?: object, submitBatchImpl?: (modelSpecOrTier: string, items: Array<object>, opts?: object) => Promise<Array<object>> }} opts модель/тир/local-provider-конфіг (інакше `defaultLocalProviders()`)/інжект submitBatch
265
+ * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
266
+ * @param {{ reporter?: object, emit?: (s: string) => void }} io прогрес-репортер і логер рядка результату
267
+ * @returns {Promise<void>}
268
+ */
269
+ async function runBatchPass(targets, root, opts, stats, { reporter, emit }) {
270
+ const model = opts.model ?? DEFAULT_LOCAL_MODEL
271
+ const submitBatchImpl = opts.submitBatchImpl ?? submitBatchNative
272
+ const out = emit ?? (s => process.stdout.write(s))
273
+
274
+ const prepared = await prepareBatchTargets(targets, root, stats, { reporter, out })
275
+ if (prepared.length === 0) return
276
+
277
+ const items = prepared.map(p => ({
278
+ customId: p.file.sourcePath,
279
+ prompt: p.messages.find(m => m.role === 'user')?.content ?? '',
280
+ system: p.messages.find(m => m.role === 'system')?.content
281
+ }))
282
+ const onProgress = makeBatchProgress(reporter, targets.length - prepared.length, targets.length)
283
+ const localProviders = opts.localProviders ?? defaultLocalProviders()
284
+ const results = await submitBatchImpl(model, items, { onProgress, localProviders })
285
+ const byId = new Map(results.map(r => [r.customId, r]))
286
+
287
+ for (const p of prepared) {
288
+ processBatchResult(byId.get(p.file.sourcePath), p, { model, tier: opts.tier ?? null, stats, out })
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Prep-фаза batch-пасу: pre-send guard + факт-лист/messages для кожного файлу
294
+ * (`prepareBatchItem`). Файли, що впали на цій фазі (завеликий src тощо), одразу
295
+ * класифікуються й записуються у `stats` — до batch-у вони не потрапляють.
296
+ * @param {Array<object>} targets елементи scanForDocFiles
297
+ * @param {string} root абсолютний корінь
298
+ * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
299
+ * @param {{ reporter?: object, out: (s: string) => void }} io прогрес-репортер і логер
300
+ * @returns {Promise<Array<object>>} елементи, готові до batch-у (file/sourceAbs/docAbs/size/facts/anchors/src/messages/intent)
301
+ */
302
+ async function prepareBatchTargets(targets, root, stats, { reporter, out }) {
303
+ const prepared = []
304
+ for (const file of targets) {
305
+ const sourceAbs = join(root, file.sourcePath)
306
+ const docAbs = join(root, file.docPath)
307
+ let size = 0
308
+ try {
309
+ size = statSync(sourceAbs).size
310
+ } catch {
311
+ // файл зник між скануванням і генерацією — лишаємо розмір 0
312
+ }
313
+ const existingMd = existsSync(docAbs) ? readFileSync(docAbs, 'utf8') : null
314
+ try {
315
+ const prep = await prepareBatchItem(sourceAbs, { existingMd })
316
+ prepared.push({ file, sourceAbs, docAbs, size, ...prep })
317
+ } catch (error) {
318
+ recordBatchOutcome(stats, out, file.sourcePath, size, error.message)
319
+ }
320
+ reporter?.concernStart(file.sourcePath)
321
+ reporter?.concernDone(file.sourcePath)
322
+ }
323
+ return prepared
324
+ }
325
+
326
+ /**
327
+ * Класифікує помилку одного item-у (`classifyDocgenError`, спільний з послідовним
328
+ * шляхом) і заносить її у `stats` — permanent → skip, інакше → err. Друкує той
329
+ * самий формат рядка, що й `generateOne`.
330
+ * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
331
+ * @param {(s: string) => void} out логер рядка результату
332
+ * @param {string} sourcePath шлях джерела (для рядка й акумулятора)
333
+ * @param {number} size розмір джерела в байтах (для рядка)
334
+ * @param {string} message повідомлення помилки
335
+ * @returns {void}
336
+ */
337
+ function recordBatchOutcome(stats, out, sourcePath, size, message) {
338
+ const cls = classifyDocgenError(message)
339
+ const prefix = ` ${sourcePath} [${fmtSize(size)}] `
340
+ if (cls === 'permanent') {
341
+ stats.skipped.push(sourcePath)
342
+ out(`${prefix}⊘ skip (permanent): ${message}\n`)
343
+ } else {
344
+ stats.err++
345
+ stats.errors.push(sourcePath)
346
+ out(`${prefix}✗ ${cls}: ${message}\n`)
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Progress-колбек для `submitBatchImpl`: `completed` — монотонний лічильник
352
+ * завершених item-ів (порядок завершення НЕ збігається з input-order при
353
+ * конкурентному виконанні в Rust-крейті, тож імʼя файлу тут недоступне —
354
+ * синтетичний ключ). `doneBase` — скільки файлів уже врахував prep-фазний
355
+ * `concernDone` (щоб бар дійшов рівно до `total`, а не `total + prep-skip`).
356
+ * @param {object|undefined} reporter ProgressReporter або undefined (не-TTY)
357
+ * @param {number} doneBase скільки файлів уже відзвітовано на prep-фазі
358
+ * @param {number} total загальна кількість targets
359
+ * @returns {(completed: number) => void} колбек для `submitBatchImpl`
360
+ */
361
+ function makeBatchProgress(reporter, doneBase, total) {
362
+ let lastCompleted = 0
363
+ return completed => {
364
+ reporter?.concernStart(`2b-batch ${doneBase + completed}/${total}`)
365
+ while (lastCompleted < completed) {
366
+ lastCompleted++
367
+ reporter?.concernDone(`2b-item-${lastCompleted}`)
368
+ }
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Постобробка одного item-результату batch-у: помилка → `recordBatchOutcome`;
374
+ * успіх → `finishBatchItem` (det-скоринг) → штамп → запис доки → `stats.ok`/
375
+ * `stats.degraded`.
376
+ * @param {{ customId: string, ok?: string, error?: string }|undefined} r результат item-у (undefined — customId не знайдено серед results)
377
+ * @param {object} p підготовлений item (з `prepareBatchTargets`)
378
+ * @param {{ model: string, tier: string|null, stats: object, out: (s: string) => void }} ctx модель/тир/акумулятор/логер
379
+ * @returns {void}
380
+ */
381
+ function processBatchResult(r, p, { model, tier, stats, out }) {
382
+ const prefix = ` ${p.file.sourcePath} [${fmtSize(p.size)}] `
383
+ if (!r || r.error) {
384
+ recordBatchOutcome(
385
+ stats,
386
+ out,
387
+ p.file.sourcePath,
388
+ p.size,
389
+ r?.error ?? 'batch: результат відсутній для цього customId'
390
+ )
391
+ return
392
+ }
393
+ const finished = finishBatchItem(r.ok, { facts: p.facts, anchors: p.anchors, src: p.src, intent: p.intent, model })
394
+ const crc = crc32(readFileSync(p.sourceAbs))
395
+ mkdirSync(dirname(p.docAbs), { recursive: true })
396
+ const quality =
397
+ finished.score === null
398
+ ? null
399
+ : { score: finished.score, issues: finished.degraded ? finished.issues : [], judge: null }
400
+ writeFileSync(p.docAbs, stampDoc(finished.md, p.file.sourcePath, crc, quality, finished.model, tier))
401
+ stats.ok++
402
+ if (finished.degraded) {
403
+ stats.degraded++
404
+ out(`${prefix}⚠ degraded score=${finished.score} crc=${crc} (2b-batch)\n`)
405
+ } else {
406
+ out(`${prefix}✓ score=${finished.score ?? '—'} crc=${crc} (2b-batch)\n`)
407
+ }
408
+ }
409
+
195
410
  /** Regex для витягу OKF-полів із frontmatter існуючої доки (швидкий, без YAML-парсера). */
196
411
  const OKF_TITLE_RE = /^title: (.+)$/mu
197
412
  const OKF_TYPE_RE = /^type: (.+)$/mu
@@ -387,6 +602,53 @@ export function runDocFilesGenCli(argv) {
387
602
  })
388
603
  }
389
604
 
605
+ /**
606
+ * Послідовний фолбек-шлях (T8): циклом по одному файлу через `generateOne`, з
607
+ * circuit-breaker'ом (K systemic-збоїв підряд → abort) і м'яким дедлайном
608
+ * fix-pipeline. Той самий шлях, що й до T8 — вихід не змінився, лише
609
+ * винесений в окрему функцію, щоб `runGenerationBatch` вибирав між ним і
610
+ * `runBatchPass`.
611
+ * @param {Array<object>} targets елементи scanForDocFiles
612
+ * @param {string} root абсолютний корінь
613
+ * @param {{ model?: string, tier?: string, deadlineAt?: number|null }} opts модель/тир/дедлайн
614
+ * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
615
+ * @param {{ reporter?: object, emit?: (s: string) => void }} io прогрес-репортер і логер
616
+ * @returns {Promise<{ done: number, aborted: boolean, deadlineHit: boolean }>} підсумок проходу
617
+ */
618
+ async function runSequentialPass(targets, root, opts, stats, { reporter, emit }) {
619
+ let done = 0
620
+ let systemicStreak = 0
621
+ let aborted = false
622
+ let deadlineHit = false
623
+ for (const file of targets) {
624
+ // М'який дедлайн (fix-pipeline): не стартуємо наступний файл після дедлайну.
625
+ if (deadlineReached(opts.deadlineAt, done)) {
626
+ deadlineHit = true
627
+ break
628
+ }
629
+ done++
630
+ reporter?.concernStart(file.sourcePath)
631
+ const status = await generateOne(file, root, { done, total: targets.length }, stats, {
632
+ model: opts.model,
633
+ tier: opts.tier,
634
+ emit,
635
+ deadlineAt: opts.deadlineAt ?? null
636
+ })
637
+ reporter?.concernDone(file.sourcePath)
638
+ // Circuit-breaker: K systemic-збоїв підряд → негайний abort (середовище впало,
639
+ // решта файлів так само згорить). Будь-який не-systemic результат скидає лічильник.
640
+ if (status === 'systemic') {
641
+ if (++systemicStreak >= SYSTEMIC_ABORT_STREAK) {
642
+ aborted = true
643
+ break
644
+ }
645
+ } else {
646
+ systemicStreak = 0
647
+ }
648
+ }
649
+ return { done, aborted, deadlineHit }
650
+ }
651
+
390
652
  /**
391
653
  * Спільне ядро генерації: preflight локального бекенда → послідовний прогін
392
654
  * `targets` через `generateOne` з circuit-breaker'ом (K systemic-збоїв підряд →
@@ -400,9 +662,15 @@ export function runDocFilesGenCli(argv) {
400
662
  * У ПРОЦЕСІ обривається на дедлайні (transient-помилка), а не живе батчем-зомбі
401
663
  * поверх наступного rung-а. Зроблене записано по одному файлу (durable, свіжий
402
664
  * CRC) — наступний прогін підбирає решту за CRC.
665
+ * T8 (2b-batch, рішення Р): коли доступний native-аддон `@7n/llm-lib` (`nativeBatchAvailable`)
666
+ * і рунг БЕЗ `deadlineAt` (fix-pipeline рунги лишаються на послідовному шляху —
667
+ * там дедлайн підтримується), увесь `targets` іде ОДНИМ `submitBatch` через
668
+ * `runBatchPass` замість цього циклу по одному файлу. Zero-native споживачі
669
+ * (аддон не зібраний/платформа не підтримується) автоматично лишаються на
670
+ * послідовному шляху нижче — жодної відмінності в CLI/skill/hook-контракті.
403
671
  * @param {Array<object>} targets елементи scanForDocFiles (sourcePath/docPath)
404
672
  * @param {string} root абсолютний корінь
405
- * @param {{ headline?: string, model?: string, tier?: string, deadlineAt?: number|null }} [opts] headline — рядок-шапка прогону у stdout; model/tier — override моделі і її типу (інакше DEFAULT_LOCAL_MODEL); deadlineAt — м'який дедлайн (epoch ms)
673
+ * @param {{ headline?: string, model?: string, tier?: string, deadlineAt?: number|null, submitBatchImpl?: (modelSpecOrTier: string, items: Array<object>, opts?: object) => Promise<Array<object>>, forceSequential?: boolean }} [opts] headline — рядок-шапка прогону у stdout; model/tier — override моделі і її типу (інакше DEFAULT_LOCAL_MODEL); deadlineAt — м'який дедлайн (epoch ms); submitBatchImpl — інжект `submitBatch` (тест); forceSequential — примусовий фолбек (тест/діагностика)
406
674
  * @returns {Promise<number>} 0 — без помилок; 1 — фейл preflight або є помилки; 2 — systemic-abort
407
675
  */
408
676
  export async function runGenerationBatch(targets, root, opts = {}) {
@@ -429,36 +697,19 @@ export async function runGenerationBatch(targets, root, opts = {}) {
429
697
  : null
430
698
  const emit = reporter ? reporter.log : undefined
431
699
 
432
- let done = 0
433
- let systemicStreak = 0
700
+ const submitBatchImpl = opts.submitBatchImpl ?? submitBatchNative
701
+ const useBatch =
702
+ !opts.forceSequential && !opts.deadlineAt && (await nativeBatchAvailable(submitBatchImpl, !opts.submitBatchImpl))
703
+
704
+ let done
434
705
  let aborted = false
435
706
  let deadlineHit = false
436
707
  try {
437
- for (const file of targets) {
438
- // М'який дедлайн (fix-pipeline): не стартуємо наступний файл після дедлайну.
439
- if (deadlineReached(opts.deadlineAt, done)) {
440
- deadlineHit = true
441
- break
442
- }
443
- done++
444
- reporter?.concernStart(file.sourcePath)
445
- const status = await generateOne(file, root, { done, total: targets.length }, stats, {
446
- model: opts.model,
447
- tier: opts.tier,
448
- emit,
449
- deadlineAt: opts.deadlineAt ?? null
450
- })
451
- reporter?.concernDone(file.sourcePath)
452
- // Circuit-breaker: K systemic-збоїв підряд → негайний abort (середовище впало,
453
- // решта файлів так само згорить). Будь-який не-systemic результат скидає лічильник.
454
- if (status === 'systemic') {
455
- if (++systemicStreak >= SYSTEMIC_ABORT_STREAK) {
456
- aborted = true
457
- break
458
- }
459
- } else {
460
- systemicStreak = 0
461
- }
708
+ if (useBatch) {
709
+ await runBatchPass(targets, root, { ...opts, submitBatchImpl }, stats, { reporter, emit })
710
+ done = targets.length
711
+ } else {
712
+ ;({ done, aborted, deadlineHit } = await runSequentialPass(targets, root, opts, stats, { reporter, emit }))
462
713
  }
463
714
  } finally {
464
715
  reporter?.stop()
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/doc-files/docgen-gen
4
+ resource: npm/rules/doc-files/docgen-gen/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------- | --------- |
9
+ | [main.mjs](main.md) | JS Module |
@@ -3,39 +3,50 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-gen/main.mjs
5
5
  docgen:
6
- crc: c02d3a1f
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
+ crc: 80c789c7
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 10
10
+ issues: no-overview,short-behavior,internal-name:isApiGap,internal-name:renderApiLine,internal-name:oneShotDoc,internal-name:finishUnsupported,anchor-miss:(abie.mdc),best-of-2:retry-lost
8
11
  ---
9
12
 
10
- ## Огляд
11
-
12
- Огляд
13
- Створює технічну документацію шляхом аналізу вихідного коду, генерації тексту, оцінки його якості та управління позиціонуванням захищеного блоку «Призначення» у фінальному документі. (abie.mdc)
14
-
15
- Поведінка
16
- splitProtected відокремлює захищений блок «Призначення» від основного вмісту документа, надаючи його тіло та текст без цього блоку.
17
- insertProtected поміщає захищений блок «Призначення» одразу після основного заголовка документа, якщо надано його тіло.
18
- scoreDoc оцінює згенерований документ за заданими критеріями, визначаючи його якість та реєструючи проблеми.
19
- DEFAULT_LOCAL_MODEL встановлює модель, яку використовувати для генерації документа, якщо не вказано іншу.
20
- generateDoc створює повний технічний документ з вихідного файлу, включаючи вихідну оцінку якості.
21
-
22
- ## Поведінка
23
-
24
- splitProtected відокремлює захищений блок «Призначення» від основного вмісту документа, повертаючи його тіло та текст без цього блоку.
25
- insertProtected розміщує захищений блок «Призначення» одразу після основного заголовка документа, якщо його тіло було надано.
26
- scoreDoc оцінює згенерований документ за набором детермінованих правил (generic-огляд, коротка поведінка, галюцинації про кеш, витік службових імен, анкор-покриття, суржик) і реєструє коди проблем. Правило R8 — refusal/чат-філер («Я готовий писати…», «Надайте мені код…», через пре-гейт docgen-judge): форсує degraded незалежно від структурної оцінки, тож LLM-суддя на такому змісті не викликається; захищене людське «Призначення» з перевірки виключене.
27
- DEFAULT_LOCAL_MODEL визначає модель за замовчуванням для генерації документа, якщо не вказано інше.
28
- generateDoc створює повний технічний документ з вихідного файлу, включаючи детерміновану оцінку якості; для непідтримуваних структур (one-shot шлях) оцінка не застосовується, але refusal-пре-гейт діє і там — філер отримує score 0 і позначку degraded, щоб доретрай батчу підібрав файл.
29
- generateDoc приймає опційний `deadlineAt` (м'який дедлайн fix-pipeline, epoch ms): кожен LLM-виклик ріже свій таймаут під залишок бюджету (`capTimeoutToDeadline`), а вичерпаний бюджет обриває генерацію помилкою зі словом «timeout» ще до старту виклику — батч класифікує її як transient, і генерація ніколи не переживає бюджет рунга (без батчу-зомбі поверх наступного rung-а).
30
-
31
13
  ## Публічний API
32
14
 
33
- splitProtectedвитягує тіло захищеної секції «Призначення» та повертає документ без неї.
34
- insertProtectedповертає захищений блок у фіксовану позицію одразу після заголовка документа.
35
- scoreDocдетермінований скоринг документа (0 токенів) проти факт-листа: оцінка 0–100 і коди проблем.
36
- DEFAULT_LOCAL_MODEL модель генерації за замовчуванням (env-конфігурована; без неї preflight фейлить гучно).
37
- generateDoc головний вхід: джерело md-дока з оцінкою якості, best-of-2 повтором і опційним семантичним суддею; опційний `deadlineAt` обмежує генерацію бюджетом рунга fix-pipeline.
38
- capTimeoutToDeadlineріже базовий per-call таймаут під залишок до дедлайну (без дедлайну базовий ліміт; після дедлайну — 0).
15
+ - capTimeoutToDeadline Ріже базовий per-call таймаут під залишок бюджету до дедлайну.
16
+ Без дедлайну базовий ліміт; після дедлайну 0 (виклик не має стартувати).
17
+ - stripLeadingPreamble R9: зрізає провідні чат-преамбули й дубль назви секції з початку тексту.
18
+ Ітерується, поки перший непорожній рядок лишається мета-нарацією модель
19
+ інколи ставить дві поспіль («Як технічний письменник…» + «Ось оновлений…»).
20
+ - splitProtected Відокремлює захищену секцію `## Призначення` (Варіант B). Межанаступний `## `
21
+ (H2); `###`+ усередині не обривають блок.
22
+ - insertProtected — Вставляє захищений блок `## Призначення` одразу після H1 (фіксована позиція).
23
+ - scoreDoc — Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
24
+ - buildApiSection — Stage 1/3 (гібрид doc-files, ADR 260719-2155): «Публічний API» — покриті
25
+ JSDoc-описом експорти рендеряться дослівно (`renderApiLine`, 0 токенів, 0
26
+ галюцинацій), LLM викликається лише на прогалини (`isApiGap`). Якщо прогалин
27
+ немає — секція збирається БЕЗ жодного LLM-виклику. Єдиний непокритий
28
+ експорт (як і раніше) лишається описаним лише в Поведінці — окремого виклику
29
+ на секцію з одного рядка не варте.
30
+ - DEFAULT_LOCAL_MODEL — Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') (→ N_LOCAL_MIN_MODEL).
31
+ Без хардкод-fallback: модель налаштовує кожен локально (`N_LOCAL_MIN_MODEL`); якщо
32
+ нічого не задано — порожньо, і preflight оркестратора фейлить гучно (а не шле
33
+ запит до неіснуючої моделі).
34
+ - generateDoc — Головний API: файл → md-дока з det-оцінкою.
35
+
36
+ Local-only (ADR 260610-2228): жодних cloud-ескалацій і pre-route — будь-який
37
+ файл генерується локальною моделлю. Якщо det-score нижче порогу, один retry
38
+ з вищою температурою (best-of-2); якщо й він не допоміг — результат
39
+ позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
40
+ - prepareBatchItem — T8 (2b-batch, рішення Р): підготовка ОДНОГО item-у для `submitBatch` — та сама
41
+ pre-send guard і той самий факт-лист/one-shot messages, що й `oneShotDoc`/
42
+ `generateDoc`, але БЕЗ виклику LLM (виклик робить batch-шар одним `submit` на
43
+ всі файли разом). Кидає ту саму помилку pre-send guard, що й `generateDoc`
44
+ (класифікується `permanent` у batch-оркестраторі — skip, не помилка прогону).
45
+ - finishBatchItem — T8 (2b-batch): постобробка ОДНОГО результату `submitBatch` — той самий фініш,
46
+ що й `oneShotDoc`/`finishUnsupported`/det-скорер, тільки без LLM-виклику
47
+ (текст уже отримано з batch-у). Judge-гейт (Stage 3) у batch-шляху НЕ
48
+ викликається (мінімальний обсяг T8 — генерація; judge лишається опційним
49
+ розширенням послідовного шляху).
39
50
 
40
51
  ## Гарантії поведінки
41
52