@nitra/cursor 1.9.12 → 1.9.16

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 (25) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/mdc/text.mdc +3 -0
  3. package/package.json +1 -1
  4. package/policy/bun/package_json/package_json_test.rego +109 -0
  5. package/policy/graphql/vscode_extensions/vscode_extensions.rego +20 -0
  6. package/policy/graphql/vscode_extensions/vscode_extensions_test.rego +34 -0
  7. package/policy/image_avif/package_json/package_json.rego +61 -0
  8. package/policy/image_avif/package_json/package_json_test.rego +69 -0
  9. package/policy/js_run/jsconfig/jsconfig_test.rego +88 -0
  10. package/policy/nginx_default_tpl/vscode_extensions/vscode_extensions.rego +16 -0
  11. package/policy/nginx_default_tpl/vscode_extensions/vscode_extensions_test.rego +30 -0
  12. package/policy/nginx_default_tpl/vscode_settings/vscode_settings.rego +36 -0
  13. package/policy/nginx_default_tpl/vscode_settings/vscode_settings_test.rego +53 -0
  14. package/policy/style_lint/vscode_extensions/vscode_extensions.rego +23 -0
  15. package/policy/style_lint/vscode_extensions/vscode_extensions_test.rego +39 -0
  16. package/policy/style_lint/vscode_settings/vscode_settings.rego +24 -0
  17. package/policy/style_lint/vscode_settings/vscode_settings_test.rego +49 -0
  18. package/policy/text/markdownlint/markdownlint.rego +49 -0
  19. package/policy/text/markdownlint/markdownlint_test.rego +98 -0
  20. package/scripts/check-bun.mjs +3 -11
  21. package/scripts/check-graphql.mjs +18 -26
  22. package/scripts/check-js-run.mjs +18 -7
  23. package/scripts/check-nginx-default-tpl.mjs +28 -18
  24. package/scripts/check-style-lint.mjs +11 -33
  25. package/scripts/lint-conftest.mjs +25 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,66 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.9.16] - 2026-05-13
8
+
9
+ ### Added
10
+
11
+ - **5 нових rego-полісі для `.vscode/extensions.json` / `.vscode/settings.json`** (мігровано канон з .mdc у rego, прибрано JS-дублі):
12
+ - `style_lint.vscode_extensions` — `recommendations` має містити `stylelint.vscode-stylelint` (style-lint.mdc).
13
+ - `style_lint.vscode_settings` — `css.validate` / `scss.validate` / `less.validate: false`; `editor.codeActionsOnSave` свідомо не enforced (smell-test, мдс показує як рекомендацію).
14
+ - `graphql.vscode_extensions` — `recommendations` має містити `graphql.vscode-graphql` (graphql.mdc). НЕ реєструється глобально у `lint-conftest` TARGETS — правило conditional на наявність `gql\`…\``у джерелах; викликається з`check-graphql.mjs`через`runConftestBatch` після gql-scan.
15
+ - `nginx_default_tpl.vscode_extensions` — `recommendations` має містити `ahmadalli.vscode-nginx-conf` (nginx-default-tpl.mdc).
16
+ - `nginx_default_tpl.vscode_settings` — `editor.formatOnSave: true` і `[nginx].editor.defaultFormatter: "ahmadalli.vscode-nginx-conf"`. Обидва nginx-полісі викликаються з `check-nginx-default-tpl.mjs` через `runConftestBatch` лише після виявлення `default.conf.template`.
17
+ - **28 нових тестів** до пʼяти полісі: 5 (style_lint.vscode_extensions) + 6 (style_lint.vscode_settings) + 5 (graphql.vscode_extensions) + 5 (nginx_default_tpl.vscode_extensions) + 7 (nginx_default_tpl.vscode_settings). `conftest verify` — **234/234 pass** (+28).
18
+
19
+ ### Removed
20
+
21
+ - **`check-style-lint.mjs::checkVscodeStylelint`** — функція повністю видалена; зміст delegated у `style_lint.vscode_extensions` і `style_lint.vscode_settings`. JSDoc-преамбулу оновлено.
22
+ - **`check-graphql.mjs::checkExtensionsRecommendation` — JS-копія тіла перевірки видалена:** функція тепер є тонкою обгорткою над `runConftestBatch`, делегує `graphql.vscode_extensions`. Зник дубль JSON-парсингу й порівняння `recommendations`.
23
+ - **`check-nginx-default-tpl.mjs::checkVscodeNginx` — JS-копія тіла перевірки видалена:** функція тепер делегує `nginx_default_tpl.vscode_extensions` і `nginx_default_tpl.vscode_settings` через `runConftestBatch`. Зник дубль перевірок `editor.formatOnSave` і `[nginx].editor.defaultFormatter` у JS.
24
+
25
+ ### Changed
26
+
27
+ - **`lint-conftest.mjs` TARGETS — два нові глобальні entry для style-lint:** `style_lint.vscode_extensions` (`single: .vscode/extensions.json`) і `style_lint.vscode_settings` (`single: .vscode/settings.json`), обидва з `rule: 'style-lint'`. Не-style-lint проєкти не зачіпають (filter по `activeRules` з `.n-cursor.json`).
28
+ - **graphql/nginx — НЕ реєструються глобально у `lint-conftest`:** правила conditional на per-package умовах, які lint-conftest не вміє виразити (`gql\`…\``у джерелах для graphql; наявність`default.conf.template`для nginx). Plan B: rego-authoritative + JS-orchestrator з`runConftestBatch`.
29
+
30
+ ## [1.9.15] - 2026-05-13
31
+
32
+ ### Added
33
+
34
+ - **`npm/policy/js_run/jsconfig/jsconfig_test.rego` — 12 нових тестів для канону `jsconfig.json`:** rego-полісі `js_run.jsconfig` (canonical compilerOptions — `lib: ["esnext"]`, `module/moduleResolution: NodeNext`, `target: esnext`, `checkJs: false`, `include: ["src/**/*"]`) існувала, але не мала тестів і не запускалась на реальних файлах. Додано happy path + 11 негативних кейсів через `json.patch`-фікстури.
35
+ - **`npm/policy/image_avif/package_json/` — структурна валідація опт-аут конфігу:** новий rego-пакет `image_avif.package_json` з 3 deny-правилами для `package.json`: значення `"@nitra/minify-image"` має бути обʼєктом (якщо присутнє), `disable-avif` має бути boolean (якщо присутнє), захист від typo `disabled-avif`. Поле опційне — більшість проєктів його не мають, deny спрацьовує лише на нелегітимну форму (typo або wrong type, що тихо ламає опт-аут). +11 тестів. Зареєстровано у `lint-conftest.mjs` TARGETS з `walk` по всіх `package.json` (з фільтром `rule: "image-avif"`).
36
+
37
+ ### Changed
38
+
39
+ - **`check-js-run.mjs::checkBackendJsconfigWhenSrcPresent` — структуру `jsconfig.json` тепер валідує rego через `runConftestBatch`:** замість FS-existence-only + посилання на `lint-conftest` (яке насправді не запускалось — rego не була зареєстрована глобально), JS тепер викликає rego-пакет `js_run.jsconfig` через `runConftestBatch` після того, як визначить, що пакет — backend (без `vite` у `devDependencies`) з каталогом `src/`. Це Plan B: Rego-authoritative + JS-orchestrator. Глобальна реєстрація `js_run.jsconfig` у `lint-conftest.mjs` свідомо не додавалась — rule стосується лише workspace-пакетів певної форми, що lint-conftest filter (`activeRules` на рівні репо) не вміє виразити.
40
+
41
+ ### Not done (Phase 1.5 — пізніше)
42
+
43
+ - **`rego.mdc`, `tauri.mdc`** — rego-полісі для канонічних `.vscode/extensions.json` / `.vscode/settings.json` потрібен JS-orchestrator. Ці правила conditional (rego — glob `**/*.rego`, tauri — лише Tauri-проєкти), тож запускати rego безумовно на кожний `.vscode/extensions.json` дало б false-positive порушення для всіх не-rego/не-tauri проєктів. Чисте розширення rego-полісі без `check-<rule>.mjs`-orchestrator-а тут не закриває правило.
44
+
45
+ ## [1.9.14] - 2026-05-13
46
+
47
+ ### Added
48
+
49
+ - **`text.markdownlint` rego — повний канон `.markdownlint-cli2.jsonc` тепер виноситься як deny:** раніше rego-полісі мала **рівно один** deny (`gitignore == true`), а решта канонічного блока з [text.mdc](mdc/text.mdc) (`config.default == true`, `MD013 == false`, `MD024.siblings_only == true`, `MD029 == false`, `MD040 == false`, `MD041 == false`) була показана як приклад, але не перевірялась. Додано 6 нових deny-правил, що покривають кожне поле канону; додаткові поля верхнього рівня (`ignores`) і додаткові MD-rules (`MD033` тощо) дозволені — канон задає мінімум. Шаблон повідомлень — через `concat` для regal style/line-length.
50
+ - **`npm/policy/text/markdownlint/markdownlint_test.rego` — 14 нових тестів:** happy path (канонічний `.markdownlint-cli2.jsonc`), дозволені розширення (`ignores`, `MD033`), порушення для `gitignore` (відсутній / `false`), `config.default` (відсутній / `false`), `MD013/029/040/041` (`true` або відсутній), `MD024` (не object / `siblings_only: false` / відсутній). `conftest verify` — **183/183 pass** (+14). `lint-conftest` на реальному `.markdownlint-cli2.jsonc` репо: 5/5 (раніше було 1/1 — додано 4 нові тестові кейси проти реального файлу).
51
+
52
+ ## [1.9.13] - 2026-05-13
53
+
54
+ ### Removed
55
+
56
+ - **`check-bun.mjs::isAllowedRootDevDependency` — видалено JS-копію, дубль rego:** функція експортувалася лише для тестів, у `check()` не викликалась; логіка «дозволено лише `@nitra/*` у кореневих `devDependencies`» давно живе у `npm/policy/bun/package_json/package_json.rego` (`not startswith(name, "@nitra/")`). Docstring помилково посилався на `check-text.mjs`, який цю функцію не імпортує. Тепер єдине джерело — rego; у `check-bun.mjs` додано коментар з посиланням на полісі.
57
+
58
+ ### Added
59
+
60
+ - **`npm/policy/bun/package_json/package_json_test.rego` — rego-тести для bun.package_json:** 12 нових `test_*`-кейсів через `json.patch`-фікстури — happy path (без `devDependencies`, з кількома `@nitra/*`), 4 негативні `devDependencies` (`@cspell/dict-uk-ua`, `@cspell/cspell-lib`, `lodash`, `@types/node`), mixed-devDeps з конкретним повідомленням про `lodash`, заборона `packageManager`, заборона кореневих `dependencies` (порожній обʼєкт теж), агрегований `lint`-скрипт (відсутній / не покриває `bun run` / без `&& oxfmt .`). Покривається `bun run lint-rego` (169/169 pass).
61
+
62
+ ### Changed
63
+
64
+ - **`npm/tests/check-bun.test.mjs` — прибрано `describe('isAllowedRootDevDependency')`:** імпорт і блок тестів видалено; залишено інтеграційні `check-bun`-тести у тимчасових каталогах (FS / cross-file частина).
65
+ - **Аудит інших `check-*.mjs`**: пройдено всі 22 скрипти на наявність дубля з rego (export не викликається внутрішньо + наявне `npm/policy/<rule>/<name>/<name>.rego`). Знайдено лише цей кейс; `httpRouteMatchesNginxDefaultTpl` у `check-nginx-default-tpl.mjs` залишається — не має rego-counterpart і свідомо лишений для майбутнього використання згідно docstring. Інші експорти або викликаються у відповідному `check()` / приватних helper-ах, або не мають rego-копії.
66
+
7
67
  ## [1.9.12] - 2026-05-13
