@nitra/cursor 3.19.0 → 3.20.0
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 +18 -0
- package/bin/n-cursor.js +12 -0
- package/package.json +1 -1
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/js/lint.mjs +1 -1
- package/rules/docker/lib/docker-hadolint.mjs +27 -55
- package/rules/ga/lint/lint.mjs +18 -54
- package/rules/k8s/lint/lint.mjs +3 -10
- package/rules/nginx-default-tpl/js/template.mjs +39 -1
- package/rules/nginx-default-tpl/nginx-default-tpl.mdc +3 -1
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/target.json +1 -0
- package/rules/rego/lint/lint.mjs +10 -55
- package/rules/text/lint/lint.mjs +11 -40
- package/rules/worktree/policy/vscode_settings/target.json +5 -0
- package/rules/worktree/policy/vscode_settings/template/settings.json.snippet.json +8 -0
- package/rules/worktree/policy/zed_settings/target.json +5 -0
- package/rules/worktree/policy/zed_settings/template/settings.json.snippet.json +12 -0
- package/rules/worktree/worktree.mdc +52 -0
- package/schemas/target.json +5 -0
- package/scripts/lib/assert-project-root.mjs +74 -0
- package/scripts/lib/ensure-tool.mjs +352 -0
- package/scripts/lib/run-conftest-batch.mjs +6 -28
- package/scripts/lib/run-rule.mjs +61 -5
- package/scripts/lib/template.mjs +29 -3
- package/scripts/lib/worktree-notice.mjs +52 -1
- package/skills/fix/SKILL.md +4 -4
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/npm-module/policy/npm_publish_yml/npm_publish_yml.rego +0 -87
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
* пер-документні правила винесені у `npm/policy/<rule>/<name>/` як rego-полісі
|
|
5
5
|
* (Rego-authoritative). JS у `check-*.mjs` робить cross-file частину (walking
|
|
6
6
|
* дерева, парність, kustomize-резолюція), а пер-документне валідаційне ядро
|
|
7
|
-
* делегується
|
|
7
|
+
* делегується сюді — один спавн `conftest` на (`namespace`, `policyDir`),
|
|
8
8
|
* незалежно від кількості файлів. Це закриває дублювання JS↔rego і прибирає
|
|
9
9
|
* ризик дрифту (типу `spec.config` vs `spec.default.config` у
|
|
10
10
|
* `health_check_policy.rego`, що ми ловили cross-check тестами).
|
|
11
11
|
*
|
|
12
|
-
* Hard-fail на відсутність `conftest`
|
|
13
|
-
*
|
|
14
|
-
* відмова приховує реальні порушення. Друкуємо install-hint (як `lint-rego.mjs`
|
|
15
|
-
* робить для opa/regal).
|
|
12
|
+
* Hard-fail на відсутність `conftest` — через `ensureTool`, що спочатку
|
|
13
|
+
* намагається авто-встановити, і лише після невдачі кидає виняток.
|
|
16
14
|
*/
|
|
17
15
|
import { spawnSync } from 'node:child_process'
|
|
18
16
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
@@ -20,7 +18,7 @@ import { tmpdir } from 'node:os'
|
|
|
20
18
|
import { dirname, join } from 'node:path'
|
|
21
19
|
import { fileURLToPath } from 'node:url'
|
|
22
20
|
|
|
23
|
-
import {
|
|
21
|
+
import { ensureTool } from './ensure-tool.mjs'
|
|
24
22
|
|
|
25
23
|
/**
|
|
26
24
|
Каталог пакета `@nitra/cursor`, від якого ресолвимо вшиті директорії правил.
|
|
@@ -30,23 +28,6 @@ const PACKAGE_ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))))
|
|
|
30
28
|
/** Шлях до кореня правил. У npm-tarball публікується через `files: ["rules"]`. Кожне правило: `rules/<id>/policy/<name>/`. */
|
|
31
29
|
const RULES_ROOT = join(PACKAGE_ROOT, 'rules')
|
|
32
30
|
|
|
33
|
-
/**
|
|
34
|
-
* Друкує install-hint для conftest і кидає виняток, щоб викликана `check-*`
|
|
35
|
-
* команда ясно завершилась з кодом 1.
|
|
36
|
-
* @returns {never} завжди кидає; для точки виклику — non-returning
|
|
37
|
-
*/
|
|
38
|
-
function failConftestMissing() {
|
|
39
|
-
throw new Error(
|
|
40
|
-
[
|
|
41
|
-
'❌ conftest не знайдено в PATH.',
|
|
42
|
-
' Без нього не запускається пер-документна валідація через rego-полісі (npm/policy/).',
|
|
43
|
-
' Встанови:',
|
|
44
|
-
' macOS: brew install conftest',
|
|
45
|
-
' Universal: https://www.conftest.dev/install/'
|
|
46
|
-
].join('\n')
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
31
|
/**
|
|
51
32
|
* @typedef {object} ConftestViolation
|
|
52
33
|
* @property {string} filename абсолютний шлях до файла, що дав порушення (з output conftest)
|
|
@@ -80,16 +61,13 @@ export function buildConftestArgs(p) {
|
|
|
80
61
|
/**
|
|
81
62
|
* Виконує `conftest test` для всіх файлів одним спавном і повертає масив
|
|
82
63
|
* порушень. Якщо `files` порожній — повертає `[]` без спавна. Якщо `conftest`
|
|
83
|
-
* не у PATH — кидає виняток (hard fail
|
|
64
|
+
* не у PATH і авто-встановлення не вдалось — кидає виняток (hard fail).
|
|
84
65
|
* @param {ConftestBatchOptions} opts параметри запуску
|
|
85
66
|
* @returns {ConftestViolation[]} масив порушень (порожній — все ок)
|
|
86
67
|
*/
|
|
87
68
|
export function runConftestBatch(opts) {
|
|
88
69
|
if (opts.files.length === 0) return []
|
|
89
|
-
const conftestBin =
|
|
90
|
-
if (!conftestBin) {
|
|
91
|
-
failConftestMissing()
|
|
92
|
-
}
|
|
70
|
+
const conftestBin = ensureTool('conftest')
|
|
93
71
|
// policyDirRel — формат `<rule>/<name>` (наприклад `abie/base_deployment_preem`).
|
|
94
72
|
// Реальний шлях у новій структурі: `rules/<rule>/policy/<name>`.
|
|
95
73
|
const slash = opts.policyDirRel.indexOf('/')
|
package/scripts/lib/run-rule.mjs
CHANGED
|
@@ -13,13 +13,20 @@
|
|
|
13
13
|
* Це дає той самий 0/1 контракт, що й попередня модель «один check.mjs на правило».
|
|
14
14
|
*/
|
|
15
15
|
import { readFile } from 'node:fs/promises'
|
|
16
|
-
import { join } from 'node:path'
|
|
16
|
+
import { join, relative } from 'node:path'
|
|
17
17
|
|
|
18
18
|
import { findMissingMdcRefs } from './check-mdc-template-refs.mjs'
|
|
19
19
|
import { createCheckReporter } from './check-reporter.mjs'
|
|
20
20
|
import { resolveTargetFiles } from './resolve-target-files.mjs'
|
|
21
21
|
import { runConftestBatch } from './run-conftest-batch.mjs'
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
checkContains,
|
|
24
|
+
checkDeny,
|
|
25
|
+
checkSnippet,
|
|
26
|
+
checkTextSubset,
|
|
27
|
+
parseByExt,
|
|
28
|
+
resolveConcernTemplateData
|
|
29
|
+
} from './template.mjs'
|
|
23
30
|
|
|
24
31
|
const APPLIES_CONCERN_NAME = 'applies'
|
|
25
32
|
|
|
@@ -52,6 +59,46 @@ async function evaluateAppliesGate(bundledRulesDir, rule) {
|
|
|
52
59
|
return Boolean(await mod.applies())
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Snippet-driven перевірка концерну (`target.json:"check":"template"`): канон зі
|
|
64
|
+
* `template/<target>.snippet|deny|contains.<ext>` звіряється з actual-файлом
|
|
65
|
+
* generic deep-subset-ом, без `.rego`. Семантика — subset-of: усі канонічні
|
|
66
|
+
* поля/елементи обовʼязкові, зайві дозволені; масиви матчаться за наявністю
|
|
67
|
+
* (order-insensitive). Сніпет — єдине джерело істини: його зміна одразу змінює enforce.
|
|
68
|
+
* @param {string} concernAbsDir абсолютний `rules/<id>/policy/<concern>/`
|
|
69
|
+
* @param {object} target розпарсений `target.json`
|
|
70
|
+
* @param {string[]} files актуальні файли-таргети (resolveTargetFiles)
|
|
71
|
+
* @param {string} ruleId id правила (для `source` у повідомленнях)
|
|
72
|
+
* @param {string} concernName імʼя концерну (для summary)
|
|
73
|
+
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
74
|
+
*/
|
|
75
|
+
export async function runTemplateSubsetConcern(concernAbsDir, target, files, ruleId, concernName) {
|
|
76
|
+
const reporter = createCheckReporter()
|
|
77
|
+
const data = await resolveConcernTemplateData(concernAbsDir, target)
|
|
78
|
+
if (!data) {
|
|
79
|
+
reporter.pass(`${concernName}: немає template-сніпета — пропущено`)
|
|
80
|
+
return reporter.getExitCode()
|
|
81
|
+
}
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const rel = relative(process.cwd(), file) || file
|
|
84
|
+
const actual = await parseByExt(file)
|
|
85
|
+
const opts = { targetPath: rel, source: `${ruleId}.mdc` }
|
|
86
|
+
const violations = [
|
|
87
|
+
...(typeof data.snippet === 'string'
|
|
88
|
+
? checkTextSubset(actual, data.snippet, opts)
|
|
89
|
+
: checkSnippet(actual, data.snippet, opts)),
|
|
90
|
+
...checkDeny(actual, data.deny, opts),
|
|
91
|
+
...checkContains(actual, data.contains, opts)
|
|
92
|
+
]
|
|
93
|
+
if (violations.length === 0) {
|
|
94
|
+
reporter.pass(`${concernName}: ${rel} відповідає канону (template subset)`)
|
|
95
|
+
} else {
|
|
96
|
+
for (const v of violations) reporter.fail(v)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return reporter.getExitCode()
|
|
100
|
+
}
|
|
101
|
+
|
|
55
102
|
/**
|
|
56
103
|
* Запускає одну policy-полісі через `runConftestBatch`. Створює локальний репортер,
|
|
57
104
|
* читає `target.json`, визначає файли, фіксує fail/pass — і повертає exit-код.
|
|
@@ -63,8 +110,9 @@ async function evaluateAppliesGate(bundledRulesDir, rule) {
|
|
|
63
110
|
*/
|
|
64
111
|
async function runPolicyConcern(bundledRulesDir, ruleId, concernName, walkCache) {
|
|
65
112
|
const reporter = createCheckReporter()
|
|
66
|
-
const
|
|
67
|
-
|
|
113
|
+
const concernAbsDir = join(bundledRulesDir, ruleId, 'policy', concernName)
|
|
114
|
+
const targetPath = join(concernAbsDir, 'target.json')
|
|
115
|
+
/** @type {{ check?: 'template', files: { single?: string, walkGlob?: string|string[], required?: boolean }, missingMessage?: string }} */
|
|
68
116
|
const target = JSON.parse(await readFile(targetPath, 'utf8'))
|
|
69
117
|
const files = await resolveTargetFiles(target.files, process.cwd(), walkCache)
|
|
70
118
|
if (files.length === 0) {
|
|
@@ -76,10 +124,18 @@ async function runPolicyConcern(bundledRulesDir, ruleId, concernName, walkCache)
|
|
|
76
124
|
}
|
|
77
125
|
return reporter.getExitCode()
|
|
78
126
|
}
|
|
127
|
+
|
|
128
|
+
// `"check": "template"` — концерн без власного `.rego`: канон зі `template/`
|
|
129
|
+
// звіряється напряму через generic deep-subset (`checkSnippet`/`checkDeny`/
|
|
130
|
+
// `checkContains`/`checkTextSubset`). Редагування сніпета автоматично змінює
|
|
131
|
+
// enforce — без правок rego й без міграторів.
|
|
132
|
+
if (target.check === 'template') {
|
|
133
|
+
return runTemplateSubsetConcern(concernAbsDir, target, files, ruleId, concernName)
|
|
134
|
+
}
|
|
135
|
+
|
|
79
136
|
// Rego не дозволяє '-' в імені пакета, тому kebab-id у `.n-cursor.json:rules`
|
|
80
137
|
// мапиться на snake у namespace. Файлова структура `rules/<id>/policy/` лишається kebab.
|
|
81
138
|
const regoNamespace = `${ruleId.replaceAll('-', '_')}.${concernName}`
|
|
82
|
-
const concernAbsDir = join(bundledRulesDir, ruleId, 'policy', concernName)
|
|
83
139
|
const templateData = await resolveConcernTemplateData(concernAbsDir, target)
|
|
84
140
|
const violations = runConftestBatch({
|
|
85
141
|
policyDirRel: `${ruleId}/${concernName}`,
|
package/scripts/lib/template.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const LEADING_BANG_RE = /^!/
|
|
|
26
26
|
* @param {string} path шлях до файлу
|
|
27
27
|
* @returns {Promise<unknown>} розпарсений вміст
|
|
28
28
|
*/
|
|
29
|
-
async function parseByExt(path) {
|
|
29
|
+
export async function parseByExt(path) {
|
|
30
30
|
const raw = await readFile(path, 'utf8')
|
|
31
31
|
const ext = extname(path).toLowerCase()
|
|
32
32
|
if (ext === '.json' || ext === '.jsonc') return JSON.parse(stripJsonComments(raw))
|
|
@@ -112,6 +112,27 @@ function quote(v) {
|
|
|
112
112
|
return typeof v === 'string' ? JSON.stringify(v) : String(v)
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/** Ключі, за якими ідентифікуємо елемент масиву обʼєктів у повідомленні (напр. workflow-крок). */
|
|
116
|
+
const ELEMENT_ID_KEYS = ['uses', 'name', 'id', 'run']
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Людинозрозумілий опис елемента масиву для повідомлення про відсутність.
|
|
120
|
+
* Для скаляра — `quote`; для обʼєкта — перший наявний ідентифікуючий ключ
|
|
121
|
+
* (`uses`/`name`/`id`/`run`), інакше компактний JSON.
|
|
122
|
+
* @param {unknown} needle елемент сніпета, якого бракує в actual
|
|
123
|
+
* @returns {string} опис для тексту порушення
|
|
124
|
+
*/
|
|
125
|
+
function describeElement(needle) {
|
|
126
|
+
if (needle !== null && typeof needle === 'object' && !Array.isArray(needle)) {
|
|
127
|
+
const obj = /** @type {Record<string, unknown>} */ (needle)
|
|
128
|
+
for (const k of ELEMENT_ID_KEYS) {
|
|
129
|
+
if (typeof obj[k] === 'string') return `елемент з ${k}: ${quote(obj[k])}`
|
|
130
|
+
}
|
|
131
|
+
return `елемент ${JSON.stringify(needle)}`
|
|
132
|
+
}
|
|
133
|
+
return quote(needle)
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
/**
|
|
116
137
|
* Deep subset-of check. Every leaf in `snippet` must equal same path in `actual`.
|
|
117
138
|
* Arrays in snippet: every element must be present in actual array.
|
|
@@ -131,10 +152,15 @@ export function checkSnippet(actual, snippet, opts, path = []) {
|
|
|
131
152
|
violations.push(`${targetPath}: ${formatPath(path)} має бути масивом (${source})`)
|
|
132
153
|
return violations
|
|
133
154
|
}
|
|
155
|
+
// Subset-of, order-insensitive: кожен елемент сніпета має структурно міститись
|
|
156
|
+
// хоча б в одному елементі actual. Для обʼєктів — рекурсивний subset
|
|
157
|
+
// (`checkSnippet` без порушень), тож порядок ключів, зайві поля й зайві елементи
|
|
158
|
+
// не ламають збіг. Критично для впорядкованих масивів як `steps`, де елементи
|
|
159
|
+
// сортувати не можна (порядок кроків семантичний) — матч лишається за наявністю.
|
|
134
160
|
for (const needle of snippet) {
|
|
135
|
-
const found = actual.some(a =>
|
|
161
|
+
const found = actual.some(a => checkSnippet(a, needle, opts, [...path, '[]']).length === 0)
|
|
136
162
|
if (!found) {
|
|
137
|
-
violations.push(`${targetPath}: ${formatPath(path)} має містити ${
|
|
163
|
+
violations.push(`${targetPath}: ${formatPath(path)} має містити ${describeElement(needle)} (${source})`)
|
|
138
164
|
}
|
|
139
165
|
}
|
|
140
166
|
return violations
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* і не паралелитись. Підказка адресована агенту, який читає `SKILL.md`, тож
|
|
6
6
|
* вставляється в текст між стабільними маркерами — ре-синк ідемпотентний:
|
|
7
7
|
* наявний блок замінюється, при `worktree:false` — видаляється.
|
|
8
|
+
*
|
|
9
|
+
* Крок 0.1 блоку додає `bun install` у щойно створеному дереві (локальна копія
|
|
10
|
+
* CLI усуває гонку з CDN) і shell-обгортку `n_cursor_npx` навколо bootstrap-виклику
|
|
11
|
+
* `npx`: на ETARGET/notarget та мережевих помилках npm падає ДО запуску бінарника,
|
|
12
|
+
* тож retry мусить жити на рівні shell-інструкції, а не в JS-хендлерах CLI.
|
|
13
|
+
* Обгортка ретраїть лише транзитні помилки реєстру/мережі (30с інтервал, дефолт
|
|
14
|
+
* 5 хв, env `N_CURSOR_NPX_RETRY_MAX_MIN`, ceiling 10 хв) і віддає реальний nonzero
|
|
15
|
+
* CLI одразу. Команди винесені окремим кроком ПІСЛЯ worktree-створення, бо
|
|
16
|
+
* вимагають command substitution, заборонену у «без-expansion» preflight-снипеті
|
|
17
|
+
* (узгоджено з worktree.mdc).
|
|
8
18
|
*/
|
|
9
19
|
|
|
10
20
|
/** Маркер початку worktree-блоку (стабільний, не залежить від тексту всередині). */
|
|
@@ -124,7 +134,48 @@ npx @nitra/cursor worktree add "feature/x-${suffix}" "n-${suffix}: worktree-only
|
|
|
124
134
|
cd ".worktrees/feature-x-${suffix}"
|
|
125
135
|
\`\`\`
|
|
126
136
|
|
|
127
|
-
Тобто branch-argument лишає slash як у git-гілці, а шлях для \`cd\` бере sanitized форму: slash →
|
|
137
|
+
Тобто branch-argument лишає slash як у git-гілці, а шлях для \`cd\` бере sanitized форму: slash → \`-\`.
|
|
138
|
+
|
|
139
|
+
**Крок 0.1 — bootstrap у новому дереві (після \`cd\`, окремий крок — поза «без-expansion» блоком вище).** Дерево щойно створене й **без** \`node_modules\`. Спершу постав залежності локально: тоді \`npx\` бере локальну копію \`@nitra/cursor\` і гонки з CDN немає взагалі. Retry-обгортка нижче — safety-net на випадок, коли версію щойно опубліковано, але edge-кеш CDN ще її не має: \`npm\` тоді падає з \`ETARGET\`/\`notarget\` **до** запуску бінарника (внутрішній JS-retry у \`n-cursor\` для цього кейсу марний — бінарник ще не стартував).
|
|
140
|
+
|
|
141
|
+
\`\`\`bash
|
|
142
|
+
# Локальна копія @nitra/cursor (девзалежність споживача) — npx бере її, без походу в реєстр.
|
|
143
|
+
bun install
|
|
144
|
+
|
|
145
|
+
# n_cursor_npx <args> — обгортка bootstrap-виклику "npx @nitra/cursor <args>".
|
|
146
|
+
# Ретраїмо ЛИШЕ транзитні помилки реєстру/мережі (CDN ще не пропагував щойно
|
|
147
|
+
# опубліковану версію). Реальний nonzero від CLI (fix повернув ❌, lint-помилка) —
|
|
148
|
+
# віддаємо одразу, без ретраю. Інтервал 30с; дефолт-ліміт 5 хв
|
|
149
|
+
# (env N_CURSOR_NPX_RETRY_MAX_MIN), hard-ceiling 10 хв.
|
|
150
|
+
# Чому 5 хв: CDN-пропагація npm зазвичай < 2 хв, 5 хв — запас; довше → ймовірно
|
|
151
|
+
# реальна проблема (невірна версія / аутейдж), краще віддати помилку, ніж висіти.
|
|
152
|
+
n_cursor_npx() {
|
|
153
|
+
max_min="\${N_CURSOR_NPX_RETRY_MAX_MIN:-5}"
|
|
154
|
+
case "$max_min" in '' | *[!0-9]*) max_min=5 ;; esac
|
|
155
|
+
[ "$max_min" -gt 10 ] && max_min=10
|
|
156
|
+
deadline=$(( $(date +%s) + max_min * 60 ))
|
|
157
|
+
attempt=1
|
|
158
|
+
transient='ETARGET|notarget|No matching version|ENOTFOUND|ETIMEDOUT|EAI_AGAIN|ECONNRESET|50[0-9] |502 Bad Gateway|503 Service Unavailable|504 Gateway'
|
|
159
|
+
while :; do
|
|
160
|
+
err=$(mktemp)
|
|
161
|
+
npx @nitra/cursor "$@" 2>"$err"
|
|
162
|
+
code=$?
|
|
163
|
+
cat "$err" >&2
|
|
164
|
+
[ "$code" -eq 0 ] && { rm -f "$err"; return 0; }
|
|
165
|
+
if grep -Eq "$transient" "$err" && [ "$(date +%s)" -lt "$deadline" ]; then
|
|
166
|
+
rm -f "$err"
|
|
167
|
+
echo "n-cursor: очікую пропагації версії по CDN… спроба $attempt, повтор через 30с" >&2
|
|
168
|
+
attempt=$((attempt + 1))
|
|
169
|
+
sleep 30
|
|
170
|
+
else
|
|
171
|
+
rm -f "$err"
|
|
172
|
+
return "$code"
|
|
173
|
+
fi
|
|
174
|
+
done
|
|
175
|
+
}
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
Усі подальші bootstrap-виклики \`npx @nitra/cursor <cmd>\` у цій сесії роби через \`n_cursor_npx <cmd>\`. Якщо опинився у свіжому shell без цієї функції — спершу повтори блок вище (\`bun install\` + визначення \`n_cursor_npx\`).`;
|
|
128
179
|
}
|
|
129
180
|
|
|
130
181
|
/**
|
package/skills/fix/SKILL.md
CHANGED
|
@@ -12,10 +12,10 @@ description: >-
|
|
|
12
12
|
|
|
13
13
|
## Workflow
|
|
14
14
|
|
|
15
|
-
1. **Діагностика** — запусти перевірку (
|
|
15
|
+
1. **Діагностика** — запусти перевірку через retry-обгортку `n_cursor_npx` (визначена у worktree-preflight, крок 0.1: переживає транзитну CDN-гонку щойно опублікованої версії, а реальний `❌` від `fix` віддає одразу). За замовчуванням — лише правила з `.cursor/rules/*.mdc`, для яких у пакеті є programmatic check; повний набір — явні аргументи: `n_cursor_npx fix bun ga …`:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
n_cursor_npx fix
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
2. **Аналіз** — зчитай вивід, знайди всі `❌` та визнач які правила порушено
|
|
@@ -40,10 +40,10 @@ bun i
|
|
|
40
40
|
oxfmt .
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
6. **Верифікація** — перевір що все
|
|
43
|
+
6. **Верифікація** — перевір що все виправлено (та сама retry-обгортка `n_cursor_npx`):
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
n_cursor_npx fix
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
7. **Результат** — всі `❌` від `npx @nitra/cursor fix` мають стати `✅`. Якщо залишились `❌` — повтори кроки 3-6. Лінт-помилки від `bun run lint` тут **не виправляй** — вони на скіл `/n-lint`.
|
package/types/bin/n-cursor.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {}
|
|
2
|
+
export {};
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Перевірка `.github/workflows/npm-publish.yml` (npm-module.mdc).
|
|
2
|
-
#
|
|
3
|
-
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
-
# Структура --data сформована з template/npm-publish.yml.snippet.yml.
|
|
5
|
-
# Per-concern field-by-field: path/substring-маркери з expected_uses_set читаються
|
|
6
|
-
# зі steps template, експектації branches/paths — subset-of.
|
|
7
|
-
#
|
|
8
|
-
# Універсальні workflow-перевірки (concurrency, заборонені setup-bun/cache/install,
|
|
9
|
-
# shell line-continuation) — у `ga.workflow_common`.
|
|
10
|
-
package npm_module.npm_publish_yml
|
|
11
|
-
|
|
12
|
-
import rego.v1
|
|
13
|
-
|
|
14
|
-
# YAML 1.1 quirk: ключ `on:` → boolean true → у конфтесті ключ "true".
|
|
15
|
-
gha_on := input["true"]
|
|
16
|
-
|
|
17
|
-
# Required marker — substring у `uses` для ідентифікації npm-publish кроку.
|
|
18
|
-
publish_action_marker := "JS-DevTools/npm-publish"
|
|
19
|
-
|
|
20
|
-
# Очікувані літерали з template.
|
|
21
|
-
expected_paths := {p | some p in data.template.snippet.on.push.paths}
|
|
22
|
-
|
|
23
|
-
expected_branches := {b | some b in data.template.snippet.on.push.branches}
|
|
24
|
-
|
|
25
|
-
expected_permissions := data.template.snippet.jobs["release-publish"].permissions
|
|
26
|
-
|
|
27
|
-
# Required publish-step (за маркером): expected `with.package` value з template.
|
|
28
|
-
expected_publish_with_package := s.with.package if {
|
|
29
|
-
some s in data.template.snippet.jobs["release-publish"].steps
|
|
30
|
-
contains(object.get(s, "uses", ""), publish_action_marker)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
# ── deny: paths містить кожне з expected_paths (subset-of) ───────────────
|
|
34
|
-
|
|
35
|
-
deny contains msg if {
|
|
36
|
-
some required_path in expected_paths
|
|
37
|
-
not path_present(required_path)
|
|
38
|
-
msg := sprintf("npm-publish.yml: у on.push.paths має бути `%s` (npm-module.mdc)", [required_path])
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# ── deny: branches містить кожне з expected_branches (subset-of) ─────────
|
|
42
|
-
|
|
43
|
-
deny contains msg if {
|
|
44
|
-
some required_branch in expected_branches
|
|
45
|
-
not required_branch in {b | some b in gha_on.push.branches}
|
|
46
|
-
msg := sprintf("npm-publish.yml: on.push.branches має містити `%s` (npm-module.mdc)", [required_branch])
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
# ── deny: id-token: write у permissions хоч одного job ────────────────────
|
|
50
|
-
|
|
51
|
-
deny contains msg if {
|
|
52
|
-
required := expected_permissions["id-token"]
|
|
53
|
-
not any_job_has_id_token(required)
|
|
54
|
-
msg := sprintf("npm-publish.yml: permissions має містити `id-token: %s` (OIDC) (npm-module.mdc)", [required])
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# ── deny: крок з uses-маркером npm-publish та канонічним with.package ────
|
|
58
|
-
|
|
59
|
-
deny contains msg if {
|
|
60
|
-
not has_npm_publish_step
|
|
61
|
-
msg := sprintf(
|
|
62
|
-
"npm-publish.yml: очікується `uses: %s` з `with.package: %s` (npm-module.mdc)",
|
|
63
|
-
[publish_action_marker, expected_publish_with_package],
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# ── helpers ────────────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
# Path присутній, якщо хоч один шлях у actual містить required як substring
|
|
70
|
-
# (npm/** glob у workflow може бути записаний як `npm/**` або `'npm/**'`).
|
|
71
|
-
path_present(required) if {
|
|
72
|
-
some p in gha_on.push.paths
|
|
73
|
-
is_string(p)
|
|
74
|
-
contains(p, required)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
any_job_has_id_token(required) if {
|
|
78
|
-
some job in object.get(input, "jobs", {})
|
|
79
|
-
job.permissions["id-token"] == required
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
has_npm_publish_step if {
|
|
83
|
-
some job in object.get(input, "jobs", {})
|
|
84
|
-
some step in object.get(job, "steps", [])
|
|
85
|
-
contains(object.get(step, "uses", ""), publish_action_marker)
|
|
86
|
-
step.with.package == expected_publish_with_package
|
|
87
|
-
}
|