Skip to content
This repository was archived by the owner on Sep 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
"автогенерація",
"автогенерованих",
"автогенерується",
"Автодетектовані",
"автозастосування",
"автозастосуванням",
"автозастосунок",
Expand Down Expand Up @@ -218,6 +219,7 @@
"валідаційного",
"валідаційні",
"валідований",
"вантажує",
"вендорені",
"вендоримо",
"вендорингу",
Expand Down Expand Up @@ -478,6 +480,7 @@
"підписочний",
"підписочних",
"підписочні",
"підтеків",
"підходящу",
"підшляховий",
"пінені",
Expand Down
5 changes: 5 additions & 0 deletions npm/.changes/260723-0740.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
bump: patch
section: Fixed
---
doc-files: явна діагностика замість тихого 0 кандидатів, коли задекларований плагін (.n-rules.json) не встановлений у node_modules
25 changes: 11 additions & 14 deletions npm/rules/doc-files/check/docs/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,27 @@ type: JS Module
title: main.mjs
resource: npm/rules/doc-files/check/main.mjs
docgen:
crc: 2af6c6b9
model: omlx/gemma-4-e4b-it-OptiQ-4bit
score: 100
issues: judge:inaccurate:0.99
judgeModel: openai-codex/gpt-5.4-mini
crc: dd3f8c2a
---

## Огляд

Lint-детектор concern-а `doc-files/check`: виявляє застарілі файлові доки (CRC-mismatch, відсутність, деградація) та «сирітські» доки, чиє джерело видалено. Виключно read-only детект — генерація й очистка живуть у `fix-worker.mjs` (docgen), не тут.
Lint-детектор doc-files: знаходить застарілі файлові доки (CRC-mismatch/missing/degraded) і сирітські доки, чиє джерело видалено. Read-only — саму генерацію/очистку виконує `fix-worker.mjs`.

## Поведінка

1. `lint(ctx)`: у delta-режимі (`ctx.files` задано) змінені шляхи зводяться до множини вихідних кодових файлів — для зміненої `docs/*.md`-доки reverse-map-ом знаходиться її джерело у батьківській теці; без `ctx.files` — повний скан репо.
2. Для кожного джерела перевіряється актуальність його доки (`describeFile`/`scanForDocFiles`); застаріла/відсутня дока → порушення з причиною (`reason`) і шляхом джерела.
3. Окремо скануються «сирітські» доки (`scanOrphanedDocs`) — `docs/*.md`, чиє джерело видалено; кожна — порушення `orphaned-doc`.
4. Повертається `LintResult` зі списком порушень — без жодних мутацій.
`lint(ctx)` збирає застарілі доки через `collectStale` (для конкретного набору змінених файлів або, якщо файлів не передано, повним скануванням дерева) і додає для кожної окреме порушення з причиною (`missing`/`crc-mismatch`/`degraded`) та шляхом джерела. Окремо сканує `docs/`-теки на сирітські доки (`scanOrphanedDocs`) — джерело яких більше не існує.

Реверс-мапінг: якщо серед змінених файлів трапляється сама `.md`-дока (а не її джерело), `sourceForDoc` шукає відповідний вихідний файл поруч (той самий basename, легальне розширення) і саме його передає далі в перевірку застарілості — так зміна доки теж тригерить звірку CRC її джерела.

Якщо мапа doc-files-розширень від активних плагінів порожня, а хоча б один плагін явно задекларований у `.n-rules.json`, але не встановлений у `node_modules` — детектор додає `diagnostics`-запис (`level: 'warn'`) з підказкою запустити `bun install`. Це відрізняє "плагін не встановлено" від "усі доки актуальні": без нього обидва випадки виглядають як 0 порушень.

## Публічний API

collectStale — перелік застарілих доків: для `files` (delta) або всього репо (undefined);
lint — детектор застарілих і сирітських файлових доків, повертає `{ violations }`.
- `lint(ctx)` — детектор concern-а `doc-files/check`: повертає `{ violations, diagnostics? }`.

