@7n/rules 1.25.1 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/n-rules.js +16 -1
  3. package/package.json +2 -1
  4. package/rules/doc-files/docgen-crc/docs/main.md +1 -1
  5. package/rules/doc-files/docgen-crc/main.mjs +5 -12
  6. package/rules/doc-files/docgen-gen/docs/main.md +1 -1
  7. package/rules/doc-files/docgen-gen/main.mjs +12 -4
  8. package/rules/doc-files/docgen-scan/docs/main.md +2 -2
  9. package/rules/doc-files/docgen-scan/main.mjs +6 -13
  10. package/rules/js/eslint/concern.json +1 -0
  11. package/schemas/concern.json +4 -0
  12. package/scripts/lib/concern-meta.mjs +14 -1
  13. package/scripts/lib/docs/concern-meta.md +3 -6
  14. package/scripts/lib/docs/plugin-api.md +1 -2
  15. package/scripts/lib/docs/resolve-plugins.md +1 -1
  16. package/scripts/lib/lint-surface/docs/ladder.md +3 -5
  17. package/scripts/lib/lint-surface/docs/run-fix.md +3 -2
  18. package/scripts/lib/lint-surface/ladder.mjs +18 -9
  19. package/scripts/lib/lint-surface/run-fix.mjs +15 -2
  20. package/scripts/lib/plugin-api.mjs +33 -4
  21. package/scripts/lib/resolve-plugins.mjs +2 -1
  22. package/scripts/utils/docs/test-helpers.md +2 -1
  23. package/skills/doc-files/SKILL.md +1 -1
  24. package/skills/taze/SKILL.md +31 -79
  25. package/skills/taze/js/docs/orchestrate.md +9 -12
  26. package/skills/taze/js/orchestrate.mjs +25 -153
  27. package/rules/doc-files/docgen-extract/concern.json +0 -3
  28. package/rules/doc-files/docgen-extract/docs/main.md +0 -38
  29. package/rules/doc-files/docgen-extract/main.mjs +0 -275
  30. package/rules/doc-files/units/concern.json +0 -3
  31. package/rules/doc-files/units/docs/main.md +0 -31
  32. package/rules/doc-files/units/main.mjs +0 -19
  33. package/rules/doc-files/units-js/concern.json +0 -3
  34. package/rules/doc-files/units-js/docs/main.md +0 -32
  35. package/rules/doc-files/units-js/main.mjs +0 -141
  36. package/skills/taze/js/diff.mjs +0 -140
  37. package/skills/taze/js/docs/diff.md +0 -55
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
17
+ ## [1.26.0] - 2026-07-19
18
+
19
+ ### Changed
20
+
21
+ - Фаза 5a spec lang-plugins-extraction: ядро — двигун без мовної специфіки. npm/bun-гілка taze виїхала в плагін `@7n/rules-lang-js` (EcosystemProvider; бекап воркспейсів, `bunx taze -w -r latest` + `bun install`, collectTazeDiff, SKILL-фрагмент); semver-примітиви (parseVersion/isBreaking) тепер визначені в `@7n/rules/plugin-api` (було — реекспорт із diff.mjs). Оркестратор — чистий цикл провайдерів (без вбудованих гілок), звіт — рівноправні секції екосистем; CLI `n-rules taze diff` резолвиться через handler lang-js. Автодетект — кореневий package.json → lang-js
22
+
3
23
  ## [1.25.1] - 2026-07-19
4
24
 
5
25
  ### Fixed
