@nitra/cursor 1.8.206 → 1.8.208

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 (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/mdc/js-run.mdc +49 -2
  3. package/package.json +1 -1
  4. package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
  5. package/policy/abie/http_route_base/http_route_base.rego +45 -0
  6. package/policy/adr/settings_json/settings_json.rego +31 -0
  7. package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
  8. package/policy/bun/bunfig/bunfig.rego +33 -0
  9. package/policy/bun/package_json/package_json.rego +94 -0
  10. package/policy/capacitor/package_json/package_json.rego +45 -0
  11. package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
  12. package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
  13. package/policy/ga/git_ai/git_ai.rego +0 -26
  14. package/policy/ga/lint_ga/lint_ga.rego +0 -26
  15. package/policy/ga/workflow_common/workflow_common.rego +161 -0
  16. package/policy/graphql/package_json/package_json.rego +35 -0
  17. package/policy/hasura/svc_hl/svc_hl.rego +27 -0
  18. package/policy/image_compress/package_json/package_json.rego +94 -0
  19. package/policy/js_bun_db/package_json/package_json.rego +28 -0
  20. package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
  21. package/policy/js_lint/package_json/package_json.rego +137 -0
  22. package/policy/js_mssql/package_json/package_json.rego +57 -0
  23. package/policy/js_run/configmap/configmap.rego +45 -0
  24. package/policy/js_run/jsconfig/jsconfig.rego +66 -0
  25. package/policy/js_run/package_json/package_json.rego +31 -0
  26. package/policy/k8s/manifest/manifest.rego +130 -0
  27. package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
  28. package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
  29. package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
  30. package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
  31. package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
  32. package/policy/php/package_json/package_json.rego +19 -0
  33. package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
  34. package/policy/style_lint/package_json/package_json.rego +49 -0
  35. package/policy/text/cspell/cspell.rego +91 -0
  36. package/policy/text/markdownlint/markdownlint.rego +21 -0
  37. package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
  38. package/policy/text/package_json/package_json.rego +88 -0
  39. package/policy/vue/package_json/package_json.rego +54 -0
  40. package/scripts/check-adr.mjs +3 -2
  41. package/scripts/check-bun.mjs +21 -117
  42. package/scripts/check-graphql.mjs +6 -45
  43. package/scripts/check-hasura.mjs +2 -3
  44. package/scripts/check-image-avif.mjs +3 -3
  45. package/scripts/check-image-compress.mjs +25 -132
  46. package/scripts/check-js-bun-db.mjs +3 -50
  47. package/scripts/check-js-run.mjs +84 -86
  48. package/scripts/check-k8s.mjs +4 -4
  49. package/scripts/check-npm-module.mjs +17 -8
  50. package/scripts/check-php.mjs +16 -51
  51. package/scripts/check-style-lint.mjs +28 -52
  52. package/scripts/check-text.mjs +47 -219
  53. package/scripts/check-vue.mjs +3 -16
  54. package/scripts/lint-conftest.mjs +351 -0
  55. package/scripts/lint-ga.mjs +39 -2
  56. package/scripts/run-shellcheck-text.mjs +2 -2
  57. package/scripts/utils/conn-file-rules.mjs +170 -0
@@ -5648,8 +5648,8 @@ export function imageReplaceDeploymentPatchInfo(patchObj) {
5648
5648
 
5649
5649
  /** @type {Array<{ containerIndex: number, newImage: string, opIndex: number }>} */
5650
5650
  const ops = []
5651
- for (let i = 0; i < parsedArr.length; i++) {
5652
- const op = asPlainObject(parsedArr[i])
5651
+ for (const [i, element] of parsedArr.entries()) {
5652
+ const op = asPlainObject(element)
5653
5653
  if (op === null) continue
5654
5654
  const containerIndex = singleImageReplaceContainerIndex(op)
5655
5655
  if (containerIndex === null) continue
@@ -6018,7 +6018,7 @@ function applyConversionsToDoc(doc, conversions) {
6018
6018
  byPatch.set(c.index, slot)
6019
6019
  }
6020
6020
 
6021
- const sortedIdx = [...byPatch.keys()].sort((a, b) => b - a)
6021
+ const sortedIdx = byPatch.keys().toSorted((a, b) => b - a)
6022
6022
  for (const i of sortedIdx) {
6023
6023
  const slot = byPatch.get(i)
6024
6024
  if (slot === undefined) continue
@@ -6071,7 +6071,7 @@ function rewriteInlinePatchWithoutOps(patchText, opIndices) {
6071
6071
  const seq = inner.contents
6072
6072
  if (!isSeq(seq)) return null
6073
6073
 
6074
- const toRemove = [...new Set(opIndices)].sort((a, b) => b - a)
6074
+ const toRemove = new Set(opIndices).toSorted((a, b) => b - a)
6075
6075
  for (const i of toRemove) {
6076
6076
  if (i < 0 || i >= seq.items.length) return null
6077
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()
@@ -1,62 +1,43 @@
1
1
  /**
2
2
  * Перевіряє текстовий стек і форматування за правилом text.mdc.
3
3
  *
4
- * oxfmt: `.oxfmtrc.json` з обовʼязковими ключами та масивом ignorePatterns (два канонічні glob-и з text.mdc для hasura metadata і schema.graphql),
5
- * VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
6
- * відсутність Prettier у конфігах і залежностях.
4
+ * **Що тут лишилося** (FS / VSCode-конфіги / markdown / лінт-скрипт):
5
+ * - `.v8rignore` (текстовий формат, рядки шляхів);
6
+ * - `.vscode/extensions.json` рекомендації (markdownlint, oxc, shellcheck) і
7
+ * `.vscode/settings.json` (`editor.formatOnSave`, `[lang].editor.defaultFormatter`);
8
+ * - наявність FS-файлів `.oxfmtrc.json`, `.cspell.json`, `.markdownlint-cli2.jsonc`,
9
+ * `package.json` (саме *існування* — структуру вже валідує Rego);
10
+ * - конфіги Prettier у корені (заборонено — FS);
11
+ * - абзац про український апостроф у `.cursor/rules/n-text.mdc` /
12
+ * `npm/mdc/text.mdc` (markdown-текст, не JSON/YAML);
13
+ * - складна валідація скрипта `lint-text` (cspell, markdownlint, v8r у трьох
14
+ * варіантах, run-shellcheck-text.mjs, обовʼязкові glob-и);
15
+ * - workflow `lint-text.yml` має крок `bun run lint-text`.
7
16
  *
8
- * cspell: `.cspell.json` з обовʼязковим набором `ignorePaths` (клон text.mdc: node_modules, vscode, git, report, svg, k8s yaml);
9
- * cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json); у кореневих **`devDependencies`**
10
- * дозволені лише **`@nitra/*`** (як у bun.mdc), зокрема **`@nitra/cspell-dict` ^2.0.0+**; без імпорту **`@cspell/dict-*`** у `.cspell.json`, заборона
11
- * `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
12
- * `.v8rignore` (vscode JSON),
13
- * workflow `lint-text.yml`, розширення VSCode (markdownlint, oxc, shellcheck), `run-shellcheck-text.mjs` у `lint-text`.
14
- *
15
- * Якщо є `.cursor/rules/n-text.mdc` і/або `npm/mdc/text.mdc`перевіряє наявність абзацу про український
16
- * апостроф (U+0027 vs U+2019) і приклад з символом U+2019 у тексті.
17
+ * **Що покрила Rego** (`bun run lint-conftest`):
18
+ * - `npm/policy/text/oxfmtrc/` обовʼязкові ключі `.oxfmtrc.json` і канонічні
19
+ * значення (semi/singleQuote/tabWidth/useTabs/printWidth) + `ignorePatterns`
20
+ * канонічні glob-и;
21
+ * - `npm/policy/text/cspell/` — `.cspell.json` `version "0.2"`, `language`,
22
+ * імпорт `@nitra/cspell-dict`, заборона `@cspell/dict-*`, обовʼязкові
23
+ * `ignorePaths`;
24
+ * - `npm/policy/text/markdownlint/``.markdownlint-cli2.jsonc` `gitignore: true`
25
+ * (працює лише якщо файл валідний JSON без коментарів);
26
+ * - `npm/policy/text/package_json/` — заборона Prettier (`prettier` поле +
27
+ * `prettier`/`@nitra/prettier-config` у залежностях), `@nitra/cspell-dict ^2.0.0+`
28
+ * у `devDependencies`, заборона `markdownlint-cli2` у залежностях.
29
+ * - `npm/policy/bun/package_json/` — у `devDependencies` лише `@nitra/*`
30
+ * (раніше дублювалося тут).
17
31
  */
18
32
  import { existsSync } from 'node:fs'
19
33
  import { readFile } from 'node:fs/promises'
20
34
 
21
- import { isAllowedRootDevDependency } from './check-bun.mjs'
22
35
  import { createCheckReporter } from './utils/check-reporter.mjs'
23
36
  import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
24
37
 
25
- const WORKSPACE_STAR_RE = /^workspace:\*/
26
- const VERSION_PREFIX_RE = /^[\^~>=<]+\s*/
27
- const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/
28
-
29
38
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
30
39
  const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
31
40
 
32
- /** Мінімальні glob-и в `ignorePatterns` у `.oxfmtrc.json` (text.mdc) — додаткові патерни локально дозволені. */
33
- const OXFMT_REQUIRED_IGNORE_PATTERNS = ['**/hasura/metadata/**', '**/schema.graphql', '**/auto-imports.d.ts']
34
-
35
- /** Канонічні записи `ignorePaths` у `.cspell.json` (text.mdc) — кожен має бути присутнім. */
36
- const CSPELL_REQUIRED_IGNORE_PATHS = [
37
- '**/node_modules/**',
38
- '**/vscode-extension/**',
39
- '**/.git/**',
40
- '.vscode',
41
- 'report',
42
- '*.svg',
43
- '**/k8s/**/*.yaml'
44
- ]
45
-
46
- /**
47
- * Чи діапазон версії `@nitra/cspell-dict` у package.json означає лінію 2.0.0+ (з цієї версії словники входять у пакет).
48
- * @param {string|undefined} range наприклад "^2.0.0"
49
- * @returns {boolean} true якщо мажорна версія >= 2
50
- */
51
- function cspellDictVersionAtLeast200(range) {
52
- if (typeof range !== 'string' || !range.trim()) return false
53
- const cleaned = range.trim().replace(WORKSPACE_STAR_RE, '').replace(VERSION_PREFIX_RE, '')
54
- const m = cleaned.match(SEMVER_RE)
55
- if (!m) return false
56
- const major = Number(m[1])
57
- return major >= 2
58
- }
59
-
60
41
  /**
61
42
  * Перевіряє абзац про український апостроф у вмісті правила text.
62
43
  * @param {string} filePath шлях до файлу (для повідомлень)
@@ -74,8 +55,8 @@ function verifyUkApostropheRuleParagraph(filePath, body, failFn, passFn) {
74
55
  failFn(`${filePath}: абзац про апостроф має містити позначки U+0027 та U+2019`)
75
56
  return
76
57
  }
77
- if (!body.includes('\u2019')) {
78
- failFn(`${filePath}: у прикладі має бути типографський символ U+2019 (\u2019)`)
58
+ if (!body.includes('')) {
59
+ failFn(`${filePath}: у прикладі має бути типографський символ U+2019 ()`)
79
60
  return
80
61
  }
81
62
  passFn(`${filePath}: абзац про український апостроф на місці`)
@@ -174,122 +155,38 @@ async function checkVscodeText(passFn, failFn) {
174
155
  }
175
156
 
176
157
  /**
177
- * Перевіряє .oxfmtrc.json.
158
+ * FS-existence стек текстових конфігів. Контент-валідація — у Rego
159
+ * (`text.oxfmtrc`, `text.cspell`, `text.markdownlint`).
178
160
  * @param {(msg: string) => void} passFn callback при успішній перевірці
179
161
  * @param {(msg: string) => void} failFn callback при помилці
162
+ * @returns {Promise<void>}
180
163
  */
181
- async function checkOxfmtRc(passFn, failFn) {
182
- if (!existsSync('.oxfmtrc.json')) {
183
- failFn('.oxfmtrc.json не існує — створи його')
184
- return
185
- }
186
- const cfg = JSON.parse(await readFile('.oxfmtrc.json', 'utf8'))
187
- const requiredKeys = [
188
- 'arrowParens',
189
- 'printWidth',
190
- 'bracketSpacing',
191
- 'bracketSameLine',
192
- 'semi',
193
- 'singleQuote',
194
- 'tabWidth',
195
- 'trailingComma',
196
- 'useTabs'
197
- ]
198
- const missing = requiredKeys.filter(k => !(k in cfg))
199
- if (missing.length === 0) {
200
- passFn('.oxfmtrc.json містить всі обовʼязкові ключі')
201
- } else {
202
- failFn(`.oxfmtrc.json відсутні ключі: ${missing.join(', ')}`)
203
- }
204
- if (cfg.semi !== false) failFn('.oxfmtrc.json: semi має бути false')
205
- if (cfg.singleQuote !== true) failFn('.oxfmtrc.json: singleQuote має бути true')
206
- if (cfg.tabWidth !== 2) failFn('.oxfmtrc.json: tabWidth має бути 2')
207
- if (cfg.useTabs !== false) failFn('.oxfmtrc.json: useTabs має бути false')
208
- if (cfg.printWidth !== 120) failFn('.oxfmtrc.json: printWidth має бути 120')
209
-
210
- if (Array.isArray(cfg.ignorePatterns)) {
211
- const set = new Set(cfg.ignorePatterns)
212
- const missingPatterns = OXFMT_REQUIRED_IGNORE_PATTERNS.filter(p => !set.has(p))
213
- if (missingPatterns.length === 0) {
214
- passFn('.oxfmtrc.json: ignorePatterns містить hasura/metadata, schema.graphql і auto-imports.d.ts')
164
+ function checkTextConfigsExistence(passFn, failFn) {
165
+ for (const [path, mdcRef] of [
166
+ ['.oxfmtrc.json', 'text.oxfmtrc'],
167
+ ['.cspell.json', 'text.cspell'],
168
+ ['.markdownlint-cli2.jsonc', 'text.markdownlint']
169
+ ]) {
170
+ if (existsSync(path)) {
171
+ passFn(`${path} є (структуру перевіряє bun run lint-conftest → ${mdcRef})`)
215
172
  } else {
216
- failFn(
217
- `.oxfmtrc.json ignorePatterns: додай відсутні елементи: ${missingPatterns.join(', ')} (канонічний приклад у text.mdc)`
218
- )
173
+ failFn(`${path} не існує — створи згідно n-text.mdc`)
219
174
  }
220
- } else {
221
- failFn(`.oxfmtrc.json: додай масив ignorePatterns з ${OXFMT_REQUIRED_IGNORE_PATTERNS.join(', ')} (див. text.mdc)`)
222
- }
223
- }
224
-
225
- /**
226
- * Перевіряє залежності package.json для текстового стека.
227
- * @param {{ dependencies?: Record<string, string>, devDependencies?: Record<string, string>, prettier?: unknown }} pkg розібраний package.json
228
- * @param {Record<string, string>} devDeps devDependencies з package.json
229
- * @param {(msg: string) => void} passFn callback при успішній перевірці
230
- * @param {(msg: string) => void} failFn callback при помилці
231
- */
232
- function checkPackageJsonTextDepsUsage(pkg, devDeps, passFn, failFn) {
233
- const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
234
- for (const dep of ['prettier', '@nitra/prettier-config']) {
235
- if (allDeps[dep]) failFn(`package.json містить залежність ${dep} — видали її`)
236
- }
237
- if (pkg.prettier) failFn('package.json містить поле "prettier" — видали його')
238
-
239
- const nonNitraDev = Object.keys(devDeps).filter(n => !isAllowedRootDevDependency(n))
240
- if (nonNitraDev.length > 0) {
241
- failFn(
242
- `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${nonNitraDev.join(', ')} (bun.mdc)`
243
- )
244
- } else {
245
- passFn('Кореневі devDependencies лише @nitra/*')
246
- }
247
-
248
- const cspellRange = devDeps['@nitra/cspell-dict']
249
- if (!cspellRange) {
250
- failFn('@nitra/cspell-dict у devDependencies обовʼязковий для cspell — bun add -d @nitra/cspell-dict@^2.0.0')
251
- } else if (cspellDictVersionAtLeast200(cspellRange)) {
252
- passFn('@nitra/cspell-dict ^2.0.0+')
253
- } else {
254
- failFn('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
255
- }
256
-
257
- if (devDeps['markdownlint-cli2'] || (pkg.dependencies || {})['markdownlint-cli2']) {
258
- failFn(
259
- 'markdownlint-cli2 не додавай у dependencies/devDependencies — лише bunx у lint-text (n-text.mdc); прибери з package.json і bun i'
260
- )
261
- }
262
- }
263
-
264
- /**
265
- * Перевіряє відсутність прямих імпортів `@cspell/dict-*` у .cspell.json.
266
- * @param {(msg: string) => void} passFn callback при успішній перевірці
267
- * @param {(msg: string) => void} failFn callback при помилці
268
- */
269
- async function checkCspellJsonDictImports(passFn, failFn) {
270
- if (!existsSync('.cspell.json')) return
271
- const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
272
- const dictImports = (cfg.import || []).filter(i => typeof i === 'string' && i.includes('@cspell/dict-'))
273
- if (dictImports.length > 0) {
274
- failFn(
275
- `.cspell.json не має імпортувати @cspell/dict-* (${dictImports.join(', ')}) — використовуй лише @nitra/cspell-dict/cspell-ext.json`
276
- )
277
- } else {
278
- passFn('.cspell.json без прямих імпортів @cspell/dict-*')
279
175
  }
176
+ return Promise.resolve()
280
177
  }
281
178
 
282
179
  /**
283
- * Перевіряє package.json для текстового стека.
180
+ * Перевіряє package.json для текстового стека: складний `lint-text` скрипт і
181
+ * виклик `bun run lint-text` у відповідному workflow. Решта (Prettier-заборона,
182
+ * `@nitra/cspell-dict ^2.0.0+`, заборона `markdownlint-cli2` у залежностях,
183
+ * `@nitra/*` гейт) — у Rego (`text.package_json`, `bun.package_json`).
284
184
  * @param {(msg: string) => void} passFn callback при успішній перевірці
285
185
  * @param {(msg: string) => void} failFn callback при помилці
286
186
  */
287
187
  async function checkPackageJsonText(passFn, failFn) {
288
188
  if (!existsSync('package.json')) return
289
189
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
290
- const devDeps = pkg.devDependencies || {}
291
-
292
- checkPackageJsonTextDepsUsage(pkg, devDeps, passFn, failFn)
293
190
  checkLintTextScript(pkg.scripts?.['lint-text'], passFn, failFn)
294
191
 
295
192
  if (existsSync('.github/workflows/lint-text.yml')) {
@@ -304,8 +201,6 @@ async function checkPackageJsonText(passFn, failFn) {
304
201
  } else {
305
202
  failFn('.github/workflows/lint-text.yml не існує — створи згідно n-text.mdc')
306
203
  }
307
-
308
- await checkCspellJsonDictImports(passFn, failFn)
309
204
  }
310
205
 
311
206
  /**
@@ -344,71 +239,7 @@ function checkLintTextScript(lintText, passFn, failFn) {
344
239
  }
345
240
 
346
241
  /**
347
- * Перевіряє .markdownlint-cli2.jsonc.
348
- * @param {(msg: string) => void} pass callback при успішній перевірці
349
- * @param {(msg: string) => void} fail callback при помилці
350
- */
351
- async function checkMarkdownlintConfig(pass, fail) {
352
- if (!existsSync('.markdownlint-cli2.jsonc')) {
353
- fail('.markdownlint-cli2.jsonc не існує — створи згідно n-text.mdc')
354
- return
355
- }
356
- try {
357
- const ml = JSON.parse(await readFile('.markdownlint-cli2.jsonc', 'utf8'))
358
- pass('.markdownlint-cli2.jsonc існує і є валідним JSON')
359
- if (ml.gitignore === true) {
360
- pass('.markdownlint-cli2.jsonc: gitignore увімкнено')
361
- } else {
362
- fail('.markdownlint-cli2.jsonc: додай на верхньому рівні "gitignore": true (див. n-text.mdc)')
363
- }
364
- } catch {
365
- fail('.markdownlint-cli2.jsonc — невалідний JSON; перевір синтаксис')
366
- }
367
- }
368
-
369
- /**
370
- * Перевіряє .cspell.json на версію, мову, імпорт і ignorePaths.
371
- * @param {(msg: string) => void} pass callback при успішній перевірці
372
- * @param {(msg: string) => void} fail callback при помилці
373
- */
374
- async function checkCspellConfig(pass, fail) {
375
- if (!existsSync('.cspell.json')) {
376
- fail('.cspell.json не існує — створи його')
377
- return
378
- }
379
- const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
380
- if (cfg.version === '0.2') {
381
- pass('.cspell.json version: 0.2')
382
- } else {
383
- fail('.cspell.json version має бути "0.2"')
384
- }
385
- if (cfg.language) {
386
- pass(`.cspell.json language: "${cfg.language}"`)
387
- } else {
388
- fail('.cspell.json не містить поле language')
389
- }
390
- if ((cfg.import || []).some(i => i.includes('@nitra/cspell-dict'))) {
391
- pass('.cspell.json імпортує @nitra/cspell-dict')
392
- } else {
393
- fail('.cspell.json не імпортує @nitra/cspell-dict/cspell-ext.json')
394
- }
395
- if (Array.isArray(cfg.ignorePaths)) {
396
- pass('.cspell.json містить ignorePaths')
397
- } else {
398
- fail('.cspell.json не містить ignorePaths')
399
- }
400
- if (Array.isArray(cfg.ignorePaths)) {
401
- const missing = CSPELL_REQUIRED_IGNORE_PATHS.filter(p => !cfg.ignorePaths.includes(p))
402
- if (missing.length === 0) {
403
- pass(`.cspell.json ignorePaths містить усі обовʼязкові glob-и з text.mdc`)
404
- } else {
405
- fail(`.cspell.json ignorePaths бракує за замовчанням: ${missing.join(', ')} (див. text.mdc)`)
406
- }
407
- }
408
- }
409
-
410
- /**
411
- * Перевіряє відповідність проєкту правилам text.mdc (oxfmt, cspell, shellcheck у lint-text, markdownlint через bunx, v8r)
242
+ * Перевіряє відповідність проєкту правилам text.mdc.
412
243
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
413
244
  */
414
245
  export async function check() {
@@ -417,15 +248,12 @@ export async function check() {
417
248
 
418
249
  await checkV8rIgnore(pass, fail)
419
250
  await checkVscodeText(pass, fail)
420
- await checkOxfmtRc(pass, fail)
251
+ await checkTextConfigsExistence(pass, fail)
421
252
 
422
253
  for (const f of ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']) {
423
254
  if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
424
255
  }
425
256
 
426
- await checkMarkdownlintConfig(pass, fail)
427
- await checkCspellConfig(pass, fail)
428
-
429
257
  const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
430
258
  if (textRulePaths.length === 0) {
431
259
  pass('n-text.mdc / npm/mdc/text.mdc відсутні — перевірку абзацу про апостроф пропущено')