@7n/rules 1.2.1 → 1.2.2

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 (32) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/rules/abie/clean_merged_ignore_branches/fix-clean_merged_ignore_branches.mjs +3 -0
  4. package/rules/docker/lint_docker_yml/fix-lint_docker_yml.mjs +3 -0
  5. package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +3 -0
  6. package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +3 -0
  7. package/rules/ga/git_ai/fix-git_ai.mjs +3 -0
  8. package/rules/ga/lint_ga/fix-lint_ga.mjs +3 -0
  9. package/rules/ga/vscode_settings/fix-vscode_settings.mjs +3 -0
  10. package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +3 -0
  11. package/rules/js/jscpd_config/fix-jscpd_config.mjs +3 -0
  12. package/rules/js/lint_js_yml/fix-lint_js_yml.mjs +5 -0
  13. package/rules/js/package_json/fix-package_json.mjs +3 -0
  14. package/rules/k8s/lint_k8s_yml/fix-lint_k8s_yml.mjs +3 -0
  15. package/rules/npm-module/emit_types_config/fix-emit_types_config.mjs +3 -0
  16. package/rules/npm-module/npm_package_json/fix-npm_package_json.mjs +3 -0
  17. package/rules/npm-module/npm_publish_yml/fix-npm_publish_yml.mjs +5 -0
  18. package/rules/npm-module/root_package_json/fix-root_package_json.mjs +3 -0
  19. package/rules/php/lint_php_yml/fix-lint_php_yml.mjs +3 -0
  20. package/rules/python/lint_python_yml/fix-lint_python_yml.mjs +3 -0
  21. package/rules/rego/vscode_settings/fix-vscode_settings.mjs +3 -0
  22. package/rules/rust/lint_rust_yml/fix-lint_rust_yml.mjs +5 -0
  23. package/rules/security/lint_security_yml/fix-lint_security_yml.mjs +3 -0
  24. package/rules/style/lint_style_yml/fix-lint_style_yml.mjs +3 -0
  25. package/rules/style/package_json/fix-package_json.mjs +3 -0
  26. package/rules/style/vscode_settings/fix-vscode_settings.mjs +3 -0
  27. package/rules/text/lint_text/fix-lint_text.mjs +5 -0
  28. package/rules/text/oxfmtrc/fix-oxfmtrc.mjs +3 -0
  29. package/rules/text/vscode_settings/fix-vscode_settings.mjs +3 -0
  30. package/rules/worktree/vscode_settings/fix-vscode_settings.mjs +5 -0
  31. package/rules/worktree/zed_settings/fix-zed_settings.mjs +3 -0
  32. package/scripts/lib/fix/template-deep-merge.mjs +182 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.2] - 2026-07-14
