@nitra/cursor 12.16.1 → 12.17.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 +12 -0
- package/lib/pi-agent-fix.mjs +10 -3
- package/package.json +1 -1
- package/scripts/auto-rules.mjs +34 -58
- package/scripts/lib/fix/orchestrator.mjs +34 -0
- package/scripts/lib/rule-predicates.mjs +15 -60
- package/scripts/utils/walkDir.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.17.0] - 2026-06-27
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Перехід на globby для обходу файлової системи з урахуванням .gitignore
|
|
8
|
+
|
|
9
|
+
## [12.16.2] - 2026-06-27
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- feat(orchestrator): skip non-actionable violation (не годувати агента tool-crash); feat(pi-agent-fix): логувати вхід LLM у trace (violation + розмір промпта)
|
|
14
|
+
|
|
3
15
|
## [12.16.1] - 2026-06-26
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/lib/pi-agent-fix.mjs
CHANGED
|
@@ -213,12 +213,11 @@ export async function runPiAgentFix(ruleId, violation, cwd, opts = {}) {
|
|
|
213
213
|
}
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
+
const fixPrompt = buildFixPrompt({ ruleId, violation, ruleText, feedback })
|
|
216
217
|
const startedAt = clock()
|
|
217
218
|
let error = null
|
|
218
219
|
try {
|
|
219
|
-
await withTimeout(session.prompt(
|
|
220
|
-
session.abort?.()
|
|
221
|
-
)
|
|
220
|
+
await withTimeout(session.prompt(fixPrompt), timeoutMs, () => session.abort?.())
|
|
222
221
|
} catch (e) {
|
|
223
222
|
error = e.message
|
|
224
223
|
}
|
|
@@ -244,9 +243,17 @@ export async function runPiAgentFix(ruleId, violation, cwd, opts = {}) {
|
|
|
244
243
|
rung: tier,
|
|
245
244
|
model: modelSpec,
|
|
246
245
|
cwd,
|
|
246
|
+
// ВХІД LLM (щоб «що подали» було видно у trace, без розкопок escalation-log):
|
|
247
|
+
// violation — обрізаний (може бути великим); promptChars — повний розмір промпта.
|
|
248
|
+
violation: typeof violation === 'string' ? violation.slice(0, 4000) : null,
|
|
249
|
+
violationChars: typeof violation === 'string' ? violation.length : 0,
|
|
250
|
+
promptChars: fixPrompt.length,
|
|
251
|
+
// вихід:
|
|
247
252
|
turnCount,
|
|
248
253
|
toolCallCount,
|
|
254
|
+
touchedFiles,
|
|
249
255
|
backstopHit,
|
|
256
|
+
wallMs: clock() - startedAt,
|
|
250
257
|
error
|
|
251
258
|
})
|
|
252
259
|
return { applied: touchedFiles.length > 0, touchedFiles, telemetry, error, rollback: guard.rollback }
|
package/package.json
CHANGED
package/scripts/auto-rules.mjs
CHANGED
|
@@ -16,10 +16,13 @@
|
|
|
16
16
|
* їх у конфіг із поправкою на legacy-id (`migrateRuleIds`).
|
|
17
17
|
*/
|
|
18
18
|
import { readdirSync } from 'node:fs'
|
|
19
|
-
import {
|
|
19
|
+
import { readFile } from 'node:fs/promises'
|
|
20
20
|
import { basename, dirname, join, relative } from 'node:path'
|
|
21
21
|
import { fileURLToPath } from 'node:url'
|
|
22
22
|
|
|
23
|
+
import { globby } from 'globby'
|
|
24
|
+
|
|
25
|
+
import { ALWAYS_IGNORE } from './utils/walkDir.mjs'
|
|
23
26
|
import { globToRegex } from '../rules/npm-module/js/package_structure.mjs'
|
|
24
27
|
import { textHasBunSqlImport } from '../rules/js-bun-db/lib/bun-sql-scan.mjs'
|
|
25
28
|
import {
|
|
@@ -84,9 +87,30 @@ export const AUTO_RULE_DEPENDENCIES = Object.freeze(
|
|
|
84
87
|
|
|
85
88
|
const HASURA_CONFIG_MARKER = 'metadata_directory: metadata'
|
|
86
89
|
const REGO_RE = /\.rego$/iu
|
|
87
|
-
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
88
90
|
const DEFAULT_DISABLED_LIST = Object.freeze([])
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Збирає relative-posix шляхи дерева (файли + директорії), **поважаючи `.gitignore`** —
|
|
94
|
+
* через той самий `globby`-канон, що й `walkDir` (звідси `ALWAYS_IGNORE`). Спільне джерело
|
|
95
|
+
* для `collectRepoPaths` (Type A glob-матчинг) і `collectAutoRuleFacts` (content-факти).
|
|
96
|
+
* Раніше тут був ручний `readdir`-обхід із хардкод skip-набором, який ігнорував `.gitignore`
|
|
97
|
+
* і помилково активував правила на згенерованих артефактах (`coverage/*.png` → image-compress).
|
|
98
|
+
* @param {string} root абсолютний шлях кореня репозиторію
|
|
99
|
+
* @returns {Promise<{ files: string[], dirs: string[] }>} relative-posix шляхи файлів і директорій
|
|
100
|
+
*/
|
|
101
|
+
async function collectTreePaths(root) {
|
|
102
|
+
const opts = { cwd: root, gitignore: true, dot: true, ignore: ALWAYS_IGNORE }
|
|
103
|
+
try {
|
|
104
|
+
const [files, dirs] = await Promise.all([
|
|
105
|
+
globby('**/*', { ...opts, onlyFiles: true }),
|
|
106
|
+
globby('**/*', { ...opts, onlyDirectories: true })
|
|
107
|
+
])
|
|
108
|
+
return { files, dirs }
|
|
109
|
+
} catch {
|
|
110
|
+
return { files: [], dirs: [] }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
90
114
|
/**
|
|
91
115
|
* Чи містить текст джерела імпорт імені `sql` або `SQL` з `"bun"` (після витягування `<script>` у `.vue`).
|
|
92
116
|
* @param {string} content вміст файлу
|
|
@@ -224,35 +248,13 @@ export async function collectAutoRuleFacts(root) {
|
|
|
224
248
|
hasTempoDir: false
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
let entries
|
|
234
|
-
try {
|
|
235
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
236
|
-
} catch {
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const entry of entries) {
|
|
241
|
-
const absPath = join(dir, entry.name)
|
|
242
|
-
if (entry.isDirectory()) {
|
|
243
|
-
if (!IGNORED_DIR_NAMES.has(entry.name)) {
|
|
244
|
-
if (entry.name === 'tempo') {
|
|
245
|
-
facts.hasTempoDir = true
|
|
246
|
-
}
|
|
247
|
-
await walk(absPath)
|
|
248
|
-
}
|
|
249
|
-
} else if (entry.isFile()) {
|
|
250
|
-
await processFileEntry(absPath, root, facts)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
251
|
+
const { files, dirs } = await collectTreePaths(root)
|
|
252
|
+
if (dirs.some(d => basename(d) === 'tempo')) {
|
|
253
|
+
facts.hasTempoDir = true
|
|
254
|
+
}
|
|
255
|
+
for (const rel of files) {
|
|
256
|
+
await processFileEntry(join(root, rel), root, facts)
|
|
253
257
|
}
|
|
254
|
-
|
|
255
|
-
await walk(root)
|
|
256
258
|
return facts
|
|
257
259
|
}
|
|
258
260
|
|
|
@@ -266,34 +268,8 @@ export async function collectAutoRuleFacts(root) {
|
|
|
266
268
|
* @returns {Promise<string[]>} шляхи відносно root у posix-форматі
|
|
267
269
|
*/
|
|
268
270
|
async function collectRepoPaths(root) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Рекурсивний обхід каталогу з пропуском службових директорій.
|
|
273
|
-
* @param {string} dir каталог
|
|
274
|
-
* @returns {Promise<void>}
|
|
275
|
-
*/
|
|
276
|
-
async function walk(dir) {
|
|
277
|
-
let entries
|
|
278
|
-
try {
|
|
279
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
280
|
-
} catch {
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
for (const entry of entries) {
|
|
284
|
-
const abs = join(dir, entry.name)
|
|
285
|
-
if (entry.isDirectory()) {
|
|
286
|
-
if (!IGNORED_DIR_NAMES.has(entry.name)) {
|
|
287
|
-
out.push(relative(root, abs).split('\\').join('/'))
|
|
288
|
-
await walk(abs)
|
|
289
|
-
}
|
|
290
|
-
} else if (entry.isFile()) {
|
|
291
|
-
out.push(relative(root, abs).split('\\').join('/'))
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
await walk(root)
|
|
296
|
-
return out
|
|
271
|
+
const { files, dirs } = await collectTreePaths(root)
|
|
272
|
+
return [...dirs, ...files]
|
|
297
273
|
}
|
|
298
274
|
|
|
299
275
|
/**
|
|
@@ -51,6 +51,19 @@ export function classifyFixError(error) {
|
|
|
51
51
|
return 'quality'
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Чи violation придатний для LLM-фіксу: містить хоч одне actionable `❌`-порушення
|
|
56
|
+
* (формат конформ-чека `❌ <file>: <інструкція>`). Без жодного ❌ — це не список
|
|
57
|
+
* фіксабельних порушень, а шум/збій тула (Usage, «лок взято», порожньо). Годувати
|
|
58
|
+
* таким агента = марні turns/timeout (вимір 7n-test: doc-files surface-ив Usage
|
|
59
|
+
* downstream-тула → агент флоундерив 4 рунги). Тоді LLM-фікс пропускаємо.
|
|
60
|
+
* @param {string|null|undefined} output violation-вивід правила
|
|
61
|
+
* @returns {boolean} true — є що фіксити
|
|
62
|
+
*/
|
|
63
|
+
export function hasActionableViolation(output) {
|
|
64
|
+
return /❌/u.test(output ?? '')
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/**
|
|
55
68
|
* Будує драбину ескалації за наявними тирами (спека 2026-06-19-fix-escalation-cascade):
|
|
56
69
|
* 1. `local-min` — `N_LOCAL_MIN_MODEL`, перший прохід;
|
|
@@ -124,6 +137,27 @@ export async function escalateRule(rule, cwd, deps) {
|
|
|
124
137
|
const skipModels = new Set()
|
|
125
138
|
let avgUsed = 0
|
|
126
139
|
|
|
140
|
+
// §2-профілактика: violation без actionable ❌ (tool-crash/Usage/шум) → не годуємо
|
|
141
|
+
// агента (інакше флоундерить рунги до timeout). Рапортуємо як non-actionable, не фіксимо.
|
|
142
|
+
if (!hasActionableViolation(rule.output)) {
|
|
143
|
+
record({
|
|
144
|
+
rung: -1,
|
|
145
|
+
tier: 'skip',
|
|
146
|
+
model: '',
|
|
147
|
+
withFeedback: false,
|
|
148
|
+
callOk: false,
|
|
149
|
+
callError: 'non-actionable violation (нема ❌ — ймовірно check-error/tool-crash)',
|
|
150
|
+
recheckOk: false,
|
|
151
|
+
remainingViolation: rule.output,
|
|
152
|
+
diagnosis: null,
|
|
153
|
+
ms: 0
|
|
154
|
+
})
|
|
155
|
+
log(
|
|
156
|
+
` ⏭️ ${rule.ruleId}: LLM-фікс пропущено — у violation немає ❌-порушень (check-error/tool-crash, не фіксабельне)`
|
|
157
|
+
)
|
|
158
|
+
return { resolved: false, avgUsed: 0 }
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
for (const [idx, rung] of ladder.entries()) {
|
|
128
162
|
if (skipModels.has(rung.model)) continue
|
|
129
163
|
|
|
@@ -8,27 +8,23 @@
|
|
|
8
8
|
* Сигнатури неоднорідні (одні беруть `facts`, інші — `cwd`/`packageJson`), бо предикати
|
|
9
9
|
* читають різні джерела; виклик диспетчиться в `auto-rules.mjs` за іменем предиката.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { readFile } from 'node:fs/promises'
|
|
12
12
|
import { join } from 'node:path'
|
|
13
13
|
|
|
14
|
+
import { findAllPackageJsonPaths } from '../utils/find-package-json-paths.mjs'
|
|
14
15
|
import { getRepositoryUrl } from './rule-meta-helpers.mjs'
|
|
15
16
|
|
|
16
|
-
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo'])
|
|
17
|
-
|
|
18
17
|
/**
|
|
19
18
|
* Чи package.json дерева містить будь-який із зазначених пакетів у dependencies.
|
|
19
|
+
* Обхід — через `findAllPackageJsonPaths` (на `walkDir`/`globby`), тож **поважає `.gitignore`**
|
|
20
|
+
* і не зчитує package.json з ігнорованих каталогів (build-артефакти, vendored-копії).
|
|
20
21
|
* @param {string} root корінь репо
|
|
21
22
|
* @param {string[]} keys імена пакетів
|
|
22
23
|
* @returns {Promise<boolean>} true, якщо знайдено хоч один
|
|
23
24
|
*/
|
|
24
|
-
function anyDepInTree(root, keys) {
|
|
25
|
+
async function anyDepInTree(root, keys) {
|
|
25
26
|
const wanted = new Set(keys)
|
|
26
|
-
|
|
27
|
-
* Чи package.json за `abs` оголошує будь-який пакет із `wanted` у `dependencies`.
|
|
28
|
-
* @param {string} abs шлях до package.json
|
|
29
|
-
* @returns {Promise<boolean>} true, якщо знайдено хоч один
|
|
30
|
-
*/
|
|
31
|
-
async function pkgDeclaresWanted(abs) {
|
|
27
|
+
for (const abs of await findAllPackageJsonPaths(root, [])) {
|
|
32
28
|
try {
|
|
33
29
|
const deps = JSON.parse(await readFile(abs, 'utf8'))?.dependencies
|
|
34
30
|
if (deps && typeof deps === 'object' && !Array.isArray(deps)) {
|
|
@@ -37,70 +33,29 @@ function anyDepInTree(root, keys) {
|
|
|
37
33
|
} catch {
|
|
38
34
|
/* ігноруємо пошкоджені package.json */
|
|
39
35
|
}
|
|
40
|
-
return false
|
|
41
36
|
}
|
|
42
|
-
|
|
43
|
-
* @param {string} dir каталог обходу
|
|
44
|
-
* @returns {Promise<boolean>} true, якщо знайдено хоч один пакет
|
|
45
|
-
*/
|
|
46
|
-
async function walk(dir) {
|
|
47
|
-
let entries
|
|
48
|
-
try {
|
|
49
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
50
|
-
} catch {
|
|
51
|
-
return false
|
|
52
|
-
}
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
const abs = join(dir, entry.name)
|
|
55
|
-
if (entry.isDirectory()) {
|
|
56
|
-
if (!IGNORED_DIR_NAMES.has(entry.name) && (await walk(abs))) return true
|
|
57
|
-
} else if (entry.isFile() && entry.name === 'package.json' && (await pkgDeclaresWanted(abs))) {
|
|
58
|
-
return true
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
return walk(root)
|
|
37
|
+
return false
|
|
64
38
|
}
|
|
65
39
|
|
|
66
40
|
/**
|
|
67
41
|
* Чи існує вкладений (не кореневий) package.json без `vite` у devDependencies.
|
|
42
|
+
* Обхід — `findAllPackageJsonPaths` (gitignore-aware), як у `anyDepInTree`.
|
|
68
43
|
* @param {string} root корінь репо
|
|
69
44
|
* @returns {Promise<boolean>} true, якщо знайдено
|
|
70
45
|
*/
|
|
71
46
|
async function nestedWithoutVite(root) {
|
|
72
47
|
const rootPkg = join(root, 'package.json')
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* @param {string} dir каталог
|
|
76
|
-
* @returns {Promise<void>}
|
|
77
|
-
*/
|
|
78
|
-
async function walk(dir) {
|
|
79
|
-
if (result) return
|
|
80
|
-
let entries
|
|
48
|
+
for (const abs of await findAllPackageJsonPaths(root, [])) {
|
|
49
|
+
if (abs === rootPkg) continue
|
|
81
50
|
try {
|
|
82
|
-
|
|
51
|
+
const dev = JSON.parse(await readFile(abs, 'utf8'))?.devDependencies
|
|
52
|
+
const hasVite = dev && typeof dev === 'object' && !Array.isArray(dev) && Object.hasOwn(dev, 'vite')
|
|
53
|
+
if (!hasVite) return true
|
|
83
54
|
} catch {
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
if (result) return
|
|
88
|
-
const abs = join(dir, entry.name)
|
|
89
|
-
if (entry.isDirectory()) {
|
|
90
|
-
if (!IGNORED_DIR_NAMES.has(entry.name)) await walk(abs)
|
|
91
|
-
} else if (entry.isFile() && entry.name === 'package.json' && abs !== rootPkg) {
|
|
92
|
-
try {
|
|
93
|
-
const dev = JSON.parse(await readFile(abs, 'utf8'))?.devDependencies
|
|
94
|
-
const hasVite = dev && typeof dev === 'object' && !Array.isArray(dev) && Object.hasOwn(dev, 'vite')
|
|
95
|
-
if (!hasVite) result = true
|
|
96
|
-
} catch {
|
|
97
|
-
/* пошкоджений package.json не вважаємо vite-проєктом */
|
|
98
|
-
}
|
|
99
|
-
}
|
|
55
|
+
/* пошкоджений package.json не вважаємо vite-проєктом */
|
|
100
56
|
}
|
|
101
57
|
}
|
|
102
|
-
|
|
103
|
-
return result
|
|
58
|
+
return false
|
|
104
59
|
}
|
|
105
60
|
|
|
106
61
|
/** Реєстр предикатів: імʼя → реалізація. Виклик за `meta.json.auto.predicate`. */
|
|
@@ -4,7 +4,7 @@ import { globby } from 'globby'
|
|
|
4
4
|
|
|
5
5
|
// .git ніколи не потрапляє в .gitignore — пропускаємо завжди.
|
|
6
6
|
// node_modules — safety net: проєкт може не мати .gitignore або запускатись поза git-репо.
|
|
7
|
-
const ALWAYS_IGNORE = ['.git/**', 'node_modules/**']
|
|
7
|
+
export const ALWAYS_IGNORE = ['.git/**', 'node_modules/**']
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Рекурсивно обходить каталог, поважаючи .gitignore (включно з вкладеними).
|