@7n/rules 1.48.2 → 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.
- package/CHANGELOG.md +19 -0
- package/bin/n-rules-cli.mjs +5 -8
- package/package.json +1 -1
- package/rules/changelog/.changes/260724-1500.md +5 -0
- package/rules/doc-files/docgen-files-batch/docs/index.md +9 -0
- package/rules/doc-files/docgen-files-batch/docs/main.md +59 -18
- package/rules/doc-files/docgen-files-batch/main.mjs +280 -29
- package/rules/doc-files/docgen-gen/docs/index.md +9 -0
- package/rules/doc-files/docgen-gen/docs/main.md +40 -29
- package/rules/doc-files/docgen-gen/main.mjs +79 -25
- package/rules/test/coverage/fix-worker.mjs +9 -1
- package/rules/test/coverage/lib/classify/verdict-schema.mjs +3 -1
- package/scripts/docs/skills-cli.md +18 -24
- package/scripts/lib/acp-runner.mjs +1 -1
- package/scripts/lib/lint-surface/collateral-veto.mjs +78 -2
- package/scripts/lib/lint-surface/docs/collateral-veto.md +30 -16
- package/scripts/lib/lint-surface/docs/index.md +1 -0
- package/scripts/lib/lint-surface/docs/run-fix.md +8 -23
- package/scripts/lib/lint-surface/docs/snapshot.md +6 -17
- package/scripts/lib/lint-surface/docs/test-gate.md +29 -0
- package/scripts/lib/lint-surface/run-fix.mjs +209 -38
- package/scripts/lib/lint-surface/snapshot.mjs +6 -0
- package/scripts/lib/lint-surface/test-gate.mjs +87 -0
- package/scripts/skills-cli.mjs +60 -10
- package/scripts/utils/docs/glob-compat.md +20 -14
- package/scripts/utils/glob-compat.mjs +18 -3
- package/skills/git-reconcile/SKILL.md +58 -0
- package/skills/git-reconcile/js/docs/index.md +9 -0
- package/skills/git-reconcile/js/docs/orchestrate.md +33 -0
- package/skills/git-reconcile/js/orchestrate.mjs +776 -0
- package/skills/git-reconcile/main.json +1 -0
- package/skills/taze/js/docs/orchestrate.md +40 -18
- package/skills/taze/js/orchestrate.mjs +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.49.0] - 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
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- 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" }
|
|
12
|
+
- n-taze: callRunner для cursor/codex передає tier 'avg' у runAcpAgent — модель тіру замість персонального CLI-конфіга (паритет з pi-гілкою)
|
|
13
|
+
- changelog presence: додано change-файл для змін у npm
|
|
14
|
+
- doc-files: генерація N файлів одним 2b-batch (submitBatch @7n/llm-lib) з per-item ізоляцією помилок; фолбек на послідовний шлях без native-аддона чи з deadlineAt
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- changelog consistency: додано change-файл для змін у npm/rules/changelog
|
|
19
|
+
- scanGlob: захист від Bun.Glob#scan(), що повертає Promise (self-hosted Linux Bun 1.3.14) — детектори не валять весь lint-прогін
|
|
20
|
+
- Виправлено profile генерації тестів для survived Stryker-мутантів
|
|
21
|
+
|
|
3
22
|
## [1.48.2] - 2026-07-24
|
|
4
23
|
|
|
5
24
|
### Fixed
|
package/bin/n-rules-cli.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
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' &&
|
|
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
|
@@ -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:
|
|
7
|
-
model:
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
+
Файл керує життєвим циклом поведінкової документації для source-файлів: `selectTargets` визначає цілі, `runGenerationBatch` оновлює docs-файли, `purgeOrphanedDocs` прибирає сирітські документи, а `generateDirIndex` підтримує індекси директорій. `nativeBatchAvailable`, `runDocFilesGenCli` і `runDocFilesStampCli` забезпечують безпечний контур CLI/batch-прогонів із кешуванням у межах одного запуску.
|
|
17
|
+
|
|
18
|
+
Модуль звертається до мережі, але працює fail-safe: перехоплює помилки, не кидає винятків назовні і дає прогону завершитися контрольовано.
|
|
16
19
|
|
|
17
20
|
## Поведінка
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
runGenerationBatch
|
|
24
|
-
|
|
25
|
-
|
|
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 —
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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()
|