agent-quality-kit 0.14.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 +88 -18
- package/README.ru.md +91 -19
- 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 +39 -41
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/doctor.mjs +18 -26
- package/tool/commands/feedback.mjs +231 -0
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/project.mjs +11 -13
- package/tool/commands/prompt.mjs +2 -1
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +19 -2
- package/tool/i18n/en-gates.mjs +35 -0
- package/tool/i18n/en.mjs +29 -2
- package/tool/i18n/ru-docs.mjs +18 -2
- package/tool/i18n/ru-gates.mjs +36 -0
- package/tool/i18n/ru.mjs +27 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/ask.mjs +118 -0
- package/tool/lib/brief.mjs +17 -38
- package/tool/lib/core.mjs +49 -12
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/gate-worker.mjs +4 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +142 -10
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/fail-closed.test.mjs +96 -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-ask.mjs +85 -0
- package/tool/selfcheck/units-brief.mjs +3 -13
- package/tool/selfcheck/units-context.mjs +2 -1
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-feedback.mjs +137 -0
- 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
|
@@ -9,7 +9,7 @@ import test from "node:test";
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import { readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { project, aqk, run } from "./_fixture.mjs";
|
|
12
|
+
import { project, aqk, run, tail } from "./_fixture.mjs";
|
|
13
13
|
|
|
14
14
|
// Цвет снимается до разбора: заголовок жирный, значки цветные, и регулярка по сырому выводу
|
|
15
15
|
// не узнаёт ни то, ни другое.
|
|
@@ -99,8 +99,8 @@ test("нет .aqk/docs и .aqk/rules — прогон всё равно зелё
|
|
|
99
99
|
const man = join(p.dir, ".aqk.yml");
|
|
100
100
|
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n тихий: "true"'), "utf8");
|
|
101
101
|
const r = aqk(p, "doctor", "--run");
|
|
102
|
-
const
|
|
103
|
-
assert.equal(r.code, 0, `прогон покраснел из-за методичек:\n${
|
|
102
|
+
const last = tail(plain(r.out), 4);
|
|
103
|
+
assert.equal(r.code, 0, `прогон покраснел из-за методичек:\n${last}`);
|
|
104
104
|
assert.match(plain(r.out), /\.aqk\/docs/, "отсутствие методичек не названо вовсе");
|
|
105
105
|
});
|
|
106
106
|
|
|
@@ -142,3 +142,67 @@ test("prompt: задание без прогона начинается с пр
|
|
|
142
142
|
assert.match(r.out, /(Как проверить, что готово|How to verify)/);
|
|
143
143
|
assert.doesNotMatch(r.out, /\x1b\[/, "задание уходит агенту текстом — без цветовых кодов");
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
// ВЫКЛЮЧЕННАЯ ПРОВЕРКА НАЗЫВАЕТСЯ ВЫКЛЮЧЕННОЙ — В ПЕРВЫЕ ПЯТЬ СЕКУНД.
|
|
147
|
+
//
|
|
148
|
+
// Найдено 2026-09-14 живым прогоном на репозитории, где выключено всё: `"test": "node --test ||
|
|
149
|
+
// true"`, `"lint": "ruff check . --exit-zero"`, шаг конвейера под `continue-on-error`. `doctor`
|
|
150
|
+
// печатал «Проверки, которые у вас УЖЕ ЕСТЬ (2)» с двумя ЗЕЛЁНЫМИ галочками: мы читали ИМЯ
|
|
151
|
+
// скрипта и ни разу не заглядывали в его тело. Тот самый класс отказа, ради которого написан
|
|
152
|
+
// комплект, — в нашем собственном первом экране.
|
|
153
|
+
//
|
|
154
|
+
// Это главная встреча постороннего с инструментом: ноль настройки, одна команда. Если здесь мы
|
|
155
|
+
// подтверждаем ложное «у вас всё хорошо», второго запуска не будет.
|
|
156
|
+
test("проверка, которую нельзя провалить, названа находкой, а не зелёной галочкой", (t) => {
|
|
157
|
+
const p = project(t, {
|
|
158
|
+
...FILES,
|
|
159
|
+
"package.json": JSON.stringify({ name: "x", scripts: { test: "node --test || true", lint: "ruff check . --exit-zero" } }),
|
|
160
|
+
});
|
|
161
|
+
const out = plain(aqk(p, "doctor").out);
|
|
162
|
+
const block = out.slice(out.indexOf("УЖЕ ЕСТЬ"));
|
|
163
|
+
assert.ok(block, "блок «проверки, которые уже есть» пропал совсем");
|
|
164
|
+
assert.doesNotMatch(block.split("\n").slice(0, 3).join("\n"), /✔/,
|
|
165
|
+
`выключенная проверка получила зелёную галочку — это ложное «у вас всё хорошо»:\n${block.slice(0, 400)}`);
|
|
166
|
+
assert.match(block, /\|\| true/, "человеку не показали строку, по которой он найдёт выключатель у себя");
|
|
167
|
+
assert.match(block, /--exit-zero/, "флаг, гасящий исход, не назван");
|
|
168
|
+
|
|
169
|
+
// И то же знание — агенту: иначе он впишет дыру в манифест, и с этого дня за неё ручается машина.
|
|
170
|
+
const ctx = plain(aqk(p, "context").out);
|
|
171
|
+
assert.match(ctx, /покраснеть не могут/,
|
|
172
|
+
`блок для агента велит объявить проверки, которые не могут провалиться:\n${ctx.slice(0, 600)}`);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ЗРЕЛЫЙ ПРОЕКТ НЕ ТЕРЯЕТ СВОИ ПРОВЕРКИ ИЗ ВИДА, КОГДА ОБЪЯВИЛ ГЕЙТ.
|
|
176
|
+
//
|
|
177
|
+
// Замер 2026-09-16 по двенадцати чужим репозиториям: у шести зрелых (requests, httpx, fastapi,
|
|
178
|
+
// black, express, flask) блок «у вас уже есть» исчезал целиком, стоило объявить один гейт, —
|
|
179
|
+
// и итог печатал «держит машина 0» проекту с настоящим pre-commit и Makefile. Чем больше
|
|
180
|
+
// человек настроил, тем меньше мы о нём знали: условие показывало чужие проверки только тому,
|
|
181
|
+
// у кого гейтов нет вовсе.
|
|
182
|
+
test("объявленный гейт не прячет проверки, которые у проекта уже есть", (t) => {
|
|
183
|
+
const p = project(t, {
|
|
184
|
+
...FILES,
|
|
185
|
+
Makefile: "test:\n\tpytest -q\n\nlint:\n\truff check .\n",
|
|
186
|
+
".aqk.yml": 'aqk: 1\nentry: [AGENTS.md]\ngates:\n project-verify: "bash scripts/verify.sh"\n',
|
|
187
|
+
"AGENTS.md": "# вход\n",
|
|
188
|
+
});
|
|
189
|
+
const out = plain(aqk(p, "doctor").out);
|
|
190
|
+
assert.match(out, /УЖЕ ЕСТЬ/,
|
|
191
|
+
`проект объявил один гейт — и его собственные проверки пропали из вывода:\n${out.slice(-1500)}`);
|
|
192
|
+
assert.match(out, /make test|npm test/, "найденная команда не названа человеку");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// И обратное: то, что УЖЕ объявлено, вторым списком не повторяется. Иначе вывод советует
|
|
196
|
+
// поставить то, что стоит, — и человек перестаёт читать этот блок целиком.
|
|
197
|
+
test("объявленная проверка не советуется второй раз", (t) => {
|
|
198
|
+
const p = project(t, {
|
|
199
|
+
...FILES,
|
|
200
|
+
Makefile: "test:\n\tpytest -q\n",
|
|
201
|
+
".aqk.yml": 'aqk: 1\nentry: [AGENTS.md]\ngates:\n test: "make test"\n lint: "npm run lint"\n',
|
|
202
|
+
"AGENTS.md": "# вход\n",
|
|
203
|
+
});
|
|
204
|
+
const out = plain(aqk(p, "doctor").out);
|
|
205
|
+
const block = out.includes("УЖЕ ЕСТЬ") ? out.slice(out.indexOf("УЖЕ ЕСТЬ")) : "";
|
|
206
|
+
assert.doesNotMatch(block.split("\n").slice(0, 6).join("\n"), /make test/,
|
|
207
|
+
`«make test» уже объявлен гейтом, а мы советуем его снова:\n${block.slice(0, 400)}`);
|
|
208
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Прогон, который не может состояться, обязан сказать это, а не показать пятнадцать крестов.
|
|
2
|
+
//
|
|
3
|
+
// НАЙДЕНО ЧУЖИМ РАЗБОРОМ 2026-09-16. `smoke.sh`, запущенный внутри песочницы Codex, дал 15
|
|
4
|
+
// падений из 123. Настоящая причина: дочерним процессам Node запрещалось запускать git и npm —
|
|
5
|
+
// `spawnSync` возвращал EPERM. Проверки об этом не знали и печатали обычные кресты. Человек
|
|
6
|
+
// час разбирал несуществующие дефекты, а строка «дефекты AQK 0.15.0» уехала в чужой отчёт.
|
|
7
|
+
//
|
|
8
|
+
// ИРОНИЯ, РАДИ КОТОРОЙ ЭТОТ ФАЙЛ И НАПИСАН. `tool/lib/execution.mjs` различает три исхода —
|
|
9
|
+
// clean · finding · infra_error — именно затем, чтобы сбой инструмента не выдавался за приговор
|
|
10
|
+
// коду. Своей собственной оснастке мы это правило не применили.
|
|
11
|
+
//
|
|
12
|
+
// ЧАСТИЧНЫЙ ПРОГОН НИЧЕГО НЕ ДОКАЗЫВАЕТ: 108 зелёных из 123 при недоступном git — не «почти
|
|
13
|
+
// всё хорошо», а «мерить было нечем». Поэтому предполёт отказывает целиком и выходит кодом 3 —
|
|
14
|
+
// тем же, которым pytest отделяет INTERNAL_ERROR от провала теста (их код 1).
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join, dirname, delimiter } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
24
|
+
const SMOKE = join(ROOT, "tool", "selfcheck", "smoke.sh");
|
|
25
|
+
|
|
26
|
+
const WIN = process.platform === "win32";
|
|
27
|
+
|
|
28
|
+
// Заглушка вместо настоящей программы: выходит кодом 126 — «найдено, но запустить нельзя».
|
|
29
|
+
// Ровно то, чем для нас выглядел запрет песочницы.
|
|
30
|
+
//
|
|
31
|
+
// ДВА ФАЙЛА, А НЕ ОДИН. Поймано windows-заданием конвейера: скрипт без расширения там не
|
|
32
|
+
// перехватывает ничего — CreateProcess ищет `.exe`/`.cmd` по PATHEXT, а Git Bash понимает
|
|
33
|
+
// shebang. Значит нужны обе заглушки сразу, иначе проверка молча меряет настоящий git и
|
|
34
|
+
// краснеет на исправной машине — то есть сама становится тем ложным красным, против которого
|
|
35
|
+
// написан предполёт.
|
|
36
|
+
function stubDir(t, name) {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), "aqk-stub-"));
|
|
38
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
39
|
+
writeFileSync(join(dir, name), "#!/bin/sh\nexit 126\n", "utf8");
|
|
40
|
+
chmodSync(join(dir, name), 0o755);
|
|
41
|
+
if (WIN) writeFileSync(join(dir, `${name}.cmd`), "@echo off\r\nexit /b 126\r\n", "utf8");
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// PATH СОБИРАЕТСЯ ЧЕРЕЗ `delimiter`, А НЕ ЧЕРЕЗ ДВОЕТОЧИЕ. На Windows разделитель — точка с
|
|
46
|
+
// запятой, а сам путь вида `C:\Users\…` содержит двоеточие внутри: строка `"C:\...:" + PATH`
|
|
47
|
+
// разваливается на первом же символе, заглушка в PATH не попадает, и проверка меряет настоящий
|
|
48
|
+
// git. Поймано windows-заданием дважды подряд — первый раз я починил вызов, а не сборку пути.
|
|
49
|
+
const withStub = (dir) => (dir ? `${dir}${delimiter}${process.env.PATH}` : process.env.PATH);
|
|
50
|
+
|
|
51
|
+
const runPreflight = (extraPath) => spawnSync("bash", [SMOKE, "--preflight-only"], {
|
|
52
|
+
encoding: "utf8", timeout: 60000,
|
|
53
|
+
env: { ...process.env, PATH: withStub(extraPath), AQK_LANG: "ru" },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("предполёт: исправная машина проходит и ничего не ломает", () => {
|
|
57
|
+
const r = runPreflight(null);
|
|
58
|
+
assert.equal(r.status, 0, `предполёт отказал на исправной машине:\n${r.stdout}${r.stderr}`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("предполёт: недоступный git — это «не смогли проверить», а не падения проверок", (t) => {
|
|
62
|
+
const r = runPreflight(stubDir(t, "git"));
|
|
63
|
+
assert.equal(r.status, 3,
|
|
64
|
+
`код ${r.status}: запрет среды неотличим от провала проверок. Нужен 3 — тот же, которым pytest отделяет внутреннюю ошибку от провала теста`);
|
|
65
|
+
const out = `${r.stdout || ""}${r.stderr || ""}`;
|
|
66
|
+
assert.match(out, /git/i, "не сказано, ЧТО именно недоступно");
|
|
67
|
+
assert.doesNotMatch(out, /✘/, `предполёт напечатал крест — значит выдал сбой среды за находку:\n${out}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Тот самый случай из отчёта: сам шелл git видит, а подпроцессы Node — нет. Проверять только
|
|
71
|
+
// первое значило бы пройти предполёт и упасть пятнадцатью крестами следом.
|
|
72
|
+
test("предполёт: git виден шеллу, но не подпроцессу Node — тоже отказ", (t) => {
|
|
73
|
+
const dir = stubDir(t, "git");
|
|
74
|
+
// `shell` — тот же, что в самом предполёте: на Windows без оболочки Node не видит ни `.cmd`
|
|
75
|
+
// заглушки, ни настоящего `npm`. Проверка обязана звать так же, как зовёт код, иначе она
|
|
76
|
+
// проверяет не его. Ровно на этом конвейер и поймал первую версию.
|
|
77
|
+
// Node зовётся НАПРЯМУЮ, без оболочки-посредника: подстановка пути в строку `bash -c` — это
|
|
78
|
+
// ещё один способ потерять заглушку, и именно так эта проверка уже ошиблась.
|
|
79
|
+
const probe = `const r=require("node:child_process").spawnSync("git",["--version"],{shell:${WIN}}); process.exit(r.status===0?0:3)`;
|
|
80
|
+
const r = spawnSync(process.execPath, ["-e", probe],
|
|
81
|
+
{ encoding: "utf8", timeout: 30000, env: { ...process.env, PATH: withStub(dir) } });
|
|
82
|
+
assert.equal(r.status, 3, "оснастка проверки неверна: заглушка не перехватывает git у Node");
|
|
83
|
+
});
|
|
@@ -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 не расходятся структурно --------------------------------
|