@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,46 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.40] - 2026-05-18
8
+
9
+ ### Fixed
10
+
11
+ - `lint-text` CLI: preflight на `shellcheck`, `patch` і `dotenv-linter` до ланцюжка cspell/shellcheck/dotenv. Канон `lint-text.yml.snippet.yml` — кроки `Install shellcheck` (apt) і `Install dotenv-linter` (curl); rego `text.lint_text`. Bump `text.mdc` `1.28` → `1.29`.
12
+
13
+ ## [1.13.39] - 2026-05-18
14
+
15
+ ### Fixed
16
+
17
+ - `lint-ga` CLI: preflight на `conftest` (поряд із `shellcheck`/`uv`) з install-hint; глобальний `catch` у `bin/n-cursor.js` більше не ковтає повідомлення `failConftestMissing()`. Канон `lint-ga.yml.snippet.yml` — крок `Install conftest` для CI; rego `ga.lint_ga` вимагає curl на release conftest.
18
+
19
+ ## [1.13.38] - 2026-05-18
20
+
21
+ ### Added
22
+
23
+ - `js-run` rule: у backend `package.json#scripts` заборонено `env $(cat …) bun` — заміна на `bun --env-file=…` (по файлу з `cat`); Rego `scriptsForbidden` `env-cat-bun`. Bump `js-run.mdc` `1.10` → `1.11`.
24
+
25
+ ## [1.13.37] - 2026-05-18
26
+
27
+ ### Added
28
+
29
+ - `js-run` rule: у backend `package.json#scripts` заборонено запуск через `node` — один runtime **Bun** у dev і prod; Rego `js_run.package_json` (`scriptsForbidden` у `package.json.deny.json`), frontend з `vite` у `devDependencies` пропускається. Bump `js-run.mdc` `1.9` → `1.10`.
30
+
31
+ ### Changed
32
+
33
+ - `k8s` rule: канон **NetworkPolicy** egress для всіх workload-ів — kube-dns; **TCP 80/443** на `0.0.0.0/0`; інші порти лише in-cluster (`namespaceSelector: {}`, `*.svc`). Заборонено `egress: [{}]`. Оновлено `buildNetworkPolicyYaml`, rego `k8s.network_policy`, template. Bump `k8s.mdc` `1.33` → `1.34`.
34
+
35
+ ## [1.13.36] - 2026-05-18
36
+
37
+ ### Changed
38
+
39
+ - `k8s` rule: **NetworkPolicy** обов'язковий не лише для **Deployment**, а й для **StatefulSet**, **DaemonSet**, **Job**, **CronJob** (`workloadAppLabel`, multi-doc `networkpolicy.yaml`, autofix/validate для всіх шарів `k8s`). HPA/PDB лишаються прив'язаними до Deployment. Bump `k8s.mdc` `1.32` → `1.33`.
40
+
41
+ ## [1.13.35] - 2026-05-18
42
+
43
+ ### Added
44
+
45
+ - `k8s` rule: для кожного **Deployment** під `k8s` обов'язковий **NetworkPolicy** — у `components/networkpolicy.yaml` для base (разом із HPA/PDB) або `networkpolicy.yaml` поруч у не-base оверлеях. Rego-пакет `k8s.network_policy`, перевірка прив'язки за `metadata.name` / міткою `app` у JS. **`check k8s`** автоматично створює відсутній `networkpolicy.yaml` і додає його в `components/kustomization.yaml` (`resources`). Bump `k8s.mdc` `1.31` → `1.32`.
46
+
7
47
  ## [1.13.34] - 2026-05-18
8
48
 
9
49
  ### Changed
@@ -192,7 +232,7 @@
192
232
 
193
233
  - `ga.mdc` — 4 inline YAML-блоки повних workflow канонів замінено на markdown-посилання до `template/<workflow>.yml.snippet.yml`. Файл скоротився суттєво — канон тепер живе як data, а не як прозовий приклад.
194
234
  - `template/clean-merged-branch.yml.snippet.yml`: `dry_run: false` (явний bool) замість `dry_run: no` — `yaml` npm (YAML 1.2) лишає `no` рядком, а Go-yaml у conftest нормалізує до `false`; пишемо канонізовану форму під runtime conftest-парсингу.