## Гарантії поведінки

- Read-only: не виконує операцій запису (ФС/БД).
- Fail-safe: помилка читання теки джерела дає `null`-reverse-map (джерело пропускається), не виняток.
- Read-only: не пише і не видаляє жодних файлів.
- Перехоплює помилки читання файлової системи (напр. нечитабельна тека) — не пропускає винятків назовні.
- Diagnostics-попередження про невстановлений плагін рендериться лише при `--verbose` на explicit CLI-виклику (`lint doc-files --no-fix --verbose`) — у PostToolUse-хуку (без `verbose`) обчислюється, але ніколи не друкується.
9 changes: 8 additions & 1 deletion npm/rules/doc-files/check/main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join, dirname, basename, extname } from 'node:path'
import { existsSync, readdirSync } from 'node:fs'

import { describeFile, isDocCandidate, isSourceFile, scanForDocFiles, scanOrphanedDocs } from '../docgen-scan/main.mjs'
import { unavailableDocFilesPlugins } from '../docgen-scan/lang-extensions.mjs'

const DOC_MD_RE = /(?:^|\/)docs\/[^/]+\.md$/u

Expand Down Expand Up @@ -95,5 +96,11 @@ export function lint(ctx) {
)
}

return { violations }
const unavailable = unavailableDocFilesPlugins(cwd)
if (unavailable.length === 0) return { violations }

const word = unavailable.length > 1 ? 'плагіни' : 'плагін'
const verb = unavailable.length > 1 ? 'не встановлені' : 'не встановлений'
const message = `doc-files: 0 кандидатів через недоступні плагіни — ${word} ${unavailable.join(', ')} ${verb} у node_modules — запусти bun install`
return { violations, diagnostics: [{ level: 'warn', message }] }
}
36 changes: 22 additions & 14 deletions npm/rules/doc-files/docgen-scan/docs/lang-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,40 @@ type: JS Module
title: lang-extensions.mjs
resource: npm/rules/doc-files/docgen-scan/lang-extensions.mjs
docgen:
crc: 3c5bc28b
model: openai-codex/gpt-5.4-mini
tier: cloud-min
score: 100
issues: judge:inaccurate:0.98
judgeModel: openai-codex/gpt-5.4-mini
crc: 14b28432
model: omlx/gemma-4-e2b-it-4bit
tier: local-min
score: 95
---

## Огляд

Збирає з активних плагінів у репозиторії мапу розширень doc-files і мовні екстрактори для них, щоб інші частини системи могли підготувати обробку файла за доступними плагінними можливостями. `pluginDocFilesExtensions` формує перелік підтримуваних doc-files розширень, `loadDocFilesExtractors` підтягує екстрактори мов із плагінів, а `clearDocFilesLangCache` скидає кеш у межах прогону. Спирається на `.n-rules.json` і `.n-cursor.json` як джерело конфігурації активних плагінів та їхніх правил. Працює fail-safe: биті handler-модулі мовчки пропускає, не кидає винятків назовні, кешує стан у межах прогону.
Огляд: Модуль керує завантаженням та очищенням документів та логіки для перевірок. Забезпечує функціональний потік, який включає отримання мапи розширень, завантаження екстракторів та очищення кешу для тестування.

## Поведінка

- `pluginDocFilesExtensions` — повертає мапу розширень doc-files, які декларують активні плагіни в репозиторії, з урахуванням кешу для поточного прогону.
- `loadDocFilesExtractors` — завантажує мовні екстрактори з handler-модулів плагінів для doc-files і повертає їх за розширеннями; биті модулі мовчки пропускає, тож для таких файлів далі можливий whole-file шлях.
- `clearDocFilesLangCache` — скидає внутрішній кеш мовних розширень і екстракторів, щоб наступний прогін прочитав актуальний стан заново.
Поведінка крос-функціональний потік
pluginDocFilesExtensions повертає мапу розширень задекларованих у конфігах
loadDocFilesExtractors вантажує мовні екстрактори з handler-модулів
unavailableDocFilesPlugins повертає список плагінів, які не встановлені
clearDocFilesLangCache скидає кеші для тестування

