@7n/rules 1.26.0 → 1.27.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 +14 -0
- package/package.json +2 -1
- package/rules/doc-files/docgen-crc/docs/main.md +1 -1
- package/rules/doc-files/docgen-crc/main.mjs +5 -12
- package/rules/doc-files/docgen-gen/docs/main.md +1 -1
- package/rules/doc-files/docgen-gen/main.mjs +12 -4
- package/rules/doc-files/docgen-scan/docs/main.md +2 -2
- package/rules/doc-files/docgen-scan/main.mjs +6 -13
- package/rules/js/eslint/concern.json +1 -0
- package/schemas/concern.json +4 -0
- package/scripts/lib/concern-meta.mjs +14 -1
- package/scripts/lib/docs/concern-meta.md +3 -6
- package/scripts/lib/lint-surface/docs/ladder.md +3 -5
- package/scripts/lib/lint-surface/docs/run-fix.md +3 -2
- package/scripts/lib/lint-surface/ladder.mjs +18 -9
- package/scripts/lib/lint-surface/run-fix.mjs +15 -2
- package/scripts/utils/docs/test-helpers.md +2 -1
- package/skills/doc-files/SKILL.md +1 -1
- package/skills/taze/js/docs/orchestrate.md +1 -1
- package/rules/doc-files/docgen-extract/concern.json +0 -3
- package/rules/doc-files/docgen-extract/docs/main.md +0 -38
- package/rules/doc-files/docgen-extract/main.mjs +0 -275
- package/rules/doc-files/units/concern.json +0 -3
- package/rules/doc-files/units/docs/main.md +0 -31
- package/rules/doc-files/units/main.mjs +0 -19
- package/rules/doc-files/units-js/concern.json +0 -3
- package/rules/doc-files/units-js/docs/main.md +0 -32
- package/rules/doc-files/units-js/main.mjs +0 -141
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.27.0] - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- LLM-ladder: окремий (більший) таймаут cloud-avg (N_CLOUD_AVG_FIX_TIMEOUT_MS, дефолт 180s) та concern-рівневий skipLocalTier (concern.json) — пропуск local-min/local-min-retry для concern-ів, де local-tier емпірично не встигає дати результат (js/eslint увімкнено за реальними даними прогону)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- doc-files: ядро без вбудованих кодових розширень (фаза 5b spec lang-plugins-extraction) — перелік розширень, OKF-типи та мовні екстрактори (факти/юніти) приходять лише з декларацій активних lang-плагінів (contributes.docFiles.extensions + handler doc-files); JS-специфіка (extractFacts, units-js, мапа типів js/mjs/ts/vue) переїхала у @7n/rules-lang-js; без активного lang-плагіна скан не бачить джерел (graceful тиша)
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Червоний lint на main: дубльовані тести мосту auto-worktree (auto-worktree.test.mjs ↔ orchestrate.test.mjs) зібрано у спільний набір `describeAutoWorktreeBridge` (scripts/utils/tests/auto-worktree-suite.mjs); `tests/**` виключено з npm-tarball; `jscpd` (спавниться як `bunx jscpd`) додано в knip ignoreDependencies
|
|
16
|
+
|
|
3
17
|
## [1.26.0] - 2026-07-19
|
|
4
18
|
|
|
5
19
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"!**/*.test.mjs",
|
|
44
44
|
"!**/*_test.rego",
|
|
45
45
|
"!**/test-helpers.mjs",
|
|
46
|
+
"!**/tests/**",
|
|
46
47
|
"!**/fixtures/**",
|
|
47
48
|
"!**/__fixtures__/**"
|
|
48
49
|
],
|
|
@@ -67,24 +67,17 @@ export function parseDocFrontmatter(md) {
|
|
|
67
67
|
/** Максимум кодів issues у frontmatter — це маркер, а не повний лог. */
|
|
68
68
|
const MAX_ISSUE_CODES = 8
|
|
69
69
|
|
|
70
|
-
/** OKF type для вбудованих розширень; мовні (`.rs`, `.py`) декларують lang-плагіни. */
|
|
71
|
-
const EXT_TYPES = {
|
|
72
|
-
'.js': 'JS Module',
|
|
73
|
-
'.mjs': 'JS Module',
|
|
74
|
-
'.cjs': 'JS Module',
|
|
75
|
-
'.ts': 'TS Module',
|
|
76
|
-
'.vue': 'Vue Component'
|
|
77
|
-
}
|
|
78
|
-
|
|
79
70
|
/**
|
|
80
|
-
* OKF `type` для файлу-джерела за розширенням
|
|
81
|
-
*
|
|
71
|
+
* OKF `type` для файлу-джерела за розширенням — лише з декларацій активних
|
|
72
|
+
* lang-плагінів (`contributes.docFiles.extensions`: js/mjs/ts/vue — lang-js,
|
|
73
|
+
* `.rs`/`.py` — lang-rust/lang-python); вбудованих типів у ядрі немає
|
|
74
|
+
* (фаза 5b spec lang-plugins-extraction). Невідоме розширення → 'Source File'.
|
|
82
75
|
* @param {string} sourcePath відносний шлях джерела
|
|
83
76
|
* @returns {string} тип концепту
|
|
84
77
|
*/
|
|
85
78
|
function typeForSource(sourcePath) {
|
|
86
79
|
const ext = extname(sourcePath).toLowerCase()
|
|
87
|
-
return
|
|
80
|
+
return pluginDocFilesExtensions(process.cwd())[ext] ?? 'Source File'
|
|
88
81
|
}
|
|
89
82
|
|
|
90
83
|
/**
|
|
@@ -8,7 +8,6 @@ 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
10
|
import { loadDocFilesExtractors } from '../docgen-scan/lang-extensions.mjs'
|
|
11
|
-
import { extractFacts } from '../docgen-extract/main.mjs'
|
|
12
11
|
import { extractAnchors, anchorTokens } from '../docgen-extract-anchors/main.mjs'
|
|
13
12
|
import { QUALITY_THRESHOLD } from '../docgen-crc/main.mjs'
|
|
14
13
|
import { JUDGE_ENABLED, JUDGE_MODEL, detectRefusalFiller, judgeDoc, judgeFailsDoc } from '../docgen-judge/main.mjs'
|
|
@@ -507,11 +506,20 @@ export async function generateDoc(
|
|
|
507
506
|
`docgen pre-send guard: джерело ~${estTokens} токенів > бюджет ${budget} (0.5× контексту) — Prompt too long, skip`
|
|
508
507
|
)
|
|
509
508
|
}
|
|
510
|
-
//
|
|
511
|
-
//
|
|
509
|
+
// Факт-лист — лише від мовного екстрактора lang-плагіна (js/mjs/ts —
|
|
510
|
+
// lang-js, `.rs` — lang-rust); без екстрактора для розширення — whole-file
|
|
511
|
+
// шлях через `unsupported` (у ядрі вбудованих екстракторів немає, фаза 5b).
|
|
512
512
|
const langExtractors = await loadDocFilesExtractors(process.cwd())
|
|
513
513
|
const ext = `.${file.split('.').pop()}`.toLowerCase()
|
|
514
|
-
const facts = langExtractors.get(ext)?.extractFacts?.(src, file) ??
|
|
514
|
+
const facts = langExtractors.get(ext)?.extractFacts?.(src, file) ?? {
|
|
515
|
+
relPath: file,
|
|
516
|
+
lang: ext.slice(1),
|
|
517
|
+
unsupported: true,
|
|
518
|
+
header: '',
|
|
519
|
+
exports: [],
|
|
520
|
+
imports: {},
|
|
521
|
+
markers: {}
|
|
522
|
+
}
|
|
515
523
|
const t0 = Date.now()
|
|
516
524
|
llmMeter = { calls: 0, ms: 0 }
|
|
517
525
|
const chain = chainFactory({ kind: 'doc-generate', unit: facts.relPath, cwd: process.cwd() })
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/doc-files/docgen-scan/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 8729f94f
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.99
|
|
@@ -15,7 +15,7 @@ docgen:
|
|
|
15
15
|
|
|
16
16
|
## Поведінка
|
|
17
17
|
|
|
18
|
-
isSourceFile визначає, чи є ім'я файлу кодовим джерелом для генерації документації, виключаючи файли тестів і файли декларацій
|
|
18
|
+
isSourceFile визначає, чи є ім'я файлу кодовим джерелом для генерації документації, виключаючи файли тестів і файли декларацій типів; перелік кодових розширень береться ВИКЛЮЧНО з декларацій активних lang-плагінів (`contributes.docFiles.extensions`: js/mjs/ts/vue — lang-js, `.rs`/`.py` — lang-rust/lang-python), у ядрі вбудованих розширень немає (фаза 5b) — без активного lang-плагіна скан не бачить жодного джерела.
|
|
19
19
|
docPathForSource обчислює очікуваний шлях до markdown-документа для заданого кодового джерельного шляху, розміщуючи його у теці `docs` поруч із джерелом.
|
|
20
20
|
isDocCandidate визначає, чи повинен файл підлягати документуванню, перевіряючи його тип, статус тесту, статус ігнорування та чи не є він частиною системних документації.
|
|
21
21
|
describeFile описує кодовий файл, надаючи шлях джерела та шлях до відповідного документа, а також статус його застарілості за CRC. Якщо док-файл існує, але без `docgen:`-CRC у frontmatter (рукописна дока), файл позначається `foreign: true` і НЕ вважається застарілим — людський зміст рахується чинною документацією і мовчки не перезаписується (перезапис лише explicit `--overwrite` у batch-CLI).
|
|
@@ -7,13 +7,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
9
|
|
|
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'])
|
|
16
|
-
|
|
17
10
|
/** `*.test.*`, `*.spec.*`, `*.stories.*` — тести й Storybook CSF-файли, документувати не треба. */
|
|
18
11
|
const TEST_FILE_RE = /\.(?:test|spec|stories)\.[^.]+$/u
|
|
19
12
|
|
|
@@ -29,18 +22,18 @@ function isSystemWideDocsRoot(root) {
|
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
|
-
* Чи є файл кодовим джерелом для документування.
|
|
33
|
-
*
|
|
25
|
+
* Чи є файл кодовим джерелом для документування. Розширення декларують ЛИШЕ
|
|
26
|
+
* активні lang-плагіни (`n-rules.contributes.docFiles.extensions` — js/mjs/ts/vue
|
|
27
|
+
* дає `@7n/rules-lang-js`, .rs/.py — lang-rust/lang-python); у ядрі вбудованих
|
|
28
|
+
* розширень немає (фаза 5b spec lang-plugins-extraction).
|
|
34
29
|
* @param {string} fileName базове ім'я файлу
|
|
35
|
-
* @param {string}
|
|
30
|
+
* @param {string} root корінь репозиторію (джерело плагінних розширень)
|
|
36
31
|
* @returns {boolean} true — документуємо; false — пропускаємо
|
|
37
32
|
*/
|
|
38
33
|
export function isSourceFile(fileName, root) {
|
|
39
34
|
if (fileName.endsWith('.d.ts')) return false
|
|
40
35
|
if (TEST_FILE_RE.test(fileName)) return false
|
|
41
|
-
|
|
42
|
-
if (SOURCE_EXTENSIONS.has(ext)) return true
|
|
43
|
-
return root !== undefined && ext in pluginDocFilesExtensions(root)
|
|
36
|
+
return extname(fileName) in pluginDocFilesExtensions(root)
|
|
44
37
|
}
|
|
45
38
|
|
|
46
39
|
/**
|
package/schemas/concern.json
CHANGED
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
],
|
|
20
20
|
"description": "Маршрутизація fix-движка. Усі мітки виконують детерміновану фазу T0; різниця — що після неї. `code` (дефолт, поле можна опускати) — джерельні порушення, які LLM може осмислено переписати → повна pi-agent-ladder (local→cloud) до чистого re-detect. `config` — canon/settings/tooling конфіг з єдиною правильною формою (jscpd/oxfmtrc/tooling/package_json/*_yml): фікс детермінований (T0/regen з канону), LLM лише вгадав би невалідне → після T0 fail-fast без ladder. `structural` — дублікати/розташування/структура (jscpd_duplicates/test-location/package-structure): авто-правка ризикована й потребує людського розсуду → після T0 fail-fast без ladder. config vs structural різняться мотивом пропуску (детермінований канон vs надто ризиковано) — для телеметрії й майбутнього routing."
|
|
21
21
|
},
|
|
22
|
+
"skipLocalTier": {
|
|
23
|
+
"type": "boolean",
|
|
24
|
+
"description": "Дефолт `false` (поле можна опускати). `true` пропускає local-min/local-min-retry rung-и LLM-ladder-а для цього concern-а — перша спроба одразу йде на cloud-min. Для concern-ів, де local-tier (слабка локальна модель, короткий бюджет) емпірично майже завжди лише витрачає час rung-а без результату, перш ніж ladder однаково ескалює далі."
|
|
25
|
+
},
|
|
22
26
|
"check": {
|
|
23
27
|
"type": "boolean",
|
|
24
28
|
"const": true,
|
|
@@ -34,6 +34,10 @@ import { join } from 'node:path'
|
|
|
34
34
|
* @property {PolicySurface|undefined} policy policy-поверхня concern-а (rego/template) або undefined.
|
|
35
35
|
* @property {LintSurface|undefined} lint lint-поверхня concern-а або undefined.
|
|
36
36
|
* @property {Fixability} fixability маршрутизація fix-движка (дефолт `code`): `config`/`structural` пропускають LLM-ladder.
|
|
37
|
+
* @property {boolean} skipLocalTier дефолт `false`: `true` пропускає local-min/local-min-retry rung-и
|
|
38
|
+
* ladder-а — ladder одразу стартує з cloud-min. Для concern-ів, де local-tier емпірично не встигає
|
|
39
|
+
* дати результат у межах свого бюджету (напр. js/eslint — 0/12 успіхів у реальному прогоні, лише
|
|
40
|
+
* витрачений час).
|
|
37
41
|
*/
|
|
38
42
|
|
|
39
43
|
/**
|
|
@@ -119,7 +123,16 @@ export async function readConcernMeta(concernDir, name) {
|
|
|
119
123
|
? raw.requires.capability
|
|
120
124
|
: undefined
|
|
121
125
|
|
|
122
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
name,
|
|
128
|
+
dir: concernDir,
|
|
129
|
+
check,
|
|
130
|
+
policy,
|
|
131
|
+
lint,
|
|
132
|
+
requiresCapability,
|
|
133
|
+
fixability: parseFixability(raw.fixability),
|
|
134
|
+
skipLocalTier: raw.skipLocalTier === true
|
|
135
|
+
}
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
/**
|
|
@@ -3,12 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: concern-meta.mjs
|
|
4
4
|
resource: npm/scripts/lib/concern-meta.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
tier: local-min
|
|
9
|
-
score: 100
|
|
10
|
-
issues: judge:inaccurate:0.97
|
|
11
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
6
|
+
crc: ccc325b1
|
|
7
|
+
model: manual
|
|
12
8
|
---
|
|
13
9
|
|
|
14
10
|
## Огляд
|
|
@@ -21,6 +17,7 @@ docgen:
|
|
|
21
17
|
readConcernMeta зчитує і перевіряє файл concern.json у вказаній директорії concern-а, повертаючи метадані або null, якщо файл відсутній чи не валідний.
|
|
22
18
|
listConcerns сканує директорію правил і повертає список усіх знайдених concern-ів у алфавітному порядку, ігноруючи каталоги без concern.json.
|
|
23
19
|
Нормалізований meta несе `fixability` (`code`|`config`|`structural`); невідоме/відсутнє значення зводиться до `code` — дефолт, за яким concern лишається eligible для LLM-fix-ladder.
|
|
20
|
+
Нормалізований meta несе також `skipLocalTier` (boolean, дефолт `false`): `true` — concern пропускає local-min/local-min-retry rung-и LLM-ladder-а, перша спроба одразу йде на cloud-min. Для concern-ів, де local-tier емпірично майже завжди лише витрачає бюджет rung-а без результату (напр. `js/eslint`).
|
|
24
21
|
|
|
25
22
|
## Публічний API
|
|
26
23
|
|
|
@@ -3,10 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: ladder.mjs
|
|
4
4
|
resource: npm/scripts/lib/lint-surface/ladder.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.97
|
|
6
|
+
crc: f02de1a2
|
|
7
|
+
model: manual
|
|
10
8
|
---
|
|
11
9
|
|
|
12
10
|
## Огляд
|
|
@@ -17,7 +15,7 @@ docgen:
|
|
|
17
15
|
|
|
18
16
|
- `DEFAULT_MAX_AVG` задає типовий ліміт звернень до середнього cloud-рівня за один прогін, щоб ескалація не витрачала надмірно дорогий ресурс.
|
|
19
17
|
- `buildLadder` формує послідовність рівнів виправлення від локального мінімального до cloud-avg і відкидає недоступні рівні без моделі.
|
|
20
|
-
- Кожен rung несе `timeoutMs` — per-tier таймаут виклику (ADR 260620-0556): локальні рівні 45s,
|
|
18
|
+
- Кожен rung несе `timeoutMs` — per-tier таймаут виклику (ADR 260620-0556): локальні рівні 45s, cloud-min 120s, cloud-avg 180s (окремий, більший дефолт — реальний прогін 2026-07-18 показав, що cloud-avg регулярно доводить concern майже до чистого re-detect, але спільний з cloud-min бюджет не лишав часу на verify, а наступного rung-а для повторної спроби нема); override без зміни коду — env `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS` / `N_CLOUD_AVG_FIX_TIMEOUT_MS` (мс на ОДИН rung відповідного класу; невалідне значення → дефолт). Runner прокидає його worker-у через `FixContext`, щоб зависла LLM-сесія переривалась (runner додатково тримає backstop ×1.25), а ladder рухався далі. Це робочий важіль для повільної локальної моделі чи великої черги batch-концерну (doc-files ріже беклог під цей ліміт м'яким дедлайном).
|
|
21
19
|
- `classifyFixError` визначає характер помилки виправлення: системна причина, транспортний збій або якісна невдача агента.
|
|
22
20
|
- `decideAfterFailure` вирішує, чи продовжувати ескалацію після невдалого рівня, чи пропустити локальну модель, чи зупинити процес.
|
|
23
21
|
|
|
@@ -3,8 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: run-fix.mjs
|
|
4
4
|
resource: npm/scripts/lib/lint-surface/run-fix.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
6
|
+
crc: 45965f7d
|
|
7
|
+
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## Огляд
|
|
@@ -23,6 +23,7 @@ Durable-write-и (issue nitra/cursor#16): worker отримує у `FixContext`
|
|
|
23
23
|
MT-tail (Фаза B, спека 2026-07-11): коли лишився невиправлений хвіст (worst=1), `renderRemaining` повертає зібрані порушення, і вони матеріалізуються у вузли MT-графа через `materializeTail` (mt-tail.mjs). Єдиний гейт — onboarded-репо (наявність `.mt.json`); fail-open: MT недоступний або будь-яка помилка → лог, lint не падає.
|
|
24
24
|
Distillation-телеметрія (Фаза C, §13 pi-migration): успішний agentic-рунг (canonical clean, без veto, з реальними правками у worker telemetry) пише запис `oldText→newText` у глобальний стор (`recordFixTelemetry`, `~/.n-rules/telemetry/<rule>/open/`) — корпус для маховика дистиляції T0. Best-effort; T0/ручні фікси не пишуться.
|
|
25
25
|
Rollback на провалі re-detect-а: якщо canonical re-detect усередині rung-а сам кидає виняток (worker/LLM лишив файл синтаксично невалідним — детектор/conftest не може його розпарсити), `runRung` спершу відкочує `snapshot` до S1, і лише потім перекидає виняток далі — без цього зіпсований проміжний стан worker-а лишався б на диску назавжди (виняток абортує весь прогін до звичайного rollback-коду).
|
|
26
|
+
skipLocalTier (concern-meta.mjs): `selectLadder` перед циклом ladder-а фільтрує з нього local-min/local-min-retry rung-и, якщо `item.entry.concern.skipLocalTier === true` — перша спроба одразу йде на cloud-min. Для concern-ів, де local-tier емпірично майже завжди лише витрачає бюджет rung-а без результату (виявлено на реальному прогоні 2026-07-18: 0/12 успіхів local-tier для `js/eslint`).
|
|
26
27
|
|
|
27
28
|
## Публічний API
|
|
28
29
|
|
|
@@ -9,16 +9,25 @@ import { env } from 'node:process'
|
|
|
9
9
|
// об'єктивно не закінчить важкий промпт за хвилини (curl 28), хмарний SSE без
|
|
10
10
|
// таймауту здатен висіти годинами на ESTABLISHED TCP — драбина має рухатись далі.
|
|
11
11
|
//
|
|
12
|
-
// Override без зміни коду — env `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS
|
|
13
|
-
// мілісекунди на ОДИН rung відповідного класу
|
|
14
|
-
//
|
|
15
|
-
// abort LLM-виклику; batch-worker-и, як doc-files, ріжуть
|
|
16
|
-
// дедлайном), а runner додатково тримає backstop ×1.25 навколо
|
|
17
|
-
// Робочий важіль для повільної локальної моделі чи великої черги
|
|
18
|
-
// підняти локальний таймаут понад вартість одного файлу. Невалідне
|
|
19
|
-
// (NaN/0/порожньо) → дефолт.
|
|
12
|
+
// Override без зміни коду — env `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS` /
|
|
13
|
+
// `N_CLOUD_AVG_FIX_TIMEOUT_MS`: мілісекунди на ОДИН rung відповідного класу
|
|
14
|
+
// (local-min/local-min-retry, cloud-min, cloud-avg). Значення потрапляє worker-у як
|
|
15
|
+
// `ctx.timeoutMs` (внутрішній abort LLM-виклику; batch-worker-и, як doc-files, ріжуть
|
|
16
|
+
// під нього беклог м'яким дедлайном), а runner додатково тримає backstop ×1.25 навколо
|
|
17
|
+
// всього worker-виклику. Робочий важіль для повільної локальної моделі чи великої черги
|
|
18
|
+
// batch-концерну: підняти локальний таймаут понад вартість одного файлу. Невалідне
|
|
19
|
+
// значення (NaN/0/порожньо) → дефолт.
|
|
20
|
+
//
|
|
21
|
+
// cloud-avg має ОКРЕМИЙ (більший за cloud-min) дефолт: реальний прогін
|
|
22
|
+
// (2026-07-18, /ai run/yoga2, chainId 6f6b4fdca71aa0c5) показав, що cloud-avg
|
|
23
|
+
// регулярно доводить concern до 1 залишкового порушення в межах спільного з
|
|
24
|
+
// cloud-min бюджету, але verify (canonical re-detect) не встигає підтвердитись —
|
|
25
|
+
// і весь прогрес відкочується, бо після cloud-avg немає наступного rung-а для
|
|
26
|
+
// повторної спроби. cloud-avg — останній шанс ladder-а (і під DEFAULT_MAX_AVG-кепом),
|
|
27
|
+
// тож дорожчий за нього бюджет виправдано менш економний, ніж cloud-min.
|
|
20
28
|
const LOCAL_TIMEOUT_MS = Number(env.N_LOCAL_FIX_TIMEOUT_MS) || 45_000
|
|
21
29
|
const CLOUD_TIMEOUT_MS = Number(env.N_CLOUD_FIX_TIMEOUT_MS) || 120_000
|
|
30
|
+
const CLOUD_AVG_TIMEOUT_MS = Number(env.N_CLOUD_AVG_FIX_TIMEOUT_MS) || 180_000
|
|
22
31
|
|
|
23
32
|
/** Дефолтний кеп на виклики cloud-avg за прогін (щоб ladder на N concern-ів не спалив avg). */
|
|
24
33
|
export const DEFAULT_MAX_AVG = 3
|
|
@@ -59,7 +68,7 @@ export function buildLadder({ localMin, cloudMin, cloudAvg }) {
|
|
|
59
68
|
timeoutMs: LOCAL_TIMEOUT_MS
|
|
60
69
|
},
|
|
61
70
|
{ tier: 'cloud-min', model: cloudMin, feedback: true, local: false, isAvg: false, timeoutMs: CLOUD_TIMEOUT_MS },
|
|
62
|
-
{ tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true, timeoutMs:
|
|
71
|
+
{ tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true, timeoutMs: CLOUD_AVG_TIMEOUT_MS }
|
|
63
72
|
].filter(r => r.model)
|
|
64
73
|
}
|
|
65
74
|
|
|
@@ -440,6 +440,18 @@ function noteT0Phase(t0, chainExtra, touchedAbs) {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
/**
|
|
444
|
+
* Ladder concern-а: повний, або без local-tier rung-ів (`concern.skipLocalTier`) —
|
|
445
|
+
* concern-и, де local-min/local-min-retry емпірично не встигають дати результат
|
|
446
|
+
* у межах свого бюджету (concern-meta.mjs).
|
|
447
|
+
* @param {Rung[]} ladder Повний ladder pipeline-у.
|
|
448
|
+
* @param {import('../concern-meta.mjs').ConcernMeta} concern Concern-meta елемента плану.
|
|
449
|
+
* @returns {Rung[]} Ladder, застосовний до цього concern-а.
|
|
450
|
+
*/
|
|
451
|
+
function selectLadder(ladder, concern) {
|
|
452
|
+
return concern.skipLocalTier ? ladder.filter(rung => !rung.local) : ladder
|
|
453
|
+
}
|
|
454
|
+
|
|
443
455
|
/**
|
|
444
456
|
* Тіло fix-pipeline одного concern-а (T0 → S1 → ladder); chain/chainExtra — акумулятори
|
|
445
457
|
* телеметрії ланцюжка (володіє ними fixConcern-обгортка).
|
|
@@ -481,7 +493,8 @@ async function fixConcernCore(item, initialViolations, deps, chain, chainExtra,
|
|
|
481
493
|
|
|
482
494
|
// ── Worker ladder ── concern-specific fix-worker.mjs, інакше дефолтний pi-agent worker.
|
|
483
495
|
const worker = await resolveWorker(concernDir, deps.workerOverride)
|
|
484
|
-
|
|
496
|
+
const effectiveLadder = selectLadder(ladder, item.entry.concern)
|
|
497
|
+
if (!worker || effectiveLadder.length === 0) {
|
|
485
498
|
chainExtra.stop = 'no-worker'
|
|
486
499
|
return false
|
|
487
500
|
}
|
|
@@ -492,7 +505,7 @@ async function fixConcernCore(item, initialViolations, deps, chain, chainExtra,
|
|
|
492
505
|
let violations = initialViolations
|
|
493
506
|
const skipModels = new Set()
|
|
494
507
|
|
|
495
|
-
for (const rung of
|
|
508
|
+
for (const rung of effectiveLadder) {
|
|
496
509
|
if (skipModels.has(rung.model)) continue
|
|
497
510
|
if (rung.isAvg && deps.avgRemaining() <= 0) {
|
|
498
511
|
log(` ⏭️ ${ruleId}/${concernName}: ${rung.tier} пропущено (avg-кеп вичерпано)\n`)
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: test-helpers.mjs
|
|
4
4
|
resource: npm/scripts/utils/test-helpers.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: a01017b0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Огляд
|
|
@@ -18,6 +18,7 @@ docgen:
|
|
|
18
18
|
- `withBinStubInPath(bin, fn)` — створює тимчасовий каталог зі стабом `<bin>` (`<bin>.exe` на Windows), що завершується з кодом 0, додає каталог на початок `PATH` на час `fn`, потім відновлює `PATH` і прибирає стаб. Реальний бінарник не запускається: і машини без інструмента, і повільні мережево-залежні інструменти (наприклад, `kubescape`, який на старті тягне артефакти з хмарних API десятки секунд) отримують детермінований швидкий прогін.
|
|
19
19
|
- `withShellcheckStubInPath(fn)` — спеціалізація `withBinStubInPath` для `shellcheck` (перевірки `check ga` на машинах без реального shellcheck).
|
|
20
20
|
- `withBinRemovedFromPath(bin, fn)` — виконує `fn` із `PATH`, з якого прибрані всі каталоги з виконуваним `<bin>`; решта `PATH` (git, bun) лишається. На час `fn` виставляє `N_CURSOR_NO_AUTO_INSTALL=1`, щоб `ensureTool` не запускав реальний brew/scoop/curl-install. Для негативних тестів «fail, коли інструмента нема».
|
|
21
|
+
- `installFakeLangJsPlugin(dir)` — кладе у tmp-репо фейковий `@7n/rules-lang-js` (маніфест із doc-files-розширеннями js/mjs/ts/vue у `node_modules`) і активує його через `.n-rules.json`. Після фази 5b ядро не має вбудованих кодових розширень — тестам doc-files без цього хелпера скан не бачить жодного джерела.
|
|
21
22
|
|
|
22
23
|
## Гарантії поведінки
|
|
23
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doc-files
|
|
3
3
|
description: >-
|
|
4
|
-
Обовʼязковий крок задачі (як lint): для кожного зміненого/нового кодового файлу (js/mjs/ts/vue
|
|
4
|
+
Обовʼязковий крок задачі (як lint): для кожного зміненого/нового кодового файлу (розширення декларують lang-плагіни: js/mjs/ts/vue — lang-js, rs/py — lang-rust/lang-python) JS-оркестрована генерація лаконічної поведінкової української md-документації у теку docs/ поряд із кодом, зі звіркою застарілості за CRC у frontmatter
|
|
5
5
|
version: '1.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: main.mjs
|
|
4
|
-
resource: npm/rules/doc-files/docgen-extract/main.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: a9332a80
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.99
|
|
10
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Огляд
|
|
14
|
-
|
|
15
|
-
Екстрагує факти за допомогою функції `extractFacts`, звертаючись до мережі та використовуючи кешування в межах одного прогону. При цьому ігнорує каталоги, такі як `.github`, `.git`, `node_modules`, `base/`, `ua/` та `.firebase`.
|
|
16
|
-
|
|
17
|
-
## Поведінка
|
|
18
|
-
|
|
19
|
-
1. Аналізує вміст коду для створення детального набору фактів про файл.
|
|
20
|
-
2. Визначає словник і визначає, чи є код написаний на JavaScript, TypeScript або TypeScript (ESM).
|
|
21
|
-
3. Витягує загальний опис файлу, якщо він присутній у верхньому блоці коментарів.
|
|
22
|
-
4. Визначає всі функції, які є публічно доступними (експортованими), разом із їхніми описами та параметрами.
|
|
23
|
-
5. Класифікує імпорти на внутрішні, сторонні (npm) та вбудовані (stdlib).
|
|
24
|
-
6. Визначає, чи є код:
|
|
25
|
-
- Стійким до винятків (catchesErrors).
|
|
26
|
-
- Здатним працювати без доступу до мережі (readOnly).
|
|
27
|
-
- Використовуючи механізми кешування (caches).
|
|
28
|
-
- Згодом повертаючи хибні значення у випадку помилки (returnsFalsyOnFail).
|
|
29
|
-
7. Ігнорує шляхи, що відповідають шаблонам: .github, .git, node_modules, base/, ua/, .firebase.
|
|
30
|
-
|
|
31
|
-
## Публічний API
|
|
32
|
-
|
|
33
|
-
extractFacts — витягує з коду файлу список значущих тверджень (фактів).
|
|
34
|
-
|
|
35
|
-
## Гарантії поведінки
|
|
36
|
-
|
|
37
|
-
- Кешує результати в межах одного прогону.
|
|
38
|
-
- Свідомо пропускає шляхи: `.github`, `.git`, `node_modules`, `base/`, `ua/`, `.firebase`.
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/docgen-extract.md */
|
|
2
|
-
|
|
3
|
-
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
4
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
const BUILTIN_MODULES = new Set([
|
|
7
|
-
'fs',
|
|
8
|
-
'path',
|
|
9
|
-
'crypto',
|
|
10
|
-
'os',
|
|
11
|
-
'util',
|
|
12
|
-
'stream',
|
|
13
|
-
'events',
|
|
14
|
-
'http',
|
|
15
|
-
'https',
|
|
16
|
-
'url',
|
|
17
|
-
'child_process',
|
|
18
|
-
'process',
|
|
19
|
-
'assert',
|
|
20
|
-
'buffer',
|
|
21
|
-
'zlib',
|
|
22
|
-
'readline'
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
26
|
-
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
27
|
-
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
28
|
-
const PARAM_LINE_RE = /^@param[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?\[?([\w.]{1,80})\]?[ \t]{0,8}(.{0,400})$/
|
|
29
|
-
const RETURNS_LINE_RE = /^@returns?[ \t]{1,8}(?:\{[^}]{0,200}\}[ \t]{1,8})?(.{0,400})$/
|
|
30
|
-
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
|
|
31
|
-
const PRECEDING_JSDOC_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
32
|
-
const EXPORT_DECL_RE = /export\s+(?:async\s+)?(function|const|class)\s+(\w+)/g
|
|
33
|
-
// Top-level function/class декларації (колонка 0) — для R6: службові функції,
|
|
34
|
-
// які не експортуються, не мають протікати у Поведінку/API як «публічні».
|
|
35
|
-
const TOP_FN_DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class)\s+(\w+)/gm
|
|
36
|
-
const IMPORT_FROM_RE = /^import[ \t]{1,8}[\s\S]{0,300}?from\s{1,8}['"]([^'"]+)['"]/gm
|
|
37
|
-
const NODE_PREFIX_RE = /^node:/
|
|
38
|
-
const INTERNAL_IMPORT_RE = /import[ \t]{1,8}([^'"]{0,300}?)from[ \t]{1,8}['"]\.[^'"]{1,300}['"]/g
|
|
39
|
-
const NAMED_BRACES_RE = /\{([^}]{1,400})\}/
|
|
40
|
-
const IDENT_RE = /^[\w$]{1,80}$/
|
|
41
|
-
const IMPORT_AS_RE = /[ \t]{1,8}as[ \t]{1,8}.{0,200}/
|
|
42
|
-
const WRITE_FS_RE = /\b(writeFile|mkdir|rmdir|unlink|appendFile|createWriteStream|rm\()/
|
|
43
|
-
const CATCH_RE = /catch\s*\(/
|
|
44
|
-
const TRY_RE = /\btry\s*\{/
|
|
45
|
-
// Falsy-return як «fail-safe» — лише коли воно в catch/error-гілці (інакше це
|
|
46
|
-
// звичайний guard `if (!x) return null`, не обробка помилки). Уникає over-claim.
|
|
47
|
-
const FALSY_RETURN_RE = /catch[\s\S]{0,400}?return\s+(false|null|''|"")/
|
|
48
|
-
// Мережа: окрім явного fetch/http, ловимо абстраговані клієнти (graphql/db/rpc/
|
|
49
|
-
// octokit/.request/.query). Хибний false-negative тут = небезпечна гарантія
|
|
50
|
-
// «без мережі», тож свідомо схиляємось до over-detection (м'якший бік помилки).
|
|
51
|
-
const NETWORK_RE =
|
|
52
|
-
/\bfetch\(|https?:\/\/|\bhttps?\.|axios|\bgot\(|graphql|\.request\(|\.query\(|\.mutate\(|octokit|node-fetch|undici|\bgrpc\b|websocket/i
|
|
53
|
-
// Будь-який `throw` назовні → НЕ можна гарантувати «fail-safe / без винятків».
|
|
54
|
-
const THROW_RE = /\bthrow\s/
|
|
55
|
-
// Запис у БД / зовнішню мутацію → НЕ read-only (навіть якщо нема ФС-запису).
|
|
56
|
-
// Розбито на кілька простіших патернів (та сама семантика через OR у `isMutation`),
|
|
57
|
-
// щоб уникнути надмірної складності одного великого regex.
|
|
58
|
-
const MUTATION_CALL_RE = /\b(insert|update|delete|upsert|drop|destroy|save)[A-Za-z]*\s*[(,]/
|
|
59
|
-
const MUTATION_NAME_RE = /[Mm]utation\b|\bmut[A-Z]\w*/
|
|
60
|
-
const MUTATION_METHOD_RE = /\.(save|create|update|delete|insert|destroy|mutate)\(/
|
|
61
|
-
// Raw-SQL tagged-template виклики (напр. `pgWrite\`UPDATE ...\``) — DML-ключове
|
|
62
|
-
// слово стоїть на початку тіла шаблону, не перед `(`, тож JS-орієнтовані
|
|
63
|
-
// патерни вище його не ловлять. Сигнал мінімальний, але навмисний: тег-функція
|
|
64
|
-
// (ідентифікатор впритул перед `` ` ``) + DML-keyword одразу після відкриття —
|
|
65
|
-
// уникає false positive на звичайних рядках/коментарях, де немає теg-виклику.
|
|
66
|
-
const SQL_TAGGED_MUTATION_RE = /\b\w+`\s*(?:UPDATE|INSERT|MERGE\s+INTO|DELETE\s+FROM|UPSERT)\b/i
|
|
67
|
-
/**
|
|
68
|
-
* @param {string} src вміст файлу
|
|
69
|
-
* @returns {boolean} чи є ознаки мутації БД / зовнішнього стану
|
|
70
|
-
*/
|
|
71
|
-
const isMutation = src =>
|
|
72
|
-
MUTATION_CALL_RE.test(src) ||
|
|
73
|
-
MUTATION_NAME_RE.test(src) ||
|
|
74
|
-
MUTATION_METHOD_RE.test(src) ||
|
|
75
|
-
SQL_TAGGED_MUTATION_RE.test(src)
|
|
76
|
-
// Кеш — лише за ІМЕНОВАНИМ маркером (`cache`/`Cache`/`memoize`), не за будь-яким
|
|
77
|
-
// `new Map()`: акумулятор (напр. `byPath = new Map()`) — не кеш, а хибна гарантія
|
|
78
|
-
// «Кешує результати» гірша за пропуск (фабрикація > мовчання).
|
|
79
|
-
const CACHE_RE = /cache|memoi[sz]e/i
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Прибирає `/** */`-обрамлення й `*`-префікси, повертає чистий текст рядками.
|
|
83
|
-
* @param {string} raw сирий JSDoc-блок з обрамленням
|
|
84
|
-
* @returns {string} очищений текст без обрамлення й префіксів
|
|
85
|
-
*/
|
|
86
|
-
function cleanJsDoc(raw) {
|
|
87
|
-
return raw
|
|
88
|
-
.replace(JSDOC_OPEN_RE, '')
|
|
89
|
-
.replace(JSDOC_CLOSE_RE, '')
|
|
90
|
-
.split('\n')
|
|
91
|
-
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
92
|
-
.join('\n')
|
|
93
|
-
.trim()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Опис (без @-тегів) + параметри з `@param` як «name — опис».
|
|
98
|
-
* @param {string} raw сирий JSDoc-блок
|
|
99
|
-
* @returns {{desc:string, params:Array<{name:string, desc:string}>, ret:string}} розпарсений опис, параметри й опис повернення
|
|
100
|
-
*/
|
|
101
|
-
function parseJsDoc(raw) {
|
|
102
|
-
const text = cleanJsDoc(raw)
|
|
103
|
-
const lines = text.split('\n')
|
|
104
|
-
const descLines = []
|
|
105
|
-
const params = []
|
|
106
|
-
let ret = ''
|
|
107
|
-
for (const l of lines) {
|
|
108
|
-
const pm = l.match(PARAM_LINE_RE)
|
|
109
|
-
if (pm) {
|
|
110
|
-
const desc = pm[2].trim()
|
|
111
|
-
// «опис.» — JSDoc-заглушка без сенсу; не тягнемо її як факт
|
|
112
|
-
params.push({ name: pm[1], desc: desc === 'опис.' ? '' : desc })
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
const rm = l.match(RETURNS_LINE_RE)
|
|
116
|
-
if (rm) {
|
|
117
|
-
ret = rm[1].trim()
|
|
118
|
-
continue
|
|
119
|
-
}
|
|
120
|
-
if (l.startsWith('@')) continue
|
|
121
|
-
descLines.push(l)
|
|
122
|
-
}
|
|
123
|
-
return { desc: descLines.join('\n').trim(), params, ret }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Провідний блок-коментар файлу (намір), якщо він перед першим import/кодом.
|
|
128
|
-
* @param {string} src вміст файлу
|
|
129
|
-
* @returns {string} текст header-коментаря або порожній рядок
|
|
130
|
-
*/
|
|
131
|
-
function extractFileHeader(src) {
|
|
132
|
-
const m = src.match(FILE_HEADER_RE)
|
|
133
|
-
if (!m) return ''
|
|
134
|
-
// має бути на самому початку (до import/код)
|
|
135
|
-
if (src.slice(0, m.index).trim() !== '') return ''
|
|
136
|
-
return parseJsDoc(m[0]).desc
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Блок-коментар, що стоїть ВПРИТУЛ перед позицією (лише пробіли між ними).
|
|
141
|
-
* `(?:(?!\*/)[\s\S])*` гарантує, що тіло не містить `*/`, тож захоплюється рівно один
|
|
142
|
-
* найближчий блок — без жадібного «перестрибування» через імпорти/код.
|
|
143
|
-
* @param {string} prefix вміст файлу до позиції експорту
|
|
144
|
-
* @returns {string|null} JSDoc-блок або null якщо немає
|
|
145
|
-
*/
|
|
146
|
-
function precedingJsDoc(prefix) {
|
|
147
|
-
const m = prefix.match(PRECEDING_JSDOC_RE)
|
|
148
|
-
return m ? m[0] : null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Експорти + JSDoc, що безпосередньо передує кожному.
|
|
153
|
-
* @param {string} src вміст файлу
|
|
154
|
-
* @returns {Array<object>} список експортів із метаданими
|
|
155
|
-
*/
|
|
156
|
-
function extractExports(src) {
|
|
157
|
-
const out = []
|
|
158
|
-
for (const m of src.matchAll(EXPORT_DECL_RE)) {
|
|
159
|
-
const [, kind, name] = m
|
|
160
|
-
const jsdocRaw = precedingJsDoc(src.slice(0, m.index))
|
|
161
|
-
out.push({ name, kind, ...(jsdocRaw ? parseJsDoc(jsdocRaw) : { desc: '', params: [], ret: '' }) })
|
|
162
|
-
}
|
|
163
|
-
return out
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Імпорти, класифіковані на stdlib / npm / internal.
|
|
168
|
-
* @param {string} src вміст файлу
|
|
169
|
-
* @returns {{stdlib:Array<string>, npm:Array<string>, internal:Array<string>}} розкласифіковані шляхи імпортів
|
|
170
|
-
*/
|
|
171
|
-
function extractImports(src) {
|
|
172
|
-
const internal = new Set(),
|
|
173
|
-
npm = new Set(),
|
|
174
|
-
stdlib = new Set()
|
|
175
|
-
for (const m of src.matchAll(IMPORT_FROM_RE)) {
|
|
176
|
-
const s = m[1]
|
|
177
|
-
if (s.startsWith('node:') || BUILTIN_MODULES.has(s.split('/', 1)[0])) stdlib.add(s.replace(NODE_PREFIX_RE, ''))
|
|
178
|
-
else if (s.startsWith('.') || s.startsWith('/')) internal.add(s)
|
|
179
|
-
else npm.add(s)
|
|
180
|
-
}
|
|
181
|
-
return { stdlib: [...stdlib], npm: [...npm], internal: [...internal] }
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Імена символів, імпортованих із внутрішніх модулів — їх модель не має згадувати.
|
|
186
|
-
* @param {string} src вміст файлу
|
|
187
|
-
* @returns {Array<string>} список імен внутрішніх символів
|
|
188
|
-
*/
|
|
189
|
-
function extractInternalSymbols(src) {
|
|
190
|
-
const out = new Set()
|
|
191
|
-
for (const m of src.matchAll(INTERNAL_IMPORT_RE)) {
|
|
192
|
-
const clause = m[1]
|
|
193
|
-
const named = clause.match(NAMED_BRACES_RE)
|
|
194
|
-
if (named) {
|
|
195
|
-
for (const n of named[1].split(',')) {
|
|
196
|
-
const name = n.replace(IMPORT_AS_RE, '').trim()
|
|
197
|
-
if (name) out.add(name)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const defName = clause.replace(NAMED_BRACES_RE, '').replaceAll(',', ' ').trim().split(' ', 1)[0]
|
|
201
|
-
if (defName && IDENT_RE.test(defName)) out.add(defName)
|
|
202
|
-
}
|
|
203
|
-
return [...out]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Імена top-level функцій/класів, які НЕ експортуються (службові помічники).
|
|
208
|
-
* Модель не має подавати їх як «публічні функції» у Поведінці/API (R6).
|
|
209
|
-
* Const-стрілки свідомо не ловимо — менше false-positive на змістовних константах.
|
|
210
|
-
* @param {string} src вміст файлу
|
|
211
|
-
* @returns {Array<string>} список імен неекспортованих функцій/класів
|
|
212
|
-
*/
|
|
213
|
-
function extractLocalSymbols(src) {
|
|
214
|
-
const exported = new Set(Array.from(src.matchAll(EXPORT_DECL_RE), m => m[2]))
|
|
215
|
-
const out = new Set()
|
|
216
|
-
for (const m of src.matchAll(TOP_FN_DECL_RE)) {
|
|
217
|
-
if (!exported.has(m[1])) out.add(m[1])
|
|
218
|
-
}
|
|
219
|
-
return [...out]
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Поведінкові маркери — евристики регулярками.
|
|
224
|
-
* @param {string} src вміст файлу
|
|
225
|
-
* @returns {object} набір прапорців-евристик
|
|
226
|
-
*/
|
|
227
|
-
function extractMarkers(src) {
|
|
228
|
-
// помітні «пропуски»: dir/segment-літерали у фільтрах
|
|
229
|
-
const skips = new Set()
|
|
230
|
-
for (const lit of ['.github', '.git', 'node_modules', 'base/', 'ua/', '.firebase']) {
|
|
231
|
-
if (src.includes(`'${lit}`) || src.includes(`"${lit}`) || src.includes(`/${lit}`)) skips.add(lit)
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
// «Фабрикація > мовчання»: прапорець true лише за high-confidence; інакше
|
|
235
|
-
// guaranteesFromMarkers/factsSummary його ОПУСКАЮТЬ (не стверджують протилежне).
|
|
236
|
-
readOnly: !WRITE_FS_RE.test(src) && !isMutation(src), // ні ФС-запису, ні DB-мутацій
|
|
237
|
-
catchesErrors: (CATCH_RE.test(src) || TRY_RE.test(src)) && !THROW_RE.test(src), // fail-safe лише якщо НЕ кидає
|
|
238
|
-
returnsFalsyOnFail: FALSY_RETURN_RE.test(src) && !THROW_RE.test(src),
|
|
239
|
-
network: NETWORK_RE.test(src),
|
|
240
|
-
caches: CACHE_RE.test(src),
|
|
241
|
-
skips: [...skips]
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Головний екстрактор: код файлу → факт-лист.
|
|
246
|
-
* @param {string} src вміст файлу
|
|
247
|
-
* @param {string} relPath шлях (для контексту/мови екстрактора)
|
|
248
|
-
* @returns {{relPath:string, lang:string, header:string, exports:Array, imports:object, markers:object}} структура фактів про файл
|
|
249
|
-
*/
|
|
250
|
-
export function extractFacts(src, relPath) {
|
|
251
|
-
const lang = relPath.split('.').pop()
|
|
252
|
-
if (!['js', 'mjs', 'ts'].includes(lang)) {
|
|
253
|
-
return { relPath, lang, unsupported: true, header: '', exports: [], imports: {}, markers: {} }
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
relPath,
|
|
257
|
-
lang,
|
|
258
|
-
header: extractFileHeader(src),
|
|
259
|
-
exports: extractExports(src),
|
|
260
|
-
imports: extractImports(src),
|
|
261
|
-
internalSymbols: extractInternalSymbols(src),
|
|
262
|
-
localSymbols: extractLocalSymbols(src),
|
|
263
|
-
markers: extractMarkers(src)
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// CLI для інспекції: node docgen-extract.mjs <file>
|
|
268
|
-
if (isRunAsCli(import.meta.url)) {
|
|
269
|
-
const file = process.argv[2]
|
|
270
|
-
if (!file) {
|
|
271
|
-
throw new Error('Usage: node docgen-extract.mjs <file>')
|
|
272
|
-
}
|
|
273
|
-
const facts = extractFacts(readFileSync(file, 'utf8'), file)
|
|
274
|
-
console.log(JSON.stringify(facts, null, 2))
|
|
275
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: main.mjs
|
|
4
|
-
resource: npm/rules/doc-files/units/main.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 8acdefc1
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.98
|
|
10
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Огляд
|
|
14
|
-
|
|
15
|
-
Цей файл забезпечує аналіз вмісту файлів різного мовного формату для вилучення інформації про юніти. Він обробляє вміст, визначаючи відповідний метод вилучення для JavaScript-подібних або Rust-файлів. Публічна функція `extractUnits` виконує механізм вилучення юнітних даних з коду.
|
|
16
|
-
|
|
17
|
-
## Поведінка
|
|
18
|
-
|
|
19
|
-
1. Метод `extractUnits` приймає вміст файлу та його відносний шлях.
|
|
20
|
-
2. Визначається розширення файлу за його відносним шляхом.
|
|
21
|
-
3. Якщо розширення відповідає JavaScript-подібним мовам (js, mjs, ts, jsx, tsx, cts, mts), викликається відповідна функція для вилучення юнітів на основі AST.
|
|
22
|
-
4. Якщо розширення 'rs', викликається відповідна функція для вилучення юнітів шляхом парсингу за допомогою регулярних виразів та підрахунку блоків.
|
|
23
|
-
5. Якщо розширення не підтримується або файл не може бути проаналізований, метод повертає null.
|
|
24
|
-
|
|
25
|
-
## Публічний API
|
|
26
|
-
|
|
27
|
-
- extractUnits — Витягує одиниці виміру, визначаючи метод аналізу на основі розширення файлу (від AST для JS/TS до простого пошуку для Vue/Py).
|
|
28
|
-
|
|
29
|
-
## Гарантії поведінки
|
|
30
|
-
|
|
31
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/units.md */
|
|
2
|
-
|
|
3
|
-
import { extractUnitsJs } from '../units-js/main.mjs'
|
|
4
|
-
|
|
5
|
-
const JS_EXT = new Set(['js', 'mjs', 'ts', 'jsx', 'tsx', 'cts', 'mts'])
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Мовно-агностичний фасад юніт-шару. Диспатчить за розширенням:
|
|
9
|
-
* js/mjs/ts → oxc AST; інші мови (rs — lang-плагін, extension-point
|
|
10
|
-
* `doc-files`.extractUnits; vue/py) → null (whole-file шлях).
|
|
11
|
-
* @param {string} src вміст файлу
|
|
12
|
-
* @param {string} relPath шлях файлу
|
|
13
|
-
* @returns {Array<object>|null} юніти або null, якщо мова ще не підтримана / файл не парситься
|
|
14
|
-
*/
|
|
15
|
-
export function extractUnits(src, relPath) {
|
|
16
|
-
const ext = (relPath.split('.').pop() || '').toLowerCase()
|
|
17
|
-
if (JS_EXT.has(ext)) return extractUnitsJs(src, relPath)
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: main.mjs
|
|
4
|
-
resource: npm/rules/doc-files/units-js/main.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 395188f4
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
issues: judge:inaccurate:0.99
|
|
10
|
-
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Огляд
|
|
14
|
-
|
|
15
|
-
Будь ласка, надайте секцію «overview», яку необхідно переписати.
|
|
16
|
-
|
|
17
|
-
## Поведінка
|
|
18
|
-
|
|
19
|
-
1. Функція extractUnitsJs аналізує вміст файлу, щоб виявити ідентифіковані юніти (функції, класи, const-функції).
|
|
20
|
-
2. Вона визначає, які юніти є експортованими та обробляє їх для включення до списку.
|
|
21
|
-
3. Для кожного знайденого юніту вона витягує відповідний опис з JSDoc, що передує його оголошенню.
|
|
22
|
-
4. Зібрана інформація про юніт включає його назву, тип (функція, клас, const), прапор експорту, діапазон у вихідному коді та повний вихідний код.
|
|
23
|
-
5. Система виявляє всі виклики інших юнітів у тілі коду кожного юніта.
|
|
24
|
-
6. В результаті повертається список об'єктів, де кожен об'єкт описує один юніт, і в якому вказано лише посилання на інші виявлені юніти.
|
|
25
|
-
|
|
26
|
-
## Публічний API
|
|
27
|
-
|
|
28
|
-
extractUnitsJs — знаходить та витягує на верхньому рівні логічні блоки (функції/класи/константи) з файлів JavaScript/TypeScript, які є частиною юніт-шару. Включає інформацію про експорт та взаємодії з іншими юнітами.
|
|
29
|
-
|
|
30
|
-
## Гарантії поведінки
|
|
31
|
-
|
|
32
|
-
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/** @see ./docs/units-js.md */
|
|
2
|
-
|
|
3
|
-
import { parseProgramOrNull, walkAstWithAncestors } from '../../../scripts/utils/ast-scan-utils.mjs'
|
|
4
|
-
|
|
5
|
-
// JSDoc-блок, що стоїть впритул перед позицією (лише пробіли між ними).
|
|
6
|
-
const JSDOC_BEFORE_RE = /\/\*\*(?:(?!\*\/)[\s\S])*\*\/\s*$/
|
|
7
|
-
const JSDOC_OPEN_RE = /^\s*\/\*\*?/
|
|
8
|
-
const JSDOC_CLOSE_RE = /\*\/\s*$/
|
|
9
|
-
const STAR_PREFIX_RE = /^\s*\*?\s?/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Очищає JSDoc від обрамлення `/** */` і `*`-префіксів.
|
|
13
|
-
* @param {string} raw сирий блок або порожній рядок
|
|
14
|
-
* @returns {string} текст опису без тегів-обрамлення
|
|
15
|
-
*/
|
|
16
|
-
function cleanDoc(raw) {
|
|
17
|
-
if (!raw) return ''
|
|
18
|
-
return raw
|
|
19
|
-
.replace(JSDOC_OPEN_RE, '')
|
|
20
|
-
.replace(JSDOC_CLOSE_RE, '')
|
|
21
|
-
.split('\n')
|
|
22
|
-
.map(l => l.replace(STAR_PREFIX_RE, '').trimEnd())
|
|
23
|
-
.join('\n')
|
|
24
|
-
.trim()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* JSDoc, що передує позиції `start` у джерелі (або порожній рядок).
|
|
29
|
-
* @param {string} src вміст файлу
|
|
30
|
-
* @param {number} start зміщення початку декларації
|
|
31
|
-
* @returns {string} очищений опис
|
|
32
|
-
*/
|
|
33
|
-
function precedingDoc(src, start) {
|
|
34
|
-
const m = src.slice(0, start).match(JSDOC_BEFORE_RE)
|
|
35
|
-
return cleanDoc(m ? m[0] : '')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Імʼя функції, що викликається (проста Identifier або `obj.method`).
|
|
40
|
-
* @param {Record<string, unknown>} node CallExpression
|
|
41
|
-
* @returns {string|null} імʼя callee або null
|
|
42
|
-
*/
|
|
43
|
-
function calleeName(node) {
|
|
44
|
-
const c = node.callee
|
|
45
|
-
if (!c || typeof c !== 'object') return null
|
|
46
|
-
if (c.type === 'Identifier') return c.name
|
|
47
|
-
if (c.type === 'MemberExpression' && !c.computed && c.property?.type === 'Identifier') return c.property.name
|
|
48
|
-
return null
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Множина імен, що викликаються у тілі вузла (сирі callee — фільтрація на ребра
|
|
53
|
-
* call-graph робиться у `extractUnitsJs` після збору всіх імен юнітів).
|
|
54
|
-
* @param {unknown} node AST-вузол юніта
|
|
55
|
-
* @returns {Set<string>} імена викликів
|
|
56
|
-
*/
|
|
57
|
-
function collectCalls(node) {
|
|
58
|
-
const names = new Set()
|
|
59
|
-
walkAstWithAncestors(node, [], n => {
|
|
60
|
-
if (n.type !== 'CallExpression') {
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const name = calleeName(n)
|
|
65
|
-
if (name) names.add(name)
|
|
66
|
-
})
|
|
67
|
-
return names
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Будує юніт із декларації, додає у `units`. Розпізнає function/class та
|
|
72
|
-
* const-функції (`const x = () => {}` / `function expression`).
|
|
73
|
-
* @param {Record<string, unknown>} decl декларація (function/class/variable)
|
|
74
|
-
* @param {boolean} exported чи експортується
|
|
75
|
-
* @param {number} docStart зміщення для пошуку JSDoc (зовнішній export-вузол)
|
|
76
|
-
* @param {string} src вміст файлу
|
|
77
|
-
* @param {Array<object>} units акумулятор
|
|
78
|
-
* @returns {void}
|
|
79
|
-
*/
|
|
80
|
-
function pushUnits(decl, exported, docStart, src, units) {
|
|
81
|
-
if (!decl || typeof decl !== 'object') return
|
|
82
|
-
const doc = precedingDoc(src, docStart)
|
|
83
|
-
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
84
|
-
const name = decl.id?.name
|
|
85
|
-
if (!name) return
|
|
86
|
-
units.push({
|
|
87
|
-
name,
|
|
88
|
-
kind: decl.type === 'ClassDeclaration' ? 'class' : 'function',
|
|
89
|
-
exported,
|
|
90
|
-
span: { start: decl.start, end: decl.end },
|
|
91
|
-
body: src.slice(decl.start, decl.end),
|
|
92
|
-
calls: collectCalls(decl),
|
|
93
|
-
doc
|
|
94
|
-
})
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
if (decl.type === 'VariableDeclaration') {
|
|
98
|
-
for (const d of decl.declarations ?? []) {
|
|
99
|
-
const init = d.init
|
|
100
|
-
const isFn = init && (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
|
|
101
|
-
if (!isFn || d.id?.type !== 'Identifier') continue
|
|
102
|
-
units.push({
|
|
103
|
-
name: d.id.name,
|
|
104
|
-
kind: 'const',
|
|
105
|
-
exported,
|
|
106
|
-
span: { start: init.start, end: init.end },
|
|
107
|
-
body: src.slice(init.start, init.end),
|
|
108
|
-
calls: collectCalls(init),
|
|
109
|
-
doc
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Юніт-шар для js/mjs/ts: top-level функції/класи/const-функції з тілом, JSDoc,
|
|
117
|
-
* прапором експорту і ребрами call-graph (виклики ІНШИХ юнітів у тілі).
|
|
118
|
-
* @param {string} src вміст файлу
|
|
119
|
-
* @param {string} [relPath] шлях (для вибору мови oxc)
|
|
120
|
-
* @returns {Array<{name:string, kind:string, exported:boolean, span:{start:number,end:number}, body:string, calls:string[], doc:string}>|null} юніти або null, якщо файл не парситься
|
|
121
|
-
*/
|
|
122
|
-
export function extractUnitsJs(src, relPath = 'scan.ts') {
|
|
123
|
-
const program = parseProgramOrNull(src, relPath)
|
|
124
|
-
if (!program || !Array.isArray(program.body)) return null
|
|
125
|
-
|
|
126
|
-
const units = []
|
|
127
|
-
for (const node of program.body) {
|
|
128
|
-
const isExport =
|
|
129
|
-
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && node.declaration
|
|
130
|
-
if (isExport) {
|
|
131
|
-
pushUnits(node.declaration, true, node.start, src, units)
|
|
132
|
-
} else {
|
|
133
|
-
pushUnits(node, false, node.start, src, units)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Ребра call-graph: лишаємо тільки виклики інших внутрішніх юнітів
|
|
138
|
-
const names = new Set(units.map(u => u.name))
|
|
139
|
-
for (const u of units) u.calls = [...u.calls].filter(n => names.has(n) && n !== u.name)
|
|
140
|
-
return units
|
|
141
|
-
}
|