@rt-tools/agent-kit 0.22.0 → 0.24.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/README.md +8 -2
- package/assets/checks/board-long-work.github.mjs +101 -0
- package/assets/checks/board-runs.github.mjs +34 -0
- package/assets/checks/board.github.mjs +1 -1
- package/assets/checks/check-board.github.mjs +30 -2
- package/assets/checks/check-doc-paths.mjs +24 -5
- package/assets/checks/check-file-size.mjs +8 -2
- package/assets/checks/check-prose-style.mjs +10 -1
- 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/gate-map.sh +13 -0
- package/assets/defaults/project.sh +26 -0
- package/assets/defaults/shell.sh +18 -3
- package/assets/hooks/browser-guard-device-id.sh +42 -12
- package/assets/hooks/browser-guard-no-asking.sh +5 -1
- package/assets/hooks/dispatch.sh +40 -9
- package/assets/hooks/docs-guard.sh +10 -0
- package/assets/hooks/exam-guard.sh +66 -16
- package/assets/hooks/git-guard-delivery-draft.sh +78 -0
- package/assets/hooks/git-guard-delivery.sh +64 -122
- package/assets/hooks/git-guard-main.sh +39 -4
- package/assets/hooks/git-guard-push-tests.sh +71 -3
- package/assets/hooks/glossary-load.sh +23 -2
- package/assets/hooks/grill-gate.sh +62 -0
- package/assets/hooks/hook-input.sh +17 -6
- package/assets/hooks/rule-source-guard.sh +11 -0
- package/assets/hooks/stand-login-guard.sh +101 -0
- package/assets/hooks/write-targets.sh +37 -4
- package/assets/laws/autonomous-work.md +30 -0
- package/assets/laws/project-documentation.md +8 -0
- package/assets/laws/verifiability.md +12 -2
- package/assets/laws/work-conduct.md +59 -65
- package/assets/patterns/autonomous-work-run.md +105 -0
- package/assets/patterns/browser-verification-measure.md +41 -1
- package/assets/patterns/browser-verification-stand.md +57 -16
- package/assets/patterns/doc-style-human.md +75 -0
- package/assets/patterns/doc-style-write.md +16 -0
- package/assets/patterns/git-workflow-commit.azure.md +12 -0
- package/assets/patterns/git-workflow-commit.github.md +16 -3
- package/assets/patterns/git-workflow-commit.gitlab.md +12 -0
- package/assets/patterns/git-workflow-merge.md +8 -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 +48 -48
- package/assets/patterns/testing-e2e.md +18 -8
- package/assets/patterns/ts-procedure.md +3 -2
- package/assets/pitfalls/task-flow.md +40 -40
- package/assets/rules/autonomous-work.md +92 -0
- package/assets/rules/browser-verification.md +35 -3
- 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 +54 -0
- package/assets/rules/git-workflow.azure.md +8 -0
- package/assets/rules/git-workflow.github.md +59 -63
- package/assets/rules/git-workflow.gitlab.md +8 -0
- package/assets/rules/reuse-first.md +25 -5
- package/assets/rules/styling-bem.md +8 -1
- package/assets/rules/task-flow.md +108 -109
- package/assets/rules/testing.md +21 -0
- package/assets/skills/agent-kit.md +72 -82
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +69 -2
- package/lib/commands.js.map +1 -1
- package/lib/enroll.d.ts.map +1 -1
- package/lib/enroll.js +1 -1
- package/lib/enroll.js.map +1 -1
- package/lib/observations.d.ts +10 -1
- package/lib/observations.d.ts.map +1 -1
- package/lib/observations.js +1 -0
- package/lib/observations.js.map +1 -1
- package/lib/override-marks.d.ts +24 -0
- package/lib/override-marks.d.ts.map +1 -0
- package/lib/override-marks.js +98 -0
- package/lib/override-marks.js.map +1 -0
- 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/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.js +1 -1
- package/lib/shipment.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.24.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
package/README.md
CHANGED
|
@@ -358,10 +358,16 @@ npx agent-kit enroll --token <токен> # положить токе
|
|
|
358
358
|
| Где | Что нужно |
|
|
359
359
|
| --- | --- |
|
|
360
360
|
| у потребителя | `jq` — его требуют и сами гарды |
|
|
361
|
-
| у потребителя | адрес приёма ключом `intake` в `.claude/rt-kit.json` |
|
|
361
|
+
| у потребителя | адрес приёма ключом `intake` в `.claude/rt-kit.json` — берётся у владельца приёма вместе с кодом приглашения |
|
|
362
362
|
| у потребителя | токен дерева в файле вне дерева, названном ключом `token` |
|
|
363
363
|
| у приёма | заведённое дерево: токен выдаёт его команда и печатает один раз |
|
|
364
364
|
|
|
365
|
+
Адреса приёма пакет не знает: приём поднимает владелец, и у каждой мастерской он свой.
|
|
366
|
+
Спрашивается он у того же человека, который выдаёт код приглашения, — код и адрес идут парой и
|
|
367
|
+
порознь не работают. Вид адреса — `https://<приём мастерской>`; без TLS команда запрещает обмен у
|
|
368
|
+
себя, не доходя до сети: токен приходит единственным ответом, и по открытому пути его прочитает
|
|
369
|
+
любой. Исключение сделано только для локальной машины.
|
|
370
|
+
|
|
365
371
|
Признак, которым дерево представляется приёму, считается хешем от адреса его удалённого
|
|
366
372
|
репозитория: адрес по нему не восстанавливается, а у двух рабочих копий одного репозитория он
|
|
367
373
|
один. Репозитория нет — признак называется ключом `tree`.
|
|
@@ -420,7 +426,7 @@ PR влит, и вливанием в текущую ветку во всех п
|
|
|
420
426
|
либо надстройка `.claude/rt-kit/overrides/commands/<имя>.md`. Свою команду дерево заводит файлом
|
|
421
427
|
рядом; файл без шапки раскладки пакет чужим считает и не перезаписывает.
|
|
422
428
|
|
|
423
|
-
Слешем зовётся
|
|
429
|
+
Слешем зовётся не только команда: скилы — правила, паттерны и скилы без закона из
|
|
424
430
|
`.claude/skills/` — и конвейеры из `.claude/workflows/`. Что из всего этого разложено и откуда
|
|
425
431
|
взялось, говорят `npx agent-kit list` и `npx agent-kit doctor`.
|
|
426
432
|
|
|
@@ -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`.
|
|
@@ -213,7 +213,7 @@ export function behindMain(branch, mainBranch, options) {
|
|
|
213
213
|
* потом значило бы второй вызов на каждый PR. */
|
|
214
214
|
export function fetchOpenPulls(options) {
|
|
215
215
|
return ghJson(
|
|
216
|
-
['pr', 'list', '--state', 'open', '--limit', '200', '--json', 'number,title,headRefName,headRefOid,isDraft,body,mergeable'],
|
|
216
|
+
['pr', 'list', '--state', 'open', '--limit', '200', '--json', 'number,title,headRefName,headRefOid,isDraft,body,mergeable,baseRefName'],
|
|
217
217
|
options
|
|
218
218
|
);
|
|
219
219
|
}
|
|
@@ -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';
|
|
@@ -195,6 +196,20 @@ function checkHeadRun(pull, options) {
|
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
// Заявка поверх соседней прогона не получает: рабочий поток слушает заявки в главную ветку и
|
|
200
|
+
// событий с другой базой не видит. Строка о потерянном событии здесь неверна дважды: событие
|
|
201
|
+
// не терялось, и перезакрытие его не вернёт — совет исполняется буквально, прогон не
|
|
202
|
+
// запускается, и на второй попытке поломку начинают искать в хостинге.
|
|
203
|
+
if (pull.baseRefName && pull.baseRefName !== MAIN_BRANCH) {
|
|
204
|
+
report(
|
|
205
|
+
`PR #${pull.number}: на вершине ${pull.headRefOid.slice(0, 8)} прогона нет и не будет — ` +
|
|
206
|
+
`заявка открыта в ветку «${pull.baseRefName}», а рабочий поток слушает заявки в главную; ` +
|
|
207
|
+
`перенеси базу на «${MAIN_BRANCH}», когда нижняя заявка влита`
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
198
213
|
report(
|
|
199
214
|
`PR #${pull.number}: на вершине ${pull.headRefOid.slice(0, 8)} прогона нет, а лежит она ${minutes} мин — ` +
|
|
200
215
|
`конвейер события не получил; верни его новым коммитом либо перезакрытием PR ` +
|
|
@@ -317,6 +332,7 @@ try {
|
|
|
317
332
|
|
|
318
333
|
similarTitles(open);
|
|
319
334
|
checkEpicLinks(open, report);
|
|
335
|
+
checkLongWork(open, report);
|
|
320
336
|
|
|
321
337
|
const openNumbers = new Set(open.map((issue) => issue.number));
|
|
322
338
|
const claimed = new Map();
|
|
@@ -415,7 +431,8 @@ try {
|
|
|
415
431
|
// сколько угодно коммитов, и заметить это неоткуда.
|
|
416
432
|
if (!offline && DEPLOY_WORKFLOW) {
|
|
417
433
|
try {
|
|
418
|
-
const
|
|
434
|
+
const token = botToken() ?? undefined;
|
|
435
|
+
const lag = deployLag(DEPLOY_WORKFLOW, MAIN_BRANCH, { token });
|
|
419
436
|
if (lag === null) {
|
|
420
437
|
report(`выкаток по «${DEPLOY_WORKFLOW}» не было ни одной — сравнить прод не с чем`);
|
|
421
438
|
} else if (lag.behind > 0) {
|
|
@@ -423,6 +440,17 @@ if (!offline && DEPLOY_WORKFLOW) {
|
|
|
423
440
|
`прод отстал от «${MAIN_BRANCH}» на ${lag.behind} коммитов: последняя выкатка — ${lag.sha.slice(0, 8)} от ${String(lag.at).slice(0, 10)}`
|
|
424
441
|
);
|
|
425
442
|
}
|
|
443
|
+
|
|
444
|
+
// Отчего прод отстал, отставание не говорит: оно считается по последней УСПЕШНОЙ
|
|
445
|
+
// выкатке, и «не запускали» с «упала» выглядят через него одинаково. Ведут они к
|
|
446
|
+
// разному — первую запускают, вторую читают журналом и чинят.
|
|
447
|
+
const deploy = lastDeploy(DEPLOY_WORKFLOW, { token });
|
|
448
|
+
if (deploy.verdict === 'failure') {
|
|
449
|
+
report(
|
|
450
|
+
`выкатка «${DEPLOY_WORKFLOW}» упала на ${String(deploy.sha).slice(0, 8)} от ${String(deploy.at).slice(0, 10)}: ` +
|
|
451
|
+
`главная ветка впереди прода, и слияния поверх уедут туда же — ${deploy.url}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
426
454
|
} catch (error) {
|
|
427
455
|
if (error instanceof OfflineError) {
|
|
428
456
|
console.log(`check-board: прод не сверялся — ${error.message}`);
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { spawnSync } from 'node:child_process';
|
|
30
30
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
31
|
-
import { join } from 'node:path';
|
|
31
|
+
import { join, posix } from 'node:path';
|
|
32
32
|
|
|
33
33
|
import { allowlistOf, CONFIG, ROOT, parseAllowlist } from './rt-kit-checks.config.mjs';
|
|
34
34
|
|
|
@@ -163,7 +163,7 @@ const TREE = treeOfRepo();
|
|
|
163
163
|
* каталог — оба ищутся по дереву, потому что адрес у них один, а написан он коротко.
|
|
164
164
|
*/
|
|
165
165
|
function looksLikePath(candidate) {
|
|
166
|
-
if (/[*<>{}$|\s]|\.\.\.|…/.test(candidate)) {
|
|
166
|
+
if (/[*<>{}$|\s()[\]'",;=]|\.\.\.|…/.test(candidate)) {
|
|
167
167
|
return false;
|
|
168
168
|
}
|
|
169
169
|
if (/^(https?:|@|~|\/|-)/.test(candidate) || candidate.includes(':')) {
|
|
@@ -187,9 +187,21 @@ function looksLikePath(candidate) {
|
|
|
187
187
|
* среди каталогов: имя каталога в обзорном документе либы означает каталог рядом, а не
|
|
188
188
|
* каталог в корне.
|
|
189
189
|
*/
|
|
190
|
-
function existsInTree(candidate) {
|
|
190
|
+
function existsInTree(candidate, fromDir = '') {
|
|
191
191
|
const bare = candidate.replace(/\/$/, '');
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Относительный адрес принадлежит каталогу документа, а не корню дерева: `../routes.ts` из
|
|
195
|
+
* `libs/x/shell/src/shell/README.md` — это `libs/x/shell/src/routes.ts`. Без разрешения от
|
|
196
|
+
* каталога такой адрес ищется по дереву строкой и не находится никогда, то есть проверка
|
|
197
|
+
* краснеет на каждой ссылке, написанной так, как её пишут в разметке.
|
|
198
|
+
*/
|
|
199
|
+
if (/^\.{1,2}(\/|$)/.test(bare)) {
|
|
200
|
+
const resolved = posix.normalize(posix.join(fromDir, bare));
|
|
201
|
+
|
|
202
|
+
return resolved.startsWith('..') ? false : existsSync(join(ROOT, resolved));
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
if (ROOTED_IN.has(bare.split('/')[0])) {
|
|
194
206
|
return existsSync(join(ROOT, bare));
|
|
195
207
|
}
|
|
@@ -260,6 +272,7 @@ function checkIndex(dir) {
|
|
|
260
272
|
|
|
261
273
|
function checkDoc(doc, allowed) {
|
|
262
274
|
const lines = readFileSync(join(ROOT, doc), 'utf8').split('\n');
|
|
275
|
+
const fromDir = posix.dirname(doc);
|
|
263
276
|
let insideFence = false;
|
|
264
277
|
|
|
265
278
|
lines.forEach((line, index) => {
|
|
@@ -276,7 +289,7 @@ function checkDoc(doc, allowed) {
|
|
|
276
289
|
if (!looksLikePath(candidate) || allowed.has(candidate)) {
|
|
277
290
|
continue;
|
|
278
291
|
}
|
|
279
|
-
if (!existsInTree(candidate)) {
|
|
292
|
+
if (!existsInTree(candidate, fromDir)) {
|
|
280
293
|
report(doc, index + 1, candidate);
|
|
281
294
|
}
|
|
282
295
|
}
|
|
@@ -308,8 +321,14 @@ INDEXED_DIRS.forEach((dir) => checkIndex(dir));
|
|
|
308
321
|
if (problems.length > 0) {
|
|
309
322
|
console.error(`check-doc-paths: расхождений ${problems.length}\n`);
|
|
310
323
|
problems.forEach((problem) => console.error(` ${problem}`));
|
|
324
|
+
// Вариантов три, и третий назван: список известного хранит принятое, а не результаты
|
|
325
|
+
// сломанной проверки. Прежний текст предлагал вносить в список всё спорное — учил обходу,
|
|
326
|
+
// который закон о проверяемости запрещает; один разбор дал 51 ложный отказ из 264, каждый
|
|
327
|
+
// на существующий адрес.
|
|
311
328
|
console.error(
|
|
312
|
-
`\n
|
|
329
|
+
`\nХодов отсюда три: поправить устаревший адрес; внести имя в ${ALLOWLIST}, если документ` +
|
|
330
|
+
`\nописывает ещё не созданное; починить саму проверку, если ошибается она, — разобрать` +
|
|
331
|
+
`\nотказы поимённо и показать разбор владельцу. Спорное в список не вносится.`
|
|
313
332
|
);
|
|
314
333
|
}
|
|
315
334
|
|
|
@@ -144,8 +144,14 @@ if (process.argv.includes('--baseline')) {
|
|
|
144
144
|
const fresh = [...tooLong].filter(([path]) => !known.has(path));
|
|
145
145
|
/** Строка на файл, которого в дереве нет, — устаревшая: иначе список копит мёртвое. */
|
|
146
146
|
const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
|
|
147
|
-
/**
|
|
148
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Файл поделили, а строку оставили: список перестал бы отвечать за то, что в нём стоит.
|
|
149
|
+
* Тяжёлый по знакам файл под предел строк не подпадает, и без второй проверки его запись
|
|
150
|
+
* читалась бы устаревшей — долг нельзя было бы ни записать, ни оставить.
|
|
151
|
+
*/
|
|
152
|
+
const shrunk = [...known.keys()].filter(
|
|
153
|
+
(path) => !tooLong.has(path) && !overweight.has(path) && existsSync(join(ROOT, path))
|
|
154
|
+
);
|
|
149
155
|
|
|
150
156
|
/** Тяжёлое по знакам судится тем же списком известного: один долг на файл, а не два. */
|
|
151
157
|
const heavy = [...overweight].filter(([path]) => !known.has(path) && !tooLong.has(path));
|
|
@@ -39,7 +39,16 @@ const MARKS = [
|
|
|
39
39
|
[/(?<![а-яёА-ЯЁ])в рамках(?![а-яёА-ЯЁ])/giu, 'назвать отношение прямо: «в», «при», «для»'],
|
|
40
40
|
[/(?<![а-яёА-ЯЁ])на основании(?![а-яёА-ЯЁ])/giu, '«по»'],
|
|
41
41
|
[/(?<![а-яёА-ЯЁ])посредством(?![а-яёА-ЯЁ])/giu, '«через», «командой», «вызовом»'],
|
|
42
|
-
[/(?<![а-яёА-ЯЁ])данн(
|
|
42
|
+
[/(?<![а-яёА-ЯЁ])данн(ый|ая|ое)(?![а-яёА-ЯЁ])/giu, '«этот» или ничего'],
|
|
43
|
+
// Форма «данные» бывает и существительным, и заменить его нечем: данные стенда — это данные.
|
|
44
|
+
// Проверяется она поэтому по тому, что стоит следом: местоимением она читается перед словом,
|
|
45
|
+
// которое сама и определяет. Перечень корней короткий намеренно — распознаётся то, что
|
|
46
|
+
// встречалось, а ложный отказ здесь дороже пропуска: проверка стоит в наборе гейта пуша, и
|
|
47
|
+
// красное на слове, которому нет замены, останавливает работу целиком.
|
|
48
|
+
[
|
|
49
|
+
/(?<![а-яёА-ЯЁ])данные\s+(требовани|услови|значени|обстоятельств|сведени|фактор|параметр|вопрос|правил|подход|принцип|случа)[а-яё]*/giu,
|
|
50
|
+
'«эти» или ничего',
|
|
51
|
+
],
|
|
43
52
|
[/(?<![а-яёА-ЯЁ])соответствующ(ий|ая|ее|ие)(?![а-яёА-ЯЁ])/giu, 'назвать, чему именно соответствует'],
|
|
44
53
|
[/(?<![а-яёА-ЯЁ])необходимо(?![а-яёА-ЯЁ])/giu, '«надо» или повелительное наклонение'],
|
|
45
54
|
[/(?<![а-яёА-ЯЁ])должен быть (выполнен|произведён|осуществлён)(?![а-яёА-ЯЁ])/giu, 'сказать, кто это делает'],
|
|
@@ -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
|
|
|
@@ -175,6 +175,19 @@ skill_for_default() {
|
|
|
175
175
|
printf '%s\n' 'git-workflow'
|
|
176
176
|
fi
|
|
177
177
|
|
|
178
|
+
# Тело задачи и тело заявки публикуются вызовом клиента и файлом дерева не
|
|
179
|
+
# становятся: гейт проверял расширение правимого файла и на такой команде молчал.
|
|
180
|
+
# Читает этот текст человек, и чаще, чем любой файл дерева: владелец прочитал семь
|
|
181
|
+
# своих задач и две заявки и назвал язык в них нечитаемым, а ни одна проверка об этом
|
|
182
|
+
# не сообщила. Признака два сразу: вызов клиента и тело в доводах — одного слова о
|
|
183
|
+
# заявке мало, оно есть в строке любой команды, которая о ней пишет.
|
|
184
|
+
if { rt_gate_invokes "$target" "(gh|glab)[[:space:]]+(pr|mr|issue)[[:space:]]+(create|edit)" \
|
|
185
|
+
|| rt_gate_invokes "$target" "[^[:space:]]*task:new"; } \
|
|
186
|
+
&& printf '%s' "$target" | grep -qE '(--body|--body-file|--description|-F[[:space:]]*body)'; then
|
|
187
|
+
printf '%s\n' 'doc-style'
|
|
188
|
+
printf '%s\n' 'doc-style-human'
|
|
189
|
+
fi
|
|
190
|
+
|
|
178
191
|
# Слияние PR — последний момент, когда папку закрытой задачи ещё можно разобрать
|
|
179
192
|
# тем же PR: после слияния сверка очереди её видит, а отвечать за неё уже
|
|
180
193
|
# некому. Требуется ВТОРЫМ слоем, дополнительно к правилу поставки.
|