## Публічний API

- pluginDocFilesExtensions — збирає з активних плагінів карту розширень для doc-files і тримає її в процесному кеші; порожній результат означає, що жоден плагін не оголосив підтримку.
- loadDocFilesExtractors — підвантажує мовні extractors із plugin handler-модулів для extension-point `doc-files`; якщо модуль зламаний, його тихо пропускає і далі обробляє файл цілком.
- clearDocFilesLangCache — очищає кеші, щоб тести починали з чистого стану.
- pluginDocFilesExtensions — Мапа doc-files-розширень від плагінів для репо (`.rs` → 'Rust Module', …),
з кешем на процес. Порожня мапа — жодний активний плагін їх не декларує.
- loadDocFilesExtractors — Асинхронно вантажить мовні екстрактори з handler-модулів плагінів
(extension-point `doc-files`): default-експорт
`{ id, extensions: string[], extractFacts?, extractUnits? }`.
Битий модуль — мовчазний пропуск (генерація тоді йде whole-file шляхом).
- unavailableDocFilesPlugins — Задекларовані у `.n-rules.json` плагіни, недоступні в `node_modules` — рахується лише
коли мапа doc-files-розширень порожня (інакше принаймні один плагін реально доступний,
шукати "недоступні" немає сенсу — не hot-path concern, рахується лише в рідкісному
порожньому випадку).
- clearDocFilesLangCache — Скидає кеші (для тестів).

## Гарантії поведінки

- Read-only: не виконує операцій запису (ФС/БД).
- Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
- Кешує результати в межах одного прогону.
19 changes: 18 additions & 1 deletion npm/rules/doc-files/docgen-scan/lang-extensions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'

import { getDocFilesExtensions, getHandlers } from '../../../scripts/lib/resolve-plugins.mjs'
import {
getDocFilesExtensions,
getHandlers,
getUnavailableDeclaredPlugins
} from '../../../scripts/lib/resolve-plugins.mjs'

/**
* Мовні розширення doc-files від плагінів (фаза 4 spec lang-plugins-extraction).
Expand Down Expand Up @@ -82,6 +86,19 @@ export async function loadDocFilesExtractors(cwd) {
return map
}

/**
* Задекларовані у `.n-rules.json` плагіни, недоступні в `node_modules` — рахується лише
* коли мапа doc-files-розширень порожня (інакше принаймні один плагін реально доступний,
* шукати "недоступні" немає сенсу — не hot-path concern, рахується лише в рідкісному
* порожньому випадку).
* @param {string} cwd корінь репозиторію
* @returns {string[]} npm-імена задекларованих, але не встановлених плагінів (порожньо — усе гаразд)
*/
export function unavailableDocFilesPlugins(cwd) {
if (Object.keys(pluginDocFilesExtensions(cwd)).length > 0) return []
return getUnavailableDeclaredPlugins(cwd, readPluginsConfigSync(cwd))
}

