@7n/rules 1.37.0 → 1.38.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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.38.0] - 2026-07-21
4
+
5
+ ### Fixed
6
+
7
+ - tauri/gitignore_target, tauri/core_test_isolation: інваріант workspace-кореня замість хибної typo-версії
8
+
3
9
  ## [1.37.0] - 2026-07-21
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -15,23 +15,37 @@ rebuild (лінковка, bundling, іноді webview) замість секу
15
15
 
16
16
  ```
17
17
  my-tauri-app/
18
- ├── src-tauri/ app-shell: Tauri runtime, window/AppHandle, НЕ містить LLM-залежностей
19
- │ └── Cargo.toml [workspace] members = [".", "../agent-core"]
20
- └── agent-core/ agent/LLM-логіка: prompt building, tool dispatch, provider abstraction
21
- ├── Cargo.toml без залежності на tauri
18
+ ├── Cargo.toml workspace root (virtual, без [package]): [workspace] members = ["src-tauri", "agent-core"]
19
+ ├── src-tauri/ app-shell: Tauri runtime, window/AppHandle, НЕ містить LLM-залежностей
20
+ └── Cargo.toml [package], без [workspace] сам по собі НЕ workspace root
21
+ └── agent-core/ agent/LLM-логіка: prompt building, tool dispatch, provider abstraction
22
+ ├── Cargo.toml ← без залежності на tauri
22
23
  └── tests/
23
24
  └── fake_provider.rs ← FakeLlmProvider / MockLlmClient — без мережі
24
25
  ```
25
26
 
27
+ `[workspace] members = [".", "../agent-core"]` **всередині** `src-tauri/Cargo.toml` — невалідний
28
+ Cargo layout: `../agent-core` лежить поза деревом workspace root (`src-tauri/`), і Cargo відмовляє
29
+ з `workspace member '.../agent-core/Cargo.toml' is not hierarchically below the workspace root`.
30
+ Тому `[workspace]` живе у product-root `Cargo.toml`, що лежить НАД обома крейтами (канон
31
+ `rust/workspace_root.mdc` — один кореневий Cargo workspace на репозиторій).
32
+
33
+ **Наслідок:** `target/` компілюється у корінь цього workspace (`my-tauri-app/target/`), а не в
34
+ `src-tauri/target/` — це впливає на очікуваний запис у `.gitignore` (`tauri/gitignore_target`) і на
35
+ шлях у `workspaces:`-полі `Swatinem/rust-cache` GitHub Action (action бере шлях буквально як
36
+ `<path>/target`, тож там має стояти корінь цього workspace, не `src-tauri`).
37
+
26
38
  Концерн `tauri/core_test_isolation.mjs` (read-only detector, `structural`, авто-фікс ризикований)
27
39
  для кожного `src-tauri/Cargo.toml` перевіряє:
28
40
 
29
41
  1. **LLM-залежність не в app-shell.** Якщо сам `src-tauri/Cargo.toml` має залежність на відомий
30
42
  LLM SDK (напр. `async-openai`, `anthropic`, `ollama-rs`, `rig-core`, ...) — фейл: винеси
31
43
  agent-логіку в окремий crate.
32
- 2. **Окремий crate не тягне tauri.** Якщо у `[workspace] members` є crate з LLM-залежністю, і
33
- цей crate сам залежить від `tauri`/`tauri-*` фейл: `cargo test -p` цього крейту все одно
34
- перезбирає Tauri runtime.
44
+ 2. **Окремий crate не тягне tauri.** `[workspace] members` резолвиться або з самого
45
+ `src-tauri/Cargo.toml` (standalone-патерн), абоколи там `[workspace]` немає з
46
+ найближчого предка-workspace root (product-root `Cargo.toml` над `src-tauri/`). Якщо серед
47
+ members є crate з LLM-залежністю, і цей crate сам залежить від `tauri`/`tauri-*` — фейл:
48
+ `cargo test -p` цього крейту все одно перезбирає Tauri runtime.
35
49
  3. **Є fake-провайдер.** Якщо такий crate є і не залежить від tauri, але ніде в його дереві