195
- - `docs/adr/template-dir-concern-inventory.md` — додано 4 нові full-canon ga.* концерни з ✓; оновлено summary (89 концернів, 43 з template — мігровано 13/43 = 30%).
235
+ - `docs/adr/template-dir-concern-inventory.md` — додано 4 нові full-canon ga.\* концерни з ✓; оновлено summary (89 концернів, 43 з template — мігровано 13/43 = 30%).
196
236
 
197
237
  ### TODO
198
238
 
package/bin/n-cursor.js CHANGED
@@ -1405,7 +1405,11 @@ try {
1405
1405
  } catch (error) {
1406
1406
  if (error instanceof ReexecHandoff) {
1407
1407
  process.exitCode = error.code
1408
+ } else if (error instanceof Error && error.message) {
1409
+ console.error(error.message)
1410
+ process.exitCode = 1
1408
1411
  } else {
1412
+ console.error(error)
1409
1413
  process.exitCode = 1
1410
1414
  }
1411
1415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.34",
3
+ "version": "1.13.40",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -26,7 +26,7 @@ import {
26
26
  getMonorepoProjectRootDirs,
27
27
  manifestFilePath,
28
28
  parsePyprojectFields,
29
- readPackageManifest,
29
+ readPackageManifest
30
30
  } from '../../../../scripts/utils/package-manifest.mjs'
31
31
 
32
32
  const execFileAsync = promisify(execFile)
@@ -46,10 +46,12 @@ const CHANGELOG_IGNORE_PATH_EXACT = Object.freeze(['docs', 'doc'])
46
46
  /** Таймаут на `npm view` / PyPI (мс) */
47
47
  const REGISTRY_TIMEOUT_MS = 10_000
48
48
 
49
+ const LEADING_DOTSLASH_RE = /^\.\//
50
+
49
51
  /**
50
52
  * Тихо запускає `git` і повертає stdout або `null` при будь-якій помилці.
51
53
  * @param {string[]} args аргументи `git`
52
- * @returns {Promise<string | null>}
54
+ * @returns {Promise<string | null>} результат
53
55
  */
54
56
  async function gitOrNull(args) {
55
57
  try {
@@ -61,7 +63,7 @@ async function gitOrNull(args) {
61
63
  }
62
64
 
63
65
  /**
64
- * @returns {Promise<boolean>}
66
+ * @returns {Promise<boolean>} результат
65
67
  */
66
68
  async function isInsideGitRepo() {
67
69
  const out = await gitOrNull(['rev-parse', '--is-inside-work-tree'])
@@ -69,7 +71,7 @@ async function isInsideGitRepo() {
69
71
  }
70
72
 
71
73
  /**
72
- * @returns {Promise<string | null>}
74
+ * @returns {Promise<string | null>} результат
73
75
  */
74
76
  async function currentBranchName() {
75
77
  const out = await gitOrNull(['rev-parse', '--abbrev-ref', 'HEAD'])
@@ -77,27 +79,27 @@ async function currentBranchName() {
77
79
  }
78
80
 
79
81
  /**
80
- * @param {string | null} branch
81
- * @returns {boolean}
82
+ * @param {string | null} branch параметр
83
+ * @returns {boolean} результат
82
84
  */
83
85
  function isIntegrationBranch(branch) {
84
86
  return branch !== null && INTEGRATION_BRANCHES.includes(branch)
85
87
  }
86
88
 
87
89
  /**
88
- * @param {string} ref
89
- * @returns {string}
90
+ * @param {string} ref параметр
91
+ * @returns {string} результат
90
92
  */
91
93
  function baseRefLabel(ref) {
92
94
  return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
93
95
  }
94
96
 
95
97
  /**
96
- * @param {string} relPath
97
- * @returns {boolean}
98
+ * @param {string} relPath параметр
99
+ * @returns {boolean} результат
98
100
  */
99
101
  function isChangelogIgnoredPath(relPath) {
100
- const p = relPath.replace(/\\/g, '/').replace(/^\.\//, '')
102
+ const p = relPath.replaceAll('\\', '/').replace(LEADING_DOTSLASH_RE, '')
101
103
  if (CHANGELOG_IGNORE_PATH_EXACT.includes(p)) {
102
104
  return true
103
105
  }
@@ -105,8 +107,8 @@ function isChangelogIgnoredPath(relPath) {
105
107
  }
106
108
 
107
109
  /**
108
- * @param {string} relPath
109
- * @returns {Promise<boolean>}
110
+ * @param {string} relPath параметр
111
+ * @returns {Promise<boolean>} результат
110
112
  */
111
113
  async function isPathGitIgnored(relPath) {
112
114
  try {
@@ -118,7 +120,7 @@ async function isPathGitIgnored(relPath) {
118
120
  }
119
121
 
120
122
  /**
121
- * @returns {Promise<string | null>}
123
+ * @returns {Promise<string | null>} результат
122
124
  */
123
125
  async function resolveBaseRef() {
124
126
  for (const name of BASE_BRANCH_CANDIDATES) {
@@ -133,8 +135,8 @@ async function resolveBaseRef() {
133
135
  }
134
136
 
135
137
  /**
136
- * @param {string} baseRef
137
- * @returns {Promise<string | null>}
138
+ * @param {string} baseRef параметр
139
+ * @returns {Promise<string | null>} результат
138
140
  */
139
141
  async function resolveMergeBase(baseRef) {
140
142
  const out = await gitOrNull(['merge-base', baseRef, 'HEAD'])
@@ -144,9 +146,9 @@ async function resolveMergeBase(baseRef) {
144
146
  }
145
147
 
146
148
  /**
147
- * @param {string} ws
148
- * @param {string[]} subWorkspaces
149
- * @returns {string[]}
149
+ * @param {string} ws параметр
150
+ * @param {string[]} subWorkspaces параметр
151
+ * @returns {string[]} результат
150
152
  */
151
153
  function pathspecForWorkspace(ws, subWorkspaces) {
152
154
  if (ws !== '.') return [`${ws}/`]
@@ -154,12 +156,14 @@ function pathspecForWorkspace(ws, subWorkspaces) {
154
156
  }
155
157
 
156
158
  /**
157
- * @param {string} baseRef
158
- * @param {string[]} pathspec
159
- * @returns {Promise<string[]>}
159
+ * @param {string} baseRef параметр
160
+ * @param {string[]} pathspec параметр
161
+ * @returns {Promise<string[]>} результат
160
162
  */
161
163
  async function listChangedPathsAgainstBase(baseRef, pathspec) {
162
- /** @type {string[]} */
164
+ /**
165
+ @type {string[]}
166
+ */
163
167
  const out = []
164
168
  const diffArgs =
165
169
  baseRef === 'HEAD'
@@ -177,10 +181,10 @@ async function listChangedPathsAgainstBase(baseRef, pathspec) {
177
181
  }
178
182
 
179
183
  /**
180
- * @param {string} baseRef
181
- * @param {string} ws
182
- * @param {string[]} subWorkspaces
183
- * @returns {Promise<boolean>}
184
+ * @param {string} baseRef параметр
185
+ * @param {string} ws параметр
186
+ * @param {string[]} subWorkspaces параметр
187
+ * @returns {Promise<boolean>} результат
184
188
  */
185
189
  async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces) {
186
190
  const pathspec = pathspecForWorkspace(ws, subWorkspaces)
@@ -199,9 +203,9 @@ async function workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces
199
203
 
200
204
  /**
201
205
  * Версія з маніфесту на `baseRef`.
202
- * @param {string} baseRef
203
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
204
- * @returns {Promise<string | null>}
206
+ * @param {string} baseRef параметр
207
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
208
+ * @returns {Promise<string | null>} результат
205
209
  */
206
210
  async function readBaseVersion(baseRef, manifest) {
207
211
  const wsPath = manifest.ws === '.' ? manifest.manifestRel : `${manifest.ws}/${manifest.manifestRel}`
@@ -219,9 +223,9 @@ async function readBaseVersion(baseRef, manifest) {
219
223
  }
220
224
 
221
225
  /**
222
- * @param {string} text
223
- * @param {string} version
224
- * @returns {boolean}
226
+ * @param {string} text параметр
227
+ * @param {string} version параметр
228
+ * @returns {boolean} результат
225
229
  */
226
230
  function changelogHasVersionEntry(text, version) {
227
231
  const needle = `## [${version}]`
@@ -229,8 +233,8 @@ function changelogHasVersionEntry(text, version) {
229
233
  }
230
234
 
231
235
  /**
232
- * @param {string} name
233
- * @returns {Promise<string | null>}
236
+ * @param {string} name параметр
237
+ * @returns {Promise<string | null>} результат
234
238
  */
235
239
  async function defaultGetPublishedNpmVersion(name) {
236
240
  try {
@@ -243,13 +247,13 @@ async function defaultGetPublishedNpmVersion(name) {
243
247
  }
244
248
 
245
249
  /**
246
- * @param {string} name
247
- * @returns {Promise<string | null>}
250
+ * @param {string} name параметр
251
+ * @returns {Promise<string | null>} результат
248
252
  */
249
253
  async function defaultGetPublishedPyPiVersion(name) {
250
254
  try {
251
255
  const res = await fetch(`https://pypi.org/pypi/${encodeURIComponent(name)}/json`, {
252
- signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
256
+ signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS)
253
257
  })
254
258
  if (!res.ok) return null
255
259
  const data = await res.json()
@@ -261,31 +265,38 @@ async function defaultGetPublishedPyPiVersion(name) {
261
265
  }
262
266
 
263
267
  /**
264
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
265
- * @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion
266
- * @returns {Promise<string | null>}
268
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
269
+ * @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion параметр
270
+ * @returns {Promise<string | null>} результат
267
271
  */
268
- async function resolvePublishedVersion(manifest, getPublishedVersion) {
269
- if (!manifest.name) return null
272
+ function resolvePublishedVersion(manifest, getPublishedVersion) {
273
+ if (!manifest.name) return Promise.resolve(null)
270
274
  return getPublishedVersion(manifest.name, manifest.kind)
271
275
  }
272
276
 
273
277
  /**
274
- * @returns {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>}
278
+ * @param {string} name пакет
279
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageKind} [kind] тип пакета
280
+ * @returns {Promise<string | null>} опублікована версія або null
275
281
  */
276
- function createDefaultGetPublishedVersion() {
277
- return async (name, kind = 'npm') => {
278
- if (kind === 'python') {
279
- return defaultGetPublishedPyPiVersion(name)
280
- }
281
- return defaultGetPublishedNpmVersion(name)
282
+ function defaultGetPublishedVersion(name, kind = 'npm') {
283
+ if (kind === 'python') {
284
+ return defaultGetPublishedPyPiVersion(name)
282
285
  }
286
+ return defaultGetPublishedNpmVersion(name)
287
+ }
288
+
289
+ /**
290
+ * @returns {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} стандартний резолвер
291
+ */
292
+ function createDefaultGetPublishedVersion() {
293
+ return defaultGetPublishedVersion
283
294
  }
284
295
 
285
296
  /**
286
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
287
- * @param {(msg: string) => void} pass
288
- * @param {(msg: string) => void} fail
297
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
298
+ * @param {(msg: string) => void} pass параметр
299
+ * @param {(msg: string) => void} fail параметр
289
300
  */
290
301
  function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
291
302
  if (manifest.kind !== 'npm' || !manifest.npmFiles) return
@@ -298,11 +309,11 @@ function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
298
309
  }
299
310
 
300
311
  /**
301
- * @param {string} ws
302
- * @param {string} version
303
- * @param {(msg: string) => void} pass
304
- * @param {(msg: string) => void} fail
305
- * @returns {Promise<boolean>}
312
+ * @param {string} ws параметр
313
+ * @param {string} version параметр
314
+ * @param {(msg: string) => void} pass параметр
315
+ * @param {(msg: string) => void} fail параметр
316
+ * @returns {Promise<boolean>} результат
306
317
  */
307
318
  async function verifyChangelogEntry(ws, version, pass, fail) {
308
319
  const label = ws === '.' ? '<root>' : ws
@@ -321,20 +332,20 @@ async function verifyChangelogEntry(ws, version, pass, fail) {
321
332
  }
322
333
 
323
334
  /**
324
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
325
- * @returns {string}
335
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
336
+ * @returns {string} результат
326
337
  */
327
338
  function workspaceLabel(manifest) {
328
339
  return manifest.ws === '.' ? '<root>' : manifest.ws
329
340
  }
330
341
 
331
342
  /**
332
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
333
- * @param {string} Vcurrent
334
- * @param {string[]} subWorkspaces
335
- * @param {(msg: string) => void} pass
336
- * @param {(msg: string) => void} fail
337
- * @returns {Promise<void>}
343
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
344
+ * @param {string} Vcurrent параметр
345
+ * @param {string[]} subWorkspaces параметр
346
+ * @param {(msg: string) => void} pass параметр
347
+ * @param {(msg: string) => void} fail параметр
348
+ * @returns {Promise<void>} результат
338
349
  */
339
350
  async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail) {
340
351
  const label = workspaceLabel(manifest)
@@ -379,12 +390,12 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
379
390
  }
380
391
 
381
392
  /**
382
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
383
- * @param {string[]} subWorkspaces
384
- * @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion
385
- * @param {(msg: string) => void} pass
386
- * @param {(msg: string) => void} fail
387
- * @returns {Promise<void>}
393
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
394
+ * @param {string[]} subWorkspaces параметр
395
+ * @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} getPublishedVersion параметр
396
+ * @param {(msg: string) => void} pass параметр
397
+ * @param {(msg: string) => void} fail параметр
398
+ * @returns {Promise<void>} результат
388
399
  */
389
400
  async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail) {
390
401
  const label = workspaceLabel(manifest)
@@ -415,11 +426,11 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
415
426
  }
416
427
 
417
428
  /**
418
- * @param {string} mergeBase
419
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest
420
- * @param {string} baseLabel
421
- * @param {(msg: string) => void} pass
422
- * @param {(msg: string) => void} fail
429
+ * @param {string} mergeBase параметр
430
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
431
+ * @param {string} baseLabel параметр
432
+ * @param {(msg: string) => void} pass параметр
433
+ * @param {(msg: string) => void} fail параметр
423
434
  */
424
435
  async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pass, fail) {
425
436
  const label = workspaceLabel(manifest)
@@ -442,10 +453,10 @@ async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pa
442
453
  }
443
454
 
444
455
  /**
445
- * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} localOnly
446
- * @param {string[]} subWorkspaces
447
- * @param {(msg: string) => void} pass
448
- * @param {(msg: string) => void} fail
456
+ * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} localOnly параметр
457
+ * @param {string[]} subWorkspaces параметр
458
+ * @param {(msg: string) => void} pass параметр
459
+ * @param {(msg: string) => void} fail параметр
449
460
  */
450
461
  async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
451
462
  if (localOnly.length === 0) return
@@ -483,9 +494,9 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
483
494
  }
484
495
 
485
496
  /**
486
- * @param {object} [opts]
497
+ * @param {object} [opts] опції перевірки
487
498
  * @param {(name: string, kind?: import('../../../../scripts/utils/package-manifest.mjs').PackageKind) => Promise<string | null>} [opts.getPublishedVersion] перевизначення npm/PyPI у тестах
488
- * @returns {Promise<number>}
499
+ * @returns {Promise<number>} exit-код перевірки
489
500
  */
490
501
  export async function check(opts = {}) {
491
502
  const reporter = createCheckReporter()
@@ -495,9 +506,13 @@ export async function check(opts = {}) {
495
506
  const workspaces = await getMonorepoProjectRootDirs(process.cwd())
496
507
  const subWorkspaces = workspaces.filter(w => w !== '.')
497
508
 
498
- /** @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} */
509
+ /**
510
+ @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]}
511
+ */
499
512
  const published = []
500
- /** @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]} */
513
+ /**
514
+ @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]}
515
+ */
501
516
  const localOnly = []
502
517
 
503
518
  for (const ws of workspaces) {
package/rules/ci4/ci4.mdc CHANGED
@@ -95,7 +95,7 @@ docs/
95
95
  - **Date** — рядок `**Date:** YYYY-MM-DD`, для впорядкування і відображення у проекції.
96
96
  - **Heading H1** — перший `#`-заголовок, як назва рішення.
97
97
  - **Розділи MADR** — `## Context and Problem Statement`, `## Considered Options`, `## Decision Outcome`, `### Consequences`, `## More Information`. Для проекції зазвичай беруться Decision Outcome + Consequences.
98
- - **`## Update YYYY-MM-DD`** — apended-секції у тому ж файлі. Враховуються як еволюція рішення в хронологічному порядку.
98
+ - **`## Update YYYY-MM-DD`** — appended-секції у тому ж файлі. Враховуються як еволюція рішення в хронологічному порядку.
99
99
 
100
100
  **Маршрутизація ADR → зони** робиться двома способами:
101
101
 
@@ -278,13 +278,13 @@ User Service автентифікує користувачів і керує п
278
278
 
279
279
  ## Типові поломки LLM і захист
280
280
 
281
- | Проблема | Захист |
282
- | ---------------------------- | --------------------------------------------------------------------------------- |
281
+ | Проблема | Захист |
282
+ | ---------------------------- | ----------------------------------------------------------------------------------------- |
283
283
  | Галюцинація деталей | Правило `[<slug>]` маркера + post-gen linter, що перевіряє існування `docs/adr/<slug>.md` |
284
- | Втрата `## Update`-еволюції | Pre-process: збирати всі `## Update YYYY-MM-DD` у ADR, передавати у промпт як патчі |
285
- | Дрейф термінології | `docs/glossary.md` як перший вхід у кожен промпт |
286
- | Перенасичення контексту | Двостадійна генерація: спочатку summary кожного ADR (кеш), потім проекція |
287
- | Inconsistency між проекціями | Cross-projection validator порівнює факти між документами |
284
+ | Втрата `## Update`-еволюції | Pre-process: збирати всі `## Update YYYY-MM-DD` у ADR, передавати у промпт як патчі |
285
+ | Дрейф термінології | `docs/glossary.md` як перший вхід у кожен промпт |
286
+ | Перенасичення контексту | Двостадійна генерація: спочатку summary кожного ADR (кеш), потім проекція |
287
+ | Inconsistency між проекціями | Cross-projection validator порівнює факти між документами |
288
288
 
289
289
  ## Валідатор (обов'язково)
290
290
 
@@ -1,5 +1,6 @@
1
1
  /**
2
- * CLI-обгортка над канонічним `lint-ga` (ga.mdc): робить preflight на `shellcheck` і `uv` (для `uvx`),
2
+ * CLI-обгортка над канонічним `lint-ga` (ga.mdc): робить preflight на `shellcheck`, `uv` (для `uvx`)
3
+ * і `conftest` (для rego-полісі у `check-ga`),
3
4
  * тоді послідовно виконує `bunx github-actionlint`, `uvx zizmor --offline --collect=workflows .` і
4
5
  * делегує до `check-ga.mjs::check()` — там і Rego-частина (через `runConftestBatch`),
5
6
  * і JS cross-file перевірки правил `ga.mdc`.
@@ -17,6 +18,10 @@
17
18
  * `uv` потрібен для `uvx zizmor`. Якщо його нема — `uvx zizmor` падає неінформативно («command not
18
19
  * found»); підказка з командою встановлення коротша й корисніша.
19
20
  *
21
+ * `conftest` потрібен для `check-ga.mjs::runAllGaRego` (`runConftestBatch`). Без preflight крок
22
+ * check-ga кидає виняток, який глобальний `catch` у `bin/n-cursor.js` раніше ковтав без логу —
23
+ * локально це виглядало як мовчазний exit 1.
24
+ *
20
25
  * Експортовано окремо `runLintGaCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-ga`.
21
26
  */
22
27
  import { platform } from 'node:process'
@@ -68,6 +73,21 @@ const UV_PREFLIGHT = {
68
73
  successMsg: '✅ uv знайдено в PATH — uvx zizmor запуститься'
69
74
  }
70
75
 
76
+ /** @type {PreflightDep} */
77
+ const CONFTEST_PREFLIGHT = {
78
+ bin: 'conftest',
79
+ winBins: ['conftest.exe'],
80
+ explanation: [
81
+ 'Без нього не запускається пер-документна валідація через rego-полісі (npm/rules/*/policy/)',
82
+ 'у кроці check-ga — `runConftestBatch` завершується hard-fail.'
83
+ ].join('\n '),
84
+ install: [
85
+ 'macOS: brew install conftest',
86
+ 'Universal: https://www.conftest.dev/install/'
87
+ ],
88
+ successMsg: '✅ conftest знайдено в PATH — check-ga виконає rego-полісі через runConftestBatch'
89
+ }
90
+
71
91
  /**
72
92
  * Шукає бінарник у PATH з урахуванням Windows: спершу `winBins`, потім `bin`.
73
93
  * @param {PreflightDep} dep опис залежності
@@ -116,7 +136,7 @@ function preflight(dep) {
116
136
  * Виконує канонічний `lint-ga` з preflight-перевірками і делегує до `check-ga.check()`.
117
137
  *
118
138
  * Послідовність:
119
- * 1) preflight: `shellcheck` (для actionlint SC-правил) і `uv` (для `uvx zizmor`); відсутній → exit 1;
139
+ * 1) preflight: `shellcheck`, `uv` (для `uvx zizmor`) і `conftest` (для check-ga); відсутній → exit 1;
120
140
  * 2) `bunx github-actionlint`;
121
141
  * 3) `uvx zizmor --offline --collect=workflows .`;
122
142
  * 4) `check-ga.mjs::check()` — Rego-полісі (батч conftest з `npm/policy/ga/`) + JS cross-file
@@ -132,7 +152,7 @@ function preflight(dep) {
132
152
  */
133
153
  export async function runLintGaCli() {
134
154
  let preflightOk = true
135
- for (const dep of [SHELLCHECK_PREFLIGHT, UV_PREFLIGHT]) {
155
+ for (const dep of [SHELLCHECK_PREFLIGHT, UV_PREFLIGHT, CONFTEST_PREFLIGHT]) {
136
156
  if (!preflight(dep)) preflightOk = false
137
157
  }
138
158
  if (!preflightOk) return 1
@@ -92,6 +92,12 @@ deny contains msg if {
92
92
  msg := sprintf("lint-ga.yml: має бути uses: %s (ga.mdc)", [required_use])
93
93
  }
94
94
 
95
+ deny contains msg if {
96
+ expected_run_blob != ""
97
+ not contains(job_run_blob, "open-policy-agent/conftest")
98
+ msg := "lint-ga.yml: має бути крок Install conftest (ga.mdc)"
99
+ }
100
+
95
101
  deny contains msg if {
96
102
  expected_run_blob != ""
97
103
  not contains(job_run_blob, "bun run lint-ga")
@@ -31,5 +31,11 @@ jobs:
31
31
 
32
32
  - uses: astral-sh/setup-uv@v8.0.0
33
33
 
34
+ - name: Install conftest
35
+ run: >-
36
+ curl -fsSL
37
+ https://github.com/open-policy-agent/conftest/releases/download/v0.62.0/conftest_0.62.0_Linux_x86_64.tar.gz
38
+ | sudo tar -xz -C /usr/local/bin conftest
39
+
34
40
  - name: Lint GA
35
41
  run: bun run lint-ga
@@ -1,7 +1,3 @@
1
1
  {
2
- "recommendations": [
3
- "dbaeumer.vscode-eslint",
4
- "github.vscode-github-actions",
5
- "oxc.oxc-vscode"
6
- ]
2
+ "recommendations": ["dbaeumer.vscode-eslint", "github.vscode-github-actions", "oxc.oxc-vscode"]
7
3
  }
@@ -28,6 +28,9 @@
28
28
  * (див. `utils/promise-settimeout-scan.mjs`);
29
29
  * - «jsconfig.json»: у backend-пакеті з каталогом `src/` у корені має бути `jsconfig.json`,
30
30
  * вміст якого збігається з каноном js-run.mdc (NodeNext і include на дерево `src`).
31
+ *
32
+ * Per-document валідація `package.json` (bunyan, `node` у `scripts`) делегована rego-пакету
33
+ * `js_run.package_json` у `npm/rules/js-run/policy/package_json/`; JS — cross-file (AST, FS).
31
34
  */
32
35
  import { existsSync, statSync } from 'node:fs'
33
36
  import { readFile } from 'node:fs/promises'
@@ -2,7 +2,7 @@
2
2
  description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
3
3
  globs: "**/package.json,**/jsconfig.json,**/src/**/*.{js,mjs,cjs,ts,tsx}"
4
4
  alwaysApply: false
5
- version: '1.9'
5
+ version: '1.11'
6
6
  ---
7
7
 
8
8
  ## Область застосування
@@ -17,6 +17,21 @@ version: '1.9'
17
17
 
18
18
  Тому **у frontend-пакетах не торкайся `process.env.*`** і **не додавай** `import { env } from 'node:process'`. Якщо натрапив на `process.env.NODE_ENV` у frontend-коді — заміна, якщо взагалі потрібна, лише на `import.meta.env.MODE`.
19
19
 
20
+ ## Runtime у `package.json#scripts`
21
+
22
+ У **backend**-пакетах (без `vite` у `devDependencies`) код запускають через **Bun**, не через бінарник **`node`** у значеннях `scripts`:
23
+
24
+ - `"start": "node src/index.js"` → `"start": "bun src/index.js"` (або `bun run …`, якщо так прийнято в репо);
25
+ - `node --watch app.js` → `bun --watch app.js`;
26
+ - `NODE_OPTIONS=… node app.js` → `NODE_OPTIONS=… bun app.js`.
27
+ - `env $(cat .env .env.local) bun src/index.js` → `bun --env-file=.env --env-file=.env.local src/index.js` (нативне завантаження env у Bun, без `env`/`cat`).
28
+
29
+ Заборонено викликати **`node`** у ланцюжках (`&&`, `;`, `|`). Заборонено обгортку **`env $(cat …) bun`** — файли з `cat` перелічуй у **`--env-file=`** (по одному прапорцю на файл, порядок як у `cat`). Допустимо: `bun`, `bunx`, `npx` (див. **bun.mdc**), інші CLI, якщо вони не підміняють рантайм на `node`.
30
+
31
+ Це **не** стосується поля `engines.node` (мінімальна версія Node для сумісності інструментів) і **не** стосується frontend-пакетів з `vite` у `devDependencies`.
32
+
33
+ Канон заборонених патернів у `scripts`: [package.json.deny.json](./policy/package_json/template/package.json.deny.json) (`scriptsForbidden`).
34
+
20
35
  ## Структура проекту
21
36
 
22
37
  Рекомендується використовувати таку структуру проекту:
@@ -18,3 +18,20 @@ deny contains msg if {
18
18
  pkg in object.keys(object.get(input, "devDependencies", {}))
19
19
  msg := sprintf("devDependencies.%s — %s", [pkg, reason])
20
20
  }
21
+
22
+ # ── deny: `node` як рантайм у `scripts` (backend-пакети без vite) ─────────
23
+
24
+ deny contains msg if {
25
+ js_run_backend_package
26
+ is_object(input.scripts)
27
+ some script_name, script_value in input.scripts
28
+ is_string(script_value)
29
+ some rule in object.get(data.template.deny, "scriptsForbidden", [])
30
+ regex.match(rule.pattern, script_value)
31
+ msg := sprintf("package.json: scripts.%s — %s", [script_name, rule.message])
32
+ }
33
+
34
+ # Frontend-пакети (`vite` у devDependencies) — поза js-run (див. js-run.mdc).
35
+ js_run_backend_package if {
36
+ not "vite" in object.keys(object.get(input, "devDependencies", {}))
37
+ }