@7n/rules 1.48.2 → 1.49.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/n-rules-cli.mjs +5 -8
  3. package/package.json +1 -1
  4. package/rules/changelog/.changes/260724-1500.md +5 -0
  5. package/rules/doc-files/docgen-files-batch/docs/index.md +9 -0
  6. package/rules/doc-files/docgen-files-batch/docs/main.md +59 -18
  7. package/rules/doc-files/docgen-files-batch/main.mjs +280 -29
  8. package/rules/doc-files/docgen-gen/docs/index.md +9 -0
  9. package/rules/doc-files/docgen-gen/docs/main.md +40 -29
  10. package/rules/doc-files/docgen-gen/main.mjs +79 -25
  11. package/rules/test/coverage/fix-worker.mjs +9 -1
  12. package/rules/test/coverage/lib/classify/verdict-schema.mjs +3 -1
  13. package/scripts/docs/skills-cli.md +18 -24
  14. package/scripts/lib/acp-runner.mjs +1 -1
  15. package/scripts/lib/lint-surface/collateral-veto.mjs +78 -2
  16. package/scripts/lib/lint-surface/docs/collateral-veto.md +30 -16
  17. package/scripts/lib/lint-surface/docs/index.md +1 -0
  18. package/scripts/lib/lint-surface/docs/run-fix.md +8 -23
  19. package/scripts/lib/lint-surface/docs/snapshot.md +6 -17
  20. package/scripts/lib/lint-surface/docs/test-gate.md +29 -0
  21. package/scripts/lib/lint-surface/run-fix.mjs +209 -38
  22. package/scripts/lib/lint-surface/snapshot.mjs +6 -0
  23. package/scripts/lib/lint-surface/test-gate.mjs +87 -0
  24. package/scripts/skills-cli.mjs +60 -10
  25. package/scripts/utils/docs/glob-compat.md +20 -14
  26. package/scripts/utils/glob-compat.mjs +18 -3
  27. package/skills/git-reconcile/SKILL.md +58 -0
  28. package/skills/git-reconcile/js/docs/index.md +9 -0
  29. package/skills/git-reconcile/js/docs/orchestrate.md +33 -0
  30. package/skills/git-reconcile/js/orchestrate.mjs +776 -0
  31. package/skills/git-reconcile/main.json +1 -0
  32. package/skills/taze/js/docs/orchestrate.md +40 -18
  33. package/skills/taze/js/orchestrate.mjs +6 -3
@@ -3,39 +3,50 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-gen/main.mjs
5
5
  docgen:
6
- crc: c02d3a1f
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
+ crc: 80c789c7
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 10
10
+ issues: no-overview,short-behavior,internal-name:isApiGap,internal-name:renderApiLine,internal-name:oneShotDoc,internal-name:finishUnsupported,anchor-miss:(abie.mdc),best-of-2:retry-lost
8
11
  ---
9
12
 
