@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.
- package/CHANGELOG.md +41 -1
- package/bin/n-cursor.js +4 -0
- package/package.json +1 -1
- package/rules/changelog/fix/consistency/check.mjs +100 -85
- package/rules/ci4/ci4.mdc +7 -7
- package/rules/ga/lint/lint.mjs +23 -3
- package/rules/ga/policy/lint_ga/lint_ga.rego +6 -0
- package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +6 -0
- package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/js-run/fix/runtime/check.mjs +3 -0
- package/rules/js-run/js-run.mdc +16 -1
- package/rules/js-run/policy/package_json/package_json.rego +17 -0
- package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
- package/rules/k8s/fix/manifests/check.mjs +775 -139
- package/rules/k8s/k8s.mdc +52 -6
- package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
- package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
- package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
- package/rules/security/fix/trufflehog/check.mjs +3 -0
- package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
- package/rules/text/fix/formatting/check.mjs +1 -1
- package/rules/text/lint/lint.mjs +113 -5
- package/rules/text/policy/cspell/cspell.rego +1 -1
- package/rules/text/policy/lint_text/lint_text.rego +100 -0
- package/rules/text/policy/lint_text/target.json +4 -0
- package/rules/text/policy/lint_text/template/lint-text.yml.snippet.yml +61 -0
- package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
- package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
- package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
- package/rules/text/text.mdc +3 -57
- package/rules/vue/vue.mdc +1 -0
- package/scripts/sync-claude-config.mjs +2 -2
- package/scripts/utils/check-mdc-template-refs.mjs +15 -5
- package/scripts/utils/inline-template-links.mjs +15 -8
- package/scripts/utils/package-manifest.mjs +24 -19
- package/scripts/utils/run-conftest-batch.mjs +22 -15
- package/scripts/utils/template.mjs +89 -21
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Reads template/ for a concern directory and returns a merged structure indexed
|
|
3
3
|
* by target basename. For each <target>, returns whichever of snippet/deny/contains
|
|
4
4
|
* exist (parsed in native format by extension).
|
|
5
|
-
*
|
|
6
5
|
* @param {string} concernDir absolute path to fix/<concern>/ or policy/<concern>/
|
|
7
6
|
* @returns {Promise<Record<string, { snippet?: any, deny?: any, contains?: any }>>}
|
|
8
7
|
*/
|
|
@@ -13,8 +12,15 @@ import { basename as _basename, extname, join, relative } from 'node:path'
|
|
|
13
12
|
import { parse as parseToml } from 'smol-toml'
|
|
14
13
|
|
|
15
14
|
const SLOTS = ['snippet', 'deny', 'contains']
|
|
15
|
+
const IDENT_RE = /^[a-zA-Z_$][\w$]*$/
|
|
16
|
+
const NEWLINE_RE = /\r?\n/
|
|
17
|
+
const LEADING_BANG_RE = /^!/
|
|
16
18
|
|
|
17
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Parse file contents by extension; returns JS object for structured formats, string for text.
|
|
21
|
+
* @param {string} path шлях до файлу
|
|
22
|
+
* @returns {Promise<unknown>} розпарсений вміст
|
|
23
|
+
*/
|
|
18
24
|
async function parseByExt(path) {
|
|
19
25
|
const raw = await readFile(path, 'utf8')
|
|
20
26
|
const ext = extname(path).toLowerCase()
|
|
@@ -27,12 +33,21 @@ async function parseByExt(path) {
|
|
|
27
33
|
return raw // text-only
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} s сирий вміст JSON/JSONC
|
|
38
|
+
* @returns {string} текст без коментарів, рядкові літерали збережено
|
|
39
|
+
*/
|
|
30
40
|
function stripJsonComments(s) {
|
|
31
41
|
// Match string literals OR comments. Strings are returned unchanged so we never
|
|
32
42
|
// strip `/*` / `//` / `*/` that appear inside values (e.g. glob `**/node_modules/**`).
|
|
33
|
-
return s.
|
|
43
|
+
return s.replaceAll(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, m => (m.startsWith('"') ? m : ''))
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} dir каталог для рекурсивного обходу
|
|
48
|
+
* @param {string} [base] базовий шлях для відносних результатів
|
|
49
|
+
* @returns {Promise<string[]>} список відносних шляхів файлів
|
|
50
|
+
*/
|
|
36
51
|
async function walk(dir, base = dir) {
|
|
37
52
|
const out = []
|
|
38
53
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
@@ -46,23 +61,48 @@ async function walk(dir, base = dir) {
|
|
|
46
61
|
/**
|
|
47
62
|
* Parse "<target>.<slot>.<ext>" or "<target>" (text-only).
|
|
48
63
|
* Returns { target, slot } where slot is one of snippet|deny|contains|null (null = text-only target).
|
|
64
|
+
* @param {string} relPath відносний шлях template-файлу
|
|
65
|
+
* @returns {{ target: string, slot: string | null }} класифікація
|
|
49
66
|
*/
|
|
50
67
|
function classifyTemplateFile(relPath) {
|
|
51
68
|
// Try ".<slot>." suffix detection
|
|
52
69
|
for (const slot of SLOTS) {
|
|
53
|
-
const m = relPath.match(new RegExp(`^(?<target>.+)
|
|
70
|
+
const m = relPath.match(new RegExp(String.raw`^(?<target>.+)\.${slot}\.[^.]+$`))
|
|
54
71
|
if (m?.groups?.target) return { target: m.groups.target, slot }
|
|
55
72
|
}
|
|
56
73
|
// No slot suffix → text-only canon for the literal target name
|
|
57
74
|
return { target: relPath, slot: null }
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {string|number} p сегмент шляху
|
|
79
|
+
* @returns {string} токен у форматі ідентифікатор / [n] / JSON-рядок
|
|
80
|
+
*/
|
|
81
|
+
function tokenizePathPart(p) {
|
|
82
|
+
if (typeof p === 'number') return `[${p}]`
|
|
83
|
+
if (IDENT_RE.test(p)) return p
|
|
84
|
+
return JSON.stringify(p)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {Array<string|number>} parts сегменти шляху
|
|
89
|
+
* @returns {string} дотовий шлях для повідомлень
|
|
90
|
+
*/
|
|
60
91
|
function formatPath(parts) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
const tokens = parts.map(p => tokenizePathPart(p))
|
|
93
|
+
let out = ''
|
|
94
|
+
for (const p of tokens) {
|
|
95
|
+
if (out === '') out = p
|
|
96
|
+
else if (p.startsWith('[')) out += p
|
|
97
|
+
else out += '.' + p
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
64
100
|
}
|
|
65
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @param {unknown} v значення
|
|
104
|
+
* @returns {string} JSON-рядок для рядків, інакше String(v)
|
|
105
|
+
*/
|
|
66
106
|
function quote(v) {
|
|
67
107
|
return typeof v === 'string' ? JSON.stringify(v) : String(v)
|
|
68
108
|
}
|
|
@@ -71,9 +111,14 @@ function quote(v) {
|
|
|
71
111
|
* Deep subset-of check. Every leaf in `snippet` must equal same path in `actual`.
|
|
72
112
|
* Arrays in snippet: every element must be present in actual array.
|
|
73
113
|
* Returns array of violation messages.
|
|
114
|
+
* @param {unknown} actual фактичне значення з документа
|
|
115
|
+
* @param {unknown} snippet канонічний фрагмент із template
|
|
116
|
+
* @param {{ targetPath: string, source: string }} opts опції джерела
|
|
117
|
+
* @param {Array<string|number>} [path] поточний шлях у дереві
|
|
118
|
+
* @returns {string[]} список порушень
|
|
74
119
|
*/
|
|
75
120
|
export function checkSnippet(actual, snippet, opts, path = []) {
|
|
76
|
-
if (snippet
|
|
121
|
+
if (snippet === null || snippet === undefined) return []
|
|
77
122
|
const { targetPath, source } = opts
|
|
78
123
|
const violations = []
|
|
79
124
|
if (Array.isArray(snippet)) {
|
|
@@ -89,8 +134,8 @@ export function checkSnippet(actual, snippet, opts, path = []) {
|
|
|
89
134
|
}
|
|
90
135
|
return violations
|
|
91
136
|
}
|
|
92
|
-
if (
|
|
93
|
-
if (actual
|
|
137
|
+
if (typeof snippet === 'object') {
|
|
138
|
+
if (actual === null || actual === undefined || typeof actual !== 'object' || Array.isArray(actual)) {
|
|
94
139
|
violations.push(`${targetPath}: ${formatPath(path)} має бути об'єктом (${source})`)
|
|
95
140
|
return violations
|
|
96
141
|
}
|
|
@@ -109,11 +154,16 @@ export function checkSnippet(actual, snippet, opts, path = []) {
|
|
|
109
154
|
/**
|
|
110
155
|
* Walks deny tree; for any leaf path that exists in actual, returns violation
|
|
111
156
|
* with the deny's leaf string as reason.
|
|
157
|
+
* @param {unknown} actual фактичне значення з документа
|
|
158
|
+
* @param {unknown} deny дерево заборонених шляхів із template
|
|
159
|
+
* @param {{ targetPath: string, source: string }} opts опції джерела
|
|
160
|
+
* @param {Array<string|number>} [path] поточний шлях у дереві
|
|
161
|
+
* @returns {string[]} список порушень
|
|
112
162
|
*/
|
|
113
163
|
export function checkDeny(actual, deny, opts, path = []) {
|
|
114
|
-
if (deny
|
|
164
|
+
if (deny === null || deny === undefined) return []
|
|
115
165
|
const { targetPath, source } = opts
|
|
116
|
-
if (
|
|
166
|
+
if (typeof deny === 'object' && !Array.isArray(deny)) {
|
|
117
167
|
const out = []
|
|
118
168
|
for (const [k, v] of Object.entries(deny)) {
|
|
119
169
|
const childActual = actual && typeof actual === 'object' ? actual[k] : undefined
|
|
@@ -132,9 +182,14 @@ export function checkDeny(actual, deny, opts, path = []) {
|
|
|
132
182
|
/**
|
|
133
183
|
* For each leaf path that has an array of strings in `contains`, every string
|
|
134
184
|
* must appear as substring in the same path of `actual` (string leaf).
|
|
185
|
+
* @param {unknown} actual фактичне значення з документа
|
|
186
|
+
* @param {unknown} contains дерево обов'язкових підрядків із template
|
|
187
|
+
* @param {{ targetPath: string, source: string }} opts опції джерела
|
|
188
|
+
* @param {Array<string|number>} [path] поточний шлях у дереві
|
|
189
|
+
* @returns {string[]} список порушень
|
|
135
190
|
*/
|
|
136
191
|
export function checkContains(actual, contains, opts, path = []) {
|
|
137
|
-
if (contains
|
|
192
|
+
if (contains === null || contains === undefined) return []
|
|
138
193
|
const { targetPath, source } = opts
|
|
139
194
|
if (Array.isArray(contains)) {
|
|
140
195
|
const out = []
|
|
@@ -146,7 +201,7 @@ export function checkContains(actual, contains, opts, path = []) {
|
|
|
146
201
|
}
|
|
147
202
|
return out
|
|
148
203
|
}
|
|
149
|
-
if (
|
|
204
|
+
if (typeof contains === 'object') {
|
|
150
205
|
const out = []
|
|
151
206
|
for (const [k, v] of Object.entries(contains)) {
|
|
152
207
|
const childActual = actual && typeof actual === 'object' ? actual[k] : undefined
|
|
@@ -160,13 +215,21 @@ export function checkContains(actual, contains, opts, path = []) {
|
|
|
160
215
|
/**
|
|
161
216
|
* For text-only targets (e.g. .stylelintignore): every non-empty, non-comment
|
|
162
217
|
* line in `template` must appear (trimmed) in `actual`.
|
|
218
|
+
* @param {unknown} actual фактичний текст документа
|
|
219
|
+
* @param {unknown} template канонічний текст із template
|
|
220
|
+
* @param {{ targetPath: string, source: string }} opts опції джерела
|
|
221
|
+
* @returns {string[]} список порушень
|
|
163
222
|
*/
|
|
164
223
|
export function checkTextSubset(actual, template, opts) {
|
|
165
|
-
if (template
|
|
224
|
+
if (template === null || template === undefined) return []
|
|
166
225
|
const { targetPath, source } = opts
|
|
167
|
-
const actualLines = new Set(
|
|
226
|
+
const actualLines = new Set(
|
|
227
|
+
String(actual ?? '')
|
|
228
|
+
.split(NEWLINE_RE)
|
|
229
|
+
.map(l => l.trim())
|
|
230
|
+
)
|
|
168
231
|
const out = []
|
|
169
|
-
for (const raw of String(template).split(
|
|
232
|
+
for (const raw of String(template).split(NEWLINE_RE)) {
|
|
170
233
|
const line = raw.trim()
|
|
171
234
|
if (line === '' || line.startsWith('#')) continue
|
|
172
235
|
if (!actualLines.has(line)) {
|
|
@@ -176,17 +239,23 @@ export function checkTextSubset(actual, template, opts) {
|
|
|
176
239
|
return out
|
|
177
240
|
}
|
|
178
241
|
|
|
242
|
+
/**
|
|
243
|
+
* @param {string} concernDir абсолютний шлях до fix/<concern>/ або policy/<concern>/
|
|
244
|
+
* @returns {Promise<Record<string, { snippet?: any, deny?: any, contains?: any }>>} merged template-дерево, індексоване за target
|
|
245
|
+
*/
|
|
179
246
|
export async function loadTemplate(concernDir) {
|
|
180
247
|
const tplDir = join(concernDir, 'template')
|
|
181
248
|
if (!existsSync(tplDir)) return {}
|
|
182
|
-
|
|
249
|
+
const tplStat = await stat(tplDir)
|
|
250
|
+
if (!tplStat.isDirectory()) return {}
|
|
183
251
|
const files = await walk(tplDir)
|
|
184
252
|
const result = {}
|
|
185
253
|
for (const rel of files) {
|
|
186
254
|
const { target, slot } = classifyTemplateFile(rel)
|
|
187
255
|
if (!result[target]) result[target] = {}
|
|
188
256
|
const value = await parseByExt(join(tplDir, rel))
|
|
189
|
-
if (slot === null)
|
|
257
|
+
if (slot === null)
|
|
258
|
+
result[target].snippet = value // text-only treated as snippet
|
|
190
259
|
else result[target][slot] = value
|
|
191
260
|
}
|
|
192
261
|
return result
|
|
@@ -204,7 +273,7 @@ export async function resolveConcernTemplateData(concernAbsDir, targetJson) {
|
|
|
204
273
|
const single = targetJson?.files?.single
|
|
205
274
|
if (single) return tpl[_basename(single)]
|
|
206
275
|
const glob = targetJson?.files?.walkGlob
|
|
207
|
-
if (typeof glob === 'string') return tpl[_basename(glob.replace(
|
|
276
|
+
if (typeof glob === 'string') return tpl[_basename(glob.replace(LEADING_BANG_RE, ''))]
|
|
208
277
|
if (Array.isArray(glob)) {
|
|
209
278
|
for (const g of glob) {
|
|
210
279
|
if (g.startsWith('!')) continue
|
|
@@ -212,5 +281,4 @@ export async function resolveConcernTemplateData(concernAbsDir, targetJson) {
|
|
|
212
281
|
if (data) return data
|
|
213
282
|
}
|
|
214
283
|
}
|
|
215
|
-
return undefined
|
|
216
284
|
}
|