@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 +10 -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/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/skills/doc-files/SKILL.md +21 -6
|
@@ -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-гейт вважає доку актуальною і вона більше не регенерується).
|