@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
@@ -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
- /** Parse file contents by extension; returns JS object for structured formats, string for text. */
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.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, m => (m.startsWith('"') ? m : ''))
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>.+)\\.${slot}\\.[^.]+$`))
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
- return parts
62
- .map(p => (typeof p === 'number' ? `[${p}]` : /^[a-zA-Z_$][\w$]*$/.test(p) ? p : JSON.stringify(p)))
63
- .reduce((acc, p) => (acc === '' ? p : p.startsWith('[') ? acc + p : acc + '.' + p), '')
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 == null) return []
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 (snippet !== null && typeof snippet === 'object') {
93
- if (actual == null || typeof actual !== 'object' || Array.isArray(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 == null) return []
164
+ if (deny === null || deny === undefined) return []
115
165
  const { targetPath, source } = opts
116
- if (deny !== null && typeof deny === 'object' && !Array.isArray(deny)) {
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 == null) return []
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 (contains !== null && typeof contains === 'object') {
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 == null) return []
224
+ if (template === null || template === undefined) return []
166
225
  const { targetPath, source } = opts
167
- const actualLines = new Set(String(actual ?? '').split(/\r?\n/).map(l => l.trim()))
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(/\r?\n/)) {
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
- if (!(await stat(tplDir)).isDirectory()) return {}
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) result[target].snippet = value // text-only treated as snippet
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
  }