agent-quality-kit 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +101 -12
  2. package/README.ru.md +101 -11
  3. package/kit/gates/README.md +15 -0
  4. package/kit/gates/_native.sh +18 -2
  5. package/kit/gates/_skip.sh +18 -0
  6. package/kit/gates/color-from-token/README.md +52 -0
  7. package/kit/gates/color-from-token/check.sh +69 -0
  8. package/kit/gates/color-from-token/gate.yml +15 -0
  9. package/kit/gates/color-from-token/green/Button.tsx +4 -0
  10. package/kit/gates/color-from-token/green/Panel.vue +4 -0
  11. package/kit/gates/color-from-token/green/card.css +5 -0
  12. package/kit/gates/color-from-token/green/notes.md +2 -0
  13. package/kit/gates/color-from-token/green/tokens.css +7 -0
  14. package/kit/gates/color-from-token/red/Button.tsx +4 -0
  15. package/kit/gates/color-from-token/red/Panel.vue +4 -0
  16. package/kit/gates/color-from-token/red/card.css +5 -0
  17. package/kit/gates/commit-explains-itself/check.sh +25 -2
  18. package/kit/gates/duplicate-code/check.sh +13 -4
  19. package/kit/gates/duplicate-code/gate.yml +11 -2
  20. package/kit/gates/gate-has-samples/check.sh +9 -3
  21. package/kit/gates/gates-are-runnable/check.sh +7 -1
  22. package/kit/gates/gates-run-in-ci/check.sh +7 -1
  23. package/kit/gates/lesson-has-outcome/check.sh +6 -1
  24. package/kit/ratchet/ratchet.sh +9 -2
  25. package/llms.txt +57 -0
  26. package/package.json +2 -1
  27. package/tool/commands/doctor.mjs +62 -7
  28. package/tool/commands/gates.mjs +9 -2
  29. package/tool/commands/project.mjs +7 -4
  30. package/tool/commands/report.mjs +2 -2
  31. package/tool/i18n/en.mjs +38 -0
  32. package/tool/i18n/ru.mjs +43 -0
  33. package/tool/lib/baseline.mjs +87 -0
  34. package/tool/lib/core.mjs +10 -2
  35. package/tool/lib/manifest.mjs +33 -2
  36. package/tool/lib/repo.mjs +7 -0
  37. package/tool/selfcheck/gates.sh +9 -2
  38. package/tool/selfcheck/mutation.sh +95 -0
  39. package/tool/selfcheck/smoke.sh +216 -8
  40. package/tool/selfcheck/syntax.sh +9 -1
  41. package/tool/selfcheck/units.mjs +76 -1
