@nitra/cursor 1.8.204 → 1.8.207

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 (61) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/bin/auto-rules.md +2 -0
  3. package/mdc/rego.mdc +77 -0
  4. package/package.json +1 -1
  5. package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
  6. package/policy/abie/http_route_base/http_route_base.rego +45 -0
  7. package/policy/adr/settings_json/settings_json.rego +31 -0
  8. package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
  9. package/policy/bun/bunfig/bunfig.rego +33 -0
  10. package/policy/bun/package_json/package_json.rego +94 -0
  11. package/policy/capacitor/package_json/package_json.rego +45 -0
  12. package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
  13. package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
  14. package/policy/ga/git_ai/git_ai.rego +83 -0
  15. package/policy/ga/lint_ga/lint_ga.rego +118 -0
  16. package/policy/ga/workflow_common/workflow_common.rego +161 -0
  17. package/policy/graphql/package_json/package_json.rego +35 -0
  18. package/policy/hasura/svc_hl/svc_hl.rego +27 -0
  19. package/policy/image_compress/package_json/package_json.rego +94 -0
  20. package/policy/js_bun_db/package_json/package_json.rego +28 -0
  21. package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
  22. package/policy/js_lint/package_json/package_json.rego +137 -0
  23. package/policy/js_mssql/package_json/package_json.rego +57 -0
  24. package/policy/js_run/configmap/configmap.rego +45 -0
  25. package/policy/js_run/jsconfig/jsconfig.rego +66 -0
  26. package/policy/js_run/package_json/package_json.rego +31 -0
  27. package/policy/k8s/manifest/manifest.rego +130 -0
  28. package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
  29. package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
  30. package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
  31. package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
  32. package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
  33. package/policy/php/package_json/package_json.rego +19 -0
  34. package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
  35. package/policy/style_lint/package_json/package_json.rego +49 -0
  36. package/policy/text/cspell/cspell.rego +91 -0
  37. package/policy/text/markdownlint/markdownlint.rego +21 -0
  38. package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
  39. package/policy/text/package_json/package_json.rego +88 -0
  40. package/policy/vue/package_json/package_json.rego +54 -0
  41. package/scripts/auto-rules.mjs +10 -0
  42. package/scripts/check-adr.mjs +7 -3
  43. package/scripts/check-bun.mjs +21 -117
  44. package/scripts/check-ga.mjs +0 -284
  45. package/scripts/check-graphql.mjs +6 -45
  46. package/scripts/check-hasura.mjs +4 -5
  47. package/scripts/check-image-avif.mjs +3 -3
  48. package/scripts/check-image-compress.mjs +25 -132
  49. package/scripts/check-js-bun-db.mjs +3 -50
  50. package/scripts/check-js-run.mjs +9 -12
  51. package/scripts/check-k8s.mjs +6 -5
  52. package/scripts/check-npm-module.mjs +17 -8
  53. package/scripts/check-php.mjs +16 -51
  54. package/scripts/check-style-lint.mjs +28 -52
  55. package/scripts/check-text.mjs +47 -219
  56. package/scripts/check-vue.mjs +3 -16
  57. package/scripts/lint-conftest.mjs +351 -0
  58. package/scripts/lint-ga.mjs +49 -2
  59. package/scripts/lint-rego.mjs +67 -21
  60. package/scripts/run-shellcheck-text.mjs +3 -6
  61. package/scripts/utils/depcheck-workflow.mjs +2 -6