/** Скидає кеші (для тестів). */
export function clearDocFilesLangCache() {
EXT_CACHE.clear()
Expand Down
110 changes: 110 additions & 0 deletions npm/rules/doc-files/docgen-scan/tests/lang-extensions.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { describe, expect, test } from 'vitest'
import { mkdir, writeFile } from 'node:fs/promises'
import { join } from 'node:path'

import { withTmpDir, installFakeLangJsPlugin } from '../../../../scripts/utils/test-helpers.mjs'
import {
pluginDocFilesExtensions,
unavailableDocFilesPlugins,
loadDocFilesExtractors,
clearDocFilesLangCache
} from '../lang-extensions.mjs'

/**
* Пише фейковий плагін з handler-модулем `doc-files` у node_modules tmp-репо
* і декларує його в `.n-rules.json`.
* @param {string} dir корінь tmp-репо
* @param {string} handlerBody вміст handler-модуля (ESM default export)
*/
async function installFakeHandlerPlugin(dir, handlerBody) {
const pkgRoot = join(dir, 'node_modules', '@x', 'lang-fake')
await mkdir(pkgRoot, { recursive: true })
await writeFile(
join(pkgRoot, 'package.json'),
JSON.stringify({
name: '@x/lang-fake',
version: '0.0.0',
'n-rules': { contributes: { rules: false, handlers: { 'doc-files': './handler.mjs' } } }
})
)
await writeFile(join(pkgRoot, 'handler.mjs'), handlerBody)
await writeFile(join(dir, '.n-rules.json'), JSON.stringify({ plugins: ['@x/lang-fake'] }))
}

describe('pluginDocFilesExtensions', () => {
test('немає .n-rules.json і немає плагінів у node_modules — порожня мапа', async () => {
await withTmpDir(dir => {
expect(pluginDocFilesExtensions(dir)).toEqual({})
})
})

test('встановлений і задекларований плагін — розширення в мапі', async () => {
await withTmpDir(async dir => {
await installFakeLangJsPlugin(dir)
expect(pluginDocFilesExtensions(dir)).toMatchObject({ '.mjs': 'JS Module' })
})
})
})

describe('unavailableDocFilesPlugins', () => {
test('плагін задекларований у .n-rules.json, але не встановлений — потрапляє у список', async () => {
await withTmpDir(async dir => {
await writeFile(join(dir, '.n-rules.json'), JSON.stringify({ plugins: ['@7n/rules-lang-js'] }))
expect(unavailableDocFilesPlugins(dir)).toEqual(['@7n/rules-lang-js'])
})
})

test('плагін встановлений — мапа розширень непорожня, список недоступних порожній', async () => {
await withTmpDir(async dir => {
await installFakeLangJsPlugin(dir)
expect(unavailableDocFilesPlugins(dir)).toEqual([])
})
})

test('немає .n-rules.json взагалі — порожній список (нема що вважати недоступним)', async () => {
await withTmpDir(dir => {
expect(unavailableDocFilesPlugins(dir)).toEqual([])
})
})

test('битий .n-rules.json (невалідний JSON) — трактується як конфіг без плагінів', async () => {
await withTmpDir(async dir => {
await writeFile(join(dir, '.n-rules.json'), '{ not valid json')
expect(unavailableDocFilesPlugins(dir)).toEqual([])
})
})
})

describe('loadDocFilesExtractors', () => {
test('плагін з handler-модулем doc-files — екстрактор доступний за розширенням', async () => {
await withTmpDir(async dir => {
await installFakeHandlerPlugin(
dir,
`export default { id: 'fake', extensions: ['.fake'], extractFacts: () => ({}) }\n`
)
const map = await loadDocFilesExtractors(dir)
expect(map.get('.fake')?.id).toBe('fake')
})
})

test('битий handler-модуль (кидає при імпорті) — мовчазний пропуск', async () => {
await withTmpDir(async dir => {
await installFakeHandlerPlugin(dir, `throw new Error('boom')\n`)
const map = await loadDocFilesExtractors(dir)
expect(map.size).toBe(0)
})
})
})

describe('clearDocFilesLangCache', () => {
test('скидає кеш — наступний виклик перечитує змінений .n-rules.json', async () => {
await withTmpDir(async dir => {
expect(pluginDocFilesExtensions(dir)).toEqual({})
await installFakeLangJsPlugin(dir)
// Без скидання кешу пропозиція лишалась би порожньою (кеш keyed за dir).
expect(pluginDocFilesExtensions(dir)).toEqual({})
clearDocFilesLangCache()
expect(pluginDocFilesExtensions(dir)).toMatchObject({ '.mjs': 'JS Module' })
})
})
})
Loading
Loading