@nitra/cursor 1.13.2 → 1.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/n-cursor.js +4 -2
  3. package/package.json +4 -2
  4. package/rules/ga/fix/workflows/check.mjs +6 -109
  5. package/rules/ga/policy/package_json/package_json.rego +24 -0
  6. package/rules/ga/policy/package_json/target.json +8 -0
  7. package/rules/ga/policy/vscode_extensions/target.json +8 -0
  8. package/rules/ga/policy/vscode_extensions/vscode_extensions.rego +16 -0
  9. package/rules/ga/policy/vscode_settings/target.json +8 -0
  10. package/rules/ga/policy/vscode_settings/vscode_settings.rego +24 -0
  11. package/rules/ga/policy/zizmor_yml/target.json +8 -0
  12. package/rules/ga/policy/zizmor_yml/zizmor_yml.rego +17 -0
  13. package/rules/js-lint/fix/tooling/check.mjs +6 -83
  14. package/rules/js-lint/policy/jscpd/jscpd.rego +38 -0
  15. package/rules/js-lint/policy/jscpd/target.json +8 -0
  16. package/rules/js-lint/policy/vscode_extensions/target.json +8 -0
  17. package/rules/js-lint/policy/vscode_extensions/vscode_extensions.rego +25 -0
  18. package/rules/security/fix/gitleaks/check.mjs +8 -45
  19. package/rules/security/fix/gitleaks/template/.gitleaks.toml.snippet.toml +12 -0
  20. package/rules/security/policy/gitleaks/gitleaks.rego +17 -0
  21. package/rules/security/policy/gitleaks/target.json +8 -0
  22. package/rules/security/policy/package_json/package_json.rego +22 -59
  23. package/rules/security/policy/package_json/template/package.json.contains.json +1 -0
  24. package/rules/security/policy/package_json/template/package.json.deny.json +4 -0
  25. package/rules/security/policy/package_json/template/package.json.snippet.json +1 -0
  26. package/rules/security/security.mdc +7 -26
  27. package/rules/vue/fix/packages/check.mjs +7 -64
  28. package/rules/vue/policy/package_json/package_json.rego +45 -2
  29. package/rules/vue/vue.mdc +15 -2
  30. package/scripts/ensure-nitra-cursor-dev-dependencies.mjs +41 -21
  31. package/scripts/utils/check-mdc-template-refs.mjs +47 -0
  32. package/scripts/utils/inline-template-links.mjs +60 -0
  33. package/scripts/utils/run-conftest-batch.mjs +60 -33
  34. package/scripts/utils/run-rule.mjs +16 -1
  35. package/scripts/utils/template.mjs +215 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,61 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.8] - 2026-05-17