8
68
 
9
69
  ### Removed
package/mdc/text.mdc CHANGED
@@ -144,6 +144,9 @@ version: '1.26'
144
144
  "config": {
145
145
  "default": true,
146
146
  "MD013": false,
147
+ "MD024": {
148
+ "siblings_only": true
149
+ },
147
150
  "MD029": false,
148
151
  "MD040": false,
149
152
  "MD041": false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.9.12",
3
+ "version": "1.9.16",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,109 @@
1
+ # Тести для `bun.package_json`. Запуск:
2
+ # conftest verify -p npm/policy/bun/package_json
3
+ package bun.package_json_test
4
+
5
+ import rego.v1
6
+
7
+ import data.bun.package_json
8
+
9
+ valid_pkg := {
10
+ "name": "n-cursor",
11
+ "devDependencies": {"@nitra/eslint-config": "^3.9.2"},
12
+ }
13
+
14
+ # ── happy path ────────────────────────────────────────────────────────────
15
+
16
+ test_allow_minimal if {
17
+ count(package_json.deny) == 0 with input as valid_pkg
18
+ }
19
+
20
+ test_allow_multiple_nitra_deps if {
21
+ pkg := json.patch(valid_pkg, [{
22
+ "op": "replace",
23
+ "path": "/devDependencies",
24
+ "value": {"@nitra/eslint-config": "^3.9.2", "@nitra/cspell-dict": "^2.0.0", "@nitra/stylelint-config": "^1.0.0"},
25
+ }])
26
+ count(package_json.deny) == 0 with input as pkg
27
+ }
28
+
29
+ test_allow_no_dev_dependencies if {
30
+ pkg := json.patch(valid_pkg, [{"op": "remove", "path": "/devDependencies"}])
31
+ count(package_json.deny) == 0 with input as pkg
32
+ }
33
+
34
+ # ── deny: devDependencies лише @nitra/* ──────────────────────────────────
35
+
36
+ test_deny_non_nitra_devdep if {
37
+ cases := [
38
+ {"@cspell/dict-uk-ua": "^2.0.0"},
39
+ {"@cspell/cspell-lib": "^9.0.0"},
40
+ {"lodash": "*"},
41
+ {"@types/node": "^24.0.0"},
42
+ ]
43
+ some bad in cases
44
+ pkg := json.patch(valid_pkg, [{"op": "replace", "path": "/devDependencies", "value": bad}])
45
+ count(package_json.deny) > 0 with input as pkg
46
+ }
47
+
48
+ test_deny_mixed_dev_deps_only_flags_non_nitra if {
49
+ pkg := json.patch(valid_pkg, [{
50
+ "op": "replace",
51
+ "path": "/devDependencies",
52
+ "value": {"@nitra/eslint-config": "^3.9.2", "lodash": "*"},
53
+ }])
54
+ some msg in package_json.deny with input as pkg
55
+ contains(msg, "lodash")
56
+ }
57
+
58
+ # ── deny: packageManager ─────────────────────────────────────────────────
59
+
60
+ test_deny_package_manager_field if {
61
+ pkg := json.patch(valid_pkg, [{"op": "add", "path": "/packageManager", "value": "pnpm@9.0.0"}])
62
+ count(package_json.deny) > 0 with input as pkg
63
+ }
64
+
65
+ # ── deny: dependencies у кореневому ──────────────────────────────────────
66
+
67
+ test_deny_root_dependencies_present if {
68
+ pkg := json.patch(valid_pkg, [{"op": "add", "path": "/dependencies", "value": {"lodash": "*"}}])
69
+ count(package_json.deny) > 0 with input as pkg
70
+ }
71
+
72
+ test_deny_empty_dependencies_object if {
73
+ pkg := json.patch(valid_pkg, [{"op": "add", "path": "/dependencies", "value": {}}])
74
+ count(package_json.deny) > 0 with input as pkg
75
+ }
76
+
77
+ # ── deny: агрегований lint ───────────────────────────────────────────────
78
+
79
+ test_deny_lint_prefixed_without_aggregate if {
80
+ pkg := json.patch(valid_pkg, [{"op": "add", "path": "/scripts", "value": {"lint-js": "echo"}}])
81
+ count(package_json.deny) > 0 with input as pkg
82
+ }
83
+
84
+ test_allow_lint_aggregate_calls_subscript_and_oxfmt if {
85
+ pkg := json.patch(valid_pkg, [{
86
+ "op": "add",
87
+ "path": "/scripts",
88
+ "value": {"lint-js": "echo", "lint": "bun run lint-js && oxfmt ."},
89
+ }])
90
+ count(package_json.deny) == 0 with input as pkg
91
+ }
92
+
93
+ test_deny_lint_aggregate_missing_oxfmt if {
94
+ pkg := json.patch(valid_pkg, [{
95
+ "op": "add",
96
+ "path": "/scripts",
97
+ "value": {"lint-js": "echo", "lint": "bun run lint-js"},
98
+ }])
99
+ count(package_json.deny) > 0 with input as pkg
100
+ }
101
+
102
+ test_deny_lint_aggregate_missing_subscript_via_bun_run if {
103
+ pkg := json.patch(valid_pkg, [{
104
+ "op": "add",
105
+ "path": "/scripts",
106
+ "value": {"lint-js": "echo", "lint-text": "echo", "lint": "bun run lint-js && oxfmt ."},
107
+ }])
108
+ count(package_json.deny) > 0 with input as pkg
109
+ }
@@ -0,0 +1,20 @@
1
+ # Перевірка `.vscode/extensions.json` для graphql (graphql.mdc).
2
+ #
3
+ # Викликається з `check-graphql.mjs` через `runConftestBatch` лише ПІСЛЯ того,
4
+ # як JS виявив `gql\`…\`` tagged template literal у джерелах (умовне правило).
5
+ # Тому в `lint-conftest.mjs` TARGETS глобально не реєструється — інакше були б
6
+ # false-positive порушення на проєктах без gql.
7
+ #
8
+ # Canonical (graphql.mdc):
9
+ # { "recommendations": ["graphql.vscode-graphql"] }
10
+ #
11
+ # Канон задає мінімум; інші записи (від markdownlint/oxc/...) дозволені.
12
+ package graphql.vscode_extensions
13
+
14
+ import rego.v1
15
+
16
+ deny contains msg if {
17
+ recs := object.get(input, "recommendations", [])
18
+ not "graphql.vscode-graphql" in {r | some r in recs}
19
+ msg := ".vscode/extensions.json: додай у recommendations \"graphql.vscode-graphql\" (graphql.mdc)"
20
+ }
@@ -0,0 +1,34 @@
1
+ # Тести для `graphql.vscode_extensions`. Запуск:
2
+ # conftest verify -p npm/policy/graphql/vscode_extensions
3
+ package graphql.vscode_extensions_test
4
+
5
+ import rego.v1
6
+
7
+ import data.graphql.vscode_extensions
8
+
9
+ test_allow_with_required_extension if {
10
+ cfg := {"recommendations": ["graphql.vscode-graphql"]}
11
+ count(vscode_extensions.deny) == 0 with input as cfg
12
+ }
13
+
14
+ test_allow_with_additional_extensions if {
15
+ cfg := {"recommendations": [
16
+ "dbaeumer.vscode-eslint",
17
+ "graphql.vscode-graphql",
18
+ "oxc.oxc-vscode",
19
+ ]}
20
+ count(vscode_extensions.deny) == 0 with input as cfg
21
+ }
22
+
23
+ test_deny_missing_extension if {
24
+ cfg := {"recommendations": ["dbaeumer.vscode-eslint"]}
25
+ count(vscode_extensions.deny) > 0 with input as cfg
26
+ }
27
+
28
+ test_deny_empty_recommendations if {
29
+ count(vscode_extensions.deny) > 0 with input as {"recommendations": []}
30
+ }
31
+
32
+ test_deny_no_recommendations_field if {
33
+ count(vscode_extensions.deny) > 0 with input as {}
34
+ }
@@ -0,0 +1,61 @@
1
+ # Структурна перевірка опт-аут конфігу для image-avif у `package.json` (image-avif.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test <pkg>/package.json -p npm/policy/image_avif/package_json \
5
+ # --namespace image_avif.package_json
6
+ #
7
+ # Канонічна форма опт-ауту з image-avif.mdc:
8
+ # { "@nitra/minify-image": { "disable-avif": true } }
9
+ #
10
+ # Поле опційне — більшість проєктів його не мають. Полісі deny лише, якщо поле
11
+ # присутнє, але має нелегітимну форму: типовий typo (`disabled-avif`) або
12
+ # неправильний тип (`"disable-avif": "yes"`). Без цієї перевірки помилкове
13
+ # написання тихо повертає AVIF-генерацію всередину пакета, де її хотіли вимкнути.
14
+ #
15
+ # FS / behavior (запуск `npx @nitra/minify-image`, walk `.vue`, видалення AVIF-сиріт) — у JS.
16
+ #
17
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
18
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
19
+ package image_avif.package_json
20
+
21
+ import rego.v1
22
+
23
+ minify_image_field := "@nitra/minify-image"
24
+
25
+ # ── deny: значення поля має бути обʼєктом, якщо присутнє ──────────────────
26
+
27
+ deny contains msg if {
28
+ value := object.get(input, minify_image_field, null)
29
+ value != null
30
+ not is_object(value)
31
+ msg := sprintf(
32
+ "package.json: поле \"@nitra/minify-image\" має бути обʼєктом (зараз: %v) (image-avif.mdc)",
33
+ [value],
34
+ )
35
+ }
36
+
37
+ # ── deny: відомі ключі мають правильний тип ──────────────────────────────
38
+
39
+ deny contains msg if {
40
+ cfg := object.get(input, minify_image_field, {})
41
+ is_object(cfg)
42
+ value := object.get(cfg, "disable-avif", null)
43
+ value != null
44
+ not is_boolean(value)
45
+ msg := sprintf(
46
+ "package.json: \"@nitra/minify-image.disable-avif\" має бути boolean (зараз: %v) (image-avif.mdc)",
47
+ [value],
48
+ )
49
+ }
50
+
51
+ # ── deny: захист від typo `disabled-avif` ────────────────────────────────
52
+
53
+ deny contains msg if {
54
+ cfg := object.get(input, minify_image_field, {})
55
+ is_object(cfg)
56
+ "disabled-avif" in object.keys(cfg)
57
+ msg := concat(" ", [
58
+ "package.json: ключ \"@nitra/minify-image.disabled-avif\" виглядає як typo —",
59
+ "канонічна назва \"disable-avif\" (image-avif.mdc)",
60
+ ])
61
+ }
@@ -0,0 +1,69 @@
1
+ # Тести для `image_avif.package_json`. Запуск:
2
+ # conftest verify -p npm/policy/image_avif/package_json
3
+ package image_avif.package_json_test
4
+
5
+ import rego.v1
6
+
7
+ import data.image_avif.package_json
8
+
9
+ # ── happy path ────────────────────────────────────────────────────────────
10
+
11
+ test_allow_no_field if {
12
+ count(package_json.deny) == 0 with input as {"name": "x"}
13
+ }
14
+
15
+ test_allow_canonical_opt_out if {
16
+ pkg := {"name": "x", "@nitra/minify-image": {"disable-avif": true}}
17
+ count(package_json.deny) == 0 with input as pkg
18
+ }
19
+
20
+ test_allow_disable_avif_false if {
21
+ pkg := {"name": "x", "@nitra/minify-image": {"disable-avif": false}}
22
+ count(package_json.deny) == 0 with input as pkg
23
+ }
24
+
25
+ test_allow_empty_config if {
26
+ pkg := {"name": "x", "@nitra/minify-image": {}}
27
+ count(package_json.deny) == 0 with input as pkg
28
+ }
29
+
30
+ test_allow_other_keys_inside if {
31
+ pkg := {"name": "x", "@nitra/minify-image": {"disable-avif": true, "future-flag": "y"}}
32
+ count(package_json.deny) == 0 with input as pkg
33
+ }
34
+
35
+ # ── deny: тип поля ───────────────────────────────────────────────────────
36
+
37
+ test_deny_field_is_string if {
38
+ pkg := {"name": "x", "@nitra/minify-image": "disable-avif"}
39
+ count(package_json.deny) > 0 with input as pkg
40
+ }
41
+
42
+ test_deny_field_is_array if {
43
+ pkg := {"name": "x", "@nitra/minify-image": ["disable-avif"]}
44
+ count(package_json.deny) > 0 with input as pkg
45
+ }
46
+
47
+ test_deny_field_is_boolean if {
48
+ pkg := {"name": "x", "@nitra/minify-image": true}
49
+ count(package_json.deny) > 0 with input as pkg
50
+ }
51
+
52
+ # ── deny: тип disable-avif ──────────────────────────────────────────────
53
+
54
+ test_deny_disable_avif_string if {
55
+ pkg := {"name": "x", "@nitra/minify-image": {"disable-avif": "yes"}}
56
+ count(package_json.deny) > 0 with input as pkg
57
+ }
58
+
59
+ test_deny_disable_avif_number if {
60
+ pkg := {"name": "x", "@nitra/minify-image": {"disable-avif": 1}}
61
+ count(package_json.deny) > 0 with input as pkg
62
+ }
63
+
64
+ # ── deny: typo disabled-avif ────────────────────────────────────────────
65
+
66
+ test_deny_typo_disabled_avif if {
67
+ pkg := {"name": "x", "@nitra/minify-image": {"disabled-avif": true}}
68
+ count(package_json.deny) > 0 with input as pkg
69
+ }
@@ -0,0 +1,88 @@
1
+ # Тести для `js_run.jsconfig`. Запуск:
2
+ # conftest verify -p npm/policy/js_run/jsconfig
3
+ package js_run.jsconfig_test
4
+
5
+ import rego.v1
6
+
7
+ import data.js_run.jsconfig
8
+
9
+ valid_cfg := {
10
+ "compilerOptions": {
11
+ "lib": ["esnext"],
12
+ "module": "NodeNext",
13
+ "moduleResolution": "NodeNext",
14
+ "target": "esnext",
15
+ "checkJs": false,
16
+ },
17
+ "include": ["src/**/*"],
18
+ }
19
+
20
+ # ── happy path ────────────────────────────────────────────────────────────
21
+
22
+ test_allow_canonical if {
23
+ count(jsconfig.deny) == 0 with input as valid_cfg
24
+ }
25
+
26
+ # ── compilerOptions.lib ───────────────────────────────────────────────────
27
+
28
+ test_deny_lib_not_array if {
29
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/compilerOptions/lib", "value": "esnext"}])
30
+ count(jsconfig.deny) > 0 with input as cfg
31
+ }
32
+
33
+ test_deny_lib_wrong_value if {
34
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/compilerOptions/lib", "value": ["es2022"]}])
35
+ count(jsconfig.deny) > 0 with input as cfg
36
+ }
37
+
38
+ test_deny_lib_missing if {
39
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/compilerOptions/lib"}])
40
+ count(jsconfig.deny) > 0 with input as cfg
41
+ }
42
+
43
+ # ── compilerOptions.module / moduleResolution / target / checkJs ──────────
44
+
45
+ test_deny_module_not_nodenext if {
46
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/compilerOptions/module", "value": "esnext"}])
47
+ count(jsconfig.deny) > 0 with input as cfg
48
+ }
49
+
50
+ test_deny_module_resolution_not_nodenext if {
51
+ cfg := json.patch(
52
+ valid_cfg,
53
+ [{"op": "replace", "path": "/compilerOptions/moduleResolution", "value": "node"}],
54
+ )
55
+ count(jsconfig.deny) > 0 with input as cfg
56
+ }
57
+
58
+ test_deny_target_not_esnext if {
59
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/compilerOptions/target", "value": "es2022"}])
60
+ count(jsconfig.deny) > 0 with input as cfg
61
+ }
62
+
63
+ test_deny_check_js_true if {
64
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/compilerOptions/checkJs", "value": true}])
65
+ count(jsconfig.deny) > 0 with input as cfg
66
+ }
67
+
68
+ test_deny_check_js_missing if {
69
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/compilerOptions/checkJs"}])
70
+ count(jsconfig.deny) > 0 with input as cfg
71
+ }
72
+
73
+ # ── include ──────────────────────────────────────────────────────────────
74
+
75
+ test_deny_include_not_array if {
76
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/include", "value": "src/**/*"}])
77
+ count(jsconfig.deny) > 0 with input as cfg
78
+ }
79
+
80
+ test_deny_include_wrong_value if {
81
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/include", "value": ["lib/**/*"]}])
82
+ count(jsconfig.deny) > 0 with input as cfg
83
+ }
84
+
85
+ test_deny_include_missing if {
86
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/include"}])
87
+ count(jsconfig.deny) > 0 with input as cfg
88
+ }
@@ -0,0 +1,16 @@
1
+ # Перевірка `.vscode/extensions.json` для nginx-default-tpl (nginx-default-tpl.mdc).
2
+ #
3
+ # Викликається з `check-nginx-default-tpl.mjs` через `runConftestBatch` лише
4
+ # ПІСЛЯ того, як JS виявив `default.conf.template` у дереві (умовне правило).
5
+ # Глобально у `lint-conftest.mjs` TARGETS не реєструється.
6
+ #
7
+ # Canonical: `recommendations` має містити `ahmadalli.vscode-nginx-conf`.
8
+ package nginx_default_tpl.vscode_extensions
9
+
10
+ import rego.v1
11
+
12
+ deny contains msg if {
13
+ recs := object.get(input, "recommendations", [])
14
+ not "ahmadalli.vscode-nginx-conf" in {r | some r in recs}
15
+ msg := ".vscode/extensions.json: recommendations має містити \"ahmadalli.vscode-nginx-conf\" (nginx-default-tpl.mdc)"
16
+ }
@@ -0,0 +1,30 @@
1
+ # Тести для `nginx_default_tpl.vscode_extensions`. Запуск:
2
+ # conftest verify -p npm/policy/nginx_default_tpl/vscode_extensions
3
+ package nginx_default_tpl.vscode_extensions_test
4
+
5
+ import rego.v1
6
+
7
+ import data.nginx_default_tpl.vscode_extensions
8
+
9
+ test_allow_with_required_extension if {
10
+ cfg := {"recommendations": ["ahmadalli.vscode-nginx-conf"]}
11
+ count(vscode_extensions.deny) == 0 with input as cfg
12
+ }
13
+
14
+ test_allow_with_additional_extensions if {
15
+ cfg := {"recommendations": ["dbaeumer.vscode-eslint", "ahmadalli.vscode-nginx-conf"]}
16
+ count(vscode_extensions.deny) == 0 with input as cfg
17
+ }
18
+
19
+ test_deny_missing_extension if {
20
+ cfg := {"recommendations": ["dbaeumer.vscode-eslint"]}
21
+ count(vscode_extensions.deny) > 0 with input as cfg
22
+ }
23
+
24
+ test_deny_empty_recommendations if {
25
+ count(vscode_extensions.deny) > 0 with input as {"recommendations": []}
26
+ }
27
+
28
+ test_deny_no_recommendations_field if {
29
+ count(vscode_extensions.deny) > 0 with input as {}
30
+ }
@@ -0,0 +1,36 @@
1
+ # Перевірка `.vscode/settings.json` для nginx-default-tpl (nginx-default-tpl.mdc).
2
+ #
3
+ # Викликається з `check-nginx-default-tpl.mjs` через `runConftestBatch` лише
4
+ # ПІСЛЯ того, як JS виявив `default.conf.template`. Глобально у `lint-conftest`
5
+ # не реєструється.
6
+ #
7
+ # Canonical:
8
+ # { "editor.formatOnSave": true,
9
+ # "[nginx]": { "editor.defaultFormatter": "ahmadalli.vscode-nginx-conf" } }
10
+ package nginx_default_tpl.vscode_settings
11
+
12
+ import rego.v1
13
+
14
+ deny contains msg if {
15
+ object.get(input, "editor.formatOnSave", null) != true
16
+ msg := ".vscode/settings.json: \"editor.formatOnSave\" має бути true (nginx-default-tpl.mdc)"
17
+ }
18
+
19
+ deny contains msg if {
20
+ nginx_block := object.get(input, "[nginx]", {})
21
+ not is_object(nginx_block)
22
+ msg := concat(" ", [
23
+ ".vscode/settings.json: \"[nginx]\" має бути обʼєктом з",
24
+ "\"editor.defaultFormatter\": \"ahmadalli.vscode-nginx-conf\" (nginx-default-tpl.mdc)",
25
+ ])
26
+ }
27
+
28
+ deny contains msg if {
29
+ nginx_block := object.get(input, "[nginx]", {})
30
+ is_object(nginx_block)
31
+ object.get(nginx_block, "editor.defaultFormatter", null) != "ahmadalli.vscode-nginx-conf"
32
+ msg := concat(" ", [
33
+ ".vscode/settings.json: \"[nginx].editor.defaultFormatter\" має бути",
34
+ "\"ahmadalli.vscode-nginx-conf\" (nginx-default-tpl.mdc)",
35
+ ])
36
+ }
@@ -0,0 +1,53 @@
1
+ # Тести для `nginx_default_tpl.vscode_settings`. Запуск:
2
+ # conftest verify -p npm/policy/nginx_default_tpl/vscode_settings
3
+ package nginx_default_tpl.vscode_settings_test
4
+
5
+ import rego.v1
6
+
7
+ import data.nginx_default_tpl.vscode_settings
8
+
9
+ valid_cfg := {
10
+ "editor.formatOnSave": true,
11
+ "[nginx]": {"editor.defaultFormatter": "ahmadalli.vscode-nginx-conf"},
12
+ }
13
+
14
+ test_allow_canonical if {
15
+ count(vscode_settings.deny) == 0 with input as valid_cfg
16
+ }
17
+
18
+ test_allow_with_additional_keys if {
19
+ cfg := json.patch(valid_cfg, [{
20
+ "op": "add",
21
+ "path": "/[javascript]",
22
+ "value": {"editor.defaultFormatter": "oxc.oxc-vscode"},
23
+ }])
24
+ count(vscode_settings.deny) == 0 with input as cfg
25
+ }
26
+
27
+ test_deny_format_on_save_false if {
28
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/editor.formatOnSave", "value": false}])
29
+ count(vscode_settings.deny) > 0 with input as cfg
30
+ }
31
+
32
+ test_deny_format_on_save_missing if {
33
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/editor.formatOnSave"}])
34
+ count(vscode_settings.deny) > 0 with input as cfg
35
+ }
36
+
37
+ test_deny_nginx_block_missing if {
38
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/[nginx]"}])
39
+ count(vscode_settings.deny) > 0 with input as cfg
40
+ }
41
+
42
+ test_deny_nginx_block_wrong_type if {
43
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/[nginx]", "value": "ahmadalli.vscode-nginx-conf"}])
44
+ count(vscode_settings.deny) > 0 with input as cfg
45
+ }
46
+
47
+ test_deny_nginx_wrong_formatter if {
48
+ cfg := json.patch(
49
+ valid_cfg,
50
+ [{"op": "replace", "path": "/[nginx]/editor.defaultFormatter", "value": "ms-vscode.cpptools"}],
51
+ )
52
+ count(vscode_settings.deny) > 0 with input as cfg
53
+ }
@@ -0,0 +1,23 @@
1
+ # Перевірка `.vscode/extensions.json` для style-lint (style-lint.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test .vscode/extensions.json -p npm/policy/style_lint/vscode_extensions \
5
+ # --namespace style_lint.vscode_extensions
6
+ #
7
+ # Canonical (style-lint.mdc):
8
+ # { "recommendations": ["stylelint.vscode-stylelint"] }
9
+ #
10
+ # Канон задає мінімум — `recommendations` має МІСТИТИ `stylelint.vscode-stylelint`;
11
+ # додаткові записи (від інших правил — markdownlint, oxc тощо) дозволені.
12
+ #
13
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
14
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
15
+ package style_lint.vscode_extensions
16
+
17
+ import rego.v1
18
+
19
+ deny contains msg if {
20
+ recs := object.get(input, "recommendations", [])
21
+ not "stylelint.vscode-stylelint" in {r | some r in recs}
22
+ msg := ".vscode/extensions.json: recommendations має містити \"stylelint.vscode-stylelint\" (style-lint.mdc)"
23
+ }
@@ -0,0 +1,39 @@
1
+ # Тести для `style_lint.vscode_extensions`. Запуск:
2
+ # conftest verify -p npm/policy/style_lint/vscode_extensions
3
+ package style_lint.vscode_extensions_test
4
+
5
+ import rego.v1
6
+
7
+ import data.style_lint.vscode_extensions
8
+
9
+ # ── happy path ────────────────────────────────────────────────────────────
10
+
11
+ test_allow_with_required_extension if {
12
+ cfg := {"recommendations": ["stylelint.vscode-stylelint"]}
13
+ count(vscode_extensions.deny) == 0 with input as cfg
14
+ }
15
+
16
+ test_allow_with_additional_extensions if {
17
+ cfg := {"recommendations": [
18
+ "dbaeumer.vscode-eslint",
19
+ "stylelint.vscode-stylelint",
20
+ "oxc.oxc-vscode",
21
+ "DavidAnson.vscode-markdownlint",
22
+ ]}
23
+ count(vscode_extensions.deny) == 0 with input as cfg
24
+ }
25
+
26
+ # ── deny ──────────────────────────────────────────────────────────────────
27
+
28
+ test_deny_missing_extension if {
29
+ cfg := {"recommendations": ["dbaeumer.vscode-eslint"]}
30
+ count(vscode_extensions.deny) > 0 with input as cfg
31
+ }
32
+
33
+ test_deny_empty_recommendations if {
34
+ count(vscode_extensions.deny) > 0 with input as {"recommendations": []}
35
+ }
36
+
37
+ test_deny_no_recommendations_field if {
38
+ count(vscode_extensions.deny) > 0 with input as {}
39
+ }
@@ -0,0 +1,24 @@
1
+ # Перевірка `.vscode/settings.json` для style-lint (style-lint.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test .vscode/settings.json -p npm/policy/style_lint/vscode_settings \
5
+ # --namespace style_lint.vscode_settings
6
+ #
7
+ # Canonical (style-lint.mdc): вимкнути вбудовану валідацію CSS/SCSS/Less, щоб
8
+ # stylelint був єдиним джерелом діагностики.
9
+ # { "css.validate": false, "less.validate": false, "scss.validate": false }
10
+ #
11
+ # `editor.codeActionsOnSave` у каноні є, але це smell-test — навмисно не deny,
12
+ # щоб не падати на пакетах, які мають свій codeActionsOnSave-конфіг.
13
+ #
14
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
15
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
16
+ package style_lint.vscode_settings
17
+
18
+ import rego.v1
19
+
20
+ deny contains msg if {
21
+ some key in {"css.validate", "less.validate", "scss.validate"}
22
+ object.get(input, key, null) != false
23
+ msg := sprintf(".vscode/settings.json: \"%s\" має бути false (style-lint.mdc)", [key])
24
+ }
@@ -0,0 +1,49 @@
1
+ # Тести для `style_lint.vscode_settings`. Запуск:
2
+ # conftest verify -p npm/policy/style_lint/vscode_settings
3
+ package style_lint.vscode_settings_test
4
+
5
+ import rego.v1
6
+
7
+ import data.style_lint.vscode_settings
8
+
9
+ valid_cfg := {
10
+ "css.validate": false,
11
+ "less.validate": false,
12
+ "scss.validate": false,
13
+ }
14
+
15
+ # ── happy path ────────────────────────────────────────────────────────────
16
+
17
+ test_allow_canonical if {
18
+ count(vscode_settings.deny) == 0 with input as valid_cfg
19
+ }
20
+
21
+ test_allow_with_additional_keys if {
22
+ cfg := json.patch(valid_cfg, [{
23
+ "op": "add",
24
+ "path": "/editor.codeActionsOnSave",
25
+ "value": {"source.fixAll": "explicit"},
26
+ }])
27
+ count(vscode_settings.deny) == 0 with input as cfg
28
+ }
29
+
30
+ # ── deny ──────────────────────────────────────────────────────────────────
31
+
32
+ test_deny_css_validate_true if {
33
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/css.validate", "value": true}])
34
+ count(vscode_settings.deny) > 0 with input as cfg
35
+ }
36
+
37
+ test_deny_scss_validate_missing if {
38
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/scss.validate"}])
39
+ count(vscode_settings.deny) > 0 with input as cfg
40
+ }
41
+
42
+ test_deny_less_validate_string if {
43
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/less.validate", "value": "off"}])
44
+ count(vscode_settings.deny) > 0 with input as cfg
45
+ }
46
+
47
+ test_deny_empty_object if {
48
+ count(vscode_settings.deny) > 0 with input as {}
49
+ }
@@ -8,6 +8,12 @@
8
8
  # У випадку справжнього JSONC з `//` коментарями цей крок мовчки ігноруватиметься
9
9
  # (conftest skip). FS-перевірка (наявність файлу) живе у JS.
10
10
  #
11
+ # Перевіряє канонічний baseline з text.mdc (мінімум — додаткові ключі дозволені):
12
+ # { "gitignore": true,
13
+ # "config": { "default": true, "MD013": false, "MD024": {"siblings_only": true},
14
+ # "MD029": false, "MD040": false, "MD041": false } }
15
+ # MD041 off навмисно — `.mdc` з frontmatter (див. text.mdc).
16
+ #
11
17
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
12
18
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
13
19
  # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
@@ -15,7 +21,50 @@ package text.markdownlint
15
21
 
16
22
  import rego.v1
17
23
 
24
+ # ── Шаблони повідомлень ────────────────────────────────────────────────────
25
+
26
+ config_rule_template := concat(" ", [
27
+ ".markdownlint-cli2.jsonc: config.%s має бути %v",
28
+ "(зараз: %v) (text.mdc)",
29
+ ])
30
+
31
+ # ── deny: gitignore ───────────────────────────────────────────────────────
32
+
18
33
  deny contains msg if {
19
34
  object.get(input, "gitignore", null) != true
20
35
  msg := ".markdownlint-cli2.jsonc: додай на верхньому рівні \"gitignore\": true (text.mdc)"
21
36
  }
37
+
38
+ # ── deny: config.default ──────────────────────────────────────────────────
39
+
40
+ deny contains msg if {
41
+ config := object.get(input, "config", {})
42
+ object.get(config, "default", null) != true
43
+ msg := sprintf(config_rule_template, ["default", true, object.get(config, "default", null)])
44
+ }
45
+
46
+ # ── deny: MD013 / MD029 / MD040 / MD041 повинні бути `false` ──────────────
47
+
48
+ deny contains msg if {
49
+ config := object.get(input, "config", {})
50
+ some rule in {"MD013", "MD029", "MD040", "MD041"}
51
+ object.get(config, rule, null) != false
52
+ msg := sprintf(config_rule_template, [rule, false, object.get(config, rule, null)])
53
+ }
54
+
55
+ # ── deny: MD024.siblings_only == true ─────────────────────────────────────
56
+
57
+ deny contains msg if {
58
+ config := object.get(input, "config", {})
59
+ md024 := object.get(config, "MD024", null)
60
+ not is_object(md024)
61
+ msg := sprintf(config_rule_template, ["MD024", "{\"siblings_only\": true}", md024])
62
+ }
63
+
64
+ deny contains msg if {
65
+ config := object.get(input, "config", {})
66
+ md024 := object.get(config, "MD024", {})
67
+ is_object(md024)
68
+ object.get(md024, "siblings_only", null) != true
69
+ msg := sprintf(config_rule_template, ["MD024.siblings_only", true, object.get(md024, "siblings_only", null)])
70
+ }
@@ -0,0 +1,98 @@
1
+ # Тести для `text.markdownlint`. Запуск:
2
+ # conftest verify -p npm/policy/text/markdownlint
3
+ package text.markdownlint_test
4
+
5
+ import rego.v1
6
+
7
+ import data.text.markdownlint
8
+
9
+ valid_cfg := {
10
+ "gitignore": true,
11
+ "config": {
12
+ "default": true,
13
+ "MD013": false,
14
+ "MD024": {"siblings_only": true},
15
+ "MD029": false,
16
+ "MD040": false,
17
+ "MD041": false,
18
+ },
19
+ }
20
+
21
+ # ── happy path ────────────────────────────────────────────────────────────
22
+
23
+ test_allow_canonical if {
24
+ count(markdownlint.deny) == 0 with input as valid_cfg
25
+ }
26
+
27
+ test_allow_with_additional_top_level_keys if {
28
+ cfg := json.patch(valid_cfg, [{"op": "add", "path": "/ignores", "value": ["**/adr/**"]}])
29
+ count(markdownlint.deny) == 0 with input as cfg
30
+ }
31
+
32
+ test_allow_with_additional_md_rules if {
33
+ cfg := json.patch(valid_cfg, [{"op": "add", "path": "/config/MD033", "value": {"allowed_elements": ["a"]}}])
34
+ count(markdownlint.deny) == 0 with input as cfg
35
+ }
36
+
37
+ # ── gitignore ─────────────────────────────────────────────────────────────
38
+
39
+ test_deny_missing_gitignore if {
40
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/gitignore"}])
41
+ count(markdownlint.deny) > 0 with input as cfg
42
+ }
43
+
44
+ test_deny_gitignore_false if {
45
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/gitignore", "value": false}])
46
+ count(markdownlint.deny) > 0 with input as cfg
47
+ }
48
+
49
+ # ── config.default ────────────────────────────────────────────────────────
50
+
51
+ test_deny_default_false if {
52
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/default", "value": false}])
53
+ count(markdownlint.deny) > 0 with input as cfg
54
+ }
55
+
56
+ test_deny_default_missing if {
57
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/config/default"}])
58
+ count(markdownlint.deny) > 0 with input as cfg
59
+ }
60
+
61
+ # ── MD013 / MD029 / MD040 / MD041 — повинні бути false ────────────────────
62
+
63
+ test_deny_md013_true if {
64
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/MD013", "value": true}])
65
+ count(markdownlint.deny) > 0 with input as cfg
66
+ }
67
+
68
+ test_deny_md029_missing if {
69
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/config/MD029"}])
70
+ count(markdownlint.deny) > 0 with input as cfg
71
+ }
72
+
73
+ test_deny_md040_true if {
74
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/MD040", "value": true}])
75
+ count(markdownlint.deny) > 0 with input as cfg
76
+ }
77
+
78
+ test_deny_md041_true if {
79
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/MD041", "value": true}])
80
+ count(markdownlint.deny) > 0 with input as cfg
81
+ }
82
+
83
+ # ── MD024.siblings_only ──────────────────────────────────────────────────
84
+
85
+ test_deny_md024_not_object if {
86
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/MD024", "value": false}])
87
+ count(markdownlint.deny) > 0 with input as cfg
88
+ }
89
+
90
+ test_deny_md024_siblings_only_false if {
91
+ cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/config/MD024/siblings_only", "value": false}])
92
+ count(markdownlint.deny) > 0 with input as cfg
93
+ }
94
+
95
+ test_deny_md024_missing if {
96
+ cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/config/MD024"}])
97
+ count(markdownlint.deny) > 0 with input as cfg
98
+ }
@@ -20,17 +20,9 @@ import { readFile } from 'node:fs/promises'
20
20
 
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
 
23
- /**
24
- * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише **`@nitra/*`**).
25
- *
26
- * Залишилася як експорт для `check-text.mjs` і тестів — `bun.package_json` Rego
27
- * робить ту саму перевірку для check-runner-а.
28
- * @param {string} name ключ з поля `devDependencies`
29
- * @returns {boolean} true, якщо префікс дозволений
30
- */
31
- export function isAllowedRootDevDependency(name) {
32
- return name.startsWith('@nitra/')
33
- }
23
+ // Перевірка `devDependencies` кореневого `package.json` (дозволено лише `@nitra/*`)
24
+ // у rego (`npm/policy/bun/package_json/`). JS-копії `isAllowedRootDevDependency`
25
+ // видалено, щоб не було двох джерел істини.
34
26
 
35
27
  /**
36
28
  * Зчитує ідентифікатори правил з `.n-cursor.json` (поле `rules`).
@@ -17,6 +17,7 @@ import {
17
17
  sourceFileHasGqlTaggedTemplate
18
18
  } from './utils/graphql-gql-scan.mjs'
19
19
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
20
+ import { runConftestBatch } from './utils/run-conftest-batch.mjs'
20
21
  import { walkDir } from './utils/walkDir.mjs'
21
22
 
22
23
  /** Очікуваний файл GraphQL Config у корені (graphql.mdc). */
@@ -68,38 +69,29 @@ async function collectGqlHits(root, candidates) {
68
69
  }
69
70
 
70
71
  /**
71
- * Перевіряє `.vscode/extensions.json` на рекомендацію GraphQL extension.
72
+ * Делегує валідацію `.vscode/extensions.json` rego-пакету `graphql.vscode_extensions`
73
+ * через `runConftestBatch`. Викликається лише після того, як JS виявив `gql` у дереві
74
+ * (умовне правило — без gql цей крок не запускається).
72
75
  * @param {(msg: string) => void} pass success-репортер
73
76
  * @param {(msg: string) => void} fail fail-репортер
74
- * @returns {Promise<void>}
77
+ * @returns {void}
75
78
  */
76
- async function checkExtensionsRecommendation(pass, fail) {
77
- if (!existsSync('.vscode/extensions.json')) {
78
- fail(
79
- '.vscode/extensions.json не існує — створи файл і додай у recommendations graphql.vscode-graphql (graphql.mdc)'
80
- )
79
+ function checkExtensionsRecommendation(pass, fail) {
80
+ const path = '.vscode/extensions.json'
81
+ if (!existsSync(path)) {
82
+ fail(`${path} не існує — створи файл і додай у recommendations ${REQUIRED_GRAPHQL_VSCODE_EXTENSION} (graphql.mdc)`)
81
83
  return
82
84
  }
83
-
84
- let ext
85
- try {
86
- ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
87
- } catch {
88
- fail('.vscode/extensions.json не є валідним JSON')
85
+ const violations = runConftestBatch({
86
+ policyDirRel: 'graphql/vscode_extensions',
87
+ namespace: 'graphql.vscode_extensions',
88
+ files: [path]
89
+ })
90
+ if (violations.length === 0) {
91
+ pass(`${path} відповідає graphql.vscode_extensions (rego)`)
89
92
  return
90
93
  }
91
-
92
- const rec = ext.recommendations
93
- if (!Array.isArray(rec)) {
94
- fail('.vscode/extensions.json: поле recommendations має бути масивом')
95
- return
96
- }
97
-
98
- if (rec.includes(REQUIRED_GRAPHQL_VSCODE_EXTENSION)) {
99
- pass(`.vscode/extensions.json: є ${REQUIRED_GRAPHQL_VSCODE_EXTENSION}`)
100
- } else {
101
- fail(`.vscode/extensions.json: додай у recommendations "${REQUIRED_GRAPHQL_VSCODE_EXTENSION}" (graphql.mdc)`)
102
- }
94
+ for (const v of violations) fail(v.message)
103
95
  }
104
96
 
105
97
  /**
@@ -133,7 +125,7 @@ export async function check() {
133
125
  )
134
126
  }
135
127
 
136
- await checkExtensionsRecommendation(pass, fail)
128
+ checkExtensionsRecommendation(pass, fail)
137
129
 
138
130
  return reporter.getExitCode()
139
131
  }
@@ -40,6 +40,7 @@ import {
40
40
  } from './utils/bunyan-imports.mjs'
41
41
  import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
42
42
  import { createCheckReporter } from './utils/check-reporter.mjs'
43
+ import { runConftestBatch } from './utils/run-conftest-batch.mjs'
43
44
  import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './utils/conn-file-rules.mjs'
44
45
  import {
45
46
  findConnFactoryImportsInText,
@@ -67,10 +68,11 @@ function backendPackageHasSrcDir(absPackageRoot) {
67
68
  }
68
69
 
69
70
  /**
70
- * FS-existence для `jsconfig.json` у backend-пакеті з каталогом `src/` (cross-file:
71
- * наявність каталогу + файла). Структуру самого `jsconfig.json` (canonical
72
- * compilerOptions і include) валідує `npm/policy/js_run/jsconfig/`; її прогоняє
73
- * `bun run lint-conftest`.
71
+ * FS-existence + структурна валідація `jsconfig.json` у backend-пакеті з
72
+ * каталогом `src/`. Структуру (canonical `compilerOptions` і `include`)
73
+ * делегуємо у rego-пакет `js_run.jsconfig` через `runConftestBatch` — Plan B:
74
+ * Rego-authoritative, JS оркеструє per-package gate (frontend з `vite` сюди
75
+ * взагалі не доходить, бо викликається лише з backend-гілки).
74
76
  * @param {string} rootDir відносний шлях workspace
75
77
  * @param {string} absPackageRoot абсолютний корінь пакета
76
78
  * @param {string} label префікс `[pkg] `
@@ -82,14 +84,23 @@ function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail
82
84
  if (!backendPackageHasSrcDir(absPackageRoot)) return
83
85
 
84
86
  const jcPath = join(rootDir, 'jsconfig.json')
85
- if (existsSync(jcPath)) {
86
- passFn(`${label}jsconfig.json є (структуру перевіряє bun run lint-conftest → js_run.jsconfig)`)
87
- } else {
87
+ if (!existsSync(jcPath)) {
88
88
  fail(
89
89
  `${label}є каталог src/, але немає jsconfig.json — додай канонічний файл з js-run.mdc ` +
90
90
  `(NodeNext, include: src/**/*).`
91
91
  )
92
+ return
93
+ }
94
+ const violations = runConftestBatch({
95
+ policyDirRel: 'js_run/jsconfig',
96
+ namespace: 'js_run.jsconfig',
97
+ files: [jcPath]
98
+ })
99
+ if (violations.length === 0) {
100
+ passFn(`${label}jsconfig.json відповідає js_run.jsconfig (rego)`)
101
+ return
92
102
  }
103
+ for (const v of violations) fail(`${label}${v.message}`)
93
104
  }
94
105
 
95
106
  /**
@@ -20,6 +20,7 @@ import { basename, dirname, join, relative } from 'node:path'
20
20
  import { findDockerfilePaths } from './check-docker.mjs'
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
23
+ import { runConftestBatch } from './utils/run-conftest-batch.mjs'
23
24
  import { walkDir } from './utils/walkDir.mjs'
24
25
 
25
26
  const LINE_SPLIT_RE = /\r?\n/u
@@ -350,36 +351,45 @@ async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
350
351
  }
351
352
 
352
353
  /**
353
- * Перевіряє VSCode extensions.json та settings.json для nginx.
354
+ * Делегує валідацію `.vscode/extensions.json` і `.vscode/settings.json` rego-пакетам
355
+ * `nginx_default_tpl.vscode_extensions` і `nginx_default_tpl.vscode_settings`
356
+ * через `runConftestBatch`. Викликається лише після того, як JS виявив
357
+ * `default.conf.template` (умовне правило — без шаблона цей крок не запускається).
354
358
  * @param {(msg: string) => void} passFn callback при успішній перевірці
355
359
  * @param {(msg: string) => void} failFn callback при помилці
360
+ * @returns {void}
356
361
  */
357
- async function checkVscodeNginx(passFn, failFn) {
358
- if (existsSync('.vscode/extensions.json')) {
359
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
360
- if (ext.recommendations?.includes('ahmadalli.vscode-nginx-conf')) {
361
- passFn('extensions.json містить ahmadalli.vscode-nginx-conf')
362
+ function checkVscodeNginx(passFn, failFn) {
363
+ const extPath = '.vscode/extensions.json'
364
+ if (existsSync(extPath)) {
365
+ const violations = runConftestBatch({
366
+ policyDirRel: 'nginx_default_tpl/vscode_extensions',
367
+ namespace: 'nginx_default_tpl.vscode_extensions',
368
+ files: [extPath]
369
+ })
370
+ if (violations.length === 0) {
371
+ passFn(`${extPath} відповідає nginx_default_tpl.vscode_extensions (rego)`)
362
372
  } else {
363
- failFn('extensions.json не містить ahmadalli.vscode-nginx-conf')
373
+ for (const v of violations) failFn(v.message)
364
374
  }
365
375
  } else {
366
376
  failFn('Очікується .vscode/extensions.json з ahmadalli.vscode-nginx-conf (див. nginx-default-tpl.mdc)')
367
377
  }
368
378
 
369
- if (!existsSync('.vscode/settings.json')) {
379
+ const setPath = '.vscode/settings.json'
380
+ if (!existsSync(setPath)) {
370
381
  failFn('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
371
382
  return
372
383
  }
373
- const s = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
374
- if (s['editor.formatOnSave'] === true) {
375
- passFn('settings.json: editor.formatOnSave увімкнено')
376
- } else {
377
- failFn('settings.json: увімкни editor.formatOnSave: true (див. nginx-default-tpl.mdc)')
378
- }
379
- if (s['[nginx]']?.['editor.defaultFormatter'] === 'ahmadalli.vscode-nginx-conf') {
380
- passFn('settings.json: [nginx] defaultFormatter налаштовано')
384
+ const violations = runConftestBatch({
385
+ policyDirRel: 'nginx_default_tpl/vscode_settings',
386
+ namespace: 'nginx_default_tpl.vscode_settings',
387
+ files: [setPath]
388
+ })
389
+ if (violations.length === 0) {
390
+ passFn(`${setPath} відповідає nginx_default_tpl.vscode_settings (rego)`)
381
391
  } else {
382
- failFn('settings.json: [nginx].editor.defaultFormatter має бути ahmadalli.vscode-nginx-conf')
392
+ for (const v of violations) failFn(v.message)
383
393
  }
384
394
  }
385
395
 
@@ -416,7 +426,7 @@ export async function check() {
416
426
  }
417
427
 
418
428
  await checkDockerfiles(root, ignorePaths, pass, fail)
419
- await checkVscodeNginx(pass, fail)
429
+ checkVscodeNginx(pass, fail)
420
430
 
421
431
  return reporter.getExitCode()
422
432
  }
@@ -1,18 +1,20 @@
1
1
  /**
2
2
  * Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
3
3
  *
4
- * **Що тут лишилося** (FS / VSCode-конфіги — не покривається conftest):
4
+ * **Що тут лишилося** (FS / cross-file — не покривається conftest):
5
5
  * - наявність зовнішнього файлу конфігу stylelint (`.stylelintrc.*`,
6
6
  * `stylelint.config.js`) як альтернатива полю `stylelint` у `package.json`
7
7
  * (cross-file: треба знати, чи є поле, чи немає);
8
- * - `.stylelintignore` у корені;
9
- * - `.vscode/extensions.json` recommendation `stylelint.vscode-stylelint`;
10
- * - `.vscode/settings.json` `css.validate` / `scss.validate` / `less.validate: false`.
8
+ * - `.stylelintignore` у корені.
11
9
  *
12
10
  * **Що покрила Rego** (`bun run lint-conftest`):
13
11
  * - `npm/policy/style_lint/package_json/` — скрипт `lint-style` через `npx stylelint`,
14
12
  * `@nitra/stylelint-config` у `devDependencies`, поле `stylelint.extends`;
15
- * - `npm/policy/style_lint/lint_style_yml/` — `npx stylelint` у `run` workflow.
13
+ * - `npm/policy/style_lint/lint_style_yml/` — `npx stylelint` у `run` workflow;
14
+ * - `npm/policy/style_lint/vscode_extensions/` — `stylelint.vscode-stylelint`
15
+ * у `recommendations` `.vscode/extensions.json`;
16
+ * - `npm/policy/style_lint/vscode_settings/` — `css.validate`/`scss.validate`/
17
+ * `less.validate: false` у `.vscode/settings.json`.
16
18
  */
17
19
  import { existsSync } from 'node:fs'
18
20
  import { readFile } from 'node:fs/promises'
@@ -39,32 +41,10 @@ async function checkStylelintConfigPresence(reporter) {
39
41
  }
40
42
  }
41
43
 
42
- /**
43
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
44
- */
45
- async function checkVscodeStylelint(reporter) {
46
- const { pass, fail } = reporter
47
- if (existsSync('.vscode/extensions.json')) {
48
- const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
49
- if (ext.recommendations?.includes('stylelint.vscode-stylelint')) {
50
- pass('extensions.json містить stylelint.vscode-stylelint')
51
- } else {
52
- fail('extensions.json не містить stylelint.vscode-stylelint')
53
- }
54
- } else {
55
- fail('.vscode/extensions.json не існує')
56
- }
57
-
58
- if (!existsSync('.vscode/settings.json')) return
59
- const s = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
60
- for (const key of ['css.validate', 'scss.validate', 'less.validate']) {
61
- if (s[key] === false) {
62
- pass(`${key} вимкнено`)
63
- } else {
64
- fail(`settings.json: ${key} має бути false`)
65
- }
66
- }
67
- }
44
+ // `.vscode/extensions.json` (`stylelint.vscode-stylelint`) і `.vscode/settings.json`
45
+ // (`css.validate`/`scss.validate`/`less.validate: false`) у rego-пакетах
46
+ // `style_lint.vscode_extensions` і `style_lint.vscode_settings`, прогоняє
47
+ // `bun run lint-conftest`. JS-копії видалено, щоб не було двох джерел істини.
68
48
 
69
49
  /**
70
50
  * Перевіряє відповідність проєкту правилам style-lint.mdc
@@ -89,7 +69,5 @@ export async function check() {
89
69
  fail(`${wfPath} не існує — створи його`)
90
70
  }
91
71
 
92
- await checkVscodeStylelint(reporter)
93
-
94
72
  return reporter.getExitCode()
95
73
  }
@@ -112,6 +112,18 @@ const TARGETS = [
112
112
  rule: 'style-lint',
113
113
  single: '.github/workflows/lint-style.yml'
114
114
  },
115
+ {
116
+ namespace: 'style_lint.vscode_extensions',
117
+ policyDir: 'style_lint',
118
+ rule: 'style-lint',
119
+ single: '.vscode/extensions.json'
120
+ },
121
+ {
122
+ namespace: 'style_lint.vscode_settings',
123
+ policyDir: 'style_lint',
124
+ rule: 'style-lint',
125
+ single: '.vscode/settings.json'
126
+ },
115
127
 
116
128
  // ── php ─────────────────────────────────────────────────────────────────
117
129
  { namespace: 'php.package_json', policyDir: 'php', rule: 'php', single: 'package.json' },
@@ -157,13 +169,19 @@ const TARGETS = [
157
169
  single: '.github/workflows/lint-js.yml'
158
170
  },
159
171
 
160
- // ── image-compress / capacitor ──────────────────────────────────────────
172
+ // ── image-compress / image-avif / capacitor ─────────────────────────────
161
173
  {
162
174
  namespace: 'image_compress.package_json',
163
175
  policyDir: 'image_compress',
164
176
  rule: 'image-compress',
165
177
  single: 'package.json'
166
178
  },
179
+ {
180
+ namespace: 'image_avif.package_json',
181
+ policyDir: 'image_avif',
182
+ rule: 'image-avif',
183
+ walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
184
+ },
167
185
  {
168
186
  namespace: 'capacitor.package_json',
169
187
  policyDir: 'capacitor',
@@ -214,6 +232,12 @@ const TARGETS = [
214
232
  rule: 'js-run',
215
233
  walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
216
234
  },
235
+ // `js_run.jsconfig` НЕ реєструємо тут — `jsconfig.json` має канонічну структуру
236
+ // лише для backend-пакетів (без `vite` у `devDependencies`) з каталогом `src/`,
237
+ // а lint-conftest фільтрує лише по `activeRules` на рівні репозиторію — не
238
+ // вміє пропустити окремий workspace-пакет за наявністю `vite`. Тому валідація
239
+ // структури делегується з `check-js-run.mjs` через `runConftestBatch` після
240
+ // того, як JS визначить, що пакет — backend з `src/`.
217
241
  {
218
242
  namespace: 'vue.package_json',
219
243
  policyDir: 'vue',