@7n/rules 1.49.25 → 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 +16 -0
- package/package.json +1 -1
- package/rules/doc-files/check/docs/index.md +10 -0
- package/rules/doc-files/check/docs/main.md +4 -2
- package/rules/doc-files/check/main.mjs +12 -4
- package/rules/doc-files/docgen-crc/docs/index.md +9 -0
- package/rules/doc-files/docgen-crc/docs/main.md +47 -26
- package/rules/doc-files/docgen-crc/main.mjs +21 -4
- package/rules/doc-files/docgen-files-batch/docs/main.md +20 -14
- package/rules/doc-files/docgen-files-batch/main.mjs +54 -20
- package/rules/doc-files/docgen-gen/docs/main.md +35 -3
- package/rules/doc-files/docgen-gen/main.mjs +253 -19
- package/rules/doc-files/docgen-prompts/docs/main.md +18 -12
- package/rules/doc-files/docgen-prompts/main.mjs +19 -12
- package/rules/doc-files/docgen-scan/docs/main.md +44 -32
- package/rules/doc-files/docgen-scan/main.mjs +6 -3
- package/rules/doc-files/docgen-test-context/docs/index.md +9 -0
- package/rules/doc-files/docgen-test-context/docs/main.md +57 -0
- package/rules/doc-files/docgen-test-context/main.mjs +212 -0
- package/rules/doc-files/main.mdc +37 -6
- package/rules/k8s/manifests/main.mjs +94 -38
- package/scripts/lib/docs/resolve-plugins.md +26 -53
- package/scripts/lib/lint-surface/lint-lock.mjs +81 -16
- package/scripts/lib/lint-surface/progress.mjs +15 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +9 -1
- package/scripts/lib/lint-surface/types.mjs +2 -0
- package/scripts/lib/resolve-plugins.mjs +70 -6
- package/skills/doc-files/SKILL.md +21 -6
|
@@ -15,7 +15,8 @@ export const STYLE = [
|
|
|
15
15
|
'Заборонено: сигнатури, типи, параметри функцій; перелік stdlib-модулів; опис regex чи внутрішніх приватних імен.',
|
|
16
16
|
// R9-профілактика: gemma-подібні малі моделі «озвучують завдання» перед відповіддю;
|
|
17
17
|
// явна заборона з прикладами різко знижує частоту (дет-зрізання у stripSection — страховка).
|
|
18
|
-
'Виведи ЛИШЕ текст секції. ЗАБОРОНЕНО починати з мета-фраз на кшталт «Ось оновлена чорнетка…», «Оновлений текст секції:», «Як технічний письменник, я створю…» — одразу перший змістовний рядок.'
|
|
18
|
+
'Виведи ЛИШЕ текст секції. ЗАБОРОНЕНО починати з мета-фраз на кшталт «Ось оновлена чорнетка…», «Оновлений текст секції:», «Як технічний письменник, я створю…» — одразу перший змістовний рядок.',
|
|
19
|
+
'Не вигадуй маркери, конфігурації або code identifiers; порожній inline code `` заборонений.'
|
|
19
20
|
].join(' ')
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -46,8 +47,8 @@ function factsSummary(facts) {
|
|
|
46
47
|
// безумовне «не пише» модель розганяє до хибного «гарантує безпечність» в Огляді.
|
|
47
48
|
if (m.readOnly) lines.push('Власних операцій запису (ФС/БД) у файлі немає (імпортовані модулі не аналізувались)')
|
|
48
49
|
if (m.network) lines.push('Звертається до мережі')
|
|
49
|
-
if (m.catchesErrors) lines.push('
|
|
50
|
-
if (m.returnsFalsyOnFail) lines.push('
|
|
50
|
+
if (m.catchesErrors) lines.push('Містить локальні fail-safe гілки; інші помилки можуть поширюватися назовні')
|
|
51
|
+
if (m.returnsFalsyOnFail) lines.push('Деякі локальні fail-safe гілки повертають порожнє значення (напр. null) замість винятку')
|
|
51
52
|
lines.push(m.caches ? 'Кешування: так, у межах прогону' : 'Кешування: НЕМАЄ — не згадуй кеш у гарантіях')
|
|
52
53
|
return lines.join('\n')
|
|
53
54
|
}
|
|
@@ -85,9 +86,10 @@ function intentContext(intent) {
|
|
|
85
86
|
* @param {string} src вміст файлу
|
|
86
87
|
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
|
|
87
88
|
* @param {string|null} [intent] захищена секція «Призначення» як read-only контекст
|
|
89
|
+
* @param {{ complementaryBehavior: boolean }} [opts] LLM доповнює лише прогалини між comments і кодом
|
|
88
90
|
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (лише behavior)
|
|
89
91
|
*/
|
|
90
|
-
export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
92
|
+
export function sectionMessages(facts, src, anchors = null, intent = null, { complementaryBehavior = false } = {}) {
|
|
91
93
|
const factsTxt = factsSummary(facts)
|
|
92
94
|
const anch = anchorsBlock(anchors)
|
|
93
95
|
const intentCtx = intentContext(intent)
|
|
@@ -99,21 +101,26 @@ export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
|
99
101
|
// його іншими словами. Натомість — крос-функціональний наратив: те, чого
|
|
100
102
|
// немає в жодному окремому JSDoc за визначенням.
|
|
101
103
|
const exportNames = (facts.exports ?? []).map(e => e.name)
|
|
102
|
-
const behaviorTask =
|
|
103
|
-
? '
|
|
104
|
-
:
|
|
104
|
+
const behaviorTask = complementaryBehavior
|
|
105
|
+
? 'короткі поведінкові абзаци про відсутній користувацький контракт, без алгоритму'
|
|
106
|
+
: multi
|
|
107
|
+
? 'крос-функціональний потік: у якому порядку і як функції взаємодіють між собою, звідки приходять дані і куди йдуть результати, спільні правила чи стан. НЕ переказуй кожну функцію окремим пунктом — одно-рядкові описи вже є в секції «Публічний API»'
|
|
108
|
+
: 'нумерований алгоритм у бізнес-термінах'
|
|
105
109
|
const onlyExports = exportNames.length
|
|
106
110
|
? ` Описуй РІВНО ці публічні імена і жодних інших: ${exportNames.join(', ')}.`
|
|
107
111
|
: ''
|
|
108
112
|
const noInternal = facts.internalSymbols?.length
|
|
109
113
|
? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.`
|
|
110
114
|
: ''
|
|
115
|
+
const complementaryInstruction = complementaryBehavior
|
|
116
|
+
? ' «Огляд» і «Публічний API» уже дослівно зібрані з авторських коментарів. Додай ЛИШЕ відсутній для користувача контракт: умови, результат, error-flow, concurrency або інваріанти. Не перефразовуй авторський текст і НЕ переказуй реалізацію: заборонені обходи каталогів, цикли, читання файлів, AST-вузли, допоміжні виклики та нумерований алгоритм. Заборонені generic-фрази без факту з коду: «не вимагає налаштування», «виклик ініціює перевірку», «успішно повертається результат». Дай 1–4 короткі абзаци; якщо доповнювати нічого — поверни рівно NONE.'
|
|
117
|
+
: ''
|
|
111
118
|
const behavior = {
|
|
112
119
|
key: 'behavior',
|
|
113
120
|
numPredict: 500,
|
|
114
121
|
messages: msgs(
|
|
115
122
|
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}${intentCtx}`,
|
|
116
|
-
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
123
|
+
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports}${complementaryInstruction} Сценарії з test/spec-файлів рендерить JS окремою секцією — не згадуй і не відтворюй їх. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
117
124
|
)
|
|
118
125
|
}
|
|
119
126
|
return [behavior]
|
|
@@ -127,7 +134,7 @@ const STUB_DESC_RE = /^опис\.?$/i
|
|
|
127
134
|
/**
|
|
128
135
|
* Stage 2 (gap-детект, 0 токенів): чи є опис експорту прогалиною — відсутній
|
|
129
136
|
* або JSDoc-заглушка без сенсу.
|
|
130
|
-
* @param {{desc
|
|
137
|
+
* @param {{desc:string}} exp запис експорту з факт-листа
|
|
131
138
|
* @returns {boolean} true — опис потрібно синтезувати LLM (Stage 3)
|
|
132
139
|
*/
|
|
133
140
|
export function isApiGap(exp) {
|
|
@@ -255,8 +262,8 @@ export function guaranteesFromMarkers(facts) {
|
|
|
255
262
|
// який file-local аналіз не може підтвердити. Обмежене твердження — може.
|
|
256
263
|
if (m.readOnly)
|
|
257
264
|
lines.push('- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.')
|
|
258
|
-
if (m.catchesErrors) lines.push('-
|
|
259
|
-
if (m.returnsFalsyOnFail) lines.push('-
|
|
265
|
+
if (m.catchesErrors) lines.push('- Містить локальні fail-safe гілки; інші помилки можуть поширюватися назовні.')
|
|
266
|
+
if (m.returnsFalsyOnFail) lines.push('- Деякі локальні fail-safe гілки повертають порожнє значення (напр. `null`) замість винятку.')
|
|
260
267
|
if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
|
|
261
268
|
if (m.skips?.length) {
|
|
262
269
|
lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
|
|
@@ -275,7 +282,7 @@ export function oneShotMessages(facts, src) {
|
|
|
275
282
|
const multi = (facts.exports?.length || 0) > 1
|
|
276
283
|
return msgs(
|
|
277
284
|
STYLE,
|
|
278
|
-
`Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії
|
|
285
|
+
`Напиши документацію для файлу. Секції: ## Огляд (1-3 речення), ## Поведінка (нумерований/маркований алгоритм), ${multi ? '## Публічний API (назва + що робить), ' : ''}## Гарантії поведінки. Не додавай «Сценарії використання»: її детерміновано рендерить JS із повʼязаних test/spec-файлів.\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\``
|
|
279
286
|
)
|
|
280
287
|
}
|
|
281
288
|
|
|
@@ -3,50 +3,62 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/doc-files/docgen-scan/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
6
|
+
crc: 9db666fe
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
8
9
|
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.
|
|
10
|
+
issues: judge-refine:kept-original,judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
10
12
|
---
|
|
11
13
|
|
|
12
14
|
## Огляд
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
Файл сканує дерево проєкту й класифікує стан сусідньої поведінкової документації в `docs/` без власних операцій запису. Він визначає кодові кандидати через активні lang-плагіни, поважає `.gitignore`, пропускає тести й службові дерева, а для згенерованої документації оцінює docgen-CRC з урахуванням повʼязаних usage-сценаріїв.
|
|
17
|
+
|
|
18
|
+
Публічні API-анкори: `isSourceFile`, `docPathForSource`, `isDocCandidate`, `scanForDocFiles`, `scanOrphanedDocs`, `describeFile`, `resolveRoot`.
|
|
19
|
+
|
|
20
|
+
Стан документації розрізняє відсутню доку як `stale:missing`, свіжу як `stale:false`, змінену відносно CRC як `crc-mismatch`, а ручні або чужі документи як `foreign:true` без позначення застарілості. Docgen-документи з CRC лишаються `foreign:false` і перевіряються за CRC-семантикою. Orphan-сканування окремо знаходить документацію, для якої зник source, але не вважає orphan ручні документи, Directory Index і docs у службових деревах.
|
|
15
21
|
|
|
16
22
|
## Поведінка
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
docPathForSource обчислює очікуваний шлях до markdown-документа для заданого кодового джерельного шляху, розміщуючи його у теці `docs` поруч із джерелом.
|
|
20
|
-
isDocCandidate визначає, чи повинен файл підлягати документуванню, перевіряючи його тип, статус тесту, статус ігнорування та чи не є він частиною системних документації.
|
|
21
|
-
describeFile описує кодовий файл, надаючи шлях джерела та шлях до відповідного документа, а також статус його застарілості за CRC. Якщо док-файл існує, але без `docgen:`-CRC у frontmatter (рукописна дока), файл позначається `foreign: true` і НЕ вважається застарілим — людський зміст рахується чинною документацією і мовчки не перезаписується (перезапис лише explicit `--overwrite` у batch-CLI).
|
|
22
|
-
scanOrphanedDocs знаходить markdown-документи, які мають метадані про джерело, але відповідний код-файл більше не існує.
|
|
23
|
-
scanForDocFiles обходить дерево від кореня і повертає список всіх кодових файлів, для яких може бути згенерована документація, з інформацією про їхній статус застарілості, виключаючи ті, що ігноруються через Git.
|
|
24
|
-
resolveRoot визначає абсолютний кореневий каталог для сканування, беручи його з аргументів або використовуючи поточний робочий каталог.
|
|
25
|
-
runDocFilesScanCli сканує вказане дерево та виводить у форматі JSON список усіх кодових файлів з їхнім статусом застарілості.
|
|
26
|
-
runDocFilesCheckCli виконує перевірку на застарілість документації: може перевіряти один файл із stdin, групу змінених файлів через `git diff`, або звітувати про неякісні документи, блокуючи процес при знаходженні застарілого коду.
|
|
24
|
+
`resolveRoot` визначає корінь обходу з CLI або поточної теки. Від цього кореня весь сканер читає конфігурацію активних lang-плагінів, застосовує правила пропуску й повертає відносні шляхи результатів.
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
`scanForDocFiles` є основним потоком: обходить дерево від кореня, відсіює службові й ignored-дерева, root-level файли в system-wide docs layout, тести, `.d.ts` і некодові файли. Статус кандидата узгоджується через `isDocCandidate`, а кодове розширення — через `isSourceFile`. Розширення не вбудовані в ядро: JS/Vue, Python і Rust-файли стають джерелами лише тоді, коли їх декларують активні lang-плагіни; без такого плагіна файл не документується.
|
|
27
|
+
|
|
28
|
+
Для кожного кандидата `docPathForSource` спрямовує документацію в сусідню теку `docs/` зі stem імені джерела. Далі `describeFile` порівнює стан документа з джерелом і повертає, чи потрібне оновлення: відсутня документація дає `missing`, зміна джерела або повʼязаного usage-сценарію дає `crc-mismatch`, збіг CRC означає актуальний документ.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
scanOrphanedDocs
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
runDocFilesScanCli — сканує дерево та виводить у форматі JSON список усіх кодових файлів зі статусом застарілості.
|
|
38
|
-
runDocFilesCheckCli — виконує перевірку на застарілість доків для хуків та CLI-інструментів.
|
|
30
|
+
Рукописна документація має пріоритет над автоматичною генерацією. Якщо очікуваний doc-файл уже існує, але не має docgen-CRC у frontmatter, `describeFile` позначає його як foreign і не вважає stale. Це покриває як документи без frontmatter, так і документи з людським frontmatter без docgen-CRC; такі файли не мають мовчки перезаписуватись звичайним скануванням.
|
|
31
|
+
|
|
32
|
+
`scanForDocFiles` поважає `.gitignore`: ignored source-файли не потрапляють у результат, а шляхи без ignore-маркера залишаються кандидатами. Якщо git-контекст недоступний або ignored-шляхів немає, сканування продовжується без помилки.
|
|
33
|
+
|
|
34
|
+
`scanOrphanedDocs` працює окремим потоком очищення: шукає лише згенеровані doc-файли з resource і docgen-CRC та повідомляє ті, для яких source вже зник. Directory Index-документи з resource, що закінчується на `/`, ручні документи без CRC або без resource, а також документи всередині `node_modules` не вважаються orphan.
|
|
35
|
+
|
|
36
|
+
## Публічний API
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
- isSourceFile — Чи є файл кодовим джерелом для документування. Розширення декларують ЛИШЕ
|
|
39
|
+
активні lang-плагіни (`n-rules.contributes.docFiles.extensions` — js/mjs/ts/vue
|
|
40
|
+
дає `@7n/rules-lang-js`, .rs/.py — lang-rust/lang-python); у ядрі вбудованих
|
|
41
|
+
розширень немає (фаза 5b spec lang-plugins-extraction).
|
|
42
|
+
- docPathForSource — Обчислює шлях md-документа для кодового файлу: тека `docs/` поряд із джерелом.
|
|
43
|
+
Якщо `sourcePath` відносний, `docPath` теж відносний; якщо абсолютний — абсолютний.
|
|
44
|
+
- isDocCandidate — Чи кодовий файл `relPath` (posix, від кореня) підлягає документуванню:
|
|
45
|
+
правильне розширення, не тест, не в ignore-дереві, не кореневий system-wide docs.
|
|
46
|
+
- describeFile — Описує один кодовий файл: шлях джерела, шлях доки, стан застарілості за CRC.
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
`foreign: true` — docPath існує, але БЕЗ `docgen:`-CRC у frontmatter: рукописна
|
|
49
|
+
(людська) дока. Така дока вважається чинною документацією файлу (`stale: false`) —
|
|
50
|
+
генерація її мовчки не перезаписує (перезапис лише explicit `--overwrite`, який
|
|
51
|
+
бере всі цілі без фільтра). Живий кейс: `npm/docs/index.md` — людський зміст модуля
|
|
52
|
+
у проєкті-споживачі; сканер бачив його як `missing` і затирав чат-філером моделі.
|
|
53
|
+
- scanOrphanedDocs — Знаходить "сирітські" доки: `docs/<stem>.md` із `resource:` + `docgen.crc` у frontmatter,
|
|
54
|
+
у яких відповідний source-файл (resource:) вже не існує. Перевіряє лише файли,
|
|
55
|
+
згенеровані `fix-doc-files` (наявність `docgen.crc` у frontmatter). Directory Index
|
|
56
|
+
(resource із `/` на кінці) та ручні доки без `resource:` або без CRC — ігноруються.
|
|
57
|
+
- scanForDocFiles — Рекурсивно обходить дерево від `root`, повертає кодові файли зі станом застарілості.
|
|
58
|
+
Синхронний `readdirSync` — детермінований порядок без гонок; обсяг дерева це дозволяє.
|
|
59
|
+
Поверх `DOCGEN_IGNORE_GLOBS` відсіює ще й те, що в `.gitignore` (через git check-ignore).
|
|
60
|
+
- resolveRoot — Парсить `--root <dir>` з argv; default — cwd.
|
|
47
61
|
|
|
48
62
|
## Гарантії поведінки
|
|
49
63
|
|
|
50
|
-
-
|
|
51
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
52
|
-
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
64
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
@@ -6,6 +6,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
6
6
|
import { isDocgenIgnored } from '../docgen-ignore/main.mjs'
|
|
7
7
|
import { parseDocFrontmatter, readDocCrc, staleness } from '../docgen-crc/main.mjs'
|
|
8
8
|
import { pluginDocFilesExtensions } from './lang-extensions.mjs'
|
|
9
|
+
import { buildTestEvidenceIndex } from '../docgen-test-context/main.mjs'
|
|
9
10
|
|
|
10
11
|
/** `*.test.*`, `*.spec.*`, `*.stories.*` — тести й Storybook CSF-файли, документувати не треба. */
|
|
11
12
|
const TEST_FILE_RE = /\.(?:test|spec|stories)\.[^.]+$/u
|
|
@@ -72,15 +73,16 @@ export function isDocCandidate(root, relPath) {
|
|
|
72
73
|
* у проєкті-споживачі; сканер бачив його як `missing` і затирав чат-філером моделі.
|
|
73
74
|
* @param {string} root абсолютний корінь
|
|
74
75
|
* @param {string} sourcePath posix-шлях джерела від кореня
|
|
76
|
+
* @param {ReturnType<typeof buildTestEvidenceIndex>|null} [testIndex] source↔tests index
|
|
75
77
|
* @returns {{sourcePath:string, docPath:string, stale:boolean, reason:'missing'|'crc-mismatch'|null, foreign:boolean}} опис файлу
|
|
76
78
|
*/
|
|
77
|
-
export function describeFile(root, sourcePath) {
|
|
79
|
+
export function describeFile(root, sourcePath, testIndex = null) {
|
|
78
80
|
const docPath = docPathForSource(sourcePath)
|
|
79
81
|
const docAbsPath = join(root, docPath)
|
|
80
82
|
if (existsSync(docAbsPath) && readDocCrc(docAbsPath) === null) {
|
|
81
83
|
return { sourcePath, docPath, stale: false, reason: null, foreign: true }
|
|
82
84
|
}
|
|
83
|
-
const { stale, reason } = staleness(join(root, sourcePath), docAbsPath)
|
|
85
|
+
const { stale, reason } = staleness(join(root, sourcePath), docAbsPath, testIndex)
|
|
84
86
|
return { sourcePath, docPath, stale, reason, foreign: false }
|
|
85
87
|
}
|
|
86
88
|
|
|
@@ -190,6 +192,7 @@ function gitIgnoredPaths(root, relPaths) {
|
|
|
190
192
|
*/
|
|
191
193
|
export function scanForDocFiles(root) {
|
|
192
194
|
const results = []
|
|
195
|
+
const testIndex = buildTestEvidenceIndex(root)
|
|
193
196
|
|
|
194
197
|
/** @param {string} dir поточний каталог обходу */
|
|
195
198
|
function walk(dir) {
|
|
@@ -209,7 +212,7 @@ export function scanForDocFiles(root) {
|
|
|
209
212
|
if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
|
|
210
213
|
const sourcePath = relPath.split(sep).join('/')
|
|
211
214
|
if (isDocgenIgnored(sourcePath)) continue
|
|
212
|
-
results.push(describeFile(root, sourcePath))
|
|
215
|
+
results.push(describeFile(root, sourcePath, testIndex))
|
|
213
216
|
}
|
|
214
217
|
}
|
|
215
218
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: npm/rules/doc-files/docgen-test-context/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 78bc0ee9
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:error
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл визначає docgen test-файли, будує індекс підтверджень із тестів, знаходить тестові підтвердження для source-файлів, рендерить сценарії для документації та визначає source-файли, повʼязані з тестом, через `isDocgenTestFile`, `buildTestEvidenceIndex`, `testEvidenceForSource`, `renderTestScenarios`, `sourceFilesForTest`.
|
|
17
|
+
|
|
18
|
+
Він існує, щоб документація могла посилатися на поведінку, підтверджену тестами, без зупинки процесу через нерозвʼязані або помилкові звʼязки. Локальні fail-safe гілки не дають окремим помилкам аналізу зірвати генерацію; інші помилки можуть поширюватися назовні.
|
|
19
|
+
|
|
20
|
+
## Поведінка
|
|
21
|
+
|
|
22
|
+
isDocgenTestFile визначає, чи файл може бути джерелом підтверджених usage-сценаріїв для документації. Це перший фільтр потоку: до подальшого аналізу потрапляють лише окремі test/spec-файли, тоді як вбудовані Rust unit-тести залишаються частиною самого source-файлу.
|
|
23
|
+
|
|
24
|
+
buildTestEvidenceIndex обходить репозиторій, знаходить релевантні test/spec-файли та будує стабільний індекс звʼязків між source-файлами й тестами. Звʼязок вважається підтвердженим лише тоді, коли тест посилається на реальний файл через relative string literal і цей звʼязок схожий саме на тестування відповідного source, а не на допоміжний import.
|
|
25
|
+
|
|
26
|
+
testEvidenceForSource читає індекс для конкретного source-файлу й перетворює знайдені тестові підтвердження на дані для документації та детермінований payload для перевірки актуальності. Тестовий код не передається в LLM prompt: у документацію потрапляють лише дослівні назви підтверджених сценаріїв, підготовлені для окремого JS-рендеру.
|
|
27
|
+
|
|
28
|
+
renderTestScenarios приймає вже підготовлені тестові підтвердження та детерміновано формує компактний Markdown-фрагмент. Він не вигадує поведінку й не перефразовує тестові назви, а лише обмежує обсяг виводу, щоб документація не дублювала весь test-suite.
|
|
29
|
+
|
|
30
|
+
sourceFilesForTest використовує той самий індекс у зворотному напрямку: для зміненого test/spec-файлу повертає source-файли, документацію яких потрібно вважати потенційно застарілою.
|
|
31
|
+
|
|
32
|
+
Локальні fail-safe гілки під час аналізу не дають непідтвердженим або нерозвʼязаним звʼязкам потрапити у результат і не зупиняють генерацію документації. Власних записів у файлову систему чи базу даних цей модуль не виконує.
|
|
33
|
+
|
|
34
|
+
## Публічний API
|
|
35
|
+
|
|
36
|
+
- isDocgenTestFile — Чи шлях має форму окремого test/spec-файлу, який може описувати usage-сценарії.
|
|
37
|
+
Rust unit-тести всередині source-файлу вже входять до самого джерела.
|
|
38
|
+
- buildTestEvidenceIndex — Будує один source↔tests index на репозиторій. Зв'язок вважається доведеним
|
|
39
|
+
лише через relative string literal, що резолвиться у реальний файл.
|
|
40
|
+
- testEvidenceForSource — Формує дані для JS-рендеру сценаріїв і детермінований payload для CRC.
|
|
41
|
+
Test-код не потрапляє до LLM prompt: опис тестового usage лишається дослівним.
|
|
42
|
+
- renderTestScenarios — Детерміновано рендерить компактні підтверджені тестами сценарії у Markdown.
|
|
43
|
+
Назви походять безпосередньо з `describe`/`test`/`it`, тому LLM не може їх
|
|
44
|
+
перефразувати або додати неіснуючу поведінку; показуємо до пʼяти прикладів,
|
|
45
|
+
а решту чесно рахуємо, щоб не дублювати весь test-suite у документації.
|
|
46
|
+
- sourceFilesForTest — Source-файли, на які посилається конкретний змінений test/spec-файл.
|
|
47
|
+
|
|
48
|
+
## Сценарії використання
|
|
49
|
+
|
|
50
|
+
- `npm/rules/doc-files/docgen-test-context/tests/main.test.mjs` (isDocgenTestFile; buildTestEvidenceIndex) — розпізнає JS/TS test/spec і Python test naming; звичайний source-файл не є тестом; звʼязує source лише з тестом, що реально посилається на нього; інший сценарій; підтримує import без розширення і vi.mock relative reference; ще 4
|
|
51
|
+
- `npm/rules/doc-files/tests/main.test.mjs` (lint — детект (read-only detector)) — ci (files=undefined): ловить відсутню доку у дереві; ci: свіжа дока → 0 violations; quick: змінене джерело без доки → violation; порожній набір → 0; quick: реверс-мапінг — змінена дока веде до перевірки джерела; quick: ігнорує test-файл без звʼязку із source; ще 5
|
|
52
|
+
|
|
53
|
+
## Гарантії поведінки
|
|
54
|
+
|
|
55
|
+
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
|
|
56
|
+
- Містить локальні fail-safe гілки; інші помилки можуть поширюватися назовні.
|
|
57
|
+
- Деякі локальні fail-safe гілки повертають порожнє значення (напр. `null`) замість винятку.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see ./docs/main.md
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'
|
|
6
|
+
|
|
7
|
+
import { isDocgenIgnored } from '../docgen-ignore/main.mjs'
|
|
8
|
+
|
|
9
|
+
const JS_TEST_RE = /\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/u
|
|
10
|
+
const RELATIVE_LITERAL_RE = /(['"])(\.{1,2}\/[^'"\n]+)\1/gu
|
|
11
|
+
const SCENARIO_RE = /\b(describe|test|it)\s*\(\s*['"`]([^'"`\n]{1,200})['"`]/gu
|
|
12
|
+
const QUERY_OR_HASH_RE = /[?#]/u
|
|
13
|
+
const SOURCE_EXTENSIONS = Object.freeze(['.mjs', '.cjs', '.js', '.jsx', '.ts', '.tsx', '.vue', '.py', '.rs'])
|
|
14
|
+
const TEST_SUFFIX_RE = /\.(?:test|spec)$/u
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Чи шлях має форму окремого test/spec-файлу, який може описувати usage-сценарії.
|
|
18
|
+
* Rust unit-тести всередині source-файлу вже входять до самого джерела.
|
|
19
|
+
* @param {string} fileName basename файлу
|
|
20
|
+
* @returns {boolean} true для JS/TS test/spec та Python test-файлів
|
|
21
|
+
*/
|
|
22
|
+
export function isDocgenTestFile(fileName) {
|
|
23
|
+
const pythonTest = fileName.endsWith('.py') && (fileName.startsWith('test_') || fileName.endsWith('_test.py'))
|
|
24
|
+
return JS_TEST_RE.test(fileName) || pythonTest
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Рекурсивно знаходить test/spec-файли, поважаючи те саме ignore-дерево, що й doc-files.
|
|
29
|
+
* @param {string} root корінь репозиторію
|
|
30
|
+
* @returns {string[]} абсолютні шляхи у стабільному порядку
|
|
31
|
+
*/
|
|
32
|
+
function collectTestFiles(root) {
|
|
33
|
+
const out = []
|
|
34
|
+
|
|
35
|
+
/** @param {string} dir поточний каталог */
|
|
36
|
+
function walk(dir) {
|
|
37
|
+
let entries
|
|
38
|
+
try {
|
|
39
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
40
|
+
} catch {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const abs = join(dir, entry.name)
|
|
45
|
+
const rel = relative(root, abs).split(sep).join('/')
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
if (!isDocgenIgnored(rel, 'dir')) walk(abs)
|
|
48
|
+
} else if (entry.isFile() && isDocgenTestFile(entry.name) && !isDocgenIgnored(rel)) {
|
|
49
|
+
out.push(abs)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
walk(root)
|
|
55
|
+
return out.toSorted()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Повертає наявний файл для relative module specifier-а з test-файлу.
|
|
60
|
+
* Підтримує explicit extension, import без розширення і directory index.
|
|
61
|
+
* @param {string} testAbs абсолютний шлях тесту
|
|
62
|
+
* @param {string} specifier relative specifier із рядкового літерала
|
|
63
|
+
* @returns {string|null} абсолютний шлях referenced-файлу
|
|
64
|
+
*/
|
|
65
|
+
function resolveRelativeReference(testAbs, specifier) {
|
|
66
|
+
const clean = specifier.split(QUERY_OR_HASH_RE, 1)[0]
|
|
67
|
+
const base = resolve(dirname(testAbs), clean)
|
|
68
|
+
const candidates = [base]
|
|
69
|
+
if (!extname(base)) {
|
|
70
|
+
for (const ext of SOURCE_EXTENSIONS) candidates.push(base + ext)
|
|
71
|
+
for (const ext of SOURCE_EXTENSIONS) candidates.push(join(base, `index${ext}`))
|
|
72
|
+
}
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return resolve(candidate)
|
|
76
|
+
} catch {
|
|
77
|
+
// Файл міг зникнути між existsSync/statSync — такий reference не беремо.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Витягує файли, на які test/spec посилається relative string literal-ом.
|
|
85
|
+
* Це охоплює static/dynamic import, require, vi.mock та аналогічні API без
|
|
86
|
+
* прив'язки до конкретного test runner-а.
|
|
87
|
+
* @param {string} testAbs абсолютний шлях тесту
|
|
88
|
+
* @param {string} content вміст тесту
|
|
89
|
+
* @returns {string[]} абсолютні referenced-файли
|
|
90
|
+
*/
|
|
91
|
+
function referencedFiles(testAbs, content) {
|
|
92
|
+
const out = new Set()
|
|
93
|
+
for (const match of content.matchAll(RELATIVE_LITERAL_RE)) {
|
|
94
|
+
const referenced = resolveRelativeReference(testAbs, match[2])
|
|
95
|
+
if (referenced) out.add(referenced)
|
|
96
|
+
}
|
|
97
|
+
return [...out]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Відсіює helper imports: relative reference є необхідним, але не достатнім
|
|
102
|
+
* доказом, що тест описує поведінку саме цього source. Додатково вимагається
|
|
103
|
+
* naming (`foo.test` → `foo`) або module layout (`module/tests/*` → main/index).
|
|
104
|
+
* @param {string} testAbs абсолютний шлях тесту
|
|
105
|
+
* @param {string} sourceAbs абсолютний шлях referenced source
|
|
106
|
+
* @returns {boolean} true, якщо тест із високою ймовірністю є тестом source
|
|
107
|
+
*/
|
|
108
|
+
function isLikelyTestSubject(testAbs, sourceAbs) {
|
|
109
|
+
const testStem = basename(testAbs, extname(testAbs)).replace(TEST_SUFFIX_RE, '')
|
|
110
|
+
const sourceStem = basename(sourceAbs, extname(sourceAbs))
|
|
111
|
+
if (testStem === sourceStem) return true
|
|
112
|
+
if (sourceStem !== 'main' && sourceStem !== 'index') return false
|
|
113
|
+
const sourceDir = dirname(sourceAbs)
|
|
114
|
+
const testRel = relative(sourceDir, testAbs).split(sep).join('/')
|
|
115
|
+
return testStem === basename(sourceDir) || testRel.startsWith('tests/')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Будує один source↔tests index на репозиторій. Зв'язок вважається доведеним
|
|
120
|
+
* лише через relative string literal, що резолвиться у реальний файл.
|
|
121
|
+
* @param {string} root корінь репозиторію
|
|
122
|
+
* @returns {{ root: string, bySource: Map<string, Array<{absPath:string,relPath:string,content:string}>>, byTest: Map<string,string[]> }} source↔tests index
|
|
123
|
+
*/
|
|
124
|
+
export function buildTestEvidenceIndex(root) {
|
|
125
|
+
const normalizedRoot = resolve(root)
|
|
126
|
+
const bySource = new Map()
|
|
127
|
+
const byTest = new Map()
|
|
128
|
+
for (const testAbs of collectTestFiles(normalizedRoot)) {
|
|
129
|
+
let content
|
|
130
|
+
try {
|
|
131
|
+
content = readFileSync(testAbs, 'utf8')
|
|
132
|
+
} catch {
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const relPath = relative(normalizedRoot, testAbs).split(sep).join('/')
|
|
136
|
+
const sources = referencedFiles(testAbs, content).filter(sourceAbs => isLikelyTestSubject(testAbs, sourceAbs))
|
|
137
|
+
byTest.set(testAbs, sources)
|
|
138
|
+
for (const sourceAbs of sources) {
|
|
139
|
+
const tests = bySource.get(sourceAbs) ?? []
|
|
140
|
+
tests.push({ absPath: testAbs, relPath, content })
|
|
141
|
+
bySource.set(sourceAbs, tests)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { root: normalizedRoot, bySource, byTest }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Витягує назви describe/test/it як короткі підтверджені usage-сценарії.
|
|
149
|
+
* `describe` дає контекст групи, а `test`/`it` — приклади поведінки; це дає
|
|
150
|
+
* змогу не перетворювати docs складного модуля на повний список unit-тестів.
|
|
151
|
+
* @param {string} content вміст тесту
|
|
152
|
+
* @returns {{ groups: string[], scenarios: string[] }} унікальні назви у порядку появи
|
|
153
|
+
*/
|
|
154
|
+
function scenarioNames(content) {
|
|
155
|
+
const groups = []
|
|
156
|
+
const scenarios = []
|
|
157
|
+
for (const match of content.matchAll(SCENARIO_RE)) {
|
|
158
|
+
const title = match[2].trim()
|
|
159
|
+
if (!title) continue
|
|
160
|
+
if (match[1] === 'describe') groups.push(title)
|
|
161
|
+
else scenarios.push(title)
|
|
162
|
+
}
|
|
163
|
+
return { groups: [...new Set(groups)], scenarios: [...new Set(scenarios)] }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Формує дані для JS-рендеру сценаріїв і детермінований payload для CRC.
|
|
168
|
+
* Test-код не потрапляє до LLM prompt: опис тестового usage лишається дослівним.
|
|
169
|
+
* @param {string} sourceAbs абсолютний шлях source-файлу
|
|
170
|
+
* @param {ReturnType<typeof buildTestEvidenceIndex>} index source↔tests index
|
|
171
|
+
* @returns {{ files: Array<{path:string,groups:string[],scenarios:string[]}>, crcPayload:string }} сценарії і повний CRC payload
|
|
172
|
+
*/
|
|
173
|
+
export function testEvidenceForSource(sourceAbs, index) {
|
|
174
|
+
const tests = index.bySource.get(resolve(sourceAbs)) ?? []
|
|
175
|
+
if (tests.length === 0) return { files: [], crcPayload: '' }
|
|
176
|
+
|
|
177
|
+
const files = tests.map(test => ({ path: test.relPath, ...scenarioNames(test.content) }))
|
|
178
|
+
const crcPayload = tests.map(test => `\0${test.relPath}\0${test.content}`).join('')
|
|
179
|
+
return { files, crcPayload }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Детерміновано рендерить компактні підтверджені тестами сценарії у Markdown.
|
|
184
|
+
* Назви походять безпосередньо з `describe`/`test`/`it`, тому LLM не може їх
|
|
185
|
+
* перефразувати або додати неіснуючу поведінку; показуємо до пʼяти прикладів,
|
|
186
|
+
* а решту чесно рахуємо, щоб не дублювати весь test-suite у документації.
|
|
187
|
+
* @param {Array<{path:string, groups?:string[], scenarios:string[]}>} files повʼязані test-файли зі сценаріями
|
|
188
|
+
* @returns {string} вміст секції «Сценарії використання» без заголовка
|
|
189
|
+
*/
|
|
190
|
+
export function renderTestScenarios(files) {
|
|
191
|
+
return files
|
|
192
|
+
.filter(test => test.scenarios.length > 0)
|
|
193
|
+
.map(test => {
|
|
194
|
+
const groups = (test.groups ?? []).slice(0, 2).join('; ')
|
|
195
|
+
const examples = test.scenarios.slice(0, 5).join('; ')
|
|
196
|
+
const rest = test.scenarios.length - 5
|
|
197
|
+
const scope = groups ? ` (${groups})` : ''
|
|
198
|
+
const more = rest > 0 ? `; ще ${rest}` : ''
|
|
199
|
+
return `- \`${test.path}\`${scope} — ${examples}${more}`
|
|
200
|
+
})
|
|
201
|
+
.join('\n')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Source-файли, на які посилається конкретний змінений test/spec-файл.
|
|
206
|
+
* @param {string} testAbs абсолютний шлях тесту
|
|
207
|
+
* @param {ReturnType<typeof buildTestEvidenceIndex>} index source↔tests index
|
|
208
|
+
* @returns {string[]} абсолютні source-шляхи
|
|
209
|
+
*/
|
|
210
|
+
export function sourceFilesForTest(testAbs, index) {
|
|
211
|
+
return index.byTest.get(resolve(testAbs)) ?? []
|
|
212
|
+
}
|
package/rules/doc-files/main.mdc
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Файлова документація — обовʼязковий крок задачі (як lint); unified lint surface, concern doc-files/check — детект застарілості за CRC
|
|
2
|
+
description: Файлова документація — обовʼязковий крок задачі (як lint); unified lint surface, concern doc-files/check — детект застарілості за CRC source + повʼязаних тестів і fix-by-default генерація local-only конвеєром (omlx)
|
|
3
3
|
globs: "**/*.{js,mjs,ts,vue,py,rs},**/docs/**"
|
|
4
4
|
alwaysApply: true
|
|
5
5
|
version: '2.0'
|
|
@@ -7,7 +7,37 @@ version: '2.0'
|
|
|
7
7
|
|
|
8
8
|
Кожен кодовий файл (`.js .mjs .ts .vue .py .rs`, крім тестів і `.d.ts`) має **актуальну**
|
|
9
9
|
файлову доку поряд: `<dir>/docs/<stem>.md`. Це **обовʼязковий крок кожної задачі**, нарівні
|
|
10
|
-
з lint. Актуальність детермінується за **CRC**
|
|
10
|
+
з lint. Актуальність детермінується за **CRC evidence** — source + повʼязані test/spec-файли,
|
|
11
|
+
записаним у frontmatter доки. Без повʼязаних тестів CRC лишається CRC самого source.
|
|
12
|
+
|
|
13
|
+
## Тести як підтверджені usage-сценарії
|
|
14
|
+
|
|
15
|
+
Test/spec-файли не отримують власної файлової доки, але доповнюють документацію source-файлу,
|
|
16
|
+
якщо relative reference у тесті (`import`, `require`, dynamic import, `vi.mock` тощо)
|
|
17
|
+
резолвиться саме в цей source і звʼязок підтверджує naming/layout (`foo.test` → `foo` або
|
|
18
|
+
`module/tests/*` → module `main`/`index`). Це відсіює shared test helpers, які тест лише
|
|
19
|
+
використовує. JS-оркестратор бере назви `describe`/`test`/`it` і детерміновано рендерить один
|
|
20
|
+
компактний рядок на test-файл: до двох груп, пʼять дослівних прикладів і точний лічильник решти.
|
|
21
|
+
Test-код і сценарії не передаються
|
|
22
|
+
моделі, тож вона не може їх перефразувати або вигадати нові.
|
|
23
|
+
|
|
24
|
+
Зміна повʼязаного тесту змінює evidence CRC і робить source-доку stale. У per-file режимі
|
|
25
|
+
змінений тест reverse-map-иться до source-файлів, на які він посилається.
|
|
26
|
+
|
|
27
|
+
## Авторські коментарі без LLM
|
|
28
|
+
|
|
29
|
+
Якщо мовний екстрактор знайшов змістовний file header і змістовний опис кожного public API,
|
|
30
|
+
doc-files збирає «Огляд» та «Публічний API» **дослівно**. Це працює для JSDoc (`/** */`),
|
|
31
|
+
rustdoc (`//!`/`///`) і Python docstring-ів. У документ потрапляють також детерміновані
|
|
32
|
+
«Сценарії використання» та «Гарантії поведінки».
|
|
33
|
+
|
|
34
|
+
JS обирає один із трьох режимів:
|
|
35
|
+
|
|
36
|
+
- `comment-only` — змістовний header вже пояснює потік; LLM і semantic judge не запускаються;
|
|
37
|
+
- `comment+behavior` — короткий header/pointer без достатнього API-контракту або явний складний flow у коді: LLM пише лише
|
|
38
|
+
коротку «Поведінку», не може змінити авторські секції, а judge перевіряє тільки цей додаток;
|
|
39
|
+
- `fallback` — header або хоча б один public API не має змістовного коментаря, тому лишається
|
|
40
|
+
звичний LLM-шлях.
|
|
11
41
|
|
|
12
42
|
## Одна команда — unified lint surface
|
|
13
43
|
|
|
@@ -25,13 +55,14 @@ unified lint surface (spec `docs/specs/2026-06-29-unified-lint-surface.md`) і
|
|
|
25
55
|
|
|
26
56
|
## Stale = `missing` ∪ `crc-mismatch`
|
|
27
57
|
|
|
28
|
-
Дока застаріла, якщо її **немає** (`missing`) або `crc(
|
|
58
|
+
Дока застаріла, якщо її **немає** (`missing`) або `crc(source + related tests) ≠ crc` у frontmatter
|
|
29
59
|
(`crc-mismatch`). Це і є `violations`, які повертає `lint(ctx)` концерну.
|
|
30
60
|
|
|
31
|
-
Алгоритм детекту (кандидати, ignore-дерево, CRC, реверс-мапінг
|
|
61
|
+
Алгоритм детекту (кандидати, ignore-дерево, evidence CRC, реверс-мапінг доки/тесту→джерело) — у
|
|
32
62
|
`docgen-scan/main.mjs` / `docgen-crc/main.mjs` / `docgen-ignore/main.mjs`, детектор — у
|
|
33
|
-
`check/main.mjs` (`lint(ctx)`), fix — лише
|
|
34
|
-
лише людинозрозумілий контракт, без
|
|
63
|
+
`docgen-test-context/main.mjs` / `check/main.mjs` (`lint(ctx)`), fix — лише
|
|
64
|
+
`check/fix-worker.mjs` (LLM-регенерація); тут — лише людинозрозумілий контракт, без
|
|
65
|
+
дублювання логіки.
|
|
35
66
|
|
|
36
67
|
T0-штампу CRC для `crc-mismatch` **немає навмисно**: свіжий CRC поверх старого тексту
|
|
37
68
|
назавжди маскує дрейф (CRC-гейт вважає доку актуальною і вона більше не регенерується).
|