8
+
9
+ ### Changed
10
+
11
+ - Перенесено частину per-document логіки з `fix` у Rego policy:
12
+ - `js-lint`: `.jscpd.json` і `.vscode/extensions.json`;
13
+ - `ga`: `package.json#scripts.lint-ga`, `.vscode/extensions.json`, `.vscode/settings.json`, `.github/zizmor.yml`;
14
+ - `security`: `.gitleaks.toml` (`[extend].useDefault = true`);
15
+ - `vue`: залежності Vue/Vite-пакетів і заборону `esbuild`.
16
+ - Відповідні JS check-и спрощено до FS/cross-file/AST/tooling частини без дублювання Rego-умов.
17
+ - `ensureNitraCursorInRootDevDependencies` тепер додає `@nitra/cursor` тільки в `package.json` поруч із запуском, якщо в ньому є `workspaces`.
18
+ - `vue.mdc` уточнює тестування через Bun Test Runner + Vue Test Utils/happy-dom замість Vitest/jsdom.
19
+
20
+ ### Fixed
21
+
22
+ - `npm/package.json#devDependencies` — прибрано self-reference `@nitra/cursor`, щоб published package знову відповідав `npm-module` compact-package canon.
23
+
24
+ ## [1.13.7] - 2026-05-17
25
+
26
+ ### Fixed
27
+
28
+ - `inlineTemplateLinks`: `String.replace(needle, replacement)` інтерпретує `$'`, `$&` тощо у `replacement`. Через це інлайнінг `.gitleaks.toml.snippet.toml` (де є `$'''`) ламав вивід — хвіст `.mdc` реінжектився всередину блока. Перехід на function-replacer (`(_) => replacement`) усуває це. Додано регресійний тест із фікстурою `with-dollar.toml`.
29
+
30
+ ## [1.13.6] - 2026-05-17
31
+
32
+ ### Added
33
+
34
+ - `npm/scripts/utils/inline-template-links.mjs` — `inlineTemplateLinks(text, ruleDir)`: під час sync знаходить markdown-лінки виду `[label](./…/template/…)` у `.mdc` і замінює їх inline fenced-блоком з вмістом відповідного файла. Відсутній файл — hard error (fail loud).
35
+
36
+ ### Changed
37
+
38
+ - `readBundledRuleContent` у `npm/bin/n-cursor.js` тепер пропускає текст правила через `inlineTemplateLinks` перед записом у `.cursor/rules/n-*.mdc`. Template-посилання у скопійованих правилах більше не зламані.
39
+
40
+ ## [1.13.5] - 2026-05-17
41
+
42
+ ### Added
43
+
44
+ - Оркестратор `run-rule.mjs` тепер викликає `findMissingMdcRefs` для кожного правила — fail, якщо файл у `template/` не згаданий як markdown-посилання у `<id>.mdc`. Поки що активно лише для `security` (єдине правило з `template/`); готова страховка для Phase 2+.
45
+
46
+ ### Fixed
47
+
48
+ - `check-mdc-template-refs.test.mjs` тест 3 — фіксував дубль test 1; тепер використовує окрему `no-templates` фікстуру, що дійсно валідує "no template/ dirs → empty result".
49
+
50
+ ## [1.13.4] - 2026-05-17
51
+
52
+ ### Removed
53
+
54
+ - `npm/package.json#devDependencies` — повторно видалено self-reference `@nitra/cursor` (порушує canon `npm-module`: «devDependencies не публікуються користувачам пакета»). Автоматично повертався у попередніх тасках template-dir роботи; цей коміт остаточно прибирає.
55
+
56
+ ## [1.13.3] - 2026-05-17
57
+
58
+ ### Changed
59
+
60
+ - `security/security.mdc` — прибрано inline merge-фрагменти (package.json snippet для `lint-security`, .gitleaks.toml повний канон), замість них markdown-посилання на файли в `template/` (single source of truth). Зміст правила залишається (описи для чого потрібен gitleaks, GitHub Actions), видалено дублювання фіксованого коду.
61
+
7
62
  ## [1.13.2] - 2026-05-17
8
63
 
9
64
  ### Changed
@@ -20,6 +75,10 @@
20
75
 
21
76
  ### Changed
22
77
 
78
+ - `security/fix/gitleaks/check.mjs` читає канон з `template/`, не з inline regex.
79
+ - `security/policy/package_json/package_json.rego` читає очікувані значення з `data.template.*`, не з inline literals.
80
+ - Оркестратор `run-rule.mjs` для policy-концернів вантажить `template/` через `resolveConcernTemplateData` і передає у `runConftestBatch.templateData`.
81
+ - Снепет `.gitleaks.toml.snippet.toml` тримає канонічний title + allowlist paths (description лишається user-specific).
23
82
  - **9 правил переведено з `alwaysApply: true` на `alwaysApply: false` + `globs:`** — AI-контекст у Cursor/Claude Code підвантажується лише при роботі з релевантними файлами; програмна валідація через `npx check <rule>` залишається повністю функціональною незалежно від AI-контексту. Економить контекстне вікно у сесіях, де редагують код, далекий від відповідних конфігів.
24
83
  - **`bun`** (`1.7 → 1.8`) — `globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"`
25
84
  - **`capacitor`** (`1.0 → 1.1`) — `globs: "**/capacitor.config.json,**/android/**,**/ios/**"`
package/bin/n-cursor.js CHANGED
@@ -67,6 +67,7 @@ import { cwd, env } from 'node:process'
67
67
  import { fileURLToPath } from 'node:url'
68
68
 
69
69
  import { buildAgentsCommandBulletItems } from '../scripts/build-agents-commands.mjs'
