agent-quality-kit 0.15.0 → 0.16.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 +61 -15
- package/README.ru.md +62 -15
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/ai/operational-gates.md +275 -0
- package/kit/gates/_target.sh +53 -0
- package/kit/gates/ci-actually-fails/check.sh +18 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +10 -0
- package/kit/gates/entry-commands-exist/check.sh +88 -12
- package/kit/gates/hook-actually-fires/README.md +12 -0
- package/kit/gates/hook-actually-fires/check.sh +66 -6
- package/kit/gates/hook-actually-fires/gate.yml +2 -2
- package/kit/gates/hook-actually-fires/green/.claude/hooks/auto-format.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/block-dangerous.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/done.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/idle.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/prompt.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/session.mjs +1 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/stop-gate.sh +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +12 -0
- package/kit/gates/test-not-adjusted/README.md +31 -0
- package/llms.txt +26 -7
- package/package.json +1 -1
- package/tool/commands/context.mjs +3 -1
- package/tool/commands/doctor-catalog.mjs +35 -10
- package/tool/commands/feedback.mjs +75 -1
- package/tool/commands/gates.mjs +12 -6
- package/tool/commands/report.mjs +19 -3
- package/tool/commands/vitals.mjs +9 -3
- package/tool/i18n/en-docs.mjs +18 -2
- package/tool/i18n/en-gates.mjs +9 -1
- package/tool/i18n/en.mjs +24 -2
- package/tool/i18n/ru-docs.mjs +17 -2
- package/tool/i18n/ru-gates.mjs +9 -1
- package/tool/i18n/ru.mjs +20 -2
- package/tool/lib/adopt.mjs +58 -4
- package/tool/lib/core.mjs +42 -6
- package/tool/lib/execution.mjs +32 -1
- package/tool/lib/manifest.mjs +39 -13
- package/tool/lib/prove.mjs +3 -3
- package/tool/lib/run.mjs +25 -6
- package/tool/selfcheck/smoke/_fixture.mjs +13 -1
- package/tool/selfcheck/smoke/feedback-send.test.mjs +87 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +67 -3
- package/tool/selfcheck/smoke/preflight.test.mjs +83 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +50 -4
- package/tool/selfcheck/smoke/version-sync.test.mjs +140 -0
- package/tool/selfcheck/smoke.sh +106 -4
- package/tool/selfcheck/units-execution.mjs +37 -1
- package/tool/selfcheck/units-level.mjs +41 -1
- package/tool/selfcheck/units-repo.mjs +75 -0
- package/tool/selfcheck/units-vitals.mjs +27 -0
- package/kit/gates/entry-links-exist/README.md +0 -27
- package/kit/gates/entry-links-exist/check.sh +0 -33
- package/kit/gates/entry-links-exist/gate.yml +0 -17
- package/kit/gates/entry-links-exist/green/AGENTS.md +0 -10
- package/kit/gates/entry-links-exist/green/rules/general.md +0 -3
- package/kit/gates/entry-links-exist/red/AGENTS.md +0 -3
- package/kit/gates/no-phantom-package/README.md +0 -84
- package/kit/gates/no-phantom-package/check.sh +0 -168
- package/kit/gates/no-phantom-package/gate.yml +0 -20
- package/kit/gates/no-phantom-package/green/AGENTS.md +0 -15
- package/kit/gates/no-phantom-package/red/AGENTS.md +0 -15
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// репозитория в отчёте нет — иначе первый же внимательный читатель назовёт это телеметрией,
|
|
18
18
|
// и будет прав.
|
|
19
19
|
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
20
21
|
import { PKG_ROOT, SELF, REPO_URL, c, stateDirs } from "../lib/core.mjs";
|
|
21
22
|
import { askAllowed, markAsked } from "../lib/ask.mjs";
|
|
22
23
|
import { join } from "node:path";
|
|
@@ -65,6 +66,9 @@ function reportText(state = {}, T = L.feedback.report) {
|
|
|
65
66
|
"",
|
|
66
67
|
T.say,
|
|
67
68
|
"",
|
|
69
|
+
// Слова человека — туда же, где приглашение их написать. Пусто — остаётся приглашение:
|
|
70
|
+
// отправленное письмо без единой своей строки всё равно полезно, но сказать об этом надо.
|
|
71
|
+
...(state.note ? [state.note, ""] : []),
|
|
68
72
|
T.mark(state.version || u),
|
|
69
73
|
];
|
|
70
74
|
}
|
|
@@ -110,6 +114,65 @@ async function maybeAsk(state, self, { agent = false } = {}) {
|
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
// ОТПРАВКА — ТОЛЬКО ПО ЯВНОМУ СЛОВУ, И СЛОВО ЭТО `--send`.
|
|
119
|
+
//
|
|
120
|
+
// Владелец 2026-09-14 спросил, нельзя ли отправлять отзыв «без согласия пользователя, чтобы
|
|
121
|
+
// агент мог быстро сообщить». Нельзя, и не из вежливости: в README и SECURITY.md написано, что
|
|
122
|
+
// исходящий запрос у комплекта ровно один — про версию. Инструмент, который втихую шлёт что-то
|
|
123
|
+
// из ЧУЖОГО репозитория, становится ровно тем, что мы критикуем, а наша аудитория — это те, кто
|
|
124
|
+
// проверяет инструменты на вранье. Одного внимательного читателя хватит.
|
|
125
|
+
//
|
|
126
|
+
// Поэтому согласие живёт в самом флаге: `--send` не набирают случайно, а агенту в умениях
|
|
127
|
+
// сказано показать текст человеку и спросить. Утверждение «без флага не уходит ничего» держит
|
|
128
|
+
// машина — smoke/feedback-send.test.mjs, — а не наше обещание в документации.
|
|
129
|
+
//
|
|
130
|
+
// ОТ ЧЬЕГО ИМЕНИ. От самого человека, его же `gh`. Своего сервера у нас нет и не будет: он
|
|
131
|
+
// означал бы приём чужих данных, а значит и ответственность за них.
|
|
132
|
+
const DISCUSSION = 90;
|
|
133
|
+
|
|
134
|
+
function sendWanted(argv = process.argv) {
|
|
135
|
+
return argv.includes("--send");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Своя строка человека — самое ценное во всём письме. Берём всё, что не флаг.
|
|
139
|
+
function userNote(argv = process.argv) {
|
|
140
|
+
const i = argv.indexOf("feedback");
|
|
141
|
+
return (i === -1 ? [] : argv.slice(i + 1)).filter((a) => !a.startsWith("-")).join(" ").trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Чем звать gh. Подменяется `AQK_GH` — тот же приём, что у `AQK_BASH` в execution.mjs: у всего,
|
|
145
|
+
// что мы решаем сами, обязан быть способ решить иначе. Через него же проверки подставляют
|
|
146
|
+
// поддельный gh и убеждаются, что без флага его не зовут вовсе.
|
|
147
|
+
function ghArgs(env = process.env) {
|
|
148
|
+
return String(env.AQK_GH || "gh").split(/\s+/).filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Запуск БЕЗ оболочки: в теле отзыва переносы строк, кавычки и обратные апострофы, и оболочка
|
|
152
|
+
// разобрала бы их как свои. Доводы уходят массивом — разбирать нечего.
|
|
153
|
+
function gh(args, timeout = 30000) {
|
|
154
|
+
const [bin, ...pre] = ghArgs();
|
|
155
|
+
return spawnSync(bin, [...pre, ...args], { encoding: "utf8", timeout });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const Q_ID = `query($o:String!,$n:String!){repository(owner:$o,name:$n){discussion(number:${DISCUSSION}){id}}}`;
|
|
159
|
+
const Q_ADD = "mutation($id:ID!,$body:String!){addDiscussionComment(input:{discussionId:$id,body:$body}){comment{url}}}";
|
|
160
|
+
|
|
161
|
+
// Три исхода, как везде: отправлено · не смогли и сказали почему · входа нет. Молчаливый отказ
|
|
162
|
+
// означал бы, что человек считает отзыв ушедшим, а его нет.
|
|
163
|
+
function postComment(body, repo) {
|
|
164
|
+
if (gh(["auth", "status"], 15000).status !== 0) return { ok: false, reason: "no-auth" };
|
|
165
|
+
const [owner, name] = repo;
|
|
166
|
+
const one = gh(["api", "graphql", "-f", `query=${Q_ID}`, "-f", `o=${owner}`, "-f", `n=${name}`]);
|
|
167
|
+
let id = "";
|
|
168
|
+
try { id = JSON.parse(one.stdout || "{}").data.repository.discussion.id; } catch { /* разберём ниже */ }
|
|
169
|
+
if (!id) return { ok: false, reason: "no-thread" };
|
|
170
|
+
const two = gh(["api", "graphql", "-f", `query=${Q_ADD}`, "-f", `id=${id}`, "-f", `body=${body}`]);
|
|
171
|
+
let url = "";
|
|
172
|
+
try { url = JSON.parse(two.stdout || "{}").data.addDiscussionComment.comment.url; } catch { /* разберём ниже */ }
|
|
173
|
+
return url ? { ok: true, url } : { ok: false, reason: "failed" };
|
|
174
|
+
}
|
|
175
|
+
|
|
113
176
|
// Предзаполненная ссылка. Параметры `title` и `body` — документация GitHub («Creating an issue
|
|
114
177
|
// from a URL query», сверено 2026-09-14). Кодируется ВСЁ: в теле переносы строк, решётки и
|
|
115
178
|
// пробелы, и незакодированная ссылка обрывается на первом же из них — а всё после решётки
|
|
@@ -142,13 +205,24 @@ async function cmdFeedback() {
|
|
|
142
205
|
version, node: process.version, platform: process.platform, level,
|
|
143
206
|
langs: facts?.langs ? [...facts.langs] : [],
|
|
144
207
|
gates: declaredGates(man).length,
|
|
145
|
-
red: run?.red || [], cannot: run?.cannot || [], blind,
|
|
208
|
+
red: run?.red || [], cannot: run?.cannot || [], blind, note: userNote(),
|
|
146
209
|
});
|
|
147
210
|
const body = lines.join("\n");
|
|
148
211
|
console.log(`\n${body}\n`);
|
|
212
|
+
|
|
213
|
+
// ОТПРАВКА — ТОЛЬКО ПО ФЛАГУ. Без него ниже печатается ссылка, и это весь путь наружу.
|
|
214
|
+
if (sendWanted()) {
|
|
215
|
+
console.log(c.dim(` ${T.sending}`));
|
|
216
|
+
const repo = REPO_URL.replace(/^https?:\/\/github\.com\//, "").split("/");
|
|
217
|
+
const r = postComment(body, repo);
|
|
218
|
+
if (r.ok) { console.log(c.green(` ${T.sent(r.url)}\n`)); return; }
|
|
219
|
+
console.log(c.yellow(` ${T.sendFailed[r.reason] || T.sendFailed.failed}\n`));
|
|
220
|
+
}
|
|
221
|
+
|
|
149
222
|
console.log(c.bold(` ${T.how}`));
|
|
150
223
|
console.log(` ${issueUrl(REPO_URL, T.issueTitle, body)}\n`);
|
|
151
224
|
console.log(c.dim(` ${T.nothingSent}`));
|
|
225
|
+
console.log(c.dim(` ${T.sendHow(`${SELF} feedback --send`)}`));
|
|
152
226
|
console.log(c.dim(` ${T.orPaste(`${SELF} feedback`)}\n`));
|
|
153
227
|
// Код возврата всегда 0: команда, которая просит об одолжении и роняет при этом конвейер, —
|
|
154
228
|
// последнее, что человек стерпит.
|
package/tool/commands/gates.mjs
CHANGED
|
@@ -15,7 +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
|
+
import { gateCommand, gateTimeout } from "../lib/execution.mjs";
|
|
19
19
|
|
|
20
20
|
// Ставит гейт из каталога в проект. Проверка КОПИРУЕТСЯ в репозиторий, а не остаётся
|
|
21
21
|
// ссылкой в пакет: при установке через npx пакет временный, и завтра команда в манифесте
|
|
@@ -43,7 +43,11 @@ async function installGate(slug, man, facts) {
|
|
|
43
43
|
|
|
44
44
|
// Общий список исключений едет вместе с проверкой: без него она читает окружение и
|
|
45
45
|
// зависимости, и человек получает тысячу чужих нарушений вместо сотни своих.
|
|
46
|
-
|
|
46
|
+
// `_target.sh` добавлен 2026-09-15 вместе с общим швом «судим только то, что можем разрешить».
|
|
47
|
+
// Без него скопированный гейт выходит с кодом 2 — это честно, но у человека он не работает.
|
|
48
|
+
// Поймано нашим же смоуком в день правки: список помощников тут — единственное место, и
|
|
49
|
+
// забыть его значит выпустить гейт, который у чужого проекта не запускается вовсе.
|
|
50
|
+
for (const helper of ["_skip.sh", "_native.sh", "_target.sh"]) {
|
|
47
51
|
const from = join(GATES_SRC, helper);
|
|
48
52
|
if (await exists(from)) await copyFile(from, join(CWD, PROJECT_GATES, helper));
|
|
49
53
|
}
|
|
@@ -170,8 +174,10 @@ async function cmdNew(args) {
|
|
|
170
174
|
// зовут установленные записи каталога. В самом комплекте оригинал уже лежит на месте (../),
|
|
171
175
|
// в чужом проекте его никто не клал, пока не было ни одной установленной записи через `add`.
|
|
172
176
|
if (!inKit) {
|
|
173
|
-
const
|
|
174
|
-
|
|
177
|
+
for (const helper of ["_skip.sh", "_target.sh"]) {
|
|
178
|
+
const from = join(GATES_SRC, helper);
|
|
179
|
+
if (await exists(from)) await copyFile(from, join(CWD, PROJECT_GATES, helper));
|
|
180
|
+
}
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
console.log(c.bold(`\naqk new ${slug}\n`));
|
|
@@ -231,7 +237,7 @@ async function cmdRatchet(args) {
|
|
|
231
237
|
|
|
232
238
|
// Снимок текущих нарушений — это и есть долг. Ключ без номера строки: правка соседней
|
|
233
239
|
// строки не должна читаться как новое нарушение.
|
|
234
|
-
const r = spawnSync(gateCommand(inner), { shell: true, cwd: CWD, encoding: "utf8", timeout:
|
|
240
|
+
const r = spawnSync(gateCommand(inner), { shell: true, cwd: CWD, encoding: "utf8", timeout: gateTimeout().ms });
|
|
235
241
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|
|
236
242
|
die(L.ratchet.notRunnable(slug, inner));
|
|
237
243
|
}
|
|
@@ -439,7 +445,7 @@ async function cmdWhy(args) {
|
|
|
439
445
|
|
|
440
446
|
// --- 3. объявлен: спрашиваем у него самого ---------------------------------
|
|
441
447
|
console.log(c.dim(` ${L.why.declaredAs(cmd)}`));
|
|
442
|
-
const r = spawnSync(gateCommand(String(cmd)), { shell: true, cwd: CWD, encoding: "utf8", timeout:
|
|
448
|
+
const r = spawnSync(gateCommand(String(cmd)), { shell: true, cwd: CWD, encoding: "utf8", timeout: gateTimeout().ms });
|
|
443
449
|
const ci = await runsInCi(slug, String(cmd));
|
|
444
450
|
|
|
445
451
|
if (r.status === 127 || (r.error && r.error.code === "ENOENT")) {
|
package/tool/commands/report.mjs
CHANGED
|
@@ -215,11 +215,27 @@ async function cmdReport() {
|
|
|
215
215
|
|
|
216
216
|
const text = lines.join("\n") + "\n";
|
|
217
217
|
const dst = join(CWD, TARGET_DIR, "report.md");
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
|
|
219
|
+
// ОТЧЁТ — ЭТО ЧТЕНИЕ, И ПАДАТЬ ЕМУ НЕ НА ЧЕМ. Разбор чужой интеграции 2026-09-16: в рабочей
|
|
220
|
+
// области, где `.aqk/` не создать, команда падала с EROFS, не напечатав ни строки. То есть
|
|
221
|
+
// побочное действие, о котором не просили, отменяло то, ради чего команду звали. Такие
|
|
222
|
+
// области бывают не по недосмотру: read-only контейнер, чужой конвейер, каталог под ревью.
|
|
223
|
+
//
|
|
224
|
+
// Молчать о несохранённом тоже нельзя: человек, увидевший отчёт, вправе считать, что файл
|
|
225
|
+
// лежит на диске, — если ему не сказали иначе. Тот же принцип, что и везде: не смогли —
|
|
226
|
+
// назови, а не выдавай за сделанное.
|
|
227
|
+
let saveError = null;
|
|
228
|
+
try {
|
|
229
|
+
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
230
|
+
await writeFile(dst, text, "utf8");
|
|
231
|
+
} catch (e) {
|
|
232
|
+
saveError = e?.code || String(e?.message || e);
|
|
233
|
+
}
|
|
220
234
|
|
|
221
235
|
console.log("\n" + text);
|
|
222
|
-
console.log(
|
|
236
|
+
console.log(saveError
|
|
237
|
+
? c.yellow(` ${L.report2.notSaved(join(TARGET_DIR, "report.md"), saveError)}\n`)
|
|
238
|
+
: c.dim(` ${L.report2.saved(join(TARGET_DIR, "report.md"))}\n`));
|
|
223
239
|
|
|
224
240
|
// Код возврата — для конвейера и для агента: отчёт с красным гейтом не должен читаться
|
|
225
241
|
// как «всё в порядке» только потому, что команда выполнилась.
|
package/tool/commands/vitals.mjs
CHANGED
|
@@ -45,10 +45,16 @@ function vitalsRows(f) {
|
|
|
45
45
|
ok: f.unparsed > 0 ? false : true,
|
|
46
46
|
detail: f.unparsed > 0 ? t.manifestBad(f.unparsed) : t.manifestOk,
|
|
47
47
|
},
|
|
48
|
+
// ЧЕТЫРЕ ОТВЕТА ПРО ХУК. «Хук чужой» — не «хука нет»: в одном случае ставить нечего, в
|
|
49
|
+
// другом дописать строку в уже стоящий. Совет разный, значит и строка разная. Ни то ни
|
|
50
|
+
// другое не отказ: AQK могут сознательно гонять в конвейере.
|
|
48
51
|
{
|
|
49
52
|
key: "preCommit",
|
|
50
|
-
ok: f.preCommit === null ? null : f.preCommit ? true : "no",
|
|
51
|
-
detail: f.preCommit === null ? t.unknownHook
|
|
53
|
+
ok: f.preCommit === null ? null : f.preCommit === true ? true : "no",
|
|
54
|
+
detail: f.preCommit === null ? t.unknownHook
|
|
55
|
+
: f.preCommit === true ? t.preCommitOk
|
|
56
|
+
: f.preCommit === "other" ? t.preCommitOther
|
|
57
|
+
: t.preCommitNo,
|
|
52
58
|
},
|
|
53
59
|
{
|
|
54
60
|
key: "sessionHook",
|
|
@@ -110,7 +116,7 @@ async function cmdVitals() {
|
|
|
110
116
|
// Найдено 2026-09-09 сверкой вывода двух команд на одном репозитории.
|
|
111
117
|
const samplesDir = typeof man?.samples === "string" ? man.samples.trim() : "";
|
|
112
118
|
for (const gate of Object.keys(gates)) {
|
|
113
|
-
const missing = await gateRequires(samplesDir, gate, whichSync);
|
|
119
|
+
const missing = await gateRequires(man, samplesDir, gate, whichSync);
|
|
114
120
|
for (const prog of missing || []) {
|
|
115
121
|
if (seen.has(prog)) continue;
|
|
116
122
|
seen.set(prog, { gate, prog, found: false });
|
package/tool/i18n/en-docs.mjs
CHANGED
|
@@ -26,6 +26,9 @@ const enDocs = {
|
|
|
26
26
|
unknownHook: "could not look — unknown, not \"no\"",
|
|
27
27
|
preCommitOk: "wired in .git/hooks",
|
|
28
28
|
preCommitNo: "not in .git/hooks: a config entry is an intent, not a guard",
|
|
29
|
+
// A hook IS there, but not ours: nothing to install, something to add — a different
|
|
30
|
+
// remedy, hence a different line. Until 2026-09-16 this case printed as "wired".
|
|
31
|
+
preCommitOther: "a .git/hooks hook exists, but AQK is not called from it — check .pre-commit-config.yaml or the hook body",
|
|
29
32
|
sessionOk: "the SessionStart hook hands the state to the agent",
|
|
30
33
|
sessionNo: (cmd) => `the agent gets no state: ${cmd}`,
|
|
31
34
|
versionOk: (v) => `${v}, current`,
|
|
@@ -83,7 +86,16 @@ const enDocs = {
|
|
|
83
86
|
nextTitle: "Next, in order — computed from this repository, not generic advice:",
|
|
84
87
|
nextStep: {
|
|
85
88
|
init: () => "Set up the standard: `aqk init` — without .aqk.yml `aqk add` refuses.",
|
|
86
|
-
|
|
89
|
+
// Слабые НЕ ПОПАДАЮТ в «впиши в манифест»: агент исполняет написанное, и объявленная
|
|
90
|
+
// проверка с `|| true` — это дыра, за которую с этого дня ручается машина.
|
|
91
|
+
adopt: (s) => {
|
|
92
|
+
const ok = s.gates.filter((g) => !g.weak);
|
|
93
|
+
const weak = s.gates.filter((g) => g.weak);
|
|
94
|
+
const out = [];
|
|
95
|
+
if (ok.length) out.push(`Declare the checks this project already has: in .aqk.yml, under gates: ${ok.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Then \`aqk doctor --run\`.`);
|
|
96
|
+
if (weak.length) out.push(`These cannot go red as written — ${weak.map((g) => `${g.cmd} (${g.weak.text})`).join(", ")} — so do not declare them yet: tell the owner and fix the command first.`);
|
|
97
|
+
return out.join(" ");
|
|
98
|
+
},
|
|
87
99
|
blind: (s) => `The "${s.slug}" defect the probe planted in ${s.file} was NOT caught by your checks.` +
|
|
88
100
|
(s.command ? ` Catch it now: \`${s.command}\`.` : "") + ` Keep it caught: \`aqk add ${s.slug}\`.`,
|
|
89
101
|
start: (s) => `Install ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (one line, no kit needed: \`${s.command}\`)` : "") +
|
|
@@ -93,7 +105,9 @@ const enDocs = {
|
|
|
93
105
|
whenTitle: "When to do what:",
|
|
94
106
|
whenCommit: (hook) => hook === true
|
|
95
107
|
? "Before a commit → the `.git/hooks/pre-commit` hook runs the checks itself; do not bypass it (`--no-verify`)."
|
|
96
|
-
:
|
|
108
|
+
: hook === "other"
|
|
109
|
+
? "Before a commit → `aqk doctor --run --since main`: a hook is installed in `.git/hooks`, but AQK is not called from it — the run will not happen by itself."
|
|
110
|
+
: "Before a commit → `aqk doctor --run --since main`: there is no pre-commit hook, it will not happen by itself.",
|
|
97
111
|
whenRules: [
|
|
98
112
|
"Added or changed a check → `aqk prove`: the gate must go red on its own red sample, otherwise it checks nothing.",
|
|
99
113
|
"Writing a rule into the rulebook → put the arbiter mark next to it, `<!-- aqk: gate-name -->`; no gate — `<!-- aqk: human -->`, which is an admission, not a check.",
|
|
@@ -155,6 +169,7 @@ const enDocs = {
|
|
|
155
169
|
whyTitle: "Why this matters — briefly",
|
|
156
170
|
whyNothing: "nothing to add: everything applicable is in place",
|
|
157
171
|
saved: (path) => `Saved: ${path}`,
|
|
172
|
+
notSaved: (f, why) => `could not save to ${f} (${why}) — the report is above, there is no file on disk. The command only reports state, so a failed write does not fail it.`,
|
|
158
173
|
docs: {
|
|
159
174
|
baseline: "the minimum a project needs, independent of language",
|
|
160
175
|
readyMade: "the map of off-the-shelf rules: look for a ready one before writing your own",
|
|
@@ -218,6 +233,7 @@ const enDocs = {
|
|
|
218
233
|
],
|
|
219
234
|
report: {
|
|
220
235
|
skippedBySelect: "not run (by --only/--skip), state unknown",
|
|
236
|
+
notWritten: (f, why) => `could not write the run report to ${f} (${why}) — the gates did run, but the next session and the hook will read this as "no run happened".`,
|
|
221
237
|
title: "aqk doctor --run",
|
|
222
238
|
version: "version",
|
|
223
239
|
level: "level",
|
package/tool/i18n/en-gates.mjs
CHANGED
|
@@ -194,8 +194,16 @@ export const enGates = {
|
|
|
194
194
|
red: (names, cmd) =>
|
|
195
195
|
`The kit caught: ${names}. One line to the author — did it help: ${cmd}`,
|
|
196
196
|
once: "Asked once per project; switch it off with AQK_FEEDBACK=0",
|
|
197
|
-
agent: "Say this to the human in one sentence, and do not insist if they do not answer.",
|
|
197
|
+
agent: "Say this to the human in one sentence, and do not insist if they do not answer. If they want to send it, add `--send`; never add the flag on your own.",
|
|
198
198
|
},
|
|
199
|
+
sending: "sending on your behalf — with your own gh account…",
|
|
200
|
+
sent: (url) => `sent: ${url}\n That is the whole price of this kit. Thank you.`,
|
|
201
|
+
sendFailed: {
|
|
202
|
+
"no-auth": "could not send: gh is missing or you are not logged in (`gh auth login`). The link below always works.",
|
|
203
|
+
"no-thread": "could not send: the discussion was not found. Use the link below.",
|
|
204
|
+
failed: "could not send. The link below always works.",
|
|
205
|
+
},
|
|
206
|
+
sendHow: (cmd) => `Send it in one command, with your own GitHub account: ${cmd}`,
|
|
199
207
|
how: "Sending is one click (a prefilled issue opens; the text stays editable):",
|
|
200
208
|
issueTitle: "Feedback on the kit",
|
|
201
209
|
nothingSent: "Nothing was sent: the kit makes no outgoing requests other than the version check.",
|
package/tool/i18n/en.mjs
CHANGED
|
@@ -51,6 +51,7 @@ export const en = {
|
|
|
51
51
|
learn: "rule candidates from local transcripts: said out loud, never written down",
|
|
52
52
|
context: "the project state in one block — for an agent's context, not for reading",
|
|
53
53
|
feedback: "a report on how the kit worked plus a prefilled link — the only payment it asks",
|
|
54
|
+
feedbackSend: "send the feedback in one command — with your own gh account; without the flag nothing leaves",
|
|
54
55
|
vitals: "is what the kit runs on wired up: gate tools, hooks, version freshness",
|
|
55
56
|
prompt: "one task for the agent: what to fix, in order, and how to prove it is done",
|
|
56
57
|
contextInstall: "the same in full — the map and the rulebook — installed as a hook",
|
|
@@ -153,7 +154,17 @@ export const en = {
|
|
|
153
154
|
startHook: "Running these by hand is a one-off. To have them run before every push: pre-commit (repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit, hooks aqk / aqk-doctor), or a plain .git/hooks/pre-push.",
|
|
154
155
|
haveAlready: (n) => `Checks you ALREADY have (${n}) — found in your own files, not invented:`,
|
|
155
156
|
haveAlreadyHow: (line) => `declare them and a machine holds them, not your attention. In .aqk.yml, under gates: ${line}`,
|
|
156
|
-
|
|
157
|
+
// Проверка, которая у проекта ЕСТЬ и провалиться НЕ МОЖЕТ. Самое ценное, что мы знаем о
|
|
158
|
+
// чужом репозитории в первые пять секунд, — и до 2026-09-14 мы об этом молчали.
|
|
159
|
+
weakOff: (text) => `cannot fail: the verdict is swallowed right in the script — «${text}»`,
|
|
160
|
+
weakZero: () => "cannot fail: --exit-zero tells the tool to exit 0 whatever it finds",
|
|
161
|
+
weakStub: (text) => `proves nothing: the whole script is a printout — «${text}»`,
|
|
162
|
+
weakHow: (n) => `${n} of them cannot go red. Declaring a check that cannot fail only makes the ` +
|
|
163
|
+
"silence machine-readable — fix the command first, then declare it.",
|
|
164
|
+
// Counted over the AQK CATALOGUE, and the line says so: a project gate named its own way
|
|
165
|
+
// closes none of our entries by name, and "held by a machine 0" read as a verdict on a
|
|
166
|
+
// repository with thirty checks of its own. Measured 2026-09-16 across twelve repositories.
|
|
167
|
+
total: "AQK catalogue totals:",
|
|
157
168
|
totalHeld: (n) => `held by a machine ${n}`,
|
|
158
169
|
totalTodo: (n) => `applicable but not installed ${n}`,
|
|
159
170
|
totalSkip: (n) => `hidden ${n}`,
|
|
@@ -168,6 +179,7 @@ export const en = {
|
|
|
168
179
|
`advisory and red: ${names.join(", ")}. These are switched-off checks: ` +
|
|
169
180
|
`either fix them and drop them from advisory, or admit the rule does not exist.`,
|
|
170
181
|
runHeading: "Running the declared gates",
|
|
182
|
+
timeoutBadEnv: (raw, secs) => `AQK_GATE_TIMEOUT="${raw}" is not a number of seconds above zero; waiting ${secs}s as before. Zero and garbage mean "wait forever" to spawnSync, and a hung gate looks exactly like a working one.`,
|
|
171
183
|
timeout: "did not finish within 5 minutes",
|
|
172
184
|
cannotCheck: (why) => `could not check: ${why}`,
|
|
173
185
|
whySpawn: (code) => `failed to start${code ? ` (${code})` : ""}`,
|
|
@@ -294,7 +306,17 @@ export const en = {
|
|
|
294
306
|
missed: ({ slug, file }, s) => `Gate \`${slug}\` is installed but missed the defect the probe planted into \`${file}\`. Find out why — a common cause is that the gate does not look at this file type or folder; \`${s} probe\` has the details. Done — \`${s} probe\` no longer names this class.`,
|
|
295
307
|
red: (name, s) => `Gate \`${name}\` is red. Run \`${s} doctor --run --only ${name}\`, read the findings and fix the code. Done — that command passes.`,
|
|
296
308
|
blind: ({ slug, file, command }, s) => `The probe planted a \`${slug}\` defect into \`${file}\` and the project's checks did not notice. Add a check: \`${s} add ${slug}\`${command ? ` (the same as one line, without the kit: \`${command}\`)` : ""}. Done — \`${s} doctor --run --only ${slug}\` passes and \`${s} probe\` no longer names this class.`,
|
|
297
|
-
|
|
309
|
+
// СЛАБЫЕ ОТДЕЛЯЮТСЯ ОТ РАБОЧИХ, и это не украшение вывода. Агент исполняет написанное:
|
|
310
|
+
// велеть ему «впиши эти проверки в манифест», когда одна из них `… || true`, значит руками
|
|
311
|
+
// агента превратить дыру в зелёную галочку — и с этого дня её будет держать машина.
|
|
312
|
+
adopt: (gates, s) => {
|
|
313
|
+
const weak = gates.filter((g) => g.weak);
|
|
314
|
+
const ok = gates.filter((g) => !g.weak);
|
|
315
|
+
const out = [];
|
|
316
|
+
if (ok.length) out.push(`The project already has its own checks: ${ok.map((g) => `\`${g.cmd}\` (${g.source})`).join(", ")}. Declare them under gates: in .aqk.yml — ${ok.map((g) => `\`${g.name}: "${g.cmd}"\``).join(", ")}. Done — \`${s} doctor --run\` runs them.`);
|
|
317
|
+
if (weak.length) out.push(`These CANNOT go red as written: ${weak.map((g) => `\`${g.cmd}\` — ${g.weak.text}`).join(", ")}. Fix the command before declaring it: a declared check that cannot fail turns a hole into a green tick, and from then on a machine vouches for it. This is the owner's call, not yours — say what you found.`);
|
|
318
|
+
return out.join(" ");
|
|
319
|
+
},
|
|
298
320
|
shim: {
|
|
299
321
|
missing: (s) => `Claude Code is set up here, but the rules live in AGENTS.md — it only reads CLAUDE.md. Create a CLAUDE.md with the single line \`@AGENTS.md\`. Done — \`${s} doctor\` no longer warns about it.`,
|
|
300
322
|
noImport: (s) => `CLAUDE.md does not import AGENTS.md — Claude Code only sees CLAUDE.md. Add the line \`@AGENTS.md\` to CLAUDE.md (mentioning the file in prose does not load it). Done — \`${s} doctor\` no longer warns about it.`,
|
package/tool/i18n/ru-docs.mjs
CHANGED
|
@@ -26,6 +26,10 @@ const ruDocs = {
|
|
|
26
26
|
unknownHook: "посмотреть не смогли — неизвестно, а не «нет»",
|
|
27
27
|
preCommitOk: "прописан в .git/hooks",
|
|
28
28
|
preCommitNo: "в .git/hooks его нет: запись в конфиге — намерение, а не защита",
|
|
29
|
+
// ХУК ЕСТЬ, НО НЕ НАШ. Ставить нечего — надо дописать: совет другой, значит и строка
|
|
30
|
+
// другая. До 2026-09-16 этот случай печатался как «прописан», и репозиторий, где AQK не
|
|
31
|
+
// вызывался ни разу, выглядел подключённым.
|
|
32
|
+
preCommitOther: "хук в .git/hooks есть, но AQK в нём не вызывается — проверьте .pre-commit-config.yaml либо само тело хука",
|
|
29
33
|
sessionOk: "хук SessionStart отдаёт состояние агенту",
|
|
30
34
|
sessionNo: (cmd) => `агент не получает состояние: ${cmd}`,
|
|
31
35
|
versionOk: (v) => `${v}, свежая`,
|
|
@@ -87,7 +91,14 @@ const ruDocs = {
|
|
|
87
91
|
// complexity-limit её поймал.
|
|
88
92
|
nextStep: {
|
|
89
93
|
init: () => "Заведи стандарт: `aqk init` — без .aqk.yml `aqk add` откажет.",
|
|
90
|
-
adopt: (s) =>
|
|
94
|
+
adopt: (s) => {
|
|
95
|
+
const ok = s.gates.filter((g) => !g.weak);
|
|
96
|
+
const weak = s.gates.filter((g) => g.weak);
|
|
97
|
+
const out = [];
|
|
98
|
+
if (ok.length) out.push(`Объяви проверки, которые у проекта уже есть: в .aqk.yml, в gates: ${ok.map((g) => `${g.name}: "${g.cmd}"`).join(", ")}. Затем \`aqk doctor --run\`.`);
|
|
99
|
+
if (weak.length) out.push(`А эти в нынешнем виде покраснеть не могут — ${weak.map((g) => `${g.cmd} (${g.weak.text})`).join(", ")} — и объявлять их пока нельзя: скажи владельцу и почини команду.`);
|
|
100
|
+
return out.join(" ");
|
|
101
|
+
},
|
|
91
102
|
blind: (s) => `Брак «${s.slug}», подсаженный пробой в ${s.file}, ваши проверки НЕ поймали.` +
|
|
92
103
|
(s.command ? ` Поймать сейчас: \`${s.command}\`.` : "") + ` Держать всегда: \`aqk add ${s.slug}\`.`,
|
|
93
104
|
start: (s) => `Поставь ${s.slug}: \`aqk add ${s.slug}\`` + (s.command ? ` (одной строкой, без комплекта: \`${s.command}\`)` : "") +
|
|
@@ -97,7 +108,9 @@ const ruDocs = {
|
|
|
97
108
|
whenTitle: "Когда что делать:",
|
|
98
109
|
whenCommit: (hook) => hook === true
|
|
99
110
|
? "Перед коммитом → хук `.git/hooks/pre-commit` прогонит проверки сам; не обходи его (`--no-verify`)."
|
|
100
|
-
:
|
|
111
|
+
: hook === "other"
|
|
112
|
+
? "Перед коммитом → `aqk doctor --run --since main`: хук в `.git/hooks` стоит, но AQK из него не вызывается — сам собой прогон не случится."
|
|
113
|
+
: "Перед коммитом → `aqk doctor --run --since main`: хука перед коммитом нет, само это не случится.",
|
|
101
114
|
whenRules: [
|
|
102
115
|
"Добавил или поменял проверку → `aqk prove`: гейт обязан покраснеть на своём красном образце, иначе он не проверяет ничего.",
|
|
103
116
|
"Пишешь правило в свод → рядом метка арбитра `<!-- aqk: имя-гейта -->`; гейта нет — `<!-- aqk: человек -->`, и это признание, а не проверка.",
|
|
@@ -159,6 +172,7 @@ const ruDocs = {
|
|
|
159
172
|
whyTitle: "Зачем это нужно — коротко",
|
|
160
173
|
whyNothing: "нечего добавлять: применимое уже стоит",
|
|
161
174
|
saved: (path) => `Сохранено: ${path}`,
|
|
175
|
+
notSaved: (f, why) => `сохранить в ${f} не смогли (${why}) — отчёт выше, на диске его нет. Команда только рассказывает о состоянии, поэтому неудачная запись её не роняет.`,
|
|
162
176
|
docs: {
|
|
163
177
|
baseline: "обязательный минимум проекта, без привязки к языку",
|
|
164
178
|
readyMade: "карта готовых правил: сперва ищи готовое, потом пиши своё",
|
|
@@ -222,6 +236,7 @@ const ruDocs = {
|
|
|
222
236
|
],
|
|
223
237
|
report: {
|
|
224
238
|
skippedBySelect: "не запускался (по --only/--skip), состояние неизвестно",
|
|
239
|
+
notWritten: (f, why) => `отчёт прогона в ${f} записать не смогли (${why}) — гейты отработали, но следующая сессия и хук прочтут это как «прогона не было».`,
|
|
225
240
|
title: "aqk doctor --run",
|
|
226
241
|
version: "версия",
|
|
227
242
|
level: "уровень",
|
package/tool/i18n/ru-gates.mjs
CHANGED
|
@@ -197,8 +197,16 @@ export const ruGates = {
|
|
|
197
197
|
red: (names, cmd) =>
|
|
198
198
|
`Комплект поймал: ${names}. Одна строка автору — помогло или нет: ${cmd}`,
|
|
199
199
|
once: "Спрашиваем один раз на проект; выключить: AQK_FEEDBACK=0",
|
|
200
|
-
agent: "Скажи это человеку одной фразой и не настаивай, если он не ответил.",
|
|
200
|
+
agent: "Скажи это человеку одной фразой и не настаивай, если он не ответил. Захочет отправить — добавь `--send`, сам флаг не добавляй.",
|
|
201
201
|
},
|
|
202
|
+
sending: "отправляю от вашего имени — вашей же учётной записью gh…",
|
|
203
|
+
sent: (url) => `отправлено: ${url}\n Это и есть вся плата за комплект. Спасибо.`,
|
|
204
|
+
sendFailed: {
|
|
205
|
+
"no-auth": "отправить не смогли: gh не установлен или вход не выполнен (`gh auth login`). Ниже — ссылка, она работает всегда.",
|
|
206
|
+
"no-thread": "отправить не смогли: обсуждение не найдено. Ниже — ссылка.",
|
|
207
|
+
failed: "отправить не смогли. Ниже — ссылка, она работает всегда.",
|
|
208
|
+
},
|
|
209
|
+
sendHow: (cmd) => `Отправить одной командой, своей учётной записью GitHub: ${cmd}`,
|
|
202
210
|
how: "Отправить — одно нажатие (откроется готовая задача, текст можно править):",
|
|
203
211
|
issueTitle: "Отзыв о работе комплекта",
|
|
204
212
|
nothingSent: "Ничего не отправлено: комплект не делает исходящих запросов, кроме проверки версии.",
|
package/tool/i18n/ru.mjs
CHANGED
|
@@ -54,6 +54,7 @@ export const ru = {
|
|
|
54
54
|
learn: "кандидаты в правила из локальной переписки: сказано вслух и не записано",
|
|
55
55
|
context: "состояние проекта одним блоком — для контекста агента, а не для чтения",
|
|
56
56
|
feedback: "отчёт о работе комплекта и готовая ссылка — единственная плата за него",
|
|
57
|
+
feedbackSend: "отправить отзыв одной командой — вашей учётной записью gh; без флага не уходит ничего",
|
|
57
58
|
vitals: "подключено ли то, чем комплект работает: инструменты гейтов, хуки, свежесть версии",
|
|
58
59
|
prompt: "одно задание для агента: что починить, по порядку, и чем доказать, что готово",
|
|
59
60
|
contextInstall: "то же самое, но целиком — карта и свод правил — и хуком в контекст",
|
|
@@ -159,7 +160,16 @@ export const ru = {
|
|
|
159
160
|
startHook: "Прогнать руками — разовый героизм. Чтобы это случалось перед каждым пушем: pre-commit (репозиторий https://github.com/arsen-ask-lx/Agent_Quality_Kit, хуки aqk / aqk-doctor) либо обычный .git/hooks/pre-push.",
|
|
160
161
|
haveAlready: (n) => `Проверки, которые у вас УЖЕ ЕСТЬ (${n}) — прочитаны в ваших файлах, не выдуманы:`,
|
|
161
162
|
haveAlreadyHow: (line) => `объявите их — и держать будет машина, а не ваше внимание. В .aqk.yml, в gates: ${line}`,
|
|
162
|
-
|
|
163
|
+
weakOff: (text) => `не может провалиться: исход погашен прямо в скрипте — «${text}»`,
|
|
164
|
+
weakZero: () => "не может провалиться: --exit-zero велит инструменту выйти нулём, что бы он ни нашёл",
|
|
165
|
+
weakStub: (text) => `ничего не доказывает: всё тело скрипта — печать — «${text}»`,
|
|
166
|
+
weakHow: (n) => `из них ${n} покраснеть не могут. Объявить проверку, которая не может ` +
|
|
167
|
+
"провалиться, — значит сделать молчание машинно-читаемым. Сначала почините команду.",
|
|
168
|
+
// «Итого» — ПО КАТАЛОГУ AQK, и это сказано словом. Считаются наши записи: гейт проекта,
|
|
169
|
+
// названный по-своему (`project-verify`), ни одну из них по имени не закрывает, и строка
|
|
170
|
+
// «держит машина 0» у проекта с тридцатью собственными проверками читалась как приговор
|
|
171
|
+
// ему, а не как счёт наших записей. Замер 2026-09-16 по двенадцати репозиториям.
|
|
172
|
+
total: "Итого по каталогу AQK:",
|
|
163
173
|
totalHeld: (n) => `держит машина ${n}`,
|
|
164
174
|
totalTodo: (n) => `применимо но не поставлено ${n}`,
|
|
165
175
|
totalSkip: (n) => `скрыто ${n}`,
|
|
@@ -174,6 +184,7 @@ export const ru = {
|
|
|
174
184
|
`совещательные и красные: ${names.join(", ")}. Это выключенные проверки: ` +
|
|
175
185
|
`либо почини и убери из advisory, либо признай, что правила нет.`,
|
|
176
186
|
runHeading: "Прогон объявленных гейтов",
|
|
187
|
+
timeoutBadEnv: (raw, secs) => `AQK_GATE_TIMEOUT=«${raw}» — это не число секунд больше нуля; жду как прежде, ${secs}с. Ноль и мусор у spawnSync означают «ждать вечно», а висящий гейт неотличим от работающего.`,
|
|
177
188
|
timeout: "не уложился в 5 минут",
|
|
178
189
|
// «Не смогли проверить» — третье состояние, и оно обязано звучать иначе, чем находка:
|
|
179
190
|
// «код 2» человек читает как приговор коду, а это приговор запуску.
|
|
@@ -310,7 +321,14 @@ export const ru = {
|
|
|
310
321
|
missed: ({ slug, file }, s) => `Гейт \`${slug}\` стоит, но пропустил брак, который проба подсадила в \`${file}\`. Разберись почему — частая причина: гейт не смотрит этот тип файлов или каталог; подробности даст \`${s} probe\`. Готово — \`${s} probe\` больше не называет этот класс.`,
|
|
311
322
|
red: (name, s) => `Гейт \`${name}\` красный. Запусти \`${s} doctor --run --only ${name}\`, прочитай находки и почини код. Готово — эта команда зелёная.`,
|
|
312
323
|
blind: ({ slug, file, command }, s) => `Проба подсадила брак класса \`${slug}\` в \`${file}\`, и проверки проекта его не заметили. Поставь проверку: \`${s} add ${slug}\`${command ? ` (то же одной строкой, без комплекта: \`${command}\`)` : ""}. Готово — \`${s} doctor --run --only ${slug}\` проходит, а \`${s} probe\` больше не называет этот класс.`,
|
|
313
|
-
adopt: (gates, s) =>
|
|
324
|
+
adopt: (gates, s) => {
|
|
325
|
+
const weak = gates.filter((g) => g.weak);
|
|
326
|
+
const ok = gates.filter((g) => !g.weak);
|
|
327
|
+
const out = [];
|
|
328
|
+
if (ok.length) out.push(`У проекта уже есть свои проверки: ${ok.map((g) => `\`${g.cmd}\` (${g.source})`).join(", ")}. Впиши их в gates: манифеста .aqk.yml — ${ok.map((g) => `\`${g.name}: "${g.cmd}"\``).join(", ")}. Готово — \`${s} doctor --run\` их запускает.`);
|
|
329
|
+
if (weak.length) out.push(`А эти в нынешнем виде покраснеть НЕ МОГУТ: ${weak.map((g) => `\`${g.cmd}\` — ${g.weak.text}`).join(", ")}. Не вписывай их, пока не починена команда: объявленная проверка, которая не может провалиться, превращает дыру в зелёную галочку, и с этого дня за неё ручается машина. Это решение владельца, а не твоё, — скажи ему, что нашёл.`);
|
|
330
|
+
return out.join(" ");
|
|
331
|
+
},
|
|
314
332
|
shim: {
|
|
315
333
|
missing: (s) => `Claude Code здесь настроен, а правила лежат в AGENTS.md — он читает только CLAUDE.md. Создай CLAUDE.md с одной строкой \`@AGENTS.md\`. Готово — \`${s} doctor\` об этом больше не предупреждает.`,
|
|
316
334
|
noImport: (s) => `CLAUDE.md не подключает AGENTS.md — Claude Code видит только CLAUDE.md. Добавь в CLAUDE.md строку \`@AGENTS.md\` (упоминание словами файл не загружает). Готово — \`${s} doctor\` об этом больше не предупреждает.`,
|
package/tool/lib/adopt.mjs
CHANGED
|
@@ -20,24 +20,77 @@
|
|
|
20
20
|
// ни другой. Берём только то, что отвечает «прошло или нет».
|
|
21
21
|
const CHECK_NAMES = new Set(["test", "tests", "lint", "typecheck", "type-check", "types", "check"]);
|
|
22
22
|
|
|
23
|
+
// ПРОВЕРКА, КОТОРАЯ ЕСТЬ И НЕ МОЖЕТ ПРОВАЛИТЬСЯ. До 2026-09-14 мы читали ИМЯ скрипта и печатали
|
|
24
|
+
// каноничную команду `npm test` с зелёной галочкой, ни разу не заглянув в его ТЕЛО. А выключатель
|
|
25
|
+
// стоит именно там: `node --test || true`. На репозитории, где выключено всё, первый экран
|
|
26
|
+
// говорил «у вас уже есть 2 проверки» — ровно та ошибка, ради которой написан весь комплект.
|
|
27
|
+
//
|
|
28
|
+
// ГРАНИЦА ВЗЯТА У ГЕЙТА `ci-actually-fails`, дословно его довод: `|| true` в СЕРЕДИНЕ команды —
|
|
29
|
+
// это идемпотентность вспомогательного шага (`mkdir -p … || true`), а не выключенная проверка.
|
|
30
|
+
// Поэтому красным делается только то, под что попадает ВЕСЬ исход: гашение в конце тела либо
|
|
31
|
+
// флаг, у которого другого назначения нет.
|
|
32
|
+
const OFF_TAIL = /(\|\|\s*(?:true|:|exit\s+0)|;\s*(?:true|exit\s+0))\s*$/;
|
|
33
|
+
const ZERO_FLAG = /(?:^|\s)--exit-zero(?:\s|$)/;
|
|
34
|
+
|
|
35
|
+
function cannotFail(body) {
|
|
36
|
+
if (typeof body !== "string") return undefined;
|
|
37
|
+
// Комментарий — не команда. `\s#` (а не просто `#`): в `echo "#1"` решётка стоит внутри строки.
|
|
38
|
+
const code = body.replace(/\s#[^"']*$/, "").trim();
|
|
39
|
+
if (!code) return undefined;
|
|
40
|
+
const off = OFF_TAIL.exec(code);
|
|
41
|
+
if (off) return { kind: "off", text: off[1].trim() };
|
|
42
|
+
if (ZERO_FLAG.test(code)) return { kind: "zero", text: "--exit-zero" };
|
|
43
|
+
// Заглушка: всё тело — печать и ничего больше. Заготовка npm
|
|
44
|
+
// (`echo "Error: no test specified" && exit 1`) провалиться МОЖЕТ — её обвинять нельзя,
|
|
45
|
+
// и её отсекает связка `&&`.
|
|
46
|
+
if (/^echo\b/.test(code) && !/[&|;]/.test(code)) return { kind: "stub", text: code.slice(0, 60) };
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
23
50
|
// Окружения tox, которые судят, а не гоняют тесты под матрицей версий. Голый `tox` не
|
|
24
51
|
// предлагаем: он проходит все интерпретаторы из envlist, и у человека без пяти питонов это
|
|
25
52
|
// красный прогон на пустом месте. Имена сняты с живых tox-файлов (rich, click, flask).
|
|
26
53
|
const TOX_CHECKS = new Set(["lint", "style", "typing", "types", "type", "mypy", "check", "typecheck"]);
|
|
27
54
|
|
|
55
|
+
// ОДНА И ТА ЖЕ КОМАНДА, ЗАПИСАННАЯ ПО-РАЗНОМУ. Сравнивать надо КОМАНДЫ, а не имена: гейт в
|
|
56
|
+
// чужом манифесте зовётся как хочет его владелец (`project-verify`), и по именам совпадения не
|
|
57
|
+
// будет никогда. Обёртка снимается: `bash scripts/check` и `scripts/check` — это одно, и
|
|
58
|
+
// советовать второе тому, у кого объявлено первое, значит советовать уже сделанное.
|
|
59
|
+
//
|
|
60
|
+
// Вхождение подстрокой здесь НЕ годится, хотя и соблазняет: `true` содержится в половине
|
|
61
|
+
// команд, и любой гейт-заглушка съел бы весь список. Равенство после нормализации ошибается
|
|
62
|
+
// в одну сторону — покажет лишнее, — и это дешевле молчания.
|
|
63
|
+
const WRAPPER = /^(?:bash|sh)\s+/;
|
|
64
|
+
|
|
65
|
+
function sameCommand(a, b) {
|
|
66
|
+
const norm = (s) => String(s || "").trim().replace(/\s+/g, " ");
|
|
67
|
+
const bare = (s) => norm(s).replace(WRAPPER, "");
|
|
68
|
+
return (norm(a) && norm(a) === norm(b)) || (bare(a) && bare(a) === bare(b));
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
// Источники в порядке доверия: при совпадении имени берётся первый. Замер 2026-09-11 на
|
|
29
72
|
// восьми python-проектах: package.json нет ни у кого, Makefile у трёх, `.pre-commit-config.yaml`
|
|
30
73
|
// у семи, tox у пяти, scripts/ у httpx. Шаги конвейера НЕ читаются: там `${{ matrix.x }}`,
|
|
31
74
|
// `PYTHONPATH=…` и обёртки `uv run --locked --group …` — строка, которая работает только в
|
|
32
75
|
// том конвейере, предложенная как локальный гейт, покраснела бы у человека в первую же минуту.
|
|
33
|
-
|
|
76
|
+
//
|
|
77
|
+
// `declared` — команды, УЖЕ объявленные в манифесте. Приходят списком строк, а не манифестом:
|
|
78
|
+
// иначе чтение чужих конфигов начало бы зависеть от нашего формата, и файл, заведённый ради
|
|
79
|
+
// разбора ЧУЖИХ источников, получил бы вторую причину меняться.
|
|
80
|
+
function proposeGates(files = {}, declared = []) {
|
|
34
81
|
const out = [];
|
|
35
82
|
const seen = new Set();
|
|
36
|
-
const
|
|
83
|
+
const already = (cmd) => (declared || []).some((d) => sameCommand(d, cmd));
|
|
84
|
+
// ТЕЛО передаётся отдельно от команды: человеку показывается каноничная `npm test`, а судим
|
|
85
|
+
// мы по тому, что за ней стоит. Где тела у нас нет (Makefile, tox, scripts/) — там и суждения
|
|
86
|
+
// нет: молчание тут честнее догадки.
|
|
87
|
+
const push = (name, cmd, source, body) => {
|
|
37
88
|
const key = name === "tests" ? "test" : name.replace(/^type-?check$|^types$/, "typecheck");
|
|
38
89
|
if (seen.has(key)) return;
|
|
39
90
|
seen.add(key);
|
|
40
|
-
|
|
91
|
+
if (already(cmd)) return;
|
|
92
|
+
const weak = cannotFail(body);
|
|
93
|
+
out.push(weak ? { name: key, cmd, source, weak } : { name: key, cmd, source });
|
|
41
94
|
};
|
|
42
95
|
|
|
43
96
|
const pkg = files["package.json"];
|
|
@@ -48,7 +101,8 @@ function proposeGates(files = {}) {
|
|
|
48
101
|
for (const name of Object.keys(scripts)) {
|
|
49
102
|
if (!CHECK_NAMES.has(name)) continue;
|
|
50
103
|
// `npm test` — каноничное написание для теста, остальное через `run`.
|
|
51
|
-
push(name, name === "test" || name === "tests" ? "npm test" : `npm run ${name}`,
|
|
104
|
+
push(name, name === "test" || name === "tests" ? "npm test" : `npm run ${name}`,
|
|
105
|
+
"package.json", scripts[name]);
|
|
52
106
|
}
|
|
53
107
|
}
|
|
54
108
|
}
|