@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 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('-') && i !== cwdIdx + 1)
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'),
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: pi-agent-fix.mjs
4
4
  resource: npm/lib/pi-agent-fix.mjs
5
5
  docgen:
6
- crc: 4cf083e2
6
+ crc: 3e849981
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -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, без розкопок escalation-log):
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.16.2",
3
+ "version": "12.18.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -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",
@@ -16,10 +16,13 @@
16
16
  * їх у конфіг із поправкою на legacy-id (`migrateRuleIds`).
17
17
  */
18
18
  import { readdirSync } from 'node:fs'
19
- import { readdir, readFile } from 'node:fs/promises'
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
- * @param {string} dir абсолютний шлях каталогу
230
- * @returns {Promise<void>}
231
- */
232
- async function walk(dir) {
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
- /** @type {string[]} */
270
- const out = []
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,7 @@ type: JS Module
3
3
  title: orchestrator.mjs
4
4
  resource: npm/scripts/lib/fix/orchestrator.mjs
5
5
  docgen:
6
- crc: 49146418
6
+ crc: c16c34f7
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -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
- * запис у escalation-лог («чи допомогло» + diagnosis). Достроковий вихід — `decideAfterFailure`
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/clock — для тестів)
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 [idx, rung] of ladder.entries()) {
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 { readdir, readFile } from 'node:fs/promises'
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
- let result = false
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
- entries = await readdir(dir, { withFileTypes: true })
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
- return
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
- await walk(root)
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 (включно з вкладеними).
@@ -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
- }