10
- ## Огляд
11
-
12
- Огляд
13
- Створює технічну документацію шляхом аналізу вихідного коду, генерації тексту, оцінки його якості та управління позиціонуванням захищеного блоку «Призначення» у фінальному документі. (abie.mdc)
14
-
15
- Поведінка
16
- splitProtected відокремлює захищений блок «Призначення» від основного вмісту документа, надаючи його тіло та текст без цього блоку.
17
- insertProtected поміщає захищений блок «Призначення» одразу після основного заголовка документа, якщо надано його тіло.
18
- scoreDoc оцінює згенерований документ за заданими критеріями, визначаючи його якість та реєструючи проблеми.
19
- DEFAULT_LOCAL_MODEL встановлює модель, яку використовувати для генерації документа, якщо не вказано іншу.
20
- generateDoc створює повний технічний документ з вихідного файлу, включаючи вихідну оцінку якості.
21
-
22
- ## Поведінка
23
-
24
- splitProtected відокремлює захищений блок «Призначення» від основного вмісту документа, повертаючи його тіло та текст без цього блоку.
25
- insertProtected розміщує захищений блок «Призначення» одразу після основного заголовка документа, якщо його тіло було надано.
26
- scoreDoc оцінює згенерований документ за набором детермінованих правил (generic-огляд, коротка поведінка, галюцинації про кеш, витік службових імен, анкор-покриття, суржик) і реєструє коди проблем. Правило R8 — refusal/чат-філер («Я готовий писати…», «Надайте мені код…», через пре-гейт docgen-judge): форсує degraded незалежно від структурної оцінки, тож LLM-суддя на такому змісті не викликається; захищене людське «Призначення» з перевірки виключене.
27
- DEFAULT_LOCAL_MODEL визначає модель за замовчуванням для генерації документа, якщо не вказано інше.
28
- generateDoc створює повний технічний документ з вихідного файлу, включаючи детерміновану оцінку якості; для непідтримуваних структур (one-shot шлях) оцінка не застосовується, але refusal-пре-гейт діє і там — філер отримує score 0 і позначку degraded, щоб доретрай батчу підібрав файл.
29
- generateDoc приймає опційний `deadlineAt` (м'який дедлайн fix-pipeline, epoch ms): кожен LLM-виклик ріже свій таймаут під залишок бюджету (`capTimeoutToDeadline`), а вичерпаний бюджет обриває генерацію помилкою зі словом «timeout» ще до старту виклику — батч класифікує її як transient, і генерація ніколи не переживає бюджет рунга (без батчу-зомбі поверх наступного rung-а).
30
-
31
13
  ## Публічний API
32
14
 
33
- splitProtectedвитягує тіло захищеної секції «Призначення» та повертає документ без неї.
34
- insertProtectedповертає захищений блок у фіксовану позицію одразу після заголовка документа.
35
- scoreDocдетермінований скоринг документа (0 токенів) проти факт-листа: оцінка 0–100 і коди проблем.
36
- DEFAULT_LOCAL_MODEL модель генерації за замовчуванням (env-конфігурована; без неї preflight фейлить гучно).
37
- generateDoc головний вхід: джерело md-дока з оцінкою якості, best-of-2 повтором і опційним семантичним суддею; опційний `deadlineAt` обмежує генерацію бюджетом рунга fix-pipeline.
38
- capTimeoutToDeadlineріже базовий per-call таймаут під залишок до дедлайну (без дедлайну базовий ліміт; після дедлайну — 0).
15
+ - capTimeoutToDeadline Ріже базовий per-call таймаут під залишок бюджету до дедлайну.
16
+ Без дедлайну базовий ліміт; після дедлайну 0 (виклик не має стартувати).
17
+ - stripLeadingPreamble R9: зрізає провідні чат-преамбули й дубль назви секції з початку тексту.
18
+ Ітерується, поки перший непорожній рядок лишається мета-нарацією модель
19
+ інколи ставить дві поспіль («Як технічний письменник…» + «Ось оновлений…»).
20
+ - splitProtected Відокремлює захищену секцію `## Призначення` (Варіант B). Межанаступний `## `
21
+ (H2); `###`+ усередині не обривають блок.
22
+ - insertProtected — Вставляє захищений блок `## Призначення` одразу після H1 (фіксована позиція).
23
+ - scoreDoc — Stage 2.5 — детермінований скоринг (0 токенів): перевіряє вихід проти фактів.
24
+ - buildApiSection — Stage 1/3 (гібрид doc-files, ADR 260719-2155): «Публічний API» — покриті
25
+ JSDoc-описом експорти рендеряться дослівно (`renderApiLine`, 0 токенів, 0
26
+ галюцинацій), LLM викликається лише на прогалини (`isApiGap`). Якщо прогалин
27
+ немає — секція збирається БЕЗ жодного LLM-виклику. Єдиний непокритий
28
+ експорт (як і раніше) лишається описаним лише в Поведінці — окремого виклику
29
+ на секцію з одного рядка не варте.
30
+ - DEFAULT_LOCAL_MODEL — Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') (→ N_LOCAL_MIN_MODEL).
31
+ Без хардкод-fallback: модель налаштовує кожен локально (`N_LOCAL_MIN_MODEL`); якщо
32
+ нічого не задано — порожньо, і preflight оркестратора фейлить гучно (а не шле
33
+ запит до неіснуючої моделі).
34
+ - generateDoc — Головний API: файл → md-дока з det-оцінкою.
35
+
36
+ Local-only (ADR 260610-2228): жодних cloud-ескалацій і pre-route — будь-який
37
+ файл генерується локальною моделлю. Якщо det-score нижче порогу, один retry
38
+ з вищою температурою (best-of-2); якщо й він не допоміг — результат
39
+ позначається `degraded`, рішення про перегенерацію приймає batch/користувач.
40
+ - prepareBatchItem — T8 (2b-batch, рішення Р): підготовка ОДНОГО item-у для `submitBatch` — та сама
41
+ pre-send guard і той самий факт-лист/one-shot messages, що й `oneShotDoc`/
42
+ `generateDoc`, але БЕЗ виклику LLM (виклик робить batch-шар одним `submit` на
43
+ всі файли разом). Кидає ту саму помилку pre-send guard, що й `generateDoc`
44
+ (класифікується `permanent` у batch-оркестраторі — skip, не помилка прогону).
45
+ - finishBatchItem — T8 (2b-batch): постобробка ОДНОГО результату `submitBatch` — той самий фініш,
46
+ що й `oneShotDoc`/`finishUnsupported`/det-скорер, тільки без LLM-виклику
47
+ (текст уже отримано з batch-у). Judge-гейт (Stage 3) у batch-шляху НЕ
48
+ викликається (мінімальний обсяг T8 — генерація; judge лишається опційним
49
+ розширенням послідовного шляху).
39
50
 
40
51
  ## Гарантії поведінки
41
52
 
@@ -602,6 +602,39 @@ const DEFAULT_CONTEXT_TOKENS = 131072
602
602
  function srcTokenBudget() {
603
603
  return Math.floor((Number(env.N_CURSOR_DOCGEN_CTX) || DEFAULT_CONTEXT_TOKENS) * 0.5)
604
604
  }
605
+
606
+ /**
607
+ * Спільний pre-LLM preflight для `generateDoc` і `prepareBatchItem` (T8 2b-batch):
608
+ * читає джерело, ріже гігантів до LLM-виклику (pre-send guard — «Prompt too long»
609
+ * без жодного виклику) і резолвить факт-лист через мовний екстрактор lang-плагіна
610
+ * (js/mjs/ts — lang-js, `.rs` — lang-rust; whole-file `unsupported`-fallback, якщо
611
+ * екстрактора для розширення нема).
612
+ * @param {string} file абсолютний шлях джерела
613
+ * @returns {Promise<{ src: string, estTokens: number, ext: string, langExtractors: Map<string, object>, facts: object }>} усе потрібне обом callers перед LLM-викликом
614
+ */
615
+ async function loadSrcAndFacts(file) {
616
+ const src = readFileSync(file, 'utf8')
617
+ const estTokens = Math.round(Buffer.byteLength(src, 'utf8') / 4)
618
+ const budget = srcTokenBudget()
619
+ if (estTokens > budget) {
620
+ throw new Error(
621
+ `docgen pre-send guard: джерело ~${estTokens} токенів > бюджет ${budget} (0.5× контексту) — Prompt too long, skip`
622
+ )
623
+ }
624
+ const langExtractors = await loadDocFilesExtractors(process.cwd())
625
+ const ext = `.${file.split('.').pop()}`.toLowerCase()
626
+ const facts = langExtractors.get(ext)?.extractFacts?.(src, file) ?? {
627
+ relPath: file,
628
+ lang: ext.slice(1),
629
+ unsupported: true,
630
+ header: '',
631
+ exports: [],
632
+ imports: {},
633
+ markers: {}
634
+ }
635
+ return { src, estTokens, ext, langExtractors, facts }
636
+ }
637
+
605
638
  /**
606
639
  * Дефолтна модель: N_CURSOR_DOCGEN_MODEL → resolveModel('min') (→ N_LOCAL_MIN_MODEL).
607
640
  * Без хардкод-fallback: модель налаштовує кожен локально (`N_LOCAL_MIN_MODEL`); якщо
@@ -654,32 +687,8 @@ export async function generateDoc(
654
687
  deadlineAt = null
655
688
  } = {}
656
689
  ) {
657
- const src = readFileSync(file, 'utf8')
658
- // Pre-send guard: весь src вшивається у промпт як є (екстракт фактів його НЕ
659
- // замінює). Для гігантів (vendored/генерат) це переповнює контекст → інстант-skip
660
- // без LLM-виклику. Маркер «Prompt too long» → classifyOmlxError → permanent → skip.
661
690
  // Guard ДО створення ланцюжка: skip без LLM — не задача.
662
- const estTokens = Math.round(Buffer.byteLength(src, 'utf8') / 4)
663
- const budget = srcTokenBudget()
664
- if (estTokens > budget) {
665
- throw new Error(
666
- `docgen pre-send guard: джерело ~${estTokens} токенів > бюджет ${budget} (0.5× контексту) — Prompt too long, skip`
667
- )
668
- }
669
- // Факт-лист — лише від мовного екстрактора lang-плагіна (js/mjs/ts —
670
- // lang-js, `.rs` — lang-rust); без екстрактора для розширення — whole-file
671
- // шлях через `unsupported` (у ядрі вбудованих екстракторів немає, фаза 5b).
672
- const langExtractors = await loadDocFilesExtractors(process.cwd())
673
- const ext = `.${file.split('.').pop()}`.toLowerCase()
674
- const facts = langExtractors.get(ext)?.extractFacts?.(src, file) ?? {
675
- relPath: file,
676
- lang: ext.slice(1),
677
- unsupported: true,
678
- header: '',
679
- exports: [],
680
- imports: {},
681
- markers: {}
682
- }
691
+ const { src, estTokens, ext, langExtractors, facts } = await loadSrcAndFacts(file)
683
692
  const t0 = Date.now()
684
693
  llmMeter = { calls: 0, ms: 0 }
685
694
  const chain = chainFactory({ kind: 'doc-generate', unit: facts.relPath, cwd: process.cwd() })
@@ -770,6 +779,51 @@ export async function generateDoc(
770
779
  }
771
780
  }
772
781
 
782
+ /**
783
+ * T8 (2b-batch, рішення Р): підготовка ОДНОГО item-у для `submitBatch` — та сама
784
+ * pre-send guard і той самий факт-лист/one-shot messages, що й `oneShotDoc`/
785
+ * `generateDoc`, але БЕЗ виклику LLM (виклик робить batch-шар одним `submit` на
786
+ * всі файли разом). Кидає ту саму помилку pre-send guard, що й `generateDoc`
787
+ * (класифікується `permanent` у batch-оркестраторі — skip, не помилка прогону).
788
+ * @param {string} file абсолютний шлях джерела
789
+ * @param {{ existingMd?: string|null }} [opts] наявна дока (для захищеної секції «Призначення»)
790
+ * @returns {Promise<{ facts: object, anchors: object|null, src: string, messages: Array<{role:string,content:string}>, intent: string|null }>} усе потрібне для item-у batch-у й пізнішого фінішу
791
+ */
792
+ export async function prepareBatchItem(file, { existingMd = null } = {}) {
793
+ const { src, facts } = await loadSrcAndFacts(file)
794
+ const anchors = facts.unsupported ? null : extractAnchors(src)
795
+ const intent = existingMd ? splitProtected(existingMd).body : null
796
+ return { facts, anchors, src, messages: oneShotMessages(facts, src), intent }
797
+ }
798
+
799
+ /**
800
+ * T8 (2b-batch): постобробка ОДНОГО результату `submitBatch` — той самий фініш,
801
+ * що й `oneShotDoc`/`finishUnsupported`/det-скорер, тільки без LLM-виклику
802
+ * (текст уже отримано з batch-у). Judge-гейт (Stage 3) у batch-шляху НЕ
803
+ * викликається (мінімальний обсяг T8 — генерація; judge лишається опційним
804
+ * розширенням послідовного шляху).
805
+ * @param {string} text сирий текст відповіді моделі для цього item-у
806
+ * @param {{ facts: object, anchors: object|null, src: string, intent: string|null, model: string, threshold?: number }} ctx контекст item-у (з `prepareBatchItem`)
807
+ * @returns {{ md: string, score: number|null, issues: string[], degraded: boolean, model: string }} результат генерації для штампу/запису
808
+ */
809
+ export function finishBatchItem(text, { facts, anchors, src, intent, model, threshold = QUALITY_THRESHOLD }) {
810
+ let md = stripSignatures(stripSection(text))
811
+ if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`
812
+ md = insertProtected(md + '\n', intent)
813
+ if (facts.unsupported) {
814
+ const refusal = detectRefusalFiller(splitProtected(md).without)
815
+ return {
816
+ md,
817
+ score: refusal ? 0 : null,
818
+ issues: refusal ? ['refusal-filler'] : [],
819
+ degraded: Boolean(refusal),
820
+ model
821
+ }
822
+ }
823
+ const { score, issues } = scoreDoc(md, facts, { anchors, src })
824
+ return { md, score, issues, degraded: score < threshold, model }
825
+ }
826
+
773
827
  // CLI: node docgen-gen.mjs <file> [--model <m>]
774
828
  if (isRunAsCli(import.meta.url)) {
775
829
  const args = process.argv.slice(2)
@@ -75,6 +75,8 @@ export async function fixWorker(violations, ctx, deps = {}) {
75
75
 
76
76
  /** @type {string[]} */
77
77
  const touchedFiles = []
78
+ /** @type {Array<{provider: string, hook: string, files: string[], error: string}>} */
79
+ const failed = []
78
80
  /**
79
81
  * Викликає опційний fix-hook провайдера, збирає touchedFiles; виняток хука не
80
82
  * валить решту хуків/провайдерів — success визначає canonical re-detect.
@@ -88,6 +90,12 @@ export async function fixWorker(violations, ctx, deps = {}) {
88
90
  try {
89
91
  const res = await provider[hook]({ ...args, cwd: ctx.cwd, ctx: hookCtx(ctx, deadlineAt) })
90
92
  touchedFiles.push(...(res?.touchedFiles ?? []))
93
+ for (const failure of res?.failed ?? []) {
94
+ const error = failure?.error ?? 'невідома причина'
95
+ const files = failure?.files ?? []
96
+ failed.push({ provider: provider.id, hook, files, error })
97
+ console.warn(`⚠ coverage fix-worker: ${provider.id}.${hook} failed/no-op: ${error}`)
98
+ }
91
99
  } catch (error) {
92
100
  console.warn(
93
101
  `⚠ coverage fix-worker: ${provider.id}.${hook} впав: ${String(error?.message ?? error).slice(0, 200)}`
@@ -107,5 +115,5 @@ export async function fixWorker(violations, ctx, deps = {}) {
107
115
  }
108
116
  }
109
117
 
110
- return { touchedFiles }
118
+ return { touchedFiles, failed }
111
119
  }
@@ -9,7 +9,9 @@
9
9
  * - glue: CLI entry / runStandardRule wrapper (integration covers)
10
10
  * - wrapper: тонкий spawn/fetch wrapper (integration covers)
11
11
  */
12
- import { z } from 'zod'
12
+ // Namespace import сумісний із Bun+Vitest трансформацією Zod 4, де named `z`
13
+ // може бути undefined попри наявність у runtime ESM namespace.
14
+ import * as z from 'zod'
13
15
 
14
16
  // Трохи ширше за prompt-ліміт (500) — запас на моделі, які трохи перевищують
15
17
  // інструкцію; понад це вже truncate-имо самі перед валідацією (REASON_SOFT_MAX нижче).
@@ -3,34 +3,28 @@ type: JS Module
3
3
  title: skills-cli.mjs
4
4
  resource: npm/scripts/skills-cli.mjs
5
5
  docgen:
6
- crc: 3e5781e6
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
+ crc: 9b3e0ad4
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 25
10
+ issues: no-overview,short-behavior,internal-name:runTazeOrchestratorCli,anchor-miss:tsconfig.json,anchor-miss:.n-rules.json,anchor-miss:.n-cursor.json,anchor-miss:main.json,best-of-2:retry-lost
8
11
  ---
9
12
 
10
- ## Огляд
11
-
12
- Цей модуль забезпечує керування функціоналом скілів пакета `@7n/rules`. Він каталогізує доступні скіли на основі наявності файлу `SKILL.md` у відповідному пакеті. Для виконання скілу збирається контекст, що включає інструкції скілу та інформацію з конфігураційних файлів проєкту: `package.json`, `tsconfig.json`, `.n-rules.json` та `main.json`. Система дозволяє або вивести список доступних скілів (`npx @7n/rules skill list`), або ініціювати виконання обраного скілу (наприклад, `npx @7n/rules skill pi taze`), з можливістю передачі додаткових аргументів. Пріоритетним механізмом виконання є інтеграція з вбудованим pi-агентом; поряд з ним підтримуються повноцінні зовнішні ACP-агенти (`cursor`, `codex`) і deprecated `claude`.
13
-
14
- ## Поведінка
15
-
16
- Поведінка
17
- normalizeSkillId знімає префікс `n-` з імені скілу для приведення його до стандартного ID.
18
- listSkillIds отримує відсортований список ID скілів, які мають файл `SKILL.md` у вказаній директорії скілів.
19
- buildSkillPrompt збирає комплексний промпт для виконання скілу, об'єднуючи інструкцію скілу з конфігураційними файлами проєкту, такими як `package.json`, `tsconfig.json` та `.n-rules.json`.
20
- resolveBundledPackageRoot визначає абсолютний шлях до кореня пакету `@7n/rules`, використовуючи інформацію про поточний модуль.
21
- runSkillsCli виконує логіку командного інтерфейсу для керування скілами: може вивести список доступних скілів, зібрати промпт для скілу або ініціювати його виконання через вбудований pi-агент чи один із зовнішніх CLI-раннерів (`cursor`, `codex`, `claude`).
22
-
23
13
  ## Публічний API
24
14
 
25
- - normalizeSkillIdзнімає префікс `n-` з імені скілу, приводячи його до id каталогу в пакеті.
26
- - listSkillIdsповертає відсортований список id скілів, що мають `SKILL.md`.
27
- - buildSkillPrompt — збирає промпт виконання: інструкція скілу + контекст проєкту (`package.json`, `tsconfig.json`, `.n-rules.json`); кидає, якщо скіл невідомий.
28
- - resolveBundledPackageRoot абсолютний шлях до кореня встановленого пакета `@7n/rules`.
29
- - runSkillsCli — асинхронний entrypoint підкоманди `skill`: `list`, друк промпта на stdout, або виконання через `pi` (рекомендовано), `cursor`/`codex` (зовнішні CLI), `claude` (deprecated). Повертає exit-код.
15
+ - resolveBundledPackageRootКорінь пакета `@7n/rules` (каталог з `skills/`, `rules/`, …).
16
+ - isTazeOrchestratorSkillArgsЧи `argv` (аргументи після `skill`) резолвиться в JS-оркестрований
17
+ worktree-only `taze`-шлях (`runTazeOrchestratorCli`) той самий критерій,
18
+ що й нижче в `runSkillsCli`. Використовується `n-rules.js`, щоб не мутувати
19
+ root `package.json` (self-upgrade `@7n/rules`) ДО власного worktree-гейту
20
+ оркестратора: той сам створює worktree і перевіряє чистоту дерева
21
+ (`ensureRunningInWorktree`, `requireCleanTree: true`) — мутація package.json
22
+ прямо перед цим викликом примусово провалила б auto-create там, де дерево
23
+ інакше було б чисте.
24
+ - isJsOrchestratedSkillArgs — Чи аргументи ведуть у будь-який JS-оркестрований skill. Потрібно верхньому
25
+ CLI, щоб не мутувати root package.json self-upgrade-ом до власного preflight
26
+ оркестратора.
30
27
 
31
28
  ## Гарантії поведінки
32
29
 
33
- - Сам модуль лише читає файли й збирає промпт; **виконання** делегується агенту: `pi` (вбудований, мутує дерево, запускає bash) або зовнішньому ACP-агенту `cursor`/`codex`/`claude` через `runAcpRunner` (`./lib/acp-runner.mjs`) — JSON-RPC поверх stdio (`cursor-agent acp`, бандловані адаптери `@agentclientprotocol/codex-acp` і `@agentclientprotocol/claude-agent-acp`), а не сирий `stdin`/`stdout`-піпінг.
34
- - Тира моделі для `pi`-runner береться з `main.json.tier` скіла (дефолт `max`).
35
- - ACP-раннер автоапрувляє `session/request_permission` (без інтерактивних питань) — паритет із колишнім non-interactive режимом; `Client`-реалізація читає/пише файли напряму через `node:fs`.
36
- - Лише `claude` — deprecated: друкує попередження й лишається як fallback, доки не налаштовано pi-модель. `cursor`/`codex` — повноцінні раннери без попередження.
30
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -3,7 +3,7 @@
3
3
  * agentclientprotocol.com) — JSON-RPC поверх stdio.
4
4
  *
5
5
  * `cursor`/`codex` мігрували на `@7n/llm-lib/acp` (napi-міст до
6
- * `llm_cascade::acp` — та сама протокольна логіка, але в Rust, без
6
+ * `llm_lib::acp` — та сама протокольна логіка, але в Rust, без
7
7
  * дублювання в JS). `claude` лишається тут як окремий JS-шим, бо Rust-крейт
8
8
  * `AcpAgentKind` його не моделює (лише `Cursor`/`Codex`); підʼєднання йде
9
9
  * через офіційний TS SDK `@zed-industries/agent-client-protocol`, а дозволи
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Semantic-collateral veto для verdict-фази fix-pipeline
3
- * (spec docs/specs/2026-06-26-pi-fix-engine-migration.md §12, addendum 2026-07-05).
3
+ * (spec docs/specs/2026-06-26-pi-fix-engine-migration.md §12, addendum 2026-07-05;
4
+ * in-file hunk-level розширення — addendum 2026-07-24 після колатеральної правки, що
5
+ * видалила задокументований обхід реального Bun SQL бага в upsert-order.js).
4
6
  *
5
7
  * Клас collateral слабких локальних моделей: «виправляючи» правило, модель робить
6
8
  * семантичну правку у сторонньому файлі, яка НЕ порушує жодного правила й тому
@@ -12,6 +14,15 @@
12
14
  * їх однаково покриває re-check зачеплених файлів і rollback. Порожній target-set
13
15
  * (whole-repo концерни без `file` у violations) → veto незастосовний (fail-open,
14
16
  * повертає []) — свідомо, щоб не ламати концерни без file-атрибуції.
17
+ *
18
+ * Другий клас collateral (§12 addendum 2026-07-24): rung змінює файл, що ВЖЕ входить
19
+ * у target-set (легітимна ціль порушення), але зачіпає рядки поза ділянкою самого
20
+ * порушення — напр. LLM виправляє відсутній JSDoc над функцією і заодно видаляє сусідній
21
+ * задокументований обхід реального бага усередині тіла тієї самої функції. Cross-file
22
+ * veto вище цього не бачить (файл — законна ціль). {@link findInFileCollateralEdits}
23
+ * рахує грубий (common-prefix/common-suffix, не повний Myers-diff) змінений рядковий
24
+ * діапазон і звіряє його з вікном навколо кожної `violation.data.line` того ж файлу;
25
+ * fail-open, якщо рядок порушення невідомий (немає як відповідально обмежити hunk).
15
26
  */
16
27
  import { realpathSync } from 'node:fs'
17
28
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
@@ -37,6 +48,18 @@ export function realpathBestEffort(p) {
37
48
  }
38
49
  }
39
50
 
51
+ /**
52
+ * Нормалізує target-set порушення у множину realpath-абсолютних шляхів — спільна
53
+ * основа і для collateral-veto (файли ПОЗА target-set), і для test-gate
54
+ * (файли ВСЕРЕДИНІ target-set, для яких перевіряються сестринські тести).
55
+ * @param {string[]} targetFiles Файли порушення, відносні до cwd або абсолютні.
56
+ * @param {string} cwd Робоча директорія для резолву відносних шляхів.
57
+ * @returns {Set<string>} Нормалізовані абсолютні шляхи target-set (може бути порожньою).
58
+ */
59
+ export function resolveTargetSet(targetFiles, cwd) {
60
+ return new Set(targetFiles.map(f => realpathBestEffort(isAbsolute(f) ? f : resolve(cwd, f))))
61
+ }
62
+
40
63
  /**
41
64
  * Обчислює collateral-правки rung-а: наявні (на момент S1) файли, змінені поза
42
65
  * target-set порушення. Runner на непорожньому результаті відхиляє clean-вердикт
@@ -49,7 +72,60 @@ export function realpathBestEffort(p) {
49
72
  * veto не спрацював (нема collateral або target-set невідомий)
50
73
  */
51
74
  export function findCollateralEdits({ modifiedExisting, targetFiles, cwd }) {
52
- const targets = new Set(targetFiles.map(f => realpathBestEffort(isAbsolute(f) ? f : resolve(cwd, f))))
75
+ const targets = resolveTargetSet(targetFiles, cwd)
53
76
  if (targets.size === 0) return []
54
77
  return modifiedExisting.map(p => realpathBestEffort(p)).filter(abs => !targets.has(abs))
55
78
  }
79
+
80
+ /** Дефолтне вікно (рядків з обох боків `violation.data.line`), у межах якого зміна вважається «поруч із порушенням». */
81
+ export const HUNK_WINDOW = 20
82
+
83
+ /**
84
+ * Обчислює грубий змінений рядковий діапазон (1-indexed, у координатах `current`) між
85
+ * pre-image і поточним вмістом файлу: найдовший спільний префікс + найдовший спільний
86
+ * суфікс рядків, все між ними — «змінене». Це НЕ повний diff (не бачить кількох
87
+ * розрізнених hunk-ів як окремих інтервалів — лише зовнішні межі першої і останньої
88
+ * розбіжності), але достатньо, щоб відрізнити «зміна лишилась у ділянці порушення» від
89
+ * «зачепило щось далеко» — для повного Myers-diff тут немає залежності, а точність
90
+ * hunk-рівня явно позначена як бажаний, але не обов'язковий бар (spec §12 addendum
91
+ * 2026-07-24).
92
+ * @param {string} preImage Вміст файлу на момент S1.
93
+ * @param {string} current Поточний вміст файлу.
94
+ * @returns {{ start: number, end: number }|null} діапазон зміни, або null якщо вміст ідентичний.
95
+ */
96
+ function changedLineRange(preImage, current) {
97
+ if (preImage === current) return null
98
+ const preLines = preImage.split('\n')
99
+ const curLines = current.split('\n')
100
+ const maxLcp = Math.min(preLines.length, curLines.length)
101
+ let lcp = 0
102
+ while (lcp < maxLcp && preLines[lcp] === curLines[lcp]) lcp++
103
+ const maxLcs = maxLcp - lcp
104
+ let lcs = 0
105
+ while (lcs < maxLcs && preLines[preLines.length - 1 - lcs] === curLines[curLines.length - 1 - lcs]) lcs++
106
+ const start = lcp + 1
107
+ const end = curLines.length - lcs
108
+ return start > end ? null : { start, end }
109
+ }
110
+
111
+ /**
112
+ * In-file hunk-level veto (§12 addendum 2026-07-24): rung змінив файл, що вже входить
113
+ * у target-set порушення, але змінений рядковий діапазон виходить за межі вікна навколо
114
+ * КОЖНОЇ `violation.data.line` цього файлу — сигнал колатеральної правки поза власне
115
+ * порушенням (клас upsert-order.js: LLM видалив сусідній задокументований обхід бага,
116
+ * виправляючи doc-comment над функцією). Fail-open: якщо для файлу немає жодного
117
+ * порушення з відомим `line`, hunk неможливо відповідально обмежити — повертає null.
118
+ * @param {{ preImage: string|null, current: string|null, violationLines: number[], window?: number }} args
119
+ * preImage/current — вміст файлу до/після rung-а; violationLines — номери рядків
120
+ * порушень ЦЬОГО rung-а в ЦЬОМУ файлі; window — половина ширини допустимого вікна.
121
+ * @returns {{ start: number, end: number }|null} діапазон відхиленої правки, або null —
122
+ * veto не спрацював (нема зміни, зміна в межах вікна, або невідома локація порушення).
123
+ */
124
+ export function findInFileCollateralEdits({ preImage, current, violationLines, window = HUNK_WINDOW }) {
125
+ if (preImage === null || preImage === undefined || current === null || current === undefined) return null
126
+ if (!violationLines || violationLines.length === 0) return null
127
+ const range = changedLineRange(preImage, current)
128
+ if (!range) return null
129
+ const inWindow = violationLines.some(line => range.start >= line - window && range.end <= line + window)
130
+ return inWindow ? null : range
131
+ }
@@ -3,26 +3,40 @@ type: JS Module
3
3
  title: collateral-veto.mjs
4
4
  resource: npm/scripts/lib/lint-surface/collateral-veto.mjs
5
5
  docgen:
6
- crc: 16d3ef02
6
+ crc: 96d4ee38
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 55
10
+ issues: no-overview,short-behavior,best-of-2:retry-lost
7
11
  ---
8
12
 
9
- ## Огляд
10
-
11
- Semantic-collateral veto для verdict-фази fix-pipeline (spec pi-fix-engine-migration §12, addendum 2026-07-05). Закриває клас collateral слабких локальних моделей: «виправляючи» правило, модель робить семантичну правку у сторонньому файлі, яка не порушує жодного правила й тому проходить canonical re-detect (кейс App.vue: хардкод версії з коментарем «we simulate it being available» замість виклику `getVersion`).
12
-
13
- ## Поведінка
14
-
15
- 1. `findCollateralEdits` порівнює список змінених наявних файлів rung-а (`snapshot.modifiedExisting()`) із target-set порушення (`violations[].file ∪ item.files`) і повертає правки поза target-set.
16
- 2. Нові файли до veto не входять — легітимний клас (scaffold, доки поряд із кодом); їх покриває re-check зачеплених файлів і rollback.
17
- 3. Порожній target-set (whole-repo концерни без `file` у violations) → veto незастосовний: свідомий fail-open, повертається порожній масив.
18
- 4. Усі шляхи realpath-нормалізуються (`realpathBestEffort`, той самий патерн, що у write-guard llm-lib) — знімає symlink-розбіжності macOS (`/tmp` → `/private/tmp`); caller relativize-ить результати від так само нормалізованого cwd.
19
-
20
13
  ## Публічний API
21
14
 
22
- - `findCollateralEdits({ modifiedExisting, targetFiles, cwd })` нормалізовані абсолютні шляхи відхилених правок; порожньо collateral немає або target-set невідомий.
23
- - `realpathBestEffort(p)` — realpath з найкращих зусиль (наявний файл → повний realpath; неіснуючий → realpath батьківської теки + basename; інакше — як є).
15
+ - realpathBestEffort realpath шляху з найкращих зусиль: для наявногоповний realpath; для ще-неіснуючого
16
+ realpath батьківської теки + basename; інакше — як є. Знімає розбіжність symlink-шляхів
17
+ (macOS `/tmp` → `/private/tmp`) між snapshot-ключами і target-set (той самий патерн,
18
+ що у write-guard llm-lib). Експортовано, щоб caller relativize-ив результати veto від
19
+ так само нормалізованого cwd.
20
+ - resolveTargetSet — Нормалізує target-set порушення у множину realpath-абсолютних шляхів — спільна
21
+ основа і для collateral-veto (файли ПОЗА target-set), і для test-gate
22
+ (файли ВСЕРЕДИНІ target-set, для яких перевіряються сестринські тести).
23
+ - findCollateralEdits — Обчислює collateral-правки rung-а: наявні (на момент S1) файли, змінені поза
24
+ target-set порушення. Runner на непорожньому результаті відхиляє clean-вердикт
25
+ rung-а (rollback + feedback + телеметрія `kind:"collateral-veto"`).
26
+ modifiedExisting — абсолютні шляхи наявних файлів, змінених відносно S1
27
+ (`snapshot.modifiedExisting()`); targetFiles — файли порушення
28
+ (`violations[].file ∪ item.files`), відносні до cwd або абсолютні.
29
+ - HUNK_WINDOW — Дефолтне вікно (рядків з обох боків `violation.data.line`), у межах якого зміна вважається «поруч із порушенням».
30
+ - findInFileCollateralEdits — In-file hunk-level veto (§12 addendum 2026-07-24): rung змінив файл, що вже входить
31
+ у target-set порушення, але змінений рядковий діапазон виходить за межі вікна навколо
32
+ КОЖНОЇ `violation.data.line` цього файлу — сигнал колатеральної правки поза власне
33
+ порушенням (клас upsert-order.js: LLM видалив сусідній задокументований обхід бага,
34
+ виправляючи doc-comment над функцією). Fail-open: якщо для файлу немає жодного
35
+ порушення з відомим `line`, hunk неможливо відповідально обмежити — повертає null.
36
+ preImage/current — вміст файлу до/після rung-а; violationLines — номери рядків
37
+ порушень ЦЬОГО rung-а в ЦЬОМУ файлі; window — половина ширини допустимого вікна.
24
38
 
25
39
  ## Гарантії поведінки
26
40
 
27
- - Read-only: не виконує операцій запису (ФС/БД).
28
- - Ніколи не кидає: помилки realpath ігноруються з поверненням шляху як є.
41
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
42
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -24,6 +24,7 @@ resource: npm/scripts/lib/lint-surface/
24
24
  | [run-fix.mjs](run-fix.md) | JS Module |
25
25
  | [scheduler.mjs](scheduler.md) | JS Module |
26
26
  | [snapshot.mjs](snapshot.md) | JS Module |
27
+ | [test-gate.mjs](test-gate.md) | JS Module |
27
28
  | [tier-sampling-bench.mjs](tier-sampling-bench.md) | JS Module |
28
29
  | [tier-sampling-experiment.mjs](tier-sampling-experiment.md) | JS Module |
29
30
  | [types.mjs](types.md) | JS Module |
@@ -3,33 +3,18 @@ type: JS Module
3
3
  title: run-fix.mjs
4
4
  resource: npm/scripts/lib/lint-surface/run-fix.mjs
5
5
  docgen:
6
- crc: 45965f7d
7
- model: manual
6
+ crc: 7f20adb7
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 50
10
+ issues: no-overview,short-behavior,anchor-miss:.mt.json,best-of-2:retry-lost
8
11
  ---
9
12
 
10
- ## Огляд
11
-
12
- Цей файл реалізує уніфіковану поверхню для фіксації порушень лінтування (unified lint surface) відповідно до специфікації `2026-06-29 §Fix Role / §Tier Ladder`. Він керує послідовним процесом виявлення та усунення проблем, який складається з етапів: виявлення → (очищення/збереження) → Т0 (перманентне виправлення) → знімок S1 → повторне виявлення → (очищення/збереження) → цикл `ladder[відновлення S1 → worker → виявлення]*` (до вичерпання) → (вичерпання/відкат S1). Публічна функція `fixConcern` відповідає за індивідуальне виправлення певного компонента, а `runFixPipeline` ініціює весь комплексне виконання пайплайну. Ролі є чітко розподілені: `detector` лише виявляє, тоді як `T0` та `worker` здійснюють зміни. Успішне завершення визначається виключно канонічним повторним виявленням.
13
-
14
- ## Поведінка
15
-
16
- Поведінка:
17
- fixConcern застосовує детерміновані патерни (T0), а потім, якщо це можливо, послідовно виконує ланцюжок фікс-операцій (ladder) для виявлення та усунення порушень певного concern-а.
18
- runFixPipeline керує повним циклом виправлення: він детектирує всі порушення, застосовує виправлення для кожного знайденого concern-а через `fixConcern`, і виводить фінальний звіт про нездоланні порушення.
19
- Per-tier timeout (ADR 260620-0556): кожен rung передає worker-у свій `timeoutMs` через `FixContext` (шлях до `runAgentFix opts.timeoutMs`, який abort-ить LLM-сесію), а сам виклик worker-а додатково огорнутий backstop-гонкою ×1.25 від `rung.timeoutMs` — worker, що ігнорує таймаут (зокрема зависла cloud-SSE), фейлить rung помилкою `fix timeout …` замість блокувати lint назавжди; така помилка класифікується як quality і ladder ескалює далі.
20
- Semantic-collateral veto (spec pi-fix-engine-migration §12, addendum 2026-07-05): clean-вердикт rung-а не приймається, якщо rung змінив наявні файли поза target-set порушення (`violations[].file ∪ item.files`, звірка через `collateral-veto.mjs` за `snapshot.modifiedExisting()`); наслідок — rollback S1, `🚫`-лог, feedback наступному rung-у й телеметрія `kind:"collateral-veto"` у глобальний llm-trace. Нові файли дозволені; порожній target-set → veto незастосовний (fail-open).
21
- Evidence-гейт рунга (Фаза A1 run-harness, спека 2026-07-11): кожен rung отримує у `FixContext` `verify` — item-scoped canonical re-detect (той самий детектор, що й фінальний вердикт), і `verifyMax` per tier (local — 1, cloud — 2); worker прокидає їх у `runAgentFix`, де провал verify інʼєктиться фідбеком у ту саму pi-сесію. Зовнішній canonical re-detect після worker-а лишається єдиним вердиктом рунга; помилка детектора всередині verify ковтається у `{ok:false}` — зовнішній detect кине її штатно.
22
- Durable-write-и (issue nitra/cursor#16): worker отримує у `FixContext` поруч із `recordWrite` опційний `recordDurableWrite` — для записів, кожен з яких є самодостатнім кінцевим станом (doc-files: дока зі свіжим CRC). Такі файли переживають rollback провального rung-а і не входять у collateral-veto: частковий прогрес великого батчу не стирається, canonical re-detect наступного rung-а/прогону рахує лише те, що реально лишилось.
23
- MT-tail (Фаза B, спека 2026-07-11): коли лишився невиправлений хвіст (worst=1), `renderRemaining` повертає зібрані порушення, і вони матеріалізуються у вузли MT-графа через `materializeTail` (mt-tail.mjs). Єдиний гейт — onboarded-репо (наявність `.mt.json`); fail-open: MT недоступний або будь-яка помилка → лог, lint не падає.
24
- Distillation-телеметрія (Фаза C, §13 pi-migration): успішний agentic-рунг (canonical clean, без veto, з реальними правками у worker telemetry) пише запис `oldText→newText` у глобальний стор (`recordFixTelemetry`, `~/.n-rules/telemetry/<rule>/open/`) — корпус для маховика дистиляції T0. Best-effort; T0/ручні фікси не пишуться.
25
- Rollback на провалі re-detect-а: якщо canonical re-detect усередині rung-а сам кидає виняток (worker/LLM лишив файл синтаксично невалідним — детектор/conftest не може його розпарсити), `runRung` спершу відкочує `snapshot` до S1, і лише потім перекидає виняток далі — без цього зіпсований проміжний стан worker-а лишався б на диску назавжди (виняток абортує весь прогін до звичайного rollback-коду).
26
- skipLocalTier (concern-meta.mjs): `selectLadder` перед циклом ladder-а фільтрує з нього local-min/local-min-retry rung-и, якщо `item.entry.concern.skipLocalTier === true` — перша спроба одразу йде на cloud-min. Для concern-ів, де local-tier емпірично майже завжди лише витрачає бюджет rung-а без результату (виявлено на реальному прогоні 2026-07-18: 0/12 успіхів local-tier для `js/eslint`).
27
-
28
13
  ## Публічний API
29
14
 
30
- fixConcern — Виконує один етап перевірки у конвеєрі (T0 → S1 → ladder) та повідомляє про результат закриття цієї перевірки.
31
- runFixPipeline — Запускає повний цикл виправлення: ідентифікує всі проблеми, виправляє кожну, що не пройшла перевірку, і завершує роботу.
15
+ - fixConcern — Проводить ОДИН concern по pipeline: T0 → S1 → ladder. Повертає чи закрито.
16
+ - runFixPipeline — Повний fix-pipeline: detect усе fix кожен провальний concern exit code.
32
17
 
33
18
  ## Гарантії поведінки
34
19
 
35
- - Сам не редагує кодові файли (мутації роблять T0/worker); пише лише телеметрію collateral-veto у глобальний llm-trace (best-effort, ніколи не валить прогін).
20
+ - Власних операцій запису (ФС/БД) у файлі немає; виклики імпортованих модулів можуть писати.
@@ -3,27 +3,16 @@ type: JS Module
3
3
  title: snapshot.mjs
4
4
  resource: npm/scripts/lib/lint-surface/snapshot.mjs
5
5
  docgen:
6
- crc: 4a2fab06
7
- model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 100
6
+ crc: 949c1cac
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 55
10
+ issues: no-overview,short-behavior,best-of-2:retry-lost
9
11
  ---
10
12
 
11
- ## Огляд
12
-
13
- Централізований знімок попереднього образу (pre-image snapshot) та механізм відкату (rollback) забезпечують функціональність фікс-пайплайну відповідно до специфікації spec 2026-06-29, §Tier Ladder. Відкат відновлює попередній стан файлів, але не видаляє ті, що відсутні у знімку. Операції виконуються послідовно для кожного окремого concern-а, гарантуючи, що стан кожного concern-а базується на успішних змінах попередніх concern-ів. До запису в tracker.record ПЕРЕД мутацією забезпечується фіксація попереднього образу для кожного потенційно зміненого файлу. Окремо підтримуються durable-write-и — самодостатні кінцеві стани (напр. файлові доки зі свіжим CRC), які rollback не чіпає.
14
-
15
- ## Поведінка
16
-
17
- 1. Створюється об'єкт-трекер попереднього стану. Цей трекер фіксує вихідний стан файлів, які можуть бути змінені в процесі виправлення.
18
- 2. Для фіксації стану файлу використовується механізм `record`, який зберігає вміст файлу або маркер відсутності, якщо файл на момент фіксації не існував.
19
- 3. Механізм `recordDurable` позначає шлях як durable: запис у такий файл — самодостатній кінцевий стан, тож rollback його не відновлює й не видаляє, а semantic-collateral veto його не враховує. Це дозволяє batch-worker-ам (doc-files) зберігати частковий прогрес після провалу/таймауту rung-а — беклог сходиться за кілька прогонів (issue nitra/cursor#16).
20
- 4. Після виконання змін, трекеру викликається `rollback`. Цей метод відновлює вміст усіх зафіксованих файлів (крім durable-позначених), а файли, які відсутні на момент фіксації, видаляє.
21
- 5. Щоб отримати список усіх файлів, чий попередній стан був зафіксований або які позначені durable, використовується метод `touched`.
22
- 6. Метод `modifiedExisting` повертає наявні на момент S1 файли, чий поточний вміст відрізняється від pre-image (видалення наявного файлу теж вважається зміною) — вхід semantic-collateral veto; нові файли та durable-шляхи до результату не входять.
23
-
24
13
  ## Публічний API
25
14
 
26
- - createSnapshot — створює фіксацію стану (snapshot S1) з порожнім набором попередніх зображень.
15
+ - createSnapshot — Створює tracker зі свіжим (порожнім) набором pre-images це і є snapshot S1.
27
16
 
28
17
  ## Гарантії поведінки
29
18