@7n/rules 1.49.26 → 1.50.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.50.0] - 2026-07-27
4
+
5
+ ### Added
6
+
7
+ - doc-files без LLM рендерить test-сценарії та повністю прокоментовані JS, Rust і Python файли, а для коротких comments додає лише відсутню «Поведінку»
8
+
9
+ ### Fixed
10
+
11
+ - Прискорено k8s full lint і додано live статус черги
12
+
3
13
  ## [1.49.26] - 2026-07-27
4
14
 
5
15
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.49.26",
3
+ "version": "1.50.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/doc-files/check
4
+ resource: npm/rules/doc-files/check/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------------------- | --------- |
9
+ | [fix-worker.mjs](fix-worker.md) | JS Module |
10
+ | [main.mjs](main.md) | JS Module |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/check/main.mjs
5
5
  docgen:
6
- crc: b5921123
6
+ crc: d13133e6
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -16,6 +16,8 @@ Lint-детектор doc-files: знаходить застарілі файл
16
16
 
17
17
  Реверс-мапінг: якщо серед змінених файлів трапляється сама `.md`-дока (а не її джерело), `sourceForDoc` шукає відповідний вихідний файл поруч (той самий basename, легальне розширення) і саме його передає далі в перевірку застарілості — так зміна доки теж тригерить звірку CRC її джерела.
18
18
 
19
+ Якщо змінено test/spec-файл, `sourceFilesForTest` через спільний source↔tests index знаходить лише пов’язані source-файли. Їхня документація також перевіряється на застарілість, тому зміна підтвердженого usage-сценарію не лишає docs зі старим CRC.
20
+
19
21
  Якщо мапа doc-files-розширень від активних плагінів порожня, а хоча б один плагін явно задекларований у `.n-rules.json`, але не встановлений у `node_modules` — детектор додає `diagnostics`-запис (`level: 'warn'`) з підказкою запустити `bun install`. Це відрізняє "плагін не встановлено" від "усі доки актуальні": без нього обидва випадки виглядають як 0 порушень.
20
22
 
21
23
  ## Публічний API
@@ -25,5 +27,5 @@ Lint-детектор doc-files: знаходить застарілі файл
25
27
  ## Гарантії поведінки
26
28
 
27
29
  - Read-only: не пише і не видаляє жодних файлів.
28
- - Перехоплює помилки читання файлової системи (напр. нечитабельна тека) не пропускає винятків назовні.
30
+ - Містить локальні fail-safe гілки для недоступних тек; інші помилки можуть поширюватися назовні.
29
31
  - Diagnostics-попередження про невстановлений плагін рендериться лише при `--verbose` на explicit CLI-виклику (`lint doc-files --no-fix --verbose`) — у PostToolUse-хуку (без `verbose`) обчислюється, але ніколи не друкується.
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * lint-поверхня doc-files: детект застарілих файлових документацій (per-file, з reverse-mapом).
3
3
  */
4
- import { join, dirname, basename, extname } from 'node:path'
4
+ import { join, dirname, basename, extname, relative } from 'node:path'
5
5
  import { existsSync, readdirSync } from 'node:fs'
6
6
 
7
7
  import { describeFile, isDocCandidate, isSourceFile, scanForDocFiles, scanOrphanedDocs } from '../docgen-scan/main.mjs'
8
8
  import { unavailableDocFilesPlugins } from '../docgen-scan/lang-extensions.mjs'
9
+ import { buildTestEvidenceIndex, isDocgenTestFile, sourceFilesForTest } from '../docgen-test-context/main.mjs'
9
10
 
10
11
  const DOC_MD_RE = /(?:^|\/)docs\/[^/]+\.md$/u
11
12
 
