agent-quality-kit 0.11.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 +28 -4
- package/README.ru.md +28 -4
- package/kit/gates/complexity-limit/gate.yml +9 -0
- package/kit/gates/dead-code/gate.yml +9 -0
- package/kit/gates/duplicate-code/gate.yml +5 -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 +9 -0
- package/kit/gates/secrets-not-in-code/gate.yml +20 -0
- package/kit/gates/swallowed-error/gate.yml +9 -0
- package/kit/gates/todo-without-task/gate.yml +9 -0
- package/llms.txt +4 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +59 -4
- package/tool/commands/doctor.mjs +171 -44
- package/tool/commands/gates.mjs +3 -2
- package/tool/commands/probe.mjs +86 -22
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +21 -12
- package/tool/i18n/en-docs.mjs +24 -2
- package/tool/i18n/en-gates.mjs +10 -1
- package/tool/i18n/en.mjs +25 -0
- package/tool/i18n/ru-docs.mjs +26 -2
- package/tool/i18n/ru-gates.mjs +10 -1
- package/tool/i18n/ru.mjs +25 -0
- package/tool/lib/adopt.mjs +112 -0
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +46 -1
- package/tool/lib/core.mjs +39 -1
- package/tool/lib/execution.mjs +50 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +45 -6
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/prove.mjs +2 -2
- package/tool/lib/repo.mjs +18 -0
- package/tool/lib/run.mjs +108 -8
- package/tool/selfcheck/gates.sh +12 -2
- 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 +51 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-execution.mjs +78 -1
- package/tool/selfcheck/units-level.mjs +99 -1
- package/tool/selfcheck/units-probe.mjs +104 -0
- package/tool/selfcheck/units-repo.mjs +146 -0
- package/tool/selfcheck/units-verdict.mjs +76 -0
- package/tool/selfcheck/units.mjs +34 -1
|
@@ -23,8 +23,12 @@
|
|
|
23
23
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
24
24
|
import { spawnSync } from "node:child_process";
|
|
25
25
|
import { join } from "node:path";
|
|
26
|
-
import { CWD, TARGET_DIR, SELF, c, exists, commandRows } from "../lib/core.mjs";
|
|
27
|
-
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
26
|
+
import { CWD, TARGET_DIR, SELF, c, exists, commandRows, preCommitHook } from "../lib/core.mjs";
|
|
27
|
+
import { readManifest, assessLevel, coversOf } from "../lib/manifest.mjs";
|
|
28
|
+
import { detectFacts, readCatalog } from "../lib/repo.mjs";
|
|
29
|
+
import { catalogBuckets, startWith, blindAdvice } from "../lib/advice.mjs";
|
|
30
|
+
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
31
|
+
import { declaredGates } from "../lib/run.mjs";
|
|
28
32
|
import { probeStatus } from "./probe.mjs";
|
|
29
33
|
import { L } from "../i18n/index.mjs";
|
|
30
34
|
|
|
@@ -33,6 +37,23 @@ import { L } from "../i18n/index.mjs";
|
|
|
33
37
|
const MAX_RED = 5;
|
|
34
38
|
const MAX_RATCHETS = 3;
|
|
35
39
|
|
|
40
|
+
// ЧТО ДЕЛАТЬ ДАЛЬШЕ — не больше трёх шагов, по убыванию того, насколько это ФАКТ о проекте:
|
|
41
|
+
// 1. объявить проверки, которые у проекта уже есть: дешевле всего, и это его собственное;
|
|
42
|
+
// 2. классы брака, которые проба подсадила в ЕГО файлы и ЕГО проверки не поймали;
|
|
43
|
+
// 3. «начните с этих трёх» из каталога.
|
|
44
|
+
// Класс, названный пробой, в третьем списке не повторяется: один шаг, а не два. Остаток —
|
|
45
|
+
// числом: молчание о нём прочиталось бы как «больше делать нечего».
|
|
46
|
+
function nextSteps({ init = false, adopt = [], blind = [], start = [] } = {}, max = 3) {
|
|
47
|
+
const all = [];
|
|
48
|
+
// Без манифеста `aqk add` отказывает — остальные шаги без этого невыполнимы.
|
|
49
|
+
if (init) all.push({ kind: "init" });
|
|
50
|
+
if (adopt.length) all.push({ kind: "adopt", gates: adopt });
|
|
51
|
+
for (const b of blind) all.push({ kind: "blind", ...b });
|
|
52
|
+
const seen = new Set(blind.map((b) => b.slug));
|
|
53
|
+
for (const st of start) if (!seen.has(st.slug)) all.push({ kind: "start", ...st });
|
|
54
|
+
return { steps: all.slice(0, max), rest: Math.max(0, all.length - max) };
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
// Чистая функция: на входе состояние, на выходе строки. Отделена от чтения диска намеренно —
|
|
37
58
|
// это единственное место комплекта, чей текст читает машина, и проверять его надо не прогоном,
|
|
38
59
|
// а перебором случаев, включая те, которых на нашем репозитории не бывает.
|
|
@@ -80,10 +101,28 @@ function contextBlock(state, T = L.context) {
|
|
|
80
101
|
if (pr.state === "never") out.push(T.probeNever);
|
|
81
102
|
else if (pr.state === "off") out.push(T.probeOff);
|
|
82
103
|
else if (pr.state === "unknown") out.push(T.probeUnknown);
|
|
83
|
-
|
|
104
|
+
// Имена — агенту они нужнее числа: «один класс» не говорит, какой файл трогать осторожно.
|
|
105
|
+
else if (pr.blind > 0) out.push(T.probeBlind(pr.blind, pr.state === "stale" ? pr.behind : 0,
|
|
106
|
+
(pr.classes || []).map((b) => `${b.slug} (${b.file})`).join(", ")));
|
|
84
107
|
else out.push(T.probeClean(pr.state === "stale" ? pr.behind : 0));
|
|
85
108
|
}
|
|
86
109
|
|
|
110
|
+
// ДАЛЬШЕ — то, что `doctor` знает, а агенту не говорилось: блок отвечал только «как дела».
|
|
111
|
+
// Шаги вычислены, а не пожелания: у каждого команда, которую можно выполнить сейчас.
|
|
112
|
+
const nx = state.next;
|
|
113
|
+
if (nx && nx.steps && nx.steps.length) {
|
|
114
|
+
out.push("", T.nextTitle);
|
|
115
|
+
nx.steps.forEach((st, i) => out.push(`${i + 1}. ${T.nextStep[st.kind](st)}`));
|
|
116
|
+
if (nx.rest) out.push(T.nextMore(nx.rest));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// КОГДА ЧТО — правила вида «ситуация → команда». Общий совет («тестируй изменения») агент
|
|
120
|
+
// пролистывает; проверяемый («перед коммитом — вот эта команда») выполняет. Первое правило
|
|
121
|
+
// зависит от факта: стоит хук — сказать не обходить его; нет — дать команду, само не случится.
|
|
122
|
+
if (state.when) {
|
|
123
|
+
out.push("", T.whenTitle, `- ${T.whenCommit(state.when.hook)}`, ...T.whenRules.map((r) => `- ${r}`));
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
// ПОЛНЫЙ БЛОК — решение владельца от 2026-09-08, принятое ПОСЛЕ возражения и вопреки ему.
|
|
88
127
|
// Возражение было такое: вход, растущий в длину, роняет качество у всех проверенных моделей,
|
|
89
128
|
// и свод, влитый целиком, даёт правило, которое в контексте есть и не выполняется. Ответ
|
|
@@ -291,9 +330,25 @@ async function cmdContext(args = []) {
|
|
|
291
330
|
fullPart = { entry, rows, text };
|
|
292
331
|
}
|
|
293
332
|
|
|
333
|
+
// ДАЛЬШЕ — теми же функциями, что у `doctor`: корзины каталога, «начните с трёх», совет под
|
|
334
|
+
// язык, чужие проверки проекта. Второй расчёт того же самого разошёлся бы с первым.
|
|
335
|
+
let next = null;
|
|
336
|
+
try {
|
|
337
|
+
const facts = await detectFacts(man);
|
|
338
|
+
const catalog = await readCatalog();
|
|
339
|
+
const { todo } = catalogBuckets(catalog, facts, coversOf(man).covered);
|
|
340
|
+
const adopt = declaredGates(man).length ? [] : proposeGates(await readAdoptFiles(CWD));
|
|
341
|
+
const blind = (probe?.classes || [])
|
|
342
|
+
.filter((b) => !facts.gateKeys.includes(b.slug))
|
|
343
|
+
.map((b) => ({ ...b, command: blindAdvice(catalog.find((r) => r.slug === b.slug), facts, {}).command }));
|
|
344
|
+
const start = startWith(todo, facts, 3).map((rec) => ({ slug: rec.slug, command: blindAdvice(rec, facts, {}).command }));
|
|
345
|
+
next = nextSteps({ init: !man, adopt, blind, start });
|
|
346
|
+
} catch { /* не посчитали — блок скажет остальное; выдумывать шаги нельзя */ }
|
|
347
|
+
|
|
294
348
|
console.log(contextBlock({
|
|
295
349
|
entry, entryExists: rules !== null, level, rules, run, ratchets, probe, full: fullPart,
|
|
350
|
+
next, when: { hook: await preCommitHook(CWD) },
|
|
296
351
|
}).join("\n"));
|
|
297
352
|
}
|
|
298
353
|
|
|
299
|
-
export { cmdContext, contextBlock, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf };
|
|
354
|
+
export { cmdContext, contextBlock, nextSteps, parseLastRun, countArbiters, withHook, hasOurHook, portableSelf };
|
package/tool/commands/doctor.mjs
CHANGED
|
@@ -4,16 +4,19 @@ import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
|
4
4
|
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
|
-
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die } from "../lib/core.mjs";
|
|
7
|
+
import { CWD, PKG_ROOT, TARGET_DIR, MANIFEST, SELF, c, exists, die, RUNTIME_FILES } from "../lib/core.mjs";
|
|
8
8
|
import { cmdProbe, probeStatus } from "./probe.mjs";
|
|
9
9
|
import { readManifest, assessLevel, unknownKeys, KNOWN_KEYS, advisorySet, layoutChecks, coversOf, coversUnproven, unparsedLines } from "../lib/manifest.mjs";
|
|
10
10
|
import { proveGates } from "../lib/prove.mjs";
|
|
11
|
-
import { detectFacts, readCatalog,
|
|
11
|
+
import { detectFacts, readCatalog, browserServerAdvice } from "../lib/repo.mjs";
|
|
12
|
+
import { startWith, catalogBuckets, blindAdvice } from "../lib/advice.mjs";
|
|
13
|
+
import { proposeGates, readAdoptFiles } from "../lib/adopt.mjs";
|
|
12
14
|
import { assessBaseline, DEP_FILES, BASELINE_TOTAL } from "../lib/baseline.mjs";
|
|
13
15
|
import { L } from "../i18n/index.mjs";
|
|
14
16
|
import { countArbiters } from "./context.mjs";
|
|
15
17
|
import { beginBrief, finishBrief } from "../lib/brief.mjs";
|
|
16
|
-
import { declaredGates, sinceRef, runGates } from "../lib/run.mjs";
|
|
18
|
+
import { declaredGates, sinceRef, runGates, progress, listArg } from "../lib/run.mjs";
|
|
19
|
+
import { autoProbeAllowed } from "../lib/cadence.mjs";
|
|
17
20
|
|
|
18
21
|
// Обязательный минимум проекта — прогоном, а не по памяти. До сих пор это было единственное
|
|
19
22
|
// место, где комплект просил верить на слово, что человек прочитал методичку и сверился.
|
|
@@ -50,7 +53,7 @@ async function reportBaseline(man, facts) {
|
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
async function reportCatalog(man, facts) {
|
|
56
|
+
async function reportCatalog(man, facts, probe = null) {
|
|
54
57
|
const catalog = await readCatalog();
|
|
55
58
|
if (!catalog.length) return;
|
|
56
59
|
|
|
@@ -58,14 +61,7 @@ async function reportCatalog(man, facts) {
|
|
|
58
61
|
// Пока их считали вместе, вывод каждый прогон называл долгом то, что уже держит biome или
|
|
59
62
|
// ruff. Просьба первого чужого пользователя; она же — наша собственная норма про вывод.
|
|
60
63
|
const { covered, unknownGates } = coversOf(man);
|
|
61
|
-
const held
|
|
62
|
-
for (const rec of catalog) {
|
|
63
|
-
const v = triggerVerdict(rec, facts);
|
|
64
|
-
if (!v.applies) skip.push([rec, v.why]);
|
|
65
|
-
else if (facts.gateKeys.includes(rec.slug)) held.push(rec);
|
|
66
|
-
else if (covered.has(rec.slug)) byOther.push([rec, covered.get(rec.slug)]);
|
|
67
|
-
else todo.push(rec);
|
|
68
|
-
}
|
|
64
|
+
const { held, todo, skip, byOther } = catalogBuckets(catalog, facts, covered);
|
|
69
65
|
|
|
70
66
|
console.log(c.bold(`\n ${L.doctor.gatesHeading}\n`));
|
|
71
67
|
const marks = ["has_ci", "has_db", "has_docker", "has_tests", "has_deps"]
|
|
@@ -77,10 +73,81 @@ async function reportCatalog(man, facts) {
|
|
|
77
73
|
);
|
|
78
74
|
|
|
79
75
|
for (const rec of held) console.log(` ${c.green("✔")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
// ЧТО У ВАС УЖЕ ЕСТЬ — до итога и до списка крестов. Комплект, поставленный в проект с
|
|
77
|
+
// eslint, mocha и конвейером, показывал двадцать крестов и «держит машина 0»: мы считали
|
|
78
|
+
// только СВОИ записи, а чужие проверки не читали вовсе. С точки зрения владельца это
|
|
79
|
+
// неправда, и первое, что он видел, было обвинением. Предлагаем, а не вписываем: гейт в
|
|
80
|
+
// чужом манифесте без спроса — наше решение в чужом файле.
|
|
81
|
+
if (!declaredGates(man).length) {
|
|
82
|
+
const found = proposeGates(await readAdoptFiles(CWD));
|
|
83
|
+
if (found.length) {
|
|
84
|
+
console.log(`\n ${c.bold(L.doctor.haveAlready(found.length))}`);
|
|
85
|
+
for (const g of found) {
|
|
86
|
+
console.log(` ${c.green("✔")} ${g.name.padEnd(12)} ${c.dim(`${g.cmd} ← ${g.source}`)}`);
|
|
87
|
+
}
|
|
88
|
+
console.log(c.dim(` ${L.doctor.haveAlreadyHow(found.map((g) => `${g.name}: "${g.cmd}"`).join(" "))}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ЧТО ВАШИ ПРОВЕРКИ ПРОПУСТИЛИ. Проба знала имена непойманных классов и писала в отметку одно
|
|
93
|
+
// число; человек в `doctor` не видел ничего. Это самое конкретное, что мы знаем о проекте, —
|
|
94
|
+
// не «хорошая практика», а брак, подсаженный в ЕГО файл и ЕГО проверками не замеченный, —
|
|
95
|
+
// поэтому стоит выше списка «с чего начать». Читается из файла: ничего не запускает.
|
|
96
|
+
const blindOnes = (probe?.classes || []).map((b) => [b, catalog.find((r) => r.slug === b.slug)]).filter(([, r]) => r);
|
|
97
|
+
if (blindOnes.length) {
|
|
98
|
+
console.log(`\n ${c.yellow("⚠")} ${c.bold(L.doctor.blindHeading(probe.behind))}`);
|
|
99
|
+
for (const [b, rec] of blindOnes) {
|
|
100
|
+
// Три случая, и сливать их нельзя. Гейт стоял и проба его ГОНЯЛА — «стоит, но здесь не
|
|
101
|
+
// ловит», самое ценное. Гейт объявлен, но проба его не гоняла (поставлен позже или
|
|
102
|
+
// медленный) — «поймает ли, покажет следующая», а не «пойман». Гейта нет — совет.
|
|
103
|
+
const ranIt = probe.ran?.has(rec.slug);
|
|
104
|
+
const now = facts.gateKeys.includes(rec.slug);
|
|
105
|
+
console.log(` ${now && !ranIt ? c.dim("~") : c.red("✘")} ${rec.slug.padEnd(22)} ${c.dim(`${rec.intent || ""} ← ${b.file}`)}`);
|
|
106
|
+
if (ranIt) { console.log(c.dim(` ${L.doctor.blindRan(rec.slug)}`)); continue; }
|
|
107
|
+
if (now) { console.log(c.dim(` ${L.doctor.blindInstalled}`)); continue; }
|
|
108
|
+
const adv = blindAdvice(rec, facts, {});
|
|
109
|
+
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
110
|
+
else console.log(c.dim(` ${L.doctor.install(`${SELF} add ${rec.slug}`)}`));
|
|
111
|
+
}
|
|
112
|
+
console.log(c.dim(` ${L.doctor.blindMore(`${SELF} probe`)}`));
|
|
113
|
+
} else if (probe?.state === "never" && declaredGates(man).length) {
|
|
114
|
+
console.log(c.dim(`\n ${L.doctor.probeNever(`${SELF} probe`)}`));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// С ЧЕГО НАЧАТЬ. Двадцать одинаковых крестов — это ноль требований: закрывают первое
|
|
118
|
+
// попавшееся или не закрывают ничего. Порядок не по нашему вкусу: сперва то, что родилось из
|
|
119
|
+
// настоящего отказа И закрывается одной готовой командой.
|
|
120
|
+
const first = todo.length > 3 ? startWith(todo, facts, 3) : [];
|
|
121
|
+
if (first.length) {
|
|
122
|
+
console.log(`\n ${c.bold(L.doctor.startWith)}`);
|
|
123
|
+
for (const rec of first) {
|
|
124
|
+
const adv = blindAdvice(rec, facts, {});
|
|
125
|
+
console.log(` ${c.yellow("→")} ${rec.slug.padEnd(22)} ${c.dim(rec.intent || "")}`);
|
|
126
|
+
if (adv.command) console.log(c.dim(` ${L.doctor.startCmd(adv.command)}`));
|
|
127
|
+
if (adv.tool) console.log(c.dim(` ${L.doctor.startTool(adv.tool)}`));
|
|
128
|
+
}
|
|
129
|
+
// Одна проверка руками — это разовый героизм. Сказать про хук здесь, а не в конце: человек
|
|
130
|
+
// читает первые строки и закрывает, а именно сейчас у него в руках список того, что стоит
|
|
131
|
+
// повесить перед пушем.
|
|
132
|
+
console.log(c.dim(`\n ${L.doctor.startHook}`));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ОСТАЛЬНОЕ — ПОСЛЕ ГЛАВНОГО И СЖАТО. Список шёл первым, по две строки на запись (вторая —
|
|
136
|
+
// «поставить: aqk add …»), и на requests главное начиналось со строки 84 из 102: человек
|
|
137
|
+
// читает сверху и закрывает раньше. Разбор соседа 2026-09-11 (research/competitors/agentlint.md):
|
|
138
|
+
// там первыми идут пять главных исправлений. Записи не теряются — теряется повтор подсказки.
|
|
139
|
+
const rest = todo.filter((r) => !first.includes(r));
|
|
140
|
+
if (rest.length) {
|
|
141
|
+
if (first.length) console.log(`\n ${c.bold(L.doctor.todoRest(rest.length))}`);
|
|
142
|
+
else console.log("");
|
|
143
|
+
// ○, а не ✘: запись не установлена — это не падение. Крест в зелёном прогоне глаз читает
|
|
144
|
+
// как провал, и через неделю человек перестаёт смотреть на красное вообще (отзыв с живого
|
|
145
|
+
// проекта 2026-09-11). ✘ остаётся за тем, что упало или пропустило брак.
|
|
146
|
+
for (const rec of rest) console.log(` ${c.dim("○")} ${rec.slug.padEnd(22)} ${rec.intent || ""}`);
|
|
147
|
+
console.log(c.dim(` ${L.doctor.todoRestHow(SELF)}`));
|
|
83
148
|
}
|
|
149
|
+
|
|
150
|
+
// Второстепенное — в конце: что закрыто чужим арбитром, что неприменимо, советы без вердикта.
|
|
84
151
|
if (byOther.length) {
|
|
85
152
|
console.log(c.dim(`\n ${L.doctor.coveredBy(byOther.length)}`));
|
|
86
153
|
for (const [rec, gate] of byOther) console.log(c.dim(` ~ ${rec.slug.padEnd(22)} ${L.doctor.coveredByGate(gate)}`));
|
|
@@ -93,14 +160,36 @@ async function reportCatalog(man, facts) {
|
|
|
93
160
|
// Заявка «эту запись держит наш линтер» сверяется с кодами правил из рецепта записи.
|
|
94
161
|
// Замерено на живом ruff.toml: девятнадцать групп правил, а print() не ловится — и заявка
|
|
95
162
|
// сняла бы запись с долга, не закрыв её ничем.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
// Конфиги — ПО ЛИНТЕРАМ, а не одной склейкой: заявка сверяется правилами того линтера,
|
|
164
|
+
// которым закрыт гейт (отзыв с живого проекта 2026-09-11 — коды ruff искались в biome.json).
|
|
165
|
+
const readAll = async (names) => {
|
|
166
|
+
let t = "";
|
|
167
|
+
for (const f of names) { try { t += await readFile(join(CWD, f), "utf8") + "\n"; } catch { /* нет файла */ } }
|
|
168
|
+
return t;
|
|
169
|
+
};
|
|
170
|
+
let scripts = {}, pkgText = "";
|
|
171
|
+
try { pkgText = await readFile(join(CWD, "package.json"), "utf8"); scripts = JSON.parse(pkgText)?.scripts || {}; } catch { /* нет или не JSON */ }
|
|
172
|
+
const configs = {
|
|
173
|
+
// ruff.toml и .ruff.toml — конфиг ruff целиком, слово «ruff» в них писать незачем (поймал наш же
|
|
174
|
+
// smoke: `extend-select = [..., "T20"]` выбрасывался). pyproject.toml — только если в нём есть
|
|
175
|
+
// раздел ruff: он есть почти у каждого python-проекта и без ruff.
|
|
176
|
+
ruff: (await readAll(["ruff.toml", ".ruff.toml"])) +
|
|
177
|
+
((await readAll(["pyproject.toml"])).match(/^\[tool\.ruff[\s\S]*/m)?.[0] || ""),
|
|
178
|
+
eslint: (await readAll([".eslintrc", ".eslintrc.json", ".eslintrc.js", ".eslintrc.cjs", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts"])) +
|
|
179
|
+
(/"eslintConfig"/.test(pkgText) ? pkgText : ""),
|
|
180
|
+
biome: await readAll(["biome.json", "biome.jsonc"]),
|
|
181
|
+
scripts,
|
|
182
|
+
};
|
|
183
|
+
for (const u of coversUnproven(man, catalog, configs)) {
|
|
184
|
+
if (u.kind === "unproven") {
|
|
185
|
+
console.log(c.yellow(`\n ${L.doctor.coversUnproven(u.entry, u.gate, u.codes.join(", "))}`));
|
|
186
|
+
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
187
|
+
} else if (u.kind === "impossible") {
|
|
188
|
+
console.log(c.yellow(`\n ${L.doctor.coversImpossible(u.entry, u.gate, u.linter)}`));
|
|
189
|
+
console.log(c.dim(` ${L.doctor.coversUnprovenHow(`${SELF} add ${u.entry}`)}`));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(c.dim(`\n ${L.doctor.coversCantCheck(u.entry, u.gate)}`));
|
|
192
|
+
}
|
|
104
193
|
}
|
|
105
194
|
// Не вердикт, а совет: отсутствие браузерного сервера — незанятая возможность, а не дефект.
|
|
106
195
|
// Поэтому строка тусклая и без значка, и её нет у проекта без интерфейса.
|
|
@@ -131,7 +220,7 @@ async function reportCatalog(man, facts) {
|
|
|
131
220
|
// сессии и для самого владельца: список объявленных гейтов молчит о том, сколько из них
|
|
132
221
|
// действительно стоят и работают именно СЕЙЧАС. Перезаписывается каждым прогоном, не копится:
|
|
133
222
|
// история — дело git-лога коммитов с этим отчётом, если владелец решит его коммитить.
|
|
134
|
-
async function writeRunReport({ version, reached, results }) {
|
|
223
|
+
async function writeRunReport({ version, reached, results, skipped = [] }) {
|
|
135
224
|
const stamp = new Date().toISOString().replace("T", " ").slice(0, 16);
|
|
136
225
|
const ok = results.filter((r) => r.ok).length;
|
|
137
226
|
const lines = [
|
|
@@ -140,6 +229,9 @@ async function writeRunReport({ version, reached, results }) {
|
|
|
140
229
|
`${L.report.level}: AQK-${reached < 0 ? L.doctor.levelNone : reached}`,
|
|
141
230
|
"",
|
|
142
231
|
...results.map((r) => `${r.ok ? "✔" : "✘"} ${r.name} — ${r.secs}s${r.ok ? "" : ` (${r.note || L.doctor.exitCode(r.code)})`}`),
|
|
232
|
+
// Пропущенные по --skip/--only — строкой «~»: блок для агента читает их как «не запускались»,
|
|
233
|
+
// а не как зелёные. Молчание о них прочиталось бы как «проверено».
|
|
234
|
+
...skipped.map((n) => `~ ${n} — ${L.report.skippedBySelect}`),
|
|
143
235
|
"",
|
|
144
236
|
L.report.summary(ok, results.length),
|
|
145
237
|
].filter((l) => l !== null);
|
|
@@ -149,6 +241,22 @@ async function writeRunReport({ version, reached, results }) {
|
|
|
149
241
|
await writeFile(dst, lines.join("\n") + "\n", "utf8");
|
|
150
242
|
}
|
|
151
243
|
|
|
244
|
+
// ПРОБА ЗАПУСКАЕТСЯ САМА, раз в сто коммитов, — кроме конвейера (там это минуты сюрпризом в
|
|
245
|
+
// быстрой проверке, отзыв с живого проекта 2026-09-11). Не влияет на код возврата никогда: это
|
|
246
|
+
// осмотр, а не порог. Отдельной функцией: внутри прогона эта лесенка дала вложенность 6, и наш же
|
|
247
|
+
// complexity-limit её поймал.
|
|
248
|
+
async function autoProbe(brief) {
|
|
249
|
+
let st;
|
|
250
|
+
try { st = await probeStatus(); } catch { return; /* пробы нет — прогон про гейты, а не про неё */ }
|
|
251
|
+
if (st.badEvery !== undefined) { console.log(c.yellow(`\n ${L.probe.badEvery(st.badEvery)}`)); return; }
|
|
252
|
+
if (st.state !== "never" && st.state !== "stale") return;
|
|
253
|
+
if (!autoProbeAllowed({ brief })) { console.log(c.dim(`\n ${L.probe.autoNotInCi(`${SELF} probe`)}`)); return; }
|
|
254
|
+
// Сообщение обязано быть верным в обоих случаях: первая версия печатала «прошло сто коммитов»
|
|
255
|
+
// и там, где пробы не было ВОВСЕ — число бралось из порога, а не из факта.
|
|
256
|
+
console.log(c.dim(`\n ${st.state === "never" ? L.probe.autoFirst : L.probe.auto(st.behind)}`));
|
|
257
|
+
try { await cmdProbe([], { auto: true }); } catch { /* проба не состоялась — прогон это не роняет */ }
|
|
258
|
+
}
|
|
259
|
+
|
|
152
260
|
async function cmdDoctor() {
|
|
153
261
|
const brief = process.argv.includes("--brief");
|
|
154
262
|
const buf = brief ? beginBrief() : null;
|
|
@@ -171,10 +279,26 @@ async function cmdDoctor() {
|
|
|
171
279
|
const checks = layoutChecks(man, inKit);
|
|
172
280
|
|
|
173
281
|
let missing = 0;
|
|
174
|
-
for (const [path, what] of checks) {
|
|
282
|
+
for (const [path, what, required] of checks) {
|
|
175
283
|
const ok = await exists(join(CWD, path));
|
|
176
|
-
if (!ok) missing++;
|
|
177
|
-
|
|
284
|
+
if (!ok && required) missing++;
|
|
285
|
+
const mark = ok ? c.green("✔") : required ? c.red("✘") : c.dim("○");
|
|
286
|
+
console.log(` ${mark} ${path.padEnd(22)} ${c.dim(what)}${!ok && !required ? c.dim(` · ${L.doctor.layoutAdvice}`) : ""}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// СЛУЖЕБНЫЙ ФАЙЛ, КОТОРЫЙ ВИДИТ GIT. Отзыв с живого проекта 2026-09-11: `.aqk/last-run.md`
|
|
290
|
+
// однажды закоммитили, и каждый прогон оставлял изменённый файл. `init` теперь кладёт их в
|
|
291
|
+
// .gitignore сам; здесь — для тех, кто поставил раньше. Спрашиваем git, а не диск.
|
|
292
|
+
const git = (...a) => spawnSync("git", a, { cwd: CWD, encoding: "utf8" });
|
|
293
|
+
if (git("rev-parse", "--git-dir").status === 0) {
|
|
294
|
+
const tracked = new Set(String(git("ls-files", "--", TARGET_DIR).stdout || "").split("\n"));
|
|
295
|
+
for (const f of RUNTIME_FILES.map((n) => `${TARGET_DIR}/${n}`)) {
|
|
296
|
+
if (tracked.has(f)) {
|
|
297
|
+
console.log(`\n ${c.yellow("!")} ${L.doctor.runtimeTracked(f, `git rm --cached ${f} && echo ${f} >> .gitignore`)}`);
|
|
298
|
+
} else if (await exists(join(CWD, f)) && git("check-ignore", "-q", f).status !== 0) {
|
|
299
|
+
console.log(c.dim(`\n ${L.doctor.runtimeNotIgnored(f, `echo ${f} >> .gitignore`)}`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
178
302
|
}
|
|
179
303
|
|
|
180
304
|
// Команды в точке входа заполнены или остались пустыми заготовками? Файл берётся тот же,
|
|
@@ -227,7 +351,11 @@ async function cmdDoctor() {
|
|
|
227
351
|
// Доказательство считается только при прогоне: узнать, ловит ли гейт брак, нельзя иначе как
|
|
228
352
|
// запустив его по образцу. Без прогона ступени со второй помечаются «не доказано» — это
|
|
229
353
|
// честнее, чем показывать их выполненными по наличию папок.
|
|
354
|
+
// Доказательство — секунды тишины до первой строки уровня; строка «идёт» их называет.
|
|
355
|
+
const bar = progress();
|
|
356
|
+
if (process.argv.includes("--run")) bar.show(c.dim(` ⋯ ${L.doctor.proving}`));
|
|
230
357
|
const proof = process.argv.includes("--run") ? await proveGates(man) : null;
|
|
358
|
+
bar.clear();
|
|
231
359
|
const { reached, steps } = await assessLevel(man, proof);
|
|
232
360
|
|
|
233
361
|
console.log(c.bold(`\n ${L.doctor.levelHeading}\n`));
|
|
@@ -274,18 +402,29 @@ async function cmdDoctor() {
|
|
|
274
402
|
await reportBaseline(man, facts);
|
|
275
403
|
process.exit(0);
|
|
276
404
|
}
|
|
277
|
-
|
|
405
|
+
// Состояние пробы — из файла отметки, миллисекунды. Нет его — блок про пробу просто молчит.
|
|
406
|
+
let probe = null;
|
|
407
|
+
try { probe = await probeStatus(); } catch { /* пробы нет — и ладно */ }
|
|
408
|
+
const cat = (await reportCatalog(man, facts, probe)) || { held: 0, todo: 0, todoRecs: [] };
|
|
278
409
|
|
|
279
410
|
// «Объявлен» ≠ «работает». Без --run говорим это вслух, а не молчим.
|
|
280
411
|
const wantRun = process.argv.includes("--run");
|
|
281
412
|
const gates = declaredGates(man);
|
|
282
413
|
let gateFailed = 0;
|
|
283
414
|
let failedNames = [];
|
|
415
|
+
let skippedNames = [];
|
|
284
416
|
if (wantRun) {
|
|
285
|
-
|
|
417
|
+
// --jobs N: сколько гейтов одновременно. Без флага — по одному, как было: чужие гейты бывают
|
|
418
|
+
// зависимыми (общий dist/), и плавающее красное хуже медленного. Не число — отказ, а не тихий
|
|
419
|
+
// последовательный прогон под видом параллельного.
|
|
420
|
+
const ji = process.argv.indexOf("--jobs");
|
|
421
|
+
const jobs = ji > -1 ? Number(process.argv[ji + 1]) : 1;
|
|
422
|
+
if (!Number.isInteger(jobs) || jobs < 1) die(L.doctor.jobsBad(process.argv[ji + 1] ?? ""));
|
|
423
|
+
const run = await runGates(man, { since: sinceRef(), only: listArg(process.argv, "--only"), skip: listArg(process.argv, "--skip"), jobs });
|
|
286
424
|
gateFailed = run.failed;
|
|
287
425
|
failedNames = run.results.filter((r) => !r.ok).map((r) => r.name);
|
|
288
|
-
|
|
426
|
+
skippedNames = run.skipped || [];
|
|
427
|
+
await writeRunReport({ version, reached, results: run.results, skipped: run.skipped });
|
|
289
428
|
|
|
290
429
|
// ПРОБА ЗАПУСКАЕТСЯ САМА. Владелец сформулировал так: «команду, о которой надо вспомнить,
|
|
291
430
|
// агент не вспомнит, а человек о ней не узнает». Это тот же класс, что файл, который можно
|
|
@@ -297,20 +436,7 @@ async function cmdDoctor() {
|
|
|
297
436
|
// надо. В кратком режиме не запускается: там хук на воротах коммита, и лишние секунды там
|
|
298
437
|
// стоят дороже. Не влияет на код возврата НИКОГДА — это осмотр, а не порог.
|
|
299
438
|
// Выключается AQK_PROBE=0 — у всего, что случается само, обязан быть выключатель.
|
|
300
|
-
if (!brief && process.env.AQK_PROBE !== "0")
|
|
301
|
-
try {
|
|
302
|
-
const st = await probeStatus();
|
|
303
|
-
if (st.badEvery !== undefined) {
|
|
304
|
-
console.log(c.yellow(`\n ${L.probe.badEvery(st.badEvery)}`));
|
|
305
|
-
} else if (st.state === "never" || st.state === "stale") {
|
|
306
|
-
// Сообщение обязано быть верным в обоих случаях. Первая версия печатала «прошло сто
|
|
307
|
-
// коммитов» и там, где пробы не было ВОВСЕ: число бралось из порога, а не из факта.
|
|
308
|
-
// Мелочь, но того же класса, что и всё остальное здесь: вывод, который не врёт.
|
|
309
|
-
console.log(c.dim(`\n ${st.state === "never" ? L.probe.autoFirst : L.probe.auto(st.behind)}`));
|
|
310
|
-
await cmdProbe([], { auto: true });
|
|
311
|
-
}
|
|
312
|
-
} catch { /* проба не состоялась — прогон это не роняет: он про гейты, а не про неё */ }
|
|
313
|
-
}
|
|
439
|
+
if (!brief && process.env.AQK_PROBE !== "0") await autoProbe(brief);
|
|
314
440
|
} else if (gates.length) {
|
|
315
441
|
console.log(
|
|
316
442
|
c.yellow(` ${L.doctor.declaredNotRun(gates.length)}`) +
|
|
@@ -345,6 +471,7 @@ async function cmdDoctor() {
|
|
|
345
471
|
if (wantRun) {
|
|
346
472
|
if (ok) {
|
|
347
473
|
console.log(c.green(` ${L.doctor.runVerdictOk}\n`));
|
|
474
|
+
if (skippedNames.length) console.log(c.yellow(` ${L.doctor.selectSkipped(skippedNames.join(", "))}\n`));
|
|
348
475
|
} else {
|
|
349
476
|
const why = [];
|
|
350
477
|
if (missing) why.push(L.doctor.whyMissing);
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../lib/repo.mjs";
|
|
16
16
|
import { GATE_YML_TEMPLATE, CHECK_SH_TEMPLATE, README_TEMPLATE } from "../lib/templates.mjs";
|
|
17
17
|
import { L } from "../i18n/index.mjs";
|
|
18
|
+
import { gateCommand } from "../lib/execution.mjs";
|
|
18
19
|
|
|
19
20
|
// Ставит гейт из каталога в проект. Проверка КОПИРУЕТСЯ в репозиторий, а не остаётся
|
|
20
21
|
// ссылкой в пакет: при установке через npx пакет временный, и завтра команда в манифесте
|
|
@@ -230,7 +231,7 @@ async function cmdRatchet(args) {
|
|
|
230
231
|
|
|
231
232
|
// Снимок текущих нарушений — это и есть долг. Ключ без номера строки: правка соседней
|
|
232
233
|
// строки не должна читаться как новое нарушение.
|
|
233
|
-
const r = spawnSync(inner, { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
234
|
+
const r = spawnSync(gateCommand(inner), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
234
235
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|
|
235
236
|
die(L.ratchet.notRunnable(slug, inner));
|
|
236
237
|
}
|
|
@@ -438,7 +439,7 @@ async function cmdWhy(args) {
|
|
|
438
439
|
|
|
439
440
|
// --- 3. объявлен: спрашиваем у него самого ---------------------------------
|
|
440
441
|
console.log(c.dim(` ${L.why.declaredAs(cmd)}`));
|
|
441
|
-
const r = spawnSync(String(cmd), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
442
|
+
const r = spawnSync(gateCommand(String(cmd)), { shell: true, cwd: CWD, encoding: "utf8", timeout: 300000 });
|
|
442
443
|
const ci = await runsInCi(slug, String(cmd));
|
|
443
444
|
|
|
444
445
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|