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/lib/run.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// tool/lib/run.mjs — ПРОГОН объявленных гейтов, отдельно от команды, которая его показывает.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Прогон жил внутри `doctor.mjs`, и `report.mjs` с `badge.mjs` лезли за
|
|
4
|
+
// ним ИЗ КОМАНДЫ В КОМАНДУ. Это не деталь: команда — это то, что человек набирает, а прогон —
|
|
5
|
+
// то, что делает машина; у них разные причины меняться, и импорт команды из команды прятал
|
|
6
|
+
// этот шов.
|
|
7
|
+
//
|
|
8
|
+
// Вскрылось 2026-09-10 нашим же гейтом: `doctor.mjs` дорос до 508 строк при пределе 500 —
|
|
9
|
+
// файл был полон, и любая следующая правка ложилась туда просто потому, что «так ближе по
|
|
10
|
+
// контексту». Ровно то, о чём предупреждает совет самого гейта.
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { scopeOutput, splitAdvice, changedFiles } from "./scope.mjs";
|
|
13
|
+
import { CWD, c, die } from "./core.mjs";
|
|
14
|
+
import { advisorySet } from "./manifest.mjs";
|
|
15
|
+
import { L } from "../i18n/index.mjs";
|
|
16
|
+
import { gateCommand } from "./execution.mjs";
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// «Гейт объявлен» и «гейт работает» — разные утверждения. Первое читается из манифеста,
|
|
20
|
+
// второе узнаётся только запуском. Пока doctor верил манифесту на слово, уровень означал
|
|
21
|
+
// добросовестность автора, а не факт — ровно то, от чего мы защищаемся.
|
|
22
|
+
//
|
|
23
|
+
// Запуск чужих команд — по явной просьбе (--run), а не втихую: гейт бывает долгим и с
|
|
24
|
+
// побочными действиями. Без флага doctor честно говорит, что не проверял.
|
|
25
|
+
|
|
26
|
+
function declaredGates(man) {
|
|
27
|
+
const g = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
28
|
+
return Object.entries(g)
|
|
29
|
+
.map(([name, cmd]) => [name, String(cmd || "").trim()])
|
|
30
|
+
.filter(([, cmd]) => cmd);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ссылка, относительно которой сужается вывод: `--since main`, `--since HEAD~5`.
|
|
34
|
+
// Без значения флаг бессмыслен — молча взять умолчание нельзя: «сужено не тем» неотличимо
|
|
35
|
+
// от «не сужено».
|
|
36
|
+
function sinceRef(argv = process.argv) {
|
|
37
|
+
const i = argv.indexOf("--since");
|
|
38
|
+
if (i === -1) return null;
|
|
39
|
+
const v = argv[i + 1];
|
|
40
|
+
return v && !v.startsWith("-") ? v : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// СТРОКА «ИДЁТ». Гейт идёт через spawnSync, и строка про него печаталась только по завершении:
|
|
44
|
+
// минута `smoke` — минута пустого экрана, «работает» неотличимо от «повисло», и человек пишет
|
|
45
|
+
// сам, не дождавшись. Просьба владельца 2026-09-10: «я запустил и должен видеть, как идёт».
|
|
46
|
+
//
|
|
47
|
+
// Только в терминал. В пайп и в конвейер — ни байта: лог там читают глазами и разбирают
|
|
48
|
+
// машиной, и строка, переписанная возвратом каретки, в файле превращается в мусор.
|
|
49
|
+
// Секунды не тикают: для этого нужен асинхронный запуск и убийство группы процессов на
|
|
50
|
+
// таймауте, а у нас windows в конвейере. Имя и номер гейта отвечают на вопрос и без них.
|
|
51
|
+
function progress({ tty = process.stdout.isTTY, write = (s) => process.stdout.write(s) } = {}) {
|
|
52
|
+
let shown = false;
|
|
53
|
+
return {
|
|
54
|
+
show(text) {
|
|
55
|
+
if (!tty) return;
|
|
56
|
+
if (shown) write("\r\x1b[K");
|
|
57
|
+
write(text);
|
|
58
|
+
shown = true;
|
|
59
|
+
},
|
|
60
|
+
clear() {
|
|
61
|
+
if (!tty || !shown) return;
|
|
62
|
+
write("\r\x1b[K");
|
|
63
|
+
shown = false;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function runGates(man, opts = {}) {
|
|
69
|
+
const gates = declaredGates(man);
|
|
70
|
+
if (!gates.length) return { failed: 0, ran: 0, results: [] };
|
|
71
|
+
const advisory = advisorySet(man);
|
|
72
|
+
const bar = progress();
|
|
73
|
+
|
|
74
|
+
// Сужение по дифу — договор с человеком, и он должен видеть, ЧТО именно сужено. Пустой диф
|
|
75
|
+
// называется вслух: иначе «все гейты зелёные» означало бы «сравнили не с тем» и читалось бы
|
|
76
|
+
// как успех. Это тот же класс, что и весь стандарт, только внутри нашего флага.
|
|
77
|
+
const scoped = opts.since ? changedFiles(opts.since, CWD) : null;
|
|
78
|
+
if (opts.since && scoped === null) die(L.doctor.sinceBadRef(opts.since));
|
|
79
|
+
if (scoped) console.log(c.dim(`\n ${L.doctor.sinceHeading(opts.since, scoped.size)}`));
|
|
80
|
+
|
|
81
|
+
console.log(c.bold(`\n ${L.doctor.runHeading}\n`));
|
|
82
|
+
let failed = 0;
|
|
83
|
+
const results = [];
|
|
84
|
+
|
|
85
|
+
for (const [i, [name, cmd]] of gates.entries()) {
|
|
86
|
+
bar.show(` ${c.dim("⋯")} ${name.padEnd(14)} ${c.dim(L.doctor.running(i + 1, gates.length))}`);
|
|
87
|
+
const t0 = Date.now();
|
|
88
|
+
const r = spawnSync(gateCommand(cmd), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
89
|
+
bar.clear();
|
|
90
|
+
const secs = (Math.max(0, Date.now() - t0) / 1000).toFixed(1);
|
|
91
|
+
// Вывод гейта запоминается целиком (с потолком, чтобы болтливый инструмент не съел память):
|
|
92
|
+
// по нему считается покрытие дифа — какой файл вообще был назван хоть одной проверкой.
|
|
93
|
+
// Без этого «готово = доказано» остаётся правилом, за которым следит только человек.
|
|
94
|
+
const outAll = `${r.stdout || ""}${r.stderr || ""}`.slice(0, 200000);
|
|
95
|
+
|
|
96
|
+
if (r.error && r.error.code === "ETIMEDOUT") {
|
|
97
|
+
console.log(` ${c.red("✘")} ${name.padEnd(14)} ${c.red(L.doctor.timeout)}`);
|
|
98
|
+
failed++;
|
|
99
|
+
results.push({ name, cmd, ok: false, secs, note: L.doctor.timeout, out: outAll });
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const code = r.status;
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
// Совещательный называется и когда он зелёный. Иначе гейт, который уронить прогон НЕ
|
|
105
|
+
// МОЖЕТ, по выводу неотличим от того, который может, — и список `advisory:` в манифесте
|
|
106
|
+
// виден только в тот день, когда он покраснел. Измерено 2026-09-09: зелёный
|
|
107
|
+
// совещательный печатался обычной галочкой, а README обещал, что список назван каждый
|
|
108
|
+
// прогон. Тот же класс, что молчащий гейт, только про сам прибор.
|
|
109
|
+
const quiet = advisory.has(name) ? ` ${c.yellow(L.doctor.advisoryQuiet)}` : "";
|
|
110
|
+
console.log(` ${c.green("✔")} ${name.padEnd(14)}${quiet} ${c.dim(`${secs}s · ${cmd}`)}`);
|
|
111
|
+
// Зелёный гейт иногда всё-таки говорит человеку что-то важное: храповик, дошедший до цели,
|
|
112
|
+
// просит убрать обёртку. Вывод успешного гейта не показывался вовсе, и это сообщение
|
|
113
|
+
// уходило в никуда — тот же класс, что обрезанный совет у красного, только тише.
|
|
114
|
+
// Показываем ровно строки с меткой совета: остальной вывод успешной проверки — шум.
|
|
115
|
+
const okAdvice = splitAdvice(`${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean)).advice;
|
|
116
|
+
for (const line of okAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
117
|
+
results.push({ name, cmd, ok: true, secs, advisory: advisory.has(name), out: outAll });
|
|
118
|
+
} else {
|
|
119
|
+
const raw = `${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean);
|
|
120
|
+
// Совет отделяется ДО сужения. Иначе он сам попадает под фильтр по путям: сообщение
|
|
121
|
+
// храповика про вышедший срок называет путь к реестру, реестра в дифе нет, и гейт,
|
|
122
|
+
// обязанный краснеть по сроку, печатался зелёным с пометкой «находки вне дифа».
|
|
123
|
+
// Ровно то, что стандарт запрещает: срок без последствия. Найдено ревью 2026-09-06.
|
|
124
|
+
const parted = splitAdvice(raw);
|
|
125
|
+
let out = parted.findings;
|
|
126
|
+
const alwaysAdvice = parted.advice;
|
|
127
|
+
|
|
128
|
+
// Сужение до дифа. Три исхода, и все три называются вслух.
|
|
129
|
+
if (scoped) {
|
|
130
|
+
const s = scopeOutput(out, scoped);
|
|
131
|
+
// Гейт, у которого находок нет вовсе, а есть только совет, сузить нечем: его вердикт
|
|
132
|
+
// не про файлы. Признать такой успешным — вернуть ту же тишину другим путём.
|
|
133
|
+
if (!s.scopable || out.length === 0) {
|
|
134
|
+
// Гейт печатает вердикт без путей — сузить нечем. Признать его успешным значило бы
|
|
135
|
+
// выдать провал за тишину; остаётся красным, и причина названа.
|
|
136
|
+
// Совещательный не роняет прогон НИКОГДА — в том числе здесь. Раньше failed++ стоял
|
|
137
|
+
// безусловно, и гейт, объявленный совещательным, валил сборку с `--since` только
|
|
138
|
+
// потому, что в его выводе нет путей. Измерено 2026-09-09.
|
|
139
|
+
const nsAdv = advisory.has(name);
|
|
140
|
+
const nsMark = nsAdv ? c.yellow("!") : c.red("✘");
|
|
141
|
+
const nsVerdict = nsAdv ? c.yellow(L.doctor.advisoryMark) : c.red(L.doctor.exitCode(code));
|
|
142
|
+
console.log(` ${nsMark} ${name.padEnd(14)} ${nsVerdict} ${c.dim(`· ${L.doctor.notScopable}`)}`);
|
|
143
|
+
if (!nsAdv) failed++;
|
|
144
|
+
results.push({ name, cmd, ok: false, secs, code, advisory: nsAdv, note: L.doctor.notScopable, out: outAll });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (s.findings === 0) {
|
|
148
|
+
// Долг есть, но не в том, что внёс диф. Зелёный — но с числом спрятанного: молчаливое
|
|
149
|
+
// «всё хорошо» здесь было бы неправдой.
|
|
150
|
+
const sQuiet = advisory.has(name) ? ` ${c.yellow(L.doctor.advisoryQuiet)}` : "";
|
|
151
|
+
console.log(` ${c.green("✔")} ${name.padEnd(14)}${sQuiet} ${c.dim(`${secs}s · ${L.doctor.outsideDiff(out.length)}`)}`);
|
|
152
|
+
results.push({ name, cmd, ok: true, secs, advisory: advisory.has(name), scopedAway: out.length, out: outAll });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
out = s.kept;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
failed++;
|
|
159
|
+
// Находки обрезаются, совет — никогда. Все записи каталога печатают «почини: …» последней
|
|
160
|
+
// строкой, и при обрезке до трёх строк человек не видел именно её: находка без действия
|
|
161
|
+
// закрывает окно, а не дефект.
|
|
162
|
+
// Совещательный гейт показывает находки и не роняет прогон. Знак другой, чтобы «показано»
|
|
163
|
+
// и «провалено» не читались одинаково; в сводке ниже он назван поимённо.
|
|
164
|
+
const isAdvisory = advisory.has(name);
|
|
165
|
+
if (isAdvisory) failed--;
|
|
166
|
+
const mark = isAdvisory ? c.yellow("!") : c.red("✘");
|
|
167
|
+
const verdict = isAdvisory ? c.yellow(L.doctor.advisoryMark) : c.red(L.doctor.exitCode(code));
|
|
168
|
+
console.log(` ${mark} ${name.padEnd(14)} ${verdict} ${c.dim(`· ${secs}s · ${cmd}`)}`);
|
|
169
|
+
// ГОЛОВА И ХВОСТ, А НЕ ТОЛЬКО ГОЛОВА. Гейт, который сам является прогоном (наш `smoke`),
|
|
170
|
+
// печатает сотни строк, и вердикт у него в конце — при обрезке до первых трёх человек
|
|
171
|
+
// видел «программа разбирается» и ни слова о том, что упало. Час поисков в конвейере
|
|
172
|
+
// 2026-09-09 стоил ровно этого. Голова нужна тоже: у сканирующих записей находки идут
|
|
173
|
+
// с первой строки.
|
|
174
|
+
const HEAD = 3, TAIL = 2;
|
|
175
|
+
for (const line of out.slice(0, HEAD)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
176
|
+
if (out.length > HEAD + TAIL) {
|
|
177
|
+
console.log(c.dim(` ${L.doctor.moreLines(out.length - HEAD - TAIL)}`));
|
|
178
|
+
for (const line of out.slice(-TAIL)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
179
|
+
} else {
|
|
180
|
+
for (const line of out.slice(HEAD)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
181
|
+
}
|
|
182
|
+
// Совет тоже не бесконечен: гейт, зовущий помощник шесть раз, печатает его шесть раз.
|
|
183
|
+
for (const line of alwaysAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
184
|
+
results.push({ name, cmd, ok: false, secs, code, advisory: isAdvisory, out: outAll });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Совещательные, которые покраснели, называются вслух ВСЕГДА. Молчание о них — ровно та
|
|
188
|
+
// тишина, против которой построен стандарт: проверка выключена, а выглядит как её отсутствие.
|
|
189
|
+
const advisoryFailed = results.filter((x) => x.advisory && !x.ok).map((x) => x.name);
|
|
190
|
+
if (advisoryFailed.length) console.log(`\n ${c.yellow(L.doctor.advisorySummary(advisoryFailed))}`);
|
|
191
|
+
return { failed, ran: gates.length, results, advisoryFailed };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { declaredGates, sinceRef, runGates, progress };
|
package/tool/selfcheck/gates.sh
CHANGED
|
@@ -98,11 +98,49 @@ for GATE in "$CAT"/*/; do
|
|
|
98
98
|
# приезжает туда с CRLF (`.gitattributes` держит LF только для `*.sh`), а `grep -i` с
|
|
99
99
|
# кириллицей в сборке MSYS ведёт себя не так, как GNU grep. Регистр первой буквы назван
|
|
100
100
|
# явно — это дешевле, чем полагаться на сворачивание регистра в чужой сборке grep.
|
|
101
|
+
# «Файла нет» и «раздела нет» — РАЗНЫЕ приговоры, а до 2026-09-10 оба звучали одинаково:
|
|
102
|
+
# `tr` из недоступного файла даёт пустую строку, grep по ней молчит, и запись обвинялась в
|
|
103
|
+
# том, чего не делала. Дважды наблюдался плавающий отказ этой самой проверки на записи,
|
|
104
|
+
# README которой на месте и раздел содержит; воспроизвести за сорок прогонов не удалось.
|
|
105
|
+
# Пока причина неизвестна, сообщение обязано называть, что именно случилось, — иначе
|
|
106
|
+
# следующий раз снова будет ложным обвинением вместо улики.
|
|
107
|
+
if [ ! -s "$GATE/README.md" ]; then
|
|
108
|
+
bad "$SLUG: README.md записи отсутствует или пуст — проверить разделы НЕЧЕМ, а не «их нет»"
|
|
109
|
+
continue
|
|
110
|
+
fi
|
|
101
111
|
README_TEXT="$(tr -d '\r' < "$GATE/README.md" 2>/dev/null)"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
if [ -z "$README_TEXT" ]; then
|
|
113
|
+
bad "$SLUG: README.md есть, но прочитать не удалось — проверка НЕ СОСТОЯЛАСЬ"
|
|
114
|
+
continue
|
|
115
|
+
fi
|
|
116
|
+
# Три исхода, а не два. `grep` отвечает 0 «нашёл», 1 «не нашёл» и 2+ «не смог»: при нехватке
|
|
117
|
+
# памяти или процессов он не запускается вовсе, и его код неотличим от «раздела нет». Отказ
|
|
118
|
+
# наблюдался трижды под тяжёлой параллельной нагрузкой, каждый раз на РАЗНОЙ записи, README
|
|
119
|
+
# которой на месте и раздел содержит; воспроизвести в покое не удалось за сорок прогонов.
|
|
120
|
+
# Причина не доказана — но обвинять запись в том, чего не проверяли, нельзя в любом случае.
|
|
121
|
+
# ПОЧЕМУ ЗДЕСЬ НЕТ КОНВЕЙЕРА. Было `printf … | grep -qE …`, и это давало ПЛАВАЮЩИЙ отказ,
|
|
122
|
+
# который мы ловили трижды и не могли объяснить: запись обвинялась в отсутствии раздела,
|
|
123
|
+
# который у неё есть. Причина найдена 2026-09-10 тем самым сообщением «grep вышел с кодом
|
|
124
|
+
# 141», ради которого исход и был разложен на три. 141 = 128 + 13 = SIGPIPE: `grep -q`
|
|
125
|
+
# выходит на ПЕРВОМ совпадении и закрывает трубу, а `printf` в этот момент ещё пишет и
|
|
126
|
+
# умирает от сигнала. При `set -o pipefail` (строка 9) статусом конвейера становится 141.
|
|
127
|
+
# Воспроизведено 100 из 100, когда совпадение в НАЧАЛЕ текста, — оттого и «случайная»
|
|
128
|
+
# запись: у одних раздел в начале README, у других в конце.
|
|
129
|
+
# Здесь-строка конвейера не создаёт, и гонки не существует.
|
|
130
|
+
grep -qE '[Гг]отовы(й аналог|й инструмент|е правил)|[Гг]отового аналога' <<< "$README_TEXT"
|
|
131
|
+
case $? in
|
|
132
|
+
0) : ;;
|
|
133
|
+
1) bad "$SLUG: в README нет раздела про готовый аналог — «не искал» и «нет» разные утверждения" ;;
|
|
134
|
+
*) bad "$SLUG: поиск раздела про готовый аналог НЕ СОСТОЯЛСЯ (grep вышел с кодом $?) — это не приговор записи" ;;
|
|
135
|
+
esac
|
|
136
|
+
# Те же три исхода, что и у поиска выше, и по той же причине.
|
|
137
|
+
# Тот же довод, что у поиска выше: конвейер + `grep -q` = гонка с SIGPIPE.
|
|
138
|
+
grep -qE 'чего НЕ ловит|Чего НЕ ловит|чего не ловит|Чего не ловит' <<< "$README_TEXT"
|
|
139
|
+
case $? in
|
|
140
|
+
0) : ;;
|
|
141
|
+
1) bad "$SLUG: в README нет раздела «чего НЕ ловит» — граница записи обязана быть названа" ;;
|
|
142
|
+
*) bad "$SLUG: поиск раздела «чего НЕ ловит» НЕ СОСТОЯЛСЯ (grep вышел с кодом $?) — это не приговор записи" ;;
|
|
143
|
+
esac
|
|
106
144
|
|
|
107
145
|
# Зрелость не объявляют — её считают. Попытка написать `lifecycle: stable` руками отклоняется:
|
|
108
146
|
# поле, которое заполняет автор, означает доверие к автору, а не факт. Это ровно тот способ,
|
|
@@ -23,12 +23,34 @@ const CLI = join(ROOT, "tool", "program.mjs");
|
|
|
23
23
|
// а ради того, чтобы висящий процесс называл себя, а не съедал бюджет задания молча.
|
|
24
24
|
const TIMEOUT_MS = 30000;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Настоящий дом СНИМАЕТСЯ ОДИН РАЗ, до всякой подмены: из него берётся PYTHONUSERBASE.
|
|
27
|
+
const REAL_HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
28
|
+
|
|
29
|
+
function env(home, dir) {
|
|
27
30
|
return {
|
|
28
31
|
...process.env,
|
|
32
|
+
// PYTHONUSERBASE — ИЗ-ЗА подмены HOME и до неё. Инструменты, поставленные `pip --user`
|
|
33
|
+
// (`vulture`, `pylint`), это python-скрипты, которые ищут свои модули в
|
|
34
|
+
// $HOME/.local/lib. С песочницей вместо HOME импорт падает, инструмент выходит ненулевым,
|
|
35
|
+
// и обёртка родного рецепта читает это как НАХОДКУ. То есть проверка краснела не на
|
|
36
|
+
// дефекте, а на собственной оснастке. Тот же довод дословно записан в шапке `smoke.sh`;
|
|
37
|
+
// здесь он был потерян при переезде на встроенный раннер.
|
|
38
|
+
PYTHONUSERBASE: process.env.PYTHONUSERBASE || `${REAL_HOME}/.local`,
|
|
29
39
|
HOME: home,
|
|
30
40
|
USERPROFILE: home,
|
|
31
41
|
TMPDIR: join(home, "tmp"),
|
|
42
|
+
// ПОТОЛОК ПОИСКА РЕПОЗИТОРИЯ. Без него `git: false` не означает «вне репозитория»: git
|
|
43
|
+
// идёт вверх по дереву, и если ЛЮБОЙ предок временного каталога окажется репозиторием,
|
|
44
|
+
// подопытный проект молча получит чужую историю. Поймано 2026-09-10: проверка «гейт без
|
|
45
|
+
// репозитория обязан сказать "не смогли"» проходила поодиночке и падала внутри `smoke.sh`,
|
|
46
|
+
// потому что там над TMPDIR оказывался git. Один и тот же тест давал разные ответы в
|
|
47
|
+
// зависимости от того, чем занят каталог этажом выше, — то есть герметичность, ради
|
|
48
|
+
// которой этот файл написан, держалась на случайности.
|
|
49
|
+
//
|
|
50
|
+
// Указывается РОДИТЕЛЬ, а не сам каталог: git не поднимается ВЫШЕ потолка, но от стартового
|
|
51
|
+
// каталога до него доходит. С потолком, равным проекту, репозиторий этажом выше находился
|
|
52
|
+
// по-прежнему — проверено прямым опытом, а не выведено из документации.
|
|
53
|
+
GIT_CEILING_DIRECTORIES: dirname(dir),
|
|
32
54
|
AQK_LANG: "ru",
|
|
33
55
|
// Сеть в тестах — отдельный класс флейков, и у нас она включалась ТОЛЬКО вне конвейера:
|
|
34
56
|
// локально проверки были сетевыми, в конвейере нет. Две разные среды по построению.
|
|
@@ -62,7 +84,7 @@ function project(t, files = {}, { git = true } = {}) {
|
|
|
62
84
|
// что команда не роняет прогон.
|
|
63
85
|
function run({ dir, home }, cmd, args = []) {
|
|
64
86
|
const r = spawnSync(cmd, args, {
|
|
65
|
-
cwd: dir, encoding: "utf8", timeout: TIMEOUT_MS, env: env(home),
|
|
87
|
+
cwd: dir, encoding: "utf8", timeout: TIMEOUT_MS, env: env(home, dir),
|
|
66
88
|
});
|
|
67
89
|
return { code: r.status, out: `${r.stdout || ""}${r.stderr || ""}` };
|
|
68
90
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// tool/selfcheck/smoke/fail-closed.test.mjs — проверка, которая НЕ МОЖЕТ работать, обязана
|
|
2
|
+
// сказать это, а не позеленеть.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ. Весь стандарт написан против одного: тишина читается как «чисто». Три раза подряд
|
|
5
|
+
// он нарушен в нём самом — найдено 2026-09-10, когда проба впервые начала гонять гейты по
|
|
6
|
+
// КОПИИ проекта и увидела их так же, как видит пользователь:
|
|
7
|
+
//
|
|
8
|
+
// 1. Подмена общей библиотеки `kit/gates/_skip.sh` даёт код 0. Гейт печатает
|
|
9
|
+
// «command not found» и объявляет проект чистым. Библиотеку подключает КАЖДАЯ запись
|
|
10
|
+
// каталога, то есть одним испорченным файлом гасится весь набор защит.
|
|
11
|
+
// 2. Гейт, читающий историю, в каталоге без git возвращает 0 за ноль секунд. Он не прошёл —
|
|
12
|
+
// он не смог посмотреть, и разницы в выводе нет никакой.
|
|
13
|
+
//
|
|
14
|
+
// Обе — отказ В ОТКРЫТУЮ сторону: чем сильнее сломано, тем зеленее ответ. Договор один и тот
|
|
15
|
+
// же, он уже записан в комплекте: 0 — проверено и чисто, 1 — находка, ВСЁ ОСТАЛЬНОЕ — «не
|
|
16
|
+
// смогли проверить». Именно так `prove` и `execution.mjs` разбирают исход, и гейты обязаны
|
|
17
|
+
// говорить на том же языке.
|
|
18
|
+
import test from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { cpSync, writeFileSync, mkdirSync } from "node:fs";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { project, run, aqk } from "./_fixture.mjs";
|
|
24
|
+
|
|
25
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
26
|
+
const posix = (p) => String(p).replace(/\\/g, "/");
|
|
27
|
+
|
|
28
|
+
// Ставит запись каталога В ПРОЕКТ вместе с общей библиотекой — так же, как это делает `aqk add`.
|
|
29
|
+
// Библиотеку кладём отдельно, чтобы проверка могла её испортить: в исходном каталоге её портить
|
|
30
|
+
// нельзя, там она общая на весь набор.
|
|
31
|
+
function installGate(p, name, { skipLib } = {}) {
|
|
32
|
+
const dest = join(p.dir, "gates", name);
|
|
33
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
34
|
+
cpSync(join(ROOT, "kit", "gates", name), dest, { recursive: true });
|
|
35
|
+
const lib = join(p.dir, "gates", "_skip.sh");
|
|
36
|
+
if (skipLib === undefined) cpSync(join(ROOT, "kit", "gates", "_skip.sh"), lib);
|
|
37
|
+
else writeFileSync(lib, skipLib, "utf8");
|
|
38
|
+
return posix(join("gates", name, "check.sh"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("гейт с ИСПОРЧЕННОЙ общей библиотекой не смеет отвечать «чисто»", (t) => {
|
|
42
|
+
const p = project(t, { "src/a.py": "x = 1\n" });
|
|
43
|
+
const check = installGate(p, "complexity-limit", { skipLib: "мусор вместо библиотеки\n" });
|
|
44
|
+
const r = run(p, "bash", [check, "."]);
|
|
45
|
+
assert.notEqual(r.code, 0,
|
|
46
|
+
`гейт вернул 0 при нерабочей библиотеке обхода. Вывод:\n${r.out}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("та же запись с ЦЕЛОЙ библиотекой на том же проекте молчит — значит дело в поломке", (t) => {
|
|
50
|
+
const p = project(t, { "src/a.py": "x = 1\n" });
|
|
51
|
+
const check = installGate(p, "complexity-limit");
|
|
52
|
+
const r = run(p, "bash", [check, "."]);
|
|
53
|
+
assert.equal(r.code, 0, `гейт покраснел на исправном проекте. Вывод:\n${r.out}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Второй отказ: истории нет — смотреть не на что. «Не смогли» и «чисто» обязаны различаться,
|
|
57
|
+
// иначе проект без git получает зелёный прогон по построению.
|
|
58
|
+
for (const name of ["commit-explains-itself", "test-not-adjusted"]) {
|
|
59
|
+
test(`«${name}» без репозитория говорит «не смогли», а не «чисто»`, (t) => {
|
|
60
|
+
const p = project(t, { "src/a.py": "x = 1\n" }, { git: false });
|
|
61
|
+
const check = installGate(p, name);
|
|
62
|
+
const r = run(p, "bash", [check, "."]);
|
|
63
|
+
assert.notEqual(r.code, 0,
|
|
64
|
+
`гейт вернул 0 там, где истории нет и посмотреть было не на что. Вывод:\n${r.out}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// У `protection-not-removed` свидетель — тоже история, но выходит он раньше: на отсутствии
|
|
69
|
+
// манифеста. Поэтому проект здесь С манифестом и объявленными гейтами: сторожить есть что,
|
|
70
|
+
// а посмотреть нечем.
|
|
71
|
+
test("«protection-not-removed» без репозитория говорит «не смогли», а не «чисто»", (t) => {
|
|
72
|
+
const p = project(t, {
|
|
73
|
+
"src/a.py": "x = 1\n",
|
|
74
|
+
".aqk.yml": "aqk: 1\nentry:\n - AGENTS.md\ngates:\n lint: \"true\"\n",
|
|
75
|
+
"AGENTS.md": "# проект\n",
|
|
76
|
+
}, { git: false });
|
|
77
|
+
const check = installGate(p, "protection-not-removed");
|
|
78
|
+
const r = run(p, "bash", [check, "."]);
|
|
79
|
+
assert.notEqual(r.code, 0,
|
|
80
|
+
`гейт вернул 0 там, где снимок сторожить нечем. Вывод:\n${r.out}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
84
|
+
// Правило со сторожем-человеком обязано быть названо ЧЕЛОВЕКУ. Написано ДО кода 2026-09-10.
|
|
85
|
+
//
|
|
86
|
+
// ЗАЧЕМ. Отчёт живого проекта: «всё, что касается масштаба, помечено `aqk: человек`. AQK
|
|
87
|
+
// отработал честно: потребовал назвать сторожа, мы назвали — и сторож не проверил». Пометка
|
|
88
|
+
// `человек` — не сторож, а расписка в том, что сторожа нет: гейт `promise-has-gate` видит её
|
|
89
|
+
// и идёт дальше (`if (tag == "человек") next`).
|
|
90
|
+
//
|
|
91
|
+
// Числа уже считаются и уже печатаются — но только в блоке `context`, который читает АГЕНТ.
|
|
92
|
+
// Человеку, который и назначен сторожем, `doctor` про это не говорит ни слова. То есть
|
|
93
|
+
// единственный, кто обязан помнить о непроверяемом обещании, — единственный, кому о нём не
|
|
94
|
+
// сообщают. В нашем собственном своде так помечено 12 правил из 14.
|
|
95
|
+
test("doctor называет человеку, сколько правил не сторожит машина", (t) => {
|
|
96
|
+
const p = project(t, {
|
|
97
|
+
"AGENTS.md": [
|
|
98
|
+
"# проект",
|
|
99
|
+
"## Правила",
|
|
100
|
+
"- **Секреты не в коде.** <!-- aqk: secrets-not-in-code -->",
|
|
101
|
+
"- **План до кода.** <!-- aqk: человек -->",
|
|
102
|
+
"- **Три попытки.** <!-- aqk: человек -->",
|
|
103
|
+
"",
|
|
104
|
+
].join("\n"),
|
|
105
|
+
".aqk.yml": 'aqk: 1\nentry:\n - AGENTS.md\ngates:\n lint: "true"\n',
|
|
106
|
+
});
|
|
107
|
+
const r = aqk(p, "doctor");
|
|
108
|
+
assert.match(r.out, /2/,
|
|
109
|
+
`doctor не назвал число правил со сторожем-человеком. Вывод:\n${r.out}`);
|
|
110
|
+
assert.ok(/человек/i.test(r.out),
|
|
111
|
+
`doctor не сказал про сторожа-человека ни слова, хотя таких правил здесь два.\n${r.out}`);
|
|
112
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// tool/selfcheck/smoke/own-samples.test.mjs — запись каталога не смеет выдавать СВОИ ЖЕ красные
|
|
2
|
+
// образцы за находки пользователя.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ. `aqk add` кладёт в чужой проект гейт ВМЕСТЕ с образцами: без них `prove` ничего не
|
|
5
|
+
// докажет. Образцы существуют, чтобы быть неправильными. Значит любой сканирующий гейт обязан их
|
|
6
|
+
// не замечать — иначе человек ставит защиту и получает вечно красный конвейер на файлах, которые
|
|
7
|
+
// положили МЫ. Обход для этого есть давно (`own_samples_filter`), и переносимые рецепты им
|
|
8
|
+
// пользуются.
|
|
9
|
+
//
|
|
10
|
+
// ЧТО СЛОМАЛОСЬ. Обход построчный: он вырезает строки, содержащие путь образца. `ruff` перешёл на
|
|
11
|
+
// многострочные блоки — путь уехал в отдельную строку («--> путь:строка:колонка»), а сообщение и
|
|
12
|
+
// кусок исходника остались, и вырезать их фильтру нечем. Найдено 2026-09-10 на фикстуре пробы:
|
|
13
|
+
// переносимый рецепт на том же проекте молчит, родной краснеет.
|
|
14
|
+
//
|
|
15
|
+
// ПОЧЕМУ ПРОВЕРКА, А НЕ ТОЛЬКО ПРАВКА. Правка чинит `ruff` сегодня. Формат вывода меняет любой
|
|
16
|
+
// инструмент и не предупреждает — без этой проверки та же дыра вернётся молча и с другим именем.
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { project, run, aqk } from "./_fixture.mjs";
|
|
23
|
+
|
|
24
|
+
// Записи с РОДНЫМ рецептом. Дыра была не в ruff: она в том, что общий обход построчный, а вывод
|
|
25
|
+
// инструмента — нет. Значит проверять надо не один инструмент, а каждый, который каталог зовёт.
|
|
26
|
+
//
|
|
27
|
+
// Таблица, а не список из четырёх имён: рецепт добавляют в каталог, и покрытие обязано
|
|
28
|
+
// появляться вместе с ним, а не через месяц. Каждая строка — чем наполнить проект, чтобы
|
|
29
|
+
// `aqk add` выбрал ИМЕННО этот рецепт, и какой командой её видно.
|
|
30
|
+
//
|
|
31
|
+
// Инструмента нет на машине — проверка ПРОПУСКАЕТСЯ вслух, а не зеленеет: «нечем проверить
|
|
32
|
+
// здесь» и «проверено» разные факты, и весь комплект написан про эту разницу.
|
|
33
|
+
const NATIVE = [
|
|
34
|
+
{ entry: "todo-without-task", tool: "ruff", files: { "src/app.py": "def add(a, b):\n return a + b\n" } },
|
|
35
|
+
{ entry: "no-print-in-prod", tool: "ruff", files: { "src/app.py": "def add(a, b):\n return a + b\n" } },
|
|
36
|
+
{ entry: "swallowed-error", tool: "ruff", files: { "src/app.py": "def add(a, b):\n return a + b\n" } },
|
|
37
|
+
{ entry: "complexity-limit", tool: "ruff", files: { "src/app.py": "def add(a, b):\n return a + b\n" } },
|
|
38
|
+
{ entry: "dead-code", tool: "vulture", files: { "src/app.py": "def add(a, b):\n return a + b\n\n\nprint(add(1, 2))\n" } },
|
|
39
|
+
{ entry: "duplicate-code", tool: "pylint", files: { "src/app.py": "def add(a, b):\n return a + b\n" } },
|
|
40
|
+
// javascript/typescript: eslint зовут четыре записи. Проект наполняется .js, чтобы `add`
|
|
41
|
+
// выбрал именно родной рецепт, а не переносимый.
|
|
42
|
+
{ entry: "todo-without-task", tool: "eslint", files: { "src/app.js": "export const add = (a, b) => a + b;\n" } },
|
|
43
|
+
{ entry: "no-print-in-prod", tool: "eslint", files: { "src/app.js": "export const add = (a, b) => a + b;\n" } },
|
|
44
|
+
{ entry: "swallowed-error", tool: "eslint", files: { "src/app.js": "export const add = (a, b) => a + b;\n" } },
|
|
45
|
+
{ entry: "complexity-limit", tool: "eslint", files: { "src/app.js": "export const add = (a, b) => a + b;\n" } },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const RUFF_ENTRIES = NATIVE.filter((n) => n.tool === "ruff").map((n) => n.entry);
|
|
49
|
+
|
|
50
|
+
// Наличие инструмента спрашивается в НАСТОЯЩЕЙ среде, а не в песочнице проекта. Песочница
|
|
51
|
+
// подменяет HOME, а инструменты, поставленные `pip --user` (vulture, pylint), — это python-
|
|
52
|
+
// скрипты, которые ищут свои модули в $HOME/.local/lib. С чужим HOME импорт падает, и
|
|
53
|
+
// установленный инструмент объявлялся отсутствующим: проверка молча пропускалась вместо того,
|
|
54
|
+
// чтобы что-то проверить. Та же шишка записана в шапке `smoke.sh` про PYTHONUSERBASE.
|
|
55
|
+
const have = (cmd) => spawnSync(cmd, ["--version"], { encoding: "utf8", timeout: 30000 }).status === 0;
|
|
56
|
+
|
|
57
|
+
function declaredCommand(p, name) {
|
|
58
|
+
const man = readFileSync(join(p.dir, ".aqk.yml"), "utf8");
|
|
59
|
+
// Жадно и до КОНЦА СТРОКИ: команда рецепта сама содержит кавычки (`eslint --rule '{"x":1}'`),
|
|
60
|
+
// и нежадный разбор обрывал её на первой внутренней — тест падал на своей же выборке, а
|
|
61
|
+
// винил рецепт.
|
|
62
|
+
const m = man.match(new RegExp(`^\\s*${name}:\\s*"(.*)"\\s*$`, "m"));
|
|
63
|
+
return m ? m[1] : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const name of RUFF_ENTRIES) {
|
|
67
|
+
test(`«${name}»: родной рецепт не краснеет на СВОИХ образцах`, (t) => {
|
|
68
|
+
if (!have("ruff")) return t.skip("ruff не установлен — проверить нечем");
|
|
69
|
+
// Чистый python-файл: находок у пользователя нет по построению, и всё красное может прийти
|
|
70
|
+
// только из образцов, которые положил `add`.
|
|
71
|
+
const p = project(t, { "src/app.py": "def add(a, b):\n return a + b\n" });
|
|
72
|
+
const init = aqk(p, "init");
|
|
73
|
+
assert.equal(init.code, 0, init.out);
|
|
74
|
+
const add = aqk(p, "add", name);
|
|
75
|
+
assert.equal(add.code, 0, add.out);
|
|
76
|
+
|
|
77
|
+
const cmd = declaredCommand(p, name);
|
|
78
|
+
assert.ok(cmd, `в манифесте нет команды для «${name}»`);
|
|
79
|
+
if (!/ruff/.test(cmd)) return t.skip(`выбран не ruff, а «${cmd}» — эта проверка про родной рецепт`);
|
|
80
|
+
|
|
81
|
+
const r = run(p, "bash", ["-c", cmd]);
|
|
82
|
+
assert.equal(r.code, 0,
|
|
83
|
+
`гейт покраснел на проекте без единой находки пользователя — значит выдал наши образцы ` +
|
|
84
|
+
`за его код.\nкоманда: ${cmd}\nвывод:\n${r.out}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// КОНТРОЛЬНАЯ. Заглушить чужой вывод легко — и так же легко заглушить его весь. Проверка выше
|
|
89
|
+
// одна означала бы «гейт молчит», а не «гейт молчит О НАШИХ ОБРАЗЦАХ»: инструмент, лишённый
|
|
90
|
+
// голоса, проходит её идеально. Поэтому тот же проект, но с НАСТОЯЩЕЙ находкой пользователя.
|
|
91
|
+
// Маркер долга собирается ИЗ КУСКОВ намеренно. Написанный целиком, он живёт в исходнике
|
|
92
|
+
// комплекта — и наш собственный `todo-without-task` считает его нашим долгом. Поймано этим же
|
|
93
|
+
// гейтом через минуту после того, как файл был написан.
|
|
94
|
+
const MARKER = ["TO", "DO"].join("");
|
|
95
|
+
for (const [name, file, code] of [
|
|
96
|
+
["todo-without-task", "src/debt.py", `def f():\n # ${MARKER}: переписать\n return 1\n`],
|
|
97
|
+
["no-print-in-prod", "src/loud.py", "def f():\n print('привет')\n return 1\n"],
|
|
98
|
+
]) {
|
|
99
|
+
test(`«${name}»: на находке ПОЛЬЗОВАТЕЛЯ тот же рецепт краснеет`, (t) => {
|
|
100
|
+
if (!have("ruff")) return t.skip("ruff не установлен — проверить нечем");
|
|
101
|
+
const p = project(t, { "src/app.py": "def add(a, b):\n return a + b\n", [file]: code });
|
|
102
|
+
assert.equal(aqk(p, "init").code, 0);
|
|
103
|
+
assert.equal(aqk(p, "add", name).code, 0);
|
|
104
|
+
const cmd = declaredCommand(p, name);
|
|
105
|
+
if (!/ruff/.test(cmd)) return t.skip(`выбран не ruff, а «${cmd}»`);
|
|
106
|
+
const r = run(p, "bash", ["-c", cmd]);
|
|
107
|
+
assert.notEqual(r.code, 0,
|
|
108
|
+
`гейт промолчал о настоящей находке — значит его заглушили целиком.\nкоманда: ${cmd}\nвывод:\n${r.out}`);
|
|
109
|
+
assert.ok(r.out.includes(file.split("/").pop()),
|
|
110
|
+
`гейт покраснел, но не назвал файл ${file}. Вывод:\n${r.out}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Общая проверка по ТАБЛИЦЕ: любой родной рецепт, чей инструмент здесь есть, обязан молчать на
|
|
115
|
+
// проекте, где единственные «нарушения» — образцы, которые положил `add`.
|
|
116
|
+
for (const { entry, tool, files } of NATIVE) {
|
|
117
|
+
test(`«${entry}» (${tool}): родной рецепт молчит о наших образцах`, (t) => {
|
|
118
|
+
if (!have(tool)) return t.skip(`${tool} не установлен — проверить нечем, и это НЕ «проверено»`);
|
|
119
|
+
const p = project(t, files);
|
|
120
|
+
assert.equal(aqk(p, "init").code, 0);
|
|
121
|
+
const add = aqk(p, "add", entry);
|
|
122
|
+
if (add.code !== 0) return t.skip(`запись не ставится здесь: ${add.out.trim().split("\n")[0]}`);
|
|
123
|
+
const cmd = declaredCommand(p, entry);
|
|
124
|
+
assert.ok(cmd, `в манифесте нет команды для «${entry}»`);
|
|
125
|
+
if (!cmd.includes(tool)) return t.skip(`выбран не ${tool}, а «${cmd}» — эта проверка про родной рецепт`);
|
|
126
|
+
const r = run(p, "bash", ["-c", cmd]);
|
|
127
|
+
assert.equal(r.code, 0,
|
|
128
|
+
`гейт покраснел на проекте без единой находки пользователя — значит выдал наши образцы ` +
|
|
129
|
+
`за его код.\nкоманда: ${cmd}\nвывод:\n${r.out}`);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// незачем, а сто коммитов за день перепроверить надо.
|
|
8
8
|
import test from "node:test";
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
|
-
import { probeDue, probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
|
|
10
|
+
import { probeDue, probeState, probeEvery, PROBE_EVERY, blindLines, parseBlind, parseRan } from "../lib/cadence.mjs";
|
|
11
11
|
|
|
12
12
|
test("порог по умолчанию — сто коммитов, и он назван числом, а не спрятан", () => {
|
|
13
13
|
assert.equal(PROBE_EVERY, 100);
|
|
@@ -67,3 +67,39 @@ test("непонятое значение порога — null, а не тих
|
|
|
67
67
|
assert.equal(probeEvery({ probe: "часто" }), null);
|
|
68
68
|
assert.equal(probeEvery({ probe: -5 }), null);
|
|
69
69
|
});
|
|
70
|
+
|
|
71
|
+
// --- отметка несёт ИМЕНА непойманных классов ------------------------------------------
|
|
72
|
+
// ЗАЧЕМ. Отметка хранила одно число — `blind: 1`. Агент в `context` видел «один класс», человек
|
|
73
|
+
// в `doctor` не видел ничего, и чтобы узнать КАКОЙ, надо было снова запускать пробу — ту самую
|
|
74
|
+
// команду, о которой никто не вспоминает. Находка, которую не показали, не предостерегает.
|
|
75
|
+
test("отметка пробы: по строке на слепой класс, в самом горячем файле, где он слеп", () => {
|
|
76
|
+
const recs = [
|
|
77
|
+
{ entry: "swallowed-error", file: "src/a.py", verdict: "blind" },
|
|
78
|
+
{ entry: "swallowed-error", file: "src/b.py", verdict: "blind" },
|
|
79
|
+
{ entry: "no-print-in-prod", file: "src/a.py", verdict: "caught" },
|
|
80
|
+
{ entry: "dead-code", file: "src/b.py", verdict: "unknown" },
|
|
81
|
+
];
|
|
82
|
+
const lines = blindLines(recs);
|
|
83
|
+
// То же правило, что у countProbe: класс, слепой хоть где-то, — непокрыт; «не смогли» — не слеп.
|
|
84
|
+
assert.deepEqual(lines, ["blind-class: swallowed-error src/a.py"]);
|
|
85
|
+
const text = ["at: 5", "blind: 1", "", ...lines, "- src/a.py (fixes: 3)"].join("\n");
|
|
86
|
+
assert.deepEqual(parseBlind(text), [{ slug: "swallowed-error", file: "src/a.py" }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("отметка старого формата и пустая — пустой список, а не падение", () => {
|
|
90
|
+
assert.deepEqual(parseBlind("at: 5\nblind: 1\n"), []);
|
|
91
|
+
assert.deepEqual(parseBlind(""), []);
|
|
92
|
+
assert.deepEqual(parseBlind(null), []);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// «Объявлен сейчас» не значит «поставлен после пробы»: проба и гоняет объявленные гейты.
|
|
96
|
+
// Первая версия так и решила — и на самом комплекте написала «поставлено после пробы» про
|
|
97
|
+
// gate-not-weakened, который стоял ДО неё и в kit/gates/_skip.sh брак пропустил. Самое ценное
|
|
98
|
+
// сообщение («объявлен, но здесь не ловит») подменилось утешительным. Поймано живым прогоном.
|
|
99
|
+
test("отметка помнит, какие гейты проба ПРОГОНЯЛА", () => {
|
|
100
|
+
const text = "at: 5\nblind: 1\nran: smoke gate-not-weakened units\n\nblind-class: x a.sh\n";
|
|
101
|
+
assert.deepEqual([...parseRan(text)].sort(), ["gate-not-weakened", "smoke", "units"]);
|
|
102
|
+
// Старая отметка этой строки не несёт: «не знаем», а не «ничего не было объявлено».
|
|
103
|
+
assert.equal(parseRan("at: 5\nblind: 1\n"), null);
|
|
104
|
+
assert.deepEqual([...parseRan("ran:\n")], []);
|
|
105
|
+
});
|