@nitra/cursor 12.12.0 → 12.13.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
+ ## [12.13.0] - 2026-06-26
4
+
5
+ ### Changed
6
+
7
+ - Заміна рекурсивного обходу файлів на використання globby для пошуку файлів
8
+
3
9
  ## [12.12.0] - 2026-06-25
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.12.0",
3
+ "version": "12.13.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@7n/mt": "^0.5.1",
57
+ "globby": "^16.0.0",
57
58
  "oxc-parser": "^0.137.0",
58
59
  "picomatch": "^4.0.4",
59
60
  "smol-toml": "^1.7.0",
@@ -6,6 +6,6 @@ resource: npm/rules/bun/
6
6
 
7
7
  # npm/rules/bun
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
11
  | [main.mjs](main.md) | JS Module |
@@ -6,7 +6,7 @@ resource: npm/rules/bun/js/
6
6
 
7
7
  # npm/rules/bun/js
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | ------------------------------- | --------- |
11
11
  | [fix-layout.mjs](fix-layout.md) | JS Module |
12
- | [layout.mjs](layout.md) | JS Module |
12
+ | [layout.mjs](layout.md) | JS Module |
@@ -38,7 +38,9 @@ function runLicenseeSteps(cwd = process.cwd(), opts = {}) {
38
38
  const configPath = join(cwd, '.licensee.json')
39
39
  if (!existsSync(configPath)) {
40
40
  if (readOnly) {
41
- fail('lint-bun: licensee — немає .licensee.json; запустіть `npx @nitra/cursor fix bun` локально для генерації (bun.mdc)')
41
+ fail(
42
+ 'lint-bun: licensee — немає .licensee.json; запустіть `npx @nitra/cursor fix bun` локально для генерації (bun.mdc)'
43
+ )
42
44
  return reporter.getExitCode()
43
45
  }
44
46
  writeFileSync(configPath, DEFAULT_LICENSEE_CONFIG, 'utf8')
@@ -6,7 +6,7 @@ resource: npm/rules/changelog/js/
6
6
 
7
7
  # npm/rules/changelog/js
8
8
 
9
- | Файл | Тип |
10
- |---|---|
11
- | [consistency.mjs](consistency.md) | JS Module |
9
+ | Файл | Тип |
10
+ | ----------------------------------------- | --------- |
11
+ | [consistency.mjs](consistency.md) | JS Module |
12
12
  | [fix-consistency.mjs](fix-consistency.md) | JS Module |
@@ -6,10 +6,10 @@ resource: npm/rules/js/js/
6
6
 
7
7
  # npm/rules/js/js
8
8
 
9
- | Файл | Тип |
10
- |---|---|
11
- | [check.mjs](check.md) | JS Module |
12
- | [dep-policy.mjs](dep-policy.md) | JS Module |
9
+ | Файл | Тип |
10
+ | ------------------------------------- | --------- |
11
+ | [check.mjs](check.md) | JS Module |
12
+ | [dep-policy.mjs](dep-policy.md) | JS Module |
13
13
  | [lint-findings.mjs](lint-findings.md) | JS Module |
14
- | [tooling.mjs](tooling.md) | JS Module |
14
+ | [tooling.mjs](tooling.md) | JS Module |
15
15
  | [utils_imports.mjs](utils_imports.md) | JS Module |
@@ -6,7 +6,7 @@ resource: npm/rules/js-run/js/
6
6
 
7
7
  # npm/rules/js-run/js
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | --------------------------------- | --------- |
11
11
  | [fix-runtime.mjs](fix-runtime.md) | JS Module |
12
- | [runtime.mjs](runtime.md) | JS Module |
12
+ | [runtime.mjs](runtime.md) | JS Module |
@@ -6,6 +6,6 @@ resource: npm/rules/python/
6
6
 
7
7
  # npm/rules/python
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
11
  | [main.mjs](main.md) | JS Module |
@@ -128,7 +128,9 @@ export function runLintPythonSteps(cwd = process.cwd(), opts = {}) {
128
128
  return true
129
129
  }
130
130
  const r = spawnSync(uvPath, ['run', '--frozen', 'pip-licenses', '--from=mixed', '--format=spdx-json'], {
131
- cwd: cwdPath, stdio: ['ignore', 'pipe', 'inherit'], shell: false,
131
+ cwd: cwdPath,
132
+ stdio: ['ignore', 'pipe', 'inherit'],
133
+ shell: false
132
134
  })
133
135
  if (r.status !== 0) {
134
136
  failF('lint-python: pip-licenses — помилка виконання')
@@ -136,7 +138,11 @@ export function runLintPythonSteps(cwd = process.cwd(), opts = {}) {
136
138
  }
137
139
  const allowed = getBronzeAndAbove()
138
140
  let doc
139
- try { doc = JSON.parse(r.stdout.toString('utf8')) } catch { doc = null }
141
+ try {
142
+ doc = JSON.parse(r.stdout.toString('utf8'))
143
+ } catch {
144
+ doc = null
145
+ }
140
146
  const packages = doc?.packages ?? []
141
147
  const violations = packages.filter(pkg => {
142
148
  const lic = pkg.licenseDeclared ?? pkg.licenseConcluded ?? 'NOASSERTION'
@@ -6,6 +6,6 @@ resource: npm/rules/rust/
6
6
 
7
7
  # npm/rules/rust
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | ------------------- | --------- |
11
11
  | [main.mjs](main.md) | JS Module |
@@ -89,7 +89,9 @@ function runRustLint(cwd = process.cwd(), opts = {}) {
89
89
  const denyConfigPath = join(cwd, 'deny.toml')
90
90
  if (!existsSync(denyConfigPath)) {
91
91
  if (readOnly) {
92
- fail('lint-rust: cargo deny — немає deny.toml; запустіть `npx @nitra/cursor fix rust` локально для генерації (rust.mdc)')
92
+ fail(
93
+ 'lint-rust: cargo deny — немає deny.toml; запустіть `npx @nitra/cursor fix rust` локально для генерації (rust.mdc)'
94
+ )
93
95
  } else {
94
96
  writeFileSync(denyConfigPath, generateDenyTomlLicenses(), 'utf8')
95
97
  pass('lint-rust: cargo deny — створено deny.toml з дефолтним allowlist')
@@ -6,7 +6,7 @@ resource: npm/rules/style/js/
6
6
 
7
7
  # npm/rules/style/js
8
8
 
9
- | Файл | Тип |
10
- |---|---|
9
+ | Файл | Тип |
10
+ | --------------------------------- | --------- |
11
11
  | [fix-tooling.mjs](fix-tooling.md) | JS Module |
12
- | [tooling.mjs](tooling.md) | JS Module |
12
+ | [tooling.mjs](tooling.md) | JS Module |
@@ -6,18 +6,18 @@ resource: npm/scripts/
6
6
 
7
7
  # npm/scripts
8
8
 
9
- | Файл | Тип |
10
- |---|---|
11
- | [auto-rules.mjs](auto-rules.md) | JS Module |
12
- | [auto-skills.mjs](auto-skills.md) | JS Module |
13
- | [build-agents-commands.mjs](build-agents-commands.md) | JS Module |
14
- | [cli-entry.mjs](cli-entry.md) | JS Module |
9
+ | Файл | Тип |
10
+ | ----------------------------------------------------------------------------------- | --------- |
11
+ | [auto-rules.mjs](auto-rules.md) | JS Module |
12
+ | [auto-skills.mjs](auto-skills.md) | JS Module |
13
+ | [build-agents-commands.mjs](build-agents-commands.md) | JS Module |
14
+ | [cli-entry.mjs](cli-entry.md) | JS Module |
15
15
  | [ensure-nitra-cursor-dev-dependencies.mjs](ensure-nitra-cursor-dev-dependencies.md) | JS Module |
16
- | [hook.mjs](hook.md) | JS Module |
17
- | [post-tool-use-check.mjs](post-tool-use-check.md) | JS Module |
18
- | [rename-yaml-extensions.mjs](rename-yaml-extensions.md) | JS Module |
19
- | [skills-cli.mjs](skills-cli.md) | JS Module |
20
- | [sync-claude-config.mjs](sync-claude-config.md) | JS Module |
21
- | [sync-setup-bun-deps-action.mjs](sync-setup-bun-deps-action.md) | JS Module |
22
- | [update-blue-oak.mjs](update-blue-oak.md) | JS Module |
23
- | [upgrade-nitra-cursor-and-install.mjs](upgrade-nitra-cursor-and-install.md) | JS Module |
16
+ | [hook.mjs](hook.md) | JS Module |
17
+ | [post-tool-use-check.mjs](post-tool-use-check.md) | JS Module |
18
+ | [rename-yaml-extensions.mjs](rename-yaml-extensions.md) | JS Module |
19
+ | [skills-cli.mjs](skills-cli.md) | JS Module |
20
+ | [sync-claude-config.mjs](sync-claude-config.md) | JS Module |
21
+ | [sync-setup-bun-deps-action.mjs](sync-setup-bun-deps-action.md) | JS Module |
22
+ | [update-blue-oak.mjs](update-blue-oak.md) | JS Module |
23
+ | [upgrade-nitra-cursor-and-install.mjs](upgrade-nitra-cursor-and-install.md) | JS Module |
@@ -17,6 +17,7 @@
17
17
  import { existsSync } from 'node:fs'
18
18
  import { readdir } from 'node:fs/promises'
19
19
  import { join } from 'node:path'
20
+ import { globby } from 'globby'
20
21
 
21
22
  /**
22
23
  * @typedef {object} JsConcern
@@ -44,20 +45,12 @@ import { join } from 'node:path'
44
45
  */
45
46
  async function listJsConcerns(jsDir) {
46
47
  if (!existsSync(jsDir)) return []
47
- const entries = await readdir(jsDir, { withFileTypes: true })
48
- /** @type {JsConcern[]} */
49
- const concerns = []
50
- for (const entry of entries) {
51
- if (!entry.isFile()) continue
52
- if (!entry.name.endsWith('.mjs')) continue
53
- if (entry.name.endsWith('.test.mjs')) continue
54
- if (entry.name.startsWith('fix-')) continue
55
- if (entry.name.startsWith('_')) continue
56
- if (entry.name.startsWith('.')) continue
57
- const name = entry.name.slice(0, -'.mjs'.length)
58
- concerns.push({ name })
59
- }
60
- return concerns.toSorted((a, b) => a.name.localeCompare(b.name))
48
+ const files = await globby(['*.mjs', '!*.test.mjs', '!fix-*.mjs', '!_*'], {
49
+ cwd: jsDir,
50
+ onlyFiles: true,
51
+ gitignore: false
52
+ })
53
+ return files.map(f => ({ name: f.slice(0, -4) })).toSorted((a, b) => a.name.localeCompare(b.name))
61
54
  }
62
55
 
63
56
  /**
@@ -6,39 +6,39 @@ resource: npm/scripts/lib/
6
6
 
7
7
  # npm/scripts/lib
8
8
 
9
- | Файл | Тип |
10
- |---|---|
11
- | [assert-project-root.mjs](assert-project-root.md) | JS Module |
12
- | [blue-oak.mjs](blue-oak.md) | JS Module |
13
- | [changed-files.mjs](changed-files.md) | JS Module |
14
- | [check-reporter.mjs](check-reporter.md) | JS Module |
15
- | [diff-added-lines.mjs](diff-added-lines.md) | JS Module |
9
+ | Файл | Тип |
10
+ | --------------------------------------------------------------------------- | --------- |
11
+ | [assert-project-root.mjs](assert-project-root.md) | JS Module |
12
+ | [blue-oak.mjs](blue-oak.md) | JS Module |
13
+ | [changed-files.mjs](changed-files.md) | JS Module |
14
+ | [check-reporter.mjs](check-reporter.md) | JS Module |
15
+ | [diff-added-lines.mjs](diff-added-lines.md) | JS Module |
16
16
  | [discover-check-rules-from-cursor.mjs](discover-check-rules-from-cursor.md) | JS Module |
17
- | [discover-checkable-rules.mjs](discover-checkable-rules.md) | JS Module |
18
- | [ensure-tool.mjs](ensure-tool.md) | JS Module |
19
- | [generated-markdown.mjs](generated-markdown.md) | JS Module |
20
- | [gha-workflow.mjs](gha-workflow.md) | JS Module |
21
- | [inline-template-links.mjs](inline-template-links.md) | JS Module |
22
- | [list-project-rules-mdc.mjs](list-project-rules-mdc.md) | JS Module |
23
- | [list-rule-ids.mjs](list-rule-ids.md) | JS Module |
24
- | [load-cursor-config.mjs](load-cursor-config.md) | JS Module |
25
- | [mirror-parity.mjs](mirror-parity.md) | JS Module |
26
- | [read-n-cursor-config-lite.mjs](read-n-cursor-config-lite.md) | JS Module |
27
- | [resolve-target-files.mjs](resolve-target-files.md) | JS Module |
28
- | [root-notice.mjs](root-notice.md) | JS Module |
29
- | [rule-meta-helpers.mjs](rule-meta-helpers.md) | JS Module |
30
- | [rule-meta.mjs](rule-meta.md) | JS Module |
31
- | [rule-predicates.mjs](rule-predicates.md) | JS Module |
32
- | [run-conftest-batch.mjs](run-conftest-batch.md) | JS Module |
33
- | [run-lint-step.mjs](run-lint-step.md) | JS Module |
34
- | [run-lint.mjs](run-lint.md) | JS Module |
35
- | [run-rule-cli.mjs](run-rule-cli.md) | JS Module |
36
- | [run-rule.mjs](run-rule.md) | JS Module |
37
- | [run-standard-lint.mjs](run-standard-lint.md) | JS Module |
38
- | [run-standard-rule.mjs](run-standard-rule.md) | JS Module |
39
- | [skill-meta.mjs](skill-meta.md) | JS Module |
40
- | [sync-gitignore-worktree.mjs](sync-gitignore-worktree.md) | JS Module |
41
- | [template.mjs](template.md) | JS Module |
42
- | [timing-summary.mjs](timing-summary.md) | JS Module |
43
- | [workspaces.mjs](workspaces.md) | JS Module |
44
- | [worktree-notice.mjs](worktree-notice.md) | JS Module |
17
+ | [discover-checkable-rules.mjs](discover-checkable-rules.md) | JS Module |
18
+ | [ensure-tool.mjs](ensure-tool.md) | JS Module |
19
+ | [generated-markdown.mjs](generated-markdown.md) | JS Module |
20
+ | [gha-workflow.mjs](gha-workflow.md) | JS Module |
21
+ | [inline-template-links.mjs](inline-template-links.md) | JS Module |
22
+ | [list-project-rules-mdc.mjs](list-project-rules-mdc.md) | JS Module |
23
+ | [list-rule-ids.mjs](list-rule-ids.md) | JS Module |
24
+ | [load-cursor-config.mjs](load-cursor-config.md) | JS Module |
25
+ | [mirror-parity.mjs](mirror-parity.md) | JS Module |
26
+ | [read-n-cursor-config-lite.mjs](read-n-cursor-config-lite.md) | JS Module |
27
+ | [resolve-target-files.mjs](resolve-target-files.md) | JS Module |
28
+ | [root-notice.mjs](root-notice.md) | JS Module |
29
+ | [rule-meta-helpers.mjs](rule-meta-helpers.md) | JS Module |
30
+ | [rule-meta.mjs](rule-meta.md) | JS Module |
31
+ | [rule-predicates.mjs](rule-predicates.md) | JS Module |
32
+ | [run-conftest-batch.mjs](run-conftest-batch.md) | JS Module |
33
+ | [run-lint-step.mjs](run-lint-step.md) | JS Module |
34
+ | [run-lint.mjs](run-lint.md) | JS Module |
35
+ | [run-rule-cli.mjs](run-rule-cli.md) | JS Module |
36
+ | [run-rule.mjs](run-rule.md) | JS Module |
37
+ | [run-standard-lint.mjs](run-standard-lint.md) | JS Module |
38
+ | [run-standard-rule.mjs](run-standard-rule.md) | JS Module |
39
+ | [skill-meta.mjs](skill-meta.md) | JS Module |
40
+ | [sync-gitignore-worktree.mjs](sync-gitignore-worktree.md) | JS Module |
41
+ | [template.mjs](template.md) | JS Module |
42
+ | [timing-summary.mjs](timing-summary.md) | JS Module |
43
+ | [workspaces.mjs](workspaces.md) | JS Module |
44
+ | [worktree-notice.mjs](worktree-notice.md) | JS Module |
@@ -6,42 +6,13 @@
6
6
  * `t0.mjs` ініціалізує результат через top-level await (один раз при завантаженні модуля).
7
7
  */
8
8
  import { existsSync } from 'node:fs'
9
- import { readdir } from 'node:fs/promises'
10
9
  import { join } from 'node:path'
10
+ import { globby } from 'globby'
11
11
 
12
12
  /**
13
13
  * @typedef {{ id: string, test: (output: string) => boolean, apply: (output: string, cwd: string) => Promise<{ok: boolean, action: string}> | {ok: boolean, action: string} }} T0Pattern
14
14
  */
15
15
 
16
- /**
17
- * Повертає абсолютні шляхи до `fix-*.mjs` файлів у директорії (плоско, без рекурсії).
18
- * @param {string} dir абсолютний шлях до директорії
19
- * @returns {Promise<string[]>}
20
- */
21
- async function findFixFiles(dir) {
22
- if (!existsSync(dir)) return []
23
- const entries = await readdir(dir, { withFileTypes: true })
24
- return entries
25
- .filter(e => e.isFile() && e.name.startsWith('fix-') && e.name.endsWith('.mjs'))
26
- .map(e => join(dir, e.name))
27
- }
28
-
29
- /**
30
- * Повертає абсолютні шляхи до `policy/{concern}/fix-*.mjs` у правилі.
31
- * @param {string} policyDir абсолютний шлях `rules/{rule}/policy/`
32
- * @returns {Promise<string[]>}
33
- */
34
- async function findPolicyFixFiles(policyDir) {
35
- if (!existsSync(policyDir)) return []
36
- const entries = await readdir(policyDir, { withFileTypes: true })
37
- const paths = []
38
- for (const entry of entries) {
39
- if (!entry.isDirectory()) continue
40
- paths.push(...(await findFixFiles(join(policyDir, entry.name))))
41
- }
42
- return paths
43
- }
44
-
45
16
  /**
46
17
  * Збирає всі T0-паттерни з `fix-*.mjs` файлів усіх правил у `rulesDir`.
47
18
  * @param {string} rulesDir абсолютний шлях до `npm/rules/`
@@ -49,26 +20,22 @@ async function findPolicyFixFiles(policyDir) {
49
20
  */
50
21
  export async function discoverT0Patterns(rulesDir) {
51
22
  if (!existsSync(rulesDir)) return []
52
- const ruleEntries = await readdir(rulesDir, { withFileTypes: true })
53
- /** @type {T0Pattern[]} */
54
- const allPatterns = []
55
-
56
- for (const ruleEntry of ruleEntries) {
57
- if (!ruleEntry.isDirectory() || ruleEntry.name.startsWith('.')) continue
58
- const ruleDir = join(rulesDir, ruleEntry.name)
59
23
 
60
- const fixPaths = [
61
- ...(await findFixFiles(join(ruleDir, 'js'))),
62
- ...(await findPolicyFixFiles(join(ruleDir, 'policy')))
63
- ]
24
+ const relPaths = await globby(['*/js/fix-*.mjs', '*/policy/*/fix-*.mjs'], {
25
+ cwd: rulesDir,
26
+ onlyFiles: true,
27
+ gitignore: false
28
+ })
64
29
 
65
- for (const fixPath of fixPaths) {
66
- try {
67
- const mod = await import(fixPath)
68
- if (Array.isArray(mod.patterns)) allPatterns.push(...mod.patterns)
69
- } catch (err) {
70
- console.error(`[discover-t0-patterns] не вдалося імпортувати ${fixPath}: ${err.message}`)
71
- }
30
+ /** @type {T0Pattern[]} */
31
+ const allPatterns = []
32
+ for (const rel of relPaths) {
33
+ const fixPath = join(rulesDir, rel)
34
+ try {
35
+ const mod = await import(fixPath)
36
+ if (Array.isArray(mod.patterns)) allPatterns.push(...mod.patterns)
37
+ } catch (err) {
38
+ console.error(`[discover-t0-patterns] не вдалося імпортувати ${fixPath}: ${err.message}`)
72
39
  }
73
40
  }
74
41
 
@@ -6,16 +6,16 @@ resource: npm/scripts/lib/fix/
6
6
 
7
7
  # npm/scripts/lib/fix
8
8
 
9
- | Файл | Тип |
10
- |---|---|
11
- | [analyze-escalation.mjs](analyze-escalation.md) | JS Module |
12
- | [discover-t0-patterns.mjs](discover-t0-patterns.md) | JS Module |
13
- | [escalation-log.mjs](escalation-log.md) | JS Module |
14
- | [llm-fix-apply.mjs](llm-fix-apply.md) | JS Module |
15
- | [llm-lint-fix.mjs](llm-lint-fix.md) | JS Module |
16
- | [llm-worker.mjs](llm-worker.md) | JS Module |
17
- | [orchestrator.mjs](orchestrator.md) | JS Module |
9
+ | Файл | Тип |
10
+ | ----------------------------------------------------- | --------- |
11
+ | [analyze-escalation.mjs](analyze-escalation.md) | JS Module |
12
+ | [discover-t0-patterns.mjs](discover-t0-patterns.md) | JS Module |
13
+ | [escalation-log.mjs](escalation-log.md) | JS Module |
14
+ | [llm-fix-apply.mjs](llm-fix-apply.md) | JS Module |
15
+ | [llm-lint-fix.mjs](llm-lint-fix.md) | JS Module |
16
+ | [llm-worker.mjs](llm-worker.md) | JS Module |
17
+ | [orchestrator.mjs](orchestrator.md) | JS Module |
18
18
  | [run-conformance-check.mjs](run-conformance-check.md) | JS Module |
19
- | [t0.mjs](t0.md) | JS Module |
20
- | [verbose-block.mjs](verbose-block.md) | JS Module |
21
- | [vscode-ext-add.mjs](vscode-ext-add.md) | JS Module |
19
+ | [t0.mjs](t0.md) | JS Module |
20
+ | [verbose-block.mjs](verbose-block.md) | JS Module |
21
+ | [vscode-ext-add.mjs](vscode-ext-add.md) | JS Module |
@@ -3,26 +3,31 @@ type: JS Module
3
3
  title: llm-worker.mjs
4
4
  resource: npm/scripts/lib/fix/llm-worker.mjs
5
5
  docgen:
6
- crc: 55419474
6
+ crc: 5d019a98
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
11
  ## Огляд
12
12
 
13
- Модуль виділяє унікальні відносні шляхи файлів, пов'язаних із порушеннями. Він також виконує роботу з великою мовною моделлю для аналізу даних про порушення, зчитуючи правила та обробляючи відповідні файли.
13
+ Модуль виділяє унікальні відносні шляхи файлів, пов'язаних із порушеннями, та виконує LLM-виклик для виправлення одного rule-порушення. Підтримує вибір sub-check `.mdc` за конкретним target-файлом — замість повного `n-{id}.mdc` передає моделі лише релевантну секцію правила (1–2 KB замість 20+ KB).
14
14
 
15
15
  ## Поведінка
16
16
 
17
- extractFilePaths витягує унікальні відносні шляхи файлів із вихідних даних про порушення, розпізнаючи як явні файли, що потребують виправлення, так і файли, що надають контекст.
18
- runLlmWorker викликає LLM для виправлення одного порушення, зчитує відповідне правило, аналізує файли, формує промпт, отримує відповідь від моделі та застосовує виправлення.
17
+ `extractFilePaths` витягує унікальні відносні шляхи файлів із violation output, пріоритетно розбираючи рядки (файли що потребують фіксу), потім generic-regex для контексту. Розуміє workspace-prefix `[npm] path/file.ext → npm/path/file.ext`.
18
+
19
+ `selectSubCheckMdc` читає `policy/<concern>/target.json` з каталогу `npm/rules/{ruleId}/` пакету, знаходить concerns із `files.single`, що відповідають failing-файлам із violation output, і повертає конкатенацію відповідних `.mdc`. Повертає `null` якщо правило не має policy-підкаталогу або жоден concern не збігається з ❌-файлами.
20
+
21
+ `runLlmWorker` спочатку викликає `selectSubCheckMdc` — і якщо отримує match, передає моделі лише той sub-check `.mdc`. Якщо match немає — fallback на повний `n-{id}.mdc`. Далі читає файли з violation output, будує prompt, викликає LLM через `callLlmRich` і застосовує зміни.
19
22
 
20
23
  ## Публічний API
21
24
 
22
- extractFilePaths — виділяє відносні шляхи файлів з виводу помилок, обробляючи префікс робочого простору та пріоритетно парсячи рядки, що вказують на необхідність виправлення.
23
- runLlmWorker — виправляє одне порушення правила за допомогою LLM, повертаючи результати змін чи діагнозу, що слугує інформацією для наступних етапів процесу.
25
+ `extractFilePaths(output)`повертає унікальні відносні шляхи файлів з violation output (❌-рядки першими).
26
+
27
+ `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення правила через LLM. Повертає `{ ok, error?, changes, diagnosis, reasoning, reasoningSource, promptSummary }`. `promptSummary.subCheckMdc: boolean` вказує чи використано точковий sub-check замість повного mdc.
24
28
 
25
29
  ## Гарантії поведінки
26
30
 
27
- - Read-only: не виконує операцій запису (ФС/БД).
28
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
31
+ - Читає `policy/` безпосередньо з пакету (`import.meta.dirname/../../../rules/`), без pre-generation файлів.
32
+ - Fallback на повний `n-{id}.mdc` при: відсутності policy-підкаталогу, `walkGlob`-таргетах, відсутності ❌-файлів у violation, нульовому match.
33
+ - Перехоплює всі помилки LLM і повертає структурований `{ ok: false, error }`.
@@ -1,6 +1,6 @@
1
1
  /** @see ./docs/llm-worker.md */
2
2
 
3
- import { existsSync, readFileSync } from 'node:fs'
3
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { env } from 'node:process'
6
6
  import { resolveModel } from '../../../lib/models.mjs'
@@ -20,6 +20,83 @@ const API_KEY_RE = /api key/i
20
20
 
21
21
  const FILE_EXTS = 'json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py'
22
22
 
23
+ /**
24
+ * Каталог `npm/rules/` у пакеті — для вибору sub-check .mdc.
25
+ * Шлях: <package>/npm/scripts/lib/fix/ → ../../.. → npm/ → rules/.
26
+ */
27
+ const PACKAGE_RULES_DIR = join(import.meta.dirname, '..', '..', '..', 'rules')
28
+
29
+ /**
30
+ * Витягує шляхи файлів лише з рядків ❌ у violation output.
31
+ * Без workspace-розгортання — повертає bare path для звірки з target.json.
32
+ * @param {string} output violation output
33
+ * @returns {string[]} унікальні шляхи з ❌-рядків
34
+ */
35
+ function extractFailPaths(output) {
36
+ const seen = new Set()
37
+ const add = p => {
38
+ seen.add(p)
39
+ }
40
+ const failSep = `(?::\\d+)?(?::\\s|[\\s—]|$)`
41
+ // ❌ [ws] path/file.ext → strip workspace, зберігаємо bare file
42
+ const failWsRe = new RegExp(`^\\s*❌\\s+\\[[\\w-]+\\]\\s+([\\w./][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
43
+ for (const m of output.matchAll(failWsRe)) add(m[1])
44
+ const failRe = new RegExp(`^\\s*❌\\s+(\\.?[\\w][\\w./-]*\\.(?:${FILE_EXTS}))${failSep}`, 'gm')
45
+ for (const m of output.matchAll(failRe)) add(m[1])
46
+ return [...seen]
47
+ }
48
+
49
+ /**
50
+ * Для правил з `policy/<concern>/` підбирає лише ті concern-.mdc, що відповідають
51
+ * файлам з ❌-рядків violation output. Читає безпосередньо з пакету (`PACKAGE_RULES_DIR`),
52
+ * тому не потребує pre-generation. Fallback — null (→ повний n-{id}.mdc у caller).
53
+ * @param {string} ruleId ID правила
54
+ * @param {string} violationOutput violation output
55
+ * @returns {string|null} конкатенація релевантних .mdc або null
56
+ */
57
+ function selectSubCheckMdc(ruleId, violationOutput) {
58
+ const policyDir = join(PACKAGE_RULES_DIR, ruleId, 'policy')
59
+ if (!existsSync(policyDir)) return null
60
+
61
+ const failPaths = extractFailPaths(violationOutput)
62
+ if (failPaths.length === 0) return null
63
+
64
+ const matched = []
65
+ let concerns
66
+ try {
67
+ concerns = readdirSync(policyDir, { withFileTypes: true })
68
+ } catch {
69
+ return null
70
+ }
71
+
72
+ for (const entry of concerns) {
73
+ if (!entry.isDirectory()) continue
74
+ const concernDir = join(policyDir, entry.name)
75
+ const targetPath = join(concernDir, 'target.json')
76
+ if (!existsSync(targetPath)) continue
77
+
78
+ let target
79
+ try {
80
+ target = JSON.parse(readFileSync(targetPath, 'utf8'))
81
+ } catch {
82
+ continue
83
+ }
84
+
85
+ const targetFile = target?.files?.single
86
+ if (!targetFile) continue // walkGlob та інші типи → skip, fallback на main.mdc
87
+
88
+ // Перевіряємо чи хоч один failing path закінчується на targetFile
89
+ const hit = failPaths.some(p => p === targetFile || p.endsWith(`/${targetFile}`))
90
+ if (!hit) continue
91
+
92
+ const mdcEntry = readdirSync(concernDir).find(f => f.endsWith('.mdc'))
93
+ if (!mdcEntry) continue
94
+ matched.push(readFileSync(join(concernDir, mdcEntry), 'utf8').trim())
95
+ }
96
+
97
+ return matched.length > 0 ? matched.join('\n\n') : null
98
+ }
99
+
23
100
  /**
24
101
  * Витягує відносні шляхи файлів із violation output.
25
102
  * Розуміє workspace-prefix: `[npm] skills/foo.mjs` → `npm/skills/foo.mjs`.
@@ -185,9 +262,11 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
185
262
  const timeoutMs = opts.timeoutMs
186
263
  const thinkingBudget = opts.thinkingBudget ?? DEFAULT_THINKING_BUDGET
187
264
 
188
- // 1. Читаємо rule .mdc
265
+ // 1. Читаємо rule .mdc: спробуємо sub-check mdc для конкретної перевірки,
266
+ // якщо не вдалося — fallback на повний n-{id}.mdc.
267
+ const subMdc = selectSubCheckMdc(ruleId, violationOutput)
189
268
  const mdcPath = join(projectRoot, '.cursor', 'rules', `n-${ruleId}.mdc`)
190
- const ruleMdc = existsSync(mdcPath) ? readFileSync(mdcPath, 'utf8') : '(rule file not found)'
269
+ const ruleMdc = subMdc ?? (existsSync(mdcPath) ? readFileSync(mdcPath, 'utf8') : '(rule file not found)')
191
270
 
192
271
  // 2. Витягуємо файли з violation output і читаємо їх
193
272
  const files = readFilesForFix(extractFilePaths(violationOutput), projectRoot)
@@ -195,6 +274,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
195
274
  // 3. Будуємо summary промпту (для verbose-блоку) до виклику моделі
196
275
  const promptSummary = {
197
276
  ruleMdcLen: ruleMdc.length,
277
+ subCheckMdc: !!subMdc,
198
278
  violationLen: violationOutput.length,
199
279
  filesCount: files.length,
200
280
  filesTotalBytes: files.reduce((s, f) => s + f.content.length, 0),
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { readFile, readdir } from 'node:fs/promises'
2
+ import { readFile } from 'node:fs/promises'
3
3
  import { basename, extname, join } from 'node:path'
4
+ import { globby } from 'globby'
4
5
 
5
6
  const MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
6
7
  const TEMPLATE_SEGMENT_RE = /\/templates?\//
@@ -80,27 +81,18 @@ export async function appendDiscoveredMdcFiles(text, ruleDir) {
80
81
 
81
82
  const jsDir = join(ruleDir, 'js')
82
83
  if (existsSync(jsDir)) {
83
- const entries = await readdir(jsDir, { withFileTypes: true })
84
- for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
85
- if (e.isFile() && e.name.endsWith('.mdc')) {
86
- sections.push((await readFile(join(jsDir, e.name), 'utf8')).trim())
87
- }
84
+ const files = await globby('*.mdc', { cwd: jsDir, onlyFiles: true, gitignore: false })
85
+ for (const f of files.toSorted()) {
86
+ sections.push((await readFile(join(jsDir, f), 'utf8')).trim())
88
87
  }
89
88
  }
90
89
 
91
90
  const policyDir = join(ruleDir, 'policy')
92
91
  if (existsSync(policyDir)) {
93
- const concerns = (await readdir(policyDir, { withFileTypes: true }))
94
- .filter(e => e.isDirectory())
95
- .sort((a, b) => a.name.localeCompare(b.name))
96
- for (const concern of concerns) {
97
- const concernDir = join(policyDir, concern.name)
98
- const files = (await readdir(concernDir, { withFileTypes: true }))
99
- .filter(e => e.isFile() && e.name.endsWith('.mdc'))
100
- .sort((a, b) => a.name.localeCompare(b.name))
101
- for (const f of files) {
102
- sections.push((await readFile(join(concernDir, f.name), 'utf8')).trim())
103
- }
92
+ // '*/*.mdc' 'concern/file.mdc'; рядкове сортування дає concern-first, потім file
93
+ const files = await globby('*/*.mdc', { cwd: policyDir, onlyFiles: true, gitignore: false })
94
+ for (const rel of files.toSorted()) {
95
+ sections.push((await readFile(join(policyDir, rel), 'utf8')).trim())
104
96
  }
105
97
  }
106
98
 
@@ -3,9 +3,9 @@
3
3
  * Винесено зі `bin/n-cursor.js`, щоб ділити між CLI-dispatch і `run-conformance-check` (конформність-детект).
4
4
  */
5
5
  import { existsSync } from 'node:fs'
6
- import { readdir } from 'node:fs/promises'
7
6
  import { join } from 'node:path'
8
7
  import { cwd as processCwd } from 'node:process'
8
+ import { globby } from 'globby'
9
9
 
10
10
  /** Каталог правил у проєкті-споживачі (відносно кореня). */
11
11
  export const CURSOR_RULES_DIR = '.cursor/rules'
@@ -17,6 +17,6 @@ export const CURSOR_RULES_DIR = '.cursor/rules'
17
17
  export async function listProjectRulesMdcFiles(cwd = processCwd()) {
18
18
  const dir = join(cwd, CURSOR_RULES_DIR)
19
19
  if (!existsSync(dir)) return []
20
- const names = await readdir(dir)
21
- return names.filter(n => n.endsWith('.mdc')).toSorted((a, b) => a.localeCompare(b))
20
+ const names = await globby('*.mdc', { cwd: dir, onlyFiles: true, gitignore: false })
21
+ return names.toSorted((a, b) => a.localeCompare(b))
22
22
  }
@@ -43,7 +43,7 @@ for (const rating of data.ratings) {
43
43
  const out = {
44
44
  version: data.version,
45
45
  source: BLUE_OAK_URL,
46
- bronzeAndAbove,
46
+ bronzeAndAbove
47
47
  }
48
48
 
49
49
  writeFileSync(OUT_PATH, JSON.stringify(out, null, 2) + '\n', 'utf8')
@@ -1,84 +1,44 @@
1
- /**
2
- * Рекурсивний обхід каталогів для скриптів перевірки (Dockerfile, k8s YAML тощо).
3
- *
4
- * Обходить дерево від заданого кореня; для кожного звичайного файлу викликає переданий callback.
5
- * Каталоги node_modules, .git, dist, coverage, .turbo, .next не заходяться.
6
- * Додатково можна передати `ignorePaths` — повні шляхи каталогів (абсолютні posix), які слід
7
- * пропускати разом з усім вмістом (поле `ignore` у `.n-cursor.json`). Якщо readdir для каталогу
8
- * не вдається — тихо виходить без throw.
9
- */
10
- import { readdir } from 'node:fs/promises'
11
- import { isAbsolute, join, resolve, sep } from 'node:path'
1
+ /** @see ./docs/walkDir.md */
2
+ import { join, relative, resolve, sep } from 'node:path'
3
+ import { globby } from 'globby'
12
4
 
13
- /**
14
- * Перетворює довільний шлях у абсолютний posix-формат без trailing-slash.
15
- * @param {string} p шлях
16
- * @returns {string} абсолютний posix-шлях
17
- */
18
- function toAbsPosix(p) {
19
- const abs = isAbsolute(p) ? p : resolve(p)
20
- let posix = abs.split(sep).join('/')
21
- while (posix.endsWith('/')) posix = posix.slice(0, -1)
22
- return posix
23
- }
5
+ // .git ніколи не потрапляє в .gitignore — пропускаємо завжди.
6
+ // node_modules safety net: проєкт може не мати .gitignore або запускатись поза git-репо.
7
+ const ALWAYS_IGNORE = ['.git/**', 'node_modules/**']
24
8
 
25
9
  /**
26
- * Чи каталог `dirAbsPosix` входить у список ignore (точний збіг або префікс з '/').
27
- * Часткові збіги басенейму не враховуються (postgres-master-test postgres-master).
28
- * @param {string} dirAbsPosix абсолютний posix-шлях каталогу
29
- * @param {string[]} ignorePosix вже нормалізовані ignore-шляхи
30
- * @returns {boolean} `true`, якщо шлях слід пропустити (точний збіг або префікс з `/`)
31
- */
32
- function isIgnoredDir(dirAbsPosix, ignorePosix) {
33
- for (const ig of ignorePosix) {
34
- if (dirAbsPosix === ig) return true
35
- if (dirAbsPosix.startsWith(`${ig}/`)) return true
36
- }
37
- return false
38
- }
39
-
40
- /**
41
- * Рекурсивно обходить каталог, пропускає типові артефакти збірки/залежностей та `ignorePaths`.
42
- * @param {string} dir абсолютний шлях
43
- * @param {(filePath: string) => void} onFile виклик для кожного файлу
44
- * @param {string[]} [ignorePaths] шляхи каталогів (відносні від cwd або абсолютні), що повністю виключаються з обходу
45
- * @returns {Promise<void>} визначається по завершенню обходу
10
+ * Рекурсивно обходить каталог, поважаючи .gitignore (включно з вкладеними).
11
+ * @param {string} dir абсолютний або відносний шлях до кореня обходу
12
+ * @param {(filePath: string) => void} onFile колбек для кожного файлу (абсолютний шлях)
13
+ * @param {string[]} [ignorePaths] додаткові шляхи для пропуску (абсолютні або відносні від cwd)
14
+ * @returns {Promise<void>}
46
15
  */
47
16
  export async function walkDir(dir, onFile, ignorePaths = []) {
48
- const ignorePosix = ignorePaths.map(p => toAbsPosix(p))
49
- await walkDirInner(dir, onFile, ignorePosix)
50
- }
17
+ const absDir = resolve(dir)
51
18
 
52
- /**
53
- * Внутрішній рекурсор. ignorePosix вже нормалізовано — не нормалізуємо повторно на кожному рівні.
54
- * @param {string} dir абсолютний шлях каталогу для обходу
55
- * @param {(filePath: string) => void} onFile колбек, що викликається для кожного звичайного файлу
56
- * @param {string[]} ignorePosix вже нормалізовані абсолютні posix-шляхи ігнорованих каталогів
57
- * @returns {Promise<void>} визначається по завершенню рекурсії
58
- */
59
- async function walkDirInner(dir, onFile, ignorePosix) {
60
- if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(dir), ignorePosix)) return
61
- let entries
19
+ const extraIgnore = ignorePaths
20
+ .map(p => {
21
+ const abs = resolve(p.replace(/\/+$/, ''))
22
+ const rel = relative(absDir, abs).split(sep).join('/')
23
+ if (rel.startsWith('..') || rel === '') return null
24
+ return `${rel}/**`
25
+ })
26
+ .filter(Boolean)
27
+
28
+ let files
62
29
  try {
63
- entries = await readdir(dir, { withFileTypes: true })
30
+ files = await globby('**/*', {
31
+ cwd: absDir,
32
+ gitignore: true,
33
+ dot: true,
34
+ onlyFiles: true,
35
+ ignore: [...ALWAYS_IGNORE, ...extraIgnore]
36
+ })
64
37
  } catch {
65
38
  return
66
39
  }
67
- for (const e of entries) {
68
- const p = join(dir, e.name)
69
- if (e.isDirectory()) {
70
- const skipDir =
71
- e.name === 'node_modules' ||
72
- e.name === '.git' ||
73
- e.name === 'dist' ||
74
- e.name === 'coverage' ||
75
- e.name === '.turbo' ||
76
- e.name === '.next'
77
- if (skipDir) continue
78
- if (ignorePosix.length > 0 && isIgnoredDir(toAbsPosix(p), ignorePosix)) continue
79
- await walkDirInner(p, onFile, ignorePosix)
80
- } else if (e.isFile()) {
81
- onFile(p)
82
- }
40
+
41
+ for (const rel of files) {
42
+ onFile(join(absDir, rel))
83
43
  }
84
44
  }
@@ -44,6 +44,7 @@ node -e "const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));
44
44
  ```
45
45
 
46
46
  Для кожного воркспейсу `<ws>`:
47
+
47
48
  - `relRoot` = `<ws>` (напр. `npm`, `demo`)
48
49
  - `docPath` = `<ws>/docs/ARCHITECTURE.md`
49
50
  - `members` — кодові файли (`.mjs .ts .vue .py`, крім тестів) у `<ws>/`
@@ -11,12 +11,15 @@ version: '1.0'
11
11
 
12
12
  ## Формат Telegram
13
13
 
14
- Telegram підтримує моноширний шрифт через markdown-розмітку:
14
+ Telegram підтримує Markdown-форматування безпосередньо в клієнті:
15
15
 
16
- - `` `inline code` `` для inline
17
- - ` ```pre``` ` (потрійні backticks) — для блоку коду / моноширного тексту
16
+ - `**bold**` жирний (заголовки, мітки секцій)
17
+ - `_italic_` курсив (акценти)
18
+ - `` `inline code` `` — inline-код, команди, назви файлів/пакетів
19
+ - ` ```block``` ` (потрійні backticks) — блок коду, коли є реальний код
20
+ - `~~strikethrough~~` — закреслення (опціонально)
18
21
 
19
- Весь пост оформлюй як **один блок** потрійних backticks, щоб текст був моноширним.
22
+ Секції (Проблема:, Рішення: тощо) виділяй `**жирним**`. Технічні терміни (назви файлів, пакетів, команди) — в `` `inline code` ``.
20
23
 
21
24
  ## Workflow
22
25
 
@@ -28,23 +31,23 @@ Telegram підтримує моноширний шрифт через markdown-
28
31
 
29
32
  ## Шаблон посту
30
33
 
31
- ```markdown
34
+ ```
32
35
  #тег
33
36
 
34
- 📌 <Короткий заголовок>
37
+ 📌 **<Короткий заголовок>**
35
38
 
36
- Проблема:
39
+ **Проблема:**
37
40
  <1-3 речення — що було не так / яке завдання>
38
41
 
39
- Рішення:
42
+ **Рішення:**
40
43
  <2-5 речень — що зробили, який підхід>
41
44
 
42
- Деталі:
45
+ **Деталі:**
43
46
  • <ключовий момент 1>
44
47
  • <ключовий момент 2>
45
48
  • <ключовий момент 3>
46
49
 
47
- Результат:
50
+ **Результат:**
48
51
  <1-2 речення — що отримали, яка вигода>
49
52
  ```
50
53
 
@@ -64,32 +67,27 @@ Telegram підтримує моноширний шрифт через markdown-
64
67
  - Тег: рівно 1 хештег першим рядком поста (#devops, #frontend, #bugfix, #refactoring, #CI, #performance тощо)
65
68
  - Emoji: лише структурні (📌 для заголовка, • для списку), не перевантажувати
66
69
  - Цільова аудиторія: розробники та менеджери — тому пояснюй простою мовою, без надмірного жаргону
67
- - Весь текст повинен бути всередині потрійних backticks для моноширного відображення в Telegram
70
+ - Назви файлів, пакетів, команди у `` `inline code` ``
71
+ - Блок коду (потрійні backticks) — лише якщо є реальний код/конфіг, не для всього тексту
68
72
 
69
73
  ## Приклад
70
74
 
71
- ````
72
75
  ```
73
76
  #dev
74
77
 
75
- 📌 Міграція з Prettier на oxfmt
78
+ 📌 **Міграція з Prettier на oxfmt**
76
79
 
77
- Проблема:
78
- Prettier повільно форматував великі файли і
79
- конфліктував з ESLint при роботі з Vue SFC.
80
+ **Проблема:**
81
+ Prettier повільно форматував великі файли і конфліктував з ESLint при роботі з Vue SFC.
80
82
 
81
- Рішення:
82
- Замінили Prettier на oxfmt — нативний
83
- форматер від OXC. Оновили VS Code settings,
84
- видалили prettier-конфіги, додали .oxfmtrc.json.
83
+ **Рішення:**
84
+ Замінили Prettier на `oxfmt` — нативний форматер від OXC. Оновили VS Code settings, видалили prettier-конфіги, додали `.oxfmtrc.json`.
85
85
 
86
- Деталі:
87
- • oxfmt працює в 10-50x швидше за Prettier
88
- • Єдиний конфіг .oxfmtrc.json у корені
89
- • CI перевіряє форматування через lint-js
86
+ **Деталі:**
87
+ `oxfmt` працює в 10-50x швидше за Prettier
88
+ • Єдиний конфіг `.oxfmtrc.json` у корені
89
+ • CI перевіряє форматування через `lint-js`
90
90
 
91
- Результат:
92
- Форматування працює миттєво при збереженні,
93
- зникли конфлікти між форматером і лінтером.
91
+ **Результат:**
92
+ Форматування працює миттєво при збереженні, зникли конфлікти між форматером і лінтером.
94
93
  ```
95
- ````