@rt-tools/agent-kit 0.14.0 → 0.15.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/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 +17 -1
- package/assets/checks/spec-anchors.mjs +18 -3
- package/assets/checks/spec-common.mjs +5 -1
- package/assets/defaults/project.sh +21 -16
- 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 +7 -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.15.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
|
просится в список игнорируемого; вписывает его проект — в чужие файлы дерева пакет не пишет.
|
|
@@ -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();
|
|
@@ -80,6 +80,19 @@ const DEFAULTS = {
|
|
|
80
80
|
* только это число.
|
|
81
81
|
*/
|
|
82
82
|
proseSizeLimit: 300,
|
|
83
|
+
/**
|
|
84
|
+
* Предел веса текста слоя правил — в знаках. Строки меряют, сколько текста помещается на
|
|
85
|
+
* экран; веса они не меряют вовсе: правило о заявках занимает 282 строки при 13 595 знаках,
|
|
86
|
+
* а правило поставки — 272 строки при 21 508. Сжатие слоя срезает знаки и оставляет число
|
|
87
|
+
* переносов прежним, поэтому строковый предел достигнутого не закрепляет, и текст отрастает
|
|
88
|
+
* обратно молча.
|
|
89
|
+
*
|
|
90
|
+
* Число назначено по сжатому слою: самое тяжёлое правило весит 21 508 знаков, и предел стоит
|
|
91
|
+
* чуть выше него. Ниже ставить нельзя — это потребовало бы резать заново то, что уже прошло
|
|
92
|
+
* сжатие; выше незачем — тогда он ничего не закрепляет. Ноль выключает проверку веса вовсе:
|
|
93
|
+
* дерево, не назвавшее числа, судится по-прежнему одними строками.
|
|
94
|
+
*/
|
|
95
|
+
proseCharLimit: 22000,
|
|
83
96
|
/**
|
|
84
97
|
* Корни, под которыми лежит текст слоя правил, и его источники. Файл отсюда судится
|
|
85
98
|
* пределом текста, всё остальное — пределом кода. Пусто — предел один на всё дерево.
|
|
@@ -258,7 +271,10 @@ export const parseAllowlist = (name, sides = ['accepted', 'debt']) => {
|
|
|
258
271
|
return new Map();
|
|
259
272
|
}
|
|
260
273
|
if (Array.isArray(entries) || typeof entries !== 'object' || entries === null) {
|
|
261
|
-
refuse(
|
|
274
|
+
refuse(
|
|
275
|
+
`«${side}» записан не объектом — у записи нет места ни для причины, ни для номера задачи. ` +
|
|
276
|
+
`Форма: {"${side}": {"<ключ>": {"reason": "<почему>", "task": "${key || 'КЛЮЧ'}-<номер>"}}}`,
|
|
277
|
+
);
|
|
262
278
|
}
|
|
263
279
|
const parsed = new Map();
|
|
264
280
|
for (const [entry, value] of Object.entries(entries)) {
|
|
@@ -108,7 +108,11 @@ function checkRuleImplementation(specFile, text, mapFile, heading = '## Прав
|
|
|
108
108
|
|
|
109
109
|
const rows = new Map();
|
|
110
110
|
for (const line of rowsOfMap(specFile, mapFile, mapHeading)) {
|
|
111
|
-
|
|
111
|
+
// Привязка записывается двумя формами, и читаются обе. Таблица — прежняя; список — та,
|
|
112
|
+
// ради которой из компаньонов уходят пробелы выравнивания: форматтер добивает столбцы до
|
|
113
|
+
// общей ширины, и в компаньонах дерева это 120 017 знаков из 328 738, то есть 37%.
|
|
114
|
+
// Связь при этом не меняется: она идёт по тексту утверждения, а не по форме строки.
|
|
115
|
+
const cells = line.match(/^\|([^|]+)\|([^|]*)\|\s*$/) ?? line.match(/^-\s+\*\*(.+?)\*\*\s+—\s+(.*)$/);
|
|
112
116
|
if (!cells) {
|
|
113
117
|
continue;
|
|
114
118
|
}
|
|
@@ -210,8 +214,14 @@ function symbolOwners() {
|
|
|
210
214
|
|
|
211
215
|
for (const file of SOURCE_ROOTS.flatMap((root) => walk(root, (name) => name.endsWith('.ts') || name.endsWith('.html')))) {
|
|
212
216
|
const text = file.endsWith('.ts') ? codeOf(read(file)) : read(file);
|
|
213
|
-
|
|
217
|
+
// Решётка входит в токен: приватное поле класса объявлено с ней, и якорь на него иначе
|
|
218
|
+
// не попадал бы в перечень владельцев ни разу. Имя без решётки помнится наравне с ним
|
|
219
|
+
// самим — привязки прежней формы остаются зелёными, и переходить разом не приходится.
|
|
220
|
+
for (const [token] of text.matchAll(/#?[A-Za-z_][\w-]*/g)) {
|
|
214
221
|
remember(token, file);
|
|
222
|
+
if (token.startsWith('#')) {
|
|
223
|
+
remember(token.slice(1), file);
|
|
224
|
+
}
|
|
215
225
|
if (token.includes('-')) {
|
|
216
226
|
token.split('-').forEach((part) => part && remember(part, file));
|
|
217
227
|
}
|
|
@@ -242,7 +252,12 @@ function checkTracedAnchors() {
|
|
|
242
252
|
|
|
243
253
|
const owners = symbolOwners();
|
|
244
254
|
for (const { mapFile, path, symbol } of declared) {
|
|
245
|
-
|
|
255
|
+
// Граница слова ставится только там, где она есть: перед решёткой её нет, и образец с
|
|
256
|
+
// ней давал бы ноль вхождений у всякого приватного имени.
|
|
257
|
+
const bound = symbol.startsWith('#')
|
|
258
|
+
? `${escapeForRegExp(symbol)}\\b`
|
|
259
|
+
: `\\b${escapeForRegExp(symbol)}\\b`;
|
|
260
|
+
const here = (codeAt(path).match(new RegExp(bound, 'g')) || []).length;
|
|
246
261
|
const elsewhere = [...(owners.get(symbol) || [])].filter((file) => file !== path).length;
|
|
247
262
|
if (here + elsewhere < 2) {
|
|
248
263
|
report(
|
|
@@ -66,8 +66,12 @@ const E2E_ROOTS = CONFIG.e2eRoots;
|
|
|
66
66
|
* переписан целиком. Алфавит не перечисляется диапазонами: перечисленные молча не покрывают
|
|
67
67
|
* соседнего, и промах выглядит отсутствием привязки. Путь при этом остаётся латинским — он
|
|
68
68
|
* адрес в дереве, а не слово текста.
|
|
69
|
+
*
|
|
70
|
+
* Решётка перед именем законна: приватное поле класса объявлено с ней, и записанное без неё имя
|
|
71
|
+
* называет метод не тем именем, каким он объявлен. Проверка при этом остаётся зелёной — граница
|
|
72
|
+
* слова перед решёткой есть, — поэтому промах не краснеет ни разу и виден только чтением.
|
|
69
73
|
*/
|
|
70
|
-
const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(
|
|
74
|
+
const ANCHOR = /`([\w./-]+\.[A-Za-z]{2,10}):(#?\p{L}[\p{L}\p{N}_-]*|#?_[\w-]*)`/gu;
|
|
71
75
|
/**
|
|
72
76
|
* Явный вердикт вместо якоря: статья, которой в дереве исполняться негде. Так бывает
|
|
73
77
|
* законно — правило говорит о службе, которой дерево не держит, или о движении человека,
|
|
@@ -258,24 +258,29 @@ rt_shell_writes_default() {
|
|
|
258
258
|
# Судится заголовок команды — именно в нём стоит тот путь, куда команда пишет.
|
|
259
259
|
#
|
|
260
260
|
# Исключение — интерпретатор: ему код приходит телом, и путь записи стоит именно там. Признак
|
|
261
|
-
#
|
|
262
|
-
#
|
|
261
|
+
# читается у той строки, которая тело открыла, а не у всей команды: тело принадлежит команде
|
|
262
|
+
# своего заголовка. Прежде он читался у всего текста разом, и слово из документа отключало
|
|
263
|
+
# вырезание целиком — строка «**Чем проверяется:** `bash projects/…`» в замысле делала запись
|
|
264
|
+
# `plan.md` правкой кода приложения. Гард отбивал тем самым запись того файла, отсутствием
|
|
265
|
+
# которого он же и отказывает.
|
|
263
266
|
rt_shell_paths_default() {
|
|
264
267
|
text="$(printf '%s' "$1" | tr "\"'\`" ' ')"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
268
|
+
text="$(printf '%s' "$text" | awk '
|
|
269
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
270
|
+
tag != "" {
|
|
271
|
+
if (keep) { print }
|
|
272
|
+
if (trim($0) == tag) { tag = ""; keep = 0 }
|
|
273
|
+
next
|
|
274
|
+
}
|
|
275
|
+
{
|
|
276
|
+
print
|
|
277
|
+
if (match($0, /<<-?[ \t]*[A-Za-z_][A-Za-z0-9_]*/)) {
|
|
278
|
+
t = substr($0, RSTART, RLENGTH)
|
|
279
|
+
sub(/^<<-?[ \t]*/, "", t)
|
|
280
|
+
tag = t
|
|
281
|
+
keep = ($0 ~ /(^|[|;&(]|[ \t])(python3?|node|ruby|perl|php|deno|bun|bash|sh|zsh)([ \t]|$)/)
|
|
282
|
+
}
|
|
283
|
+
}')"
|
|
279
284
|
|
|
280
285
|
# Пути берутся только у тех кусков команды, которые пишут. Прежде брались у всей строки
|
|
281
286
|
# целиком, и команда чтения, сцепленная с записью, отдавала свои пути как цели записи:
|
|
@@ -9,19 +9,17 @@
|
|
|
9
9
|
|
|
10
10
|
## Состояния и обязательные действия
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| `папка-разобрана` | снять черновик и попросить влить | `task-flow-archive` |
|
|
24
|
-
| `влито` | разбор работы правилами и сверка очереди | `task-flow-archive` |
|
|
12
|
+
- `просьба-не-разобрана` — разведка по дереву, затем вопросы; ведёт `task-flow-start`
|
|
13
|
+
- `разбор-закрыт` — договорённость о продукте либо причина её отсутствия; ведёт `task-flow-start`
|
|
14
|
+
- `договорённость-записана` — завести задачу, ветку и папку; ведёт `task-flow-start`
|
|
15
|
+
- `задача-взята` — написать замысел; ведёт `task-flow-start`
|
|
16
|
+
- `замысел-записан` — делать первый этап; ведёт `task-flow-start`
|
|
17
|
+
- `этап-идёт` — доделать этап и отметить в ходе работы; ведёт `task-flow-resume`
|
|
18
|
+
- `этапы-кончились` — прогнать набор и открыть PR черновиком; ведёт `task-flow-close`
|
|
19
|
+
- `работа-отдана` — взять следующую задачу; ведёт `task-flow-resume`
|
|
20
|
+
- `разбор-кончился` — влить договорённость, привести тексты, разобрать папку; ведёт `task-flow-close`
|
|
21
|
+
- `папка-разобрана` — снять черновик и попросить влить; ведёт `task-flow-archive`
|
|
22
|
+
- `влито` — разбор работы правилами и сверка очереди; ведёт `task-flow-archive`
|
|
25
23
|
|
|
26
24
|
Ни у одного состояния обязательное действие не звучит как «ждать». Прогон, разбор владельцем и
|
|
27
25
|
слияние идут без исполнителя и от взгляда быстрее не становятся.
|
|
@@ -30,12 +28,10 @@
|
|
|
30
28
|
|
|
31
29
|
Способов четыре, и других нет.
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
| заполненное окно там, где сжатия нет | ход работы дописан, передача написана |
|
|
38
|
-
| работа отдана, и следующая начата | PR открыт, и по следующей задаче сделано действие, а не сказано |
|
|
31
|
+
- **вопрос владельцу, ответа на который в правилах нет** — вопрос задан, и за тот же ход правила читались
|
|
32
|
+
- **отказ гарда** — отказ назван владельцу, обход не искался
|
|
33
|
+
- **заполненное окно там, где сжатия нет** — ход работы дописан, передача написана
|
|
34
|
+
- **работа отдана, и следующая начата** — PR открыт, и по следующей задаче сделано действие, а не сказано
|
|
39
35
|
|
|
40
36
|
Там, где дерево объявило порог сжатия ниже порога остановки, заполненное окно ход не кончает:
|
|
41
37
|
контекст сжимается, передача приходит входом, и работа идёт дальше тем же заходом. Порог
|