agent-quality-kit 0.10.1 → 0.12.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.
Files changed (67) hide show
  1. package/README.md +10 -3
  2. package/README.ru.md +10 -3
  3. package/kit/gates/_native.sh +4 -0
  4. package/kit/gates/_skip.sh +7 -0
  5. package/kit/gates/api-contract-has-arbiter/check.sh +7 -0
  6. package/kit/gates/color-from-token/check.sh +7 -0
  7. package/kit/gates/commit-explains-itself/check.sh +8 -2
  8. package/kit/gates/complexity-limit/check.sh +22 -4
  9. package/kit/gates/complexity-limit/gate.yml +7 -2
  10. package/kit/gates/complexity-limit/red/deep.js +15 -0
  11. package/kit/gates/dead-code/gate.yml +5 -0
  12. package/kit/gates/duplicate-code/check.sh +7 -0
  13. package/kit/gates/duplicate-code/gate.yml +10 -1
  14. package/kit/gates/file-size-limit/check.sh +7 -0
  15. package/kit/gates/file-size-limit/red/big.js +600 -0
  16. package/kit/gates/gate-not-weakened/check.sh +7 -0
  17. package/kit/gates/gate-not-weakened/green/suppress.js +4 -0
  18. package/kit/gates/gate-not-weakened/red/suppress.js +5 -0
  19. package/kit/gates/mcp-server-resolves/check.sh +7 -0
  20. package/kit/gates/no-phantom-package/check.sh +7 -0
  21. package/kit/gates/no-print-in-prod/gate.yml +8 -3
  22. package/kit/gates/personal-config-not-shared/check.sh +7 -0
  23. package/kit/gates/secrets-not-in-code/check.sh +7 -0
  24. package/kit/gates/secrets-not-in-code/gate.yml +20 -0
  25. package/kit/gates/secrets-not-in-code/green/config.js +4 -0
  26. package/kit/gates/secrets-not-in-code/red/leak.js +6 -0
  27. package/kit/gates/swallowed-error/gate.yml +8 -3
  28. package/kit/gates/test-has-assertion/check.sh +7 -0
  29. package/kit/gates/test-has-assertion/green/checkout.test.js +5 -0
  30. package/kit/gates/test-has-assertion/red/checkout.test.js +8 -0
  31. package/kit/gates/test-not-adjusted/check.sh +8 -2
  32. package/kit/gates/todo-without-task/check.sh +7 -0
  33. package/kit/gates/todo-without-task/gate.yml +7 -2
  34. package/kit/gates/todo-without-task/green/app.js +2 -0
  35. package/kit/gates/todo-without-task/red/later.js +4 -0
  36. package/llms.txt +4 -2
  37. package/package.json +1 -1
  38. package/tool/commands/badge.mjs +1 -1
  39. package/tool/commands/context.mjs +3 -1
  40. package/tool/commands/doctor.mjs +94 -152
  41. package/tool/commands/gates.mjs +3 -2
  42. package/tool/commands/probe.mjs +284 -44
  43. package/tool/commands/report.mjs +1 -1
  44. package/tool/commands/vitals.mjs +6 -1
  45. package/tool/i18n/en-docs.mjs +2 -1
  46. package/tool/i18n/en-gates.mjs +23 -5
  47. package/tool/i18n/en.mjs +16 -0
  48. package/tool/i18n/ru-docs.mjs +2 -1
  49. package/tool/i18n/ru-gates.mjs +24 -6
  50. package/tool/i18n/ru.mjs +16 -0
  51. package/tool/lib/adopt.mjs +98 -0
  52. package/tool/lib/cadence.mjs +36 -1
  53. package/tool/lib/execution.mjs +50 -1
  54. package/tool/lib/history.mjs +89 -8
  55. package/tool/lib/prove.mjs +2 -2
  56. package/tool/lib/repo.mjs +47 -0
  57. package/tool/lib/run.mjs +194 -0
  58. package/tool/selfcheck/gates.sh +42 -4
  59. package/tool/selfcheck/smoke/_fixture.mjs +24 -2
  60. package/tool/selfcheck/smoke/fail-closed.test.mjs +112 -0
  61. package/tool/selfcheck/smoke/own-samples.test.mjs +131 -0
  62. package/tool/selfcheck/units-cadence.mjs +37 -1
  63. package/tool/selfcheck/units-execution.mjs +78 -1
  64. package/tool/selfcheck/units-level.mjs +24 -0
  65. package/tool/selfcheck/units-probe.mjs +385 -22
  66. package/tool/selfcheck/units-repo.mjs +123 -1
  67. package/tool/selfcheck/units.mjs +34 -1
