@7n/rules 1.35.0 → 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 CHANGED
@@ -1,5 +1,20 @@
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
+
3
18
  ## [1.35.0] - 2026-07-20
4
19
 
5
20
  ### Added
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -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: 10bad2ce
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 { glob, readFile } from 'node:fs/promises'
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 glob('**/pyproject.toml', { cwd: repoRoot, ignore: PYPROJECT_GLOB_IGNORE })) {
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
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-gen/main.mjs
5
5
  docgen:
6
- crc: ed99ac58
6
+ crc: c9c9970b
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -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
- * Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
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
- * @returns {boolean} true секцію API варто прогнати через критика
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 apiNeedsRefine(facts) {
357
+ export async function buildApiSection(facts, anchors, model, timeoutMs, temperature = 0.2) {
347
358
  const exps = facts.exports ?? []
348
- if (exps.length <= 1) return false
349
- return exps.every(e => !e.desc)
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
- // Спершу Поведінка (+API) секції з фактажем
434
+ // Спершу Поведінка — єдина секція з кодом (sectionMessages повертає лише її)
411
435
  for (const s of sectionMessages(facts, src, anc, intent)) {
412
- let draft = stripSignatures(stripSection(await callLlm(s.messages, model, { timeoutMs, temperature })))
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(
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/doc-files/docgen-prompts/main.mjs
5
5
  docgen:
6
- crc: 3fbc97f2
6
+ crc: fb4d1242
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -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[, api])
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
- // APIлише список експортів (без коду)
102
- if (!multi && !facts.exports?.some(e => e.desc)) return [behavior]
103
- const list = facts.exports.map(e => `- ${e.name}: ${e.desc || '(сформулюй стисло з наміру файлу)'}`).join('\n')
104
- const api = {
105
- key: 'api',
106
- numPredict: 320,
107
- messages: msgs(
108
- `${STYLE}${anch}`,
109
- `Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
110
- )
111
- }
112
- return [behavior, api]
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 або строгіше».
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/tauri/core_test_isolation/main.mjs
5
5
  docgen:
6
- crc: f8cf8ea5
6
+ crc: cfb870f5
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: best-of-2:retry-won,judge:inaccurate:0.98
@@ -7,11 +7,12 @@
7
7
  * ризикований (виділення крейту, перенесення коду) — лише репорт (core_test_isolation.mdc).
8
8
  */
9
9
  import { existsSync } from 'node:fs'
10
- import { glob, readFile } from 'node:fs/promises'
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 glob(pattern, { cwd: srcTauriDir })) {
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 glob('**/*.rs', { cwd: crateDir })) {
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
@@ -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: 995c1633
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
- `WORKSPACE_GLOB_IGNORE`: Ігнорує каталоги `node_modules`, `.git`, `.venv` та `venv`.
17
+ `WORKSPACE_IGNORED_DIRS`: Ігноровані каталоги `node_modules`, `.git`, `.venv` та `venv`.
18
18
 
19
19
  ## Публічний API
20
20
 
21
- - WORKSPACE_GLOB_IGNOREІгнорує шаблони workspace, що містять `*`, як визначено в `rules/changelog/js/consistency/package-manifest.mjs`.
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` у кореневому каталозі або в каталогах, що відповідають `WORKSPACE_GLOB_IGNORE`.
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 { glob, readFile } from 'node:fs/promises'
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
- /** Glob-ігнор для workspace-патернів із `*` (узгоджено з `rules/changelog/js/consistency/package-manifest.mjs`). */
15
- export const WORKSPACE_GLOB_IGNORE = Object.freeze(['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**'])
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 segments.has('node_modules') || segments.has('.git') || segments.has('.venv') || segments.has('venv')
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 glob(globPat, {
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 |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: resolve-js-root.mjs
4
4
  resource: npm/scripts/utils/resolve-js-root.mjs
5
5
  docgen:
6
- crc: 5202013e
6
+ crc: 46c5af46
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -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 { glob, readFile } from 'node:fs/promises'
7
+ import { readFile } from 'node:fs/promises'
8
8
  import { join } from 'node:path'
9
9
 
10
- const WORKSPACE_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**']
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-патерни — через node:fs/promises#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 glob(`${pattern}/package.json`, { cwd, exclude: WORKSPACE_GLOB_IGNORE })) {
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
  }