@nitra/cursor 1.13.75 → 1.13.82
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.
- package/CHANGELOG.md +76 -0
- package/package.json +1 -1
- package/rules/abie/abie.mdc +11 -3
- package/rules/abie/policy/base_deployment_preem/base_deployment_preem.rego +9 -11
- package/rules/abie/policy/health_check_policy/health_check_policy.rego +9 -10
- package/rules/abie/policy/http_route_base/http_route_base.rego +7 -8
- package/rules/changelog/fix/consistency/check.mjs +16 -16
- package/{scripts/utils → rules/changelog/fix/consistency}/package-manifest.mjs +1 -1
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/fix/lint/check.mjs +3 -3
- package/{scripts/utils → rules/docker/fix/lint}/docker-hadolint.mjs +3 -3
- package/rules/docker/lint/lint.mjs +2 -2
- package/rules/ga/fix/workflows/check.mjs +5 -1
- package/rules/ga/ga.mdc +8 -1
- package/rules/ga/lint/lint.mjs +4 -1
- package/rules/ga/policy/workflow_common/template/uses-min-versions.snippet.json +4 -0
- package/rules/ga/policy/workflow_common/workflow_common.rego +89 -0
- package/rules/graphql/fix/tooling/check.mjs +1 -1
- package/{scripts/utils → rules/graphql/fix/tooling}/graphql-gql-scan.mjs +47 -12
- package/rules/hasura/fix/internal_urls/check.mjs +1 -1
- package/{scripts/utils → rules/js-bun-db/fix/safety}/bun-sql-scan.mjs +1 -1
- package/rules/js-bun-db/fix/safety/check.mjs +1 -1
- package/rules/js-lint/fix/tooling/check.mjs +3 -15
- package/{scripts/utils → rules/js-lint/fix/tooling}/rebuild-oxlint-canonical.mjs +1 -1
- package/rules/js-lint/js-lint.mdc +2 -2
- package/rules/js-mssql/fix/deps/check.mjs +1 -1
- package/{scripts/utils → rules/js-mssql/fix/deps}/mssql-pool-scan.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/bunyan-imports.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/check-env-scan.mjs +1 -1
- package/rules/js-run/fix/runtime/check.mjs +5 -5
- package/{scripts/utils → rules/js-run/fix/runtime}/conn-file-rules.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/conn-imports-scan.mjs +1 -1
- package/{scripts/utils → rules/js-run/fix/runtime}/promise-settimeout-scan.mjs +1 -1
- package/rules/k8s/k8s.mdc +1 -1
- package/rules/npm-module/fix/package_structure/check.mjs +11 -1
- package/rules/test/auto.md +1 -0
- package/rules/test/fix/location/check.mjs +77 -0
- package/rules/test/test.mdc +60 -0
- package/rules/vue/fix/packages/check.mjs +2 -2
- package/rules/vue/vue.mdc +1 -1
- package/scripts/auto-rules.mjs +3 -3
- package/scripts/utils/ast-scan-utils.mjs +3 -2
- package/scripts/utils/with-lock.mjs +120 -0
- package/scripts/utils/workspaces.mjs +1 -1
- package/scripts/utils/worktree-fingerprint.mjs +30 -0
- /package/{scripts/utils → rules/docker/fix/lint}/docker-mirror.mjs +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/knip-canonical.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical-skeleton.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-canonical.json +0 -0
- /package/{scripts/utils → rules/js-lint/fix/tooling}/oxlint-rules.tsv +0 -0
- /package/{scripts/utils → rules/vue/fix/packages}/vue-forbidden-imports.mjs +0 -0
|
@@ -55,6 +55,11 @@ setup_bun_no_checkout_template := concat(" ", [
|
|
|
55
55
|
"інакше runner не знайде action.yml (ga.mdc)",
|
|
56
56
|
])
|
|
57
57
|
|
|
58
|
+
min_uses_version_template := concat(" ", [
|
|
59
|
+
"jobs.%s.steps[%d]: %s має бути >= v%s (зараз %q) —",
|
|
60
|
+
"онови ref у uses: (ga.mdc)",
|
|
61
|
+
])
|
|
62
|
+
|
|
58
63
|
forbidden_run_command_template := concat(" ", [
|
|
59
64
|
"jobs.%s.steps[%d]: `%s` заборонено у workflow —",
|
|
60
65
|
"мігровано на knip (js-lint.mdc, ga.mdc)",
|
|
@@ -147,6 +152,28 @@ deny contains msg if {
|
|
|
147
152
|
msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
|
|
148
153
|
}
|
|
149
154
|
|
|
155
|
+
# ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
|
|
156
|
+
#
|
|
157
|
+
# Канон — `template/uses-min-versions.snippet.json` (через --data).
|
|
158
|
+
# Перевіряє semver-подібні теги `vX.Y.Z` / `vN`; SHA-pin (40 hex) пропускаємо.
|
|
159
|
+
|
|
160
|
+
deny contains msg if {
|
|
161
|
+
some entry in all_flat_steps
|
|
162
|
+
uses := object.get(entry.step, "uses", "")
|
|
163
|
+
uses != ""
|
|
164
|
+
some action_slug, min_ver in data.template.snippet
|
|
165
|
+
action_uses_matches(uses, action_slug)
|
|
166
|
+
ref := action_uses_ref(uses)
|
|
167
|
+
not action_ref_meets_min(ref, min_ver)
|
|
168
|
+
msg := sprintf(min_uses_version_template, [
|
|
169
|
+
entry.job_id,
|
|
170
|
+
entry.step_index,
|
|
171
|
+
action_slug,
|
|
172
|
+
min_ver,
|
|
173
|
+
ref,
|
|
174
|
+
])
|
|
175
|
+
}
|
|
176
|
+
|
|
150
177
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
151
178
|
|
|
152
179
|
# Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
|
|
@@ -182,3 +209,65 @@ has_checkout_before(job, before) if {
|
|
|
182
209
|
uses := object.get(step, "uses", "")
|
|
183
210
|
contains(uses, "actions/checkout@")
|
|
184
211
|
}
|
|
212
|
+
|
|
213
|
+
# `uses:` починається з `owner/repo@` для заданого slug.
|
|
214
|
+
action_uses_matches(uses, slug) if {
|
|
215
|
+
startswith(uses, concat("", [slug, "@"]))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Ref після останнього `@` у `uses:` (owner/repo@ref).
|
|
219
|
+
action_uses_ref(uses) := ref if {
|
|
220
|
+
parts := split(uses, "@")
|
|
221
|
+
count(parts) >= 2
|
|
222
|
+
ref := parts[count(parts) - 1]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# SHA-pin — semver-політика не застосовується.
|
|
226
|
+
action_ref_is_sha_pin(ref) if {
|
|
227
|
+
regex.match(`^[0-9a-fA-F]{40}$`, ref)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Semver ref >= min (обидва як X.Y.Z після optional `v`).
|
|
231
|
+
action_ref_meets_min(ref, _) if {
|
|
232
|
+
action_ref_is_sha_pin(ref)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
action_ref_meets_min(ref, min_ver) if {
|
|
236
|
+
not action_ref_is_sha_pin(ref)
|
|
237
|
+
version_triple_gte(version_triple(ref), version_triple(min_ver))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
version_triple(raw) := [major, minor, patch] if {
|
|
241
|
+
stripped := trim_prefix(trim_prefix(raw, "v"), "V")
|
|
242
|
+
parts := split_to_numbers(stripped)
|
|
243
|
+
major := version_part(parts, 0)
|
|
244
|
+
minor := version_part(parts, 1)
|
|
245
|
+
patch := version_part(parts, 2)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
version_part(parts, idx) := parts[idx] if {
|
|
249
|
+
count(parts) > idx
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
else := 0
|
|
253
|
+
|
|
254
|
+
version_triple_gte(a, b) if {
|
|
255
|
+
a[0] > b[0]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
version_triple_gte(a, b) if {
|
|
259
|
+
a[0] == b[0]
|
|
260
|
+
a[1] > b[1]
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
version_triple_gte(a, b) if {
|
|
264
|
+
a[0] == b[0]
|
|
265
|
+
a[1] == b[1]
|
|
266
|
+
a[2] >= b[2]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
split_to_numbers(spec) := nums if {
|
|
270
|
+
tokens := regex.split(`\D+`, spec)
|
|
271
|
+
non_empty := [t | some t in tokens; t != ""]
|
|
272
|
+
nums := [n | some t in non_empty; n := to_number(t)]
|
|
273
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
isGqlScanSourceFile,
|
|
16
16
|
shouldSkipFileForGqlScan,
|
|
17
17
|
sourceFileHasGqlTaggedTemplate
|
|
18
|
-
} from '
|
|
18
|
+
} from './graphql-gql-scan.mjs'
|
|
19
19
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
20
20
|
import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
|
|
21
21
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
@@ -1,18 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Пошук tagged template **`gql\`…\``** у джерелах для правила graphql.mdc.
|
|
3
3
|
*
|
|
4
|
-
* Для **`.vue`** береться лише вміст `<script>` / `<script setup>` (
|
|
4
|
+
* Для **`.vue`** береться лише вміст `<script>` / `<script setup>` (паралельна реалізація екстрактора;
|
|
5
|
+
* аналог у `rules/vue/fix/packages/vue-forbidden-imports.mjs` — модулі не діляться кодом, щоб уникати
|
|
6
|
+
* cross-rule імпортів і тримати кожне правило самодостатнім).
|
|
5
7
|
* Семантику визначає **oxc-parser** (`program`): рекурсивний обхід AST, збіг лише для **Identifier** з іменем **`gql`** як тега шаблону.
|
|
6
8
|
*/
|
|
7
9
|
import { parseSync } from 'oxc-parser'
|
|
8
10
|
|
|
9
|
-
import {
|
|
10
|
-
contentForVueImportScan,
|
|
11
|
-
isVueImportScanSourceFile,
|
|
12
|
-
shouldSkipFileForVueImportScan
|
|
13
|
-
} from './vue-forbidden-imports.mjs'
|
|
14
|
-
|
|
15
11
|
const VUE_EXTENSION_RE = /\.vue$/u
|
|
12
|
+
const SOURCE_FILE_RE = /\.(vue|[cm]?[jt]sx?)$/u
|
|
13
|
+
const VUE_SCRIPT_BLOCK_RE = /<script\b[^>]*>([\s\S]*?)<\/script>/gi
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Витягує з SFC лише код усередині `<script>` блоків, ігноруючи `<template>` і `<style>`.
|
|
17
|
+
* @param {string} sfc вміст .vue файлу
|
|
18
|
+
* @returns {string} конкатенований текст усіх блоків `<script>` (через подвійний переклад рядка)
|
|
19
|
+
*/
|
|
20
|
+
function extractVueScriptBlocks(sfc) {
|
|
21
|
+
const chunks = []
|
|
22
|
+
VUE_SCRIPT_BLOCK_RE.lastIndex = 0
|
|
23
|
+
let m = VUE_SCRIPT_BLOCK_RE.exec(sfc)
|
|
24
|
+
while (m) {
|
|
25
|
+
chunks.push(m[1])
|
|
26
|
+
m = VUE_SCRIPT_BLOCK_RE.exec(sfc)
|
|
27
|
+
}
|
|
28
|
+
return chunks.join('\n\n')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Підбирає текст для сканування: для .vue — лише script-блоки, інакше — увесь вміст.
|
|
33
|
+
* @param {string} content сирий вміст файлу
|
|
34
|
+
* @param {string} filePath відносний шлях (для вибору режиму)
|
|
35
|
+
* @returns {string} текст для `parseSync`
|
|
36
|
+
*/
|
|
37
|
+
function contentForGqlScan(content, filePath) {
|
|
38
|
+
if (filePath.endsWith('.vue')) {
|
|
39
|
+
return extractVueScriptBlocks(content)
|
|
40
|
+
}
|
|
41
|
+
return content
|
|
42
|
+
}
|
|
16
43
|
|
|
17
44
|
/**
|
|
18
45
|
* Мова для Oxc за шляхом файлу (розширення).
|
|
@@ -81,7 +108,7 @@ function astContainsGqlTag(node) {
|
|
|
81
108
|
* @returns {boolean} true, якщо знайдено `gql`…``
|
|
82
109
|
*/
|
|
83
110
|
export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
|
|
84
|
-
const scan =
|
|
111
|
+
const scan = contentForGqlScan(content, relativePath)
|
|
85
112
|
const pathForLang = virtualPathForParse(relativePath)
|
|
86
113
|
const lang = langFromPath(pathForLang)
|
|
87
114
|
try {
|
|
@@ -96,19 +123,27 @@ export function sourceFileHasGqlTaggedTemplate(content, relativePath) {
|
|
|
96
123
|
}
|
|
97
124
|
|
|
98
125
|
/**
|
|
99
|
-
* Чи підлягає файл скануванню за розширенням (
|
|
126
|
+
* Чи підлягає файл скануванню за розширенням (`.vue`, `.[cm]?[jt]sx?`).
|
|
100
127
|
* @param {string} relativePath відносний шлях
|
|
101
128
|
* @returns {boolean} true якщо файл підлягає скануванню
|
|
102
129
|
*/
|
|
103
130
|
export function isGqlScanSourceFile(relativePath) {
|
|
104
|
-
return
|
|
131
|
+
return SOURCE_FILE_RE.test(relativePath)
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
/**
|
|
108
|
-
* Чи пропустити файл (декларації, auto-imports) —
|
|
135
|
+
* Чи пропустити файл (декларації, auto-imports/components.d.ts) — типові generated-файли,
|
|
136
|
+
* у яких немає авторського коду з gql-тегами.
|
|
109
137
|
* @param {string} relativePosix шлях з posix-слешами
|
|
110
138
|
* @returns {boolean} true якщо файл потрібно пропустити
|
|
111
139
|
*/
|
|
112
140
|
export function shouldSkipFileForGqlScan(relativePosix) {
|
|
113
|
-
|
|
141
|
+
const base = relativePosix.split('/').pop() || ''
|
|
142
|
+
if (base === 'auto-imports.d.ts' || base === 'components.d.ts') {
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
if (relativePosix.endsWith('.d.ts')) {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
return false
|
|
114
149
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Запускається лише якщо в кореневому `package.json` поле `repository`
|
|
7
7
|
* вказує на `https://github.com/nitra/...` або `https://github.com/abinbevefes/...`
|
|
8
|
-
* (інші репозиторії пропускаються без помилок — як у
|
|
8
|
+
* (інші репозиторії пропускаються без помилок — як у abie-перевірках).
|
|
9
9
|
*
|
|
10
10
|
* Очікуваний формат URL — кластерний DNS-суфікс `<cluster>.internal`:
|
|
11
11
|
* - GKE / GCP: `http://<service>.<namespace>.svc.<cluster>.internal:<port>`
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
parseProgramOrNull,
|
|
28
28
|
templateQuasisText,
|
|
29
29
|
walkAstWithAncestors
|
|
30
|
-
} from '
|
|
30
|
+
} from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
31
31
|
|
|
32
32
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
33
33
|
const BUN_SQL_IMPORT_RE = /\bimport\s*\{[\s\S]*?\b(sql|SQL)\b[\s\S]*?\}\s*from\s*["']bun["']/u
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
isBunSqlScanSourceFile,
|
|
49
49
|
textHasBunSqlImport,
|
|
50
50
|
textHasPgLibImport
|
|
51
|
-
} from '
|
|
51
|
+
} from './bun-sql-scan.mjs'
|
|
52
52
|
import { findAllPackageJsonPaths } from '../../../../scripts/utils/find-package-json-paths.mjs'
|
|
53
53
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
54
54
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Перевіряє лінт JavaScript за правилом js-lint.mdc.
|
|
3
3
|
*
|
|
4
4
|
* Flat ESLint з getConfig і ignore для auto-imports,
|
|
5
|
-
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/
|
|
5
|
+
* `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`):
|
|
6
6
|
* plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
|
|
7
7
|
* globals, ignorePatterns. Також перевіряє workspace `package.json` на `type: "module"`
|
|
8
8
|
* і `engines`, workflow-дубль у `lint.yml`, `knip.json` autofill і застарілі `.eslintrc*`.
|
|
@@ -20,24 +20,12 @@ import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mj
|
|
|
20
20
|
/** Шлях до канонічного oxlint JSON у цьому пакеті (для перевірки та тестів). */
|
|
21
21
|
export const OXLINT_CANONICAL_JSON_PATH = join(
|
|
22
22
|
dirname(fileURLToPath(import.meta.url)),
|
|
23
|
-
'..',
|
|
24
|
-
'..',
|
|
25
|
-
'..',
|
|
26
|
-
'..',
|
|
27
|
-
'scripts',
|
|
28
|
-
'utils',
|
|
29
23
|
'oxlint-canonical.json'
|
|
30
24
|
)
|
|
31
25
|
|
|
32
26
|
/** Шлях до канонічного knip JSON у цьому пакеті — копіюється у корінь проєкту-споживача, якщо відсутній. */
|
|
33
27
|
export const KNIP_CANONICAL_JSON_PATH = join(
|
|
34
28
|
dirname(fileURLToPath(import.meta.url)),
|
|
35
|
-
'..',
|
|
36
|
-
'..',
|
|
37
|
-
'..',
|
|
38
|
-
'..',
|
|
39
|
-
'scripts',
|
|
40
|
-
'utils',
|
|
41
29
|
'knip-canonical.json'
|
|
42
30
|
)
|
|
43
31
|
|
|
@@ -166,7 +154,7 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
|
|
|
166
154
|
|
|
167
155
|
if (!deepEqualOxlintCanonical(actual, expected)) {
|
|
168
156
|
failures.push(
|
|
169
|
-
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/
|
|
157
|
+
`.oxlintrc.json: поле "${key}" має збігатися з каноном пакета @nitra/cursor (npm/rules/js-lint/fix/tooling/oxlint-canonical.json)`
|
|
170
158
|
)
|
|
171
159
|
}
|
|
172
160
|
}
|
|
@@ -396,7 +384,7 @@ async function checkKnipConfig(passFn, failFn) {
|
|
|
396
384
|
return
|
|
397
385
|
}
|
|
398
386
|
await copyFile(KNIP_CANONICAL_JSON_PATH, 'knip.json')
|
|
399
|
-
passFn('knip.json створено з канонічного npm/
|
|
387
|
+
passFn('knip.json створено з канонічного npm/rules/js-lint/fix/tooling/knip-canonical.json (js-lint.mdc)')
|
|
400
388
|
}
|
|
401
389
|
|
|
402
390
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Збирає `oxlint-canonical.json` з `oxlint-canonical-skeleton.json` (без поля rules) та списку
|
|
3
3
|
* правил у `oxlint-rules.tsv` (колонки: ім’я правила, TAB, severity: deny | off | error).
|
|
4
4
|
*
|
|
5
|
-
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./
|
|
5
|
+
* Після змін у TSV або скелеті запускай з каталогу пакета: `bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`,
|
|
6
6
|
* потім скопіюй оновлений канон у корінь споживача як `.oxlintrc.json` за потреби.
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -25,7 +25,7 @@ version: '1.23'
|
|
|
25
25
|
|
|
26
26
|
У `.vscode/extensions.json` `recommendations` мають містити `dbaeumer.vscode-eslint`, `github.vscode-github-actions`, `oxc.oxc-vscode`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
|
|
27
27
|
|
|
28
|
-
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/
|
|
28
|
+
У корені має бути **`.oxlintrc.json`**, який **збігається з каноном** oxlint з пакета **`@nitra/cursor`**: файл **`npm/rules/js-lint/fix/tooling/oxlint-canonical.json`** (plugins, jsPlugins з **`@e18e/eslint-plugin`**, categories, повний набір **rules** із канону — додаткові записи в **`rules`** дозволені; також **`settings`**, **`env`**, **`globals`**). Поле **`ignorePatterns`** працює як **`rules`**: канонічні патерни з **`oxlint-canonical.json`** (наразі **`**/schema.graphql`**, **`**/auto-imports.d.ts`**) мають бути присутні, додаткові локальні glob-и дозволені. Оновити канон можна з репозиторію пакета або скопіювавши файл після **`bun ./rules/js-lint/fix/tooling/rebuild-oxlint-canonical.mjs`** (джерело правил — **`oxlint-rules.tsv`** + скелет **`oxlint-canonical-skeleton.json`**). Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.8.0**), oxlint підвантажує його з **`node_modules`**.
|
|
29
29
|
|
|
30
30
|
Мінімум для розуміння структури (реальний корінь конфігу має збігатися з каноном повністю):
|
|
31
31
|
|
|
@@ -62,7 +62,7 @@ version: '1.23'
|
|
|
62
62
|
|
|
63
63
|
Перевірку невикористаних залежностей і експортів виконує **knip** (заміна `depcheck`). Викликається у скрипті `lint-js` і в CI разом з oxlint/eslint/jscpd — окремий крок у CI не потрібен.
|
|
64
64
|
|
|
65
|
-
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/
|
|
65
|
+
У корені проєкту має бути **`knip.json`**, який стартує з канонічного baseline з пакета `@nitra/cursor` — файл [`npm/rules/js-lint/fix/tooling/knip-canonical.json`](./fix/tooling/knip-canonical.json). Він покриває типові false-positives для наших правил: `entry` зі CLI-конфігами (eslint, stylelint, oxlint, jscpd, markdownlint-cli2, `commitlint`), `project` для `**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}`, `ignore` для `**/__fixtures__/**`, `ignoreDependencies` для пакетів, посилання на які є лише в не-JS-конфігах (`@nitra/cspell-dict`, `/@cspell\/dict-.+/`), і `ignoreBinaries` для CLI, які канон вимагає викликати через `npx`/`bunx` і яких заборонено додавати в `devDependencies` (`actionlint`, `cspell`, `depcheck`, `eslint`, `git-ai`, `jscpd`, `markdownlint-cli2`, `oxfmt`, `oxlint`, `shellcheck`, `uvx`, `v8r`, `zizmor`).
|
|
66
66
|
|
|
67
67
|
Якщо `knip.json` відсутній — `npx @nitra/cursor check js-lint` копіює канон у корінь проєкту (side effect). Після створення модифікуй файл під свій проєкт як завгодно: перевіряємо лише наявність, зміст подальших змін не валідується.
|
|
68
68
|
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
findUnsafeMssqlInListUnparsedInText,
|
|
23
23
|
findUnsafeMssqlInListMissingEmptyGuardInText,
|
|
24
24
|
isMssqlScanSourceFile
|
|
25
|
-
} from '
|
|
25
|
+
} from './mssql-pool-scan.mjs'
|
|
26
26
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
27
27
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
28
28
|
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
normalizeSnippet,
|
|
31
31
|
offsetToLine,
|
|
32
32
|
walkAstWithAncestors
|
|
33
|
-
} from '
|
|
33
|
+
} from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
34
34
|
|
|
35
35
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
36
36
|
const IN_PLACEHOLDER_END_RE = /\bin\s*\(\s*$/iu
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
offsetToLine,
|
|
19
19
|
requireCallModule,
|
|
20
20
|
walkAstWithAncestors
|
|
21
|
-
} from '
|
|
21
|
+
} from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
22
22
|
|
|
23
23
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
24
24
|
const FORBIDDEN_MODULES = new Set(['@nitra/bunyan', 'bunyan'])
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* Якщо ключ обчислюваний (`process.env[varName]`) — пропускаємо без помилки,
|
|
30
30
|
* бо за статичним AST неможливо встановити, яка саме змінна оточення використовується.
|
|
31
31
|
*/
|
|
32
|
-
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '
|
|
32
|
+
import { offsetToLine, parseProgramOrNull, walkAstWithAncestors } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
33
33
|
|
|
34
34
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
35
35
|
const IGNORE_DIRECTIVE_RE = /\/\/\s*@nitra\/cursor\s+ignore-next-line\s+checkEnv\b/u
|
|
@@ -40,22 +40,22 @@ import {
|
|
|
40
40
|
findBunyanImportsInText,
|
|
41
41
|
isBunyanScanSourceFile,
|
|
42
42
|
shouldSkipFileForBunyanScan
|
|
43
|
-
} from '
|
|
44
|
-
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from '
|
|
43
|
+
} from './bunyan-imports.mjs'
|
|
44
|
+
import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './check-env-scan.mjs'
|
|
45
45
|
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
46
46
|
import { runConftestBatch } from '../../../../scripts/utils/run-conftest-batch.mjs'
|
|
47
|
-
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from '
|
|
47
|
+
import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './conn-file-rules.mjs'
|
|
48
48
|
import {
|
|
49
49
|
findConnFactoryImportsInText,
|
|
50
50
|
isConnImportsScanSourceFile,
|
|
51
51
|
isInsideConnDir,
|
|
52
52
|
resolveConnDirFromPackageJson
|
|
53
|
-
} from '
|
|
53
|
+
} from './conn-imports-scan.mjs'
|
|
54
54
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
55
55
|
import {
|
|
56
56
|
findPromiseSetTimeoutInText,
|
|
57
57
|
isPromiseSetTimeoutScanSourceFile
|
|
58
|
-
} from '
|
|
58
|
+
} from './promise-settimeout-scan.mjs'
|
|
59
59
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
60
60
|
import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
|
|
61
61
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Парсимо через oxc-parser; коли файл не парситься — повертаємо порожні результати, щоб
|
|
15
15
|
* не змішувати помилки синтаксису з порушеннями цього правила.
|
|
16
16
|
*/
|
|
17
|
-
import { parseProgramOrNull } from '
|
|
17
|
+
import { parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
18
18
|
|
|
19
19
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
20
20
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* використовується. Якщо файл не парситься — повертаємо порожній результат, спочатку
|
|
17
17
|
* треба полагодити синтаксис.
|
|
18
18
|
*/
|
|
19
|
-
import { langFromPath, normalizeSnippet, offsetToLine } from '
|
|
19
|
+
import { langFromPath, normalizeSnippet, offsetToLine } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
20
20
|
import { parseSync } from 'oxc-parser'
|
|
21
21
|
|
|
22
22
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/u
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Сканер не вимагає, щоб файл компілювався: при синтаксичних помилках повертається
|
|
13
13
|
* порожній результат (як інші сканери — спочатку треба полагодити синтаксис).
|
|
14
14
|
*/
|
|
15
|
-
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '
|
|
15
|
+
import { normalizeSnippet, offsetToLine, parseProgramOrNull } from '../../../../scripts/utils/ast-scan-utils.mjs'
|
|
16
16
|
|
|
17
17
|
const SOURCE_FILE_RE = /\.([cm]?[jt]sx?)$/
|
|
18
18
|
|
package/rules/k8s/k8s.mdc
CHANGED
|
@@ -375,7 +375,7 @@ images:
|
|
|
375
375
|
|
|
376
376
|
Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
|
|
377
377
|
|
|
378
|
-
**`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — **`
|
|
378
|
+
**`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — Rego-пакет **`abie.health_check_policy`** + **abie.mdc**.
|
|
379
379
|
|
|
380
380
|
За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
|
|
381
381
|
|
|
@@ -453,6 +453,11 @@ export function findTestFrameworkImport(content, virtualPath) {
|
|
|
453
453
|
* Класифікує опублікований файл як test/fixture, якщо хоча б одна з ознак:
|
|
454
454
|
* (1) у шляху є каталог із `TEST_DIR_NAMES`; (2) basename відповідає
|
|
455
455
|
* `TEST_FILE_PATTERNS`; (3) для JS/TS-розширень — імпорт test-фреймворку.
|
|
456
|
+
*
|
|
457
|
+
* Carve-out: для шляху `rules/<rule-name>/...` сегмент `<rule-name>` (індекс 1)
|
|
458
|
+
* — це ім'я правила, а не каталог. Зокрема, правило з id `test` (або `tests`)
|
|
459
|
+
* описує конвенцію розміщення тестів і саме по собі не є test-fixture'ом.
|
|
460
|
+
* Подальші сегменти (наприклад, `rules/<r>/fix/<c>/tests/`) продовжують перевірятись.
|
|
456
461
|
* @param {string} relPath posix-шлях відносно `npm/`
|
|
457
462
|
* @returns {Promise<string | null>} причина порушення або `null`
|
|
458
463
|
*/
|
|
@@ -460,7 +465,12 @@ export async function classifyPublishedFileAsTest(relPath) {
|
|
|
460
465
|
const segments = relPath.split('/')
|
|
461
466
|
const base = segments.at(-1)
|
|
462
467
|
const dirs = segments.slice(0, -1)
|
|
463
|
-
const testDir = dirs.find(seg =>
|
|
468
|
+
const testDir = dirs.find((seg, idx) => {
|
|
469
|
+
if (idx === 1 && dirs[0] === 'rules') {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
return TEST_DIR_NAMES.has(seg.toLowerCase())
|
|
473
|
+
})
|
|
464
474
|
if (testDir) return `test-style каталог "${testDir}/"`
|
|
465
475
|
if (TEST_FILE_PATTERNS.some(re => re.test(base))) return `test-style ім'я файлу`
|
|
466
476
|
if (JS_LIKE_EXT_RE.test(base)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
якщо у проекті є хоча б один файл `*.test.mjs`
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє, що всі `*.test.mjs` лежать у каталозі `tests/` (а не поряд із джерельним файлом).
|
|
3
|
+
*
|
|
4
|
+
* Конвенція (test.mdc): `dir/foo.mjs` → тест у `dir/tests/foo.test.mjs`.
|
|
5
|
+
* `*_test.rego` виключені: Rego unit-тести живуть поряд із полісі (OPA community convention).
|
|
6
|
+
*
|
|
7
|
+
* Пропускає: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv` (через `walkDir`)
|
|
8
|
+
* і шляхи з `.n-cursor.json:ignore`.
|
|
9
|
+
*/
|
|
10
|
+
import { basename, dirname, relative } from 'node:path'
|
|
11
|
+
|
|
12
|
+
import { createCheckReporter } from '../../../../scripts/utils/check-reporter.mjs'
|
|
13
|
+
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
14
|
+
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
15
|
+
|
|
16
|
+
const TESTS_DIR_NAME = 'tests'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Чи файл є JS-тестом (`*.test.mjs`).
|
|
20
|
+
* @param {string} absPath абсолютний шлях
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
function isTestFile(absPath) {
|
|
24
|
+
return basename(absPath).endsWith('.test.mjs')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Перевіряє, чи лежить тест у каталозі з іменем `tests`.
|
|
29
|
+
* @param {string} absPath абсолютний шлях до тесту
|
|
30
|
+
* @returns {boolean} `true`, якщо басенейм батьківської директорії — `tests`
|
|
31
|
+
*/
|
|
32
|
+
function isInsideTestsDir(absPath) {
|
|
33
|
+
return basename(dirname(absPath)) === TESTS_DIR_NAME
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Перевіряє розміщення тестових файлів у каталозі `tests/` (test.mdc).
|
|
38
|
+
* @returns {Promise<number>} 0 — всі тести у `tests/`, 1 — є порушення
|
|
39
|
+
*/
|
|
40
|
+
export async function check() {
|
|
41
|
+
const reporter = createCheckReporter()
|
|
42
|
+
const { pass, fail } = reporter
|
|
43
|
+
|
|
44
|
+
const cwd = process.cwd()
|
|
45
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
46
|
+
|
|
47
|
+
/** @type {string[]} */
|
|
48
|
+
const offenders = []
|
|
49
|
+
let totalTests = 0
|
|
50
|
+
|
|
51
|
+
await walkDir(
|
|
52
|
+
cwd,
|
|
53
|
+
absPath => {
|
|
54
|
+
if (!isTestFile(absPath)) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
totalTests++
|
|
58
|
+
if (!isInsideTestsDir(absPath)) {
|
|
59
|
+
offenders.push(relative(cwd, absPath))
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
ignorePaths
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (offenders.length === 0) {
|
|
66
|
+
pass(`Всі ${totalTests} файлів *.test.mjs у каталозі tests/ (test.mdc)`)
|
|
67
|
+
return reporter.getExitCode()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const offenderPath of offenders) {
|
|
71
|
+
const parentDir = dirname(offenderPath)
|
|
72
|
+
const base = basename(offenderPath)
|
|
73
|
+
fail(`${offenderPath}: тест має лежати у tests/ — перенеси у ${parentDir}/${TESTS_DIR_NAME}/${base} (test.mdc)`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return reporter.getExitCode()
|
|
77
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: JS-тести (*.test.mjs) живуть у каталозі tests/ поряд із джерельним файлом, а не безпосередньо в тій же директорії
|
|
3
|
+
version: '1.1'
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Конвенція розміщення тестів
|
|
8
|
+
|
|
9
|
+
JS-тести у пакеті лежать у **піддиректорії `tests/`** поряд із кодом, який тестується, а **не безпосередньо біля джерельного файлу**.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
rules/foo/fix/bar/
|
|
13
|
+
├── check.mjs ← JS-джерело
|
|
14
|
+
└── tests/
|
|
15
|
+
└── check.test.mjs ← тест check.mjs
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Чому так:**
|
|
19
|
+
|
|
20
|
+
- Каталог-з-кодом залишається чистим: продакшен-модулі, политики, темплейти не миготять серед тестових файлів.
|
|
21
|
+
- Один погляд на дерево показує, що піде у npm tarball (все, крім `tests/` і `__fixtures__/` — їх ловить `package.json#files` через негативні globs).
|
|
22
|
+
- Якщо тест переростає у мульти-файловий — він уже у власному каталозі, без хаотичного розкидання.
|
|
23
|
+
|
|
24
|
+
**Виняток — Rego unit-тести (`*_test.rego`):** лишаються **поряд із полісі-файлом** відповідно до загальноприйнятого OPA/Conftest community-патерну. `conftest verify -p <dir>` рекурсивно підхоплює їх незалежно від місця в каталозі.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
rules/foo/policy/package_json/
|
|
28
|
+
├── package_json.rego ← Rego-правило
|
|
29
|
+
├── package_json_test.rego ← Rego unit-тести (поряд, не у tests/)
|
|
30
|
+
└── target.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Спеціальні випадки:**
|
|
34
|
+
|
|
35
|
+
- **Integration-тести на рівні пакета** — у `<root>/tests/` (наприклад `npm/tests/`). Це не «біля файлу», а виокремлений каталог для тестів, що покривають весь пакет.
|
|
36
|
+
- **Fixtures**: `__fixtures__/` (для shared) і `fixtures/` (для rule-specific) також живуть **усередині `tests/`**: `tests/__fixtures__/...` і `tests/fixtures/...`.
|
|
37
|
+
- **Test helpers** (`test-helpers.mjs`) можуть лишатися у `scripts/utils/` як shared infra — конвенція стосується файлів `*.test.mjs`.
|
|
38
|
+
|
|
39
|
+
**Гарантії tarball-чистоти** через `package.json#files`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
"files": [
|
|
43
|
+
"...",
|
|
44
|
+
"!**/*.test.mjs",
|
|
45
|
+
"!**/*_test.rego",
|
|
46
|
+
"!**/__fixtures__/**",
|
|
47
|
+
"!**/fixtures/**",
|
|
48
|
+
"!**/test-helpers.mjs"
|
|
49
|
+
]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Recursive globs ловлять файли всередині `tests/` так само, як ловили б їх біля джерела.
|
|
53
|
+
|
|
54
|
+
## Що перевіряє правило
|
|
55
|
+
|
|
56
|
+
`npx @nitra/cursor check test` (concern `location`) проходить деревом пакета й перевіряє: **кожен `*.test.mjs` файл лежить усередині каталогу з ім'ям `tests`** (точне співпадіння басенейму батьківської директорії). Файли поза цим каталогом репортуються з порадою куди їх перенести.
|
|
57
|
+
|
|
58
|
+
`*_test.rego` перевіркою **не охоплюються** — вони не переміщуються.
|
|
59
|
+
|
|
60
|
+
Пропускаються: `node_modules`, `.git`, `dist`, `build`, `.venv`, `venv`, шляхи з `.n-cursor.json:ignore`.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* натомість використовуй `mode` з `defineConfig(({ mode }) => ...)`.
|
|
16
16
|
*
|
|
17
17
|
* Заборонені явні value-імпорти з `vue` у джерелах пакета — сканування `.vue`/`.ts`/`.js` тощо
|
|
18
|
-
* через **oxc-parser** (`module.staticImports`; див.
|
|
18
|
+
* через **oxc-parser** (`module.staticImports`; див. `./vue-forbidden-imports.mjs`); дозволені лише type-only та side-effect `import 'vue'`.
|
|
19
19
|
*
|
|
20
20
|
* Окремо в `.vue` SFC заборонено імпорти Node-нативних модулів — `node:*` префікс або bare-ім’я
|
|
21
21
|
* вбудованого модуля Node (`fs`, `path`, `timers/promises` тощо). Vue SFC виконується у браузері,
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
findForbiddenVueImportsInSourceFile,
|
|
32
32
|
isVueImportScanSourceFile,
|
|
33
33
|
shouldSkipFileForVueImportScan
|
|
34
|
-
} from '
|
|
34
|
+
} from './vue-forbidden-imports.mjs'
|
|
35
35
|
import { loadCursorIgnorePaths } from '../../../../scripts/utils/load-cursor-config.mjs'
|
|
36
36
|
import { walkDir } from '../../../../scripts/utils/walkDir.mjs'
|
|
37
37
|
import { getMonorepoPackageRootDirs } from '../../../../scripts/utils/workspaces.mjs'
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -341,4 +341,4 @@ import path from 'node:path'
|
|
|
341
341
|
|
|
342
342
|
## Перевірка
|
|
343
343
|
|
|
344
|
-
`npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/
|
|
344
|
+
`npx @nitra/cursor check vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/rules/vue/fix/packages/vue-forbidden-imports.mjs`).
|