@7n/rules 1.48.2 → 1.49.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/n-rules-cli.mjs +5 -8
  3. package/package.json +1 -1
  4. package/rules/changelog/.changes/260724-1500.md +5 -0
  5. package/rules/doc-files/docgen-files-batch/docs/index.md +9 -0
  6. package/rules/doc-files/docgen-files-batch/docs/main.md +59 -18
  7. package/rules/doc-files/docgen-files-batch/main.mjs +280 -29
  8. package/rules/doc-files/docgen-gen/docs/index.md +9 -0
  9. package/rules/doc-files/docgen-gen/docs/main.md +40 -29
  10. package/rules/doc-files/docgen-gen/main.mjs +79 -25
  11. package/rules/test/coverage/fix-worker.mjs +9 -1
  12. package/rules/test/coverage/lib/classify/verdict-schema.mjs +3 -1
  13. package/scripts/docs/skills-cli.md +18 -24
  14. package/scripts/lib/acp-runner.mjs +1 -1
  15. package/scripts/lib/lint-surface/collateral-veto.mjs +78 -2
  16. package/scripts/lib/lint-surface/docs/collateral-veto.md +30 -16
  17. package/scripts/lib/lint-surface/docs/index.md +1 -0
  18. package/scripts/lib/lint-surface/docs/run-fix.md +8 -23
  19. package/scripts/lib/lint-surface/docs/snapshot.md +6 -17
  20. package/scripts/lib/lint-surface/docs/test-gate.md +29 -0
  21. package/scripts/lib/lint-surface/run-fix.mjs +209 -38
  22. package/scripts/lib/lint-surface/snapshot.mjs +6 -0
  23. package/scripts/lib/lint-surface/test-gate.mjs +87 -0
  24. package/scripts/skills-cli.mjs +60 -10
  25. package/scripts/utils/docs/glob-compat.md +20 -14
  26. package/scripts/utils/glob-compat.mjs +18 -3
  27. package/skills/git-reconcile/SKILL.md +58 -0
  28. package/skills/git-reconcile/js/docs/index.md +9 -0
  29. package/skills/git-reconcile/js/docs/orchestrate.md +33 -0
  30. package/skills/git-reconcile/js/orchestrate.mjs +776 -0
  31. package/skills/git-reconcile/main.json +1 -0
  32. package/skills/taze/js/docs/orchestrate.md +40 -18
  33. package/skills/taze/js/orchestrate.mjs +6 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.49.1] - 2026-07-25
4
+
5
+ ### Added
6
+
7
+ - Додано JS-оркестрований skill `n-git-reconcile` для аналізу гілок, worktree та stash і перенесення корисних змін у готові PR. CI smoke-check використовує завантажений Linux native addon до release, тому перевіряє native-залежні CLI-модулі в тому самому runtime, що й publish.
8
+
9
+ ### Fixed
10
+
11
+ - Виправлено profile генерації тестів для survived Stryker-мутантів
12
+
13
+ ## [1.49.0] - 2026-07-25
14
+
15
+ ### Added
16
+
17
+ - Додано JS-оркестрований skill `n-git-reconcile` для аналізу гілок, worktree та stash і перенесення корисних змін у готові PR. CI smoke-check використовує завантажений Linux native addon до release, тому перевіряє native-залежні CLI-модулі в тому самому runtime, що й publish.
18
+
19
+ ### Changed
20
+
21
+ - Rust-крейти перейменовано: llm-cascade → llm-lib, llm-cascade-napi → llm-lib-napi, CascadeError → LlmError; napi-артефакти llm-lib-napi.`triple`.node; git-споживачам — dependency-alias llm-cascade = { package = "llm-lib" }
22
+ - n-taze: callRunner для cursor/codex передає tier 'avg' у runAcpAgent — модель тіру замість персонального CLI-конфіга (паритет з pi-гілкою)
23
+ - changelog presence: додано change-файл для змін у npm
24
+ - doc-files: генерація N файлів одним 2b-batch (submitBatch @7n/llm-lib) з per-item ізоляцією помилок; фолбек на послідовний шлях без native-аддона чи з deadlineAt
25
+
26
+ ### Fixed
27
+
28
+ - changelog consistency: додано change-файл для змін у npm/rules/changelog
29
+ - scanGlob: захист від Bun.Glob#scan(), що повертає Promise (self-hosted Linux Bun 1.3.14) — детектори не валять весь lint-прогін
30
+ - Виправлено profile генерації тестів для survived Stryker-мутантів
31
+
3
32
  ## [1.48.2] - 2026-07-24
