agent-quality-kit 0.12.0 → 0.13.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 +26 -4
- package/README.ru.md +26 -4
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -0
- package/kit/gates/env-secrets-not-committed/README.md +73 -0
- package/kit/gates/env-secrets-not-committed/check.sh +139 -0
- package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
- package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
- package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
- package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
- package/kit/gates/env-secrets-not-committed/red/.env +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +4 -0
- package/kit/gates/swallowed-error/gate.yml +4 -0
- package/kit/gates/todo-without-task/gate.yml +4 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +56 -3
- package/tool/commands/doctor.mjs +136 -81
- package/tool/commands/probe.mjs +46 -66
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +15 -11
- package/tool/i18n/en-docs.mjs +22 -1
- package/tool/i18n/en-gates.mjs +6 -1
- package/tool/i18n/en.mjs +13 -1
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +13 -1
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +11 -1
- package/tool/lib/core.mjs +39 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +34 -5
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/repo.mjs +2 -31
- package/tool/lib/run.mjs +78 -7
- package/tool/selfcheck/smoke/first-run.test.mjs +105 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +51 -2
- package/tool/selfcheck/smoke.sh +6 -2
- package/tool/selfcheck/units-cadence.mjs +15 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-level.mjs +76 -2
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-repo.mjs +25 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
|
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
|
|
|
12
12
|
import { commandFor, verdict } from "../lib/prove.mjs";
|
|
13
13
|
import { assessLevel, layoutChecks, unknownKeys, KNOWN_KEYS, parseManifest, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
14
14
|
import { pickLang, langFromText } from "../i18n/index.mjs";
|
|
15
|
-
import { progress } from "../lib/run.mjs";
|
|
15
|
+
import { progress, selectGates } from "../lib/run.mjs";
|
|
16
16
|
|
|
17
17
|
// --- строка «идёт» во время прогона ---------------------------------------------------
|
|
18
18
|
// ЗАЧЕМ. Гейт идёт через spawnSync, и строка про него печаталась только по завершении: минута
|
|
@@ -249,7 +249,7 @@ test("заявка подтверждена, когда коды правил е
|
|
|
249
249
|
test("заявка не подтверждена, когда кодов нет нигде", () => {
|
|
250
250
|
const man = parseManifest('gates:\n lint: "ruff check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
251
251
|
const catalog = [{ slug: "no-print-in-prod", recipes: { python: "ruff check --select T20 {dir}" } }];
|
|
252
|
-
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"] }]);
|
|
252
|
+
assert.deepEqual(coversUnproven(man, catalog, ""), [{ entry: "no-print-in-prod", gate: "lint", codes: ["T20"], linter: "ruff", kind: "unproven" }]);
|
|
253
253
|
});
|
|
254
254
|
|
|
255
255
|
// Правило может стоять не в команде, а в конфиге линтера — это нормальный уклад, и краснеть
|
|
@@ -267,6 +267,49 @@ test("запись без кодов правил в рецепте не пор
|
|
|
267
267
|
assert.deepEqual(coversUnproven(man, catalog, ""), []);
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
// --- заявка сверяется правилами ТОГО линтера, которым закрыт гейт --------------------
|
|
271
|
+
// Отзыв с живого проекта 2026-09-11 (TypeScript на Biome): заявка «lint держит no-print-in-prod»
|
|
272
|
+
// всегда была «не подтверждена» — комплект искал коды ruff (T20, C901, BLE), а у Biome это
|
|
273
|
+
// noConsole, noExcessiveCognitiveComplexity, noEmptyBlockStatements, и они стояли. Поле, которое
|
|
274
|
+
// должно снимать шум, само его производило и подталкивало ставить второй линтер.
|
|
275
|
+
const NP = { slug: "no-print-in-prod", biome_rules: "noConsole",
|
|
276
|
+
recipes: { python: "ruff check --select T20 {dir}", javascript: `eslint --rule '{"no-console":"error"}' {dir}` } };
|
|
277
|
+
const TODO = { slug: "todo-without-task", biome_rules: "none", recipes: { python: "ruff check --select FIX,TD {dir}" } };
|
|
278
|
+
|
|
279
|
+
test("Biome: правило записи стоит в biome.json — заявка подтверждена", () => {
|
|
280
|
+
const man = parseManifest('gates:\n lint: "npx biome check ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
281
|
+
const biome = '{"linter":{"rules":{"suspicious":{"noConsole":"error"}}}}';
|
|
282
|
+
assert.deepEqual(coversUnproven(man, [NP], { biome }), []);
|
|
283
|
+
const miss = coversUnproven(man, [NP], { biome: '{"linter":{"enabled":true}}' });
|
|
284
|
+
assert.equal(miss[0].kind, "unproven");
|
|
285
|
+
assert.deepEqual(miss[0].codes, ["noConsole"], "человеку названо ПРАВИЛО BIOME, а не код ruff");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("линтер гейта узнаётся и через npm-скрипт", () => {
|
|
289
|
+
const man = parseManifest('gates:\n lint: "npm run lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
290
|
+
const scripts = { lint: "biome check ." };
|
|
291
|
+
const r = coversUnproven(man, [NP], { biome: '"noConsole": "error"', scripts });
|
|
292
|
+
assert.deepEqual(r, []);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("eslint: правило берётся из рецепта записи и ищется в конфиге eslint", () => {
|
|
296
|
+
const man = parseManifest('gates:\n lint: "eslint ."\ncovers:\n lint: [no-print-in-prod]\n');
|
|
297
|
+
assert.deepEqual(coversUnproven(man, [NP], { eslint: "rules: { 'no-console': 'error' }" }), []);
|
|
298
|
+
assert.equal(coversUnproven(man, [NP], { eslint: "rules: {}" })[0].kind, "unproven");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("у линтера нет такого правила вовсе — заявка заведомо неверна, а не «не подтверждена»", () => {
|
|
302
|
+
const man = parseManifest('gates:\n lint: "biome check ."\ncovers:\n lint: [todo-without-task]\n');
|
|
303
|
+
const r = coversUnproven(man, [TODO], { biome: "{}" });
|
|
304
|
+
assert.equal(r[0].kind, "impossible");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("линтер не распознан — «не умею проверить», а не обвинение", () => {
|
|
308
|
+
const man = parseManifest('gates:\n lint: "make lint"\ncovers:\n lint: [no-print-in-prod]\n');
|
|
309
|
+
const r = coversUnproven(man, [NP], {});
|
|
310
|
+
assert.equal(r[0].kind, "unknown");
|
|
311
|
+
});
|
|
312
|
+
|
|
270
313
|
// --- строка манифеста, которую разбор не понял, не исчезает молча ---------------
|
|
271
314
|
// Найдено 2026-09-09 случайно: подсаживал падающий гейт с именем «плохой», чтобы посмотреть
|
|
272
315
|
// на строку присутствия, — и прогон вышел с НУЛЁМ. Гейт не упал: его вообще не было. Разбор
|
|
@@ -321,3 +364,34 @@ test("сбой арбитра отнимает ступень, законная
|
|
|
321
364
|
|
|
322
365
|
assert.equal(verdict([{ state: "proven" }, { state: "broken" }]).ok, false);
|
|
323
366
|
});
|
|
367
|
+
|
|
368
|
+
// --- группы гейтов и --only / --skip ----------------------------------------------------
|
|
369
|
+
// ЗАЧЕМ. Отзыв с живого проекта 2026-09-11: гейт цены меряет план на засеянной базе. Объявишь
|
|
370
|
+
// его — он валит прогон без стенда; уберёшь — promise-has-gate справедливо ругается. Пришлось
|
|
371
|
+
// выкручиваться через advisory. Прогон бывал только «всё или ничего».
|
|
372
|
+
test("выбор гейтов: --skip группой и именем, --only сужает, пропущенные названы", () => {
|
|
373
|
+
const man = parseManifest('gates:\n lint: "x"\n unit: "x"\n cost: "x"\n e2e: "x"\ngroups:\n stack: [cost, e2e]\n');
|
|
374
|
+
const names = Object.entries(man.gates);
|
|
375
|
+
const s = selectGates(names, man, { skip: ["stack"] });
|
|
376
|
+
assert.deepEqual(s.run.map(([n]) => n), ["lint", "unit"]);
|
|
377
|
+
assert.deepEqual(s.skipped, ["cost", "e2e"]);
|
|
378
|
+
const o = selectGates(names, man, { only: ["lint", "cost"] });
|
|
379
|
+
assert.deepEqual(o.run.map(([n]) => n), ["lint", "cost"]);
|
|
380
|
+
assert.deepEqual(o.skipped, ["unit", "e2e"]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("выбор гейтов: неизвестное имя — ошибка, а не тихое «пропустили ничего»", () => {
|
|
384
|
+
const man = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [lint]\n');
|
|
385
|
+
const s = selectGates(Object.entries(man.gates), man, { skip: ["stak"] });
|
|
386
|
+
assert.deepEqual(s.unknown, ["stak"]);
|
|
387
|
+
// Группа, в которой назван необъявленный гейт, — тоже названа: иначе группа молча пустеет.
|
|
388
|
+
const man2 = parseManifest('gates:\n lint: "x"\ngroups:\n stack: [cots]\n');
|
|
389
|
+
assert.deepEqual(selectGates(Object.entries(man2.gates), man2, { skip: ["stack"] }).unknown, ["cots"]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("выбор гейтов: без флагов гоняется всё, как раньше", () => {
|
|
393
|
+
const man = parseManifest('gates:\n a: "x"\n b: "x"\n');
|
|
394
|
+
const s = selectGates(Object.entries(man.gates), man, {});
|
|
395
|
+
assert.equal(s.run.length, 2);
|
|
396
|
+
assert.deepEqual(s.skipped, []);
|
|
397
|
+
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { isFix, fixHotspots, probeSummary, probeVerdictPaired, countProbe } from "../lib/history.mjs";
|
|
6
|
-
import { probeableGates, gatesState, extAlternatives, planProbeGates,
|
|
6
|
+
import { probeableGates, gatesState, extAlternatives, planProbeGates, isCode } from "../commands/probe.mjs";
|
|
7
|
+
import { blindAdvice } from "../lib/advice.mjs";
|
|
7
8
|
|
|
8
9
|
// Признак починки берётся из ТЕМЫ коммита, а не из тела: тема — единственное, что пишут все,
|
|
9
10
|
// и единственное, что видно в `git log --oneline`. Три написания, потому что репозитории
|
|
@@ -8,7 +8,8 @@
|
|
|
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, isApiSpec
|
|
11
|
+
import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec } from "../lib/repo.mjs";
|
|
12
|
+
import { startWith } from "../lib/advice.mjs";
|
|
12
13
|
import { proposeGates } from "../lib/adopt.mjs";
|
|
13
14
|
import { CATALOGS, L } from "../i18n/index.mjs";
|
|
14
15
|
import { dirname } from "node:path";
|
|
@@ -284,3 +285,26 @@ test("с чего начать: при равенстве признаков п
|
|
|
284
285
|
];
|
|
285
286
|
assert.deepEqual(startWith(list, { langs: new Set() }, 2).map((e) => e.slug), ["a", "b"]);
|
|
286
287
|
});
|
|
288
|
+
|
|
289
|
+
// Совет под линтер ПРОЕКТА, а не под язык. Отзыв с живого проекта 2026-09-11: «начни с этих
|
|
290
|
+
// трёх» советовал завести eslint проекту на Biome — комплект читал package.json и Makefile, но не
|
|
291
|
+
// biome.json. Команда для Biome — одно правило разово (`--only`), и `--error-on-warnings`
|
|
292
|
+
// обязателен: правило вне рекомендованных Biome ставит на «предупреждение», и без флага команда
|
|
293
|
+
// выходила с нулём, напечатав находку. Проверено на Biome 2.5.12.
|
|
294
|
+
test("совет проекту на Biome: команда Biome, а не eslint", async () => {
|
|
295
|
+
const { blindAdvice } = await import("../lib/advice.mjs");
|
|
296
|
+
const entry = {
|
|
297
|
+
slug: "no-print-in-prod", biome_rules: "suspicious/noConsole",
|
|
298
|
+
tool: "https://github.com/eslint/eslint",
|
|
299
|
+
recipes: { javascript: `eslint --no-config-lookup --rule '{"no-console":"error"}' {dir}` },
|
|
300
|
+
};
|
|
301
|
+
const biome = blindAdvice(entry, { langs: new Set(["typescript"]), has_biome: true });
|
|
302
|
+
assert.equal(biome.command, "npx @biomejs/biome lint --error-on-warnings --only=suspicious/noConsole .");
|
|
303
|
+
assert.equal(biome.tool, "https://github.com/biomejs/biome");
|
|
304
|
+
// Без biome.json — прежний совет под язык.
|
|
305
|
+
const plain = blindAdvice(entry, { langs: new Set(["javascript"]), has_biome: false });
|
|
306
|
+
assert.match(plain.command, /^eslint /);
|
|
307
|
+
// У Biome правила нет вовсе (`none`) — прежний совет, а не выдуманная команда Biome.
|
|
308
|
+
const none = blindAdvice({ ...entry, biome_rules: "none" }, { langs: new Set(["javascript"]), has_biome: true });
|
|
309
|
+
assert.match(none.command, /^eslint /);
|
|
310
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// tool/selfcheck/units-verdict.mjs — вердикт пробы: «поймано» только ПО ДЕЛУ.
|
|
2
|
+
//
|
|
3
|
+
// ОТДЕЛЬНЫМ ФАЙЛОМ: units-probe.mjs у предела в 500 строк, и шов настоящий — там разбор
|
|
4
|
+
// истории и план пробы, здесь то, на чём держится главное обещание пробы.
|
|
5
|
+
//
|
|
6
|
+
// ЗАЧЕМ. Отзыв с живого проекта 2026-09-11 (Amplifie, TypeScript на Biome): класс «цвет из
|
|
7
|
+
// токена темы» проба отметила пойманным линтером, хотя Biome цвета не проверяет. Подсаженный
|
|
8
|
+
// кусок сломал форматирование — упал форматтер. Вердикт считал «покраснел» равным «поймал»:
|
|
9
|
+
// код возврата сравнивался, вывод выбрасывался. Итог — «every applicable class is caught by
|
|
10
|
+
// something», ложная уверенность ровно в сторону самоуспокоения. К себе комплект был мягче,
|
|
11
|
+
// чем к проекту: от чужого гейта мы требуем покраснеть НА БРАКЕ, а свой вердикт этого не
|
|
12
|
+
// проверял.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { namesPlant, probeVerdictPaired, catchVerdict } from "../lib/history.mjs";
|
|
16
|
+
|
|
17
|
+
test("назвал подсаженный файл: путь появился в выводе после подсадки", () => {
|
|
18
|
+
assert.equal(namesPlant("", "src/ui/card.tsx:12:5 lint/style/noColorLiteral", "src/ui/card.tsx"), true);
|
|
19
|
+
// Любая форма пути: относительная с ./, абсолютная из песочницы, с обратными слешами Windows.
|
|
20
|
+
assert.equal(namesPlant("", "./src/ui/card.tsx:3 print found", "src/ui/card.tsx"), true);
|
|
21
|
+
assert.equal(namesPlant("", "/tmp/aqk-probe-x/src/ui/card.tsx:3", "src/ui/card.tsx"), true);
|
|
22
|
+
assert.equal(namesPlant("", "src\\ui\\card.tsx(3,1): error", "src/ui/card.tsx"), true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("не назвал: упал по своей причине — форматтер, чужой файл, общий итог", () => {
|
|
26
|
+
assert.equal(namesPlant("", "Formatter would have printed the following content.\nFound 1 error.", "src/ui/card.tsx"), false);
|
|
27
|
+
assert.equal(namesPlant("", "src/other.tsx:1 error", "src/ui/card.tsx"), false);
|
|
28
|
+
// Файл упоминался и до подсадки столько же раз — нового ничего не сказано.
|
|
29
|
+
assert.equal(namesPlant("src/ui/card.tsx:1 warn", "src/ui/card.tsx:1 warn", "src/ui/card.tsx"), false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const pairs = (before, after) => ({
|
|
33
|
+
before: before.map(([name, code]) => ({ name, code })),
|
|
34
|
+
after: after.map(([name, code, named]) => ({ name, code, named })),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("покраснел, но подсаженного файла не назвал — «неизвестно», а не «поймано»", () => {
|
|
38
|
+
const { before, after } = pairs([["lint", 0], ["units", 0]], [["lint", 1, false], ["units", 0]]);
|
|
39
|
+
const r = probeVerdictPaired(before, after);
|
|
40
|
+
assert.equal(r.verdict, "unknown");
|
|
41
|
+
assert.equal(r.unattributed, 1);
|
|
42
|
+
assert.equal(r.caught, 0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("поймал по делу хоть один — «поймано»; безымянное падение соседа этого не отменяет", () => {
|
|
46
|
+
const { before, after } = pairs([["fmt", 0], ["lint", 0]], [["fmt", 1, false], ["lint", 1, true]]);
|
|
47
|
+
const r = probeVerdictPaired(before, after);
|
|
48
|
+
assert.equal(r.verdict, "caught");
|
|
49
|
+
assert.equal(r.caught, 1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("без сведений о выводе (старые вызовы) поведение прежнее", () => {
|
|
53
|
+
const { before, after } = pairs([["lint", 0]], [["lint", 1, undefined]]);
|
|
54
|
+
assert.equal(probeVerdictPaired(before, after).verdict, "caught");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ИМЕНИ ФАЙЛА МАЛО. Biome, падая на форматировании, тоже называет файл: «src/card.tsx format».
|
|
58
|
+
// Отличить «упал на браке» от «упал на подсадке» можно только ПАРОЙ — ровно тем, чем мы проверяем
|
|
59
|
+
// чужие гейты: в то же место кладётся ЗЕЛЁНЫЙ образец той же записи. Краснеет и на нём — гейт
|
|
60
|
+
// падает от самой подсадки (форматирование, синтаксис), поимка не доказана.
|
|
61
|
+
test("пара: на красном назвал файл, на зелёном молчит — поймал", () => {
|
|
62
|
+
const red = { code: 1, out: "src/card.tsx:3 lint/noColorLiteral" };
|
|
63
|
+
const green = { code: 0, out: "" };
|
|
64
|
+
assert.equal(catchVerdict("", red, green, "src/card.tsx"), "caught");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("пара: краснеет и на зелёном, называя тот же файл, — падает от подсадки, не поймал", () => {
|
|
68
|
+
const red = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
|
|
69
|
+
const green = { code: 1, out: "src/card.tsx format ━━━ Formatter would have printed" };
|
|
70
|
+
assert.equal(catchVerdict("", red, green, "src/card.tsx"), "planting");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("пара: файла не назвал — «безымянно»; зелёного образца под расширение нет — судим по имени", () => {
|
|
74
|
+
assert.equal(catchVerdict("", { code: 1, out: "Found 1 error." }, { code: 0, out: "" }, "src/a.py"), "nameless");
|
|
75
|
+
assert.equal(catchVerdict("", { code: 1, out: "src/a.py:1 T201" }, null, "src/a.py"), "caught");
|
|
76
|
+
});
|