@@ -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 } from "../lib/repo.mjs";
11
+ import { triggerVerdict, recipeFor, EXT_LANG, whichSync, browserServerAdvice, MARKS, isApiSpec, startWith } from "../lib/repo.mjs";
12
+ import { proposeGates } from "../lib/adopt.mjs";
12
13
  import { CATALOGS, L } from "../i18n/index.mjs";
13
14
  import { dirname } from "node:path";
14
15
 
@@ -162,3 +163,124 @@ test("у каждого признака репозитория есть объ
162
163
  // запись каталога, выключенная навсегда и молча. Сторожим ту сторону, которая ломает.
163
164
  }
164
165
  });
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // Чужие проверки, которые у проекта УЖЕ есть. Написано ДО кода 2026-09-10.
169
+ //
170
+ // ЗАЧЕМ. Поставил комплект в `express` — проект с eslint, mocha и конвейером — и первое, что он
171
+ // увидел: двадцать крестов подряд и «держит машина 0». Это неправда с его точки зрения: его
172
+ // проверки держат, просто мы считаем только СВОИ записи.
173
+ //
174
+ // Мы видим, что конвейер ЕСТЬ (`has_ci`), но не читаем, что в нём. Человек должен вручную
175
+ // переписать в манифест то, что мы могли прочитать сами. Отсюда и «не понимает, что хорошо»:
176
+ // хорошее у него уже есть, а мы о нём молчим.
177
+ //
178
+ // Предлагаем, а не объявляем: гейт, вписанный без спроса, — это чужое решение в чужом файле.
179
+ test("чужие проверки: из package.json берутся test, lint и проверка типов", () => {
180
+ const pkg = JSON.stringify({ scripts: {
181
+ test: "mocha", lint: "eslint .", typecheck: "tsc --noEmit",
182
+ build: "rollup -c", start: "node server.js", prepare: "husky",
183
+ } });
184
+ const got = proposeGates({ "package.json": pkg });
185
+ assert.deepEqual(got.map((g) => g.name).sort(), ["lint", "test", "typecheck"]);
186
+ assert.equal(got.find((g) => g.name === "test").cmd, "npm test");
187
+ assert.equal(got.find((g) => g.name === "lint").cmd, "npm run lint");
188
+ });
189
+
190
+ // `npm test` и `npm run test` — разные написания одного; берём каноничное. А `build` и `start`
191
+ // проверками не являются: они собирают и запускают, а не судят.
192
+ test("чужие проверки: сборка и запуск проверками не считаются", () => {
193
+ const pkg = JSON.stringify({ scripts: { build: "tsc", start: "node .", dev: "vite" } });
194
+ assert.deepEqual(proposeGates({ "package.json": pkg }), []);
195
+ });
196
+
197
+ // Makefile — вторая по частоте точка входа, и в python-проектах чаще первая.
198
+ test("чужие проверки: цели Makefile тоже видны", () => {
199
+ const mk = "install:\n\tpip install -e .\n\ntest:\n\tpytest -q\n\nlint:\n\truff check .\n";
200
+ const got = proposeGates({ Makefile: mk });
201
+ assert.deepEqual(got.map((g) => g.name).sort(), ["lint", "test"]);
202
+ assert.equal(got.find((g) => g.name === "test").cmd, "make test");
203
+ });
204
+
205
+ test("чужие проверки: нечего предложить — пустой список, а не выдумка", () => {
206
+ assert.deepEqual(proposeGates({}), []);
207
+ assert.deepEqual(proposeGates({ "package.json": "{ не json" }), []);
208
+ });
209
+
210
+ // Источник называется: человек обязан видеть, ОТКУДА мы это взяли, иначе предложение
211
+ // неотличимо от нашей догадки.
212
+ test("чужие проверки: у каждого предложения назван источник", () => {
213
+ const got = proposeGates({ "package.json": JSON.stringify({ scripts: { test: "jest" } }) });
214
+ assert.equal(got[0].source, "package.json");
215
+ });
216
+
217
+ // Python-проекты. Замер 2026-09-11 на восьми живых (requests, click, flask, httpx, black,
218
+ // fastapi, pydantic, rich): package.json нет ни у кого, Makefile у трёх — и пятеро из восьми
219
+ // слышали «у вас ничего нет». А `.pre-commit-config.yaml` лежит у семи, tox — у пяти (у click и
220
+ // flask в pyproject.toml), у httpx — исполняемые scripts/test и scripts/check.
221
+ // Куски ниже сняты с этих репозиториев, а не придуманы.
222
+ test("чужие проверки: pre-commit предлагается целиком, хуки названы поимённо", () => {
223
+ const cfg = "repos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n hooks:\n" +
224
+ " - id: check-yaml\n - id: end-of-file-fixer\n" +
225
+ " - repo: https://github.com/astral-sh/ruff-pre-commit\n hooks:\n - id: ruff-check\n";
226
+ const got = proposeGates({ ".pre-commit-config.yaml": cfg });
227
+ assert.equal(got.length, 1);
228
+ assert.equal(got[0].cmd, "pre-commit run --all-files");
229
+ assert.match(got[0].source, /ruff-check/, "человек видит, ЧТО там стоит, а не только файл");
230
+ });
231
+
232
+ test("чужие проверки: из tox берутся окружения-проверки, а не матрица версий", () => {
233
+ const ini = "[tox]\nenvlist = py{310,311}-{default}\n\n[testenv]\ncommands = pytest\n\n" +
234
+ "[testenv:lint]\ncommands = ruff check .\n\n[testenv:docs]\ncommands = sphinx-build\n\n[testenv:{,ci-}pypy3]\n";
235
+ assert.deepEqual(proposeGates({ "tox.ini": ini }).map((g) => g.cmd), ["tox -e lint"]);
236
+ // click и flask держат tox в pyproject.toml, новым синтаксисом.
237
+ const py = "[tool.tox]\nenv_list = [\"py3\"]\n\n[tool.tox.env.style]\ncommands = []\n\n" +
238
+ "[tool.tox.env.typing]\ncommands = []\n\n[tool.tox.env.docs]\ncommands = []\n";
239
+ const got = proposeGates({ "pyproject.toml": py });
240
+ assert.deepEqual(got.map((g) => g.cmd).sort(), ["tox -e style", "tox -e typing"]);
241
+ assert.equal(got[0].source, "pyproject.toml");
242
+ });
243
+
244
+ test("чужие проверки: исполняемые scripts/test и scripts/check — как у httpx", () => {
245
+ const got = proposeGates({ "scripts/test": "", "scripts/check": "", "scripts/publish": "" });
246
+ assert.deepEqual(got.map((g) => g.cmd).sort(), ["scripts/check", "scripts/test"]);
247
+ });
248
+
249
+ // ─────────────────────────────────────────────────────────────────────────────
250
+ // С чего начать: три записи вместо двадцати равнозначных крестов. Написано ДО кода.
251
+ //
252
+ // ЗАЧЕМ. После установки человек видит двадцать крестов одинаковой формы и не знает, за что
253
+ // взяться. Двадцать одинаковых требований — это ноль требований: закрывают первое попавшееся
254
+ // или не закрывают ничего.
255
+ //
256
+ // Порядок НЕ ПО НАШЕМУ ВКУСУ. Два признака, оба — факты, которые у нас уже есть:
257
+ // · запись родилась из настоящего отказа (`proof` ссылается на журнал шишек — тот же
258
+ // признак, которым каталог отделяет условную запись) — она про боль, которая случалась, а не про «хорошую практику»;
259
+ // · её можно закрыть ОДНОЙ ГОТОВОЙ КОМАНДОЙ — значит цена входа минутная.
260
+ // Сначала то, что и больно, и дёшево.
261
+ test("с чего начать: сперва рождённые из отказа и закрываемые одной командой", () => {
262
+ const list = [
263
+ { slug: "praktika", proof: "хорошая практика", recipes: { any: "bash {gate}/c.sh {dir}" } },
264
+ { slug: "bol-i-deshevo", proof: "incidents/README.md, 2026-09-01", recipes: { native: "gitleaks dir {dir}" } },
265
+ { slug: "bol-no-dorogo", proof: "incidents/README.md, 2026-08-02", recipes: { any: "bash {gate}/c.sh {dir}" } },
266
+ { slug: "deshevo", proof: "методичка", recipes: { python: "ruff check {dir}" } },
267
+ ];
268
+ assert.deepEqual(startWith(list, { langs: new Set(["python"]) }, 3).map((e) => e.slug),
269
+ ["bol-i-deshevo", "deshevo", "bol-no-dorogo"]);
270
+ });
271
+
272
+ test("с чего начать: список короче трёх не ломается", () => {
273
+ assert.deepEqual(startWith([], { langs: new Set() }, 3), []);
274
+ const one = [{ slug: "a", proof: "incidents/README.md", recipes: {} }];
275
+ assert.deepEqual(startWith(one, { langs: new Set() }, 3).map((e) => e.slug), ["a"]);
276
+ });
277
+
278
+ // Порядок обязан быть УСТОЙЧИВЫМ: одинаковый ввод — одинаковый ответ, иначе человек видит
279
+ // разный совет на двух прогонах подряд и перестаёт верить обоим.
280
+ test("с чего начать: при равенстве признаков порядок стабилен", () => {
281
+ const list = [
282
+ { slug: "b", proof: "incidents/README.md", recipes: {} },
283
+ { slug: "a", proof: "incidents/README.md", recipes: {} },
284
+ ];
285
+ assert.deepEqual(startWith(list, { langs: new Set() }, 2).map((e) => e.slug), ["a", "b"]);
286
+ });
@@ -12,7 +12,7 @@
12
12
  import test from "node:test";
