agent-quality-kit 0.2.2
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/LICENSE +21 -0
- package/README.md +155 -0
- package/kit/docs/ai/agent-harness-playbook.md +596 -0
- package/kit/docs/ai/ai-native-development.md +371 -0
- package/kit/docs/ai/ai-sdlc.md +221 -0
- package/kit/docs/ai/anthropic-ai-native-sdlc-2026-08.md +294 -0
- package/kit/docs/ai/app-owner-strategy.md +921 -0
- package/kit/docs/ai/deep-research-2026-07.md +161 -0
- package/kit/docs/ai/harness-best-practices.md +385 -0
- package/kit/docs/ai/index.md +64 -0
- package/kit/docs/ai/project-baseline.md +261 -0
- package/kit/docs/ai/quality-gates-checklist.md +322 -0
- package/kit/docs/ai/sources-building-with-agents.md +111 -0
- package/kit/docs/ai/stream-2026-08-ai-coding-panel.md +304 -0
- package/kit/docs/ready-made-rules.md +170 -0
- package/kit/gates/README.md +231 -0
- package/kit/gates/_skip.sh +75 -0
- package/kit/gates/commit-explains-itself/README.md +45 -0
- package/kit/gates/commit-explains-itself/check.sh +63 -0
- package/kit/gates/commit-explains-itself/gate.yml +10 -0
- package/kit/gates/commit-explains-itself/green/COMMIT_MSG +6 -0
- package/kit/gates/commit-explains-itself/red/COMMIT_MSG +3 -0
- package/kit/gates/complexity-limit/README.md +37 -0
- package/kit/gates/complexity-limit/check.sh +44 -0
- package/kit/gates/complexity-limit/gate.yml +13 -0
- package/kit/gates/complexity-limit/green/flat.py +10 -0
- package/kit/gates/complexity-limit/red/deep.py +9 -0
- package/kit/gates/dead-code/README.md +30 -0
- package/kit/gates/dead-code/gate.yml +23 -0
- package/kit/gates/dead-code/green/mod.py +9 -0
- package/kit/gates/dead-code/red/mod.py +9 -0
- package/kit/gates/deps-are-pinned/README.md +29 -0
- package/kit/gates/deps-are-pinned/check.sh +49 -0
- package/kit/gates/deps-are-pinned/gate.yml +9 -0
- package/kit/gates/deps-are-pinned/green/nodep-go/go.mod +3 -0
- package/kit/gates/deps-are-pinned/green/package-lock.json +3 -0
- package/kit/gates/deps-are-pinned/green/package.json +4 -0
- package/kit/gates/deps-are-pinned/green/requirements.txt +2 -0
- package/kit/gates/deps-are-pinned/red/package.json +4 -0
- package/kit/gates/deps-are-pinned/red/requirements.txt +2 -0
- package/kit/gates/deps-are-pinned/red/withdep-go/go.mod +5 -0
- package/kit/gates/duplicate-code/README.md +40 -0
- package/kit/gates/duplicate-code/check.sh +58 -0
- package/kit/gates/duplicate-code/gate.yml +12 -0
- package/kit/gates/duplicate-code/green/common.py +9 -0
- package/kit/gates/duplicate-code/green/use.py +9 -0
- package/kit/gates/duplicate-code/red/a.py +12 -0
- package/kit/gates/duplicate-code/red/b.py +12 -0
- package/kit/gates/entry-links-exist/README.md +22 -0
- package/kit/gates/entry-links-exist/check.sh +24 -0
- package/kit/gates/entry-links-exist/gate.yml +16 -0
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/entry-links-exist/green/rules/general.md +3 -0
- package/kit/gates/entry-links-exist/red/AGENTS.md +3 -0
- package/kit/gates/file-size-limit/README.md +22 -0
- package/kit/gates/file-size-limit/check.sh +34 -0
- package/kit/gates/file-size-limit/gate.yml +9 -0
- package/kit/gates/file-size-limit/green/a.py +251 -0
- package/kit/gates/file-size-limit/green/b.py +251 -0
- package/kit/gates/file-size-limit/red/big.py +601 -0
- package/kit/gates/gate-has-samples/README.md +29 -0
- package/kit/gates/gate-has-samples/check.sh +48 -0
- package/kit/gates/gate-has-samples/gate.yml +9 -0
- package/kit/gates/gate-has-samples/green/.aqk.yml +10 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/check.sh +2 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/green/good.py +2 -0
- package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/red/bad.py +1 -0
- package/kit/gates/gate-has-samples/red/.aqk.yml +10 -0
- package/kit/gates/gate-has-samples/red/gates/no-print-in-prod/check.sh +2 -0
- package/kit/gates/gates-are-runnable/README.md +23 -0
- package/kit/gates/gates-are-runnable/check.sh +35 -0
- package/kit/gates/gates-are-runnable/gate.yml +9 -0
- package/kit/gates/gates-are-runnable/green/.aqk.yml +9 -0
- package/kit/gates/gates-are-runnable/green/checks/lint.sh +2 -0
- package/kit/gates/gates-are-runnable/red/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/README.md +29 -0
- package/kit/gates/gates-run-in-ci/check.sh +42 -0
- package/kit/gates/gates-run-in-ci/gate.yml +12 -0
- package/kit/gates/gates-run-in-ci/green/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/green/.github/workflows/ci.yml +7 -0
- package/kit/gates/gates-run-in-ci/green/checks/lint.sh +2 -0
- package/kit/gates/gates-run-in-ci/red/.aqk.yml +6 -0
- package/kit/gates/gates-run-in-ci/red/.github/workflows/ci.yml +7 -0
- package/kit/gates/gates-run-in-ci/red/checks/lint.sh +2 -0
- package/kit/gates/lesson-has-outcome/README.md +37 -0
- package/kit/gates/lesson-has-outcome/check.sh +50 -0
- package/kit/gates/lesson-has-outcome/gate.yml +11 -0
- package/kit/gates/lesson-has-outcome/green/.aqk.yml +2 -0
- package/kit/gates/lesson-has-outcome/green/incidents/README.md +32 -0
- package/kit/gates/lesson-has-outcome/red/.aqk.yml +2 -0
- package/kit/gates/lesson-has-outcome/red/incidents/README.md +13 -0
- package/kit/gates/no-print-in-prod/README.md +44 -0
- package/kit/gates/no-print-in-prod/check.sh +36 -0
- package/kit/gates/no-print-in-prod/gate.yml +15 -0
- package/kit/gates/no-print-in-prod/green/docs.ts +15 -0
- package/kit/gates/no-print-in-prod/green/legacy.py +9 -0
- package/kit/gates/no-print-in-prod/green/main.go +8 -0
- package/kit/gates/no-print-in-prod/green/main.rs +4 -0
- package/kit/gates/no-print-in-prod/green/service.py +8 -0
- package/kit/gates/no-print-in-prod/red/main.go +8 -0
- package/kit/gates/no-print-in-prod/red/main.rs +4 -0
- package/kit/gates/no-print-in-prod/red/service.py +3 -0
- package/kit/gates/secrets-not-in-code/README.md +29 -0
- package/kit/gates/secrets-not-in-code/check.sh +18 -0
- package/kit/gates/secrets-not-in-code/gate.yml +9 -0
- package/kit/gates/secrets-not-in-code/green/settings.py +4 -0
- package/kit/gates/secrets-not-in-code/red/settings.py +2 -0
- package/kit/gates/swallowed-error/README.md +26 -0
- package/kit/gates/swallowed-error/check.sh +54 -0
- package/kit/gates/swallowed-error/gate.yml +12 -0
- package/kit/gates/swallowed-error/green/loader.py +11 -0
- package/kit/gates/swallowed-error/green/run.js +8 -0
- package/kit/gates/swallowed-error/red/loader.py +5 -0
- package/kit/gates/swallowed-error/red/run.js +3 -0
- package/kit/gates/todo-without-task/README.md +26 -0
- package/kit/gates/todo-without-task/check.sh +19 -0
- package/kit/gates/todo-without-task/gate.yml +12 -0
- package/kit/gates/todo-without-task/green/order.py +9 -0
- package/kit/gates/todo-without-task/red/order.py +8 -0
- package/kit/ratchet/ratchet.sh +62 -0
- package/kit/rules/general.md +55 -0
- package/kit/rules/security.md +33 -0
- package/kit/rules/testing.md +46 -0
- package/package.json +41 -0
- package/tool/commands/doctor.mjs +230 -0
- package/tool/commands/gates.mjs +445 -0
- package/tool/commands/project.mjs +316 -0
- package/tool/lib/core.mjs +98 -0
- package/tool/lib/manifest.mjs +140 -0
- package/tool/lib/repo.mjs +270 -0
- package/tool/lib/templates.mjs +187 -0
- package/tool/program.mjs +81 -0
- package/tool/selfcheck/conditional.sh +24 -0
- package/tool/selfcheck/gates.sh +127 -0
- package/tool/selfcheck/smoke.sh +539 -0
- package/tool/selfcheck/syntax.sh +23 -0
- package/tool/selfcheck/units.mjs +105 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// tool/lib/repo.mjs — осмотр репозитория и каталог обещаний: языки и признаки, условия
|
|
2
|
+
// триггера, выбор рецепта, сверка по намерению.
|
|
3
|
+
|
|
4
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { CWD, GATES_SRC, c, exists } from "./core.mjs";
|
|
8
|
+
import { parseManifest } from "./manifest.mjs";
|
|
9
|
+
|
|
10
|
+
// Каталог лежит в комплекте, а не в проекте: записи общие для всех, проект лишь
|
|
11
|
+
// решает, какие из них у него стоят. Показывать все подряд нельзя — это и есть
|
|
12
|
+
// разница между каталогом и списком: у каждой записи обязателен триггер, и до
|
|
13
|
+
// глаз человека доходит только применимое к его репозиторию.
|
|
14
|
+
|
|
15
|
+
// Расширения перечислены полностью, включая модульные варианты: .mjs пропускался, и на самом
|
|
16
|
+
// aqk — где вся программа лежит в .mjs — запись про отладочную печать пряталась с пояснением
|
|
17
|
+
// «нет языков: javascript». Гейт, спрятанный по неверно опознанному языку, молчит так же, как
|
|
18
|
+
// отсутствующий.
|
|
19
|
+
const EXT_LANG = {
|
|
20
|
+
".py": "python", ".js": "javascript", ".jsx": "javascript",
|
|
21
|
+
".mjs": "javascript", ".cjs": "javascript",
|
|
22
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
23
|
+
".go": "go", ".rs": "rust", ".rb": "ruby", ".java": "java", ".php": "php",
|
|
24
|
+
".cs": "csharp", ".sh": "shell", ".kt": "kotlin", ".swift": "swift", ".scala": "scala",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const SKIP_DIRS = new Set([".git", "node_modules", ".venv", "venv", "dist", "build", "__pycache__", ".aqk"]);
|
|
28
|
+
|
|
29
|
+
// Факты о репозитории. Только то, что видно машине: спрашивать человека анкетой
|
|
30
|
+
// значит снова получить мнение вместо факта.
|
|
31
|
+
// Признаки репозитория — по файлам, а не по анкете. Ответы человека это мнение;
|
|
32
|
+
// файлы — факт. Каждый признак нужен какому-то триггеру: признак, который никто не
|
|
33
|
+
// спрашивает, — это лишний обход дерева.
|
|
34
|
+
const MARKS = [
|
|
35
|
+
["has_ci", [".github/workflows", ".gitlab-ci.yml", ".circleci", "Jenkinsfile", "azure-pipelines.yml"]],
|
|
36
|
+
["has_docker", ["Dockerfile", "compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"]],
|
|
37
|
+
["has_deps", ["package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml", "Gemfile", "pom.xml", "composer.json"]],
|
|
38
|
+
["has_env", [".env", ".env.example", ".env.sample"]],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
async function detectFacts(man) {
|
|
42
|
+
// Каталог образцов самого проекта: туда `add` копирует красные и зелёные примеры записей.
|
|
43
|
+
// Считать их кодом проекта значит врать о репозитории — после установки сторожей пустой
|
|
44
|
+
// проект «становился» проектом на Python, и ему показывались записи про мёртвый код.
|
|
45
|
+
const samplesDir = man?.samples ? resolve(CWD, String(man.samples)) : null;
|
|
46
|
+
const langs = new Set();
|
|
47
|
+
let files = 0;
|
|
48
|
+
let hasDb = false;
|
|
49
|
+
let hasTests = false;
|
|
50
|
+
|
|
51
|
+
async function walk(dir, depth) {
|
|
52
|
+
if (depth > 4 || files > 4000) return;
|
|
53
|
+
let items;
|
|
54
|
+
try {
|
|
55
|
+
items = await readdir(dir, { withFileTypes: true });
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
for (const it of items) {
|
|
60
|
+
const full = join(dir, it.name);
|
|
61
|
+
if (it.isDirectory()) {
|
|
62
|
+
if (SKIP_DIRS.has(it.name)) continue;
|
|
63
|
+
// Образцы каталога — код специально сломанный и специально исправный.
|
|
64
|
+
// Считать его языками проекта значит врать о репозитории.
|
|
65
|
+
if (full === GATES_SRC || (samplesDir && full === samplesDir)) continue;
|
|
66
|
+
if (/^(tests?|spec|__tests__)$/i.test(it.name)) hasTests = true;
|
|
67
|
+
if (/^migrations?$/i.test(it.name)) hasDb = true;
|
|
68
|
+
await walk(full, depth + 1);
|
|
69
|
+
} else {
|
|
70
|
+
files++;
|
|
71
|
+
if (/\.(test|spec)\.[a-z]+$/i.test(it.name) || /^test_.*\.py$/i.test(it.name) || /_test\.go$/i.test(it.name)) hasTests = true;
|
|
72
|
+
if (it.name.endsWith(".sql")) hasDb = true;
|
|
73
|
+
const dot = it.name.lastIndexOf(".");
|
|
74
|
+
if (dot > 0) {
|
|
75
|
+
const lang = EXT_LANG[it.name.slice(dot)];
|
|
76
|
+
if (lang) langs.add(lang);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await walk(CWD, 0);
|
|
82
|
+
|
|
83
|
+
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
84
|
+
const facts = {
|
|
85
|
+
langs,
|
|
86
|
+
files,
|
|
87
|
+
has_db: hasDb,
|
|
88
|
+
has_tests: hasTests,
|
|
89
|
+
has_gates: Object.values(gates).some((c) => String(c || "").trim()),
|
|
90
|
+
gateKeys: Object.keys(gates),
|
|
91
|
+
};
|
|
92
|
+
for (const [name, paths] of MARKS) {
|
|
93
|
+
facts[name] = false;
|
|
94
|
+
for (const rel of paths) if (await exists(join(CWD, rel))) { facts[name] = true; break; }
|
|
95
|
+
}
|
|
96
|
+
return facts;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function readCatalog() {
|
|
100
|
+
if (!(await exists(GATES_SRC))) return [];
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const name of (await readdir(GATES_SRC, { withFileTypes: true })).filter((d) => d.isDirectory())) {
|
|
103
|
+
const yml = join(GATES_SRC, name.name, "gate.yml");
|
|
104
|
+
if (!(await exists(yml))) continue;
|
|
105
|
+
const rec = parseManifest(await readFile(yml, "utf8"));
|
|
106
|
+
out.push({ slug: name.name, ...rec });
|
|
107
|
+
}
|
|
108
|
+
return out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Применима ли запись к этому репозиторию — и если нет, то почему.
|
|
112
|
+
// Причина обязательна: «скрыто без объяснения» неотличимо от «потеряно».
|
|
113
|
+
// Все условия триггера должны выполниться разом. Раньше проверялось первое попавшееся —
|
|
114
|
+
// значит «есть база И нет конвейера» читалось как «есть база», и запись показывалась не тем.
|
|
115
|
+
//
|
|
116
|
+
// Причина отказа обязательна: скрытое без объяснения неотличимо от потерянного.
|
|
117
|
+
const CONDITIONS = {
|
|
118
|
+
always: () => ({ ok: true }),
|
|
119
|
+
|
|
120
|
+
langs: (val, f) => {
|
|
121
|
+
const want = String(val).split(",").map((x) => x.trim()).filter(Boolean);
|
|
122
|
+
return want.some((l) => f.langs.has(l))
|
|
123
|
+
? { ok: true }
|
|
124
|
+
: { ok: false, why: `нет языков: ${want.join(", ")}` };
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
files_gt: (val, f) =>
|
|
128
|
+
f.files > Number(val) ? { ok: true } : { ok: false, why: `меньше ${val} файлов — рано` },
|
|
129
|
+
|
|
130
|
+
files_lt: (val, f) =>
|
|
131
|
+
f.files < Number(val) ? { ok: true } : { ok: false, why: `больше ${val} файлов` },
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const FLAG_WHY = {
|
|
135
|
+
has_gates: ["в манифесте не объявлено ни одного гейта", "гейты уже объявлены"],
|
|
136
|
+
has_ci: ["в репозитории нет конвейера", "конвейер уже есть"],
|
|
137
|
+
has_db: ["не видно базы данных: ни миграций, ни sql", "база данных есть"],
|
|
138
|
+
has_docker: ["нет Dockerfile или compose", "docker уже есть"],
|
|
139
|
+
has_deps: ["не видно файла зависимостей", "зависимости объявлены"],
|
|
140
|
+
has_tests: ["не видно тестов", "тесты есть"],
|
|
141
|
+
has_env: ["нет файла окружения", "файл окружения есть"],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function triggerVerdict(rec, facts) {
|
|
145
|
+
const t = rec.trigger && typeof rec.trigger === "object" && !Array.isArray(rec.trigger) ? rec.trigger : {};
|
|
146
|
+
const keys = Object.keys(t);
|
|
147
|
+
if (!keys.length) return { applies: false, why: "триггер не задан" };
|
|
148
|
+
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
const raw = String(t[key]).trim();
|
|
151
|
+
|
|
152
|
+
if (CONDITIONS[key]) {
|
|
153
|
+
// always: false — не «условие отсутствует», а «запись никогда не применима». Раньше
|
|
154
|
+
// continue пропускал ключ молча, и `always: false` читалось как отсутствие ограничения.
|
|
155
|
+
if (key === "always" && raw !== "true") return { applies: false, why: "always: false" };
|
|
156
|
+
const r = CONDITIONS[key](raw, facts);
|
|
157
|
+
if (!r.ok) return { applies: false, why: r.why };
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (key in FLAG_WHY) {
|
|
162
|
+
const want = raw === "true";
|
|
163
|
+
if (Boolean(facts[key]) !== want) {
|
|
164
|
+
return { applies: false, why: FLAG_WHY[key][want ? 0 : 1] };
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { applies: false, why: `условие «${key}» программа не умеет считать` };
|
|
170
|
+
}
|
|
171
|
+
return { applies: true };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Арбитр под стек этого проекта: сначала родной рецепт, иначе — переносимый `any`.
|
|
175
|
+
// Подсказка, которую нельзя скопировать и выполнить, бесполезна.
|
|
176
|
+
// Выбор рецепта под стек проекта. Одна логика на два места: и `doctor`, и `add` показывают
|
|
177
|
+
// команду, но подставляют в неё разные пути — один в каталог пакета, другой в каталог проекта.
|
|
178
|
+
// Пока это были две копии, правка доезжала до одной из них — нашёл собственный гейт дублей.
|
|
179
|
+
function pickRecipe(rec, facts) {
|
|
180
|
+
const recipes = rec.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
|
|
181
|
+
|
|
182
|
+
// Родной рецепт лучше переносимого — но только если его есть чем выполнить. Поставить
|
|
183
|
+
// команду с неустановленной программой значит завести гейт, который встаёт с «not found»:
|
|
184
|
+
// отсутствие сигнала неотличимо от успеха.
|
|
185
|
+
const runnable = (c0) => {
|
|
186
|
+
const prog = String(c0).trim().split(/\s+/)[0];
|
|
187
|
+
return spawnSync(`command -v ${prog}`, { shell: true, stdio: "ignore" }).status === 0;
|
|
188
|
+
};
|
|
189
|
+
for (const lang of facts.langs) {
|
|
190
|
+
if (!recipes[lang]) continue;
|
|
191
|
+
if (runnable(recipes[lang])) return recipes[lang];
|
|
192
|
+
console.log(c.dim(` ${c.yellow("!")} рецепт под ${lang} пропущен: «${String(recipes[lang]).split(/\s+/)[0]}» не установлен`));
|
|
193
|
+
}
|
|
194
|
+
return recipes.any || null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function recipeFor(rec, facts) {
|
|
198
|
+
const cmd = pickRecipe(rec, facts);
|
|
199
|
+
if (!cmd) return "рецепт не описан";
|
|
200
|
+
return String(cmd)
|
|
201
|
+
.replace(/\{gate\}/g, join(GATES_SRC, rec.slug))
|
|
202
|
+
.replace(/\{dir\}/g, ".");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const STOP = new Set([
|
|
206
|
+
"и","в","на","не","что","с","по","для","как","из","от","до","за","при","или","а","но","же",
|
|
207
|
+
"это","то","так","бы","бы","ли","у","о","об","под","над","без","есть","быть","был","была",
|
|
208
|
+
"если","чтобы","только","уже","ещё","еще","мы","он","она","они","их","его","её","ее","все",
|
|
209
|
+
"всё","код","кода","коде","проект","проекта","должен","должна","должно","надо","нужно",
|
|
210
|
+
// Латинские связки из имён записей: no-print-IN-prod, secrets-NOT-IN-code. Без них
|
|
211
|
+
// «dead-code-not-shipped» совпадает с «secrets-not-in-code» по служебным словам.
|
|
212
|
+
"not","in","on","of","to","and","or","the","a","an","is","are","be","with","for","no","has",
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
const stems = (text) =>
|
|
216
|
+
new Set(
|
|
217
|
+
String(text || "")
|
|
218
|
+
.toLowerCase()
|
|
219
|
+
.replace(/[^a-zа-яё0-9\s-]/gi, " ")
|
|
220
|
+
.split(/\s+/)
|
|
221
|
+
.filter((w) => w.length >= 3 && !STOP.has(w))
|
|
222
|
+
.map((w) => w.slice(0, 4))
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
function overlap(query, target) {
|
|
226
|
+
if (!query.size) return 0;
|
|
227
|
+
let hit = 0;
|
|
228
|
+
for (const q of query) if (target.has(q)) hit++;
|
|
229
|
+
return hit / query.size;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// Сверка запроса с каталогом ПО НАМЕРЕНИЮ. Одна логика на три места: `find` показывает
|
|
234
|
+
// человеку, `new` не даёт завести дубль, `why` выясняет, был ли вообще такой сторож. Пока это
|
|
235
|
+
// были две копии с разными порогами, «нашлось» в одной команде означало «не нашлось» в другой.
|
|
236
|
+
//
|
|
237
|
+
// Заголовок README считается наравне с intent: это такое же формулирование намерения, написанное
|
|
238
|
+
// человеком. Остальной текст пояснения весит впятеро меньше — слова «гейт», «проверка»,
|
|
239
|
+
// «образец» есть в каждой записи, и по ним совпало бы что угодно с чем угодно.
|
|
240
|
+
async function matchCatalog(query) {
|
|
241
|
+
const q = stems(query);
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const rec of await readCatalog()) {
|
|
244
|
+
const readme = join(GATES_SRC, rec.slug, "README.md");
|
|
245
|
+
const text = (await exists(readme)) ? await readFile(readme, "utf8") : "";
|
|
246
|
+
const title = (text.match(/^#\s+(.+)$/m) || [, ""])[1];
|
|
247
|
+
const head = stems(`${rec.slug.replace(/-/g, " ")} ${rec.intent || ""} ${title}`);
|
|
248
|
+
const body = stems(text.slice(0, 1200));
|
|
249
|
+
|
|
250
|
+
// Одно совпавшее слово — совпадение обрезки, а не смысла: «обратимы» и «образец» дают
|
|
251
|
+
// одно и то же начало. Считаем ещё и сколько слов совпало, и требуем минимум два.
|
|
252
|
+
let hits = 0;
|
|
253
|
+
for (const w of q) if (head.has(w)) hits++;
|
|
254
|
+
// Правило «минимум два совпавших слова» защищает от совпадения обрезки. Для уверенного
|
|
255
|
+
// вывода оно нужно; для подсказки «посмотри вот на эти» — нет: там человек решает сам,
|
|
256
|
+
// и одно совпавшее слово лучше, чем «ничего не найдено» при существующей записи.
|
|
257
|
+
const ok = hits >= 2 || q.size < 2;
|
|
258
|
+
const raw = 0.8 * overlap(q, head) + 0.2 * overlap(q, body);
|
|
259
|
+
out.push({ rec, hits, rawScore: raw, headScore: ok ? overlap(q, head) : 0, score: ok ? raw : 0 });
|
|
260
|
+
}
|
|
261
|
+
out.sort((a, b) => b.score - a.score);
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Наружу — то, что действительно импортируют другие файлы и модульные проверки. Экспорт,
|
|
266
|
+
// который никто не берёт, читается как часть договора и мешает менять внутренности.
|
|
267
|
+
export {
|
|
268
|
+
EXT_LANG, detectFacts, readCatalog, triggerVerdict, pickRecipe, recipeFor,
|
|
269
|
+
stems, overlap, matchCatalog,
|
|
270
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// tool/lib/templates.mjs — тексты, которые программа кладёт в чужой проект.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНО. Это не код, а содержимое: правят его чаще и по другим причинам, чем логику.
|
|
4
|
+
// Лёжа вперемешку с логикой, они мешали читать и то, и другое.
|
|
5
|
+
|
|
6
|
+
import { SELF } from "./core.mjs";
|
|
7
|
+
|
|
8
|
+
// ОДИН источник правил, НЕСКОЛЬКО входов. Каждый инструмент читает свой файл, но оба ведут в
|
|
9
|
+
// .aqk/. Два расходящихся свода правил — худшее, что можно сделать: через месяц они врут
|
|
10
|
+
// по-разному, и никто не знает, какой настоящий.
|
|
11
|
+
|
|
12
|
+
const AGENTS_MD = `# AGENTS.md
|
|
13
|
+
|
|
14
|
+
> Точка входа для агента. Держи файл коротким: раздутый свод правил вытесняет саму задачу из
|
|
15
|
+
> контекста, и тогда игнорируются все правила разом. Всё длинное — по ссылкам ниже.
|
|
16
|
+
|
|
17
|
+
## Железные правила
|
|
18
|
+
|
|
19
|
+
- **План до кода.** Нетривиальная задача начинается с плана, который человек одобрил словами.
|
|
20
|
+
- **Красный тест до кода.** Сначала проверка, которая падает, потом реализация.
|
|
21
|
+
- **Максимум 3 попытки.** Не решил за три — стоп и человеку, а не четвёртый заход.
|
|
22
|
+
- **Секреты только в окружении.** Никогда в коде, логах и коммитах.
|
|
23
|
+
- **Только файлы из задачи.** Заодно ничего не чиним.
|
|
24
|
+
- **Готово = доказано.** Назови арбитра: тест, живой прогон, сверка с источником. «Выглядит
|
|
25
|
+
рабочим» — не готово.
|
|
26
|
+
- **Ошибку не глотать.** Либо обработана и залогирована, либо проброшена.
|
|
27
|
+
- **Развилка — вопрос человеку.** Отступление от принятого решения не оформляется комментарием
|
|
28
|
+
в коде.
|
|
29
|
+
|
|
30
|
+
## Оснастка AQK — это твои команды, а не человека
|
|
31
|
+
|
|
32
|
+
В репозитории стоит комплект AQK. Его смысл: **обещание проекта превращается в команду с кодом
|
|
33
|
+
возврата**, и дальше его держит машина, а не чья-то внимательность. «Не оставляем отладочную
|
|
34
|
+
печать» — текст, который можно проигнорировать; команда, возвращающая не ноль, — нельзя.
|
|
35
|
+
|
|
36
|
+
Запускать их — твоя работа. Человек смотрит на список дыр и решает, какие закрывать.
|
|
37
|
+
|
|
38
|
+
| Команда | Что делает | Когда звать |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| \`aqk doctor\` | смотрит репозиторий и печатает три списка: что уже держит машина, чего не хватает, что этому проекту не нужно и почему | начало работы над качеством; «что тут вообще есть» |
|
|
41
|
+
| \`aqk doctor --run\` | **запускает** объявленные проверки и показывает, кто нашёл брак | перед сдачей; после правок; всегда, когда нужен факт, а не обещание |
|
|
42
|
+
| \`aqk add <имя>\` | ставит проверку из каталога: копирует её и образцы в проект, объявляет в манифесте | человек согласился закрыть дыру из списка \`doctor\` |
|
|
43
|
+
| \`aqk ratchet <имя>\` | старые нарушения записывает долгом, новые перестаёт пускать | проверка краснеет на старом коде, и чинить его сейчас никто не будет |
|
|
44
|
+
| \`aqk find "…"\` | ищет по смыслу, есть ли уже такая проверка | прежде чем изобретать свою |
|
|
45
|
+
| \`aqk note "…"\` | пишет урок в общий журнал | процесс или прибор подвели: проверка соврала, правило обошли |
|
|
46
|
+
|
|
47
|
+
Если команды \`aqk\` нет в системе — комплект ставили разово, без установки. Тогда вместо
|
|
48
|
+
\`aqk\` пиши \`npx github:arsen-ask-lx/Agent_Quality_Kit\`. Любая команда сама печатает тот
|
|
49
|
+
вызов, который сработает у тебя.
|
|
50
|
+
|
|
51
|
+
**Три вещи, которые надо понимать, а не запоминать:**
|
|
52
|
+
|
|
53
|
+
1. **«Объявлен» и «работает» — разные утверждения.** \`doctor\` без \`--run\` честно говорит, что
|
|
54
|
+
проверки не запускал. Не выдавай объявленное за работающее.
|
|
55
|
+
2. **Проверка без двух образцов ничего не доказывает.** Красный — код, на котором она обязана
|
|
56
|
+
сработать; зелёный — правильный, на котором обязана молчать. Зелёный важнее: без него однажды
|
|
57
|
+
она покраснеет на верном коде, и её выключат вместе с остальными.
|
|
58
|
+
3. **Правило вводится храповиком, а не большой чисткой.** Чистка откладывается навсегда, потому
|
|
59
|
+
что она большая. Храповик даёт действующее правило со дня установки.
|
|
60
|
+
|
|
61
|
+
**Повторился дефект того же класса — это не повод быть внимательнее, а повод завести проверку.**
|
|
62
|
+
Дисциплина не масштабируется, механика — да.
|
|
63
|
+
|
|
64
|
+
## Где что лежит
|
|
65
|
+
|
|
66
|
+
- \`.aqk/rules/\` — стандарты: общие, тесты, безопасность
|
|
67
|
+
- \`.aqk/docs/\` — методички: минимум проекта, харнес, процесс, исследования
|
|
68
|
+
- \`.aqk/docs/project-baseline.md\` — **начни отсюда**, если проект новый
|
|
69
|
+
|
|
70
|
+
## Команды
|
|
71
|
+
|
|
72
|
+
<!-- Заполни под свой проект. Команда, которую нельзя скопировать и выполнить, — не команда. -->
|
|
73
|
+
|
|
74
|
+
- сборка: \`\`
|
|
75
|
+
- тесты: \`\`
|
|
76
|
+
- линтер: \`\`
|
|
77
|
+
- всё разом перед пушем: \`\`
|
|
78
|
+
|
|
79
|
+
## Чего в этом проекте нет
|
|
80
|
+
|
|
81
|
+
<!-- Пиши сюда честно. Ненаписанное «нет» агент додумает как «есть». -->
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const CLAUDE_MD = `# CLAUDE.md
|
|
85
|
+
|
|
86
|
+
Правила этого проекта живут в \`AGENTS.md\` — читай его.
|
|
87
|
+
|
|
88
|
+
Один свод правил, несколько точек входа: \`AGENTS.md\` для агентов, понимающих его,
|
|
89
|
+
\`CLAUDE.md\` — для Claude Code. Держать два расходящихся свода нельзя: через месяц они врут
|
|
90
|
+
по-разному, и непонятно, какой настоящий.
|
|
91
|
+
|
|
92
|
+
@AGENTS.md
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
// Манифест — единственный машиночитаемый файл стандарта. Всё остальное человекочитаемо.
|
|
96
|
+
// Пустые значения оставлены НАМЕРЕННО: заполненная заглушка врала бы про уровень.
|
|
97
|
+
const MANIFEST_YML = `# .aqk.yml — манифест Agent Quality Kit
|
|
98
|
+
# Что это: машиночитаемое описание того, как в этом репозитории живут агенты.
|
|
99
|
+
# Уровень соответствия считает \`aqk doctor\`. Пустое поле = ступень не пройдена,
|
|
100
|
+
# и это честно: заполнять заглушками бессмысленно, проверяются файлы, а не слова.
|
|
101
|
+
|
|
102
|
+
aqk: 1
|
|
103
|
+
|
|
104
|
+
# AQK-0 — что агент читает первым.
|
|
105
|
+
entry:
|
|
106
|
+
- AGENTS.md
|
|
107
|
+
|
|
108
|
+
# AQK-1 — где стандарты и какие проверки обязательны.
|
|
109
|
+
rules: .aqk/rules
|
|
110
|
+
gates:
|
|
111
|
+
# Имя: команда, возвращающая 0 или не 0. Пустое объявление защиты не даёт и бракуется
|
|
112
|
+
# проверкой «объявленный гейт запускается» — поэтому здесь примеры, а не заготовки.
|
|
113
|
+
# lint: "ruff check ."
|
|
114
|
+
# test: "pytest -q"
|
|
115
|
+
# Поставить готовую запись из каталога вместе с образцами: aqk add <имя>
|
|
116
|
+
|
|
117
|
+
# AQK-2 — чем доказано, что гейты работают, и где реестры долга.
|
|
118
|
+
# samples: каталог с красными и зелёными образцами (гейт обязан краснеть на первом
|
|
119
|
+
# и молчать на втором). ratchets: списки известных нарушений, которые могут только
|
|
120
|
+
# укорачиваться.
|
|
121
|
+
samples: ""
|
|
122
|
+
ratchets: ""
|
|
123
|
+
|
|
124
|
+
# AQK-3 — где копятся уроки. Путь или адрес.
|
|
125
|
+
lessons: ""
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const GATE_YML_TEMPLATE = (slug) => `# Запись каталога AQK. Норма и все поля — kit/gates/README.md
|
|
129
|
+
# Пока строки ниже не заполнены, проверка отклонит эту запись — так и задумано.
|
|
130
|
+
|
|
131
|
+
# Одной фразой: какой класс брака ловит. По этому полю идёт сверка «есть ли уже такое».
|
|
132
|
+
intent: ЗАПОЛНИ — какой класс брака ловит
|
|
133
|
+
|
|
134
|
+
# Когда запись показывается человеку. Условие обязано быть запросом к репозиторию,
|
|
135
|
+
# который умеет вычислить программа: always | langs: python, go | has_gates: true
|
|
136
|
+
trigger:
|
|
137
|
+
always: true
|
|
138
|
+
|
|
139
|
+
# Команда-арбитр под каждый стек. {gate} — папка записи, {dir} — что проверяем.
|
|
140
|
+
# any — переносимая команда без сторонних программ.
|
|
141
|
+
recipes:
|
|
142
|
+
any: bash {gate}/check.sh {dir}
|
|
143
|
+
|
|
144
|
+
# Реальный отказ, который эта проверка поймала. «Хорошая практика» не принимается:
|
|
145
|
+
# ссылайся на запись журнала — incidents/README.md
|
|
146
|
+
proof: ЗАПОЛНИ — какой отказ поймала и чего он стоил
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
const CHECK_SH_TEMPLATE = `#!/usr/bin/env sh
|
|
150
|
+
# Проверка. Возвращает 0 — чисто, не 0 — брак. В тексте отказа должно быть НАПИСАНО,
|
|
151
|
+
# что сделать: он попадает прямо в контекст агента, и с инструкцией он чинит сам.
|
|
152
|
+
DIR="\${1:-.}"
|
|
153
|
+
. "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
|
|
154
|
+
|
|
155
|
+
# own_samples_filter прячет ТОЛЬКО gates/<имя>/red|green/ — не любую папку с таким именем
|
|
156
|
+
# в проекте. --exclude-dir=red по голому имени однажды спрятал бы настоящую пользовательскую
|
|
157
|
+
# папку red/ (нашли на secrets-not-in-code — kit/docs/ai/... журнал, 2026-09-04).
|
|
158
|
+
HITS=$(grep -rnE $(skip_grep "$DIR") 'ЗАПОЛНИ_ШАБЛОН_ПОИСКА' "$DIR" 2>/dev/null | own_samples_filter "$DIR")
|
|
159
|
+
if [ -n "$HITS" ]; then
|
|
160
|
+
echo "$HITS"
|
|
161
|
+
echo " почини: ЗАПОЛНИ — что именно сделать"
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
exit 0
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const README_TEMPLATE = (slug) => `# ЗАПОЛНИ — заголовок одной строкой
|
|
168
|
+
|
|
169
|
+
**Намерение.** Что и почему не должно попадать в код.
|
|
170
|
+
|
|
171
|
+
**Какой отказ это поймало.** Что сломалось, в каком проекте, чего это стоило. Без этого
|
|
172
|
+
запись не принимается: «это хорошая практика» набирает сотни пунктов, среди которых нельзя
|
|
173
|
+
выбрать.
|
|
174
|
+
|
|
175
|
+
**Почему машина, а не внимательность.** В какой момент человек это пропускает.
|
|
176
|
+
|
|
177
|
+
**Готовый аналог.** Есть ли такая проверка в ruff, eslint, semgrep? Если есть — рецепт под этот
|
|
178
|
+
язык обязан брать её, а своя проверка остаётся запасной. Если нет — написать, что именно
|
|
179
|
+
проверено: «не искал» и «нет» — разные утверждения.
|
|
180
|
+
|
|
181
|
+
**Чего НЕ ловит.** Честно назвать границу. Гейт, о пределах которого умалчивают, опаснее
|
|
182
|
+
отсутствующего: на него понадеются.
|
|
183
|
+
|
|
184
|
+
**Образцы.** \`red/\` — что здесь нарушено. \`green/\` — то же самое, но правильно.
|
|
185
|
+
`;
|
|
186
|
+
|
|
187
|
+
export { AGENTS_MD, CLAUDE_MD, MANIFEST_YML, GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE };
|
package/tool/program.mjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// aqk — разложить оснастку в проект и вести общий журнал шишек.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ ЭТО, А НЕ ПЛАГИН. Плагин Claude Code работает только в Claude Code. Правила, документация
|
|
5
|
+
// и гейты не зависят от того, какой нейросетью пишут код, — значит и способ установки не должен
|
|
6
|
+
// зависеть. `npx` есть везде, где есть Node.
|
|
7
|
+
//
|
|
8
|
+
// ЗАВИСИМОСТЕЙ НЕТ НАМЕРЕННО. Каждая чужая библиотека — лишний узел надёжности и лишняя дверь в
|
|
9
|
+
// цепочке поставок. Инструмент, который ставят одной командой в чужой проект, обязан быть
|
|
10
|
+
// проверяемым глазами за один присест.
|
|
11
|
+
//
|
|
12
|
+
// npx github:arsen-ask-lx/Agent_Quality_Kit init разложить комплект в текущий проект
|
|
13
|
+
// npx github:arsen-ask-lx/Agent_Quality_Kit init --force перезаписать уже существующие файлы
|
|
14
|
+
// npx github:arsen-ask-lx/Agent_Quality_Kit note "..." записать урок в общий журнал
|
|
15
|
+
// npx github:arsen-ask-lx/Agent_Quality_Kit doctor проверить, что разложено и чего не хватает
|
|
16
|
+
|
|
17
|
+
import { realpathSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { c, SELF } from "./lib/core.mjs";
|
|
20
|
+
import { cmdInit, cmdNote, cmdBlob, cmdStart } from "./commands/project.mjs";
|
|
21
|
+
import { cmdDoctor } from "./commands/doctor.mjs";
|
|
22
|
+
import { cmdAdd, cmdNew, cmdRatchet, cmdFind, cmdWhy } from "./commands/gates.mjs";
|
|
23
|
+
|
|
24
|
+
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
25
|
+
// а так его читают модульные проверки tool/selfcheck/units.mjs — CLI запускаться не должен.
|
|
26
|
+
// Сравниваем по реальному пути: npx ставит `aqk` симлинком, и без realpath запуск через него
|
|
27
|
+
// программой считаться перестал бы.
|
|
28
|
+
let IS_MAIN = false;
|
|
29
|
+
try {
|
|
30
|
+
IS_MAIN = !!process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
31
|
+
} catch { IS_MAIN = false; }
|
|
32
|
+
|
|
33
|
+
if (IS_MAIN) {
|
|
34
|
+
const [, , cmd, ...rest] = process.argv;
|
|
35
|
+
switch (cmd) {
|
|
36
|
+
case "init":
|
|
37
|
+
await cmdInit(rest);
|
|
38
|
+
break;
|
|
39
|
+
case "start":
|
|
40
|
+
await cmdStart(rest);
|
|
41
|
+
break;
|
|
42
|
+
case "doctor":
|
|
43
|
+
await cmdDoctor();
|
|
44
|
+
break;
|
|
45
|
+
case "note":
|
|
46
|
+
await cmdNote(rest);
|
|
47
|
+
break;
|
|
48
|
+
case "add":
|
|
49
|
+
await cmdAdd(rest);
|
|
50
|
+
break;
|
|
51
|
+
case "find":
|
|
52
|
+
await cmdFind(rest);
|
|
53
|
+
break;
|
|
54
|
+
case "why":
|
|
55
|
+
await cmdWhy(rest);
|
|
56
|
+
break;
|
|
57
|
+
case "ratchet":
|
|
58
|
+
await cmdRatchet(rest);
|
|
59
|
+
break;
|
|
60
|
+
case "new":
|
|
61
|
+
await cmdNew(rest);
|
|
62
|
+
break;
|
|
63
|
+
case "blob":
|
|
64
|
+
await cmdBlob();
|
|
65
|
+
break;
|
|
66
|
+
default:
|
|
67
|
+
console.log(`
|
|
68
|
+
${c.bold("aqk")} — оснастка для разработки с агентами
|
|
69
|
+
|
|
70
|
+
${c.bold(`${SELF} init`)} разложить правила и методички в текущий проект
|
|
71
|
+
${c.bold(`${SELF} init --force`)} перезаписать уже существующие файлы
|
|
72
|
+
${c.bold(`${SELF} start`)} кода ещё нет: сторожа дня 0 и порядок работы
|
|
73
|
+
${c.bold(`${SELF} doctor`)} проверить, что разложено и чего не хватает\n ${c.bold(`${SELF} doctor --run`)} ещё и запустить объявленные гейты\n ${c.bold(`${SELF} add`)} <имя> поставить гейт из каталога в проект\n ${c.bold(`${SELF} find`)} "…" есть ли уже такой гейт — сверка по намерению\n ${c.bold(`${SELF} why`)} "…" поймал ошибку — почему её не поймал сторож\n ${c.bold(`${SELF} ratchet`)} <имя> храповик: старые нарушения — долг, новые не пускать\n ${c.bold(`${SELF} new`)} <имя> заготовка своего гейта для каталога
|
|
74
|
+
${c.bold(`${SELF} note`)} "…" записать урок в общий журнал шишек
|
|
75
|
+
${c.bold(`${SELF} blob`)} собрать методички в один файл GOD_AI.md
|
|
76
|
+
|
|
77
|
+
${c.dim("Без установки: npx github:arsen-ask-lx/Agent_Quality_Kit init")}
|
|
78
|
+
`);
|
|
79
|
+
process.exit(cmd ? 1 : 0);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Записи каталога, чьё доказательство не ссылается на журнал шишек.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ОТДЕЛЬНОЙ ПРОВЕРКОЙ. «Это хорошая практика» — самый частый способ, которым каталог
|
|
5
|
+
# набирает пункты, среди которых нельзя выбрать. Условная запись допустима как долг, но их
|
|
6
|
+
# число не должно расти: список под храповиком может только укорачиваться.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
9
|
+
|
|
10
|
+
for GATE in "$ROOT"/kit/gates/*/; do
|
|
11
|
+
YML="$GATE/gate.yml"
|
|
12
|
+
[ -f "$YML" ] || continue
|
|
13
|
+
PROOF="$(sed -n 's/^proof:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1)"
|
|
14
|
+
case "$PROOF" in
|
|
15
|
+
*incidents/*) ;;
|
|
16
|
+
*)
|
|
17
|
+
# Ключом служит только имя: правка формулировки доказательства не должна читаться
|
|
18
|
+
# как новое нарушение. Сам текст — отступом, такие строки в ключ не идут.
|
|
19
|
+
echo "$(basename "$GATE"): доказательство не ссылается на журнал шишек"
|
|
20
|
+
echo " сейчас: $PROOF"
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
done
|
|
24
|
+
exit 0
|