@nitra/cursor 12.16.2 → 12.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/bin/n-cursor.js +1 -1
- package/lib/docs/pi-agent-fix.md +1 -1
- package/lib/pi-agent-fix.mjs +1 -1
- package/package.json +1 -1
- package/rules/js/js/data/tooling/oxlint-canonical.json +1 -1
- package/scripts/auto-rules.mjs +34 -58
- package/scripts/lib/fix/docs/index.md +0 -1
- package/scripts/lib/fix/docs/orchestrator.md +1 -1
- package/scripts/lib/fix/orchestrator.mjs +3 -40
- package/scripts/lib/rule-predicates.mjs +15 -60
- package/scripts/utils/walkDir.mjs +1 -1
- package/types/bin/n-cursor.d.ts +1 -1
- package/scripts/lib/fix/docs/escalation-log.md +0 -28
- package/scripts/lib/fix/escalation-log.mjs +0 -92
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.18.0] - 2026-06-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Видалено escalation-лог та його тести, оптимізовано pi-agent-fix та orchestrator
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- CLI lint: не відкидати перший позиційний rule-аргумент, коли `--cwd` не передано
|
|
12
|
+
|
|
13
|
+
## [12.17.0] - 2026-06-27
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Перехід на globby для обходу файлової системи з урахуванням .gitignore
|
|
18
|
+
|
|
3
19
|
## [12.16.2] - 2026-06-27
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -1518,7 +1518,7 @@ try {
|
|
|
1518
1518
|
// прогнати лише конформність цих правил, без лінтер-скану (мапить колишній `fix <rule>`).
|
|
1519
1519
|
const cwdIdx = args.indexOf('--cwd')
|
|
1520
1520
|
const cwdArg = cwdIdx !== -1 ? resolve(args[cwdIdx + 1]) : undefined
|
|
1521
|
-
const rules = args.filter((a, i) => !a.startsWith('-') &&
|
|
1521
|
+
const rules = args.filter((a, i) => !a.startsWith('-') && !(cwdIdx !== -1 && i === cwdIdx + 1))
|
|
1522
1522
|
process.exitCode = await runLint({
|
|
1523
1523
|
full: args.includes('--full'),
|
|
1524
1524
|
readOnly: args.includes('--read-only'),
|
package/lib/docs/pi-agent-fix.md
CHANGED
package/lib/pi-agent-fix.mjs
CHANGED
|
@@ -243,7 +243,7 @@ export async function runPiAgentFix(ruleId, violation, cwd, opts = {}) {
|
|
|
243
243
|
rung: tier,
|
|
244
244
|
model: modelSpec,
|
|
245
245
|
cwd,
|
|
246
|
-
// ВХІД LLM (щоб «що подали» було видно у trace
|
|
246
|
+
// ВХІД LLM (щоб «що подали» було видно прямо у trace):
|
|
247
247
|
// violation — обрізаний (може бути великим); promptChars — повний розмір промпта.
|
|
248
248
|
violation: typeof violation === 'string' ? violation.slice(0, 4000) : null,
|
|
249
249
|
violationChars: typeof violation === 'string' ? violation.length : 0,
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"e18e/prefer-nullish-coalescing": "deny",
|
|
13
13
|
"e18e/prefer-spread-syntax": "deny",
|
|
14
14
|
"e18e/prefer-url-canparse": "deny",
|
|
15
|
-
"e18e/ban-dependencies": "deny",
|
|
15
|
+
"e18e/ban-dependencies": ["deny", { "allowed": ["globby"] }],
|
|
16
16
|
"e18e/prefer-array-from-map": "deny",
|
|
17
17
|
"e18e/prefer-timer-args": "deny",
|
|
18
18
|
"e18e/prefer-static-regex": "deny",
|
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
|
/**
|
|
@@ -9,7 +9,6 @@ resource: npm/scripts/lib/fix/
|
|
|
9
9
|
| Файл | Тип |
|
|
10
10
|
| ----------------------------------------------------- | --------- |
|
|
11
11
|
| [discover-t0-patterns.mjs](discover-t0-patterns.md) | JS Module |
|
|
12
|
-
| [escalation-log.mjs](escalation-log.md) | JS Module |
|
|
13
12
|
| [llm-fix-apply.mjs](llm-fix-apply.md) | JS Module |
|
|
14
13
|
| [llm-lint-fix.mjs](llm-lint-fix.md) | JS Module |
|
|
15
14
|
| [llm-worker.mjs](llm-worker.md) | JS Module |
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { env } from 'node:process'
|
|
4
4
|
import { runConformanceCheck } from './run-conformance-check.mjs'
|
|
5
5
|
import { runT0AutoCli } from './t0.mjs'
|
|
6
|
-
import { logEscalation } from './escalation-log.mjs'
|
|
7
6
|
import { runPiAgentFix } from '../../../lib/pi-agent-fix.mjs'
|
|
8
7
|
import { recordFixTelemetry } from '../../../lib/pi-telemetry-store.mjs'
|
|
9
8
|
import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/pi-model-tiers.mjs'
|
|
@@ -112,7 +111,7 @@ function decideAfterFailure(rung, error) {
|
|
|
112
111
|
/**
|
|
113
112
|
* Проводить ОДНЕ правило по драбині ескалації до першого зеленого re-check.
|
|
114
113
|
* Кожен рунг: виклик worker (з feedback від попереднього) → re-check цього правила →
|
|
115
|
-
*
|
|
114
|
+
* persist у глобальний telemetry-стор (`recordFixTelemetry`). Достроковий вихід — `decideAfterFailure`
|
|
116
115
|
* (обрив на no-key, пропуск моделі на systemic) і вичерпаний avg-кеп (залогувати, не мовчки).
|
|
117
116
|
* @param {{ ruleId: string, output: string }} rule провальне правило з violation-output
|
|
118
117
|
* @param {string} cwd корінь проєкту
|
|
@@ -121,16 +120,13 @@ function decideAfterFailure(rung, error) {
|
|
|
121
120
|
* worker: { runFix: (ruleId: string, violation: string, cwd: string, opts: object) => Promise<object> },
|
|
122
121
|
* check: (rules: string[], cwd: string) => Promise<{rules: Array<{ruleId:string,ok:boolean,output:string}>}>,
|
|
123
122
|
* avgBudget: number,
|
|
124
|
-
* clock?: () => number,
|
|
125
123
|
* log?: (s: string) => void
|
|
126
|
-
* }} deps інжектовані залежності (worker/check
|
|
124
|
+
* }} deps інжектовані залежності (worker/check — для тестів)
|
|
127
125
|
* @returns {Promise<{ resolved: boolean, avgUsed: number }>} чи закрито правило і скільки avg-викликів витрачено
|
|
128
126
|
*/
|
|
129
127
|
export async function escalateRule(rule, cwd, deps) {
|
|
130
128
|
const { ladder, worker, check, avgBudget } = deps
|
|
131
|
-
const clock = deps.clock ?? (() => Date.now())
|
|
132
129
|
const log = deps.log ?? (s => console.log(s))
|
|
133
|
-
const record = base => logEscalation({ ts: new Date(clock()).toISOString(), ruleId: rule.ruleId, ...base })
|
|
134
130
|
|
|
135
131
|
let feedback = null
|
|
136
132
|
let currentViolation = rule.output
|
|
@@ -140,43 +136,20 @@ export async function escalateRule(rule, cwd, deps) {
|
|
|
140
136
|
// §2-профілактика: violation без actionable ❌ (tool-crash/Usage/шум) → не годуємо
|
|
141
137
|
// агента (інакше флоундерить рунги до timeout). Рапортуємо як non-actionable, не фіксимо.
|
|
142
138
|
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
139
|
log(
|
|
156
140
|
` ⏭️ ${rule.ruleId}: LLM-фікс пропущено — у violation немає ❌-порушень (check-error/tool-crash, не фіксабельне)`
|
|
157
141
|
)
|
|
158
142
|
return { resolved: false, avgUsed: 0 }
|
|
159
143
|
}
|
|
160
144
|
|
|
161
|
-
for (const
|
|
145
|
+
for (const rung of ladder) {
|
|
162
146
|
if (skipModels.has(rung.model)) continue
|
|
163
147
|
|
|
164
|
-
const common = { rung: idx, tier: rung.tier, model: rung.model, withFeedback: rung.feedback }
|
|
165
148
|
if (rung.isAvg && avgBudget - avgUsed <= 0) {
|
|
166
|
-
record({
|
|
167
|
-
...common,
|
|
168
|
-
callOk: false,
|
|
169
|
-
callError: 'cloud-avg cap reached',
|
|
170
|
-
recheckOk: false,
|
|
171
|
-
remainingViolation: currentViolation,
|
|
172
|
-
diagnosis: null,
|
|
173
|
-
ms: 0
|
|
174
|
-
})
|
|
175
149
|
log(` ⏭️ ${rule.ruleId}: ${rung.tier} пропущено (avg-кеп вичерпано)`)
|
|
176
150
|
continue
|
|
177
151
|
}
|
|
178
152
|
|
|
179
|
-
const startedAt = clock()
|
|
180
153
|
// self_check (advisory §4+5) — той самий verdict-helper, що й зовнішній re-check.
|
|
181
154
|
const selfCheck = async () => {
|
|
182
155
|
const r = await check([rule.ruleId], cwd)
|
|
@@ -207,16 +180,6 @@ export async function escalateRule(rule, cwd, deps) {
|
|
|
207
180
|
})
|
|
208
181
|
}
|
|
209
182
|
|
|
210
|
-
record({
|
|
211
|
-
...common,
|
|
212
|
-
callOk: !res.error,
|
|
213
|
-
callError: res.error ?? null,
|
|
214
|
-
recheckOk,
|
|
215
|
-
remainingViolation: remaining,
|
|
216
|
-
diagnosis: res.telemetry ? `turns=${res.telemetry.turnCount} tools=${res.telemetry.toolCallCount}` : null,
|
|
217
|
-
ms: clock() - startedAt
|
|
218
|
-
})
|
|
219
|
-
|
|
220
183
|
if (recheckOk) {
|
|
221
184
|
log(` ✅ ${rung.tier} (${rung.model || 'pi'}): ${rule.ruleId}`)
|
|
222
185
|
return { resolved: true, avgUsed }
|
|
@@ -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 (включно з вкладеними).
|
package/types/bin/n-cursor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {}
|
|
2
|
+
export {};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: JS Module
|
|
3
|
-
title: escalation-log.mjs
|
|
4
|
-
resource: npm/scripts/lib/fix/escalation-log.mjs
|
|
5
|
-
docgen:
|
|
6
|
-
crc: 91898427
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Огляд
|
|
12
|
-
|
|
13
|
-
Файл створює append-only JSONL-лог ескалації конформність-фіксу (спека 2026-06-19-fix-escalation-cascade-design). Він фіксує кожен запис на рунг драбини, що включає деталі використаної моделі (`model`), подання зворотного зв'язку (`withFeedback`), результат виклику (`callOk`/`callError`), оцінку, чи допомогло це усунути порушення (`recheckOk`), залишковий `violation` та самоаналіз моделі (`diagnosis`). Цей лог доповнює always-on wire-trace (`lib/pi-trace.mjs`) і формує логіку для join за полем `caller` (`fix:<rule>:<rung>`). Запис відбувається за певним шільною: або через kill-switch `N_CURSOR_FIX_ESCALATION_LOG`, або за явним шляхом, дефолтно у `<cwd>/.n-cursor/fix-escalation.jsonl`.
|
|
14
|
-
|
|
15
|
-
## Поведінка
|
|
16
|
-
|
|
17
|
-
Поведінка:
|
|
18
|
-
escalationLogPath визначає шлях до файлу журналу ескалації конформності. Якщо встановлено змінну середовища N_CURSOR_FIX_ESCALATION_LOG, використовується цей шлях; інакше, за замовчуванням, це .n-cursor/fix-escalation.jsonl у поточній робочій директорії.
|
|
19
|
-
logEscalation записує один запис про рунг у JSONL-лог, якщо шлях до логу визначено. Запис містить метадані про спробу фіксування, результати виклику та аналіз. Помилки під час запису логу ігноруються.
|
|
20
|
-
|
|
21
|
-
## Публічний API
|
|
22
|
-
|
|
23
|
-
escalationLogPath — вказує на місце зберігання логу ескалацій, якщо функція не вимкнена.
|
|
24
|
-
logEscalation — записує один подію виконання в спеціальний лог у форматі JSONL, ігноруючи внутрішні помилки запису.
|
|
25
|
-
|
|
26
|
-
## Гарантії поведінки
|
|
27
|
-
|
|
28
|
-
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Append-only JSONL-лог ескалації конформність-фіксу (спека
|
|
3
|
-
* 2026-06-19-fix-escalation-cascade-design). Один запис **на рунг драбини** —
|
|
4
|
-
* фіксує `model`, `withFeedback`, чи виклик удався (`callOk`/`callError`), чи
|
|
5
|
-
* правило стало зеленим після цього рунга (`recheckOk` = «чи допомогло»),
|
|
6
|
-
* залишковий violation і `diagnosis` (само-аналіз моделі «чому не вдалося»).
|
|
7
|
-
*
|
|
8
|
-
* Це доповнення до always-on wire-trace (`lib/pi-trace.mjs`): trace знає
|
|
9
|
-
* `messages`/`reasoning`/`usage` кожного виклику, але **не** знає результату
|
|
10
|
-
* re-check — а саме «чи допомогло» й потрібне для пост-аналізу драбини. Join із
|
|
11
|
-
* trace — за полем `caller` (`fix:<rule>:<rung>`), яке цей модуль і формує.
|
|
12
|
-
*
|
|
13
|
-
* Шлях — дзеркало `tracePath()`: `N_CURSOR_FIX_ESCALATION_LOG` (kill-switch
|
|
14
|
-
* `0|false|off|no` → не писати; інакше явний шлях) → дефолт
|
|
15
|
-
* `<cwd>/.n-cursor/fix-escalation.jsonl`.
|
|
16
|
-
*/
|
|
17
|
-
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
18
|
-
import { dirname, join } from 'node:path'
|
|
19
|
-
import { cwd, env } from 'node:process'
|
|
20
|
-
|
|
21
|
-
/** Значення `N_CURSOR_FIX_ESCALATION_LOG`, що вимикають лог повністю. */
|
|
22
|
-
const KILL_VALUES = new Set(['0', 'false', 'off', 'no'])
|
|
23
|
-
|
|
24
|
-
/** Межа обрізки `remainingViolation`/`diagnosis` у записі (символів). */
|
|
25
|
-
const MAX_FIELD_CHARS = 2000
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Шлях активного escalation-логу або `null`, якщо вимкнено kill-switch-ем.
|
|
29
|
-
* @returns {string|null} шлях до .jsonl або null
|
|
30
|
-
*/
|
|
31
|
-
export function escalationLogPath() {
|
|
32
|
-
const override = env.N_CURSOR_FIX_ESCALATION_LOG
|
|
33
|
-
if (override !== undefined) {
|
|
34
|
-
if (KILL_VALUES.has(override.toLowerCase())) return null
|
|
35
|
-
if (override) return override
|
|
36
|
-
}
|
|
37
|
-
return join(cwd(), '.n-cursor', 'fix-escalation.jsonl')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Обрізає рядок до `MAX_FIELD_CHARS` (null/undefined → null).
|
|
42
|
-
* @param {string|null|undefined} s вхід
|
|
43
|
-
* @returns {string|null} обрізаний рядок або null
|
|
44
|
-
*/
|
|
45
|
-
function cap(s) {
|
|
46
|
-
if (s === null || s === undefined) return null
|
|
47
|
-
return s.length > MAX_FIELD_CHARS ? s.slice(0, MAX_FIELD_CHARS) : s
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Дописує один запис рунга у JSONL-лог (no-op, якщо вимкнено). Помилки запису
|
|
52
|
-
* ковтаються — лог діагностичний, не має валити сам фікс.
|
|
53
|
-
* @param {object} rec запис рунга
|
|
54
|
-
* @param {string} rec.ts ISO-час завершення рунга
|
|
55
|
-
* @param {string} rec.ruleId id правила
|
|
56
|
-
* @param {number} rec.rung індекс рунга драбини (0-based)
|
|
57
|
-
* @param {string} rec.tier мітка тиру (`local-min`|`local-min-retry`|`cloud-min`|`cloud-avg`)
|
|
58
|
-
* @param {string} rec.model model-id (порожній → pi-дефолт)
|
|
59
|
-
* @param {boolean} rec.withFeedback чи передавався feedback попереднього рунга
|
|
60
|
-
* @param {boolean} rec.callOk чи виклик моделі+apply удався
|
|
61
|
-
* @param {string|null} rec.callError помилка виклику (null, якщо callOk)
|
|
62
|
-
* @param {boolean} rec.recheckOk чи правило стало зеленим після рунга («чи допомогло»)
|
|
63
|
-
* @param {string|null} rec.remainingViolation залишковий violation (null, якщо recheckOk)
|
|
64
|
-
* @param {string|null} rec.diagnosis само-аналіз моделі «чому попередній рунг не вдався»
|
|
65
|
-
* @param {number} rec.ms тривалість рунга (мс)
|
|
66
|
-
* @returns {void}
|
|
67
|
-
*/
|
|
68
|
-
export function logEscalation(rec) {
|
|
69
|
-
const path = escalationLogPath()
|
|
70
|
-
if (!path) return
|
|
71
|
-
const line =
|
|
72
|
-
JSON.stringify({
|
|
73
|
-
ts: rec.ts,
|
|
74
|
-
ruleId: rec.ruleId,
|
|
75
|
-
rung: rec.rung,
|
|
76
|
-
tier: rec.tier,
|
|
77
|
-
model: rec.model,
|
|
78
|
-
withFeedback: rec.withFeedback,
|
|
79
|
-
callOk: rec.callOk,
|
|
80
|
-
callError: cap(rec.callError),
|
|
81
|
-
recheckOk: rec.recheckOk,
|
|
82
|
-
remainingViolation: rec.recheckOk ? null : cap(rec.remainingViolation),
|
|
83
|
-
diagnosis: cap(rec.diagnosis),
|
|
84
|
-
ms: rec.ms
|
|
85
|
-
}) + '\n'
|
|
86
|
-
try {
|
|
87
|
-
mkdirSync(dirname(path), { recursive: true })
|
|
88
|
-
appendFileSync(path, line, 'utf8')
|
|
89
|
-
} catch {
|
|
90
|
-
/* лог діагностичний — ковтаємо помилки запису */
|
|
91
|
-
}
|
|
92
|
-
}
|