@nitra/cursor 1.13.34 → 1.13.40

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 (37) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/bin/n-cursor.js +4 -0
  3. package/package.json +1 -1
  4. package/rules/changelog/fix/consistency/check.mjs +100 -85
  5. package/rules/ci4/ci4.mdc +7 -7
  6. package/rules/ga/lint/lint.mjs +23 -3
  7. package/rules/ga/policy/lint_ga/lint_ga.rego +6 -0
  8. package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +6 -0
  9. package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  10. package/rules/js-run/fix/runtime/check.mjs +3 -0
  11. package/rules/js-run/js-run.mdc +16 -1
  12. package/rules/js-run/policy/package_json/package_json.rego +17 -0
  13. package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
  14. package/rules/k8s/fix/manifests/check.mjs +775 -139
  15. package/rules/k8s/k8s.mdc +52 -6
  16. package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
  17. package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
  18. package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
  19. package/rules/security/fix/trufflehog/check.mjs +3 -0
  20. package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
  21. package/rules/text/fix/formatting/check.mjs +1 -1
  22. package/rules/text/lint/lint.mjs +113 -5
  23. package/rules/text/policy/cspell/cspell.rego +1 -1
  24. package/rules/text/policy/lint_text/lint_text.rego +100 -0
  25. package/rules/text/policy/lint_text/target.json +4 -0
  26. package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +61 -0
  27. package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
  28. package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
  29. package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  30. package/rules/text/text.mdc +3 -57
  31. package/rules/vue/vue.mdc +1 -0
  32. package/scripts/sync-claude-config.mjs +2 -2
  33. package/scripts/utils/check-mdc-template-refs.mjs +15 -5
  34. package/scripts/utils/inline-template-links.mjs +15 -8
  35. package/scripts/utils/package-manifest.mjs +24 -19
  36. package/scripts/utils/run-conftest-batch.mjs +22 -15
  37. package/scripts/utils/template.mjs +89 -21
@@ -0,0 +1,61 @@
1
+ name: Lint Text
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ paths:
9
+ - '**/*.js'
10
+ - '**/*.ts'
11
+ - '**/*.vue'
12
+ - '**/*.html'
13
+ - '**/*.css'
14
+ - '**/*.scss'
15
+ - '**/*.less'
16
+ - '**/*.json'
17
+ - '**/*.jsonc'
18
+ - '**/*.yaml'
19
+ - '**/*.yml'
20
+ - '**/*.toml'
21
+ - '**/*.xml'
22
+ - '**/*.md'
23
+ - '**/*.mdc'
24
+ - '**/*.mdс'
25
+ - '**/*.txt'
26
+ - '**/*.go'
27
+ - '**/*.py'
28
+ - '**/*.php'
29
+ - '**/*.sh'
30
+
31
+ pull_request:
32
+ branches:
33
+ - dev
34
+ - main
35
+
36
+ concurrency:
37
+ group: ${{ github.ref }}-${{ github.workflow }}
38
+ cancel-in-progress: true
39
+
40
+ jobs:
41
+ text:
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ contents: read
45
+ steps:
46
+ - uses: actions/checkout@v6
47
+ with:
48
+ persist-credentials: false
49
+
50
+ - uses: ./.github/actions/setup-bun-deps
51
+
52
+ - name: Install shellcheck
53
+ run: sudo apt-get update && sudo apt-get install -y shellcheck
54
+
55
+ - name: Install dotenv-linter
56
+ run: >-
57
+ curl -sSfL https://git.io/JLbXn
58
+ | sh -s -- -b /usr/local/bin
59
+
60
+ - name: Lint text
61
+ run: bun run lint-text
@@ -47,7 +47,7 @@ deny contains msg if {
47
47
  msg := sprintf(".markdownlint-cli2.jsonc: %s.%s.%s має бути %v (text.mdc)", [section, inner_key, leaf, expected])
48
48
  }
49
49
 
50
- # ── deny: vкладеного обʼєкта взагалі немає ──────────────────────────────
50
+ # ── deny: вкладеного обʼєкта взагалі немає ──────────────────────────────
51
51
 