13
13
  import assert from "node:assert/strict";
14
14
  import { parseManifest, manifestWithGate, unknownKeys, entryLifecycle, advisorySet, KNOWN_KEYS } from "../lib/manifest.mjs";
15
- import { triggerVerdict, recipeFor, stems, overlap, EXT_LANG, whichSync, browserServerAdvice, MARKS } from "../lib/repo.mjs";
15
+ import { triggerVerdict, recipeFor, pickRecipe, stems, overlap, EXT_LANG, whichSync, browserServerAdvice, MARKS } from "../lib/repo.mjs";
16
16
  import { scopeOutput, splitAdvice } from "../lib/scope.mjs";
17
17
  import { assessBaseline, ITEMS, BASELINE_TOTAL } from "../lib/baseline.mjs";
18
18
  import { CATALOGS, pickLang, L } from "../i18n/index.mjs";
@@ -398,3 +398,36 @@ test("наборы файлов правил совпадают на обоих
398
398
 
399
399
 
400
400
 
401
+
402
+ // ─────────────────────────────────────────────────────────────────────────────
403
+ // Рецепт, не привязанный к языку. Написано ДО кода 2026-09-10.
404
+ //
405
+ // ЗАЧЕМ. Ключи рецептов — языки: `python`, `javascript`, `go`. Это верно для линтеров, но
406
+ // неверно для инструментов, которым язык безразличен. `secrets-not-in-code` объявлен
407
+ // `always: true`, готовый аналог у него — `gitleaks`, и он ищет ключи в любом файле.
408
+ //
409
+ // Без такого ключа выбор один из двух, и оба плохие: продублировать `gitleaks` под восемь
410
+ // языков (тот самый повтор, против которого у нас есть гейт) или оставить запись без родного
411
+ // рецепта. Второе и было: замер на десяти проектах показал секреты в НЕПОКРЫТЫХ у axios и у
412
+ // всех четырёх python-проектов — то есть там, где польза наибольшая, команды для вставки не
413
+ // было.
414
+ //
415
+ // Порядок предпочтения: свой язык → безъязыковой родной → переносимый. Родной сильнее
416
+ // переносимого (у `gitleaks` сотни форматов токенов и чтение истории), но только если его есть
417
+ // чем выполнить: непоставленная программа даёт «not found», а это отсутствие сигнала, выданное
418
+ // за успех.
419
+ test("рецепт: безъязыковой родной берётся, когда своего языка нет", () => {
420
+ const rec = { recipes: { native: "true --scan", any: "bash {gate}/check.sh {dir}" } };
421
+ assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "true --scan");
422
+ });
423
+
424
+ test("рецепт: свой язык сильнее безъязыкового", () => {
425
+ const rec = { recipes: { python: "true --py", native: "true --scan", any: "bash x {dir}" } };
426
+ assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "true --py");
427
+ });
428
+
429
+ // Программы нет — родной не берётся ни в каком виде, иначе гейт встанет с «not found».
430
+ test("рецепт: безъязыковой без программы уступает переносимому", () => {
431
+ const rec = { recipes: { native: "aqk-nesuschestvuyuschiy --scan", any: "bash {gate}/check.sh {dir}" } };
432
+ assert.equal(pickRecipe(rec, { langs: new Set(["python"]) }), "bash {gate}/check.sh {dir}");
433
+ });