@7n/test 0.13.0 → 0.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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.14.0] - 2026-07-11
4
+
5
+ ### Added
6
+
7
+ - Ланцюжки (chains) @nitra/llm-lib ^1.1.0: mutant-classify — chain per mutant (tier1/tier2 = кроки, cache hit без chain), test-generate — chain per file (header + local/cloud спроби + vitest/length-retry = кроки), test-fix — chain per прогін (batch-виклики = кроки); адаптер callText/callAgent прокидає opts.chain. Аналітика ланцюжків — n-llm-chains-report і вкладка «Ланцюжки» в myllm.
8
+
9
+ ### Changed
10
+
11
+ - підняти @7n/llm-lib до ^2.0.2 (rename @nitra/llm-lib → @7n/llm-lib уже застосовано в dacc051; нова мажорна версія без змін API — chains/компресія/body-capture, лише перейменування пакета)
12
+
3
13
  ## [0.13.0] - 2026-07-05
4
14
 
5
15
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@earendil-works/pi-coding-agent": "^0.80.3",
43
43
  "@nitra/check-env": "^4.2.1",
44
- "@nitra/llm-lib": "^1.0.0",
44
+ "@7n/llm-lib": "^2.0.2",
45
45
  "@vitest/coverage-v8": "^4.1.9",
46
46
  "rollup": "^4.62.2",
47
47
  "vitest": "^4.1.9",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: index.mjs
4
4
  resource: npm/src/coverage-classify/index.mjs
5
5
  docgen:
6
- crc: bc7320ff
6
+ crc: 6f120c02
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -10,7 +10,8 @@
10
10
  import { join } from 'node:path'
11
11
 
12
12
  import { callText } from '../lib/llm.mjs'
13
- import { CLOUD_MIN, LOCAL_MIN } from '@nitra/llm-lib/model-tiers'
13
+ import { CLOUD_MIN, LOCAL_MIN } from '@7n/llm-lib/model-tiers'
14
+ import { startChain } from '@7n/llm-lib/chain'
14
15
  import { deriveCacheKey, readCache, writeCache } from './cache.mjs'
15
16
  import { buildUserPrompt, SYSTEM_PROMPT } from './prompt.mjs'
16
17
  import { parseVerdict } from './verdict-schema.mjs'
