agent-quality-kit 0.2.4 → 0.3.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 +118 -94
- package/README.ru.md +178 -0
- package/kit/gates/_skip.sh +31 -3
- package/kit/gates/commit-explains-itself/gate.yml +1 -0
- package/kit/gates/complexity-limit/gate.yml +1 -0
- package/kit/gates/dead-code/gate.yml +1 -0
- package/kit/gates/deps-are-pinned/gate.yml +1 -0
- package/kit/gates/duplicate-code/gate.yml +1 -0
- package/kit/gates/entry-links-exist/gate.yml +1 -0
- package/kit/gates/file-size-limit/gate.yml +1 -0
- package/kit/gates/gate-has-samples/gate.yml +1 -0
- package/kit/gates/gates-are-runnable/gate.yml +1 -0
- package/kit/gates/gates-run-in-ci/gate.yml +1 -0
- package/kit/gates/lesson-has-outcome/gate.yml +1 -0
- package/kit/gates/no-print-in-prod/check.sh +3 -1
- package/kit/gates/no-print-in-prod/gate.yml +1 -0
- package/kit/gates/secrets-not-in-code/gate.yml +1 -0
- package/kit/gates/swallowed-error/gate.yml +1 -0
- package/kit/gates/todo-without-task/gate.yml +1 -0
- package/package.json +2 -1
- package/tool/commands/doctor.mjs +38 -41
- package/tool/commands/gates.mjs +103 -109
- package/tool/commands/project.mjs +84 -85
- package/tool/commands/report.mjs +194 -0
- package/tool/i18n/en.mjs +423 -0
- package/tool/i18n/index.mjs +33 -0
- package/tool/i18n/ru.mjs +424 -0
- package/tool/i18n/templates-en.mjs +164 -0
- package/tool/i18n/templates-ru.mjs +170 -0
- package/tool/lib/core.mjs +5 -1
- package/tool/lib/manifest.mjs +12 -31
- package/tool/lib/repo.mjs +45 -21
- package/tool/lib/templates.mjs +38 -182
- package/tool/program.mjs +31 -9
- package/tool/selfcheck/gates.sh +7 -1
- package/tool/selfcheck/smoke.sh +97 -0
- package/tool/selfcheck/units.mjs +80 -5
|
@@ -5,13 +5,14 @@ import { readdir, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { join, dirname, relative } from "node:path";
|
|
7
7
|
import {
|
|
8
|
-
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF,
|
|
8
|
+
CWD, PKG_ROOT, DOCS_SRC, RULES_SRC, TARGET_DIR, MANIFEST, SELF, REPO_URL, c, exists, die,
|
|
9
9
|
copyDir, writeIfAbsent, FEEDBACK_MARK,
|
|
10
10
|
} from "../lib/core.mjs";
|
|
11
11
|
import { AGENTS_MD, CLAUDE_MD, MANIFEST_YML } from "../lib/templates.mjs";
|
|
12
12
|
import { readManifest } from "../lib/manifest.mjs";
|
|
13
13
|
import { detectFacts, readCatalog, triggerVerdict } from "../lib/repo.mjs";
|
|
14
14
|
import { installGate } from "./gates.mjs";
|
|
15
|
+
import { L } from "../i18n/index.mjs";
|
|
15
16
|
|
|
16
17
|
async function cmdInit(args) {
|
|
17
18
|
const force = args.includes("--force");
|
|
@@ -21,7 +22,7 @@ async function cmdInit(args) {
|
|
|
21
22
|
const track = (ok, path) => (ok ? created : skipped).push(relative(CWD, path));
|
|
22
23
|
|
|
23
24
|
if (!(await exists(DOCS_SRC))) {
|
|
24
|
-
die(
|
|
25
|
+
die(L.init.noDocs(DOCS_SRC));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const refs = await copyDir(DOCS_SRC, join(CWD, TARGET_DIR, "docs"), { force });
|
|
@@ -42,14 +43,14 @@ async function cmdInit(args) {
|
|
|
42
43
|
|
|
43
44
|
console.log(c.bold("\naqk init\n"));
|
|
44
45
|
if (created.length) {
|
|
45
|
-
console.log(c.green(`
|
|
46
|
+
console.log(c.green(` ${L.init.created(created.length)}`));
|
|
46
47
|
for (const f of created.slice(0, 8)) console.log(` ${f}`);
|
|
47
|
-
if (created.length > 8) console.log(c.dim(`
|
|
48
|
+
if (created.length > 8) console.log(c.dim(` ${L.init.andMore(created.length - 8)}`));
|
|
48
49
|
}
|
|
49
50
|
if (skipped.length) {
|
|
50
|
-
console.log(c.yellow(`\n
|
|
51
|
+
console.log(c.yellow(`\n ${L.init.kept(skipped.length)}`));
|
|
51
52
|
for (const f of skipped) console.log(` ${f}`);
|
|
52
|
-
console.log(c.dim(`
|
|
53
|
+
console.log(c.dim(` ${L.init.overwrite(`${SELF} init --force`)}`));
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Путь ищем, а не пишем: раскладка внутри .aqk — дело владельца комплекта, и жёстко
|
|
@@ -58,18 +59,18 @@ async function cmdInit(args) {
|
|
|
58
59
|
for (const f of created) if (f.endsWith("project-baseline.md")) baseline = f;
|
|
59
60
|
|
|
60
61
|
console.log(`
|
|
61
|
-
${c.bold(
|
|
62
|
-
|
|
63
|
-
1.
|
|
64
|
-
|
|
65
|
-
2.
|
|
66
|
-
|
|
67
|
-
3.
|
|
68
|
-
|
|
69
|
-
4.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
${c.dim(
|
|
62
|
+
${c.bold(L.init.nextTitle)}
|
|
63
|
+
|
|
64
|
+
1. ${L.init.n1a} ${c.bold("AGENTS.md")} ${L.init.n1b}
|
|
65
|
+
${L.init.n1c}
|
|
66
|
+
2. ${L.init.n2a} ${c.bold(baseline)} ${L.init.n2b}
|
|
67
|
+
${L.init.n2c}
|
|
68
|
+
3. ${L.init.n3a} ${c.bold(".aqk.yml")} ${L.init.n3b}
|
|
69
|
+
${L.init.n3c(c.bold(`${SELF} doctor`))}
|
|
70
|
+
4. ${L.init.n4a} ${c.bold(L.init.n4b)}${L.init.n4c}
|
|
71
|
+
${L.init.n4d}
|
|
72
|
+
|
|
73
|
+
${c.dim(L.init.burned(`${SELF} note "…"`))}
|
|
73
74
|
`);
|
|
74
75
|
await maybeAskFeedback();
|
|
75
76
|
}
|
|
@@ -80,13 +81,13 @@ ${c.dim(`Обжёгся на чём-то — запиши: ${SELF} note "что
|
|
|
80
81
|
// звезды, но без звезды меньше шансов, что кто-то вообще дойдёт до фидбека.
|
|
81
82
|
async function maybeAskFeedback() {
|
|
82
83
|
if (await exists(FEEDBACK_MARK)) return;
|
|
83
|
-
const url =
|
|
84
|
+
const url = REPO_URL;
|
|
84
85
|
console.log(`
|
|
85
|
-
${c.bold(
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
${c.bold(L.feedback.title)}
|
|
87
|
+
${L.feedback.star(url)}
|
|
88
|
+
${L.feedback.issue}
|
|
88
89
|
${url}/issues/new
|
|
89
|
-
${c.dim(
|
|
90
|
+
${c.dim(` ${L.feedback.once}`)}
|
|
90
91
|
`);
|
|
91
92
|
await writeIfAbsent(FEEDBACK_MARK, "shown\n", { force: false });
|
|
92
93
|
}
|
|
@@ -107,19 +108,34 @@ function findJournal() {
|
|
|
107
108
|
|
|
108
109
|
async function cmdNote(args) {
|
|
109
110
|
const title = args.find((a) => !a.startsWith("--"));
|
|
110
|
-
if (!title) die(
|
|
111
|
+
if (!title) die(L.note.needTitle(`${SELF} note "…"`));
|
|
112
|
+
|
|
113
|
+
// Сначала журнал ЭТОГО проекта: `lessons:` в манифесте — это и есть ответ на вопрос «куда
|
|
114
|
+
// складывать уроки», данный владельцем репозитория. Пока команда его игнорировала, она
|
|
115
|
+
// требовала клон нашего репозитория и писала урок туда — то есть в чужой проект. Найдено
|
|
116
|
+
// первым же чужим прогоном: человек завёл журнал руками, потому что команда не сработала.
|
|
117
|
+
const man = await readManifest();
|
|
118
|
+
const own = String(man?.lessons || "").trim();
|
|
119
|
+
let home = null;
|
|
120
|
+
let journal = null;
|
|
121
|
+
if (own && !/^https?:/i.test(own)) {
|
|
122
|
+
const dir = join(CWD, own);
|
|
123
|
+
if (await exists(dir)) {
|
|
124
|
+
home = CWD;
|
|
125
|
+
journal = join(dir, "README.md");
|
|
126
|
+
// Журнал объявлен, но файла нет — заводим, а не отказываем: пустой журнал это норма
|
|
127
|
+
// первого дня, и отказ на нём отучает пользоваться командой.
|
|
128
|
+
if (!(await exists(journal))) await writeFile(journal, `# ${L.note.journalTitle}\n`, "utf8");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
111
131
|
|
|
112
|
-
const home = findJournal();
|
|
113
132
|
if (!home) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
home = findJournal();
|
|
134
|
+
if (!home) die(L.note.noJournal(REPO_URL));
|
|
135
|
+
journal = join(home, "incidents", "README.md");
|
|
136
|
+
if (!(await exists(journal))) die(L.note.journalMissing(journal));
|
|
118
137
|
}
|
|
119
138
|
|
|
120
|
-
const journal = join(home, "incidents", "README.md");
|
|
121
|
-
if (!(await exists(journal))) die(`Журнал не найден: ${journal}`);
|
|
122
|
-
|
|
123
139
|
let body = "";
|
|
124
140
|
if (!process.stdin.isTTY) {
|
|
125
141
|
body = await new Promise((res) => {
|
|
@@ -131,14 +147,7 @@ async function cmdNote(args) {
|
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
if (!body.trim()) {
|
|
134
|
-
die(
|
|
135
|
-
|
|
136
|
-
aqk note "заголовок" <<'EOF'
|
|
137
|
-
**Класс:** гейт молчал
|
|
138
|
-
**Что случилось.** ...
|
|
139
|
-
**Чем это стоило.** ...
|
|
140
|
-
**Вывод.** 🔧 ...
|
|
141
|
-
EOF`);
|
|
150
|
+
die(L.note.emptyBody);
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
// ГЕЙТ. Урок без вывода — это история, а не урок: в следующий раз обожжёмся так же.
|
|
@@ -146,18 +155,12 @@ async function cmdNote(args) {
|
|
|
146
155
|
// тексте не значит вывод, если рядом нет одной из трёх настоящих отметок. Запись проходила бы
|
|
147
156
|
// здесь и тут же краснела на doctor --run — разные требования в двух местах одного правила.
|
|
148
157
|
if (!/[✅🔧📜👤]/.test(body)) {
|
|
149
|
-
die(
|
|
150
|
-
"В записи нет отметки решения. Урок без вывода — это история, а не урок.\n" +
|
|
151
|
-
"Припиши одну из трёх:\n" +
|
|
152
|
-
" ✅ **Стало гейтом:** <имя записи каталога>\n" +
|
|
153
|
-
" 🔧 **Стало правкой оснастки:** <что именно изменено>\n" +
|
|
154
|
-
" 👤 **Гейтом не станет:** <почему>"
|
|
155
|
-
);
|
|
158
|
+
die(L.note.noOutcome);
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
const date = new Date().toISOString().slice(0, 10);
|
|
159
|
-
const project = CWD.split("/").filter(Boolean).pop() ||
|
|
160
|
-
const entry = `\n## ${date} — ${title}\n\n
|
|
162
|
+
const project = CWD.split("/").filter(Boolean).pop() || L.note.unknownProject;
|
|
163
|
+
const entry = `\n## ${date} — ${title}\n\n**${L.note.projectField}:** ${project}\n\n${body.trim()}\n`;
|
|
161
164
|
|
|
162
165
|
const prev = await readFile(journal, "utf8");
|
|
163
166
|
await writeFile(journal, prev + entry, "utf8");
|
|
@@ -168,8 +171,8 @@ async function cmdNote(args) {
|
|
|
168
171
|
const pushed = run("push", "-q");
|
|
169
172
|
console.log(
|
|
170
173
|
pushed.status === 0
|
|
171
|
-
? c.green(
|
|
172
|
-
: c.yellow(
|
|
174
|
+
? c.green(L.note.pushed(title))
|
|
175
|
+
: c.yellow(L.note.localOnly(`git -C ${home} push`))
|
|
173
176
|
);
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -179,13 +182,10 @@ async function cmdNote(args) {
|
|
|
179
182
|
|
|
180
183
|
async function cmdBlob() {
|
|
181
184
|
const dir = join(PKG_ROOT, "kit", "docs");
|
|
182
|
-
if (!(await exists(dir))) die(
|
|
185
|
+
if (!(await exists(dir))) die(L.blob.noDocs(dir));
|
|
183
186
|
|
|
184
187
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
185
|
-
let out =
|
|
186
|
-
`<!-- СОБРАНО КОМАНДОЙ aqk blob ${stamp} из kit/docs. Не править руками:\n` +
|
|
187
|
-
` правки затрёт следующая сборка. Источник — отдельные файлы. -->\n\n` +
|
|
188
|
-
`# AQK — методички одним файлом\n`;
|
|
188
|
+
let out = L.blob.header(stamp);
|
|
189
189
|
|
|
190
190
|
// Методички могут лежать в подпапках — обходим дерево, порядок стабильный.
|
|
191
191
|
const found = [];
|
|
@@ -199,7 +199,7 @@ async function cmdBlob() {
|
|
|
199
199
|
await walk(dir);
|
|
200
200
|
|
|
201
201
|
for (const full of found) {
|
|
202
|
-
out += `\n\n${"=".repeat(78)}\n<!--
|
|
202
|
+
out += `\n\n${"=".repeat(78)}\n<!-- ${L.blob.source(relative(PKG_ROOT, full))} -->\n${"=".repeat(78)}\n\n`;
|
|
203
203
|
// Ссылки на соседние файлы в склейке ведут в никуда: соседей рядом больше нет,
|
|
204
204
|
// все они внутри этого же текста. Оставляем подпись, снимаем разметку.
|
|
205
205
|
const body = (await readFile(full, "utf8")).replace(
|
|
@@ -212,10 +212,9 @@ async function cmdBlob() {
|
|
|
212
212
|
const dst = join(CWD, "GOD_AI.md");
|
|
213
213
|
await writeFile(dst, out, "utf8");
|
|
214
214
|
console.log(
|
|
215
|
-
`\n ${c.green("✔")}
|
|
216
|
-
`${Math.round(Buffer.byteLength(out) / 1024)} КБ\n`
|
|
215
|
+
`\n ${c.green("✔")} ${L.blob.done(found.length, Math.round(Buffer.byteLength(out) / 1024))}\n`
|
|
217
216
|
);
|
|
218
|
-
console.log(c.dim(
|
|
217
|
+
console.log(c.dim(` ${L.blob.rebuilt}\n`));
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
|
|
@@ -234,19 +233,19 @@ async function cmdStart(args) {
|
|
|
234
233
|
if (!man) {
|
|
235
234
|
await cmdInit(args.filter((a) => a !== "--force"));
|
|
236
235
|
man = await readManifest();
|
|
237
|
-
if (!man) die(
|
|
236
|
+
if (!man) die(L.start.initFailed(`${SELF} init`));
|
|
238
237
|
}
|
|
239
238
|
|
|
240
239
|
const facts = await detectFacts(man);
|
|
241
240
|
console.log(c.bold("\naqk start\n"));
|
|
242
241
|
|
|
243
242
|
if (facts.files > 30 && !force) {
|
|
244
|
-
console.log(c.yellow(`
|
|
245
|
-
console.log(` ${c.bold(`${SELF} doctor`)}
|
|
246
|
-
console.log(c.dim(
|
|
247
|
-
console.log(c.dim(`
|
|
248
|
-
console.log(c.dim(
|
|
249
|
-
console.log(c.dim(`
|
|
243
|
+
console.log(c.yellow(` ${L.start.tooManyFiles(facts.files)}\n`));
|
|
244
|
+
console.log(` ${L.start.useDoctor(c.bold(`${SELF} doctor`))}`);
|
|
245
|
+
console.log(c.dim(` ${L.start.threeLists}\n`));
|
|
246
|
+
console.log(c.dim(` ${L.start.anyway(`${SELF} start --force`)}`));
|
|
247
|
+
console.log(c.dim(` ${L.start.expectRed}`));
|
|
248
|
+
console.log(c.dim(` ${L.start.expectRedFix(`${SELF} ratchet ${L.help.name}`)}\n`));
|
|
250
249
|
return;
|
|
251
250
|
}
|
|
252
251
|
|
|
@@ -280,35 +279,35 @@ async function cmdStart(args) {
|
|
|
280
279
|
}
|
|
281
280
|
|
|
282
281
|
if (put.length) {
|
|
283
|
-
console.log(c.green(`
|
|
282
|
+
console.log(c.green(` ${L.start.installed(put.length)}\n`));
|
|
284
283
|
for (const [slug, , intent] of put) console.log(` ${c.green("✔")} ${slug.padEnd(22)} ${c.dim(intent)}`);
|
|
285
|
-
console.log(c.dim(
|
|
286
|
-
console.log(c.dim(
|
|
284
|
+
console.log(c.dim(`\n ${L.start.noDebt1}`));
|
|
285
|
+
console.log(c.dim(` ${L.start.noDebt2}\n`));
|
|
287
286
|
} else {
|
|
288
|
-
console.log(c.dim(
|
|
287
|
+
console.log(c.dim(` ${L.start.allDeclared}\n`));
|
|
289
288
|
}
|
|
290
289
|
if (skipped.length) {
|
|
291
|
-
console.log(c.dim(
|
|
290
|
+
console.log(c.dim(` ${L.start.notYet}`));
|
|
292
291
|
for (const [slug, why] of skipped.slice(0, 6)) console.log(c.dim(` ${slug.padEnd(22)} ${why}`));
|
|
293
|
-
console.log(c.dim(
|
|
292
|
+
console.log(c.dim(` ${L.start.notYetWhy}\n`));
|
|
294
293
|
}
|
|
295
294
|
|
|
296
295
|
// --- порядок работы --------------------------------------------------------
|
|
297
|
-
console.log(`${c.bold(
|
|
296
|
+
console.log(`${c.bold(L.start.orderTitle)}
|
|
298
297
|
|
|
299
|
-
1. ${c.bold(
|
|
300
|
-
${c.dim(
|
|
301
|
-
2. ${c.bold(
|
|
302
|
-
${c.dim(
|
|
303
|
-
3. ${c.bold(
|
|
304
|
-
${c.dim(
|
|
305
|
-
4. ${c.bold(
|
|
298
|
+
1. ${c.bold(L.start.o1)} ${L.start.o1What}
|
|
299
|
+
${c.dim(L.start.o1Why)}
|
|
300
|
+
2. ${c.bold(L.start.o2)} ${L.start.o2What}
|
|
301
|
+
${c.dim(L.start.o2Why)}
|
|
302
|
+
3. ${c.bold(L.start.o3)} ${L.start.o3What}
|
|
303
|
+
${c.dim(L.start.o3Why)}
|
|
304
|
+
4. ${c.bold(L.start.o4)} ${L.start.o4What}
|
|
306
305
|
|
|
307
|
-
${c.dim(
|
|
308
|
-
${c.dim(
|
|
309
|
-
${c.dim(
|
|
306
|
+
${c.dim(L.start.softNote1)}
|
|
307
|
+
${c.dim(L.start.softNote2)}
|
|
308
|
+
${c.dim(L.start.softNote3)}
|
|
310
309
|
|
|
311
|
-
${c.bold(
|
|
310
|
+
${c.bold(L.start.next)} ${c.bold(`${SELF} doctor --run`)} ${c.dim(L.start.nextWhy)}
|
|
312
311
|
`);
|
|
313
312
|
await maybeAskFeedback();
|
|
314
313
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// tool/commands/report.mjs — обязательная форма отчёта после работы с комплектом.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНАЯ КОМАНДА, А НЕ ПРАВИЛО В ТЕКСТЕ. Первый чужой прогон дал отчёт, в котором
|
|
4
|
+
// «12 гейтов зелёные» стояло рядом с тем фактом, что все 12 встали на слабейший рецепт, а
|
|
5
|
+
// половина методичек, на которые указал сам `init`, не была прочитана. Агент не соврал — он
|
|
6
|
+
// пересказал по памяти то, что счёл главным. Пересказ по памяти всегда выбирает удобное.
|
|
7
|
+
//
|
|
8
|
+
// Поэтому отчёт СОБИРАЕТСЯ ПРОГОНОМ. Всё, что в нём написано, — результат запуска здесь и
|
|
9
|
+
// сейчас: какой рецепт реально выбран, что зелёное, что красное, что применимо и не стоит,
|
|
10
|
+
// какие документы комплект велел прочитать и лежат ли они на диске.
|
|
11
|
+
//
|
|
12
|
+
// Чего команда НЕ умеет и не делает вид, что умеет: проверить, что документ прочитали.
|
|
13
|
+
// Она печатает список — дальше отвечает человек или агент. Разница между «не прочитал» и
|
|
14
|
+
// «прочитал и решил не применять» машине не видна, и притворяться иначе было бы враньём.
|
|
15
|
+
|
|
16
|
+
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
|
|
17
|
+
import { join, relative } from "node:path";
|
|
18
|
+
import { CWD, TARGET_DIR, SELF, c, exists } from "../lib/core.mjs";
|
|
19
|
+
import { readManifest, assessLevel } from "../lib/manifest.mjs";
|
|
20
|
+
import { detectFacts, readCatalog, triggerVerdict, whichSync } from "../lib/repo.mjs";
|
|
21
|
+
import { runGates, declaredGates } from "./doctor.mjs";
|
|
22
|
+
import { L } from "../i18n/index.mjs";
|
|
23
|
+
|
|
24
|
+
// Каким рецептом стоит гейт: родным инструментом или переносимой проверкой. Именно это
|
|
25
|
+
// различие потерялось в первом чужом отчёте, и именно оно решает, что гейт на самом деле ловит.
|
|
26
|
+
function recipeKind(cmd, rec) {
|
|
27
|
+
const recipes = rec?.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
|
|
28
|
+
const norm = (x) => String(x || "").replace(/\{gate\}|\{dir\}/g, "").replace(/\s+/g, " ").trim();
|
|
29
|
+
const c0 = norm(cmd);
|
|
30
|
+
for (const [lang, r] of Object.entries(recipes)) {
|
|
31
|
+
if (lang === "any") continue;
|
|
32
|
+
const prog = String(r).trim().split(/\s+/)[0];
|
|
33
|
+
if (c0.includes(norm(r)) || c0.startsWith(prog)) return { native: true, how: lang, prog };
|
|
34
|
+
}
|
|
35
|
+
// Родной рецепт есть в записи, но стоит переносимый — назвать это вслух: разница измерима.
|
|
36
|
+
const alt = Object.entries(recipes).filter(([k]) => k !== "any");
|
|
37
|
+
return { native: false, alternatives: alt.map(([lang, r]) => [lang, String(r).trim().split(/\s+/)[0]]) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Документы, на которые комплект указывает сам. Список не выдуман здесь: это те же файлы,
|
|
41
|
+
// которые `init` кладёт в проект и на которые ссылается его же вывод.
|
|
42
|
+
const REQUIRED_DOCS = [
|
|
43
|
+
["project-baseline.md", "baseline"],
|
|
44
|
+
["ready-made-rules.md", "readyMade"],
|
|
45
|
+
["general.md", "rulesGeneral"],
|
|
46
|
+
["testing.md", "rulesTesting"],
|
|
47
|
+
["security.md", "rulesSecurity"],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Путь ИЩЕМ, а не пишем. Жёстко вписанный `.aqk/docs/project-baseline.md` уже соврал в первой
|
|
51
|
+
// же версии этой команды: файл лежит в подпапке `ai/`, и отчёт сообщил «не разложен» о том,
|
|
52
|
+
// что разложено. Раскладка внутри .aqk — дело владельца комплекта, а не наше знание.
|
|
53
|
+
async function findDoc(name) {
|
|
54
|
+
const root = join(CWD, TARGET_DIR);
|
|
55
|
+
if (!(await exists(root))) return null;
|
|
56
|
+
const walk = async (dir) => {
|
|
57
|
+
let entries = [];
|
|
58
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return null; }
|
|
59
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
60
|
+
const full = join(dir, e.name);
|
|
61
|
+
if (e.isDirectory()) { const hit = await walk(full); if (hit) return hit; }
|
|
62
|
+
else if (e.name === name) return relative(CWD, full);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
};
|
|
66
|
+
return walk(root);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function cmdReport() {
|
|
70
|
+
const man = await readManifest();
|
|
71
|
+
if (!man) {
|
|
72
|
+
console.log(c.red(`\n ${L.report2.noManifest(`${SELF} init`)}\n`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { reached } = await assessLevel(man);
|
|
77
|
+
const facts = await detectFacts(man);
|
|
78
|
+
const catalog = await readCatalog();
|
|
79
|
+
const bySlug = Object.fromEntries(catalog.map((r) => [r.slug, r]));
|
|
80
|
+
const declared = declaredGates(man);
|
|
81
|
+
|
|
82
|
+
// Прогон, а не чтение манифеста: «объявлен» и «работает» — разные утверждения, и весь
|
|
83
|
+
// смысл этой команды в том, чтобы в отчёт попало второе.
|
|
84
|
+
const run = declared.length ? runGates(man) : { results: [], failed: 0 };
|
|
85
|
+
|
|
86
|
+
const held = [], broken = [], todo = [], skip = [];
|
|
87
|
+
for (const res of run.results) {
|
|
88
|
+
const rec = bySlug[res.name];
|
|
89
|
+
const kind = recipeKind(res.cmd, rec);
|
|
90
|
+
(res.ok ? held : broken).push({ ...res, kind, rec });
|
|
91
|
+
}
|
|
92
|
+
for (const rec of catalog) {
|
|
93
|
+
if (declared.some(([n]) => n === rec.slug)) continue;
|
|
94
|
+
const v = triggerVerdict(rec, facts);
|
|
95
|
+
if (!v.applies) { skip.push([rec, v.why]); continue; }
|
|
96
|
+
// Запись применима, но её родного инструмента нет — это другая причина, чем «не поставили».
|
|
97
|
+
const recipes = rec.recipes && typeof rec.recipes === "object" ? rec.recipes : {};
|
|
98
|
+
const needs = recipes.any ? null : Object.values(recipes).map((r) => String(r).trim().split(/\s+/)[0]).find((prog) => !whichSync(prog));
|
|
99
|
+
todo.push([rec, needs]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const lines = [];
|
|
103
|
+
const say = (s = "") => { lines.push(s); };
|
|
104
|
+
|
|
105
|
+
say(`# ${L.report2.title} — ${new Date().toISOString().slice(0, 16).replace("T", " ")}`);
|
|
106
|
+
say("");
|
|
107
|
+
say(`${L.report2.level}: AQK-${reached < 0 ? L.doctor.levelNone : reached}`);
|
|
108
|
+
say("");
|
|
109
|
+
|
|
110
|
+
say(`## ${L.report2.holdsTitle}`);
|
|
111
|
+
say("");
|
|
112
|
+
if (!held.length && !broken.length) say(`- ${L.report2.nothingRuns}`);
|
|
113
|
+
for (const g of held) {
|
|
114
|
+
const how = g.kind.native ? L.report2.native(g.kind.prog) : L.report2.portable;
|
|
115
|
+
say(`- ✅ ${g.name} — ${how}, ${g.secs}s`);
|
|
116
|
+
}
|
|
117
|
+
for (const g of broken) {
|
|
118
|
+
const how = g.kind.native ? L.report2.native(g.kind.prog) : L.report2.portable;
|
|
119
|
+
say(`- ❌ ${g.name} — ${how}, ${g.note || L.doctor.exitCode(g.code)}`);
|
|
120
|
+
}
|
|
121
|
+
// Гейт стоит на переносимом, хотя родной инструмент есть в системе: слабее, чем возможно.
|
|
122
|
+
const weaker = [...held, ...broken].filter(
|
|
123
|
+
(g) => !g.kind.native && (g.kind.alternatives || []).some(([, prog]) => whichSync(prog))
|
|
124
|
+
);
|
|
125
|
+
if (weaker.length) {
|
|
126
|
+
say("");
|
|
127
|
+
say(`### ${L.report2.weakerTitle}`);
|
|
128
|
+
say("");
|
|
129
|
+
for (const g of weaker) {
|
|
130
|
+
const have = (g.kind.alternatives || []).filter(([, prog]) => whichSync(prog)).map(([, prog]) => prog);
|
|
131
|
+
say(`- ⚠️ ${g.name} — ${L.report2.weaker(have.join(", "))}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
say("");
|
|
136
|
+
say(`## ${L.report2.missingTitle}`);
|
|
137
|
+
say("");
|
|
138
|
+
if (!todo.length) say(`- ${L.report2.nothingMissing}`);
|
|
139
|
+
for (const [rec, needs] of todo) {
|
|
140
|
+
say(`- ❌ ${rec.slug} — ${needs ? L.report2.needsTool(needs) : L.report2.notInstalled}`);
|
|
141
|
+
}
|
|
142
|
+
if (skip.length) {
|
|
143
|
+
say("");
|
|
144
|
+
say(`### ${L.report2.hiddenTitle}`);
|
|
145
|
+
say("");
|
|
146
|
+
for (const [rec, why] of skip) say(`- ⬜ ${rec.slug} — ${why}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
say("");
|
|
150
|
+
say(`## ${L.report2.readTitle}`);
|
|
151
|
+
say("");
|
|
152
|
+
for (const [name, key] of REQUIRED_DOCS) {
|
|
153
|
+
const found = await findDoc(name);
|
|
154
|
+
say(`- ${found ? "📖" : "⬜"} ${found || name} — ${L.report2.docs[key]}`);
|
|
155
|
+
}
|
|
156
|
+
say("");
|
|
157
|
+
say(`> ${L.report2.readWarn}`);
|
|
158
|
+
|
|
159
|
+
say("");
|
|
160
|
+
say(`## ${L.report2.ignoreTitle}`);
|
|
161
|
+
say("");
|
|
162
|
+
// Читаем тем же способом, каким это делает _skip.sh: комментарии и пустые строки прочь.
|
|
163
|
+
let ignored = [];
|
|
164
|
+
try {
|
|
165
|
+
ignored = (await readFile(join(CWD, ".aqkignore"), "utf8"))
|
|
166
|
+
.split("\n").map((l) => l.replace(/#.*$/, "").trim()).filter(Boolean);
|
|
167
|
+
} catch { /* файла нет — это норма, а не ошибка */ }
|
|
168
|
+
if (!ignored.length) say(`- ✅ ${L.report2.ignoreNone}`);
|
|
169
|
+
for (const pat of ignored) say(`- 🚫 ${pat}`);
|
|
170
|
+
if (ignored.length) {
|
|
171
|
+
say("");
|
|
172
|
+
say(`> ${L.report2.ignoreWarn}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
say("");
|
|
176
|
+
say(`## ${L.report2.whyTitle}`);
|
|
177
|
+
say("");
|
|
178
|
+
for (const [rec] of todo.slice(0, 6)) say(`- **${rec.slug}** — ${rec.intent || ""}`);
|
|
179
|
+
if (!todo.length) say(`- ${L.report2.whyNothing}`);
|
|
180
|
+
|
|
181
|
+
const text = lines.join("\n") + "\n";
|
|
182
|
+
const dst = join(CWD, TARGET_DIR, "report.md");
|
|
183
|
+
await mkdir(join(CWD, TARGET_DIR), { recursive: true });
|
|
184
|
+
await writeFile(dst, text, "utf8");
|
|
185
|
+
|
|
186
|
+
console.log("\n" + text);
|
|
187
|
+
console.log(c.dim(` ${L.report2.saved(join(TARGET_DIR, "report.md"))}\n`));
|
|
188
|
+
|
|
189
|
+
// Код возврата — для конвейера и для агента: отчёт с красным гейтом не должен читаться
|
|
190
|
+
// как «всё в порядке» только потому, что команда выполнилась.
|
|
191
|
+
process.exit(broken.length ? 1 : 0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { cmdReport };
|