agent-quality-kit 0.9.0 → 0.10.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 +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 +92 -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 +28 -1
- package/tool/commands/doctor.mjs +56 -2
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/vitals.mjs +11 -3
- package/tool/i18n/en-docs.mjs +8 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +10 -281
- package/tool/i18n/ru-docs.mjs +8 -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/history.mjs +82 -0
- package/tool/lib/manifest.mjs +1 -1
- 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/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +179 -6
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +31 -1
- package/tool/selfcheck/units-vitals.mjs +19 -0
package/llms.txt
CHANGED
|
@@ -47,6 +47,13 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
47
47
|
config is an intention, not a guard), does the agent receive the state, is the version current.
|
|
48
48
|
Four states, not two: connected · BROKEN · not connected and that is a choice · could not look.
|
|
49
49
|
Only BROKEN affects the exit code
|
|
50
|
+
- See what your declared checks CANNOT see: `npx agent-quality-kit probe` — takes the files your
|
|
51
|
+
own fix history calls hot, plants a proven red sample from the catalogue into a copy of each,
|
|
52
|
+
and runs YOUR declared gates against it. Coverage is not declared, it is proven by planting.
|
|
53
|
+
Three states that never merge: caught · nothing catches it · nothing to check with. Measured on
|
|
54
|
+
the kit itself: a real file, a swallowed error and a debug print planted, 21 declared gates,
|
|
55
|
+
none went red. The working tree is untouched and the exit code is always 0 — a look, not a
|
|
56
|
+
threshold
|
|
50
57
|
- See what you told the agent and never wrote down: `npx agent-quality-kit learn` — reads Claude Code
|
|
51
58
|
transcripts for this project on this machine and prints rule candidates missing from the entry
|
|
52
59
|
point. Current project only, terminal only, writes nothing, always exits 0
|
|
@@ -56,13 +63,12 @@ Zero runtime dependencies. Node 18+ and an `sh` shell. MIT.
|
|
|
56
63
|
package itself; there are no dependencies to pull in.
|
|
57
64
|
- Without Node at all (a Python, Go or Rust project where nobody installed it):
|
|
58
65
|
`docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/arsen-ask-lx/aqk doctor`.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-v "$PWD:/work" aqk doctor`. Keep the `--user` flag: without it the container runs as root and
|
|
66
|
+
Pushed to the registry by the same run, from the same tag, that publishes the package.
|
|
67
|
+
Keep the `--user` flag: without it the container runs as root and
|
|
62
68
|
the files `init` writes are owned by root, so you cannot edit your own manifest. Debian-based
|
|
63
69
|
on purpose: the gates are `sh`, `grep`, `awk`, `find` — under alpine's busybox they behave
|
|
64
70
|
differently, and an image where the gates behave differently is worse than no image
|
|
65
|
-
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
71
|
+
- As a GitHub Action: `uses: arsen-ask-lx/Agent_Quality_Kit@v0.10.0` with `min: 1`
|
|
66
72
|
(https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
67
73
|
|
|
68
74
|
## What makes it different
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-quality-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Turns the rules an agent is supposed to follow into commands with exit codes, and reports which of them actually run. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"knip": {
|
|
45
45
|
"entry": [
|
|
46
46
|
"tool/selfcheck/units*.mjs",
|
|
47
|
+
"tool/selfcheck/smoke/*.test.mjs",
|
|
47
48
|
"tool/selfcheck/lifecycle.mjs"
|
|
48
49
|
],
|
|
49
50
|
"project": [
|
|
@@ -25,6 +25,7 @@ import { spawnSync } from "node:child_process";
|
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { CWD, TARGET_DIR, SELF, c, exists, commandRows } from "../lib/core.mjs";
|
|
27
27
|
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
28
|
+
import { probeStatus } from "./probe.mjs";
|
|
28
29
|
import { L } from "../i18n/index.mjs";
|
|
29
30
|
|
|
30
31
|
// Больше пяти имён подряд агент всё равно не удержит, а блок ради них раздувается. Остаток
|
|
@@ -63,6 +64,18 @@ function contextBlock(state, T = L.context) {
|
|
|
63
64
|
const rat = (state.ratchets || []).slice(0, MAX_RATCHETS);
|
|
64
65
|
if (rat.length) out.push(T.ratchets(rat.map((r) => `${r.name} (${r.count})`).join(", ")));
|
|
65
66
|
|
|
67
|
+
// ЧТО НЕ ПРИКРЫТО НИЧЕМ — сюда попадает потому, что иначе об этом не узнает никто. Команду
|
|
68
|
+
// `probe` надо вспомнить, а агент не вспомнит: это тот же класс, что файл, который можно не
|
|
69
|
+
// прочитать. Блок читается по построению, поэтому знание живёт здесь, а не в команде.
|
|
70
|
+
// Состояние «не делалась» печатается как НЕИЗВЕСТНО, а не опускается: молчание тут
|
|
71
|
+
// прочиталось бы как «всё прикрыто», а прикрыто ли — мы не знаем.
|
|
72
|
+
const pr = state.probe;
|
|
73
|
+
if (pr) {
|
|
74
|
+
if (pr.state === "never") out.push(T.probeNever);
|
|
75
|
+
else if (pr.blind > 0) out.push(T.probeBlind(pr.blind, pr.state === "stale" ? pr.behind : 0));
|
|
76
|
+
else out.push(T.probeClean(pr.state === "stale" ? pr.behind : 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
// ПОЛНЫЙ БЛОК — решение владельца от 2026-09-08, принятое ПОСЛЕ возражения и вопреки ему.
|
|
67
80
|
// Возражение было такое: вход, растущий в длину, роняет качество у всех проверенных моделей,
|
|
68
81
|
// и свод, влитый целиком, даёт правило, которое в контексте есть и не выполняется. Ответ
|
|
@@ -233,6 +246,20 @@ async function cmdContext(args = []) {
|
|
|
233
246
|
if (run) run.stale = runIsStale(run.when);
|
|
234
247
|
}
|
|
235
248
|
|
|
249
|
+
// Проба: сколько классов не ловит никто и насколько отметка отстала. Читается из файла,
|
|
250
|
+
// ничего не запускает — блок обязан укладываться в секунду.
|
|
251
|
+
let probe = null;
|
|
252
|
+
try {
|
|
253
|
+
const st = await probeStatus();
|
|
254
|
+
let blind = null;
|
|
255
|
+
const mark = join(CWD, TARGET_DIR, "last-probe.md");
|
|
256
|
+
if (await exists(mark)) {
|
|
257
|
+
const m = /^blind:\s*(\d+)/m.exec(await readFile(mark, "utf8"));
|
|
258
|
+
if (m) blind = Number(m[1]);
|
|
259
|
+
}
|
|
260
|
+
probe = { ...st, blind };
|
|
261
|
+
} catch { /* пробы нет — блок просто не покажет строку про неё */ }
|
|
262
|
+
|
|
236
263
|
const ratchets = [];
|
|
237
264
|
const dir = typeof man?.ratchets === "string" ? man.ratchets.trim() : "";
|
|
238
265
|
if (dir && (await exists(join(CWD, dir)))) {
|
|
@@ -257,7 +284,7 @@ async function cmdContext(args = []) {
|
|
|
257
284
|
}
|
|
258
285
|
|
|
259
286
|
console.log(contextBlock({
|
|
260
|
-
entry, entryExists: rules !== null, level, rules, run, ratchets, full: fullPart,
|
|
287
|
+
entry, entryExists: rules !== null, level, rules, run, ratchets, probe, full: fullPart,
|
|
261
288
|
}).join("\n"));
|
|
262
289
|
}
|
|
263
290
|
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { scopeOutput, splitAdvice, changedFiles } from "../lib/scope.mjs";
|
|
7
7
|
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die } from "../lib/core.mjs";
|
|
8
|
+
import { cmdProbe, probeStatus } from "./probe.mjs";
|
|
8
9
|
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet, layoutChecks, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
9
10
|
import { proveGates } from "../lib/prove.mjs";
|
|
10
11
|
import { detectFacts, readCatalog, triggerVerdict, browserServerAdvice } from "../lib/repo.mjs";
|
|
@@ -246,8 +247,19 @@ function runGates(man, opts = {}) {
|
|
|
246
247
|
const mark = isAdvisory ? c.yellow("!") : c.red("✘");
|
|
247
248
|
const verdict = isAdvisory ? c.yellow(L.doctor.advisoryMark) : c.red(L.doctor.exitCode(code));
|
|
248
249
|
console.log(` ${mark} ${name.padEnd(14)} ${verdict} ${c.dim(`· ${secs}s · ${cmd}`)}`);
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
// ГОЛОВА И ХВОСТ, А НЕ ТОЛЬКО ГОЛОВА. Гейт, который сам является прогоном (наш `smoke`),
|
|
251
|
+
// печатает сотни строк, и вердикт у него в конце — при обрезке до первых трёх человек
|
|
252
|
+
// видел «программа разбирается» и ни слова о том, что упало. Час поисков в конвейере
|
|
253
|
+
// 2026-09-09 стоил ровно этого. Голова нужна тоже: у сканирующих записей находки идут
|
|
254
|
+
// с первой строки.
|
|
255
|
+
const HEAD = 3, TAIL = 2;
|
|
256
|
+
for (const line of out.slice(0, HEAD)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
257
|
+
if (out.length > HEAD + TAIL) {
|
|
258
|
+
console.log(c.dim(` ${L.doctor.moreLines(out.length - HEAD - TAIL)}`));
|
|
259
|
+
for (const line of out.slice(-TAIL)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
260
|
+
} else {
|
|
261
|
+
for (const line of out.slice(HEAD)) console.log(c.dim(` ${line.slice(0, 100)}`));
|
|
262
|
+
}
|
|
251
263
|
// Совет тоже не бесконечен: гейт, зовущий помощник шесть раз, печатает его шесть раз.
|
|
252
264
|
for (const line of alwaysAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
253
265
|
results.push({ name, cmd, ok: false, secs, code, advisory: isAdvisory, out: outAll });
|
|
@@ -404,6 +416,31 @@ async function cmdDoctor() {
|
|
|
404
416
|
gateFailed = run.failed;
|
|
405
417
|
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
406
418
|
await writeRunReport({ version, reached, results: run.results });
|
|
419
|
+
|
|
420
|
+
// ПРОБА ЗАПУСКАЕТСЯ САМА. Владелец сформулировал так: «команду, о которой надо вспомнить,
|
|
421
|
+
// агент не вспомнит, а человек о ней не узнает». Это тот же класс, что файл, который можно
|
|
422
|
+
// не прочитать, — и весь комплект написан против него. `probe` отвечает на важнейший
|
|
423
|
+
// вопрос («что здесь не прикрыто ничем») и, оставаясь ручной, не задаётся никем.
|
|
424
|
+
//
|
|
425
|
+
// Поэтому не напоминание, а действие: раз в сто коммитов прогон делает пробу сам. Единица
|
|
426
|
+
// — коммиты, а не сутки: месяц без работы перепроверять незачем, сто коммитов за день —
|
|
427
|
+
// надо. В кратком режиме не запускается: там хук на воротах коммита, и лишние секунды там
|
|
428
|
+
// стоят дороже. Не влияет на код возврата НИКОГДА — это осмотр, а не порог.
|
|
429
|
+
// Выключается AQK_PROBE=0 — у всего, что случается само, обязан быть выключатель.
|
|
430
|
+
if (!brief && process.env.AQK_PROBE !== "0") {
|
|
431
|
+
try {
|
|
432
|
+
const st = await probeStatus();
|
|
433
|
+
if (st.badEvery !== undefined) {
|
|
434
|
+
console.log(c.yellow(`\n ${L.probe.badEvery(st.badEvery)}`));
|
|
435
|
+
} else if (st.state === "never" || st.state === "stale") {
|
|
436
|
+
// Сообщение обязано быть верным в обоих случаях. Первая версия печатала «прошло сто
|
|
437
|
+
// коммитов» и там, где пробы не было ВОВСЕ: число бралось из порога, а не из факта.
|
|
438
|
+
// Мелочь, но того же класса, что и всё остальное здесь: вывод, который не врёт.
|
|
439
|
+
console.log(c.dim(`\n ${st.state === "never" ? L.probe.autoFirst : L.probe.auto(st.behind)}`));
|
|
440
|
+
await cmdProbe([], { auto: true });
|
|
441
|
+
}
|
|
442
|
+
} catch { /* проба не состоялась — прогон это не роняет: он про гейты, а не про неё */ }
|
|
443
|
+
}
|
|
407
444
|
} else if (gates.length) {
|
|
408
445
|
console.log(
|
|
409
446
|
c.yellow(` ${L.doctor.declaredNotRun(gates.length)}`) +
|
|
@@ -429,6 +466,23 @@ async function cmdDoctor() {
|
|
|
429
466
|
process.exit(pass ? 0 : 1);
|
|
430
467
|
}
|
|
431
468
|
const ok = !(missing || reached < 0 || gateFailed);
|
|
469
|
+
// ВЕРДИКТ НАЗЫВАЕТСЯ СЛОВАМИ, а не только кодом возврата. С `--min` он печатался всегда, без
|
|
470
|
+
// него — никогда: прогон выходил с единицей, а внизу человек видел список зелёных гейтов и
|
|
471
|
+
// шёл искать причину. Обратная сторона нашего же принципа: молчание неотличимо не только от
|
|
472
|
+
// успеха, но и от отказа. Найдено аудитом фич 2026-09-09.
|
|
473
|
+
//
|
|
474
|
+
// Печатается и на зелёном тоже: «ничего не сказал» и «всё проверено» обязаны различаться.
|
|
475
|
+
if (wantRun) {
|
|
476
|
+
if (ok) {
|
|
477
|
+
console.log(c.green(` ${L.doctor.runVerdictOk}\n`));
|
|
478
|
+
} else {
|
|
479
|
+
const why = [];
|
|
480
|
+
if (missing) why.push(L.doctor.whyMissing);
|
|
481
|
+
if (reached < 0) why.push(L.doctor.whyLevel);
|
|
482
|
+
if (gateFailed) why.push(L.doctor.whyGates(gateFailed, failedNames.join(", ")));
|
|
483
|
+
console.log(c.red(` ${L.doctor.runVerdictFail(why.join(", "))}\n`));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
432
486
|
await finishBrief(buf, { held: cat.held, todo: cat.todo, level: reached, red: [] }, cat.todoRecs, ok);
|
|
433
487
|
process.exit(ok ? 0 : 1);
|
|
434
488
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// tool/commands/probe.mjs — `aqk probe`: чего объявленные проверки НЕ видят.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ЭТО ОТДЕЛЬНАЯ КОМАНДА. `doctor` отвечает «держит машина 21». Двадцать один из чего?
|
|
4
|
+
// Знаменателя нет: 21 — это то, что мы успели написать в каталог, а не то, что важно в этом
|
|
5
|
+
// проекте. `prove` доказывает, что гейт ловит брак НА СВОЁМ образце. Ни один из них не
|
|
6
|
+
// отвечает на вопрос владельца: «что у меня не прикрыто вообще».
|
|
7
|
+
//
|
|
8
|
+
// Замер, с которого команда началась, — на самом комплекте, 2026-09-09. Взят настоящий файл
|
|
9
|
+
// проекта, в копию подсажены проглоченная ошибка и отладочная печать, прогнаны ВСЕ 21
|
|
10
|
+
// сканирующих гейта из манифеста. Покраснело: ноль. У проекта с AQK-3 есть брак, невидимый
|
|
11
|
+
// всем его проверкам, — и узнать об этом было нечем.
|
|
12
|
+
//
|
|
13
|
+
// КАК УСТРОЕНО. Два источника, и оба — факты, а не наш вкус:
|
|
14
|
+
// 1. история репозитория: где брак ВОЗВРАЩАЕТСЯ (коммиты-починки, `history.mjs`);
|
|
15
|
+
// 2. красные образцы каталога: каждый доказан прогоном, каждый — настоящий брак.
|
|
16
|
+
// Образец кладётся во временный каталог по пути горячего файла, и по нему прогоняются
|
|
17
|
+
// ОБЪЯВЛЕННЫЕ гейты проекта. Никто не покраснел — класс не прикрыт, и это доказано, а не
|
|
18
|
+
// выведено из списка.
|
|
19
|
+
//
|
|
20
|
+
// ЧЕГО КОМАНДА НЕ ДЕЛАЕТ. Не трогает рабочее дерево: проба живёт в каталоге mkdtemp и
|
|
21
|
+
// удаляется. Не меняет манифест. Не роняет прогон: код возврата всегда 0 — это осмотр, а
|
|
22
|
+
// не порог. Порог — у `doctor --run --min`.
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { mkdtemp, mkdir, copyFile, rm, readdir, writeFile, readFile } from "node:fs/promises";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { join, dirname, extname } from "node:path";
|
|
27
|
+
import { readManifest } from "../lib/manifest.mjs";
|
|
28
|
+
import { commandFor } from "../lib/prove.mjs";
|
|
29
|
+
import { fixHotspots, probeVerdict } from "../lib/history.mjs";
|
|
30
|
+
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
31
|
+
import { CWD, GATES_SRC, TARGET_DIR, c, SELF, exists } from "../lib/core.mjs";
|
|
32
|
+
import { probeState, probeEvery, PROBE_EVERY } from "../lib/cadence.mjs";
|
|
33
|
+
import { L } from "../i18n/index.mjs";
|
|
34
|
+
|
|
35
|
+
// Тот же набор расширений, что у привязки доказательства к дифу. Список один на программу:
|
|
36
|
+
// второй через месяц разошёлся бы с первым.
|
|
37
|
+
const CODE_EXT = new Set([
|
|
38
|
+
"c", "cjs", "cpp", "cs", "css", "go", "h", "java", "js", "json", "jsx", "kt", "mjs", "mts",
|
|
39
|
+
"php", "pl", "py", "rb", "rs", "scala", "sh", "sql", "swift", "ts", "tsx", "vue",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const isCode = (p) =>
|
|
43
|
+
CODE_EXT.has(extname(p).slice(1).toLowerCase()) &&
|
|
44
|
+
!/(^|\/)gates\/[^/]+\/(red|green)(\/|$)/.test(p);
|
|
45
|
+
|
|
46
|
+
// Мелкий клон истории не содержит. `fetch-depth: 2` в конвейере — обычная настройка, и на нём
|
|
47
|
+
// рейтинг починок пуст ВСЕГДА. Сказать там «коммитов-починок не найдено» значит выдать
|
|
48
|
+
// отсутствие данных за факт о репозитории: та же подмена, что «зелено, потому что не
|
|
49
|
+
// проверялось». Найдено собственным конвейером 2026-09-09.
|
|
50
|
+
function isShallow() {
|
|
51
|
+
const r = spawnSync("git", ["rev-parse", "--is-shallow-repository"], { cwd: CWD, encoding: "utf8" });
|
|
52
|
+
return r.status === 0 && String(r.stdout || "").trim() === "true";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// История берётся одним вызовом: тема коммита и его файлы. Слияния исключены — в них файлы
|
|
56
|
+
// второй ветки, а починку делали не в них.
|
|
57
|
+
function gitLog(limit) {
|
|
58
|
+
const r = spawnSync(
|
|
59
|
+
"git",
|
|
60
|
+
["log", "--no-merges", `--max-count=${limit}`, "--format=%s", "--name-only"],
|
|
61
|
+
{ cwd: CWD, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
|
|
62
|
+
);
|
|
63
|
+
return r.status === 0 ? r.stdout || "" : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Сколько коммитов в репозитории сейчас. Единица каденции — коммиты, а не сутки: месяц без
|
|
67
|
+
// работы перепроверять незачем, а сто коммитов за день — надо.
|
|
68
|
+
function commitCount() {
|
|
69
|
+
const r = spawnSync("git", ["rev-list", "--count", "HEAD"], { cwd: CWD, encoding: "utf8" });
|
|
70
|
+
if (r.status !== 0) return null;
|
|
71
|
+
const n = Number(String(r.stdout || "").trim());
|
|
72
|
+
return Number.isFinite(n) ? n : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MARK = () => join(CWD, TARGET_DIR, "last-probe.md");
|
|
76
|
+
|
|
77
|
+
// Отметка о прошлой пробе. Формат человеческий намеренно: файл читают глазами и агентом,
|
|
78
|
+
// а не только программой. Разбирается одна строка — та, что несёт число коммитов.
|
|
79
|
+
async function readMark() {
|
|
80
|
+
try {
|
|
81
|
+
const text = await readFile(MARK(), "utf8");
|
|
82
|
+
const m = /^at:\s*(\d+)/m.exec(text);
|
|
83
|
+
return m ? { at: Number(m[1]), text } : {};
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function writeMark(now, blind, lines) {
|
|
88
|
+
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
89
|
+
const body = [
|
|
90
|
+
"# Проба покрытия — что объявленные проверки НЕ видят",
|
|
91
|
+
"",
|
|
92
|
+
`at: ${now === null ? "?" : now}`,
|
|
93
|
+
`blind: ${blind}`,
|
|
94
|
+
"",
|
|
95
|
+
...lines,
|
|
96
|
+
"",
|
|
97
|
+
"Файл эфемерный: его переписывает каждая проба. В .gitignore его стоит держать самому.",
|
|
98
|
+
].join("\n");
|
|
99
|
+
await writeFile(MARK(), body + "\n", "utf8");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Гейты, которым можно подставить каталог. Команда записи каталога кончается каталогом
|
|
103
|
+
// проверки; написанная руками — чем угодно, и подставлять там некуда. Ровно то же правило,
|
|
104
|
+
// по которому `prove` объявляет запись недоказуемой, а не сломанной.
|
|
105
|
+
function scanningGates(man) {
|
|
106
|
+
const gates = man?.gates && typeof man.gates === "object" && !Array.isArray(man.gates) ? man.gates : {};
|
|
107
|
+
return Object.entries(gates)
|
|
108
|
+
.map(([name, raw]) => [name, String(raw || "").trim()])
|
|
109
|
+
.filter(([, cmd]) => cmd && /(\.|\.\/)$/.test(cmd));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Красный образец записи, подходящий по расширению горячего файла. Расширение обязано
|
|
113
|
+
// совпадать: питоновский образец в проекте на TypeScript не проверит ничего, а покажет
|
|
114
|
+
// «не прикрыто» — ложная тревога того же класса, что молчащий гейт, только наоборот.
|
|
115
|
+
async function redSampleFor(entry, ext) {
|
|
116
|
+
const dir = join(GATES_SRC, entry, "red");
|
|
117
|
+
if (!(await exists(dir))) return null;
|
|
118
|
+
let names = [];
|
|
119
|
+
try { names = await readdir(dir); } catch { return null; }
|
|
120
|
+
const hit = names.find((n) => extname(n).toLowerCase() === ext);
|
|
121
|
+
return hit ? join(dir, hit) : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Проба: временный каталог, в нём образец по пути горячего файла. Путь сохраняется целиком —
|
|
125
|
+
// правила, привязанные к путям (`.aqkignore`, исключения гейтов), обязаны действовать так же,
|
|
126
|
+
// как в настоящем репозитории. Без этого проба отвечала бы про несуществующее место.
|
|
127
|
+
async function buildProbe(relPath, sample) {
|
|
128
|
+
const root = await mkdtemp(join(tmpdir(), "aqk-probe-"));
|
|
129
|
+
const dest = join(root, relPath);
|
|
130
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
131
|
+
await copyFile(sample, dest);
|
|
132
|
+
// Переносится ТОЛЬКО .aqkignore: правила, привязанные к путям, обязаны действовать так же,
|
|
133
|
+
// как в настоящем репозитории. Манифест НЕ переносится намеренно — иначе записи, читающие
|
|
134
|
+
// `.aqk.yml` (`gates-are-runnable`, `gate-has-samples`, `protection-not-removed`), краснеют
|
|
135
|
+
// на том, что в пробе нет объявленных ими файлов, и проба объявляет класс прикрытым, хотя
|
|
136
|
+
// на подсаженный брак не отреагировал никто. Ошибка в сторону «прикрыто» — это тишина,
|
|
137
|
+
// а тишина здесь и есть предмет спора. Поймано первым же прогоном на своём репозитории.
|
|
138
|
+
if (await exists(join(CWD, ".aqkignore"))) {
|
|
139
|
+
await copyFile(join(CWD, ".aqkignore"), join(root, ".aqkignore"));
|
|
140
|
+
}
|
|
141
|
+
return root;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runGates(gates, dir) {
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const [name, cmd] of gates) {
|
|
147
|
+
const r = spawnSync(commandFor(cmd, dir), { shell: true, cwd: CWD, encoding: "utf8", timeout: 120000 });
|
|
148
|
+
out.push({ name, code: r.status === null ? 2 : r.status });
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `auto` — проба запущена САМА, по каденции, из `doctor --run`. Тогда она короче и говорит
|
|
154
|
+
// вслух, почему случилась: команда, возникшая без спроса, обязана объяснить себя, иначе её
|
|
155
|
+
// читают как сбой.
|
|
156
|
+
async function cmdProbe(args, { auto = false } = {}) {
|
|
157
|
+
const P = L.probe;
|
|
158
|
+
const topArg = Number(args[args.indexOf("--top") + 1]);
|
|
159
|
+
const TOP = args.includes("--top") && Number.isFinite(topArg) && topArg > 0 ? topArg : (auto ? 3 : 5);
|
|
160
|
+
|
|
161
|
+
console.log(c.bold(`\n${P.title}\n`));
|
|
162
|
+
|
|
163
|
+
const man = await readManifest();
|
|
164
|
+
const gates = scanningGates(man);
|
|
165
|
+
if (!gates.length) { console.log(c.yellow(` ${P.noGates(`${SELF} add <имя>`)}\n`)); return; }
|
|
166
|
+
|
|
167
|
+
const raw = gitLog(2000);
|
|
168
|
+
if (raw === null) { console.log(c.yellow(` ${P.noGit}\n`)); return; }
|
|
169
|
+
const hot = fixHotspots(raw, { isCode }).slice(0, TOP);
|
|
170
|
+
if (!hot.length) { console.log(c.yellow(` ${isShallow() ? P.shallow : P.noFixes}\n`)); return; }
|
|
171
|
+
|
|
172
|
+
// Записи каталога, применимые к ЭТОМУ репозиторию. Показывать пробы записей, которые
|
|
173
|
+
// проекту не подходят, значит советовать закрыть дыру, которой нет.
|
|
174
|
+
const facts = await detectFacts();
|
|
175
|
+
const catalog = await readCatalog();
|
|
176
|
+
const entries = catalog.filter((e) => triggerVerdict(e, facts).applies);
|
|
177
|
+
|
|
178
|
+
console.log(c.dim(` ${P.method(hot.length, entries.length)}\n`));
|
|
179
|
+
|
|
180
|
+
let blind = 0;
|
|
181
|
+
for (const { path: rel, fixes } of hot) {
|
|
182
|
+
console.log(` ${c.bold(rel)} ${c.dim(P.fixes(fixes))}`);
|
|
183
|
+
const ext = extname(rel).toLowerCase();
|
|
184
|
+
let probed = 0;
|
|
185
|
+
|
|
186
|
+
for (const e of entries) {
|
|
187
|
+
const sample = await redSampleFor(e.slug, ext);
|
|
188
|
+
if (!sample) continue;
|
|
189
|
+
probed++;
|
|
190
|
+
const dir = await buildProbe(rel, sample);
|
|
191
|
+
let results;
|
|
192
|
+
try { results = runGates(gates, dir); } finally { await rm(dir, { recursive: true, force: true }); }
|
|
193
|
+
const verdict = probeVerdict(results);
|
|
194
|
+
const caught = results.filter((r) => r.code === 1).map((r) => r.name);
|
|
195
|
+
if (verdict === "caught") {
|
|
196
|
+
console.log(` ${c.green("✔")} ${e.intent.padEnd(48)} ${c.dim(P.caught(caught.join(", ")))}`);
|
|
197
|
+
} else if (verdict === "blind") {
|
|
198
|
+
blind++;
|
|
199
|
+
console.log(` ${c.red("✘")} ${e.intent.padEnd(48)} ${c.red(P.blind)}`);
|
|
200
|
+
console.log(c.dim(` ${P.install(`${SELF} add ${e.slug}`)}`));
|
|
201
|
+
} else {
|
|
202
|
+
console.log(` ${c.dim("~")} ${c.dim(e.intent.padEnd(48))} ${c.dim(P.unknown)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!probed) console.log(c.dim(` ${P.noSampleFor(ext || "—")}`));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(blind ? c.yellow(`\n ${P.summaryBlind(blind)}\n`) : c.green(`\n ${P.summaryClean}\n`));
|
|
209
|
+
|
|
210
|
+
// Отметка нужна не для отчёта, а для КАДЕНЦИИ: по ней следующий прогон поймёт, что пора.
|
|
211
|
+
// Без неё команда снова становится тем, о чём надо вспомнить.
|
|
212
|
+
await writeMark(commitCount(), blind, hot.map(({ path: p2, fixes }) => `- ${p2} (${P.fixes(fixes)})`));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Состояние пробы для тех, кто только ПОКАЗЫВАЕТ его: прогон и блок для агента.
|
|
216
|
+
//
|
|
217
|
+
// Порог берётся из манифеста (`probe: 250`), умолчание — PROBE_EVERY. Непонятое значение не
|
|
218
|
+
// подменяется умолчанием молча: в манифесте было бы написано одно, а происходило бы другое.
|
|
219
|
+
// Возвращается пометка `badEvery`, и вызывающий говорит о ней вслух.
|
|
220
|
+
async function probeStatus() {
|
|
221
|
+
const man = await readManifest();
|
|
222
|
+
const every = probeEvery(man);
|
|
223
|
+
if (every === null) return { state: "unknown", behind: null, badEvery: String(man?.probe) };
|
|
224
|
+
if (every === 0) return { state: "off", behind: null };
|
|
225
|
+
return probeState(await readMark(), commitCount(), every);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { cmdProbe, probeStatus, scanningGates, isCode };
|
package/tool/commands/vitals.mjs
CHANGED
|
@@ -55,13 +55,21 @@ function vitalsRows(f) {
|
|
|
55
55
|
},
|
|
56
56
|
];
|
|
57
57
|
// Устаревшая версия — не отказ: человек мог закрепить её сознательно, и ронять за это нельзя.
|
|
58
|
+
//
|
|
59
|
+
// СОСТОЯНИЙ ТРИ, А НЕ ДВА. Реестр может ответить ошибкой — не упасть, а вернуть не-200; тогда
|
|
60
|
+
// `latest` пустой, и прежняя ветка печатала «свежая». Посмотреть не смогли, а сказали «всё
|
|
61
|
+
// хорошо»: тот самый грех, против которого написан весь комплект, у него самого. Найдено
|
|
62
|
+
// аудитом фич 2026-09-09.
|
|
58
63
|
if (f.version) {
|
|
64
|
+
const latest = String(f.version.latest || "");
|
|
59
65
|
rows.push({
|
|
60
66
|
key: "version",
|
|
61
67
|
ok: null,
|
|
62
|
-
detail:
|
|
63
|
-
? t.
|
|
64
|
-
:
|
|
68
|
+
detail: !latest
|
|
69
|
+
? t.versionUnknown(f.version.current)
|
|
70
|
+
: latest !== f.version.current
|
|
71
|
+
? t.versionOld(latest, f.version.current)
|
|
72
|
+
: t.versionOk(f.version.current),
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
return rows;
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -29,6 +29,7 @@ const enDocs = {
|
|
|
29
29
|
sessionOk: "the SessionStart hook hands the state to the agent",
|
|
30
30
|
sessionNo: (cmd) => `the agent gets no state: ${cmd}`,
|
|
31
31
|
versionOk: (v) => `${v}, current`,
|
|
32
|
+
versionUnknown: (v) => `${v}, could not reach the registry — freshness unknown`,
|
|
32
33
|
versionOld: (l, cur) => `${l} is out, you have ${cur}`,
|
|
33
34
|
},
|
|
34
35
|
// Блок краткого вывода переехал сюда 2026-09-09: терминальный каталог снова перерос
|
|
@@ -66,6 +67,13 @@ const enDocs = {
|
|
|
66
67
|
runRed: (when, names) => `Last run ${when} — RED: ${names}.`,
|
|
67
68
|
andMore: (n) => `and ${n} more`,
|
|
68
69
|
skipped: (n) => `Not run: ${n} — the tool is absent on this machine, their state is unknown.`,
|
|
70
|
+
probeNever: "No coverage probe has run — what is covered by nothing here is UNKNOWN. That is not \"covered\": `aqk probe`.",
|
|
71
|
+
probeBlind: (n, behind) =>
|
|
72
|
+
`Covered by nothing: ${n} defect classes in the places people most often come back to fix` +
|
|
73
|
+
(behind ? ` (the probe is ${behind} commits behind)` : "") + ". Details: `aqk probe`.",
|
|
74
|
+
probeClean: (behind) =>
|
|
75
|
+
"Coverage probe: in the places probed, every applicable class is caught by something" +
|
|
76
|
+
(behind ? ` (${behind} commits behind)` : "") + ".",
|
|
69
77
|
ratchets: (list) => `Ratchets: ${list}. The list may only get shorter, never longer.`,
|
|
70
78
|
where: (entry) => `The rulebook: ${entry}. What proves a diff: \`aqk report --since main\`.`,
|
|
71
79
|
mapTitle: "WHAT THIS TOOL CAN DO. The full list of commands — not a retelling, the same list\nthe help is built from:",
|