@7n/rules 1.34.1 → 1.36.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 +26 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/rules/changelog/lib/docs/package-manifest.md +1 -1
- package/rules/changelog/lib/package-manifest.mjs +3 -3
- package/rules/doc-files/docgen-gen/docs/main.md +1 -1
- package/rules/doc-files/docgen-gen/main.mjs +35 -14
- package/rules/doc-files/docgen-prompts/docs/main.md +1 -1
- package/rules/doc-files/docgen-prompts/main.mjs +48 -14
- package/rules/k8s/dremio_logging/dremio_logging.mdc +2 -0
- package/rules/k8s/dremio_logging/dremio_logging.rego +4 -0
- package/rules/tauri/core_test_isolation/docs/main.md +1 -1
- package/rules/tauri/core_test_isolation/main.mjs +4 -3
- package/rules/tauri/gitignore_target/concern.json +8 -0
- package/rules/tauri/gitignore_target/docs/fix-gitignore_target.md +31 -0
- package/rules/tauri/gitignore_target/docs/index.md +10 -0
- package/rules/tauri/gitignore_target/docs/main.md +37 -0
- package/rules/tauri/gitignore_target/fix-gitignore_target.mjs +78 -0
- package/rules/tauri/gitignore_target/gitignore_target.mdc +24 -0
- package/rules/tauri/gitignore_target/main.mjs +84 -0
- package/rules/text/run-v8r/docs/main.md +1 -1
- package/rules/text/run-v8r/main.mjs +63 -2
- package/scripts/lib/acp-runner.mjs +56 -9
- package/scripts/lib/docs/workspaces.md +4 -4
- package/scripts/lib/workspaces.mjs +7 -8
- package/scripts/utils/docs/glob-compat.md +31 -0
- package/scripts/utils/docs/index.md +1 -0
- package/scripts/utils/docs/resolve-js-root.md +1 -1
- package/scripts/utils/glob-compat.mjs +36 -0
- package/scripts/utils/resolve-js-root.mjs +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.36.0] - 2026-07-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- doc-files: секція «Публічний API» рендериться дослівно для експортів із JSDoc-описом (0 LLM-токенів, 0 перефразувань), LLM викликається лише на прогалини (без опису або зі stub-заглушкою «опис.»)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- тестовий no-op touch README для перевірки npm-publish CI
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- workspaces/resolve-js-root/package-manifest/tauri core_test_isolation: node:fs/promises#glob → Bun.Glob — платформна прогалина Node-compat шиму на self-hosted Linux Bun (export 'glob' відсутній), спостережено в @7n/rules-ci-azure lint job
|
|
16
|
+
- hook під Node (npx): «detector changelog/consistency: import впав: Bun is not defined» — top-level `new Bun.Glob(...)` у workspaces.mjs/package-manifest.mjs валив import детектора; усі використання Bun.Glob (разом із resolve-js-root і tauri/core_test_isolation) переведені на runtime-нейтральний scanGlob (Bun.Glob під Bun, node:fs/promises#glob під Node)
|
|
17
|
+
|
|
18
|
+
## [1.35.0] - 2026-07-20
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- tauri: додано концерн gitignore_target — перевірка й автофікс ignore-запису ws/src-tauri/target/ у корінному .gitignore (typo-інцидент nitra/task)
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- run-v8r: не рахувати ajv schema-compile-помилку (несправна зовнішня схема, напр. unicodeRegExp-несумісність у офіційній azure-pipelines-vscode/service-schema.json) як порушення файлу — друкує ⚠-попередження й пропускає, замість постійного false-positive ✖
|
|
27
|
+
- acp-runner (skill claude): watchdog дочірнього ACP-процесу — fail-fast, якщо агент не запустився чи вийшов до кінця ходу, замість вічного зависання connection.prompt
|
|
28
|
+
|
|
3
29
|
## [1.34.1] - 2026-07-20
|
|
4
30
|
|
|
5
31
|
### Fixed
|
package/README.md
CHANGED
|
@@ -187,3 +187,4 @@ npm/rules/<id>/
|
|
|
187
187
|
Консольна утиліта яка дозволить оновлювати в локальних GIT репозиторіях правила для cursor з можливістю наслідування правил від файлів в цьому репозиторії та забезпечення версійності правил для cursor.
|
|
188
188
|
|
|
189
189
|
<!-- no-op: touch npm/** щоб retrigger-нути npm-publish CI для кореневого bun.lock-фіксу (без цього path-фільтр push-тригера не спрацьовує) -->
|
|
190
|
+
<!-- no-op: тестовий touch npm/** для перевірки npm-publish CI (2026-07-20) -->
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: package-manifest.mjs
|
|
4
4
|
resource: npm/rules/changelog/lib/package-manifest.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: b6f24bb6
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Модуль `package-manifest.mjs` реалізує **уніфіковану абстракцію маніфесту пакета** для перевірок changelog у багатомовному монорепо. Він приховує відмінності між двома типами маніфестів:
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* або `pyproject.toml` (Python / PEP 621, Poetry).
|
|
4
4
|
*/
|
|
5
5
|
import { existsSync } from 'node:fs'
|
|
6
|
-
import {
|
|
6
|
+
import { readFile } from 'node:fs/promises'
|
|
7
7
|
import { dirname, join, relative } from 'node:path'
|
|
8
8
|
|
|
9
9
|
import { parse as parseToml } from 'smol-toml'
|
|
10
10
|
|
|
11
|
+
import { scanGlob } from '../../../scripts/utils/glob-compat.mjs'
|
|
11
12
|
import { getMonorepoPackageRootDirs, isIgnoredWorkspaceRoot } from '../../../scripts/lib/workspaces.mjs'
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -26,7 +27,6 @@ import { getMonorepoPackageRootDirs, isIgnoredWorkspaceRoot } from '../../../scr
|
|
|
26
27
|
* @property {'major' | 'minor' | 'patch' | null} maxBump стеля для `n-rules release` (з `package.json#release.maxBump`); `null` — без обмеження
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
|
-
const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']
|
|
30
30
|
const VALID_MAX_BUMPS = new Set(['major', 'minor', 'patch'])
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -146,7 +146,7 @@ export async function getMonorepoProjectRootDirs(repoRoot = '.') {
|
|
|
146
146
|
roots.add('.')
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
for await (const relPy of
|
|
149
|
+
for await (const relPy of scanGlob('**/pyproject.toml', repoRoot)) {
|
|
150
150
|
const absDir = dirname(join(repoRoot, relPy))
|
|
151
151
|
const relRoot = relative(repoRoot, absDir)
|
|
152
152
|
const ws = relRoot === '' ? '.' : relRoot
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
overviewMessages,
|
|
18
18
|
criticMessages,
|
|
19
19
|
refineMessages,
|
|
20
|
-
guaranteesFromMarkers
|
|
20
|
+
guaranteesFromMarkers,
|
|
21
|
+
isApiGap,
|
|
22
|
+
renderApiLine,
|
|
23
|
+
apiGapMessages
|
|
21
24
|
} from '../docgen-prompts/main.mjs'
|
|
22
25
|
|
|
23
26
|
/** Облік LLM-викликів і часу в них у межах однієї генерації (скидається на старті generateDoc). */
|
|
@@ -338,15 +341,36 @@ async function critiqueRefineSection(sectionKey, draft, facts, anchors, model, t
|
|
|
338
341
|
}
|
|
339
342
|
|
|
340
343
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
344
|
+
* Stage 1/3 (гібрид doc-files, ADR 260719-2155): «Публічний API» — покриті
|
|
345
|
+
* JSDoc-описом експорти рендеряться дослівно (`renderApiLine`, 0 токенів, 0
|
|
346
|
+
* галюцинацій), LLM викликається лише на прогалини (`isApiGap`). Якщо прогалин
|
|
347
|
+
* немає — секція збирається БЕЗ жодного LLM-виклику. Єдиний непокритий
|
|
348
|
+
* експорт (як і раніше) лишається описаним лише в Поведінці — окремого виклику
|
|
349
|
+
* на секцію з одного рядка не варте.
|
|
343
350
|
* @param {object} facts факт-лист
|
|
344
|
-
* @
|
|
351
|
+
* @param {object|null} anchors анкори файлу
|
|
352
|
+
* @param {string} model model-id
|
|
353
|
+
* @param {number} timeoutMs ліміт на LLM-виклик
|
|
354
|
+
* @param {number} [temperature] температура LLM-виклику (best-of-2 підвищує)
|
|
355
|
+
* @returns {Promise<string>} текст секції «Публічний API» (може бути порожнім рядком)
|
|
345
356
|
*/
|
|
346
|
-
function
|
|
357
|
+
export async function buildApiSection(facts, anchors, model, timeoutMs, temperature = 0.2) {
|
|
347
358
|
const exps = facts.exports ?? []
|
|
348
|
-
if (exps.length
|
|
349
|
-
|
|
359
|
+
if (!exps.length) return ''
|
|
360
|
+
if (exps.length === 1 && isApiGap(exps[0])) return ''
|
|
361
|
+
const covered = exps.filter(e => !isApiGap(e))
|
|
362
|
+
const gap = exps.filter(isApiGap)
|
|
363
|
+
const coveredBlock = covered.map(e => renderApiLine(e)).join('\n')
|
|
364
|
+
if (!gap.length) return coveredBlock
|
|
365
|
+
let gapDraft = stripSignatures(
|
|
366
|
+
stripSection(await callLlm(apiGapMessages(gap, anchors), model, { timeoutMs, temperature }))
|
|
367
|
+
)
|
|
368
|
+
// E2: critique→refine лише коли ВСІ експорти — прогалина (там модель найбільш
|
|
369
|
+
// схильна зривати на generic-фрази без жодного JSDoc-«якоря» поруч).
|
|
370
|
+
if (gap.length === exps.length) {
|
|
371
|
+
gapDraft = await critiqueRefineSection('api', gapDraft, facts, anchors, model, timeoutMs)
|
|
372
|
+
}
|
|
373
|
+
return [coveredBlock, gapDraft].filter(Boolean).join('\n')
|
|
350
374
|
}
|
|
351
375
|
|
|
352
376
|
/**
|
|
@@ -407,15 +431,12 @@ async function orchestratedDoc(
|
|
|
407
431
|
const anc = anchors ?? extractAnchors(src)
|
|
408
432
|
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
|
|
409
433
|
sections.guarantees = guaranteesFromMarkers(facts)
|
|
410
|
-
// Спершу Поведінка
|
|
434
|
+
// Спершу Поведінка — єдина секція з кодом (sectionMessages повертає лише її)
|
|
411
435
|
for (const s of sectionMessages(facts, src, anc, intent)) {
|
|
412
|
-
|
|
413
|
-
// E2: critique→refine для API, коли всі описи порожні (модель зриває на generic)
|
|
414
|
-
if (s.key === 'api' && apiNeedsRefine(facts)) {
|
|
415
|
-
draft = await critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
|
|
416
|
-
}
|
|
417
|
-
sections[s.key] = draft
|
|
436
|
+
sections[s.key] = stripSignatures(stripSection(await callLlm(s.messages, model, { timeoutMs, temperature })))
|
|
418
437
|
}
|
|
438
|
+
// Stage 1/3: «Публічний API» — покриті JSDoc експорти дослівно, LLM лише на прогалини
|
|
439
|
+
sections.api = await buildApiSection(facts, anc, model, timeoutMs, temperature)
|
|
419
440
|
// R3: «Огляд» — ОСТАННІМ, узагальненням уже написаної Поведінки (не голого факт-листа)
|
|
420
441
|
let overview = stripSignatures(
|
|
421
442
|
stripSection(
|
|
@@ -65,12 +65,15 @@ function intentContext(intent) {
|
|
|
65
65
|
/**
|
|
66
66
|
* Секційні набори messages з МІНІМАЛЬНИМ контекстом під кожну секцію.
|
|
67
67
|
* Код потрапляє лише в `behavior`; «Огляд» генерується окремо ОСТАННІМ
|
|
68
|
-
* (`overviewMessages`) з уже написаної Поведінки — тут його немає.
|
|
68
|
+
* (`overviewMessages`) з уже написаної Поведінки — тут його немає. «Публічний
|
|
69
|
+
* API» сюди більше не входить (Stage 1/3, гібрид doc-files ADR 260719-2155):
|
|
70
|
+
* покриті JSDoc-описом експорти рендеряться дослівно без LLM (`renderApiLine`),
|
|
71
|
+
* LLM викликається лише на прогалини (`apiGapMessages`) — див. `isApiGap`.
|
|
69
72
|
* @param {object} facts факт-лист про файл
|
|
70
73
|
* @param {string} src вміст файлу
|
|
71
74
|
* @param {object|null} [anchors] анкори файлу для обовʼязкового включення
|
|
72
75
|
* @param {string|null} [intent] захищена секція «Призначення» як read-only контекст
|
|
73
|
-
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (behavior
|
|
76
|
+
* @returns {Array<{key:string, messages:object[], numPredict:number}>} набір секційних промптів (лише behavior)
|
|
74
77
|
*/
|
|
75
78
|
export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
76
79
|
const factsTxt = factsSummary(facts)
|
|
@@ -97,19 +100,50 @@ export function sectionMessages(facts, src, anchors = null, intent = null) {
|
|
|
97
100
|
`Напиши вміст секції «Поведінка»: ${behaviorTask}.${onlyExports} Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${noInternal} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
|
|
98
101
|
)
|
|
99
102
|
}
|
|
103
|
+
return [behavior]
|
|
104
|
+
}
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
// «опис.» — та сама JSDoc-заглушка без сенсу, яку lang-екстрактори (extractors.mjs)
|
|
107
|
+
// вже відкидають при парсингу; тут — другий, незалежний gate на рівні facts.exports
|
|
108
|
+
// (захист від прогалин, що прийшли з інших джерел фактів, напр. майбутніх мов).
|
|
109
|
+
const STUB_DESC_RE = /^опис\.?$/i
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stage 2 (gap-детект, 0 токенів): чи є опис експорту прогалиною — відсутній
|
|
113
|
+
* або JSDoc-заглушка без сенсу.
|
|
114
|
+
* @param {{desc?:string}} exp запис експорту з факт-листа
|
|
115
|
+
* @returns {boolean} true — опис потрібно синтезувати LLM (Stage 3)
|
|
116
|
+
*/
|
|
117
|
+
export function isApiGap(exp) {
|
|
118
|
+
const desc = (exp.desc ?? '').trim()
|
|
119
|
+
return !desc || STUB_DESC_RE.test(desc)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Stage 1 (скриптовий рендер, 0 токенів, 0 галюцинацій): дослівний рядок
|
|
124
|
+
* «Публічного API» з покритого JSDoc-описом експорту — без перефразування LLM.
|
|
125
|
+
* @param {{name:string, desc:string}} exp запис експорту з непорожнім desc
|
|
126
|
+
* @returns {string} рядок маркованого списку
|
|
127
|
+
*/
|
|
128
|
+
export function renderApiLine(exp) {
|
|
129
|
+
return `- ${exp.name} — ${exp.desc.trim()}`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stage 3: messages ЛИШЕ для експортів-прогалин (без desc) — вужчий промпт,
|
|
134
|
+
* ніж попередній «переписати весь список своїми словами» (жодного контакту з
|
|
135
|
+
* уже покритими JSDoc експортами, 0 ризику спотворити авторський текст).
|
|
136
|
+
* @param {Array<{name:string}>} gapExports експорти без опису (isApiGap === true)
|
|
137
|
+
* @param {object|null} [anchors] анкори файлу
|
|
138
|
+
* @returns {Array<{role:string,content:string}>} messages-масив для LLM
|
|
139
|
+
*/
|
|
140
|
+
export function apiGapMessages(gapExports, anchors = null) {
|
|
141
|
+
const anch = anchorsBlock(anchors)
|
|
142
|
+
const list = gapExports.map(e => `- ${e.name}`).join('\n')
|
|
143
|
+
return msgs(
|
|
144
|
+
`${STYLE}${anch}`,
|
|
145
|
+
`Для кожної названої публічної функції напиши один рядок маркованого списку «назва — що робить», СВОЇМИ словами, без типів і сигнатур, РІВНО у цьому порядку й з РІВНО цими назвами:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
|
|
146
|
+
)
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
/**
|
|
@@ -14,6 +14,7 @@ Rego-пакет: `k8s.dremio_logging`
|
|
|
14
14
|
| `com.dremio.service.reflection.descriptor.MaterializationCache` |
|
|
15
15
|
| `com.dremio.exec.planner.plancache.PlanCacheSynchronizer` |
|
|
16
16
|
| `com.dremio.exec.planner.plancache.CacheRefresher` |
|
|
17
|
+
| `com.dremio.sabot.exec.FragmentExecutors` (множина, пакет `exec` БЕЗ `fragment` — не плутати з `fragment.FragmentExecutor` вище) |
|
|
17
18
|
|
|
18
19
|
**Чому:** ці loggerи (усі на одному `planning-cache-refresher` треді; підтверджено живою перевіркою в проді) пишуть INFO на кожен запит/фоновий цикл і домінують обсяг логів у Cloud Logging без діагностичної цінності понад WARN. Список фіксований — не розширюй «про запас»; нові loggerи — окремою ітерацією.
|
|
19
20
|
|
|
@@ -26,6 +27,7 @@ Rego-пакет: `k8s.dremio_logging`
|
|
|
26
27
|
<logger name="com.dremio.service.reflection.descriptor.MaterializationCache" level="warn"/>
|
|
27
28
|
<logger name="com.dremio.exec.planner.plancache.PlanCacheSynchronizer" level="warn"/>
|
|
28
29
|
<logger name="com.dremio.exec.planner.plancache.CacheRefresher" level="warn"/>
|
|
30
|
+
<logger name="com.dremio.sabot.exec.FragmentExecutors" level="warn"/>
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
**Autofix:** відсутній (detection-only, як `hasura_httproute`) — XML-трансформ складніший за YAML `setIn`; T0-fix, за потреби, окремою ітерацією.
|
|
@@ -35,6 +35,10 @@ required_loggers := {
|
|
|
35
35
|
"com.dremio.service.reflection.descriptor.MaterializationCache",
|
|
36
36
|
"com.dremio.exec.planner.plancache.PlanCacheSynchronizer",
|
|
37
37
|
"com.dremio.exec.planner.plancache.CacheRefresher",
|
|
38
|
+
# NB: FragmentExecutors (множина, пакет exec, БЕЗ fragment) — окремий клас від
|
|
39
|
+
# fragment.FragmentExecutor вище, легко переплутати за назвою. Логує
|
|
40
|
+
# "Received remote fragment start instruction..." на кожен фрагмент.
|
|
41
|
+
"com.dremio.sabot.exec.FragmentExecutors",
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
# Рівні logback, що задовольняють вимогу «warn або строгіше».
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
* ризикований (виділення крейту, перенесення коду) — лише репорт (core_test_isolation.mdc).
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync } from 'node:fs'
|
|
10
|
-
import {
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
11
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
12
12
|
|
|
13
13
|
import { parse as parseToml } from 'smol-toml'
|
|
14
14
|
|
|
15
|
+
import { scanGlob } from '../../../scripts/utils/glob-compat.mjs'
|
|
15
16
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
16
17
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
17
18
|
|
|
@@ -65,7 +66,7 @@ async function resolveWorkspaceMemberDirs(srcTauriDir, members) {
|
|
|
65
66
|
const found = new Set()
|
|
66
67
|
for (const pattern of members) {
|
|
67
68
|
if (pattern.includes('*')) {
|
|
68
|
-
for await (const rel of
|
|
69
|
+
for await (const rel of scanGlob(pattern, srcTauriDir)) {
|
|
69
70
|
const abs = resolve(srcTauriDir, rel)
|
|
70
71
|
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
71
72
|
}
|
|
@@ -84,7 +85,7 @@ async function resolveWorkspaceMemberDirs(srcTauriDir, members) {
|
|
|
84
85
|
* @returns {Promise<boolean>} true, якщо знайдено відповідний маркер
|
|
85
86
|
*/
|
|
86
87
|
async function hasFakeLlmProviderMarker(crateDir) {
|
|
87
|
-
for await (const relPath of
|
|
88
|
+
for await (const relPath of scanGlob('**/*.rs', crateDir)) {
|
|
88
89
|
if (relPath.includes('target/')) continue
|
|
89
90
|
const content = await readFile(join(crateDir, relPath), 'utf8')
|
|
90
91
|
if (FAKE_PROVIDER_RE.test(content)) return true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: fix-gitignore_target.mjs
|
|
4
|
+
resource: npm/rules/tauri/gitignore_target/fix-gitignore_target.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 0ceef709
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
score: 100
|
|
9
|
+
issues: judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
T0-autofix для `tauri/gitignore_target`: дописує в корінний `.gitignore` відсутні ignore-записи `<ws>/src-tauri/target/` у секцію з міткою `# Tauri — Rust build artifacts (tauri.mdc)` (константа `GITIGNORE_TARGET_HEADER`). `insertMissingTargetEntries` виконує сам текстовий splice; `patterns` — масив T0-визначень (`id`/`test`/`apply`) центрального fix-pipeline, що зчитує відсутні записи з `violation.data.missing` і застосовує `insertMissingTargetEntries` до файлів через `applyToFiles`.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- `GITIGNORE_TARGET_HEADER` — мітка секції в корінному `.gitignore`, куди групуються записи для Tauri build artifacts.
|
|
20
|
+
- `insertMissingTargetEntries` — дописує відсутні ignore-записи для `<ws>/src-tauri/target/` у `.gitignore`: або в наявну секцію з `GITIGNORE_TARGET_HEADER` (поруч з іншими entries), або в новий блок у кінець файла; якщо нічого додавати не треба (`missingEntries` порожній), повертає `null` і вміст не змінюється.
|
|
21
|
+
- `patterns` — один T0-патерн (`tauri-gitignore-target-insert`): спрацьовує на violations з `data.kind === MISSING_GITIGNORE_TARGET_ENTRIES` і застосовує `insertMissingTargetEntries` до кожного цільового `.gitignore`.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
- GITIGNORE_TARGET_HEADER — заголовок секції в корінному `.gitignore` для Tauri build-артефактів.
|
|
26
|
+
- insertMissingTargetEntries — додає відсутні `"<ws>/src-tauri/target/"` у відповідну секцію `.gitignore` або створює новий блок наприкінці файла.
|
|
27
|
+
- patterns — T0-визначення autofix-у для detector-а `tauri/gitignore_target`, що підключається до центрального fix-pipeline.
|
|
28
|
+
|
|
29
|
+
## Гарантії поведінки
|
|
30
|
+
|
|
31
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: npm/rules/tauri/gitignore_target
|
|
4
|
+
resource: npm/rules/tauri/gitignore_target/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| --------------------------------------------------- | --------- |
|
|
9
|
+
| [fix-gitignore_target.mjs](fix-gitignore_target.md) | JS Module |
|
|
10
|
+
| [main.mjs](main.md) | JS Module |
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: npm/rules/tauri/gitignore_target/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 83aa81ee
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 85
|
|
10
|
+
issues: internal-name:findSrcTauriDirs,anchor-miss:(tauri.mdc),judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл read-only обходить усі `<ws>/src-tauri/` у монорепо спільним механізмом із `tauri/cargo_mutants_config` (`findSrcTauriDirs`), без дублювання обходу. Для кожного знайденого workspace корінний `ROOT_GITIGNORE = ".gitignore"` повинен містити точний запис `<ws>/src-tauri/target/`, звірений по точному рядку — typo на кшталт `owner/target/` (реальний інцидент у `nitra/task`) не рахується присутністю потрібного запису.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `MISSING_GITIGNORE_TARGET_ENTRIES` — reason `"missing-gitignore-target-entries"` для порушення, коли в корінному `.gitignore` бракує записів для Tauri build-артефактів.
|
|
21
|
+
- `ROOT_GITIGNORE` — назва корінного файлу `".gitignore"`, у якому мають бути всі очікувані ignore-записи.
|
|
22
|
+
- `expectedTargetEntry` — формує точний ignore-запис для `target/` конкретного `src-tauri/` відносно кореня монорепо.
|
|
23
|
+
- `findMissingEntries` — визначає, яких очікуваних ignore-записів немає у `.gitignore`, за точним збігом рядка (після trim).
|
|
24
|
+
- `lint` — read-only перевіряє всі знайдені `src-tauri/` у монорепо й повідомляє про відсутні ignore-записи одним violation на весь `.gitignore`.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- MISSING_GITIGNORE_TARGET_ENTRIES — reason `missing-gitignore-target-entries` для випадків, коли в кореневому `.gitignore` немає записів для `src-tauri/target/`.
|
|
29
|
+
- ROOT_GITIGNORE — файл `.gitignore` у корені монорепо, який є єдиним місцем для цих ignore-записів.
|
|
30
|
+
- expectedTargetEntry — формує потрібний запис для ігнорування build-артефактів окремого workspace з `src-tauri/`.
|
|
31
|
+
- findMissingEntries — визначає, яких очікуваних записів бракує в кореневому `.gitignore`; збіг рахується лише для точного рядка після trim, тому `target/` або `owner/target/` не замінюють потрібний запис.
|
|
32
|
+
- lint — повідомляє про workspace з Tauri, для яких у кореневому `.gitignore` бракує `src-tauri/target/`.
|
|
33
|
+
|
|
34
|
+
## Гарантії поведінки
|
|
35
|
+
|
|
36
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
37
|
+
- Свідомо пропускає шляхи: `.git`.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** @see ./docs/fix-gitignore_target.md */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* T0-autofix для `tauri/gitignore_target` — детерміновано дописує в корінний
|
|
5
|
+
* `.gitignore` відсутні ignore-записи `<ws>/src-tauri/target/` (з violation.data.missing,
|
|
6
|
+
* а не з повторного сканування монорепо — детектор уже визначив, яких саме
|
|
7
|
+
* записів бракує). Текстовий splice (як `tauri/linux_deps`): зберігає коментарі
|
|
8
|
+
* й формат, мінімальний diff. Ідемпотентно: повторний прогін на вже
|
|
9
|
+
* виправленому файлі нічого не змінює (`findMissingEntries` у main.mjs
|
|
10
|
+
* повторно перевіряє стан файла).
|
|
11
|
+
*/
|
|
12
|
+
import { applyToFiles } from '../../../scripts/utils/apply-to-files.mjs'
|
|
13
|
+
|
|
14
|
+
import { MISSING_GITIGNORE_TARGET_ENTRIES } from './main.mjs'
|
|
15
|
+
|
|
16
|
+
/** Заголовок-коментар секції Tauri build-артефактів у корінному `.gitignore`. */
|
|
17
|
+
export const GITIGNORE_TARGET_HEADER = '# Tauri — Rust build artifacts (tauri.mdc)'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Знаходить кінець контурного блоку entries, що йде одразу за заголовком
|
|
21
|
+
* (перший порожній рядок, наступний коментар або кінець файла).
|
|
22
|
+
* @param {string[]} lines рядки `.gitignore`
|
|
23
|
+
* @param {number} headerIdx індекс рядка заголовка
|
|
24
|
+
* @returns {number} індекс, куди вставляти нові entries
|
|
25
|
+
*/
|
|
26
|
+
function findBlockEnd(lines, headerIdx) {
|
|
27
|
+
let i = headerIdx + 1
|
|
28
|
+
while (i < lines.length && lines[i].trim() !== '' && !lines[i].trim().startsWith('#')) i++
|
|
29
|
+
return i
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Дописує відсутні `<ws>/src-tauri/target/` entries: якщо секція `GITIGNORE_TARGET_HEADER`
|
|
34
|
+
* вже є — вставляє в кінець її блоку (поруч з наявними entries); інакше додає
|
|
35
|
+
* новий блок (заголовок + entries) у кінець файла.
|
|
36
|
+
* @param {string} content вміст `.gitignore`
|
|
37
|
+
* @param {string[]} missingEntries відсутні ignore-рядки (`<ws>/src-tauri/target/`)
|
|
38
|
+
* @returns {string|null} новий вміст або null, якщо нічого не змінилось
|
|
39
|
+
*/
|
|
40
|
+
export function insertMissingTargetEntries(content, missingEntries) {
|
|
41
|
+
if (missingEntries.length === 0) return null
|
|
42
|
+
|
|
43
|
+
const lines = content.split('\n')
|
|
44
|
+
const headerIdx = lines.findIndex(l => l.trim() === GITIGNORE_TARGET_HEADER)
|
|
45
|
+
|
|
46
|
+
if (headerIdx !== -1) {
|
|
47
|
+
const blockEnd = findBlockEnd(lines, headerIdx)
|
|
48
|
+
lines.splice(blockEnd, 0, ...missingEntries)
|
|
49
|
+
return lines.join('\n')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const trailingBlank = lines.length > 0 && lines.at(-1) === ''
|
|
53
|
+
const body = trailingBlank ? lines.slice(0, -1) : lines
|
|
54
|
+
const needsBlankSep = body.length > 0 && body.at(-1).trim() !== ''
|
|
55
|
+
|
|
56
|
+
const next = [...body]
|
|
57
|
+
if (needsBlankSep) next.push('')
|
|
58
|
+
next.push(GITIGNORE_TARGET_HEADER, ...missingEntries, '')
|
|
59
|
+
return next.join('\n')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
|
|
63
|
+
export const patterns = [
|
|
64
|
+
{
|
|
65
|
+
id: 'tauri-gitignore-target-insert',
|
|
66
|
+
test: violations => violations.some(v => v.data?.kind === MISSING_GITIGNORE_TARGET_ENTRIES && v.file),
|
|
67
|
+
apply: (violations, ctx) => {
|
|
68
|
+
const targets = violations.filter(v => v.data?.kind === MISSING_GITIGNORE_TARGET_ENTRIES && v.file)
|
|
69
|
+
const touchedFiles = applyToFiles(targets, ctx, rel => content => {
|
|
70
|
+
const v = targets.find(x => x.file === rel)
|
|
71
|
+
return insertMissingTargetEntries(content, v?.data?.missing ?? [])
|
|
72
|
+
})
|
|
73
|
+
return touchedFiles.length > 0
|
|
74
|
+
? { touchedFiles, message: `Tauri build-артефакти → .gitignore (${touchedFiles.length} file(s))` }
|
|
75
|
+
: { touchedFiles: [] }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## `.gitignore`: ignore build-артефактів `<ws>/src-tauri/target/`
|
|
2
|
+
|
|
3
|
+
Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в `<ws>/src-tauri/target/` — build-артефакти Cargo (сотні MB, десятки тисяч файлів). Корінний `.gitignore` монорепо повинен містити точний ignore-запис для кожного такого воркспейсу:
|
|
4
|
+
|
|
5
|
+
```gitignore
|
|
6
|
+
# Tauri — Rust build artifacts (tauri.mdc)
|
|
7
|
+
<ws>/src-tauri/target/
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
### Чому точний шлях, а не голий `target/`
|
|
11
|
+
|
|
12
|
+
Реальний інцидент: у `nitra/task` корінний `.gitignore` мав `owner/target/` замість `owner/src-tauri/target/` — воркспейс скопійований з іншого проєкту без заміни шляху (typo). `target/` там ігнорувався не там, де компілятор його реально створює, тож ~600MB / ~28k файлів build-артефактів не потрапляли під жоден ignore-патерн і мало не потрапили в коміт через `git add -A`.
|
|
13
|
+
|
|
14
|
+
Тому перевірка звіряє **точний рядок** `<ws>/src-tauri/target/` для кожного знайденого `src-tauri/`, а не факт наявності підрядка `target/` десь у файлі — голий `target/` (чи вже наявний, але помилковий `owner/target/`) не закриває violation.
|
|
15
|
+
|
|
16
|
+
### Область дії
|
|
17
|
+
|
|
18
|
+
- Обхід усіх `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config` (`findSrcTauriDirs`, без дублювання обходу монорепо).
|
|
19
|
+
- Ціль фіксу — **один** корінний `.gitignore` монорепо (не по workspace).
|
|
20
|
+
- Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи (на кшталт помилкового `owner/target/` з інциденту вище) не видаляються — вони можуть бути навмисними чи потребувати ручного review.
|
|
21
|
+
|
|
22
|
+
### Автофікс (T0)
|
|
23
|
+
|
|
24
|
+
Дописує відсутні записи в кінець секції `# Tauri — Rust build artifacts (tauri.mdc)`: якщо секція вже є — поруч із наявними entries (мінімальний diff, без переписування файла); якщо нема — створює новий блок у кінці `.gitignore`. Ідемпотентно: повторний прогін на вже виправленому файлі нічого не змінює.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @see ./docs/main.md
|
|
3
|
+
*
|
|
4
|
+
* Read-only detector: для кожного знайденого `<ws>/src-tauri/` у монорепо
|
|
5
|
+
* корінний `.gitignore` повинен містити точний ignore-запис
|
|
6
|
+
* `<ws>/src-tauri/target/` (tauri.mdc).
|
|
7
|
+
*
|
|
8
|
+
* Реальний інцидент: у `nitra/task` `.gitignore` мав `owner/target/` замість
|
|
9
|
+
* `owner/src-tauri/target/` (typo — воркспейс скопійований без заміни шляху),
|
|
10
|
+
* тому build-артефакти (~600MB, ~28k файлів) не ігнорувались і мало не
|
|
11
|
+
* потрапили в коміт через `git add -A`. Тому перевірка — точний match рядка
|
|
12
|
+
* повного шляху, не пошук голого `target/` десь у файлі: голий `target/`
|
|
13
|
+
* (чи `owner/target/`) дав би false negative саме на цьому кейсі.
|
|
14
|
+
*
|
|
15
|
+
* Обхід `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config`
|
|
16
|
+
* (`findSrcTauriDirs`), без дублювання.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from 'node:fs'
|
|
19
|
+
import { readFile } from 'node:fs/promises'
|
|
20
|
+
import { join, relative } from 'node:path'
|
|
21
|
+
|
|
22
|
+
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
23
|
+
import { findSrcTauriDirs } from '../cargo_mutants_config/main.mjs'
|
|
24
|
+
|
|
25
|
+
/** Стабільний reason: у корінному `.gitignore` бракує ignore-запису(ів) для `src-tauri/target/`. */
|
|
26
|
+
export const MISSING_GITIGNORE_TARGET_ENTRIES = 'missing-gitignore-target-entries'
|
|
27
|
+
|
|
28
|
+
/** Корінний `.gitignore` — один на монорепо, не по workspace. */
|
|
29
|
+
export const ROOT_GITIGNORE = '.gitignore'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri/`.
|
|
33
|
+
* @param {string} cwd корінь монорепо
|
|
34
|
+
* @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
|
|
35
|
+
* @returns {string} очікуваний рядок у корінному `.gitignore` (`<ws>/src-tauri/target/`)
|
|
36
|
+
*/
|
|
37
|
+
export function expectedTargetEntry(cwd, srcTauriDir) {
|
|
38
|
+
return `${relative(cwd, srcTauriDir)}/target/`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Знаходить очікувані entries, яких бракує у вмісті кореневого `.gitignore`.
|
|
43
|
+
* Точний match рядка (після trim) — голий `target/` чи typo на кшталт
|
|
44
|
+
* `owner/target/` не рахуються присутністю очікуваного запису.
|
|
45
|
+
* @param {string} content вміст `.gitignore`
|
|
46
|
+
* @param {string[]} expectedEntries очікувані ignore-рядки
|
|
47
|
+
* @returns {string[]} відсутні entries (зі збереженням порядку expectedEntries)
|
|
48
|
+
*/
|
|
49
|
+
export function findMissingEntries(content, expectedEntries) {
|
|
50
|
+
const present = new Set(content.split('\n').map(l => l.trim()))
|
|
51
|
+
return expectedEntries.filter(e => !present.has(e))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінт-прогону
|
|
56
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат перевірки
|
|
57
|
+
*/
|
|
58
|
+
export async function lint(ctx) {
|
|
59
|
+
const cwd = ctx.cwd
|
|
60
|
+
const reporter = createViolationReporter(ctx)
|
|
61
|
+
|
|
62
|
+
const srcTauriDirs = await findSrcTauriDirs(cwd)
|
|
63
|
+
if (srcTauriDirs.length === 0) return reporter.result()
|
|
64
|
+
|
|
65
|
+
const expectedEntries = srcTauriDirs.map(dir => expectedTargetEntry(cwd, dir))
|
|
66
|
+
const abs = join(cwd, ROOT_GITIGNORE)
|
|
67
|
+
const content = existsSync(abs) ? await readFile(abs, 'utf8') : ''
|
|
68
|
+
|
|
69
|
+
const missing = findMissingEntries(content, expectedEntries)
|
|
70
|
+
if (missing.length === 0) {
|
|
71
|
+
reporter.pass(`${ROOT_GITIGNORE}: build-артефакти всіх src-tauri/ ігноруються`)
|
|
72
|
+
return reporter.result()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reporter.fail(
|
|
76
|
+
`${ROOT_GITIGNORE}: бракує ignore-запису(ів) для Tauri build-артефактів [${missing.join(', ')}] — build-артефакти можуть потрапити в коміт (tauri.mdc)`,
|
|
77
|
+
{
|
|
78
|
+
reason: MISSING_GITIGNORE_TARGET_ENTRIES,
|
|
79
|
+
file: ROOT_GITIGNORE,
|
|
80
|
+
data: { kind: MISSING_GITIGNORE_TARGET_ENTRIES, missing }
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return reporter.result()
|
|
84
|
+
}
|
|
@@ -116,6 +116,55 @@ const PROCESSING_LINE_RE = /^ℹ Processing (.+)$/u
|
|
|
116
116
|
const FOUND_REMOTE_SCHEMA_RE = /^ℹ Found schema in (https?:\/\/\S+)/u
|
|
117
117
|
const NOISE_LINE_RE = /^(?:ℹ .*|Resolving dependencies|Resolved, downloaded and extracted.*|Saved lockfile)$/u
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Рядок ajv-помилки компіляції самої схеми (не документа). v8r ловить це в `try/catch` навколо
|
|
121
|
+
* `ajv.compileAsync(schema)` (`validateDocument` у v8r/src/cli.js) і друкує голий
|
|
122
|
+
* `SyntaxError.message` без файлового контексту чи "is invalid"-заголовка — на відміну від
|
|
123
|
+
* genuine validation-помилки, де ajv повертає `errors[]` з прив'язкою до документа. Найчастіша
|
|
124
|
+
* причина: ajv за замовчуванням компілює `pattern` з прапорцем `/u` (`unicodeRegExp: true`), а
|
|
125
|
+
* реальні опубліковані схеми (напр. офіційна `azure-pipelines-vscode/service-schema.json`) містять
|
|
126
|
+
* legacy over-escaped regex, валідний поза Unicode-режимом, але `SyntaxError` у ньому. Це несправна
|
|
127
|
+
* схема, а не невалідний файл користувача — v8r не дає способу це відрізнити нативно (немає опції
|
|
128
|
+
* `unicodeRegExp` у config-schema.json), тому розрізняємо тут за форматом самого повідомлення.
|
|
129
|
+
* `logger.error` у v8r (src/logger.js) додає префікс `✖ ` перед `e.message` — опційний у regex,
|
|
130
|
+
* бо `extractFailureLines` не чіпає цей префікс (лише прибирає `ℹ`-шум).
|
|
131
|
+
*/
|
|
132
|
+
const AJV_SCHEMA_COMPILE_ERROR_RE = /^(?:✖ )?Invalid regular expression:.*$/mu
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Чи складається `detail` ВИКЛЮЧНО з рядків ajv-помилки компіляції схеми (без жодної genuine
|
|
136
|
+
* validation-помилки). Навмисно консервативно: якщо серед рядків `detail` є хоч один, що НЕ
|
|
137
|
+
* збігається з `AJV_SCHEMA_COMPILE_ERROR_RE` (напр. "file.yml is invalid" чи ajv `errors[]`-деталь
|
|
138
|
+
* genuine порушення в тому ж batch-виклику v8r по glob-у) — не втручаємось, викликач лишає
|
|
139
|
+
* оригінальний `code`/`detail` без змін, щоб не замаскувати реальну проблему.
|
|
140
|
+
* @param {string} detail рядки `✖ …` з `extractFailureLines`
|
|
141
|
+
* @returns {boolean} true — усі непорожні рядки `detail` є ajv schema-compile-помилками
|
|
142
|
+
*/
|
|
143
|
+
function isOnlyAjvSchemaCompileErrors(detail) {
|
|
144
|
+
const lines = detail
|
|
145
|
+
.split('\n')
|
|
146
|
+
.map(line => line.trim())
|
|
147
|
+
.filter(line => line.length > 0)
|
|
148
|
+
if (lines.length === 0) return false
|
|
149
|
+
return lines.every(line => AJV_SCHEMA_COMPILE_ERROR_RE.test(line))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Друкує інформаційне попередження (не violation) для кожного `detail`-рядка ajv schema-compile-
|
|
154
|
+
* помилки — пояснює, що причина у несправній зовнішній схемі, не в нашому файлі.
|
|
155
|
+
* @param {string} detail рядки `✖ …`, для яких `isOnlyAjvSchemaCompileErrors` вже повернув true
|
|
156
|
+
* @returns {void}
|
|
157
|
+
*/
|
|
158
|
+
function reportAjvSchemaCompileFailures(detail) {
|
|
159
|
+
for (const line of detail.split('\n')) {
|
|
160
|
+
const trimmed = line.trim()
|
|
161
|
+
if (trimmed.length === 0) continue
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`⚠ run-v8r: зовнішня схема не компілюється в ajv (не файл) — ${trimmed} Ймовірно, ajv unicodeRegExp-несумісність зі старим стилем escape у чужій схемі; помилка не рахується як порушення.\n`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
119
168
|
/**
|
|
120
169
|
* Прибирає v8r/bunx noise-рядки (весь `ℹ`-статус: Loaded config file, Patterns and relative
|
|
121
170
|
* paths, Pre-warming the cache, Processing <file>, Found schema in …, Validating …; і службовий
|
|
@@ -190,7 +239,11 @@ export function stripBunNodeShimDirs(pathValue) {
|
|
|
190
239
|
* @param {boolean} [verbose] друкувати повний raw stdout/stderr v8r при помилці; інакше — лише
|
|
191
240
|
* рядки `✖ …` без `ℹ`-шуму (Pre-warming the cache, Processing <file>, Found schema in …)
|
|
192
241
|
* @returns {Promise<{ exitError: true } | { exitError: false, code: number, detail: string }>}
|
|
193
|
-
* помилка spawn або код v8r (0/98 — трактує викликач) + деталь `✖
|
|
242
|
+
* помилка spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків. Якщо ВЕСЬ `detail`
|
|
243
|
+
* складається лише з ajv schema-compile-помилок (несправна зовнішня схема, не наш файл —
|
|
244
|
+
* `isOnlyAjvSchemaCompileErrors`) — `code` примусово 0, а причина друкується окремим `⚠`-попередженням
|
|
245
|
+
* (`reportAjvSchemaCompileFailures`), а не як `✖`-порушення; мішаний випадок (є хоч один
|
|
246
|
+
* genuine validation-рядок) лишається без змін навмисно, щоб не замаскувати реальну проблему.
|
|
194
247
|
*/
|
|
195
248
|
async function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
196
249
|
const bunPath = resolveCmd('bun') ?? process.execPath
|
|
@@ -206,16 +259,24 @@ async function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
|
206
259
|
|
|
207
260
|
warnAboutRemoteSchemaFallback(result.stderr ?? '')
|
|
208
261
|
|
|
209
|
-
|
|
262
|
+
let exitCode = result.exitCode ?? 1
|
|
210
263
|
let detail = ''
|
|
211
264
|
if (exitCode !== 0 && exitCode !== 98) {
|
|
212
265
|
detail = extractFailureLines(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)
|
|
266
|
+
const onlySchemaCompileErrors = isOnlyAjvSchemaCompileErrors(detail)
|
|
213
267
|
if (verbose) {
|
|
214
268
|
if (result.stdout?.length) process.stdout.write(result.stdout)
|
|
215
269
|
if (result.stderr?.length) process.stderr.write(result.stderr)
|
|
270
|
+
} else if (onlySchemaCompileErrors) {
|
|
271
|
+
reportAjvSchemaCompileFailures(detail)
|
|
216
272
|
} else if (detail.length) {
|
|
217
273
|
process.stdout.write(`${detail}\n`)
|
|
218
274
|
}
|
|
275
|
+
if (onlySchemaCompileErrors) {
|
|
276
|
+
if (verbose) reportAjvSchemaCompileFailures(detail)
|
|
277
|
+
exitCode = 0
|
|
278
|
+
detail = ''
|
|
279
|
+
}
|
|
219
280
|
}
|
|
220
281
|
return { exitError: false, code: exitCode, detail }
|
|
221
282
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawn } from 'node:child_process'
|
|
19
|
+
import { once } from 'node:events'
|
|
19
20
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
20
21
|
import { createRequire } from 'node:module'
|
|
21
22
|
import { dirname, join } from 'node:path'
|
|
@@ -118,6 +119,47 @@ class AcpSkillClient {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
/**
|
|
123
|
+
* @param {import('node:child_process').ChildProcess} child дочірній процес
|
|
124
|
+
* @returns {Promise<{ event: 'error', error: Error }>} подія спавн-помилки
|
|
125
|
+
*/
|
|
126
|
+
async function watchSpawnError(child) {
|
|
127
|
+
const [error] = await once(child, 'error')
|
|
128
|
+
return { event: 'error', error }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {import('node:child_process').ChildProcess} child дочірній процес
|
|
133
|
+
* @returns {Promise<{ event: 'exit', code: number|null, signal: string|null }>} подія завершення процесу
|
|
134
|
+
*/
|
|
135
|
+
async function watchExit(child) {
|
|
136
|
+
const [code, signal] = await once(child, 'exit')
|
|
137
|
+
return { event: 'exit', code, signal }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Watchdog дочірнього процесу: провалюється швидко, якщо ACP-агент не запустився чи
|
|
142
|
+
* вийшов до кінця ходу — без цього мертвий процес вішає `connection.prompt` назавжди
|
|
143
|
+
* (`Connection#receive` у `@zed-industries/agent-client-protocol` тихо завершує читання
|
|
144
|
+
* на закритому stdout, не відхиляючи pending-запити).
|
|
145
|
+
* @param {import('node:child_process').ChildProcess} child дочірній процес ACP-агента
|
|
146
|
+
* @param {string} kind провайдер (для тексту помилки)
|
|
147
|
+
* @returns {Promise<never>} ніколи не резолвиться успішно — лише провалюється
|
|
148
|
+
* @throws {Error} на спавн-помилку чи будь-який вихід процесу до завершення ходу
|
|
149
|
+
*/
|
|
150
|
+
async function waitForChildFailure(child, kind) {
|
|
151
|
+
const result = await Promise.race([watchSpawnError(child), watchExit(child)])
|
|
152
|
+
|
|
153
|
+
if (result.event === 'error') {
|
|
154
|
+
throw new Error(`acp ${kind}: не вдалося запустити процес: ${result.error.message}`)
|
|
155
|
+
}
|
|
156
|
+
throw new Error(
|
|
157
|
+
result.signal
|
|
158
|
+
? `acp ${kind}: процес вбитий сигналом ${result.signal} до завершення ходу`
|
|
159
|
+
: `acp ${kind}: процес завершився з кодом ${result.code} до завершення ходу`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
/**
|
|
122
164
|
* Виконує скіл через зовнішнього ACP-агента. Єдиний лишений провайдер —
|
|
123
165
|
* deprecated `claude` (`cursor`/`codex` — див. `@7n/llm-lib/acp`).
|
|
@@ -139,6 +181,7 @@ export async function runAcpRunner(kind, prompt, projectDir, logError, deps = {}
|
|
|
139
181
|
|
|
140
182
|
const acp = deps.acp ?? (await import('@zed-industries/agent-client-protocol'))
|
|
141
183
|
const child = spawnFn(bin, args, { cwd: projectDir, stdio: ['pipe', 'pipe', 'inherit'], env: process.env })
|
|
184
|
+
const failure = waitForChildFailure(child, kind)
|
|
142
185
|
|
|
143
186
|
try {
|
|
144
187
|
const input = Writable.toWeb(child.stdin)
|
|
@@ -147,15 +190,19 @@ export async function runAcpRunner(kind, prompt, projectDir, logError, deps = {}
|
|
|
147
190
|
const stream = acp.ndJsonStream(input, output)
|
|
148
191
|
const connection = new acp.ClientSideConnection(() => client, stream)
|
|
149
192
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
const turn = (async () => {
|
|
194
|
+
await connection.initialize({
|
|
195
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
196
|
+
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
|
|
197
|
+
})
|
|
198
|
+
const session = await connection.newSession({ cwd: projectDir, mcpServers: [] })
|
|
199
|
+
return connection.prompt({
|
|
200
|
+
sessionId: session.sessionId,
|
|
201
|
+
prompt: [{ type: 'text', text: prompt }]
|
|
202
|
+
})
|
|
203
|
+
})()
|
|
204
|
+
|
|
205
|
+
const result = await Promise.race([turn, failure])
|
|
159
206
|
|
|
160
207
|
if (result.stopReason !== 'end_turn') {
|
|
161
208
|
logError(`acp ${kind}: stopReason=${result.stopReason}`)
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: workspaces.mjs
|
|
4
4
|
resource: npm/scripts/lib/workspaces.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 8382904f
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Цей файл визначає список кореневих каталогів пакетів у монорепо, використовуючи конфігурацію `workspaces` з `package.json`. Він використовується скриптами перевірки монорепо для ідентифікації всіх проектів, які потрібно перевірити. Результат повертається для подальшого використання в процесах перевірки та аналізу.
|
|
@@ -14,18 +14,18 @@ docgen:
|
|
|
14
14
|
`normalizeWorkspacePattern`: Нормалізує воркспейс-патерн до POSIX-формату, видаляючи хвостові `/`.
|
|
15
15
|
`normalizeWorkspacePatterns`: Перетворює значення `workspaces` в масив воркспейс-патернів.
|
|
16
16
|
`getMonorepoPackageRootDirs`: Збирає список коренів пакетів воркспейсу з `package.json`.
|
|
17
|
-
`
|
|
17
|
+
`WORKSPACE_IGNORED_DIRS`: Ігноровані каталоги `node_modules`, `.git`, `.venv` та `venv`.
|
|
18
18
|
|
|
19
19
|
## Публічний API
|
|
20
20
|
|
|
21
|
-
-
|
|
21
|
+
- WORKSPACE_IGNORED_DIRS — Теки, ігноровані при розгортанні workspace-патернів із `*` (узгоджено з `rules/changelog/lib/package-manifest.mjs`).
|
|
22
22
|
- isIgnoredWorkspaceRoot — Визначає, чи слід виключити каталог з списку коренів workspace (не враховує `.`).
|
|
23
23
|
- normalizeWorkspacePatterns — Перетворює поле `workspaces` з `package.json` на масив шляхів або glob-шаблонів.
|
|
24
24
|
- getMonorepoPackageRootDirs — Повертає список кореневих каталогів пакетів, включаючи корінь репозиторію та всі пакети, визначені в `workspaces`.
|
|
25
25
|
|
|
26
26
|
## Гарантії поведінки
|
|
27
27
|
|
|
28
|
-
- Повертає порожній список каталогів, якщо не знайдено жодного `package.json` у кореневому каталозі або в каталогах, що
|
|
28
|
+
- Повертає порожній список каталогів, якщо не знайдено жодного `package.json` у кореневому каталозі або в каталогах, що входять до `WORKSPACE_IGNORED_DIRS`.
|
|
29
29
|
- Повертає `false` якщо не вдалося прочитати `package.json` у будь-якому з каталогів.
|
|
30
30
|
- Не включає в список каталог `node_modules`.
|
|
31
31
|
- Не включає в список каталог `.git`.
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* з `workspaces`, з урахуванням glob).
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
|
-
import {
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
9
|
import { dirname, join, relative } from 'node:path'
|
|
10
10
|
|
|
11
|
+
import { scanGlob } from '../utils/glob-compat.mjs'
|
|
12
|
+
|
|
11
13
|
const TRAILING_SLASH_RE = /\/$/
|
|
12
14
|
const LEADING_DOTSLASH_RE = /^\.\//
|
|
13
15
|
|
|
14
|
-
/**
|
|
15
|
-
export const
|
|
16
|
+
/** Теки, ігноровані при розгортанні workspace-патернів із `*` (узгоджено з `rules/changelog/lib/package-manifest.mjs`). */
|
|
17
|
+
export const WORKSPACE_IGNORED_DIRS = Object.freeze(['node_modules', '.git', '.venv', 'venv'])
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Чи слід виключити каталог зі списку workspace-коренів (не стосується `.`).
|
|
@@ -23,7 +25,7 @@ export function isIgnoredWorkspaceRoot(ws) {
|
|
|
23
25
|
if (ws === '.') return false
|
|
24
26
|
const p = ws.replaceAll('\\', '/').replace(LEADING_DOTSLASH_RE, '')
|
|
25
27
|
const segments = new Set(p.split('/'))
|
|
26
|
-
return
|
|
28
|
+
return WORKSPACE_IGNORED_DIRS.some(dir => segments.has(dir))
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -49,10 +51,7 @@ function normalizeWorkspacePattern(pattern) {
|
|
|
49
51
|
async function addWorkspaceRootsByPattern(roots, repoRoot, workspacePattern) {
|
|
50
52
|
if (workspacePattern.includes('*')) {
|
|
51
53
|
const globPat = `${workspacePattern}/package.json`
|
|
52
|
-
for await (const relPkgJsonPath of
|
|
53
|
-
cwd: repoRoot,
|
|
54
|
-
ignore: [...WORKSPACE_GLOB_IGNORE]
|
|
55
|
-
})) {
|
|
54
|
+
for await (const relPkgJsonPath of scanGlob(globPat, repoRoot)) {
|
|
56
55
|
const absPkgJsonPath = join(repoRoot, relPkgJsonPath)
|
|
57
56
|
const relRoot = relative(repoRoot, dirname(absPkgJsonPath))
|
|
58
57
|
const ws = relRoot === '' ? '.' : relRoot
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: glob-compat.mjs
|
|
4
|
+
resource: npm/scripts/utils/glob-compat.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: a226a048
|
|
7
|
+
model: manual
|
|
8
|
+
tier: manual
|
|
9
|
+
score: 100
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Огляд
|
|
13
|
+
|
|
14
|
+
Runtime-нейтральний glob-обхід для коду, що виконується і під Bun, і під Node. Потрібен, бо hook запускається через `npx` → Node, де глобал `Bun` не визначений (top-level `new Bun.Glob(...)` зривав import модуля-детектора), а прямий `node:fs/promises#glob` не працює на self-hosted Linux Bun 1.3.14, де Node-compat шим не надає export `glob`. Реалізація вибирається за середовищем виконання: `Bun.Glob` під Bun, `node:fs/promises#glob` під Node (гарантовано за `engines: node >=25`).
|
|
15
|
+
|
|
16
|
+
## Публічний API
|
|
17
|
+
|
|
18
|
+
- `scanGlob(pattern, cwd)` — async-генератор: ітерує відносні (до `cwd`) шляхи файлів, що відповідають glob-патерну (наприклад, `cf/*/package.json`).
|
|
19
|
+
- `hasIgnoredPathSegment(relPath, ignoredDirs)` — чи містить відносний шлях сегмент зі службових тек (наприклад, `node_modules`), які glob-обхід має ігнорувати; еквівалент ignore-патернів `**/<dir>/**` по кожній теці з `ignoredDirs`. Розділювачі `\` нормалізуються до `/`.
|
|
20
|
+
|
|
21
|
+
## Де використовується
|
|
22
|
+
|
|
23
|
+
- `npm/scripts/lib/workspaces.mjs` — розгортання workspace-патернів із `*`.
|
|
24
|
+
- `npm/scripts/utils/resolve-js-root.mjs` — резолв JS-roots за workspace-патернами.
|
|
25
|
+
- `npm/rules/changelog/lib/package-manifest.mjs` — пошук `pyproject.toml` по репо.
|
|
26
|
+
- `npm/rules/tauri/core_test_isolation/main.mjs` — glob-члени `[workspace] members` і обхід `**/*.rs`.
|
|
27
|
+
|
|
28
|
+
## Гарантії поведінки
|
|
29
|
+
|
|
30
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
31
|
+
- Не фільтрує результати сам: ігнорування службових тек — відповідальність викликача (через `hasIgnoredPathSegment` або власні перевірки).
|
|
@@ -11,6 +11,7 @@ resource: npm/scripts/utils/
|
|
|
11
11
|
| [ast-scan-utils.mjs](ast-scan-utils.md) | JS Module |
|
|
12
12
|
| [ensure-gitignore-entries.mjs](ensure-gitignore-entries.md) | JS Module |
|
|
13
13
|
| [find-package-json-paths.mjs](find-package-json-paths.md) | JS Module |
|
|
14
|
+
| [glob-compat.mjs](glob-compat.md) | JS Module |
|
|
14
15
|
| [lock-cache-dir.mjs](lock-cache-dir.md) | JS Module |
|
|
15
16
|
| [pass.mjs](pass.md) | JS Module |
|
|
16
17
|
| [resolve-cargo-manifest.mjs](resolve-cargo-manifest.md) | JS Module |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-нейтральний glob-обхід для коду, що виконується і під Bun, і під Node
|
|
3
|
+
* (hook запускається через npx → Node, де глобал `Bun` не визначений, тож
|
|
4
|
+
* top-level `new Bun.Glob(...)` валить сам import модуля). Пряме
|
|
5
|
+
* `node:fs/promises#glob` теж не варіант: спостережено self-hosted Linux Bun
|
|
6
|
+
* 1.3.14, де Node-compat шим не надає export 'glob'. Тож вибір реалізації —
|
|
7
|
+
* за середовищем виконання: `Bun.Glob` під Bun, `node:fs/promises#glob` під
|
|
8
|
+
* Node (engines: node >=25).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ітерує відносні шляхи файлів за glob-патерном.
|
|
13
|
+
* @param {string} pattern glob-патерн (наприклад, `cf/*\/package.json`)
|
|
14
|
+
* @param {string} cwd корінь обходу
|
|
15
|
+
* @yields {string} кожен відносний шлях збігу
|
|
16
|
+
*/
|
|
17
|
+
export async function* scanGlob(pattern, cwd) {
|
|
18
|
+
if (typeof Bun !== 'undefined') {
|
|
19
|
+
yield* new Bun.Glob(pattern).scan({ cwd })
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
const { glob } = await import('node:fs/promises')
|
|
23
|
+
yield* glob(pattern, { cwd })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Чи містить відносний шлях сегмент зі службових тек, які glob-обхід має ігнорувати.
|
|
28
|
+
* Еквівалент колишніх ignore-патернів `**\/<dir>/**` по кожній теці з `ignoredDirs`.
|
|
29
|
+
* @param {string} relPath відносний шлях зі `scanGlob`
|
|
30
|
+
* @param {readonly string[]} ignoredDirs імена ігнорованих тек (наприклад, `node_modules`)
|
|
31
|
+
* @returns {boolean} true — шлях лежить в ігнорованій теці
|
|
32
|
+
*/
|
|
33
|
+
export function hasIgnoredPathSegment(relPath, ignoredDirs) {
|
|
34
|
+
const segments = relPath.replaceAll('\\', '/').split('/')
|
|
35
|
+
return ignoredDirs.some(dir => segments.includes(dir))
|
|
36
|
+
}
|
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
* Спільна утиліта для coverage-провайдера js і test-концерну stryker_config (DRY).
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
-
import {
|
|
7
|
+
import { readFile } from 'node:fs/promises'
|
|
8
8
|
import { join } from 'node:path'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import { hasIgnoredPathSegment, scanGlob } from './glob-compat.mjs'
|
|
11
|
+
|
|
12
|
+
const WORKSPACE_IGNORED_DIRS = Object.freeze(['node_modules', '.git'])
|
|
11
13
|
const PACKAGE_JSON_SUFFIX_RE = /[/\\]package\.json$/
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Розгортає один workspace-патерн у список абсолютних шляхів каталогів з package.json.
|
|
15
|
-
* Літеральні патерни перевіряються через existsSync; glob-патерни — через
|
|
17
|
+
* Літеральні патерни перевіряються через existsSync; glob-патерни — через scanGlob.
|
|
16
18
|
* @param {string} cwd корінь проєкту
|
|
17
19
|
* @param {string} pattern workspace-патерн з package.json (наприклад, `app` або `cf/*`)
|
|
18
20
|
* @returns {Promise<string[]>} абсолютні шляхи до workspace-каталогів
|
|
@@ -23,7 +25,8 @@ async function expandWorkspacePattern(cwd, pattern) {
|
|
|
23
25
|
return existsSync(join(wsPath, 'package.json')) ? [wsPath] : []
|
|
24
26
|
}
|
|
25
27
|
const results = []
|
|
26
|
-
for await (const rel of
|
|
28
|
+
for await (const rel of scanGlob(`${pattern}/package.json`, cwd)) {
|
|
29
|
+
if (hasIgnoredPathSegment(rel, WORKSPACE_IGNORED_DIRS)) continue
|
|
27
30
|
const wsRel = rel.replace(PACKAGE_JSON_SUFFIX_RE, '')
|
|
28
31
|
results.push(join(cwd, wsRel))
|
|
29
32
|
}
|