70
+ import { inlineTemplateLinks } from '../scripts/utils/inline-template-links.mjs'
70
71
  import {
71
72
  detectAutoRules,
72
73
  detectLegacyRuleIds,
@@ -394,7 +395,7 @@ function normalizeRuleName(ruleName) {
394
395
  * @param {string} [bundledRulesDir] каталог `rules/` у корені пакету-джерела
395
396
  * @returns {Promise<string>} текст правила для запису в `.cursor/rules/n-*.mdc`
396
397
  */
397
- function readBundledRuleContent(rule, bundledRulesDir = BUNDLED_RULES_DIR) {
398
+ async function readBundledRuleContent(rule, bundledRulesDir = BUNDLED_RULES_DIR) {
398
399
  const id = normalizeRuleName(rule)
399
400
  const bundledPath = join(bundledRulesDir, id, `${id}.mdc`)
400
401
  if (!existsSync(bundledPath)) {
@@ -402,7 +403,8 @@ function readBundledRuleContent(rule, bundledRulesDir = BUNDLED_RULES_DIR) {
402
403
  `Немає файлу ${id}/${id}.mdc у ${bundledRulesDir}. Оновіть ${PACKAGE_NAME} або приберіть "${rule}" з rules у ${CONFIG_FILE}.`
403
404
  )
404
405
  }
405
- return readFile(bundledPath, 'utf8')
406
+ const text = await readFile(bundledPath, 'utf8')
407
+ return inlineTemplateLinks(text, dirname(bundledPath))
406
408
  }
407
409
 
408
410
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.2",
3
+ "version": "1.13.8",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -36,7 +36,8 @@
36
36
  "!**/*.test.mjs",
37
37
  "!**/*_test.rego",
38
38
  "!**/test-helpers.mjs",
39
- "!**/fixtures/**"
39
+ "!**/fixtures/**",
40
+ "!**/__fixtures__/**"
40
41
  ],
41
42
  "type": "module",
42
43
  "types": "./types/bin/n-cursor.d.ts",
@@ -47,6 +48,7 @@
47
48
  "dependencies": {
48
49
  "oxc-parser": "^0.128.0",
49
50
  "picomatch": "^4.0.4",
51
+ "smol-toml": "^1.6.1",
50
52
  "yaml": "^2.8.3"
51
53
  },
52
54
  "engines": {
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * Перевіряє GitHub Actions за правилом ga.mdc.
3
3
  *
4
- * Workflows лише з розширенням `.yml`, наявність clean/lint workflow, конфіг zizmor з ref-pin,
5
- * відсутність MegaLinter, коректний скрипт `lint-ga` у `package.json`, виклик у `lint-ga.yml`,
4
+ * Workflows лише з розширенням `.yml`, наявність clean/lint workflow,
5
+ * відсутність MegaLinter, виклик у `lint-ga.yml`,
6
6
  * наявність composite `.github/actions/setup-bun-deps/action.yml` (його записує npx `\@nitra/cursor`),
7
- * `\.vscode/settings.json` — `editor.defaultFormatter` **oxc** для `[github-actions-workflow]`.
8
7
  *
9
8
  * Структурні поля 4 канонічних workflow (`clean-ga-workflows.yml`, `clean-merged-branch.yml`,
10
9
  * `lint-ga.yml`, `git-ai.yml`) і УНІВЕРСАЛЬНІ перевірки для всіх `.github/workflows/*.yml`
11
10
  * (`concurrency`, заборонені `oven-sh/setup-bun` / `actions/cache` / `bun install` у `uses`/`run`,
12
11
  * shell-продовження `\` у `run`, обов'язковий `actions/checkout@v6` перед локальним
13
- * `setup-bun-deps`) у Rego-полісі під `npm/policy/ga/` і запускаються через
14
- * `bun run lint-ga` (`runConftestStep` у `lint-ga.mjs`). Тут лишилася лише git-залежна
15
- * перевірка `on.*.paths` glob-ів через `git ls-files :(glob)`.
12
+ * `setup-bun-deps`), а також `package.json`, `.vscode/*` і `.github/zizmor.yml`
13
+ * у Rego-полісі під `npm/policy/ga/`. Тут лишилися FS/git/tooling перевірки:
14
+ * наявність файлів, MegaLinter leftovers, `on.*.paths` через `git ls-files :(glob)`,
15
+ * і локальний `shellcheck`.
16
16
  */
17
17
  import { existsSync } from 'node:fs'
18
18
  import { readdir, readFile } from 'node:fs/promises'
@@ -30,8 +30,6 @@ const MEGALINTER_USE_PATTERNS = [/oxsecurity\/megalinter-action/i, /megalinter\/
30
30
  /** Типові конфіги MegaLinter у корені репо */
31
31
  const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-linter.yaml']
32
32
 
33
- const N_CURSOR_LINT_GA_RE = /\bn-cursor\s+lint-ga\b/
34
-
35
33
  /** Обовʼязкові workflow-файли (ga.mdc). */
36
34
  const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']
37
35
 
@@ -179,92 +177,6 @@ async function checkMegalinter(wfDir, ymlWorkflows, passFn, failFn) {
179
177
  if (!found) passFn('Залишків MegaLinter не виявлено')
180
178
  }
181
179
 
182
- /**
183
- * Перевіряє zizmor конфіг.
184
- * @param {(msg: string) => void} passFn callback при успішній перевірці
185
- * @param {(msg: string) => void} failFn callback при помилці
186
- */
187
- async function checkZizmor(passFn, failFn) {
188
- const zizmorPath = '.github/zizmor.yml'
189
- if (!existsSync(zizmorPath)) {
190
- failFn(`Відсутній ${zizmorPath} — потрібен для zizmor (ga.mdc)`)
191
- return
192
- }
193
- const z = await readFile(zizmorPath, 'utf8')
194
- passFn(`${zizmorPath} існує`)
195
- if (z.includes('ref-pin')) {
196
- passFn(`${zizmorPath} містить політику ref-pin (zizmor)`)
197
- } else {
198
- failFn(`${zizmorPath}: додай policies ref-pin для unpinned-uses (ga.mdc)`)
199
- }
200
- }
201
-
202
- /**
203
- * Перевіряє `.vscode/settings.json`: oxfmt/oxc як default formatter для GitHub Actions workflow (мова
204
- * `github-actions-workflow` з розширення github.vscode-github-actions), узгоджено з oxc для yaml/workflow.
205
- * @param {(msg: string) => void} passFn callback при успішній перевірці
206
- * @param {(msg: string) => void} failFn callback при помилці
207
- */
208
- async function checkVscodeSettingsForGa(passFn, failFn) {
209
- const rel = '.vscode/settings.json'
210
- if (!existsSync(rel)) {
211
- failFn(`${rel} не існує — додай [github-actions-workflow].editor.defaultFormatter = oxc.oxc-vscode (ga.mdc)`)
212
- return
213
- }
214
- let settings
215
- try {
216
- settings = JSON.parse(await readFile(rel, 'utf8'))
217
- } catch {
218
- failFn(`${rel}: невалідний JSON (ga.mdc)`)
219
- return
220
- }
221
- if (!settings || typeof settings !== 'object') {
222
- failFn(`${rel}: очікується об’єкт налаштувань (ga.mdc)`)
223
- return
224
- }
225
- const block = /** @type {Record<string, unknown>} */ (settings)['[github-actions-workflow]']
226
- if (!block || typeof block !== 'object' || block === null || Array.isArray(block)) {
227
- failFn(`${rel}: додай "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } (ga.mdc)`)
228
- return
229
- }
230
- const df = String(/** @type {Record<string, unknown>} */ (block)['editor.defaultFormatter'] ?? '')
231
- if (df !== 'oxc.oxc-vscode') {
232
- failFn(
233
- `${rel}: [github-actions-workflow].editor.defaultFormatter має бути "oxc.oxc-vscode" (зараз: ${df || '∅'}) (ga.mdc)`
234
- )
235
- return
236
- }
237
- passFn(`${rel}: [github-actions-workflow] → oxc.oxc-vscode`)
238
- }
239
-
240
- /**
241
- * Перевіряє скрипт lint-ga в package.json.
242
- * @param {(msg: string) => void} passFn callback при успішній перевірці
243
- * @param {(msg: string) => void} failFn callback при помилці
244
- */
245
- async function checkLintGaScript(passFn, failFn) {
246
- if (!existsSync('package.json')) {
247
- failFn('package.json не існує — потрібен lint-ga у scripts')
248
- return
249
- }
250
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
251
- const lg = pkg.scripts?.['lint-ga']
252
- if (typeof lg !== 'string') {
253
- failFn('package.json: додай скрипт "lint-ga" (ga.mdc)')
254
- return
255
- }
256
- passFn('package.json містить lint-ga')
257
- // Канонічний скрипт делегує виконання CLI `n-cursor lint-ga` (bin з `@nitra/cursor`) — там preflight
258
- // на shellcheck + послідовно `bunx github-actionlint` і `uvx zizmor --offline --collect=workflows .`.
259
- // Виклик через bin-ім’я `n-cursor`, а не `npx --no @nitra/cursor`, бо `bun run` транслює `npx` у `bun x`,
260
- // а `bun x @nitra/cursor` для скоупованого пакету з одним bin-ім’ям повертає 0 без виконання.
261
- if (N_CURSOR_LINT_GA_RE.test(lg)) {
262
- passFn('lint-ga делегує CLI n-cursor lint-ga (preflight shellcheck + actionlint + zizmor)')
263
- } else {
264
- failFn('lint-ga має бути "n-cursor lint-ga" — CLI робить preflight shellcheck перед actionlint/zizmor (ga.mdc)')
265
- }
266
- }
267
-
268
180
  /**
269
181
  * Перевіряє наявність локального `shellcheck` у PATH. `actionlint` (`bunx github-actionlint`)
270
182
  * запускає shell-перевірки в кроках `run:` workflow тільки коли `shellcheck` доступний; інакше
@@ -437,19 +349,6 @@ export async function check() {
437
349
  await checkApplyWorkflow(wfDir, files, 'apply-k8s.yml', '**/k8s/**/*.yaml', pass, fail)
438
350
  await checkApplyWorkflow(wfDir, files, 'apply-nats-consumer.yml', '**/consumer.yaml', pass, fail)
439
351
 
440
- if (existsSync('.vscode/extensions.json')) {
441
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
442
- if (ext.recommendations?.includes('github.vscode-github-actions')) {
443
- pass('extensions.json містить github.vscode-github-actions')
444
- } else {
445
- fail('extensions.json не містить github.vscode-github-actions')
446
- }
447
- } else {
448
- fail('.vscode/extensions.json не існує')
449
- }
450
-
451
- await checkVscodeSettingsForGa(pass, fail)
452
-
453
352
  await checkMegalinter(wfDir, ymlWorkflows, pass, fail)
454
353
 
455
354
  // git-залежна перевірка `on.push.paths` glob-ів (вимагає `git ls-files`) —
@@ -462,8 +361,6 @@ export async function check() {
462
361
  }
463
362
  }
464
363
 
465
- await checkZizmor(pass, fail)
466
- await checkLintGaScript(pass, fail)
467
364
  checkShellcheckInstalled(pass, fail)
468
365
 
469
366
  return reporter.getExitCode()
@@ -0,0 +1,24 @@
1
+ # Перевірка кореневого `package.json` для GitHub Actions tooling (ga.mdc).
2
+ #
3
+ # Структурні workflow-перевірки живуть у `ga.workflow_common` і per-workflow
4
+ # policy-пакетах. JS лишається для PATH-preflight (`shellcheck`) і git-залежної
5
+ # перевірки `on.*.paths` через `git ls-files`.
6
+ #
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
+ package ga.package_json
11
+
12
+ import rego.v1
13
+
14
+ deny contains msg if {
15
+ not is_string(object.get(object.get(input, "scripts", {}), "lint-ga", null))
16
+ msg := "package.json: додай скрипт \"lint-ga\" (ga.mdc)"
17
+ }
18
+
19
+ deny contains msg if {
20
+ lint_ga := object.get(object.get(input, "scripts", {}), "lint-ga", "")
21
+ is_string(lint_ga)
22
+ not regex.match(`\bn-cursor\s+lint-ga\b`, lint_ga)
23
+ msg := "lint-ga має делегувати CLI `n-cursor lint-ga` (ga.mdc)"
24
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": "package.json",
5
+ "required": true
6
+ },
7
+ "missingMessage": "package.json не існує — потрібен lint-ga у scripts (ga.mdc)"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": ".vscode/extensions.json",
5
+ "required": true
6
+ },
7
+ "missingMessage": ".vscode/extensions.json не існує — додай github.vscode-github-actions (ga.mdc)"
8
+ }
@@ -0,0 +1,16 @@
1
+ # Перевірка `.vscode/extensions.json` для GitHub Actions (ga.mdc).
2
+ #
3
+ # Canonical: у `recommendations` має бути `github.vscode-github-actions`.
4
+ # Додаткові рекомендації від інших правил дозволені.
5
+ #
6
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
7
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
8
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
9
+ package ga.vscode_extensions
10
+
11
+ import rego.v1
12
+
13
+ deny contains msg if {
14
+ not "github.vscode-github-actions" in {r | some r in object.get(input, "recommendations", [])}
15
+ msg := ".vscode/extensions.json: recommendations має містити \"github.vscode-github-actions\" (ga.mdc)"
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": ".vscode/settings.json",
5
+ "required": true
6
+ },
7
+ "missingMessage": ".vscode/settings.json не існує — додай [github-actions-workflow].editor.defaultFormatter (ga.mdc)"
8
+ }
@@ -0,0 +1,24 @@
1
+ # Перевірка `.vscode/settings.json` для GitHub Actions workflow (ga.mdc).
2
+ #
3
+ # Мова `github-actions-workflow` має форматуватись через `oxc.oxc-vscode`,
4
+ # узгоджено з oxc для YAML/workflow.
5
+ #
6
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
7
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
8
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
9
+ package ga.vscode_settings
10
+
11
+ import rego.v1
12
+
13
+ deny contains msg if {
14
+ block := object.get(input, "[github-actions-workflow]", null)
15
+ not is_object(block)
16
+ msg := ".vscode/settings.json: додай \"[github-actions-workflow]\": { \"editor.defaultFormatter\": \"oxc.oxc-vscode\" } (ga.mdc)"
17
+ }
18
+
19
+ deny contains msg if {
20
+ block := object.get(input, "[github-actions-workflow]", null)
21
+ is_object(block)
22
+ object.get(block, "editor.defaultFormatter", null) != "oxc.oxc-vscode"
23
+ msg := ".vscode/settings.json: [github-actions-workflow].editor.defaultFormatter має бути \"oxc.oxc-vscode\" (ga.mdc)"
24
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": ".github/zizmor.yml",
5
+ "required": true
6
+ },
7
+ "missingMessage": ".github/zizmor.yml не існує — потрібен для zizmor (ga.mdc)"
8
+ }
@@ -0,0 +1,17 @@
1
+ # Перевірка `.github/zizmor.yml` для GitHub Actions (ga.mdc).
2
+ #
3
+ # JS раніше перевіряв сирий текст на `ref-pin`; у policy це робиться по
4
+ # JSON-представленню розпарсеного YAML-документа. Коментарі не враховуються,
5
+ # тож збіг має бути у фактичній конфігурації.
6
+ #
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
+ package ga.zizmor_yml
11
+
12
+ import rego.v1
13
+
14
+ deny contains msg if {
15
+ not contains(json.marshal(input), "ref-pin")
16
+ msg := ".github/zizmor.yml: додай policies ref-pin для unpinned-uses (ga.mdc)"
17
+ }
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * Перевіряє лінт JavaScript за правилом js-lint.mdc.
3
3
  *
