@nitra/cursor 1.9.14 → 1.9.17

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 +56 -0
  2. package/package.json +1 -1
  3. package/policy/graphql/vscode_extensions/vscode_extensions.rego +20 -0
  4. package/policy/graphql/vscode_extensions/vscode_extensions_test.rego +34 -0
  5. package/policy/image_avif/package_json/package_json.rego +61 -0
  6. package/policy/image_avif/package_json/package_json_test.rego +69 -0
  7. package/policy/js_run/jsconfig/jsconfig_test.rego +88 -0
  8. package/policy/nginx_default_tpl/vscode_extensions/vscode_extensions.rego +16 -0
  9. package/policy/nginx_default_tpl/vscode_extensions/vscode_extensions_test.rego +30 -0
  10. package/policy/nginx_default_tpl/vscode_settings/vscode_settings.rego +36 -0
  11. package/policy/nginx_default_tpl/vscode_settings/vscode_settings_test.rego +53 -0
  12. package/policy/style_lint/vscode_extensions/vscode_extensions.rego +23 -0
  13. package/policy/style_lint/vscode_extensions/vscode_extensions_test.rego +39 -0
  14. package/policy/style_lint/vscode_settings/vscode_settings.rego +24 -0
  15. package/policy/style_lint/vscode_settings/vscode_settings_test.rego +49 -0
  16. package/policy/text/vscode_extensions/vscode_extensions.rego +36 -0
  17. package/policy/text/vscode_extensions/vscode_extensions_test.rego +51 -0
  18. package/policy/text/vscode_settings/vscode_settings.rego +56 -0
  19. package/policy/text/vscode_settings/vscode_settings_test.rego +85 -0
  20. package/scripts/check-graphql.mjs +18 -26
  21. package/scripts/check-js-run.mjs +18 -7
  22. package/scripts/check-nginx-default-tpl.mjs +28 -18
  23. package/scripts/check-style-lint.mjs +11 -33
  24. package/scripts/check-text.mjs +8 -66
  25. package/scripts/lint-conftest.mjs +27 -1
@@ -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
+ }
@@ -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
  }
@@ -89,70 +89,11 @@ async function checkV8rIgnore(passFn, failFn) {
89
89
  }
90
90
  }
91
91
 
92
- /**
93
- * Перевіряє VSCode extensions.json для текстового стека.
94
- * @param {(msg: string) => void} passFn callback при успішній перевірці
95
- * @param {(msg: string) => void} failFn callback при помилці
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' },
@@ -112,6 +114,18 @@ const TARGETS = [
112
114
  rule: 'style-lint',
113
115
  single: '.github/workflows/lint-style.yml'
114
116
  },
117
+ {
118
+ namespace: 'style_lint.vscode_extensions',
119
+ policyDir: 'style_lint',
120
+ rule: 'style-lint',
121
+ single: '.vscode/extensions.json'
122
+ },
123
+ {
124
+ namespace: 'style_lint.vscode_settings',
125
+ policyDir: 'style_lint',
126
+ rule: 'style-lint',
127
+ single: '.vscode/settings.json'
128
+ },
115
129
 
116
130
  // ── php ─────────────────────────────────────────────────────────────────
117
131
  { namespace: 'php.package_json', policyDir: 'php', rule: 'php', single: 'package.json' },
@@ -157,13 +171,19 @@ const TARGETS = [
157
171
  single: '.github/workflows/lint-js.yml'
158
172
  },
159
173
 
160
- // ── image-compress / capacitor ──────────────────────────────────────────
174
+ // ── image-compress / image-avif / capacitor ─────────────────────────────
161
175
  {
162
176
  namespace: 'image_compress.package_json',
163
177
  policyDir: 'image_compress',
164
178
  rule: 'image-compress',
165
179
  single: 'package.json'
166
180
  },
181
+ {
182
+ namespace: 'image_avif.package_json',
183
+ policyDir: 'image_avif',
184
+ rule: 'image-avif',
185
+ walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
186
+ },
167
187
  {
168
188
  namespace: 'capacitor.package_json',
169
189
  policyDir: 'capacitor',
@@ -214,6 +234,12 @@ const TARGETS = [
214
234
  rule: 'js-run',
215
235
  walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
216
236
  },
237
+ // `js_run.jsconfig` НЕ реєструємо тут — `jsconfig.json` має канонічну структуру
238
+ // лише для backend-пакетів (без `vite` у `devDependencies`) з каталогом `src/`,
239
+ // а lint-conftest фільтрує лише по `activeRules` на рівні репозиторію — не
240
+ // вміє пропустити окремий workspace-пакет за наявністю `vite`. Тому валідація
241
+ // структури делегується з `check-js-run.mjs` через `runConftestBatch` після
242
+ // того, як JS визначить, що пакет — backend з `src/`.
217
243
  {
218
244
  namespace: 'vue.package_json',
219
245
  policyDir: 'vue',