@rt-tools/agent-kit 0.5.3 → 0.7.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 (85) hide show
  1. package/README.md +15 -1
  2. package/assets/checks/check-file-size.mjs +127 -0
  3. package/assets/checks/check-push-gate.mjs +139 -0
  4. package/assets/checks/rt-kit-checks.config.mjs +21 -0
  5. package/assets/defaults/gate-map.sh +90 -34
  6. package/assets/defaults/project.sh +26 -3
  7. package/assets/hooks/skill-gate-layers.sh +156 -0
  8. package/assets/hooks/skill-gate.sh +11 -2
  9. package/assets/laws/code-structure.md +3 -0
  10. package/assets/laws/delivery.md +9 -0
  11. package/assets/laws/observability.md +46 -0
  12. package/assets/laws/project-documentation.md +4 -0
  13. package/assets/laws/reuse-first.md +2 -0
  14. package/assets/laws/verifiability.md +5 -0
  15. package/assets/patterns/browser-verification-stand.md +22 -2
  16. package/assets/patterns/doc-style-trace.md +111 -0
  17. package/assets/patterns/git-workflow-commit.azure.md +18 -0
  18. package/assets/patterns/git-workflow-commit.github.md +19 -1
  19. package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
  20. package/assets/patterns/git-workflow-docker.md +203 -0
  21. package/assets/patterns/git-workflow-secrets.md +93 -0
  22. package/assets/patterns/observability-record.md +114 -0
  23. package/assets/patterns/seo-verify.md +1 -1
  24. package/assets/patterns/spec-driven-domain.md +2 -2
  25. package/assets/patterns/spec-driven-rule.md +5 -0
  26. package/assets/patterns/styling-bem-sheet.md +178 -0
  27. package/assets/patterns/task-flow-close.md +20 -0
  28. package/assets/patterns/task-flow-resume.md +5 -0
  29. package/assets/patterns/translations-content.md +107 -0
  30. package/assets/patterns/translations-key.md +1 -1
  31. package/assets/rules/angular-patterns.md +5 -0
  32. package/assets/rules/browser-verification.md +17 -12
  33. package/assets/rules/component-structure.md +6 -2
  34. package/assets/rules/doc-style.md +16 -0
  35. package/assets/rules/git-workflow.azure.md +60 -1
  36. package/assets/rules/git-workflow.github.md +67 -1
  37. package/assets/rules/git-workflow.gitlab.md +61 -1
  38. package/assets/rules/lists.md +13 -0
  39. package/assets/rules/observability.md +147 -0
  40. package/assets/rules/permissions.md +23 -0
  41. package/assets/rules/reuse-first.md +9 -0
  42. package/assets/rules/seo.md +57 -9
  43. package/assets/rules/shared-code.md +6 -0
  44. package/assets/rules/spec-driven.md +9 -0
  45. package/assets/rules/styling-bem.md +34 -1
  46. package/assets/rules/task-flow.md +5 -0
  47. package/assets/rules/testing.md +46 -8
  48. package/assets/rules/translations.md +11 -5
  49. package/assets/rules/typescript-conventions.md +5 -0
  50. package/lib/assets.d.ts +1 -1
  51. package/lib/assets.d.ts.map +1 -1
  52. package/lib/assets.js +2 -4
  53. package/lib/assets.js.map +1 -1
  54. package/lib/catalog.d.ts +73 -0
  55. package/lib/catalog.d.ts.map +1 -1
  56. package/lib/catalog.js +121 -0
  57. package/lib/catalog.js.map +1 -1
  58. package/lib/commands.d.ts.map +1 -1
  59. package/lib/commands.js +84 -5
  60. package/lib/commands.js.map +1 -1
  61. package/lib/integrity.d.ts +25 -12
  62. package/lib/integrity.d.ts.map +1 -1
  63. package/lib/integrity.js +40 -18
  64. package/lib/integrity.js.map +1 -1
  65. package/lib/proposals.d.ts +9 -1
  66. package/lib/proposals.d.ts.map +1 -1
  67. package/lib/proposals.js +11 -2
  68. package/lib/proposals.js.map +1 -1
  69. package/lib/retired.d.ts +30 -0
  70. package/lib/retired.d.ts.map +1 -0
  71. package/lib/retired.js +19 -0
  72. package/lib/retired.js.map +1 -0
  73. package/lib/sync.d.ts +45 -1
  74. package/lib/sync.d.ts.map +1 -1
  75. package/lib/sync.js +44 -10
  76. package/lib/sync.js.map +1 -1
  77. package/package.json +1 -1
  78. package/rt-tools-agent-kit-0.7.0.tgz +0 -0
  79. package/assets/laws/application/money.md +0 -41
  80. package/assets/laws/application/ownership.md +0 -32
  81. package/assets/patterns/ownership-scope-resolve.md +0 -69
  82. package/assets/patterns/pricing-quote.md +0 -71
  83. package/assets/rules/ownership-scope.md +0 -63
  84. package/assets/rules/pricing.md +0 -64
  85. package/rt-tools-agent-kit-0.5.3.tgz +0 -0