36
50
  (`**/*.rs`) немає маркера `Fake*Llm`/`Mock*Provider`/`Stub*Client` — фейл: інтеграційні тести
37
51
  на prompt/tool ітерації або мовчки пропускають LLM-логіку, або б'ють по реальному провайдеру.
@@ -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: cfb870f5
6
+ crc: 2fe501fb
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
@@ -14,6 +14,7 @@ import { parse as parseToml } from 'smol-toml'
14
14
 
15
15
  import { scanGlob } from '../../../scripts/utils/glob-compat.mjs'
16
16
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
17
+ import { findAncestorWorkspaceRoot, resolveWorkspaceMemberDirs } from '../../../scripts/utils/cargo-workspace.mjs'
17
18
  import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
18
19
 
19
20
  /** Стабільні reasons для трьох типів порушення. */
@@ -55,29 +56,6 @@ async function findSrcTauriDirs(cwd) {
55
56
  return result
56
57
  }
57
58
 
58
- /**
59
- * Резолвить `[workspace] members` (шляхи й прості glob-патерни) відносно `src-tauri/`
60
- * у список абсолютних каталогів крейтів, що мають власний Cargo.toml.
61
- * @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
62
- * @param {string[]} members патерни з `workspace.members`
63
- * @returns {Promise<string[]>} абсолютні шляхи членів воркспейсу (без дублікатів)
64
- */
65
- async function resolveWorkspaceMemberDirs(srcTauriDir, members) {
66
- const found = new Set()
67
- for (const pattern of members) {
68
- if (pattern.includes('*')) {
69
- for await (const rel of scanGlob(pattern, srcTauriDir)) {
70
- const abs = resolve(srcTauriDir, rel)
71
- if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
72
- }
73
- continue
74
- }
75
- const abs = resolve(srcTauriDir, pattern)
76
- if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
77
- }
78
- return [...found]
79
- }
80
-
81
59
  /**
82
60
  * Рекурсивно шукає в дереві крейту рядок, що відповідає `FAKE_PROVIDER_RE`
83
61
  * (fake/mock/stub-реалізацію LLM-провайдера, зазвичай у `tests/` чи `src/`).
@@ -117,12 +95,19 @@ async function checkOneSrcTauri(srcTauriDir, cwd, reporter) {
117
95
  return
118
96
  }
119
97
 
120
- const members = parsed?.workspace?.members
121
- if (!Array.isArray(members) || members.length === 0) {
122
- return
98
+ // `[workspace]` живе або у самому src-tauri/Cargo.toml (старий/standalone патерн), або —
99
+ // канонічно (rust/workspace_root.mdc) у предку-workspace root над src-tauri/.
100
+ let workspaceRootDir = srcTauriDir
101
+ let members = Array.isArray(parsed?.workspace?.members) ? parsed.workspace.members : []
102
+ if (members.length === 0) {
103
+ const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
104
+ const ancestorMembers = ancestor?.parsed?.workspace?.members
105
+ if (!ancestor || !Array.isArray(ancestorMembers) || ancestorMembers.length === 0) return
106
+ workspaceRootDir = ancestor.rootDir
107
+ members = ancestorMembers
123
108
  }
124
109
 
125
- const memberDirs = await resolveWorkspaceMemberDirs(srcTauriDir, members)
110
+ const memberDirs = await resolveWorkspaceMemberDirs(workspaceRootDir, members)
126
111
  const otherMemberDirs = memberDirs.filter(d => resolve(d) !== resolve(srcTauriDir))
127
112
 
128
113
  for (const memberDir of otherMemberDirs) {
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/tauri/gitignore_target/main.mjs
5
5
  docgen:
6
- crc: 83aa81ee
6
+ crc: c9fcfc58
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 85
10
9
  issues: internal-name:findSrcTauriDirs,anchor-miss:(tauri.mdc),judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -1,23 +1,42 @@
1
- ## `.gitignore`: ignore build-артефактів `<ws>/src-tauri/target/`
1
+ ## `.gitignore`: ignore build-артефактів фактичного Cargo workspace root
2
2
 
3
- Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в `<ws>/src-tauri/target/` build-артефакти Cargo (сотні MB, десятки тисяч файлів). Корінний `.gitignore` монорепо повинен містити точний ignore-запис для кожного такого воркспейсу:
3
+ Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в `target/` свого фактичного
4
+ Cargo workspace root — а не завжди в `<ws>/src-tauri/target/`. Якщо над `src-tauri/` є
5
+ предок-workspace (канон `rust/workspace_root.mdc` — один кореневий Cargo workspace на
6
+ репозиторій, наприклад `<ws>/Cargo.toml` чи кореневий `Cargo.toml` монорепо), Cargo кладе
7
+ `target/` саме туди. Корінний `.gitignore` монорепо повинен містити точний ignore-запис
8
+ для кожного такого крейту, обчислений від цього фактичного workspace root:
4
9
 
5
10
  ```gitignore
6
11
  # Tauri — Rust build artifacts (tauri.mdc)
7
- <ws>/src-tauri/target/
12
+ <workspace-root>/target/
8
13
  ```
9
14
 
15
+ де `<workspace-root>` — найближчий предок `src-tauri/` з `[workspace]`, чиї `members`
16
+ покривають цей крейт (якщо це корінь монорепо — запис голий `target/`); якщо предка-workspace
17
+ немає — крейт сам собі workspace root, і запис лишається `<ws>/src-tauri/target/`.
18
+
10
19
  ### Чому точний шлях, а не голий `target/`
11
20
 
12
- Реальний інцидент: у `nitra/task` корінний `.gitignore` мав `owner/target/` замість `owner/src-tauri/target/` воркспейс скопійований з іншого проєкту без заміни шляху (typo). `target/` там ігнорувався не там, де компілятор його реально створює, тож ~600MB / ~28k файлів build-артефактів не потрапляли під жоден ignore-патерн і мало не потрапили в коміт через `git add -A`.
21
+ Реальний прецедент: у `nitra/task` коміт `3cb0df3` помилково "виправив" коректний запис
22
+ `owner/target/` назад на `owner/src-tauri/target/` — автор вважав це друкарською помилкою,
23
+ не врахувавши, що `owner/Cargo.toml` є Cargo workspace root для `owner/src-tauri` і тому
24
+ `target/` справді компілюється в `owner/target/`, а не в `owner/src-tauri/target/`. Коміт
25
+ `ac3451e` це скасував. Тобто `<ws>/src-tauri/target/` — не універсальний дефолт, а лише
26
+ окремий випадок (crate без workspace root над собою); перевірка мусить рахувати очікуваний
27
+ шлях від реального workspace root, а не від фіксованого суфікса.
13
28
 
14
- Тому перевірка звіряє **точний рядок** `<ws>/src-tauri/target/` для кожного знайденого `src-tauri/`, а не факт наявності підрядка `target/` десь у файлі — голий `target/` (чи вже наявний, але помилковий `owner/target/`) не закриває violation.
29
+ Тому перевірка звіряє **точний рядок** обчисленого запису для кожного знайденого `src-tauri/`,
30
+ а не факт наявності підрядка `target/` десь у файлі — голий `target/` чи запис не того рівня
31
+ не закривають violation. Сторонній/мертвий каталог `target/` на диску (за старим, вже
32
+ невірним шляхом) також не задовольняє перевірку — вона звіряє лише вміст `.gitignore`.
15
33
 
16
34
  ### Область дії
17
35
 
18
36
  - Обхід усіх `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config` (`findSrcTauriDirs`, без дублювання обходу монорепо).
37
+ - Очікуваний шлях — `findAncestorWorkspaceRoot` (`scripts/utils/cargo-workspace.mjs`, спільно з `rust/workspace_root`).
19
38
  - Ціль фіксу — **один** корінний `.gitignore` монорепо (не по workspace).
20
- - Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи (на кшталт помилкового `owner/target/` з інциденту вище) не видаляються — вони можуть бути навмисними чи потребувати ручного review.
39
+ - Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи не видаляються — вони можуть бути навмисними чи потребувати ручного review.
21
40
 
22
41
  ### Автофікс (T0)
23
42
 
@@ -2,15 +2,19 @@
2
2
  * @see ./docs/main.md
3
3
  *
4
4
  * Read-only detector: для кожного знайденого `<ws>/src-tauri/` у монорепо
5
- * корінний `.gitignore` повинен містити точний ignore-запис
6
- * `<ws>/src-tauri/target/` (tauri.mdc).
5
+ * корінний `.gitignore` повинен містити точний ignore-запис для build-артефактів
6
+ * але шлях залежить від фактичного Cargo workspace root крейту, не від
7
+ * розташування самого `src-tauri/` (tauri.mdc).
7
8
  *
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 саме на цьому кейсі.
9
+ * Крейт `src-tauri/` компілюється в `<workspace-root>/target/`, а не обов'язково
10
+ * в `<ws>/src-tauri/target/`: коли над `src-tauri/` є предок-workspace (канон
11
+ * `rust/workspace_root` один кореневий Cargo workspace на репозиторій), Cargo
12
+ * кладе `target/` саме туди. Реальний прецедент: у `nitra/task` коміт `3cb0df3`
13
+ * помилково "виправив" коректний запис `owner/target/` назад на `owner/src-tauri/target/`
14
+ * (бо `owner/Cargo.toml` workspace root для `owner/src-tauri`), що скасував наступний
15
+ * коміт `ac3451e`. Тому перевірка обчислює очікуваний шлях від реального workspace root,
16
+ * а не від фіксованого суфікса `src-tauri/target/`, і звіряє точний рядок — голий
17
+ * `target/` чи запис не-того рівня не рахуються присутністю потрібного запису.
14
18
  *
15
19
  * Обхід `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config`
16
20
  * (`findSrcTauriDirs`), без дублювання.
@@ -20,6 +24,7 @@ import { readFile } from 'node:fs/promises'
20
24
  import { join, relative } from 'node:path'
21
25
 
22
26
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
27
+ import { findAncestorWorkspaceRoot } from '../../../scripts/utils/cargo-workspace.mjs'
23
28
  import { findSrcTauriDirs } from '../cargo_mutants_config/main.mjs'
24
29
 
25
30
  /** Стабільний reason: у корінному `.gitignore` бракує ignore-запису(ів) для `src-tauri/target/`. */
