@nitra/cursor 3.22.0 → 3.23.1
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/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
- package/AGENTS.template.md +4 -0
- package/CHANGELOG.md +37 -3
- package/bin/docs/n-cursor.md +636 -0
- package/bin/docs/rename-yaml-extensions.md +207 -0
- package/bin/n-cursor.js +30 -3
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +18 -0
- package/rules/abie/js/docs/applies.md +26 -0
- package/rules/abie/js/docs/env_dns.md +32 -0
- package/rules/abie/js/docs/firebase_hosting.md +23 -0
- package/rules/abie/js/docs/hc_pairing.md +35 -0
- package/rules/abie/js/docs/ua_http_route.md +28 -0
- package/rules/abie/js/docs/ua_node_selector.md +28 -0
- package/rules/abie/lib/docs/enabled.md +29 -0
- package/rules/abie/lib/docs/env-dns.md +35 -0
- package/rules/abie/lib/docs/hc-yaml.md +33 -0
- package/rules/abie/lib/docs/http-route.md +44 -0
- package/rules/abie/lib/docs/k8s-tree.md +40 -0
- package/rules/abie/lib/docs/kustomization-patches.md +47 -0
- package/rules/abie/lib/docs/overlay-paths.md +38 -0
- package/rules/abie/lib/docs/yaml.md +29 -0
- package/rules/adr/docs/fix.md +148 -0
- package/rules/adr/js/docs/hooks.md +259 -0
- package/rules/bun/docs/fix.md +156 -0
- package/rules/bun/js/docs/layout.md +393 -0
- package/rules/capacitor/docs/fix.md +121 -0
- package/rules/capacitor/js/docs/platforms.md +295 -0
- package/rules/changelog/changelog.mdc +4 -2
- package/rules/changelog/docs/fix.md +174 -0
- package/rules/changelog/js/consistency.mjs +114 -13
- package/rules/changelog/js/docs/consistency.md +387 -0
- package/rules/changelog/lib/docs/package-manifest.md +210 -0
- package/rules/ci4/docs/fix.md +179 -0
- package/rules/ci4/js/docs/marksman_config.md +128 -0
- package/rules/docker/docker.mdc +8 -3
- package/rules/docker/docs/fix.md +171 -0
- package/rules/docker/js/docs/lint.md +258 -0
- package/rules/docker/lib/docs/docker-hadolint.md +184 -0
- package/rules/docker/lib/docs/docker-mirror.md +247 -0
- package/rules/docker/lib/docs/docker-native-addon.md +170 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
- package/rules/docker/lint/docs/lint.md +193 -0
- package/rules/efes/docs/fix.md +203 -0
- package/rules/feedback/docs/fix.md +140 -0
- package/rules/flow/docs/fix.md +152 -0
- package/rules/ga/docs/fix.md +158 -0
- package/rules/ga/js/docs/lint.md +100 -0
- package/rules/ga/js/docs/workflows.md +217 -0
- package/rules/ga/lint/docs/lint.md +209 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
- package/rules/graphql/docs/fix.md +126 -0
- package/rules/graphql/js/docs/tooling.md +264 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
- package/rules/hasura/docs/fix.md +120 -0
- package/rules/hasura/hasura.mdc +14 -0
- package/rules/hasura/js/docs/internal_urls.md +326 -0
- package/rules/image-avif/docs/fix.md +132 -0
- package/rules/image-avif/js/docs/avif_generation.md +241 -0
- package/rules/image-compress/docs/fix.md +150 -0
- package/rules/image-compress/js/docs/package_setup.md +191 -0
- package/rules/js-bun-db/docs/fix.md +148 -0
- package/rules/js-bun-db/js/docs/safety.md +231 -0
- package/rules/js-bun-db/js-bun-db.mdc +42 -13
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
- package/rules/js-bun-redis/docs/fix.md +123 -0
- package/rules/js-bun-redis/js/docs/imports.md +176 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
- package/rules/js-lint/docs/fix.md +117 -0
- package/rules/js-lint/js/docs/lint.md +250 -0
- package/rules/js-lint/js/docs/tooling.md +348 -0
- package/rules/js-lint/js/docs/utils_imports.md +207 -0
- package/rules/js-lint-ci/docs/fix.md +154 -0
- package/rules/js-lint-ci/js/docs/lint.md +144 -0
- package/rules/js-mssql/docs/fix.md +128 -0
- package/rules/js-mssql/js/docs/deps.md +263 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
- package/rules/js-run/docs/fix.md +144 -0
- package/rules/js-run/js/docs/runtime.md +388 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
- package/rules/js-run/lib/docs/check-env-scan.md +433 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
- package/rules/k8s/docs/fix.md +129 -0
- package/rules/k8s/js/docs/manifests.md +344 -0
- package/rules/k8s/js/manifests.mjs +6 -2
- package/rules/k8s/k8s.mdc +4 -2
- package/rules/k8s/lint/docs/lint.md +411 -0
- package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
- package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
- package/rules/nginx-default-tpl/docs/fix.md +124 -0
- package/rules/nginx-default-tpl/js/docs/template.md +378 -0
- package/rules/npm-module/docs/fix.md +98 -0
- package/rules/npm-module/js/docs/package_structure.md +274 -0
- package/rules/npm-module/js/docs/rule_meta.md +137 -0
- package/rules/npm-module/js/docs/skill_meta.md +190 -0
- package/rules/php/docs/fix.md +107 -0
- package/rules/php/js/docs/tooling.md +152 -0
- package/rules/php/lint/docs/lint.md +215 -0
- package/rules/python/docs/fix.md +163 -0
- package/rules/python/js/docs/applies.md +108 -0
- package/rules/python/js/docs/tooling.md +153 -0
- package/rules/python/lint/docs/lint.md +322 -0
- package/rules/rego/docs/fix.md +121 -0
- package/rules/rego/js/docs/applies.md +174 -0
- package/rules/rego/js/docs/lint.md +118 -0
- package/rules/rego/lint/docs/lint.md +204 -0
- package/rules/release/docs/change.md +185 -0
- package/rules/release/docs/fix.md +119 -0
- package/rules/release/docs/release.md +222 -0
- package/rules/release/lib/docs/aggregate.md +246 -0
- package/rules/release/lib/docs/change-file.md +200 -0
- package/rules/release/lib/docs/fallback.md +203 -0
- package/rules/rust/docs/fix.md +129 -0
- package/rules/rust/js/docs/applies.md +140 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
- package/rules/security/docs/fix.md +86 -0
- package/rules/security/js/docs/lint.md +171 -0
- package/rules/security/js/docs/sample_secret.md +190 -0
- package/rules/security/js/docs/trufflehog.md +137 -0
- package/rules/security/js/lint.mjs +9 -1
- package/rules/style-lint/docs/fix.md +155 -0
- package/rules/style-lint/js/docs/lint.md +184 -0
- package/rules/style-lint/js/docs/tooling.md +194 -0
- package/rules/tauri/docs/fix.md +158 -0
- package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
- package/rules/tauri/js/docs/tooling.md +228 -0
- package/rules/test/coverage/coverage.mjs +15 -3
- package/rules/test/docs/fix.md +132 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
- package/rules/test/js/docs/cargo_mutants_config.md +173 -0
- package/rules/test/js/docs/location.md +136 -0
- package/rules/test/js/docs/no-process-chdir.md +160 -0
- package/rules/test/js/docs/no-relative-fs-path.md +271 -0
- package/rules/test/js/docs/stryker_config.md +152 -0
- package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
- package/rules/text/docs/fix.md +118 -0
- package/rules/text/js/docs/forbidden-prettier.md +143 -0
- package/rules/text/js/docs/formatting.md +256 -0
- package/rules/text/js/docs/lint.md +122 -0
- package/rules/text/lint/docs/lint.md +220 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
- package/rules/text/lint/docs/run-shellcheck.md +212 -0
- package/rules/text/lint/docs/run-v8r.md +197 -0
- package/rules/vue/docs/fix.md +127 -0
- package/rules/vue/js/docs/packages.md +335 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
- package/rules/worktree/docs/fix.md +161 -0
- package/schemas/rule-meta.json +5 -1
- package/scripts/auto-rules.mjs +7 -4
- package/scripts/coverage-classify/docs/apply.md +202 -0
- package/scripts/coverage-classify/docs/cache.md +203 -0
- package/scripts/coverage-classify/docs/index.md +218 -0
- package/scripts/coverage-classify/docs/prompt.md +132 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
- package/scripts/coverage-fix-extract.mjs +122 -0
- package/scripts/coverage-fix.mjs +1 -1
- package/scripts/dispatcher/docs/graph.md +346 -0
- package/scripts/dispatcher/docs/index.md +236 -0
- package/scripts/dispatcher/docs/trace.md +296 -0
- package/scripts/dispatcher/index.mjs +1 -1
- package/scripts/dispatcher/lib/active.mjs +4 -8
- package/scripts/dispatcher/lib/commands.mjs +7 -11
- package/scripts/dispatcher/lib/docs/active.md +348 -0
- package/scripts/dispatcher/lib/docs/artifact.md +232 -0
- package/scripts/dispatcher/lib/docs/budget.md +167 -0
- package/scripts/dispatcher/lib/docs/capability.md +196 -0
- package/scripts/dispatcher/lib/docs/commands.md +210 -0
- package/scripts/dispatcher/lib/docs/events.md +182 -0
- package/scripts/dispatcher/lib/docs/executor.md +190 -0
- package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
- package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
- package/scripts/dispatcher/lib/docs/gate.md +231 -0
- package/scripts/dispatcher/lib/docs/level.md +335 -0
- package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
- package/scripts/dispatcher/lib/docs/plan.md +200 -0
- package/scripts/dispatcher/lib/docs/planner.md +269 -0
- package/scripts/dispatcher/lib/docs/review.md +255 -0
- package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
- package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
- package/scripts/dispatcher/lib/docs/spec.md +203 -0
- package/scripts/dispatcher/lib/docs/state-store.md +303 -0
- package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
- package/scripts/dispatcher/lib/executor.mjs +6 -1
- package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
- package/scripts/dispatcher/lib/level.mjs +29 -3
- package/scripts/dispatcher/lib/review.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
- package/scripts/docs/auto-rules.md +376 -0
- package/scripts/docs/auto-skills.md +173 -0
- package/scripts/docs/build-agents-commands.md +183 -0
- package/scripts/docs/cli-entry.md +153 -0
- package/scripts/docs/coverage-fix.md +177 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
- package/scripts/lib/changed-files.mjs +4 -1
- package/scripts/lib/docs/changed-files.md +149 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
- package/scripts/lib/docs/check-reporter.md +175 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
- package/scripts/lib/docs/discover-checkable-rules.md +165 -0
- package/scripts/lib/docs/ensure-tool.md +254 -0
- package/scripts/lib/docs/generated-markdown.md +275 -0
- package/scripts/lib/docs/gha-workflow.md +326 -0
- package/scripts/lib/docs/inline-template-links.md +303 -0
- package/scripts/lib/docs/list-rule-ids.md +156 -0
- package/scripts/lib/docs/load-cursor-config.md +147 -0
- package/scripts/lib/docs/mirror-parity.md +167 -0
- package/scripts/lib/worktree.mjs +26 -0
- package/scripts/worktree-cli.mjs +12 -2
- package/skills/coverage-fix/SKILL.md +34 -45
- package/skills/docgen/SKILL.md +44 -23
- package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
- package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
- package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
- package/skills/docgen/js/docgen-ignore.mjs +54 -0
- package/skills/docgen/js/docgen-scan.mjs +37 -21
- package/skills/llm-patch/SKILL.md +23 -2
- package/skills/start-check/SKILL.md +26 -53
- package/skills/start-check/js/check.mjs +211 -0
- package/skills/taze/SKILL.md +9 -3
- package/skills/taze/js/diff.mjs +154 -0
- package/types/bin/n-cursor.d.ts +1 -1
- package/skills/fix-tests/SKILL.md +0 -119
- package/skills/fix-tests/meta.json +0 -1
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# gha-workflow.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `gha-workflow.mjs` — це набір допоміжних чистих функцій для **структурного аналізу GitHub Actions workflow-файлів** (`.yml`) після їх розбору як YAML.
|
|
6
|
+
|
|
7
|
+
Призначення модуля — замінити крихкий пошук підрядків у сирому тексті workflow-файла на **типобезпечну** перевірку значень `uses:` та `run:` у кроках (`steps`) робіт (`jobs`). Модуль використовується сценаріями перевірки (checkers) проєктних правил:
|
|
8
|
+
|
|
9
|
+
- `check-ga` — загальна перевірка GitHub Actions workflows;
|
|
10
|
+
- `check-js-lint` — перевірка структури `lint-js.yml`;
|
|
11
|
+
- `check-text` — перевірка наявності викликів `bun run lint-text` у CI;
|
|
12
|
+
- `check-style-lint` — перевірка викликів стайл-лінту в CI;
|
|
13
|
+
- `check-npm-module` — перевірка workflow npm-модуля.
|
|
14
|
+
|
|
15
|
+
Крім перевірки значень `uses:` та `run:`, модуль уміє:
|
|
16
|
+
|
|
17
|
+
- розпізнавати локальну composite-action `./.github/actions/setup-bun-deps`;
|
|
18
|
+
- перевіряти, що `actions/checkout@v6` викликається з `with.persist-credentials: false`;
|
|
19
|
+
- виявляти заборонені в CI прапорці `--fix` у викликах `oxlint` та `eslint`;
|
|
20
|
+
- перевіряти включення точного `glob` у списки `on.push.paths` / `on.pull_request.paths`.
|
|
21
|
+
|
|
22
|
+
Модуль виконує лише читання та обчислення — він **не змінює** жодних файлів і **не виконує** жодних команд. Поведінка детермінована й залежить тільки від переданих аргументів.
|
|
23
|
+
|
|
24
|
+
## Експорти / API
|
|
25
|
+
|
|
26
|
+
Усі експорти з модуля — це **named exports** (іменовані експорти), `default export` немає.
|
|
27
|
+
|
|
28
|
+
| Експорт | Тип | Короткий опис |
|
|
29
|
+
| -------------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
|
|
30
|
+
| `parseWorkflowYaml(content)` | function | Парсить YAML вміст у звичайний об’єкт; при помилці повертає `null`. |
|
|
31
|
+
| `flattenWorkflowSteps(root)` | function | Збирає всі кроки з усіх jobs у плоский список з метаданими `{ jobId, stepIndex, step }`. |
|
|
32
|
+
| `getStepUses(step)` | function | Повертає значення `uses:` кроку або порожній рядок. |
|
|
33
|
+
| `getStepRun(step)` | function | Повертає значення `run:` кроку (підтримує рядок та масив рядків). |
|
|
34
|
+
| `eventPathsIncludeExact(root, event, exact)` | function | Перевіряє, чи містить `on.<event>.paths` точне значення glob. |
|
|
35
|
+
| `verifyLintJsWorkflowStructure(root)` | function | Виконує повний набір структурних перевірок для `lint-js.yml`. |
|
|
36
|
+
| `anyRunStepIncludes(root, needle)` | function | Перевіряє, чи містить будь-який `run` кроку заданий підрядок. |
|
|
37
|
+
|
|
38
|
+
Внутрішні (неекспортовані) функції-помічники:
|
|
39
|
+
|
|
40
|
+
- `workflowJobsEntries(root)` — повертає `[jobId, job][]`;
|
|
41
|
+
- `workflowJobSteps(job)` — повертає масив об’єктних кроків job;
|
|
42
|
+
- `hasCheckoutWithPersistCredentialsFalse(steps)` — перевіряє `checkout@v6` з `persist-credentials: false`;
|
|
43
|
+
- `appendCiFixFlagFailures(failures, steps)` — додає у `failures` рядки про заборонені `--fix` у CI.
|
|
44
|
+
|
|
45
|
+
Внутрішні константи модуля:
|
|
46
|
+
|
|
47
|
+
- `CHECKOUT_V6_USES = 'actions/checkout@v6'` — очікувана дія checkout та її версія.
|
|
48
|
+
- `LOCAL_SETUP_BUN_DEPS_MARKER = './.github/actions/setup-bun-deps'` — шлях до локальної composite-action для встановлення Bun-залежностей.
|
|
49
|
+
- `BUNX_OXLINT_FIX_RE = /bunx\s+oxlint[^\n]*--fix/u` — регулярний вираз для виявлення `bunx oxlint ... --fix` в одному рядку.
|
|
50
|
+
|
|
51
|
+
## Функції
|
|
52
|
+
|
|
53
|
+
### `parseWorkflowYaml(content)`
|
|
54
|
+
|
|
55
|
+
**Сигнатура:** `parseWorkflowYaml(content: string): Record<string, unknown> | null`
|
|
56
|
+
|
|
57
|
+
**Параметри:**
|
|
58
|
+
|
|
59
|
+
- `content` — рядок із вмістом workflow-файла `.yml`.
|
|
60
|
+
|
|
61
|
+
**Повертає:**
|
|
62
|
+
|
|
63
|
+
- розібраний YAML як звичайний об’єкт (`Record<string, unknown>`), якщо вміст парситься і має тип `object` та не є `null`;
|
|
64
|
+
- `null` — якщо `yaml.parse` кинув виняток або результат не є об’єктом.
|
|
65
|
+
|
|
66
|
+
**Side effects:** немає. Помилка парсингу мовчки перехоплюється `try/catch`.
|
|
67
|
+
|
|
68
|
+
**Примітки:** ця функція безпечна для викликача — навіть на некоректному YAML вона не падає, а повертає `null`, що далі обробляється у `verifyLintJsWorkflowStructure` як спеціальний випадок.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `flattenWorkflowSteps(root)`
|
|
73
|
+
|
|
74
|
+
**Сигнатура:** `flattenWorkflowSteps(root: Record<string, unknown>): { jobId: string, stepIndex: number, step: Record<string, unknown> }[]`
|
|
75
|
+
|
|
76
|
+
**Параметри:**
|
|
77
|
+
|
|
78
|
+
- `root` — корінь розібраного YAML (об’єкт із полем `jobs`).
|
|
79
|
+
|
|
80
|
+
**Повертає:** плоский масив об’єктів, кожен з яких містить:
|
|
81
|
+
|
|
82
|
+
- `jobId` — ім’я job (ключ у `jobs`);
|
|
83
|
+
- `stepIndex` — порядковий номер кроку всередині `steps` цього job (починається з 0);
|
|
84
|
+
- `step` — сам об’єкт кроку.
|
|
85
|
+
|
|
86
|
+
**Side effects:** немає.
|
|
87
|
+
|
|
88
|
+
**Алгоритм:** ітерує через `workflowJobsEntries(root)`, для кожного job отримує `workflowJobSteps(job)`, нумерує кроки за допомогою `Array.prototype.entries()` та пуш у акумулятор. Невалідні (необ’єктні) jobs та невалідні steps пропускаються.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### `getStepUses(step)`
|
|
93
|
+
|
|
94
|
+
**Сигнатура:** `getStepUses(step: Record<string, unknown>): string`
|
|
95
|
+
|
|
96
|
+
**Параметри:**
|
|
97
|
+
|
|
98
|
+
- `step` — об’єкт одного елемента масиву `steps`.
|
|
99
|
+
|
|
100
|
+
**Повертає:** значення `step.uses`, якщо це рядок; інакше — порожній рядок `''`.
|
|
101
|
+
|
|
102
|
+
**Side effects:** немає.
|
|
103
|
+
|
|
104
|
+
**Призначення:** уніфікований доступ до значення `uses:` без перевірок типу у викликачів.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `getStepRun(step)`
|
|
109
|
+
|
|
110
|
+
**Сигнатура:** `getStepRun(step: Record<string, unknown>): string`
|
|
111
|
+
|
|
112
|
+
**Параметри:**
|
|
113
|
+
|
|
114
|
+
- `step` — об’єкт одного елемента масиву `steps`.
|
|
115
|
+
|
|
116
|
+
**Повертає:** текст команди `run:`:
|
|
117
|
+
|
|
118
|
+
- якщо `step.run` — рядок, повертається як є;
|
|
119
|
+
- якщо `step.run` — масив, кожен елемент конвертується через `String(...)` та з’єднується через `\n`;
|
|
120
|
+
- інакше — `''`.
|
|
121
|
+
|
|
122
|
+
**Side effects:** немає.
|
|
123
|
+
|
|
124
|
+
**Примітки:** YAML дозволяє запис `run:` як багаторядкового скаляра (`|` / `>-`) або як масиву рядків. Функція нормалізує обидва випадки до одного `string`, який потім зручно перевіряти через `.includes(...)` або регулярним виразом.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### `eventPathsIncludeExact(root, event, exact)`
|
|
129
|
+
|
|
130
|
+
**Сигнатура:** `eventPathsIncludeExact(root: Record<string, unknown>, event: 'push' | 'pull_request', exact: string): boolean`
|
|
131
|
+
|
|
132
|
+
**Параметри:**
|
|
133
|
+
|
|
134
|
+
- `root` — корінь workflow;
|
|
135
|
+
- `event` — ім’я ключа в `on`: `'push'` або `'pull_request'`;
|
|
136
|
+
- `exact` — очікуваний рядок (glob), який має бути присутній у `paths`.
|
|
137
|
+
|
|
138
|
+
**Повертає:** `true`, якщо у `root.on[event].paths` є масив і він містить точне значення `exact`. У всіх інших випадках (відсутній `on`, відсутній `event`, `paths` не масив тощо) — `false`.
|
|
139
|
+
|
|
140
|
+
**Side effects:** немає.
|
|
141
|
+
|
|
142
|
+
**Гарантії безпеки:** функція захищена від відсутніх та некоректних типів проміжних об’єктів, тому її можна викликати на будь-якому `root`, повернутому з `parseWorkflowYaml`.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### `verifyLintJsWorkflowStructure(root)`
|
|
147
|
+
|
|
148
|
+
**Сигнатура:** `verifyLintJsWorkflowStructure(root: Record<string, unknown> | null): { ok: boolean, failures: string[] }`
|
|
149
|
+
|
|
150
|
+
**Параметри:**
|
|
151
|
+
|
|
152
|
+
- `root` — корінь розібраного workflow або `null`, якщо парсинг не вдався.
|
|
153
|
+
|
|
154
|
+
**Повертає:** об’єкт результату:
|
|
155
|
+
|
|
156
|
+
- `{ ok: true, failures: [] }` — усі перевірки пройдено;
|
|
157
|
+
- `{ ok: false, failures: [...] }` — список причин відмови у вигляді людинозрозумілих українських повідомлень.
|
|
158
|
+
|
|
159
|
+
**Side effects:** немає.
|
|
160
|
+
|
|
161
|
+
**Перевірки, які виконуються (у порядку додавання до `failures`):**
|
|
162
|
+
|
|
163
|
+
1. Якщо `root === null` — повертається одразу `{ ok: false, failures: ['YAML не вдалося розібрати — перевір синтаксис workflow'] }`.
|
|
164
|
+
2. У жодному кроці немає `uses:` з підрядком `'actions/checkout@v6'` → `'немає кроку uses: actions/checkout@v6'`.
|
|
165
|
+
3. Серед кроків з `actions/checkout@v6` немає такого, що містить `with.persist-credentials === false` → `'checkout@v6 без with.persist-credentials: false'`.
|
|
166
|
+
4. У жодному кроці немає `uses:` з підрядком `'./.github/actions/setup-bun-deps'` → `'немає uses: ./.github/actions/setup-bun-deps'`.
|
|
167
|
+
5. У сумарному `run`-блобі немає `'bunx oxlint'` → `'у run немає bunx oxlint'`.
|
|
168
|
+
6. У сумарному `run`-блобі немає `'bunx eslint .'` → `'у run немає bunx eslint .'`.
|
|
169
|
+
7. У сумарному `run`-блобі немає `'bunx jscpd .'` → `'у run немає bunx jscpd .'`.
|
|
170
|
+
8. Для кожного кроку, чий `run` матчиться `BUNX_OXLINT_FIX_RE`, додається `'у run є oxlint з --fix (у CI заборонено)'`.
|
|
171
|
+
9. Для кожного кроку, чий `run` містить `'eslint --fix'`, додається `'у run є eslint --fix (у CI заборонено)'`.
|
|
172
|
+
|
|
173
|
+
**Примітка:** «сумарний `run`-блоб» — це `flattenWorkflowSteps(root).map(s => getStepRun(s.step)).join('\n')`. Тобто перевірки 5–7 пасять, навіть якщо `bunx oxlint`, `bunx eslint .` та `bunx jscpd .` рознесені по різних кроках.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### `anyRunStepIncludes(root, needle)`
|
|
178
|
+
|
|
179
|
+
**Сигнатура:** `anyRunStepIncludes(root: Record<string, unknown>, needle: string): boolean`
|
|
180
|
+
|
|
181
|
+
**Параметри:**
|
|
182
|
+
|
|
183
|
+
- `root` — корінь workflow;
|
|
184
|
+
- `needle` — підрядок для пошуку в текстах `run:`.
|
|
185
|
+
|
|
186
|
+
**Повертає:** `true`, якщо знайдено принаймні один крок, у `run:` якого є `needle`; інакше `false`.
|
|
187
|
+
|
|
188
|
+
**Side effects:** немає. Ітерація припиняється на першому збігу (рання передача).
|
|
189
|
+
|
|
190
|
+
**Типовий приклад:** `anyRunStepIncludes(root, 'bun run lint-text')` для `check-text` — перевірити, що CI взагалі викликає таргет лінту текстів.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### `workflowJobsEntries(root)` (internal)
|
|
195
|
+
|
|
196
|
+
**Сигнатура:** `workflowJobsEntries(root: Record<string, unknown>): [string, Record<string, unknown>][]`
|
|
197
|
+
|
|
198
|
+
**Параметри:** `root` — корінь workflow.
|
|
199
|
+
|
|
200
|
+
**Повертає:** список пар `[jobId, job]` для тих ключів `jobs`, у яких значення є непорожнім об’єктом.
|
|
201
|
+
|
|
202
|
+
**Side effects:** немає.
|
|
203
|
+
|
|
204
|
+
**Алгоритм:** перевіряє наявність та тип `root.jobs`, далі `Object.entries(jobs).flatMap(...)` фільтрує невалідні значення (масив порожніх або одно-елементних масивів).
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### `workflowJobSteps(job)` (internal)
|
|
209
|
+
|
|
210
|
+
**Сигнатура:** `workflowJobSteps(job: Record<string, unknown>): Record<string, unknown>[]`
|
|
211
|
+
|
|
212
|
+
**Параметри:** `job` — один job-об’єкт.
|
|
213
|
+
|
|
214
|
+
**Повертає:** масив об’єктних кроків з `job.steps`; невалідні (необ’єктні / `null`) елементи фільтруються.
|
|
215
|
+
|
|
216
|
+
**Side effects:** немає.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
### `hasCheckoutWithPersistCredentialsFalse(steps)` (internal)
|
|
221
|
+
|
|
222
|
+
**Сигнатура:** `hasCheckoutWithPersistCredentialsFalse(steps: { step: Record<string, unknown> }[]): boolean`
|
|
223
|
+
|
|
224
|
+
**Параметри:** `steps` — результат `flattenWorkflowSteps` (використовується тільки поле `step`).
|
|
225
|
+
|
|
226
|
+
**Повертає:** `true`, якщо знайдено крок, у якого:
|
|
227
|
+
|
|
228
|
+
- `uses` містить `'actions/checkout@v6'`;
|
|
229
|
+
- `step.with` — об’єкт;
|
|
230
|
+
- `step.with['persist-credentials'] === false` (саме `false`, а не «фолсі»).
|
|
231
|
+
|
|
232
|
+
**Side effects:** немає.
|
|
233
|
+
|
|
234
|
+
**Призначення:** перевірити, що `actions/checkout@v6` явно вимкнув збереження токена в git-конфізі — це вимога безпеки в правилі `n-ga`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### `appendCiFixFlagFailures(failures, steps)` (internal)
|
|
239
|
+
|
|
240
|
+
**Сигнатура:** `appendCiFixFlagFailures(failures: string[], steps: { step: Record<string, unknown> }[]): void`
|
|
241
|
+
|
|
242
|
+
**Параметри:**
|
|
243
|
+
|
|
244
|
+
- `failures` — акумулятор-масив, у який функція **пушить** нові рядки помилок;
|
|
245
|
+
- `steps` — результат `flattenWorkflowSteps`.
|
|
246
|
+
|
|
247
|
+
**Повертає:** `undefined` (мутує `failures`).
|
|
248
|
+
|
|
249
|
+
**Side effects:** мутація переданого масиву `failures` через `Array.prototype.push`.
|
|
250
|
+
|
|
251
|
+
**Логіка:** для кожного кроку:
|
|
252
|
+
|
|
253
|
+
- якщо `BUNX_OXLINT_FIX_RE.test(run)` — додає повідомлення про заборонений `--fix` у `bunx oxlint`;
|
|
254
|
+
- якщо `run.includes('eslint --fix')` — додає повідомлення про заборонений `eslint --fix`.
|
|
255
|
+
|
|
256
|
+
**Примітка:** функція може додати **обидва** повідомлення для одного і того ж кроку, якщо в `run` присутні обидва патерни. Якщо в різних кроках присутній один і той самий патерн — повідомлення додасться **кілька разів** (по одному на крок).
|
|
257
|
+
|
|
258
|
+
## Залежності
|
|
259
|
+
|
|
260
|
+
**Зовнішні npm-пакети:**
|
|
261
|
+
|
|
262
|
+
- `yaml` — функція `parse(content)` для розбору YAML у JS-об’єкт. Імпорт іменований: `import { parse } from 'yaml'`.
|
|
263
|
+
|
|
264
|
+
**Стандартна бібліотека JS:** `Object.entries`, `Array.isArray`, `Array.prototype.flatMap`, `Array.prototype.map`, `Array.prototype.entries`, `Array.prototype.some`, `Array.prototype.includes`, `Array.prototype.join`, `RegExp.prototype.test`, `String.prototype.includes`.
|
|
265
|
+
|
|
266
|
+
**Внутрішні залежності проєкту:** жодних модулів проєкту не імпортує — це листовий хелпер.
|
|
267
|
+
|
|
268
|
+
**Хто залежить від цього модуля (зворотні залежності):** згідно з docstring файла — скрипти `check-ga`, `check-js-lint`, `check-text`, `check-style-lint`, `check-npm-module`. Конкретні шляхи до цих скриптів живуть у `npm/scripts/` / `npm/checks/` та використовують іменовані експорти модуля.
|
|
269
|
+
|
|
270
|
+
## Потік виконання / Використання
|
|
271
|
+
|
|
272
|
+
### Типовий цикл використання сценарієм-checker
|
|
273
|
+
|
|
274
|
+
1. Сценарій читає вміст файла `.github/workflows/<name>.yml` як рядок (наприклад через `node:fs/promises`).
|
|
275
|
+
2. Викликає `const root = parseWorkflowYaml(content)`.
|
|
276
|
+
3. Якщо `root === null` — повідомляє про синтаксичну помилку YAML.
|
|
277
|
+
4. Інакше викликає одну зі спеціалізованих перевірок (наприклад `verifyLintJsWorkflowStructure(root)`) або серію загальних (`flattenWorkflowSteps`, `getStepUses`, `getStepRun`, `anyRunStepIncludes`, `eventPathsIncludeExact`).
|
|
278
|
+
5. На основі результату формує звіт перевірки.
|
|
279
|
+
|
|
280
|
+
### Приклад: перевірка `lint-js.yml`
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
import { readFile } from 'node:fs/promises'
|
|
284
|
+
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './gha-workflow.mjs'
|
|
285
|
+
|
|
286
|
+
const content = await readFile('.github/workflows/lint-js.yml', 'utf8')
|
|
287
|
+
const root = parseWorkflowYaml(content)
|
|
288
|
+
const result = verifyLintJsWorkflowStructure(root)
|
|
289
|
+
|
|
290
|
+
if (!result.ok) {
|
|
291
|
+
for (const f of result.failures) {
|
|
292
|
+
console.error('lint-js.yml:', f)
|
|
293
|
+
}
|
|
294
|
+
process.exit(1)
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Приклад: перевірка наявності таргета `bun run lint-text`
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
import { parseWorkflowYaml, anyRunStepIncludes } from './gha-workflow.mjs'
|
|
302
|
+
|
|
303
|
+
const root = parseWorkflowYaml(content)
|
|
304
|
+
if (root && !anyRunStepIncludes(root, 'bun run lint-text')) {
|
|
305
|
+
console.error('у CI відсутній виклик bun run lint-text')
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Приклад: перевірка `on.pull_request.paths`
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import { parseWorkflowYaml, eventPathsIncludeExact } from './gha-workflow.mjs'
|
|
313
|
+
|
|
314
|
+
const root = parseWorkflowYaml(content)
|
|
315
|
+
if (root && !eventPathsIncludeExact(root, 'pull_request', '**/*.vue')) {
|
|
316
|
+
console.error('on.pull_request.paths не містить **/*.vue')
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Властивості, корисні викликачам
|
|
321
|
+
|
|
322
|
+
- **Чисті функції.** Жодних бічних ефектів (крім явної мутації `failures` в `appendCiFixFlagFailures`, яка інкапсульована всередині `verifyLintJsWorkflowStructure`).
|
|
323
|
+
- **Безпечність до помилок типів.** Усі публічні функції захищені від некоректних або відсутніх ключів — повертають `''`, `false` або `[]` замість падіння.
|
|
324
|
+
- **Уніфікований доступ.** `getStepUses` та `getStepRun` нормалізують форму YAML (рядок vs масив), щоб викликач завжди працював із `string`.
|
|
325
|
+
- **Точне `paths`-зіставлення.** `eventPathsIncludeExact` вимагає **точного** елемента масиву, а не підрядка — тому glob-патерни мають бути записані як є.
|
|
326
|
+
- **Сумарний `run`-блоб.** У `verifyLintJsWorkflowStructure` пункти 5–7 не вимагають, щоб усі команди жили в одному `run`-кроці — вони можуть бути рознесені по кроках/jobs.
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# inline-template-links.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `inline-template-links.mjs` — допоміжна утиліта для **build-кроку обробки `.mdc`-правил**. Її роль: у текстовому вмісті `.mdc`-документа знайти Markdown-посилання, які ведуть на template-файли (шлях містить сегмент `/template/` або `/templates/`), і **замінити** ці посилання на **інлайн fenced-блоки** з фактичним вмістом target-файлу.
|
|
6
|
+
|
|
7
|
+
Простими словами: замість того щоб у згенерованому правилі читач бачив посилання `[конфіг](./templates/package.json.snippet.json)`, він побачить безпосередньо назву реального файлу (`package.json`) і fenced-блок із його вмістом — це робить правило «самодостатнім», без потреби клікати по лінках.
|
|
8
|
+
|
|
9
|
+
Особливості:
|
|
10
|
+
|
|
11
|
+
- Працює асинхронно (`async`), бо читає файли через `node:fs/promises`.
|
|
12
|
+
- Робить **fail-loud** валідацію: якщо посилання вказує на неіснуючий файл — кидає `Error` (а не мовчки пропускає), щоб автор правила одразу побачив проблему.
|
|
13
|
+
- «Розгортає» спеціальні суфікси `.snippet.<ext>` / `.deny.<ext>` / `.contains.<ext>` до імені реального target-файлу, який вони описують (наприклад `package.json.snippet.json` → `package.json`).
|
|
14
|
+
- Безпечно щодо ReDoS: усі regexp — статичні літерали з обмеженням довжини, без `new RegExp(variable)` із користувацьких даних.
|
|
15
|
+
|
|
16
|
+
Модуль експортує єдину функцію `inlineTemplateLinks(text, ruleDir)`.
|
|
17
|
+
|
|
18
|
+
## Експорти / API
|
|
19
|
+
|
|
20
|
+
| Експорт | Тип | Призначення |
|
|
21
|
+
| --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `inlineTemplateLinks` | `async function(text: string, ruleDir: string): Promise<string>` | Замінює Markdown-посилання на template-файли в `.mdc`-тексті на інлайн fenced-блоки з фактичним вмістом цих файлів. |
|
|
23
|
+
|
|
24
|
+
Усі інші імена в модулі (`langFromExt`, `normalizeTargetName`, константи `MD_LINK_RE`, `TEMPLATE_SEGMENT_RE`, `SLOT_SUFFIX_RES`) — **внутрішні** (не експортуються).
|
|
25
|
+
|
|
26
|
+
## Внутрішні константи
|
|
27
|
+
|
|
28
|
+
### `MD_LINK_RE`
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
;/\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Глобальний regexp, який ловить **Markdown-посилання вигляду `[label](./path)`** із обов'язковим префіксом `./` у href. Group 1 — текст посилання (до 200 символів), group 2 — шлях (до 500 символів, що починається з `./`). Обмеження довжин — захист від ReDoS / pathological input.
|
|
35
|
+
|
|
36
|
+
### `TEMPLATE_SEGMENT_RE`
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
;/\/templates?\//
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Перевіряє, чи шлях містить сегмент `/template/` або `/templates/`. Тільки такі посилання вважаються «template-посиланнями» і підлягають заміні; інші Markdown-лінки залишаються недоторканими.
|
|
43
|
+
|
|
44
|
+
### `SLOT_SUFFIX_RES`
|
|
45
|
+
|
|
46
|
+
Масив із трьох **статичних** regexp:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
;[/^(.+)\.snippet\.[^.]+$/, /^(.+)\.deny\.[^.]+$/, /^(.+)\.contains\.[^.]+$/]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Кожен ловить ім'я файлу з суфіксом-«слотом»: `<name>.snippet.<ext>`, `<name>.deny.<ext>`, `<name>.contains.<ext>`. Group 1 — це ім'я реального target-файлу (без суфікса слоту і без власного розширення). Коментар у коді явно зазначає: regexp-літерали статичні, без `RegExp(variable)`.
|
|
53
|
+
|
|
54
|
+
## Функції
|
|
55
|
+
|
|
56
|
+
### `langFromExt(filePath)` — internal
|
|
57
|
+
|
|
58
|
+
Сигнатура:
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
function langFromExt(filePath: string): string
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Параметри:
|
|
65
|
+
|
|
66
|
+
- `filePath` — рядок зі шляхом до файлу (досить навіть базового імені, бо використовується лише розширення).
|
|
67
|
+
|
|
68
|
+
Повертає:
|
|
69
|
+
|
|
70
|
+
- Рядок-ідентифікатор мови для Markdown fenced-блока:
|
|
71
|
+
- `'json'` — якщо розширення `.json`;
|
|
72
|
+
- `'toml'` — якщо `.toml`;
|
|
73
|
+
- `'yaml'` — якщо `.yml` або `.yaml`;
|
|
74
|
+
- `''` (порожній рядок) — для будь-яких інших розширень.
|
|
75
|
+
|
|
76
|
+
Side effects: жодних — чиста функція над рядком.
|
|
77
|
+
|
|
78
|
+
Призначення: визначити, який мовний таг ставити після відкривального ` ``` ` у згенерованому fenced-блоці, щоб підсвічування синтаксису працювало коректно.
|
|
79
|
+
|
|
80
|
+
### `normalizeTargetName(fileBasename)` — internal
|
|
81
|
+
|
|
82
|
+
Сигнатура:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
function normalizeTargetName(fileBasename: string): string
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Параметри:
|
|
89
|
+
|
|
90
|
+
- `fileBasename` — базове ім'я файлу (без шляху), наприклад `package.json.snippet.json`.
|
|
91
|
+
|
|
92
|
+
Повертає:
|
|
93
|
+
|
|
94
|
+
- Якщо ім'я **збігається з одним із regexp у `SLOT_SUFFIX_RES`** (тобто має суфікс `.snippet.<ext>`, `.deny.<ext>` або `.contains.<ext>`) — повертається **group 1** першого збігу (ім'я без слоту). Приклади:
|
|
95
|
+
- `package.json.snippet.json` → `package.json`
|
|
96
|
+
- `eslint.config.js.deny.js` → `eslint.config.js`
|
|
97
|
+
- `Caddyfile.contains.txt` → `Caddyfile`
|
|
98
|
+
- Якщо жоден з regexp не збігся — повертається оригінальне `fileBasename` без змін.
|
|
99
|
+
|
|
100
|
+
Side effects: відсутні.
|
|
101
|
+
|
|
102
|
+
Призначення: для template-файлу з суфіксом-слотом відновити **реальне ім'я target-файлу**, на який цей template посилається; саме це ім'я потім підставляється як заголовок перед fenced-блоком у згенерованому Markdown.
|
|
103
|
+
|
|
104
|
+
Коментар над функцією у вихіднику прямо описує цю поведінку: «Strip `.<slot>.<ext>` suffix (slot ∈ snippet/deny/contains) to recover the real target file name».
|
|
105
|
+
|
|
106
|
+
### `inlineTemplateLinks(text, ruleDir)` — **exported**
|
|
107
|
+
|
|
108
|
+
Сигнатура:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
export async function inlineTemplateLinks(
|
|
112
|
+
text: string,
|
|
113
|
+
ruleDir: string,
|
|
114
|
+
): Promise<string>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Параметри:
|
|
118
|
+
|
|
119
|
+
- `text` — вміст `.mdc`-файлу (повний текст) як рядок.
|
|
120
|
+
- `ruleDir` — **абсолютний** шлях до директорії правила (наприклад `.../npm/rules/security/`). Усі відносні href із `./` резолвляться **відносно цього каталогу**.
|
|
121
|
+
|
|
122
|
+
Повертає:
|
|
123
|
+
|
|
124
|
+
- `Promise<string>` — трансформований текст, у якому всі **template-посилання** замінено на блоки виду:
|
|
125
|
+
|
|
126
|
+
````text
|
|
127
|
+
`<targetName>`:
|
|
128
|
+
|
|
129
|
+
```<lang>
|
|
130
|
+
<contents>
|
|
131
|
+
````
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
де `targetName` — результат `normalizeTargetName(basename(absPath))`, `lang` — результат `langFromExt(absPath)`, а `contents` — вміст файлу після `.trim()`.
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Якщо у тексті немає жодного template-посилання — повертається **той самий `text` без змін** (early-exit).
|
|
140
|
+
|
|
141
|
+
Алгоритм роботи:
|
|
142
|
+
|
|
143
|
+
1. Знаходимо **всі** збіги `MD_LINK_RE` у `text` через `text.matchAll(...)`.
|
|
144
|
+
2. Фільтруємо їх: залишаємо лише ті, у яких href (group 2) містить `/template/` або `/templates/` (через `TEMPLATE_SEGMENT_RE.test(m[2])`).
|
|
145
|
+
3. Якщо після фільтрації збігів **немає** — повертаємо `text` як є.
|
|
146
|
+
4. Кладемо стартовий результат `result = text`.
|
|
147
|
+
5. Для кожного збігу послідовно (`for ... of`, з `await` на читанні файлу):
|
|
148
|
+
1. Деструктуруємо: `const [fullMatch, , href] = match` (label не використовується, тому позиція пропущена).
|
|
149
|
+
2. Будуємо відносний шлях: `relPath = href.slice(2)` — обрізаємо префікс `./` (його гарантує regexp).
|
|
150
|
+
3. Збираємо абсолютний шлях: `absPath = join(ruleDir, relPath)`.
|
|
151
|
+
4. Перевіряємо існування: `existsSync(absPath)`. Якщо файлу немає — **кидаємо** `Error`:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
inlineTemplateLinks: file not found: <absPath> (referenced from .mdc)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Жодного fallback / тихого пропуску — це fail-loud за дизайном.
|
|
158
|
+
|
|
159
|
+
5. Читаємо файл: `raw = await readFile(absPath, 'utf8')`, далі `contents = raw.trim()` (прибираємо хвостові пробіли / переноси).
|
|
160
|
+
6. Обчислюємо `lang = langFromExt(absPath)`.
|
|
161
|
+
7. Обчислюємо `targetName = normalizeTargetName(basename(absPath))`.
|
|
162
|
+
8. Формуємо `replacement` — backtick-екранований заголовок, порожній рядок і fenced-блок із `lang`:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
;`\`${targetName}\`:\n\n\`\`\`${lang}\n${contents}\n\`\`\``
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
9. Робимо заміну: `result = result.replace(fullMatch, () => replacement)`. Передача **callback-форми** в `.replace` критично важлива: інакше спецсимволи у `replacement` (наприклад `$&`, `$1` із вмісту шаблону) трактувалися б як backreferences і зламали б вивід.
|
|
169
|
+
|
|
170
|
+
6. Повертаємо `result`.
|
|
171
|
+
|
|
172
|
+
Side effects:
|
|
173
|
+
|
|
174
|
+
- **Читання** файлів із диска (синхронна перевірка `existsSync` + асинхронне `readFile`).
|
|
175
|
+
- **Кидання `Error`** при відсутності target-файлу — це навмисна поведінка («fail loud — user must know»), а не баг.
|
|
176
|
+
- Запису на диск або мережевих викликів **не робить**.
|
|
177
|
+
|
|
178
|
+
Складність та обмеження:
|
|
179
|
+
|
|
180
|
+
- Цикл лінійний за кількістю template-посилань у тексті; для кожного — один `existsSync` і один `readFile`.
|
|
181
|
+
- Файли читаються **послідовно** (через `await` у тілі `for...of`), а не паралельно через `Promise.all`. Це осмислений вибір: правил, як правило, мало, а послідовність робить порядок помилок передбачуваним.
|
|
182
|
+
- Заміна виконується через простий `result.replace(fullMatch, ...)` — перший збіг `fullMatch` у `result`. Якщо однакове Markdown-посилання трапляється кілька разів — модифікується лише перше входження (фактичний `matchAll` дасть і інші входження, але кожен з них має той самий `fullMatch`, і їх теж замінить — по одному за крок ітерації; для повних дублікатів це працює коректно).
|
|
183
|
+
|
|
184
|
+
## Залежності
|
|
185
|
+
|
|
186
|
+
### Стандартна бібліотека Node.js
|
|
187
|
+
|
|
188
|
+
- `node:fs` → `existsSync` — синхронна перевірка наявності файлу перед читанням.
|
|
189
|
+
- `node:fs/promises` → `readFile` — асинхронне читання вмісту target-файлу як UTF-8.
|
|
190
|
+
- `node:path` → `basename`, `extname`, `join` — робота з шляхами:
|
|
191
|
+
- `extname` — у `langFromExt` для визначення мови;
|
|
192
|
+
- `basename` — для отримання базового імені файлу, з якого `normalizeTargetName` витягне target-ім'я;
|
|
193
|
+
- `join` — для побудови абсолютного шляху від `ruleDir` + `relPath`.
|
|
194
|
+
|
|
195
|
+
### Зовнішні залежності
|
|
196
|
+
|
|
197
|
+
Жодних npm-пакетів. Модуль працює лише на Node.js стандарті.
|
|
198
|
+
|
|
199
|
+
### Споживачі модуля
|
|
200
|
+
|
|
201
|
+
Файл лежить у `npm/scripts/lib/` поряд із іншими допоміжними утилітами для збірки правил, тому очікувані споживачі — build-скрипти у `npm/scripts/`, які генерують підсумкові `.mdc`-документи для cursor-rules / Claude-rules. Експортована функція `inlineTemplateLinks` викликається на проміжній стадії пайплайна обробки тексту `.mdc`-файлу разом із `ruleDir`, обчисленим від шляху до самого `.mdc`.
|
|
202
|
+
|
|
203
|
+
## Потік виконання / Використання
|
|
204
|
+
|
|
205
|
+
Типовий сценарій інтеграції в build-скрипт:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
209
|
+
import { dirname } from 'node:path'
|
|
210
|
+
|
|
211
|
+
import { inlineTemplateLinks } from './lib/inline-template-links.mjs'
|
|
212
|
+
|
|
213
|
+
const mdcPath = '/abs/path/to/npm/rules/security/n-security.mdc'
|
|
214
|
+
const original = await readFile(mdcPath, 'utf8')
|
|
215
|
+
|
|
216
|
+
const ruleDir = dirname(mdcPath) // важливо: каталог, де лежить .mdc
|
|
217
|
+
const transformed = await inlineTemplateLinks(original, ruleDir)
|
|
218
|
+
|
|
219
|
+
await writeFile(mdcPath, transformed, 'utf8')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Що відбувається крок-за-кроком на прикладі.
|
|
223
|
+
|
|
224
|
+
Вхідний `.mdc`-фрагмент (`ruleDir = .../npm/rules/security/`):
|
|
225
|
+
|
|
226
|
+
```text
|
|
227
|
+
Snippet вимоги до `package.json` — див. [тут](./templates/package.json.snippet.json).
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Файл `.../npm/rules/security/templates/package.json.snippet.json`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"scripts": {
|
|
235
|
+
"lint": "eslint ."
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Що зробить `inlineTemplateLinks`:
|
|
241
|
+
|
|
242
|
+
1. `matchAll(MD_LINK_RE)` знайде один збіг із href `./templates/package.json.snippet.json`.
|
|
243
|
+
2. `TEMPLATE_SEGMENT_RE` пропустить його (бо є `/templates/`).
|
|
244
|
+
3. `relPath = 'templates/package.json.snippet.json'`, `absPath = '.../npm/rules/security/templates/package.json.snippet.json'`.
|
|
245
|
+
4. `existsSync(absPath)` → `true`, файл читається.
|
|
246
|
+
5. `langFromExt(absPath)` → `'json'`.
|
|
247
|
+
6. `normalizeTargetName('package.json.snippet.json')` → `'package.json'` (спрацює regexp `/^(.+)\.snippet\.[^.]+$/`).
|
|
248
|
+
7. `replacement` буде:
|
|
249
|
+
|
|
250
|
+
````text
|
|
251
|
+
`package.json`:
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{
|
|
255
|
+
"scripts": {
|
|
256
|
+
"lint": "eslint ."
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
````
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
8. Результат заміняє оригінальний Markdown-лінк у тексті.
|
|
266
|
+
|
|
267
|
+
Випадки помилок:
|
|
268
|
+
|
|
269
|
+
- Якщо `href` веде на неіснуючий файл — кидається `Error` із повним абсолютним шляхом у повідомленні; build-скрипт має право або впасти, або зловити цю помилку.
|
|
270
|
+
- Якщо у `text` немає Markdown-посилань або жодне з них не містить `/template(s)/` — функція повертає `text` без модифікацій.
|
|
271
|
+
- Якщо template-файл має нерозпізнаване розширення (наприклад `.txt` або `.conf`) — `langFromExt` поверне порожній рядок, і fenced-блок буде без мовного тегу (Markdown це допускає).
|
|
272
|
+
- Якщо ім'я template-файлу **не** має одного з суфіксів `.snippet.<ext>` / `.deny.<ext>` / `.contains.<ext>` — `normalizeTargetName` поверне його як є; це нормальна поведінка для «звичайних» template-файлів, у яких саме ім'я і є target-ім'ям.
|
|
273
|
+
|
|
274
|
+
## Rebuild Test
|
|
275
|
+
|
|
276
|
+
Якщо видалити цей файл і відтворити його з нуля, мінімально достатній рецепт такий:
|
|
277
|
+
|
|
278
|
+
1. Створи модуль `inline-template-links.mjs` у `npm/scripts/lib/`.
|
|
279
|
+
2. Імпортуй з `node:fs` функцію `existsSync`, з `node:fs/promises` — `readFile`, з `node:path` — `basename`, `extname`, `join`.
|
|
280
|
+
3. Оголоси константи:
|
|
281
|
+
- `MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g` — глобальний regexp для Markdown-посилань `[label](./path)`.
|
|
282
|
+
- `TEMPLATE_SEGMENT_RE = /\/templates?\//` — фільтр шляхів, що містять `/template/` чи `/templates/`.
|
|
283
|
+
- `SLOT_SUFFIX_RES` — масив із трьох **статичних** regexp: `/^(.+)\.snippet\.[^.]+$/`, `/^(.+)\.deny\.[^.]+$/`, `/^(.+)\.contains\.[^.]+$/`. Принципово: жодного `new RegExp(variable)` — захист від ReDoS.
|
|
284
|
+
4. Реалізуй `langFromExt(filePath)`:
|
|
285
|
+
- `extname(filePath)` → за вмістом повернути `'json' | 'toml' | 'yaml' | ''` (для `.yml` теж `'yaml'`).
|
|
286
|
+
5. Реалізуй `normalizeTargetName(fileBasename)`:
|
|
287
|
+
- Пройди `SLOT_SUFFIX_RES` у заданому порядку; при першому збігу поверни `match[1]`. Інакше — оригінал.
|
|
288
|
+
6. Експортуй `async function inlineTemplateLinks(text, ruleDir)`:
|
|
289
|
+
- `matchAll(MD_LINK_RE)` → відфільтруй за `TEMPLATE_SEGMENT_RE.test(href)`.
|
|
290
|
+
- Якщо нічого не залишилося — поверни `text`.
|
|
291
|
+
- Для кожного збігу: `relPath = href.slice(2)`, `absPath = join(ruleDir, relPath)`; якщо `!existsSync(absPath)` — `throw new Error('inlineTemplateLinks: file not found: <absPath> (referenced from .mdc)')`.
|
|
292
|
+
- Читай файл `utf8`, роби `.trim()`, обчисли `lang` і `targetName`, побудуй `replacement = \`\\\`${targetName}\\\`:\\n\\n\\\`\\\`\\\`${lang}\\n${contents}\\n\\\`\\\`\\\``.
|
|
293
|
+
- Заміни через `result = result.replace(fullMatch, () => replacement)` (саме callback-форма — щоб уникнути інтерпретації `$&`/`$1` у вмісті template-файлу).
|
|
294
|
+
7. Поверни `result`.
|
|
295
|
+
|
|
296
|
+
Контракт, який має зберегтися:
|
|
297
|
+
|
|
298
|
+
- Чиста функція над текстом + читання файлів (без записів і без мережі).
|
|
299
|
+
- **Fail-loud** на відсутній target.
|
|
300
|
+
- Підтримка трьох слот-суфіксів: `snippet`, `deny`, `contains`.
|
|
301
|
+
- Підтримка мов підсвічування: `json`, `toml`, `yaml`, інакше — без таргу.
|
|
302
|
+
- Жодного `RegExp(variable)`.
|
|
303
|
+
- Префікс href повинен починатися з `./`, інакше посилання ігнорується (це закладено в `MD_LINK_RE`).
|