@@ -26,52 +27,77 @@ const FALLBACK_VERDICT = {
26
27
  * @param {string} prompt
27
28
  * @param {string} model provider/model-id або '' для pi-дефолту
28
29
  * @param {string} cwd
30
+ * @param {{chain?: object}} [callOpts] chain handle поточного мутанта
29
31
  * @returns {Promise<string>}
30
32
  */
31
- function callModel(prompt, model, cwd) {
32
- return callText(prompt, { cwd, ...(model && { model }) })
33
+ function callModel(prompt, model, cwd, { chain } = {}) {
34
+ return callText(prompt, { cwd, chain, ...(model && { model }) })
33
35
  }
34
36
 
35
37
  /**
36
38
  * Два тири: tier1 (local-min) → tier2 (cloud-min) → FALLBACK_VERDICT.
39
+ * Кожен мутант — окремий ланцюжок (kind: mutant-classify): tier1 = крок 1,
40
+ * tier2 = крок 2; fallback-вердикт = outcome:'fail' (LLM не впорався).
37
41
  * @param {{file: string, mutants: object[]}} group
38
42
  * @param {object} mutant
39
43
  * @param {string} cwd
40
- * @param {(prompt: string, model: string, cwd: string) => Promise<string>} callModelFn
44
+ * @param {(prompt: string, model: string, cwd: string, callOpts?: {chain?: object}) => Promise<string>} callModelFn
41
45
  * @param {string} tier1 model-spec першого тиру ('' = pi-дефолт)
42
46
  * @param {string} tier2 model-spec другого тиру ('' = pi-дефолт)
47
+ * @param {typeof startChain} makeChain фабрика ланцюжка
43
48
  * @returns {Promise<object>} verdict
44
49
  */
45
- async function classifyOne(group, mutant, cwd, callModelFn, tier1, tier2) {
50
+ async function classifyOne(group, mutant, cwd, callModelFn, tier1, tier2, makeChain) {
46
51
  const prompt = `${SYSTEM_PROMPT}\n\n${buildUserPrompt({ ...mutant, file: group.file }, cwd)}`
47
52
  const loc = `${group.file}:${mutant.line}:${mutant.col}`
53
+ const chain = makeChain({ kind: 'mutant-classify', unit: loc, cwd })
48
54
 
49
55
  try {
50
- const text = await callModelFn(prompt, tier1, cwd)
51
- return parseVerdict(text)
56
+ const text = await callModelFn(prompt, tier1, cwd, { chain })
57
+ const verdict = parseVerdict(text)
58
+ chain.end({ outcome: 'success', extra: verdictExtra(verdict, mutant) })
59
+ return verdict
52
60
  } catch {
53
61
  try {
54
- const text = await callModelFn(prompt, tier2, cwd)
55
- return parseVerdict(text)
62
+ const text = await callModelFn(prompt, tier2, cwd, { chain })
63
+ const verdict = parseVerdict(text)
64
+ chain.end({ outcome: 'success', extra: verdictExtra(verdict, mutant) })
65
+ return verdict
56
66
  } catch (error) {
57
67
  console.warn(`⚠ coverage classify: ${loc} both tiers failed: ${error.message}`)
68
+ chain.end({ outcome: 'fail', extra: { error: String(error.message ?? error).slice(0, 200) } })
58
69
  return { ...FALLBACK_VERDICT }
59
70
  }
60
71
  }
61
72
  }
62
73
 
74
+ /**
75
+ * Extra-поля фінального chain-запису мутанта.
76
+ * @param {{verdict: string, confidence: number}} verdict розпарсений вердикт
77
+ * @param {{replacement?: string}} mutant мутант
78
+ * @returns {object} extra
79
+ */
80
+ function verdictExtra(verdict, mutant) {
81
+ return {
82
+ verdict: verdict.verdict,
83
+ confidence: verdict.confidence,
84
+ replacement: String(mutant.replacement ?? '').slice(0, 120)
85
+ }
86
+ }
87
+
63
88
  /**
64
89
  * Класифікує survived мутантів через pi (N_LOCAL_MIN_MODEL → N_CLOUD_MIN_MODEL → fallback).
65
90
  * @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived
66
91
  * @param {string} cwd
67
- * @param {{cachePath?: string, callModel?: (prompt: string, model: string, cwd: string) => Promise<string>,
68
- * tier1?: string, tier2?: string}} [opts] `tier1`/`tier2` — явні model-specs (дефолт: LOCAL_MIN/CLOUD_MIN пакета;
69
- * інжектовні, бо тир-константи фіксуються при імпорті й у тестах не стабляться через env)
92
+ * @param {{cachePath?: string, callModel?: (prompt: string, model: string, cwd: string, callOpts?: {chain?: object}) => Promise<string>,
93
+ * tier1?: string, tier2?: string, startChain?: typeof startChain}} [opts] `tier1`/`tier2` — явні model-specs (дефолт: LOCAL_MIN/CLOUD_MIN пакета;
94
+ * інжектовні, бо тир-константи фіксуються при імпорті й у тестах не стабляться через env); `startChain` — фабрика ланцюжка (інжект для тестів)
70
95
  * @returns {Promise<Array<{key: string, verdict: object}>>}
71
96
  */
72
97
  export async function classify(survived, cwd, opts = {}) {
73
98
  const cachePath = opts.cachePath ?? join(cwd, 'npm/reports/coverage-classify.cache.json')
74
99
  const callModelFn = opts.callModel ?? callModel
100
+ const makeChain = opts.startChain ?? startChain
75
101
  const tier1 = opts.tier1 ?? LOCAL_MIN
76
102
  const tier2 = opts.tier2 ?? CLOUD_MIN
77
103
  const cacheModel = `${tier1 || 'default'}+${tier2 || 'cloud'}`
@@ -99,7 +125,7 @@ export async function classify(survived, cwd, opts = {}) {
99
125
  }
100
126
  }
101
127
  if (!verdict) {
102
- verdict = await classifyOne(group, mutant, cwd, callModelFn, tier1, tier2)
128
+ verdict = await classifyOne(group, mutant, cwd, callModelFn, tier1, tier2, makeChain)
103
129
  if (cacheKey) {
104
130
  cache.entries[cacheKey] = { ...verdict, classifiedAt: new Date().toISOString() }
105
131
  }
@@ -9,7 +9,7 @@
9
9
  import { readFile } from 'node:fs/promises'
10
10
  import { join } from 'node:path'
11
11
  import { env } from 'node:process'
12
- import { CLOUD_MAX } from '@nitra/llm-lib/model-tiers'
12
+ import { CLOUD_MAX } from '@7n/llm-lib/model-tiers'
13
13
  import { callAgent } from './lib/llm.mjs'
14
14
 
15
15
  const MODEL = env.N_CURSOR_COVERAGE_FIX_MODEL ?? CLOUD_MAX
@@ -41,7 +41,7 @@ export async function fixSurvivedMutants(survived, projectRoot, opts = {}) {
41
41
  }
42
42
 
43
43
  /**
44
- * Викликає агента через @nitra/llm-lib (SDK-embed, live-output до stdout) —
44
+ * Викликає агента через `@7n/llm-lib` (SDK-embed, live-output до stdout) —
45
45
  * заміна колишнього spawnSync pi CLI.
46
46
  * @param {string} prompt текст промпта
47
47
  * @param {string} model provider/model-id або '' для pi-дефолту
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: assess-need.mjs
4
4
  resource: npm/src/assess-need.mjs
5
5
  docgen:
6
- crc: 960f8332
6
+ crc: 20c3d440
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: coverage-fix.mjs
4
4
  resource: npm/src/coverage-fix.mjs
5
5
  docgen:
6
- crc: d0a20c67
6
+ crc: db577cf1
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-tests.mjs
4
4
  resource: npm/src/fix-tests.mjs
5
5
  docgen:
6
- crc: 6597b7fc
6
+ crc: 713e7889
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: gen-tests.mjs
4
4
  resource: npm/src/gen-tests.mjs
5
5
  docgen:
6
- crc: 99d3e9b7
6
+ crc: 0183f9b9
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
package/src/fix-tests.mjs CHANGED
@@ -20,8 +20,9 @@ import { tmpdir } from 'node:os'
20
20
  import { join, relative } from 'node:path'
21
21
  import { env } from 'node:process'
22
22
  import { callText, MEMORY_ERROR_RE } from './lib/llm.mjs'
23
- import { CLOUD_MAX } from '@nitra/llm-lib/model-tiers'
24
- import { budgetFor, capText, packBatch } from '@nitra/llm-lib/prompt-budget'
23
+ import { CLOUD_MAX } from '@7n/llm-lib/model-tiers'
24
+ import { startChain } from '@7n/llm-lib/chain'
25
+ import { budgetFor, capText, packBatch } from '@7n/llm-lib/prompt-budget'
25
26
  import { findTestRules } from './gen-tests.mjs'
26
27
  import { parseFailingTests } from './coverage-per-file.mjs'
27
28
  import { resolveVitestRun } from './lib/vitest-shim.mjs'
@@ -322,17 +323,28 @@ function writeFixedFiles(fixed, remaining, dir) {
322
323
  * @param {string} dir project root
323
324
  * @param {{
324
325
  * failures?: Array<{file: string, errors: string[]}>,
325
- * callTextFn?: (prompt: string, opts?: object) => Promise<string>
326
- * }} [opts] overrides for callText and pre-fetched failures
326
+ * callTextFn?: (prompt: string, opts?: object) => Promise<string>,
327
+ * startChain?: typeof startChain
328
+ * }} [opts] overrides for callText, pre-fetched failures і фабрика ланцюжка (інжект для тестів)
327
329
  * @returns {Promise<{count: number, fixed: number, remaining: number}>} fix result summary
328
330
  */
329
331
  export async function fixFailingTests(dir, opts = {}) {
330
- const callTextFn =
331
- opts.callTextFn ?? (prompt => callText(prompt, { model: MODEL, cwd: dir, maxTokens: budgetFor('fix').maxTokens }))
332
332
  const failures = opts.failures ?? (await getFailingTests(dir))
333
333
 
334
334
  if (failures.length === 0) return { count: 0, fixed: 0, remaining: 0 }
335
335
 
336
+ // Один ланцюжок на прогін: батчі змішують файли, per-file chain неможливий
337
+ // без ламання батчингу; кожен batch-виклик = крок.
338
+ const chain = (opts.startChain ?? startChain)({
339
+ kind: 'test-fix',
340
+ unit: `fix:${failures.length}files`,
341
+ cwd: dir
342
+ })
343
+ let batches = 0
344
+ const callTextFn =
345
+ opts.callTextFn ??
346
+ (prompt => callText(prompt, { model: MODEL, cwd: dir, maxTokens: budgetFor('fix').maxTokens, chain }))
347
+
336
348
  console.log(`\n🔧 Виправляю ${failures.length} падаючих test-файлів (pi text mode)...\n`)
337
349
  for (const f of failures) {
338
350
  console.log(` • ${f.file} (${f.errors.length} помилок)`)
@@ -342,50 +354,64 @@ export async function fixFailingTests(dir, opts = {}) {
342
354
  let remaining = failures
343
355
  const attempts = new Map()
344
356
  let prevDeferred = new Set()
345
- for (;;) {
346
- // Спроби рахуються per-file (лише коли файл реально був у батчі),
347
- // тож deferred-черга не з'їдає ліміт файлів, які ще не пробували
348
- const eligible = remaining
349
- .filter(f => (attempts.get(f.file) ?? 0) < MAX_FIX_ATTEMPTS)
350
- // Анти-starvation: відкладені минулого разу файли йдуть першими
351
- .toSorted((a, b) => (prevDeferred.has(b.file) ? 1 : 0) - (prevDeferred.has(a.file) ? 1 : 0))
352
- if (eligible.length === 0) break
353
-
354
- const batch = buildFixTestsBatch(eligible, dir)
355
- if (batch.deferred.length) {
356
- console.log(
357
- ` 📦 батч: ${batch.included.length} файлів, відкладено на наступний прохід: ${batch.deferred.length}`
357
+ try {
358
+ for (;;) {
359
+ // Спроби рахуються per-file (лише коли файл реально був у батчі),
360
+ // тож deferred-черга не з'їдає ліміт файлів, які ще не пробували
361
+ const eligible = remaining
362
+ .filter(f => (attempts.get(f.file) ?? 0) < MAX_FIX_ATTEMPTS)
363
+ // Анти-starvation: відкладені минулого разу файли йдуть першими
364
+ .toSorted((a, b) => (prevDeferred.has(b.file) ? 1 : 0) - (prevDeferred.has(a.file) ? 1 : 0))
365
+ if (eligible.length === 0) break
366
+
367
+ const batch = buildFixTestsBatch(eligible, dir)
368
+ if (batch.deferred.length) {
369
+ console.log(
370
+ ` 📦 батч: ${batch.included.length} файлів, відкладено на наступний прохід: ${batch.deferred.length}`
371
+ )
372
+ }
373
+ for (const file of batch.included) attempts.set(file, (attempts.get(file) ?? 0) + 1)
374
+ prevDeferred = new Set(batch.deferred)
375
+
376
+ let response
377
+ try {
378
+ batches++
379
+ response = await callTextFn(batch.prompt)
380
+ } catch (error) {
381
+ // memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
382
+ // до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
383
+ if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
384
+ console.error(` ✗ pi помилка: ${error.message}`)
385
+ break
386
+ }
387
+
388
+ const fixed = parseFixedFiles(response)
389
+
390
+ if (fixed.length === 0) {
391
+ console.error(' ✗ pi не повернула виправлений код')
392
+ break
393
+ }
394
+
395
+ const includedSet = new Set(batch.included)
396
+ writeFixedFiles(
397
+ fixed,
398
+ eligible.filter(f => includedSet.has(f.file)),
399
+ dir
358
400
  )
359
- }
360
- for (const file of batch.included) attempts.set(file, (attempts.get(file) ?? 0) + 1)
361
- prevDeferred = new Set(batch.deferred)
362
401
 
363
- let response
364
- try {
365
- response = await callTextFn(batch.prompt)
366
- } catch (error) {
367
- // memory-guard: не звичайна per-file помилка — RAM-стеля фіксована, продовжувати
368
- // до наступного файлу немає сенсу. Пробиваємо нагору до CLI, аби процес завершився.
369
- if (MEMORY_ERROR_RE.test(error.message ?? '')) throw error
370
- console.error(` ✗ pi помилка: ${error.message}`)
371
- break
402
+ remaining = await getFailingTests(dir)
372
403
  }
373
-
374
- const fixed = parseFixedFiles(response)
375
-
376
- if (fixed.length === 0) {
377
- console.error(' ✗ pi не повернула виправлений код')
378
- break
379
- }
380
-
381
- const includedSet = new Set(batch.included)
382
- writeFixedFiles(
383
- fixed,
384
- eligible.filter(f => includedSet.has(f.file)),
385
- dir
386
- )
387
-
388
- remaining = await getFailingTests(dir)
404
+ } finally {
405
+ const fixedSoFar = failures.length - remaining.length
406
+ chain.end({
407
+ outcome: remaining.length === 0 ? 'success' : fixedSoFar > 0 ? 'partial' : 'fail',
408
+ extra: {
409
+ files: failures.map(f => f.file),
410
+ batches,
411
+ fixed: fixedSoFar,
412
+ remaining: remaining.length
413
+ }
414
+ })
389
415
  }
390
416
 
391
417
  const fixedCount = failures.length - remaining.length
package/src/gen-tests.mjs CHANGED
@@ -18,7 +18,8 @@ import { spawnSync } from 'node:child_process'
18
18
  import { join, relative, dirname } from 'node:path'
19
19
  import { callText, MEMORY_ERROR_RE } from './lib/llm.mjs'
20
20
  import { env } from 'node:process'
21
- import { budgetFor } from '@nitra/llm-lib/prompt-budget'
21
+ import { budgetFor } from '@7n/llm-lib/prompt-budget'
22
+ import { startChain } from '@7n/llm-lib/chain'
22
23
  import { resolveVitestRun } from './lib/vitest-shim.mjs'
23
24
  import { extractExportsWithComplexity } from './classify-exports.mjs'
24
25
  import { analyzeModule } from './lib/ast-analyze.mjs'
@@ -1149,9 +1150,10 @@ function resolveLocalModel(opts) {
1149
1150
  * @param {PiCallFn} callTextFn cloud LLM caller
1150
1151
  * @param {PiCallFn | null} localFn local LLM caller
1151
1152
  * @param {GenerateOneFn | undefined} generateOne custom single-file generator
1153
+ * @param {typeof startChain} [makeChain] фабрика ланцюжка (інжект для тестів)
1152
1154
  * @returns {Promise<void>} resolves after generation for this file completes
1153
1155
  */
1154
- async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generateOne) {
1156
+ async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generateOne, makeChain = startChain) {
1155
1157
  console.log(` → ${fileInfo.file} (${fileInfo.pct.toFixed(1)}%)`)
1156
1158
 
1157
1159
  if (generateOne) {
@@ -1159,13 +1161,26 @@ async function generateTestsForFile(fileInfo, dir, callTextFn, localFn, generate
1159
1161
  return
1160
1162
  }
1161
1163
 
1162
- const exportsInfo = extractExportsWithComplexity(readSourceSnippet(join(dir, fileInfo.file)))
1163
- if (localFn && exportsInfo.length > 0) {
1164
- await generatePerExport(fileInfo, dir, callTextFn, localFn)
1165
- return
1166
- }
1164
+ // Ланцюжок файлу: усі виклики (header, per-export local/cloud спроби,
1165
+ // vitest-retry, length-retry) кроки одного chain.
1166
+ const chain = makeChain({ kind: 'test-generate', unit: fileInfo.file, cwd: dir })
1167
+ const chainedCloud = (prompt, callOpts = {}) => callTextFn(prompt, { ...callOpts, chain })
1168
+ const chainedLocal = localFn ? (prompt, callOpts = {}) => localFn(prompt, { ...callOpts, chain }) : null
1169
+ let failed = null
1170
+ try {
1171
+ const exportsInfo = extractExportsWithComplexity(readSourceSnippet(join(dir, fileInfo.file)))
1172
+ if (chainedLocal && exportsInfo.length > 0) {
1173
+ await generatePerExport(fileInfo, dir, chainedCloud, chainedLocal)
1174
+ return
1175
+ }
1167
1176
 
1168
- await generateOneTest(fileInfo, dir, callTextFn)
1177
+ await generateOneTest(fileInfo, dir, chainedCloud)
1178
+ } catch (error) {
1179
+ failed = String(error.message ?? error).slice(0, 200)
1180
+ throw error
1181
+ } finally {
1182
+ chain.end({ outcome: failed ? 'fail' : 'success', extra: failed ? { error: failed } : {} })
1183
+ }
1169
1184
  }
1170
1185
 
1171
1186
  /**
package/src/index.js CHANGED
@@ -11,9 +11,22 @@ export async function run(args) {
11
11
  console.log(' Runs coverage analysis, generates missing tests, then mutation testing.')
12
12
  console.log(' --no-mutation Skip mutation testing phase.')
13
13
  console.log(' Defaults to current directory when no argument is given.')
14
+ console.log('Usage: n coverage [--fix] [--changed]')
15
+ console.log(' Coverage + mutation testing only (no test generation) — writes COVERAGE.md.')
16
+ console.log(' --fix Agent fixes survived mutants, then re-runs coverage.')
17
+ console.log(' --changed Scope to files changed vs merge-base with main — no COVERAGE.md write.')
14
18
  return 0
15
19
  }
16
20
 
21
+ if (first === 'coverage') {
22
+ const { runCoverageCli } = await import('./coverage/coverage.mjs')
23
+ return runCoverageCli({
24
+ cwd: getCwd(),
25
+ fix: flags.includes('--fix'),
26
+ changed: flags.includes('--changed')
27
+ })
28
+ }
29
+
17
30
  const dir = first ? resolve(first) : getCwd()
18
31
  const noMutation = flags.includes('--no-mutation')
19
32
  const { runAutoTest } = await import('./run.mjs')
@@ -3,12 +3,12 @@ type: JS Module
3
3
  title: llm.mjs
4
4
  resource: npm/src/lib/llm.mjs
5
5
  docgen:
6
- crc: 41c6ec97
6
+ crc: b126a1aa
7
7
  ---
8
8
 
9
9
  ## Огляд
10
10
 
11
- Тонкий адаптер @7n/test над пакетом `@nitra/llm-lib` (Ф3 спеки llm-lib-extraction у репо cursor): зберігає звичний контракт `callText`/`callAgent` для внутрішніх колерів (gen-tests, fix-tests, assess-need, coverage-classify, coverage-fix), а транспорт, registry і трасування повністю живуть у пакеті. Замінив колишній `pi-client.mjs` з власним SDK-плюмбінгом, retry/backoff і прямим omlx-HTTP.
11
+ Тонкий адаптер @7n/test над пакетом `@7n/llm-lib` (Ф3 спеки llm-lib-extraction у репо cursor): зберігає звичний контракт `callText`/`callAgent` для внутрішніх колерів (gen-tests, fix-tests, assess-need, coverage-classify, coverage-fix), а транспорт, registry і трасування повністю живуть у пакеті. Замінив колишній `pi-client.mjs` з власним SDK-плюмбінгом, retry/backoff і прямим omlx-HTTP.
12
12
 
13
13
  ## Поведінка
14
14
 
package/src/lib/llm.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Тонкий адаптер @7n/test над @nitra/llm-lib (Ф3 спеки llm-lib-extraction):
2
+ * Тонкий адаптер `@7n/test` над `@7n/llm-lib` (Ф3 спеки llm-lib-extraction):
3
3
  * зберігає звичний контракт `callText`/`callAgent` для внутрішніх колерів,
4
4
  * але транспорт/registry/trace повністю живуть у пакеті.
5
5
  *
@@ -10,10 +10,10 @@
10
10
  * подвоєння `maxTokens` на обрізаній відповіді (`stopReason: 'length'`):
11
11
  * це семантичний повтор без пауз, а не очікування зайнятого сервера.
12
12
  */
13
- import { MEMORY_ERROR_RE, runOneShot } from '@nitra/llm-lib/one-shot'
14
- import { runAgentSkill } from '@nitra/llm-lib/agent-skill'
13
+ import { runOneShot } from '@7n/llm-lib/one-shot'
14
+ import { runAgentSkill } from '@7n/llm-lib/agent-skill'
15
+
15
16
 
16
- export { MEMORY_ERROR_RE }
17
17
 
18
18
  /**
19
19
  * Стеля відповіді моделі для подвоєння на `stopReason: 'length'` — межа
@@ -33,6 +33,7 @@ const AGENT_TIMEOUT_MS = 900_000
33
33
  * @param {string} [opts.model] provider/model-id (напр. "openai/gpt-5.5"); без значення — default pi
34
34
  * @param {number} [opts.maxTokens] стеля відповіді для цього виклику; на
35
35
  * `stopReason: 'length'` виклик повторюється один раз із подвоєною стелею
36
+ * @param {object} [opts.chain] chain handle (`@7n/llm-lib/chain`) — виклик стає кроком ланцюжка
36
37
  * @param {object} [opts.deps] інжекти для тестів (прокидаються у runOneShot)
37
38
  * @returns {Promise<string>} текстова відповідь моделі
38
39
  */
@@ -44,6 +45,7 @@ export async function callText(prompt, opts = {}) {
44
45
  timeoutMs: 0,
45
46
  cwd: opts.cwd,
46
47
  caller: '7n-test:text',
48
+ chain: opts.chain ?? null,
47
49
  deps: opts.deps
48
50
  })
49
51
  if (r.error) throw new Error(r.error)
@@ -66,6 +68,7 @@ export async function callText(prompt, opts = {}) {
66
68
  * @param {string} cwd робоча директорія, куди агент може писати файли
67
69
  * @param {object} [opts] додаткові параметри
68
70
  * @param {string} [opts.model] provider/model-id або '' для pi-дефолту
71
+ * @param {object} [opts.chain] chain handle (`@7n/llm-lib/chain`) — виклик стає кроком ланцюжка
69
72
  * @param {object} [opts.deps] інжекти для тестів (прокидаються у runAgentSkill)
70
73
  * @returns {Promise<void>} проміс завершується після виконання агента
71
74
  */
@@ -77,7 +80,10 @@ export async function callAgent(prompt, cwd, opts = {}) {
77
80
  timeoutMs: AGENT_TIMEOUT_MS,
78
81
  maxTokens: 0, // без стелі: агент пише цілі тест-файли (паритет зі старим CLI-шляхом)
79
82
  caller: 'agent:7n-test',
83
+ chain: opts.chain ?? null,
80
84
  deps: opts.deps
81
85
  })
82
86
  if (r.error) throw new Error(r.error)
83
87
  }
88
+
89
+ export {MEMORY_ERROR_RE} from '@7n/llm-lib/one-shot'