@@ -1,121 +1,32 @@
1
1
  /**
2
- * Перевіряє відповідність репозиторію правилу `image-compress.mdc`: канонічний скрипт
3
- * `lint-image` для оптимізації raster/SVG через `@nitra/minify-image` ≥ 3.2.0 (локально).
2
+ * Перевіряє вимоги правила `image-compress.mdc` для оптимізації raster/SVG через
3
+ * `@nitra/minify-image` ≥ 3.2.0 (локально).
4
4
  *
5
- * Очікування:
6
- * - у кореневому `package.json` є скрипт `lint-image`, який викликає `npx @nitra/minify-image`
7
- * з обовʼязковими `--src=.` і `--write`. Прапорець `--avif` у `lint-image` заборонений —
8
- * AVIF-генерацію виконує окреме правило `image-avif` (інакше `bun run lint` плодив би `.avif`
9
- * для зображень, що ніде не вживаються);
10
- * - якщо в `package.json` є агрегований скрипт `lint`, він викликає `bun run lint-image`
11
- * (симетрично до `lint-text`, `lint-js`, `lint-ga`);
12
- * - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
13
- * CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
14
- * - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
15
- * в `.gitignore` — він має бути в git. Локальний mtime-кеш у
16
- * `node_modules/.cache/@nitra/minify-image/mtime.tsv` авто-gitignored через `node_modules/`,
17
- * окремої перевірки не вимагає;
18
- * - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня — інакше
19
- * проєкт лишається у напівпереміщеному стані.
5
+ * **Що тут лишилося** (FS / cross-file):
6
+ * - наявність `package.json` у корені;
7
+ * - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
8
+ * в `.gitignore` він має бути в git;
9
+ * - застарілий `.minify-image-cache.tsv` версій < 3.2) видалений з кореня та
10
+ * з `.gitignore`.
11
+ *
12
+ * **Що покрила Rego** (`bun run lint-conftest`,
13
+ * `npm/policy/image_compress/package_json/`):
14
+ * - `scripts.lint-image` викликає `npx @nitra/minify-image --src=. --write`
15
+ * без `--avif` (AVIF окреме правило `image-avif`);
16
+ * - агрегований `lint` (якщо є) містить `bun run lint-image`;
17
+ * - `@nitra/minify-image` НЕ у `dependencies` / `devDependencies` (через `npx`).
20
18
  */
21
19
  import { existsSync } from 'node:fs'
22
20
  import { readFile } from 'node:fs/promises'
23
21
 
24
22
  import { createCheckReporter } from './utils/check-reporter.mjs'
25
23
 
26
- /** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
27
- const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
28
-
29
24
  /** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
30
25
  const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
31
26
 
32
27
  /** Імʼя застарілого 4-колонкового кешу (`@nitra/minify-image` < 3.2). Має бути видалений після міграції. */
33
28
  const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
34
29
 