4
+
5
+ ### Changed
6
+
7
+ - release: @7n/llm-lib@2.6.1, @7n/rules@1.1.0
8
+
3
9
  ## [1.2.1] - 2026-07-14
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'abie-clean_merged_ignore_branches-template', targetPath: '.github/workflows/clean-merged-branch.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'docker-lint_docker_yml-template', targetPath: '.github/workflows/lint-docker.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-clean_ga_workflows-template', targetPath: '.github/workflows/clean-ga-workflows.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-clean_merged_branch-template', targetPath: '.github/workflows/clean-merged-branch.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-git_ai-template', targetPath: '.github/workflows/git-ai.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-lint_ga-template', targetPath: '.github/workflows/lint-ga.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-vscode_settings-template', targetPath: '.vscode/settings.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'ga-zizmor_yml-template', targetPath: '.github/zizmor.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'js-jscpd_config-template', targetPath: '.jscpd.json' })]
@@ -0,0 +1,5 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'js-lint_js_yml-template', targetPath: '.github/workflows/lint-js.yml' })
5
+ ]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'js-package_json-template', targetPath: 'package.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'k8s-lint_k8s_yml-template', targetPath: '.github/workflows/lint-k8s.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'npm-module-emit_types_config-template', targetPath: 'npm/tsconfig.emit-types.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'npm-module-npm_package_json-template', targetPath: 'npm/package.json' })]
@@ -0,0 +1,5 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'npm-module-npm_publish_yml-template', targetPath: '.github/workflows/npm-publish.yml' })
5
+ ]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'npm-module-root_package_json-template', targetPath: 'package.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'php-lint_php_yml-template', targetPath: '.github/workflows/lint-php.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'python-lint_python_yml-template', targetPath: '.github/workflows/lint-python.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'rego-vscode_settings-template', targetPath: '.vscode/settings.json' })]
@@ -0,0 +1,5 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'rust-lint_rust_yml-template', targetPath: '.github/workflows/lint-rust.yml' })
5
+ ]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'security-lint_security_yml-template', targetPath: '.github/workflows/lint-security.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'style-lint_style_yml-template', targetPath: '.github/workflows/lint-style.yml' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'style-package_json-template', targetPath: 'package.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'style-vscode_settings-template', targetPath: '.vscode/settings.json' })]
@@ -0,0 +1,5 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'text-lint_text-template', targetPath: '.github/workflows/lint-text.yml' })
5
+ ]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'text-oxfmtrc-template', targetPath: '.oxfmtrc.json' })]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'text-vscode_settings-template', targetPath: '.vscode/settings.json' })]
@@ -0,0 +1,5 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [
4
+ createTemplateFixPattern({ id: 'worktree-vscode_settings-template', targetPath: '.vscode/settings.json' })
5
+ ]
@@ -0,0 +1,3 @@
1
+ import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
+
3
+ export const patterns = [createTemplateFixPattern({ id: 'worktree-zed_settings-template', targetPath: '.zed/settings.json' })]
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Спільний T0-autofix writer для policy-концернів "один target-файл + один канонічний
3
+ * `template/*.snippet.{json,jsonc,yml,yaml}`" (`engine:"template"` і `engine:"rego"`
4
+ * з тим самим snippet-шаблоном). Deep-merge snippet → target: об'єкти мерджаться по
5
+ * ключах, масиви — union за структурним підмножинним збігом (`checkSnippet`-семантика,
6
+ * як у детекторі — жодного окремого визначення "збігу"), листя — перезаписується
7
+ * канонічним значенням. Файл відсутній → копіюється сам snippet (без merge).
8
+ *
9
+ * JSON/JSONC — plain-object merge + `JSON.stringify`. YAML — `yaml` Document API
10
+ * (`setIn`/`addIn`/`hasIn`), щоб зберегти коментарі й форматування наявного файлу;
11
+ * створюється лише те, чого бракує.
12
+ *
13
+ * Кожен викличний concern передає лише `{ id, targetPath }` — сам writer резолвить
14
+ * snippet-файл у `template/` свого concern-а через `ctx.concernDir` (той самий
15
+ * механізм, що й `vscode-ext-add.mjs`).
16
+ */
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
18
+ import { basename, dirname, extname, join } from 'node:path'
19
+
20
+ import { checkSnippet } from '../template.mjs'
21
+
22
+ const SNIPPET_EXTS = ['yml', 'yaml', 'json', 'jsonc']
23
+
24
+ /**
25
+ * Чи `needle` структурно вже присутній у якомусь елементі `actualArray`
26
+ * (та сама subset-семантика, що й у детекторі — жодного окремого визначення "збігу").
27
+ * @param {unknown[]} actualArray наявний масив
28
+ * @param {unknown} needle елемент снippета, наявність якого перевіряємо
29
+ * @returns {boolean} true — вже присутній структурно
30
+ */
31
+ function containedIn(actualArray, needle) {
32
+ return actualArray.some(a => checkSnippet(a, needle, { targetPath: '', source: '' }).length === 0)
33
+ }
34
+
35
+ /**
36
+ * Шукає `<basename>.snippet.<ext>` у `template/` concern-а (перебір відомих розширень).
37
+ * @param {string} templateDir абсолютний шлях до `template/` concern-а
38
+ * @param {string} targetBasename basename цільового файлу (напр. `npm-publish.yml`)
39
+ * @returns {string|null} абсолютний шлях до snippet-файлу або null
40
+ */
41
+ function findSnippetFile(templateDir, targetBasename) {
42
+ if (!existsSync(templateDir)) return null
43
+ for (const ext of SNIPPET_EXTS) {
44
+ const p = join(templateDir, `${targetBasename}.snippet.${ext}`)
45
+ if (existsSync(p)) return p
46
+ }
47
+ return null
48
+ }
49
+
50
+ /**
51
+ * Рекурсивний deep-merge snippet у plain JS-значення (JSON/JSONC-гілка).
52
+ * @param {unknown} actual наявне значення (або undefined)
53
+ * @param {unknown} snippet канонічний фрагмент
54
+ * @returns {unknown} злите значення
55
+ */
56
+ function mergeJsonValue(actual, snippet) {
57
+ if (Array.isArray(snippet)) {
58
+ const arr = Array.isArray(actual) ? [...actual] : []
59
+ for (const needle of snippet) {
60
+ if (!containedIn(arr, needle)) arr.push(needle)
61
+ }
62
+ return arr
63
+ }
64
+ if (snippet !== null && typeof snippet === 'object') {
65
+ const obj = actual !== null && typeof actual === 'object' && !Array.isArray(actual) ? { ...actual } : {}
66
+ for (const [k, v] of Object.entries(snippet)) obj[k] = mergeJsonValue(obj[k], v)
67
+ return obj
68
+ }
69
+ return snippet // leaf — перезаписуємо канонічним значенням
70
+ }
71
+
72
+ /**
73
+ * Deep-merge snippet у YAML `Document` за шляхом (мутує `doc`). Масиви — `addIn` лише
74
+ * відсутніх (структурно) елементів; об'єкти — рекурсія по ключах (`setIn` створює
75
+ * проміжні мапи автоматично); листя — `setIn`.
76
+ * @param {import('yaml').Document} doc YAML-документ (мутується)
77
+ * @param {unknown} snippet канонічний фрагмент на цьому шляху
78
+ * @param {Array<string|number>} path шлях у документі
79
+ * @returns {void}
80
+ */
81
+ function mergeYamlDoc(doc, snippet, path) {
82
+ if (Array.isArray(snippet)) {
83
+ // `doc.createNode([])` — примусово YAMLSeq-вузол; голий `setIn(path, [])` іноді
84
+ // лишає сирий JS-масив (коли батьківська мапа вже існує), і подальший `addIn` кидає
85
+ // `Expected YAML collection at …` — createNode гарантує коректний тип вузла завжди.
86
+ if (!doc.hasIn(path)) doc.setIn(path, doc.createNode([]))
87
+ const existing = doc.getIn(path)
88
+ const existingJs = existing && typeof existing.toJS === 'function' ? existing.toJS(doc) : []
89
+ for (const needle of snippet) {
90
+ if (!containedIn(existingJs, needle)) doc.addIn(path, needle)
91
+ }
92
+ return
93
+ }
94
+ if (snippet !== null && typeof snippet === 'object') {
95
+ for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [...path, k])
96
+ return
97
+ }
98
+ doc.setIn(path, snippet) // leaf — перезаписуємо канонічним значенням
99
+ }
100
+
101
+ /**
102
+ * Створює T0-патерн, що приводить `targetPath` у відповідність `template/*.snippet.*`
103
+ * свого concern-а (deep-merge, idempotent). Один writer — для будь-якого single-target
104
+ * snippet-концерну (`engine:"template"` чи `engine:"rego"` з тим самим snippet-шаблоном).
105
+ * @param {{ id: string, targetPath: string }} opts `id` — унікальний id T0-патерну; `targetPath` — posix-relative шлях цільового файлу від cwd
106
+ * @returns {import('../lint-surface/types.mjs').T0Pattern} T0-патерн для `fix-<concern>.mjs`
107
+ */
108
+ export function createTemplateFixPattern({ id, targetPath }) {
109
+ return {
110
+ id,
111
+ test: violations => violations.some(v => v.file === targetPath),
112
+ apply: async (violations, ctx) => {
113
+ if (!violations.some(v => v.file === targetPath)) return { touchedFiles: [] }
114
+ if (!ctx.concernDir) return { touchedFiles: [] }
115
+
116
+ const snippetPath = findSnippetFile(join(ctx.concernDir, 'template'), basename(targetPath))
117
+ if (!snippetPath) return { touchedFiles: [] }
118
+
119
+ const absTarget = join(ctx.cwd, targetPath)
120
+ const ext = extname(snippetPath).toLowerCase()
121
+ const isJson = ext === '.json' || ext === '.jsonc'
122
+
123
+ const prevText = existsSync(absTarget) ? readFileSync(absTarget, 'utf8') : null
124
+
125
+ // Файл відсутній → копіюємо snippet як є (без merge — немає з чим мерджити).
126
+ if (prevText === null) {
127
+ const rawSnippet = readFileSync(snippetPath, 'utf8')
128
+ ctx.recordWrite?.(absTarget)
129
+ mkdirSync(dirname(absTarget), { recursive: true })
130
+ writeFileSync(absTarget, rawSnippet, 'utf8')
131
+ return { touchedFiles: [absTarget], message: `${targetPath}: створено зі snippet` }
132
+ }
133
+
134
+ let nextText
135
+ if (isJson) {
136
+ let snippet
137
+ let actual
138
+ try {
139
+ snippet = JSON.parse(readFileSync(snippetPath, 'utf8'))
140
+ actual = JSON.parse(prevText)
141
+ } catch {
142
+ return { touchedFiles: [] } // невалідний JSON — не чіпаємо детермінованим фіксом
143
+ }
144
+ // Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
145
+ // взагалі: без цієї гейтки JSON.stringify переформатував би вже коректний файл
146
+ // (напр. компактний однорядковий snippet → pretty-print) без жодної реальної зміни.
147
+ if (checkSnippet(actual, snippet, { targetPath: '', source: '' }).length === 0) {
148
+ return { touchedFiles: [] }
149
+ }
150
+ const merged = mergeJsonValue(actual, snippet)
151
+ nextText = JSON.stringify(merged, null, 2) + '\n'
152
+ } else {
153
+ const { parse, parseDocument } = await import('yaml')
154
+ let snippet
155
+ let actualPlain
156
+ try {
157
+ snippet = parse(readFileSync(snippetPath, 'utf8'))
158
+ actualPlain = parse(prevText)
159
+ } catch {
160
+ return { touchedFiles: [] } // невалідний YAML — не чіпаємо детермінованим фіксом
161
+ }
162
+ // Уже відповідає snippet-у (та сама перевірка, що й детектор) → не чіпаємо файл
163
+ // взагалі: Document.toString() не завжди byte-identical на вже коректному вмісті
164
+ // (напр. folded block scalars переформатовуються при round-trip без потреби).
165
+ if (checkSnippet(actualPlain, snippet, { targetPath: '', source: '' }).length === 0) {
166
+ return { touchedFiles: [] }
167
+ }
168
+ const doc = parseDocument(prevText)
169
+ if (doc.errors.length > 0) return { touchedFiles: [] }
170
+ if (snippet !== null && typeof snippet === 'object' && !Array.isArray(snippet)) {
171
+ for (const [k, v] of Object.entries(snippet)) mergeYamlDoc(doc, v, [k])
172
+ }
173
+ nextText = doc.toString()
174
+ }
175
+
176
+ if (nextText === prevText) return { touchedFiles: [] }
177
+ ctx.recordWrite?.(absTarget)
178
+ writeFileSync(absTarget, nextText, 'utf8')
179
+ return { touchedFiles: [absTarget], message: `${targetPath}: приведено у відповідність snippet` }
180
+ }
181
+ }
182
+ }