package/bin/n-rules.js CHANGED
@@ -1883,7 +1883,22 @@ try {
1883
1883
  // n-rules taze diff — read-only semver-diff package.json ↔ package.json.taze-bak
1884
1884
  // (root + воркспейси) для скілу n-taze: скрипт класифікує major-оновлення,
1885
1885
  // агент отримує готовий список замість ручного порівняння бекапів.
1886
- const { runTazeCli } = await import('../skills/taze/js/diff.mjs')
1886
+ // Живе у плагіні @7n/rules-lang-js (фаза 5a spec lang-plugins-extraction)
1887
+ // резолвимо його taze-handler і беремо named-експорт runTazeCli.
1888
+ const { getHandlers } = await import('../scripts/lib/resolve-plugins.mjs')
1889
+ const { readNRulesConfigLite } = await import('../scripts/lib/read-n-rules-config-lite.mjs')
1890
+ const config = await readNRulesConfigLite(cwd())
1891
+ const handler = getHandlers(cwd(), config, 'taze').find(h => h.pluginName === '@7n/rules-lang-js')
1892
+ if (!handler) {
1893
+ console.error(
1894
+ '❌ taze diff потребує плагін @7n/rules-lang-js (npm/bun-гілка) — запусти npx @7n/rules для авто-встановлення'
1895
+ )
1896
+ process.exitCode = 1
1897
+ break
1898
+ }
1899
+ const { pathToFileURL } = await import('node:url')
1900
+ // eslint-disable-next-line no-unsanitized/method
1901
+ const { runTazeCli } = await import(pathToFileURL(handler.modulePath).href)
1887
1902
  process.exitCode = await runTazeCli(args)
1888
1903
 
1889
1904
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.25.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
  ],
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-crc/main.mjs
5
5
  docgen:
6
- crc: c10183ef
6
+ crc: 8976f6f8
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -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
- * активних lang-плагінів).
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 EXT_TYPES[ext] ?? pluginDocFilesExtensions(process.cwd())[ext] ?? 'Source File'
80
+ return pluginDocFilesExtensions(process.cwd())[ext] ?? 'Source File'
88
81
  }
89
82
 
90
83
  /**
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-gen/main.mjs
5
5
  docgen:
6
- crc: 358294dc
6
+ crc: ed99ac58
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -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
- // Мовний екстрактор з lang-плагіна (напр. `.rs` у @7n/rules-lang-rust) має
511
- // пріоритет; вбудований extractFacts покриває JS-екосистему, решта — whole-file.
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) ?? 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: 66000db2
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
- * Чи є файл кодовим джерелом для документування. З `root` — враховує і
33
- * розширення активних lang-плагінів; без нього лише вбудовані JS-екосистемні.
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} [root] корінь репозиторію (для плагінних розширень)
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
- const ext = extname(fileName)
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
  /**
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
+ "skipLocalTier": true,
3
4
  "lint": {
4
5
  "scope": "per-file",
5
6
  "glob": ["**/*.{js,mjs,cjs,jsx,ts,tsx,vue}"]
@@ -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 { name, dir: concernDir, check, policy, lint, requiresCapability, fixability: parseFixability(raw.fixability) }
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: 1675429b
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
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,9 +3,8 @@ type: JS Module
3
3
  title: plugin-api.mjs
4
4
  resource: npm/scripts/lib/plugin-api.mjs
5
5
  docgen:
6
- crc: dc4f4a12
6
+ crc: c614a880
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min-retry
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -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: 0d197e39
6
+ crc: fb58860d
7
7
  ---
8
8
 
9
9
  Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