package/README.md CHANGED
@@ -185,6 +185,19 @@ npx agent-kit init --laws access,delivery,verifiability
185
185
  трогает. Имена законов — из `agent-kit list`, там же видно, что взято, что не выбрано и что
186
186
  пропущено.
187
187
 
188
+ **Отказ от закона снимает его правила и паттерны сам.** Связь читается из вступления ресурсов —
189
+ у правила стоит `law`, у паттерна `rule`, — и перечислять потомков в `skip` не надо: строка,
190
+ которая после этого ничего не снимает, называется вслух предупреждением, а раскладка идёт
191
+ дальше. Каскад идёт только сверху вниз: отказ от паттерна ни правила, ни закона не трогает.
192
+
193
+ Правило, названное в `only` при невыбранном законе, тоже называется вслух: раскладка говорит,
194
+ что оно не приедет и по какому родителю. Файл такого правила, разложенный прежней настройкой,
195
+ перечисляется отдельно от брошенного — дерево от него не отказывалось.
196
+
197
+ Ресурс, ушедший из набора совсем, пакет помнит по имени: файл, положенный им в дерево прежней
198
+ редакцией, назван при раскладке вместе с причиной снятия. Убирает его дерево — в чужие файлы
199
+ пакет не пишет.
200
+
188
201
  ## Конфиг
189
202
 
190
203
  `.claude/rt-kit.json` коммитится: раскладка обязана повторяться на чужой машине без вопросов.
@@ -212,7 +225,8 @@ npx agent-kit init --laws access,delivery,verifiability
212
225
  называет: перечислив законы, проект говорит о законах, а не обо всём, что пакет везёт, —
213
226
  шаблоны при нём остаются.
214
227
  - **`skip`** — ресурсы, от которых проект отказался, теми же идентификаторами. Вычитает из
215
- выбранного, поэтому отказ от одного закона не требует переписывать весь список.
228
+ выбранного, поэтому отказ от одного закона не требует переписывать весь список. Правила при
229
+ отвергнутом законе и паттерны при них перечислять не надо: их снимает каскад.
216
230
 
217
231
  ## Надстройки