package/README.md CHANGED
@@ -7,24 +7,61 @@
7
7
  [![MIT licence](https://img.shields.io/npm/l/agent-quality-kit)](LICENSE)
8
8
  [![AQK-3](https://img.shields.io/badge/AQK-3-2ea44f)](https://github.com/arsen-ask-lx/Agent_Quality_Kit)
9
9
 
10
- **A standard for whether a repository is ready to have its code written by agents.** Every
11
- promise the project makes turns into a command with an exit code — held by a machine, not by
12
- someone's good intentions.
10
+ **Check whether a repository is ready to have its code written by AI coding agents — and turn
11
+ the rules it promises to follow into commands with exit codes.**
13
12
 
14
- What a project needs before that is even possible, in plain words, independent of language and
15
- tooling: [the dark factory and the minimum that isn't optional](kit/docs/ai/project-baseline.md).
13
+ Your `AGENTS.md` says what the project promises. Nothing checks that those promises are true, or
14
+ that the commands it lists even run. AQK is that missing layer: one command reads the repository,
15
+ reports a level from AQK-0 to AQK-3, and names every guard that is missing.
16
16
 
17
17
  ```bash
18
- npx agent-quality-kit start # no code yet: day-zero guards, right away
19
18
  npx agent-quality-kit doctor # code already exists: your level and what to install
19
+ npx agent-quality-kit start # no code yet: day-zero guards, right away
20
20
  ```
21
21
 
22
- `doctor` only reads: it writes no file and sends nothing anywhere. It is safe to point at
23
- a repository you have not decided anything about yet.
22
+ `doctor` only reads. It writes no file and sends nothing anywhere safe to point at a repository
23
+ you have decided nothing about yet. Nothing to install: `npx` fetches the package (230 KB).
24
+
25
+ ### Works with any agent, any language
26
+
27
+ **Any agent.** Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, Aider, OpenCode
28
+ — and with no AI at all. AQK reads and writes plain files (`AGENTS.md`, `.aqk.yml`); it calls no
29
+ vendor API, needs no key, and is tied to no model. A promise only one tool can keep is not a
30
+ promise.
31
+
32
+ **Any language.** The portable checks are plain `sh` and work on any stack — Python, TypeScript,
33
+ Go, Rust, Java, Ruby, PHP, C#, Kotlin, Swift, Scala. Where the project already has a native tool
34
+ (`ruff`, `eslint`, `knip`, `jscpd`), the check uses it instead, because it is more precise — and
35
+ says so out loud when it falls back.
36
+
37
+ **Requirements:** Node 18+ and an `sh` shell. Present on macOS, Linux and WSL; Git Bash on Windows.
38
+
39
+ ### One movement, and everything follows from it
24
40
 
25
- Nothing to install — `npx` fetches the package itself (230 KB). The bleeding edge straight from
26
- the repository is `npx github:arsen-ask-lx/Agent_Quality_Kit doctor`, but the first run that way
27
- stays silent for two or three minutes: it clones the whole repository.
41
+ ```mermaid
42
+ flowchart LR
43
+ A["<b>AGENTS.md</b><br/>“never commit secrets”<br/><br/><i>a human reads it<br/>and may ignore it</i>"]
44
+ B["<b>.aqk.yml</b><br/>secrets-not-in-code:<br/>bash gates/…/check.sh<br/><br/><i>a machine holds it<br/>and cannot forget</i>"]
45
+ C["<b>exit code</b><br/>0 or 1<br/><br/><i>CI acts on it<br/>and cannot argue</i>"]
46
+ A -- "declare" --> B
47
+ B -- "run" --> C
48
+ ```
49
+
50
+ A promise the project makes turns into a command with an exit code. From then on a machine holds
51
+ it, not somebody's attention.
52
+
53
+ What a project needs before that is even possible, in plain words, independent of language and
54
+ tooling: [the dark factory and the minimum that isn't optional](kit/docs/ai/project-baseline.md).
55
+
56
+ ```
57
+ █████╗ ██████╗ ██╗ ██╗
58
+ ██╔══██╗ ██╔═══██╗██║ ██╔╝
59
+ ███████║ ██║ ██║█████╔╝
60
+ ██╔══██║ ██║▄▄ ██║██╔═██╗
61
+ ██║ ██║ ╚██████╔╝██║ ██╗
62
+ ╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝
63
+ a promise without an exit code is just a sentence
64
+ ```
28
65
 
29
66
  ## What this looks like
30
67
 
@@ -100,6 +137,21 @@ them empty, and they fill in as there becomes something real to put in them.
100
137
  **If a claim cannot be checked by a machine, it is not in this standard.** Otherwise the badge
101
138
  would mean trust in the author rather than a fact.
102
139
 
140
+ ### The minimum a project needs
141
+
142
+ ```bash
143
+ aqk doctor --baseline # ✔/✘ over the points a machine can confirm
144
+ ```
145
+
146
+ The guide [project-baseline.md](kit/docs/ai/project-baseline.md) lists 50 points a project needs
147
+ before the work can be handed to agents. Fourteen of them a machine can confirm from the
148
+ repository — a lockfile of any ecosystem, a linter config of any language, an error tracker in
149
+ the dependencies, a pipeline, tests. It says what proved each one. The remaining 36 are named as
150
+ a number rather than hidden: they are for your eyes.
151
+
152
+ Presence is what gets checked, not whether it works: "a linter is configured" and "a linter
153
+ catches things" are different claims, and the output says so out loud.
154
+
103
155
  ## Four levels
104
156
 
105
157
  | Level | Required | What it proves |
@@ -109,6 +161,18 @@ would mean trust in the author rather than a fact.
109
161
  | **AQK-2** | gates have red and green samples, debt under a ratchet | the gate catches defects and stays quiet on correct code |
110
162
  | **AQK-3** | a lesson journal with conclusions | the same bruise is not collected twice |
111
163
 
164
+ ```mermaid
165
+ flowchart LR
166
+ L0["<b>AQK-0</b><br/>a manifest<br/>and an entry point"]
167
+ L1["<b>AQK-1</b><br/>rules exist,<br/>gates are commands"]
168
+ L2["<b>AQK-2</b><br/>red and green samples,<br/>debt under a ratchet"]
169
+ L3["<b>AQK-3</b><br/>a lesson journal<br/>with conclusions"]
170
+ L0 --> L1 --> L2 --> L3
171
+ ```
172
+
173
+ A level is not a verdict on the project — it measures how **machine-readable** the practice is.
174
+ A hundred working checks with no manifest is AQK-0, and that is honest: nothing can read them.
175
+
112
176
  ```bash
113
177
  aqk doctor --run --min 1 # in CI: fails below AQK-1 OR if any gate failed
114
178
  ```
@@ -125,10 +189,35 @@ replaces. So `aqk badge` prints nothing over a red gate, and `aqk badge --check`
125
189
  pipeline on the day the README and the repository part ways. The badge at the top of this file
126
190
  is checked that way on every push.
127
191
 
192
+ ## As a pre-commit hook
193
+
194
+ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you already have:
195
+
196
+ ```yaml
197
+ repos:
198
+ - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
199
+ rev: v0.5.0
200
+ hooks:
201
+ - id: aqk # runs what the repository declares; blocks below AQK-1
202
+ # - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
203
+ # - id: aqk-baseline # the minimum a project needs, confirmed by a run
204
+ ```
205
+
206
+ `pre-commit` installs the package itself — there is nothing else to set up, and the package has
207
+ no dependencies.
208
+
209
+ **This does not replace pre-commit, it sits on top of it.** pre-commit runs checks; it says
210
+ nothing about *which* checks exist here, whether they work, and what this project has already
211
+ been burned by. Its own documentation is explicit about both gaps: no built-in compliance levels,
212
+ scoring or reporting — and it does not verify that a hook catches what it claims. That is the
213
+ layer AQK adds.
214
+
128
215
  ## In your pipeline
129
216
 
217
+ [![on the GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
218
+
130
219
  ```yaml
131
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.1
220
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
132
221
  with:
133
222
  min: 1 # the build fails below AQK-1, or if any declared gate failed
134
223
  ```
package/README.ru.md CHANGED
@@ -7,23 +7,62 @@
7
7
  [![лицензия MIT](https://img.shields.io/npm/l/agent-quality-kit)](LICENSE)
8
8
  [![AQK-3](https://img.shields.io/badge/AQK-3-2ea44f)](https://github.com/arsen-ask-lx/Agent_Quality_Kit)
9
9
 
10
- **Стандарт готовности репозитория к тому, что код в нём пишет агент.** Обещание проекта
11
- становится командой с кодом возврата и его держит машина, а не чья-то добрая воля.
10
+ **Проверить, готов ли репозиторий к тому, что код в нём пишет ИИ-агент и превратить правила,
11
+ которые проект обещает соблюдать, в команды с кодом возврата.**
12
12
 
13
- Что вообще должно быть на проекте, чтобы это было возможно, словами, без привязки к языку и
14
- инструменту: [«тёмная фабрика» и обязательный минимум](kit/docs/ai/project-baseline.md).
13
+ `AGENTS.md` говорит, что проект обещает. Никто не проверяет, правда ли это и запускаются ли
14
+ вообще перечисленные там команды. AQK — тот самый недостающий слой: одна команда читает
15
+ репозиторий, называет ступень от AQK-0 до AQK-3 и перечисляет каждого недостающего сторожа.
15
16
 
16
17
  ```bash
17
- npx agent-quality-kit start # кода ещё нет: сторожа дня 0 сразу
18
18
  npx agent-quality-kit doctor # код уже есть: уровень и что поставить
19
+ npx agent-quality-kit start # кода ещё нет: сторожа дня 0 сразу
19
20
  ```
20
21
 
21
- `doctor` только читает: ни одного файла не пишет и никуда ничего не отправляет. Его можно
22
- направить на репозиторий, о котором ещё ничего не решено.
22
+ `doctor` только читает: ни одного файла не пишет и никуда ничего не отправляет его можно
23
+ направить на репозиторий, о котором ещё ничего не решено. Ставить ничего не нужно, `npx` скачает
24
+ пакет сам (230 КБ).
25
+
26
+ ### Работает с любым агентом и любым языком
27
+
28
+ **Любой агент.** Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, Windsurf, Aider,
29
+ OpenCode — и вообще без ИИ. AQK читает и пишет обычные файлы (`AGENTS.md`, `.aqk.yml`), не
30
+ обращается ни к одному API вендора, не требует ключа и не привязан к модели. Обещание, которое
31
+ держит только один инструмент, — не обещание.
32
+
33
+ **Любой язык.** Переносимые проверки написаны на `sh` и работают на любом стеке — Python,
34
+ TypeScript, Go, Rust, Java, Ruby, PHP, C#, Kotlin, Swift, Scala. Там, где у проекта уже есть
35
+ родной инструмент (`ruff`, `eslint`, `knip`, `jscpd`), проверка берёт его — он точнее, — и вслух
36
+ сообщает, когда откатилась на переносимый.
37
+
38
+ **Требуется:** Node 18+ и оболочка `sh`. Есть на macOS, Linux и в WSL; на Windows — Git Bash.
39
+
40
+ ### Одно движение, из которого следует всё остальное
23
41
 
24
- Ставить ничего не нужно, `npx` скачает пакет сам (230 КБ). Свежая версия прямо из репозитория —
25
- `npx github:arsen-ask-lx/Agent_Quality_Kit doctor`, но первый запуск такого вида молчит две-три
26
- минуты: он клонирует репозиторий целиком.
42
+ ```mermaid
43
+ flowchart LR
44
+ A["<b>AGENTS.md</b><br/>«не коммить секреты»<br/><br/><i>читает человек<br/>и может забыть</i>"]
45
+ B["<b>.aqk.yml</b><br/>secrets-not-in-code:<br/>bash gates/…/check.sh<br/><br/><i>держит машина<br/>и забыть не может</i>"]
46
+ C["<b>код возврата</b><br/>0 или 1<br/><br/><i>знает конвейер<br/>и спорить не станет</i>"]
47
+ A -- "объявили" --> B
48
+ B -- "запустили" --> C
49
+ ```
50
+
51
+ Обещание проекта становится командой с кодом возврата. Дальше его держит машина, а не чьё-то
52
+ внимание.
53
+
54
+ Что вообще должно быть на проекте, чтобы это было возможно, — словами, без привязки к языку и
55
+ инструменту: [«тёмная фабрика» и обязательный минимум](kit/docs/ai/project-baseline.md).
56
+
57
+ ```
58
+ █████╗ ██████╗ ██╗ ██╗
59
+ ██╔══██╗ ██╔═══██╗██║ ██╔╝
60
+ ███████║ ██║ ██║█████╔╝
61
+ ██╔══██║ ██║▄▄ ██║██╔═██╗
62
+ ██║ ██║ ╚██████╔╝██║ ██╗
63
+ ╚═╝ ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═╝
64
+ обещание без кода возврата — просто предложение
65
+ ```
27
66
 
28
67
  ## Что это выглядит так
29
68
 
@@ -99,6 +138,21 @@ lessons: incidents # где копятся уроки
99
138
  **Если утверждение нельзя проверить машиной — его в стандарте нет.** Иначе значок означает
100
139
  доверие к автору, а не факт.
101
140
 
141
+ ### Обязательный минимум проекта
142
+
143
+ ```bash
144
+ aqk doctor --baseline # ✔/✘ по пунктам, которые машина может подтвердить
145
+ ```
146
+
147
+ Методичка [project-baseline.md](kit/docs/ai/project-baseline.md) перечисляет 50 пунктов, без
148
+ которых работу нельзя отдать агентам. Четырнадцать из них машина подтверждает по репозиторию —
149
+ файл-замок любой экосистемы, конфиг линтера любого языка, трекер ошибок в зависимостях,
150
+ конвейер, тесты. И называет, чем именно подтвердила. Оставшиеся 36 названы числом, а не спрятаны:
151
+ они — глазами.
152
+
153
+ Проверяется НАЛИЧИЕ признака, а не то, что он работает: «линтер настроен» и «линтер ловит» —
154
+ разные утверждения, и вывод говорит это вслух.
155
+
102
156
  ## Четыре ступени
103
157
 
104
158
  | Уровень | Требуется | Что доказано |
@@ -108,6 +162,18 @@ lessons: incidents # где копятся уроки
108
162
  | **AQK-2** | у гейтов красные и зелёные образцы, долг под храповиком | гейт ловит брак и молчит на исправном коде |
109
163
  | **AQK-3** | журнал уроков с выводами | шишка не набивается дважды |
110
164
 
165
+ ```mermaid
166
+ flowchart LR
167
+ L0["<b>AQK-0</b><br/>манифест<br/>и точка входа"]
168
+ L1["<b>AQK-1</b><br/>правила есть,<br/>гейты — команды"]
169
+ L2["<b>AQK-2</b><br/>красный и зелёный образцы,<br/>долг под храповиком"]
170
+ L3["<b>AQK-3</b><br/>журнал шишек<br/>с выводами"]
171
+ L0 --> L1 --> L2 --> L3
172
+ ```
173
+
174
+ Ступень — не приговор проекту: она мерит, насколько практика **машиночитаема**. Сто работающих
175
+ проверок без манифеста — это AQK-0, и это честно: их никто не может прочитать.
176
+
111
177
  ```bash
112
178
  aqk doctor --run --min 1 # в конвейере: ошибка, если ниже AQK-1 ИЛИ упал хоть один гейт
113
179
  ```
@@ -124,10 +190,34 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
124
190
  роняет конвейер в тот день, когда README и репозиторий разошлись. Значок в начале этого файла
125
191
  проверяется так на каждом пуше.
126
192
 
193
+ ## Как хук pre-commit
194
+
195
+ Уже пользуешься [pre-commit](https://pre-commit.com)? Три строки в файл, который у тебя и так
196
+ лежит:
197
+
198
+ ```yaml
199
+ repos:
200
+ - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
201
+ rev: v0.5.0
202
+ hooks:
203
+ - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
204
+ # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
205
+ # - id: aqk-baseline # обязательный минимум проекта, подтверждённый прогоном
206
+ ```
207
+
208
+ `pre-commit` ставит пакет сам — настраивать больше нечего, зависимостей у пакета нет.
209
+
210
+ **Это не замена pre-commit, а слой над ним.** pre-commit запускает проверки, но ничего не
211
+ говорит о том, **какие** проверки в репозитории есть, работают ли они и на чём здесь уже
212
+ обжигались. Его собственная документация признаёт обе дыры прямо: нет ни уровней, ни оценки, ни
213
+ отчётности — и он не проверяет, что хук ловит то, что заявляет. Этот слой и добавляет AQK.
214
+
127
215
  ## В твоём конвейере
128
216
 
217
+ [![в GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
218
+
129
219
  ```yaml
130
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.4.1
220
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
131
221
  with:
132
222
  min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
133
223
  ```
@@ -113,6 +113,20 @@ samples_for: python
113
113
  если завтра поправят источник?» Правильный ответ — «результат изменится сам», а не «нужно
114
114
  поправить ещё и здесь».
115
115
 
116
+ Седьмой, с прогона по живому проекту: **гейт, тонущий в собственном шуме.** Родной инструмент
117
+ не получал общий список исключений и читал `.aqk/` — каталог, который положил сам комплект. Из
118
+ 1846 находок 435 были не про код вовсе. Гейт остаётся правильным и становится нечитаемым, а
119
+ нечитаемый выключают целиком. Мера здесь — доля находок не по делу, а не длина вывода.
120
+
121
+ Восьмой, оттуда же: **родной и переносимый рецепты одной записи мерят разное.** Переносимый
122
+ смотрел только в расширения кода, родной — во всё подряд. Один гейт означал разное на разных
123
+ машинах, и какое именно — зависело от того, что стоит в системе.
124
+
125
+ Девятый, найденный мутационной проверкой: **гейт держится за то, чего не заявляет.** Четыре
126
+ записи переставали работать на windows-переносах строк — жадный захват в `sed` проглатывал
127
+ `\r`. Красный образец зеленел, зелёный краснел. Пара образцов этого не видит: она доказывает
128
+ одно срабатывание, а не класс. Ловит `tool/selfcheck/mutation.sh`.
129
+
116
130
  Зелёный образец закрывает третий способ, и он важнее красного: что гейт ловит брак, проверяют
117
131
  при установке; что он молчит на исправном коде — не проверяют почти никогда.
118
132
 
@@ -159,6 +173,7 @@ samples_for: python
159
173
  | `has_deps: true` | есть файл зависимостей |
160
174
  | `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
161
175
  | `has_env: true` | есть файл окружения |
176
+ | `has_ui: true` | есть стили или однофайловые компоненты (`.css`, `.scss`, `.vue`, `.svelte`, `.astro`) |
162
177
 
163
178
  Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
164
179
  программа не знает, отклоняется явно, а не пропускается молча.
@@ -22,8 +22,24 @@ shift || true
22
22
 
23
23
  . "$(dirname "$0")/_skip.sh" 2>/dev/null || { echo "не найден _skip.sh рядом с _native.sh"; exit 2; }
24
24
 
25
- OUT="$("$@" 2>&1)"; CODE=$?
26
- LEFT="$(printf '%s' "$OUT" | own_samples_filter "$DIR")"
25
+ # Два фильтра, а не один: own_samples_filter прячет образцы гейтов и то, что назвал проект
26
+ # в .aqkignore, а skip_paths_filter — общий список каталогов, который переносимые проверки
27
+ # получают при обходе. Родному инструменту он не доставался вовсе, и он читал `.aqk/`,
28
+ # `node_modules` и `.venv`. На живом проекте это дало 5597 строк вывода, где первой находкой
29
+ # были методички самого комплекта.
30
+ # Цвет снимается ДО фильтров. Родные инструменты печатают путь внутри escape-последовательности
31
+ # («\033[32m.aqk/docs/…»), и тогда имя каталога стоит не после «/» и не с начала строки — фильтр
32
+ # по границе пути его не видит. На живом проекте это выглядело как работающая правка: вывод
33
+ # сократился с 5597 строк до 5505, то есть не сократился. Заодно лог конвейера читается глазами.
34
+ # Код возврата берётся у САМОГО инструмента, до всякой обработки: в конвейере `$?` — это код
35
+ # последней команды, и снятие цвета молча делало бы любой прогон успешным.
36
+ OUT_RAW="$("$@" 2>&1)"; CODE=$?
37
+
38
+ # ESC подставляется через printf, а не пишется как \x1b: это расширение GNU sed, а гейты
39
+ # обязаны работать на любом sh.
40
+ ESC="$(printf '\033')"
41
+ OUT="$(printf '%s' "$OUT_RAW" | sed -e "s/${ESC}\[[0-9;]*[a-zA-Z]//g")"
42
+ LEFT="$(printf '%s' "$OUT" | skip_paths_filter | own_samples_filter "$DIR")"
27
43
 
28
44
  # Отказ не выдумываем: если инструмент завершился успешно, результат успешен, что бы ни
29
45
  # осталось в выводе. Красным делаем только то, что инструмент И счёл отказом, И что пережило
@@ -82,6 +82,24 @@ skip_grep() {
82
82
  for N in $SKIP_NAMES; do printf -- '--exclude-dir=%s ' "$N"; done
83
83
  }
84
84
 
85
+ # Для вывода родного инструмента: отбрасывает строки, чей путь лежит внутри пропускаемого
86
+ # каталога. Переносимые проверки получают этот список при обходе (skip_grep/skip_find), а
87
+ # родному инструменту он не доставался вовсе: на живом проекте первой находкой duplicate-code
88
+ # оказались методички самого комплекта в `.aqk/docs`, которые туда положил `init`.
89
+ #
90
+ # По сегменту пути, а не по подстроке — тот же урок, что с папкой `red`: имя «build» встречается
91
+ # и внутри слова, и файл `src/rebuild.py` прятать нельзя.
92
+ skip_paths_filter() {
93
+ RE_SKIP="$(printf '%s' "$SKIP_NAMES" | tr '\n' ' ' | sed -e 's/^ *//' -e 's/ *$//' \
94
+ -e 's/\./\\./g' -e 's/ */|/g')"
95
+ [ -n "$RE_SKIP" ] || { cat; return; }
96
+ # Граница слева — начало строки, «/» или любой символ, которого не бывает внутри имени пути.
97
+ # Родные инструменты печатают путь не с начала строки: у jscpd это « - путь». Пока требовалось
98
+ # начало строки или «/», такие строки не отсеивались вовсе. Буквы, цифры, «_», «.» и «-»
99
+ # границей не считаются — иначе «build/» отсеяло бы и «rebuild/».
100
+ grep -vE "(^|/|[^[:alnum:]_.-])($RE_SKIP)/"
101
+ }
102
+
85
103
  # Для find: -name X -prune -o …
86
104
  skip_find() {
87
105
  for N in $SKIP_NAMES; do printf -- '-name %s -prune -o ' "$N"; done
@@ -0,0 +1,52 @@
1
+ # Цвет приходит из токена темы, а не литералом в компоненте
2
+
3
+ **Намерение.** Литеральный цвет тема не перекрашивает. В проекте с несколькими темами экран,
4
+ свёрстанный литералами, в тёмной теме остаётся светлым пятном — и видит это не автор, а
5
+ пользователь, который тему переключил. Дефект тихий по построению: в теме автора всё правильно.
6
+
7
+ **Какой отказ это поймало.** Живой проект на React с двенадцатью темами. Запрет на сырой цвет
8
+ там оказался проверкой №8 из семнадцати в собственной оснастке — и завели его не из вкуса, а
9
+ после экранов, не переживших переключение темы. Запись в журнале: `incidents/README.md`,
10
+ 2026-09-06, там же измерено соотношение, ради которого запись переносили в каталог: из тридцати
11
+ с лишним запретов интерфейса машина ловила одиннадцать, остальные держались на глазах человека.
12
+
13
+ **Файл-источник значений называет проект.** Где-то литерал обязан быть — иначе токену неоткуда
14
+ взяться. По умолчанию проверка пропускает файлы, чьё имя содержит `tokens`, `theme`, `palette`,
15
+ `colors`, `variables`, `design-system`. Своё имя проект называет сам, рядом с командой в
16
+ манифесте — тем же приёмом, что каталоги с законной печатью в `no-print-in-prod`:
17
+
18
+ ```yaml
19
+ color-from-token: "AQK_COLOR_SOURCE='tokens|brand' bash .aqk/gates/color-from-token/check.sh ."
20
+ ```
21
+
22
+ **Готовый аналог есть, но он покрывает меньше, и поэтому его здесь нет.** У `stylelint` есть
23
+ правило `color-no-hex`, и для файлов стилей оно точнее нашего поиска. Но оно видит только стили,
24
+ а литерал в компоненте — `style={{ background: "#2563EB" }}`, `className="bg-[#2563EB]"` —
25
+ остаётся вне его. Родной рецепт покрывал бы половину предмета, и запись означала бы разное в
26
+ зависимости от того, что стоит в системе. Урок этого же дня: родной и переносимый рецепты одной
27
+ записи обязаны мерить одно и то же. Поэтому рецепт один, переносимый; кому нужна точность по
28
+ стилям — ставит `stylelint` отдельно, это не отменяет запись.
29
+
30
+ **Замер на живом проекте.** React, двенадцать тем, 2750 файлов кода. Первый прогон дал 20
31
+ находок, и **все 20 были чужими** — литералы в отдельных html-документах внутри `.claude/`:
32
+ отчёты и справочники оснастки агента. Отдельный html-документ темы не имеет и иметь не может,
33
+ литерал там законен; `.html` убран из списка расширений — не на вкус, а по этому замеру.
34
+ Второй прогон: одна находка во фронтенде — палитра диаграммы загрузки, где цвета треков вписаны
35
+ литералами Tailwind-500 и потому не меняются вместе с темой. Ложных срабатываний: **одно**, и
36
+ оно тоже исправлено — цвет, упомянутый внутри многострочного комментария, читался как находка.
37
+
38
+ **Чего НЕ ловит.**
39
+
40
+ - Цвет, собранный по частям: `"#" + hex`, `rgb(37, 99, 235)`, `hsl(...)`, именованные `red` и
41
+ `white`. Проверка ищет литерал в форме `#rgb`, `#rrggbb`, `#rrggbbaa`, а не «цвет вообще».
42
+ - Селектор по идентификатору, записанный шестнадцатеричными буквами (`#abcdef { … }`), и якорь
43
+ в ссылке той же формы читаются как цвет. Это цена простоты; лечится именем в allowlist.
44
+ - Литерал в разметке и в документации: расширений `.md` и `.html` в списке нет намеренно.
45
+ В разметке цвет упоминают, а не применяют; отдельный html-документ не имеет темы, и литерал
46
+ в нём законен — как печать в скрипте для `no-print-in-prod`.
47
+ - Цвет, вычисленный в рантайме: `lighten(base, 0.2)`, значение из ответа сервера, инлайн-стиль,
48
+ собранный из переменной. Проверка читает исходник, а не то, что окажется в браузере.
49
+
50
+ **Образцы.** `red/` — три стека, где цвет вписан литералом: React со `style`, обычный CSS,
51
+ однофайловый компонент Vue. `green/` — то же самое через токены, плюс `tokens.css`, где литерал
52
+ законен, и заметка в разметке, на которой проверка обязана молчать.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env sh
2
+ # Цвет приходит из токена темы, а не из литерала в компоненте.
3
+ #
4
+ # ЗАЧЕМ. Литеральный цвет тема не перекрашивает. Проект с несколькими темами получает экран,
5
+ # который в тёмной теме остаётся светлым пятном, — и это видно не автору, а пользователю,
6
+ # который переключил тему. Дефект тихий: в теме автора всё выглядит правильно.
7
+ DIR="${1:-.}"
8
+ . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
9
+
10
+ # Расширения, где живёт ТЕМИЗИРУЕМЫЙ интерфейс. Своё, а не общий CODE_EXT: там нет ни css, ни
11
+ # vue — они не код в смысле «отладочная печать», но именно в них живёт цвет.
12
+ #
13
+ # `.html` в списке НЕТ, и это измерено, а не выбрано на вкус. Первый прогон по живому проекту
14
+ # (React, 12 тем) дал 20 находок, и ВСЕ 20 — в отдельных html-документах внутри `.claude/`:
15
+ # отчёты и справочники оснастки агента. Во фронтенде — ноль. Отдельный html-документ не имеет
16
+ # темы и не может её иметь: литерал там законен, как печать в скрипте. Гейт, на 100%
17
+ # состоящий из чужих находок, выключают в первый день — см. шапку `_skip.sh`.
18
+ COLOR_EXT="css scss sass less styl ts tsx js jsx mjs cjs vue svelte astro"
19
+
20
+ # Файлы-ИСТОЧНИКИ значений: там литерал законен, иначе токену неоткуда взяться. Имя источника
21
+ # проект называет сам — как и каталоги с законной печатью в no-print-in-prod. По умолчанию
22
+ # общепринятые имена; свои задаются переменной, рядом с командой в манифесте.
23
+ SOURCE_RE="${AQK_COLOR_SOURCE:-tokens|theme|themes|palette|colors|colours|variables|design-system}"
24
+
25
+ EXT_RE="$(printf '%s' "$COLOR_EXT" | tr ' ' '|')"
26
+
27
+ # shellcheck disable=SC2086
28
+ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
29
+ | grep -E "\.($EXT_RE)$" \
30
+ | grep -viE "(^|/)[^/]*($SOURCE_RE)[^/]*\.($EXT_RE)$" \
31
+ | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
32
+ | LC_ALL=C sort \
33
+ | xargs -r awk '
34
+ # Блочные комментарии вырезаются ПО СОСТОЯНИЮ, а не построчно. Однострочный фильтр
35
+ # смотрит только на начало строки, и цвет, упомянутый в середине многострочного
36
+ # пояснения, читался как находка: на живом проекте так и вышло — единственное
37
+ # ложное срабатывание пришло из строки внутри /* … */. Тот же класс, что запись
38
+ # журнала «печать внутри комментария считалась печатью».
39
+ FNR == 1 { inblock = 0 }
40
+ {
41
+ line = $0
42
+ if (inblock) {
43
+ i = index(line, "*/")
44
+ if (i == 0) next
45
+ line = substr(line, i + 2); inblock = 0
46
+ }
47
+ # Вырезаем закрытые /* … */ внутри строки, затем открытый хвост.
48
+ while ((s = index(line, "/*")) > 0) {
49
+ rest = substr(line, s + 2)
50
+ e = index(rest, "*/")
51
+ if (e == 0) { line = substr(line, 1, s - 1); inblock = 1; break }
52
+ line = substr(line, 1, s - 1) substr(rest, e + 2)
53
+ }
54
+ if (line ~ /#[0-9a-fA-F]{8}([^0-9a-fA-F]|$)|#[0-9a-fA-F]{6}([^0-9a-fA-F]|$)|#[0-9a-fA-F]{3}([^0-9a-fA-F]|$)/)
55
+ print FILENAME ":" FNR ":" $0
56
+ }
57
+ ' 2>/dev/null \
58
+ | drop_comments \
59
+ | own_samples_filter "$DIR")
60
+
61
+ if [ -n "$HITS" ]; then
62
+ printf '%s\n' "$HITS" | head -20
63
+ N=$(printf '%s\n' "$HITS" | grep -c .); [ "$N" -gt 20 ] && echo " … и ещё $((N - 20))"
64
+ echo " почини: замени литерал на токен темы — var(--color-…), theme('colors.…'), \$color-…"
65
+ echo " литеральный цвет тема не перекрашивает: в другой теме экран останется чужим пятном."
66
+ echo " файл-источник значений называется сам: AQK_COLOR_SOURCE='tokens|brand' перед командой."
67
+ exit 1
68
+ fi
69
+ exit 0
@@ -0,0 +1,15 @@
1
+ intent: цвет приходит из токена темы, а не литералом в компоненте
2
+ intent_en: colour comes from a theme token, not a literal in the component
3
+
4
+ # Не по языку, а по наличию интерфейса. Триггер `langs: javascript, typescript` показывал бы
5
+ # запись каждому бэкенду, библиотеке и утилите командной строки на JS — а их большинство, и
6
+ # интерфейса там нет. Записи, показанной не тому, не верят, и это стоит доверия всему каталогу.
7
+ trigger:
8
+ has_ui: true
9
+
10
+ recipes:
11
+ any: bash {gate}/check.sh {dir}
12
+
13
+ proof: incidents/README.md, 2026-09-06 «из тридцати с лишним запретов интерфейса гейт ловил
14
+ одиннадцать» — прогон по audit_project (Django + React, 12 тем): проверка №8 в собственной
15
+ оснастке проекта родилась после экранов, остававшихся светлыми в тёмной теме
@@ -0,0 +1,4 @@
1
+ export function Button({ children }: { children: React.ReactNode }) {
2
+ // Цвет приходит из темы: перекрашивается вместе с ней.
3
+ return <button className="bg-[var(--color-accent)] text-[var(--color-on-accent)]">{children}</button>;
4
+ }
@@ -0,0 +1,4 @@
1
+ <template><div class="panel">панель</div></template>
2
+ <style scoped>
3
+ .panel { background: var(--surface-base); box-shadow: 0 1px 2px var(--shadow-sm); }
4
+ </style>
@@ -0,0 +1,5 @@
1
+ .card {
2
+ background: var(--surface-elevated);
3
+ border: 1px solid var(--border-hairline);
4
+ color: var(--text-primary);
5
+ }
@@ -0,0 +1,2 @@
1
+ Заметка рядом с кодом: цвет `#2563EB` упоминается в тексте, а не применяется.
2
+ Разметка в список расширений не входит — проверка сюда не смотрит.
@@ -0,0 +1,7 @@
1
+ /* Файл-ИСТОЧНИК значений: здесь литерал законен, иначе токену неоткуда взяться.
2
+ Имя распознаётся по умолчанию; своё задаётся переменной AQK_COLOR_SOURCE. */
3
+ :root {
4
+ --color-accent: #2563EB;
5
+ --surface-base: #0B0B0B;
6
+ --text-primary: #F4F1EA;
7
+ }
@@ -0,0 +1,4 @@
1
+ export function Button({ children }: { children: React.ReactNode }) {
2
+ // Литерал: в тёмной теме кнопка останется светлой.
3
+ return <button style={{ background: "#2563EB", color: "#fff" }}>{children}</button>;
4
+ }
@@ -0,0 +1,4 @@
1
+ <template><div class="panel">панель</div></template>
2
+ <style scoped>
3
+ .panel { background: #1a1a1a; box-shadow: 0 1px 2px #00000022; }
4
+ </style>
@@ -0,0 +1,5 @@
1
+ .card {
2
+ background: #F4F1EA;
3
+ border: 1px solid #ddd;
4
+ color: #0B0B0B;
5
+ }
@@ -26,7 +26,30 @@ else
26
26
  exit 0
27
27
  fi
28
28
 
29
- MSG=$(cd "$DIR" && git log -1 --format=%B 2>/dev/null)
29
+ # Сообщение слияния сочиняет не автор, а инструмент. GitHub делает это дважды и по-разному:
30
+ # при разборе предложения изменений выкладывает синтетический коммит «Merge <sha> into <sha>»,
31
+ # а кнопка Merge на сайте пишет «Merge pull request #N from …». Первую форму гейт научился
32
+ # различать вчера по словам — и на второй покраснел бы снова.
33
+ #
34
+ # Поэтому смотрим не на слова, а на форму: у слияния два родителя. Если при этом в сообщении
35
+ # нет разделов отчёта — его писал инструмент, и спрашивать надо второго родителя, последний
36
+ # коммит автора. Слияние, которое автор описал сам, проходит по HEAD и ничего не теряет.
37
+ REF=HEAD
38
+ PARENTS=$(cd "$DIR" && git rev-list --parents -n 1 HEAD 2>/dev/null | wc -w)
39
+ HEADMSG=$(cd "$DIR" && git log -1 --format=%B 2>/dev/null)
40
+ if [ "${PARENTS:-0}" -ge 3 ] &&
41
+ ! printf '%s\n' "$HEADMSG" | grep -q "^Сделано:" &&
42
+ ! printf '%s\n' "$HEADMSG" | grep -q "^Не уверен:"; then
43
+ if (cd "$DIR" && git rev-parse -q --verify HEAD^2 >/dev/null 2>&1); then
44
+ REF=HEAD^2
45
+ else
46
+ echo "слияние без отчёта, второго родителя не видно — проверка пропущена"
47
+ echo " дай конвейеру два коммита истории: actions/checkout@v4 с fetch-depth: 2"
48
+ exit 0
49
+ fi
50
+ fi
51
+
52
+ MSG=$(cd "$DIR" && git log -1 --format=%B "$REF" 2>/dev/null)
30
53
 
31
54
  # Коммит, который трогает только журнал, отчёта в теле не требует: сама запись и есть отчёт,
32
55
  # причём подробнее — и её сторожит `lesson-has-outcome`. Иначе гейт воюет с командой `note`,
@@ -38,7 +61,7 @@ else
38
61
  [ -z "$LESSONS" ] && LESSONS="incidents"
39
62
  case "$LESSONS" in http*) LESSONS="" ;; esac # journal по адресу, а не путём — не применимо
40
63
  if [ -n "$LESSONS" ]; then
41
- FILES=$(cd "$DIR" && git show --pretty=format: --name-only HEAD 2>/dev/null | grep -v '^$')
64
+ FILES=$(cd "$DIR" && git show --pretty=format: --name-only "$REF" 2>/dev/null | grep -v '^$')
42
65
  if [ -n "$FILES" ]; then
43
66
  OUTSIDE=$(printf '%s\n' "$FILES" | grep -v "^$LESSONS/" | grep -v "^$LESSONS\$")
44
67
  if [ -z "$OUTSIDE" ]; then