@@ -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: a5e45f08
7
- model: openai-codex/gpt-5.5
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, хмарні 120s; override без зміни коду — env `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS` (мс на ОДИН rung відповідного класу; невалідне значення → дефолт). Runner прокидає його worker-у через `FixContext`, щоб зависла LLM-сесія переривалась (runner додатково тримає backstop ×1.25), а ladder рухався далі. Це робочий важіль для повільної локальної моделі чи великої черги batch-концерну (doc-files ріже беклог під цей ліміт м'яким дедлайном).
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: c4235855
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
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 відповідного класу (local-min/local-min-retry та
14
- // cloud-min/cloud-avg). Значення потрапляє worker-у як `ctx.timeoutMs` (внутрішній
15
- // abort LLM-виклику; batch-worker-и, як doc-files, ріжуть під нього беклог м'яким
16
- // дедлайном), а runner додатково тримає backstop ×1.25 навколо всього worker-виклику.
17
- // Робочий важіль для повільної локальної моделі чи великої черги batch-концерну:
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: CLOUD_TIMEOUT_MS }
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
- if (!worker || ladder.length === 0) {
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 ladder) {
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`)
@@ -7,11 +7,40 @@
7
7
  * модуль-обробник експортує обʼєкт провайдера як `default`. Наступні порти
8
8
  * (doc-files, lint) додаються окремими фазами — не проєктуються наперед.
9
9
  *
10
- * Semver-утиліти ядра реекспортуються звідси, щоб плагіни класифікували
11
- * major/minor за тим самим правилом caret-семантики, що й ядро, без
12
- * дублювання коду і без імпорту внутрішніх шляхів `@7n/rules`.
10
+ * Semver-утиліти (caret-класифікація major/minor) живуть саме тут — єдине
11
+ * джерело правила для всіх мовних плагінів, без імпорту внутрішніх шляхів
12
+ * `@7n/rules` і без циклу plugin-api плагін.
13
13
  */
14
- export { isBreaking, parseVersion } from '../../skills/taze/js/diff.mjs'
14
+ // Заякорено на початок (після можливих range-операторів `^~>=<`, пробілів, `v`),
15
+ // щоб НЕ ловити версію всередині protocol-specifier-ів (`workspace:1.0.0`, `npm:x@1.2.3`).
16
+ const SEMVER_RE = /^[\s~^>=<v]*(\d+)\.(\d+)\.(\d+)/
17
+
18
+ /**
19
+ * Парсить semver-ядро зі specifier-а (ігнорує range-префікси `^`/`~`/`>=` тощо).
20
+ * @param {string} spec версійний specifier із package.json
21
+ * @returns {{major:number, minor:number, patch:number}|null} ядро або null для не-semver (`workspace:*`, git-url, `*`)
22
+ */
23
+ export function parseVersion(spec) {
24
+ if (typeof spec !== 'string') return null
25
+ const m = SEMVER_RE.exec(spec)
26
+ if (!m) return null
27
+ return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }
28
+ }
29
+
30
+ /**
31
+ * Чи є перехід `from → to` breaking за caret-семантикою (змінилась найлівіша
32
+ * ненульова компонента).
33
+ * @param {{major:number,minor:number,patch:number}} from стара версія
34
+ * @param {{major:number,minor:number,patch:number}} to нова версія
35
+ * @returns {boolean} true — major/breaking
36
+ */
37
+ export function isBreaking(from, to) {
38
+ if (from.major !== to.major) return true
39
+ if (from.major > 0) return false
40
+ if (from.minor !== to.minor) return true
41
+ if (from.minor > 0) return false
42
+ return from.patch !== to.patch
43
+ }
15
44
 
16
45
  /** Версія контракту plugin-api: плагін декларує `requiresPluginApi`, несумісність → skip, не креш. */
17
46
  export const PLUGIN_API_VERSION = 1
@@ -31,11 +31,12 @@ export const KNOWN_CI_PLUGINS = Object.freeze({
31
31
  /**
32
32
  * Відомі мовні плагіни: файловий сигнал екосистеми → npm-пакет. `maxDepth` —
33
33
  * до якої глибини шукати сигнал: python — лише корінь (uv-провайдер v1
34
- * обробляє тільки кореневий pyproject.toml); rust — до 3 рівнів, бо в
34
+ * обробляє тільки кореневий pyproject.toml; js — кореневий package.json); rust — до 3 рівнів, бо в
35
35
  * монорепо Cargo.toml часто вкладений (Tauri `app/src-tauri/Cargo.toml`),
36
36
  * а провайдер обробляє всі знайдені маніфести.
37
37
  */
38
38
  export const KNOWN_LANG_PLUGINS = Object.freeze({
39
+ js: { signal: 'package.json', pkg: '@7n/rules-lang-js', maxDepth: 0 },
39
40
  python: { signal: 'pyproject.toml', pkg: '@7n/rules-lang-python', maxDepth: 0 },
40
41
  rust: { signal: 'Cargo.toml', pkg: '@7n/rules-lang-rust', maxDepth: 3 }
41
42
  })
@@ -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: 9af8ee18
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 вбудовано; rs/py — через lang-плагіни) JS-оркестрована генерація лаконічної поведінкової української md-документації у теку docs/ поряд із кодом, зі звіркою застарілості за CRC у frontmatter
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