@nitra/cursor 1.13.12 → 1.13.14
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 +23 -0
- package/package.json +1 -1
- package/rules/bun/bun.mdc +3 -6
- package/rules/bun/policy/bunfig/bunfig.rego +19 -23
- package/rules/bun/policy/bunfig/template/bunfig.toml.snippet.toml +2 -0
- package/rules/bun/policy/package_json/package_json.rego +15 -33
- package/rules/bun/policy/package_json/template/package.json.deny.json +4 -0
- package/rules/ci4/ci4.mdc +290 -61
- package/rules/text/lint/lint.mjs +9 -3
- package/rules/text/lint/run-dotenv-linter.mjs +95 -0
- package/rules/text/text.mdc +5 -3
- package/skills/abie-clean/SKILL.md +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
+
## [1.13.14] - 2026-05-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `bun` rule template/ міграція (Phase 4): 2 концерни — `bunfig` (snippet-walker 2-level, `[install].linker = "hoisted"`) + `package_json` (partial — top-level deny-fields у template).
|
|
12
|
+
- `bunfig_test.rego` створено з нуля (раніше тестів не було) — 5 позитивних/негативних + drift.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- `bun.bunfig.rego` — використовує той самий 2-level snippet-walker, що `ga.vscode_settings` (leaf-by-leaf + guard на non-object section).
|
|
17
|
+
- `bun.package_json.rego` — top-level deny-fields (`packageManager`, `dependencies`) тепер читаються з `data.template.deny`. Сентинельний value у `object.get(input, field, "__bun_missing__")` зберігає behavior «field present навіть якщо порожній обʼєкт». Логіка `@nitra/*`-only у devDependencies та lint-aggregator (cross-script) лишається у rego — це inverse-patterns, які не виносяться у template.
|
|
18
|
+
- `bun.mdc` — inline `bunfig.toml` snippet замінено на template-link; додано посилання на `package.json.deny.json` для документації заборонених top-level полів.
|
|
19
|
+
- `docs/adr/template-dir-concern-inventory.md` — `bun.*` концерни позначено ✓; додано Phase 4 у прогрес-секцію; tally: 15/39 (38%).
|
|
20
|
+
|
|
21
|
+
## [1.13.13] - 2026-05-17
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `text` rule: до ланцюжка `lint-text` додано крок `dotenv-linter` (`runDotenvLinter()` у `npm/rules/text/lint/run-dotenv-linter.mjs`). На знайдених `.env*` рекурсивно по проєкту виконується `dotenv-linter fix -r --no-backup --quiet . --exclude node_modules --exclude .envrc`, після чого симетричний `check` для фінальної перевірки. Якщо інструмент відсутній у `PATH` — друкуються підказки встановлення (`brew install dotenv-linter` для macOS).
|
|
26
|
+
- `text.mdc` (canonical + `.cursor/rules/n-text.mdc` mirror) описує новий крок і вимоги до `dotenv-linter` (тільки в `PATH`, **не** у `dependencies`/`devDependencies`).
|
|
27
|
+
- `.cspell.json` — додано слово `envrc` (направлення на direnv-файл, не key=value).
|
|
28
|
+
- Тест `npm/rules/text/lint/run-dotenv-linter.test.mjs` — порожнє дерево, авто-фікс `LowercaseKey`, ігнор `node_modules`/`.envrc`.
|
|
29
|
+
|
|
7
30
|
## [1.13.12] - 2026-05-17
|
|
8
31
|
|
|
9
32
|
### Added
|
package/package.json
CHANGED
package/rules/bun/bun.mdc
CHANGED
|
@@ -34,10 +34,7 @@ Lockfile у репозиторії: `bun.lock`.
|
|
|
34
34
|
|
|
35
35
|
У корені репозиторію має бути **`bunfig.toml`** з **hoisted** лінкером (пласке `node_modules`, сумісне з інструментами, які не розуміють ізольований layout Bun):
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
[install]
|
|
39
|
-
linker = "hoisted"
|
|
40
|
-
```
|
|
37
|
+
- Канон `bunfig.toml`: [bunfig.toml.snippet.toml](./policy/bunfig/template/bunfig.toml.snippet.toml)
|
|
41
38
|
|
|
42
39
|
Для Bun monorepo:
|
|
43
40
|
|
|
@@ -45,9 +42,9 @@ linker = "hoisted"
|
|
|
45
42
|
- Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
|
|
46
43
|
- У CI та локально запускати скрипти через `bun run`.
|
|
47
44
|
|
|
48
|
-
В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra
|
|
45
|
+
В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
|
|
49
46
|
|
|
50
|
-
|
|
47
|
+
- Заборонені top-level поля у root `package.json` (з причинами): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
51
48
|
|
|
52
49
|
Коли зміна відбувається в Dockerfile, то використовувати
|
|
53
50
|
|
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка `bunfig.toml` для bun (bun.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# `package-lock.json` тощо, директорія `.yarn/`) живуть у `check-bun.mjs` — Rego
|
|
9
|
-
# працює лише з вже завантаженим input.
|
|
10
|
-
#
|
|
11
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
12
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
13
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
3
|
+
# Канон надходить через --data: { "template": { "snippet": ... } }
|
|
4
|
+
# Структура --data сформована з template/bunfig.toml.snippet.toml.
|
|
5
|
+
# Snippet — 2-рівнева мапа (section → key → expected). Walker такий самий,
|
|
6
|
+
# як для ga.vscode_settings: leaf-by-leaf коли section-обʼєкт існує + окремий
|
|
7
|
+
# deny коли section відсутній / не обʼєкт.
|
|
14
8
|
package bun.bunfig
|
|
15
9
|
|
|
16
10
|
import rego.v1
|
|
17
11
|
|
|
12
|
+
# Leaf-by-leaf: section присутня й обʼєкт.
|
|
18
13
|
deny contains msg if {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
some section, expected_inner in data.template.snippet
|
|
15
|
+
inner := object.get(input, section, {})
|
|
16
|
+
is_object(inner)
|
|
17
|
+
some leaf_key, expected_value in expected_inner
|
|
18
|
+
actual := object.get(inner, leaf_key, null)
|
|
19
|
+
actual != expected_value
|
|
20
|
+
msg := sprintf("bunfig.toml: у секції [%s] має бути %s = %q (bun.mdc)", [section, leaf_key, expected_value])
|
|
24
21
|
}
|
|
25
22
|
|
|
23
|
+
# Section відсутня (null) або не обʼєкт.
|
|
26
24
|
deny contains msg if {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
object.get(input.install, "linker", null) != "hoisted"
|
|
32
|
-
msg := "bunfig.toml: у секції [install] має бути linker = \"hoisted\" (bun.mdc)"
|
|
25
|
+
some section in object.keys(data.template.snippet)
|
|
26
|
+
raw := object.get(input, section, null)
|
|
27
|
+
not is_object(raw)
|
|
28
|
+
msg := sprintf("bunfig.toml: відсутня секція [%s] (bun.mdc)", [section])
|
|
33
29
|
}
|
|
@@ -1,47 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Перевірка кореневого `package.json` для bun (bun.mdc).
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# Канон надходить через --data: { "template": { "deny": ... } }
|
|
4
|
+
# Структура --data сформована з template/package.json.deny.json
|
|
5
|
+
# (top-level fields заборонені у root).
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Логіка, що ЛИШАЄТЬСЯ у rego (inverse-patterns, не виносяться у template):
|
|
8
|
+
# - `devDependencies` лише `@nitra/*` (inverse-pattern: every dep must match)
|
|
9
|
+
# - Агрегований `lint` скрипт (cross-script aggregation logic)
|
|
9
10
|
#
|
|
10
11
|
# Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
|
|
11
12
|
# - `lint-docker` / `lint-k8s` коли `.n-cursor.json:rules` містить відповідне
|
|
12
13
|
# правило (потрібен другий файл-вхід — у Rego без `--combine` не зробити).
|
|
13
|
-
#
|
|
14
|
-
# Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
|
|
15
|
-
# Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
|
|
16
|
-
# (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
|
|
17
14
|
package bun.package_json
|
|
18
15
|
|
|
19
16
|
import rego.v1
|
|
20
17
|
|
|
21
18
|
# ── Шаблони повідомлень ────────────────────────────────────────────────────
|
|
22
19
|
|
|
23
|
-
# Через `concat` — дотримуємося regal style/line-length.
|
|
24
20
|
lint_aggregate_missing_template := concat(" ", [
|
|
25
21
|
"У package.json є скрипти %v, але немає агрегованого `lint`.",
|
|
26
22
|
"Додай скрипт, який запускає їх через `bun run` (bun.mdc)",
|
|
27
23
|
])
|
|
28
24
|
|
|
29
|
-
# ── deny: заборонені поля
|
|
30
|
-
|
|
31
|
-
deny contains msg if {
|
|
32
|
-
pm := object.get(input, "packageManager", "")
|
|
33
|
-
pm != ""
|
|
34
|
-
msg := sprintf("package.json містить поле packageManager: %q — видали його (bun.mdc)", [pm])
|
|
35
|
-
}
|
|
25
|
+
# ── deny: заборонені top-level поля (template-driven) ─────────────────────
|
|
36
26
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
27
|
+
# Сентинельний value відрізняє «поле відсутнє» від «поле є з будь-яким значенням»
|
|
28
|
+
# (наприклад `dependencies: {}` — присутнє але порожнє → теж заборонено).
|
|
39
29
|
deny contains msg if {
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
some field, reason in data.template.deny
|
|
31
|
+
object.get(input, field, "__bun_missing__") != "__bun_missing__"
|
|
32
|
+
msg := sprintf("package.json: поле %s — %s", [field, reason])
|
|
42
33
|
}
|
|
43
34
|
|
|
44
|
-
# ── deny: devDependencies — лише `@nitra/*`
|
|
35
|
+
# ── deny: devDependencies — лише `@nitra/*` (inverse pattern; не виноситься у template) ─
|
|
45
36
|
|
|
46
37
|
deny contains msg if {
|
|
47
38
|
is_object(input.devDependencies)
|
|
@@ -50,11 +41,7 @@ deny contains msg if {
|
|
|
50
41
|
msg := sprintf("Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: %s (bun.mdc)", [name])
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
# ── deny: агрегований lint-скрипт
|
|
54
|
-
#
|
|
55
|
-
# Якщо в `scripts` є хоч один `lint-*`, має бути скрипт `lint`, у якому
|
|
56
|
-
# через `bun run <ім'я>` викликається кожен такий скрипт; рядок завершується
|
|
57
|
-
# на `&& oxfmt .`.
|
|
44
|
+
# ── deny: агрегований lint-скрипт (cross-script aggregation logic) ───────
|
|
58
45
|
|
|
59
46
|
deny contains msg if {
|
|
60
47
|
count(lint_prefixed_scripts) > 0
|
|
@@ -73,22 +60,17 @@ deny contains msg if {
|
|
|
73
60
|
deny contains msg if {
|
|
74
61
|
count(lint_prefixed_scripts) > 0
|
|
75
62
|
lint_script != ""
|
|
76
|
-
|
|
77
|
-
# Перевіряємо, що рядок завершується `&& oxfmt .` (з можливими пробілами/табами).
|
|
78
|
-
# Trim не потрібен — пробіли/таби в кінці допускаємо в самому regex (`[ \t]*$`).
|
|
79
63
|
not regex.match(`&&[ \t]+oxfmt[ \t]+\.[ \t]*$`, lint_script)
|
|
80
64
|
msg := "Скрипт `lint` має закінчуватися на `&& oxfmt .` (bun.mdc)"
|
|
81
65
|
}
|
|
82
66
|
|
|
83
67
|
# ── helpers ────────────────────────────────────────────────────────────────
|
|
84
68
|
|
|
85
|
-
# Ключі скриптів, що починаються з `lint-` (наприклад `lint-js`, `lint-ga`).
|
|
86
69
|
lint_prefixed_scripts := [name |
|
|
87
70
|
some name, _ in object.get(input, "scripts", {})
|
|
88
71
|
startswith(name, "lint-")
|
|
89
72
|
]
|
|
90
73
|
|
|
91
|
-
# Значення `scripts.lint` як рядок (порожній, якщо поля немає або тип не string).
|
|
92
74
|
default lint_script := ""
|
|
93
75
|
|
|
94
76
|
lint_script := input.scripts.lint if is_string(input.scripts.lint)
|
package/rules/ci4/ci4.mdc
CHANGED
|
@@ -1,103 +1,332 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Архітектурна документація продукту — Markdown як джерело істини; рекомендований стек arc42 + Diátaxis + ADR (MADR v4, формат описаний у правилі `adr`) + C4 як набір нотацій; гібридна модель manual + autogen-зон, що регенеруються з accepted ADR; MkDocs Material як viewer з collapsible engineer-блоками для змішаної аудиторії
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '
|
|
4
|
+
version: '2.0'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Архітектурна документація проєкту живе у Markdown поряд із кодом. Це не довідник «для людей із порталу архітектора» — це **джерело істини**, з якого LLM-агент і людина читають намір системи перед будь-якою зміною коду. Тому правила нижче — не оформлення, а робочий процес: який стек використовуємо, як зберігаємо рішення, як автоматично перегенеровуємо проекції з ADR і як рендеримо для змішаної аудиторії (менеджери + інженери + ops).
|
|
8
|
+
|
|
9
|
+
Формат самих ADR-файлів — MADR v4.0.0 minimal — описаний у правилі **`adr`** (`npm/rules/adr/adr.mdc`). Тут описано, як ADR використовуються як вхід для архітектурних проекцій, а не як вони створюються.
|
|
10
10
|
|
|
11
11
|
## Markdown як джерело істини
|
|
12
12
|
|
|
13
|
-
Уся ключова інформація про проєкт —
|
|
14
|
-
зберігається у `.md`/`.mdc`-файлах і є **офіційним джерелом істини**. Це єдиний спосіб
|
|
15
|
-
тримати знання разом із кодом — версійно, в одному PR і доступно для агентів. Якщо щось
|
|
16
|
-
важливе про систему існує лише у Confluence/Notion/месенджері — для проєкту цього **немає**.
|
|
13
|
+
Уся ключова інформація про проєкт — архітектура, рішення (ADR), тести й документація — зберігається у `.md`/`.mdc`-файлах і є **офіційним джерелом істини**. Це єдиний спосіб тримати знання разом із кодом — версійно, в одному PR і доступно для агентів. Якщо щось важливе про систему існує лише у Confluence/Notion/месенджері — для проєкту цього **немає**.
|
|
17
14
|
|
|
18
15
|
## Специфікація як джерело істини (Spec-as-Source)
|
|
19
16
|
|
|
20
|
-
У AI-native розробці первинним артефактом, який підтримують інженери, є **специфікація**, а не
|
|
21
|
-
кодова база. Код — похідний артефакт, «будівельне риштування», яке агент генерує, верифікує або
|
|
22
|
-
повністю відновлює зі специфікації. Якщо поведінка системи має змінитися — **спочатку оновлюємо
|
|
23
|
-
специфікацію (C4/ADR/опис компонента), і лише потім** агент генерує відповідний код. Зворотний
|
|
24
|
-
порядок («код вже написаний, доку напишемо потім») перетворює специфікацію на декорацію.
|
|
17
|
+
У AI-native розробці первинним артефактом, який підтримують інженери, є **специфікація**, а не кодова база. Код — похідний артефакт, «будівельне риштування», яке агент генерує, верифікує або повністю відновлює зі специфікації. Якщо поведінка системи має змінитися — **спочатку оновлюємо специфікацію (arc42-розділ, ADR, опис компонента), і лише потім** агент генерує відповідний код. Зворотний порядок («код вже написаний, доку напишемо потім») перетворює специфікацію на декорацію.
|
|
25
18
|
|
|
26
19
|
## Тест на повне відтворення (Rebuild Test)
|
|
27
20
|
|
|
28
|
-
Документація вважається повною лише тоді, коли проходить бінарний тест: якщо видалити теку
|
|
29
|
-
`src/`, відкрити нову LLM-сесію з чистим контекстом і дати агенту доступ лише до `.md`-файлів —
|
|
30
|
-
агент має **відтворити робочу кодову базу**. Архітектурні збої (агент не знає структуру тек),
|
|
31
|
-
інтеграційні (неописані міжсервісні контракти), падіння юніт-тестів (неописана бізнес-логіка) —
|
|
32
|
-
це не «складність задачі», а **прогалини документації**, які треба заповнити у тому ж PR.
|
|
21
|
+
Документація вважається повною лише тоді, коли проходить бінарний тест: якщо видалити теку `src/`, відкрити нову LLM-сесію з чистим контекстом і дати агенту доступ лише до `.md`-файлів — агент має **відтворити робочу кодову базу**. Архітектурні збої (агент не знає структуру тек), інтеграційні (неописані міжсервісні контракти), падіння юніт-тестів (неописана бізнес-логіка) — це не «складність задачі», а **прогалини документації**, які треба заповнити у тому ж PR.
|
|
33
22
|
|
|
34
23
|
## Ефективність токенів і чистий Markdown
|
|
35
24
|
|
|
36
|
-
Вікно контексту LLM обмежене, а якість міркування деградує в міру його заповнення (ефект
|
|
37
|
-
«Lost in the Middle»). Тому документація **очищається від HTML-розмітки, CSS-класів,
|
|
38
|
-
навігаційних обгорток** і всього, що не несе семантичного навантаження. Чистий Markdown замість
|
|
39
|
-
HTML економить 80–90 % токенів (≈16 000 → 1 600 на сторінку), що прямо впливає на точність
|
|
40
|
-
згенерованого коду і знижує вартість API. Жодних `<div>`/`<span>`/класів у тілі `.md`/`.mdc`.
|
|
25
|
+
Вікно контексту LLM обмежене, а якість міркування деградує в міру його заповнення (ефект «Lost in the Middle»). Тому документація **очищається від HTML-розмітки, CSS-класів, навігаційних обгорток** і всього, що не несе семантичного навантаження. Чистий Markdown замість HTML економить 80–90 % токенів (≈16 000 → 1 600 на сторінку), що прямо впливає на точність згенерованого коду і знижує вартість API. Жодних `<div>`/`<span>`/класів у тілі `.md`/`.mdc`. Єдиний виняток — HTML-коментарі-маркери AUTOGEN-зон, які не рендеряться у viewer і несуть службову інформацію для генератора (`<!-- AUTOGEN:start ... -->`).
|
|
41
26
|
|
|
42
27
|
## Контекстна незалежність розділів
|
|
43
28
|
|
|
44
|
-
RAG витягує **фрагменти**, не цілі документи. Тому кожен розділ має сенс **без сусіднього
|
|
45
|
-
тексту**. Заборонено: «як було згадано вище», «ця змінна», «попередній метод», «той самий
|
|
46
|
-
сервіс». Замість цього — щоразу явно повторюємо назву сутності: «автентифікація OAuth 2.0»,
|
|
47
|
-
«функція `calculateTotal()`», «контейнер `api-gateway`». Коли агент отримає лише цей фрагмент,
|
|
48
|
-
у нього має бути повний словник для коректної генерації.
|
|
29
|
+
RAG витягує **фрагменти**, не цілі документи. Тому кожен розділ має сенс **без сусіднього тексту**. Заборонено: «як було згадано вище», «ця змінна», «попередній метод», «той самий сервіс». Замість цього — щоразу явно повторюємо назву сутності: «автентифікація OAuth 2.0», «функція `calculateTotal()`», «контейнер `api-gateway`». Коли агент отримає лише цей фрагмент, у нього має бути повний словник для коректної генерації.
|
|
49
30
|
|
|
50
31
|
## Docs-as-Code
|
|
51
32
|
|
|
52
|
-
Документація живе у Git поруч із кодом, проходить **той самий Code Review**, версіонується і
|
|
53
|
-
автоматично перевіряється у CI (лінтери Markdown, валідатори посилань, `npx @nitra/cursor
|
|
54
|
-
check`). Биті посилання й документація, що «не компілюється», — **блокуючий баг**, не
|
|
55
|
-
косметика. Це продовження принципу «оновлення синхронно зі змінами» нижче: один PR — код +
|
|
56
|
-
схема + ADR + тести.
|
|
33
|
+
Документація живе у Git поруч із кодом, проходить **той самий Code Review**, версіонується і автоматично перевіряється у CI (лінтери Markdown, валідатори посилань, `npx @nitra/cursor check`). Биті посилання й документація, що «не компілюється», — **блокуючий баг**, не косметика.
|
|
57
34
|
|
|
58
35
|
## Трасування як документація недетермінованої поведінки
|
|
59
36
|
|
|
60
|
-
Для компонентів, де рішення приймає нейромережа або складна евристика, класичної форми
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
37
|
+
Для компонентів, де рішення приймає нейромережа або складна евристика, класичної форми «вхід X → вихід Y» **недостатньо** — поведінка недетермінована. Джерелом істини стає **пайплайн логування й трасування**: які інструменти використав агент, який контекст мав, чому ухвалив це рішення. У описі компонента такого типу — посилання на дашборд/storage трасувань обов'язкове нарівні з посиланнями на тести.
|
|
38
|
+
|
|
39
|
+
## Рекомендований стек
|
|
40
|
+
|
|
41
|
+
Беремо **завжди**:
|
|
42
|
+
|
|
43
|
+
- **arc42** — скелет документа архітектури. 12 розділів із природним розподілом аудиторій: 1-3 (Introduction, Constraints, Context) — менеджерська проза, 5-9 (Building Blocks, Runtime, Deployment, Crosscutting, Decisions) — інженерні. Розділ 9 «Architecture Decisions» — індекс ADR.
|
|
44
|
+
- **Diátaxis** — мета-структура сайту документації: tutorials / how-to / reference / explanation. arc42 живе в `explanation/`. Для більшості продуктів tutorials опціональні; реально потрібні explanation + reference + how-to.
|
|
45
|
+
- **ADR (MADR v4.0.0 minimal)** — append-only event log архітектурних рішень. Формат, фази capture/normalize, Stop-hook автоматика описані у правилі **`adr`**. Clean ADR-файл — `docs/adr/<slug>.md` без frontmatter, з англомовними MADR-заголовками і `**Status:** Accepted` / `**Date:** YYYY-MM-DD` рядками. Це єдине джерело правди для регенерації проекцій.
|
|
46
|
+
- **C4 model** — як набір нотацій для діаграм усередині arc42: контекстна (C4 рівень 1) і контейнерна (рівень 2) у `explanation/architecture.md`, компонентна (рівень 3) — на сторінках компонентів. Не самостійна метамодель, а інструмент візуалізації.
|
|
47
|
+
|
|
48
|
+
Доповнюємо **під domain**:
|
|
49
|
+
|
|
50
|
+
- **DDD Context Maps** — коли є >1 bounded context (типово для multi-agent: agents, orchestration, memory, tooling). Одна Mermaid-діаграма в `explanation/architecture.md`, ховається під `??? engineer`.
|
|
51
|
+
- **EventModeling** — для event-driven систем (агентські pipeline, CQRS, Event Sourcing). Структура: trigger → command → event → read model. Читається менеджером як наратив, інженером — як креслення.
|
|
52
|
+
- **Business Capability Map** — верхньорівневий manager-overview. Autogen через семантичну класифікацію accepted ADR за тематикою.
|
|
53
|
+
- **Wardley Map** — стратегічний артефакт для board/інвесторів. Manual, не autogen. Квартальне оновлення. Живе в `explanation/strategy.md`.
|
|
54
|
+
|
|
55
|
+
**Не використовуємо** для product docs:
|
|
56
|
+
|
|
57
|
+
- 4+1 View Model, Viewpoints & Perspectives — academic/enterprise, дублюють arc42.
|
|
58
|
+
- ISO/IEC/IEEE 42010 — мета-стандарт без шаблонів.
|
|
59
|
+
- UML повністю — беремо точково Mermaid (Sequence, Component, Deployment) всередині arc42.
|
|
60
|
+
- Service Blueprint, Value Stream Mapping, JTBD, Impact Mapping, User Story Mapping — planning/UX інструменти, не docs.
|
|
61
|
+
- EventStorming, Domain Storytelling — workshop-формати; артефакт з них конвертується в EventModeling або Context Map.
|
|
62
|
+
|
|
63
|
+
## Структура репозиторію
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
docs/
|
|
67
|
+
├── adr/
|
|
68
|
+
│ ├── <slug>.md # clean ADR-и (MADR v4 minimal, без frontmatter)
|
|
69
|
+
│ ├── YYYYMMDD-HHMMSS-<sid>.md # drafts (frontmatter session:/captured:/transcript:)
|
|
70
|
+
│ └── index.md # autogen — список accepted ADR за датою
|
|
71
|
+
├── explanation/ # Diátaxis: чому і що
|
|
72
|
+
│ ├── overview.md # Capability Map (autogen)
|
|
73
|
+
│ ├── architecture.md # arc42 + Context Map + EventModeling + C4 діаграми
|
|
74
|
+
│ ├── strategy.md # Wardley Map (MANUAL)
|
|
75
|
+
│ └── components/ # сторінки на компонент
|
|
76
|
+
├── reference/ # API, конфіги, схеми
|
|
77
|
+
├── how-to/ # runbooks, інтеграції
|
|
78
|
+
├── snapshots/ # періодичні зрізи
|
|
79
|
+
└── .docgen/
|
|
80
|
+
├── config.yaml
|
|
81
|
+
├── prompts/ # системні промпти LLM
|
|
82
|
+
└── manifest.json # реєстр зон, ADR-slug → зони
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Інших місць для архітектурних схем у репозиторії немає: розпорошення по підтеках сервісів ламає навігацію «від системи вглиб» і робить агентський аналіз перед зміною дорожчим. Структуру drafts і clean ADR у `docs/adr/` створює і підтримує автоматика правила **`adr`** — окремий формат тут не визначаємо.
|
|
86
|
+
|
|
87
|
+
## ADR як вхід проекцій
|
|
88
|
+
|
|
89
|
+
Регенератор працює **тільки з clean ADR** (`docs/adr/<slug>.md`, без frontmatter, з рядком `**Status:** Accepted`). Drafts (`YYYYMMDD-HHMMSS-*.md`) ігноруються — їх перетворить у clean ADR `normalize-decisions.sh` із правила `adr`.
|
|
90
|
+
|
|
91
|
+
Поля, які регенератор читає з clean ADR:
|
|
92
|
+
|
|
93
|
+
- **Slug** — ім'я файлу без `.md`, використовується як ідентифікатор у маркерах і `manifest.json`.
|
|
94
|
+
- **Status** — рядок `**Status:** Accepted` у тілі. ADR з іншим статусом (або без рядка) у проекції не потрапляє.
|
|
95
|
+
- **Date** — рядок `**Date:** YYYY-MM-DD`, для впорядкування і відображення у проекції.
|
|
96
|
+
- **Heading H1** — перший `#`-заголовок, як назва рішення.
|
|
97
|
+
- **Розділи MADR** — `## Context and Problem Statement`, `## Considered Options`, `## Decision Outcome`, `### Consequences`, `## More Information`. Для проекції зазвичай беруться Decision Outcome + Consequences.
|
|
98
|
+
- **`## Update YYYY-MM-DD`** — apended-секції у тому ж файлі. Враховуються як еволюція рішення в хронологічному порядку.
|
|
99
|
+
|
|
100
|
+
**Маршрутизація ADR → зони** робиться двома способами:
|
|
101
|
+
|
|
102
|
+
1. **Explicit** — у самій AUTOGEN-зоні в атрибуті `sources` перелічені slug-и: `sources="ланцюжок-запуску-abie,npm-publish-flow"`. Це єдиний робочий механізм у MADR v4 minimal (frontmatter `affects:` поля немає).
|
|
103
|
+
2. **Discovery** — семантичний індекс: ембеддинги розділів `## Context and Problem Statement` + `## Decision Outcome` кожного ADR; зона декларує тематику, регенератор підтягує top-K релевантних slug-ів. Використовується для оглядових проекцій (`overview.md`, Capability Map).
|
|
104
|
+
|
|
105
|
+
## Документація як derived state від ADR
|
|
106
|
+
|
|
107
|
+
Документація = **derived state** від ADR. Інженер ніколи не редагує autogen-тексти вручну — тільки через ADR. Якщо потрібно щось виправити в проекції — це означає, що або є accepted ADR, який треба еволюціонувати через `## Update YYYY-MM-DD` (механіка з правила `adr`), або є некоректний промпт у `docs/.docgen/prompts/`, або взагалі потрібен новий ADR.
|
|
108
|
+
|
|
109
|
+
## Гібрид manual + autogen
|
|
110
|
+
|
|
111
|
+
Реальні проєкти мають legacy docs, написані до появи ADR. Розв'язок — **зони** через HTML-коментарі (виживають у Markdown, не рендеряться у MkDocs):
|
|
112
|
+
|
|
113
|
+
```markdown
|
|
114
|
+
# User Service
|
|
115
|
+
|
|
116
|
+
User Service відповідає за автентифікацію та профілі.
|
|
117
|
+
*Цей абзац — manual, LLM не чіпає.*
|
|
118
|
+
|
|
119
|
+
<!-- AUTOGEN:start id="user-service-decisions" hash="sha256:a1b2c3..." sources="ланцюжок-запуску-abie,npm-publish-flow,oidc-pkce-flow" -->
|
|
120
|
+
## Ключові рішення
|
|
121
|
+
|
|
122
|
+
Сервіс працює на Bun runtime [npm-publish-flow]. Автентифікація — OIDC з PKCE
|
|
123
|
+
[oidc-pkce-flow]. Запуск організовано через [ланцюжок-запуску-abie].
|
|
124
|
+
<!-- AUTOGEN:end id="user-service-decisions" -->
|
|
125
|
+
|
|
126
|
+
## Історичний контекст
|
|
127
|
+
Сервіс виник у 2023 як частина моноліту...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Типи зон:
|
|
131
|
+
|
|
132
|
+
- **`AUTOGEN`** — повністю від ADR, регенерується з нуля. Hash перевіряється строго.
|
|
133
|
+
- **`MANUAL`** (default — усе поза маркерами) — LLM не торкається. Опційно явні маркери `<!-- MANUAL:start id="..." -->` для документації наміру.
|
|
134
|
+
- **`MERGED`** — LLM отримує існуючий manual + ADR, робить узгоджене оновлення. Найризикованіший, на старті уникати.
|
|
135
|
+
|
|
136
|
+
**Backfill legacy:** поступово витягуйте архітектурні знання з manual-текстів у retroactive ADR. Не одразу — тільки коли торкаєтесь області. Природна еволюція: manual → новий accepted ADR (через звичайний capture-flow) → manual замінюється AUTOGEN-зоною з посиланням на slug.
|
|
137
|
+
|
|
138
|
+
## Manifest з інвертованим індексом
|
|
139
|
+
|
|
140
|
+
`docs/.docgen/manifest.json`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"zones": {
|
|
145
|
+
"user-service-decisions": {
|
|
146
|
+
"file": "explanation/components/user-service.md",
|
|
147
|
+
"sources": ["ланцюжок-запуску-abie", "npm-publish-flow", "oidc-pkce-flow"],
|
|
148
|
+
"content_hash": "sha256:...",
|
|
149
|
+
"generated_at": "2026-05-17T10:32:00Z",
|
|
150
|
+
"generator_version": "v3"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"adr_index": {
|
|
154
|
+
"ланцюжок-запуску-abie": {
|
|
155
|
+
"affects_zones": ["user-service-decisions", "startup-overview"],
|
|
156
|
+
"status": "Accepted",
|
|
157
|
+
"date": "2026-04-12",
|
|
158
|
+
"updates": ["2026-05-03"]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Інвертований індекс `adr_index` критичний: коли accepted ADR отримує `## Update YYYY-MM-DD`, регенератор миттєво знає, які зони ререндерити без сканування всіх файлів. Hash зони захищає від тихої перезаписки ручних правок: якщо вміст у файлі не збігається з hash у manifest — генератор зупиняється і вимагає рішення людини.
|
|
65
165
|
|
|
66
|
-
##
|
|
166
|
+
## Цикл оновлення
|
|
67
167
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
168
|
+
```
|
|
169
|
+
1. Інженер/менеджер читає docs/
|
|
170
|
+
2. Просить LLM: «хочу змінити X»
|
|
171
|
+
3. LLM:
|
|
172
|
+
a. знаходить релевантні clean ADR (за slug у sources зон + семантичним пошуком)
|
|
173
|
+
b. формулює draft-рішення (буде підхоплене capture-decisions.sh як ADR-чернетка)
|
|
174
|
+
c. показує impact: які зони зміняться після прийняття
|
|
175
|
+
4. Після завершення сесії capture-decisions.sh пише draft у docs/adr/
|
|
176
|
+
5. Коли поріг ADR_NORMALIZE_THRESHOLD досягнуто (або /n-adr-normalize вручну):
|
|
177
|
+
normalize-decisions.sh перетворює draft → clean ADR <slug>.md з **Status:** Accepted
|
|
178
|
+
6. Регенератор:
|
|
179
|
+
a. читає clean ADR з **Status:** Accepted (інші ігнорує)
|
|
180
|
+
b. для кожної AUTOGEN-зони збирає relevant ADR (sources= + discovery)
|
|
181
|
+
c. перевіряє hash зони — якщо контент != hash, STOP (хтось редагував вручну)
|
|
182
|
+
d. викликає LLM з промптом проекції
|
|
183
|
+
e. замінює вміст між маркерами, оновлює hash і adr_index у manifest
|
|
184
|
+
7. Валідатор (детермінований, без LLM)
|
|
185
|
+
8. Git commit з ADR + regenerated docs
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Full rebuild vs incremental:**
|
|
189
|
+
|
|
190
|
+
- **Full rebuild** (рекомендовано на старті): LLM отримує всі clean ADR зі `**Status:** Accepted` + шаблон — видає документ з нуля. Zero drift.
|
|
191
|
+
- **Incremental**: LLM отримує поточний doc + новий ADR (або `## Update`) — видає diff. Дешевше, але дрейф накопичується.
|
|
192
|
+
- **Компроміс**: incremental + періодичний full rebuild (раз на місяць або після N нових ADR).
|
|
193
|
+
|
|
194
|
+
## MkDocs Material: collapsible engineer-блоки
|
|
195
|
+
|
|
196
|
+
Менеджер читає прозу зверху, інженер клікає `??? engineer` і провалюється глибше. Один документ — дві аудиторії без дублювання.
|
|
197
|
+
|
|
198
|
+
`mkdocs.yml`:
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
theme:
|
|
202
|
+
name: material
|
|
203
|
+
features:
|
|
204
|
+
- content.code.copy
|
|
205
|
+
- navigation.tabs
|
|
206
|
+
|
|
207
|
+
markdown_extensions:
|
|
208
|
+
- admonition
|
|
209
|
+
- pymdownx.details
|
|
210
|
+
- pymdownx.superfences
|
|
211
|
+
- pymdownx.tabbed: { alternate_style: true }
|
|
212
|
+
- attr_list
|
|
213
|
+
|
|
214
|
+
extra_css:
|
|
215
|
+
- stylesheets/audience.css
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Синтаксис у документах:
|
|
219
|
+
|
|
220
|
+
```markdown
|
|
221
|
+
User Service автентифікує користувачів і керує профілями.
|
|
222
|
+
Підтримуємо OIDC-логін та email/password.
|
|
223
|
+
|
|
224
|
+
??? engineer "Технічна реалізація автентифікації"
|
|
225
|
+
OIDC-сервер на Fastify з PKCE flow [oidc-pkce-flow].
|
|
226
|
+
Refresh-токени з rotation, TTL 30 днів, Redis storage.
|
|
227
|
+
|
|
228
|
+
Профілі зберігаються в основній БД, кешуються в Redis на 5 хв.
|
|
229
|
+
|
|
230
|
+
??? ops "Деталі кешу та інвалідації"
|
|
231
|
+
Cache key: `user:profile:{userId}:v2`.
|
|
232
|
+
Інвалідація — NATS подія `user.profile.updated` [user-profile-events].
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`???` = згорнуто за замовчуванням, `???+` = розгорнуто.
|
|
236
|
+
|
|
237
|
+
Кастомні audience-styles у `docs/stylesheets/audience.css`:
|
|
238
|
+
|
|
239
|
+
```css
|
|
240
|
+
.md-typeset details.engineer > summary {
|
|
241
|
+
background-color: rgba(84, 110, 122, 0.1);
|
|
242
|
+
border-color: #546e7a;
|
|
243
|
+
}
|
|
244
|
+
.md-typeset details.ops > summary {
|
|
245
|
+
background-color: rgba(255, 152, 0, 0.1);
|
|
246
|
+
border-color: #ff9800;
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Промпт для LLM-проекцій
|
|
251
|
+
|
|
252
|
+
Тримати у `docs/.docgen/prompts/projection.md` як артефакт першого класу — версіонується, ревʼюється, тестується на golden-наборі ADR. Скелет:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
Ти пишеш документацію для змішаної аудиторії (менеджери + інженери).
|
|
256
|
+
|
|
257
|
+
Структура кожної секції:
|
|
258
|
+
1. 2-4 речення прози рівня менеджера: що це робить, яку цінність дає,
|
|
259
|
+
як пов'язано з іншими частинами продукту. Без технологій, версій, коду.
|
|
260
|
+
|
|
261
|
+
2. Технічні деталі — тільки всередині блоків:
|
|
262
|
+
??? engineer "Описова назва блоку"
|
|
263
|
+
Код, конфіги, версії, посилання на ADR.
|
|
264
|
+
|
|
265
|
+
3. Operational деталі:
|
|
266
|
+
??? ops "Що моніторити та як реагувати"
|
|
267
|
+
|
|
268
|
+
Правила:
|
|
269
|
+
- Якщо прибрати всі ??? блоки, текст лишається зв'язним менеджерським документом.
|
|
270
|
+
- Кожен факт з ADR маркуй [<slug>] всередині engineer-блоку — slug це ім'я clean ADR
|
|
271
|
+
без розширення (наприклад `[ланцюжок-запуску-abie]`).
|
|
272
|
+
- Назви блоків — описові («Чому Percona, а не MariaDB»).
|
|
273
|
+
- НЕ виходь за межі <!-- AUTOGEN:start --> ... <!-- AUTOGEN:end -->.
|
|
274
|
+
- ADR без рядка `**Status:** Accepted` ігноруй.
|
|
275
|
+
- Якщо в ADR є `## Update YYYY-MM-DD` секції — враховуй найсвіжіший стан рішення,
|
|
276
|
+
старі формулювання вважай застарілими.
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Типові поломки LLM і захист
|
|
280
|
+
|
|
281
|
+
| Проблема | Захист |
|
|
282
|
+
| ---------------------------- | --------------------------------------------------------------------------------- |
|
|
283
|
+
| Галюцинація деталей | Правило `[<slug>]` маркера + post-gen linter, що перевіряє існування `docs/adr/<slug>.md` |
|
|
284
|
+
| Втрата `## Update`-еволюції | Pre-process: збирати всі `## Update YYYY-MM-DD` у ADR, передавати у промпт як патчі |
|
|
285
|
+
| Дрейф термінології | `docs/glossary.md` як перший вхід у кожен промпт |
|
|
286
|
+
| Перенасичення контексту | Двостадійна генерація: спочатку summary кожного ADR (кеш), потім проекція |
|
|
287
|
+
| Inconsistency між проекціями | Cross-projection validator порівнює факти між документами |
|
|
288
|
+
|
|
289
|
+
## Валідатор (обов'язково)
|
|
290
|
+
|
|
291
|
+
Окремий детермінований прохід після регенерації, **без LLM**. Bun-скрипт на ~100-200 рядків, запускається як pre-commit hook:
|
|
292
|
+
|
|
293
|
+
- Усі `[<slug>]` посилання вказують на існуючі файли `docs/adr/<slug>.md` із рядком `**Status:** Accepted`
|
|
294
|
+
- Усі `AUTOGEN:start` мають парний `AUTOGEN:end` з тим самим `id`
|
|
295
|
+
- Hash у `manifest.json` відповідає фактичному контенту зон
|
|
296
|
+
- Жоден `??? engineer` блок не з'являється в manager-only проекціях
|
|
297
|
+
- Якщо в зоні згадано компонент — він є в `docs/glossary.md`
|
|
298
|
+
- ADR без `**Status:** Accepted` не потрапили у проекцію
|
|
299
|
+
|
|
300
|
+
Без валідатора накопичується тихий дрейф: посилання гниють, hash розходиться з контентом, термінологія дрейфує.
|
|
73
301
|
|
|
74
302
|
## Аналіз перед зміною
|
|
75
303
|
|
|
76
|
-
Перш ніж вносити
|
|
77
|
-
тієї ділянки, до якої входить редагований код. Без цього кроку легко зламати приховані
|
|
78
|
-
залежності між контейнерами або «загубити» зовнішню інтеграцію.
|
|
304
|
+
Перш ніж вносити зміни в код, агент **читає відповідні docs**: контекст, контейнери, компоненти тієї ділянки, до якої входить редагований код, плюс accepted ADR за тематикою області (`docs/adr/<slug>.md` зі `**Status:** Accepted`). Без цього кроку легко зламати приховані залежності між сервісами або «загубити» зовнішню інтеграцію, описану в `explanation/architecture.md`.
|
|
79
305
|
|
|
80
306
|
## Оновлення синхронно зі змінами
|
|
81
307
|
|
|
82
|
-
Кожна зміна, що впливає на
|
|
83
|
-
зміна напрямку залежності, видалення сервісу — **супроводжується оновленням C4-схеми у тому ж
|
|
84
|
-
PR**. C4 не оновлюється «потім» окремою задачею: розсинхрон між кодом і моделлю — найчастіша
|
|
85
|
-
причина, чому архітектурні документи перестають читати.
|
|
308
|
+
Кожна зміна, що впливає на архітектуру — нова інтеграція, новий компонент, перейменування, зміна напрямку залежності, видалення сервісу — **супроводжується новим ADR (або `## Update YYYY-MM-DD` у наявному) і регенерацією проекцій у тому ж PR**. Документація не оновлюється «потім» окремою задачею: розсинхрон між кодом і моделлю — найчастіша причина, чому архітектурні документи перестають читати. Capture/normalize flow з правила `adr` забезпечує саму появу ADR — регенерацію проекцій ініціює окремий hook (наприклад, post-merge або `/regen-docs`).
|
|
86
309
|
|
|
87
310
|
## Зв'язок із ADR
|
|
88
311
|
|
|
89
|
-
ADR (`docs/adr
|
|
90
|
-
зникають, змінюють відповідальність. Якщо рішення суттєве — у тілі ADR явно пишемо, які
|
|
91
|
-
C4-схеми потрібно оновити, і робимо це у тому ж PR.
|
|
312
|
+
ADR (`docs/adr/<slug>.md`) — джерело правди для autogen-проекцій. Маршрутизація «ADR → зони, на які впливає» виконується через `sources="<slug>,<slug>"` атрибут у самій AUTOGEN-зоні та через семантичний discovery (ембеддинги розділів `## Context and Problem Statement` + `## Decision Outcome`). Поля `affects.components` у frontmatter немає за визначенням MADR v4 minimal — маршрутизація навмисно винесена у зону, а не вшита в ADR, щоб ADR лишався читабельним як автономний документ.
|
|
92
313
|
|
|
93
314
|
## Зв'язок із тестами
|
|
94
315
|
|
|
95
|
-
Кожен
|
|
96
|
-
від рівня. Так перехід «компонент → як його перевіряємо» займає один клік, а не пошук по
|
|
97
|
-
репозиторію.
|
|
316
|
+
Кожен опис компонента в `explanation/components/<name>.md` має **посилання на відповідні тести** — інтеграційні, e2e або юніт залежно від рівня. Так перехід «компонент → як його перевіряємо» займає один клік, а не пошук по репозиторію. Для агентських компонентів додаткове посилання на storage трасувань — нарівні з тестами, бо без них недетермінована поведінка непідтверджена.
|
|
98
317
|
|
|
99
318
|
## Зв'язок із документацією
|
|
100
319
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
320
|
+
Архітектурні артефакти — частина **користувацької документації**, а не закритий артефакт для команди. Контекстна діаграма (C4 рівень 1) і контейнерна (рівень 2) живуть там, де читач шукає вступ у проєкт — у `explanation/architecture.md`, не у відокремленій теці «for-architects». MkDocs Material рендерить це з collapsible-блоками, тому менеджер бачить прозу, інженер провалюється в деталі.
|
|
321
|
+
|
|
322
|
+
## Інструменти
|
|
323
|
+
|
|
324
|
+
- **Claude Code** як runner — slash-команда `/regen-docs` або post-commit hook на зміни `docs/adr/**`
|
|
325
|
+
- **capture-decisions.sh** + **normalize-decisions.sh** — Stop-hooks створення ADR (керує правило `adr`)
|
|
326
|
+
- **gray-matter** на Bun — лише для парсингу draft frontmatter; clean ADR парситься regexp по `**Status:**` / `**Date:**`
|
|
327
|
+
- **MkDocs + Material** як viewer
|
|
328
|
+
- **Mermaid** для C4-діаграм, EventModeling, Context Maps у проекціях
|
|
329
|
+
|
|
330
|
+
## Query mode як бонус
|
|
331
|
+
|
|
332
|
+
Поверх проекцій — інтерактивний RAG над clean ADR. Інженер питає: «чому ми обрали Percona замість MariaDB?». LLM шукає по semantic index `Context and Problem Statement + Decision Outcome`, повертає `docs/adr/<slug>.md` з прямою цитатою. Корпус малий (десятки-сотні clean ADR після normalize) і структурований (фіксовані MADR-заголовки) — дешево й точно, без галюцинацій.
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
* CLI-обгортка над канонічним `lint-text` (text.mdc): послідовно
|
|
3
3
|
* 1) `cspell .` — перевірка правопису з `@nitra/cspell-dict`;
|
|
4
4
|
* 2) `runShellcheckText()` — авто-фікс і фінальна перевірка `*.sh` через `shellcheck`;
|
|
5
|
-
* 3) `
|
|
6
|
-
* 4) `
|
|
5
|
+
* 3) `runDotenvLinter()` — авто-фікс і фінальна перевірка `.env*` через `dotenv-linter`;
|
|
6
|
+
* 4) `bunx markdownlint-cli2 --fix "**\/*.md" "**\/*.mdc"` — авто-фікс Markdown;
|
|
7
|
+
* 5) `runV8rWithGlobs()` — schema-валідація json/json5/yaml/yml/toml через v8r з каталогом `@nitra/cursor`.
|
|
7
8
|
*
|
|
8
9
|
* Перший ненульовий код з ланцюжка повертається як код виходу; наступні кроки не запускаються.
|
|
9
10
|
* Експортовано як `runLintTextCli` — використовується з `bin/n-cursor.js` як підкоманда `lint-text`.
|
|
10
11
|
*/
|
|
11
12
|
import { runLintStep } from '../../../scripts/utils/run-lint-step.mjs'
|
|
13
|
+
import { runDotenvLinter } from './run-dotenv-linter.mjs'
|
|
12
14
|
import { runShellcheckText } from './run-shellcheck.mjs'
|
|
13
15
|
import { runV8rWithGlobs } from './run-v8r.mjs'
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Виконує канонічний `lint-text`: cspell → run-shellcheck → markdownlint-cli2 → run-v8r.
|
|
18
|
+
* Виконує канонічний `lint-text`: cspell → run-shellcheck → run-dotenv-linter → markdownlint-cli2 → run-v8r.
|
|
17
19
|
* Першу помилку повертаємо як код виходу; наступні кроки не запускаються.
|
|
18
20
|
* Усі кроки синхронні (`spawnSync` + sync-ентрі з пакета), тому функція не async.
|
|
19
21
|
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
@@ -26,6 +28,10 @@ export function runLintTextCli() {
|
|
|
26
28
|
const shellcheckCode = runShellcheckText()
|
|
27
29
|
if (shellcheckCode !== 0) return shellcheckCode
|
|
28
30
|
|
|
31
|
+
console.log('\n▶ dotenv-linter (авто-фікс + фінальна перевірка .env*)')
|
|
32
|
+
const dotenvCode = runDotenvLinter()
|
|
33
|
+
if (dotenvCode !== 0) return dotenvCode
|
|
34
|
+
|
|
29
35
|
const markdownlintCode = runLintStep('markdownlint', 'bunx', ['markdownlint-cli2', '--fix', '**/*.md', '**/*.mdc'])
|
|
30
36
|
if (markdownlintCode !== 0) return markdownlintCode
|
|
31
37
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Запуск dotenv-linter у ланцюжку lint-text: спочатку авто-фікс, потім фінальна перевірка.
|
|
3
|
+
*
|
|
4
|
+
* dotenv-linter — швидкий лінтер для `.env`-файлів (LowercaseKey, DuplicatedKey, IncorrectDelimiter,
|
|
5
|
+
* UnorderedKey тощо). Інструмент очікується у PATH і **не** додається в `dependencies`/`devDependencies`
|
|
6
|
+
* (аналогічно shellcheck). Якщо `dotenv-linter` відсутній — друкуємо підказки встановлення
|
|
7
|
+
* (`brew install dotenv-linter` на macOS) і повертаємо 1.
|
|
8
|
+
*
|
|
9
|
+
* Файли шукає сам `dotenv-linter` у режимі `-r` (рекурсивно по дереву проєкту). Виключаємо
|
|
10
|
+
* `node_modules` і `.envrc` (direnv shell-синтаксис, не key=value формат). `.bak`-файли інструмент
|
|
11
|
+
* ігнорує самостійно. Якщо `.env*`-файлів немає, dotenv-linter повертає 0 ("Nothing to check").
|
|
12
|
+
*
|
|
13
|
+
* Авто-фікс — один прогон `dotenv-linter fix -r --no-backup --quiet . --exclude …` (інструмент сам
|
|
14
|
+
* застосовує усі виправлення без потреби в diff/patch, на відміну від shellcheck). Після цього —
|
|
15
|
+
* фінальний `dotenv-linter check -r --quiet . --exclude …`; будь-яке залишкове порушення — ненульовий
|
|
16
|
+
* код виходу.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import { resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
22
|
+
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
23
|
+
|
|
24
|
+
/** Каталоги/файли, які виключаємо з рекурсивного сканування dotenv-linter. */
|
|
25
|
+
const EXCLUDED_PATHS = ['node_modules', '.envrc']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Друкує підказки встановлення dotenv-linter у stderr.
|
|
29
|
+
* @returns {void}
|
|
30
|
+
*/
|
|
31
|
+
function printDotenvLinterInstallHints() {
|
|
32
|
+
process.stderr.write(
|
|
33
|
+
[
|
|
34
|
+
'❌ dotenv-linter не знайдено в PATH.',
|
|
35
|
+
'Встанови інструмент і повтори lint-text:',
|
|
36
|
+
' macOS: brew install dotenv-linter',
|
|
37
|
+
' Linux: curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin',
|
|
38
|
+
' cargo: cargo install dotenv-linter',
|
|
39
|
+
''
|
|
40
|
+
].join('\n')
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Будує перелік аргументів `--exclude <path>` для dotenv-linter.
|
|
46
|
+
* @returns {string[]} плоский масив `['--exclude', 'node_modules', '--exclude', '.envrc']`
|
|
47
|
+
*/
|
|
48
|
+
function buildExcludeArgs() {
|
|
49
|
+
return EXCLUDED_PATHS.flatMap(p => ['--exclude', p])
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Запускає dotenv-linter з авто-фіксом і фінальною перевіркою.
|
|
54
|
+
* @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
|
|
55
|
+
* @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
|
|
56
|
+
*/
|
|
57
|
+
export function runDotenvLinter(cwd = process.cwd()) {
|
|
58
|
+
const root = resolve(cwd)
|
|
59
|
+
const bin = resolveCmd('dotenv-linter')
|
|
60
|
+
if (!bin) {
|
|
61
|
+
printDotenvLinterInstallHints()
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const exclude = buildExcludeArgs()
|
|
66
|
+
const fixRun = spawnSync(bin, ['fix', '-r', '--no-backup', '--quiet', ...exclude, '.'], {
|
|
67
|
+
cwd: root,
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
env: process.env,
|
|
70
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
71
|
+
})
|
|
72
|
+
if (fixRun.error) {
|
|
73
|
+
process.stderr.write(`${fixRun.error.message}\n`)
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const checkRun = spawnSync(bin, ['check', '-r', '--quiet', ...exclude, '.'], {
|
|
78
|
+
cwd: root,
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
env: process.env,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
82
|
+
})
|
|
83
|
+
if (checkRun.error) {
|
|
84
|
+
process.stderr.write(`${checkRun.error.message}\n`)
|
|
85
|
+
return 1
|
|
86
|
+
}
|
|
87
|
+
if (checkRun.status === 0) return 0
|
|
88
|
+
if (checkRun.stdout?.length) process.stdout.write(checkRun.stdout)
|
|
89
|
+
if (checkRun.stderr?.length) process.stderr.write(checkRun.stderr)
|
|
90
|
+
return 1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isRunAsCli()) {
|
|
94
|
+
process.exitCode = runDotenvLinter()
|
|
95
|
+
}
|
package/rules/text/text.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), markdownlint-cli2, v8r, CI
|
|
2
|
+
description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.27'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
|
|
7
|
+
**oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -115,6 +115,8 @@ version: '1.26'
|
|
|
115
115
|
|
|
116
116
|
**shellcheck:** інструмент лише в **`PATH`** (як у **ga.mdc** для `lint-ga`), **не** додавай до `dependencies` / `devDependencies`. Якщо `shellcheck` відсутній — встанови локально (**macOS:** `brew install shellcheck`; **Debian/Ubuntu:** `sudo apt-get install -y shellcheck`; **Arch:** `sudo pacman -S shellcheck`). Канонічний **`lint-text`** делегує до CLI **`n-cursor lint-text`** (бінарка з `node_modules/.bin/` пакету `@nitra/cursor`): після **`cspell .`** виконується **`runShellcheckText()`** з пакета — циклічно **`shellcheck -f diff`** і **`patch -p1`** для авто-виправлень, потім повний прогін **`shellcheck`** по всіх tracked `*.sh` (у git) або по `**/*.sh` без `node_modules`. Потрібен також **`patch`** у `PATH` (на macOS зазвичай уже є).
|
|
117
117
|
|
|
118
|
+
**dotenv-linter:** після shellcheck CLI запускає **`runDotenvLinter()`** з пакета — рекурсивно по `.env*`-файлах проєкту через **`dotenv-linter fix -r --no-backup --quiet . --exclude node_modules --exclude .envrc`**, далі такий самий **`check`** для фінальної перевірки. Інструмент має бути в **`PATH`** і **не** додається в `dependencies` / `devDependencies`. Якщо `dotenv-linter` відсутній — встанови локально (**macOS:** `brew install dotenv-linter`; **Linux:** `curl -sSfL https://git.io/JLbXn | sh -s -- -b /usr/local/bin`; через **cargo:** `cargo install dotenv-linter`).
|
|
119
|
+
|
|
118
120
|
У v8r **немає** прапорця тихого режиму; CLI-обгортка **`n-cursor lint-text`** на четвертому кроці викликає **`runV8rWithGlobs()`** з пакета (реалізація — `npm/rules/text/js/run-v8r.mjs`): під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` обгортка підставляє в v8r сама.
|
|
119
121
|
|
|
120
122
|
```json title="package.json"
|
|
@@ -9,12 +9,16 @@ version: '1.1'
|
|
|
9
9
|
|
|
10
10
|
Скіл прибирає з проєкту все, що належить **ru-середовищу**. Залишаються тільки `dev` (як база) та `ua` як активне продакшн-середовище. Працюй послідовно по секціях нижче — після кожної секції перевіряй, що проєкт лишається консистентним (`kustomization.yaml` посилається лише на наявні файли, GitHub Actions-вирази синтаксично коректні).
|
|
11
11
|
|
|
12
|
+
## 0. Що НЕ чіпати
|
|
13
|
+
|
|
14
|
+
**`node_modules/`** (і будь-які `**/node_modules/`) — повністю виключаються з аналізу й модифікацій. Це згенеровані залежності: збіги на `ru`, `values-ru.*`, `cr.yandex` тощо там нерелевантні і відновляться при наступному `install`. Усі `find` / `git grep` / автозаміни мають пропускати ці шляхи. Аналогічно не чіпай `.git/`, `dist/`, `build/`, `.next/`, `.nuxt/`, `.output/`, `coverage/`.
|
|
15
|
+
|
|
12
16
|
## 1. Директорії з назвою `ru`
|
|
13
17
|
|
|
14
|
-
Видали всі директорії з назвою `ru` у
|
|
18
|
+
Видали всі директорії з назвою `ru` у проєкті (крім тих, що всередині `node_modules`):
|
|
15
19
|
|
|
16
20
|
```bash
|
|
17
|
-
find . -type d -name "ru" -exec rm -rf {} +
|
|
21
|
+
find . -type d \( -name node_modules -o -name .git \) -prune -o -type d -name "ru" -print -exec rm -rf {} +
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
Це чистить як `country/ru/`, так і `k8s/<...>/ru/` (overlay у kustomize). Після видалення overlay `ru/` обов’язково прибери відповідний запис у `resources:` у батьківському `kustomization.yaml`, якщо він там лишився.
|
|
@@ -27,7 +31,8 @@ find . -type d -name "ru" -exec rm -rf {} +
|
|
|
27
31
|
- будь-які файли, що закінчуються на `-ru` або `-ru.<ext>`, наприклад `site/.env.prod-ru`, `*.env.prod-ru`, `*.prod-ru.conf`
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
find . -type
|
|
34
|
+
find . -type d \( -name node_modules -o -name .git \) -prune -o \
|
|
35
|
+
-type f \( -name "values-ru.*" -o -name "*-ru" -o -name "*.prod-ru" -o -name "*.prod-ru.*" \) -print -delete
|
|
31
36
|
```
|
|
32
37
|
|
|
33
38
|
## 3. `.github/workflows/*.yml`
|
|
@@ -194,5 +199,5 @@ const tr = {
|
|
|
194
199
|
## 6. Після очистки
|
|
195
200
|
|
|
196
201
|
- Переконайся, що `kustomization.yaml` у кожній директорії `k8s/` не посилається на видалені overlay або файли.
|
|
197
|
-
- Пройдись `git grep` по репозиторію на залишки: `git grep -n -i -e '\bru\b' -e cr\.yandex -e country/ru -e prod-ru -e values-ru -e "'ya'"` — переглянь усі знахідки вручну, бо `ru` як слово може траплятися в легітимних контекстах (наприклад, `truncate`, `Aurum`, `cruft`). Видаляй лише ті входження, що належать ru-середовищу.
|
|
202
|
+
- Пройдись `git grep` по репозиторію на залишки: `git grep -n -i -e '\bru\b' -e cr\.yandex -e country/ru -e prod-ru -e values-ru -e "'ya'"` — переглянь усі знахідки вручну, бо `ru` як слово може траплятися в легітимних контекстах (наприклад, `truncate`, `Aurum`, `cruft`). Видаляй лише ті входження, що належать ru-середовищу. `git grep` за замовчуванням пропускає невідстежувані шляхи, тож `node_modules/` у вихід не потрапить, поки воно у `.gitignore`.
|
|
198
203
|
- Перевір CI локально: `npx @nitra/cursor check abie` (якщо правило **abie** ввімкнене у проєкті).
|