agent-quality-kit 0.10.1 → 0.12.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 +10 -3
- package/README.ru.md +10 -3
- package/kit/gates/_native.sh +4 -0
- package/kit/gates/_skip.sh +7 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +7 -0
- package/kit/gates/color-from-token/check.sh +7 -0
- package/kit/gates/commit-explains-itself/check.sh +8 -2
- package/kit/gates/complexity-limit/check.sh +22 -4
- package/kit/gates/complexity-limit/gate.yml +7 -2
- package/kit/gates/complexity-limit/red/deep.js +15 -0
- package/kit/gates/dead-code/gate.yml +5 -0
- package/kit/gates/duplicate-code/check.sh +7 -0
- package/kit/gates/duplicate-code/gate.yml +10 -1
- package/kit/gates/file-size-limit/check.sh +7 -0
- package/kit/gates/file-size-limit/red/big.js +600 -0
- package/kit/gates/gate-not-weakened/check.sh +7 -0
- package/kit/gates/gate-not-weakened/green/suppress.js +4 -0
- package/kit/gates/gate-not-weakened/red/suppress.js +5 -0
- package/kit/gates/mcp-server-resolves/check.sh +7 -0
- package/kit/gates/no-phantom-package/check.sh +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +8 -3
- package/kit/gates/personal-config-not-shared/check.sh +7 -0
- package/kit/gates/secrets-not-in-code/check.sh +7 -0
- package/kit/gates/secrets-not-in-code/gate.yml +20 -0
- package/kit/gates/secrets-not-in-code/green/config.js +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.js +6 -0
- package/kit/gates/swallowed-error/gate.yml +8 -3
- package/kit/gates/test-has-assertion/check.sh +7 -0
- package/kit/gates/test-has-assertion/green/checkout.test.js +5 -0
- package/kit/gates/test-has-assertion/red/checkout.test.js +8 -0
- package/kit/gates/test-not-adjusted/check.sh +8 -2
- package/kit/gates/todo-without-task/check.sh +7 -0
- package/kit/gates/todo-without-task/gate.yml +7 -2
- package/kit/gates/todo-without-task/green/app.js +2 -0
- package/kit/gates/todo-without-task/red/later.js +4 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +3 -1
- package/tool/commands/doctor.mjs +94 -152
- package/tool/commands/gates.mjs +3 -2
- package/tool/commands/probe.mjs +284 -44
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +6 -1
- package/tool/i18n/en-docs.mjs +2 -1
- package/tool/i18n/en-gates.mjs +23 -5
- package/tool/i18n/en.mjs +16 -0
- package/tool/i18n/ru-docs.mjs +2 -1
- package/tool/i18n/ru-gates.mjs +24 -6
- package/tool/i18n/ru.mjs +16 -0
- package/tool/lib/adopt.mjs +98 -0
- package/tool/lib/cadence.mjs +36 -1
- package/tool/lib/execution.mjs +50 -1
- package/tool/lib/history.mjs +89 -8
- package/tool/lib/prove.mjs +2 -2
- package/tool/lib/repo.mjs +47 -0
- package/tool/lib/run.mjs +194 -0
- package/tool/selfcheck/gates.sh +42 -4
- package/tool/selfcheck/smoke/_fixture.mjs +24 -2
- package/tool/selfcheck/smoke/fail-closed.test.mjs +112 -0
- package/tool/selfcheck/smoke/own-samples.test.mjs +131 -0
- package/tool/selfcheck/units-cadence.mjs +37 -1
- package/tool/selfcheck/units-execution.mjs +78 -1
- package/tool/selfcheck/units-level.mjs +24 -0
- package/tool/selfcheck/units-probe.mjs +385 -22
- package/tool/selfcheck/units-repo.mjs +123 -1
- package/tool/selfcheck/units.mjs +34 -1
package/tool/commands/probe.mjs
CHANGED
|
@@ -21,16 +21,17 @@
|
|
|
21
21
|
// удаляется. Не меняет манифест. Не роняет прогон: код возврата всегда 0 — это осмотр, а
|
|
22
22
|
// не порог. Порог — у `doctor --run --min`.
|
|
23
23
|
import { spawnSync } from "node:child_process";
|
|
24
|
-
import { mkdtemp, mkdir, copyFile, rm, readdir, writeFile, readFile } from "node:fs/promises";
|
|
24
|
+
import { mkdtemp, mkdir, copyFile, rm, readdir, writeFile, readFile, symlink } from "node:fs/promises";
|
|
25
|
+
import { readFileSync } from "node:fs";
|
|
25
26
|
import { tmpdir } from "node:os";
|
|
26
27
|
import { join, dirname, extname } from "node:path";
|
|
27
28
|
import { readManifest } from "../lib/manifest.mjs";
|
|
28
|
-
import {
|
|
29
|
-
import { fixHotspots, probeVerdict } from "../lib/history.mjs";
|
|
29
|
+
import { fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
|
|
30
30
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
31
31
|
import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
|
|
32
|
-
import { probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
|
|
32
|
+
import { probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
|
|
33
33
|
import { L } from "../i18n/index.mjs";
|
|
34
|
+
import { gateCommand } from "../lib/execution.mjs";
|
|
34
35
|
|
|
35
36
|
// Тот же набор расширений, что у привязки доказательства к дифу. Список один на программу:
|
|
36
37
|
// второй через месяц разошёлся бы с первым.
|
|
@@ -99,14 +100,107 @@ async function writeMark(now, blind, lines) {
|
|
|
99
100
|
await writeFile(MARK(), body + "\n", "utf8");
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
103
|
+
|
|
104
|
+
// Отчего проба не состоялась. Раньше здесь было три состояния: гейты, чья команда не кончается
|
|
105
|
+
// каталогом, объявлялись непригодными — подставить образец было некуда. С песочницей подставлять
|
|
106
|
+
// в команду больше не нужно: образец кладётся в КОПИЮ ПРОЕКТА, а гейт запускается в ней как есть.
|
|
107
|
+
// Поэтому пригодна любая непустая команда, и состояний осталось два.
|
|
108
|
+
function gatesState(man) {
|
|
109
|
+
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
110
|
+
const declared = Object.entries(gates)
|
|
111
|
+
.map(([name, raw]) => [name, String(raw || "").trim()])
|
|
112
|
+
.filter(([, cmd]) => cmd);
|
|
113
|
+
if (!declared.length) return { state: "none", declared: 0, probeable: 0 };
|
|
114
|
+
return { state: "ok", declared: declared.length, probeable: declared.length };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Гейты, пригодные для пробы: все объявленные с непустой командой.
|
|
118
|
+
function probeableGates(man) {
|
|
106
119
|
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
107
120
|
return Object.entries(gates)
|
|
108
121
|
.map(([name, raw]) => [name, String(raw || "").trim()])
|
|
109
|
-
.filter(([, cmd]) => cmd
|
|
122
|
+
.filter(([, cmd]) => cmd);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Семьи расширений. Образец подбирается по ТОЧНОМУ расширению, и правило верное: питоновский
|
|
126
|
+
// образец в проекте на TypeScript не проверит ничего, а покажет «не прикрыто» — ложная тревога
|
|
127
|
+
// того же класса, что молчащий гейт, только наоборот.
|
|
128
|
+
//
|
|
129
|
+
// Но `.js` и `.mjs` — одно и то же содержимое, а не два языка. Прогон на самом комплекте
|
|
130
|
+
// 2026-09-10: два горячих файла из пяти — `.mjs`, и обоим ответили «нет образца под .mjs»;
|
|
131
|
+
// комплект целиком написан в этом расширении, то есть проба была слепа к собственному коду.
|
|
132
|
+
// Заводить второй набор файлов ради той же строчки — дублирование, которое разойдётся.
|
|
133
|
+
//
|
|
134
|
+
// Семьи узкие намеренно: `.jsx`/`.tsx` сюда не входят, у них своя разметка.
|
|
135
|
+
const EXT_FAMILIES = [[".js", ".mjs", ".cjs"], [".ts", ".mts", ".cts"]];
|
|
136
|
+
|
|
137
|
+
function extAlternatives(ext) {
|
|
138
|
+
const fam = EXT_FAMILIES.find((f) => f.includes(ext));
|
|
139
|
+
return fam ? [ext, ...fam.filter((e) => e !== ext)] : [ext];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Показать САМ ОБРАЗЕЦ, а не пересказ. «Класс не прикрыт» остаётся словами, пока человек не
|
|
143
|
+
// увидел, что именно мы подсадили в его файл.
|
|
144
|
+
//
|
|
145
|
+
// Первая версия печатала одну «показательную» строку — и угадывала плохо: у мёртвого кода дефект
|
|
146
|
+
// во ВТОРОЙ функции, у отладочной печати во второй строке тела. Угадывать не надо: образцы
|
|
147
|
+
// каталога маленькие по норме, и четырёх строк хватает, чтобы стало видно. Комментарии
|
|
148
|
+
// выброшены: в наших образцах они объясняют замысел коллеге, а не показывают дефект.
|
|
149
|
+
function sampleLines(path, max = 4) {
|
|
150
|
+
let text = "";
|
|
151
|
+
try { text = readFileSync(path, "utf8"); } catch { return []; }
|
|
152
|
+
return text.split("\n")
|
|
153
|
+
.map((l) => l.replace(/\s+$/, ""))
|
|
154
|
+
.filter((l) => l.trim() && !/^\s*(#|\/\/|\/\*|\*|--|<!--)/.test(l))
|
|
155
|
+
.slice(0, max)
|
|
156
|
+
.map((l) => l.slice(0, 88));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Совет по НЕПОКРЫТОМУ классу: команда, которую можно вставить прямо сейчас.
|
|
160
|
+
//
|
|
161
|
+
// Проба находит настоящие дыры и печатала про них «close it: aqk add <имя>» — то есть «поставь
|
|
162
|
+
// нашу штуку». Человек, впервые увидевший комплект, закрывает окно. А готовая однострочная
|
|
163
|
+
// команда под его стек У НАС УЖЕ ЛЕЖИТ в `recipes` записи каталога; мы её не показывали.
|
|
164
|
+
//
|
|
165
|
+
// Замер руками на `requests` (самый скачиваемый python-пакет) 2026-09-10: в
|
|
166
|
+
// `src/requests/utils.py` — 75 коммитов-починок; дописана функция с `except Exception: pass`;
|
|
167
|
+
// их собственные `ruff` и `pytest` дали 0 и на чистой копии, и на подсаженной. Строка, которая
|
|
168
|
+
// поймала бы это, лежала в нашем каталоге всё это время.
|
|
169
|
+
//
|
|
170
|
+
// Переносимый рецепт (`any`) в совет НЕ идёт: он зовёт файл из комплекта, и человеку без
|
|
171
|
+
// комплекта вставить его некуда. Нет родного рецепта под стек — команды нет, и это честнее
|
|
172
|
+
// выдуманной.
|
|
173
|
+
function blindAdvice(entry, facts, hot = {}) {
|
|
174
|
+
const recipes = entry?.recipes && typeof entry.recipes === "object" ? entry.recipes : {};
|
|
175
|
+
// `langs` приходит МНОЖЕСТВОМ, а не массивом — `Array.isArray` тихо давал пустой список, и
|
|
176
|
+
// совет не печатался вовсе. Поймано на живом `requests`: langs = Set(1) { python }.
|
|
177
|
+
const langs = facts?.langs ? [...facts.langs] : [];
|
|
178
|
+
// Тот же порядок, что у `pickRecipe`: свой язык → безъязыковой родной → ничего. Переносимый
|
|
179
|
+
// (`any`) сюда не идёт никогда: он зовёт файл из комплекта, и человеку без комплекта вставить
|
|
180
|
+
// его некуда.
|
|
181
|
+
let cmd = null;
|
|
182
|
+
for (const key of [...langs, "native"]) {
|
|
183
|
+
const r = recipes[key];
|
|
184
|
+
if (!r || /\{gate\}/.test(r)) continue;
|
|
185
|
+
cmd = String(r).replace(/\{dir\}/g, ".")
|
|
186
|
+
// Вычистить то, что относится к НАМ, а не к его проекту. Исключение наших красных
|
|
187
|
+
// образцов нужно УСТАНОВЛЕННОМУ гейту — рядом с ним лежат образцы. Человеку, который
|
|
188
|
+
// команду только копирует, этих каталогов не существует, и флаги про них подрывают
|
|
189
|
+
// доверие: инструмент говорит про чужое хозяйство вместо его кода.
|
|
190
|
+
.replace(/\s--ignore-pattern\s+'[^']*gates\/[^']*'/g, "")
|
|
191
|
+
.replace(/\s--ignore-paths=?\s*'[^']*gates\/[^']*'/g, "")
|
|
192
|
+
.replace(/\s+/g, " ")
|
|
193
|
+
.trim();
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
// Адрес — того инструмента, которым команда начинается: поле `tool` общее на все языки, и
|
|
197
|
+
// python-проекту показывалась ссылка на eslint. Первые три слова, а не одно: `npx knip`,
|
|
198
|
+
// `python -m vulture`. Не совпало — весь список: лишняя ссылка лучше, чем ни одной.
|
|
199
|
+
const urls = entry?.tool ? String(entry.tool).split(/\s+·\s+/) : [];
|
|
200
|
+
const head = cmd ? cmd.split(" ").slice(0, 3) : [];
|
|
201
|
+
const own = urls.find((u) => head.includes(u.replace(/\/+$/, "").split("/").pop()));
|
|
202
|
+
const tool = own ?? (entry?.tool ? String(entry.tool) : null);
|
|
203
|
+
return { command: cmd, tool, file: hot.file ?? null, fixes: hot.fixes ?? null, slug: entry?.slug ?? null };
|
|
110
204
|
}
|
|
111
205
|
|
|
112
206
|
// Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
|
|
@@ -117,39 +211,114 @@ async function redSampleFor(entry, ext) {
|
|
|
117
211
|
if (!(await exists(dir))) return null;
|
|
118
212
|
let names = [];
|
|
119
213
|
try { names = await readdir(dir); } catch { return null; }
|
|
120
|
-
const
|
|
121
|
-
|
|
214
|
+
for (const want of extAlternatives(ext)) {
|
|
215
|
+
const hit = names.find((n) => extname(n).toLowerCase() === want);
|
|
216
|
+
if (hit) return join(dir, hit);
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
122
219
|
}
|
|
123
220
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
221
|
+
// Песочница: КОПИЯ ПРОЕКТА, в которую подсаживается образец. Раньше здесь был временный каталог
|
|
222
|
+
// с одним файлом, а путь к нему подставлялся в команду гейта — отчего пробовать можно было
|
|
223
|
+
// только команды, кончающиеся каталогом. Замер 2026-09-10 на семи чужих репозиториях: у шести
|
|
224
|
+
// команды такие (`xo`, `eslint lib/**/*.js`, `mocha --require…`, `pytest`), и проба не
|
|
225
|
+
// запускалась вовсе.
|
|
226
|
+
//
|
|
227
|
+
// Способ взят из мутационного тестирования, где та же задача решена двадцать лет назад: Stryker
|
|
228
|
+
// копирует проект во временный каталог, СИМЛИНКУЕТ `node_modules` и гоняет там родную команду.
|
|
229
|
+
// Копируются только ОТСЛЕЖИВАЕМЫЕ файлы (`git archive HEAD`) — рабочее дерево не трогается, а
|
|
230
|
+
// мусор сборки не тащится; тяжёлые каталоги зависимостей симлинкуются, иначе `npm test` в
|
|
231
|
+
// песочнице падал бы с «модуль не найден», и это читалось бы как сбой инструмента.
|
|
232
|
+
const DEP_DIRS = ["node_modules", ".venv", "venv", "vendor", "target", ".tox", ".bundle"];
|
|
233
|
+
|
|
234
|
+
async function buildSandbox() {
|
|
235
|
+
// Копируется РАБОЧЕЕ ДЕРЕВО, а не HEAD. Первая версия брала `git archive HEAD`, и это было
|
|
236
|
+
// неверно: комплект зовут из хука ДО коммита, и пользователь пробует то, что у него сейчас,
|
|
237
|
+
// а не то, что уже записано. На свежем `init` + `add` без коммита проба вообще ничего не
|
|
238
|
+
// видела — гейты в песочнице отсутствовали и «не запускались».
|
|
239
|
+
//
|
|
240
|
+
// Список — `git ls-files --cached --others --exclude-standard`: отслеживаемые плюс новые, но
|
|
241
|
+
// БЕЗ игнорируемых. Игнорируемое — это сборка и зависимости; первое пробе не нужно, второе
|
|
242
|
+
// приходит симлинком.
|
|
243
|
+
//
|
|
244
|
+
// Копирование средствами node, а не `tar`: у конвейера есть windows-задание, и полагаться на
|
|
245
|
+
// ключи GNU tar там нельзя.
|
|
246
|
+
const r = spawnSync("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
247
|
+
cwd: CWD, encoding: "utf8", timeout: 60000, maxBuffer: 64 * 1024 * 1024,
|
|
248
|
+
});
|
|
249
|
+
if (r.status !== 0) return null;
|
|
250
|
+
const files = String(r.stdout || "").split("\0").filter(Boolean);
|
|
251
|
+
if (!files.length) return null;
|
|
252
|
+
|
|
253
|
+
const root = await mkdtemp(join(tmpdir(), "aqk-sandbox-"));
|
|
254
|
+
const made = new Set();
|
|
255
|
+
for (const rel of files) {
|
|
256
|
+
const dest = join(root, rel);
|
|
257
|
+
const dir = dirname(dest);
|
|
258
|
+
if (!made.has(dir)) { await mkdir(dir, { recursive: true }); made.add(dir); }
|
|
259
|
+
// Файл мог исчезнуть между списком и копией, а каталог — оказаться подмодулем.
|
|
260
|
+
try { await copyFile(join(CWD, rel), dest); } catch { /* пропускаем, не роняя пробу */ }
|
|
261
|
+
}
|
|
262
|
+
for (const dep of DEP_DIRS) {
|
|
263
|
+
const from = join(CWD, dep);
|
|
264
|
+
if (await exists(from)) { try { await symlink(from, join(root, dep), "junction"); } catch { /* уже есть */ } }
|
|
265
|
+
}
|
|
266
|
+
return root;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Подсадка образца на место горячего файла и возврат как было. Файл СНАЧАЛА удаляется:
|
|
270
|
+
// в песочнице он может быть жёсткой ссылкой, и запись поверх задела бы оригинал.
|
|
271
|
+
async function plant(root, relPath, sample) {
|
|
129
272
|
const dest = join(root, relPath);
|
|
130
273
|
await mkdir(dirname(dest), { recursive: true });
|
|
274
|
+
let backup = null;
|
|
275
|
+
try { backup = await readFile(dest); } catch { /* файла может не быть */ }
|
|
276
|
+
await rm(dest, { force: true });
|
|
131
277
|
await copyFile(sample, dest);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// на подсаженный брак не отреагировал никто. Ошибка в сторону «прикрыто» — это тишина,
|
|
137
|
-
// а тишина здесь и есть предмет спора. Поймано первым же прогоном на своём репозитории.
|
|
138
|
-
if (await exists(join(CWD, ".aqkignore"))) {
|
|
139
|
-
await copyFile(join(CWD, ".aqkignore"), join(root, ".aqkignore"));
|
|
140
|
-
}
|
|
141
|
-
return root;
|
|
278
|
+
return async () => {
|
|
279
|
+
await rm(dest, { force: true });
|
|
280
|
+
if (backup !== null) await writeFile(dest, backup);
|
|
281
|
+
};
|
|
142
282
|
}
|
|
143
283
|
|
|
144
|
-
|
|
284
|
+
// Гейт запускается В ПЕСОЧНИЦЕ и командой КАК ЕСТЬ — ничего в неё не подставляется. Именно это
|
|
285
|
+
// и делает пробу независимой от формы команды.
|
|
286
|
+
//
|
|
287
|
+
// `stopOnRed` — ранний выход: как только гейт покраснел, вердикт «поймано» уже получен, и гонять
|
|
288
|
+
// остальные незачем. На сухом прогоне выхода нет: там нужны ВСЕ длительности и все коды.
|
|
289
|
+
function runGates(gates, sandbox, { stopOnRed = false } = {}) {
|
|
145
290
|
const out = [];
|
|
146
291
|
for (const [name, cmd] of gates) {
|
|
147
|
-
const
|
|
148
|
-
|
|
292
|
+
const t0 = Date.now();
|
|
293
|
+
const r = spawnSync(gateCommand(cmd), { shell: true, cwd: sandbox, encoding: "utf8", timeout: 120000 });
|
|
294
|
+
const code = r.status === null ? 2 : r.status;
|
|
295
|
+
out.push({ name, code, ms: Date.now() - t0 });
|
|
296
|
+
if (stopOnRed && code === 1) break;
|
|
149
297
|
}
|
|
150
298
|
return out;
|
|
151
299
|
}
|
|
152
300
|
|
|
301
|
+
// Каким гейтом пробовать и в каком порядке.
|
|
302
|
+
//
|
|
303
|
+
// Цена пробы = (файлы × записи) × сумма длительностей гейтов. На самом комплекте после перехода
|
|
304
|
+
// на песочницу это стало больше десяти минут и упёрлось в таймаут: среди тридцати гейтов есть
|
|
305
|
+
// `smoke` на 58 секунд, и он гонялся заново на каждую подсадку. Команда, идущая четверть часа,
|
|
306
|
+
// не запускается никем.
|
|
307
|
+
//
|
|
308
|
+
// Длительности берутся из сухого прогона, который и так обязателен. Порядок — от быстрых к
|
|
309
|
+
// медленным, чтобы ранний выход срабатывал раньше. Слишком медленные исключаются, но НЕ молча:
|
|
310
|
+
// их имена обязаны попасть в вывод, иначе «никто не ловит» будет означать «никто из тех, кого
|
|
311
|
+
// мы решили спросить».
|
|
312
|
+
const SLOW_MS = 20000;
|
|
313
|
+
|
|
314
|
+
function planProbeGates(before, { slowMs = SLOW_MS } = {}) {
|
|
315
|
+
const usable = before.filter((r) => r.code === 0).sort((a, b) => (a.ms || 0) - (b.ms || 0));
|
|
316
|
+
return {
|
|
317
|
+
use: usable.filter((r) => (r.ms || 0) <= slowMs),
|
|
318
|
+
tooSlow: usable.filter((r) => (r.ms || 0) > slowMs),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
153
322
|
// `auto` — проба запущена САМА, по каденции, из `doctor --run`. Тогда она короче и говорит
|
|
154
323
|
// вслух, почему случилась: команда, возникшая без спроса, обязана объяснить себя, иначе её
|
|
155
324
|
// читают как сбой.
|
|
@@ -161,8 +330,9 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
161
330
|
console.log(c.bold(`\n${P.title}\n`));
|
|
162
331
|
|
|
163
332
|
const man = await readManifest();
|
|
164
|
-
const gates =
|
|
165
|
-
|
|
333
|
+
const gates = probeableGates(man);
|
|
334
|
+
const gs = gatesState(man);
|
|
335
|
+
if (gs.state === "none") { console.log(c.yellow(` ${P.noGates(`${SELF} add <имя>`)}\n`)); return; }
|
|
166
336
|
|
|
167
337
|
const raw = gitLog(2000);
|
|
168
338
|
if (raw === null) { console.log(c.yellow(` ${P.noGit}\n`)); return; }
|
|
@@ -175,9 +345,40 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
175
345
|
const catalog = await readCatalog();
|
|
176
346
|
const entries = catalog.filter((e) => triggerVerdict(e, facts).applies);
|
|
177
347
|
|
|
178
|
-
|
|
348
|
+
// ПЕСОЧНИЦА строится ОДИН раз на прогон, а не на каждую пробу: копия отслеживаемых файлов
|
|
349
|
+
// стоит доли секунды, но умножать её на файлы × записи незачем — между пробами меняется
|
|
350
|
+
// ровно один файл.
|
|
351
|
+
const sandbox = await buildSandbox();
|
|
352
|
+
if (!sandbox) { console.log(c.yellow(` ${P.noSandbox}\n`)); return; }
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// СУХОЙ ПРОГОН по чистой песочнице. Без него «покраснел от подсадки» неотличимо от «был
|
|
356
|
+
// красным и до неё»: у чужого проекта гейты краснеют на своём накопленном долге, и
|
|
357
|
+
// засчитывать эту красноту за поимку значит выдавать чужой долг за свою заслугу.
|
|
358
|
+
// В мутационном тестировании этот прогон обязателен по той же причине.
|
|
359
|
+
const before = runGates(gates, sandbox);
|
|
360
|
+
const plan = planProbeGates(before);
|
|
361
|
+
if (!plan.use.length) {
|
|
362
|
+
const red = before.filter((r) => r.code === 1).map((r) => r.name);
|
|
363
|
+
const broke = before.filter((r) => r.code !== 0 && r.code !== 1).map((r) => r.name);
|
|
364
|
+
if (plan.tooSlow.length) console.log(c.yellow(` ${P.allSlow(plan.tooSlow.map((g) => g.name))}\n`));
|
|
365
|
+
else console.log(c.yellow(` ${P.noBaseline(red, broke)}\n`));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
// Пробуем только запланированными, в порядке плана.
|
|
369
|
+
const byName = new Map(gates);
|
|
370
|
+
const probeGates = plan.use.map((g) => [g.name, byName.get(g.name)]);
|
|
371
|
+
const baseline = plan.use.map((g) => ({ name: g.name, code: g.code }));
|
|
179
372
|
|
|
180
|
-
|
|
373
|
+
console.log(c.dim(` ${P.method(hot.length, entries.length, plan.use.length)}\n`));
|
|
374
|
+
if (plan.tooSlow.length) {
|
|
375
|
+
console.log(c.dim(` ${P.tooSlow(plan.tooSlow.map((g) => `${g.name} (${Math.round(g.ms / 1000)}s)`))}\n`));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Записи проб: по ним считаются КЛАССЫ, а не события. Счётчики на месте были
|
|
379
|
+
// событиями и втрое завышали итог — см. countProbe.
|
|
380
|
+
const records = [];
|
|
381
|
+
let unprobedN = 0;
|
|
181
382
|
for (const { path: rel, fixes } of hot) {
|
|
182
383
|
console.log(` ${c.bold(rel)} ${c.dim(P.fixes(fixes))}`);
|
|
183
384
|
const ext = extname(rel).toLowerCase();
|
|
@@ -187,29 +388,67 @@ async function cmdProbe(args, { auto = false } = {}) {
|
|
|
187
388
|
const sample = await redSampleFor(e.slug, ext);
|
|
188
389
|
if (!sample) continue;
|
|
189
390
|
probed++;
|
|
190
|
-
const
|
|
191
|
-
let
|
|
192
|
-
try {
|
|
193
|
-
|
|
194
|
-
|
|
391
|
+
const restore = await plant(sandbox, rel, sample);
|
|
392
|
+
let after;
|
|
393
|
+
try { after = runGates(probeGates, sandbox, { stopOnRed: true }); } finally { await restore(); }
|
|
394
|
+
// Ранний выход обрывает список: гейты, до которых не дошли, считаются такими же, как на
|
|
395
|
+
// сухом прогоне. Иначе их отсутствие прочиталось бы как сбой запуска.
|
|
396
|
+
const seen = new Set(after.map((a) => a.name));
|
|
397
|
+
const full = after.concat(baseline.filter((b) => !seen.has(b.name)));
|
|
398
|
+
const r = probeVerdictPaired(baseline, full);
|
|
399
|
+
const verdict = r.verdict;
|
|
400
|
+
const caught = full.filter((a) => a.code === 1 && baseline.find((b) => b.name === a.name)?.code === 0)
|
|
401
|
+
.map((a) => a.name);
|
|
402
|
+
records.push({ entry: e.slug, file: rel, verdict });
|
|
195
403
|
if (verdict === "caught") {
|
|
196
404
|
console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
|
|
197
405
|
} else if (verdict === "blind") {
|
|
198
|
-
blind++;
|
|
199
406
|
console.log(` ${c.red("✘")} ${e.intent.padEnd(48)} ${c.red(P.blind)}`);
|
|
407
|
+
// Объяснить, а не назвать. Три строки, каждая отвечает на свой вопрос человека:
|
|
408
|
+
// «почему именно здесь», «что вы вообще подсадили» и «что мне сделать ПРЯМО СЕЙЧАС».
|
|
409
|
+
// Последняя обязана работать БЕЗ комплекта: польза до установки — единственный
|
|
410
|
+
// способ заслужить установку.
|
|
411
|
+
const adv = blindAdvice(e, facts, { file: rel, fixes });
|
|
412
|
+
console.log(c.dim(` ${P.blindWhere(rel, fixes)}`));
|
|
413
|
+
const lines = sampleLines(sample);
|
|
414
|
+
if (lines.length) {
|
|
415
|
+
console.log(c.dim(` ${P.blindWhat}`));
|
|
416
|
+
for (const l of lines) console.log(c.dim(` ${l}`));
|
|
417
|
+
}
|
|
418
|
+
if (adv.command) console.log(` ${c.yellow(P.blindFix(adv.command))}`);
|
|
419
|
+
if (adv.tool) console.log(c.dim(` ${P.blindTool(adv.tool)}`));
|
|
200
420
|
console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
|
|
201
421
|
} else {
|
|
202
422
|
console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(P.unknown)}`);
|
|
203
423
|
}
|
|
204
424
|
}
|
|
205
|
-
if (!probed) console.log(c.dim(` ${P.noSampleFor(ext || "—")}`));
|
|
425
|
+
if (!probed) { unprobedN++; console.log(c.dim(` ${P.noSampleFor(ext || "—")}`)); }
|
|
206
426
|
}
|
|
207
427
|
|
|
208
|
-
|
|
428
|
+
const n = countProbe(records);
|
|
429
|
+
const blind = n.blindClasses;
|
|
430
|
+
const state = probeSummary({
|
|
431
|
+
caught: n.caughtClasses, blind, unknown: n.unknownClasses, unprobed: unprobedN,
|
|
432
|
+
});
|
|
433
|
+
const say = {
|
|
434
|
+
blind: () => c.yellow(P.summaryBlind(blind, n.probes)),
|
|
435
|
+
partial: () => c.yellow(P.summaryPartial(n.caughtClasses, n.unknownClasses, unprobedN)),
|
|
436
|
+
clean: () => c.green(P.summaryClean),
|
|
437
|
+
"nothing-ran": () => c.yellow(P.summaryNothingRan(n.unknownClasses)),
|
|
438
|
+
"nothing-probed": () => c.yellow(P.summaryNothingProbed(unprobedN)),
|
|
439
|
+
};
|
|
440
|
+
console.log(`\n ${say[state]()}\n`);
|
|
209
441
|
|
|
210
442
|
// Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
|
|
211
443
|
// Без неё команда снова становится тем, о чём надо вспомнить.
|
|
212
|
-
await writeMark(commitCount(), blind,
|
|
444
|
+
await writeMark(commitCount(), blind, [
|
|
445
|
+
`ran: ${probeGates.map(([name]) => name).join(" ")}`, "",
|
|
446
|
+
...blindLines(records), "",
|
|
447
|
+
...hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`),
|
|
448
|
+
]);
|
|
449
|
+
} finally {
|
|
450
|
+
await rm(sandbox, { recursive: true, force: true });
|
|
451
|
+
}
|
|
213
452
|
}
|
|
214
453
|
|
|
215
454
|
// Состояние пробы для тех, кто только ПОКАЗЫВАЕТ его: прогон и блок для агента.
|
|
@@ -222,7 +461,8 @@ async function probeStatus() {
|
|
|
222
461
|
const every = probeEvery(man);
|
|
223
462
|
if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
|
|
224
463
|
if (every === 0) return { state: "off", behind: null };
|
|
225
|
-
|
|
464
|
+
const mark = await readMark();
|
|
465
|
+
return { ...probeState(mark, commitCount(), every), classes: parseBlind(mark?.text), ran: parseRan(mark?.text) };
|
|
226
466
|
}
|
|
227
467
|
|
|
228
|
-
export { cmdProbe, probeStatus,
|
|
468
|
+
export { cmdProbe, probeStatus, probeableGates, gatesState, extAlternatives, planProbeGates, blindAdvice, isCode };
|
package/tool/commands/report.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
|
21
21
|
import { proveGates } from "../lib/prove.mjs";
|
|
22
22
|
import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
|
|
23
23
|
import { changedCode, coverage, evidenceHash, readForHash } from "../lib/evidence.mjs";
|
|
24
|
-
import { runGates, declaredGates, sinceRef } from "
|
|
24
|
+
import { runGates, declaredGates, sinceRef } from "../lib/run.mjs";
|
|
25
25
|
import { L } from "../i18n/index.mjs";
|
|
26
26
|
|
|
27
27
|
// Каким рецептом стоит гейт: родным инструментом или переносимой проверкой. Именно это
|
package/tool/commands/vitals.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
import { CWD, MANIFEST, SELF, c, exists } from "../lib/core.mjs";
|
|
22
22
|
import { readManifest, unparsedLines, gateRequires } from "../lib/manifest.mjs";
|
|
23
23
|
import { whichSync } from "../lib/repo.mjs";
|
|
24
|
+
import { gitBash } from "../lib/execution.mjs";
|
|
24
25
|
import { updateWanted } from "../lib/brief.mjs";
|
|
25
26
|
import { L } from "../i18n/index.mjs";
|
|
26
27
|
|
|
@@ -94,7 +95,11 @@ async function cmdVitals() {
|
|
|
94
95
|
for (const [gate, cmd] of Object.entries(gates)) {
|
|
95
96
|
const prog = progOf(cmd);
|
|
96
97
|
if (!prog || seen.has(prog)) continue;
|
|
97
|
-
|
|
98
|
+
// На Windows слово `bash` в PATH — часто заглушка WSL, и «найден» было бы неправдой: гейт
|
|
99
|
+
// запустится через Git Bash (gateCommand) или не запустится вовсе. Спрашиваем того же, кого
|
|
100
|
+
// спросит прогон, — иначе vitals и doctor --run снова разойдутся.
|
|
101
|
+
const found = prog === "bash" && process.platform === "win32" ? Boolean(gitBash()) : Boolean(whichSync(prog));
|
|
102
|
+
seen.set(prog, { gate, prog, found });
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
// Первого слова мало. Запись каталога бывает обёрткой: команда начинается с `bash`, который
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -70,8 +70,9 @@ const enDocs = {
|
|
|
70
70
|
probeNever: "No coverage probe has run — what is covered by nothing here is UNKNOWN. That is not \"covered\": `aqk probe`.",
|
|
71
71
|
probeOff: "The coverage probe is switched off in the manifest (`probe: 0`) — nobody counts what is covered by nothing here.",
|
|
72
72
|
probeUnknown: "The coverage probe could not run — what is covered by nothing here is UNKNOWN. That is not \"covered\".",
|
|
73
|
-
probeBlind: (n, behind) =>
|
|
73
|
+
probeBlind: (n, behind, names = "") =>
|
|
74
74
|
`Covered by nothing: ${n} defect classes in the places people most often come back to fix` +
|
|
75
|
+
(names ? ` — ${names}` : "") +
|
|
75
76
|
(behind ? ` (the probe is ${behind} commits behind)` : "") + ". Details: `aqk probe`.",
|
|
76
77
|
probeClean: (behind) =>
|
|
77
78
|
"Coverage probe: in the places probed, every applicable class is caught by something" +
|
package/tool/i18n/en-gates.mjs
CHANGED
|
@@ -256,23 +256,41 @@ export const enGates = {
|
|
|
256
256
|
autoFirst: "no coverage probe has ever run here — running it myself. Turn off: AQK_PROBE=0",
|
|
257
257
|
auto: (n) => `${n} commits since the last probe — running it myself. Turn off: AQK_PROBE=0`,
|
|
258
258
|
title: "aqk probe — what the declared checks cannot see",
|
|
259
|
-
method: (files, entries) =>
|
|
260
|
-
`method: a red sample from a catalogue entry is planted into a
|
|
261
|
-
`
|
|
259
|
+
method: (files, entries, gates) =>
|
|
260
|
+
`method: a red sample from a catalogue entry is planted into a COPY of the project, then the ` +
|
|
261
|
+
`DECLARED gates are run there — the command is used as written, nothing is substituted into it. ` +
|
|
262
|
+
`Files: ${files}, applicable entries: ${entries}, gates green on a clean checkout: ${gates}. ` +
|
|
262
263
|
`The working tree is not touched.`,
|
|
263
264
|
fixes: (n) => `fixes in history: ${n}`,
|
|
264
265
|
caught: (names) => `caught by: ${names}`,
|
|
265
266
|
blind: "NOTHING CATCHES IT",
|
|
266
267
|
unknown: "nothing to check with — the gate did not run (delegated tool missing)",
|
|
268
|
+
blindWhere: (file, fixes) => `where: ${file} — ${fixes} fix commits in its history`,
|
|
269
|
+
blindWhat: "what we planted into your file:",
|
|
270
|
+
blindFix: (cmd) => `catch it right now, no kit needed: ${cmd}`,
|
|
271
|
+
blindTool: (url) => `the tool: ${url}`,
|
|
267
272
|
install: (cmd) => `close it: ${cmd}`,
|
|
268
273
|
noSampleFor: (ext) => `the catalogue has no red sample for "${ext}" — nothing to check with`,
|
|
269
274
|
noGates: (cmd) => `no gates declared — nothing to probe with. First: ${cmd}`,
|
|
275
|
+
noSandbox: "could not build a sandbox: `git archive HEAD` failed. The probe needs a copy of the tracked files to plant a sample into — it never touches the working tree.",
|
|
276
|
+
noBaseline: (red, broke) =>
|
|
277
|
+
`nothing to judge by: on a CLEAN checkout ${red.length ? `these gates are ALREADY red (${red.join(", ")})` : ""}${red.length && broke.length ? " and " : ""}${broke.length ? `these failed to run (${broke.join(", ")})` : ""}. A gate that is red before the sample is planted says nothing about the sample. Get the pipeline green first, then repeat.`,
|
|
278
|
+
tooSlow: (names) => `not probed with (too slow to run on every planting): ${names.join(", ")}. If a class below is caught by nobody, one of these may still catch it — run them by hand.`,
|
|
279
|
+
allSlow: (names) => `every gate that is green on a clean checkout is too slow to probe with: ${names.join(", ")}. Probing would re-run them for every planting. Declare a fast gate, or run these by hand.`,
|
|
270
280
|
noGit: "not a git repository — there is no fix history to read",
|
|
271
281
|
noFixes: "no fix commits found: the subject starts with fix / bugfix / hotfix",
|
|
272
|
-
summaryBlind: (n) =>
|
|
273
|
-
`classes
|
|
282
|
+
summaryBlind: (n, probes) =>
|
|
283
|
+
`classes caught by nobody: ${n}` +
|
|
284
|
+
(probes ? ` (over ${probes} plantings — one class repeated across several hot files is still ONE class)` : "") +
|
|
285
|
+
`. This is not a judgement of the code: these are the places ` +
|
|
274
286
|
`people come back to with a fix, and defects none of your checks would see there.`,
|
|
275
287
|
summaryClean: "in the places probed, every applicable class is caught by something.",
|
|
288
|
+
summaryPartial: (caught, unknown, unprobed) =>
|
|
289
|
+
`caught: ${caught}. Could NOT be checked: ${unknown} (the tool is missing, not the protection). Not probed at all: ${unprobed} file(s) — the catalogue has no red sample for their type. "Checked and clean", "not checked" and "not looked at" are three different facts and are not merged here.`,
|
|
290
|
+
summaryNothingRan: (n) =>
|
|
291
|
+
`NOTHING was checked: all ${n} probe(s) failed to run — the delegated tools are missing. This is not a clean result, it is the absence of a result. Install the tools, then repeat.`,
|
|
292
|
+
summaryNothingProbed: (unprobed) =>
|
|
293
|
+
`not a single probe was made${unprobed ? ` — ${unprobed} hot file(s) have no red sample for their type in the catalogue` : ""}. Nothing is known about coverage: this is the absence of a measurement, not a clean result.`,
|
|
276
294
|
},
|
|
277
295
|
prove: {
|
|
278
296
|
title: "aqk prove — proving the gates",
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -65,6 +65,9 @@ export const en = {
|
|
|
65
65
|
gitignore: "repository hygiene",
|
|
66
66
|
git: "project under version control",
|
|
67
67
|
|
|
68
|
+
rulesByHuman: (total, machine, human) =>
|
|
69
|
+
`${human} of ${total} rules in the entry point are guarded by a HUMAN, ${machine} by a machine.`,
|
|
70
|
+
rulesByHumanWhy: "A rule guarded by a human is guarded by nobody the day the human is busy. That is the hole this kit exists to close — and the reminder belongs to the human, not only to the agent. Counted in the ENTRY POINT only: a promise kept in any other file is checked by nothing at all, and this kit will not tell you it exists.",
|
|
68
71
|
emptyCommands: (n) => `AGENTS.md has ${n} unfilled commands.`,
|
|
69
72
|
emptyCommandsWhy: "An agent cannot execute an empty line.",
|
|
70
73
|
|
|
@@ -101,6 +104,17 @@ export const en = {
|
|
|
101
104
|
`claim unverified: "${e}" is declared held by gate "${g}", but neither its command nor the\n linter config names rules ${codes} — the entry may be held by nothing`,
|
|
102
105
|
coversUnprovenHow: (cmd) => `settle it: add those rules to the linter, or install the entry — ${cmd}`,
|
|
103
106
|
totalCovered: (n) => `held by another arbiter ${n}`,
|
|
107
|
+
blindHeading: (behind) => `The probe${behind ? ` (${behind} commits ago)` : ""} planted defects in your files — your checks did NOT catch them:`,
|
|
108
|
+
blindRan: (g) => `gate ${g} is declared and was run — and still missed the defect in this file`,
|
|
109
|
+
blindInstalled: "declared, but the probe did not run this gate (added later or too slow) — the next probe will show whether it catches",
|
|
110
|
+
blindMore: (cmd) => `what was planted and where — ${cmd}`,
|
|
111
|
+
probeNever: (cmd) => `Whether your checks catch a real defect has not been tested yet: ${cmd} plants one in a copy of the project and shows (a minute or two, your files are not touched).`,
|
|
112
|
+
startWith: "Start with these three — born from a real failure, and each closes with one ready command:",
|
|
113
|
+
startCmd: (cmd) => `one line, no kit needed: ${cmd}`,
|
|
114
|
+
startTool: (url) => `the tool: ${url}`,
|
|
115
|
+
startHook: "Running these by hand is a one-off. To have them run before every push: pre-commit (repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit, hooks aqk / aqk-doctor), or a plain .git/hooks/pre-push.",
|
|
116
|
+
haveAlready: (n) => `Checks you ALREADY have (${n}) — found in your own files, not invented:`,
|
|
117
|
+
haveAlreadyHow: (line) => `declare them and a machine holds them, not your attention. In .aqk.yml, under gates: ${line}`,
|
|
104
118
|
total: "Total:",
|
|
105
119
|
totalHeld: (n) => `held by a machine ${n}`,
|
|
106
120
|
totalTodo: (n) => `applicable but not installed ${n}`,
|
|
@@ -117,6 +131,8 @@ export const en = {
|
|
|
117
131
|
`either fix them and drop them from advisory, or admit the rule does not exist.`,
|
|
118
132
|
runHeading: "Running the declared gates",
|
|
119
133
|
timeout: "did not finish within 5 minutes",
|
|
134
|
+
running: (i, n) => `[${i}/${n}] running…`,
|
|
135
|
+
proving: "checking that the gates catch defects on their own samples…",
|
|
120
136
|
exitCode: (code) => `exit ${code}`,
|
|
121
137
|
moreLines: (n) => `… and ${n} more lines`,
|
|
122
138
|
declaredNotRun: (n) => `${n} gates declared, but never run.`,
|
package/tool/i18n/ru-docs.mjs
CHANGED
|
@@ -72,8 +72,9 @@ const ruDocs = {
|
|
|
72
72
|
probeNever: "Проба покрытия не делалась — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто»: `aqk probe`.",
|
|
73
73
|
probeOff: "Проба покрытия выключена в манифесте (`probe: 0`) — что здесь не прикрыто ничем, никто не считает.",
|
|
74
74
|
probeUnknown: "Пробу покрытия провести не удалось — что здесь не прикрыто ничем, НЕИЗВЕСТНО. Это не «прикрыто».",
|
|
75
|
-
probeBlind: (n, behind) =>
|
|
75
|
+
probeBlind: (n, behind, names = "") =>
|
|
76
76
|
`Не прикрыто ничем: ${n} классов брака в местах, куда чаще всего возвращаются с починкой` +
|
|
77
|
+
(names ? ` — ${names}` : "") +
|
|
77
78
|
(behind ? ` (проба отстала на ${behind} коммитов)` : "") + ". Подробно: `aqk probe`.",
|
|
78
79
|
probeClean: (behind) =>
|
|
79
80
|
"Проба покрытия: в проверенных местах каждый применимый класс кто-то ловит" +
|
package/tool/i18n/ru-gates.mjs
CHANGED
|
@@ -258,23 +258,41 @@ export const ruGates = {
|
|
|
258
258
|
autoFirst: "пробы покрытия здесь ещё не делали — делаю её сам. Выключить: AQK_PROBE=0",
|
|
259
259
|
auto: (n) => `прошло ${n} коммитов с прошлой пробы — делаю её сам. Выключить: AQK_PROBE=0`,
|
|
260
260
|
title: "aqk probe — чего объявленные проверки не видят",
|
|
261
|
-
method: (files, entries) =>
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
method: (files, entries, gates) =>
|
|
262
|
+
`способ: красный образец записи каталога подсаживается в КОПИЮ проекта, и там гоняются ` +
|
|
263
|
+
`ОБЪЯВЛЕННЫЕ гейты — команда берётся как написана, в неё ничего не подставляется. ` +
|
|
264
|
+
`Файлов: ${files}, применимых записей: ${entries}, гейтов зелёных на чистом дереве: ${gates}. ` +
|
|
264
265
|
`Рабочее дерево не трогается.`,
|
|
265
266
|
fixes: (n) => `починок в истории: ${n}`,
|
|
266
267
|
caught: (names) => `ловит: ${names}`,
|
|
267
268
|
blind: "НЕ ЛОВИТ НИКТО",
|
|
268
269
|
unknown: "проверить нечем — гейт не состоялся (нет делегированной программы)",
|
|
270
|
+
blindWhere: (file, fixes) => `где: ${file} — починок в истории: ${fixes}`,
|
|
271
|
+
blindWhat: "что подсадили в ваш файл:",
|
|
272
|
+
blindFix: (cmd) => `поймать прямо сейчас, без комплекта: ${cmd}`,
|
|
273
|
+
blindTool: (url) => `инструмент: ${url}`,
|
|
269
274
|
install: (cmd) => `закрыть: ${cmd}`,
|
|
270
275
|
noSampleFor: (ext) => `в каталоге нет красного образца под «${ext}» — проверить нечем`,
|
|
271
276
|
noGates: (cmd) => `гейтов не объявлено — пробовать нечем. Сначала: ${cmd}`,
|
|
277
|
+
noSandbox: "не удалось построить песочницу: `git archive HEAD` не отработал. Пробе нужна копия отслеживаемых файлов, чтобы подсадить в неё образец, — рабочее дерево она не трогает никогда.",
|
|
278
|
+
noBaseline: (red, broke) =>
|
|
279
|
+
`судить не по чему: на ЧИСТОМ дереве ${red.length ? `уже красные (${red.join(", ")})` : ""}${red.length && broke.length ? ", а " : ""}${broke.length ? `не запустились (${broke.join(", ")})` : ""}. Гейт, красный ДО подсадки, о подсадке не говорит ничего. Сначала позеленить конвейер, потом повторить.`,
|
|
280
|
+
tooSlow: (names) => `не пробовали ими (слишком долго гонять на каждую подсадку): ${names.join(", ")}. Если класс ниже не ловит никто — может ловить кто-то из них, проверьте руками.`,
|
|
281
|
+
allSlow: (names) => `все гейты, зелёные на чистом дереве, слишком медленны для пробы: ${names.join(", ")}. Проба гоняла бы их заново на каждую подсадку. Объявите быстрый гейт или прогоните эти руками.`,
|
|
272
282
|
noGit: "это не репозиторий git — истории починок взять неоткуда",
|
|
273
283
|
noFixes: "в истории не нашлось коммитов-починок: тема начинается с fix / исправ / почин",
|
|
274
|
-
summaryBlind: (n) =>
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
summaryBlind: (n, probes) =>
|
|
285
|
+
`классов не ловит никто: ${n}` +
|
|
286
|
+
(probes ? ` (на ${probes} подсадках — один класс, повторённый по нескольким горячим файлам, остаётся ОДНИМ)` : "") +
|
|
287
|
+
`. Это не приговор коду: это места, куда возвращаются с починкой, и дефекты, ` +
|
|
288
|
+
`которых там не увидела бы ни одна ваша проверка.`,
|
|
277
289
|
summaryClean: "в проверенных местах каждый применимый класс кто-то ловит.",
|
|
290
|
+
summaryPartial: (caught, unknown, unprobed) =>
|
|
291
|
+
`поймано: ${caught}. Проверить НЕ смогли: ${unknown} (не хватает инструмента, а не защиты). Не пробовали вовсе: файлов ${unprobed} — в каталоге нет красного образца их типа. «Проверено и чисто», «не проверено» и «не смотрели» — три разных факта, и здесь они не сливаются.`,
|
|
292
|
+
summaryNothingRan: (n) =>
|
|
293
|
+
`НЕ проверено ничего: все ${n} пробы не смогли запуститься — делегированные инструменты не установлены. Это не чистый результат, это отсутствие результата. Поставьте инструменты и повторите.`,
|
|
294
|
+
summaryNothingProbed: (unprobed) =>
|
|
295
|
+
`не сделано ни одной пробы${unprobed ? `: у ${unprobed} горячих файлов нет в каталоге красного образца их типа` : ""}. О прикрытии не известно ничего — это отсутствие замера, а не чистый результат.`,
|
|
278
296
|
},
|
|
279
297
|
prove: {
|
|
280
298
|
title: "aqk prove — доказательство гейтов",
|