218
232
 
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Проверка того, что файл не длиннее предела.
4
+ *
5
+ * Файл, который не влезает на экран целиком, читают по частям, и правку в нём
6
+ * делают, не увидев остального. У кода длину стережёт линтер; здесь — всё, до
7
+ * чего он не доходит: проза, стили, шаблоны, обвязка разработки и сами гарды.
8
+ *
9
+ * Судятся `.md`, `.scss`, `.html`, `.js`, `.mjs` и `.sh`. Данные не судятся
10
+ * вовсе: словарь локали и настройка сборки читаются поиском, а не подряд, и
11
+ * делить их не на что. Код на языке, где длину стережёт линтер, тоже не
12
+ * судится — два отказа на один файл читаются как две разные претензии.
13
+ *
14
+ * Строки считаются все, включая пустые и комментарии, и тем же способом, каким
15
+ * их считает линтер: по числу разрывов плюс один. Файл, кончающийся переводом
16
+ * строки, поэтому весит на строку больше, чем показывает `wc -l`, — зато у обеих
17
+ * проверок дерева одно понятие длины.
18
+ *
19
+ * Описание прошлого из счёта выведено: архив по устройству перечисляет то, чего
20
+ * в дереве уже нет, а папка задачи умирает со слиянием. Сгенерированное выведено
21
+ * каталогом: его переписывает генератор целиком, и спорить с ним о длине некому.
22
+ *
23
+ * Накопленное к моменту заведения проверки лежит в списке известного и отказом
24
+ * не считается: гейт падает на НОВОМ длинном файле, а старое остаётся видимым
25
+ * числом в сводке. Принятое и долг там различаются: принятое дерево делить не
26
+ * собирается, на долг заведена работа. Строка снимается вместе с делением своего
27
+ * файла, и проверка сама говорит, какую строку пора убрать.
28
+ *
29
+ * Ненулевой код возврата и перечень расхождений.
30
+ */
31
+ import { execFileSync } from 'node:child_process';
32
+ import { existsSync, readFileSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+
35
+ import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
36
+
37
+ const ALLOWLIST = allowlistOf('file-size');
38
+ /** Предел один на все роды файлов: своё число каждому роду — спор о числе на каждой правке. */
39
+ const LIMIT = CONFIG.fileSizeLimit;
40
+
41
+ /** Роды файлов, которых не читает линтер. Код остаётся за ним. */
42
+ const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
43
+
44
+ /** Описание прошлого, папка задачи и то, что переписывает генератор. */
45
+ const SKIPPED_PREFIXES = [CONFIG.archiveDir, `${CONFIG.tasksDir}/`, ...CONFIG.generatedDirs];
46
+
47
+ /** Дерево спрашивается у системы контроля версий: иначе каталоги с точки не видны, а сборка видна. */
48
+ function trackedFiles() {
49
+ return execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 1024 * 1024 * 32 })
50
+ .split('\n')
51
+ .filter(Boolean);
52
+ }
53
+
54
+ function judged(path) {
55
+ if (SKIPPED_PREFIXES.some((prefix) => prefix && path.startsWith(prefix))) {
56
+ return false;
57
+ }
58
+
59
+ return JUDGED.some((extension) => path.endsWith(extension));
60
+ }
61
+
62
+ /** Тем же способом, каким считает линтер: число разрывов плюс один. */
63
+ function lineCount(path) {
64
+ return readFileSync(join(ROOT, path), 'utf8').split('\n').length;
65
+ }
66
+
67
+ /**
68
+ * Список известного читается отдельно от общего читателя: у этой проверки нет файла — это
69
+ * не пустой список, а нечитаемая настройка, и молчать о ней нельзя. Пустой список законен
70
+ * ровно один раз — в дереве, где длинных файлов нет вовсе.
71
+ */
72
+ function readKnown() {
73
+ const path = join(ROOT, ALLOWLIST);
74
+ if (!existsSync(path)) {
75
+ return { accepted: [], debt: [] };
76
+ }
77
+ try {
78
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
79
+
80
+ return { accepted: parsed.accepted ?? [], debt: parsed.debt ?? [] };
81
+ } catch (error) {
82
+ console.error(`check-file-size: список известного не прочитан — ${ALLOWLIST}: ${error.message}`);
83
+ process.exit(1);
84
+ }
85
+ }
86
+
87
+ const { accepted, debt } = readKnown();
88
+ const known = new Map([...accepted.map((path) => [path, 'принято']), ...debt.map((path) => [path, 'долг'])]);
89
+
90
+ const tooLong = new Map();
91
+ const tracked = trackedFiles().filter(judged);
92
+
93
+ for (const path of tracked) {
94
+ const lines = lineCount(path);
95
+ if (lines > LIMIT) {
96
+ tooLong.set(path, lines);
97
+ }
98
+ }
99
+
100
+ if (process.argv.includes('--baseline')) {
101
+ console.log(JSON.stringify({ accepted, debt: [...tooLong.keys()].sort() }, null, 4));
102
+ process.exit(0);
103
+ }
104
+
105
+ const fresh = [...tooLong].filter(([path]) => !known.has(path));
106
+ /** Строка на файл, которого в дереве нет, — устаревшая: иначе список копит мёртвое. */
107
+ const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
108
+ /** Файл поделили, а строку оставили: список перестал бы отвечать за то, что в нём стоит. */
109
+ const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
110
+
111
+ const problems = [
112
+ ...fresh.map(([path, lines]) => `${path}: ${lines} строк, предел ${LIMIT} — делить, а не дописывать строку в ${ALLOWLIST}`),
113
+ ...gone.map((path) => `${path}: строка в ${ALLOWLIST} устарела — файла в дереве нет`),
114
+ ...shrunk.map((path) => `${path}: значится в ${ALLOWLIST}, но уже короче предела — строку убрать`),
115
+ ];
116
+
117
+ if (problems.length > 0) {
118
+ console.error(`check-file-size: расхождений ${problems.length}\n`);
119
+ problems.forEach((problem) => console.error(` ${problem}`));
120
+ console.error('\nПредел длины файла — правило о языке для кода и правило о текстах для прозы.');
121
+ process.exit(1);
122
+ }
123
+
124
+ console.log(
125
+ `check-file-size: проверено ${tracked.length} файлов, длиннее ${LIMIT} строк ${tooLong.size}, ` +
126
+ `из них принято ${accepted.length}, долг ${debt.length} — новых нет`
127
+ );
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Проверка того, что набор гейта пуша не уже набора конвейера.
4
+ *
5
+ * Гейт пуша — обещание, что пуш не приедет красным. Пока его набор уже набора
6
+ * конвейера, обещание шире того, что гейт проверяет, и неверно оно молча: ни
7
+ * гейт, ни его вывод не говорят, чего в нём нет. Дважды подряд правка, прошедшая
8
+ * гейт целиком, была отбита конвейером — и оба раза зелёный гейт был прочитан
9
+ * как «локально всё зелено».
10
+ *
11
+ * Файл конвейера машиной не толкуется: команды там произвольны, а часть шагов
12
+ * локально неисполнима вовсе — кэш, выгрузка следов, вход в реестр. Отсюда
13
+ * берутся только ИМЕНА шагов, а чем каждое из них закрывается в гейте,
14
+ * объявляет дерево. Необъявленное имя краснеет: дыра видна на месте, а не
15
+ * выводится сверкой двух списков глазами.
16
+ *
17
+ * Объявление бывает двух родов. Строка — та самая команда, которую печатает
18
+ * `rt_push_checks` профиля; она сверяется с его выводом, потому что объявление
19
+ * без исполнения — та же дыра. Исключение — причина, по которой шага в гейте
20
+ * нет; пустая причина исключением не считается: она единственное, чем
21
+ * постоянная дыра отличается от забытой строки.
22
+ *
23
+ * Дерево без файла конвейера сверки не получает: проверка, падающая там, где
24
+ * конвейера нет, отбивала бы работу вместо промаха.
25
+ *
26
+ * Ненулевой код возврата и перечень расхождений.
27
+ */
28
+ import { execFileSync } from 'node:child_process';
29
+ import { existsSync, readFileSync } from 'node:fs';
30
+ import { join } from 'node:path';
31
+
32
+ import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
33
+
34
+ const GATE = CONFIG.pushGate ?? {};
35
+ const PIPELINE = GATE.pipelineFile ?? '';
36
+ const DECLARED = GATE.steps ?? {};
37
+
38
+ /**
39
+ * Профиль дерева ищется той же цепочкой, что и у гарда пуша: умолчание пакета, поверх него
40
+ * надстройка проекта. Разойдись они — проверка судила бы не тот набор, который гоняет гард.
41
+ */
42
+ const PROFILES = ['.claude/rt-kit/defaults/project.sh', '.claude/rt-kit/project.sh'];
43
+
44
+ if (!PIPELINE || !existsSync(join(ROOT, PIPELINE))) {
45
+ console.log('check-push-gate: пропущено — файла конвейера в дереве нет');
46
+ process.exit(0);
47
+ }
48
+
49
+ /**
50
+ * Имена шагов конвейера. Образец задаёт дерево: у каждого хостинга своя форма записи, и
51
+ * угадывать её за все три пакет не берётся. Умолчание — форма GitHub Actions.
52
+ */
53
+ const stepPattern = new RegExp(GATE.stepPattern ?? '^\\s*-\\s*name:\\s*(.+?)\\s*$');
54
+
55
+ function pipelineSteps() {
56
+ return readFileSync(join(ROOT, PIPELINE), 'utf8')
57
+ .split('\n')
58
+ .map((line) => line.match(stepPattern)?.[1])
59
+ .filter(Boolean)
60
+ .map((name) => name.replace(/^['"]|['"]$/g, ''));
61
+ }
62
+
63
+ /**
64
+ * Набор гейта берётся у профиля его же оболочкой, а не переписывается сюда: два списка одного
65
+ * набора расходятся молча, и расходиться начинают в тот день, когда правят один из них.
66
+ *
67
+ * Профиля нет, функции в нём нет или оболочка отказала — набор неизвестен, и тогда проверка
68
+ * судит только полноту объявлений: сверять строки не с чем.
69
+ */
70
+ function gateChecks() {
71
+ const present = PROFILES.filter((path) => existsSync(join(ROOT, path)));
72
+ if (present.length === 0) {
73
+ return null;
74
+ }
75
+ const script = `${present.map((path) => `. "${path}"`).join('\n')}
76
+ command -v rt_push_checks >/dev/null 2>&1 || exit 42
77
+ rt_push_checks ""`;
78
+ try {
79
+ return execFileSync('bash', ['-c', script], { cwd: ROOT, encoding: 'utf8' })
80
+ .split('\n')
81
+ .map((line) => line.trim())
82
+ .filter(Boolean);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ const steps = pipelineSteps();
89
+ const checks = gateChecks();
90
+ const problems = [];
91
+
92
+ for (const step of steps) {
93
+ const declaration = DECLARED[step];
94
+
95
+ if (declaration === undefined) {
96
+ problems.push(
97
+ `шаг конвейера «${step}» не объявлен: закрой его строкой набора в pushGate.steps ` + 'либо объяви исключением с причиной'
98
+ );
99
+ continue;
100
+ }
101
+
102
+ if (typeof declaration === 'object' && declaration !== null) {
103
+ const reason = String(declaration.skip ?? '').trim();
104
+ if (!reason) {
105
+ problems.push(`шаг конвейера «${step}» объявлен исключением без причины — пустая причина не считается`);
106
+ }
107
+ continue;
108
+ }
109
+
110
+ const line = String(declaration).trim();
111
+ if (!line) {
112
+ problems.push(`шаг конвейера «${step}» объявлен пустой строкой — назови команду либо объяви исключение`);
113
+ continue;
114
+ }
115
+
116
+ if (checks && !checks.some((check) => check.includes(line))) {
117
+ problems.push(`шаг конвейера «${step}» объявлен строкой «${line}», а набор гейта её не печатает`);
118
+ }
119
+ }
120
+
121
+ /** Строка о шаге, которого в конвейере нет, — устаревшая: иначе объявления копят мёртвое. */
122
+ const known = new Set(steps);
123
+ for (const step of Object.keys(DECLARED)) {
124
+ if (!known.has(step)) {
125
+ problems.push(`объявление «${step}» устарело — такого шага в ${PIPELINE} нет`);
126
+ }
127
+ }
128
+
129
+ if (problems.length > 0) {
130
+ console.error(`check-push-gate: расхождений ${problems.length}\n`);
131
+ problems.forEach((problem) => console.error(` ${problem}`));
132
+ console.error('\nНабор гейта пуша не бывает уже набора конвейера — правило поставки.');
133
+ process.exit(1);
134
+ }
135
+
136
+ const skipped = Object.values(DECLARED).filter((value) => typeof value === 'object' && value !== null).length;
137
+ console.log(
138
+ `check-push-gate: шагов конвейера ${steps.length}, закрыто набором ${steps.length - skipped}, ` + `объявлено исключениями ${skipped}`
139
+ );
@@ -53,6 +53,16 @@ const DEFAULTS = {
53
53
  tasksDir: 'docs/tasks',
54
54
  /** Куда сложены списки принятых долгов. */
55
55
  allowlistDir: 'tools',
56
+ /**
57
+ * Каталоги, которые переписывает генератор целиком: контракт, клиент хранилища, разложенное
58
+ * из пакета. Спорить с генератором о длине файла и о повторе некому.
59
+ */
60
+ generatedDirs: [],
61
+ /**
62
+ * Предел длины файла — одно число на все роды: своё число каждому роду означает спор о
63
+ * числе на каждой правке, а не о длине файла.
64
+ */
65
+ fileSizeLimit: 500,
56
66
  /** Корни сквозных тестов; пусто — их в дереве нет. */
57
67
  e2eRoots: ['apps/site-e2e', 'apps/admin-e2e'],
58
68
  /** Корни бэкенда: у него нет ни компонентов, ни шаблонов, и часть признаков к нему не применяется. */
@@ -74,6 +84,17 @@ const DEFAULTS = {
74
84
  * не считается никогда.
75
85
  */
76
86
  productionMarks: [],
87
+ /**
88
+ * Гейт пуша против конвейера. `pipelineFile` — файл, из которого берутся имена шагов;
89
+ * пусто или файла нет — сверки нет вовсе. `stepPattern` — чем имя шага оттуда достаётся;
90
+ * умолчание записано под GitHub Actions. `steps` — чем каждое имя закрыто: строка набора
91
+ * либо `{ "skip": "<причина>" }`. Необъявленный шаг отбивает пуш.
92
+ */
93
+ pushGate: {
94
+ pipelineFile: '',
95
+ stepPattern: '^\\s*-\\s*name:\\s*(.+?)\\s*$',
96
+ steps: {},
97
+ },
77
98
  /** Очередь работ: владелец, репозиторий, борда и учётная запись машинной работы. */
78
99
  board: {
79
100
  owner: '',
@@ -15,32 +15,21 @@
15
15
  #
16
16
  # Порядок веток решает: первое совпадение выигрывает, поэтому частное идёт раньше общего.
17
17
 
18
- # Правила, которые вступают не от рода файла, а от того, что в него пишут.
19
- #
20
- # Обращение к среде исполнения приходит в обычный сервис, а число-настройка и перечисление —
21
- # в обычный класс: по имени файла ни то ни другое не видно, и правило, требуемое только по
22
- # расширению, здесь молчало бы.
23
- skill_for_written() {
24
- target="$1"
25
- written="$2"
26
-
27
- [ -z "$written" ] && return 0
28
-
29
- case "$target" in
30
- *.spec.ts | */docs/* | *.md) return 0 ;;
31
- esac
32
-
33
- printf '%s' "$written" | grep -qE '(globalThis|window\.|document\.defaultView|PLATFORM_ID|isPlatformBrowser|localStorage|sessionStorage)' \
34
- && printf '%s\n' 'platform-access'
18
+ # Правила, вступающие не от рода файла, а от того, что в него пишут, здесь не выбираются:
19
+ # они приходят слоем поверх доменного — `hooks/skill-gate-layers.sh`. Карта судит путь, слой
20
+ # судит текст, и оба зовутся из гейта в одной оболочке.
35
21
 
36
- case "$target" in
37
- *.ts)
38
- printf '%s' "$written" | grep -qE '^[[:space:]]*(export[[:space:]]+)?(const[[:space:]]+[A-Z][A-Z0-9_]*[[:space:]]*(:[^=]*)?=[[:space:]]*-?[0-9]|enum[[:space:]])' \
39
- && printf '%s\n' 'shared-code'
40
- ;;
41
- esac
42
-
43
- return 0
22
+ # Команда считается ВЫЗОВОМ, только когда стоит в начале строки или сразу за разделителем.
23
+ # Совпадение по подстроке ловит любое УПОМИНАНИЕ: строка о коммите в теле самого коммита и поиск
24
+ # по истории отбивались как настоящий коммит.
25
+ #
26
+ # `([A-Za-z_]…=…[[:space:]]+)*` — переменные окружения перед вызовом: адрес хранилища ставят
27
+ # приставкой самой команды, и без этого куска вызов не опознавался вовсе. `(npx…)?` — запуск
28
+ # через раннер пакетов, `([^[:space:]]*/)?` — путь до исполняемого файла. Многострочную команду
29
+ # поиск разбирает построчно, поэтому начало строки — начало каждой.
30
+ rt_gate_invokes() {
31
+ printf '%s\n' "$1" \
32
+ | grep -qE "(^|[;&|(])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*((npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+)?([^[:space:]]*/)?$2([[:space:]]|$)"
44
33
  }
45
34
 
46
35
  skill_for_default() {
@@ -81,6 +70,37 @@ skill_for_default() {
81
70
  # строк комментария стоят захода, а второе прочитанное правило не пригождается.
82
71
  */tools/check-dupes.mjs | */tools/dupes-allowlist.json) printf '%s\n' 'shared-code' ;;
83
72
 
73
+ # Остальные проверки — то же самое: проверка исполняет утверждения своего
74
+ # правила, и признаки, по которым она судит, объявлены у него в привязке. Правя
75
+ # признак в проверке, второе место открывают рядом — иначе они расходятся молча,
76
+ # и проверка числит отказом то, что правило разрешает.
77
+ */check-specs.mjs) printf '%s\n' 'spec-driven' ;;
78
+ */check-doc-paths.mjs | */doc-paths-allowlist.json | */check-file-size.mjs)
79
+ printf '%s\n' 'doc-style' ;;
80
+ */check-styles.mjs | */styles-allowlist.json | */stylelint-rules/*)
81
+ printf '%s\n' 'styling-bem' ;;
82
+ */check-lib-layers.mjs | */lib-layers-allowlist.json) printf '%s\n' 'lib-layers' ;;
83
+ */check-reuse.mjs | */reuse-allowlist.json) printf '%s\n' 'reuse-first' ;;
84
+ */check-board.mjs | */board.mjs | */task-new.mjs | */check-schema-drift.mjs)
85
+ printf '%s\n' 'git-workflow' ;;
86
+ # Своё правило линтера кода пишется по тем же соглашениям, что и код под ним.
87
+ */eslint-rules/*) printf '%s\n' 'typescript-conventions' ;;
88
+
89
+ # Схема хранилища и её миграции: порядок каталогов лексикографический, а метку
90
+ # времени ставит инструмент в момент заведения — цепочка ломается молча и падает
91
+ # только накатом с нуля, то есть уже после слияния. Правило живёт при поставке.
92
+ */schema.prisma | */prisma/migrations/*) printf '%s\n' 'git-workflow' ;;
93
+ # Конвейер и образ: проверки решают, что вообще гоняется до слияния, а образ —
94
+ # что приезжает на прод. И то и другое правилось без единого правила поставки.
95
+ */.github/workflows/*.yml | */.gitlab-ci.yml | */azure-pipelines*.yml)
96
+ printf '%s\n' 'git-workflow' ;;
97
+ */Dockerfile | */*.Dockerfile | */docker-compose*.yml | */docker-compose*.yaml)
98
+ printf '%s\n' 'git-workflow' ;;
99
+
100
+ # Сквозная спека проверяет поднятое приложение, а не класс: по имени файла она от
101
+ # обычного модуля не отличается, и без этой ветки уходила бы в соглашения языка.
102
+ *-e2e/*) printf '%s\n' 'testing' ;;
103
+
84
104
  # Поставка: состав зависимостей — это то, что приезжает на прод. Правка
85
105
  # скриптов зависимостью не является, и правило про версии на неё не вступает.
86
106
  # Оговорка: удаление зависимости приходит правкой без номера версии и сюда не
@@ -105,18 +125,54 @@ skill_for_default() {
105
125
 
106
126
  *.ts) printf '%s\n' 'typescript-conventions' ;;
107
127
  esac
108
- skill_for_written "$target" "$written"
109
128
  ;;
110
129
  bash)
111
- case "$target" in
112
- *git\ commit* | *git\ push* | *git\ merge* | *git\ rebase* | *git\ cherry-pick* | *gh\ pr\ * | *glab\ mr\ * | *az\ repos\ *)
113
- printf '%s\n' 'git-workflow' ;;
114
- *git\ worktree\ add* | *git\ worktree\ remove*)
115
- printf '%s\n' 'git-workflow' ;;
116
- *prisma\ migrate* | *prisma\ db\ *) printf '%s\n' 'git-workflow' ;;
117
- *curl\ *localhost* | *wget\ *localhost*) printf '%s\n' 'browser-verification' ;;
118
- esac
130
+ # Ветки идут проверкой на вызов, а не совпадением по подстроке: упоминание команды
131
+ # командой не является, и гейт отбивал собственный текст о коммите.
132
+ if rt_gate_invokes "$target" "git[[:space:]]+(commit|push|merge|rebase|cherry-pick)" \
133
+ || rt_gate_invokes "$target" "git[[:space:]]+worktree[[:space:]]+(add|remove)" \
134
+ || rt_gate_invokes "$target" "git[[:space:]]+checkout[[:space:]]+-b" \
135
+ || rt_gate_invokes "$target" "git[[:space:]]+switch[[:space:]]+-c" \
136
+ || rt_gate_invokes "$target" "(gh|glab)[[:space:]]+(pr|mr|issue)[[:space:]]+(create|merge|edit)" \
137
+ || rt_gate_invokes "$target" "az[[:space:]]+(repos|boards)" \
138
+ || rt_gate_invokes "$target" "[^[:space:]]*task:new" \
139
+ || rt_gate_invokes "$target" "prisma[[:space:]]+(migrate|db)"; then
140
+ printf '%s\n' 'git-workflow'
141
+ # Правка тела отчёта через клиент хостинга ловится двумя признаками сразу — вызовом
142
+ # клиента И адресом запроса: одного слова о заявке мало, оно попадает в строку любой
143
+ # команды, которая о ней пишет. Тело отчёта не читает ни одна проверка, и утверждение
144
+ # о дереве стареет в нём молча.
145
+ elif rt_gate_invokes "$target" "(gh|glab)[[:space:]]+api" \
146
+ && printf '%s' "$target" | grep -qE '(-X|--method)[[:space:]]+(PATCH|PUT).*(pulls|merge_requests)/[0-9]+'; then
147
+ printf '%s\n' 'git-workflow'
148
+ # Образы и реестр на машине владельца: там же лежат его собственные стенды и работы
149
+ # других его веток. Снятие и чистка важнее сборки — они уносят чужое безвозвратно.
150
+ # Команды чтения остаются вне гейта: ими нехватку места и разбирают, и требовать на
151
+ # них правило значило бы отбивать сам приём. Поэтому общая чистка ловится с `prune`.
152
+ elif rt_gate_invokes "$target" "docker[[:space:]]+(build|buildx|pull|push|run|compose|login|rm|rmi|stop|start|restart|image|volume|builder|network)" \
153
+ || rt_gate_invokes "$target" "docker[[:space:]]+system[[:space:]]+prune"; then
154
+ printf '%s\n' 'git-workflow'
155
+ fi
156
+
157
+ # Слияние отчёта — последний момент, когда папку закрытой задачи ещё можно разобрать
158
+ # тем же отчётом: после слияния сверка очереди её видит, а отвечать за неё уже
159
+ # некому. Требуется ВТОРЫМ слоем, дополнительно к правилу поставки.
160
+ rt_gate_invokes "$target" "(gh[[:space:]]+pr|glab[[:space:]]+mr)[[:space:]]+merge" \
161
+ && printf '%s\n' 'task-flow'
162
+
163
+ # Обращение к поднятому приложению: врёт здесь не код, а то, что отвечает на порту.
164
+ # Ответ сборки прошлого захода неотличим от ответа живой ветки. Нужны оба признака —
165
+ # вызов клиента И адрес: одного адреса мало, он попадает в строку любой команды,
166
+ # которая о нём пишет, и гейт отбивал проверку самого гейта. Порт не перечисляется:
167
+ # свой разовый стенд поднимается на любом свободном.
168
+ if printf '%s' "$target" | grep -qE '(localhost|127\.0\.0\.1):[0-9]{4,5}' \
169
+ && { rt_gate_invokes "$target" curl || rt_gate_invokes "$target" wget; }; then
170
+ printf '%s\n' 'browser-verification'
171
+ fi
119
172
  ;;
173
+ # Проверка через браузер — единственная область, где правило нужно не под правку файла, а
174
+ # под инструмент: врут там не файлы, а стенд и координаты.
175
+ browser) printf '%s\n' 'browser-verification' ;;
120
176
  esac
121
177
 
122
178
  return 0
@@ -25,19 +25,42 @@ rt_runner() {
25
25
  # Порты у каждого дерева свои, поэтому умолчание молчит: назвать чужой порт хуже, чем не назвать.
26
26
  RT_STANDS="${RT_STANDS:-}"
27
27
 
28
+ # Где лежат проверки дерева и набор сценариев его гардов. Проверку, которой в дереве нет, гейт
29
+ # пуша не зовёт: список печатается по тому, что лежит на диске.
30
+ RT_CHECKS_DIR="${RT_CHECKS_DIR:-tools}"
31
+ RT_HOOKS_TESTS="${RT_HOOKS_TESTS:-.claude/hooks/tests/run.sh}"
32
+
28
33
  # Команды, которые обязаны пройти перед пушем. По одной на строку; первая упавшая отбивает пуш.
29
34
  # Линтер стилей отдельной строкой: линтер кода файлы стилей не читает вовсе.
30
35
  #
31
36
  # Первый параметр — база: ветка, относительно которой считается вклад. Пустая означает, что
32
37
  # удалённого нет, и тогда гоняется всё: набор строже нужного безопасен, набор уже нужного — нет.
38
+ #
39
+ # Сборка идёт наравне с линтом и спеками. Линтер типов не читает, а спеки читают только то, что
40
+ # кто-то ввёз в них импортом: ошибка типов в непокрытом коде доживает до сборки образа, то есть
41
+ # до слияния. Стоит это мало — дальше работает кэш прогонщика.
42
+ #
43
+ # Заведённая проверка встаёт сюда, а не только в общий прогон, который никто не зовёт сам:
44
+ # новая строка в её списке известного уезжает в главную ветку молча, а список при этом читается
45
+ # как действующая охрана.
33
46
  rt_push_checks_default() {
34
47
  runner="$(rt_runner)"
48
+ root="${CLAUDE_PROJECT_DIR:-.}"
35
49
  if [ -n "$1" ]; then
36
- printf '%s\n' "$runner nx affected -t lint test --base=$1"
50
+ printf '%s\n' "$runner nx affected -t lint test build --base=$1"
37
51
  else
38
- printf '%s\n' "$runner nx run-many -t lint test --all"
52
+ printf '%s\n' "$runner nx run-many -t lint test build --all"
39
53
  fi
40
- [ -f "${CLAUDE_PROJECT_DIR:-.}/stylelint.config.js" ] && printf '%s\n' "$runner stylelint \"**/*.scss\" --max-warnings 0"
54
+ [ -f "$root/stylelint.config.js" ] && printf '%s\n' "$runner stylelint \"**/*.scss\" --max-warnings 0"
55
+
56
+ # Сценарии гардов — такой же код, как всё остальное: на них держится и разбор ветки, и
57
+ # уверенность, что обвязка ещё работает. Прогон занимает секунды: он ничего не собирает.
58
+ [ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
59
+
60
+ for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
61
+ check-lib-layers check-reuse check-schema-drift check-push-gate; do
62
+ [ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
63
+ done
41
64
 
42
65
  return 0
43
66
  }