agent-quality-kit 0.15.0 → 0.16.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 +61 -15
- package/README.ru.md +62 -15
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/operational-gates.md +275 -0
- package/kit/gates/_target.sh +53 -0
- package/kit/gates/ci-actually-fails/check.sh +18 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
- package/kit/gates/entry-commands-exist/check.sh +88 -12
- package/kit/gates/hook-actually-fires/README.md +12 -0
- package/kit/gates/hook-actually-fires/check.sh +66 -6
- package/kit/gates/hook-actually-fires/gate.yml +2 -2
- package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
- package/kit/gates/test-not-adjusted/README.md +31 -0
- package/llms.txt +26 -7
- package/package.json +1 -1
- package/tool/commands/context.mjs +3 -1
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/feedback.mjs +75 -1
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +18 -2
- package/tool/i18n/en-gates.mjs +9 -1
- package/tool/i18n/en.mjs +24 -2
- package/tool/i18n/ru-docs.mjs +17 -2
- package/tool/i18n/ru-gates.mjs +9 -1
- package/tool/i18n/ru.mjs +20 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/core.mjs +42 -6
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +25 -6
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
- package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
- package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
- package/tool/selfcheck/smoke.sh +106 -4
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-level.mjs +41 -1
- package/tool/selfcheck/units-repo.mjs +75 -0
- package/tool/selfcheck/units-vitals.mjs +27 -0
- package/kit/gates/entry-links-exist/README.md +0 -27
- package/kit/gates/entry-links-exist/check.sh +0 -33
- package/kit/gates/entry-links-exist/gate.yml +0 -17
- package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
- package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
- package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
- package/kit/gates/no-phantom-package/README.md +0 -84
- package/kit/gates/no-phantom-package/check.sh +0 -168
- package/kit/gates/no-phantom-package/gate.yml +0 -20
- package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
- package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// Обратная сторона нашего же принципа: молчание неотличимо не только от успеха, но и от отказа.
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
9
|
+
import { readFileSync, writeFileSync, rmSync, chmodSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { project, aqk, aqkEnv } from "./_fixture.mjs";
|
|
11
|
+
import { project, aqk, aqkEnv, tail } from "./_fixture.mjs";
|
|
12
|
+
|
|
13
|
+
const plainText = (s) => String(s).replace(/\x1b\[[0-9;]*m/g, "");
|
|
12
14
|
|
|
13
15
|
test("прогон называет свой вердикт словами в обоих исходах", (t) => {
|
|
14
16
|
const p = project(t, { "src/a.py": "def s():\n return 1\n" });
|
|
@@ -30,14 +32,14 @@ test("прогон называет свой вердикт словами в о
|
|
|
30
32
|
const red = aqk(p, "doctor", "--run");
|
|
31
33
|
// Сообщение утверждения обязано нести ВЕСЬ хвост: проверка, которая говорит только «не
|
|
32
34
|
// совпало», отправляет читателя гадать — ровно то, за что мы ругаем чужие проверки.
|
|
33
|
-
const redTail = red.out
|
|
35
|
+
const redTail = tail(red.out);
|
|
34
36
|
assert.notEqual(red.code, 0, `прогон без .gitignore прошёл зелёным:\n${redTail}`);
|
|
35
37
|
assert.match(redTail, /красн/, `вердикт не назван в конце вывода (код ${red.code}):\n${redTail}`);
|
|
36
38
|
|
|
37
39
|
// Причина устранена — и «ничего не сказал» обязано отличаться от «всё проверено».
|
|
38
40
|
writeFileSync(join(p.dir, ".gitignore"), "x\n", "utf8");
|
|
39
41
|
const green = aqk(p, "doctor", "--run");
|
|
40
|
-
const greenTail = green.out
|
|
42
|
+
const greenTail = tail(green.out);
|
|
41
43
|
assert.equal(green.code, 0, `прогон остался красным:\n${greenTail}`);
|
|
42
44
|
assert.match(greenTail, /зелён/, `успех не назван словами:\n${greenTail}`);
|
|
43
45
|
});
|
|
@@ -126,3 +128,47 @@ test("в GitHub Actions упавший гейт даёт пометку у ст
|
|
|
126
128
|
const local = aqkEnv(p, { GITHUB_ACTIONS: "" }, "doctor", "--run");
|
|
127
129
|
assert.doesNotMatch(local.out, /^::error/m, "вне GitHub Actions пометок нет");
|
|
128
130
|
});
|
|
131
|
+
|
|
132
|
+
// ОТЧЁТ — ЭТО ЧТЕНИЕ, И ПАДАТЬ ЕМУ НЕ НА ЧЕМ.
|
|
133
|
+
//
|
|
134
|
+
// Разбор чужой интеграции 2026-09-16: `aqk report` в рабочей области, где `.aqk/` не создать,
|
|
135
|
+
// упал с EROFS — не напечатав ничего. Команда, которая ТОЛЬКО рассказывает о состоянии,
|
|
136
|
+
// перестала работать из-за побочного действия, о котором её не просили. Такие рабочие области
|
|
137
|
+
// бывают не по недосмотру: read-only контейнер, чужой CI, каталог под ревью.
|
|
138
|
+
//
|
|
139
|
+
// Отчёт обязан уехать в stdout, а невозможность сохранить — быть названной, а не проглоченной:
|
|
140
|
+
// человек, увидевший отчёт, вправе думать, что файл на диске, если ему не сказали иначе.
|
|
141
|
+
// ПРОПУСК НА WINDOWS — С ПРИЧИНОЙ, А НЕ МОЛЧА. Там POSIX-права ничего не запрещают: каталог
|
|
142
|
+
// с `chmod 0555` остаётся записываемым, отчёт сохраняется, и проверка краснеет на исправном
|
|
143
|
+
// коде. Настоящий read-only там делается через `icacls` и ACL — это оснастка дороже самой
|
|
144
|
+
// проверки. Пропуск назван вслух: «не смогли проверить» и «проверено» — разные слова, и
|
|
145
|
+
// раннер печатает SKIP с этой причиной.
|
|
146
|
+
test("report печатает отчёт и тогда, когда сохранить его некуда", (t) => {
|
|
147
|
+
// Пропуск через `t.skip`, а НЕ третьим аргументом `test(name, {skip}, fn)`. Форма с объектом
|
|
148
|
+
// законна в Node, но меняет сигнатуру вызова, и арбитр подгонки тестов прочитал это как
|
|
149
|
+
// «тест исчез» (checkwash, TEST_DISABLED). Спорить с чужим инструментом здесь нечем и незачем:
|
|
150
|
+
// привычная форма стоит дешевле, чем подавление находки, которое пришлось бы объяснять вечно.
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
return t.skip("chmod 0555 на Windows прав не отнимает — каталог остаётся записываемым, проверить нечем");
|
|
153
|
+
}
|
|
154
|
+
const p = project(t, {
|
|
155
|
+
"AGENTS.md": "# вход\n",
|
|
156
|
+
".aqk.yml": 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n',
|
|
157
|
+
"src/a.js": "export const a = 1;\n",
|
|
158
|
+
});
|
|
159
|
+
// Права возвращаются ЗДЕСЬ, а не через `t.after`: уборка фикстуры зарегистрирована раньше
|
|
160
|
+
// нашей и сносит каталог первой — на каталоге 0555 её `rmSync` падает сам, и проверка
|
|
161
|
+
// краснеет не тем, что проверяет. Поймано первым же прогоном.
|
|
162
|
+
let r;
|
|
163
|
+
chmodSync(p.dir, 0o555);
|
|
164
|
+
try { r = aqk(p, "report"); } finally { chmodSync(p.dir, 0o755); }
|
|
165
|
+
const out = plainText(r.out);
|
|
166
|
+
// Ищется НЕОБРАБОТАННОЕ падение — трассировка Node, — а не слово «EACCES»: код ошибки в
|
|
167
|
+
// сообщении «сохранить не смогли (EACCES)» стоит законно и человеку нужен. Первая версия
|
|
168
|
+
// этой проверки запрещала само слово и краснела на исправном выводе.
|
|
169
|
+
assert.doesNotMatch(out, /node:internal|\bat Object\.|\bthrow err\b/,
|
|
170
|
+
`команда упала вместо того, чтобы напечатать отчёт:\n${out.slice(0, 600)}`);
|
|
171
|
+
assert.match(out, /aqk report|Отчёт|отчёт/i, `отчёта в выводе нет:\n${out.slice(0, 600)}`);
|
|
172
|
+
assert.match(out, /сохранить|не записан|read-only|только для чтения/i,
|
|
173
|
+
`отчёт не сохранён, и об этом не сказано — человек решит, что файл на диске:\n${out.slice(-600)}`);
|
|
174
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// tool/selfcheck/smoke/version-sync.test.mjs — ВЕРСИЯ ОБЪЯВЛЕНА В ПЯТИ МЕСТАХ, И ОНИ ОБЯЗАНЫ
|
|
2
|
+
// СОВПАДАТЬ.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ. Выпуск поднимает номер руками в `package.json`, обоих README (`rev:` для pre-commit и
|
|
5
|
+
// `uses: …@vX` для действия), `llms.txt` и манифесте плагина. Пять мест, одно знание, и ни одно
|
|
6
|
+
// из них до сегодня не сторожила машина: забытая строка в README означает, что человек закрепит
|
|
7
|
+
// `v0.14.0` на день после выхода 0.15.0 и не получит ни одной правки, а забытая строка в манифесте
|
|
8
|
+
// плагина — что витрина Claude Code раздаёт версию, которой уже нет.
|
|
9
|
+
//
|
|
10
|
+
// Ровно тот класс, ради которого написан весь комплект: утверждение («это версия 0.15.0»), которое
|
|
11
|
+
// никто не проверяет. Найдено 2026-09-14, когда к четырём местам добавилось пятое.
|
|
12
|
+
//
|
|
13
|
+
// ПОЧЕМУ НЕ «ЛЮБОЕ ЧИСЛО ВИДА vX.Y.Z». Ищем только строки, которые ОБЕЩАЮТ ТЕКУЩУЮ версию:
|
|
14
|
+
// закрепление в примере установки. Номера в журнале изменений — это история, они обязаны
|
|
15
|
+
// оставаться прежними, и требовать от них свежести значило бы переписывать прошлое.
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
24
|
+
const read = (rel) => readFileSync(join(ROOT, rel), "utf8");
|
|
25
|
+
|
|
26
|
+
const VERSION = JSON.parse(read("package.json")).version;
|
|
27
|
+
|
|
28
|
+
// Каждое место — файл и то, чем из него достаётся ОБЕЩАНИЕ версии. Список здесь, а не в коде
|
|
29
|
+
// выпуска: список, который знает только тот, кто выпускает, живёт в его памяти, а не в машине.
|
|
30
|
+
const PLACES = [
|
|
31
|
+
{ file: "README.md", why: "закрепление pre-commit и действия в примере установки" },
|
|
32
|
+
{ file: "README.ru.md", why: "то же по-русски" },
|
|
33
|
+
{ file: "llms.txt", why: "выжимка для агента: `uses: …@vX.Y.Z`" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
test(`версия в package.json — ${VERSION}, и она непустая`, () => {
|
|
37
|
+
assert.match(VERSION, /^\d+\.\d+\.\d+$/, `странный номер версии: ${VERSION}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
for (const { file, why } of PLACES) {
|
|
41
|
+
test(`«${file}» обещает текущую версию (${why})`, () => {
|
|
42
|
+
const text = read(file);
|
|
43
|
+
const pinned = [...text.matchAll(/v(\d+\.\d+\.\d+)/g)].map((m) => m[1]);
|
|
44
|
+
assert.ok(pinned.length, `в ${file} нет ни одного закрепления вида vX.Y.Z — пример установки исчез?`);
|
|
45
|
+
const stale = [...new Set(pinned.filter((v) => v !== VERSION))];
|
|
46
|
+
assert.equal(stale.length, 0,
|
|
47
|
+
`${file} закрепляет ${stale.join(", ")}, а пакет уже ${VERSION}: человек, скопировавший ` +
|
|
48
|
+
`пример, получит старую версию и ни одной правки из новой`);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Манифест плагина — пятое место, и самое тихое: его читает витрина Claude Code, а не человек.
|
|
53
|
+
// Расхождение здесь никто не заметит глазами.
|
|
54
|
+
test("манифест плагина обещает ту же версию", () => {
|
|
55
|
+
const man = JSON.parse(read("plugin/.claude-plugin/plugin.json"));
|
|
56
|
+
assert.equal(man.version, VERSION,
|
|
57
|
+
`плагин объявляет ${man.version}, пакет ${VERSION}: витрина будет раздавать версию, ` +
|
|
58
|
+
"которой в npm уже нет. Обновления у плагина приходят только по смене этого поля");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Плагин — тонкая обёртка над той же программой, и она не имеет права разойтись с ней. Если
|
|
62
|
+
// когда-нибудь здесь появится собственная логика, эта проверка о ней скажет.
|
|
63
|
+
test("плагин остаётся обёрткой: команды зовут ту же программу из реестра", () => {
|
|
64
|
+
const hooks = read("plugin/hooks/hooks.json");
|
|
65
|
+
assert.match(hooks, /agent-quality-kit/, "хук плагина зовёт не наш пакет");
|
|
66
|
+
assert.doesNotMatch(hooks, /node\s+\//, "в хук плагина попал абсолютный путь — у чужого его нет");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// РАЗМЕР ПАКЕТА — ЧИСЛО, КОТОРОЕ ДО СЕГОДНЯ НЕ СТОРОЖИЛ НИКТО, и оно врало трижды: «230 КБ» в
|
|
70
|
+
// core.mjs при настоящих 692, «574.1 kB» в обоих README при тех же 692. В самом README рядом
|
|
71
|
+
// стояла честная приписка «это число не сторожит никто» — вот она и есть признание, что
|
|
72
|
+
// утверждение без арбитра живёт ровно до первой правки.
|
|
73
|
+
//
|
|
74
|
+
// СВЕРЯЕТСЯ ОКРУГЛЁННОЕ до десятой доли мегабайта: точный байт меняется от версии npm и от
|
|
75
|
+
// сжатия, и требовать его совпадения значило бы получить красное на пустом месте. А вот переход
|
|
76
|
+
// через десятую долю — это уже рост, о котором человек обязан узнать и либо признать его в
|
|
77
|
+
// тексте, либо остановить.
|
|
78
|
+
test("обещанный в README размер пакета совпадает с настоящим", () => {
|
|
79
|
+
// WINDOWS, ДВА ШАГА, И ОБА ПОЙМАЛ КОНВЕЙЕР В ДЕНЬ НАПИСАНИЯ ПРОВЕРКИ. Сперва `npm` дал
|
|
80
|
+
// ENOENT — там программа называется `npm.cmd`. Потом `npm.cmd` дал EINVAL: Node отказывается
|
|
81
|
+
// запускать `.cmd` без оболочки, это его защита от подстановки через аргументы. Значит
|
|
82
|
+
// оболочка нужна, и она здесь безопасна: доводы постоянные, снаружи в них ничего не приходит.
|
|
83
|
+
// Тот же класс, что записан у нас про `bash` в execution.mjs, — и снова на Linux его не видно.
|
|
84
|
+
const win = process.platform === "win32";
|
|
85
|
+
const out = execFileSync(win ? "npm.cmd" : "npm", ["pack", "--dry-run", "--json"],
|
|
86
|
+
{ cwd: ROOT, encoding: "utf8", shell: win });
|
|
87
|
+
const bytes = JSON.parse(out)[0].size;
|
|
88
|
+
const real = (Math.round(bytes / 1024 / 1024 * 10) / 10).toFixed(1);
|
|
89
|
+
for (const [file, re] of [["README.md", /≈([\d.]+) MB/], ["README.ru.md", /≈([\d,]+) МБ/]]) {
|
|
90
|
+
const m = re.exec(read(file));
|
|
91
|
+
assert.ok(m, `в ${file} нет обещания размера пакета — строка «≈… МБ» пропала`);
|
|
92
|
+
const said = m[1].replace(",", ".");
|
|
93
|
+
assert.equal(said, real,
|
|
94
|
+
`${file} обещает ≈${said} МБ, а пакет весит ${real} МБ (${bytes} байт). ` +
|
|
95
|
+
"Либо поправьте текст, либо остановите рост — молча расходиться этим двум нельзя");
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// УСТАНОВКА ПЛАГИНА — ЕЩЁ ОДНО ОБЕЩАНИЕ БЕЗ АРБИТРА, и оно уже соврало. Найдено 2026-09-14:
|
|
100
|
+
// `plugin/README.md` — файл, который человек видит первым, открыв папку плагина на GitHub, —
|
|
101
|
+
// велел `/plugin marketplace add anthropics/claude-plugins-community` и
|
|
102
|
+
// `/plugin install aqk@claude-community`. В той витрине нас нет: заявка туда даже не подана.
|
|
103
|
+
// То есть пришедший по нашей же ссылке получал бы «плагин не найден» — и уходил, решив, что
|
|
104
|
+
// сломан инструмент, а не строчка в документации.
|
|
105
|
+
//
|
|
106
|
+
// Класс тот же самый, ради которого написан комплект: утверждение, выглядящее рабочим, потому
|
|
107
|
+
// что его никто не пробовал. Здесь его держит машина: витрина названа в `marketplace.json`,
|
|
108
|
+
// и всякая напечатанная команда установки обязана называть ЕЁ.
|
|
109
|
+
test("всякая напечатанная команда установки ведёт в существующую витрину", () => {
|
|
110
|
+
const market = JSON.parse(read(".claude-plugin/marketplace.json"));
|
|
111
|
+
const names = market.plugins.map((p) => p.name);
|
|
112
|
+
const repo = "arsen-ask-lx/Agent_Quality_Kit";
|
|
113
|
+
|
|
114
|
+
// Перебираем ТО, ЧТО ЛЕЖИТ В РЕПОЗИТОРИИ, а не заранее выписанный список файлов: список
|
|
115
|
+
// пришлось бы помнить человеку, а забытый файл — это ровно тот файл, который соврёт.
|
|
116
|
+
//
|
|
117
|
+
// КРОМЕ ЗАПИСЕЙ О ПРОШЛОМ. Журнал шишек и разборы ЦИТИРУЮТ неверную команду — в этом их работа:
|
|
118
|
+
// запись от 2026-09-14 приводит дословно ту строку, которая вела в чужую витрину. Требовать от
|
|
119
|
+
// истории свежести значит переписывать её, и это тот же довод, что абзацем выше про номера
|
|
120
|
+
// версий в журнале изменений. Здесь проверяется то, что читатель воспримет как УКАЗАНИЕ.
|
|
121
|
+
const HISTORY = ["incidents/", "research/", "CHANGELOG.md"];
|
|
122
|
+
const files = execFileSync("git", ["ls-files", "-z", "*.md", "*.txt"], { cwd: ROOT, encoding: "utf8" })
|
|
123
|
+
.split("\0").filter(Boolean)
|
|
124
|
+
.filter((f) => !HISTORY.some((h) => f.startsWith(h)));
|
|
125
|
+
|
|
126
|
+
const bad = [];
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
const text = read(f);
|
|
129
|
+
for (const m of text.matchAll(/\/plugin\s+marketplace\s+add\s+([\w./-]+)/g)) {
|
|
130
|
+
if (m[1] !== repo) bad.push(`${f}: витрина «${m[1]}», а наша — ${repo}`);
|
|
131
|
+
}
|
|
132
|
+
for (const m of text.matchAll(/\/plugin\s+install\s+([\w.-]+)@([\w.-]+)/g)) {
|
|
133
|
+
if (!names.includes(m[1])) bad.push(`${f}: плагин «${m[1]}», а в витрине ${names.join(", ")}`);
|
|
134
|
+
if (m[2] !== market.name) bad.push(`${f}: витрина «${m[2]}», а называется она «${market.name}»`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
assert.equal(bad.length, 0,
|
|
138
|
+
"команда установки ведёт туда, где нас нет — пришедший получит «плагин не найден»:\n" +
|
|
139
|
+
bad.map((b) => " · " + b).join("\n"));
|
|
140
|
+
});
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -44,6 +44,62 @@ bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}";
|
|
|
44
44
|
# потому что памятку третий раз забудут ровно так же, как забыли второй.
|
|
45
45
|
node_in() { D="$1"; shift; ( cd "$D" && node "$@" ); }
|
|
46
46
|
|
|
47
|
+
# ПРЕДПОЛЁТ: МОЖНО ЛИ ВООБЩЕ МЕРИТЬ.
|
|
48
|
+
#
|
|
49
|
+
# Чужой разбор 2026-09-16: этот файл, запущенный в песочнице Codex, дал 15 падений из 123.
|
|
50
|
+
# Настоящая причина — дочерним процессам Node запрещалось запускать git и npm, `spawnSync`
|
|
51
|
+
# возвращал EPERM. Проверки об этом не знали и печатали обычные кресты; человек час разбирал
|
|
52
|
+
# несуществующие дефекты, а «дефекты AQK 0.15.0» уехало в чужой отчёт.
|
|
53
|
+
#
|
|
54
|
+
# ИРОНИЯ, РАДИ КОТОРОЙ ЭТО НАПИСАНО. `tool/lib/execution.mjs` различает три исхода —
|
|
55
|
+
# clean · finding · infra_error — ровно затем, чтобы сбой инструмента не выдавался за приговор
|
|
56
|
+
# коду. Своей собственной оснастке мы это правило не применили.
|
|
57
|
+
#
|
|
58
|
+
# ОТКАЗ ЦЕЛИКОМ, А НЕ ЧАСТИЧНЫЙ ПРОГОН. 108 зелёных из 123 при недоступном git — это не «почти
|
|
59
|
+
# всё хорошо», это «мерить было нечем»; такое число вводит в заблуждение сильнее, чем молчание.
|
|
60
|
+
# Код возврата 3 — тем же pytest отделяет INTERNAL_ERROR от провала теста (у него провал это 1).
|
|
61
|
+
#
|
|
62
|
+
# СПРАШИВАЕМ ТОГО ЖЕ, КОГО СПРОСИТ ПРОГОН. Мало проверить git в этом шелле: ломалось именно у
|
|
63
|
+
# подпроцессов Node, и предполёт, смотрящий только на шелл, прошёл бы, а кресты пришли следом.
|
|
64
|
+
preflight() {
|
|
65
|
+
WHY=""
|
|
66
|
+
PT="$(mktemp -d 2>/dev/null)" || WHY="mktemp не создаёт временный каталог"
|
|
67
|
+
if [ -z "$WHY" ]; then
|
|
68
|
+
git init -q "$PT" >/dev/null 2>&1 || WHY="git init не отработал — без него большинство проверок меряет пустоту"
|
|
69
|
+
rm -rf "$PT"
|
|
70
|
+
fi
|
|
71
|
+
if [ -z "$WHY" ]; then
|
|
72
|
+
#
|
|
73
|
+
# ЧЕРЕЗ ОБОЛОЧКУ НА WINDOWS. Поймано конвейером в первый же прогон: `npm` там — это
|
|
74
|
+
# `npm.cmd`, и `spawnSync("npm")` без оболочки даёт ENOENT на исправной машине. Предполёт
|
|
75
|
+
# отказал на windows-задании, то есть сам стал тем ложным красным, против которого написан.
|
|
76
|
+
# Приём в комплекте уже дважды применён — `vitals.mjs` (shell при win32) и
|
|
77
|
+
# `smoke/version-sync.test.mjs` (`npm.cmd`); здесь он был потерян.
|
|
78
|
+
ERR=$(node -e '
|
|
79
|
+
const { spawnSync } = require("node:child_process");
|
|
80
|
+
const win = process.platform === "win32";
|
|
81
|
+
for (const p of ["git", "npm"]) {
|
|
82
|
+
const r = spawnSync(p, ["--version"], { encoding: "utf8", shell: win });
|
|
83
|
+
if (r.error || r.status !== 0) {
|
|
84
|
+
console.error(p + ": " + (r.error ? r.error.code : "код " + r.status));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
' 2>&1) || WHY="из подпроцесса Node не запускается то, чем мы меряем — $ERR"
|
|
89
|
+
fi
|
|
90
|
+
[ -z "$WHY" ] && return 0
|
|
91
|
+
printf '\n \033[33m~\033[0m прогон невозможен: %s\n' "$WHY"
|
|
92
|
+
printf ' Это НЕ «проверки упали»: мерить было нечем, и частичный прогон ничего не доказывает.\n'
|
|
93
|
+
printf ' Код возврата 3 — «не смогли проверить», как INTERNAL_ERROR у pytest.\n\n'
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
preflight || exit 3
|
|
98
|
+
if [ "${1:-}" = "--preflight-only" ]; then
|
|
99
|
+
printf ' предполёт пройден: git и npm доступны и шеллу, и подпроцессам Node\n'
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
47
103
|
WORK="$(mktemp -d)"
|
|
48
104
|
trap 'rm -rf "$WORK"' EXIT
|
|
49
105
|
|
|
@@ -101,7 +157,7 @@ fi
|
|
|
101
157
|
for f in .aqk.yml AGENTS.md CLAUDE.md; do
|
|
102
158
|
[ -f "$f" ] || bad "не создан обязательный файл" "$f"
|
|
103
159
|
done
|
|
104
|
-
for n in general.md index.md project-baseline.md; do
|
|
160
|
+
for n in general.md index.md project-baseline.md operational-gates.md; do
|
|
105
161
|
find .aqk -type f -name "$n" | grep -q . || bad "не создан обязательный файл" "$n"
|
|
106
162
|
done
|
|
107
163
|
ok "обязательные файлы на месте"
|
|
@@ -1706,6 +1762,52 @@ else
|
|
|
1706
1762
|
fi
|
|
1707
1763
|
rm -rf "$VITP"
|
|
1708
1764
|
|
|
1765
|
+
# --- 112. vitals: чужой хук не выдаётся за подключённый AQK -------------------
|
|
1766
|
+
# Разбор живой интеграции 2026-09-16: в репозитории стоял диспетчер фреймворка pre-commit, AQK
|
|
1767
|
+
# из него не вызывался ни разу, а `vitals` печатал «хук pre-commit прописан в .git/hooks».
|
|
1768
|
+
# Человек прочитал это как «обвязка на месте». Проверялось ИМЯ в теле хука (`/pre-commit|aqk/i`),
|
|
1769
|
+
# а слово «pre-commit» у такого хука есть всегда. Наш собственный класс «объявлено ≠ работает».
|
|
1770
|
+
#
|
|
1771
|
+
# Проверяются три конца сразу, иначе починка соврёт в обратную сторону: чужой хук; наш хук,
|
|
1772
|
+
# подключённый через `.pre-commit-config.yaml` (там в .git/hooks лежит диспетчер, а решает
|
|
1773
|
+
# конфиг); прямой вызов в теле хука.
|
|
1774
|
+
HKP="$(mktemp -d)"
|
|
1775
|
+
( cd "$HKP" && git init -q . && printf '# вход
|
|
1776
|
+
' > AGENTS.md &&
|
|
1777
|
+
printf 'aqk: 1
|
|
1778
|
+
entry: [AGENTS.md]
|
|
1779
|
+
lang: ru
|
|
1780
|
+
gates:
|
|
1781
|
+
ok: "true"
|
|
1782
|
+
' > .aqk.yml &&
|
|
1783
|
+
printf '#!/usr/bin/env bash
|
|
1784
|
+
# File generated by pre-commit: https://pre-commit.com
|
|
1785
|
+
ARGS=(hook-impl --hook-type=pre-commit)
|
|
1786
|
+
' > .git/hooks/pre-commit &&
|
|
1787
|
+
chmod +x .git/hooks/pre-commit ) >/dev/null 2>&1
|
|
1788
|
+
HK_OTHER=$( cd "$HKP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
|
|
1789
|
+
( cd "$HKP" && printf 'repos:
|
|
1790
|
+
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
1791
|
+
rev: v0.15.0
|
|
1792
|
+
hooks:
|
|
1793
|
+
- id: aqk
|
|
1794
|
+
' > .pre-commit-config.yaml )
|
|
1795
|
+
HK_OURS=$( cd "$HKP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
|
|
1796
|
+
( cd "$HKP" && rm -f .pre-commit-config.yaml &&
|
|
1797
|
+
printf '#!/usr/bin/env bash
|
|
1798
|
+
exec aqk doctor --run --min 1 --brief
|
|
1799
|
+
' > .git/hooks/pre-commit )
|
|
1800
|
+
HK_DIRECT=$( cd "$HKP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
|
|
1801
|
+
if printf '%s' "$HK_OTHER" | grep -q "AQK в нём не вызывается" &&
|
|
1802
|
+
! printf '%s' "$HK_OURS" | grep -q "AQK в нём не вызывается" &&
|
|
1803
|
+
! printf '%s' "$HK_DIRECT" | grep -q "AQK в нём не вызывается"; then
|
|
1804
|
+
ok "vitals: чужой хук назван чужим, наш — нашим, и оба пути подключения сочтены"
|
|
1805
|
+
else
|
|
1806
|
+
bad "vitals путает чужой хук с подключённым AQK" \
|
|
1807
|
+
"чужой: $(printf '%s' "$HK_OTHER" | grep -c 'не вызывается'), через конфиг: $(printf '%s' "$HK_OURS" | grep -c 'не вызывается'), прямой: $(printf '%s' "$HK_DIRECT" | grep -c 'не вызывается')"
|
|
1808
|
+
fi
|
|
1809
|
+
rm -rf "$HKP"
|
|
1810
|
+
|
|
1709
1811
|
# --- 106. совещательный гейт назван всегда и не роняет прогон никогда ---------
|
|
1710
1812
|
# Два отказа, найденные 2026-09-09 сверкой документации с кодом. ПЕРВЫЙ: зелёный совещательный
|
|
1711
1813
|
# печатался обычной галочкой — гейт, который уронить сборку НЕ МОЖЕТ, по выводу неотличим от
|
|
@@ -1784,7 +1886,7 @@ else
|
|
|
1784
1886
|
fi
|
|
1785
1887
|
rm -rf "$PRVP"
|
|
1786
1888
|
|
|
1787
|
-
# --- 104. опись в
|
|
1889
|
+
# --- 104. опись в MAP.md покрывает все исходники ------------------------------
|
|
1788
1890
|
# AGENTS.md — первое, что читает агент, и раздел «Где что лежит» для него карта. Карта,
|
|
1789
1891
|
# отставшая от дерева, хуже её отсутствия: агент уверен, что видел всё. Проверено 2026-09-09 —
|
|
1790
1892
|
# в описи не было восьми файлов из четырнадцати добавленных за неделю (`brief.mjs`, `banner.mjs`
|
|
@@ -1793,12 +1895,12 @@ rm -rf "$PRVP"
|
|
|
1793
1895
|
MISSING=""
|
|
1794
1896
|
for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck/* "$ROOT"/tool/selfcheck/*/*; do
|
|
1795
1897
|
B=$(basename "$F")
|
|
1796
|
-
grep -qF "$B" "$ROOT/
|
|
1898
|
+
grep -qF "$B" "$ROOT/MAP.md" || MISSING="$MISSING $B"
|
|
1797
1899
|
done
|
|
1798
1900
|
if [ -z "$MISSING" ]; then
|
|
1799
1901
|
ok "опись «Где что лежит» называет каждый исходник"
|
|
1800
1902
|
else
|
|
1801
|
-
bad "
|
|
1903
|
+
bad "MAP.md не называет файлы" "$MISSING"
|
|
1802
1904
|
fi
|
|
1803
1905
|
|
|
1804
1906
|
# --- 105. два README не расходятся структурно --------------------------------
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// рядом с инструментом — нормализующим адаптером, а протокол остаётся простым.
|
|
18
18
|
import test from "node:test";
|
|
19
19
|
import assert from "node:assert/strict";
|
|
20
|
-
import { classify, findingCodes, gitBash, launchable, gateCommand } from "../lib/execution.mjs";
|
|
20
|
+
import { classify, findingCodes, gitBash, launchable, gateCommand, gateTimeout, GATE_TIMEOUT_DEFAULT } from "../lib/execution.mjs";
|
|
21
21
|
|
|
22
22
|
// Вход — то, что отдаёт spawnSync: { status, signal, error }.
|
|
23
23
|
const R = (over = {}) => ({ status: 0, signal: null, error: undefined, ...over });
|
|
@@ -163,3 +163,39 @@ test("Windows: команда гейта со словом bash отвечает
|
|
|
163
163
|
assert.equal(r.status, 0, r.stderr);
|
|
164
164
|
assert.match(r.stdout, /MINGW|MSYS/, `ответил не Git Bash: ${r.stdout}`);
|
|
165
165
|
});
|
|
166
|
+
|
|
167
|
+
// СКОЛЬКО ЖДАТЬ ЧУЖУЮ КОМАНДУ — ОДНО ЗНАНИЕ, А НЕ ПЯТЬ КОПИЙ.
|
|
168
|
+
//
|
|
169
|
+
// До 2026-09-16 число 300000 было вписано в четырёх местах: run.mjs, prove.mjs и дважды в
|
|
170
|
+
// gates.mjs. Это не похожие строки, а одно знание в четырёх файлах, и правится оно по одному.
|
|
171
|
+
//
|
|
172
|
+
// СПОСОБ РЕШИТЬ ИНАЧЕ ОБЯЗАН БЫТЬ. Он записан в соседнем файле про выбор bash: «у всего, что мы
|
|
173
|
+
// решаем сами, обязан быть способ решить иначе». У таймаута его не было, и живой проект с
|
|
174
|
+
// verify-прогоном длиннее пяти минут просто выкинул свою главную проверку из манифеста.
|
|
175
|
+
//
|
|
176
|
+
// ПЯТЬ МИНУТ ПО УМОЛЧАНИЮ НЕ ВЫДУМАНЫ: у SonarQube SONAR_QUALITY_GATE_TIMEOUT ровно 300 секунд.
|
|
177
|
+
// Переопределение переменной среды — тоже их способ, и GitLab делает так же
|
|
178
|
+
// (RUNNER_AFTER_SCRIPT_TIMEOUT). Поля «timeout» у команды нет ни у lefthook, ни у pre-commit,
|
|
179
|
+
// поэтому в манифест мы его не заводим: соглашения нет, а просьба была одна.
|
|
180
|
+
test("без переменной среды — прежние пять минут", () => {
|
|
181
|
+
assert.equal(gateTimeout({}).ms, GATE_TIMEOUT_DEFAULT);
|
|
182
|
+
assert.equal(GATE_TIMEOUT_DEFAULT, 300000);
|
|
183
|
+
assert.equal(gateTimeout({ AQK_GATE_TIMEOUT: "" }).ms, GATE_TIMEOUT_DEFAULT);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("переменная задаёт срок в секундах", () => {
|
|
187
|
+
assert.equal(gateTimeout({ AQK_GATE_TIMEOUT: "900" }).ms, 900000);
|
|
188
|
+
assert.equal(gateTimeout({ AQK_GATE_TIMEOUT: " 60 " }).ms, 60000);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// МУСОР НЕ ПРЕВРАЩАЕТСЯ В «БЕЗ ПРЕДЕЛА». Ноль или буквы в NaN дали бы spawnSync поведение
|
|
192
|
+
// «ждать вечно» — то есть висящий гейт вместо честного «не смогли проверить». И молчать об
|
|
193
|
+
// этом нельзя: человек задал переменную и ждёт от неё действия.
|
|
194
|
+
test("мусор в переменной — прежний срок и слово об этом, а не вечное ожидание", () => {
|
|
195
|
+
for (const bad of ["abc", "0", "-5", "NaN", "1e999"]) {
|
|
196
|
+
const t = gateTimeout({ AQK_GATE_TIMEOUT: bad });
|
|
197
|
+
assert.equal(t.ms, GATE_TIMEOUT_DEFAULT, `«${bad}» изменило срок`);
|
|
198
|
+
assert.equal(t.ok, false, `«${bad}» принято за исправное значение`);
|
|
199
|
+
assert.equal(t.raw, bad);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import test from "node:test";
|
|
11
11
|
import assert from "node:assert/strict";
|
|
12
12
|
import { commandFor, verdict } from "../lib/prove.mjs";
|
|
13
|
-
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
13
|
+
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines, gateRequires } from "../lib/manifest.mjs";
|
|
14
14
|
import { pickLang, langFromText, langFromDocs } from "../i18n/index.mjs";
|
|
15
15
|
import { progress, selectGates } from "../lib/run.mjs";
|
|
16
16
|
|
|
@@ -415,3 +415,43 @@ test("выбор гейтов: без флагов гоняется всё, ка
|
|
|
415
415
|
assert.equal(s.run.length, 2);
|
|
416
416
|
assert.deepEqual(s.skipped, []);
|
|
417
417
|
});
|
|
418
|
+
|
|
419
|
+
// ЧЕМ ГЕЙТ РАБОТАЕТ — МОЖЕТ СКАЗАТЬ И САМ ПРОЕКТ, НЕ ТОЛЬКО ЗАПИСЬ КАТАЛОГА.
|
|
420
|
+
//
|
|
421
|
+
// Разбор чужой интеграции 2026-09-16: гейт объявлен как
|
|
422
|
+
// `docker run --rm … promtool test rules …`. `vitals` смотрит ПЕРВОЕ СЛОВО, находит `docker` и
|
|
423
|
+
// говорит «инструменты на месте». Поле `requires` для такого случая у нас уже есть — но
|
|
424
|
+
// читалось оно только из `<samples>/<гейт>/gate.yml`, то есть было доступно нашим записям и
|
|
425
|
+
// недоступно гейтам проекта. У проекта с чужими командами `samples` пуст по построению, и
|
|
426
|
+
// сказать «этому гейту нужен docker» было нечем.
|
|
427
|
+
//
|
|
428
|
+
// СНАРУЖИ СОГЛАШЕНИЯ НЕТ — проверено 2026-09-16. У pre-commit ровно эта просьба (issue #2042:
|
|
429
|
+
// трактовать `additional_dependencies` как список программ в $PATH и пропускать хук, если
|
|
430
|
+
// программы нет) закрыта нерешённой: `system`-хуки окружения не ставят. У lefthook такого поля
|
|
431
|
+
// нет вовсе. Поэтому мы не копируем чужую форму, а распространяем СВОЮ, уже существующую, —
|
|
432
|
+
// и это сказано вслух, а не выдано за общепринятое.
|
|
433
|
+
test("requires в манифесте проекта называет программу гейта", async () => {
|
|
434
|
+
const man = parseManifest('gates:\n rules: "docker run x"\nrequires:\n rules: docker\n');
|
|
435
|
+
assert.deepEqual(await gateRequires(man, "", "rules", () => false), ["docker"]);
|
|
436
|
+
assert.equal(await gateRequires(man, "", "rules", () => true), null, "программа на месте — жаловаться не на что");
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("requires принимает и список, и перечисление через запятую", async () => {
|
|
440
|
+
const list = parseManifest('gates:\n g: "x"\nrequires:\n g: [docker, jq]\n');
|
|
441
|
+
assert.deepEqual(await gateRequires(list, "", "g", () => false), ["docker", "jq"]);
|
|
442
|
+
const csv = parseManifest('gates:\n g: "x"\nrequires:\n g: docker, jq\n');
|
|
443
|
+
assert.deepEqual(await gateRequires(csv, "", "g", () => false), ["docker", "jq"]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Гейт, про который в манифесте ничего не сказано, остаётся как был: молчание — не требование.
|
|
447
|
+
test("без requires поведение прежнее", async () => {
|
|
448
|
+
const man = parseManifest('gates:\n g: "x"\n');
|
|
449
|
+
assert.equal(await gateRequires(man, "", "g", () => false), null);
|
|
450
|
+
assert.equal(await gateRequires(null, "", "g", () => false), null);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Иначе `doctor` напечатает «неизвестное поле» на том, что сам же и читает.
|
|
454
|
+
test("requires — известное поле манифеста", () => {
|
|
455
|
+
assert.ok(KNOWN_KEYS.includes("requires"));
|
|
456
|
+
assert.deepEqual(unknownKeys({ requires: { g: "docker" } }), []);
|
|
457
|
+
});
|
|
@@ -327,3 +327,78 @@ test("свод в AGENTS.md виден Claude Code только через CLAUD
|
|
|
327
327
|
assert.equal(claudeSeesRules({ ...base, claude: "x", claudeLink: true }), null, "ссылка на AGENTS.md — тот же файл");
|
|
328
328
|
assert.equal(claudeSeesRules({ ...base, agents: false, dotClaude: true }), null, "AGENTS.md нет — подключать нечего");
|
|
329
329
|
});
|
|
330
|
+
|
|
331
|
+
// --- проверка, которая у проекта есть и не может провалиться -----------------------
|
|
332
|
+
//
|
|
333
|
+
// САМЫЙ ЦЕННЫЙ ОТВЕТ В ПЕРВЫЕ ПЯТЬ СЕКУНД, и до 2026-09-14 мы его не давали. `doctor` читал
|
|
334
|
+
// ИМЯ скрипта («test», «lint») и печатал каноничную команду `npm test` с зелёной галочкой,
|
|
335
|
+
// ни разу не заглянув в ТЕЛО скрипта. А выключатель стоит именно там: `node --test || true`.
|
|
336
|
+
// То есть на репозитории, где выключено всё, мы говорили «у вас уже есть 2 проверки» —
|
|
337
|
+
// ровно та ошибка, ради которой написан весь комплект, в нашем собственном первом экране.
|
|
338
|
+
//
|
|
339
|
+
// ГРАНИЦА НАМЕРЕННО УЗКАЯ, и она взята у гейта `ci-actually-fails`: `|| true` в СЕРЕДИНЕ
|
|
340
|
+
// команды — это идемпотентность вспомогательного шага (`mkdir -p … || true`), а не выключенная
|
|
341
|
+
// проверка. Красным делается только гашение, под которое попадает ВЕСЬ исход: в конце тела
|
|
342
|
+
// либо флаг, у которого другого назначения нет.
|
|
343
|
+
test("выключатель в теле скрипта виден, а не прячется за каноничной командой", () => {
|
|
344
|
+
const pkg = (scripts) => ({ "package.json": JSON.stringify({ scripts }) });
|
|
345
|
+
const one = (scripts) => proposeGates(pkg(scripts))[0];
|
|
346
|
+
|
|
347
|
+
assert.equal(one({ test: "node --test" }).weak, undefined, "рабочая проверка не должна обвиняться");
|
|
348
|
+
assert.equal(one({ test: "node --test || true" }).weak?.kind, "off", "«|| true» в конце гасит весь исход");
|
|
349
|
+
assert.equal(one({ test: "node --test || :" }).weak?.kind, "off");
|
|
350
|
+
assert.equal(one({ test: "node --test || exit 0" }).weak?.kind, "off");
|
|
351
|
+
assert.equal(one({ lint: "ruff check . --exit-zero" }).weak?.kind, "zero", "флаг, у которого нет другого назначения");
|
|
352
|
+
assert.equal(one({ test: 'echo "no tests yet"' }).weak?.kind, "stub", "заглушка проходит всегда");
|
|
353
|
+
assert.equal(one({ test: 'echo "Error: no test specified" && exit 1' }).weak, undefined,
|
|
354
|
+
"заготовка npm провалиться МОЖЕТ — обвинять её нельзя");
|
|
355
|
+
|
|
356
|
+
// Ложные, на которых узость границы и проверяется.
|
|
357
|
+
assert.equal(one({ test: "mkdir -p tmp || true && node --test" }).weak, undefined,
|
|
358
|
+
"гашение вспомогательного шага в середине — идемпотентность, а не выключенная проверка");
|
|
359
|
+
assert.equal(one({ test: "node --test # было || true" }).weak, undefined, "упоминание в комментарии — не выключатель");
|
|
360
|
+
|
|
361
|
+
// Своя строка доезжает до человека целиком: без неё он не поверит и не найдёт, что чинить.
|
|
362
|
+
assert.match(one({ test: "node --test || true" }).weak.text, /\|\| true/);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// ОБЪЯВЛЕННЫЙ ГЕЙТ НЕ ДЕЛАЕТ ЧУЖИЕ ПРОВЕРКИ НЕВИДИМЫМИ.
|
|
366
|
+
//
|
|
367
|
+
// ЗАМЕР 2026-09-16 по двенадцати чужим репозиториям. У шести зрелых (requests, httpx, fastapi,
|
|
368
|
+
// black, express, flask) `doctor` без манифеста читал в их файлах от одной до трёх настоящих
|
|
369
|
+
// проверок. Стоило объявить ОДИН гейт — блок «у вас уже есть» исчезал у всех шести, и итог
|
|
370
|
+
// говорил «держит машина 0». То есть чем больше проект настроил, тем меньше мы о нём знали.
|
|
371
|
+
//
|
|
372
|
+
// Это ВТОРАЯ встреча с тем же классом: первый чужой пользователь 2026-09-08 назвал «применимо,
|
|
373
|
+
// но не поставлено: 5» неправдой, и ответом стало поле `covers`. Оно заполняется руками — и в
|
|
374
|
+
// зрелом проекте с двумя десятками записей его не заполнил никто. Лекарство, которое просит
|
|
375
|
+
// человека делать то, что умеет машина, не работает.
|
|
376
|
+
//
|
|
377
|
+
// Сравнение — ПО КОМАНДЕ, а не по имени: имена гейтов у проекта свои (`project-verify`), и по
|
|
378
|
+
// ним совпадения не будет никогда.
|
|
379
|
+
test("чужие проверки видны и тогда, когда гейты уже объявлены", () => {
|
|
380
|
+
const files = { Makefile: "test:\n\tpytest -q\n\nlint:\n\truff check .\n" };
|
|
381
|
+
const got = proposeGates(files, ["bash scripts/verify.sh"]);
|
|
382
|
+
assert.deepEqual(got.map((g) => g.name).sort(), ["lint", "test"],
|
|
383
|
+
"объявленный гейт с ДРУГОЙ командой скрыл чужие проверки");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("уже объявленная проверка второй раз не предлагается", () => {
|
|
387
|
+
const files = { Makefile: "test:\n\tpytest -q\n\nlint:\n\truff check .\n" };
|
|
388
|
+
const got = proposeGates(files, ["make test"]);
|
|
389
|
+
assert.deepEqual(got.map((g) => g.name), ["lint"], "`make test` объявлен, а мы советуем его снова");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// Обёртка снимается: человек пишет `bash scripts/check`, мы нашли `scripts/check` — это одно и
|
|
393
|
+
// то же, и предлагать его второй раз значит советовать то, что уже стоит.
|
|
394
|
+
test("обёртка bash/sh не мешает узнать объявленную команду", () => {
|
|
395
|
+
const got = proposeGates({ "scripts/check": "" }, ["bash scripts/check"]);
|
|
396
|
+
assert.deepEqual(got, [], "`bash scripts/check` и `scripts/check` не узнаны как одна команда");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Пустой список объявленных — прежнее поведение до последней запятой: ничего не фильтруем.
|
|
400
|
+
test("без объявленных гейтов список тот же, что и раньше", () => {
|
|
401
|
+
const files = { Makefile: "test:\n\tpytest -q\n" };
|
|
402
|
+
assert.deepEqual(proposeGates(files).map((g) => g.name), ["test"]);
|
|
403
|
+
assert.deepEqual(proposeGates(files, []).map((g) => g.name), ["test"]);
|
|
404
|
+
});
|
|
@@ -79,3 +79,30 @@ test("старая версия сообщается, но не роняет", (
|
|
|
79
79
|
assert.match(v.detail, /0\.9\.0/);
|
|
80
80
|
assert.equal(vitalsVerdict(rows), 0);
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
// ЧУЖОЙ ХУК — НЕ НАША ОБВЯЗКА. ЧЕТВЁРТОЕ СОСТОЯНИЕ, И ОНО ЗАРАБОТАНО.
|
|
84
|
+
//
|
|
85
|
+
// Разбор интеграции в живом проекте 2026-09-16: vitals печатал «хук pre-commit прописан в
|
|
86
|
+
// .git/hooks», а AQK внутри этого хука не вызывался вовсе — там стоял диспетчер фреймворка
|
|
87
|
+
// pre-commit. Человек прочитал строку как «обвязка на месте» и ушёл; на деле ни один коммит в
|
|
88
|
+
// том репозитории AQK не запускал. Это ровно наш собственный класс «объявлено ≠ работает»,
|
|
89
|
+
// только у нас самих, — и цена ему та же, что мы называем чужим инструментам.
|
|
90
|
+
//
|
|
91
|
+
// Как это делают снаружи: pre-commit узнаёт свой хук функцией `is_our_script()` — ищет в теле
|
|
92
|
+
// файла собственный маркер (CURRENT_HASH и пять PRIOR_HASHES), а не имя. Имя чужого
|
|
93
|
+
// инструмента в теле хука не значит ничего: у них же есть режим миграции, в котором рядом
|
|
94
|
+
// живут оба хука сразу.
|
|
95
|
+
test("хук стоит, но AQK в нём не вызывается — это не «подключено»", () => {
|
|
96
|
+
const rows = vitalsRows({ ...ok, preCommit: "other" });
|
|
97
|
+
const hook = rows.find((r) => r.key === "preCommit");
|
|
98
|
+
assert.notEqual(hook.ok, true, "чужой хук засчитан как подключённый AQK");
|
|
99
|
+
assert.match(hook.detail, /AQK|aqk/, `в строке не сказано, чего именно нет: ${hook.detail}`);
|
|
100
|
+
|
|
101
|
+
// И это НЕ то же самое, что «хука нет вовсе»: в одном случае ставить нечего, в другом —
|
|
102
|
+
// дописать строку в уже стоящий хук. Совет разный, значит и строки разные.
|
|
103
|
+
const none = vitalsRows({ ...ok, preCommit: false }).find((r) => r.key === "preCommit");
|
|
104
|
+
assert.notEqual(hook.detail, none.detail, "«хук чужой» и «хука нет» неразличимы по выводу");
|
|
105
|
+
|
|
106
|
+
// Чужой хук — не поломка: человек мог сознательно гонять AQK в конвейере.
|
|
107
|
+
assert.equal(vitalsVerdict(rows), 0, "команда кричит «сломано» про чужой выбор");
|
|
108
|
+
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Ссылки точки входа ведут на существующие файлы
|
|
2
|
-
|
|
3
|
-
**Намерение.** Свод правил, который ссылается на несуществующий файл, утверждает то, чего нет.
|
|
4
|
-
|
|
5
|
-
**Какой отказ это поймало.** В `audit_project` чек-лист гейтов четыре недели числил работающими
|
|
6
|
-
изоляцию исполнителя и генерацию контрактов. Обе были отключены владельцем за три недели до
|
|
7
|
-
этого — инструмент ломал файлы. Планирование опиралось на защиту, которой не существовало.
|
|
8
|
-
Запись в журнале: `incidents/README.md`, 2026-08-24.
|
|
9
|
-
|
|
10
|
-
**Почему машина, а не внимательность.** Расхождение появляется не в момент написания документа, а
|
|
11
|
-
через недели, когда файл удалили в другой задаче. Человек в этот момент смотрит не сюда.
|
|
12
|
-
|
|
13
|
-
**Готовый аналог есть и он сильнее.** `lychee` и `markdown-link-check` проверяют ещё и внешние
|
|
14
|
-
адреса, и якоря внутри страницы. Рецепта под них в записи нет по устройству формата: рецепты
|
|
15
|
-
выбираются **по языку проекта**, а эти инструменты к языку не привязаны. Если такой инструмент у
|
|
16
|
-
вас стоит — он лучше нашего.
|
|
17
|
-
|
|
18
|
-
**Адрес страницы сайта не считается битой ссылкой.** Путь, кончающийся косой чертой —
|
|
19
|
-
`[руководство](tutorial/#install)`, — это адрес на опубликованном сайте, а не файл в
|
|
20
|
-
репозитории. Так ссылаются mkdocs, docusaurus и jekyll. Найдено замером по `fastapi`: гейт
|
|
21
|
-
объявлял битой рабочую ссылку из их README.
|
|
22
|
-
|
|
23
|
-
**Чего НЕ ловит.** Только файлы `*.md` в самом каталоге, без обхода вложенных: намерение записи —
|
|
24
|
-
точка входа, а не вся документация. Не проверяет внешние адреса (сеть) и якоря внутри файла.
|
|
25
|
-
|
|
26
|
-
**Образцы.** `red/` — свод ссылается на `rules/nope.md`, которого нет: гейт обязан краснеть.
|
|
27
|
-
`green/` — ссылается на существующий `rules/general.md`: гейт обязан молчать.
|