4
33
 
5
34
  ### Fixed
@@ -99,7 +99,7 @@ import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
99
99
  import { syncGitignoreWorktree } from '../scripts/lib/sync-gitignore-worktree.mjs'
100
100
  import { upgradeNRulesToLatestAndBunInstall } from '../scripts/upgrade-n-rules-and-install.mjs'
101
101
  import { runRenameYamlExtensionsCli } from './rename-yaml-extensions.mjs'
102
- import { isTazeOrchestratorSkillArgs, runSkillsCli } from '../scripts/skills-cli.mjs'
102
+ import { isJsOrchestratedSkillArgs, runSkillsCli } from '../scripts/skills-cli.mjs'
103
103
  import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
104
104
 
105
105
  /**
@@ -1778,19 +1778,16 @@ export async function runCli(argv) {
1778
1778
  }
1779
1779
  // `ci` (plan) — read-only гейт-команда для CI-джоб: не мутує package.json
1780
1780
  // (ensure дописав би devDependency прямо в чекауті pipeline-агента).
1781
- // `skill <runner> taze` — JS-оркестрований worktree-only шлях
1782
- // (skills/taze/js/orchestrate.mjs): сам створює worktree і гейтить на
1783
- // чистоту дерева ДО checkout. Мутація тут забруднила б дерево прямо
1784
- // перед тим гейтом і провалювала б auto-create на інакше чистому дереві —
1785
- // оркестратор сам робить self-upgrade devDependency вже ВСЕРЕДИНІ
1786
- // щойно створеного worktree, після власного гейту чистоти.
1781
+ // `skill <runner> taze|git-reconcile` — JS-оркестровані шляхи: самі
1782
+ // виконують Git/worktree preflight до мутацій. Self-upgrade тут
1783
+ // забруднив би дерево до їхнього власного гейту.
1787
1784
  // `lint --full` (без --no-fix/--path/--repo-wide) — той самий клас ризику:
1788
1785
  // `needsWorktreeIsolation` нижче в `case 'lint'` гейтить на чистоту дерева
1789
1786
  // ЧЕРЕЗ `ensureRunningInWorktree`; self-upgrade тут забруднив би те саме
1790
1787
  // дерево прямо перед тим гейтом. Відкладаємо ensure до ПІСЛЯ
1791
1788
  // `ensureRunningInWorktree` (виклик у `case 'lint'`, спрямований на runCwd).
1792
1789
  const skipDevDepsEnsure =
1793
- (command === 'skill' && isTazeOrchestratorSkillArgs(args)) || (command === 'lint' && isLintFullFixArgs(args))
1790
+ (command === 'skill' && isJsOrchestratedSkillArgs(args)) || (command === 'lint' && isLintFullFixArgs(args))
1794
1791
  if (command !== 'ci' && !skipDevDepsEnsure) await ensureNRulesInRootDevDependencies(effectiveRoot)
1795
1792
  // Підкоманди-оркестратори (hook/lint/skill/adr-normalize-local/taze/release тощо)
1796
1793
  // можуть спавнити внутрішню agent/LLM-сесію — ADR Stop-hooks (capture/normalize)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.48.2",
3
+ "version": "1.49.1",
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 |