@nitra/cursor 1.9.16 → 1.9.18
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 +36 -0
- package/mdc/docker.mdc +2 -19
- package/package.json +1 -1
- package/policy/docker/lint_docker_yml/lint_docker_yml.rego +91 -0
- package/policy/docker/lint_docker_yml/lint_docker_yml_test.rego +104 -0
- package/policy/docker/package_json/package_json.rego +35 -0
- package/policy/docker/package_json/package_json_test.rego +42 -0
- package/policy/text/vscode_extensions/vscode_extensions.rego +36 -0
- package/policy/text/vscode_extensions/vscode_extensions_test.rego +51 -0
- package/policy/text/vscode_settings/vscode_settings.rego +56 -0
- package/policy/text/vscode_settings/vscode_settings_test.rego +85 -0
- package/scripts/check-text.mjs +8 -66
- package/scripts/lint-conftest.mjs +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,42 @@
|
|
|
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.18] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **`docker.mdc` (`1.8 → 1.9`) — узгоджено канонічний `lint-docker.yml` з ga.mdc:** видалено 4 застарілих кроки з прикладу workflow (`actions/setup-node@v6` + `oven-sh/setup-bun@v2` + `actions/cache@v5` + `bun install --frozen-lockfile`) і замінено на один `uses: ./.github/actions/setup-bun-deps`. Раніше docker.mdc показував саме той патерн, який `ga.mdc` явно називає «❌ НЕПРАВИЛЬНО» і який `ga.workflow_common.rego` ловить через `forbidden_step_substrings` (3 заборонені підрядки). Конфлікт виник тому, що інші правила (`lint-style.mdc`, `lint-text.mdc`, `lint-js.mdc`) уже мігровано на composite, а docker.mdc пропустили. Hadolint-install залишається як окремий `curl`-крок (так само як `Install conftest` у `lint-ga.yml`).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`docker.package_json` rego — канонічний `scripts.lint-docker`:** deny, якщо ключ `scripts.lint-docker` присутній, але його значення ≠ `"bun ./npm/scripts/run-docker.mjs"`. Умовну обовʼязковість (правило `docker` у `.n-cursor.json` → `scripts.lint-docker` ЗОБОВ'ЯЗАНИЙ існувати) перевіряє `check-bun.mjs` cross-file, тут rego видно лише один документ. +6 тестів через `json.patch`-фікстури (canonical / lint-docker absent / whitespace / wrong value).
|
|
16
|
+
- **`docker.lint_docker_yml` rego — структура `.github/workflows/lint-docker.yml`:** 4 deny — (1) `on.push.paths` має містити 3 канонічні glob-и (`**/Dockerfile`, `**/*.Dockerfile`, `**/*.dockerfile`); (2) у `run:` будь-якого кроку має бути URL з версією `v2.12.0` (узгоджено з `HADOLINT_IMAGE` у `npm/scripts/utils/docker-hadolint.mjs`); (3) у `uses:` має бути `./.github/actions/setup-bun-deps` (canonical composite per ga.mdc); (4) у `run:` має бути `bun run lint-docker`. +9 тестів через `json.patch`-фікстури.
|
|
17
|
+
- **`lint-conftest.mjs` TARGETS — два нові entry:** `docker.package_json` (single: `package.json`) і `docker.lint_docker_yml` (single: `.github/workflows/lint-docker.yml`), обидва з `rule: 'docker'`. Цей репо (cursor) не має `docker` у `.n-cursor.json:rules` → docker таргети тут не активуються; полісі діятиме на проєкти-споживачі.
|
|
18
|
+
- **15 нових rego-тестів** (6 + 9), `conftest verify` — **267/267 pass** (+15).
|
|
19
|
+
|
|
20
|
+
### Regal fixes during migration
|
|
21
|
+
|
|
22
|
+
- `idiomatic/prefer-set-or-object-rule` у `lint_docker_yml.rego`: `all_step_uses := {u | …}` (comprehension) → `all_step_uses contains u if { … }` (incremental set rule).
|
|
23
|
+
- `style/line-length` × 3: винесено довгі hadolint-URL і шаблон повідомлення у проміжні константи через `concat`.
|
|
24
|
+
|
|
25
|
+
## [1.9.17] - 2026-05-13
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **2 нові rego-полісі для text.mdc VSCode-канону** (мігровано з `check-text.mjs`):
|
|
30
|
+
- `text.vscode_extensions` — `recommendations` має містити три розширення: `DavidAnson.vscode-markdownlint`, `oxc.oxc-vscode`, `timonwong.shellcheck`. Шаблон повідомлень + множина `recommendations_set` (винесена поза `deny`, щоб не порушити `performance/non-loop-expression`).
|
|
31
|
+
- `text.vscode_settings` — `editor.formatOnSave: true` плюс шість мов-блоків (`[javascript]`/`[typescript]`/`[json]`/`[vue]`/`[css]`/`[html]`) з `editor.defaultFormatter: "oxc.oxc-vscode"`. Окремі deny для «не object» і «неправильний defaultFormatter». Канон задає мінімум — додаткові lang-блоки дозволені.
|
|
32
|
+
- **18 нових rego-тестів** (7 для `vscode_extensions` + 11 для `vscode_settings`): happy path, додаткові поля, відсутність кожного розширення, відсутність `formatOnSave`, неправильний defaultFormatter, відсутні lang-блоки. `conftest verify` — **252/252 pass** (+18).
|
|
33
|
+
- **`lint-conftest.mjs` TARGETS — два нові entry для text:** `text.vscode_extensions` (`single: '.vscode/extensions.json'`) і `text.vscode_settings` (`single: '.vscode/settings.json'`), обидва з `rule: 'text'`. Глобально активуються для всіх проєктів з `text` у `.n-cursor.json:rules`.
|
|
34
|
+
|
|
35
|
+
### Removed
|
|
36
|
+
|
|
37
|
+
- **`check-text.mjs::checkVscodeTextExtensions` / `checkVscodeTextSettings` / `checkVscodeText`** — три JS-функції видалено разом з викликом `await checkVscodeText(pass, fail)` у `check()`. Зміст delegated у rego (`text.vscode_extensions` + `text.vscode_settings`).
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`check-text.mjs::checkTextConfigsExistence` — розширено двома записами:** тепер вимагає FS-існування `.vscode/extensions.json` і `.vscode/settings.json` поряд з `.oxfmtrc.json` / `.cspell.json` / `.markdownlint-cli2.jsonc`. lint-conftest з rego skip-ить неіснуючі файли, тому FS-existence лишається в JS — це працює як «єдина точка контролю наявності файлу + delegated content-валідація у rego».
|
|
42
|
+
|
|
7
43
|
## [1.9.16] - 2026-05-13
|
|
8
44
|
|
|
9
45
|
### Added
|
package/mdc/docker.mdc
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Dockerfile — lint-docker / hadolint; перевірка check-docker
|
|
3
|
-
version: '1.
|
|
3
|
+
version: '1.9'
|
|
4
4
|
globs: "**/Dockerfile*"
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
@@ -153,24 +153,7 @@ jobs:
|
|
|
153
153
|
chmod +x /tmp/hadolint
|
|
154
154
|
sudo mv /tmp/hadolint /usr/local/bin/hadolint
|
|
155
155
|
|
|
156
|
-
- uses: actions/setup-
|
|
157
|
-
with:
|
|
158
|
-
node-version: '24'
|
|
159
|
-
|
|
160
|
-
- uses: oven-sh/setup-bun@v2
|
|
161
|
-
|
|
162
|
-
- name: Cache Bun dependencies
|
|
163
|
-
uses: actions/cache@v5
|
|
164
|
-
with:
|
|
165
|
-
path: |
|
|
166
|
-
~/.bun/install/cache
|
|
167
|
-
node_modules
|
|
168
|
-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
169
|
-
restore-keys: |
|
|
170
|
-
${{ runner.os }}-bun-
|
|
171
|
-
|
|
172
|
-
- name: Install dependencies
|
|
173
|
-
run: bun install --frozen-lockfile
|
|
156
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
174
157
|
|
|
175
158
|
- name: Lint Docker
|
|
176
159
|
run: bun run lint-docker
|
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Перевірка `.github/workflows/lint-docker.yml` (docker.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test .github/workflows/lint-docker.yml -p npm/policy/docker/lint_docker_yml \
|
|
5
|
+
# --namespace docker.lint_docker_yml
|
|
6
|
+
#
|
|
7
|
+
# Canonical (docker.mdc):
|
|
8
|
+
# - `on.push.paths` містить glob-и для Dockerfile (`**/Dockerfile`, `**/*.Dockerfile`,
|
|
9
|
+
# `**/*.dockerfile`);
|
|
10
|
+
# - крок `Install hadolint` з URL версії `v2.12.0` (узгоджено з `HADOLINT_IMAGE`
|
|
11
|
+
# у `npm/scripts/utils/docker-hadolint.mjs`);
|
|
12
|
+
# - крок `uses: ./.github/actions/setup-bun-deps` (canonical composite per ga.mdc;
|
|
13
|
+
# прямі `oxen-sh/setup-bun`/`actions/cache`/`bun install` заборонено через
|
|
14
|
+
# `ga.workflow_common`);
|
|
15
|
+
# - крок `run: bun run lint-docker`.
|
|
16
|
+
#
|
|
17
|
+
# Універсальні workflow-перевірки (concurrency, заборонені setup-bun/cache/install,
|
|
18
|
+
# shell line-continuation) — у `ga.workflow_common`.
|
|
19
|
+
#
|
|
20
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
21
|
+
package docker.lint_docker_yml
|
|
22
|
+
|
|
23
|
+
import rego.v1
|
|
24
|
+
|
|
25
|
+
required_push_paths := {"**/Dockerfile", "**/*.Dockerfile", "**/*.dockerfile"}
|
|
26
|
+
required_hadolint_version := "v2.12.0"
|
|
27
|
+
canonical_setup_bun_action := "./.github/actions/setup-bun-deps"
|
|
28
|
+
|
|
29
|
+
# Усі тексти `uses:` зі steps усіх jobs (incremental set rule per regal:
|
|
30
|
+
# prefer-set-or-object-rule — це краще за comprehension).
|
|
31
|
+
all_step_uses contains u if {
|
|
32
|
+
some job in object.get(input, "jobs", {})
|
|
33
|
+
some step in object.get(job, "steps", [])
|
|
34
|
+
u := object.get(step, "uses", "")
|
|
35
|
+
u != ""
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
all_run_text := concat("\n", [run_text |
|
|
39
|
+
some job in object.get(input, "jobs", {})
|
|
40
|
+
some step in object.get(job, "steps", [])
|
|
41
|
+
run_text := step_run_to_text(step)
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
# Множина значень `on.push.paths` (підтримує `on: { push: { paths: [...] } }`).
|
|
45
|
+
push_paths_set := {p |
|
|
46
|
+
some p in object.get(object.get(object.get(input, "on", {}), "push", {}), "paths", [])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ── deny: on.push.paths ──────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
deny contains msg if {
|
|
52
|
+
some required in required_push_paths
|
|
53
|
+
not required in push_paths_set
|
|
54
|
+
msg := sprintf("lint-docker.yml: on.push.paths має містити %q (docker.mdc)", [required])
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# ── deny: hadolint install version ───────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
deny contains msg if {
|
|
60
|
+
not contains(all_run_text, required_hadolint_version)
|
|
61
|
+
msg := sprintf(
|
|
62
|
+
"lint-docker.yml: крок hadolint install має містити версію %q (узгоджено з HADOLINT_IMAGE) (docker.mdc)",
|
|
63
|
+
[required_hadolint_version],
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# ── deny: setup-bun-deps composite ───────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
deny contains msg if {
|
|
70
|
+
not canonical_setup_bun_action in all_step_uses
|
|
71
|
+
msg := concat(" ", [
|
|
72
|
+
"lint-docker.yml: відсутній крок",
|
|
73
|
+
"`uses: ./.github/actions/setup-bun-deps` (canonical composite per ga.mdc) (docker.mdc)",
|
|
74
|
+
])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ── deny: bun run lint-docker ────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
deny contains msg if {
|
|
80
|
+
not contains(all_run_text, "bun run lint-docker")
|
|
81
|
+
msg := "lint-docker.yml: жоден крок run не містить `bun run lint-docker` (docker.mdc)"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ── helpers ──────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
# Текст `run:` як один рядок: підтримує string і array форми (YAML).
|
|
87
|
+
step_run_to_text(step) := step.run if is_string(step.run)
|
|
88
|
+
|
|
89
|
+
else := concat("\n", [s | some s in step.run]) if is_array(step.run)
|
|
90
|
+
|
|
91
|
+
else := ""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Тести для `docker.lint_docker_yml`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/docker/lint_docker_yml
|
|
3
|
+
package docker.lint_docker_yml_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.docker.lint_docker_yml
|
|
8
|
+
|
|
9
|
+
hadolint_install_run := concat("", [
|
|
10
|
+
"curl -sSL -o /tmp/hadolint",
|
|
11
|
+
" https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64",
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
valid_wf := {
|
|
15
|
+
"name": "Lint Docker",
|
|
16
|
+
"on": {"push": {
|
|
17
|
+
"branches": ["dev", "main"],
|
|
18
|
+
"paths": ["**/Dockerfile", "**/*.Dockerfile", "**/*.dockerfile"],
|
|
19
|
+
}},
|
|
20
|
+
"jobs": {"lint-docker": {"steps": [
|
|
21
|
+
{"uses": "actions/checkout@v6"},
|
|
22
|
+
{"name": "Install hadolint", "run": hadolint_install_run},
|
|
23
|
+
{"uses": "./.github/actions/setup-bun-deps"},
|
|
24
|
+
{"name": "Lint Docker", "run": "bun run lint-docker"},
|
|
25
|
+
]}},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# ── happy path ────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
test_allow_canonical if {
|
|
31
|
+
count(lint_docker_yml.deny) == 0 with input as valid_wf
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ── deny: on.push.paths ──────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test_deny_missing_path_dockerfile if {
|
|
37
|
+
wf := json.patch(
|
|
38
|
+
valid_wf,
|
|
39
|
+
[{"op": "replace", "path": "/on/push/paths", "value": ["**/*.Dockerfile", "**/*.dockerfile"]}],
|
|
40
|
+
)
|
|
41
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test_deny_missing_paths_field if {
|
|
45
|
+
wf := json.patch(valid_wf, [{"op": "remove", "path": "/on/push/paths"}])
|
|
46
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# ── deny: hadolint version ──────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
test_deny_wrong_hadolint_version if {
|
|
52
|
+
wrong_version_run := concat("", [
|
|
53
|
+
"curl -sSL",
|
|
54
|
+
" https://github.com/hadolint/hadolint/releases/download/v2.11.0/hadolint-Linux-x86_64",
|
|
55
|
+
])
|
|
56
|
+
wf := json.patch(valid_wf, [{
|
|
57
|
+
"op": "replace",
|
|
58
|
+
"path": "/jobs/lint-docker/steps/1/run",
|
|
59
|
+
"value": wrong_version_run,
|
|
60
|
+
}])
|
|
61
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test_deny_no_hadolint_install if {
|
|
65
|
+
wf := json.patch(valid_wf, [{
|
|
66
|
+
"op": "replace",
|
|
67
|
+
"path": "/jobs/lint-docker/steps/1",
|
|
68
|
+
"value": {"name": "Noop", "run": "echo ok"},
|
|
69
|
+
}])
|
|
70
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# ── deny: composite setup-bun-deps ──────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
test_deny_inline_setup_bun_instead_of_composite if {
|
|
76
|
+
# Старий канон (НЕПРАВИЛЬНО per ga.mdc): пряме `oven-sh/setup-bun` замість composite.
|
|
77
|
+
wf := json.patch(valid_wf, [{
|
|
78
|
+
"op": "replace",
|
|
79
|
+
"path": "/jobs/lint-docker/steps/2",
|
|
80
|
+
"value": {"uses": "oven-sh/setup-bun@v2"},
|
|
81
|
+
}])
|
|
82
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test_deny_no_setup_step if {
|
|
86
|
+
wf := json.patch(valid_wf, [{"op": "remove", "path": "/jobs/lint-docker/steps/2"}])
|
|
87
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ── deny: bun run lint-docker ──────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
test_deny_missing_lint_docker_run if {
|
|
93
|
+
wf := json.patch(valid_wf, [{
|
|
94
|
+
"op": "replace",
|
|
95
|
+
"path": "/jobs/lint-docker/steps/3/run",
|
|
96
|
+
"value": "echo noop",
|
|
97
|
+
}])
|
|
98
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test_deny_no_run_steps_at_all if {
|
|
102
|
+
wf := json.patch(valid_wf, [{"op": "replace", "path": "/jobs/lint-docker/steps", "value": []}])
|
|
103
|
+
count(lint_docker_yml.deny) > 0 with input as wf
|
|
104
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Перевірка `package.json` для docker (docker.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test package.json -p npm/policy/docker/package_json \
|
|
5
|
+
# --namespace docker.package_json
|
|
6
|
+
#
|
|
7
|
+
# Canonical (docker.mdc): якщо у проєкті є правило `docker` (у `.n-cursor.json`),
|
|
8
|
+
# у кореневому `package.json` має бути канонічний `scripts.lint-docker`.
|
|
9
|
+
#
|
|
10
|
+
# Цей пакет перевіряє ЛИШЕ зміст значення `scripts.lint-docker`, якщо ключ
|
|
11
|
+
# присутній. Умовну обовʼязковість (правило `docker` у `.n-cursor.json` →
|
|
12
|
+
# `scripts.lint-docker` ЗОБОВ'ЯЗАНИЙ існувати) перевіряє `check-bun.mjs` через
|
|
13
|
+
# cross-file логіку (читає `.n-cursor.json` і `package.json`). Тут rego видно
|
|
14
|
+
# лише один документ, тому без `.n-cursor.json`-контексту вимагати наявність
|
|
15
|
+
# `scripts.lint-docker` означало б false-positive порушення для проєктів без docker.
|
|
16
|
+
#
|
|
17
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
18
|
+
package docker.package_json
|
|
19
|
+
|
|
20
|
+
import rego.v1
|
|
21
|
+
|
|
22
|
+
canonical_lint_docker := "bun ./npm/scripts/run-docker.mjs"
|
|
23
|
+
|
|
24
|
+
lint_docker_template := concat(" ", [
|
|
25
|
+
"package.json: scripts.lint-docker має бути %q",
|
|
26
|
+
"(зараз: %q) (docker.mdc)",
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
deny contains msg if {
|
|
30
|
+
scripts := object.get(input, "scripts", {})
|
|
31
|
+
lint_docker := object.get(scripts, "lint-docker", "")
|
|
32
|
+
lint_docker != ""
|
|
33
|
+
trim_space(lint_docker) != canonical_lint_docker
|
|
34
|
+
msg := sprintf(lint_docker_template, [canonical_lint_docker, lint_docker])
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Тести для `docker.package_json`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/docker/package_json
|
|
3
|
+
package docker.package_json_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.docker.package_json
|
|
8
|
+
|
|
9
|
+
canonical_lint_docker := "bun ./npm/scripts/run-docker.mjs"
|
|
10
|
+
|
|
11
|
+
# ── happy path ────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
test_allow_canonical if {
|
|
14
|
+
pkg := {"scripts": {"lint-docker": canonical_lint_docker}}
|
|
15
|
+
count(package_json.deny) == 0 with input as pkg
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test_allow_lint_docker_absent if {
|
|
19
|
+
# rego не вимагає наявність — cross-file умовно вимагає `check-bun.mjs`.
|
|
20
|
+
count(package_json.deny) == 0 with input as {"scripts": {}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test_allow_no_scripts_at_all if {
|
|
24
|
+
count(package_json.deny) == 0 with input as {"name": "x"}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test_allow_with_extra_whitespace if {
|
|
28
|
+
pkg := {"scripts": {"lint-docker": concat("", [" ", canonical_lint_docker, " "])}}
|
|
29
|
+
count(package_json.deny) == 0 with input as pkg
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# ── deny ──────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
test_deny_lint_docker_wrong_value if {
|
|
35
|
+
pkg := {"scripts": {"lint-docker": "hadolint Dockerfile"}}
|
|
36
|
+
count(package_json.deny) > 0 with input as pkg
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test_deny_lint_docker_old_npx_form if {
|
|
40
|
+
pkg := {"scripts": {"lint-docker": "npx hadolint ."}}
|
|
41
|
+
count(package_json.deny) > 0 with input as pkg
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Перевірка `.vscode/extensions.json` для text (text.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test .vscode/extensions.json -p npm/policy/text/vscode_extensions \
|
|
5
|
+
# --namespace text.vscode_extensions
|
|
6
|
+
#
|
|
7
|
+
# Canonical (text.mdc): у `recommendations` мають бути три розширення
|
|
8
|
+
# - DavidAnson.vscode-markdownlint
|
|
9
|
+
# - oxc.oxc-vscode
|
|
10
|
+
# - timonwong.shellcheck
|
|
11
|
+
#
|
|
12
|
+
# Канон задає мінімум — додаткові записи (від інших правил) дозволені.
|
|
13
|
+
#
|
|
14
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
15
|
+
package text.vscode_extensions
|
|
16
|
+
|
|
17
|
+
import rego.v1
|
|
18
|
+
|
|
19
|
+
required_extensions := {
|
|
20
|
+
"DavidAnson.vscode-markdownlint",
|
|
21
|
+
"oxc.oxc-vscode",
|
|
22
|
+
"timonwong.shellcheck",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
missing_extension_template := ".vscode/extensions.json: recommendations має містити %q (text.mdc)"
|
|
26
|
+
|
|
27
|
+
# Множина усіх записів `recommendations` (вираз поза deny — щоб regal не лаявся
|
|
28
|
+
# performance/non-loop-expression: інакше `object.get` виконувався б на кожній
|
|
29
|
+
# ітерації по `required_extensions`).
|
|
30
|
+
recommendations_set := {r | some r in object.get(input, "recommendations", [])}
|
|
31
|
+
|
|
32
|
+
deny contains msg if {
|
|
33
|
+
some required in required_extensions
|
|
34
|
+
not required in recommendations_set
|
|
35
|
+
msg := sprintf(missing_extension_template, [required])
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Тести для `text.vscode_extensions`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/text/vscode_extensions
|
|
3
|
+
package text.vscode_extensions_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.text.vscode_extensions
|
|
8
|
+
|
|
9
|
+
canonical := {"recommendations": [
|
|
10
|
+
"DavidAnson.vscode-markdownlint",
|
|
11
|
+
"oxc.oxc-vscode",
|
|
12
|
+
"timonwong.shellcheck",
|
|
13
|
+
]}
|
|
14
|
+
|
|
15
|
+
test_allow_canonical if {
|
|
16
|
+
count(vscode_extensions.deny) == 0 with input as canonical
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test_allow_with_additional_extensions if {
|
|
20
|
+
cfg := {"recommendations": [
|
|
21
|
+
"DavidAnson.vscode-markdownlint",
|
|
22
|
+
"oxc.oxc-vscode",
|
|
23
|
+
"timonwong.shellcheck",
|
|
24
|
+
"dbaeumer.vscode-eslint",
|
|
25
|
+
"stylelint.vscode-stylelint",
|
|
26
|
+
]}
|
|
27
|
+
count(vscode_extensions.deny) == 0 with input as cfg
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test_deny_missing_markdownlint if {
|
|
31
|
+
cfg := {"recommendations": ["oxc.oxc-vscode", "timonwong.shellcheck"]}
|
|
32
|
+
count(vscode_extensions.deny) > 0 with input as cfg
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test_deny_missing_oxc if {
|
|
36
|
+
cfg := {"recommendations": ["DavidAnson.vscode-markdownlint", "timonwong.shellcheck"]}
|
|
37
|
+
count(vscode_extensions.deny) > 0 with input as cfg
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test_deny_missing_shellcheck if {
|
|
41
|
+
cfg := {"recommendations": ["DavidAnson.vscode-markdownlint", "oxc.oxc-vscode"]}
|
|
42
|
+
count(vscode_extensions.deny) > 0 with input as cfg
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test_deny_empty_recommendations if {
|
|
46
|
+
count(vscode_extensions.deny) > 0 with input as {"recommendations": []}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test_deny_no_recommendations_field if {
|
|
50
|
+
count(vscode_extensions.deny) > 0 with input as {}
|
|
51
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Перевірка `.vscode/settings.json` для text (text.mdc).
|
|
2
|
+
#
|
|
3
|
+
# Запуск (локально):
|
|
4
|
+
# conftest test .vscode/settings.json -p npm/policy/text/vscode_settings \
|
|
5
|
+
# --namespace text.vscode_settings
|
|
6
|
+
#
|
|
7
|
+
# Canonical (text.mdc):
|
|
8
|
+
# { "editor.formatOnSave": true,
|
|
9
|
+
# "[javascript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
|
|
10
|
+
# "[typescript]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
|
|
11
|
+
# "[json]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
|
|
12
|
+
# "[vue]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
|
|
13
|
+
# "[css]": { "editor.defaultFormatter": "oxc.oxc-vscode" },
|
|
14
|
+
# "[html]": { "editor.defaultFormatter": "oxc.oxc-vscode" } }
|
|
15
|
+
#
|
|
16
|
+
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
17
|
+
package text.vscode_settings
|
|
18
|
+
|
|
19
|
+
import rego.v1
|
|
20
|
+
|
|
21
|
+
language_keys := {"[javascript]", "[typescript]", "[json]", "[vue]", "[css]", "[html]"}
|
|
22
|
+
|
|
23
|
+
# Шаблони повідомлень — через `concat` для regal style/line-length.
|
|
24
|
+
lang_block_not_object_template := concat(" ", [
|
|
25
|
+
".vscode/settings.json: %q має бути обʼєктом з",
|
|
26
|
+
"\"editor.defaultFormatter\": \"oxc.oxc-vscode\" (text.mdc)",
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
lang_wrong_formatter_template := concat(" ", [
|
|
30
|
+
".vscode/settings.json: %q має використовувати",
|
|
31
|
+
"\"oxc.oxc-vscode\" як editor.defaultFormatter (text.mdc)",
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
# ── deny: editor.formatOnSave ────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
deny contains msg if {
|
|
37
|
+
object.get(input, "editor.formatOnSave", null) != true
|
|
38
|
+
msg := ".vscode/settings.json: \"editor.formatOnSave\" має бути true (text.mdc)"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# ── deny: [lang].editor.defaultFormatter ────────────────────────────────
|
|
42
|
+
|
|
43
|
+
deny contains msg if {
|
|
44
|
+
some key in language_keys
|
|
45
|
+
block := object.get(input, key, {})
|
|
46
|
+
not is_object(block)
|
|
47
|
+
msg := sprintf(lang_block_not_object_template, [key])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
deny contains msg if {
|
|
51
|
+
some key in language_keys
|
|
52
|
+
block := object.get(input, key, {})
|
|
53
|
+
is_object(block)
|
|
54
|
+
object.get(block, "editor.defaultFormatter", null) != "oxc.oxc-vscode"
|
|
55
|
+
msg := sprintf(lang_wrong_formatter_template, [key])
|
|
56
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Тести для `text.vscode_settings`. Запуск:
|
|
2
|
+
# conftest verify -p npm/policy/text/vscode_settings
|
|
3
|
+
package text.vscode_settings_test
|
|
4
|
+
|
|
5
|
+
import rego.v1
|
|
6
|
+
|
|
7
|
+
import data.text.vscode_settings
|
|
8
|
+
|
|
9
|
+
valid_cfg := {
|
|
10
|
+
"editor.formatOnSave": true,
|
|
11
|
+
"[javascript]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
12
|
+
"[typescript]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
13
|
+
"[json]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
14
|
+
"[vue]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
15
|
+
"[css]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
16
|
+
"[html]": {"editor.defaultFormatter": "oxc.oxc-vscode"},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# ── happy path ────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
test_allow_canonical if {
|
|
22
|
+
count(vscode_settings.deny) == 0 with input as valid_cfg
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test_allow_with_additional_lang_block if {
|
|
26
|
+
cfg := json.patch(valid_cfg, [{
|
|
27
|
+
"op": "add",
|
|
28
|
+
"path": "/[python]",
|
|
29
|
+
"value": {"editor.defaultFormatter": "ms-python.python"},
|
|
30
|
+
}])
|
|
31
|
+
count(vscode_settings.deny) == 0 with input as cfg
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ── deny: editor.formatOnSave ────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
test_deny_format_on_save_false if {
|
|
37
|
+
cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/editor.formatOnSave", "value": false}])
|
|
38
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test_deny_format_on_save_missing if {
|
|
42
|
+
cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/editor.formatOnSave"}])
|
|
43
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ── deny: lang formatters (per-key) ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
test_deny_javascript_missing if {
|
|
49
|
+
cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/[javascript]"}])
|
|
50
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test_deny_typescript_wrong_formatter if {
|
|
54
|
+
cfg := json.patch(
|
|
55
|
+
valid_cfg,
|
|
56
|
+
[{"op": "replace", "path": "/[typescript]/editor.defaultFormatter", "value": "prettier"}],
|
|
57
|
+
)
|
|
58
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test_deny_json_block_not_object if {
|
|
62
|
+
cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/[json]", "value": "oxc.oxc-vscode"}])
|
|
63
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test_deny_vue_missing_default_formatter if {
|
|
67
|
+
cfg := json.patch(valid_cfg, [{"op": "replace", "path": "/[vue]", "value": {"editor.tabSize": 2}}])
|
|
68
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test_deny_css_missing if {
|
|
72
|
+
cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/[css]"}])
|
|
73
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test_deny_html_missing if {
|
|
77
|
+
cfg := json.patch(valid_cfg, [{"op": "remove", "path": "/[html]"}])
|
|
78
|
+
count(vscode_settings.deny) > 0 with input as cfg
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# ── deny: empty object ───────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
test_deny_empty_object if {
|
|
84
|
+
count(vscode_settings.deny) > 0 with input as {}
|
|
85
|
+
}
|
package/scripts/check-text.mjs
CHANGED
|
@@ -89,70 +89,11 @@ async function checkV8rIgnore(passFn, failFn) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
async function checkVscodeTextExtensions(passFn, failFn) {
|
|
98
|
-
if (!existsSync('.vscode/extensions.json')) {
|
|
99
|
-
failFn('.vscode/extensions.json не існує — створи з recommendations згідно n-text.mdc')
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const ext = JSON.parse(await readFile('.vscode/extensions.json', 'utf8'))
|
|
104
|
-
const rec = ext.recommendations
|
|
105
|
-
for (const id of ['DavidAnson.vscode-markdownlint', 'oxc.oxc-vscode', 'timonwong.shellcheck']) {
|
|
106
|
-
if (Array.isArray(rec) && rec.includes(id)) {
|
|
107
|
-
passFn(`extensions.json містить ${id}`)
|
|
108
|
-
} else {
|
|
109
|
-
failFn(`extensions.json: додай "${id}" у recommendations (див. n-text.mdc)`)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
failFn('.vscode/extensions.json — невалідний JSON')
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Перевіряє VSCode settings.json для текстового стека.
|
|
119
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
120
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
121
|
-
*/
|
|
122
|
-
async function checkVscodeTextSettings(passFn, failFn) {
|
|
123
|
-
if (!existsSync('.vscode/settings.json')) {
|
|
124
|
-
failFn('.vscode/settings.json не існує — створи згідно n-text.mdc')
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
try {
|
|
128
|
-
const settings = JSON.parse(await readFile('.vscode/settings.json', 'utf8'))
|
|
129
|
-
if (settings['editor.formatOnSave'] === true) {
|
|
130
|
-
passFn('settings.json: editor.formatOnSave увімкнено')
|
|
131
|
-
} else {
|
|
132
|
-
failFn('settings.json: editor.formatOnSave має бути true')
|
|
133
|
-
}
|
|
134
|
-
for (const t of ['javascript', 'typescript', 'json', 'vue', 'css', 'html']) {
|
|
135
|
-
const key = `[${t}]`
|
|
136
|
-
if (settings[key]?.['editor.defaultFormatter'] === 'oxc.oxc-vscode') {
|
|
137
|
-
passFn(`settings.json: ${key} використовує oxc.oxc-vscode`)
|
|
138
|
-
} else {
|
|
139
|
-
failFn(`settings.json: ${key} має використовувати oxc.oxc-vscode як defaultFormatter`)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
} catch {
|
|
143
|
-
failFn('.vscode/settings.json — невалідний JSON')
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Перевіряє VSCode extensions.json та settings.json для текстового стека.
|
|
149
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
150
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
151
|
-
*/
|
|
152
|
-
async function checkVscodeText(passFn, failFn) {
|
|
153
|
-
await checkVscodeTextExtensions(passFn, failFn)
|
|
154
|
-
await checkVscodeTextSettings(passFn, failFn)
|
|
155
|
-
}
|
|
92
|
+
// `.vscode/extensions.json` (`DavidAnson.vscode-markdownlint`, `oxc.oxc-vscode`,
|
|
93
|
+
// `timonwong.shellcheck`) і `.vscode/settings.json` (`editor.formatOnSave` +
|
|
94
|
+
// `[lang].editor.defaultFormatter`) валідують rego-пакети `text.vscode_extensions`
|
|
95
|
+
// і `text.vscode_settings` (зареєстровані глобально у `lint-conftest.mjs` TARGETS
|
|
96
|
+
// з `rule: 'text'`). FS-existence файлів — у `checkTextConfigsExistence`.
|
|
156
97
|
|
|
157
98
|
/**
|
|
158
99
|
* FS-existence стек текстових конфігів. Контент-валідація — у Rego
|
|
@@ -165,7 +106,9 @@ function checkTextConfigsExistence(passFn, failFn) {
|
|
|
165
106
|
for (const [path, mdcRef] of [
|
|
166
107
|
['.oxfmtrc.json', 'text.oxfmtrc'],
|
|
167
108
|
['.cspell.json', 'text.cspell'],
|
|
168
|
-
['.markdownlint-cli2.jsonc', 'text.markdownlint']
|
|
109
|
+
['.markdownlint-cli2.jsonc', 'text.markdownlint'],
|
|
110
|
+
['.vscode/extensions.json', 'text.vscode_extensions'],
|
|
111
|
+
['.vscode/settings.json', 'text.vscode_settings']
|
|
169
112
|
]) {
|
|
170
113
|
if (existsSync(path)) {
|
|
171
114
|
passFn(`${path} є (структуру перевіряє bun run lint-conftest → ${mdcRef})`)
|
|
@@ -247,7 +190,6 @@ export async function check() {
|
|
|
247
190
|
const { pass, fail } = reporter
|
|
248
191
|
|
|
249
192
|
await checkV8rIgnore(pass, fail)
|
|
250
|
-
await checkVscodeText(pass, fail)
|
|
251
193
|
await checkTextConfigsExistence(pass, fail)
|
|
252
194
|
|
|
253
195
|
for (const f of ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']) {
|
|
@@ -103,6 +103,8 @@ const TARGETS = [
|
|
|
103
103
|
{ namespace: 'text.cspell', policyDir: 'text', rule: 'text', single: '.cspell.json' },
|
|
104
104
|
{ namespace: 'text.markdownlint', policyDir: 'text', rule: 'text', single: '.markdownlint-cli2.jsonc' },
|
|
105
105
|
{ namespace: 'text.package_json', policyDir: 'text', rule: 'text', single: 'package.json' },
|
|
106
|
+
{ namespace: 'text.vscode_extensions', policyDir: 'text', rule: 'text', single: '.vscode/extensions.json' },
|
|
107
|
+
{ namespace: 'text.vscode_settings', policyDir: 'text', rule: 'text', single: '.vscode/settings.json' },
|
|
106
108
|
|
|
107
109
|
// ── style-lint ──────────────────────────────────────────────────────────
|
|
108
110
|
{ namespace: 'style_lint.package_json', policyDir: 'style_lint', rule: 'style-lint', single: 'package.json' },
|
|
@@ -134,6 +136,15 @@ const TARGETS = [
|
|
|
134
136
|
single: '.github/workflows/lint-php.yml'
|
|
135
137
|
},
|
|
136
138
|
|
|
139
|
+
// ── docker ──────────────────────────────────────────────────────────────
|
|
140
|
+
{ namespace: 'docker.package_json', policyDir: 'docker', rule: 'docker', single: 'package.json' },
|
|
141
|
+
{
|
|
142
|
+
namespace: 'docker.lint_docker_yml',
|
|
143
|
+
policyDir: 'docker',
|
|
144
|
+
rule: 'docker',
|
|
145
|
+
single: '.github/workflows/lint-docker.yml'
|
|
146
|
+
},
|
|
147
|
+
|
|
137
148
|
// ── npm-module ──────────────────────────────────────────────────────────
|
|
138
149
|
{
|
|
139
150
|
namespace: 'npm_module.root_package_json',
|