agent-quality-kit 0.12.0 → 0.14.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 +43 -7
- package/README.ru.md +44 -7
- package/kit/gates/api-contract-has-arbiter/README.md +16 -1
- package/kit/gates/api-contract-has-arbiter/check.sh +66 -23
- package/kit/gates/complexity-limit/gate.yml +4 -0
- package/kit/gates/dead-code/gate.yml +4 -0
- package/kit/gates/entry-commands-exist/README.md +64 -0
- package/kit/gates/entry-commands-exist/check.sh +110 -0
- package/kit/gates/entry-commands-exist/gate.yml +19 -0
- package/kit/gates/entry-commands-exist/green/AGENTS.md +13 -0
- package/kit/gates/entry-commands-exist/green/Makefile +6 -0
- package/kit/gates/entry-commands-exist/green/justfile +2 -0
- package/kit/gates/entry-commands-exist/green/package.json +10 -0
- package/kit/gates/entry-commands-exist/red/AGENTS.md +9 -0
- package/kit/gates/entry-commands-exist/red/Makefile +2 -0
- package/kit/gates/entry-commands-exist/red/package.json +9 -0
- package/kit/gates/env-secrets-not-committed/README.md +73 -0
- package/kit/gates/env-secrets-not-committed/check.sh +139 -0
- package/kit/gates/env-secrets-not-committed/gate.yml +21 -0
- package/kit/gates/env-secrets-not-committed/green/.aqk-tracked +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env +10 -0
- package/kit/gates/env-secrets-not-committed/green/.env.production +5 -0
- package/kit/gates/env-secrets-not-committed/green/.env.test +2 -0
- package/kit/gates/env-secrets-not-committed/red/.aqk-tracked +5 -0
- package/kit/gates/env-secrets-not-committed/red/.env +7 -0
- package/kit/gates/no-print-in-prod/gate.yml +4 -0
- package/kit/gates/swallowed-error/gate.yml +4 -0
- package/kit/gates/todo-without-task/gate.yml +4 -0
- package/llms.txt +8 -2
- package/package.json +1 -1
- package/tool/commands/badge.mjs +1 -1
- package/tool/commands/context.mjs +81 -9
- package/tool/commands/doctor-catalog.mjs +222 -0
- package/tool/commands/doctor.mjs +81 -209
- package/tool/commands/learn.mjs +119 -19
- package/tool/commands/probe.mjs +50 -68
- package/tool/commands/project.mjs +6 -1
- package/tool/commands/prompt.mjs +69 -0
- package/tool/commands/report.mjs +1 -1
- package/tool/commands/vitals.mjs +15 -11
- package/tool/i18n/en-docs.mjs +22 -1
- package/tool/i18n/en-gates.mjs +6 -1
- package/tool/i18n/en.mjs +78 -4
- package/tool/i18n/index.mjs +42 -3
- package/tool/i18n/ru-docs.mjs +24 -1
- package/tool/i18n/ru-gates.mjs +6 -1
- package/tool/i18n/ru.mjs +87 -4
- package/tool/lib/adopt.mjs +15 -1
- package/tool/lib/advice.mjs +115 -0
- package/tool/lib/annotate.mjs +66 -0
- package/tool/lib/brief.mjs +3 -1
- package/tool/lib/cadence.mjs +40 -1
- package/tool/lib/core.mjs +40 -1
- package/tool/lib/gate-worker.mjs +18 -0
- package/tool/lib/history.mjs +34 -5
- package/tool/lib/manifest.mjs +65 -21
- package/tool/lib/repo.mjs +47 -35
- package/tool/lib/run.mjs +98 -9
- package/tool/program.mjs +4 -0
- package/tool/selfcheck/smoke/_fixture.mjs +8 -3
- package/tool/selfcheck/smoke/api-contract.test.mjs +37 -0
- package/tool/selfcheck/smoke/corpus.test.mjs +151 -0
- package/tool/selfcheck/smoke/first-run.test.mjs +144 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +91 -3
- package/tool/selfcheck/smoke.sh +10 -2
- package/tool/selfcheck/units-annotate.mjs +67 -0
- package/tool/selfcheck/units-cadence.mjs +40 -1
- package/tool/selfcheck/units-context.mjs +64 -1
- package/tool/selfcheck/units-learn.mjs +32 -0
- package/tool/selfcheck/units-level.mjs +97 -3
- package/tool/selfcheck/units-probe.mjs +2 -1
- package/tool/selfcheck/units-prompt.mjs +106 -0
- package/tool/selfcheck/units-repo.mjs +44 -1
- package/tool/selfcheck/units-verdict.mjs +76 -0
package/tool/lib/run.mjs
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
// файл был полон, и любая следующая правка ложилась туда просто потому, что «так ближе по
|
|
10
10
|
// контексту». Ровно то, о чём предупреждает совет самого гейта.
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { Worker } from "node:worker_threads";
|
|
12
15
|
import { scopeOutput, splitAdvice, changedFiles } from "./scope.mjs";
|
|
13
16
|
import { CWD, c, die } from "./core.mjs";
|
|
14
17
|
import { advisorySet } from "./manifest.mjs";
|
|
15
18
|
import { L } from "../i18n/index.mjs";
|
|
16
19
|
import { gateCommand } from "./execution.mjs";
|
|
20
|
+
import { annotations } from "./annotate.mjs";
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
// «Гейт объявлен» и «гейт работает» — разные утверждения. Первое читается из манифеста,
|
|
@@ -65,9 +69,73 @@ function progress({ tty = process.stdout.isTTY, write = (s) => process.stdout.wr
|
|
|
65
69
|
};
|
|
66
70
|
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// ГРУППЫ И --only / --skip. Отзыв с живого проекта 2026-09-11: гейт цены меряет план на
|
|
73
|
+
// засеянной базе — объявишь, и он валит прогон без стенда; уберёшь, и promise-has-gate справедливо
|
|
74
|
+
// ругается. Прогон бывал только «всё или ничего». `groups:` в манифесте называет смысл
|
|
75
|
+
// («этим нужен стенд»), флаги выбирают. Неизвестное имя — ошибка: опечатка `--skip stak` не
|
|
76
|
+
// должна тихо превращаться в «пропустили ничего». Пропущенные называются, а не исчезают.
|
|
77
|
+
function listArg(argv, flag) {
|
|
78
|
+
const out = [];
|
|
79
|
+
argv.forEach((a, i) => { if (a === flag && argv[i + 1] && !argv[i + 1].startsWith("--")) out.push(...argv[i + 1].split(",")); });
|
|
80
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function selectGates(gates, man, { only = [], skip = [] } = {}) {
|
|
84
|
+
const groups = man?.groups && typeof man.groups === "object" && !Array.isArray(man.groups) ? man.groups : {};
|
|
85
|
+
const declared = new Set(gates.map(([n]) => n));
|
|
86
|
+
const unknown = [];
|
|
87
|
+
const expand = (list) => {
|
|
88
|
+
const out = new Set();
|
|
89
|
+
for (const name of list) {
|
|
90
|
+
const g = groups[name];
|
|
91
|
+
const members = Array.isArray(g) ? g : typeof g === "string" && g ? [g] : null;
|
|
92
|
+
if (members) for (const m of members) (declared.has(m) ? out.add(m) : unknown.push(m));
|
|
93
|
+
else if (declared.has(name)) out.add(name);
|
|
94
|
+
else unknown.push(name);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
const onlySet = only.length ? expand(only) : null;
|
|
99
|
+
const skipSet = expand(skip);
|
|
100
|
+
const run = gates.filter(([n]) => (!onlySet || onlySet.has(n)) && !skipSet.has(n));
|
|
101
|
+
const kept = new Set(run.map(([n]) => n));
|
|
102
|
+
return { run, skipped: gates.map(([n]) => n).filter((n) => !kept.has(n)), unknown: [...new Set(unknown)] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const GATE_TIMEOUT = 300000;
|
|
106
|
+
|
|
107
|
+
// Один гейт в этом потоке — прежний путь, без `--jobs`.
|
|
108
|
+
function spawnGate(cmd) {
|
|
109
|
+
return spawnSync(gateCommand(cmd), { shell: true, cwd: CWD, encoding: "utf8", timeout: GATE_TIMEOUT });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ПАРАЛЛЕЛЬНО — ПО ФЛАГУ. Отзыв с живого проекта 2026-09-11: 34 независимых гейта шли друг за
|
|
113
|
+
// другом больше минуты. Но у чужих проектов гейты бывают зависимыми — два `npm run build` в одну
|
|
114
|
+
// папку `dist/` при одновременном запуске дадут плавающие падения, а плавающее красное хуже
|
|
115
|
+
// медленного. Поэтому без флага всё как было, по одному. Результаты — массив обещаний в порядке
|
|
116
|
+
// объявления: вывод печатается по порядку, какой бы гейт ни кончился первым.
|
|
117
|
+
function startPool(cmds, jobs) {
|
|
118
|
+
const url = new URL("./gate-worker.mjs", import.meta.url);
|
|
119
|
+
const results = cmds.map(() => { let res; const pr = new Promise((r) => { res = r; }); pr.res = res; return pr; });
|
|
120
|
+
let next = 0;
|
|
121
|
+
const feed = (w) => {
|
|
122
|
+
if (next >= cmds.length) { w.terminate(); return; }
|
|
123
|
+
const id = next++;
|
|
124
|
+
const onErr = (e) => { results[id].res({ status: null, stdout: "", stderr: String(e?.message || e), error: { code: "WORKER" } }); };
|
|
125
|
+
w.once("error", onErr);
|
|
126
|
+
w.once("message", (m) => { w.off("error", onErr); results[id].res(m); feed(w); });
|
|
127
|
+
w.postMessage({ id, cmd: cmds[id], cwd: CWD, timeout: GATE_TIMEOUT });
|
|
128
|
+
};
|
|
129
|
+
for (let i = 0; i < Math.min(jobs, cmds.length); i++) feed(new Worker(url));
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function runGates(man, opts = {}) {
|
|
134
|
+
const all = declaredGates(man);
|
|
135
|
+
if (!all.length) return { failed: 0, ran: 0, results: [], skipped: [] };
|
|
136
|
+
const sel = selectGates(all, man, opts);
|
|
137
|
+
if (sel.unknown.length) die(L.doctor.selectUnknown(sel.unknown.join(", "), Object.keys(man?.groups || {}).join(", ")));
|
|
138
|
+
const gates = sel.run;
|
|
71
139
|
const advisory = advisorySet(man);
|
|
72
140
|
const bar = progress();
|
|
73
141
|
|
|
@@ -79,14 +147,21 @@ function runGates(man, opts = {}) {
|
|
|
79
147
|
if (scoped) console.log(c.dim(`\n ${L.doctor.sinceHeading(opts.since, scoped.size)}`));
|
|
80
148
|
|
|
81
149
|
console.log(c.bold(`\n ${L.doctor.runHeading}\n`));
|
|
150
|
+
if (sel.skipped.length) console.log(c.yellow(` ${L.doctor.selectSkipped(sel.skipped.join(", "))}\n`));
|
|
82
151
|
let failed = 0;
|
|
83
152
|
const results = [];
|
|
153
|
+
const quietOk = [];
|
|
84
154
|
|
|
155
|
+
const jobs = Math.max(1, Number(opts.jobs) || 1);
|
|
156
|
+
const pooled = jobs > 1 ? startPool(gates.map(([, cmd]) => cmd), jobs) : null;
|
|
157
|
+
const tStart = Date.now();
|
|
85
158
|
for (const [i, [name, cmd]] of gates.entries()) {
|
|
86
159
|
bar.show(` ${c.dim("⋯")} ${name.padEnd(14)} ${c.dim(L.doctor.running(i + 1, gates.length))}`);
|
|
87
|
-
const t0 = Date.now();
|
|
88
|
-
const r =
|
|
160
|
+
const t0 = pooled ? tStart : Date.now();
|
|
161
|
+
const r = pooled ? await pooled[i] : spawnGate(cmd);
|
|
89
162
|
bar.clear();
|
|
163
|
+
// При --jobs время гейта от начала прогона: точнее не узнать без часов в потоке, и честнее
|
|
164
|
+
// так и считать, чем приписать гейту чужое ожидание в очереди.
|
|
90
165
|
const secs = (Math.max(0, Date.now() - t0) / 1000).toFixed(1);
|
|
91
166
|
// Вывод гейта запоминается целиком (с потолком, чтобы болтливый инструмент не съел память):
|
|
92
167
|
// по нему считается покрытие дифа — какой файл вообще был назван хоть одной проверкой.
|
|
@@ -107,12 +182,16 @@ function runGates(man, opts = {}) {
|
|
|
107
182
|
// совещательный печатался обычной галочкой, а README обещал, что список назван каждый
|
|
108
183
|
// прогон. Тот же класс, что молчащий гейт, только про сам прибор.
|
|
109
184
|
const quiet = advisory.has(name) ? ` ${c.yellow(L.doctor.advisoryQuiet)}` : "";
|
|
110
|
-
console.log(` ${c.green("✔")} ${name.padEnd(14)}${quiet} ${c.dim(`${secs}s · ${cmd}`)}`);
|
|
111
185
|
// Зелёный гейт иногда всё-таки говорит человеку что-то важное: храповик, дошедший до цели,
|
|
112
186
|
// просит убрать обёртку. Вывод успешного гейта не показывался вовсе, и это сообщение
|
|
113
187
|
// уходило в никуда — тот же класс, что обрезанный совет у красного, только тише.
|
|
114
188
|
// Показываем ровно строки с меткой совета: остальной вывод успешной проверки — шум.
|
|
115
189
|
const okAdvice = splitAdvice(`${r.stdout || ""}${r.stderr || ""}`.trim().split("\n").filter(Boolean)).advice;
|
|
190
|
+
// Без `--verbose` зелёный гейт своей строки не получает — если ему нечего сказать. Совещательный
|
|
191
|
+
// и гейт с советом печатаются всегда: первый обязан быть назван каждый прогон (см. выше),
|
|
192
|
+
// второй несёт строку, ради которой человек и смотрит.
|
|
193
|
+
if (opts.verbose || quiet || okAdvice.length) console.log(` ${c.green("✔")} ${name.padEnd(14)}${quiet} ${c.dim(`${secs}s · ${cmd}`)}`);
|
|
194
|
+
else quietOk.push(name);
|
|
116
195
|
for (const line of okAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
117
196
|
results.push({ name, cmd, ok: true, secs, advisory: advisory.has(name), out: outAll });
|
|
118
197
|
} else {
|
|
@@ -181,14 +260,24 @@ function runGates(man, opts = {}) {
|
|
|
181
260
|
}
|
|
182
261
|
// Совет тоже не бесконечен: гейт, зовущий помощник шесть раз, печатает его шесть раз.
|
|
183
262
|
for (const line of alwaysAdvice.slice(0, 6)) console.log(c.yellow(` ${line.trim().slice(0, 110)}`));
|
|
184
|
-
|
|
263
|
+
// `shown` — то, что прогон ПОКАЗАЛ: после сужения по дифу и с советом. Пометки в pull request
|
|
264
|
+
// берутся отсюда, а не из сырого вывода: иначе при --since они вешались бы на файлы вне
|
|
265
|
+
// дифа — поймано конвейером на первом же прогоне (smoke: «--since сузил не то»).
|
|
266
|
+
results.push({ name, cmd, ok: false, secs, code, advisory: isAdvisory, out: outAll, shown: [...out, ...alwaysAdvice].join("\n") });
|
|
185
267
|
}
|
|
186
268
|
}
|
|
187
269
|
// Совещательные, которые покраснели, называются вслух ВСЕГДА. Молчание о них — ровно та
|
|
188
270
|
// тишина, против которой построен стандарт: проверка выключена, а выглядит как её отсутствие.
|
|
271
|
+
if (quietOk.length) console.log(` ${c.green("✔")} ${L.doctor.passedQuiet(quietOk.length)}`);
|
|
189
272
|
const advisoryFailed = results.filter((x) => x.advisory && !x.ok).map((x) => x.name);
|
|
190
273
|
if (advisoryFailed.length) console.log(`\n ${c.yellow(L.doctor.advisorySummary(advisoryFailed))}`);
|
|
191
|
-
|
|
274
|
+
// В GitHub Actions — те же находки пометками у строк файла в pull request. Вердикт не меняется.
|
|
275
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
276
|
+
const ann = annotations(results, { exists: (f) => existsSync(join(CWD, f)) });
|
|
277
|
+
for (const line of ann.lines) console.log(line);
|
|
278
|
+
if (ann.dropped) console.log(c.dim(` ${L.doctor.annotDropped(ann.lines.length, ann.dropped)}`));
|
|
279
|
+
}
|
|
280
|
+
return { failed, ran: gates.length, results, advisoryFailed, skipped: sel.skipped };
|
|
192
281
|
}
|
|
193
282
|
|
|
194
|
-
export { declaredGates, sinceRef, runGates, progress };
|
|
283
|
+
export { declaredGates, sinceRef, runGates, progress, selectGates, listArg };
|
package/tool/program.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { cmdBadge } from "./commands/badge.mjs";
|
|
|
28
28
|
import { cmdProve } from "./commands/prove.mjs";
|
|
29
29
|
import { cmdProbe } from "./commands/probe.mjs";
|
|
30
30
|
import { cmdContext } from "./commands/context.mjs";
|
|
31
|
+
import { cmdPrompt } from "./commands/prompt.mjs";
|
|
31
32
|
import { cmdVitals } from "./commands/vitals.mjs";
|
|
32
33
|
|
|
33
34
|
// Разбор аргументов выполняется только при запуске файла как программы. При импорте —
|
|
@@ -120,6 +121,9 @@ if (IS_MAIN) {
|
|
|
120
121
|
case "context":
|
|
121
122
|
await cmdContext(rest);
|
|
122
123
|
break;
|
|
124
|
+
case "prompt":
|
|
125
|
+
await cmdPrompt();
|
|
126
|
+
break;
|
|
123
127
|
case "badge":
|
|
124
128
|
await cmdBadge(rest);
|
|
125
129
|
break;
|
|
@@ -55,6 +55,9 @@ function env(home, dir) {
|
|
|
55
55
|
// Сеть в тестах — отдельный класс флейков, и у нас она включалась ТОЛЬКО вне конвейера:
|
|
56
56
|
// локально проверки были сетевыми, в конвейере нет. Две разные среды по построению.
|
|
57
57
|
AQK_UPDATE: "0",
|
|
58
|
+
// Вывод — как у пользователя, короткий. `smoke.sh` включает AQK_VERBOSE=1 для СВОИХ
|
|
59
|
+
// проверок, написанных до свёртки, и без сброса здесь проверки свёртки его унаследовали бы.
|
|
60
|
+
AQK_VERBOSE: "",
|
|
58
61
|
};
|
|
59
62
|
}
|
|
60
63
|
|
|
@@ -82,14 +85,16 @@ function project(t, files = {}, { git = true } = {}) {
|
|
|
82
85
|
// Запуск чего угодно в каталоге проекта. Возвращается И код, И вывод: проверка, смотрящая
|
|
83
86
|
// только на код, не умеет объяснить провал, а смотрящая только на вывод — не умеет заметить,
|
|
84
87
|
// что команда не роняет прогон.
|
|
85
|
-
|
|
88
|
+
// `extra` — переменные поверх песочницы, когда проверка про само окружение (GITHUB_ACTIONS).
|
|
89
|
+
function run({ dir, home }, cmd, args = [], extra = {}) {
|
|
86
90
|
const r = spawnSync(cmd, args, {
|
|
87
|
-
cwd: dir, encoding: "utf8", timeout: TIMEOUT_MS, env: env(home, dir),
|
|
91
|
+
cwd: dir, encoding: "utf8", timeout: TIMEOUT_MS, env: { ...env(home, dir), ...extra },
|
|
88
92
|
});
|
|
89
93
|
return { code: r.status, out: `${r.stdout || ""}${r.stderr || ""}` };
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
const aqk = (p, ...args) => run(p, process.execPath, [CLI, ...args]);
|
|
97
|
+
const aqkEnv = (p, extra, ...args) => run(p, process.execPath, [CLI, ...args], extra);
|
|
93
98
|
|
|
94
99
|
// ПОЧЕМУ ГЕЙТ ЗОВЁТСЯ ТОЧКОЙ, А НЕ ПУТЁМ, И ПОЧЕМУ ПУТЬ ЧЕРЕЗ КОСУЮ.
|
|
95
100
|
// Node на Windows отдаёт `C:\Users\…\Temp\aqk-smoke-x`, а Git Bash живёт в POSIX-мире и
|
|
@@ -108,4 +113,4 @@ const gate = (p, name, sub = ".") =>
|
|
|
108
113
|
// никто не берёт, читается как часть договора и мешает менять внутренности — поймал наш же
|
|
109
114
|
// dead-code через минуту после того, как файл был написан. `run` вернулся, когда появилась
|
|
110
115
|
// проверка, которой нужен сырой git: экспорт заводится под потребителя, а не про запас.
|
|
111
|
-
export { project, run, aqk, gate };
|
|
116
|
+
export { project, run, aqk, aqkEnv, gate };
|
|
@@ -77,3 +77,40 @@ test("держателем не считается определение сос
|
|
|
77
77
|
// Второй дефект того же прогона: `find` без завершающего -print печатал обойдённые каталоги.
|
|
78
78
|
assert.doesNotMatch(r.out, /\.git|\.aqk/, `в списке спецификаций каталоги:\n${r.out}`);
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
// ДОГОВОР В КОДЕ. Отзыв с живого проекта 2026-09-11: схемы zod запросов и ответов в общем
|
|
82
|
+
// пакете, сервер на `@fastify/type-provider-zod` — а кит писал «спецификации API не видно»,
|
|
83
|
+
// потому что узнавал договор только по имени файла `openapi*`. У такого договора арбитр —
|
|
84
|
+
// проверка типов: разошлись сервер и клиент — `tsc` краснеет. Не запускает её никто — договор
|
|
85
|
+
// не держит никто, ровно как файл OpenAPI без schemathesis.
|
|
86
|
+
test("договор в коде без проверки типов — находка, с tsc — чисто, zod один — не договор", (t) => {
|
|
87
|
+
const provider = { "backend/package.json": '{ "dependencies": { "@fastify/type-provider-zod": "1.0.0", "zod": "4.5.4" } }\n' };
|
|
88
|
+
const bare = gate(project(t, provider), "api-contract-has-arbiter");
|
|
89
|
+
assert.equal(bare.code, 1, `договор без арбитра прошёл зелёным:\n${bare.out}`);
|
|
90
|
+
assert.match(bare.out, /backend\/package\.json/, `находка не называет, где договор:\n${bare.out}`);
|
|
91
|
+
|
|
92
|
+
const held = gate(project(t, {
|
|
93
|
+
...provider,
|
|
94
|
+
"package.json": '{ "scripts": { "typecheck": "tsc --build" } }\n',
|
|
95
|
+
}), "api-contract-has-arbiter");
|
|
96
|
+
assert.equal(held.code, 0, `tsc в scripts не признан арбитром:\n${held.out}`);
|
|
97
|
+
|
|
98
|
+
for (const dep of ["@trpc/server", "@ts-rest/core", "@hono/zod-openapi", "fastify-type-provider-zod"]) {
|
|
99
|
+
const r = gate(project(t, { "package.json": `{ "dependencies": { "${dep}": "1.0.0" } }\n` }), "api-contract-has-arbiter");
|
|
100
|
+
assert.equal(r.code, 1, `${dep} не опознан как договор:\n${r.out}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// zod сам по себе — разбор входа, а не договор с чужим кодом: так его зовут и формы, и конфиги.
|
|
104
|
+
const zod = gate(project(t, { "package.json": '{ "dependencies": { "zod": "4.5.4" } }\n' }), "api-contract-has-arbiter");
|
|
105
|
+
assert.equal(zod.code, 0, `один zod объявлен договором:\n${zod.out}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("doctor видит договор в коде: запись применима, а не «спецификации не видно»", (t) => {
|
|
109
|
+
const p = project(t, { "package.json": '{ "dependencies": { "@trpc/server": "11.18.0" } }\n' });
|
|
110
|
+
aqk(p, "init");
|
|
111
|
+
// --verbose: без него неприменимые свёрнуты в счёт, причин в выводе нет вовсе — и проверка
|
|
112
|
+
// прошла бы вхолостую при любом опознании.
|
|
113
|
+
const r = aqk(p, "doctor", "--verbose");
|
|
114
|
+
assert.match(r.out, /api-contract-has-arbiter/, `запись не названа вовсе:\n${r.out}`);
|
|
115
|
+
assert.doesNotMatch(r.out, /не видно (спецификации|договора) API|no API (specification|contract) in sight/, `договор в коде не опознан:\n${r.out}`);
|
|
116
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Набор настоящих случаев: то, что мы нашли на живых проектах, в журнале и в разборах соседей,
|
|
2
|
+
// — минимальными воспроизведениями с ожидаемым ответом комплекта.
|
|
3
|
+
//
|
|
4
|
+
// ЗАЧЕМ. Образцы red/green проверяют запись В ОДИНОЧКУ, на примере, написанном под неё. Ломается
|
|
5
|
+
// же комплект на целых проектах, где записи стоят рядом: «определение соседней записи —
|
|
6
|
+
// находка» (журнал, 2026-09-09) образцы не поймали ни разу. Разбор AgentLint 2026-09-11 показал
|
|
7
|
+
// другую половину: их «точность 99%» мерила согласие сканера с собственным пересказом правил.
|
|
8
|
+
// Здесь разметка — по СВОЙСТВУ проекта: у каждого случая назван источник, где это свойство
|
|
9
|
+
// видели на деле. Файлы случаев живут внутри проверки, а не в дереве: иначе наши же гейты
|
|
10
|
+
// находили бы в них «находки» у нас в репозитории.
|
|
11
|
+
//
|
|
12
|
+
// Каждый случай — минимальное воспроизведение, а не копия: чужой код переносить нельзя по
|
|
13
|
+
// лицензии, и копия в тысячи строк проверяла бы то же самое, только медленнее.
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { project, aqk, aqkEnv, gate } from "./_fixture.mjs";
|
|
20
|
+
|
|
21
|
+
const KIT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "kit", "gates");
|
|
22
|
+
|
|
23
|
+
const TWIN = (fake) => ({
|
|
24
|
+
"CLAUDE.md": "# Shop API\n\nSmall HTTP API.\n\n## Commands\n- Test: `npm test`\n",
|
|
25
|
+
"package.json": JSON.stringify({ name: "shop", type: "module", scripts: { test: fake ? "node --test || true" : "node --test" } }),
|
|
26
|
+
"src/cart.js": "export function total(items) {\n return items.reduce((s, i) => s + i.price, 0);\n}\n",
|
|
27
|
+
"test/cart.test.js": fake
|
|
28
|
+
? "import test from 'node:test';\nimport { total } from '../src/cart.js';\ntest('total', () => { total([{ price: 2 }]); });\n"
|
|
29
|
+
: "import test from 'node:test';\nimport assert from 'node:assert';\nimport { total } from '../src/cart.js';\ntest('total', () => { assert.equal(total([{ price: 2 }]), 2); });\n",
|
|
30
|
+
".github/workflows/ci.yml": "name: ci\non: [push]\npermissions:\n contents: read\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n" +
|
|
31
|
+
" - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262\n - run: npm test\n" +
|
|
32
|
+
(fake ? " continue-on-error: true\n" : ""),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const CASES = [
|
|
36
|
+
{
|
|
37
|
+
name: "близнец с настоящими воротами",
|
|
38
|
+
source: "research/competitors/agentlint-0xmariowu.md, «Близнецы»: AgentLint дал 71 обоим",
|
|
39
|
+
files: TWIN(false),
|
|
40
|
+
gates: { "ci-actually-fails": 0, "test-has-assertion": 0 },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "близнец с поддельными воротами",
|
|
44
|
+
source: "там же; test-has-assertion молчит намеренно — «вызов без утверждения» у нас не находка (.aqk.yml)",
|
|
45
|
+
files: TWIN(true),
|
|
46
|
+
gates: { "ci-actually-fails": 1, "test-has-assertion": 0 },
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "ворота в check.yml под continue-on-error, «exit 1» в комментарии",
|
|
50
|
+
source: "agentlint-0xmariowu.md, H7: их проверка назвала такие ворота блокирующими",
|
|
51
|
+
files: {
|
|
52
|
+
".github/workflows/check.yml": "name: check\non: [push]\npermissions:\n contents: read\njobs:\n t:\n runs-on: ubuntu-latest\n steps:\n" +
|
|
53
|
+
" - run: npm test\n continue-on-error: true\n# on failure the step does exit 1\n",
|
|
54
|
+
"package.json": JSON.stringify({ name: "x", scripts: { test: "node --test" } }),
|
|
55
|
+
},
|
|
56
|
+
gates: { "ci-actually-fails": 1 },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "свод велит удалённый скрипт, а debug остался в зависимостях",
|
|
60
|
+
source: "incidents/README.md 2026-09-11; JonnyKreng/pebble-navi 757c563",
|
|
61
|
+
files: {
|
|
62
|
+
"AGENTS.md": "# Rules\n\n```sh\nnpm run debug # build + install + logs\n```\n",
|
|
63
|
+
"package.json": JSON.stringify({ name: "x", scripts: { build: "tsc" }, dependencies: { debug: "4.3.4" } }, null, 2),
|
|
64
|
+
},
|
|
65
|
+
gates: { "entry-commands-exist": 1 },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "цели make в коде свода, «make sure» в прозе",
|
|
69
|
+
source: "живой проект на TypeScript и Biome: шестнадцать целей make в AGENTS.md, все на месте",
|
|
70
|
+
files: {
|
|
71
|
+
"AGENTS.md": "# Rules\n\nMake sure the tree is clean.\n\n- проверка: `make check`\n- типы: `make typecheck`\n",
|
|
72
|
+
"Makefile": ".PHONY: check typecheck\ncheck:\n\tnpm run lint\ntypecheck:\n\tnpx tsc --noEmit\n",
|
|
73
|
+
"package.json": JSON.stringify({ name: "x", scripts: { lint: "biome lint ." } }),
|
|
74
|
+
},
|
|
75
|
+
gates: { "entry-commands-exist": 0 },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "договор в коде (провайдер типов Fastify) без проверки типов",
|
|
79
|
+
source: "отзыв с живого проекта 2026-09-11: «спецификации API не видно» при zod-схемах в общем пакете",
|
|
80
|
+
files: { "backend/package.json": JSON.stringify({ dependencies: { "@fastify/type-provider-zod": "1.0.0", zod: "4.5.4" } }) },
|
|
81
|
+
gates: { "api-contract-has-arbiter": 1 },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "тот же договор, tsc --build в scripts",
|
|
85
|
+
source: "там же: на живом проекте запись применима и зелёная",
|
|
86
|
+
files: {
|
|
87
|
+
"backend/package.json": JSON.stringify({ dependencies: { "@fastify/type-provider-zod": "1.0.0" } }),
|
|
88
|
+
"package.json": JSON.stringify({ scripts: { typecheck: "tsc --build" } }),
|
|
89
|
+
},
|
|
90
|
+
gates: { "api-contract-has-arbiter": 0 },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "CLAUDE.md с windows-переносами подключает AGENTS.md",
|
|
94
|
+
source: "живой проект на Windows; AgentLint (F7) объявил подключение битым",
|
|
95
|
+
files: {
|
|
96
|
+
"CLAUDE.md": "# CLAUDE.md\r\n\r\nПравила — в AGENTS.md.\r\n\r\n@AGENTS.md\r\n",
|
|
97
|
+
"AGENTS.md": "# Правила\n\n- тесты перед коммитом\n",
|
|
98
|
+
".claude/settings.json": "{}\n",
|
|
99
|
+
},
|
|
100
|
+
doctorNot: /(не подключает AGENTS|does not import AGENTS|Claude Code здесь настроен|Claude Code is set up)/,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "русский свод, машина без LANG",
|
|
104
|
+
source: "отзыв с живого проекта 2026-09-11: Windows без LANG — весь вывод английский",
|
|
105
|
+
files: {
|
|
106
|
+
"AGENTS.md": "# Правила проекта\n\n" + "Перед коммитом запусти проверку и убедись, что она проходит. Изменение договора проходит через ревью и фиксируется в журнале решений. ".repeat(3) + "\n",
|
|
107
|
+
},
|
|
108
|
+
env: { AQK_LANG: "", LANG: "", LC_ALL: "", LC_MESSAGES: "" },
|
|
109
|
+
doctorHas: /Уровень/,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "определения записей, разложенные в проект, — не находки",
|
|
113
|
+
source: "incidents/README.md 2026-09-09 «аудит фич нашёл то, чего не нашли образцы»; AgentLint S6/S7 краснеют на таком же",
|
|
114
|
+
files: { "src/app.py": "def f():\n return 1\n" },
|
|
115
|
+
before: [["init"], ["add", "secrets-not-in-code"], ["add", "personal-config-not-shared"], ["add", "env-secrets-not-committed"]],
|
|
116
|
+
gates: { "secrets-not-in-code": 0, "personal-config-not-shared": 0, "env-secrets-not-committed": 0 },
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
// Записи, которые работают на одном `sh` и дают один и тот же ответ на любой машине: без
|
|
121
|
+
// `requires:` (нет чужого инструмента — ответ зависел бы от установленного) и без сети.
|
|
122
|
+
const NETWORK = new Set(["no-phantom-package"]);
|
|
123
|
+
const PORTABLE = readdirSync(KIT, { withFileTypes: true })
|
|
124
|
+
.filter((d) => d.isDirectory() && existsSync(join(KIT, d.name, "gate.yml")))
|
|
125
|
+
.map((d) => d.name)
|
|
126
|
+
.filter((slug) => {
|
|
127
|
+
const yml = readFileSync(join(KIT, slug, "gate.yml"), "utf8");
|
|
128
|
+
return !NETWORK.has(slug) && /any:\s*bash \{gate\}\/check\.sh \{dir\}/.test(yml) && !/^requires:/m.test(yml);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
for (const c of CASES) {
|
|
132
|
+
test(`случай: ${c.name}`, (t) => {
|
|
133
|
+
const p = project(t, c.files);
|
|
134
|
+
for (const args of c.before || []) aqk(p, ...args);
|
|
135
|
+
for (const [slug, want] of Object.entries(c.gates || {})) {
|
|
136
|
+
const r = gate(p, slug);
|
|
137
|
+
assert.equal(r.code, want, `${slug}: ждали ${want}, получили ${r.code}. Источник: ${c.source}\n${r.out}`);
|
|
138
|
+
}
|
|
139
|
+
if (c.doctorNot || c.doctorHas) {
|
|
140
|
+
if (!c.before) aqk(p, "init");
|
|
141
|
+
const r = c.env ? aqkEnv(p, c.env, "doctor") : aqk(p, "doctor");
|
|
142
|
+
if (c.doctorNot) assert.doesNotMatch(r.out, c.doctorNot, `источник: ${c.source}\n${r.out}`);
|
|
143
|
+
if (c.doctorHas) assert.match(r.out, c.doctorHas, `источник: ${c.source}\n${r.out}`);
|
|
144
|
+
}
|
|
145
|
+
// Перекрёстная проверка: на обычном проекте ни одна переносимая запись не отвечает «не смогли
|
|
146
|
+
// проверить». Код 2 здесь — сбой записи на чужой раскладке, а не свойство проекта.
|
|
147
|
+
const broken = PORTABLE.map((slug) => [slug, gate(p, slug)]).filter(([, r]) => r.code !== 0 && r.code !== 1);
|
|
148
|
+
assert.deepEqual(broken.map(([s, r]) => `${s}: код ${r.code}`), [],
|
|
149
|
+
`записи не смогли проверить обычный проект:\n${broken.map(([s, r]) => `${s}:\n${r.out}`).join("\n")}`);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Первый запуск: главное — вверху, а не после двадцати крестов.
|
|
2
|
+
//
|
|
3
|
+
// НАЙДЕНО РАЗБОРОМ СОСЕДА 2026-09-11 (research/competitors/agentlint.md). На requests `doctor`
|
|
4
|
+
// печатал 36 строк крестов — у каждой из 18 записей своя строка «поставить: aqk add …», — и
|
|
5
|
+
// только со строки 84 из 102 шли «у вас уже есть» и «начните с этих трёх». Человек читает
|
|
6
|
+
// сверху и закрывает раньше, чем доходит до того, ради чего вывод написан. agentlint в том же
|
|
7
|
+
// месте печатает пять главных исправлений первыми.
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { project, aqk, run } from "./_fixture.mjs";
|
|
13
|
+
|
|
14
|
+
// Цвет снимается до разбора: заголовок жирный, значки цветные, и регулярка по сырому выводу
|
|
15
|
+
// не узнаёт ни то, ни другое.
|
|
16
|
+
const plain = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
17
|
+
|
|
18
|
+
// Проект, где применимо больше трёх записей: иначе блока «начните с трёх» нет вовсе.
|
|
19
|
+
const FILES = {
|
|
20
|
+
"package.json": JSON.stringify({ name: "x", scripts: { test: "node --test", lint: "eslint ." } }),
|
|
21
|
+
"src/app.js": "export function f(a) {\n return a + 1;\n}\n",
|
|
22
|
+
"src/util.py": "def g(x):\n return x * 2\n",
|
|
23
|
+
"tests/app.test.js": "import test from 'node:test';\ntest('x', () => {});\n",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
test("первый запуск: «начните с трёх» стоит выше списка остальных записей", (t) => {
|
|
27
|
+
const p = project(t, FILES);
|
|
28
|
+
const out = plain(aqk(p, "doctor").out);
|
|
29
|
+
const lines = out.split("\n");
|
|
30
|
+
const start = lines.findIndex((l) => /Начните с этих трёх/.test(l));
|
|
31
|
+
assert.ok(start > -1, `блока «начните с трёх» нет:\n${out}`);
|
|
32
|
+
// Кресты каталога — строки «✘ <slug>» ниже заголовка «Гейты»; кресты раскладки (.aqk/docs,
|
|
33
|
+
// AGENTS.md) стоят выше него и к делу не относятся.
|
|
34
|
+
const heading = lines.findIndex((l) => /^\s*Гейты\s*$/.test(l));
|
|
35
|
+
const firstCross = lines.findIndex((l, i) => i > heading && /^\s*○\s+[a-z][a-z-]+\s/.test(l));
|
|
36
|
+
assert.ok(firstCross > -1, `список записей пропал:\n${out}`);
|
|
37
|
+
assert.ok(start < firstCross, `«начните с трёх» на строке ${start + 1}, а первый крест — на ${firstCross + 1}`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("первый запуск: «поставить: aqk add» не повторяется у каждой записи, записи не теряются", (t) => {
|
|
41
|
+
const p = project(t, FILES);
|
|
42
|
+
const out = plain(aqk(p, "doctor").out);
|
|
43
|
+
const hints = out.split("\n").filter((l) => /поставить:/.test(l));
|
|
44
|
+
// Одна общая подсказка и те, что стоят у находок пробы, — но не по строке на каждую запись.
|
|
45
|
+
assert.ok(hints.length <= 3, `подсказок «поставить» ${hints.length} — по одной на запись:\n${hints.join("\n")}`);
|
|
46
|
+
const crosses = out.split("\n").filter((l) => /^\s*○\s+[a-z][a-z-]+\s/.test(l));
|
|
47
|
+
assert.ok(crosses.length >= 4, `записей к установке ${crosses.length} — список сократили вместо того, чтобы сжать:\n${out}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// СЛУЖЕБНЫЕ ФАЙЛЫ НЕ ПОПАДАЮТ В GIT. Отзыв с живого проекта 2026-09-11: `.aqk/last-run.md`
|
|
51
|
+
// однажды закоммитили, и с тех пор каждый `make check` оставляет изменённый файл. А проба
|
|
52
|
+
// печатала «рабочее дерево не трогается», записывая `.aqk/last-probe.md`. В коде лежал
|
|
53
|
+
// комментарий «.aqk/ в .gitignore» — только `init` его туда не клал. Целиком `.aqk/`
|
|
54
|
+
// игнорировать нельзя: методички и правила в нём — содержимое проекта, их коммитят.
|
|
55
|
+
test("init кладёт служебные файлы .aqk в .gitignore, повторный init строк не дублирует", (t) => {
|
|
56
|
+
const p = project(t, { "src/a.py": "x = 1\n", ".gitignore": "node_modules/\n" });
|
|
57
|
+
aqk(p, "init");
|
|
58
|
+
aqk(p, "init");
|
|
59
|
+
const gi = readFileSync(join(p.dir, ".gitignore"), "utf8");
|
|
60
|
+
for (const f of ["last-run.md", "last-probe.md", "advice-shown", "update-checked"]) {
|
|
61
|
+
const n = gi.split("\n").filter((l) => l.trim() === `.aqk/${f}`).length;
|
|
62
|
+
assert.equal(n, 1, `.aqk/${f} в .gitignore ${n} раз(а):\n${gi}`);
|
|
63
|
+
}
|
|
64
|
+
assert.match(gi, /^node_modules\/$/m, "чужие строки .gitignore не тронуты");
|
|
65
|
+
// И методички при этом не спрятаны: их коммитят.
|
|
66
|
+
assert.equal(run(p, "git", ["check-ignore", "-q", ".aqk/docs"]).code, 1, ".aqk/docs оказался в игноре");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("doctor называет служебный файл, который отслеживает git, — с командой, как вынуть", (t) => {
|
|
70
|
+
const p = project(t, { "src/a.py": "x = 1\n", ".aqk/last-run.md": "# старый отчёт\n" });
|
|
71
|
+
run(p, "git", ["add", "-A"]);
|
|
72
|
+
run(p, "git", ["commit", "-qm", "init"]);
|
|
73
|
+
const out = plain(aqk(p, "doctor").out);
|
|
74
|
+
assert.match(out, /\.aqk\/last-run\.md/, `служебный файл в git не назван:\n${out}`);
|
|
75
|
+
assert.match(out, /git rm --cached \.aqk\/last-run\.md/, "нет готовой команды");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ✘ — ТОЛЬКО ДЛЯ НАСТОЯЩЕГО ПАДЕНИЯ. Отзыв с живого проекта 2026-09-11: в зелёном прогоне висели
|
|
79
|
+
// девять крестов — записи каталога, которые просто не установлены. Глаз читает их как провал, и
|
|
80
|
+
// через неделю человек перестаёт смотреть на красное вообще. Неустановленное — своим знаком.
|
|
81
|
+
test("неустановленная запись — ○, а не ✘: крест значит «упало», и больше ничего", (t) => {
|
|
82
|
+
const p = project(t, FILES);
|
|
83
|
+
const out = plain(aqk(p, "doctor").out);
|
|
84
|
+
const lines = out.split("\n");
|
|
85
|
+
const heading = lines.findIndex((l) => /^\s*Гейты\s*$/.test(l));
|
|
86
|
+
const crosses = lines.filter((l, i) => i > heading && /^\s*✘\s/.test(l));
|
|
87
|
+
assert.deepEqual(crosses, [], `в осмотре без прогона стоят кресты:\n${crosses.join("\n")}`);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// МЕТОДИЧКИ — СОВЕТ, А НЕ ПРИГОВОР. Отзыв с живого проекта 2026-09-11: первый `doctor --run`
|
|
91
|
+
// покраснел только из-за отсутствия `.aqk/docs` — «files from the header are missing». Проект
|
|
92
|
+
// вправе держать пособия где-то ещё или не держать вовсе; гейты при этом зелёные. Точка входа,
|
|
93
|
+
// .gitignore и .git остаются обязательными: на них стоит своя проверка вердикта.
|
|
94
|
+
test("нет .aqk/docs и .aqk/rules — прогон всё равно зелёный, пункт назван советом", (t) => {
|
|
95
|
+
const p = project(t, { "src/a.py": "x = 1\n", ".gitignore": "x\n" });
|
|
96
|
+
aqk(p, "init");
|
|
97
|
+
rmSync(join(p.dir, ".aqk", "docs"), { recursive: true, force: true });
|
|
98
|
+
rmSync(join(p.dir, ".aqk", "rules"), { recursive: true, force: true });
|
|
99
|
+
const man = join(p.dir, ".aqk.yml");
|
|
100
|
+
writeFileSync(man, readFileSync(man, "utf8").replace(/^gates:\s*$/m, 'gates:\n тихий: "true"'), "utf8");
|
|
101
|
+
const r = aqk(p, "doctor", "--run");
|
|
102
|
+
const tail = plain(r.out).trimEnd().split("\n").slice(-4).join("\n");
|
|
103
|
+
assert.equal(r.code, 0, `прогон покраснел из-за методичек:\n${tail}`);
|
|
104
|
+
assert.match(plain(r.out), /\.aqk\/docs/, "отсутствие методичек не названо вовсе");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Отзыв с живого проекта 2026-09-11: «AQK-3, All levels reached» при конвейере, который ни разу
|
|
108
|
+
// не запускался. Под уровнем — чего он не доказывает, и что про это знает проба. Здесь пробы
|
|
109
|
+
// не было: строка обязана это сказать, а не промолчать — тишину глаз читает как «всё хорошо».
|
|
110
|
+
test("под уровнем стоит, чего он не доказывает", (t) => {
|
|
111
|
+
const p = project(t, {
|
|
112
|
+
".aqk.yml": 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n lint: "true"\n',
|
|
113
|
+
"AGENTS.md": "# rules\n",
|
|
114
|
+
"rules/a.md": "# a\n",
|
|
115
|
+
});
|
|
116
|
+
const r = aqk(p, "doctor");
|
|
117
|
+
assert.match(r.out, /AQK-1/, r.out);
|
|
118
|
+
assert.match(r.out, /(оснащённость, а не надёжность|tooling, not reliability)/, `нет оговорки под уровнем:\n${r.out}`);
|
|
119
|
+
assert.match(r.out, /(проба не запускалась|probe has never run)/, `про пробу — тишина:\n${r.out}`);
|
|
120
|
+
assert.match(r.out, /(конвейер: прошёл ли он|pipeline: whether it passed)/, r.out);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Claude Code читает CLAUDE.md, а не AGENTS.md (документация, сверено 2026-09-11). Проект с
|
|
124
|
+
// .claude/ и сводом только в AGENTS.md пишет правила агенту, который их не читает.
|
|
125
|
+
test("doctor называет свод, которого не видит Claude Code, и молчит, когда он подключён", (t) => {
|
|
126
|
+
const p = project(t, { ".claude/settings.json": "{}\n", "AGENTS.md": "# Rules\n\n- run tests\n" });
|
|
127
|
+
assert.match(plain(aqk(p, "doctor").out), /(Claude Code здесь настроен|Claude Code is set up here)/);
|
|
128
|
+
writeFileSync(join(p.dir, "CLAUDE.md"), "@AGENTS.md\n", "utf8");
|
|
129
|
+
assert.doesNotMatch(plain(aqk(p, "doctor").out), /(Claude Code здесь настроен|не подключает AGENTS|Claude Code is set up|does not import AGENTS)/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// `aqk prompt` на свежем проекте: прогона не было — задание так и говорит и первым пунктом
|
|
133
|
+
// велит прогнать, а не выдаёт пустой список за «всё чисто». Ничего не пишет на диск.
|
|
134
|
+
test("prompt: задание без прогона начинается с прогона и кончается проверкой", (t) => {
|
|
135
|
+
const p = project(t, FILES);
|
|
136
|
+
aqk(p, "init");
|
|
137
|
+
const r = aqk(p, "prompt");
|
|
138
|
+
assert.equal(r.code, 0, r.out);
|
|
139
|
+
const items = r.out.split("\n").filter((l) => /^\d+\. /.test(l));
|
|
140
|
+
assert.match(items[0] || "", /(Прогона ещё не было|no run yet)/, `первый пункт не про прогон:\n${r.out}`);
|
|
141
|
+
assert.ok(items.length <= 5, "больше пяти пунктов за раз");
|
|
142
|
+
assert.match(r.out, /(Как проверить, что готово|How to verify)/);
|
|
143
|
+
assert.doesNotMatch(r.out, /\x1b\[/, "задание уходит агенту текстом — без цветовых кодов");
|
|
144
|
+
});
|