@@ -38,15 +39,21 @@ function sourceForDoc(cwd, docRel) {
38
39
  * Зводить перелік змінених файлів до множини вихідних кодових файлів.
39
40
  * @param {string[]} files змінені шляхи (джерела або .md-доки)
40
41
  * @param {string} cwd робочий каталог
42
+ * @param {ReturnType<typeof buildTestEvidenceIndex>} testIndex source↔tests index
41
43
  * @returns {string[]} відносні шляхи джерел
42
44
  */
43
- function sourcesFromChanged(files, cwd) {
45
+ function sourcesFromChanged(files, cwd, testIndex) {
44
46
  const out = new Set()
45
47
  for (const raw of files) {
46
48
  const rel = raw.split('\\').join('/')
47
49
  if (DOC_MD_RE.test(rel)) {
48
50
  const src = sourceForDoc(cwd, rel)
49
51
  if (src) out.add(src)
52
+ } else if (isDocgenTestFile(basename(rel))) {
53
+ for (const sourceAbs of sourceFilesForTest(join(cwd, rel), testIndex)) {
54
+ const sourceRel = relative(cwd, sourceAbs).split('\\').join('/')
55
+ if (isDocCandidate(cwd, sourceRel)) out.add(sourceRel)
56
+ }
50
57
  } else if (isDocCandidate(cwd, rel) && existsSync(join(cwd, rel))) {
51
58
  out.add(rel)
52
59
  }
@@ -61,8 +68,9 @@ function sourcesFromChanged(files, cwd) {
61
68
  */
62
69
  export function collectStale(files, cwd) {
63
70
  if (files === undefined) return scanForDocFiles(cwd).filter(f => f.stale)
64
- const sources = sourcesFromChanged(files, cwd)
65
- return sources.map(src => describeFile(cwd, src)).filter(f => f.stale)
71
+ const testIndex = buildTestEvidenceIndex(cwd)
72
+ const sources = sourcesFromChanged(files, cwd, testIndex)
73
+ return sources.map(src => describeFile(cwd, src, testIndex)).filter(f => f.stale)
66
74
  }
67
75
 
68
76
  /**
@@ -0,0 +1,9 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/doc-files/docgen-crc
4
+ resource: npm/rules/doc-files/docgen-crc/
5
+ ---
6
+
7
+ | Файл | Тип |
8
+ | ------------------- | --------- |
9
+ | [main.mjs](main.md) | JS Module |
@@ -3,44 +3,65 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-crc/main.mjs
5
5
  docgen:
6
- crc: 8976f6f8
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
+ crc: fdddffc3
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
8
9
  score: 100
9
- issues: judge:inaccurate:0.99
10
+ issues: judge:error
10
11
  judgeModel: openai-codex/gpt-5.4-mini
11
12
  ---
12
13
 
13
14
  ## Огляд
14
15
 
15
- Модуль забезпечує керування якістю, метаданими та актуальністю документації. Він дозволяє встановлювати мінімальний поріг якості за допомогою `QUALITY_THRESHOLD`, обчислювати та зчитувати контрольні суми (CRC32) за допомогою `crc32` та `readDocCrc`, а також парсити та генерувати метадані документації за допомогою `parseDocFrontmatter` та `buildDocFrontmatter`.
16
+ `crc32` обчислює детермінований 8-символьний hex CRC для рядка або Buffer; `documentationCrc` і `parseDocFrontmatter` зв’язують вміст документа з його frontmatter. `buildDocFrontmatter` і `stampDoc` записують fresh metadata для `CRC`, `model`, `tier` та `quality`, причому `stampDoc` ще й керує маркером `degraded`. `readDocCrc`, `readDocModel`, `readDocTier` і `readDocQuality` відновлюють ці значення з доки, а `QUALITY_THRESHOLD` задає дефолтний поріг 70. `staleness` порівнює source і doc, щоб відрізняти `missing`, `crc-mismatch` і `fresh`.
16
17
 
17
18
  ## Поведінка
18
19
 
19
- Поведінка:
20
- QUALITY_THRESHOLD — Встановлює поріг якості документації, нижче якого доки вважаються неякісними.
21
- crc32 Обчислює та повертає CRC32 вмісту у вигляді 8-символьного шестнадцяткового рядка.
22
- parseDocFrontmatter — Розбирає вміст markdown-файлу, виділяючи метадані з блоку frontmatter та повертаючи тіло документа.
23
- buildDocFrontmatter Формує YAML-блок frontmatter, сумісний з OKF, для посилання на джерело та його метаданих.
24
- stampDoc — Генерує повний markdown-документ, вставляючи новостворений frontmatter у тіло документа.
25
- readDocCrc Зчитує та повертає CRC32, збережений у frontmatter markdown-документа.
26
- readDocQuality — Зчитує зі frontmatter markdown-документа оцінку якості та пов'язані з нею помилки.
27
- readDocModel Зчитує зі frontmatter markdown-документа ідентифікатор моделі, яка створила документ.
28
- readDocTier — Зчитує зі frontmatter markdown-документа рівень моделі-генератора.
29
- stalenessВизначає, чи застарілий markdown-документ порівняно з його вихідним джерелом за допомогою CRC32.
20
+ QUALITY_THRESHOLD задає дефолтний поріг оцінки якості для читання й маркування доки; у перевірених сценаріях цей поріг дорівнює 70.
21
+
22
+ crc32 дає детермінований 8-символьний hex для рядка або Buffer; однаковий вміст завжди дає той самий CRC, а різний — інший; відомий вектор `123456789` зводиться до `cbf43926`.
23
+
24
+ documentationCrc обчислює CRC для поведінкової документації не лише з джерела, а й з пов’язаного evidence тестів, якщо він є; без пов’язаних тестів лишається сумісним із CRC самого source, тож зміна лише сценарію використання робить доку застарілою.
25
+
26
+ parseDocFrontmatter відокремлює frontmatter від тіла доки й повертає нормалізовані метадані; якщо frontmatter немає, тіло лишається без змін, а метадані відсутні; старі доки без частини полів читаються як сумісні: відсутні `model`, `tier`, `score`, `issues` та `judgeModel` стають порожніми значеннями.
27
+
28
+ buildDocFrontmatter формує frontmatter так, щоб спочатку були OKF-поля джерела, а потім вкладений блок якості та генератора; `model` і `tier` додаються лише коли вони є, а `issues` скорочуються до YAML-безпечних кодів і мають обмеження на кількість; quality може співіснувати з model, причому score та issues зберігаються й читаються назад без втрат.
29
+
30
+ stampDoc переоформлює існуючу MD-доку: знімає старий frontmatter і додає свіжий, не змінюючи тіло; коли quality є, у frontmatter лишається degraded-сигнал разом із score/issues, а коли quality зникає цей стан теж знімається; `model` переноситься у новий frontmatter разом із рештою актуальних метаданих.
31
+
32
+ readDocCrc повертає CRC, уже записаний у frontmatter; якщо доки немає або CRC не зафіксований, результат `null`.
33
+
34
+ readDocQuality читає збережену оцінку доки; за відсутності доки або score повертає `score: null`, порожній список issues і `judgeModel: null`; коли якість записана, значення відновлюються назад без втрат.
35
+
36
+ readDocModel повертає збережену модель генератора або `null`, якщо доки немає чи поле не записане.
37
+
38
+ readDocTier повертає tier моделі генератора або `null`, якщо доки немає чи поле не записане.
39
+
40
+ staleness порівнює evidence source з CRC, записаним у відповідній доці: коли доки немає, стан `missing`; коли CRC не збігається, `crc-mismatch`; при збігу дока свіжа; для пов’язаних тестів у evidence враховується й їхній вплив на CRC доки, тому зміна сценарію використання може зробити документацію stale навіть без зміни source.
30
41
 
31
42
  ## Публічний API
32
43
 
33
- QUALITY_THRESHOLD — визначає нижню межу якості документа; оцінки нижче цієї межі вважаються неякісними.
34
- crc32 — генерує хеш вмісту документа у 8-символьтному hex-форматі, використовуючи нативний модуль `node:zlib.crc32`.
35
- parseDocFrontmatterвитягує метадані з початку документа. Якщо метаданих немає, повертає порожні дані та вхідний текст.
36
- buildDocFrontmatterстворює блок метаданих, сумісний зі стандартом OKF. Він містить основні поля OKF та вкладений блок `docgen:` з інформацією про CRC, модель та якість.
37
- stampDoc додає інформацію про створення або оновлення документа (наприклад, CRC та метки) до самого вмісту.
38
- readDocCrc зчитує хеш документа з метаданих. Повертає `null`, якщо метадані відсутні або хеш не знайдено.
39
- readDocQualityзчитує оцінку якості документа з його метаданих.
40
- readDocModel зчитує назву моделі, яка генерувала документ, із метаданих. Повертає `null`, якщо це поле не визначено.
41
- readDocTier — зчитує рівень (tier) моделі-генератора з метаданих. Повертає `null`, якщо поле відсутнє.
42
- stalenessвизначає, чи є документ актуальним порівняно з його джерелом. Позначає як `missing` (якщо документа немає), `crc-mismatch` (якщо хеш у документа не збігається з хешем джерела) або "свіжий" (інакше).
44
+ - QUALITY_THRESHOLD — Поріг degraded: дока зі `score` нижче вважається неякісною.
45
+ - crc32 — CRC32 вмісту у hex (8 символів, з провідними нулями). Делегує у нативний
46
+ `node:zlib.crc32`без ручної бітової арифметики.
47
+ - documentationCrc CRC повного evidence для файлової доки. Без повʼязаних тестів лишається
48
+ back-compatible CRC самого source; за наявності тестів додає їхні шляхи та
49
+ вміст, тому зміна usage-сценарію детерміновано робить доку stale.
50
+ - parseDocFrontmatter Парсить frontmatter файлової доки. Без блоку — `data:null` і `body` дорівнює входу.
51
+ Поля `model`/`score`/`issues` опційні (back-compat зі старими доками): без них
52
+ `model:null`, `score:null`, `issues:[]`.
53
+ - buildDocFrontmatter Будує OKF-сумісний frontmatter-блок: OKF-поля верхнього рівня + вкладений `docgen:`
54
+ з CRC/model/quality. OKF-поля виводяться першими, щоб будь-який OKF-парсер міг їх
55
+ читати незалежно від `docgen:`-простору назв.
56
+ - stampDoc — (Пере)штампує frontmatter у md-доку: знімає наявний блок і додає свіжий.
57
+ - readDocCrc — CRC, збережений у frontmatter доки; `null` — доки немає або CRC відсутній.
58
+ - readDocQuality — Якість, збережена у frontmatter доки.
59
+ - readDocModel — Модель-генератор, збережена у frontmatter доки; `null` — доки немає або поле відсутнє
60
+ (старі доки до введення `model`).
61
+ - readDocTier — Tier моделі-генератора зі frontmatter доки; `null` — доки немає або поле відсутнє.
62
+ - staleness — Стан застарілості доки відносно evidence: source + повʼязані тести.
63
+ `missing` — доки немає; `crc-mismatch` — evidence CRC ≠ CRC у доці; інакше свіжа.
43
64
 
44
65
  ## Гарантії поведінки
45
66
 
46
- - Read-only: не виконує операцій запису (ФС/БД).
67
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -4,6 +4,7 @@ import { basename, extname } from 'node:path'
4
4
  import { crc32 as zlibCrc32 } from 'node:zlib'
5
5
  import { env } from 'node:process'
6
6
  import { pluginDocFilesExtensions } from '../docgen-scan/lang-extensions.mjs'
7
+ import { testEvidenceForSource } from '../docgen-test-context/main.mjs'
7
8
 
8
9
  /** Поріг degraded: дока зі `score` нижче вважається неякісною. */
9
10
  export const QUALITY_THRESHOLD = Number(env.N_CURSOR_DOC_FILES_THRESHOLD ?? 70) || 70
@@ -19,6 +20,21 @@ export function crc32(input) {
19
20
  return zlibCrc32(buf).toString(16).padStart(8, '0')
20
21
  }
21
22
 
23
+ /**
24
+ * CRC повного evidence для файлової доки. Без повʼязаних тестів лишається
25
+ * back-compatible CRC самого source; за наявності тестів додає їхні шляхи та
26
+ * вміст, тому зміна usage-сценарію детерміновано робить доку stale.
27
+ * @param {string} sourceAbsPath абсолютний шлях source-файлу
28
+ * @param {ReturnType<import('../docgen-test-context/main.mjs').buildTestEvidenceIndex>|null} [testIndex] source↔tests index
29
+ * @returns {string} CRC32 source + повʼязаних тестів
30
+ */
31
+ export function documentationCrc(sourceAbsPath, testIndex = null) {
32
+ const source = readFileSync(sourceAbsPath)
33
+ if (!testIndex) return crc32(source)
34
+ const { crcPayload } = testEvidenceForSource(sourceAbsPath, testIndex)
35
+ return crc32(crcPayload ? Buffer.concat([source, Buffer.from(crcPayload, 'utf8')]) : source)
36
+ }
37
+
22
38
  /** Провідний YAML-frontmatter-блок `---\n…\n---`. */
23
39
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/u
24
40
  const RESOURCE_RE = /^resource:[ \t]+(\S.*)$/mu
@@ -186,16 +202,17 @@ export function readDocTier(docAbsPath) {
186
202
  }
187
203
 
188
204
  /**
189
- * Стан застарілості доки відносно її джерела.
190
- * `missing` — доки немає; `crc-mismatch` — CRC джерела ≠ CRC у доці; інакше свіжа.
205
+ * Стан застарілості доки відносно evidence: source + повʼязані тести.
206
+ * `missing` — доки немає; `crc-mismatch` — evidence CRC ≠ CRC у доці; інакше свіжа.
191
207
  * @param {string} sourceAbsPath абсолютний шлях джерела
192
208
  * @param {string} docAbsPath абсолютний шлях md-доки
209
+ * @param {ReturnType<import('../docgen-test-context/main.mjs').buildTestEvidenceIndex>|null} [testIndex] source↔tests index
193
210
  * @returns {{ stale: boolean, reason: 'missing'|'crc-mismatch'|null }} стан застарілості
194
211
  */
195
- export function staleness(sourceAbsPath, docAbsPath) {
212
+ export function staleness(sourceAbsPath, docAbsPath, testIndex = null) {
196
213
  const docCrc = readDocCrc(docAbsPath)
197
214
  if (docCrc === null) return { stale: true, reason: 'missing' }
198
- const srcCrc = crc32(readFileSync(sourceAbsPath))
215
+ const srcCrc = documentationCrc(sourceAbsPath, testIndex)
199
216
  if (srcCrc !== docCrc) return { stale: true, reason: 'crc-mismatch' }
200
217
  return { stale: false, reason: null }
201
218
  }
@@ -3,33 +3,33 @@ 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: 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
6
+ crc: 309bc059
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 75
10
+ issues: internal-name:generateOne,internal-name:runBatchPass,anchor-miss:http://127.0.0.1:8000/v1/,judge-refine:kept-original,judge:inaccurate:0.99
11
11
  judgeModel: openai-codex/gpt-5.4-mini
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
+ Визначає цілі для оновлення, генерує й синхронізує docs/ з кодом, оновлює directory index і прибирає orphaned docs, які більше не прив’язані до актуальних джерел. Працює як fail-safe: мережеві звернення не виносять винятки назовні, а результати прогону кешуються в межах одного запуску.
19
17
 
20
18
  ## Поведінка
21
19
 
22
- runDocFilesGenCli запускає повний прогін документації: прибирає сирітські файли через purgeOrphanedDocs, обирає актуальні цілі через selectTargets і передає їх у runGenerationBatch. Результати записуються у відповідні docs-файли, а після проходу оновлюються директорійні індекси через generateDirIndex.
20
+ Генерація документації стартує з виявлення цілей через selectTargets: у звичайному режимі беруться застарілі або degraded-доки, які ще не отримували повторної спроби для поточної версії джерела; у режимі overwrite обробляються всі. Це робить прогін сходинковим: після невдалої спроби degraded-док більше не чіпається, доки не зміниться джерело, а новий CRC автоматично повертає його в потік.
21
+
22
+ runDocFilesGenCli збирає підсумковий сценарій: спершу прибирає сирітські доки, потім запускає генерацію для відібраних цілей, а в кінці оновлює індекс директорії. Якщо прогін зупиняється достроково або через помилки, зроблене лишається на диску з актуальними CRC, тому наступний запуск підхоплює тільки пропущене.
23
23
 
24
- selectTargets підтримує збіжний режим роботи: за замовчуванням бере відсутні, застарілі або degraded-документи, але не ганяє безкінечно ті самі degraded-версії без зміни джерела. Режим перезапису переводить вибір у повну регенерацію всіх знайдених цілей.
24
+ runGenerationBatch є спільним ядром усієї генерації. Воно бере відібрані цілі, робить preflight для локального бекенда і далі вибирає між послідовним шляхом та batch-шляхом. Якщо доступний native batch-аддон і немає м’якого дедлайну, весь набір іде одним submitBatch, і результати розкладаються назад по файлах. Якщо batch-режим недоступний або примусово вимкнений, обробка йде по одному файлу з fail-safe обробкою помилок та circuit-breaker для системних збоїв підряд. М’який дедлайн підтримується лише на послідовному шляху: перший файл завжди стартує, а наступні зупиняються, коли час вичерпано. Усі стани та лічильники накопичуються в спільній статистиці, а вихідний код відображає лише підсумок прогону.
25
25
 
26
- runGenerationBatch є спільним ядром для CLI і автоматичних scoped-прогонів. Перед роботою перевіряє локальний LLM-backend, далі або використовує native batch-шлях після позитивного nativeBatchAvailable, або переходить у послідовний безпечний режим. Для локального provider-контуру очікується endpoint http://127.0.0.1:8000/v1/. Помилки класифікуються так, щоб незворотні пропуски не ламали весь прогін, інфраструктурні збої потрапляли у підсумкову статистику, а системні падіння могли зупинити batch fail-safe exit-кодом без винятків назовні.
26
+ nativeBatchAvailable використовується як перемикач між batch і fallback-потоком. Перевірка не виконує LLM-виклику, а лише підтверджує, що native-реалізація доступна; результат кешується в межах прогону, щоб не повторювати однакову перевірку.
27
27
 
28
- nativeBatchAvailable кешує результат перевірки в межах прогону, щоб не повторювати однаковий тест доступності native-аддона. Якщо batch-недоступний або є м’який дедлайн, runGenerationBatch лишається на послідовному шляху, де частковий прогрес безпечно зберігається по файлах і наступний запуск продовжує за станом CRC.
28
+ generateDirIndex підтримує актуальний directory index у docs/ після будь-яких змін у наборах доків. Він читає наявні markdown-файли, витягує frontmatter і будує оглядову таблицю лише для реальних документів; сам index.md не чіпається, якщо в директорії більше нічого немає.
29
29
 
30
- purgeOrphanedDocs видаляє документацію без відповідного source-файлу, після чого підтримує docs-директорії у чистому стані: оновлює index.md або прибирає порожню директорію. generateDirIndex формує локальний огляд наявних документів у конкретній docs-директорії й не створює індекс там, де немає документів для переліку.
30
+ purgeOrphanedDocs прибирає доки, для яких уже немає source-файлів, і після цього синхронізує індекс директорії. Це тримає docs/ у стані, де в ньому лишається тільки те, що ще прив’язане до коду, а порожні директорії очищуються до мінімально можливого стану.
31
31
 
32
- runDocFilesStampCli не звертається до LLM: він лише приводить наявні документи до актуального frontmatter-штампа source і CRC, зберігаючи вже наявні метадані моделі та якості. Це дає міграційний шлях для старих документів без повторної генерації змісту.
32
+ runDocFilesStampCli працює окремо від генерації: він детерміновано оновлює frontmatter у вже наявних доках без звернення до LLM. Це корисно для міграції та для відновлення метаданих, коли треба синхронізувати source і crc без зміни тексту документа.
33
33
 
34
34
  ## Публічний API
35
35
 
@@ -73,6 +73,12 @@ T8 (2b-batch, рішення Р): коли доступний native-аддон
73
73
  Поля `model`, `tier` та якості (`score`/`issues`/`judgeModel`) при цьому зберігаються
74
74
  з наявного frontmatter.
75
75
 
76
+ ## Сценарії використання
77
+
78
+ - `npm/rules/doc-files/docgen-files-batch/tests/docgen-files-batch.test.mjs` (runDocFilesGenCli — circuit-breaker / класифікація; selectTargets — stale + degraded-once guard) — 3 systemic підряд → abort, exit 2, решта не обробляється; permanent → skip, прогін триває, exit 0; ok між systemic скидає streak → без abort; default: stale | degraded-not-cloud-avg → обрано; good | degraded-cloud-avg → пропущено; --overwrite → усі цілі незалежно від стану; ще 14
79
+ - `npm/rules/doc-files/docgen-files-batch/tests/docgen-files-stamp.test.mjs` (runDocFilesStampCli — збереження frontmatter-полів) — stamp оновлює crc і НЕ губить tier/judgeModel/model/score/issues; stamp доки без quality-полів не вигадує їх і зберігає tier
80
+ - `npm/rules/doc-files/docgen-files-batch/tests/generate-dir-index.test.mjs` (generateDirIndex — MD025/single-title; generateDirIndex — чужий index.md не перезаписується) — згенерований index.md без H1 у тілі; markdownlint не репортить MD025; контроль чутливості: frontmatter title + H1 у тілі → markdownlint репортить MD025; людський index.md без frontmatter лишається недоторканим; index.md як дока source-файлу (type JS Module) лишається недоторканою; власний Directory Index перегенеровується; без інших док index не створюється
81
+
76
82
  ## Гарантії поведінки
77
83
 
78
84
  - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -16,8 +16,16 @@ import { env } from 'node:process'
16
16
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
17
17
  import { createProgressReporter } from '../../../scripts/lib/lint-surface/progress.mjs'
18
18
  import { generateDoc, DEFAULT_LOCAL_MODEL, prepareBatchItem, finishBatchItem } from '../docgen-gen/main.mjs'
19
- import { crc32, stampDoc, readDocQuality, readDocModel, readDocTier, QUALITY_THRESHOLD } from '../docgen-crc/main.mjs'
19
+ import {
20
+ documentationCrc,
21
+ stampDoc,
22
+ readDocQuality,
23
+ readDocModel,
24
+ readDocTier,
25
+ QUALITY_THRESHOLD
26
+ } from '../docgen-crc/main.mjs'
20
27
  import { resolveRoot, scanForDocFiles, scanOrphanedDocs } from '../docgen-scan/main.mjs'
28
+ import { buildTestEvidenceIndex } from '../docgen-test-context/main.mjs'
21
29
  import { submitBatch as submitBatchNative } from '@7n/llm-lib/batch'
22
30
 
23
31
  /** Regex-класифікатори помилки генерації (module-scope, без ре-компіляції на виклик). */
@@ -147,10 +155,16 @@ function fmtSize(bytes) {
147
155
  * @param {string} root абсолютний корінь
148
156
  * @param {{ done: number, total: number }} progress позиція у прогресі
149
157
  * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор
150
- * @param {{ model?: string, tier?: string|null, emit?: (s: string) => void, deadlineAt?: number|null }} [opts] модель/тир для штампу; emit — логер рядка результату; deadlineAt — мʼякий дедлайн fix-pipeline для generateDoc
158
+ * @param {{ model?: string, tier?: string|null, emit?: (s: string) => void, deadlineAt?: number|null, testIndex?: ReturnType<typeof buildTestEvidenceIndex> }} [opts] модель/тир для штампу; emit — логер рядка результату; deadlineAt — мʼякий дедлайн fix-pipeline для generateDoc; testIndex — спільний source↔tests index
151
159
  * @returns {Promise<'ok'|'permanent'|'systemic'|'transient'>} результат для керування циклом
152
160
  */
153
- async function generateOne(file, root, progress, stats, { model, tier, emit, deadlineAt = null } = {}) {
161
+ async function generateOne(
162
+ file,
163
+ root,
164
+ progress,
165
+ stats,
166
+ { model, tier, emit, deadlineAt = null, testIndex = buildTestEvidenceIndex(root) } = {}
167
+ ) {
154
168
  const out = emit ?? (s => process.stdout.write(s))
155
169
  const sourceAbs = join(root, file.sourcePath)
156
170
  let size = 0
@@ -164,8 +178,8 @@ async function generateOne(file, root, progress, stats, { model, tier, emit, dea
164
178
  const docAbs = join(root, file.docPath)
165
179
  // Варіант B: передаємо наявну доку, щоб зберегти захищену секцію «Призначення»
166
180
  const existingMd = existsSync(docAbs) ? readFileSync(docAbs, 'utf8') : null
167
- const result = await generateDoc(sourceAbs, { existingMd, model, deadlineAt })
168
- const crc = crc32(readFileSync(sourceAbs))
181
+ const result = await generateDoc(sourceAbs, { existingMd, model, deadlineAt, testIndex })
182
+ const crc = documentationCrc(sourceAbs, testIndex)
169
183
  mkdirSync(dirname(docAbs), { recursive: true })
170
184
  const quality =
171
185
  result.score === null
@@ -261,7 +275,7 @@ function defaultLocalProviders() {
261
275
  * де м'який дедлайн підтримується).
262
276
  * @param {Array<object>} targets елементи scanForDocFiles
263
277
  * @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
278
+ * @param {{ model?: string, tier?: string|null, localProviders?: object, submitBatchImpl?: (modelSpecOrTier: string, items: Array<object>, opts?: object) => Promise<Array<object>>, testIndex: ReturnType<typeof buildTestEvidenceIndex> }} opts модель/тир/local-provider-конфіг (інакше `defaultLocalProviders()`)/інжект submitBatch/source↔tests index
265
279
  * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
266
280
  * @param {{ reporter?: object, emit?: (s: string) => void }} io прогрес-репортер і логер рядка результату
267
281
  * @returns {Promise<void>}
@@ -271,20 +285,28 @@ async function runBatchPass(targets, root, opts, stats, { reporter, emit }) {
271
285
  const submitBatchImpl = opts.submitBatchImpl ?? submitBatchNative
272
286
  const out = emit ?? (s => process.stdout.write(s))
273
287
 
274
- const prepared = await prepareBatchTargets(targets, root, stats, { reporter, out })
288
+ const prepared = await prepareBatchTargets(targets, root, stats, { reporter, out }, opts.testIndex)
275
289
  if (prepared.length === 0) return
276
290
 
277
- const items = prepared.map(p => ({
291
+ // Авторські comments уже є джерелом істини: штампуємо такі документи тут,
292
+ // не створюючи порожній native batch і не торкаючись LLM.
293
+ const llmPrepared = prepared.filter(p => p.mode !== 'comment-only')
294
+ for (const p of prepared.filter(p => p.mode === 'comment-only')) {
295
+ processBatchResult({ ok: '' }, p, { model, tier: opts.tier ?? null, stats, out })
296
+ }
297
+ if (llmPrepared.length === 0) return
298
+
299
+ const items = llmPrepared.map(p => ({
278
300
  customId: p.file.sourcePath,
279
301
  prompt: p.messages.find(m => m.role === 'user')?.content ?? '',
280
302
  system: p.messages.find(m => m.role === 'system')?.content
281
303
  }))
282
- const onProgress = makeBatchProgress(reporter, targets.length - prepared.length, targets.length)
304
+ const onProgress = makeBatchProgress(reporter, targets.length - llmPrepared.length, targets.length)
283
305
  const localProviders = opts.localProviders ?? defaultLocalProviders()
284
306
  const results = await submitBatchImpl(model, items, { onProgress, localProviders })
285
307
  const byId = new Map(results.map(r => [r.customId, r]))
286
308
 
287
- for (const p of prepared) {
309
+ for (const p of llmPrepared) {
288
310
  processBatchResult(byId.get(p.file.sourcePath), p, { model, tier: opts.tier ?? null, stats, out })
289
311
  }
290
312
  }
@@ -297,9 +319,10 @@ async function runBatchPass(targets, root, opts, stats, { reporter, emit }) {
297
319
  * @param {string} root абсолютний корінь
298
320
  * @param {{ ok: number, degraded: number, err: number, errors: string[], skipped: string[] }} stats акумулятор (мутується)
299
321
  * @param {{ reporter?: object, out: (s: string) => void }} io прогрес-репортер і логер
300
- * @returns {Promise<Array<object>>} елементи, готові до batch-у (file/sourceAbs/docAbs/size/facts/anchors/src/messages/intent)
322
+ * @param {ReturnType<typeof buildTestEvidenceIndex>} testIndex source↔tests index
323
+ * @returns {Promise<Array<object>>} елементи, готові до batch-у або 0-LLM запису (file/sourceAbs/docAbs/size/facts/anchors/src/mode/messages/intent/testIndex)
301
324
  */
302
- async function prepareBatchTargets(targets, root, stats, { reporter, out }) {
325
+ async function prepareBatchTargets(targets, root, stats, { reporter, out }, testIndex) {
303
326
  const prepared = []
304
327
  for (const file of targets) {
305
328
  const sourceAbs = join(root, file.sourcePath)
@@ -312,8 +335,8 @@ async function prepareBatchTargets(targets, root, stats, { reporter, out }) {
312
335
  }
313
336
  const existingMd = existsSync(docAbs) ? readFileSync(docAbs, 'utf8') : null
314
337
  try {
315
- const prep = await prepareBatchItem(sourceAbs, { existingMd })
316
- prepared.push({ file, sourceAbs, docAbs, size, ...prep })
338
+ const prep = await prepareBatchItem(sourceAbs, { existingMd, testIndex })
339
+ prepared.push({ file, sourceAbs, docAbs, size, testIndex, ...prep })
317
340
  } catch (error) {
318
341
  recordBatchOutcome(stats, out, file.sourcePath, size, error.message)
319
342
  }
@@ -390,8 +413,15 @@ function processBatchResult(r, p, { model, tier, stats, out }) {
390
413
  )
391
414
  return
392
415
  }
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))
416
+ const finished = finishBatchItem(r.ok, {
417
+ facts: p.facts,
418
+ anchors: p.anchors,
419
+ src: p.src,
420
+ intent: p.intent,
421
+ model,
422
+ mode: p.mode
423
+ })
424
+ const crc = documentationCrc(p.sourceAbs, p.testIndex)
395
425
  mkdirSync(dirname(p.docAbs), { recursive: true })
396
426
  const quality =
397
427
  finished.score === null
@@ -632,7 +662,8 @@ async function runSequentialPass(targets, root, opts, stats, { reporter, emit })
632
662
  model: opts.model,
633
663
  tier: opts.tier,
634
664
  emit,
635
- deadlineAt: opts.deadlineAt ?? null
665
+ deadlineAt: opts.deadlineAt ?? null,
666
+ testIndex: opts.testIndex
636
667
  })
637
668
  reporter?.concernDone(file.sourcePath)
638
669
  // Circuit-breaker: K systemic-збоїв підряд → негайний abort (середовище впало,
@@ -682,6 +713,8 @@ export async function runGenerationBatch(targets, root, opts = {}) {
682
713
 
683
714
  if (headline) console.log(headline)
684
715
  const stats = { ok: 0, degraded: 0, err: 0, errors: [], skipped: [] }
716
+ const testIndex = buildTestEvidenceIndex(root)
717
+ const runOpts = { ...opts, testIndex }
685
718
 
686
719
  // ProgressReporter (канон scripts.mdc): бар по файлах лише в TTY; не-TTY лишає
687
720
  // поточні append-рядки [done/total] без дубльованого ⏱-зведення.
@@ -706,10 +739,10 @@ export async function runGenerationBatch(targets, root, opts = {}) {
706
739
  let deadlineHit = false
707
740
  try {
708
741
  if (useBatch) {
709
- await runBatchPass(targets, root, { ...opts, submitBatchImpl }, stats, { reporter, emit })
742
+ await runBatchPass(targets, root, { ...runOpts, submitBatchImpl }, stats, { reporter, emit })
710
743
  done = targets.length
711
744
  } else {
712
- ;({ done, aborted, deadlineHit } = await runSequentialPass(targets, root, opts, stats, { reporter, emit }))
745
+ ;({ done, aborted, deadlineHit } = await runSequentialPass(targets, root, runOpts, stats, { reporter, emit }))
713
746
  }
714
747
  } finally {
715
748
  reporter?.stop()
@@ -738,12 +771,13 @@ export async function runGenerationBatch(targets, root, opts = {}) {
738
771
  */
739
772
  export function runDocFilesStampCli(argv) {
740
773
  const root = resolveRoot(argv)
774
+ const testIndex = buildTestEvidenceIndex(root)
741
775
  let stamped = 0
742
776
  for (const file of scanForDocFiles(root)) {
743
777
  const docAbs = join(root, file.docPath)
744
778
  if (!existsSync(docAbs)) continue
745
779
  const sourceAbs = join(root, file.sourcePath)
746
- const crc = crc32(readFileSync(sourceAbs))
780
+ const crc = documentationCrc(sourceAbs, testIndex)
747
781
  const md = readFileSync(docAbs, 'utf8')
748
782
  const { score, issues, judgeModel } = readDocQuality(docAbs)
749
783
  const model = readDocModel(docAbs)
@@ -3,13 +3,33 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-gen/main.mjs
5
5
  docgen:
6
- crc: 80c789c7
6
+ crc: 55d2b8ff
7
7
  model: openai-codex/gpt-5.4-mini
8
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
9
+ score: 50
10
+ issues: internal-name:isApiGap,internal-name:renderApiLine,internal-name:oneShotDoc,internal-name:finishUnsupported,anchor-miss:(foo.mdc),anchor-miss:(abie.mdc),best-of-2:retry-lost
11
11
  ---
12
12
 
13
+ ## Огляд
14
+
15
+ Модуль формує лаконічну поведінкову документацію для коду через набір публічних кроків: `prepareBatchItem` готує окремий елемент, `generateDoc` створює текст документації, а `finishBatchItem` завершує обробку результату. Для локальних сценаріїв використовується `DEFAULT_LOCAL_MODEL`, а кешування працює у межах прогону, щоб повторні звернення в одному запуску не дублювали однакову роботу. Додаткові операції підтримують цілісність вхідного контексту (`capTimeoutToDeadline`, `stripLeadingPreamble`, `splitProtected`, `insertProtected`) і керують якістю та складом вихідного опису (`scoreDoc`, `buildApiSection`, `hasCompleteCommentDocumentation`, `commentDocumentationMode`, `insertTestScenarios`).
16
+
17
+ ## Поведінка
18
+
19
+ Документ збирається як керований конвеєр: джерело й факти проходять preflight, після чого модуль або бере повністю детермінований шлях, або підключає локальну LLM-генерацію з подальшою перевіркою якості. Бюджет часу ріжеться через capTimeoutToDeadline, тож будь-який виклик не виходить за межі дедлайну; коли бюджет вичерпано, генерація зупиняється без старту нового запиту.
20
+
21
+ Початковий текст моделі очищується stripLeadingPreamble, щоб прибрати чатову самореференцію, а splitProtected та insertProtected зберігають захищену секцію «Призначення» під час усіх перетворень. Це важливо для повторних прогонів: наявний намір із попередньої документації не губиться, навіть якщо решта документа перегенеровується.mdc) мають зберігатися в точному вигляді, бо вони використовуються як дослівні якорі для перевірки відповідності.
22
+
23
+ Оцінка якості через scoreDoc працює на зібраному Markdown, а не на сирому відповіді моделі: секції спочатку нормалізуються, потім порівнюються з фактами, захищеним блоком і дослівними якорями. Саме цей score визначає, чи результат придатний одразу, чи потрібен повторний прохід, і чи слід позначати документ degraded. buildApiSection додає в документ лише те, що справді випливає з уже відомого public API, тому не дублює очевидне й не вигадує відсутні деталі.
24
+
25
+ hasCompleteCommentDocumentation і commentDocumentationMode керують тим, чи можна обійтися без LLM або обмежитися мінімальним доповненням з авторських коментарів. Якщо header і змістовні описи вже повністю покривають документ, генерація лишається детермінованою; якщо ж є лише часткові підказки, запускається comment-only або змішаний режим із коротким добудовуванням поведінки. У такому випадку commentDocumentationMode лише вибирає маршрут, а не переписує зміст.
26
+
27
+ insertTestScenarios додає окрему секцію сценаріїв з test/spec-файлів поверх уже зібраного Markdown, не змішуючи їх із поведінковим описом. Це дає змогу зберігати один і той самий основний текст документа незалежно від того, чи прийшов він із one-shot, batch або коментованого режиму.
28
+
29
+ DEFAULT_LOCAL_MODEL задає локальну модель за замовчуванням для всього конвеєра, а generateDoc є головною точкою входу: вона читає файл, отримує факти, вибирає режим, збирає промпт, викликає генерацію, рахує score і повертає готовий документ з метаданими. Якщо початкова версія не дотягує до порогу, виконується один повторний прохід із більш агресивним налаштуванням; якщо й він не допоміг, результат лишається degraded, але не ламає весь прогін.
30
+
31
+ prepareBatchItem і finishBatchItem ділять batch-потік на підготовку та фіналізацію: перша частина збирає все потрібне до submit, друга — перетворює вже отриманий текст у такий самий результат, як у послідовному шляху. Це забезпечує однакові правила для single-file і batch-режиму, але без дублювання LLM-викликів у batch-шарі та без змішування стану між файлами.
32
+
13
33
  ## Публічний API
14
34
 
15
35
  - capTimeoutToDeadline — Ріже базовий per-call таймаут під залишок бюджету до дедлайну.
@@ -27,6 +47,14 @@ JSDoc-описом експорти рендеряться дослівно (`re
27
47
  немає — секція збирається БЕЗ жодного LLM-виклику. Єдиний непокритий
28
48
  експорт (як і раніше) лишається описаним лише в Поведінці — окремого виклику
29
49
  на секцію з одного рядка не варте.
50
+ - hasCompleteCommentDocumentation — Чи коментарі автора повністю покривають машинну документацію: header дає
51
+ «Огляд», а змістовні описи всіх public API — відповідну секцію. У такому
52
+ разі LLM не потрібна: текст зберігається дослівно для JS, Rust і Python.
53
+ - commentDocumentationMode — Вибирає гібридний режим для повністю прокоментованого source. Короткий
54
+ header майже напевно є pointer-ом, а середній header разом із явним flow у
55
+ коді потребує короткого LLM-доповнення. Детальний наратив лишається 0-LLM.
56
+ - insertTestScenarios — Додає test-сценарії до one-shot/batch-документа. Для unsupported мов основний
57
+ Markdown ще повертає LLM, але test-секція лишається виключно JS-рендером.
30
58
  - DEFAULT_LOCAL_MODEL — Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') (→ N_LOCAL_MIN_MODEL).
31
59
  Без хардкод-fallback: модель налаштовує кожен локально (`N_LOCAL_MIN_MODEL`); якщо
32
60
  нічого не задано — порожньо, і preflight оркестратора фейлить гучно (а не шле
@@ -48,6 +76,10 @@ pre-send guard і той самий факт-лист/one-shot messages, що й
48
76
  викликається (мінімальний обсяг T8 — генерація; judge лишається опційним
49
77
  розширенням послідовного шляху).
50
78
 
79
+ ## Сценарії використання
80
+
81
+ - `npm/rules/doc-files/docgen-gen/tests/docgen-gen.test.mjs` (scoreDoc — R4 generic-overview; scoreDoc — R6 витік службових імен) — абстрактний Огляд штрафується і опускає score під поріг; конкретний Огляд не штрафується; неекспортована функція у Поведінці → internal-name; пропущений валідний анкор → anchor-miss + штраф; наявний анкор → без штрафу; ще 58
82
+
51
83
  ## Гарантії поведінки
52
84
 
53
85
  - Кешує результати в межах одного прогону.