@7n/rules 1.13.1 → 1.14.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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.14.0] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- js/eslint: fix-worker.mjs — обмежений паралельний пул (MAX_PARALLEL_FILES=4) замість послідовного циклу по файлах; профайлінг спростував гіпотезу про bootstrap-накладні витрати сесії
|
|
8
|
+
|
|
3
9
|
## [1.13.1] - 2026-07-18
|
|
4
10
|
|
|
5
11
|
### Changed
|
package/package.json
CHANGED
|
@@ -3,21 +3,21 @@ type: JS Module
|
|
|
3
3
|
title: fix-worker.mjs
|
|
4
4
|
resource: npm/rules/js/eslint/fix-worker.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 802ef1f3
|
|
7
7
|
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## Огляд
|
|
11
11
|
|
|
12
|
-
Custom fix-worker `js/eslint` (перекриває дефолтний `default-worker.mjs`): замість одного `runAgentFix`-виклику з усіма порушеннями з усіх файлів concern-а — окрема агентна сесія на кожен файл,
|
|
12
|
+
Custom fix-worker `js/eslint` (перекриває дефолтний `default-worker.mjs`): замість одного `runAgentFix`-виклику з усіма порушеннями з усіх файлів concern-а — окрема агентна сесія на кожен файл, ПАРАЛЕЛЬНО (обмежений пул, не більше `MAX_PARALLEL_FILES`). Мотивація й еволюція дизайну — `docs/adr/260718-0754-js-eslint-fix-worker-per-session-overhead.md`: послідовна версія стабільно впиралась у timeout на всіх 4 rung-ах драбини (local-min→cloud-avg); профайлінг реального прогону показав, що домінує кількість/довжина раундів моделі на файл, а не фіксовані bootstrap-витрати сесії — тож паралелізм (кілька файлів одночасно замість черги) дає пряме пришвидшення незалежно від причини.
|
|
13
13
|
|
|
14
14
|
## Поведінка
|
|
15
15
|
|
|
16
16
|
1. Групує вхідні `violations` за `file`; порушення без `file` ігноруються.
|
|
17
|
-
2. Дедлайн — `DEADLINE_FRACTION` (0.8) від `ctx.timeoutMs`;
|
|
18
|
-
3.
|
|
17
|
+
2. Дедлайн — `DEADLINE_FRACTION` (0.8) від `ctx.timeoutMs`; гейтить лише СТАРТ нового файлу з черги (не скасовує вже запущені).
|
|
18
|
+
3. `runPooled` — обмежений пул воркерів (`MAX_PARALLEL_FILES=4`): при `files.length ≤ MAX_PARALLEL_FILES` усі файли стартують майже одночасно й отримують практично весь бюджет незалежно один від одного; при більшій кількості черга природно звужує бюджет пізніших хвиль (`callTimeoutMs` рахується в момент старту файлу з черги, не наперед).
|
|
19
19
|
4. На кожен файл — окремий `runAgentFix` із `targetFiles: [file]` і власним `verify` (`verifyFile`): item-scoped canonical re-detect лише цього файлу, не всього concern-а — інакше evidence-гейт хибно вважав би файл незакритим через порушення в ІНШИХ файлах.
|
|
20
|
-
5. Один файл, що завершився з `error
|
|
20
|
+
5. Один файл, що завершився з `error` АБО кинув виняток (try/catch у тілі воркера), не обриває пул — пропускається, решта файлів обробляються незалежно.
|
|
21
21
|
6. Повертає лише `touchedFiles` з успішних (`!error`) викликів; success rung-а все одно визначає whole-concern canonical re-detect runner-а (`runRung`), не цей worker.
|
|
22
22
|
|
|
23
23
|
## Публічний API
|
|
@@ -28,3 +28,4 @@ Custom fix-worker `js/eslint` (перекриває дефолтний `default-
|
|
|
28
28
|
|
|
29
29
|
- Пише лише у файли з переданих `violations` (`targetFiles: [file]` на кожен виклик) — той самий semantic-collateral guard, що й у дефолтного worker-а.
|
|
30
30
|
- `recordWrite` (не durable) на кожен файл: rollback-контракт незмінний — якщо після worker-а в concern-і лишилось хоч одне порушення будь-де, `runRung` відкочує ВСІ правки цього rung-а, включно з уже полагодженими файлами.
|
|
31
|
+
- Конкурентні виклики `recordWrite` безпечні: central snapshot — синхронні Map-операції без `await` усередині, event loop не перериває їх посеред виконання.
|
|
@@ -4,22 +4,33 @@
|
|
|
4
4
|
* з УСІХ файлів в один `runAgentFix`-виклик — на реальних lint-прогонах (tauri-components)
|
|
5
5
|
* це давало 100% timeout на всіх 4 rung-ах драбини (local-min→cloud-avg), навіть коли
|
|
6
6
|
* порушень було лише 20-90 у 1-3 файлах: одна сесія жонглює кількома файлами одразу і не
|
|
7
|
-
* встигає в rung-таймаут.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* встигає в rung-таймаут.
|
|
8
|
+
*
|
|
9
|
+
* Файли обробляються ПАРАЛЕЛЬНО (обмежений пул, `MAX_PARALLEL_FILES`), не послідовно —
|
|
10
|
+
* ADR docs/adr/260718-0754-js-eslint-fix-worker-per-session-overhead.md. Профайлінг
|
|
11
|
+
* реального прогону (2026-07-18, trace `runAgentFix`) спростував гіпотезу про фіксовані
|
|
12
|
+
* bootstrap-витрати сесії: `dispatch.test.js` (2 порушення) — 9 раундів моделі, 29.4с;
|
|
13
|
+
* тривіальний фікс так само вимагав багато tool-раундів, а не одноразової плати за bootstrap.
|
|
14
|
+
* Оскільки домінує саме кількість/довжина turn-раундів, а не старт сесії, паралелізм
|
|
15
|
+
* (кілька файлів одночасно замість черги) дає пряме пришвидшення незалежно від причини —
|
|
16
|
+
* секвенційна версія витрачала весь бюджет rung-а на 1 файл, лишаючи іншим 0 шансів
|
|
17
|
+
* навіть стартувати.
|
|
10
18
|
*
|
|
11
19
|
* Rollback-контракт незмінний: `recordWrite` (не durable) на кожен файл — правки наявного
|
|
12
20
|
* стороннього коду. Якщо після worker-а `runRung`-ів canonical re-detect (whole-concern)
|
|
13
21
|
* знайде хоч одне порушення будь-де, увесь rung однаково відкотиться (S1) — той самий
|
|
14
|
-
* контракт, що й у дефолтного worker-а;
|
|
15
|
-
* до 0 порушень, а не замінює rollback-семантику.
|
|
22
|
+
* контракт, що й у дефолтного worker-а; паралелізм підвищує ймовірність, що ЦЕЙ rung
|
|
23
|
+
* дійде до 0 порушень, а не замінює rollback-семантику. `snapshot.record`/`recordWrite` —
|
|
24
|
+
* синхронні Map-операції без `await` усередині, тож конкурентні виклики безпечні (Node
|
|
25
|
+
* event loop не перериває синхронний блок).
|
|
16
26
|
*
|
|
17
27
|
* Дедлайн (`DEADLINE_FRACTION` від `ctx.timeoutMs`, той самий підхід, що й
|
|
18
|
-
* `doc-files/check/fix-worker.mjs`)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
28
|
+
* `doc-files/check/fix-worker.mjs`) гейтить лише СТАРТ нового файлу з черги — не
|
|
29
|
+
* скасовує вже запущені. При files.length ≤ MAX_PARALLEL_FILES усі стартують майже
|
|
30
|
+
* одночасно й отримують практично весь бюджет незалежно один від одного (справжній
|
|
31
|
+
* паралелізм); при більшій кількості — черга природно звужує бюджет пізніших хвиль
|
|
32
|
+
* (`callTimeoutMs` рахується в момент старту, не наперед), не даючи перевищити
|
|
33
|
+
* backstop ×1.25 runner-а.
|
|
23
34
|
* @typedef {import('../../../scripts/lib/lint-surface/types.mjs').FixWorkerFn} FixWorkerFn
|
|
24
35
|
*/
|
|
25
36
|
import { resolve } from 'node:path'
|
|
@@ -28,9 +39,12 @@ import { anchoredEnabled } from '../../../scripts/lib/lint-surface/default-worke
|
|
|
28
39
|
import { renderViolations } from '../../../scripts/lib/lint-surface/render.mjs'
|
|
29
40
|
import { lint } from './main.mjs'
|
|
30
41
|
|
|
31
|
-
/** Частка ctx.timeoutMs, після якої
|
|
42
|
+
/** Частка ctx.timeoutMs, після якої черга не стартує новий файл (запас до backstop ×1.25). */
|
|
32
43
|
const DEADLINE_FRACTION = 0.8
|
|
33
44
|
|
|
45
|
+
/** Максимум файлів, що обробляються одночасно (без необмеженого burst на великих concern-ах). */
|
|
46
|
+
const MAX_PARALLEL_FILES = 4
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* Item-scoped (один файл) canonical re-detect для evidence-гейта `runAgentFix` —
|
|
36
50
|
* НЕ те саме, що whole-concern `ctx.verify` з `runRung` (той перевірив би ВСІ файли
|
|
@@ -47,6 +61,27 @@ async function verifyFile(cwd, ruleId, concernId, file) {
|
|
|
47
61
|
return { ok: stamped.length === 0, output: renderViolations(stamped) }
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Обробляє `items` обмеженим пулом воркерів (не більше `MAX_PARALLEL_FILES` одночасно) —
|
|
66
|
+
* власна черга замість `Promise.all(items.map(...))`, щоб великий concern не відкрив
|
|
67
|
+
* необмежену кількість конкурентних агентних сесій одразу.
|
|
68
|
+
* @param {string[]} items елементи черги (шляхи файлів)
|
|
69
|
+
* @param {(item: string) => Promise<void>} worker обробник одного елемента; винятки ловить сам
|
|
70
|
+
* @returns {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
async function runPooled(items, worker) {
|
|
73
|
+
const queue = [...items]
|
|
74
|
+
const runnerCount = Math.min(MAX_PARALLEL_FILES, items.length)
|
|
75
|
+
await Promise.all(
|
|
76
|
+
Array.from({ length: runnerCount }, async () => {
|
|
77
|
+
let item
|
|
78
|
+
while ((item = queue.shift())) {
|
|
79
|
+
await worker(item)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
/** @type {FixWorkerFn} */
|
|
51
86
|
export async function fixWorker(violations, ctx) {
|
|
52
87
|
// lazy import — тримає detect-шлях вільним від pi/oxc (read-only --no-fix не вантажить їх).
|
|
@@ -71,29 +106,34 @@ export async function fixWorker(violations, ctx) {
|
|
|
71
106
|
const anchoredEdits = anchoredEnabled(ctx.model, isLocalModel)
|
|
72
107
|
|
|
73
108
|
const touchedFiles = []
|
|
74
|
-
|
|
75
|
-
if (deadlineAt && Date.now() >= deadlineAt)
|
|
109
|
+
await runPooled(files, async file => {
|
|
110
|
+
if (deadlineAt && Date.now() >= deadlineAt) return
|
|
76
111
|
const callTimeoutMs = deadlineAt ? Math.max(1000, deadlineAt - Date.now()) : ctx.timeoutMs
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
let res
|
|
114
|
+
try {
|
|
115
|
+
res = await runAgentFix(ctx.ruleId, renderViolations(byFile.get(file)), ctx.cwd, {
|
|
116
|
+
model: ctx.model,
|
|
117
|
+
tier: ctx.tier,
|
|
118
|
+
timeoutMs: callTimeoutMs,
|
|
119
|
+
feedback: ctx.feedback ?? null,
|
|
120
|
+
caller: `fix:${ctx.ruleId}/${ctx.concernId}:${ctx.tier}:${file}`,
|
|
121
|
+
recordWrite: ctx.recordWrite,
|
|
122
|
+
chain: ctx.chain ?? null,
|
|
123
|
+
targetFiles: [file],
|
|
124
|
+
verify: () => verifyFile(ctx.cwd, ctx.ruleId, ctx.concernId, file),
|
|
125
|
+
verifyMax: ctx.verifyMax,
|
|
126
|
+
anchoredEdits,
|
|
127
|
+
deps: { astContext: p => extractContext(resolve(ctx.cwd, p)) }
|
|
128
|
+
})
|
|
129
|
+
} catch {
|
|
130
|
+
// Один файл впав винятком — не валимо решту паралельних воркерів у пулі; whole-concern
|
|
131
|
+
// canonical re-detect runner-а (не цей worker) визначить, чи rung закрито.
|
|
132
|
+
return
|
|
133
|
+
}
|
|
92
134
|
|
|
93
|
-
// Один файл не впорався — не кидаємо, пробуємо решту в межах дедлайну; whole-concern
|
|
94
|
-
// canonical re-detect runner-а (не цей worker) визначить, чи rung закрито.
|
|
95
135
|
if (!res.error) touchedFiles.push(...(res.touchedFiles ?? []))
|
|
96
|
-
}
|
|
136
|
+
})
|
|
97
137
|
|
|
98
138
|
return { touchedFiles }
|
|
99
139
|
}
|