agent-quality-kit 0.9.0 → 0.10.1
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 +69 -10
- package/README.ru.md +68 -11
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +85 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +101 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +10 -4
- package/package.json +2 -1
- package/tool/commands/context.mjs +36 -1
- package/tool/commands/doctor.mjs +56 -2
- package/tool/commands/gates.mjs +2 -0
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/vitals.mjs +11 -3
- package/tool/i18n/en-docs.mjs +10 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +10 -281
- package/tool/i18n/ru-docs.mjs +10 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +10 -280
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +1 -0
- package/tool/lib/evidence.mjs +15 -2
- package/tool/lib/execution.mjs +67 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +1 -1
- package/tool/lib/protection.mjs +52 -0
- package/tool/lib/prove.mjs +40 -13
- package/tool/lib/repo.mjs +12 -2
- package/tool/program.mjs +7 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/protection.test.mjs +101 -0
- package/tool/selfcheck/smoke/release-tools.test.mjs +55 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +194 -7
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +24 -0
- package/tool/selfcheck/units-evidence.mjs +34 -0
- package/tool/selfcheck/units-execution.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +32 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +31 -1
- package/tool/selfcheck/units-vitals.mjs +19 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Проверки разбора истории для `aqk probe`. Написаны ДО кода: без них нельзя отличить
|
|
2
|
+
// «функция работает» от «функция написана».
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { isFix, fixHotspots, probeVerdict } from "../lib/history.mjs";
|
|
6
|
+
import { scanningGates, isCode } from "../commands/probe.mjs";
|
|
7
|
+
|
|
8
|
+
// Признак починки берётся из ТЕМЫ коммита, а не из тела: тема — единственное, что пишут все,
|
|
9
|
+
// и единственное, что видно в `git log --oneline`. Три написания, потому что репозитории
|
|
10
|
+
// бывают на двух языках и с conventional commits.
|
|
11
|
+
test("темой починки считаются fix, исправ и почин — на обоих языках и с областью", () => {
|
|
12
|
+
assert.equal(isFix("fix: гейт молчал на красном"), true);
|
|
13
|
+
assert.equal(isFix("fix(prove): windows-пути"), true);
|
|
14
|
+
assert.equal(isFix("Исправлено: разбор манифеста"), true);
|
|
15
|
+
assert.equal(isFix("починка отчёта"), true);
|
|
16
|
+
assert.equal(isFix("FIX: заглавными тоже"), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Слово «fix» внутри обычной темы починкой не делает: иначе «feat: prefix для путей» попадёт
|
|
20
|
+
// в рейтинг, и рейтинг перестанет что-либо значить.
|
|
21
|
+
test("не всякое упоминание fix — починка", () => {
|
|
22
|
+
assert.equal(isFix("feat: prefix для путей"), false);
|
|
23
|
+
assert.equal(isFix("docs: как чинить гейт"), false);
|
|
24
|
+
assert.equal(isFix("refactor: суффиксы"), false);
|
|
25
|
+
assert.equal(isFix(""), false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// НАСТОЯЩИЙ формат `git log --format=%s --name-only`, снятый с живого репозитория 2026-09-09:
|
|
29
|
+
// тема, ПУСТАЯ строка, пути, и сразу следующая тема — без пустой строки перед ней.
|
|
30
|
+
// Первая версия этого теста была написана по моему представлению о формате (пустая строка
|
|
31
|
+
// СЛЕДОВАЛА за файлами), код под неё разобрал историю неверно и нашёл ноль починок там, где
|
|
32
|
+
// их сорок пять. Тот же класс, что и дважды сегодня: проверял замысел, а не вывод.
|
|
33
|
+
const LOG = [
|
|
34
|
+
"fix: разбор манифеста",
|
|
35
|
+
"",
|
|
36
|
+
"tool/lib/manifest.mjs",
|
|
37
|
+
"tool/selfcheck/units.mjs",
|
|
38
|
+
"feat: новая запись",
|
|
39
|
+
"",
|
|
40
|
+
"kit/gates/x/check.sh",
|
|
41
|
+
"fix(manifest): кавычки",
|
|
42
|
+
"",
|
|
43
|
+
"tool/lib/manifest.mjs",
|
|
44
|
+
"README.md",
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
test("рейтинг считает только файлы из коммитов-починок", () => {
|
|
48
|
+
const hot = fixHotspots(LOG, { isCode: () => true });
|
|
49
|
+
assert.deepEqual(hot[0], { path: "tool/lib/manifest.mjs", fixes: 2 });
|
|
50
|
+
// Файл из коммита-фичи в рейтинг не попадает вовсе.
|
|
51
|
+
assert.equal(hot.some((h) => h.path === "kit/gates/x/check.sh"), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("рейтинг отсеивает то, что не код: документ чинят иначе, чем программу", () => {
|
|
55
|
+
const hot = fixHotspots(LOG, { isCode: (p) => p.endsWith(".mjs") });
|
|
56
|
+
assert.equal(hot.some((h) => h.path === "README.md"), false);
|
|
57
|
+
assert.equal(hot.length, 2);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("пустая история — пустой рейтинг, а не падение", () => {
|
|
61
|
+
assert.deepEqual(fixHotspots("", { isCode: () => true }), []);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Три состояния, и сливать их нельзя. «Не смогли проверить» — не «прикрыто»: ровно та
|
|
65
|
+
// подмена, против которой написан весь комплект.
|
|
66
|
+
test("вердикт пробы различает поймано, не поймано и нечем проверить", () => {
|
|
67
|
+
assert.equal(probeVerdict([{ code: 1 }, { code: 0 }]), "caught");
|
|
68
|
+
assert.equal(probeVerdict([{ code: 0 }, { code: 0 }]), "blind");
|
|
69
|
+
assert.equal(probeVerdict([{ code: 2 }, { code: 0 }]), "unknown");
|
|
70
|
+
// Поймавший гейт сильнее непроверенного: класс закрыт, даже если рядом чего-то не хватает.
|
|
71
|
+
assert.equal(probeVerdict([{ code: 2 }, { code: 1 }]), "caught");
|
|
72
|
+
assert.equal(probeVerdict([]), "unknown");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Пробовать можно только те гейты, которым есть куда подставить каталог: рецепт каталога
|
|
76
|
+
// кончается каталогом проверки, команда, написанная руками, — чем угодно. То же правило, по
|
|
77
|
+
// которому `prove` объявляет запись недоказуемой, а не сломанной.
|
|
78
|
+
test("пробуются только гейты, кончающиеся каталогом проверки", () => {
|
|
79
|
+
const man = { gates: {
|
|
80
|
+
ok: "bash gates/x/check.sh .",
|
|
81
|
+
slash: "bash gates/y/check.sh ./",
|
|
82
|
+
handmade: "eslint . --max-warnings 0",
|
|
83
|
+
empty: "",
|
|
84
|
+
} };
|
|
85
|
+
assert.deepEqual(scanningGates(man).map(([n]) => n), ["ok", "slash"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("манифест без гейтов не роняет разбор", () => {
|
|
89
|
+
assert.deepEqual(scanningGates(null), []);
|
|
90
|
+
assert.deepEqual(scanningGates({ gates: [] }), []);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Образцы каталога исключены по той же причине, по какой их исключает каждая сканирующая
|
|
94
|
+
// проверка: они существуют, чтобы быть неправильными, и «горячими» быть не могут.
|
|
95
|
+
test("горячим считается код, но не документ и не образец каталога", () => {
|
|
96
|
+
assert.equal(isCode("src/a.py"), true);
|
|
97
|
+
assert.equal(isCode("tool/lib/core.mjs"), true);
|
|
98
|
+
assert.equal(isCode("README.md"), false);
|
|
99
|
+
assert.equal(isCode("gates/secrets-not-in-code/red/a.py"), false);
|
|
100
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// node --test tool/selfcheck/units-repo.mjs
|
|
9
9
|
import test from "node:test";
|
|
10
10
|
import assert from "node:assert/strict";
|
|
11
|
-
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS } from "../lib/repo.mjs";
|
|
11
|
+
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
|
|
12
12
|
import { CATALOGS, L } from "../i18n/index.mjs";
|
|
13
13
|
import { dirname } from "node:path";
|
|
14
14
|
|
|
@@ -115,6 +115,36 @@ test("сервер уже объявлен — совета нет", () => {
|
|
|
115
115
|
assert.equal(browserServerAdvice({ has_ui: true }, pw), null);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
// Признак «здесь есть договор с чужим кодом»: спецификация API. Считается обходом дерева, а не
|
|
119
|
+
// списком путей, — файл кладут где угодно (`openapi.yaml`, `docs/api/openapi.json`, `schema.yml`
|
|
120
|
+
// рядом с приложением), и фиксированный список промахнулся бы на большинстве проектов.
|
|
121
|
+
//
|
|
122
|
+
// ПОЧЕМУ ПРИЗНАК ОТДЕЛЬНЫЙ, а не часть has_deps. Запись про арбитра контракта не касается
|
|
123
|
+
// библиотеки, консольной программы и монолита без внешнего интерфейса — а таких большинство.
|
|
124
|
+
// Показанной не тому записи не верят, и каталог теряет доверие целиком, а не одной строкой.
|
|
125
|
+
test("спецификация API опознаётся по имени файла, а не по расположению", () => {
|
|
126
|
+
assert.equal(isApiSpec("openapi.yaml"), true);
|
|
127
|
+
assert.equal(isApiSpec("openapi.json"), true);
|
|
128
|
+
assert.equal(isApiSpec("swagger.yml"), true);
|
|
129
|
+
assert.equal(isApiSpec("asyncapi.yaml"), true);
|
|
130
|
+
assert.equal(isApiSpec("openapi-v2.yaml"), true);
|
|
131
|
+
// Имя без расширения спецификации — не она: договор лежит в разбираемом формате.
|
|
132
|
+
assert.equal(isApiSpec("openapi.md"), false);
|
|
133
|
+
// Соседи по алфавиту, которые не договор: обычные файлы проекта.
|
|
134
|
+
assert.equal(isApiSpec("api.yaml"), false);
|
|
135
|
+
assert.equal(isApiSpec("schema.sql"), false);
|
|
136
|
+
assert.equal(isApiSpec("package.json"), false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Объяснение обязано быть на обоих языках. Признак без объяснения выключает запись НАВСЕГДА и
|
|
140
|
+
// молча — тот же класс, что поймал has_mcp ниже, поэтому сторожится отдельной строкой: в MARKS
|
|
141
|
+
// этого признака нет, он считается обходом, и общая проверка его не увидит.
|
|
142
|
+
test("у признака спецификации API есть объяснение на обоих языках", () => {
|
|
143
|
+
for (const lang of ["ru", "en"]) {
|
|
144
|
+
assert.ok(CATALOGS[lang].trigger.flags.has_api_spec, `${lang}: нет объяснения has_api_spec`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
118
148
|
// Признак репозитория без объяснения — это запись каталога, ВЫКЛЮЧЕННАЯ НАВСЕГДА. Триггер
|
|
119
149
|
// с неизвестным ключом даёт «условие программа не умеет считать», и запись не показывается
|
|
120
150
|
// никому и никогда. Поймано на себе 2026-09-08: завёл has_mcp в списке признаков, объяснение
|
|
@@ -52,6 +52,25 @@ test("вердикт краснеет от отказов, но не от нез
|
|
|
52
52
|
assert.equal(vitalsVerdict(vitalsRows({ ...ok, tools: [{ gate: "g", prog: "x", found: false }] })), 1);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
+
// НАЙДЕНО АУДИТОМ ФИЧ 2026-09-09. Реестр ответил ошибкой (не упал запрос, а вернул не-200) —
|
|
56
|
+
// `latest` остаётся пустым, и строка печатала «0.9.0, свежая». То есть посмотреть не смогли, а
|
|
57
|
+
// сказали «всё хорошо»: ровно тот грех, против которого написан весь комплект, у него самого.
|
|
58
|
+
// Три состояния, а не два: свежая · старая · посмотреть не смогли.
|
|
59
|
+
test("не дозвонились до реестра — так и говорим, а не «свежая»", () => {
|
|
60
|
+
const unknown = vitalsRows({ ...ok, version: { current: "0.9.0", latest: "" } });
|
|
61
|
+
const u = unknown.find((r) => r.key === "version");
|
|
62
|
+
assert.equal(u.ok, null);
|
|
63
|
+
assert.match(u.detail, /0\.9\.0/);
|
|
64
|
+
// Утверждение по СМЫСЛУ, а не по слову: «свежая ли, неизвестно» тоже содержит корень «свеж».
|
|
65
|
+
// Первая версия этого теста ловила именно слово и краснела на честном тексте.
|
|
66
|
+
assert.match(u.detail, /неизвестн|unknown/i, `незнание не названо: ${u.detail}`);
|
|
67
|
+
|
|
68
|
+
const fresh = vitalsRows({ ...ok, version: { current: "0.9.0", latest: "0.9.0" } });
|
|
69
|
+
const fd = fresh.find((r) => r.key === "version").detail;
|
|
70
|
+
assert.doesNotMatch(fd, /неизвестн|unknown/i);
|
|
71
|
+
assert.notEqual(fd, u.detail, "«свежая» и «не знаем» неразличимы по выводу");
|
|
72
|
+
});
|
|
73
|
+
|
|
55
74
|
// Устаревшая версия — не отказ: человек мог закрепить её сознательно.
|
|
56
75
|
test("старая версия сообщается, но не роняет", () => {
|
|
57
76
|
const rows = vitalsRows({ ...ok, version: { current: "0.8.0", latest: "0.9.0" } });
|