@rt-tools/agent-kit 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/checks/board-long-work.github.mjs +101 -0
- package/assets/checks/board-runs.github.mjs +34 -0
- package/assets/checks/check-board.github.mjs +16 -2
- package/assets/checks/check-reuse.mjs +4 -1
- package/assets/checks/check-schema-drift.mjs +65 -6
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/signals.mjs +41 -1
- package/assets/commands/next-session.md +16 -5
- package/assets/defaults/project.sh +16 -0
- package/assets/hooks/browser-guard-no-asking.sh +5 -1
- package/assets/hooks/git-guard-delivery.sh +15 -11
- package/assets/hooks/git-guard-push-tests.sh +22 -0
- package/assets/hooks/glossary-load.sh +23 -2
- package/assets/hooks/grill-gate.sh +62 -0
- package/assets/laws/autonomous-work.md +30 -0
- package/assets/laws/project-documentation.md +8 -0
- package/assets/patterns/autonomous-work-run.md +105 -0
- package/assets/patterns/browser-verification-stand.md +1 -1
- package/assets/patterns/doc-style-write.md +16 -0
- package/assets/patterns/git-workflow-pr-ready.md +93 -0
- package/assets/patterns/git-workflow-pr.azure.md +1 -1
- package/assets/patterns/git-workflow-pr.github.md +1 -1
- package/assets/patterns/git-workflow-pr.gitlab.md +1 -1
- package/assets/patterns/task-flow-start.md +47 -47
- package/assets/patterns/ts-procedure.md +3 -2
- package/assets/rules/autonomous-work.md +92 -0
- package/assets/rules/browser-verification.md +6 -0
- package/assets/rules/deploy-flow.azure.md +7 -0
- package/assets/rules/deploy-flow.github.md +7 -0
- package/assets/rules/deploy-flow.gitlab.md +7 -0
- package/assets/rules/doc-style.md +18 -0
- package/assets/rules/git-workflow.azure.md +8 -0
- package/assets/rules/git-workflow.github.md +44 -45
- package/assets/rules/git-workflow.gitlab.md +8 -0
- package/assets/rules/reuse-first.md +17 -3
- package/assets/rules/task-flow.md +62 -63
- package/assets/skills/agent-kit.md +20 -20
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +6 -0
- package/lib/commands.js.map +1 -1
- package/lib/push-gate.d.ts +14 -0
- package/lib/push-gate.d.ts.map +1 -0
- package/lib/push-gate.js +93 -0
- package/lib/push-gate.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.23.0.tgz +0 -0
- package/assets/laws/application/access.md +0 -34
- package/assets/laws/application/locales.md +0 -33
- package/assets/laws/application/search-visibility.md +0 -24
- package/assets/patterns/permissions-procedure.md +0 -71
- package/assets/patterns/seo-page.md +0 -104
- package/assets/patterns/seo-verify.md +0 -83
- package/assets/patterns/translations-content.md +0 -107
- package/assets/patterns/translations-key.md +0 -64
- package/assets/rules/permissions.md +0 -116
- package/assets/rules/seo.md +0 -139
- package/assets/rules/translations.md +0 -96
- package/rt-tools-agent-kit-0.22.0.tgz +0 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Работа, которую одним заходом не закрыть: метка карточки против записи в линии работ.
|
|
3
|
+
*
|
|
4
|
+
* Своим файлом по той же причине, что и связь с эпиком: сверка очереди работ и без них стоит у
|
|
5
|
+
* предела длины, а читают эти проверки порознь.
|
|
6
|
+
*
|
|
7
|
+
* Помечена такая работа в двух местах, и одна метка без другой лжёт молча: исполнитель открывает
|
|
8
|
+
* карточку раньше, чем линию работ, а планирует по линии. Карточка без записи обещает
|
|
9
|
+
* многозаходность, которой линия не знает; запись без метки оставляет карточку выглядеть работой
|
|
10
|
+
* на один заход — и следующий заход берёт её, рассчитывая закрыть за раз.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { TASK_KEY } from './board.mjs';
|
|
16
|
+
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Метка многозаходной карточки и каталог линий работ. Не названо любое из двух — связь не
|
|
20
|
+
* судится вовсе: отличить многозаходную карточку от обычной станет нечем, а каталог линий у
|
|
21
|
+
* каждого дерева свой.
|
|
22
|
+
*/
|
|
23
|
+
const LONG_LABEL = CONFIG.longWork?.label ?? '';
|
|
24
|
+
const PLANS_DIR = CONFIG.longWork?.plansDir ?? '';
|
|
25
|
+
|
|
26
|
+
/** Строки линий работ, где стоит слово метки: только они считаются записью о многозаходности. */
|
|
27
|
+
function markedRows() {
|
|
28
|
+
const dir = join(ROOT, PLANS_DIR);
|
|
29
|
+
if (!existsSync(dir)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rows = [];
|
|
34
|
+
for (const name of readdirSync(dir).filter((one) => one.endsWith('.md'))) {
|
|
35
|
+
for (const line of readFileSync(join(dir, name), 'utf8').split('\n')) {
|
|
36
|
+
if (line.includes(LONG_LABEL)) {
|
|
37
|
+
rows.push({ file: name, line });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Связь метки с линией работ, прочитанная в обе стороны.
|
|
47
|
+
*
|
|
48
|
+
* Записью считается строка, где стоят и слово метки, и номер задачи. Голое упоминание номера не
|
|
49
|
+
* годится: линия работ называет все свои задачи, и большинство из них однозаходные — обратная
|
|
50
|
+
* сторона краснела бы на каждой.
|
|
51
|
+
*
|
|
52
|
+
* Судится только открытое, как и вся остальная сверка: закрытая задача — история, и строку о ней
|
|
53
|
+
* нечем закрыть.
|
|
54
|
+
*/
|
|
55
|
+
export function checkLongWork(open, report) {
|
|
56
|
+
if (!LONG_LABEL || !PLANS_DIR) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rows = markedRows();
|
|
61
|
+
if (rows === null) {
|
|
62
|
+
report(`каталога линий работ «${PLANS_DIR}» нет на диске — многозаходную работу сверять не с чем`);
|
|
63
|
+
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Номера, названные в помеченных строках. Ключ задачи и решётка читаются оба: линия пишется
|
|
68
|
+
// человеком, и форма номера в ней от строки к строке разная.
|
|
69
|
+
const written = new Set();
|
|
70
|
+
const whereWritten = new Map();
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
for (const match of row.line.matchAll(new RegExp(`(?:#|${TASK_KEY}-)(\\d+)`, 'g'))) {
|
|
73
|
+
const number = Number(match[1]);
|
|
74
|
+
written.add(number);
|
|
75
|
+
if (!whereWritten.has(number)) {
|
|
76
|
+
whereWritten.set(number, row.file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const labelled = new Set(
|
|
82
|
+
open.filter((issue) => (issue.labels ?? []).some((label) => label.name === LONG_LABEL)).map((issue) => issue.number)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const number of labelled) {
|
|
86
|
+
if (!written.has(number)) {
|
|
87
|
+
report(
|
|
88
|
+
`#${number}: помечена как «${LONG_LABEL}», а в линиях работ «${PLANS_DIR}» такой строки нет — планируют по линии, а не по карточке`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Обратная сторона: линия знает работу многозаходной, а карточка выглядит работой на один
|
|
94
|
+
// заход — и следующий заход берёт её, рассчитывая закрыть за раз.
|
|
95
|
+
const byNumber = new Set(open.map((issue) => issue.number));
|
|
96
|
+
for (const number of written) {
|
|
97
|
+
if (byNumber.has(number) && !labelled.has(number)) {
|
|
98
|
+
report(`#${number}: линия работ «${whereWritten.get(number)}» знает её многозаходной, а метки «${LONG_LABEL}» на карточке нет`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -65,6 +65,40 @@ export function deployLag(workflow, mainBranch, options) {
|
|
|
65
65
|
return { sha: run.sha, at: run.at, behind: Number(String(behind).trim()) };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Чем кончилась последняя выкатка: `success`, `failure`, `running` либо `none`, если её не
|
|
70
|
+
* запускали ни разу.
|
|
71
|
+
*
|
|
72
|
+
* Спрашивается отдельно от отставания прода. Отставание считается по последней УСПЕШНОЙ выкатке,
|
|
73
|
+
* и две разные беды выглядят через него одинаково: выкатку не запускали и выкатка упала. Ведут
|
|
74
|
+
* они к разному — первую запускают, вторую читают журналом и чинят, — а четыре слияния подряд
|
|
75
|
+
* уехали поверх поломки, которую принёс первый, и прод простоял почти два часа.
|
|
76
|
+
*
|
|
77
|
+
* Идущая выкатка расхождением не считается: она ещё может кончиться успехом.
|
|
78
|
+
*/
|
|
79
|
+
export function lastDeploy(workflow, options) {
|
|
80
|
+
const runs = gh(
|
|
81
|
+
[
|
|
82
|
+
'api',
|
|
83
|
+
`repos/${OWNER}/${REPO}/actions/workflows/${encodeURIComponent(workflow)}/runs?per_page=1`,
|
|
84
|
+
'--jq',
|
|
85
|
+
'[.workflow_runs[] | {status, conclusion, sha: .head_sha, at: .created_at, url: .html_url}] | first // empty',
|
|
86
|
+
],
|
|
87
|
+
options
|
|
88
|
+
);
|
|
89
|
+
const last = String(runs).trim();
|
|
90
|
+
if (!last) {
|
|
91
|
+
return { verdict: 'none' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const run = JSON.parse(last);
|
|
95
|
+
if (run.status !== 'completed') {
|
|
96
|
+
return { verdict: 'running', ...run };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { verdict: run.conclusion === 'success' ? 'success' : 'failure', ...run };
|
|
100
|
+
}
|
|
101
|
+
|
|
68
102
|
/**
|
|
69
103
|
* Чем кончились прогоны на этой вершине: `success`, если все завершились успехом, `running`,
|
|
70
104
|
* если хоть один ещё идёт, `failure` — если хоть один упал. Прогонов нет вовсе — `none`.
|
|
@@ -50,7 +50,8 @@ import {
|
|
|
50
50
|
taskDirs,
|
|
51
51
|
} from './board.mjs';
|
|
52
52
|
import { onlyIgnoredPaths } from './board-paths.mjs';
|
|
53
|
-
import {
|
|
53
|
+
import { checkLongWork } from './board-long-work.mjs';
|
|
54
|
+
import { HAS_PIPELINE, deployLag, evictedOnHead, headCommittedAt, lastDeploy, runsOnHead, verdictOnHead } from './board-runs.mjs';
|
|
54
55
|
import { checkEpicLinks } from './board-epics.mjs';
|
|
55
56
|
import { similarTitles } from './board-titles.mjs';
|
|
56
57
|
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
@@ -317,6 +318,7 @@ try {
|
|
|
317
318
|
|
|
318
319
|
similarTitles(open);
|
|
319
320
|
checkEpicLinks(open, report);
|
|
321
|
+
checkLongWork(open, report);
|
|
320
322
|
|
|
321
323
|
const openNumbers = new Set(open.map((issue) => issue.number));
|
|
322
324
|
const claimed = new Map();
|
|
@@ -415,7 +417,8 @@ try {
|
|
|
415
417
|
// сколько угодно коммитов, и заметить это неоткуда.
|
|
416
418
|
if (!offline && DEPLOY_WORKFLOW) {
|
|
417
419
|
try {
|
|
418
|
-
const
|
|
420
|
+
const token = botToken() ?? undefined;
|
|
421
|
+
const lag = deployLag(DEPLOY_WORKFLOW, MAIN_BRANCH, { token });
|
|
419
422
|
if (lag === null) {
|
|
420
423
|
report(`выкаток по «${DEPLOY_WORKFLOW}» не было ни одной — сравнить прод не с чем`);
|
|
421
424
|
} else if (lag.behind > 0) {
|
|
@@ -423,6 +426,17 @@ if (!offline && DEPLOY_WORKFLOW) {
|
|
|
423
426
|
`прод отстал от «${MAIN_BRANCH}» на ${lag.behind} коммитов: последняя выкатка — ${lag.sha.slice(0, 8)} от ${String(lag.at).slice(0, 10)}`
|
|
424
427
|
);
|
|
425
428
|
}
|
|
429
|
+
|
|
430
|
+
// Отчего прод отстал, отставание не говорит: оно считается по последней УСПЕШНОЙ
|
|
431
|
+
// выкатке, и «не запускали» с «упала» выглядят через него одинаково. Ведут они к
|
|
432
|
+
// разному — первую запускают, вторую читают журналом и чинят.
|
|
433
|
+
const deploy = lastDeploy(DEPLOY_WORKFLOW, { token });
|
|
434
|
+
if (deploy.verdict === 'failure') {
|
|
435
|
+
report(
|
|
436
|
+
`выкатка «${DEPLOY_WORKFLOW}» упала на ${String(deploy.sha).slice(0, 8)} от ${String(deploy.at).slice(0, 10)}: ` +
|
|
437
|
+
`главная ветка впереди прода, и слияния поверх уедут туда же — ${deploy.url}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
426
440
|
} catch (error) {
|
|
427
441
|
if (error instanceof OfflineError) {
|
|
428
442
|
console.log(`check-board: прод не сверялся — ${error.message}`);
|
|
@@ -137,7 +137,10 @@ if (process.argv.includes('--baseline')) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
const problems = [
|
|
140
|
-
|
|
140
|
+
// Готовое названо оговоркой намеренно: признак судит тег, а место показа — нет. Замена своего
|
|
141
|
+
// абзаца отказа на готовый компонент того же экрана закрывает признак и оставляет нарушение
|
|
142
|
+
// на месте — так починили тринадцать шаблонов сразу. Строка вывода — измерение, а не задание.
|
|
143
|
+
...fresh.map((finding) => `${finding.key} — готовое (если место показа верно): ${finding.instead}`),
|
|
141
144
|
...stale.map((key) => `${key}: значится в ${ALLOWLIST}, а в дереве такого расхождения больше нет — строку поправить или убрать`),
|
|
142
145
|
];
|
|
143
146
|
|
|
@@ -83,6 +83,43 @@ function shadowAddresses(url) {
|
|
|
83
83
|
// которой нечего смотреть, поломкой не является.
|
|
84
84
|
const SKIP = Number(process.env.RT_SKIP_CODE ?? 7);
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Тронула ли ветка хранилище — схему либо каталог миграций.
|
|
88
|
+
*
|
|
89
|
+
* Пропуск проверки законен ровно до этой черты. База на машине разработчика бывает погашена
|
|
90
|
+
* буднично, и отбивать за это пуш документации не за что; но ветка, правившая миграции, без
|
|
91
|
+
* прогона цепочки уезжает в главную вслепую — и падает не у неё, а на выкатке. Так и упал прод:
|
|
92
|
+
* пять полей появились в схеме без миграций, часть страниц стала отвечать «не найдено», полчаса
|
|
93
|
+
* недоступности, чинили откатом. Гейт при этом был зелёным: проверка вернула код пропуска.
|
|
94
|
+
*
|
|
95
|
+
* Смотрятся обе стороны — незакоммиченное в рабочем дереве и вклад ветки от главной. Одного
|
|
96
|
+
* вклада мало: правка, ещё не попавшая в коммит, уходит тем же пушем следом.
|
|
97
|
+
*
|
|
98
|
+
* ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет git, нет главной ветки, вызов упал — считается, что не тронула.
|
|
99
|
+
* Проверка, отбивающая пуш по своей слепоте, хуже пропуска: чинить в ней нечего.
|
|
100
|
+
*/
|
|
101
|
+
function touchedStorage() {
|
|
102
|
+
const paths = [CONFIG.schemaFile, CONFIG.migrationsDir].filter(Boolean);
|
|
103
|
+
if (!paths.length) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const git = (args) => {
|
|
108
|
+
const out = spawnSync('git', args, { cwd: ROOT, encoding: 'utf8' });
|
|
109
|
+
|
|
110
|
+
return out.status === 0 ? (out.stdout ?? '') : '';
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Главная ветка берётся удалённой ссылкой: локальная — снимок последнего подтягивания, и
|
|
114
|
+
// вклад, посчитанный от неё, врёт ровно в ту сторону, где проверка молчит.
|
|
115
|
+
const main = CONFIG.mainBranch ?? 'main';
|
|
116
|
+
const base = [`origin/${main}`, main].find((ref) => git(['rev-parse', '--verify', '--quiet', ref]).trim()) ?? '';
|
|
117
|
+
|
|
118
|
+
const changed = [git(['status', '--porcelain']), base ? git(['diff', '--name-only', `${base}...HEAD`]) : ''].join('\n');
|
|
119
|
+
|
|
120
|
+
return paths.some((one) => changed.split('\n').some((line) => line.includes(one)));
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
function prisma(args, url) {
|
|
87
124
|
return spawnSync('npx', ['prisma', ...args], {
|
|
88
125
|
cwd: ROOT,
|
|
@@ -103,9 +140,7 @@ async function withServiceClient(serviceUrl, run) {
|
|
|
103
140
|
// Погашенный докер — обычное состояние машины, а не повод не дать запушить
|
|
104
141
|
// документацию.
|
|
105
142
|
if (SERVER_DOWN_CODES.includes(error?.code)) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return SKIP;
|
|
143
|
+
return unavailable('база недоступна');
|
|
109
144
|
}
|
|
110
145
|
throw error;
|
|
111
146
|
}
|
|
@@ -117,6 +152,32 @@ async function withServiceClient(serviceUrl, run) {
|
|
|
117
152
|
}
|
|
118
153
|
}
|
|
119
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Ответ на «проверять негде»: пропуск либо отказ — смотря тронула ли ветка хранилище.
|
|
157
|
+
*
|
|
158
|
+
* Отказ называет, чем поднять базу. Сказанное только «негде» исполнитель читает как разрешение:
|
|
159
|
+
* поднимать её он не обязан, а гейт зелёный.
|
|
160
|
+
*/
|
|
161
|
+
function unavailable(why) {
|
|
162
|
+
if (!touchedStorage()) {
|
|
163
|
+
console.log(`check-schema-drift: ${why} — сверять негде`);
|
|
164
|
+
|
|
165
|
+
return SKIP;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.error(`check-schema-drift: ${why}, а ветка правила схему или миграции — сверять негде, но было чем\n`);
|
|
169
|
+
console.error(
|
|
170
|
+
'Цепочка миграций на пустом хранилище — единственное место, где виден их настоящий порядок:\n' +
|
|
171
|
+
'метку времени ставит момент создания, и миграция из ветки, начатой раньше, встаёт перед той,\n' +
|
|
172
|
+
'от которой зависит. На развёрнутом хранилище разработчика она ложится, на чистом падает — и\n' +
|
|
173
|
+
'видно это в первый раз на выкатке.\n\n' +
|
|
174
|
+
'Подними базу и повтори; когда её под рукой нет, цепочка гоняется на одноразовом контейнере —\n' +
|
|
175
|
+
'готовые команды в паттерне `git-workflow-migration`.'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
120
181
|
async function main() {
|
|
121
182
|
// Незаданное имя спрашивается отдельно от несуществующего файла. Склеенное с корнем, пустое
|
|
122
183
|
// имя даёт сам корень — а он есть всегда, и проверка шла дальше, будто схема на месте.
|
|
@@ -135,9 +196,7 @@ async function main() {
|
|
|
135
196
|
|
|
136
197
|
const url = databaseUrl();
|
|
137
198
|
if (!url) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return SKIP;
|
|
199
|
+
return unavailable('адрес базы не задан');
|
|
141
200
|
}
|
|
142
201
|
|
|
143
202
|
if (PRODUCTION_MARKS.some((mark) => url.includes(mark))) {
|
|
@@ -175,6 +175,19 @@ const DEFAULTS = {
|
|
|
175
175
|
workflow: '',
|
|
176
176
|
mainBranch: 'main',
|
|
177
177
|
},
|
|
178
|
+
/**
|
|
179
|
+
* Работа, которую одним заходом не закрыть. `label` — метка такой карточки; `plansDir` —
|
|
180
|
+
* каталог, где живут линии работ. Метка и запись в линии сверяются в обе стороны: одна без
|
|
181
|
+
* другой лжёт молча, потому что исполнитель открывает карточку раньше, чем линию, а
|
|
182
|
+
* планирует по линии.
|
|
183
|
+
*
|
|
184
|
+
* Не названо любое из двух — связь не судится вовсе: отличить многозаходную карточку от
|
|
185
|
+
* обычной станет нечем, а каталог линий у каждого дерева свой.
|
|
186
|
+
*/
|
|
187
|
+
longWork: {
|
|
188
|
+
label: '',
|
|
189
|
+
plansDir: '',
|
|
190
|
+
},
|
|
178
191
|
/** Очередь работ: владелец, репозиторий, борда и учётная запись машинной работы. */
|
|
179
192
|
board: {
|
|
180
193
|
owner: '',
|
|
@@ -48,6 +48,46 @@ function readBundle(name) {
|
|
|
48
48
|
return parseSignals(readFileSync(path, 'utf8'));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Признак, ищущий нативный тег: `<input\\b`, `<textarea\\b`, `<select\\b`. */
|
|
52
|
+
const NATIVE_TAG = /^<([a-z]+)\\b$/;
|
|
53
|
+
|
|
54
|
+
/** Спецзнаки выражения в имени директивы: имя приходит из настройки дерева, а не из кода. */
|
|
55
|
+
function escaped(name) {
|
|
56
|
+
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Директивы своей дизайн-системы вычёркиваются из признаков нативных тегов.
|
|
61
|
+
*
|
|
62
|
+
* Источник вида в дереве бывает не один: панель владельца собирается набором кита, публичный
|
|
63
|
+
* сайт — своей системой, и закон о единообразии разводит их по приложениям. Директива такой
|
|
64
|
+
* системы стоит на нативном теге ровно так же, как директива кнопки кита, — и признак кнопки её
|
|
65
|
+
* уже вырезает своим `strip`, а признаки поля, области текста и списка не вырезают ничего: форма,
|
|
66
|
+
* собранная из готового своего, числится расхождением целиком, пять полей из пяти.
|
|
67
|
+
*
|
|
68
|
+
* Список директив называет дерево — ключом `reuse.kitDirectives`. Не назвало — не вырезается
|
|
69
|
+
* ничего, и дерево с одним источником вида ведёт себя как прежде.
|
|
70
|
+
*/
|
|
71
|
+
export function withKitDirectives(signals, directives) {
|
|
72
|
+
const names = (directives ?? []).filter(Boolean).map(escaped);
|
|
73
|
+
if (names.length === 0) {
|
|
74
|
+
return signals;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const alternatives = names.join('|');
|
|
78
|
+
|
|
79
|
+
return signals.map((signal) => {
|
|
80
|
+
const tag = NATIVE_TAG.exec(signal.find ?? '')?.[1];
|
|
81
|
+
if (!tag || signal.ext !== '.html') {
|
|
82
|
+
return signal;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const own = `<${tag}\\b[^>]*(?:${alternatives})[^>]*>`;
|
|
86
|
+
|
|
87
|
+
return { ...signal, strip: signal.strip ? `${signal.strip}|${own}` : own };
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
/**
|
|
52
92
|
* Признаки объявленных наборов, а поверх них — свои признаки дерева.
|
|
53
93
|
*
|
|
@@ -84,5 +124,5 @@ export function loadSignals(config, root) {
|
|
|
84
124
|
);
|
|
85
125
|
}
|
|
86
126
|
|
|
87
|
-
return [...byKey.values()];
|
|
127
|
+
return withKitDirectives([...byKey.values()], config.kitDirectives);
|
|
88
128
|
}
|
|
@@ -54,12 +54,19 @@ git merge-base --is-ancestor HEAD "origin/${RT_MAIN_BRANCH:-main}" && echo вл
|
|
|
54
54
|
|
|
55
55
|
## 4. Приведи дерево к главной ветке
|
|
56
56
|
|
|
57
|
-
- **Работа по правилу и PR влит** — задача закрыта, ветка больше не
|
|
57
|
+
- **Работа по правилу и PR влит** — задача закрыта, ветка больше не нужна. Локальная главная
|
|
58
|
+
двигается до удалённой без перехода на неё:
|
|
58
59
|
|
|
59
60
|
```bash
|
|
60
|
-
git
|
|
61
|
+
git fetch origin "${RT_MAIN_BRANCH:-main}:${RT_MAIN_BRANCH:-main}"
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
Рабочее дерево при этом не трогается вовсе, поэтому шаг исполним и тогда, когда в нём лежит
|
|
65
|
+
незакоммиченное — а лежит оно там по слову самой этой команды, велевшей шагом раньше его не
|
|
66
|
+
трогать. Переход на главную нужен, только чтобы там работать; уборке он не нужен, а
|
|
67
|
+
`git pull` на ветке задачи тянет не главную вовсе и отвечает «no such ref was fetched»:
|
|
68
|
+
ветку к этой минуте уже сняли с удалённого слиянием заявки.
|
|
69
|
+
|
|
63
70
|
- **Всё остальное** — работа не кончилась, и ветка остаётся местом, где она продолжится:
|
|
64
71
|
|
|
65
72
|
```bash
|
|
@@ -74,17 +81,21 @@ git merge-base --is-ancestor HEAD "origin/${RT_MAIN_BRANCH:-main}" && echo вл
|
|
|
74
81
|
Снимаются только влитые в главную: их коммиты есть в ней, и восстанавливать нечего.
|
|
75
82
|
|
|
76
83
|
```bash
|
|
77
|
-
git branch --merged "
|
|
84
|
+
git branch --merged "origin/${RT_MAIN_BRANCH:-main}" \
|
|
78
85
|
| grep -vE "^\*|^\s*${RT_MAIN_BRANCH:-main}$" \
|
|
79
86
|
| xargs -r git branch -d
|
|
80
87
|
```
|
|
81
88
|
|
|
89
|
+
Влитость судится от удалённой ссылки, а не от локальной главной: локальная отстаёт молча, и
|
|
90
|
+
влитая ветка при ней числится невлитой. Три ветки так показались несущими по два коммита мимо
|
|
91
|
+
главной, а мимо удалённой у них было ноль — заход кончился бы списком невлитого, которого нет.
|
|
92
|
+
|
|
82
93
|
Невлитую ветку **не сноси**. Назови её владельцу вместе с числом коммитов, которых нет в
|
|
83
94
|
главной, — по ним видно, что именно потеряется, если её снести:
|
|
84
95
|
|
|
85
96
|
```bash
|
|
86
|
-
for b in $(git branch --no-merged "
|
|
87
|
-
printf '%s: %s коммитов мимо главной\n' "$b" "$(git rev-list --count "
|
|
97
|
+
for b in $(git branch --no-merged "origin/${RT_MAIN_BRANCH:-main}" --format='%(refname:short)'); do
|
|
98
|
+
printf '%s: %s коммитов мимо главной\n' "$b" "$(git rev-list --count "origin/${RT_MAIN_BRANCH:-main}..$b")"
|
|
88
99
|
done
|
|
89
100
|
```
|
|
90
101
|
|
|
@@ -241,6 +241,21 @@ rt_task_branch_ok_default() {
|
|
|
241
241
|
| grep -qE '^([A-Z]+-)?[0-9]+-[a-z0-9][a-z0-9-]*$|^(feat|fix|refactor|docs|chore|style|perf|test|build|ci)/[0-9]+-[a-z0-9][a-z0-9-]*$'
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
# Номер задачи из имени ветки. Второй вопрос к тому же имени: форма отвечает «годится ли»,
|
|
245
|
+
# а это — «какой он», и свести их в одно нельзя: одна возвращает код, другой строку.
|
|
246
|
+
#
|
|
247
|
+
# Формы те же, что знает проверка формы выше: приставкой стоит либо ключ задач через дефис,
|
|
248
|
+
# либо род правки через косую черту, либо её нет вовсе. Разойдясь с ней, разбор оставит форму
|
|
249
|
+
# законной, а номер пустым — и сверка номера ветки с номером заголовка заявки пропустится молча,
|
|
250
|
+
# выглядя при этом сошедшейся.
|
|
251
|
+
#
|
|
252
|
+
# Хвост после номера здесь не судится намеренно: `RT-9_guest` — это промах в разделителе, а не
|
|
253
|
+
# осознанно беззадачная ветка, и номер из неё вынуть надо, чтобы форму имени было с чем сверить.
|
|
254
|
+
# Требование дефиса оставляло такое имя без номера, и гард пропускал его как ветку под пробу.
|
|
255
|
+
rt_task_branch_number_default() {
|
|
256
|
+
printf '%s' "$1" | sed -nE 's#^([A-Za-z]+[-/])?([0-9]+).*#\2#p'
|
|
257
|
+
}
|
|
258
|
+
|
|
244
259
|
# Адрес боевого хранилища: образец, по которому его узнают в командной строке — порт туннеля,
|
|
245
260
|
# хост, домен. Любая запись по нему отбивается совсем, и опт-аут не действует. Умолчание
|
|
246
261
|
# молчит: чужой адрес здесь опаснее неназванного — он отбивал бы работу с локальной базой.
|
|
@@ -384,6 +399,7 @@ rt_push_docs_only() { rt_push_docs_only_default "$@"; }
|
|
|
384
399
|
rt_docs_pair_for() { rt_docs_pair_for_default "$@"; }
|
|
385
400
|
rt_lint_for() { rt_lint_for_default "$@"; }
|
|
386
401
|
rt_task_branch_ok() { rt_task_branch_ok_default "$@"; }
|
|
402
|
+
rt_task_branch_number() { rt_task_branch_number_default "$@"; }
|
|
387
403
|
rt_reinvented_in() { rt_reinvented_in_default "$@"; }
|
|
388
404
|
rt_is_app_code() { rt_is_app_code_default "$@"; }
|
|
389
405
|
# Каталог источников пакета правил в этом дереве, от корня. Пусто — дерево пакета не везёт, и
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
# being about which Chrome profile to automate. Anchor on selection phrasing instead
|
|
13
13
|
# (deviceId, "which/what browser", "какой браузер").
|
|
14
14
|
#
|
|
15
|
+
# The Russian pronoun is spelled out case by case on purpose: an optional ending would leave a
|
|
16
|
+
# bare "как" in the pattern, and "какие браузеры поддерживаем" — a support question, not a
|
|
17
|
+
# selection one — got denied by exactly that.
|
|
18
|
+
#
|
|
15
19
|
# FAIL-OPEN when browser-device-id.sh prints nothing.
|
|
16
20
|
|
|
17
21
|
# Своё имя в наблюдениях: отбой пишет общий хвост отказа, а не сам гард.
|
|
@@ -29,7 +33,7 @@ device_id="$("${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/browser-device-id.sh" 2>/de
|
|
|
29
33
|
questions="$(printf '%s' "$input" | jq -r '[.tool_input.questions[]? | .question, .header, (.options[]?.label)] | join(" ")' 2>/dev/null)"
|
|
30
34
|
[ -z "$questions" ] && exit 0
|
|
31
35
|
|
|
32
|
-
printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(
|
|
36
|
+
printf '%s' "$questions" | grep -qiE 'deviceid|device id|(which|what|pick|choose|select)[^.]{0,25}\bbrowser\b|\bbrowser\b[^.]{0,25}(profile|to use)|как(ой|ую|им|ого)[^.]{0,15}браузер|брауз[а-я]*[^.]{0,20}(использовать|выбрать|выбор|нужен|запустить)' || exit 0
|
|
33
37
|
|
|
34
38
|
# Общий хвост отказа: два законных хода и законная форма обхода, если она у отказа есть. Файл
|
|
35
39
|
# может быть не разложен — тогда хвоста нет, а причина отказа остаётся прежней.
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#
|
|
26
26
|
# Что здесь чем зовётся, знает профиль дерева:
|
|
27
27
|
# rt_task_branch_ok — форма имени ветки под задачу;
|
|
28
|
+
# rt_task_branch_number — номер задачи из этого имени: приставкой бывает и род правки;
|
|
28
29
|
# RT_TASK_TITLE_RE — форма номера в заголовке заявки;
|
|
29
30
|
# rt_task_state — состояние задачи одним объектом (existsize, open, onBoard, assigned,
|
|
30
31
|
# numbered); молчание значит «спросить некого»;
|
|
@@ -95,6 +96,7 @@ done
|
|
|
95
96
|
[ -f "$rt_hooks_dir/profile-check.sh" ] && . "$rt_hooks_dir/profile-check.sh"
|
|
96
97
|
command -v rt_needs >/dev/null 2>&1 || rt_needs() { command -v "$1" >/dev/null 2>&1; }
|
|
97
98
|
rt_needs rt_task_branch_ok git-guard-delivery || exit 0
|
|
99
|
+
rt_needs rt_task_branch_number git-guard-delivery || exit 0
|
|
98
100
|
|
|
99
101
|
title_re="${RT_TASK_TITLE_RE:-^\[[A-Za-z]+-[0-9]+\][[:space:]]+[^[:space:]]}"
|
|
100
102
|
task_new="${RT_TASK_NEW_CMD:-npm run task:new}"
|
|
@@ -146,10 +148,9 @@ deny() {
|
|
|
146
148
|
exit 0
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
# Несошедшиеся условия копятся и называются
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
# было известно уже на первом. Готовность к поставке — одно состояние, и называется оно целиком.
|
|
151
|
+
# Несошедшиеся условия копятся и называются разом: отказ по первому промаху заставляет чинить их
|
|
152
|
+
# по одному — правка основания, повтор, заголовок, снова повтор, задача, — хотя всё несошедшееся
|
|
153
|
+
# известно уже на первом круге. Готовность к поставке — одно состояние, и называется оно целиком.
|
|
153
154
|
faults=''
|
|
154
155
|
|
|
155
156
|
fault() {
|
|
@@ -163,10 +164,9 @@ fault() {
|
|
|
163
164
|
# shellcheck disable=SC1090
|
|
164
165
|
[ -f "$rt_hooks_dir/git-guard-delivery-folder.sh" ] && . "$rt_hooks_dir/git-guard-delivery-folder.sh" 2>/dev/null
|
|
165
166
|
|
|
166
|
-
# Конфликтующая своя заявка: тот же приём, что у папки и
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
# ярус не судится, а работа идёт дальше.
|
|
167
|
+
# Конфликтующая своя заявка: тот же приём, что у папки и подписи. Зовётся помощник до всех ярусов
|
|
168
|
+
# ниже и судит не готовность этой работы, а право брать следующую: пока отданное конфликтует, его
|
|
169
|
+
# чинят первым действием хода. Нет помощника — ярус не судится, а работа идёт дальше.
|
|
170
170
|
# shellcheck disable=SC1090
|
|
171
171
|
[ -f "$rt_hooks_dir/git-guard-delivery-conflict.sh" ] && . "$rt_hooks_dir/git-guard-delivery-conflict.sh" 2>/dev/null
|
|
172
172
|
command -v rt_delivery_conflict >/dev/null 2>&1 && rt_delivery_conflict
|
|
@@ -225,10 +225,14 @@ fi
|
|
|
225
225
|
if [ -n "$branch_arg" ]; then
|
|
226
226
|
# Имя, притворяющееся веткой под задачу, но не совпадающее с формой, — это промах в имени,
|
|
227
227
|
# а не осознанная беззадачная ветка. Ловится до первого коммита.
|
|
228
|
-
|
|
228
|
+
# Номер вынимает профиль: своя регулярка знала одну приставку — ключ задач, — и ветка вида
|
|
229
|
+
# `feat/88-slug`, законная по форме того же профиля, номера не давала вовсе. Проверка формы
|
|
230
|
+
# при заведении не срабатывала на ней ни разу, а сверка номеров пропускалась молча.
|
|
231
|
+
number_arg="$(rt_task_branch_number "$branch_arg")"
|
|
232
|
+
if [ -n "$number_arg" ]; then
|
|
229
233
|
rt_task_branch_ok "$branch_arg" \
|
|
230
234
|
|| deny "BLOCKED: имя ветки «${branch_arg}» не той формы, что принята здесь. Номер у ветки тот же, что у задачи и у заголовка заявки на слияние."
|
|
231
|
-
check_task "$
|
|
235
|
+
check_task "$number_arg" "ветка «${branch_arg}»"
|
|
232
236
|
|
|
233
237
|
# Условия поставки, известные уже здесь, спрашиваются здесь. Прежде их спрашивали на
|
|
234
238
|
# пуше и на открытии PR — то есть после того, как работа сделана: основание чинится
|
|
@@ -374,7 +378,7 @@ branch="$(git branch --show-current 2>/dev/null)"
|
|
|
374
378
|
rt_task_branch_ok "$branch" \
|
|
375
379
|
|| deny "BLOCKED: заявка с ветки «${branch}», за которой не стоит задачи. Правка начинается с задачи, видимой в очереди работ: заведи её — ${task_new} — и перенеси работу в ветку с её номером."
|
|
376
380
|
|
|
377
|
-
number="$(
|
|
381
|
+
number="$(rt_task_branch_number "$branch")"
|
|
378
382
|
|
|
379
383
|
title=''
|
|
380
384
|
if command -v perl >/dev/null 2>&1; then
|
|
@@ -154,6 +154,28 @@ if [ -z "$failed" ] && [ -n "$skipped" ]; then
|
|
|
154
154
|
printf 'гейт пуша: набор прошёл, но эти проверки смотреть было не на что:\n%s\n' "$skipped" >&2
|
|
155
155
|
fi
|
|
156
156
|
|
|
157
|
+
# Чем набор гейта уже набора конвейера — сказанное один раз за сессию.
|
|
158
|
+
#
|
|
159
|
+
# Правило поставки требует, чтобы гейт не был уже конвейера, а собрать это требование нечем:
|
|
160
|
+
# файл конвейера у каждого дерева свой, и вывести из него шаги набора пакет не может. Молчание
|
|
161
|
+
# при этом читается как «проверено всё»: расхождение узнаётся из красного конвейера после
|
|
162
|
+
# заявки, когда правка уже отдана человеку.
|
|
163
|
+
#
|
|
164
|
+
# Отметка живёт в каталоге временных файлов, как у слова о нехватке функции профиля: на каждый
|
|
165
|
+
# пуш та же строка повторялась бы за заход десятки раз и перестала бы читаться.
|
|
166
|
+
gap_key="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)"
|
|
167
|
+
[ -z "$gap_key" ] && gap_key="$(date +%Y%m%d 2>/dev/null || printf 'nosession')"
|
|
168
|
+
gap_mark="${TMPDIR:-/tmp}/rt-kit-push-gate-gap-$gap_key"
|
|
169
|
+
if [ ! -f "$gap_mark" ]; then
|
|
170
|
+
printf 'гейт пуша: набор гейта — не набор конвейера. Сборка образов, снимки показа и проверка
|
|
171
|
+
' >&2
|
|
172
|
+
printf 'собранных пакетов в него не входят: что в нём стоит, показывает разбор состояния
|
|
173
|
+
' >&2
|
|
174
|
+
printf '(agent-kit doctor, раздел «набор перед пушем»), — прогони недостающее до заявки.
|
|
175
|
+
' >&2
|
|
176
|
+
: >"$gap_mark" 2>/dev/null || true
|
|
177
|
+
fi
|
|
178
|
+
|
|
157
179
|
[ -z "$failed" ] && exit 0
|
|
158
180
|
|
|
159
181
|
# Хвост вывода, а не весь: у прогонщика он длинный, а нужна причина отказа.
|
|
@@ -15,11 +15,32 @@ GLOSSARY="${CLAUDE_PROJECT_DIR:-.}/docs/GLOSSARY.md"
|
|
|
15
15
|
[ -f "$GLOSSARY" ] || exit 0
|
|
16
16
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
17
17
|
|
|
18
|
+
# Где слово заводят. Словарь бывает разложен пакетом — тогда правка на его месте теряется на
|
|
19
|
+
# следующей раскладке, а до тех пор раскладка отказывает по нему целиком. Вводная, которую читает
|
|
20
|
+
# каждая сессия, обязана вести в надстройку: она — единственный текст о словаре, доезжающий до
|
|
21
|
+
# читателя гарантированно, и, ведя в собранный файл, она отменяет собой всё сказанное на других
|
|
22
|
+
# слоях. Опровержение стоит строкой ниже, в шапке раскладки, но читается служебной строкой сборки:
|
|
23
|
+
# начинается оно с версии и суммы, а не со слова.
|
|
24
|
+
#
|
|
25
|
+
# Адрес выводится из идентификатора ресурса в самой шапке, а не зашивается: каталог надстроек у
|
|
26
|
+
# каждого дерева свой, и зашитый путь врал бы в первом же, которое держит их иначе. Шапки нет —
|
|
27
|
+
# словарь принадлежит дереву целиком, и «заводится здесь же» верно как есть.
|
|
28
|
+
header="$(head -1 "$GLOSSARY" 2>/dev/null)"
|
|
29
|
+
resource="$(printf '%s' "$header" | sed -n 's/.*rt-kit v[^ ]* · \([^ ]*\) · .*/\1/p')"
|
|
30
|
+
|
|
31
|
+
if [ -n "$resource" ]; then
|
|
32
|
+
where="$(printf 'Словарь разложен пакетом и на месте не правится: новое слово идёт в надстройку\n.claude/rt-kit/overrides/%s — она сливается по разделу «## » и раскладку переживает.\n' "$resource")"
|
|
33
|
+
else
|
|
34
|
+
where='Словарь принадлежит дереву целиком: новое слово заводится в нём же той же правкой.
|
|
35
|
+
'
|
|
36
|
+
fi
|
|
37
|
+
|
|
18
38
|
{
|
|
19
39
|
printf 'СЛОВАРЬ ПРОЕКТА — `docs/GLOSSARY.md`, ниже целиком.\n\n'
|
|
20
40
|
printf 'Слово отсюда употребляется в том значении, что здесь: и в файлах, и в ответах владельцу.\n'
|
|
21
41
|
printf 'Раздел «Так не пишем» действует так же — левая колонка не пишется и не произносится нигде.\n'
|
|
22
|
-
printf 'Слова, которого здесь нет, у читателя нет тоже: оно
|
|
23
|
-
printf 'либо заменяется тем, что уже есть.\n
|
|
42
|
+
printf 'Слова, которого здесь нет, у читателя нет тоже: оно вводится тем же изменением, которым\n'
|
|
43
|
+
printf 'впервые употреблено, либо заменяется тем, что уже есть.\n'
|
|
44
|
+
printf '%s\n' "$where"
|
|
24
45
|
cat "$GLOSSARY"
|
|
25
46
|
} | jq -Rs '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:.}}'
|