@rt-tools/agent-kit 0.14.0 → 0.16.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 +17 -0
- package/assets/checks/archive-age.mjs +107 -0
- package/assets/checks/archive-prune.mjs +46 -0
- package/assets/checks/check-archive-age.mjs +42 -0
- package/assets/checks/check-board.github.mjs +16 -0
- package/assets/checks/check-descriptions.mjs +123 -0
- package/assets/checks/check-dupes.mjs +31 -3
- package/assets/checks/check-file-size.mjs +47 -2
- package/assets/checks/check-turn-map.mjs +20 -3
- package/assets/checks/lib-common.mjs +12 -1
- package/assets/checks/lib-domains.mjs +1 -1
- package/assets/checks/rt-kit-checks.config.mjs +24 -1
- package/assets/checks/spec-anchors.mjs +18 -3
- package/assets/checks/spec-common.mjs +5 -1
- package/assets/defaults/project.sh +22 -17
- package/assets/defaults/turn-map.md +15 -19
- package/assets/docs/GLOSSARY.md +52 -58
- package/assets/hooks/rule-article.sh +12 -0
- package/assets/hooks/skill-gate.sh +5 -4
- package/assets/hooks/task-flow-guard.sh +20 -0
- package/assets/hooks/turn-exit-guard.sh +229 -4
- package/assets/laws/delivery.md +92 -104
- package/assets/laws/frontend-application.md +4 -0
- package/assets/laws/project-documentation.md +64 -68
- package/assets/laws/verifiability.md +32 -33
- package/assets/laws/work-conduct.md +167 -157
- package/assets/patterns/doc-style-sweep.md +1 -1
- package/assets/patterns/doc-style-trace.md +1 -1
- package/assets/patterns/git-workflow-commit.azure.md +1 -1
- package/assets/patterns/git-workflow-commit.github.md +7 -1
- package/assets/patterns/git-workflow-commit.gitlab.md +1 -1
- package/assets/patterns/git-workflow-docker.md +1 -1
- package/assets/patterns/git-workflow-merge.md +14 -3
- 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/git-workflow-restart.md +1 -1
- package/assets/patterns/git-workflow-secrets.md +1 -1
- package/assets/patterns/git-workflow-stack.md +93 -0
- package/assets/patterns/seo-page.md +1 -1
- package/assets/patterns/spec-driven-rule.md +55 -0
- package/assets/patterns/status-report-table.github.md +88 -0
- package/assets/patterns/task-flow-archive.md +3 -4
- package/assets/patterns/task-flow-close.md +6 -1
- package/assets/patterns/task-flow-start.md +17 -5
- package/assets/patterns/ts-procedure.md +1 -1
- package/assets/pitfalls/doc-style.md +5 -0
- package/assets/pitfalls/git-workflow.github.md +47 -0
- package/assets/pitfalls/task-flow.md +28 -0
- package/assets/pitfalls/testing.md +14 -0
- package/assets/pitfalls/turn-conduct.md +33 -0
- package/assets/rules/angular-patterns.md +1 -1
- package/assets/rules/api-layer.md +3 -3
- package/assets/rules/browser-verification.md +15 -1
- package/assets/rules/dependencies.md +1 -1
- package/assets/rules/deploy-flow.azure.md +1 -1
- package/assets/rules/deploy-flow.github.md +1 -1
- package/assets/rules/deploy-flow.gitlab.md +1 -1
- package/assets/rules/doc-style.md +18 -0
- package/assets/rules/entity-conventions.needs-admin.md +1 -1
- package/assets/rules/entity-models.md +1 -1
- package/assets/rules/git-workflow.azure.md +1 -1
- package/assets/rules/git-workflow.github.md +154 -181
- package/assets/rules/git-workflow.gitlab.md +1 -1
- package/assets/rules/lib-layers.md +1 -1
- package/assets/rules/observability.needs-app.md +1 -1
- package/assets/rules/platform-access.md +1 -1
- package/assets/rules/reuse-first.md +1 -1
- package/assets/rules/seo.md +4 -3
- package/assets/rules/shared-code.md +1 -1
- package/assets/rules/spec-driven.md +68 -1
- package/assets/rules/status-report.md +97 -0
- package/assets/rules/styling-bem.md +12 -0
- package/assets/rules/task-flow.md +102 -100
- package/assets/rules/testing.md +67 -66
- package/assets/rules/turn-conduct.md +146 -105
- package/assets/rules/turn-entry.md +7 -1
- package/assets/rules/typescript-conventions.md +1 -1
- package/assets/skills/agent-kit-extend.md +1 -1
- package/assets/skills/agent-kit.md +18 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +25 -0
- package/bin/agent-kit.js.map +1 -1
- package/lib/cost.d.ts +44 -0
- package/lib/cost.d.ts.map +1 -0
- package/lib/cost.js +181 -0
- package/lib/cost.js.map +1 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.16.0.tgz +0 -0
- package/rt-tools-agent-kit-0.14.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ npx agent-kit sync # разложить выбранное в docs/constitu
|
|
|
27
27
|
npx agent-kit doctor # что разложено, что отстало, чего не хватает
|
|
28
28
|
npx agent-kit adopt # отдать пакету файлы, лежащие на его путях не от него
|
|
29
29
|
npx agent-kit stats # чем пользовались, чем ни разу, обо что спотыкались
|
|
30
|
+
npx agent-kit cost # сколько весит вход в работу, одно правило и весь слой
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
`init` называет и то, чего пакет ждёт от дерева: значения дырок, которые придётся вписать в
|
|
@@ -274,6 +275,22 @@ npx agent-kit stats --json # то же машиночитаемо
|
|
|
274
275
|
Самая ценная строка сводки — не «чем пользовались», а **что разложено и не загружено ни разу**:
|
|
275
276
|
чем пользуются, видно и по работе, а мёртвый ресурс ничем себя не выдаёт.
|
|
276
277
|
|
|
278
|
+
## Цена контекста
|
|
279
|
+
|
|
280
|
+
Сводка говорит, чем пользовались; цена — сколько это стоило. Заход платит за слой правил окном,
|
|
281
|
+
и до этой команды вес того, что он получает целиком, не считало ничто.
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npx agent-kit cost # вход в работу, самое тяжёлое правило, весь слой
|
|
285
|
+
npx agent-kit cost --rule <имя> # взвесить названное правило
|
|
286
|
+
npx agent-kit cost --json # то же машиночитаемо — этим числа кладут в замысел
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Считается не файл, а то, что заход получает: описание правила приходит ему полем, словарь и
|
|
290
|
+
карта хода — выводом хуков. Меряется в символах и байтах — тем, что берётся на месте, без сети.
|
|
291
|
+
Число сравнимо только с числом, снятым той же командой; чем считано, стоит в первой строке
|
|
292
|
+
вывода.
|
|
293
|
+
|
|
277
294
|
Наблюдения лежат в `.claude/rt-kit/observations/` файлом на день и снимаются через тридцать
|
|
278
295
|
дней. Запись выключается ключом `"observe": false` в конфиге — целиком, а не частями. Каталог
|
|
279
296
|
просится в список игнорируемого; вписывает его проект — в чужие файлы дерева пакет не пишет.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Возраст записей описания прошлого.
|
|
3
|
+
*
|
|
4
|
+
* Каталог набирает по записи на каждую закрытую работу и не отдаёт обратно ничего: папку
|
|
5
|
+
* задачи разбирает паттерн закрытия работы, и запись оттуда переживает всё дерево. Срок
|
|
6
|
+
* назначает дерево ключом настройки; пакет умолчания не даёт — установка новой версии не
|
|
7
|
+
* вправе начать сносить чужой архив, а снимается там разбор просьбы, которого нет больше нигде.
|
|
8
|
+
*
|
|
9
|
+
* Отбор живёт здесь один на двоих: команда чистки снимает по нему, проверка по нему же
|
|
10
|
+
* краснеет. Разойдясь, они говорили бы о каталоге разное, а заметить это нечем — чистка молча
|
|
11
|
+
* оставляла бы то, на что проверка молча не смотрит.
|
|
12
|
+
*
|
|
13
|
+
* Возраст меряется датой последнего коммита файла. Время файла на диске не годится: свежий
|
|
14
|
+
* чекаут делает все записи одновременными, и чистка на чужой машине не сняла бы ни одной.
|
|
15
|
+
* Шапка записи не годится тоже — день слияния стоит в ней не всегда и не всюду одинаково.
|
|
16
|
+
*
|
|
17
|
+
* Запись, которой в истории ещё нет, считается сегодняшней: она приехала этой же веткой и
|
|
18
|
+
* перестоять не могла.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
|
|
24
|
+
import { CONFIG } from './rt-kit-checks.config.mjs';
|
|
25
|
+
|
|
26
|
+
/** Каталог описания прошлого — без завершающей косой черты: её несёт настройка. */
|
|
27
|
+
export const ARCHIVE_DIR = CONFIG.archiveDir.replace(/\/$/, '');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Срок хранения записи в сутках. `null` — срок не назначен, и тогда молчат обе стороны:
|
|
31
|
+
* проверка не краснеет, чистка не снимает. Дерево называет своё число ключом настройки.
|
|
32
|
+
*/
|
|
33
|
+
export const RETENTION_DAYS = CONFIG.archiveRetentionDays ?? null;
|
|
34
|
+
|
|
35
|
+
/** Сутки в миллисекундах — считать возраст удобнее в них. */
|
|
36
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Дата последнего коммита у каждой записи каталога.
|
|
40
|
+
*
|
|
41
|
+
* Один проход по истории вместо вызова на файл: на трёх сотнях записей это разница между
|
|
42
|
+
* секундой и полуминутой. Лог идёт новыми вперёд, поэтому первая встреченная дата файла и есть
|
|
43
|
+
* последняя.
|
|
44
|
+
*/
|
|
45
|
+
function lastCommitDates(root) {
|
|
46
|
+
const log = execFileSync('git', ['log', '--format=%cI', '--name-only', '--', ARCHIVE_DIR], {
|
|
47
|
+
cwd: root,
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const dates = new Map();
|
|
53
|
+
let current = null;
|
|
54
|
+
|
|
55
|
+
for (const line of log.split('\n')) {
|
|
56
|
+
if (line === '') {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (line.startsWith(`${ARCHIVE_DIR}/`)) {
|
|
61
|
+
if (current !== null && !dates.has(line)) {
|
|
62
|
+
dates.set(line, current);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
current = line;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return dates;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Записи каталога с их возрастом в сутках.
|
|
76
|
+
*
|
|
77
|
+
* @param root Корень дерева.
|
|
78
|
+
* @param now Момент отсчёта — передаётся, чтобы проверка и чистка судили по одному времени.
|
|
79
|
+
* @returns Записи: путь, дата последнего коммита и возраст в сутках.
|
|
80
|
+
*/
|
|
81
|
+
export function archiveRecords(root, now = new Date()) {
|
|
82
|
+
if (!existsSync(join(root, ARCHIVE_DIR))) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dates = lastCommitDates(root);
|
|
87
|
+
|
|
88
|
+
return readdirSync(join(root, ARCHIVE_DIR))
|
|
89
|
+
.filter((name) => name.endsWith('.md'))
|
|
90
|
+
.map((name) => {
|
|
91
|
+
const path = `${ARCHIVE_DIR}/${name}`;
|
|
92
|
+
const committed = dates.get(path);
|
|
93
|
+
const ageDays = committed === undefined ? 0 : (now.getTime() - new Date(committed).getTime()) / DAY_MS;
|
|
94
|
+
|
|
95
|
+
return { path, name, committed, ageDays };
|
|
96
|
+
})
|
|
97
|
+
.sort((one, other) => other.ageDays - one.ageDays);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Записи, перестоявшие срок. Срок не назначен — перестоявших нет ни одной. */
|
|
101
|
+
export function staleRecords(root, now = new Date()) {
|
|
102
|
+
if (RETENTION_DAYS === null) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return archiveRecords(root, now).filter((record) => record.ageDays > RETENTION_DAYS);
|
|
107
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Чистка описания прошлого по сроку.
|
|
4
|
+
*
|
|
5
|
+
* Запись живёт назначенный деревом срок, дальше снимается из дерева и остаётся в истории
|
|
6
|
+
* системы контроля версий — достать её оттуда можно по имени файла, оно же единственный
|
|
7
|
+
* указатель каталога.
|
|
8
|
+
*
|
|
9
|
+
* Сухой прогон — умолчание. Команда сносит разбор просьбы — единственную запись слов
|
|
10
|
+
* владельца, — и снос называется явно: `--apply`. Перечень снимаемого печатается в обоих
|
|
11
|
+
* случаях одинаково, чтобы решение принималось по тому же списку, который потом уедет.
|
|
12
|
+
*
|
|
13
|
+
* Срок и отбор берутся у `archive-age.mjs` — того же модуля, по которому краснеет проверка.
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
import { RETENTION_DAYS, archiveRecords, staleRecords } from './archive-age.mjs';
|
|
18
|
+
import { ROOT } from './rt-kit-checks.config.mjs';
|
|
19
|
+
|
|
20
|
+
if (RETENTION_DAYS === null) {
|
|
21
|
+
console.log('archive-prune: срок хранения описания прошлого деревом не назначен — снимать нечего');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const apply = process.argv.includes('--apply');
|
|
26
|
+
const stale = staleRecords(ROOT);
|
|
27
|
+
const total = archiveRecords(ROOT).length;
|
|
28
|
+
|
|
29
|
+
if (stale.length === 0) {
|
|
30
|
+
console.log(`archive-prune: записей ${total}, перестоявших срок в ${RETENTION_DAYS} суток нет`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`archive-prune: перестояло ${stale.length} из ${total} при сроке в ${RETENTION_DAYS} суток`);
|
|
35
|
+
|
|
36
|
+
for (const record of stale) {
|
|
37
|
+
console.log(` ${record.name} — ${Math.floor(record.ageDays)} суток, последний коммит ${record.committed.slice(0, 10)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!apply) {
|
|
41
|
+
console.log('\nЭто сухой прогон: не снято ничего. Снос идёт доводом --apply.');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
execFileSync('git', ['rm', '--quiet', '--', ...stale.map((record) => record.path)], { cwd: ROOT, stdio: 'inherit' });
|
|
46
|
+
console.log(`\nСнято записей: ${stale.length}. Они остаются в истории — найти их можно по имени файла.`);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Сверка срока хранения описания прошлого.
|
|
4
|
+
*
|
|
5
|
+
* Одной чистки хватает ровно на срок: каталог набирает по записи на каждую закрытую работу, и
|
|
6
|
+
* первая уцелевшая перестаивает снова. Без сверки срок держался бы памятью того, кто помнит
|
|
7
|
+
* про команду чистки, — то есть не держался бы вовсе.
|
|
8
|
+
*
|
|
9
|
+
* Сверка не чистит: снос — решение, а не следствие проверки. Она называет перестоявшие записи
|
|
10
|
+
* и команду, которой их снимают.
|
|
11
|
+
*
|
|
12
|
+
* FAIL-OPEN: срок деревом не назначен — сверять нечего, нулевой код. Умолчания у срока нет
|
|
13
|
+
* намеренно: пакет, назначивший его за дерево, начал бы сносить чужой архив в день установки.
|
|
14
|
+
* Каталога нет — то же самое. Пустой каталог отказом не считается: он означает, что всё снято
|
|
15
|
+
* по сроку.
|
|
16
|
+
*
|
|
17
|
+
* Ненулевой код возврата и перечень перестоявших записей.
|
|
18
|
+
*/
|
|
19
|
+
import { ARCHIVE_DIR, RETENTION_DAYS, archiveRecords, staleRecords } from './archive-age.mjs';
|
|
20
|
+
import { ROOT } from './rt-kit-checks.config.mjs';
|
|
21
|
+
|
|
22
|
+
if (RETENTION_DAYS === null) {
|
|
23
|
+
console.log('check-archive-age: срок хранения описания прошлого деревом не назначен — сверять нечего');
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const total = archiveRecords(ROOT).length;
|
|
28
|
+
const stale = staleRecords(ROOT);
|
|
29
|
+
|
|
30
|
+
if (stale.length > 0) {
|
|
31
|
+
console.error(`check-archive-age: расхождений ${stale.length}`);
|
|
32
|
+
|
|
33
|
+
for (const record of stale) {
|
|
34
|
+
console.error(` ${record.path}: ${Math.floor(record.ageDays)} суток при сроке в ${RETENTION_DAYS}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.error(`\nЗапись живёт ${RETENTION_DAYS} суток и снимается: \`node tools/archive-prune.mjs --apply\`.`);
|
|
38
|
+
console.error('Снятая остаётся в истории — найти её можно по имени файла.');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`check-archive-age: записей ${total} в ${ARCHIVE_DIR}, ни одна не перестояла срок в ${RETENTION_DAYS} суток`);
|
|
@@ -138,6 +138,12 @@ function folderInBranch(branch, options) {
|
|
|
138
138
|
*
|
|
139
139
|
* Свежая вершина не судится: между пушем и прогоном проходит время, и красная строка на этом
|
|
140
140
|
* промежутке значила бы «подожди», а не «чини».
|
|
141
|
+
*
|
|
142
|
+
* У конфликтующей заявки прогона не бывает вовсе, и причина не в потерянном событии: конвейер
|
|
143
|
+
* проверяет слияние ветки с базой, а слияния при конфликте нет. Совет вернуть событие
|
|
144
|
+
* выполняется буквально и не помогает — за один заход заявка перезакрывалась дважды подряд, и
|
|
145
|
+
* прогон встал только после вливания главной ветки. Строка поэтому называет ту причину, которая
|
|
146
|
+
* чинится.
|
|
141
147
|
*/
|
|
142
148
|
function checkHeadRun(pull, options) {
|
|
143
149
|
if (checkEvicted(pull, options)) {
|
|
@@ -154,6 +160,16 @@ function checkHeadRun(pull, options) {
|
|
|
154
160
|
return;
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
if (pull.mergeable === 'CONFLICTING') {
|
|
164
|
+
report(
|
|
165
|
+
`PR #${pull.number}: на вершине ${pull.headRefOid.slice(0, 8)} прогона нет и не будет, пока она конфликтует — ` +
|
|
166
|
+
`конвейер проверяет слияние ветки с базой, а слияния при конфликте нет; влей главную ветку и запушь, ` +
|
|
167
|
+
`перезакрытие PR тут не помогает`
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
report(
|
|
158
174
|
`PR #${pull.number}: на вершине ${pull.headRefOid.slice(0, 8)} прогона нет, а лежит она ${minutes} мин — ` +
|
|
159
175
|
`конвейер события не получил; верни его новым коммитом либо перезакрытием PR ` +
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Сверка длины описаний правил и паттернов.
|
|
4
|
+
*
|
|
5
|
+
* Описание едет в системный промпт каждого захода — все, сколько их есть в дереве, — и
|
|
6
|
+
* платит их заход, чем бы ни занимался. Тем оно и отличается от тела правила: тело
|
|
7
|
+
* исполнитель читает сам и платит за это ходом, описание приходит даром. Даром — пока
|
|
8
|
+
* оно короткое.
|
|
9
|
+
*
|
|
10
|
+
* Растёт оно само: описание пишут вслед за правилом и пересказывают в нём содержимое.
|
|
11
|
+
* Ни одна проверка длины не считала, и на дереве, где эта сверка заводилась, сорок
|
|
12
|
+
* описаний из семидесяти четырёх переросли предел.
|
|
13
|
+
*
|
|
14
|
+
* Отвечает описание на один вопрос — брать это правило или нет. Всё, что отвечает на
|
|
15
|
+
* вопрос «а что там внутри», приходит вторым разом вместе с самим правилом.
|
|
16
|
+
*
|
|
17
|
+
* FAIL-OPEN: каталога скилов в дереве нет — сверять нечего, нулевой код.
|
|
18
|
+
*
|
|
19
|
+
* Описание длиннее предела, оставленное намеренно, называется в перечне принятого долга
|
|
20
|
+
* рядом — по имени скила, с причиной. Молчаливое превышение и осознанное выглядят
|
|
21
|
+
* одинаково, поэтому второе называется списком.
|
|
22
|
+
*
|
|
23
|
+
* Ненулевой код возврата и перечень превысивших с числами.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
const ROOT = process.cwd();
|
|
29
|
+
const SKILLS = join(ROOT, '.claude/skills');
|
|
30
|
+
const DEBT = join(ROOT, '.claude/rt-kit/description-debt.json');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Предел длины описания в знаках.
|
|
34
|
+
*
|
|
35
|
+
* Считаются знаки, а не байты: байт о цене окна не говорит, а кириллица делает его в
|
|
36
|
+
* полтора раза больше знака. Триста — число владельца, назначенное от первого замера.
|
|
37
|
+
*/
|
|
38
|
+
const LIMIT = 300;
|
|
39
|
+
|
|
40
|
+
/** Описание из шапки: строка `description:` до конца строки. */
|
|
41
|
+
function descriptionOf(text) {
|
|
42
|
+
const match = /^description:\s*(.+)$/m.exec(text);
|
|
43
|
+
|
|
44
|
+
return match === null ? null : match[1].trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Перечень принятого долга: имя скила → причина. Нет файла — долга нет. */
|
|
48
|
+
function debt() {
|
|
49
|
+
if (!existsSync(DEBT)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(DEBT, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function main() {
|
|
61
|
+
if (!existsSync(SKILLS)) {
|
|
62
|
+
console.log('check-descriptions: каталога скилов нет — сверять нечего');
|
|
63
|
+
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const accepted = debt();
|
|
68
|
+
const over = [];
|
|
69
|
+
const owed = [];
|
|
70
|
+
let counted = 0;
|
|
71
|
+
|
|
72
|
+
for (const name of readdirSync(SKILLS)) {
|
|
73
|
+
const file = join(SKILLS, name, 'SKILL.md');
|
|
74
|
+
|
|
75
|
+
if (!existsSync(file)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const description = descriptionOf(readFileSync(file, 'utf8'));
|
|
80
|
+
|
|
81
|
+
if (description === null) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
counted += 1;
|
|
86
|
+
|
|
87
|
+
if (description.length <= LIMIT) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (Object.hasOwn(accepted, name)) {
|
|
92
|
+
owed.push(`${name}: ${description.length} знаков — ${accepted[name]}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
over.push({ name, length: description.length });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const line of owed) {
|
|
100
|
+
console.log(` долг ${line}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (over.length > 0) {
|
|
104
|
+
over.sort((first, second) => second.length - first.length);
|
|
105
|
+
console.log(`check-descriptions: длиннее предела ${over.length} из ${counted}, предел ${LIMIT} знаков\n`);
|
|
106
|
+
|
|
107
|
+
for (const item of over) {
|
|
108
|
+
console.log(` ${item.name}: ${item.length} знаков, лишних ${item.length - LIMIT}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('\nОписание отвечает на один вопрос — брать это правило или нет. Перечисление разделов');
|
|
112
|
+
console.log('и пересказ статей приходят вторым разом вместе с самим правилом.');
|
|
113
|
+
console.log('Оставленное намеренно называется в .claude/rt-kit/description-debt.json с причиной.');
|
|
114
|
+
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log(`check-descriptions: описаний ${counted}, все в пределе ${LIMIT} знаков` + (owed.length > 0 ? `, принятого долга ${owed.length}` : ''));
|
|
119
|
+
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.exit(main());
|
|
@@ -163,6 +163,12 @@ const PAIR_RE = /([\w'"[\].]+)\s*:\s*([^,\n]+)/g;
|
|
|
163
163
|
const MIN_VALUE_LENGTH = 4;
|
|
164
164
|
/** Минимум пар, при котором совпадение таблиц о чём-то говорит */
|
|
165
165
|
const MIN_TABLE_PAIRS = 2;
|
|
166
|
+
/**
|
|
167
|
+
* Доля совпавших пар, начиная с которой таблицы считаются одной. Полное равенство слепо ровно
|
|
168
|
+
* там, где копия разошлась с оригиналом на строку, — а это и есть тот случай, ради которого
|
|
169
|
+
* копии сводят. Порог высокий: таблицы одного домена делят по две-три пары без всякого родства.
|
|
170
|
+
*/
|
|
171
|
+
const MIN_TABLE_SHARE = 0.8;
|
|
166
172
|
|
|
167
173
|
/**
|
|
168
174
|
* Пакеты, чьи наборы считаются наравне с либами. Своё перечисление под уже
|
|
@@ -237,7 +243,7 @@ for (const path of SOURCE_ROOTS.flatMap((root) => collectFiles(root))) {
|
|
|
237
243
|
([, key, value]) => `${key.replaceAll(/['"[\]]/g, '')}:${value.trim().replace(/,$/, '')}`
|
|
238
244
|
);
|
|
239
245
|
if (pairs.length >= MIN_TABLE_PAIRS) {
|
|
240
|
-
tables.push({ name, lib, pairs:
|
|
246
|
+
tables.push({ name, lib, pairs: new Set(pairs) });
|
|
241
247
|
}
|
|
242
248
|
}
|
|
243
249
|
|
|
@@ -286,14 +292,36 @@ for (const [value, places] of [...namesByValue.entries()].sort()) {
|
|
|
286
292
|
findings.push({ key: `value ${value} @ ${where}`, text: `значение ${value} объявлено в ${libs.size} либах: ${where}` });
|
|
287
293
|
}
|
|
288
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Доля совпавших пар считается от большей таблицы: от меньшей таблица из двух пар, целиком
|
|
297
|
+
* лежащая внутри таблицы из двадцати, читалась бы полной копией.
|
|
298
|
+
*/
|
|
299
|
+
const tableOverlap = (first, second) => {
|
|
300
|
+
let same = 0;
|
|
301
|
+
for (const pair of first.pairs) {
|
|
302
|
+
if (second.pairs.has(pair)) {
|
|
303
|
+
same += 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const larger = Math.max(first.pairs.size, second.pairs.size);
|
|
307
|
+
|
|
308
|
+
return { same, larger, share: same / larger };
|
|
309
|
+
};
|
|
310
|
+
|
|
289
311
|
for (let i = 0; i < tables.length; i++) {
|
|
290
312
|
for (let j = i + 1; j < tables.length; j++) {
|
|
291
313
|
const [first, second] = [tables[i], tables[j]];
|
|
292
|
-
if (first.lib === second.lib
|
|
314
|
+
if (first.lib === second.lib) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const { same, larger, share } = tableOverlap(first, second);
|
|
318
|
+
if (share < MIN_TABLE_SHARE) {
|
|
293
319
|
continue;
|
|
294
320
|
}
|
|
295
321
|
const key = `table ${[`${first.name} @ ${first.lib}`, `${second.name} @ ${second.lib}`].sort().join(' ~ ')}`;
|
|
296
|
-
|
|
322
|
+
const apart = larger - same;
|
|
323
|
+
const tail = apart === 0 ? 'одна таблица соответствий' : `одна таблица соответствий, разошедшаяся на ${apart} из ${larger} пар`;
|
|
324
|
+
findings.push({ key, text: `${first.name} (${first.lib}) и ${second.name} (${second.lib}) — ${tail}` });
|
|
297
325
|
}
|
|
298
326
|
}
|
|
299
327
|
|
|
@@ -38,6 +38,14 @@ const ALLOWLIST = allowlistOf('file-size');
|
|
|
38
38
|
/** Пределов два: код и текст слоя правил. Какой из них применён, каждая строка отказа называет. */
|
|
39
39
|
const LIMIT = CONFIG.fileSizeLimit;
|
|
40
40
|
const PROSE_LIMIT = CONFIG.proseSizeLimit ?? CONFIG.fileSizeLimit;
|
|
41
|
+
/**
|
|
42
|
+
* Второй предел текста — в знаках. Строки меряют, сколько текста помещается на экран, но веса
|
|
43
|
+
* не меряют вовсе: правило о заявках занимает 282 строки при 13 595 знаках, а правило поставки —
|
|
44
|
+
* 272 строки при 21 508. Сжатие слоя срезает знаки, а число переносов оставляет прежним, и
|
|
45
|
+
* строковый предел достигнутого не закрепляет. Дерево, не назвавшее этого числа, судится
|
|
46
|
+
* по-прежнему одними строками.
|
|
47
|
+
*/
|
|
48
|
+
const PROSE_CHARS = CONFIG.proseCharLimit ?? 0;
|
|
41
49
|
/** Корни текста слоя правил; дерево, их не назвавшее, судится одним пределом. */
|
|
42
50
|
const PROSE_ROOTS = CONFIG.proseRoots ?? [];
|
|
43
51
|
|
|
@@ -74,6 +82,16 @@ function lineCount(path) {
|
|
|
74
82
|
return readFileSync(join(ROOT, path), 'utf8').split('\n').length;
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/** Спутник — таблица связи, а не проза: компаньон правила и перечень сценариев спека. */
|
|
86
|
+
function companion(path) {
|
|
87
|
+
return path.endsWith('/implementation.md') || path.endsWith('/scenarios.md');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Знаки, а не байты: кириллица весит по два байта, и байтовый счёт судил бы язык, а не текст. */
|
|
91
|
+
function charCount(path) {
|
|
92
|
+
return readFileSync(join(ROOT, path), 'utf8').length;
|
|
93
|
+
}
|
|
94
|
+
|
|
77
95
|
/**
|
|
78
96
|
* Список известного читается отдельно от общего читателя: у этой проверки нет файла — это
|
|
79
97
|
* не пустой список, а нечитаемая настройка, и молчать о ней нельзя. Пустой список законен
|
|
@@ -86,15 +104,32 @@ const known = new Map([...[...accepted.keys()].map((path) => [path, 'приня
|
|
|
86
104
|
const tooLong = new Map();
|
|
87
105
|
const tracked = trackedFiles().filter(judged);
|
|
88
106
|
|
|
107
|
+
const overweight = new Map();
|
|
108
|
+
|
|
89
109
|
for (const path of tracked) {
|
|
90
110
|
const lines = lineCount(path);
|
|
91
111
|
if (lines > limitOf(path).limit) {
|
|
92
112
|
tooLong.set(path, lines);
|
|
93
113
|
}
|
|
114
|
+
|
|
115
|
+
// Вес судится только у текста слоя правил и только там, где дерево назвало число: у кода
|
|
116
|
+
// длину стережёт ещё и линтер, а у прозы — одни эти два предела.
|
|
117
|
+
//
|
|
118
|
+
// Спутники из счёта веса выведены. Компаньон правила и перечень сценариев — таблицы связи:
|
|
119
|
+
// заголовок привязки дословно повторяет утверждение, потому что связь идёт по его тексту, и
|
|
120
|
+
// резать там нечего, не порвав саму связь. Вес такого файла растёт с числом утверждений, а
|
|
121
|
+
// не с многословием: у правила поставки семьдесят шесть привязок на 24 326 знаков, из них
|
|
122
|
+
// пояснений всего 5 729. Строковый предел на них остаётся — он ловит другое.
|
|
123
|
+
if (PROSE_CHARS > 0 && PROSE_ROOTS.length > 0 && limitOf(path).title === 'предел текста' && !companion(path)) {
|
|
124
|
+
const chars = charCount(path);
|
|
125
|
+
if (chars > PROSE_CHARS) {
|
|
126
|
+
overweight.set(path, chars);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
94
129
|
}
|
|
95
130
|
|
|
96
131
|
if (process.argv.includes('--baseline')) {
|
|
97
|
-
console.log(baselineOf([...tooLong.keys()].sort(), allowlist));
|
|
132
|
+
console.log(baselineOf([...new Set([...tooLong.keys(), ...overweight.keys()])].sort(), allowlist));
|
|
98
133
|
process.exit(0);
|
|
99
134
|
}
|
|
100
135
|
|
|
@@ -104,7 +139,11 @@ const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
|
|
|
104
139
|
/** Файл поделили, а строку оставили: список перестал бы отвечать за то, что в нём стоит. */
|
|
105
140
|
const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
|
|
106
141
|
|
|
142
|
+
/** Тяжёлое по знакам судится тем же списком известного: один долг на файл, а не два. */
|
|
143
|
+
const heavy = [...overweight].filter(([path]) => !known.has(path) && !tooLong.has(path));
|
|
144
|
+
|
|
107
145
|
const problems = [
|
|
146
|
+
...heavy.map(([path, chars]) => `${path}: ${chars} знаков, предел веса текста ${PROSE_CHARS} — резать довод, а не дописывать строку в ${ALLOWLIST}`),
|
|
108
147
|
...fresh.map(([path, lines]) => {
|
|
109
148
|
const { limit, title } = limitOf(path);
|
|
110
149
|
return `${path}: ${lines} строк, ${title} ${limit} — делить, а не дописывать строку в ${ALLOWLIST}`;
|
|
@@ -121,8 +160,14 @@ if (problems.length > 0) {
|
|
|
121
160
|
}
|
|
122
161
|
|
|
123
162
|
const limits = PROSE_ROOTS.length > 0 ? `предел кода ${LIMIT}, предел текста ${PROSE_LIMIT}` : `предел ${LIMIT}`;
|
|
163
|
+
/**
|
|
164
|
+
* Предел веса называется только там, где дерево задало и число, и корни текста: вес судится у
|
|
165
|
+
* прозы слоя правил, а дерево, её корней не назвавшее, судится одним числом строк — и вторая
|
|
166
|
+
* цифра в сводке говорила бы о проверке, которая там не работает.
|
|
167
|
+
*/
|
|
168
|
+
const weight = PROSE_CHARS > 0 && PROSE_ROOTS.length > 0 ? `, предел веса текста ${PROSE_CHARS} знаков` : '';
|
|
124
169
|
|
|
125
170
|
console.log(
|
|
126
|
-
`check-file-size: проверено ${tracked.length} файлов, ${limits}, длиннее предела ${tooLong.size}, ` +
|
|
171
|
+
`check-file-size: проверено ${tracked.length} файлов, ${limits}${weight}, длиннее предела ${tooLong.size}, ` +
|
|
127
172
|
`из них принято ${accepted.size}, долг ${debt.size} — новых нет`
|
|
128
173
|
);
|
|
@@ -41,11 +41,28 @@ const RULE = join(ROOT, '.claude/skills/task-flow/SKILL.md');
|
|
|
41
41
|
*/
|
|
42
42
|
const LIMIT_BYTES = 6144;
|
|
43
43
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Имена состояний: строка таблицы, у которой первая ячейка стоит в обратных кавычках, — а для
|
|
46
|
+
* карты хода ещё и строка списка «- `имя` — действие; ведёт `паттерн`». В правиле список так не
|
|
47
|
+
* читается: тем же видом там записаны паттерны, и они попали бы в состояния.
|
|
48
|
+
* у которой первая ячейка стоит в обратных кавычках.
|
|
49
|
+
*
|
|
50
|
+
* Обе формы читаются намеренно. Список дешевле таблицы на треть — форматтер добивает столбцы
|
|
51
|
+
* пробелами до общей ширины, и эти пробелы едут в контекст каждого захода, ничего не значая;
|
|
52
|
+
* таблица при этом остаётся законной, и дерево, которое её не переписывало, работает как
|
|
53
|
+
* прежде.
|
|
54
|
+
*/
|
|
55
|
+
function statesOf(text, { listed: readListed = false } = {}) {
|
|
46
56
|
const states = [];
|
|
47
57
|
|
|
48
58
|
for (const line of text.split('\n')) {
|
|
59
|
+
const listed = readListed && line.match(/^-\s+`([^`]+)`\s+—\s+(.+)$/);
|
|
60
|
+
|
|
61
|
+
if (listed) {
|
|
62
|
+
states.push({ name: listed[1], rest: listed[2].split(';').map((part) => part.trim()) });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
if (!line.startsWith('|')) {
|
|
50
67
|
continue;
|
|
51
68
|
}
|
|
@@ -87,7 +104,7 @@ function main() {
|
|
|
87
104
|
faults.push(`карта выросла: ${bytes} байт при пределе ${LIMIT_BYTES}`);
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
const inMap = statesOf(text);
|
|
107
|
+
const inMap = statesOf(text, { listed: true });
|
|
91
108
|
|
|
92
109
|
if (inMap.length === 0) {
|
|
93
110
|
faults.push('в карте нет ни одного состояния — таблица сломана');
|
|
@@ -60,7 +60,18 @@ const isLib = (path) => existsSync(join(ROOT, path, 'project.json'));
|
|
|
60
60
|
* нет вовсе, и прямое чтение роняло проверку отказом «нет такого файла» — то есть первая же
|
|
61
61
|
* установка получала поломку вместо отчёта о том, что долгов нет.
|
|
62
62
|
*/
|
|
63
|
-
|
|
63
|
+
// `flatLibRoots` стоит в перечне наравне с остальными: сбор плоских либ читает эту сторону, а
|
|
64
|
+
// разбор её не собирал — значение выходило пустым и подставлялось пустым списком молча. Дерево с
|
|
65
|
+
// непустым набором плоских корней получало ноль плоских либ и зелёную проверку.
|
|
66
|
+
const allowlist = parseAllowlist('lib-layers', [
|
|
67
|
+
'notDomains',
|
|
68
|
+
'legacyDomains',
|
|
69
|
+
'legacyLibs',
|
|
70
|
+
'singleLayerDomains',
|
|
71
|
+
'flatLibRoots',
|
|
72
|
+
'accepted',
|
|
73
|
+
'debt',
|
|
74
|
+
]);
|
|
64
75
|
const pathsOf = (key) => [...allowlist[key].keys()];
|
|
65
76
|
|
|
66
77
|
/** Паттерн `<корень>/x/*` покрывает и сам каталог `<корень>/x`: исключение снимается целиком */
|
|
@@ -196,7 +196,7 @@ function collectStrayLibs(knownLibs) {
|
|
|
196
196
|
* `nx test` по ней молча не гонял ни одной спеки.
|
|
197
197
|
*/
|
|
198
198
|
function collectFlatLibs() {
|
|
199
|
-
return (allowlist.flatLibRoots ?? [])
|
|
199
|
+
return [...(allowlist.flatLibRoots?.keys() ?? [])]
|
|
200
200
|
.flatMap((root) => dirsIn(root).map((entry) => `${root}/${entry}`))
|
|
201
201
|
.filter((path) => isLib(path))
|
|
202
202
|
.sort();
|