35
- /**
36
- * Перевіряє скрипт `lint-image` у `package.json`.
37
- *
38
- * Має містити виклик `npx @nitra/minify-image` з обовʼязковими прапорцями `--src=.`
39
- * і `--write` (авто-оптимізація на місці). Прапорець `--avif` у `lint-image`
40
- * заборонений — AVIF-генерацію виконує `check image-avif`, інакше `bun run lint` плодить
41
- * `.avif` для зображень, що ніде не вживаються.
42
- * @param {string|undefined} lintImage значення `scripts['lint-image']`
43
- * @param {(msg: string) => void} pass callback при успішній перевірці
44
- * @param {(msg: string) => void} fail callback при помилці
45
- * @returns {void}
46
- */
47
- function checkLintImageScript(lintImage, pass, fail) {
48
- const canonical = `npx ${MINIFY_PACKAGE_NAME} --src=. --write`
49
- if (typeof lintImage !== 'string' || !lintImage.trim()) {
50
- fail(`package.json: додай скрипт "lint-image" з \`${canonical}\` (image-compress.mdc)`)
51
- return
52
- }
53
- if (!lintImage.includes(`npx ${MINIFY_PACKAGE_NAME}`)) {
54
- fail(`package.json: lint-image має викликати \`npx ${MINIFY_PACKAGE_NAME}\` (image-compress.mdc)`)
55
- return
56
- }
57
- /** @type {{ flag: string, variants: string[], hint: string }[]} */
58
- const requiredFlags = [
59
- { flag: '--src=.', variants: ['--src=.', '--src .'], hint: '`--src=.`' },
60
- { flag: '--write', variants: ['--write'], hint: '`--write` (авто-оптимізація на місці)' }
61
- ]
62
- const missing = requiredFlags.filter(f => !f.variants.some(v => lintImage.includes(v)))
63
- if (missing.length > 0) {
64
- fail(
65
- `package.json: lint-image має містити ${missing.map(f => f.hint).join(', ')} — канонічний виклик: \`${canonical}\` (image-compress.mdc)`
66
- )
67
- return
68
- }
69
- if (lintImage.includes('--avif')) {
70
- fail(
71
- `package.json: прибери \`--avif\` з lint-image — AVIF-генерацію виконує \`npx @nitra/cursor check image-avif\` (image-compress.mdc). Канонічний виклик: \`${canonical}\``
72
- )
73
- return
74
- }
75
- pass(`package.json: lint-image викликає \`${canonical}\``)
76
- }
77
-
78
- /**
79
- * Перевіряє, що агрегований `lint` (якщо є) кличе `bun run lint-image` —
80
- * симетрично до `lint-text`, `lint-js`, `lint-ga`.
81
- * @param {string|undefined} lintAggregate значення `scripts.lint`
82
- * @param {(msg: string) => void} pass callback при успішній перевірці
83
- * @param {(msg: string) => void} fail callback при помилці
84
- * @returns {void}
85
- */
86
- function checkLintAggregateIncludesImage(lintAggregate, pass, fail) {
87
- if (typeof lintAggregate !== 'string' || !lintAggregate.trim()) {
88
- return
89
- }
90
- if (lintAggregate.includes('bun run lint-image')) {
91
- pass('package.json: агрегований `lint` викликає `bun run lint-image`')
92
- } else {
93
- fail(
94
- 'package.json: у `lint` додай `bun run lint-image` (image-compress.mdc, симетрично до lint-text / lint-js / lint-ga)'
95
- )
96
- }
97
- }
98
-
99
- /**
100
- * Забороняє `@nitra/minify-image` у `dependencies` чи `devDependencies` —
101
- * CLI завжди запускається через `npx` (як `markdownlint-cli2` у `text.mdc`).
102
- * @param {{ dependencies?: Record<string, unknown>, devDependencies?: Record<string, unknown> }} pkg розібраний package.json
103
- * @param {(msg: string) => void} pass callback при успішній перевірці
104
- * @param {(msg: string) => void} fail callback при помилці
105
- * @returns {void}
106
- */
107
- function checkMinifyImageNotInDeps(pkg, pass, fail) {
108
- const inDeps = Boolean(pkg.dependencies && MINIFY_PACKAGE_NAME in pkg.dependencies)
109
- const inDevDeps = Boolean(pkg.devDependencies && MINIFY_PACKAGE_NAME in pkg.devDependencies)
110
- if (inDeps || inDevDeps) {
111
- fail(
112
- `package.json: ${MINIFY_PACKAGE_NAME} не додавай у dependencies/devDependencies — лише через \`npx\` (image-compress.mdc)`
113
- )
114
- } else {
115
- pass(`package.json: ${MINIFY_PACKAGE_NAME} не оголошено в dependencies/devDependencies`)
116
- }
117
- }
118
-
119
30
  /**
120
31
  * Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
121
32
  * @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
@@ -176,41 +87,23 @@ async function checkLegacyCacheRemoved(pass, fail) {
176
87
  }
177
88
 
178
89
  /**
179
- * Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
180
- * @param {(msg: string) => void} pass callback при успішній перевірці
181
- * @param {(msg: string) => void} fail callback при помилці
182
- * @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
183
- */
184
- async function checkPackageJsonImage(pass, fail) {
185
- if (!existsSync('package.json')) {
186
- fail('package.json не знайдено в корені — додай (image-compress.mdc)')
187
- return false
188
- }
189
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
190
- const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
191
- checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
192
- checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
193
- checkMinifyImageNotInDeps(pkg, pass, fail)
194
- return true
195
- }
196
-
197
- /**
198
- * Перевіряє відповідність проєкту правилу `image-compress.mdc`: канонічний `lint-image`
199
- * (через `npx @nitra/minify-image --src=. --write`, без `--avif`!), агрегований `lint`,
200
- * `@nitra/minify-image` не у залежностях, `.n-minify-image.tsv` НЕ в `.gitignore`,
201
- * застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image не вимагається —
202
- * лінт зображень виконується лише локально.
90
+ * Перевіряє відповідність проєкту правилу `image-compress.mdc`: `.n-minify-image.tsv` НЕ
91
+ * в `.gitignore`, застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image
92
+ * не вимагається лінт зображень виконується лише локально.
203
93
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
204
94
  */