@@ -29,13 +34,22 @@ export const MISSING_GITIGNORE_TARGET_ENTRIES = 'missing-gitignore-target-entrie
29
34
  export const ROOT_GITIGNORE = '.gitignore'
30
35
 
31
36
  /**
32
- * Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri/`.
37
+ * Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri/`: шлях до
38
+ * `target/` фактичного Cargo workspace root цього крейту (найближчий предок з
39
+ * `[workspace]`, чиї `members` покривають `src-tauri/`), а не суфікс `src-tauri/target/`
40
+ * за замовчуванням. Якщо предка-workspace немає — крейт сам собі workspace root
41
+ * (standalone `src-tauri/Cargo.toml`), і `target/` лишається під `src-tauri/`.
33
42
  * @param {string} cwd корінь монорепо
34
43
  * @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
35
- * @returns {string} очікуваний рядок у корінному `.gitignore` (`<ws>/src-tauri/target/`)
44
+ * @returns {Promise<string>} очікуваний рядок у корінному `.gitignore`
36
45
  */
37
- export function expectedTargetEntry(cwd, srcTauriDir) {
38
- return `${relative(cwd, srcTauriDir)}/target/`
46
+ export async function expectedTargetEntry(cwd, srcTauriDir) {
47
+ const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
48
+ if (!ancestor) {
49
+ return `${relative(cwd, srcTauriDir)}/target/`
50
+ }
51
+ const relRoot = relative(cwd, ancestor.rootDir)
52
+ return relRoot === '' ? 'target/' : `${relRoot}/target/`
39
53
  }
40
54
 
41
55
  /**
@@ -62,7 +76,8 @@ export async function lint(ctx) {
62
76
  const srcTauriDirs = await findSrcTauriDirs(cwd)
63
77
  if (srcTauriDirs.length === 0) return reporter.result()
64
78
 
65
- const expectedEntries = srcTauriDirs.map(dir => expectedTargetEntry(cwd, dir))
79
+ const expectedEntries = []
80
+ for (const dir of srcTauriDirs) expectedEntries.push(await expectedTargetEntry(cwd, dir))
66
81
  const abs = join(cwd, ROOT_GITIGNORE)
67
82
  const content = existsSync(abs) ? await readFile(abs, 'utf8') : ''
68
83
 
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Спільні T0 (без spawn `cargo`) утиліти для роботи з Cargo workspace-структурою:
3
+ * читання Cargo.toml, резолв `[workspace].members`-glob-патернів у каталоги,
4
+ * пошук найближчого предка-workspace root для крейту. Спільно використовується
5
+ * `plugins/lang-rust/rules/rust/workspace_root` і `npm/rules/tauri/gitignore_target`.
6
+ */
7
+ import { existsSync } from 'node:fs'
8
+ import { readFile } from 'node:fs/promises'
9
+ import { dirname, join, resolve } from 'node:path'
10
+
11
+ import { parse as parseToml } from 'smol-toml'
12
+
13
+ import { scanGlob } from './glob-compat.mjs'
14
+
15
+ const TRAILING_SLASH_RE = /\/$/
16
+
17
+ /**
18
+ * Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
19
+ * @param {string} absPath абсолютний шлях до Cargo.toml
20
+ * @returns {Promise<Record<string, unknown>|null>} розпарсений маніфест або null
21
+ */
22
+ export async function readCargoManifest(absPath) {
23
+ if (!existsSync(absPath)) return null
24
+ try {
25
+ return parseToml(await readFile(absPath, 'utf8'))
26
+ } catch {
27
+ return null
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
33
+ * glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
34
+ * Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
35
+ * @param {string} rootDir корінь workspace (каталог з Cargo.toml, де живе `[workspace]`)
36
+ * @param {string[]} patterns патерни з `members`/`exclude`
37
+ * @returns {Promise<string[]>} абсолютні шляхи (без дублікатів)
38
+ */
39
+ export async function resolveWorkspaceMemberDirs(rootDir, patterns) {
40
+ const found = new Set()
41
+ for (const pattern of patterns ?? []) {
42
+ const norm = pattern.replace(TRAILING_SLASH_RE, '')
43
+ if (norm.includes('*')) {
44
+ // Шаблон для `Cargo.toml` напряму (не для каталогів) — деякі раннери (Bun.Glob)
45
+ // за замовчуванням матчать лише файли, тож патерн на голий каталог даватиме 0 збігів.
46
+ for await (const relManifest of scanGlob(`${norm}/Cargo.toml`, rootDir)) {
47
+ found.add(resolve(rootDir, dirname(relManifest)))
48
+ }
49
+ continue
50
+ }
51
+ const abs = resolve(rootDir, norm)
52
+ if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
53
+ }
54
+ return [...found]
55
+ }
56
+
57
+ /**
58
+ * Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
59
+ * @param {string} rootDir корінь workspace
60
+ * @param {string} crateDirAbs абсолютний шлях крейту
61
+ * @param {string[]} members `workspace.members`
62
+ * @param {string[]} excludes `workspace.exclude`
63
+ * @returns {Promise<boolean>} true — крейт є членом workspace
64
+ */
65
+ export async function isWorkspaceMemberDir(rootDir, crateDirAbs, members, excludes) {
66
+ const target = resolve(crateDirAbs)
67
+ const memberDirs = await resolveWorkspaceMemberDirs(rootDir, members)
68
+ if (memberDirs.every(d => resolve(d) !== target)) return false
69
+ if (!excludes || excludes.length === 0) return true
70
+ const excludedDirs = await resolveWorkspaceMemberDirs(rootDir, excludes)
71
+ return excludedDirs.every(d => resolve(d) !== target)
72
+ }
73
+
74
+ /**
75
+ * Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
76
+ * шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
77
+ * покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
78
+ * @param {string} crateDirAbs абсолютний шлях крейту (напр. `<repo>/owner/src-tauri`)
79
+ * @param {string} repoRootAbs абсолютний корінь репозиторію (межа обходу вгору)
80
+ * @returns {Promise<{rootDir: string, parsed: Record<string, unknown>}|null>} найближчий ancestor workspace root або null
81
+ */
82
+ export async function findAncestorWorkspaceRoot(crateDirAbs, repoRootAbs) {
83
+ const stopAt = resolve(repoRootAbs)
84
+ let dir = dirname(resolve(crateDirAbs))
85
+ for (;;) {
86
+ const parsed = await readCargoManifest(join(dir, 'Cargo.toml'))
87
+ if (parsed?.workspace) {
88
+ const members = Array.isArray(parsed.workspace.members) ? parsed.workspace.members : []
89
+ const excludes = Array.isArray(parsed.workspace.exclude) ? parsed.workspace.exclude : []
90
+ if (await isWorkspaceMemberDir(dir, crateDirAbs, members, excludes)) {
91
+ return { rootDir: dir, parsed }
92
+ }
93
+ }
94
+ if (dir === stopAt) return null
95
+ const parent = dirname(dir)
96
+ if (parent === dir) return null
97
+ dir = parent
98
+ }
99
+ }
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: JS Module
3
+ title: cargo-workspace.mjs
4
+ resource: npm/scripts/utils/cargo-workspace.mjs
5
+ docgen:
6
+ crc: c852e1fe
7
+ model: openai-codex/gpt-5.4-mini
8
+ tier: cloud-min
9
+ score: 100
10
+ issues: judge:inaccurate:0.98
11
+ judgeModel: openai-codex/gpt-5.4-mini
12
+ ---
13
+
14
+ ## Огляд
15
+
16
+ Спільні утиліти для читання `Cargo.toml`, розгортання `[workspace].members`-glob-патернів у каталоги та пошуку найближчого предка-workspace root для крейта. Публічні `readCargoManifest`, `resolveWorkspaceMemberDirs`, `isWorkspaceMemberDir`, `findAncestorWorkspaceRoot` працюють без запуску `cargo`, перехоплюють помилки й у частині випадків повертають порожнє значення замість винятку, щоб інші правила могли безпечно перевіряти належність каталогу до workspace або знаходити workspace root.
17
+
18
+ ## Поведінка
19
+
20
+ - **readCargoManifest** — читає `Cargo.toml` і повертає розпарсений manifest, або `null`, якщо файл відсутній чи TOML невалідний.
21
+ - **resolveWorkspaceMemberDirs** — перетворює workspace-патерни на список абсолютних каталогів із власним `Cargo.toml`, усуваючи дублікати.
22
+ - **isWorkspaceMemberDir** — визначає, чи належить конкретний каталог до workspace з урахуванням `members` і `exclude`.
23
+ - **findAncestorWorkspaceRoot** — шукає найближчий предок `Cargo.toml` з `[workspace]`, чий workspace покриває крейт; сам каталог крейту не перевіряє.
24
+
25
+ ## Публічний API
26
+
27
+ - readCargoManifest — Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
28
+ - resolveWorkspaceMemberDirs — Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
29
+ glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
30
+ Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
31
+ - isWorkspaceMemberDir — Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
32
+ - findAncestorWorkspaceRoot — Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
33
+ шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
34
+ покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
35
+
36
+ ## Гарантії поведінки
37
+
38
+ - Read-only: не виконує операцій запису (ФС/БД).
39
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
40
+ - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
@@ -9,6 +9,7 @@ resource: npm/scripts/utils/
9
9
  | [apply-to-files.mjs](apply-to-files.md) | JS Module |
10
10
  | [ast-extract.mjs](ast-extract.md) | JS Module |
11
11
  | [ast-scan-utils.mjs](ast-scan-utils.md) | JS Module |
12
+ | [cargo-workspace.mjs](cargo-workspace.md) | JS Module |
12
13
  | [ensure-gitignore-entries.mjs](ensure-gitignore-entries.md) | JS Module |
13
14
  | [find-package-json-paths.mjs](find-package-json-paths.md) | JS Module |
14
15
  | [glob-compat.mjs](glob-compat.md) | JS Module |