@rt-tools/agent-kit 0.16.1 → 0.18.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-epics.github.mjs +123 -0
- package/assets/checks/board-gh.github.mjs +121 -0
- package/assets/checks/board-paths.github.mjs +88 -0
- package/assets/checks/board-runs.github.mjs +8 -0
- package/assets/checks/board-titles.github.mjs +66 -0
- package/assets/checks/board.github.mjs +77 -73
- package/assets/checks/check-board.github.mjs +56 -7
- package/assets/checks/check-file-size.mjs +10 -2
- package/assets/checks/check-glossary.mjs +170 -0
- package/assets/checks/check-hook-scope.mjs +126 -0
- package/assets/checks/check-profile-drift.mjs +195 -0
- package/assets/checks/check-push-gate.mjs +59 -1
- package/assets/checks/check-schema-drift.mjs +12 -5
- package/assets/checks/check-specs.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +13 -0
- package/assets/checks/spec-anchors.mjs +2 -2
- package/assets/checks/spec-contract.mjs +9 -0
- package/assets/defaults/gate-map.sh +13 -4
- package/assets/defaults/project.sh +31 -108
- package/assets/defaults/shell.sh +139 -0
- package/assets/docs/GLOSSARY.md +1 -1
- package/assets/hooks/browser-device-id.sh +20 -4
- package/assets/hooks/browser-guard-device-id.sh +5 -2
- package/assets/hooks/browser-guard-no-other-drivers.sh +41 -2
- package/assets/hooks/claim-guard.sh +22 -1
- package/assets/hooks/docs-guard.sh +1 -1
- package/assets/hooks/exam-guard.sh +85 -12
- package/assets/hooks/git-guard-delivery-conflict.sh +85 -0
- package/assets/hooks/git-guard-delivery-folder.sh +19 -0
- package/assets/hooks/git-guard-delivery-signature.sh +14 -5
- package/assets/hooks/git-guard-delivery.sh +54 -2
- package/assets/hooks/git-guard-push-tests.sh +21 -1
- package/assets/hooks/grill-gate-ask.sh +25 -0
- package/assets/hooks/grill-gate.sh +75 -11
- package/assets/hooks/lint-after-edit.sh +44 -16
- package/assets/hooks/proposal-guard.sh +11 -4
- package/assets/hooks/rule-source-guard.sh +8 -13
- package/assets/hooks/skill-gate-layers.sh +7 -0
- package/assets/hooks/skill-gate.sh +29 -0
- package/assets/hooks/task-context-load.sh +32 -0
- package/assets/hooks/task-flow-context.sh +194 -0
- package/assets/hooks/task-flow-draft-guard.sh +109 -0
- package/assets/hooks/task-flow-guard.sh +27 -159
- package/assets/hooks/turn-exit-guard.sh +53 -88
- package/assets/hooks/waiting-turn-guard.sh +65 -2
- package/assets/hooks/window-fill-guard.sh +5 -1
- package/assets/hooks/work-start-guard.sh +166 -0
- package/assets/hooks/write-targets.sh +24 -0
- package/assets/laws/delivery.md +19 -0
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/verifiability.md +44 -0
- package/assets/laws/work-conduct.md +15 -0
- package/assets/patterns/browser-verification-measure.md +3 -1
- package/assets/patterns/browser-verification-stand.md +17 -4
- package/assets/patterns/doc-style-write.md +62 -2
- package/assets/patterns/git-workflow-commit.github.md +33 -5
- package/assets/patterns/git-workflow-docker.md +22 -0
- package/assets/patterns/git-workflow-freshness.md +87 -0
- package/assets/patterns/git-workflow-merge.md +41 -0
- package/assets/patterns/git-workflow-migration.md +11 -0
- package/assets/patterns/git-workflow-pr.github.md +12 -1
- package/assets/patterns/git-workflow-restart.md +18 -0
- package/assets/patterns/git-workflow-secrets.md +14 -0
- package/assets/patterns/git-workflow-stack.md +93 -1
- package/assets/patterns/lib-layers-move.md +4 -0
- package/assets/patterns/spec-driven-domain.md +47 -2
- package/assets/patterns/spec-driven-rule.md +16 -4
- package/assets/patterns/spec-driven-sweep.md +57 -0
- package/assets/patterns/status-report-table.github.md +1 -1
- package/assets/patterns/task-flow-archive.md +58 -16
- package/assets/patterns/task-flow-close.md +106 -23
- package/assets/patterns/task-flow-handoff.md +14 -2
- package/assets/patterns/task-flow-resume.md +35 -8
- package/assets/patterns/task-flow-start.md +60 -22
- package/assets/patterns/testing-e2e.md +23 -0
- package/assets/patterns/turn-entry-map.md +1 -1
- package/assets/pitfalls/agent-kit.md +71 -3
- package/assets/pitfalls/doc-style.md +15 -0
- package/assets/pitfalls/git-workflow.github.md +88 -0
- package/assets/pitfalls/spec-driven.md +22 -0
- package/assets/pitfalls/task-flow.md +113 -0
- package/assets/pitfalls/testing.md +6 -0
- package/assets/pitfalls/turn-conduct.md +49 -0
- package/assets/rules/browser-verification.md +58 -0
- package/assets/rules/deploy-flow.azure.md +8 -0
- package/assets/rules/deploy-flow.github.md +27 -0
- package/assets/rules/deploy-flow.gitlab.md +8 -0
- package/assets/rules/doc-style.md +20 -0
- package/assets/rules/git-workflow.azure.md +22 -2
- package/assets/rules/git-workflow.github.md +127 -118
- package/assets/rules/git-workflow.gitlab.md +17 -4
- package/assets/rules/lists.md +5 -0
- package/assets/rules/observability.needs-app.md +4 -0
- package/assets/rules/reuse-first.md +14 -0
- package/assets/rules/shared-code.md +5 -0
- package/assets/rules/spec-driven.md +33 -15
- package/assets/rules/task-flow.md +132 -126
- package/assets/rules/testing.md +41 -2
- package/assets/rules/turn-conduct.md +73 -57
- package/assets/rules/turn-entry.md +6 -0
- package/assets/samples/tasks/_template/grill.md +5 -0
- package/assets/samples/tasks/_template/plan.md +3 -0
- package/assets/skills/agent-kit.md +108 -76
- package/assets/templates/postmortem.md +5 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +30 -6
- package/bin/agent-kit.js.map +1 -1
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +2 -1
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +96 -7
- package/lib/commands.js.map +1 -1
- package/lib/hooks-map.d.ts +26 -0
- package/lib/hooks-map.d.ts.map +1 -1
- package/lib/hooks-map.js +58 -2
- package/lib/hooks-map.js.map +1 -1
- package/lib/sections.d.ts +6 -0
- package/lib/sections.d.ts.map +1 -1
- package/lib/sections.js +19 -0
- package/lib/sections.js.map +1 -1
- package/lib/shipment.d.ts +2 -0
- package/lib/shipment.d.ts.map +1 -1
- package/lib/shipment.fixture.d.ts +5 -0
- package/lib/shipment.fixture.d.ts.map +1 -1
- package/lib/shipment.fixture.js +7 -0
- package/lib/shipment.fixture.js.map +1 -1
- package/lib/shipment.js +13 -1
- package/lib/shipment.js.map +1 -1
- package/lib/sync.d.ts +35 -3
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +59 -8
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.18.0.tgz +0 -0
- package/rt-tools-agent-kit-0.16.1.tgz +0 -0
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Ненулевой код возврата и перечень расхождений.
|
|
30
30
|
*/
|
|
31
|
-
import { existsSync, statSync } from 'node:fs';
|
|
31
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
|
|
34
34
|
import {
|
|
@@ -42,12 +42,17 @@ import {
|
|
|
42
42
|
fetchBoard,
|
|
43
43
|
fetchIssues,
|
|
44
44
|
fetchOpenPulls,
|
|
45
|
+
behindMain,
|
|
45
46
|
gh,
|
|
47
|
+
ghJson,
|
|
46
48
|
numberFromTaskDir,
|
|
47
49
|
numberFromTitle,
|
|
48
50
|
taskDirs,
|
|
49
51
|
} from './board.mjs';
|
|
50
|
-
import {
|
|
52
|
+
import { onlyIgnoredPaths } from './board-paths.mjs';
|
|
53
|
+
import { HAS_PIPELINE, deployLag, evictedOnHead, headCommittedAt, runsOnHead, verdictOnHead } from './board-runs.mjs';
|
|
54
|
+
import { checkEpicLinks } from './board-epics.mjs';
|
|
55
|
+
import { similarTitles } from './board-titles.mjs';
|
|
51
56
|
import { CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
52
57
|
|
|
53
58
|
const IN_REVIEW = STATUS_OPTIONS[IN_REVIEW_STATUS].name;
|
|
@@ -63,12 +68,22 @@ const RUN_GRACE_MINUTES = 10;
|
|
|
63
68
|
* Конвейер дерева. Прогоны спрашиваются только там, где ему есть откуда взяться: дерево без
|
|
64
69
|
* конвейера получило бы строку на каждый открытый PR, и не о чём.
|
|
65
70
|
*/
|
|
66
|
-
const PIPELINE = CONFIG.pushGate?.pipelineFile ?? '';
|
|
67
|
-
const HAS_PIPELINE = PIPELINE !== '' && existsSync(join(ROOT, PIPELINE));
|
|
68
71
|
/**
|
|
69
72
|
* Рабочий поток выкатки и ветка, с которой прод сравнивают. Не назвав потока, дерево сверки
|
|
70
73
|
* прода не получает — и сверка говорит об этом вслух: молчание читалось бы как «прод сошёлся».
|
|
71
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Метки, которыми в очереди работ помечен груз — присланные деревьями разборы происшествий и
|
|
77
|
+
* предложения. Задачами они не являются: заголовок у них без номера, исполнителя нет, на борде
|
|
78
|
+
* их нет, — и сверка печатала на каждую по три строки, среди которых настоящее расхождение уже
|
|
79
|
+
* не читалось. Заводить такие записи в очередь перестали, а заведённые прежним порядком оттуда
|
|
80
|
+
* никуда не денутся: закрывать их — решение владельца, а не сверки.
|
|
81
|
+
*
|
|
82
|
+
* Имя метки называет дерево: у каждого оно своё, а выдуманное умолчание не совпало бы ни с чем
|
|
83
|
+
* и молча выключило бы отсев. Дерево, метки не назвавшее, судится как прежде.
|
|
84
|
+
*/
|
|
85
|
+
const CARGO_LABELS = new Set(CONFIG.board?.cargoLabels ?? []);
|
|
86
|
+
|
|
72
87
|
const DEPLOY_WORKFLOW = CONFIG.deploy?.workflow ?? '';
|
|
73
88
|
const MAIN_BRANCH = CONFIG.deploy?.mainBranch ?? 'main';
|
|
74
89
|
|
|
@@ -94,6 +109,12 @@ function checkDrafts() {
|
|
|
94
109
|
}
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Значимые слова заголовка: ключ задачи и короткие служебные слова выброшены.
|
|
114
|
+
*
|
|
115
|
+
* Разбор нарочно грубый — совпадение слов здесь не приговор, а повод посмотреть: точное
|
|
116
|
+
* сравнение заголовков не находит ничего, потому что дубль пишут другими словами.
|
|
117
|
+
*/
|
|
97
118
|
function closesNumbers(body) {
|
|
98
119
|
return [...String(body ?? '').matchAll(/\bCloses\s+#(\d+)\b/gi)].map((match) => Number(match[1]));
|
|
99
120
|
}
|
|
@@ -160,6 +181,10 @@ function checkHeadRun(pull, options) {
|
|
|
160
181
|
return;
|
|
161
182
|
}
|
|
162
183
|
|
|
184
|
+
if (onlyIgnoredPaths(pull, options)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
163
188
|
if (pull.mergeable === 'CONFLICTING') {
|
|
164
189
|
report(
|
|
165
190
|
`PR #${pull.number}: на вершине ${pull.headRefOid.slice(0, 8)} прогона нет и не будет, пока она конфликтует — ` +
|
|
@@ -254,7 +279,7 @@ function checkConflicting(pull) {
|
|
|
254
279
|
);
|
|
255
280
|
}
|
|
256
281
|
|
|
257
|
-
let checked = { issues: 0, pulls: 0 };
|
|
282
|
+
let checked = { issues: 0, pulls: 0, cargo: 0 };
|
|
258
283
|
|
|
259
284
|
// Черновики судятся по диску и потому проверяются всегда: связи для этого не нужно.
|
|
260
285
|
checkDrafts();
|
|
@@ -264,9 +289,13 @@ try {
|
|
|
264
289
|
const options = { token: botToken() ?? undefined };
|
|
265
290
|
const board = fetchBoard(options);
|
|
266
291
|
const issues = fetchIssues('all', options);
|
|
267
|
-
const
|
|
292
|
+
const allOpen = issues.filter((issue) => issue.state === 'OPEN');
|
|
293
|
+
// Отсев идёт один раз и до всех проверок задачи: помеченная запись не задача целиком, а не
|
|
294
|
+
// наполовину, — её не судят ни заголовком, ни исполнителем, ни бордой, ни сводкой совпавших
|
|
295
|
+
// заголовков, ни связью с эпиком.
|
|
296
|
+
const open = CARGO_LABELS.size === 0 ? allOpen : allOpen.filter((issue) => !(issue.labels ?? []).some((label) => CARGO_LABELS.has(label.name)));
|
|
268
297
|
const pulls = fetchOpenPulls(options);
|
|
269
|
-
checked = { issues: issues.length, pulls: pulls.length };
|
|
298
|
+
checked = { issues: issues.length, pulls: pulls.length, cargo: allOpen.length - open.length };
|
|
270
299
|
|
|
271
300
|
for (const item of board.foreign) {
|
|
272
301
|
report(`борда: ${item} — на борде стоят задачи, а не PR о них`);
|
|
@@ -286,6 +315,9 @@ try {
|
|
|
286
315
|
}
|
|
287
316
|
}
|
|
288
317
|
|
|
318
|
+
similarTitles(open);
|
|
319
|
+
checkEpicLinks(open, report);
|
|
320
|
+
|
|
289
321
|
const openNumbers = new Set(open.map((issue) => issue.number));
|
|
290
322
|
const claimed = new Map();
|
|
291
323
|
for (const pull of pulls) {
|
|
@@ -317,6 +349,17 @@ try {
|
|
|
317
349
|
|
|
318
350
|
checkConflicting(pull);
|
|
319
351
|
|
|
352
|
+
// Отставание заявки от главной ветки: гард судит основание один раз, в минуту открытия,
|
|
353
|
+
// а заявка стоит днями. Влитая с отставанием, она везёт в главную сочетание, которого не
|
|
354
|
+
// проверял никто, — и по странице заявки этого не видно: прогон на ней зелёный.
|
|
355
|
+
const behind = behindMain(pull.headRefName, MAIN_BRANCH, options);
|
|
356
|
+
if (behind > 0) {
|
|
357
|
+
report(
|
|
358
|
+
`PR #${pull.number}: ветка отстала от «${MAIN_BRANCH}» на ${behind} коммитов — прогон шёл от основания, ` +
|
|
359
|
+
`которого в главной ветке уже нет. Влей главную, пересмотри набор проверок по тому, что ветка везёт теперь, и прогони заново`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
if (!FOLDER_SKIP.test(String(pull.body ?? '')) && pull.headRefName) {
|
|
321
364
|
const folder = folderInBranch(pull.headRefName, options);
|
|
322
365
|
if (folder !== null) {
|
|
@@ -394,6 +437,12 @@ if (!offline && !DEPLOY_WORKFLOW) {
|
|
|
394
437
|
console.log('check-board: прод с главной веткой не сверялся — рабочий поток выкатки в настройке дерева не назван');
|
|
395
438
|
}
|
|
396
439
|
|
|
440
|
+
// Отсеянное называется числом: молчаливый отсев неотличим от сломанной сверки — метка,
|
|
441
|
+
// названная с опечаткой, выключила бы проверку целиком и не сказала бы об этом.
|
|
442
|
+
if (!offline && checked.cargo > 0) {
|
|
443
|
+
console.log(`check-board: записей груза в очереди ${checked.cargo} — задачами они не судятся`);
|
|
444
|
+
}
|
|
445
|
+
|
|
397
446
|
if (!offline && !HAS_PIPELINE) {
|
|
398
447
|
console.log('check-board: прогоны на вершинах не спрашивались — файла конвейера в дереве нет');
|
|
399
448
|
}
|
|
@@ -62,11 +62,19 @@ const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
|
|
|
62
62
|
/** Описание прошлого, папка задачи и то, что переписывает генератор. */
|
|
63
63
|
const SKIPPED_PREFIXES = [CONFIG.archiveDir, `${CONFIG.tasksDir}/`, ...CONFIG.generatedDirs];
|
|
64
64
|
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Дерево спрашивается у системы контроля версий: иначе каталоги с точки не видны, а сборка видна.
|
|
67
|
+
*
|
|
68
|
+
* Снятое из рабочего дерева отсеивается здесь, а не в счётчиках: система контроля версий помнит
|
|
69
|
+
* файл, пока снос не заведён в историю, а прочитать его нечем — проверка падала трассировкой
|
|
70
|
+
* `ENOENT` и читалась как сломанная, хотя сломан был только незаведённый снос. Счётчиков два, и
|
|
71
|
+
* охрана при каждом разошлась бы.
|
|
72
|
+
*/
|
|
66
73
|
function trackedFiles() {
|
|
67
74
|
return execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 1024 * 1024 * 32 })
|
|
68
75
|
.split('\n')
|
|
69
|
-
.filter(Boolean)
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.filter((path) => existsSync(join(ROOT, path)));
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
function judged(path) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Слова, которых здесь не пишут: раздел «Так не пишем» словаря против дерева.
|
|
4
|
+
*
|
|
5
|
+
* Раздел лежал без единой проверки, и ровно поэтому расхождение росло годами: словарь звал
|
|
6
|
+
* службу одним словом, дерево — другим, и обе стороны выглядели действующими. Правило прямо
|
|
7
|
+
* велит брать слово из словаря или заводить его там же, а не краснело ничто — сверка адресов
|
|
8
|
+
* читает пути, сверка спеков читает сценарии, а словарь не читает никто.
|
|
9
|
+
*
|
|
10
|
+
* Проверка едет пакетом, а не пишется деревом: раздел «Так не пишем» везёт он же, и дерево,
|
|
11
|
+
* которое словарь получило, а проверки на него нет, живёт ровно с той дырой, ради которой она
|
|
12
|
+
* заведена. Слова при этом принадлежат словарю: пакет их не перечисляет, а читает из раздела.
|
|
13
|
+
* Общее для проверок — разбор списка известного и список пропускаемых каталогов — приезжает
|
|
14
|
+
* модулем настройки проверок, а не пишется здесь заново.
|
|
15
|
+
*
|
|
16
|
+
* Ищется левая колонка пар «слева запретное — справа принятое», и не вся: слово со скобочным
|
|
17
|
+
* уточнением («приём (о службе)») поиском не судится вовсе. Уточнение и означает, что запрещено
|
|
18
|
+
* одно значение из двух, а различить их в строке машине нечем: «операция приёма» законна, а
|
|
19
|
+
* «приём принимает груз» — нет, и обе строки для поиска одинаковы. Такое слово остаётся
|
|
20
|
+
* требованием к читателю; проверка о нём говорит вслух, чтобы молчание не читалось как охват.
|
|
21
|
+
*
|
|
22
|
+
* Сам словарь из поиска выведен: в нём запретное слово стоит по делу — тем и живёт левая
|
|
23
|
+
* колонка. Выведено и описание прошлого: оно по устройству называет то, чего в дереве уже нет.
|
|
24
|
+
*
|
|
25
|
+
* Ненулевой код возврата и перечень мест; накопленное к дню заведения проверки перечислено
|
|
26
|
+
* поимённо, с причиной и номером задачи.
|
|
27
|
+
*/
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import process from 'node:process';
|
|
32
|
+
|
|
33
|
+
import { CONFIG, ROOT, baselineOf, parseAllowlist } from './rt-kit-checks.config.mjs';
|
|
34
|
+
|
|
35
|
+
const GLOSSARY = 'docs/GLOSSARY.md';
|
|
36
|
+
const SECTION = '## Так не пишем';
|
|
37
|
+
/** Пара словаря: `- **слева** — справа`. Слева бывает несколько слов через запятую. */
|
|
38
|
+
const PAIR = /^-\s+\*\*(.+?)\*\*\s+—/;
|
|
39
|
+
/** Скобочное уточнение при слове: запрещено одно значение из двух, и поиском их не развести. */
|
|
40
|
+
const HINT = /\([^)]*\)\s*$/;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Что проверка не читает.
|
|
44
|
+
*
|
|
45
|
+
* Всякий словарь — здешний, его надстройка и источник в пакете — выведен по делу: левая колонка
|
|
46
|
+
* тем и живёт, что называет запретное слово вслух. Описание прошлого и папки задач выведены по
|
|
47
|
+
* устройству: первое перечисляет то, чего в дереве уже нет, вторая умирает со слиянием.
|
|
48
|
+
*/
|
|
49
|
+
const UNREAD = ['docs/archive/', 'docs/tasks/', 'CHANGELOG'];
|
|
50
|
+
const GLOSSARY_NAME = /(^|\/)GLOSSARY\.md$/;
|
|
51
|
+
|
|
52
|
+
/** Левая колонка раздела «Так не пишем»: слова, которых в дереве быть не должно. */
|
|
53
|
+
function forbiddenWords(text) {
|
|
54
|
+
const lines = text.split('\n');
|
|
55
|
+
const from = lines.findIndex((line) => line.trim() === SECTION);
|
|
56
|
+
if (from < 0) {
|
|
57
|
+
return { words: [], byReader: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const words = [];
|
|
61
|
+
const byReader = [];
|
|
62
|
+
for (const line of lines.slice(from + 1)) {
|
|
63
|
+
if (line.startsWith('## ')) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const found = PAIR.exec(line);
|
|
67
|
+
if (!found) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const part of found[1].split(',')) {
|
|
71
|
+
const word = part.trim();
|
|
72
|
+
if (!word) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (HINT.test(word)) {
|
|
76
|
+
byReader.push(word);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
words.push(word);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { words, byReader };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Файлы дерева, которые проверка читает. Список берётся у системы контроля версий. */
|
|
87
|
+
function readableFiles() {
|
|
88
|
+
const listed = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], {
|
|
89
|
+
cwd: ROOT,
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
92
|
+
});
|
|
93
|
+
if (listed.status !== 0) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const skip = CONFIG.skipDirs ?? [];
|
|
98
|
+
|
|
99
|
+
return listed.stdout
|
|
100
|
+
.split('\n')
|
|
101
|
+
.map((line) => line.trim())
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.filter((file) => file.endsWith('.md'))
|
|
104
|
+
.filter((file) => !GLOSSARY_NAME.test(file))
|
|
105
|
+
.filter((file) => !UNREAD.some((where) => file === where || file.startsWith(where) || file.includes(`/${where}`)))
|
|
106
|
+
.filter((file) => !skip.some((where) => file.includes(`/${where}/`) || file.startsWith(`${where}/`)));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function main() {
|
|
110
|
+
let glossary;
|
|
111
|
+
try {
|
|
112
|
+
glossary = readFileSync(join(ROOT, GLOSSARY), 'utf8');
|
|
113
|
+
} catch {
|
|
114
|
+
console.log(`check-glossary: словаря нет по адресу ${GLOSSARY} — сверять нечем`);
|
|
115
|
+
|
|
116
|
+
return Number(process.env.RT_SKIP_CODE ?? 7);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { words, byReader } = forbiddenWords(glossary);
|
|
120
|
+
if (!words.length && !byReader.length) {
|
|
121
|
+
console.log('check-glossary: раздела запретных слов в словаре нет — сверять нечем');
|
|
122
|
+
|
|
123
|
+
return Number(process.env.RT_SKIP_CODE ?? 7);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const parsed = parseAllowlist('glossary');
|
|
127
|
+
const found = [];
|
|
128
|
+
for (const file of readableFiles()) {
|
|
129
|
+
let text;
|
|
130
|
+
try {
|
|
131
|
+
text = readFileSync(join(ROOT, file), 'utf8');
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const [index, line] of text.split('\n').entries()) {
|
|
136
|
+
for (const word of words) {
|
|
137
|
+
if (!new RegExp(`(^|[^\\p{L}])${word}([^\\p{L}]|$)`, 'iu').test(line)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
found.push({ key: `${file}:${word}`, file, line: index + 1, word });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const fresh = baselineOf(
|
|
146
|
+
found.map((one) => one.key),
|
|
147
|
+
parsed,
|
|
148
|
+
);
|
|
149
|
+
const news = found.filter((one) => fresh.includes(one.key));
|
|
150
|
+
|
|
151
|
+
if (news.length) {
|
|
152
|
+
console.error(`check-glossary: расхождений ${news.length}`);
|
|
153
|
+
for (const one of news) {
|
|
154
|
+
console.error(` ${one.file}:${one.line} — «${one.word}»: слово стоит в разделе «Так не пишем» словаря`);
|
|
155
|
+
}
|
|
156
|
+
console.error(' либо слово меняется на принятое здесь, либо словарь перестаёт его запрещать');
|
|
157
|
+
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`check-glossary: запретных слов ${words.length}, читано документов ${readableFiles().length}, расхождений нет`);
|
|
162
|
+
if (byReader.length) {
|
|
163
|
+
// Молчание о невыполнимом поиске читалось бы как охват: эти слова не судит ничто.
|
|
164
|
+
console.log(` поиском не судятся, остаются требованием к читателю: ${byReader.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.exit(main());
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Сверка объявления гарда с тем, что разбирает его тело.
|
|
4
|
+
*
|
|
5
|
+
* Гард несёт своё событие и образец вызова сам — строкой `# rt-hook:` во второй строке файла; по
|
|
6
|
+
* ней его подписывают в настройке агента. Тело при этом умеет больше, чем объявлено: разбирает
|
|
7
|
+
* имя инструмента, которого в образце нет, — и до этой ветки вызов не доходит никогда. Снаружи
|
|
8
|
+
* гард выглядит работающим: путь назван, файл разложен, набор сценариев зелёный, потому что зовёт
|
|
9
|
+
* гард напрямую с подставленным вводом и объявления не читает вовсе.
|
|
10
|
+
*
|
|
11
|
+
* Хуже того, расхождение читается как промах дерева. Дерево, подписавшее гард шире объявления,
|
|
12
|
+
* получает отказ сверки раскладки и сужает подписку до объявления — вместе с расхождением снимая
|
|
13
|
+
* работавшее покрытие. Чинится это в пакете, а платит за него дерево.
|
|
14
|
+
*
|
|
15
|
+
* Что сверяется: имена инструментов из веток `case` по имени инструмента против образца
|
|
16
|
+
* объявления. Комментарии и тексты отказов не читаются: имя инструмента упоминают и там, а
|
|
17
|
+
* судится то, на что гард ветвится.
|
|
18
|
+
*
|
|
19
|
+
* FAIL-OPEN: каталога хуков нет — сверять нечего, нулевой код.
|
|
20
|
+
*
|
|
21
|
+
* Ненулевой код возврата и перечень расхождений.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
|
|
26
|
+
const ROOT = process.cwd();
|
|
27
|
+
const HOOKS = join(ROOT, '.claude/hooks');
|
|
28
|
+
|
|
29
|
+
/** Объявление гарда: событие и образец вызова. */
|
|
30
|
+
const DECLARATION = /^#\s*rt-hook:\s*(\S+)(?:[ \t]+(\S.*))?$/m;
|
|
31
|
+
|
|
32
|
+
/** Ветка `case` по имени инструмента: `Bash | mcp__webstorm__execute_tool)`. */
|
|
33
|
+
const TOOL_CASE = /^[ \t]*([A-Za-z_][\w-]*(?:__[\w-]+)*(?:[ \t]*\|[ \t]*[A-Za-z_][\w-]*(?:__[\w-]+)*)*)\)/;
|
|
34
|
+
|
|
35
|
+
/** Имена инструментов, на которые ветвится тело гарда. */
|
|
36
|
+
function branchedTools(text) {
|
|
37
|
+
const lines = text.split('\n');
|
|
38
|
+
const tools = new Set();
|
|
39
|
+
let inside = false;
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const code = line.replace(/^([^#]*)#.*$/, '$1');
|
|
43
|
+
|
|
44
|
+
if (/case[ \t]+"?\$(\{)?(tool|RT_HOOK_TOOL)/.test(code) || /case[ \t]+"\$\(rt_hook_tool\)"/.test(code)) {
|
|
45
|
+
inside = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (inside && /^[ \t]*esac\b/.test(code)) {
|
|
50
|
+
inside = false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!inside) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const branch = code.match(TOOL_CASE);
|
|
59
|
+
if (branch) {
|
|
60
|
+
for (const name of branch[1].split('|')) {
|
|
61
|
+
const tool = name.trim();
|
|
62
|
+
// Звёздочка — ветка «всё остальное», именем инструмента она не является.
|
|
63
|
+
if (tool && tool !== '*') {
|
|
64
|
+
tools.add(tool);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...tools];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Покрывает ли образец объявления это имя инструмента. */
|
|
74
|
+
function covers(matcher, tool) {
|
|
75
|
+
try {
|
|
76
|
+
return new RegExp(`^(?:${matcher})$`).test(tool);
|
|
77
|
+
} catch {
|
|
78
|
+
// Образец, который не разобрать, — своё расхождение, и молчать о нём нельзя: имя
|
|
79
|
+
// инструмента под ним не совпадёт ни с чем, а выглядит объявление написанным.
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
if (!existsSync(HOOKS)) {
|
|
86
|
+
console.log('check-hook-scope: каталога хуков в дереве нет — сверять нечего');
|
|
87
|
+
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const faults = [];
|
|
92
|
+
|
|
93
|
+
for (const file of readdirSync(HOOKS).filter((one) => one.endsWith('.sh')).sort()) {
|
|
94
|
+
const text = readFileSync(join(HOOKS, file), 'utf8');
|
|
95
|
+
const declared = text.match(DECLARATION);
|
|
96
|
+
if (!declared || !declared[2]) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const matcher = declared[2].trim();
|
|
101
|
+
for (const tool of branchedTools(text)) {
|
|
102
|
+
if (!covers(matcher, tool)) {
|
|
103
|
+
faults.push(`${file}: тело ветвится на «${tool}», а объявление его не называет`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (faults.length > 0) {
|
|
109
|
+
console.log(`check-hook-scope: расхождений ${faults.length}\n`);
|
|
110
|
+
|
|
111
|
+
for (const fault of faults) {
|
|
112
|
+
console.log(` ${fault}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('\nВетка тела, которой нет в объявлении, не исполняется ни разу: под этим именем гард');
|
|
116
|
+
console.log('не зовут. Объявление правится в источнике пакета, а не в разложенной копии.');
|
|
117
|
+
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('check-hook-scope: объявления гардов покрывают то, на что ветвятся их тела — сошлось');
|
|
122
|
+
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.exit(main());
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Сверка надстроек профиля с таблицами компаньонов.
|
|
4
|
+
*
|
|
5
|
+
* Правило говорит одно, а дерево работает по-другому — и оба места законны. Расхождение
|
|
6
|
+
* объявляется переменной профиля, а читатель ищет его в компаньоне правила, где перечислено, чем
|
|
7
|
+
* здесь зовётся сказанное правилом. Пока эти два места никто не сравнивает, компаньон обещает
|
|
8
|
+
* пакетное умолчание там, где дерево давно работает по-своему: правило требовало одной формы
|
|
9
|
+
* заголовка заявки, дерево замещало её переменной, и гард, читавший форму, номера не доставал
|
|
10
|
+
* вовсе — сверка номера молча не выполнялась, выглядя сошедшейся.
|
|
11
|
+
*
|
|
12
|
+
* Что сверяется: имя каждой переменной профиля, чьё значение разошлось с умолчанием пакета,
|
|
13
|
+
* против текстов компаньонов правил. Значение не сверяется ни с чем: сказать, верно ли оно,
|
|
14
|
+
* машине нечем, а назвать замещённое в компаньоне — можно.
|
|
15
|
+
*
|
|
16
|
+
* FAIL-OPEN: нет профиля, нет умолчаний, нет каталога правил — сверять нечего, нулевой код.
|
|
17
|
+
* Дерево вправе не держать ни того, ни другого.
|
|
18
|
+
*
|
|
19
|
+
* Ненулевой код возврата и перечень расхождений.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { basename, join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
const ROOT = process.cwd();
|
|
26
|
+
const PROFILE = join(ROOT, '.claude/rt-kit/project.sh');
|
|
27
|
+
const DEFAULTS = join(ROOT, '.claude/rt-kit/defaults/project.sh');
|
|
28
|
+
const RULES = join(ROOT, '.claude/skills');
|
|
29
|
+
|
|
30
|
+
/** Присвоение верхнего уровня: `RT_ИМЯ='значение'` либо `RT_ИМЯ="значение"`. */
|
|
31
|
+
const ASSIGN = /^(RT_[A-Z0-9_]+)=(.*)$/;
|
|
32
|
+
|
|
33
|
+
/** Умолчание пакета: `RT_ИМЯ="${RT_ИМЯ:-значение}"` — значение берётся из подстановки. */
|
|
34
|
+
const FALLBACK = /^RT_[A-Z0-9_]+="\$\{RT_[A-Z0-9_]+:-(.*)\}"$/;
|
|
35
|
+
|
|
36
|
+
/** Значение без окружающих кавычек: сравниваются строки, а не их запись. */
|
|
37
|
+
function unquote(text) {
|
|
38
|
+
const trimmed = text.trim();
|
|
39
|
+
const paired = trimmed.length > 1 && (trimmed.startsWith("'") || trimmed.startsWith('"')) && trimmed.endsWith(trimmed[0]);
|
|
40
|
+
|
|
41
|
+
return paired ? trimmed.slice(1, -1) : trimmed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Карта «имя переменной → значение» по присвоениям верхнего уровня. */
|
|
45
|
+
function valuesOf(path) {
|
|
46
|
+
const values = new Map();
|
|
47
|
+
|
|
48
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
49
|
+
const assign = line.match(ASSIGN);
|
|
50
|
+
if (!assign) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fallback = line.match(FALLBACK);
|
|
55
|
+
values.set(assign[1], fallback ? unquote(fallback[1]) : unquote(assign[2]));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return values;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Тексты всех компаньонов правил дерева, склеенные в один: ищется в них имя, а не место. */
|
|
62
|
+
function companionsText() {
|
|
63
|
+
if (!existsSync(RULES)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const texts = [];
|
|
68
|
+
|
|
69
|
+
for (const rule of readdirSync(RULES, { withFileTypes: true })) {
|
|
70
|
+
if (!rule.isDirectory()) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const companion = join(RULES, rule.name, 'implementation.md');
|
|
75
|
+
if (existsSync(companion)) {
|
|
76
|
+
texts.push(readFileSync(companion, 'utf8'));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return texts.length > 0 ? texts.join('\n') : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Набор гейта пуша, собранный оболочкой: с надстройкой дерева и без неё.
|
|
85
|
+
*
|
|
86
|
+
* Собирается вызовом, а не чтением текста: набор — функция, и она смотрит на дерево — есть ли
|
|
87
|
+
* настройка раскладки, лежит ли конфиг оформления, исполним ли набор сценариев. Прочитанный
|
|
88
|
+
* текстом, он назвал бы командами то, чего в этом дереве нет вовсе.
|
|
89
|
+
*
|
|
90
|
+
* Пусто — собрать нечем: нет оболочки, нет функции, отказ вызова. Сверять тогда нечего.
|
|
91
|
+
*/
|
|
92
|
+
function gateSet(withProfile) {
|
|
93
|
+
const source = withProfile ? `. '${DEFAULTS}'; . '${PROFILE}';` : `. '${DEFAULTS}';`;
|
|
94
|
+
const run = spawnSync('bash', ['-c', `${source} command -v rt_push_checks >/dev/null 2>&1 || exit 9; rt_push_checks ''`], {
|
|
95
|
+
cwd: ROOT,
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return run.status === 0 && typeof run.stdout === 'string' ? run.stdout.split('\n').filter(Boolean) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Проверки, названные в наборе, — по имени файла, а не по всей строке команды.
|
|
104
|
+
*
|
|
105
|
+
* Строка целиком сверке не годится: дерево вправе позвать ту же проверку другим запускателем или
|
|
106
|
+
* с другим доводом, и расхождением это не является. Пропажа самой проверки — является.
|
|
107
|
+
*/
|
|
108
|
+
function checksIn(lines) {
|
|
109
|
+
const names = new Set();
|
|
110
|
+
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
for (const [word] of line.matchAll(/[\w./-]+\.(?:mjs|sh)/g)) {
|
|
113
|
+
names.add(basename(word));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return names;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Проверки, которые умолчание пакета зовёт, а набор дерева — нет.
|
|
122
|
+
*
|
|
123
|
+
* Набор гейта собирается умолчанием и надстройкой, и надстройка вправе объявить функцию заново.
|
|
124
|
+
* Выкушенная так проверка ничем не отличима от проверки, которой в дереве нет вовсе: гейт зелен,
|
|
125
|
+
* потому что её никто не звал, а сводка раскладки о наборе не знает ничего — она сличает
|
|
126
|
+
* переменные, а тут заменена функция.
|
|
127
|
+
*
|
|
128
|
+
* Отказ в пользу работы: собрать набор нечем — строк нет, и сверка идёт дальше своим делом.
|
|
129
|
+
*/
|
|
130
|
+
function cutFromGate() {
|
|
131
|
+
const packaged = gateSet(false);
|
|
132
|
+
const here = gateSet(true);
|
|
133
|
+
if (packaged === null || here === null) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mine = checksIn(here);
|
|
138
|
+
|
|
139
|
+
return [...checksIn(packaged)].filter((name) => !mine.has(name)).sort((first, second) => first.localeCompare(second, 'ru'));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function main() {
|
|
143
|
+
if (!existsSync(PROFILE) || !existsSync(DEFAULTS)) {
|
|
144
|
+
console.log('check-profile-drift: профиля дерева или умолчаний пакета нет — сверять нечего');
|
|
145
|
+
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const companions = companionsText();
|
|
150
|
+
if (companions === null) {
|
|
151
|
+
console.log('check-profile-drift: компаньонов правил в дереве нет — сверять не с чем');
|
|
152
|
+
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const cut = cutFromGate();
|
|
157
|
+
if (cut.length > 0) {
|
|
158
|
+
console.log(`check-profile-drift: набор гейта пуша не зовёт проверок умолчания: ${cut.length}\n`);
|
|
159
|
+
|
|
160
|
+
for (const name of cut) {
|
|
161
|
+
console.log(` ${name}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log('\nНабор гейта собирается умолчанием пакета и надстройкой дерева. Проверка, выкушенная');
|
|
165
|
+
console.log('надстройкой, ничем не отличима от проверки, которой в дереве нет: гейт зелен потому,');
|
|
166
|
+
console.log('что её никто не звал. Верни её в набор либо объясни отказ в компаньоне правила поставки.');
|
|
167
|
+
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const defaults = valuesOf(DEFAULTS);
|
|
172
|
+
const profile = valuesOf(PROFILE);
|
|
173
|
+
const overridden = [...profile].filter(([name, value]) => !defaults.has(name) || defaults.get(name) !== value);
|
|
174
|
+
const unnamed = overridden.filter(([name]) => !companions.includes(name));
|
|
175
|
+
|
|
176
|
+
if (unnamed.length > 0) {
|
|
177
|
+
console.log(`check-profile-drift: замещено ${overridden.length}, не названо компаньоном ${unnamed.length}\n`);
|
|
178
|
+
|
|
179
|
+
for (const [name, value] of unnamed) {
|
|
180
|
+
console.log(` ${name} = ${value.length > 60 ? `${value.slice(0, 57)}…` : value}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log('\nЗамещённое читают в компаньоне правила: там перечислено, чем здесь зовётся сказанное');
|
|
184
|
+
console.log('правилом. Не названное там, оно оставляет читателю пакетное умолчание вместо того, по');
|
|
185
|
+
console.log('чему дерево работает на самом деле.');
|
|
186
|
+
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`check-profile-drift: замещено ${overridden.length}, все названы компаньонами — сошлось`);
|
|
191
|
+
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
process.exit(main());
|