@rt-tools/agent-kit 0.5.3 → 0.6.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/check-file-size.mjs +127 -0
- package/assets/checks/rt-kit-checks.config.mjs +10 -0
- package/assets/defaults/gate-map.sh +90 -34
- package/assets/defaults/project.sh +26 -3
- package/assets/hooks/skill-gate-layers.sh +156 -0
- package/assets/hooks/skill-gate.sh +11 -2
- package/assets/laws/code-structure.md +3 -0
- package/assets/laws/delivery.md +9 -0
- package/assets/laws/observability.md +46 -0
- package/assets/laws/project-documentation.md +4 -0
- package/assets/laws/reuse-first.md +2 -0
- package/assets/laws/verifiability.md +5 -0
- package/assets/patterns/browser-verification-stand.md +22 -2
- package/assets/patterns/doc-style-trace.md +111 -0
- package/assets/patterns/git-workflow-commit.github.md +1 -1
- package/assets/patterns/git-workflow-docker.md +203 -0
- package/assets/patterns/git-workflow-secrets.md +93 -0
- package/assets/patterns/observability-record.md +114 -0
- package/assets/patterns/ownership-session-procedure.md +102 -0
- package/assets/patterns/seo-verify.md +1 -1
- package/assets/patterns/spec-driven-rule.md +5 -0
- package/assets/patterns/styling-bem-sheet.md +178 -0
- package/assets/patterns/task-flow-close.md +20 -0
- package/assets/patterns/task-flow-resume.md +5 -0
- package/assets/patterns/translations-content.md +107 -0
- package/assets/patterns/translations-key.md +1 -1
- package/assets/rules/angular-patterns.md +5 -0
- package/assets/rules/browser-verification.md +17 -12
- package/assets/rules/component-structure.md +6 -2
- package/assets/rules/doc-style.md +16 -0
- package/assets/rules/git-workflow.azure.md +45 -1
- package/assets/rules/git-workflow.github.md +52 -1
- package/assets/rules/git-workflow.gitlab.md +46 -1
- package/assets/rules/lists.md +13 -0
- package/assets/rules/observability.md +147 -0
- package/assets/rules/ownership-scope.md +5 -2
- package/assets/rules/ownership-session.md +124 -0
- package/assets/rules/permissions.md +23 -0
- package/assets/rules/pricing.md +4 -0
- package/assets/rules/reuse-first.md +9 -0
- package/assets/rules/seo.md +57 -9
- package/assets/rules/shared-code.md +6 -0
- package/assets/rules/spec-driven.md +9 -0
- package/assets/rules/styling-bem.md +34 -1
- package/assets/rules/task-flow.md +5 -0
- package/assets/rules/testing.md +46 -8
- package/assets/rules/translations.md +11 -5
- package/assets/rules/typescript-conventions.md +5 -0
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.6.0.tgz +0 -0
- package/rt-tools-agent-kit-0.5.3.tgz +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Проверка того, что файл не длиннее предела.
|
|
4
|
+
*
|
|
5
|
+
* Файл, который не влезает на экран целиком, читают по частям, и правку в нём
|
|
6
|
+
* делают, не увидев остального. У кода длину стережёт линтер; здесь — всё, до
|
|
7
|
+
* чего он не доходит: проза, стили, шаблоны, обвязка разработки и сами гарды.
|
|
8
|
+
*
|
|
9
|
+
* Судятся `.md`, `.scss`, `.html`, `.js`, `.mjs` и `.sh`. Данные не судятся
|
|
10
|
+
* вовсе: словарь локали и настройка сборки читаются поиском, а не подряд, и
|
|
11
|
+
* делить их не на что. Код на языке, где длину стережёт линтер, тоже не
|
|
12
|
+
* судится — два отказа на один файл читаются как две разные претензии.
|
|
13
|
+
*
|
|
14
|
+
* Строки считаются все, включая пустые и комментарии, и тем же способом, каким
|
|
15
|
+
* их считает линтер: по числу разрывов плюс один. Файл, кончающийся переводом
|
|
16
|
+
* строки, поэтому весит на строку больше, чем показывает `wc -l`, — зато у обеих
|
|
17
|
+
* проверок дерева одно понятие длины.
|
|
18
|
+
*
|
|
19
|
+
* Описание прошлого из счёта выведено: архив по устройству перечисляет то, чего
|
|
20
|
+
* в дереве уже нет, а папка задачи умирает со слиянием. Сгенерированное выведено
|
|
21
|
+
* каталогом: его переписывает генератор целиком, и спорить с ним о длине некому.
|
|
22
|
+
*
|
|
23
|
+
* Накопленное к моменту заведения проверки лежит в списке известного и отказом
|
|
24
|
+
* не считается: гейт падает на НОВОМ длинном файле, а старое остаётся видимым
|
|
25
|
+
* числом в сводке. Принятое и долг там различаются: принятое дерево делить не
|
|
26
|
+
* собирается, на долг заведена работа. Строка снимается вместе с делением своего
|
|
27
|
+
* файла, и проверка сама говорит, какую строку пора убрать.
|
|
28
|
+
*
|
|
29
|
+
* Ненулевой код возврата и перечень расхождений.
|
|
30
|
+
*/
|
|
31
|
+
import { execFileSync } from 'node:child_process';
|
|
32
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { allowlistOf, CONFIG, ROOT } from './rt-kit-checks.config.mjs';
|
|
36
|
+
|
|
37
|
+
const ALLOWLIST = allowlistOf('file-size');
|
|
38
|
+
/** Предел один на все роды файлов: своё число каждому роду — спор о числе на каждой правке. */
|
|
39
|
+
const LIMIT = CONFIG.fileSizeLimit;
|
|
40
|
+
|
|
41
|
+
/** Роды файлов, которых не читает линтер. Код остаётся за ним. */
|
|
42
|
+
const JUDGED = ['.md', '.scss', '.html', '.js', '.mjs', '.sh'];
|
|
43
|
+
|
|
44
|
+
/** Описание прошлого, папка задачи и то, что переписывает генератор. */
|
|
45
|
+
const SKIPPED_PREFIXES = [CONFIG.archiveDir, `${CONFIG.tasksDir}/`, ...CONFIG.generatedDirs];
|
|
46
|
+
|
|
47
|
+
/** Дерево спрашивается у системы контроля версий: иначе каталоги с точки не видны, а сборка видна. */
|
|
48
|
+
function trackedFiles() {
|
|
49
|
+
return execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 1024 * 1024 * 32 })
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter(Boolean);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function judged(path) {
|
|
55
|
+
if (SKIPPED_PREFIXES.some((prefix) => prefix && path.startsWith(prefix))) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return JUDGED.some((extension) => path.endsWith(extension));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Тем же способом, каким считает линтер: число разрывов плюс один. */
|
|
63
|
+
function lineCount(path) {
|
|
64
|
+
return readFileSync(join(ROOT, path), 'utf8').split('\n').length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Список известного читается отдельно от общего читателя: у этой проверки нет файла — это
|
|
69
|
+
* не пустой список, а нечитаемая настройка, и молчать о ней нельзя. Пустой список законен
|
|
70
|
+
* ровно один раз — в дереве, где длинных файлов нет вовсе.
|
|
71
|
+
*/
|
|
72
|
+
function readKnown() {
|
|
73
|
+
const path = join(ROOT, ALLOWLIST);
|
|
74
|
+
if (!existsSync(path)) {
|
|
75
|
+
return { accepted: [], debt: [] };
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
79
|
+
|
|
80
|
+
return { accepted: parsed.accepted ?? [], debt: parsed.debt ?? [] };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`check-file-size: список известного не прочитан — ${ALLOWLIST}: ${error.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { accepted, debt } = readKnown();
|
|
88
|
+
const known = new Map([...accepted.map((path) => [path, 'принято']), ...debt.map((path) => [path, 'долг'])]);
|
|
89
|
+
|
|
90
|
+
const tooLong = new Map();
|
|
91
|
+
const tracked = trackedFiles().filter(judged);
|
|
92
|
+
|
|
93
|
+
for (const path of tracked) {
|
|
94
|
+
const lines = lineCount(path);
|
|
95
|
+
if (lines > LIMIT) {
|
|
96
|
+
tooLong.set(path, lines);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (process.argv.includes('--baseline')) {
|
|
101
|
+
console.log(JSON.stringify({ accepted, debt: [...tooLong.keys()].sort() }, null, 4));
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fresh = [...tooLong].filter(([path]) => !known.has(path));
|
|
106
|
+
/** Строка на файл, которого в дереве нет, — устаревшая: иначе список копит мёртвое. */
|
|
107
|
+
const gone = [...known.keys()].filter((path) => !existsSync(join(ROOT, path)));
|
|
108
|
+
/** Файл поделили, а строку оставили: список перестал бы отвечать за то, что в нём стоит. */
|
|
109
|
+
const shrunk = [...known.keys()].filter((path) => !tooLong.has(path) && existsSync(join(ROOT, path)));
|
|
110
|
+
|
|
111
|
+
const problems = [
|
|
112
|
+
...fresh.map(([path, lines]) => `${path}: ${lines} строк, предел ${LIMIT} — делить, а не дописывать строку в ${ALLOWLIST}`),
|
|
113
|
+
...gone.map((path) => `${path}: строка в ${ALLOWLIST} устарела — файла в дереве нет`),
|
|
114
|
+
...shrunk.map((path) => `${path}: значится в ${ALLOWLIST}, но уже короче предела — строку убрать`),
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
if (problems.length > 0) {
|
|
118
|
+
console.error(`check-file-size: расхождений ${problems.length}\n`);
|
|
119
|
+
problems.forEach((problem) => console.error(` ${problem}`));
|
|
120
|
+
console.error('\nПредел длины файла — правило о языке для кода и правило о текстах для прозы.');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(
|
|
125
|
+
`check-file-size: проверено ${tracked.length} файлов, длиннее ${LIMIT} строк ${tooLong.size}, ` +
|
|
126
|
+
`из них принято ${accepted.length}, долг ${debt.length} — новых нет`
|
|
127
|
+
);
|
|
@@ -53,6 +53,16 @@ const DEFAULTS = {
|
|
|
53
53
|
tasksDir: 'docs/tasks',
|
|
54
54
|
/** Куда сложены списки принятых долгов. */
|
|
55
55
|
allowlistDir: 'tools',
|
|
56
|
+
/**
|
|
57
|
+
* Каталоги, которые переписывает генератор целиком: контракт, клиент хранилища, разложенное
|
|
58
|
+
* из пакета. Спорить с генератором о длине файла и о повторе некому.
|
|
59
|
+
*/
|
|
60
|
+
generatedDirs: [],
|
|
61
|
+
/**
|
|
62
|
+
* Предел длины файла — одно число на все роды: своё число каждому роду означает спор о
|
|
63
|
+
* числе на каждой правке, а не о длине файла.
|
|
64
|
+
*/
|
|
65
|
+
fileSizeLimit: 500,
|
|
56
66
|
/** Корни сквозных тестов; пусто — их в дереве нет. */
|
|
57
67
|
e2eRoots: ['apps/site-e2e', 'apps/admin-e2e'],
|
|
58
68
|
/** Корни бэкенда: у него нет ни компонентов, ни шаблонов, и часть признаков к нему не применяется. */
|
|
@@ -15,32 +15,21 @@
|
|
|
15
15
|
#
|
|
16
16
|
# Порядок веток решает: первое совпадение выигрывает, поэтому частное идёт раньше общего.
|
|
17
17
|
|
|
18
|
-
# Правила,
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# в обычный класс: по имени файла ни то ни другое не видно, и правило, требуемое только по
|
|
22
|
-
# расширению, здесь молчало бы.
|
|
23
|
-
skill_for_written() {
|
|
24
|
-
target="$1"
|
|
25
|
-
written="$2"
|
|
26
|
-
|
|
27
|
-
[ -z "$written" ] && return 0
|
|
28
|
-
|
|
29
|
-
case "$target" in
|
|
30
|
-
*.spec.ts | */docs/* | *.md) return 0 ;;
|
|
31
|
-
esac
|
|
32
|
-
|
|
33
|
-
printf '%s' "$written" | grep -qE '(globalThis|window\.|document\.defaultView|PLATFORM_ID|isPlatformBrowser|localStorage|sessionStorage)' \
|
|
34
|
-
&& printf '%s\n' 'platform-access'
|
|
18
|
+
# Правила, вступающие не от рода файла, а от того, что в него пишут, здесь не выбираются:
|
|
19
|
+
# они приходят слоем поверх доменного — `hooks/skill-gate-layers.sh`. Карта судит путь, слой
|
|
20
|
+
# судит текст, и оба зовутся из гейта в одной оболочке.
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
# Команда считается ВЫЗОВОМ, только когда стоит в начале строки или сразу за разделителем.
|
|
23
|
+
# Совпадение по подстроке ловит любое УПОМИНАНИЕ: строка о коммите в теле самого коммита и поиск
|
|
24
|
+
# по истории отбивались как настоящий коммит.
|
|
25
|
+
#
|
|
26
|
+
# `([A-Za-z_]…=…[[:space:]]+)*` — переменные окружения перед вызовом: адрес хранилища ставят
|
|
27
|
+
# приставкой самой команды, и без этого куска вызов не опознавался вовсе. `(npx…)?` — запуск
|
|
28
|
+
# через раннер пакетов, `([^[:space:]]*/)?` — путь до исполняемого файла. Многострочную команду
|
|
29
|
+
# поиск разбирает построчно, поэтому начало строки — начало каждой.
|
|
30
|
+
rt_gate_invokes() {
|
|
31
|
+
printf '%s\n' "$1" \
|
|
32
|
+
| grep -qE "(^|[;&|(])[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*((npx|pnpm|yarn|bun|npm)([[:space:]]+(exec|run|dlx))?[[:space:]]+)?([^[:space:]]*/)?$2([[:space:]]|$)"
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
skill_for_default() {
|
|
@@ -81,6 +70,37 @@ skill_for_default() {
|
|
|
81
70
|
# строк комментария стоят захода, а второе прочитанное правило не пригождается.
|
|
82
71
|
*/tools/check-dupes.mjs | */tools/dupes-allowlist.json) printf '%s\n' 'shared-code' ;;
|
|
83
72
|
|
|
73
|
+
# Остальные проверки — то же самое: проверка исполняет утверждения своего
|
|
74
|
+
# правила, и признаки, по которым она судит, объявлены у него в привязке. Правя
|
|
75
|
+
# признак в проверке, второе место открывают рядом — иначе они расходятся молча,
|
|
76
|
+
# и проверка числит отказом то, что правило разрешает.
|
|
77
|
+
*/check-specs.mjs) printf '%s\n' 'spec-driven' ;;
|
|
78
|
+
*/check-doc-paths.mjs | */doc-paths-allowlist.json | */check-file-size.mjs)
|
|
79
|
+
printf '%s\n' 'doc-style' ;;
|
|
80
|
+
*/check-styles.mjs | */styles-allowlist.json | */stylelint-rules/*)
|
|
81
|
+
printf '%s\n' 'styling-bem' ;;
|
|
82
|
+
*/check-lib-layers.mjs | */lib-layers-allowlist.json) printf '%s\n' 'lib-layers' ;;
|
|
83
|
+
*/check-reuse.mjs | */reuse-allowlist.json) printf '%s\n' 'reuse-first' ;;
|
|
84
|
+
*/check-board.mjs | */board.mjs | */task-new.mjs | */check-schema-drift.mjs)
|
|
85
|
+
printf '%s\n' 'git-workflow' ;;
|
|
86
|
+
# Своё правило линтера кода пишется по тем же соглашениям, что и код под ним.
|
|
87
|
+
*/eslint-rules/*) printf '%s\n' 'typescript-conventions' ;;
|
|
88
|
+
|
|
89
|
+
# Схема хранилища и её миграции: порядок каталогов лексикографический, а метку
|
|
90
|
+
# времени ставит инструмент в момент заведения — цепочка ломается молча и падает
|
|
91
|
+
# только накатом с нуля, то есть уже после слияния. Правило живёт при поставке.
|
|
92
|
+
*/schema.prisma | */prisma/migrations/*) printf '%s\n' 'git-workflow' ;;
|
|
93
|
+
# Конвейер и образ: проверки решают, что вообще гоняется до слияния, а образ —
|
|
94
|
+
# что приезжает на прод. И то и другое правилось без единого правила поставки.
|
|
95
|
+
*/.github/workflows/*.yml | */.gitlab-ci.yml | */azure-pipelines*.yml)
|
|
96
|
+
printf '%s\n' 'git-workflow' ;;
|
|
97
|
+
*/Dockerfile | */*.Dockerfile | */docker-compose*.yml | */docker-compose*.yaml)
|
|
98
|
+
printf '%s\n' 'git-workflow' ;;
|
|
99
|
+
|
|
100
|
+
# Сквозная спека проверяет поднятое приложение, а не класс: по имени файла она от
|
|
101
|
+
# обычного модуля не отличается, и без этой ветки уходила бы в соглашения языка.
|
|
102
|
+
*-e2e/*) printf '%s\n' 'testing' ;;
|
|
103
|
+
|
|
84
104
|
# Поставка: состав зависимостей — это то, что приезжает на прод. Правка
|
|
85
105
|
# скриптов зависимостью не является, и правило про версии на неё не вступает.
|
|
86
106
|
# Оговорка: удаление зависимости приходит правкой без номера версии и сюда не
|
|
@@ -105,18 +125,54 @@ skill_for_default() {
|
|
|
105
125
|
|
|
106
126
|
*.ts) printf '%s\n' 'typescript-conventions' ;;
|
|
107
127
|
esac
|
|
108
|
-
skill_for_written "$target" "$written"
|
|
109
128
|
;;
|
|
110
129
|
bash)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
# Ветки идут проверкой на вызов, а не совпадением по подстроке: упоминание команды
|
|
131
|
+
# командой не является, и гейт отбивал собственный текст о коммите.
|
|
132
|
+
if rt_gate_invokes "$target" "git[[:space:]]+(commit|push|merge|rebase|cherry-pick)" \
|
|
133
|
+
|| rt_gate_invokes "$target" "git[[:space:]]+worktree[[:space:]]+(add|remove)" \
|
|
134
|
+
|| rt_gate_invokes "$target" "git[[:space:]]+checkout[[:space:]]+-b" \
|
|
135
|
+
|| rt_gate_invokes "$target" "git[[:space:]]+switch[[:space:]]+-c" \
|
|
136
|
+
|| rt_gate_invokes "$target" "(gh|glab)[[:space:]]+(pr|mr|issue)[[:space:]]+(create|merge|edit)" \
|
|
137
|
+
|| rt_gate_invokes "$target" "az[[:space:]]+(repos|boards)" \
|
|
138
|
+
|| rt_gate_invokes "$target" "[^[:space:]]*task:new" \
|
|
139
|
+
|| rt_gate_invokes "$target" "prisma[[:space:]]+(migrate|db)"; then
|
|
140
|
+
printf '%s\n' 'git-workflow'
|
|
141
|
+
# Правка тела отчёта через клиент хостинга ловится двумя признаками сразу — вызовом
|
|
142
|
+
# клиента И адресом запроса: одного слова о заявке мало, оно попадает в строку любой
|
|
143
|
+
# команды, которая о ней пишет. Тело отчёта не читает ни одна проверка, и утверждение
|
|
144
|
+
# о дереве стареет в нём молча.
|
|
145
|
+
elif rt_gate_invokes "$target" "(gh|glab)[[:space:]]+api" \
|
|
146
|
+
&& printf '%s' "$target" | grep -qE '(-X|--method)[[:space:]]+(PATCH|PUT).*(pulls|merge_requests)/[0-9]+'; then
|
|
147
|
+
printf '%s\n' 'git-workflow'
|
|
148
|
+
# Образы и реестр на машине владельца: там же лежат его собственные стенды и работы
|
|
149
|
+
# других его веток. Снятие и чистка важнее сборки — они уносят чужое безвозвратно.
|
|
150
|
+
# Команды чтения остаются вне гейта: ими нехватку места и разбирают, и требовать на
|
|
151
|
+
# них правило значило бы отбивать сам приём. Поэтому общая чистка ловится с `prune`.
|
|
152
|
+
elif rt_gate_invokes "$target" "docker[[:space:]]+(build|buildx|pull|push|run|compose|login|rm|rmi|stop|start|restart|image|volume|builder|network)" \
|
|
153
|
+
|| rt_gate_invokes "$target" "docker[[:space:]]+system[[:space:]]+prune"; then
|
|
154
|
+
printf '%s\n' 'git-workflow'
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Слияние отчёта — последний момент, когда папку закрытой задачи ещё можно разобрать
|
|
158
|
+
# тем же отчётом: после слияния сверка очереди её видит, а отвечать за неё уже
|
|
159
|
+
# некому. Требуется ВТОРЫМ слоем, дополнительно к правилу поставки.
|
|
160
|
+
rt_gate_invokes "$target" "(gh[[:space:]]+pr|glab[[:space:]]+mr)[[:space:]]+merge" \
|
|
161
|
+
&& printf '%s\n' 'task-flow'
|
|
162
|
+
|
|
163
|
+
# Обращение к поднятому приложению: врёт здесь не код, а то, что отвечает на порту.
|
|
164
|
+
# Ответ сборки прошлого захода неотличим от ответа живой ветки. Нужны оба признака —
|
|
165
|
+
# вызов клиента И адрес: одного адреса мало, он попадает в строку любой команды,
|
|
166
|
+
# которая о нём пишет, и гейт отбивал проверку самого гейта. Порт не перечисляется:
|
|
167
|
+
# свой разовый стенд поднимается на любом свободном.
|
|
168
|
+
if printf '%s' "$target" | grep -qE '(localhost|127\.0\.0\.1):[0-9]{4,5}' \
|
|
169
|
+
&& { rt_gate_invokes "$target" curl || rt_gate_invokes "$target" wget; }; then
|
|
170
|
+
printf '%s\n' 'browser-verification'
|
|
171
|
+
fi
|
|
119
172
|
;;
|
|
173
|
+
# Проверка через браузер — единственная область, где правило нужно не под правку файла, а
|
|
174
|
+
# под инструмент: врут там не файлы, а стенд и координаты.
|
|
175
|
+
browser) printf '%s\n' 'browser-verification' ;;
|
|
120
176
|
esac
|
|
121
177
|
|
|
122
178
|
return 0
|
|
@@ -25,19 +25,42 @@ rt_runner() {
|
|
|
25
25
|
# Порты у каждого дерева свои, поэтому умолчание молчит: назвать чужой порт хуже, чем не назвать.
|
|
26
26
|
RT_STANDS="${RT_STANDS:-}"
|
|
27
27
|
|
|
28
|
+
# Где лежат проверки дерева и набор сценариев его гардов. Проверку, которой в дереве нет, гейт
|
|
29
|
+
# пуша не зовёт: список печатается по тому, что лежит на диске.
|
|
30
|
+
RT_CHECKS_DIR="${RT_CHECKS_DIR:-tools}"
|
|
31
|
+
RT_HOOKS_TESTS="${RT_HOOKS_TESTS:-.claude/hooks/tests/run.sh}"
|
|
32
|
+
|
|
28
33
|
# Команды, которые обязаны пройти перед пушем. По одной на строку; первая упавшая отбивает пуш.
|
|
29
34
|
# Линтер стилей отдельной строкой: линтер кода файлы стилей не читает вовсе.
|
|
30
35
|
#
|
|
31
36
|
# Первый параметр — база: ветка, относительно которой считается вклад. Пустая означает, что
|
|
32
37
|
# удалённого нет, и тогда гоняется всё: набор строже нужного безопасен, набор уже нужного — нет.
|
|
38
|
+
#
|
|
39
|
+
# Сборка идёт наравне с линтом и спеками. Линтер типов не читает, а спеки читают только то, что
|
|
40
|
+
# кто-то ввёз в них импортом: ошибка типов в непокрытом коде доживает до сборки образа, то есть
|
|
41
|
+
# до слияния. Стоит это мало — дальше работает кэш прогонщика.
|
|
42
|
+
#
|
|
43
|
+
# Заведённая проверка встаёт сюда, а не только в общий прогон, который никто не зовёт сам:
|
|
44
|
+
# новая строка в её списке известного уезжает в главную ветку молча, а список при этом читается
|
|
45
|
+
# как действующая охрана.
|
|
33
46
|
rt_push_checks_default() {
|
|
34
47
|
runner="$(rt_runner)"
|
|
48
|
+
root="${CLAUDE_PROJECT_DIR:-.}"
|
|
35
49
|
if [ -n "$1" ]; then
|
|
36
|
-
printf '%s\n' "$runner nx affected -t lint test --base=$1"
|
|
50
|
+
printf '%s\n' "$runner nx affected -t lint test build --base=$1"
|
|
37
51
|
else
|
|
38
|
-
printf '%s\n' "$runner nx run-many -t lint test --all"
|
|
52
|
+
printf '%s\n' "$runner nx run-many -t lint test build --all"
|
|
39
53
|
fi
|
|
40
|
-
[ -f "$
|
|
54
|
+
[ -f "$root/stylelint.config.js" ] && printf '%s\n' "$runner stylelint \"**/*.scss\" --max-warnings 0"
|
|
55
|
+
|
|
56
|
+
# Сценарии гардов — такой же код, как всё остальное: на них держится и разбор ветки, и
|
|
57
|
+
# уверенность, что обвязка ещё работает. Прогон занимает секунды: он ничего не собирает.
|
|
58
|
+
[ -x "$root/$RT_HOOKS_TESTS" ] && printf '%s\n' "bash $RT_HOOKS_TESTS"
|
|
59
|
+
|
|
60
|
+
for check in check-doc-paths check-specs check-file-size check-dupes check-styles \
|
|
61
|
+
check-lib-layers check-reuse check-schema-drift; do
|
|
62
|
+
[ -f "$root/$RT_CHECKS_DIR/$check.mjs" ] && printf '%s\n' "node $RT_CHECKS_DIR/$check.mjs"
|
|
63
|
+
done
|
|
41
64
|
|
|
42
65
|
return 0
|
|
43
66
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Слои гейта правил: требования, которые приходят ПОВЕРХ доменного.
|
|
3
|
+
#
|
|
4
|
+
# Доменное правило выбирается один раз по пути файла — у правки один предмет, и правило под него
|
|
5
|
+
# одно. Слоёв поверх него полтора десятка: доступ к среде исполнения виден только в тексте
|
|
6
|
+
# правки, наблюдаемость приходит вместе с доменом, а не вместо него, проза и ведение работы
|
|
7
|
+
# судят тот же файл вторым признаком. Вместе они не помещаются в карту гейта, которую читают
|
|
8
|
+
# целиком, — поэтому лежат здесь.
|
|
9
|
+
#
|
|
10
|
+
# Подключается из `skill-gate.sh` в его же оболочке: читает `$input`, `$target` и `$req` и
|
|
11
|
+
# дописывает имена правил в `$req`. Отдельным процессом слои возвращали бы то же самое через
|
|
12
|
+
# диск.
|
|
13
|
+
#
|
|
14
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нечем разобрать вход — слой молчит. Разбор входа здесь побочная
|
|
15
|
+
# работа, и её поломка не имеет права остановить правку.
|
|
16
|
+
#
|
|
17
|
+
# Адреса дерева слои не знают: где у него бэкенд, витрина и сквозные тесты, говорит само дерево
|
|
18
|
+
# — функцией `skill_layer_skip <правило> <цель>` в своей карте гейта. Нет её — слой действует
|
|
19
|
+
# везде, где подошёл его признак.
|
|
20
|
+
|
|
21
|
+
# Уже названное вторым разом не требуется: отказ, перечисляющий одно правило дважды, читается
|
|
22
|
+
# как два разных требования.
|
|
23
|
+
rt_layer_add() {
|
|
24
|
+
case " $req " in
|
|
25
|
+
*" $1 "*) return 0 ;;
|
|
26
|
+
esac
|
|
27
|
+
req="${req:+$req }$1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Дерево вправе снять слой с места, где признак законен: прямое обращение к среде исполнения на
|
|
31
|
+
# бэкенде, чтение окружения в обвязке, заведение файла в сквозных тестах.
|
|
32
|
+
rt_layer_allowed() {
|
|
33
|
+
command -v skill_layer_skip >/dev/null 2>&1 || return 0
|
|
34
|
+
skill_layer_skip "$1" "$2" && return 1
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Текст правки читается один раз на все слои: разбор входа стоит дороже самих признаков.
|
|
39
|
+
rt_layer_payload=""
|
|
40
|
+
if command -v jq >/dev/null 2>&1; then
|
|
41
|
+
rt_layer_payload="$(printf '%s' "$input" \
|
|
42
|
+
| jq -r '[.tool_input.content, .tool_input.text, .tool_input.new_string, (.tool_input.edits[]?.new_string)]
|
|
43
|
+
| map(select(. != null)) | join("\n")' 2>/dev/null)"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Спека проверяет поведение, а не заводит его: там нужен `testing`, и слои поведения её обходят.
|
|
47
|
+
rt_layer_is_spec=1
|
|
48
|
+
case "$target" in *.spec.ts) rt_layer_is_spec=0 ;; esac
|
|
49
|
+
|
|
50
|
+
# --- слой по тексту: обращение к среде исполнения ---------------------------------------------
|
|
51
|
+
#
|
|
52
|
+
# Путь говорит, ЧТО за файл, а обращение к глобальному объекту видно только в содержимом: гейт,
|
|
53
|
+
# знающий один путь, пропускает его молча. Значение, полученное внедрением, признаком не
|
|
54
|
+
# считается — это уже зависимость, а не прямое обращение.
|
|
55
|
+
if [ -n "$rt_layer_payload" ] && [ "$rt_layer_is_spec" = 1 ]; then
|
|
56
|
+
case "$target" in
|
|
57
|
+
*/main.ts|*/main.server.ts|*/server.ts|*/index.html) ;;
|
|
58
|
+
*.ts|*.html)
|
|
59
|
+
if printf '%s' "$rt_layer_payload" | grep -qE \
|
|
60
|
+
'globalThis|PLATFORM_ID|isPlatformBrowser|defaultView|(^|[^[:alnum:]_.#$])window[[:space:]]*\.'; then
|
|
61
|
+
rt_layer_allowed platform-access "$target" && rt_layer_add platform-access
|
|
62
|
+
fi
|
|
63
|
+
;;
|
|
64
|
+
esac
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# --- слой по тексту: наблюдаемость ------------------------------------------------------------
|
|
68
|
+
#
|
|
69
|
+
# Чтение переменной окружения и есть тот момент, когда заводится новая необязательная
|
|
70
|
+
# возможность. Сводка старта перечисляет их руками, и забывшая дописать себя не попадёт ни в
|
|
71
|
+
# один из трёх списков — на проде она выглядит не выключенной, а несуществующей.
|
|
72
|
+
if [ -n "$rt_layer_payload" ] && [ "$rt_layer_is_spec" = 1 ]; then
|
|
73
|
+
case "$target" in
|
|
74
|
+
*.ts)
|
|
75
|
+
if printf '%s' "$rt_layer_payload" | grep -qE 'process\.env'; then
|
|
76
|
+
rt_layer_allowed observability "$target" && rt_layer_add observability
|
|
77
|
+
fi
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
# --- слой по тексту: общий код ----------------------------------------------------------------
|
|
83
|
+
#
|
|
84
|
+
# Заводимое число-настройка и заводимое перечисление — тот момент, когда рядом с уже общим
|
|
85
|
+
# появляется копия. Каждая копия сама по себе исправна, и ни линт, ни сборка второй не видят.
|
|
86
|
+
# Спеки не в счёт: там значения местные, это фикстуры.
|
|
87
|
+
if [ -n "$rt_layer_payload" ] && [ "$rt_layer_is_spec" = 1 ]; then
|
|
88
|
+
case "$target" in
|
|
89
|
+
*.ts)
|
|
90
|
+
if printf '%s' "$rt_layer_payload" | grep -qE \
|
|
91
|
+
'(^|[[:space:]])(export[[:space:]]+)?const[[:space:]]+[A-Z][A-Z0-9_]*[[:space:]]*(:[[:space:]]*number[[:space:]]*)?=[[:space:]]*[0-9]|(^|[[:space:]])export[[:space:]]+enum[[:space:]]'; then
|
|
92
|
+
rt_layer_allowed shared-code "$target" && rt_layer_add shared-code
|
|
93
|
+
fi
|
|
94
|
+
;;
|
|
95
|
+
esac
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# --- слой по пути: классы Angular -------------------------------------------------------------
|
|
99
|
+
#
|
|
100
|
+
# Компонент и стор — тоже классы: сигнальный API входов, обнаружение изменений и место подписки
|
|
101
|
+
# живут в `angular-patterns`, а первым слоем эти файлы уходят в устройство компонента и в
|
|
102
|
+
# соглашения языка, где ничего этого нет.
|
|
103
|
+
if [ "$rt_layer_is_spec" = 1 ]; then
|
|
104
|
+
case "$target" in
|
|
105
|
+
*.component.ts|*.store.ts)
|
|
106
|
+
rt_layer_allowed angular-patterns "$target" && rt_layer_add angular-patterns
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# --- слой по пути: проза ----------------------------------------------------------------------
|
|
112
|
+
#
|
|
113
|
+
# Формат спека держит `spec-driven`, а как формулировать — `doc-style`, и нужен он не только
|
|
114
|
+
# спекам. Список известного у проверки — та же проза: его поле объясняет, что перечисленное
|
|
115
|
+
# отказом не считается, а сверка текстов читает только `.md`.
|
|
116
|
+
#
|
|
117
|
+
# Хозяйство самого агента слой обходит: правило на него — оно само, и карта гейта решает про
|
|
118
|
+
# эти файлы целиком. Слой, наложенный поверх, требовал бы правило там, где карта его нарочно
|
|
119
|
+
# не назвала.
|
|
120
|
+
case "$target" in
|
|
121
|
+
*/.claude/*) ;;
|
|
122
|
+
*.md|*-allowlist.json) rt_layer_allowed doc-style "$target" && rt_layer_add doc-style ;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
# --- слой по пути: ведение работы -------------------------------------------------------------
|
|
126
|
+
#
|
|
127
|
+
# Папка задачи, договорённость о продукте до кода и линия работ — первые файлы, которые
|
|
128
|
+
# заводятся в работе. Требование ловит на них того, кто пошёл мимо порядка: «пришла новая
|
|
129
|
+
# задача» инструментом не является, и поймать это больше нечем.
|
|
130
|
+
case "$target" in
|
|
131
|
+
*/docs/tasks/*|*/docs/specs/*/proposed/*|*/docs/plans/*)
|
|
132
|
+
rt_layer_allowed task-flow "$target" && rt_layer_add task-flow
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
|
|
136
|
+
# --- слой по заведению файла ------------------------------------------------------------------
|
|
137
|
+
#
|
|
138
|
+
# Ничего не пишется с нуля, и спрашивается это там, где решение и принимается, — на заведении
|
|
139
|
+
# нового файла: у правки существующего опора уже выбрана, а требовать правило на каждую строку
|
|
140
|
+
# значит сделать его фоном.
|
|
141
|
+
case "$target" in
|
|
142
|
+
*/docs/*) ;;
|
|
143
|
+
*.spec.ts|*.stories.ts) ;;
|
|
144
|
+
*.ts|*.html|*.scss)
|
|
145
|
+
[ -f "$target" ] || { rt_layer_allowed reuse-first "$target" && rt_layer_add reuse-first; }
|
|
146
|
+
;;
|
|
147
|
+
esac
|
|
148
|
+
|
|
149
|
+
# Куда встаёт заведённая проверка и какой формы у неё список известного — утверждения `testing`,
|
|
150
|
+
# и нужны они ровно в момент заведения: у существующей проверки и место в гейте, и форма списка
|
|
151
|
+
# уже выбраны.
|
|
152
|
+
case "$target" in
|
|
153
|
+
*/check-*.mjs|*-allowlist.json)
|
|
154
|
+
[ -f "$target" ] || { rt_layer_allowed testing "$target" && rt_layer_add testing; }
|
|
155
|
+
;;
|
|
156
|
+
esac
|
|
@@ -62,6 +62,11 @@ case "$tool" in
|
|
|
62
62
|
# приходит в обычный сервис, а число-настройка в обычный класс.
|
|
63
63
|
written="$(printf '%s' "$input" | jq -r '[.tool_input.content, .tool_input.text, .tool_input.new_string, (.tool_input.edits[]?.new_string)] | map(select(. != null)) | join("\n")' 2>/dev/null)"
|
|
64
64
|
req="$(skill_for edit "$target" "$written" 2>/dev/null)"
|
|
65
|
+
# Слои поверх доменного правила лежат отдельным файлом и зовутся в этой же оболочке:
|
|
66
|
+
# доменное правило выбирается один раз по пути, а слоёв полтора десятка, и вместе они не
|
|
67
|
+
# помещаются в карту, которую читают целиком. Нет файла — гейт остаётся одним слоем.
|
|
68
|
+
# shellcheck disable=SC1090
|
|
69
|
+
[ -f "$rt_hooks_dir/skill-gate-layers.sh" ] && . "$rt_hooks_dir/skill-gate-layers.sh" 2>/dev/null
|
|
65
70
|
# Род правки для наблюдения. Одно расширение, без пути и без имени файла: наблюдение
|
|
66
71
|
# уезжает наружу, и всё, кроме рода, там было бы адресом этого дерева.
|
|
67
72
|
case "${target##*/}" in
|
|
@@ -123,7 +128,11 @@ req="$want"
|
|
|
123
128
|
[ -f "$rt_hooks_dir/observe.sh" ] && . "$rt_hooks_dir/observe.sh" 2>/dev/null
|
|
124
129
|
command -v rt_note >/dev/null 2>&1 && rt_note gate-deny "res=$req" "kind=$kind" "sid=$sid"
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
# Запасной ход называется прямо в отказе: правило, заведённое в этой же ветке, реестру правил
|
|
132
|
+
# неизвестно — он собирается на запуске сессии, а гейт читает диск. Без этой строки следующий
|
|
133
|
+
# заход ищет обход перебором и обычно находит не тот.
|
|
134
|
+
fallback="Если инструмент такого имени не знает, правило завели после начала сессии — прочитай ${rules_dir}/${req}/SKILL.md и спутник рядом с ним."
|
|
135
|
+
reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
|
|
127
136
|
|
|
128
137
|
# Правило называет свой закон одним словом, а слоёв законов два: общий лежит в корне, закон
|
|
129
138
|
# приложения — в каталоге под ним. Путь ищется, а не собирается из имени, иначе отказ ведёт в
|
|
@@ -134,7 +143,7 @@ if [ -n "$law" ]; then
|
|
|
134
143
|
law_path="$laws_dir/${law}.md"
|
|
135
144
|
[ -f "$root/$law_path" ] || law_path="$laws_dir/application/${law}.md"
|
|
136
145
|
[ -f "$root/$law_path" ] \
|
|
137
|
-
&& reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill — оно применяет закон ${law_path} к этому дереву — и повтори действие. Для этой области это происходит один раз за сессию."
|
|
146
|
+
&& reason="Отбито гейтом правил: загрузи правило «${req}» инструментом Skill — оно применяет закон ${law_path} к этому дереву — и повтори действие. ${fallback} Для этой области это происходит один раз за сессию."
|
|
138
147
|
fi
|
|
139
148
|
|
|
140
149
|
jq -n --arg r "$reason" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}' 2>/dev/null \
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
причина названа рядом.
|
|
21
21
|
- **Отметка об устаревании — повод убрать, а не повод оставить.** Устаревшее объявление,
|
|
22
22
|
которое молча продолжает работать, переживает того, кто его пометил.
|
|
23
|
+
- **Файл читается целиком.** Длина, при которой его читают по частям, объявлена одним числом на
|
|
24
|
+
все роды файлов, и накопленное до объявления перечислено поимённо: перечень отмечает долг, а
|
|
25
|
+
не выдаёт разрешение.
|
|
23
26
|
|
|
24
27
|
## Открытые вопросы
|
|
25
28
|
|
package/assets/laws/delivery.md
CHANGED
|
@@ -75,3 +75,12 @@
|
|
|
75
75
|
его написали, а разбора ждёт днями: за это время главная ветка вливается в ветку, и
|
|
76
76
|
утверждение отчёта о соседних файлах становится неправдой молча — тел отчётов не читает ни
|
|
77
77
|
одна проверка. Всё, что вливается в ветку после публикации отчёта, — повод перечитать его.
|
|
78
|
+
- **Слияние в главную ветку ещё не означает, что правка доехала.** Отказ выкатки не трогает ни
|
|
79
|
+
задачу, ни очередь работ, поэтому расхождение главной ветки с тем, что работает, обязано быть
|
|
80
|
+
видно там, где очередь читают. Иначе следующие работы вливаются поверх поломки, которую не
|
|
81
|
+
приносили, и каждая выглядит доехавшей.
|
|
82
|
+
- **Состоявшаяся поломка разбирается записью, которая переживает задачу.** Починка уезжает
|
|
83
|
+
веткой, задача закрывается — и причина, по которой приложение встало, остаётся знанием одного
|
|
84
|
+
исполнителя. Запись называет, что сломалось, чем это стало видно и почему починка чинит
|
|
85
|
+
причину, а не признак; живёт она среди описаний состоявшегося, а не там, что умирает вместе с
|
|
86
|
+
задачей.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Закон о наблюдаемости
|
|
2
|
+
|
|
3
|
+
Что владелец знает о работе своего приложения. Если о поломке можно узнать только через доступ
|
|
4
|
+
к серверу, владелец узнаёт о ней от гостя и с опозданием.
|
|
5
|
+
|
|
6
|
+
**Ревизия:** 2026-08-13
|
|
7
|
+
|
|
8
|
+
## Статьи
|
|
9
|
+
|
|
10
|
+
- **Отказ приложения виден владельцу без доступа к серверу.** На сервер владелец не заходит,
|
|
11
|
+
поэтому всё, что видно только оттуда, для него недоступно.
|
|
12
|
+
- **Отказ хранится и после перезапуска приложения, и спустя месяцы.** Поломку разбирают уже
|
|
13
|
+
после того, как она случилась, а вывод приложения к этому времени затирается.
|
|
14
|
+
- **Все отказы одного обращения находятся по одному признаку.** При десятке обращений в секунду
|
|
15
|
+
по времени их не разделить.
|
|
16
|
+
- **Тот, кто наткнулся на отказ, получает номер обращения и может его назвать.** Иначе по жалобе
|
|
17
|
+
нечего искать.
|
|
18
|
+
- **Наружу уходит номер обращения и общий текст ошибки. Подробности остаются внутри.**
|
|
19
|
+
Подробности показывают, как приложение устроено.
|
|
20
|
+
- **Подробности отказа закрыты правом.** В них видно, кто был в системе и чем именно приложение
|
|
21
|
+
сломалось.
|
|
22
|
+
- **Владелец узнаёт о новом отказе сам, без захода на экран.** Иначе поломку заметят только
|
|
23
|
+
тогда, когда до экрана дойдут руки.
|
|
24
|
+
- **У обращения к чужой службе есть предел ожидания.** Молчащая служба отличается от отказавшей
|
|
25
|
+
только временем: без предела обращение висит до умолчания среды, отказ не наступает вовсе, и
|
|
26
|
+
записывать нечего. Всё это время владелец видит исправную работу.
|
|
27
|
+
- **Обращение к чужой службе оставляет запись, и заводится она до обращения.** Запись после
|
|
28
|
+
ответа говорит только о том, что дошло: попытка, оборвавшаяся посередине, не отличается от не
|
|
29
|
+
начатой, а владелец видит лишь удачные исходы.
|
|
30
|
+
- **Отказ не записывается в ущерб работе приложения.** Ответ не ждёт записи, запрос из-за неё не
|
|
31
|
+
падает. Если записать не удалось, это видно.
|
|
32
|
+
- **Приложение не пишет отказы о собственной записи отказов.** Иначе поломка хранилища порождает
|
|
33
|
+
поток, который сам себя разгоняет.
|
|
34
|
+
- **Отказом считается то, что не сработало, а не каждый обслуженный запрос.** Иначе поток чужих
|
|
35
|
+
обращений определяет, сколько приложение о себе хранит.
|
|
36
|
+
- **У хранилища отказов есть предел, и он проверяется до того, как кончится место.** Иначе
|
|
37
|
+
хранилище отказов положит приложение, за которым следит.
|
|
38
|
+
- **Секреты и личные данные вычищаются перед записью.** После записи их убирают правкой данных,
|
|
39
|
+
а это дороже и не всегда возможно.
|
|
40
|
+
|
|
41
|
+
## Открытые вопросы
|
|
42
|
+
|
|
43
|
+
- **Q-O-1.** Куда девать отказ, случившийся до того, как приложение поднялось. Хранилища в этот
|
|
44
|
+
момент ещё нет. Сейчас такой отказ остаётся только в выводе приложения.
|
|
45
|
+
- **Q-O-2.** Хранить ли отказы обвязки — того, что стоит перед приложением и отдаёт страницы.
|
|
46
|
+
Её вывод к приложению не приходит, и читать его надо отдельно.
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
— это намерение, а не свойство приложения: сверить его не с чем, и оно проходит любую
|
|
34
34
|
проверку. Машине это не поручить: открытый вопрос пишется теми же словами, что и обещание,
|
|
35
35
|
и проверка отбивала бы оба.
|
|
36
|
+
- **Полнота текстов проверяется и со стороны работы, а не только со стороны текста.** Обход
|
|
37
|
+
написанного судит каждое утверждение, но утверждения, которого нет, в этом обходе нет тоже:
|
|
38
|
+
приём, применённый и нигде не описанный, так не находится никогда. Поэтому закрытая работа
|
|
39
|
+
спрашивается отдельно — оставила она след в текстах или явно его не требует.
|
|
36
40
|
- **Документ утверждает о состоявшемся, а не о том, что должно сработать.** Лечение,
|
|
37
41
|
записанное готовым до того, как его прогнали, дороже отсутствия записи: следующий читатель
|
|
38
42
|
берёт его за проверенное — и берёт в тот день, когда лечение понадобилось, а времени на
|