4
- * Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
4
+ * Flat ESLint з getConfig і ignore для auto-imports,
5
5
  * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
6
6
  * plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
7
- * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.9.2** 3.8.0 правило
8
- * `no-restricted-syntax` для `ForInStatement` забороняє `for...in`; з 3.9.2 у `getConfig` вбудовано
9
- * ignore для ADR-каталогів — локально цей glob додавати не потрібно; також тягне транзитивний
10
- * `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
11
- * `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
12
- * `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
7
+ * globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
8
+ * і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
9
+ *
10
+ * Per-document вимоги (`lint-js`, `@nitra/eslint-config`, root `engines`, `.jscpd.json`,
11
+ * `.vscode/extensions.json`, `lint-js.yml`) у policy-пакетах `js_lint.*`.
13
12
  */
14
13
  import { existsSync } from 'node:fs'
15
14
  import { copyFile, readFile } from 'node:fs/promises'
@@ -42,9 +41,6 @@ export const KNIP_CANONICAL_JSON_PATH = join(
42
41
  'knip-canonical.json'
43
42
  )
44
43
 
45
- /** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
46
- export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
47
-
48
44
  const NON_DIGITS_RE = /\D+/u
49
45
 
50
46
  // Канонічний рядок `lint-js`-скрипта і мінімальна версія `@nitra/eslint-config` —
@@ -353,36 +349,6 @@ async function checkOxlintRc(passFn, failFn) {
353
349
  }
354
350
  }
355
351
 
356
- /**
357
- * Перевіряє .vscode/extensions.json на потрібні розширення.
358
- * @param {(msg: string) => void} passFn callback при успішній перевірці
359
- * @param {(msg: string) => void} failFn callback при помилці
360
- */
361
- async function checkVscodeExtensions(passFn, failFn) {
362
- if (!existsSync('.vscode/extensions.json')) {
363
- failFn('.vscode/extensions.json не існує — додай recommendations з js-lint.mdc (див. check-js-lint.mjs)')
364
- return
365
- }
366
- let ext
367
- try {
368
- ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
369
- } catch {
370
- failFn('.vscode/extensions.json не є валідним JSON')
371
- return
372
- }
373
- const rec = ext.recommendations
374
- if (!Array.isArray(rec)) {
375
- failFn('.vscode/extensions.json: поле recommendations має бути масивом')
376
- return
377
- }
378
- const missing = REQUIRED_VSCODE_EXTENSIONS.filter(id => !rec.includes(id))
379
- if (missing.length > 0) {
380
- failFn(`.vscode/extensions.json: додай у recommendations: ${missing.join(', ')} (мінімум для js-lint.mdc)`)
381
- } else {
382
- passFn('.vscode/extensions.json: є рекомендації oxlint, eslint і GitHub Actions')
383
- }
384
- }
385
-
386
352
  /**
387
353
  * FS-existence для `lint-js.yml` + cross-file перевірка, що `lint.yml` (якщо існує)
388
354
  * не дублює лінт JS-кроки. Структуру `lint-js.yml` (`actions/checkout@v6`,
@@ -408,47 +374,6 @@ async function checkLintJsWorkflows(passFn, failFn) {
408
374
  }
409
375
  }
410
376
 
411
- /**
412
- * Перевіряє .jscpd.json.
413
- * @param {(msg: string) => void} passFn callback при успішній перевірці
414
- * @param {(msg: string) => void} failFn callback при помилці
415
- */
416
- async function checkJscpdConfig(passFn, failFn) {
417
- if (!existsSync('.jscpd.json')) {
418
- failFn('.jscpd.json не існує — створи з полями згідно check js-lint')
419
- return
420
- }
421
- let jscpdCfg
422
- try {
423
- jscpdCfg = JSON.parse(await readFile('.jscpd.json', 'utf8'))
424
- } catch {
425
- failFn('.jscpd.json не є валідним JSON')
426
- return
427
- }
428
- passFn('.jscpd.json існує')
429
- if (jscpdCfg.gitignore === true) {
430
- passFn('.jscpd.json: gitignore увімкнено')
431
- } else {
432
- failFn('.jscpd.json має містити "gitignore": true')
433
- }
434
- if (jscpdCfg.exitCode === 1) {
435
- passFn('.jscpd.json: exitCode 1 при дублікатах')
436
- } else {
437
- failFn('.jscpd.json має містити "exitCode": 1 (інакше CI не впаде на клонах)')
438
- }
439
- if (Array.isArray(jscpdCfg.reporters) && jscpdCfg.reporters.includes('console')) {
440
- passFn('.jscpd.json: reporters містить console')
441
- } else {
442
- failFn('.jscpd.json має містити "reporters": ["console"] (або масив із "console")')
443
- }
444
- const minLines = jscpdCfg.minLines
445
- if (typeof minLines === 'number' && minLines >= 25) {
446
- passFn(`.jscpd.json: minLines ${minLines} (>=25)`)
447
- } else {
448
- failFn('.jscpd.json має містити "minLines" як число >= 25')
449
- }
450
- }
451
-
452
377
  /**
453
378
  * Перевіряє наявність `knip.json` у корені проєкту. Якщо файл відсутній —
454
379
  * копіює канонічний `knip-canonical.json` з пакета `@nitra/cursor` як стартовий
@@ -485,9 +410,7 @@ export async function check() {
485
410
  await checkEslintConfig(pass, fail)
486
411
  await checkPackageJsonJsLint(pass, fail)
487
412
  await checkOxlintRc(pass, fail)
488
- await checkVscodeExtensions(pass, fail)
489
413
  await checkLintJsWorkflows(pass, fail)
490
- await checkJscpdConfig(pass, fail)
491
414
  await checkKnipConfig(pass, fail)
492
415
 
493
416
  for (const dup of ['.eslintrc', '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml']) {
@@ -0,0 +1,38 @@
1
+ # Перевірка `.jscpd.json` для js-lint (js-lint.mdc).
2
+ #
3
+ # JS-частина лишається для FS/cross-file: наявність workflow, flat ESLint config,
4
+ # `.oxlintrc.json` проти embedded canonical snapshot, `knip.json` autofill.
5
+ # Цей пакет покриває лише структуру одного JSON-документа.
6
+ #
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
+ package js_lint.jscpd
11
+
12
+ import rego.v1
13
+
14
+ deny contains msg if {
15
+ input.gitignore != true
16
+ msg := ".jscpd.json має містити \"gitignore\": true (js-lint.mdc)"
17
+ }
18
+
19
+ deny contains msg if {
20
+ input.exitCode != 1
21
+ msg := ".jscpd.json має містити \"exitCode\": 1 (інакше CI не впаде на клонах) (js-lint.mdc)"
22
+ }
23
+
24
+ deny contains msg if {
25
+ not "console" in {r | some r in object.get(input, "reporters", [])}
26
+ msg := ".jscpd.json має містити \"reporters\": [\"console\"] або масив із \"console\" (js-lint.mdc)"
27
+ }
28
+
29
+ deny contains msg if {
30
+ not is_number(object.get(input, "minLines", null))
31
+ msg := ".jscpd.json має містити \"minLines\" як число >= 25 (js-lint.mdc)"
32
+ }
33
+
34
+ deny contains msg if {
35
+ is_number(input.minLines)
36
+ input.minLines < 25
37
+ msg := ".jscpd.json має містити \"minLines\" як число >= 25 (js-lint.mdc)"
38
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": ".jscpd.json",
5
+ "required": true
6
+ },
7
+ "missingMessage": ".jscpd.json не існує — створи з полями згідно js-lint.mdc"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": {
4
+ "single": ".vscode/extensions.json",
5
+ "required": true
6
+ },
7
+ "missingMessage": ".vscode/extensions.json не існує — додай recommendations з js-lint.mdc"
8
+ }
@@ -0,0 +1,25 @@
1
+ # Перевірка `.vscode/extensions.json` для js-lint (js-lint.mdc).
2
+ #
3
+ # Canonical: у `recommendations` мають бути ESLint, GitHub Actions і Oxlint.
4
+ # Канон задає мінімум — додаткові рекомендації від інших правил дозволені.
5
+ #
6
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
7
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
8
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
9
+ package js_lint.vscode_extensions
10
+
11
+ import rego.v1
12
+
13
+ required_extensions := {
14
+ "dbaeumer.vscode-eslint",
15
+ "github.vscode-github-actions",
16
+ "oxc.oxc-vscode",
17
+ }
18
+
19
+ recommendations_set := {r | some r in object.get(input, "recommendations", [])}
20
+
21
+ deny contains msg if {
22
+ some required in required_extensions
23
+ not required in recommendations_set
24
+ msg := sprintf(".vscode/extensions.json: recommendations має містити %q (js-lint.mdc)", [required])
25
+ }