@7n/rules 1.21.0 → 1.23.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/main.md +1 -1
- package/rules/doc-files/check/main.mjs +1 -1
- package/rules/doc-files/docgen-crc/docs/main.md +1 -1
- package/rules/doc-files/docgen-crc/main.mjs +7 -6
- package/rules/doc-files/docgen-extract/docs/main.md +1 -1
- package/rules/doc-files/docgen-extract/main.mjs +0 -196
- package/rules/doc-files/docgen-gen/docs/main.md +1 -1
- package/rules/doc-files/docgen-gen/main.mjs +6 -1
- package/rules/doc-files/docgen-scan/docs/index.md +10 -0
- package/rules/doc-files/docgen-scan/docs/lang-extensions.md +34 -0
- package/rules/doc-files/docgen-scan/docs/main.md +1 -1
- package/rules/doc-files/docgen-scan/lang-extensions.mjs +89 -0
- package/rules/doc-files/docgen-scan/main.mjs +16 -7
- package/rules/doc-files/units/docs/main.md +1 -2
- package/rules/doc-files/units/main.mjs +2 -3
- package/scripts/lib/docs/resolve-plugins.md +1 -1
- package/scripts/lib/resolve-plugins.mjs +30 -4
- package/skills/doc-files/SKILL.md +1 -1
- package/skills/taze/js/docs/orchestrate.md +1 -1
- package/skills/taze/js/orchestrate.mjs +175 -64
- package/rules/doc-files/units-rs/concern.json +0 -3
- package/rules/doc-files/units-rs/docs/main.md +0 -36
- package/rules/doc-files/units-rs/main.mjs +0 -311
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.23.0] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Фаза 4a spec lang-plugins-extraction: doc-files отримав мовний extension-point — розширення (`.rs` → 'Rust Module', `.py` → 'Python Module') декларуються в маніфесті плагіна (`contributes.docFiles.extensions`, синхронно для hot-path hook), екстрактори фактів/юнітів — handler-модулем (`contributes.handlers['doc-files']`, вантажиться лише на шляху генерації). Rust-екстрактори (extractFactsRust + units-rs) виїхали в `@7n/rules-lang-rust`; ядро документує js/mjs/ts/vue вбудовано, .rs/.py — за активним lang-плагіном
|
|
8
|
+
|
|
9
|
+
## [1.22.0] - 2026-07-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- taze: по завершенню переносить зміни з автоствореного worktree назад у вихідне дерево (untracked) і прибирає worktree
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- taze: worktree-only гейт сам створює .worktrees/branch-taze і продовжує там замість throw-and-stop
|
|
18
|
+
|
|
3
19
|
## [1.21.0] - 2026-07-18
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ function sourceForDoc(cwd, docRel) {
|
|
|
25
25
|
return null
|
|
26
26
|
}
|
|
27
27
|
for (const e of entries) {
|
|
28
|
-
if (!e.isFile() || !isSourceFile(e.name)) continue
|
|
28
|
+
if (!e.isFile() || !isSourceFile(e.name, cwd)) continue
|
|
29
29
|
if (basename(e.name, extname(e.name)) !== stem) continue
|
|
30
30
|
const rel = srcDir === '.' ? e.name : `${srcDir}/${e.name}`
|
|
31
31
|
if (isDocCandidate(cwd, rel)) return rel
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import { basename, extname } from 'node:path'
|
|
4
4
|
import { crc32 as zlibCrc32 } from 'node:zlib'
|
|
5
5
|
import { env } from 'node:process'
|
|
6
|
+
import { pluginDocFilesExtensions } from '../docgen-scan/lang-extensions.mjs'
|
|
6
7
|
|
|
7
8
|
/** Поріг degraded: дока зі `score` нижче вважається неякісною. */
|
|
8
9
|
export const QUALITY_THRESHOLD = Number(env.N_CURSOR_DOC_FILES_THRESHOLD ?? 70) || 70
|
|
@@ -66,24 +67,24 @@ export function parseDocFrontmatter(md) {
|
|
|
66
67
|
/** Максимум кодів issues у frontmatter — це маркер, а не повний лог. */
|
|
67
68
|
const MAX_ISSUE_CODES = 8
|
|
68
69
|
|
|
69
|
-
/** OKF type для
|
|
70
|
+
/** OKF type для вбудованих розширень; мовні (`.rs`, `.py`) декларують lang-плагіни. */
|
|
70
71
|
const EXT_TYPES = {
|
|
71
72
|
'.js': 'JS Module',
|
|
72
73
|
'.mjs': 'JS Module',
|
|
73
74
|
'.cjs': 'JS Module',
|
|
74
75
|
'.ts': 'TS Module',
|
|
75
|
-
'.vue': 'Vue Component'
|
|
76
|
-
'.py': 'Python Module',
|
|
77
|
-
'.rs': 'Rust Module'
|
|
76
|
+
'.vue': 'Vue Component'
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
/**
|
|
81
|
-
* OKF `type` для файлу-джерела за
|
|
80
|
+
* OKF `type` для файлу-джерела за розширенням (вбудовані + декларації
|
|
81
|
+
* активних lang-плагінів).
|
|
82
82
|
* @param {string} sourcePath відносний шлях джерела
|
|
83
83
|
* @returns {string} тип концепту
|
|
84
84
|
*/
|
|
85
85
|
function typeForSource(sourcePath) {
|
|
86
|
-
|
|
86
|
+
const ext = extname(sourcePath).toLowerCase()
|
|
87
|
+
return EXT_TYPES[ext] ?? pluginDocFilesExtensions(process.cwd())[ext] ?? 'Source File'
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
/**
|
|
@@ -241,201 +241,6 @@ function extractMarkers(src) {
|
|
|
241
241
|
skips: [...skips]
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
|
|
245
|
-
// ── Rust-екстрактор ──────────────────────────────────────────────────────────
|
|
246
|
-
|
|
247
|
-
// pub fn / pub struct / pub enum / pub trait (та fn із exposure-атрибутом)
|
|
248
|
-
// матчаться у два кроки по trim-нутому рядку — прості регекспи без бектрекінгу:
|
|
249
|
-
// спершу опційний pub(...)-префікс, потім сама декларація
|
|
250
|
-
const RS_PUB_PREFIX_RE = /^pub(?:\([^)]*\))?\s+/
|
|
251
|
-
const RS_ITEM_DECL_RE = /^(?:async\s+)?(?:unsafe\s+)?(fn|struct|enum|trait|type)\s+(\w+)/
|
|
252
|
-
|
|
253
|
-
// fn-декларація у trim-нутому рядку одразу після exposure-атрибута
|
|
254
|
-
const RS_FN_AFTER_ATTR_RE = /^(?:pub\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+/
|
|
255
|
-
|
|
256
|
-
// Будь-яка fn-декларація без pub (для приватних localSymbols)
|
|
257
|
-
const RS_PRIVATE_FN_RE = /^[ \t]*(?:async\s+)?(?:unsafe\s+)?fn\s+(\w+)/
|
|
258
|
-
|
|
259
|
-
// Exposure-атрибути (#[tauri::command] тощо)
|
|
260
|
-
const RS_EXPOSURE_ATTR_RE = /#\[(?:tauri::command|wasm_bindgen|uniffi::export|pyo3::pyfunction|napi)/gm
|
|
261
|
-
|
|
262
|
-
// use crate::module::{A, B} або use std::..;
|
|
263
|
-
const RS_USE_RE = /^[ \t]*use\s+([\w:]+(?:::\{[^}]+\})?(?:::\*)?(?:::\w+)?)\s*;/gm
|
|
264
|
-
|
|
265
|
-
// Файловий запис: fs::write / File::create / remove_file / create_dir / write_all
|
|
266
|
-
const RS_WRITE_RE = /fs::write|File::create|remove_file|create_dir|BufWriter::new|OpenOptions[^;]*\.write\s*\(\s*true/
|
|
267
|
-
|
|
268
|
-
// Raw-SQL запис через sqlx-подібні макро/виклики (query!/query/execute) — DML-
|
|
269
|
-
// keyword одразу після відкриваючої лапки рядкового літералу (той самий
|
|
270
|
-
// мінімальний тег+вміст сигнал, що й для JS tagged-template, SQL_TAGGED_MUTATION_RE)
|
|
271
|
-
const RS_SQL_WRITE_RE = /\b(?:query!?|execute)\s*\(\s*"\s*(?:UPDATE|INSERT|MERGE\s+INTO|DELETE\s+FROM|UPSERT)\b/i
|
|
272
|
-
|
|
273
|
-
// Обробка помилок (але не просто `?`): прості маркери; випадок
|
|
274
|
-
// «match … з Err(-гілкою» — віконним обходом рядків у rsHasMatchWithErrArm
|
|
275
|
-
const RS_CATCH_SIMPLE_RES = [/\.unwrap_or(?:_else|_default)?/, /if\s+let\s+Err\s*\(/, /\.map_err\s*\(/, /\.ok\s*\(\)/]
|
|
276
|
-
const RS_MATCH_KW_RE = /\bmatch\s/
|
|
277
|
-
const RS_ERR_ARM_RE = /\bErr\s*\(/
|
|
278
|
-
|
|
279
|
-
// Функції, що повертають Result або Option
|
|
280
|
-
const RS_RESULT_RE = /->\s*(?:Result|Option)\s*</
|
|
281
|
-
|
|
282
|
-
// Мережа
|
|
283
|
-
const RS_NETWORK_RE = /reqwest|hyper::|TcpStream|UdpSocket|tokio::net/
|
|
284
|
-
|
|
285
|
-
// Кешування
|
|
286
|
-
const RS_CACHE_RE = /\bcache\b|\bCache\b|lazy_static!|OnceCell|OnceLock|DashMap/i
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Чи містить джерело `match`-вираз із `Err(`-гілкою неподалік (вікно 12 рядків).
|
|
290
|
-
* Замінює бектрекінг-вразливу регулярку детермінованим обходом рядків.
|
|
291
|
-
* @param {string[]} srcLines рядки файлу
|
|
292
|
-
* @returns {boolean} true, якщо за `match` слідує `Err(`
|
|
293
|
-
*/
|
|
294
|
-
function rsHasMatchWithErrArm(srcLines) {
|
|
295
|
-
for (let i = 0; i < srcLines.length; i++) {
|
|
296
|
-
if (!RS_MATCH_KW_RE.test(srcLines[i])) continue
|
|
297
|
-
const end = Math.min(i + 12, srcLines.length)
|
|
298
|
-
for (let j = i; j < end; j++) if (RS_ERR_ARM_RE.test(srcLines[j])) return true
|
|
299
|
-
}
|
|
300
|
-
return false
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Видобуває `///` doc-рядки перед рядком `lineIdx` (назад через `#[...]` та пусті рядки).
|
|
305
|
-
* @param {string[]} lines рядки файлу
|
|
306
|
-
* @param {number} lineIdx індекс рядка декларації
|
|
307
|
-
* @returns {string} опис або ''
|
|
308
|
-
*/
|
|
309
|
-
function rsDocBefore(lines, lineIdx) {
|
|
310
|
-
const doc = []
|
|
311
|
-
for (let i = lineIdx - 1; i >= 0; i--) {
|
|
312
|
-
const t = lines[i].trim()
|
|
313
|
-
if (t.startsWith('///')) doc.unshift(t.slice(3).trim())
|
|
314
|
-
else if (t.startsWith('#[') || t.startsWith('#![') || t === '') {
|
|
315
|
-
/* skip */
|
|
316
|
-
} else break
|
|
317
|
-
}
|
|
318
|
-
return doc.join(' ').trim()
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Витягує module-level doc (`//!`) із голови `.rs` файлу.
|
|
323
|
-
* @param {string} src вміст файлу
|
|
324
|
-
* @returns {string} злитий header-текст
|
|
325
|
-
*/
|
|
326
|
-
function rsExtractHeader(src) {
|
|
327
|
-
const headerLines = []
|
|
328
|
-
for (const line of src.split('\n')) {
|
|
329
|
-
const t = line.trim()
|
|
330
|
-
if (t.startsWith('//!')) headerLines.push(t.slice(3).trim())
|
|
331
|
-
else if (t === '' || t.startsWith('//')) continue
|
|
332
|
-
else break
|
|
333
|
-
}
|
|
334
|
-
return headerLines.join(' ').trim()
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Обчислює номери рядків із `fn`, які стають фактично `pub` через exposure-атрибути.
|
|
339
|
-
* @param {string} src вміст файлу
|
|
340
|
-
* @param {string[]} srcLines рядки файлу
|
|
341
|
-
* @returns {Set<number>} індекси exposure-exposed fn-рядків
|
|
342
|
-
*/
|
|
343
|
-
function rsExposedLineSet(src, srcLines) {
|
|
344
|
-
const exposedLineSet = new Set()
|
|
345
|
-
for (const m of src.matchAll(RS_EXPOSURE_ATTR_RE)) {
|
|
346
|
-
// Знаходимо, який рядок містить цей атрибут
|
|
347
|
-
let pos = 0
|
|
348
|
-
for (let li = 0; li < srcLines.length; li++) {
|
|
349
|
-
if (pos + srcLines[li].length >= m.index) {
|
|
350
|
-
// Шукаємо наступний не-атрибутний рядок з fn
|
|
351
|
-
for (let nli = li + 1; nli < Math.min(li + 5, srcLines.length); nli++) {
|
|
352
|
-
const t = srcLines[nli].trim()
|
|
353
|
-
if (t.startsWith('#[') || t === '') continue
|
|
354
|
-
if (RS_FN_AFTER_ATTR_RE.test(t)) exposedLineSet.add(nli)
|
|
355
|
-
break
|
|
356
|
-
}
|
|
357
|
-
break
|
|
358
|
-
}
|
|
359
|
-
pos += srcLines[li].length + 1
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return exposedLineSet
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Збирає публічні items (`pub` + exposure-exposed) `.rs` файлу.
|
|
367
|
-
* @param {string[]} srcLines рядки файлу
|
|
368
|
-
* @param {Set<number>} exposedLineSet індекси exposure-exposed fn-рядків
|
|
369
|
-
* @returns {Array<{name:string, kind:string, desc:string}>} перелік exports
|
|
370
|
-
*/
|
|
371
|
-
function rsCollectExports(srcLines, exposedLineSet) {
|
|
372
|
-
const exports = []
|
|
373
|
-
for (let li = 0; li < srcLines.length; li++) {
|
|
374
|
-
const trimmed = srcLines[li].trimStart()
|
|
375
|
-
const pubM = trimmed.match(RS_PUB_PREFIX_RE)
|
|
376
|
-
const m = (pubM ? trimmed.slice(pubM[0].length) : trimmed).match(RS_ITEM_DECL_RE)
|
|
377
|
-
if (m) {
|
|
378
|
-
const isPub = Boolean(pubM) || exposedLineSet.has(li)
|
|
379
|
-
if (isPub) {
|
|
380
|
-
const desc = rsDocBefore(srcLines, li)
|
|
381
|
-
exports.push({ name: m[2], kind: m[1], desc })
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return exports
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Класифікує `use`-рядки `.rs` файлу на std / external / internal.
|
|
390
|
-
* @param {string} src вміст файлу
|
|
391
|
-
* @returns {{stdlib:string[], external:string[], internal:string[]}} згруповані imports
|
|
392
|
-
*/
|
|
393
|
-
function rsExtractImports(src) {
|
|
394
|
-
const stdlib = new Set()
|
|
395
|
-
const external = new Set()
|
|
396
|
-
for (const m of src.matchAll(RS_USE_RE)) {
|
|
397
|
-
const path = m[1]
|
|
398
|
-
const root = path.split('::', 1)[0]
|
|
399
|
-
if (root === 'std' || root === 'core' || root === 'alloc') stdlib.add(path)
|
|
400
|
-
else external.add(path)
|
|
401
|
-
}
|
|
402
|
-
return { stdlib: [...stdlib], external: [...external], internal: [] }
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Витягує факт-лист для `.rs` файлу.
|
|
407
|
-
* @param {string} src вміст файлу
|
|
408
|
-
* @param {string} relPath відносний шлях
|
|
409
|
-
* @returns {object} факт-лист без `unsupported`
|
|
410
|
-
*/
|
|
411
|
-
function extractFactsRust(src, relPath) {
|
|
412
|
-
const header = rsExtractHeader(src)
|
|
413
|
-
const srcLines = src.split('\n')
|
|
414
|
-
const exposedLineSet = rsExposedLineSet(src, srcLines)
|
|
415
|
-
const exports = rsCollectExports(srcLines, exposedLineSet)
|
|
416
|
-
|
|
417
|
-
// localSymbols — приватні fn (не pub і не exposed) — не документуємо як публічний API
|
|
418
|
-
const localSymbols = []
|
|
419
|
-
for (const line of srcLines) {
|
|
420
|
-
const m = line.match(RS_PRIVATE_FN_RE)
|
|
421
|
-
if (m && exports.every(e => e.name !== m[1])) localSymbols.push(m[1])
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const imports = rsExtractImports(src)
|
|
425
|
-
|
|
426
|
-
// markers
|
|
427
|
-
const markers = {
|
|
428
|
-
readOnly: !RS_WRITE_RE.test(src) && !RS_SQL_WRITE_RE.test(src),
|
|
429
|
-
catchesErrors: RS_CATCH_SIMPLE_RES.some(re => re.test(src)) || rsHasMatchWithErrArm(srcLines),
|
|
430
|
-
returnsFalsyOnFail: RS_RESULT_RE.test(src),
|
|
431
|
-
network: RS_NETWORK_RE.test(src),
|
|
432
|
-
caches: RS_CACHE_RE.test(src),
|
|
433
|
-
skips: []
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return { relPath, lang: 'rs', header, exports, imports, internalSymbols: [], localSymbols, markers }
|
|
437
|
-
}
|
|
438
|
-
|
|
439
244
|
/**
|
|
440
245
|
* Головний екстрактор: код файлу → факт-лист.
|
|
441
246
|
* @param {string} src вміст файлу
|
|
@@ -444,7 +249,6 @@ function extractFactsRust(src, relPath) {
|
|
|
444
249
|
*/
|
|
445
250
|
export function extractFacts(src, relPath) {
|
|
446
251
|
const lang = relPath.split('.').pop()
|
|
447
|
-
if (lang === 'rs') return extractFactsRust(src, relPath)
|
|
448
252
|
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
449
253
|
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
450
254
|
}
|
|
@@ -7,6 +7,7 @@ import { runOneShot } from '@7n/llm-lib/one-shot'
|
|
|
7
7
|
import { startChain } from '@7n/llm-lib/chain'
|
|
8
8
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
9
9
|
import { docPathForSource } from '../docgen-scan/main.mjs'
|
|
10
|
+
import { loadDocFilesExtractors } from '../docgen-scan/lang-extensions.mjs'
|
|
10
11
|
import { extractFacts } from '../docgen-extract/main.mjs'
|
|
11
12
|
import { extractAnchors, anchorTokens } from '../docgen-extract-anchors/main.mjs'
|
|
12
13
|
import { QUALITY_THRESHOLD } from '../docgen-crc/main.mjs'
|
|
@@ -506,7 +507,11 @@ export async function generateDoc(
|
|
|
506
507
|
`docgen pre-send guard: джерело ~${estTokens} токенів > бюджет ${budget} (0.5× контексту) — Prompt too long, skip`
|
|
507
508
|
)
|
|
508
509
|
}
|
|
509
|
-
|
|
510
|
+
// Мовний екстрактор з lang-плагіна (напр. `.rs` у @7n/rules-lang-rust) має
|
|
511
|
+
// пріоритет; вбудований extractFacts покриває JS-екосистему, решта — whole-file.
|
|
512
|
+
const langExtractors = await loadDocFilesExtractors(process.cwd())
|
|
513
|
+
const ext = `.${file.split('.').pop()}`.toLowerCase()
|
|
514
|
+
const facts = langExtractors.get(ext)?.extractFacts?.(src, file) ?? extractFacts(src, file)
|
|
510
515
|
const t0 = Date.now()
|
|
511
516
|
llmMeter = { calls: 0, ms: 0 }
|
|
512
517
|
const chain = chainFactory({ kind: 'doc-generate', unit: facts.relPath, cwd: process.cwd() })
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: npm/rules/doc-files/docgen-scan
|
|
4
|
+
resource: npm/rules/doc-files/docgen-scan/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ----------------------------------------- | --------- |
|
|
9
|
+
| [lang-extensions.mjs](lang-extensions.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: lang-extensions.mjs
|
|
4
|
+
resource: npm/rules/doc-files/docgen-scan/lang-extensions.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 3c5bc28b
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Збирає з активних плагінів у репозиторії мапу розширень doc-files і мовні екстрактори для них, щоб інші частини системи могли підготувати обробку файла за доступними плагінними можливостями. `pluginDocFilesExtensions` формує перелік підтримуваних doc-files розширень, `loadDocFilesExtractors` підтягує екстрактори мов із плагінів, а `clearDocFilesLangCache` скидає кеш у межах прогону. Спирається на `.n-rules.json` і `.n-cursor.json` як джерело конфігурації активних плагінів та їхніх правил. Працює fail-safe: биті handler-модулі мовчки пропускає, не кидає винятків назовні, кешує стан у межах прогону.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `pluginDocFilesExtensions` — повертає мапу розширень doc-files, які декларують активні плагіни в репозиторії, з урахуванням кешу для поточного прогону.
|
|
21
|
+
- `loadDocFilesExtractors` — завантажує мовні екстрактори з handler-модулів плагінів для doc-files і повертає їх за розширеннями; биті модулі мовчки пропускає, тож для таких файлів далі можливий whole-file шлях.
|
|
22
|
+
- `clearDocFilesLangCache` — скидає внутрішній кеш мовних розширень і екстракторів, щоб наступний прогін прочитав актуальний стан заново.
|
|
23
|
+
|
|
24
|
+
## Публічний API
|
|
25
|
+
|
|
26
|
+
- pluginDocFilesExtensions — збирає з активних плагінів карту розширень для doc-files і тримає її в процесному кеші; порожній результат означає, що жоден плагін не оголосив підтримку.
|
|
27
|
+
- loadDocFilesExtractors — підвантажує мовні extractors із plugin handler-модулів для extension-point `doc-files`; якщо модуль зламаний, його тихо пропускає і далі обробляє файл цілком.
|
|
28
|
+
- clearDocFilesLangCache — очищає кеші, щоб тести починали з чистого стану.
|
|
29
|
+
|
|
30
|
+
## Гарантії поведінки
|
|
31
|
+
|
|
32
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
33
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
34
|
+
- Кешує результати в межах одного прогону.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** @see ./docs/lang-extensions.md */
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { getDocFilesExtensions, getHandlers } from '../../../scripts/lib/resolve-plugins.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Мовні розширення doc-files від плагінів (фаза 4 spec lang-plugins-extraction).
|
|
10
|
+
*
|
|
11
|
+
* Розширення (`.rs` → 'Rust Module') декларуються в МАНІФЕСТІ плагіна
|
|
12
|
+
* (`n-rules.contributes.docFiles.extensions`) — hot-path (hook на кожен файл)
|
|
13
|
+
* читає їх синхронно без динамічного import. Екстрактори фактів/юнітів —
|
|
14
|
+
* у handler-модулі (`contributes.handlers['doc-files']`), вантажаться лише
|
|
15
|
+
* на асинхронному шляху генерації.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Кеш ініціалізації на процес: cwd → мапа розширення → тип-мітка. */
|
|
19
|
+
const EXT_CACHE = new Map()
|
|
20
|
+
/** Кеш завантажених екстракторів: cwd → мапа розширення → модуль-екстрактор. */
|
|
21
|
+
const EXTRACTOR_CACHE = new Map()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Синхронно читає `plugins` з `.n-rules.json` (легка версія: лише це поле,
|
|
25
|
+
* без merge/схем — той самий контракт, що `readNRulesConfigLite`, але sync
|
|
26
|
+
* для hot-path).
|
|
27
|
+
* @param {string} cwd корінь репозиторію
|
|
28
|
+
* @returns {{ plugins?: string[] }} конфіг-стаб для resolvePlugins
|
|
29
|
+
*/
|
|
30
|
+
function readPluginsConfigSync(cwd) {
|
|
31
|
+
for (const name of ['.n-rules.json', '.n-cursor.json']) {
|
|
32
|
+
const p = join(cwd, name)
|
|
33
|
+
if (!existsSync(p)) continue
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'))
|
|
36
|
+
return { plugins: parsed.plugins }
|
|
37
|
+
} catch {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Мапа doc-files-розширень від плагінів для репо (`.rs` → 'Rust Module', …),
|
|
46
|
+
* з кешем на процес. Порожня мапа — жодний активний плагін їх не декларує.
|
|
47
|
+
* @param {string} cwd корінь репозиторію
|
|
48
|
+
* @returns {Record<string, string>} розширення → тип-мітка
|
|
49
|
+
*/
|
|
50
|
+
export function pluginDocFilesExtensions(cwd) {
|
|
51
|
+
const cached = EXT_CACHE.get(cwd)
|
|
52
|
+
if (cached) return cached
|
|
53
|
+
const out = getDocFilesExtensions(cwd, readPluginsConfigSync(cwd))
|
|
54
|
+
EXT_CACHE.set(cwd, out)
|
|
55
|
+
return out
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Асинхронно вантажить мовні екстрактори з handler-модулів плагінів
|
|
60
|
+
* (extension-point `doc-files`): default-експорт
|
|
61
|
+
* `{ id, extensions: string[], extractFacts?, extractUnits? }`.
|
|
62
|
+
* Битий модуль — мовчазний пропуск (генерація тоді йде whole-file шляхом).
|
|
63
|
+
* @param {string} cwd корінь репозиторію
|
|
64
|
+
* @returns {Promise<Map<string, { id: string, extractFacts?: (src: string, relPath: string) => object, extractUnits?: (src: string, relPath: string) => Array<object>|null }>>} розширення → екстрактор
|
|
65
|
+
*/
|
|
66
|
+
export async function loadDocFilesExtractors(cwd) {
|
|
67
|
+
const cached = EXTRACTOR_CACHE.get(cwd)
|
|
68
|
+
if (cached) return cached
|
|
69
|
+
const map = new Map()
|
|
70
|
+
for (const handler of getHandlers(cwd, readPluginsConfigSync(cwd), 'doc-files')) {
|
|
71
|
+
try {
|
|
72
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
73
|
+
const mod = await import(pathToFileURL(handler.modulePath).href)
|
|
74
|
+
const extractor = mod.default
|
|
75
|
+
if (!extractor || typeof extractor !== 'object' || !Array.isArray(extractor.extensions)) continue
|
|
76
|
+
for (const ext of extractor.extensions) map.set(ext, extractor)
|
|
77
|
+
} catch {
|
|
78
|
+
/* битий handler — пропускаємо, доки згенеруються whole-file шляхом */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
EXTRACTOR_CACHE.set(cwd, map)
|
|
82
|
+
return map
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Скидає кеші (для тестів). */
|
|
86
|
+
export function clearDocFilesLangCache() {
|
|
87
|
+
EXT_CACHE.clear()
|
|
88
|
+
EXTRACTOR_CACHE.clear()
|
|
89
|
+
}
|
|
@@ -5,9 +5,14 @@ import { execFileSync } from 'node:child_process'
|
|
|
5
5
|
|
|
6
6
|
import { isDocgenIgnored } from '../docgen-ignore/main.mjs'
|
|
7
7
|
import { parseDocFrontmatter, readDocCrc, staleness } from '../docgen-crc/main.mjs'
|
|
8
|
+
import { pluginDocFilesExtensions } from './lang-extensions.mjs'
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Вбудовані кодові розширення, для яких генеруємо документацію. Мовні
|
|
12
|
+
* розширення поза JS-екосистемою (`.rs`, `.py`) декларують lang-плагіни
|
|
13
|
+
* (`n-rules.contributes.docFiles.extensions`) — див. lang-extensions.mjs.
|
|
14
|
+
*/
|
|
15
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.vue'])
|
|
11
16
|
|
|
12
17
|
/** `*.test.*`, `*.spec.*`, `*.stories.*` — тести й Storybook CSF-файли, документувати не треба. */
|
|
13
18
|
const TEST_FILE_RE = /\.(?:test|spec|stories)\.[^.]+$/u
|
|
@@ -24,14 +29,18 @@ function isSystemWideDocsRoot(root) {
|
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
|
-
* Чи є файл кодовим джерелом для документування.
|
|
32
|
+
* Чи є файл кодовим джерелом для документування. З `root` — враховує і
|
|
33
|
+
* розширення активних lang-плагінів; без нього — лише вбудовані JS-екосистемні.
|
|
28
34
|
* @param {string} fileName базове ім'я файлу
|
|
35
|
+
* @param {string} [root] корінь репозиторію (для плагінних розширень)
|
|
29
36
|
* @returns {boolean} true — документуємо; false — пропускаємо
|
|
30
37
|
*/
|
|
31
|
-
export function isSourceFile(fileName) {
|
|
38
|
+
export function isSourceFile(fileName, root) {
|
|
32
39
|
if (fileName.endsWith('.d.ts')) return false
|
|
33
40
|
if (TEST_FILE_RE.test(fileName)) return false
|
|
34
|
-
|
|
41
|
+
const ext = extname(fileName)
|
|
42
|
+
if (SOURCE_EXTENSIONS.has(ext)) return true
|
|
43
|
+
return root !== undefined && ext in pluginDocFilesExtensions(root)
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -55,7 +64,7 @@ export function docPathForSource(sourcePath) {
|
|
|
55
64
|
*/
|
|
56
65
|
export function isDocCandidate(root, relPath) {
|
|
57
66
|
const fileName = posix.basename(relPath)
|
|
58
|
-
if (!isSourceFile(fileName)) return false
|
|
67
|
+
if (!isSourceFile(fileName, root)) return false
|
|
59
68
|
if (isSystemWideDocsRoot(root) && posix.dirname(relPath) === '.') return false
|
|
60
69
|
return !isDocgenIgnored(relPath)
|
|
61
70
|
}
|
|
@@ -203,7 +212,7 @@ export function scanForDocFiles(root) {
|
|
|
203
212
|
if (entry.isDirectory()) {
|
|
204
213
|
if (isDocgenIgnored(relPath, 'dir')) continue
|
|
205
214
|
walk(fullPath)
|
|
206
|
-
} else if (entry.isFile() && isSourceFile(entry.name)) {
|
|
215
|
+
} else if (entry.isFile() && isSourceFile(entry.name, root)) {
|
|
207
216
|
if (isSystemWideDocsRoot(root) && dirname(relPath) === '.') continue
|
|
208
217
|
const sourcePath = relPath.split(sep).join('/')
|
|
209
218
|
if (isDocgenIgnored(sourcePath)) continue
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/doc-files/units/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 8acdefc1
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
tier: local-min
|
|
9
8
|
score: 100
|
|
10
9
|
issues: judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** @see ./docs/units.md */
|
|
2
2
|
|
|
3
3
|
import { extractUnitsJs } from '../units-js/main.mjs'
|
|
4
|
-
import { extractUnitsRs } from '../units-rs/main.mjs'
|
|
5
4
|
|
|
6
5
|
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Мовно-агностичний фасад юніт-шару. Диспатчить за розширенням:
|
|
10
|
-
* js/mjs/ts → oxc AST;
|
|
9
|
+
* js/mjs/ts → oxc AST; інші мови (rs — lang-плагін, extension-point
|
|
10
|
+
* `doc-files`.extractUnits; vue/py) → null (whole-file шлях).
|
|
11
11
|
* @param {string} src вміст файлу
|
|
12
12
|
* @param {string} relPath шлях файлу
|
|
13
13
|
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
|
|
@@ -15,6 +15,5 @@ const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
|
15
15
|
export function extractUnits(src, relPath) {
|
|
16
16
|
const ext = (relPath.split('.').pop() || '').toLowerCase()
|
|
17
17
|
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
|
|
18
|
-
if (ext === 'rs') return extractUnitsRs(src, relPath)
|
|
19
18
|
return null
|
|
20
19
|
}
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: resolve-plugins.mjs
|
|
4
4
|
resource: npm/scripts/lib/resolve-plugins.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 0d197e39
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
|