205
95
  export async function check() {
206
96
  const reporter = createCheckReporter()
207
97
  const { pass, fail } = reporter
208
98
 
209
- const pkgFound = await checkPackageJsonImage(pass, fail)
210
- if (pkgFound) {
211
- await checkHashCacheNotIgnored(pass, fail)
212
- await checkLegacyCacheRemoved(pass, fail)
99
+ if (!existsSync('package.json')) {
100
+ fail('package.json не знайдено в корені — додай (image-compress.mdc)')
101
+ return reporter.getExitCode()
213
102
  }
103
+ pass('package.json є (структуру перевіряє bun run lint-conftest → image_compress.package_json)')
104
+
105
+ await checkHashCacheNotIgnored(pass, fail)
106
+ await checkLegacyCacheRemoved(pass, fail)
214
107
 
215
108
  return reporter.getExitCode()
216
109
  }
@@ -39,18 +39,6 @@ import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
39
39
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
40
40
  import { walkDir } from './utils/walkDir.mjs'
41
41
 
42
- /** Імена забороненої залежності у будь-якому `package.json`. */
43
- const FORBIDDEN_DEPENDENCIES = Object.freeze(['pg', 'pg-format', 'mysql2'])
44
-
45
- /**
46
- * @param {unknown} v parsed JSON
47
- * @returns {Record<string, unknown>} object або {}
48
- */
49
- function asObject(v) {
50
- if (!v || typeof v !== 'object' || Array.isArray(v)) return {}
51
- return /** @type {Record<string, unknown>} */ (v)
52
- }
53
-
54
42
  /**
55
43
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
56
44
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
@@ -74,43 +62,6 @@ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
74
62
  return paths
75
63
  }
76
64
 
77
- /**
78
- * Перевіряє, чи в кореневому `package.json` присутні заборонені пакети у `dependencies`.
79
- * @param {string[]} pkgJsonPaths абсолютні шляхи всіх `package.json` у репо
80
- * @param {string} repoRoot абсолютний шлях до кореня
81
- * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
82
- * @returns {Promise<number>} кількість знайдених порушень
83
- */
84
- async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
85
- const { pass, fail } = reporter
86
- let bad = 0
87
- for (const absPath of pkgJsonPaths) {
88
- const rel = relative(repoRoot, absPath).split('\\').join('/')
89
- let parsed
90
- try {
91
- parsed = JSON.parse(await readFile(absPath, 'utf8'))
92
- } catch {
93
- fail(`js-bun-db: ${rel} — невалідний JSON`)
94
- bad++
95
- continue
96
- }
97
- const deps = asObject(parsed.dependencies)
98
- for (const name of FORBIDDEN_DEPENDENCIES) {
99
- if (Object.hasOwn(deps, name)) {
100
- bad++
101
- fail(
102
- `js-bun-db: ${rel}: dependencies.${name} — замінити на Bun native SQL ` +
103
- `(import { sql, SQL } from 'bun', https://bun.com/docs/runtime/sql) (js-bun-db.mdc)`
104
- )
105
- }
106
- }
107
- }
108
- if (bad === 0) {
109
- pass(`js-bun-db: жоден package.json не містить ${FORBIDDEN_DEPENDENCIES.join(' / ')} у dependencies`)
110
- }
111
- return bad
112
- }
113
-
114
65
  /**
115
66
  * Сканує JS/TS-джерела на небезпечні патерни Bun SQL.
116
67
  * @param {string[]} sourcePaths абсолютні шляхи джерел
@@ -233,7 +184,9 @@ export async function check() {
233
184
  return reporter.getExitCode()
234
185
  }
235
186
 
236
- await checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter)
187
+ // Перевірку `dependencies` (заборона `pg` / `pg-format` / `mysql2`) перенесено
188
+ // в Rego-полісі `npm/policy/js_bun_db/package_json/`; `bun run lint-conftest`
189
+ // запускає її по всіх workspace-`package.json`. Тут лишився лише AST-скан коду.
237
190
 
238
191
  const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
239
192
  if (sourcePaths.length === 0) {
@@ -47,10 +47,7 @@ import {
47
47
  resolveConnDirFromPackageJson
48
48
  } from './utils/conn-imports-scan.mjs'
49
49
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
50
- import {
51
- findPromiseSetTimeoutInText,
52
- isPromiseSetTimeoutScanSourceFile
53
- } from './utils/promise-settimeout-scan.mjs'
50
+ import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from './utils/promise-settimeout-scan.mjs'
54
51
  import { walkDir } from './utils/walkDir.mjs'
55
52
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
56
53
 
@@ -68,9 +65,9 @@ const CANONICAL_BACKEND_JSCONFIG = Object.freeze({
68
65
 
69
66
  /**
70
67
  * Глибока рівність для JSON-подібних значень (масиви — порядок важливий).
71
- * @param {unknown} a
72
- * @param {unknown} b
73
- * @returns {boolean}
68
+ * @param {unknown} a перше значення
69
+ * @param {unknown} b друге значення
70
+ * @returns {boolean} true, якщо значення структурно ідентичні
74
71
  */