52
52
  deny contains msg if {
53
53
  some section, expected_inner in data.template.snippet
@@ -4,9 +4,5 @@
4
4
  "tabWidth": 2,
5
5
  "useTabs": false,
6
6
  "printWidth": 120,
7
- "ignorePatterns": [
8
- "**/hasura/metadata/**",
9
- "**/schema.graphql",
10
- "**/auto-imports.d.ts"
11
- ]
7
+ "ignorePatterns": ["**/hasura/metadata/**", "**/schema.graphql", "**/auto-imports.d.ts"]
12
8
  }
@@ -1,7 +1,3 @@
1
1
  {
2
- "recommendations": [
3
- "DavidAnson.vscode-markdownlint",
4
- "oxc.oxc-vscode",
5
- "timonwong.shellcheck"
6
- ]
2
+ "recommendations": ["DavidAnson.vscode-markdownlint", "oxc.oxc-vscode", "timonwong.shellcheck"]
7
3
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
3
3
  alwaysApply: true
4
- version: '1.28'
4
+ version: '1.29'
5
5
  ---
6
6
 
7
7
  **oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
@@ -172,63 +172,9 @@ version: '1.28'
172
172
 
173
173
  Додай workflow `.github/workflows/lint-text.yml`:
174
174
 
175
- ```yaml title=".github/workflows/lint-text.yml"
176
- name: Lint Text
177
-
178
- on:
179
- push:
180
- branches:
181
- - dev
182
- - main
183
- paths:
184
- - '**/*.js'
185
- - '**/*.ts'
186
- - '**/*.vue'
187
- - '**/*.html'
188
- - '**/*.css'
189
- - '**/*.scss'
190
- - '**/*.less'
191
- - '**/*.json'
192
- - '**/*.jsonc'
193
- - '**/*.yaml'
194
- - '**/*.yml'
195
- - '**/*.toml'
196
- - '**/*.xml'
197
- - '**/*.md'
198
- - '**/*.mdc'
199
- - '**/*.mdс'
200
- - '**/*.txt'
201
- - '**/*.go'
202
- - '**/*.py'
203
- - '**/*.php'
204
- - '**/*.sh'
205
-
206
- pull_request:
207
- branches:
208
- - dev
209
- - main
210
-
211
- concurrency:
212
- group: ${{ github.ref }}-${{ github.workflow }}
213
- cancel-in-progress: true
214
-
215
- jobs:
216
- text:
217
- runs-on: ubuntu-latest
218
- permissions:
219
- contents: read
220
- steps:
221
- - uses: actions/checkout@v6
222
- with:
223
- persist-credentials: false
224
-
225
- - uses: ./.github/actions/setup-bun-deps
226
-
227
- - name: Lint text
228
- run: bun run lint-text
229
- ```
175
+ - Канон: [lint-text.yml.snippet.yml](./policy/lint_text/template/lint-text.yml.snippet.yml)
230
176
 
231
- Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
177
+ Перед **`./.github/actions/setup-bun-deps`** — **`actions/checkout@v6`** (див. **ga.mdc**). Після composite — кроки **`Install shellcheck`** (apt) і **`Install dotenv-linter`** (curl), бо `n-cursor lint-text` вимагає обидва бінарники в PATH; на ubuntu-latest shellcheck часто вже є, dotenv-linter — ні. Composite: Node 24, Bun, кеш, `bun install --frozen-lockfile`.
232
178
 
233
179
  Не дублюй окремий workflow з тими самими кроками cspell/markdownlint.
234
180
 
package/rules/vue/vue.mdc CHANGED
@@ -118,6 +118,7 @@ GlobalRegistrator.register()
118
118
  ```
119
119
 
120
120
  `jsdom` не використовуй — happy-dom швидший і достатній для типових Vue-компонентних тестів.
121
+
121
122
  - **E2E:** **Playwright** змістовні сценарії користувацьких потоків.
122
123
 
123
124
  ### Інструменти (узгоджено з Vite і цим правилом)
@@ -84,7 +84,7 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
84
84
  /** Канонічний Cursor stop-hook для ADR capture. Cursor передає payload через stdin JSON. */
85
85
  const CURSOR_ADR_STOP_HOOK = Object.freeze({
86
86
  command: [
87
- "bash -lc 'root=\"$PWD\";",
87
+ 'bash -lc \'root="$PWD";',
88
88
  `if [ ! -f "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
89
89
  `bash "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}"'`
90
90
  ].join(' '),
@@ -94,7 +94,7 @@ const CURSOR_ADR_STOP_HOOK = Object.freeze({
94
94
  /** Канонічний Cursor stop-hook для ADR normalize. */
95
95
  const CURSOR_ADR_NORMALIZE_STOP_HOOK = Object.freeze({
96
96
  command: [
97
- "bash -lc 'root=\"$PWD\";",
97
+ 'bash -lc \'root="$PWD";',
98
98
  `if [ ! -f "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
99
99
  `bash "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}"'`
100
100
  ].join(' '),
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Returns list of template/ files that are NOT referenced in <id>.mdc as
3
3
  * markdown link targets. Paths returned are relative to ruleDir.
4
- *
5
- * @param {string} ruleDir absolute path to npm/rules/<id>/
6
- * @param {string} ruleId basename (e.g. "security")
7
- * @returns {Promise<string[]>}
8
4
  */
9
5
  import { existsSync } from 'node:fs'
10
6
  import { readdir, readFile, stat } from 'node:fs/promises'
11
7
  import { join, relative } from 'node:path'
12
8
 
9
+ /**
10
+ * @param {string} ruleDir абсолютний шлях до каталогу правила
11
+ * @returns {Promise<string[]>} абсолютні шляхи всіх файлів у template/
12
+ */
13
13
  async function walkTemplateDirs(ruleDir) {
14
14
  const out = []
15
15
  for (const kind of ['fix', 'policy']) {
@@ -18,13 +18,18 @@ async function walkTemplateDirs(ruleDir) {
18
18
  for (const concern of await readdir(kindDir)) {
19
19
  const tpl = join(kindDir, concern, 'template')
20
20
  if (!existsSync(tpl)) continue
21
- if (!(await stat(tpl)).isDirectory()) continue
21
+ const tplStat = await stat(tpl)
22
+ if (!tplStat.isDirectory()) continue
22
23
  out.push(...(await collectFiles(tpl)))
23
24
  }
24
25
  }
25
26
  return out.map(p => relative(ruleDir, p))
26
27
  }
27
28
 
29
+ /**
30
+ * @param {string} dir каталог для обходу
31
+ * @returns {Promise<string[]>} абсолютні шляхи знайдених файлів
32
+ */
28
33
  async function collectFiles(dir) {
29
34
  const out = []
30
35
  for (const entry of await readdir(dir, { withFileTypes: true })) {
@@ -35,6 +40,11 @@ async function collectFiles(dir) {
35
40
  return out
36
41
  }
37
42
 
43
+ /**
44
+ * @param {string} ruleDir абсолютний шлях до npm/rules/<id>/
45
+ * @param {string} ruleId basename правила (напр. "security")
46
+ * @returns {Promise<string[]>} відносні шляхи template-файлів без посилань у .mdc
47
+ */
38
48
  export async function findMissingMdcRefs(ruleDir, ruleId) {
39
49
  const mdcPath = join(ruleDir, `${ruleId}.mdc`)
40
50
  if (!existsSync(mdcPath)) return []
@@ -2,10 +2,14 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { basename, extname, join } from 'node:path'
4
4
 
5
- const TEMPLATE_LINK_RE = /\[([^\]]+)\]\((\.\/[^)]*\/template\/[^)]+)\)/g
5
+ const MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
6
+ const TEMPLATE_SEGMENT_RE = /\/template\//
6
7
  const SLOTS = ['snippet', 'deny', 'contains']
7
8
 
8
- /** @param {string} filePath */
9
+ /**
10
+ * @param {string} filePath шлях до файлу
11
+ * @returns {string} назва мови для fenced-блока
12
+ */
9
13
  function langFromExt(filePath) {
10
14
  const ext = extname(filePath)
11
15
  if (ext === '.json') return 'json'
@@ -16,10 +20,13 @@ function langFromExt(filePath) {
16
20
 
17
21
  // Strip `.<slot>.<ext>` suffix (slot ∈ snippet/deny/contains) to recover the
18
22
  // real target file name (e.g. `package.json.snippet.json` → `package.json`).
19
- /** @param {string} fileBasename */
23
+ /**
24
+ * @param {string} fileBasename базове ім'я template-файлу
25
+ * @returns {string} ім'я реального target-файлу
26
+ */
20
27
  function normalizeTargetName(fileBasename) {
21
28
  for (const slot of SLOTS) {
22
- const m = fileBasename.match(new RegExp(`^(.+)\\.${slot}\\.[^.]+$`))
29
+ const m = fileBasename.match(new RegExp(String.raw`^(.+)\.${slot}\.[^.]+$`))
23
30
  if (m) return m[1]
24
31
  }
25
32
  return fileBasename
@@ -29,19 +36,18 @@ function normalizeTargetName(fileBasename) {
29
36
  * Finds markdown links whose path contains /template/ and replaces them with
30
37
  * inline fenced blocks. Reads file from join(ruleDir, rel-path).
31
38
  * Throws Error if a matched link target doesn't exist (fail loud — user must know).
32
- *
33
39
  * @param {string} text .mdc file contents
34
40
  * @param {string} ruleDir absolute path to the rule directory (e.g. .../npm/rules/security/)
35
41
  * @returns {Promise<string>} transformed text
36
42
  */
37
43
  export async function inlineTemplateLinks(text, ruleDir) {
38
- const matches = [...text.matchAll(TEMPLATE_LINK_RE)]
44
+ const matches = [...text.matchAll(MD_LINK_RE)].filter(m => TEMPLATE_SEGMENT_RE.test(m[2]))
39
45
  if (matches.length === 0) return text
40
46
 
41
47
  let result = text
42
48
  for (const match of matches) {
43
49
  const [fullMatch, , href] = match
44
- // href starts with ./ and contains /template/ already guaranteed by regex
50
+ // href starts with ./ (regex) and contains /template/ (filter above)
45
51
  const relPath = href.slice(2) // strip leading ./
46
52
  const absPath = join(ruleDir, relPath)
47
53
 
@@ -49,7 +55,8 @@ export async function inlineTemplateLinks(text, ruleDir) {
49
55
  throw new Error(`inlineTemplateLinks: file not found: ${absPath} (referenced from .mdc)`)
50
56
  }
51
57
 
52
- const contents = (await readFile(absPath, 'utf8')).trim()
58
+ const raw = await readFile(absPath, 'utf8')
59
+ const contents = raw.trim()
53
60
  const lang = langFromExt(absPath)
54
61
  const targetName = normalizeTargetName(basename(absPath))
55
62
  const replacement = `\`${targetName}\`:\n\n\`\`\`${lang}\n${contents}\n\`\`\``
@@ -10,16 +10,24 @@ import { parse as parseToml } from 'smol-toml'
10
10
 
11
11
  import { getMonorepoPackageRootDirs } from './workspaces.mjs'
12
12
 
13
- /** @typedef {'npm' | 'python'} PackageKind */
14
-
15
13
  /**
14
+ @typedef {'npm' | 'python'} PackageKind
15
+ */
16
+
17
+ /*
16
18
  * @typedef {object} PackageManifest
17
- * @property {PackageKind} kind
19
+
20
+ * @property {PackageKind} kind поле
18
21
  * @property {string} ws відносний шлях воркспейсу (`'.'` для кореня)
22
+
19
23
  * @property {string} manifestRel `package.json` | `pyproject.toml`
24
+
20
25
  * @property {string | null} name ім'я пакета (npm / PyPI)
26
+
21
27
  * @property {string | null} version semver-рядок
28
+
22
29
  * @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
30
+
23
31
  * @property {string[] | null} [npmFiles] лише npm: `files` з package.json
24
32
  */
25
33
 
@@ -27,7 +35,7 @@ const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**'
27
35
 
28
36
  /**
29
37
  * @param {unknown} doc розпарсений pyproject.toml
30
- * @returns {{ name: string | null, version: string | null }}
38
+ * @returns {{ name: string | null, version: string | null }} витягнуті поля project / tool.poetry
31
39
  */
32
40
  function projectFieldsFromPyprojectDoc(doc) {
33
41
  if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
@@ -39,7 +47,7 @@ function projectFieldsFromPyprojectDoc(doc) {
39
47
  const p = /** @type {Record<string, unknown>} */ (project)
40
48
  return {
41
49
  name: typeof p.name === 'string' ? p.name : null,
42
- version: typeof p.version === 'string' ? p.version : null,
50
+ version: typeof p.version === 'string' ? p.version : null
43
51
  }
44
52
  }
45
53
  const tool = root.tool
@@ -49,7 +57,7 @@ function projectFieldsFromPyprojectDoc(doc) {
49
57
  const po = /** @type {Record<string, unknown>} */ (poetry)
50
58
  return {
51
59
  name: typeof po.name === 'string' ? po.name : null,
52
- version: typeof po.version === 'string' ? po.version : null,
60
+ version: typeof po.version === 'string' ? po.version : null
53
61
  }
54
62
  }
55
63
  }
@@ -58,7 +66,7 @@ function projectFieldsFromPyprojectDoc(doc) {
58
66
 
59
67
  /**
60
68
  * @param {string} text вміст pyproject.toml
61
- * @returns {{ name: string | null, version: string | null }}
69
+ * @returns {{ name: string | null, version: string | null }} витягнуті поля project / tool.poetry
62
70
  */
63
71
  export function parsePyprojectFields(text) {
64
72
  try {
@@ -70,7 +78,7 @@ export function parsePyprojectFields(text) {
70
78
 
71
79
  /**
72
80
  * @param {string} ws шлях воркспейсу
73
- * @returns {Promise<PackageManifest | null>}
81
+ * @returns {Promise<PackageManifest | null>} маніфест пакета або null
74
82
  */
75
83
  export async function readPackageManifest(ws) {
76
84
  const pkgPath = join(ws, 'package.json')
@@ -82,10 +90,7 @@ export async function readPackageManifest(ws) {
82
90
  }
83
91
  const pkg = /** @type {Record<string, unknown>} */ (parsed)
84
92
  const registryPublishable =
85
- typeof pkg.name === 'string' &&
86
- pkg.name.length > 0 &&
87
- pkg.private !== true &&
88
- Array.isArray(pkg.files)
93
+ typeof pkg.name === 'string' && pkg.name.length > 0 && pkg.private !== true && Array.isArray(pkg.files)
89
94
  return {
90
95
  kind: 'npm',
91
96
  ws,
@@ -93,7 +98,7 @@ export async function readPackageManifest(ws) {
93
98
  name: typeof pkg.name === 'string' ? pkg.name : null,
94
99
  version: typeof pkg.version === 'string' ? pkg.version : null,
95
100
  registryPublishable,
96
- npmFiles: Array.isArray(pkg.files) ? pkg.files : null,
101
+ npmFiles: Array.isArray(pkg.files) ? pkg.files : null
97
102
  }
98
103
  } catch {
99
104
  return null
@@ -113,14 +118,14 @@ export async function readPackageManifest(ws) {
113
118
  name: fields.name,
114
119
  version: fields.version,
115
120
  registryPublishable,
116
- npmFiles: null,
121
+ npmFiles: null
117
122
  }
118
123
  }
119
124
 
120
125
  /**
121
126
  * Каталоги пакетів: npm (`package.json` / workspaces) + Python (`pyproject.toml` без package.json).
122
- * @param {string} [repoRoot]
123
- * @returns {Promise<string[]>}
127
+ * @param {string} [repoRoot] параметр
128
+ * @returns {Promise<string[]>} результат
124
129
  */
125
130
  export async function getMonorepoProjectRootDirs(repoRoot = '.') {
126
131
  const roots = new Set(await getMonorepoPackageRootDirs(repoRoot))
@@ -149,9 +154,9 @@ export async function getMonorepoProjectRootDirs(repoRoot = '.') {
149
154
 
150
155
  /**
151
156
  * Шлях до файлу маніфесту воркспейсу.
152
- * @param {string} ws
153
- * @param {PackageManifest} manifest
154
- * @returns {string}
157
+ * @param {string} ws параметр
158
+ * @param {PackageManifest} manifest параметр
159
+ * @returns {string} результат
155
160
  */
156
161
  export function manifestFilePath(ws, manifest) {
157
162
  return join(ws, manifest.manifestRel)
@@ -22,7 +22,9 @@ import { fileURLToPath } from 'node:url'
22
22
 
23
23
  import { resolveCmd } from './resolve-cmd.mjs'
24
24
 
25
- /** Каталог пакета `@nitra/cursor`, від якого ресолвимо вшиті директорії правил. */
25
+ /**
26
+ Каталог пакета `@nitra/cursor`, від якого ресолвимо вшиті директорії правил.
27
+ */
26
28
  const PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
27
29
 
28
30
  /** Шлях до кореня правил. У npm-tarball публікується через `files: ["rules"]`. Кожне правило: `rules/<id>/policy/<name>/`. */
@@ -45,19 +47,27 @@ function failConftestMissing() {
45
47
  )
46
48
  }
47
49
 
48
- /**
50
+ /*
49
51
  * @typedef {object} ConftestViolation
52
+
50
53
  * @property {string} filename абсолютний шлях до файла, що дав порушення (з output conftest)
54
+
51
55
  * @property {string} message текст порушення (як у `deny` rego-пакета)
56
+
52
57
  * @property {string} namespace namespace rego-пакета (наприклад `abie.base_deployment_preem`)
53
58
  */
54
59
 
55
- /**
60
+ /*
56
61
  * @typedef {object} ConftestBatchOptions
62
+
57
63
  * @property {string} policyDirRel шлях до підкаталогу `npm/policy/...` (наприклад `abie/base_deployment_preem`)
64
+
58
65
  * @property {string} namespace повне імʼя rego-пакета (наприклад `abie.base_deployment_preem`)
66
+
59
67
  * @property {string[]} files список абсолютних шляхів файлів для перевірки (порожній — повертаємо порожньо)
68
+
60
69
  * @property {string[]} [extraArgs] додаткові аргументи для conftest (наприклад `--combine` для крос-документних правил)
70
+
61
71
  * @property {object} [templateData] опціональне merged-дерево; серіалізується у JSON `{ "template": <data> }` і передається як `--data <tmpfile>` (cleanup після завершення)
62
72
  */
63
73
 
@@ -65,18 +75,11 @@ function failConftestMissing() {
65
75
  * Pure args builder for conftest test. Extracted for unit-testability.
66
76
  * Preserves the existing args layout (files before -p; --output json --no-color
67
77
  * for parseable output); inserts --data right after --namespace when provided.
68
- * @param {{ policyAbs: string, namespace: string, files: string[], extraArgs: string[], tmpDataFile: string|null }} p
69
- * @returns {string[]}
78
+ * @param {{ policyAbs: string, namespace: string, files: string[], extraArgs: string[], tmpDataFile: string|null }} p параметри батчу
79
+ * @returns {string[]} args для виклику conftest
70
80
  */
71
81
  export function buildConftestArgs(p) {
72
- const args = [
73
- 'test',
74
- ...p.files,
75
- '-p',
76
- p.policyAbs,
77
- '--namespace',
78
- p.namespace
79
- ]
82
+ const args = ['test', ...p.files, '-p', p.policyAbs, '--namespace', p.namespace]
80
83
  if (p.tmpDataFile) args.push('--data', p.tmpDataFile)
81
84
  args.push('--output', 'json', '--no-color', ...p.extraArgs)
82
85
  return args
@@ -125,14 +128,18 @@ export function runConftestBatch(opts) {
125
128
  if (result.status !== 0 && result.status !== 1) {
126
129
  throw new Error(`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
127
130
  }
128
- /** @type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>} */
131
+ /**
132
+ @type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>}
133
+ */
129
134
  let parsed
130
135
  try {
131
136
  parsed = JSON.parse(result.stdout)
132
137
  } catch {
133
138
  throw new Error(`conftest stdout не парситься як JSON: ${(result.stdout || '').slice(0, 200)}`)
134
139
  }
135
- /** @type {ConftestViolation[]} */
140
+ /**
141
+ @type {ConftestViolation[]}
142
+ */
136
143
  const out = []
137
144
  for (const entry of parsed) {
138
145
  const failures = entry.failures ?? []