agent-quality-kit 0.7.0 → 0.8.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 +45 -2
- package/README.ru.md +45 -2
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +6 -2
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/complexity-limit/check.sh +6 -7
- package/kit/gates/duplicate-code/check.sh +5 -1
- package/kit/gates/entry-links-exist/check.sh +4 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
- package/kit/gates/file-size-limit/check.sh +1 -1
- package/kit/gates/secrets-not-in-code/check.sh +16 -3
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/todo-without-task/check.sh +1 -1
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/llms.txt +22 -1
- package/package.json +4 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +25 -18
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +1 -0
- package/tool/commands/report.mjs +33 -1
- package/tool/i18n/en-docs.mjs +85 -1
- package/tool/i18n/en.mjs +21 -36
- package/tool/i18n/ru-docs.mjs +87 -1
- package/tool/i18n/ru.mjs +21 -36
- package/tool/lib/core.mjs +30 -1
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +29 -2
- package/tool/lib/prove.mjs +13 -1
- package/tool/lib/scope.mjs +10 -1
- package/tool/lib/templates.mjs +1 -0
- package/tool/program.mjs +19 -23
- package/tool/selfcheck/smoke.sh +242 -2
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +65 -3
- package/tool/selfcheck/units.mjs +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// tool/selfcheck/units-evidence.mjs — проверки привязки доказательства к дифу.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫМ ФАЙЛОМ. `units.mjs` во второй раз перерос собственный предел в 500 строк —
|
|
4
|
+
// его поймал наш же гейт file-size-limit, и это уже второй такой развод (первым был
|
|
5
|
+
// `units-level.mjs`). Шов по смыслу: здесь всё про то, чем доказан диф, и ничего больше.
|
|
6
|
+
//
|
|
7
|
+
// node --test tool/selfcheck/units-evidence.mjs
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
// --- покрытие дифа доказательством ------------------------------------------
|
|
13
|
+
//
|
|
14
|
+
// ЗАЧЕМ. Правило «готово = доказано» было единственным центральным правилом свода, за которым
|
|
15
|
+
// не следила машина: сторожем стоял человек. Прогон donecheck по восьми нашим коммитам показал,
|
|
16
|
+
// чего это стоило: `.github/workflows/publish.yml` менялся и не был назван ни одной командой
|
|
17
|
+
// проверки — и именно он оказался сломан. Механизм взят оттуда (AtharvaMaik/donecheck, MIT),
|
|
18
|
+
// сопоставление сделано СТРОГИМ, в отличие от него: он считает файл покрытым и по голому имени,
|
|
19
|
+
// а имя `check.sh` в нашем каталоге носят двадцать разных файлов.
|
|
20
|
+
test("покрытие: файл считается доказанным, только если гейт назвал ЕГО путь", async () => {
|
|
21
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
22
|
+
const res = [
|
|
23
|
+
{ name: "tests", cmd: "node --test", out: "ok 1 - src/pay.js:12 покрыт" },
|
|
24
|
+
{ name: "lint", cmd: "bash kit/gates/x/check.sh .", out: "" },
|
|
25
|
+
];
|
|
26
|
+
const cov = coverage(["src/pay.js", "src/refund.js"], res);
|
|
27
|
+
assert.deepEqual(cov.covered.get("src/pay.js"), ["tests"]);
|
|
28
|
+
// refund.js не назван никем, но `lint` был направлен в корень — значит он в «молчании»,
|
|
29
|
+
// а не в «никто не смотрел». Разница между этими двумя и есть смысл третьего состояния.
|
|
30
|
+
assert.deepEqual(cov.silent.get("src/refund.js"), ["lint"]);
|
|
31
|
+
assert.deepEqual(cov.uncovered, []);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("покрытие: голое имя файла не считается доказательством", async () => {
|
|
35
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
36
|
+
// «check.sh» в выводе не означает, что проверен ИМЕННО kit/gates/todo/check.sh: этим именем
|
|
37
|
+
// в каталоге зовутся два десятка разных файлов. Мягкое сравнение дало бы тишину — то есть
|
|
38
|
+
// ровно тот отказ, против которого написана вся эта проверка.
|
|
39
|
+
const res = [{ name: "g", cmd: "bash run.sh", out: "проверено check.sh" }];
|
|
40
|
+
const cov = coverage(["kit/gates/todo/check.sh"], res);
|
|
41
|
+
assert.deepEqual(cov.uncovered, ["kit/gates/todo/check.sh"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("покрытие: путь с ./ и цветом в выводе всё равно засчитывается", async () => {
|
|
45
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
46
|
+
const esc = String.fromCharCode(27);
|
|
47
|
+
const res = [{ name: "g", cmd: "x", out: `${esc}[31m./src/pay.js:9${esc}[0m нашлось` }];
|
|
48
|
+
const cov = coverage(["src/pay.js"], res);
|
|
49
|
+
assert.deepEqual(cov.uncovered, []);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Расписка обязана меняться, когда меняется хоть что-то из того, о чём она отчитывается:
|
|
53
|
+
// базовый коммит, набор команд, содержимое файлов. Иначе «прогнал, потом поправил ещё три
|
|
54
|
+
// файла» неотличимо от «прогнал».
|
|
55
|
+
test("расписка: хеш доказательства меняется от правки файла и от смены команды", async () => {
|
|
56
|
+
const { evidenceHash } = await import("../lib/evidence.mjs");
|
|
57
|
+
const a = evidenceHash("base1", [{ cmd: "t" }], [["f.js", "one"]]);
|
|
58
|
+
assert.equal(evidenceHash("base1", [{ cmd: "t" }], [["f.js", "one"]]), a);
|
|
59
|
+
assert.notEqual(evidenceHash("base1", [{ cmd: "t" }], [["f.js", "two"]]), a);
|
|
60
|
+
assert.notEqual(evidenceHash("base1", [{ cmd: "u" }], [["f.js", "one"]]), a);
|
|
61
|
+
assert.notEqual(evidenceHash("base2", [{ cmd: "t" }], [["f.js", "one"]]), a);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("покрытие: молчащая проверка отличается и от назвавшей, и от не смотревшей", async () => {
|
|
65
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
66
|
+
const res = [
|
|
67
|
+
{ name: "size", cmd: "bash check.sh .", out: "" }, // направлен в корень, промолчал
|
|
68
|
+
{ name: "tests", cmd: "node --test", out: "src/pay.js:1 ок" }, // назвал файл
|
|
69
|
+
];
|
|
70
|
+
const dirs = new Set(["."]);
|
|
71
|
+
const cov = coverage(["src/pay.js", "src/quiet.js"], res, (p) => dirs.has(p));
|
|
72
|
+
assert.deepEqual(cov.covered.get("src/pay.js"), ["tests"]);
|
|
73
|
+
assert.deepEqual(cov.silent.get("src/quiet.js"), ["size"]);
|
|
74
|
+
assert.deepEqual(cov.uncovered, []);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("покрытие: файл вне всех целей остаётся не тронутым никем", async () => {
|
|
78
|
+
const { coverage } = await import("../lib/evidence.mjs");
|
|
79
|
+
const res = [{ name: "size", cmd: "bash check.sh tool", out: "" }];
|
|
80
|
+
const cov = coverage(["tool/a.js", ".github/workflows/ci.yml"], res, (p) => p === "tool");
|
|
81
|
+
assert.deepEqual(cov.silent.get("tool/a.js"), ["size"]);
|
|
82
|
+
assert.deepEqual(cov.uncovered, [".github/workflows/ci.yml"]);
|
|
83
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// tool/selfcheck/units-learn.mjs — проверки разбора локальных логов сессий.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНО. Файл модульных проверок разводится по смыслу третий раз (после units-level и
|
|
4
|
+
// units-evidence) — предел в 500 строк держит наш же гейт file-size-limit. Здесь всё про то,
|
|
5
|
+
// как из переписки достаются кандидаты в правила, и ничего больше.
|
|
6
|
+
//
|
|
7
|
+
// node --test tool/selfcheck/units-learn.mjs
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
// Каталог логов зовётся по рабочему пути, где всё, кроме букв и цифр, заменено на дефис.
|
|
13
|
+
// Проверено на живой машине: /home/ser/projects/aqk → -home-ser-projects-aqk,
|
|
14
|
+
// /home/ser/projects/audit_project → -home-ser-projects-audit-project.
|
|
15
|
+
test("logSlug: путь проекта превращается в имя каталога логов", async () => {
|
|
16
|
+
const { logSlug } = await import("../commands/learn.mjs");
|
|
17
|
+
assert.equal(logSlug("/home/ser/projects/aqk"), "-home-ser-projects-aqk");
|
|
18
|
+
assert.equal(logSlug("/home/ser/projects/audit_project"), "-home-ser-projects-audit-project");
|
|
19
|
+
assert.equal(logSlug("C:\\work\\my.app"), "c-work-my-app");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Отбор наставлений. Меряно на 1619 уникальных напечатанных репликах: маркеры дают 79 штук,
|
|
23
|
+
// то есть 4%. Точность неполная и названа вслух — это список кандидатов, а не находок.
|
|
24
|
+
test("looksLikeRule: наставление отличается от обычной реплики", async () => {
|
|
25
|
+
const { looksLikeRule } = await import("../commands/learn.mjs");
|
|
26
|
+
assert.equal(looksLikeRule("файл не трогай AI_main_inst.md"), true);
|
|
27
|
+
assert.equal(looksLikeRule("делай прогон с базой обязательно"), true);
|
|
28
|
+
assert.equal(looksLikeRule("never commit secrets"), true);
|
|
29
|
+
assert.equal(looksLikeRule("ок го дальше"), false);
|
|
30
|
+
assert.equal(looksLikeRule("а что там по отчёту"), false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Длинная вставка наставлением не считается: в логе лежат и вставленный вывод команд, и куски
|
|
34
|
+
// файлов. Первый прогон без этого отсева выдал «agent quality kit» 44 раза — то есть пути и
|
|
35
|
+
// ссылки, а не правила.
|
|
36
|
+
test("looksLikeRule: длинная вставка и код не считаются правилом", async () => {
|
|
37
|
+
const { looksLikeRule } = await import("../commands/learn.mjs");
|
|
38
|
+
assert.equal(looksLikeRule("никогда " + "x".repeat(500)), false);
|
|
39
|
+
assert.equal(looksLikeRule("нельзя\n```\ncode\n```"), false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Главное утверждение команды: сказано вслух и НЕ записано. Слово из наставления, которого нет
|
|
43
|
+
// в точке входа, — повод завести правило; совпавшее — повод не шуметь.
|
|
44
|
+
test("saidNotWritten: правило, уже стоящее в точке входа, не показывается", async () => {
|
|
45
|
+
const { saidNotWritten } = await import("../commands/learn.mjs");
|
|
46
|
+
const entry = "# правила\n- Секреты никогда не попадают в код.\n";
|
|
47
|
+
assert.equal(saidNotWritten("никогда не коммить секреты в код", entry), false);
|
|
48
|
+
assert.equal(saidNotWritten("делай прогон с базой обязательно", entry), true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Русский язык склоняет. «локальный костыль» в реплике и «до местного костыля» в своде — одно и
|
|
52
|
+
// то же правило, и по целому слову они не совпадают. Найдено живым прогоном: без сверки по
|
|
53
|
+
// основе первым же кандидатом вышло правило, внесённое в точку входа в тот же день.
|
|
54
|
+
test("saidNotWritten: склонение не делает записанное правило незаписанным", async () => {
|
|
55
|
+
const { saidNotWritten } = await import("../commands/learn.mjs");
|
|
56
|
+
const entry = "- Любое сомнение проверяется снаружи, чтобы не выдумать местного костыля.";
|
|
57
|
+
assert.equal(saidNotWritten("любое сомнение проверяем всегда чтобы не сделать костыль", entry), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Отбор по promptSource — то, ради чего команда вообще работает. В логе 25 353 записи `user`,
|
|
61
|
+
// из них человеком напечатано 1912; остальное результаты инструментов, служебные вставки и
|
|
62
|
+
// принятые подсказки. Первая версия отбирала по длине и языку и выдавала вставленные пути.
|
|
63
|
+
test("typedFrom: берётся только напечатанное человеком", async () => {
|
|
64
|
+
const { typedFrom } = await import("../commands/learn.mjs");
|
|
65
|
+
const lines = [
|
|
66
|
+
JSON.stringify({ type: "user", promptSource: "typed", timestamp: "2026-09-08T10:00:00Z", message: { role: "user", content: "никогда так не делай" } }),
|
|
67
|
+
JSON.stringify({ type: "user", promptSource: "system", message: { role: "user", content: "служебное" } }),
|
|
68
|
+
JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "tool_result", content: "вывод" }] } }),
|
|
69
|
+
JSON.stringify({ type: "assistant", message: { role: "assistant", content: "ответ" } }),
|
|
70
|
+
"не json вовсе",
|
|
71
|
+
"",
|
|
72
|
+
].join("\n");
|
|
73
|
+
const got = typedFrom(lines);
|
|
74
|
+
assert.equal(got.length, 1);
|
|
75
|
+
assert.equal(got[0].text, "никогда так не делай");
|
|
76
|
+
assert.equal(got[0].when, "2026-09-08");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Текст блоками, а не строкой: так приходит реплика с приложенным файлом. Берём текстовые
|
|
80
|
+
// блоки и только их — картинка и результат инструмента правилом быть не могут.
|
|
81
|
+
test("typedFrom: реплика блоками собирается из текстовых блоков", async () => {
|
|
82
|
+
const { typedFrom } = await import("../commands/learn.mjs");
|
|
83
|
+
const line = JSON.stringify({
|
|
84
|
+
type: "user", promptSource: "typed", timestamp: "2026-09-08T11:00:00Z",
|
|
85
|
+
message: { role: "user", content: [{ type: "image" }, { type: "text", text: "всегда так" }] },
|
|
86
|
+
});
|
|
87
|
+
assert.equal(typedFrom(line)[0].text, "всегда так");
|
|
88
|
+
});
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// tool/selfcheck/units-level.mjs — проверки уровня и доказательства гейтов.
|
|
2
2
|
//
|
|
3
3
|
// ОТДЕЛЬНЫМ ФАЙЛОМ, а не в units.mjs: тот перерос собственный предел в 500 строк, и поймал
|
|
4
|
-
// это наш же гейт `file-size-limit` на прогоне. Шов по смыслу: здесь
|
|
5
|
-
//
|
|
4
|
+
// это наш же гейт `file-size-limit` на прогоне. Шов по смыслу: здесь всё, что программа
|
|
5
|
+
// вычитывает ИЗ МАНИФЕСТА и объявляет о проекте, — ступень, доказательство гейтов и раскладка
|
|
6
|
+
// (где правила, методички, точка входа). Общее у них одно и важное: ответ обязан приходить из
|
|
7
|
+
// манифеста, а не из умолчаний, совпадающих с нашими собственными значениями.
|
|
6
8
|
//
|
|
7
9
|
// node --test tool/selfcheck/units-level.mjs
|
|
8
10
|
import test from "node:test";
|
|
9
11
|
import assert from "node:assert/strict";
|
|
10
12
|
import { commandFor } from "../lib/prove.mjs";
|
|
11
|
-
import { assessLevel } from "../lib/manifest.mjs";
|
|
13
|
+
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS } from "../lib/manifest.mjs";
|
|
12
14
|
|
|
13
15
|
// --- доказательство гейтов ------------------------------------------------------------
|
|
14
16
|
// ЗАЧЕМ. Ступень AQK-2 называлась «гейты доказаны» и проверяла существование двух папок.
|
|
@@ -58,3 +60,63 @@ test("обе обёртки снимаются вместе", () => {
|
|
|
58
60
|
"bash gates/_native.sh gates/x/green ruff check gates/x/green"
|
|
59
61
|
);
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
// Отзыв второго пользователя, 2026-09-08: на Windows `prove` объявил два ИСПРАВНЫХ гейта
|
|
65
|
+
// сломанными. Путь к образцу собирался `path.join`, то есть `gates\x\red`, и уезжал в строку
|
|
66
|
+
// команды — а её исполняет `sh`, который обратный слэш съедает как экранирование: остаётся
|
|
67
|
+
// `gatesxred`. Каталога нет → `find` молчит → код 0 → «промолчал на КРАСНОМ образце».
|
|
68
|
+
// Проверка идёт здесь, а не в самом сборщике пути: `commandFor` — единственная дверь, через
|
|
69
|
+
// которую каталог попадает в оболочку, и закрывать её надо там, кто бы путь ни собрал.
|
|
70
|
+
test("каталог образца уходит в оболочку с прямыми слэшами", () => {
|
|
71
|
+
assert.equal(
|
|
72
|
+
commandFor("bash gates/x/check.sh .", "gates\\x\\red"),
|
|
73
|
+
"bash gates/x/check.sh gates/x/red",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Тот же путь едет ВТОРЫМ адресом — первым аргументом фильтра образцов. Пропустить его значит
|
|
78
|
+
// починить половину: фильтр не узнает образец и спрячет ровно то, что образец обязан показать.
|
|
79
|
+
test("обёртка родного инструмента тоже получает прямые слэши", () => {
|
|
80
|
+
assert.equal(
|
|
81
|
+
commandFor("bash gates/_native.sh . npx knip --directory .", "gates\\x\\red"),
|
|
82
|
+
"bash gates/_native.sh gates/x/red npx knip --directory gates/x/red",
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// --- где у проекта лежат правила и методички ----------------------------------
|
|
87
|
+
// Отзыв второго пользователя, 2026-09-08: `doctor` рисовал два красных креста за сделанное.
|
|
88
|
+
// У проекта `rules: .temper/rules`, правила на месте, гейт entry-links-exist их видит, уровень
|
|
89
|
+
// AQK-1 считается ПО МАНИФЕСТУ — а список в шапке проверял литеральные `.aqk/rules` и
|
|
90
|
+
// `.aqk/docs` и советовал сделать сделанное. Уровень и вывод расходились в разные стороны:
|
|
91
|
+
// хуже неверного вывода только вывод, который расходится с собственным вердиктом.
|
|
92
|
+
test("каталог правил берётся из манифеста, а не из умолчания", () => {
|
|
93
|
+
const paths = layoutChecks({ rules: ".temper/rules" }, false).map(([p]) => p);
|
|
94
|
+
assert.ok(paths.includes(".temper/rules"), "путь из манифеста обязан попасть в список");
|
|
95
|
+
assert.ok(!paths.includes(".aqk/rules"), "умолчание обязано уступить манифесту");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Поля `docs:` не было вовсе: перенести методички было НЕКУДА, и проект, разложивший их иначе,
|
|
99
|
+
// получал крест без единого способа его снять. Умолчание остаётся для тех, кто поля не завёл.
|
|
100
|
+
test("каталог методичек тоже берётся из манифеста", () => {
|
|
101
|
+
const paths = layoutChecks({ docs: ".temper/docs" }, false).map(([p]) => p);
|
|
102
|
+
assert.ok(paths.includes(".temper/docs"));
|
|
103
|
+
assert.ok(!paths.includes(".aqk/docs"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("без манифеста остаются умолчания", () => {
|
|
107
|
+
const paths = layoutChecks(null, false).map(([p]) => p);
|
|
108
|
+
assert.ok(paths.includes(".aqk/rules") && paths.includes(".aqk/docs"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// В самом комплекте лежат оригиналы, а не разложенная копия: копия завтра разошлась бы с ними.
|
|
112
|
+
test("внутри комплекта проверяются его собственные каталоги", () => {
|
|
113
|
+
const paths = layoutChecks({ rules: "kit/rules" }, true).map(([p]) => p);
|
|
114
|
+
assert.ok(paths.includes("kit/rules") && paths.includes("kit/docs"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Поле, которое программа читает, обязано быть в списке известных: иначе манифест с ним
|
|
118
|
+
// получает предупреждение «неизвестное поле» за то, что работает.
|
|
119
|
+
test("docs — известное поле манифеста", () => {
|
|
120
|
+
assert.ok(KNOWN_KEYS.includes("docs"));
|
|
121
|
+
assert.deepEqual(unknownKeys({ docs: ".aqk/docs" }), []);
|
|
122
|
+
});
|
package/tool/selfcheck/units.mjs
CHANGED