75
72
  function deepEqualJson(a, b) {
76
73
  if (a === b) return true
@@ -86,8 +83,8 @@ function deepEqualJson(a, b) {
86
83
  }
87
84
  const ao = /** @type {Record<string, unknown>} */ (a)
88
85
  const bo = /** @type {Record<string, unknown>} */ (b)
89
- const keysA = Object.keys(ao).sort()
90
- const keysB = Object.keys(bo).sort()
86
+ const keysA = Object.keys(ao).toSorted()
87
+ const keysB = Object.keys(bo).toSorted()
91
88
  if (keysA.length !== keysB.length) return false
92
89
  for (const [i, k] of keysA.entries()) {
93
90
  if (k !== keysB[i]) return false
@@ -99,7 +96,7 @@ function deepEqualJson(a, b) {
99
96
  /**
100
97
  * Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
101
98
  * @param {string} absPackageRoot абсолютний корінь пакета
102
- * @returns {boolean}
99
+ * @returns {boolean} true, якщо `src/` існує і є каталогом
103
100
  */
104
101
  function backendPackageHasSrcDir(absPackageRoot) {
105
102
  const srcPath = join(absPackageRoot, 'src')
@@ -115,8 +112,8 @@ function backendPackageHasSrcDir(absPackageRoot) {
115
112
  * @param {string} rootDir відносний шлях workspace
116
113
  * @param {string} absPackageRoot абсолютний корінь пакета
117
114
  * @param {string} label префікс `[pkg] `
118
- * @param {(msg: string) => void} fail
119
- * @param {(msg: string) => void} passFn
115
+ * @param {(msg: string) => void} fail callback для повідомлень про порушення
116
+ * @param {(msg: string) => void} passFn callback для повідомлень про успішну перевірку
120
117
  * @returns {Promise<void>}
121
118
  */
122
119
  async function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
@@ -524,7 +524,8 @@ function kustomizationPatchSortKey(patchItem) {
524
524
  const rec = /** @type {Record<string, unknown>} */ (patchItem)
525
525
  const t = rec.target
526
526
  /** @type {Record<string, unknown>} */
527
- const target = t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
527
+ const target =
528
+ t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
528
529
  const kind = typeof target.kind === 'string' ? target.kind : ''
529
530
  const name = typeof target.name === 'string' ? target.name : ''
530
531
  const ns = typeof target.namespace === 'string' ? target.namespace : ''
@@ -5647,8 +5648,8 @@ export function imageReplaceDeploymentPatchInfo(patchObj) {
5647
5648
 
5648
5649
  /** @type {Array<{ containerIndex: number, newImage: string, opIndex: number }>} */
5649
5650
  const ops = []
5650
- for (let i = 0; i < parsedArr.length; i++) {
5651
- const op = asPlainObject(parsedArr[i])
5651
+ for (const [i, element] of parsedArr.entries()) {
5652
+ const op = asPlainObject(element)
5652
5653
  if (op === null) continue
5653
5654
  const containerIndex = singleImageReplaceContainerIndex(op)
5654
5655
  if (containerIndex === null) continue
@@ -6017,7 +6018,7 @@ function applyConversionsToDoc(doc, conversions) {
6017
6018
  byPatch.set(c.index, slot)
6018
6019
  }
6019
6020
 
6020
- const sortedIdx = [...byPatch.keys()].sort((a, b) => b - a)
6021
+ const sortedIdx = byPatch.keys().toSorted((a, b) => b - a)
6021
6022
  for (const i of sortedIdx) {
6022
6023
  const slot = byPatch.get(i)
6023
6024
  if (slot === undefined) continue
@@ -6070,7 +6071,7 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
6070
6071
  const seq = inner.contents
6071
6072
  if (!isSeq(seq)) return null
6072
6073
 
6073
- const toRemove = [...new Set(opIndices)].sort((a, b) => b - a)
6074
+ const toRemove = new Set(opIndices).toSorted((a, b) => b - a)
6074
6075
  for (const i of toRemove) {
6075
6076
  if (i < 0 || i >= seq.items.length) return null
6076
6077
  seq.delete(i)
@@ -39,6 +39,9 @@ const TYPES_FILE_RE = /^\.\/types\/.+\.d\.(ts|mts)$/
39
39
  /** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
40
40
  const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
41
41
 
42
+ /** Поле `version` у текстовому зрізі `package.json` (для `git show HEAD:npm/package.json`). */
43
+ const PACKAGE_JSON_VERSION_RE = /"version":\s*"([^"]+)"/u
44
+
42
45
  /** Канонічний entrypoint типів для пакетів із вихідним `.js` під каталогом `npm/src` */
43
46
  const TYPES_INDEX = './types/index.d.ts'
44
47
 
@@ -246,7 +249,7 @@ async function checkEmitTypesConfig(passFn, failFn) {
246
249
  */
247
250
  /**
248
251
  * Чи виконано `git` у корені робочого дерева.
249
- * @returns {Promise<boolean>}
252
+ * @returns {Promise<boolean>} true, якщо процес запущено в межах git work tree
250
253
  */
251
254
  async function gitInsideWorkTree() {
252
255
  try {
@@ -273,7 +276,7 @@ async function gitDiffNameOnlyNpm() {
273
276
  /**
274
277
  * Поле `version` з `npm/package.json` на заданому git-ref (`HEAD:npm/package.json`).
275
278
  * @param {string} refPath на кшталт `HEAD:npm/package.json`
276
- * @returns {Promise<string | null>}
279
+ * @returns {Promise<string | null>} значення поля `version` або `null`, якщо ref недоступний
277
280
  */
278
281
  async function gitShowNpmPackageVersionAt(refPath) {
279
282
  try {
@@ -287,8 +290,8 @@ async function gitShowNpmPackageVersionAt(refPath) {
287
290
 
288
291
  /**
289
292
  * Версія з першого заголовка `## […]` у тексті CHANGELOG.
290
- * @param {string} changelogText
291
- * @returns {string | null}
293
+ * @param {string} changelogText вміст файлу CHANGELOG.md
294
+ * @returns {string | null} версія з першої секції або `null`, якщо заголовка немає
292
295
  */
293
296
  function firstChangelogSectionVersion(changelogText) {
294
297
  const m = changelogText.match(CHANGELOG_FIRST_VERSION_RE)
@@ -297,8 +300,8 @@ function firstChangelogSectionVersion(changelogText) {
297
300
 
298
301
  /**
299
302
  * Перший реліз у CHANGELOG має збігатися з `version` у `npm/package.json`.
300
- * @param {(msg: string) => void} passFn
301
- * @param {(msg: string) => void} failFn
303
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
304
+ * @param {(msg: string) => void} failFn callback при виявленому порушенні
302
305
  * @returns {Promise<void>}
303
306
  */
304
307
  async function checkChangelogTopMatchesPackageVersion(passFn, failFn) {
@@ -327,8 +330,8 @@ async function checkChangelogTopMatchesPackageVersion(passFn, failFn) {
327
330
 
328
331
  /**
329
332
  * Незакомічені зміни під `npm/` вимагають підвищення `version` відносно `HEAD`.
330
- * @param {(msg: string) => void} passFn
331
- * @param {(msg: string) => void} failFn
333
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
334
+ * @param {(msg: string) => void} failFn callback при виявленому порушенні
332
335
  * @returns {Promise<void>}
333
336
  */
334
337
  async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
@@ -360,6 +363,12 @@ async function checkDirtyNpmRequiresVersionBump(passFn, failFn) {
360
363
  passFn(`npm/: незакомічені зміни під npm/ узгоджені з підвищенням version (${headVer} → ${cur})`)
361
364
  }
362
365
 
366
+ /**
367
+ * Перевіряє npm-publish.yml workflow на наявність потрібних полів і кроків.
368
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
369
+ * @param {(msg: string) => void} failFn callback при виявленому порушенні
370
+ * @returns {Promise<void>}
371
+ */
363
372
  async function checkPublishWorkflow(passFn, failFn) {
364
373
  const publishWf = '.github/workflows/npm-publish.yml'
365
374
  if (!existsSync(publishWf)) {
@@ -1,79 +1,44 @@
1
1
  /**
2
2
  * Перевіряє вимоги правила php.mdc для PHP-проєктів.
3
3
  *
4
- * Очікування:
5
- * - у корені є `composer.json`;
6
- * - у `package.json` є скрипт `lint-php` (рекомендовано делегувати в `run-php.mjs`);
7
- * - у `.github/workflows/lint-php.yml` є крок `run: bun run lint-php` (для Bun-репозиторіїв).
4
+ * **Що тут лишилося** (FS-existence — не покривається conftest):
5
+ * - наявність `composer.json` у корені;
6
+ * - наявність `.github/workflows/lint-php.yml`.
7
+ *
8
+ * **Що покрила Rego** (`bun run lint-conftest`):
9
+ * - `npm/policy/php/package_json/` — наявність скрипта `lint-php` у `package.json`;
10
+ * - `npm/policy/php/lint_php_yml/` — крок `run: bun run lint-php` у workflow.
8
11
  */
9
12
  import { existsSync } from 'node:fs'
10
- import { readFile } from 'node:fs/promises'
11
13
 
12
14
  import { createCheckReporter } from './utils/check-reporter.mjs'
13
- import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
14
15
 
15
16
  /**
16
- * Перевіряє наявність `composer.json`.
17
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
17
+ * Перевіряє відповідність проєкту правилам php.mdc.
18
+ * @returns {Promise<number>} 0 все OK, 1 — є проблеми
18
19
  */
19
- function checkComposer(reporter) {
20
+ export async function check() {
21
+ const reporter = createCheckReporter()
20
22
  const { pass, fail } = reporter
23
+
21
24
  if (existsSync('composer.json')) {
22
25
  pass('composer.json існує')
23
26
  } else {
24
27
  fail('composer.json не знайдено в корені — додай (php.mdc)')
25
28
  }
26
- }
27
29
 
28
- /**
29
- * Перевіряє кореневий `package.json` на скрипт `lint-php`.
30
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
31
- */
32
- async function checkPackageJson(reporter) {
33
- const { pass, fail } = reporter
34
30
  if (!existsSync('package.json')) {
35
31
  fail('package.json не знайдено в корені — додай (php.mdc)')
36
- return
37
- }
38
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
39
- const lintPhp = pkg.scripts?.['lint-php']
40
- if (lintPhp) {
41
- pass('package.json містить скрипт lint-php')
42
32
  } else {
43
- fail('package.json: додай скрипт "lint-php" (php.mdc)')
33
+ pass('package.json є (наявність lint-php перевіряє bun run lint-conftest → php.package_json)')
44
34
  }
45
- }
46
35
 
47
- /**
48
- * Перевіряє workflow `lint-php.yml`.
49
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
50
- */
51
- async function checkWorkflow(reporter) {
52
- const { pass, fail } = reporter
53
36
  const wfPath = '.github/workflows/lint-php.yml'
54
- if (!existsSync(wfPath)) {
55
- fail(`${wfPath} не існує створи згідно php.mdc`)
56
- return
57
- }
58
- const content = await readFile(wfPath, 'utf8')
59
- pass('lint-php.yml існує')
60
- const root = parseWorkflowYaml(content)
61
- const ok = root ? anyRunStepIncludes(root, 'bun run lint-php') : content.includes('bun run lint-php')
62
- if (ok) {
63
- pass('lint-php.yml викликає bun run lint-php')
37
+ if (existsSync(wfPath)) {
38
+ pass(`${wfPath} є (структуру перевіряє bun run lint-conftest → php.lint_php_yml)`)
64
39
  } else {
65
- fail('lint-php.yml має містити крок run: bun run lint-php (php.mdc)')
40
+ fail(`${wfPath} не існує створи згідно php.mdc`)
66
41
  }
67
- }
68
42
 
69
- /**
70
- * Перевіряє відповідність проєкту правилам php.mdc.
71
- * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
72
- */
73
- export async function check() {
74
- const reporter = createCheckReporter()
75
- checkComposer(reporter)
76
- await checkPackageJson(reporter)
77
- await checkWorkflow(reporter)
78
43
  return reporter.getExitCode()
79
44
  }
@@ -1,74 +1,44 @@
1
1
  /**
2
2
  * Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
3
3
  *
4
- * Очікування: `@nitra/stylelint-config`, `lint-style` через `npx stylelint`, `.stylelintignore`,
5
- * workflow `lint-style.yml` `run` лише `npx stylelint`, не `bun run lint-style`), VSCode stylelint,
6
- * `css.validate` / `scss.validate` / `less.validate`: false.
4
+ * **Що тут лишилося** (FS / VSCode-конфіги не покривається conftest):
5
+ * - наявність зовнішнього файлу конфігу stylelint (`.stylelintrc.*`,
6
+ * `stylelint.config.js`) як альтернатива полю `stylelint` у `package.json`
7
+ * (cross-file: треба знати, чи є поле, чи немає);
8
+ * - `.stylelintignore` у корені;
9
+ * - `.vscode/extensions.json` recommendation `stylelint.vscode-stylelint`;
10
+ * - `.vscode/settings.json` `css.validate` / `scss.validate` / `less.validate: false`.
11
+ *
12
+ * **Що покрила Rego** (`bun run lint-conftest`):
13
+ * - `npm/policy/style_lint/package_json/` — скрипт `lint-style` через `npx stylelint`,
14
+ * `@nitra/stylelint-config` у `devDependencies`, поле `stylelint.extends`;
15
+ * - `npm/policy/style_lint/lint_style_yml/` — `npx stylelint` у `run` workflow.
7
16
  */
8
17
  import { existsSync } from 'node:fs'
9
18
  import { readFile } from 'node:fs/promises'
10
19
 
11
20
  import { createCheckReporter } from './utils/check-reporter.mjs'
12
- import { anyRunStepIncludesStylelint, parseWorkflowYaml } from './utils/gha-workflow.mjs'
13
21
 
14
22
  /**
15
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
23
+ * Альтернатива полю `stylelint` у `package.json` зовнішній файл конфігу. Якщо
24
+ * поля немає і файлу немає, фейлимося; якщо є хоч щось — пропускаємо. Поле
25
+ * `stylelint.extends == "@nitra/stylelint-config"` сам формат — у Rego.
26
+ * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер
16
27
  */
17
- async function checkPackageJson(reporter) {
28
+ async function checkStylelintConfigPresence(reporter) {
18
29
  const { pass, fail } = reporter
19
30
  if (!existsSync('package.json')) return
20
31
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
21
-
22
- const lintStyle = pkg.scripts?.['lint-style']
23
- if (lintStyle) {
24
- pass('package.json містить скрипт lint-style')
25
- if (String(lintStyle).includes('npx stylelint')) {
26
- pass('lint-style викликає stylelint через npx')
27
- } else {
28
- fail("lint-style має викликати stylelint через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix")
29
- }
30
- } else {
31
- fail('package.json не містить скрипт "lint-style"')
32
- }
33
-
34
- if (pkg.devDependencies?.['@nitra/stylelint-config']) {
35
- pass('@nitra/stylelint-config є в devDependencies')
36
- } else {
37
- fail('@nitra/stylelint-config відсутній — bun add -d @nitra/stylelint-config')
38
- }
39
-
40
- const stylelintCfg = pkg.stylelint
32
+ const hasField = pkg.stylelint && typeof pkg.stylelint === 'object'
41
33
  const hasExternalCfg =
42
34
  existsSync('.stylelintrc.json') || existsSync('.stylelintrc.js') || existsSync('stylelint.config.js')
43
- if (stylelintCfg?.extends === '@nitra/stylelint-config') {
44
- pass('package.json stylelint extends @nitra/stylelint-config')
45
- } else if (hasExternalCfg) {
46
- pass('Окремий файл конфігу stylelint існує')
35
+ if (hasField || hasExternalCfg) {
36
+ pass('Конфіг stylelint є — у package.json або окремим файлом')
47
37
  } else {
48
38
  fail('Немає конфігу stylelint — додай "stylelint": { "extends": "@nitra/stylelint-config" } до package.json')
49
39
  }
50
40
  }
51
41
 
52
- /**
53
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
54
- */
55
- async function checkStylelintWorkflow(reporter) {
56
- const { pass, fail } = reporter
57
- if (!existsSync('.github/workflows/lint-style.yml')) {
58
- fail('.github/workflows/lint-style.yml не існує — створи його')
59
- return
60
- }
61
- const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
62
- pass('lint-style.yml існує')
63
- const root = parseWorkflowYaml(content)
64
- const ok = root ? anyRunStepIncludesStylelint(root) : content.includes('npx stylelint')
65
- if (ok) {
66
- pass('lint-style.yml містить npx stylelint у кроці run')
67
- } else {
68
- fail("lint-style.yml має викликати stylelint у CI через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix")
69
- }
70
- }
71
-
72
42
  /**
73
43
  * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
74
44
  */
@@ -104,7 +74,7 @@ export async function check() {
104
74
  const reporter = createCheckReporter()
105
75
  const { pass, fail } = reporter
106
76
 
107
- await checkPackageJson(reporter)
77
+ await checkStylelintConfigPresence(reporter)
108
78
 
109
79
  if (existsSync('.stylelintignore')) {
110
80
  pass('.stylelintignore існує')
@@ -112,7 +82,13 @@ export async function check() {
112
82
  fail('.stylelintignore не існує — створи з вмістом: dist/')
113
83
  }
114
84
 
115
- await checkStylelintWorkflow(reporter)
85
+ const wfPath = '.github/workflows/lint-style.yml'
86
+ if (existsSync(wfPath)) {
87
+ pass(`${wfPath} є (структуру перевіряє bun run lint-conftest → style_lint.lint_style_yml)`)
88
+ } else {
89
+ fail(`${wfPath} не існує — створи його`)
90
+ }
91
+
116
92
  await checkVscodeStylelint(reporter)
117
93
 
118
94
  return reporter.getExitCode()